Skip to content

fix(provider-codex): sync Codex Responses fingerprint with official CLI#107

Open
M1k0t0 wants to merge 21 commits into
Menci:mainfrom
M1k0t0:floway/codex-fingerprint
Open

fix(provider-codex): sync Codex Responses fingerprint with official CLI#107
M1k0t0 wants to merge 21 commits into
Menci:mainfrom
M1k0t0:floway/codex-fingerprint

Conversation

@M1k0t0

@M1k0t0 M1k0t0 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #92.

Context

#92 narrowed the Codex Responses 403 to request fingerprint differences: /codex/models worked, the official Codex CLI worked from the same egress path, but Floway /backend-api/codex/responses still received upstream HTML 403s.

This PR continues that work by aligning Floway’s Codex Responses request shape with the official openai/codex Rust client instead of only matching another relay implementation.

Thanks to @Menci for the detailed #92 review, the follow-up items called out there have been addressed in this PR.

Changes

  • Synthesize Codex-style session/thread/turn/window identity for /codex/responses.
  • Send canonical session-id, thread-id, x-client-request-id, x-codex-window-id, x-codex-turn-metadata, client_metadata, and prompt_cache_key using the official Codex semantics.
  • Persist Codex session/window metadata in Responses snapshots so replay, forks, and compaction keep the right upstream thread/window scope.
  • Normalize session_id to canonical session-id at the provider boundary.
  • Scrub downstream-supplied Codex metadata headers before provider dispatch and replace them with Floway-owned metadata.
  • adds responses_snapshots.metadata_json via D1 migration
  • Add snapshot metadata storage for Responses snapshots.
  • Update the Codex data-plane identity to the current official CLI version/UA shape.
  • Remove the uncited ~88% cache hit comment while rewriting session-id injection, replacing it with qualitative Codex session/cache scope wording. Note: the ~88% cache hit comment called out in fix(provider-codex): match CLIProxyAPI response headers #92 was already present on upstream/main before this branch.

Verification

  • pnpm vitest run --project @floway-dev/provider-codex packages/provider-codex/src/fetch_test.ts packages/provider-codex/src/compaction_test.ts packages/provider-codex/src/interceptors/responses/inject-session-id_test.ts
  • pnpm vitest run --project @floway-dev/gateway packages/gateway/src/data-plane/llm/responses/serve_test.ts packages/gateway/src/data-plane/llm/responses/items/store_test.ts packages/gateway/src/repo/responses-items_test.ts packages/gateway/src/data-plane/shared/inbound-headers_test.ts
  • pnpm --filter @floway-dev/provider-codex run typecheck

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to allow some concepts of ONE provider to the whole gateway.

@M1k0t0 M1k0t0 force-pushed the floway/codex-fingerprint branch from abb0262 to dcc4f4a Compare June 25, 2026 15:02
@M1k0t0

M1k0t0 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

I agree with the concern, and I changed the implementation to keep Codex-specific concepts out of the gateway.

In the latest version, the gateway no longer knows about Codex session ids, turn ids, account ids, or Codex-specific metadata keys. Those are all owned by provider-codex.

The gateway-side changes are now limited to provider-neutral lifecycle hooks:

  • prepareResponsesRequest: lets a provider adjust the upstream Responses request before dispatch.
  • beforeResponsesSnapshotCommit: lets a provider attach continuation metadata before a terminal snapshot is committed.

@M1k0t0 M1k0t0 force-pushed the floway/codex-fingerprint branch from 61259c8 to ec75d53 Compare June 25, 2026 15:33
M1k0t0 added 14 commits June 26, 2026 13:23
Use the official codex_cli_rs data-plane fingerprint for Codex models and responses, normalize session_id to session-id, and synthesize Floway-owned Codex metadata instead of forwarding raw downstream x-codex-* values.
Clone the official Codex turn metadata shape, keep session/thread scope stable, and generate Codex-shaped UUIDv7 session, window, and turn identifiers.
Store Floway-owned Codex session metadata on Responses snapshots, validate snapshot metadata JSON on read, and clear pending metadata after each attempted snapshot commit.
Persist Codex window scope alongside the Floway-owned session id and format upstream window ids as session generation markers instead of random UUIDs.
Prefer caller-supplied Codex session ids in the gateway before falling back to previous_response_id snapshot metadata. Reuse a Floway-owned Codex turn id across repeated provider dispatches in one gateway attempt and mark compaction requests with compaction turn metadata.
@M1k0t0 M1k0t0 force-pushed the floway/codex-fingerprint branch from ec75d53 to a494d48 Compare June 26, 2026 05:49
Menci added 7 commits June 27, 2026 16:13
…tion_id

The Codex upstream's `client_metadata['x-codex-installation-id']` is
meant to look like one persisted device per account; the previous
SHA-256 derivation of `(upstreamId, accountId)` was stable per account
but exposed the gateway's internal keying as the fingerprint. Mint a
fresh UUIDv4 at OAuth import time, persist it on the credential row, and
read it back at request time.

Migration 0047 backfills openaiDeviceId on existing state_json rows
(per CodexUpstreamConfig's exactly-one-account invariant) so the field
can be required end-to-end without an in-code legacy fallback.
The previous build path overwrote every identity surface from gateway
defaults, throwing away parent_thread_id, custom turn_started_at_unix_ms,
and the caller's own installation id even when they were the more
accurate fingerprint. Identity now resolves through a single ordered
chain (dedicated header → caller body `client_metadata` key → parsed
`x-codex-turn-metadata` key → gateway default) for every mirrored id
and is projected back onto every surface so a caller that splits its
identity across surfaces gets consistent values out.

Specifics:
- thread-id and x-client-request-id pass through; default to session-id
  / thread-id only when the caller did not supply them. Sub-agent flows
  that want a distinct thread or per-request id under the same session
  now work.
- x-codex-window-id passes through for direct provider callers too
  (gateway-routed calls still hit the FLOWAY_CODEX_WINDOW_ID_HEADER
  resolver set by the responses-state hook).
- client_metadata is merged key by key: caller's identity-mirror keys
  get absorbed into identity at build time; their non-identity extras
  (custom telemetry) survive into the outbound body.
- x-codex-turn-metadata's parsed JSON is merged the same way, picking
  up fields the official CLI sets but Floway has no opinion on
  (turn_started_at_unix_ms, sandbox, workspaces, parent_thread_id).
- installation_id resolution gains a top tier:
  client_metadata['x-codex-installation-id'] (real client) →
  parsed x-codex-turn-metadata installation_id → account.openaiDeviceId.
…rough

The build-from-clean test stopped asserting `installation_id` against
the v4 shape now that the field passes through the caller-supplied
value verbatim; remove the helper to satisfy lint.
…ller supplies none

A stateless OpenAI Responses client that re-sends the full conversation
every turn used to get a fresh UUIDv7 session-id on each request, which
collapses chatgpt.com's prompt-cache hit rate to roughly zero for that
shape of caller. Restore a content-derived fallback ahead of the random
mint: SHA-256 of `instructions + JSON(first input item)`.

Hashing the *first item* keeps the id stable as later turns append tail
items, which is what makes the upstream prompt cache continue to hit
across turns of the same conversation. The seed is type-agnostic so a
post-compaction snapshot (where the leading item is a `type: compaction`
blob with no preceding user message) still produces a stable id within
that new context window — the alternative of only seeding on user
messages would drop the cache on every post-compaction turn.

Stateful callers using `previous_response_id` reach this code path with
input already expanded from the snapshot in attempt.ts, so they pass
the same first item across turns and get the same id without ever
needing the snapshot-bound session resolver to fire.
…ctually runs

`prepareCodexResponsesRequest` (the provider's `prepareResponsesRequest`
hook) runs before the interceptor chain and always writes
`FLOWAY_CODEX_SESSION_ID_HEADER`, so the derive-from-input fallback in
`inject-session-id.ts` could never reach production — the interceptor
saw the FLOWAY header already populated and returned. The unit tests
exercised the path only by bypassing the prepare hook, giving false
coverage.

Move the derivation into `ensureCodexSessionId` (responses-state.ts)
where it sits between the snapshot-metadata lookup and the UUIDv7
fallback, so a stateless caller that re-sends the full conversation
each turn actually gets a stable session-id. Extend
`ProviderResponsesRequestContext` with `payload` so the hook can see
the input. Delete the now-redundant `inject-session-id.ts` interceptor
(fetch.ts's identity-resolution chain still covers direct provider
callers that bypass the gateway hook).
…SessionId removal

`synthesize-metadata-user-id.ts` had two comments pointing at the
deleted `injectSessionId` interceptor by name. Update the references
to name the live Codex equivalents (`responses-state.ts`'s derivation
chain, and `sha256Uuid` in `provider-codex/ids.ts`).
…seed tests

`deriveSessionIdFromInput` advertises a U+0001 separator between
instructions and the first-item JSON via its inline comment, but the
template literal carried plain concatenation — a crafted `instructions`
ending in `{"role":"user","content":"hello"}` could collide with another
conversation whose actual first item is that exact string. Add the
separator the comment already documents.

Round 2 review also flagged that two coverage cases from the deleted
`inject-session-id_test.ts` were not migrated when the derive logic
moved into `responses-state.ts`: distinct-instructions and
distinct-first-item should both yield distinct session-ids. Without
them, a regression that dropped either seed component from the hash
input would pass every other test. Add both as separate cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants