feat(gateway): add client-carried model affinity#225
Open
Menci wants to merge 116 commits into
Open
Conversation
Encode gateway affinity metadata as an authenticated AES-GCM trailer with a two-byte length marker. Preserve canonical base64 and base64url payload bytes without nested encoding, classify other strings as raw UTF-8, and treat unrecognized or unauthenticated carriers as foreign for gateway cascades.
Carry the upstream configuration revision on provider candidates and encode direct-versus-alias rule identity explicitly. Route forcing affinity only to an exact target and use the latest available preferred target without narrowing normal fallback order.
Decrypt recognized carriers before normal gateway processing, retain only target evidence inside the affinity membrane, and rebuild a fresh source payload for each candidate. Restore original opaque state only for an exact target, strip owned state on fallback, and preserve unrecognized values for cascaded gateways.
Build a request-scoped codec from the authenticated key, decrypt source-protocol carriers before candidate enumeration, and supply each fallback attempt with a fresh candidate-specific payload. Exact force/prefer routing now applies to Chat Completions, Messages, Gemini, and their count endpoints without exposing the raw secret to providers.
Align Anthropic signature_delta and Chat reasoning_opaque with last-write-wins snapshot semantics. Reassemble every Chat choice independently, including per-choice extras, tools, reasoning, and finish state, while retaining response-level usage and metadata. Update Chat-target translators so repeated source signatures replace rather than concatenate.
Wrap or synthesize client-carried affinity at each source protocol boundary while keeping visible stream content ahead of delayed opaque-carrier encoding. Preserve per-choice and per-candidate semantics and reuse Responses replacements across lifecycle snapshots.
Generate a distinct hidden 32-byte secret for every API key and preserve it across key rotation. Backfill existing rows with a constrained D1/SQLite migration, expose the secret only through admin data transfer v10, and reject non-canonical imports. Keep normal key APIs secret-free and cover repository, migration, lifecycle, and transfer behavior.
Wait for async generators to advance across suppressed opaque frames before resolving the controlled codec promises.
Keep the API-key migration test's external imports aligned with the workspace ESLint ordering rule.
Apply protocol-native affinity transforms after clean frames have passed dump and telemetry observation, so normal gateway internals never see gateway ciphertext. Streaming and non-streaming responses share the same transformer and only opaque state is withheld while visible deltas continue immediately.
Keep every finish reason behind the final carrier frame when any candidate in an event still needs affinity, so terminal consumers cannot stop before another candidate's synthetic signature.
Hydrate snapshot and item-reference payloads before affinity classification so previous_response_id carries the same evidence as explicit client items. Reapply the membrane after candidate-specific stored-item rewrite, before interceptors and upstream dispatch, and select the successful exact candidate for response encoding.
Move state ownership out of candidate attempts and into the native Responses source edge. Observe clean events first, wrap all complete encrypted-content snapshots with item provenance, then mint public ids and persist the same stream. Compact follows the same affinity and storage path, while translated inner Responses calls remain stateless.
Give every test provider an explicit configuration revision, pass owned ArrayBuffers to Web Crypto on both Workers and Node type surfaces, and fix the compact result flow and hydrated-row narrowing exposed by the first integrated typecheck.
Replace upstream-affinity metadata and nullable payload rows with complete client-wire items scoped to one API key. Move item provenance entirely into client-carried encrypted blobs, make store=false truly stateless on HTTP, keep WebSocket-local state, and align item/snapshot row retention with the existing 30-day payload-file lifecycle.
Remove the retired database-affinity and metadata-only fixtures. Cover complete item persistence, creation-time cleanup, key scoping, HTTP versus WebSocket store behavior, stored-item hydration, source-edge ownership, and private hosted-tool replay under the new state model.
Remove the last database-affinity implementation and encrypted-content hash cache. Non-Responses source protocols now receive only a transient invocation store for inner Responses translation, while current tests target client-carried routing and complete-state persistence.
Run the WebSocket stream through the same clean observation, affinity egress, public-id rewrite, and complete-state persistence pipeline as HTTP. This restores socket-local store=false chaining and ensures response.done is emitted only after the client-visible snapshot is committed.
Allow the keepalive regression to drain the added affinity encryption and complete-state commit microtasks before asserting the terminal WebSocket frames.
Keep visible Gemini Parts streaming immediately while carrying each wrapped signature in a following signature-only Part. Authenticated placement metadata reattaches the original signature to the correct prior Part on ingress, including SDK histories that preserve each streamed Content separately.
Keep the literal alias-row diagnostic fixture aligned with the exact provider identity contract.
Validate Gemini placement as a positive integer before constructing the authenticated target and close the transformed WebSocket generator with its explicit void return value.
Give isolated responder tests a real selected candidate and await the WebSocket completion signal directly after the keepalive, matching the source-edge contract used by production serve paths.
The full-state migration clears all prior Responses rows, so payload reads now accept only gzip descriptors produced by the current writer. This removes compatibility branches for data that cannot survive the schema transition.
Retain the UTF-8 decoder used after gzip decompression while keeping obsolete descriptor formats removed.
Rewrite the architecture documentation around per-key encrypted affinity, exact force/prefer candidate routing, protocol-specific source membranes, and complete thirty-day Responses state. Keep pairwise translation independent from gateway affinity and align admin transfer, HTTP, WebSocket, streaming, and deployment guidance with the current implementation.
Route a first Chat Completions turn to candidate A, return its encrypted client carrier, put candidate B first on the next resolution, and prove the gateway reorders to A while restoring only A's original opaque state upstream.
Apply the workspace import-order rules and remove stale state-model imports after the affinity and Responses refactors.
Flush Chat affinity before a DONE sentinel even when finish_reason is missing, preserve upstream error frames, key Responses wrappers by item identity, and decide synthetic Responses state from the final replacement snapshot rather than an obsolete earlier item image.
Authenticate original carrier bytes and protocol domains, emit force evidence for program state, restore nested Responses item provenance, preserve Gemini signature placement and staggered candidates, eliminate duplicate payload cloning and Responses preparation, separate request-private tool state from persistence, enforce unique per-key secrets, and propagate malformed upstream stream state instead of fabricating defaults.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
preferandforceonly at ingress from current protocol structure; routing strength is not serializedserverSecretto each API key and derive an affinity-specific encryption key from itWire behavior
The version 1 AES-256-GCM trailer has no magic or delimiter, preserves canonical Base64/Base64URL bytes without nested encoding, and authenticates the original bytes plus protocol/slot domain. Values that cannot be authenticated by the current key remain byte-for-byte foreign for cascaded Floway deployments.
Affinity identity contains upstream ID, canonical model ID, and exact alias-rule presence/value. Protocol restore data may include upstream item identity or a whole-element synthetic marker. It contains neither an upstream configuration revision nor routing strength.
Egress has two responsibilities:
Ingress decrypts the target first, restores or removes owned state per candidate, and derives request-local routing evidence. Ordinary assistant state prefers a target. Responses compaction and program state force the associated target.
Protocol placement
reasoning_opaqueimmediately beforefinish_reason.output_item.done. Other first item types are shifted behind a synthetic reasoning added/done prefix; all lateroutput_indexandsequence_numbervalues plus terminal output are rewritten consistently.Responses state and migration
Native Responses input expands
previous_response_idand hydrates complete stored items before affinity routing. Output applies affinity before public ID rewriting and persistence. HTTPstore: falseperforms no writes; WebSocketstore: falsekeeps state only for that socket session.Migration
0057_responses_full_state.sqlrebuilds the schema while copying every key-scoped row that still has a complete payload. It preserves payload descriptors, hashes, IDs, item types, creation timestamps, and every fully backed snapshot. Legacy database affinity (upstream_id/upstream_item_id) is intentionally not migrated because the old rows lack canonical model and alias data. Preserved pre-0057 history resumes through normal routing and may be rejected by an upstream as a known breaking transition. Metadata-only and unscoped rows are omitted. Retention is uniformly creation-based at 30 days.Compatibility
Client payloads containing Floway-owned carriers are a Floway wire contract and are expected to continue through Floway. Replaying a modified payload directly to an upstream is not guaranteed.
Research against current official OpenAI, Anthropic, and Google SDKs plus Agents, Vercel AI SDK, ADKs, LangChain, LlamaIndex, Continue, and Cline informed placement. Responses and official Messages structured clients retain prefix items/blocks. Chat
reasoning_opaqueremains opportunistic in high-level clients that discard unknown Chat fields. Gemini sliding lookahead avoids repeated signatures even when the natural value arrives after multiple same-element continuation events; only the newest event is ever buffered.Normal API-key CRUD and dashboard DTOs never expose
serverSecret. Admin data transfer format version 10 includes it so restored deployments retain gateway-private per-key state.Test plan
pnpm run test— 361 files, 4295 testspnpm run typecheckpnpm run lint