Files
OmniRoute/open-sse/config/codexIdentity.ts
Diego Rodrigues de Sa e Souza 1045e57aa1 feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)
* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697)

Codex CLI compatibility shim: the Responses API response.created/
response.in_progress/response.completed payloads now carry a `model`
field (previously absent), and for Codex-CLI-originated requests it
echoes the client-requested effort-suffixed model id (e.g.
gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex
CLI status line/model button shows the active reasoning effort.

- openai-responses.ts translator threads the upstream model into the
  Responses event objects (additive, omitted when unknown).
- New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's
  originator/User-Agent detection, header-based so it still fires when
  a combo routes codex/gpt-5.5-xhigh to a non-codex upstream.
- chatCore's existing opt-in #1311 echoModel pipeline now also fires
  automatically for Codex clients on the Responses API, regardless of
  the echoRequestedModelName setting.
- responseModelEcho.ts now also rewrites the nested response.model
  field the Responses API uses (previously only top-level model).
- /v1/models keeps returning models: [] for Codex (unchanged, #3481).

Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

Closes #3697

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)
2026-07-11 07:39:09 -03:00

119 lines
4.3 KiB
TypeScript

import { createHash, randomUUID } from "node:crypto";
import { normalizeCodexSessionId } from "./codexClient.ts";
const CODEX_INSTALLATION_SALT = "omniroute-codex-installation";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export type CodexClientIdentity = {
sessionId: string;
turnId: string;
windowId: string;
installationId: string;
};
function normalizeUuid(value: unknown): string | null {
return typeof value === "string" && UUID_PATTERN.test(value.trim()) ? value.trim() : null;
}
function uuidFromStableValue(value: string): string {
const hash = createHash("sha256").update(value).digest("hex");
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
}
export function getCodexInstallationId(
providerSpecificData?: Record<string, unknown> | null
): string {
const explicit = normalizeUuid(providerSpecificData?.codexInstallationId);
if (explicit) return explicit;
const stableSource =
typeof providerSpecificData?.workspaceId === "string" && providerSpecificData.workspaceId.trim()
? providerSpecificData.workspaceId.trim()
: typeof providerSpecificData?.accountId === "string" && providerSpecificData.accountId.trim()
? providerSpecificData.accountId.trim()
: typeof providerSpecificData?.email === "string" && providerSpecificData.email.trim()
? providerSpecificData.email.trim()
: "default";
return uuidFromStableValue(`${CODEX_INSTALLATION_SALT}:${stableSource}`);
}
export function createCodexClientIdentity(
sessionId: string | null,
providerSpecificData?: Record<string, unknown> | null
): CodexClientIdentity | null {
const normalizedSessionId = normalizeCodexSessionId(sessionId);
if (!normalizedSessionId) return null;
return {
sessionId: normalizedSessionId,
turnId: randomUUID(),
windowId: `${normalizedSessionId}:0`,
installationId: getCodexInstallationId(providerSpecificData),
};
}
export function applyCodexClientIdentityHeaders(
headers: Record<string, string>,
identity?: CodexClientIdentity | null
): void {
if (!identity) return;
headers["session_id"] = identity.sessionId;
headers["x-client-request-id"] = identity.sessionId;
headers["x-codex-window-id"] = identity.windowId;
headers["x-codex-turn-metadata"] = JSON.stringify({
session_id: identity.sessionId,
thread_source: "user",
turn_id: identity.turnId,
sandbox: "none",
});
}
/**
* #3697: detect the Codex CLI as the request *client* (not the routed provider) from
* request headers, so the model-echo shim can fire regardless of which upstream provider
* ultimately serves the request (e.g. `codex/gpt-5.5-xhigh` routed through a combo).
* Mirrors the `originator`/User-Agent detection proven in `isCodexModelCatalogClient`
* (PR #3481, `src/app/api/v1/models/catalogRequest.ts`) — Codex CLI sends an `originator`
* header of `codex_exec`/`codex_cli_rs` and a matching `codex_*` User-Agent — but works off
* a plain headers bag (`Headers` or a header-name→value record) instead of a `Request`,
* since chatCore's `clientRawRequest.headers` is not always a `Request`.
*/
export function isCodexOriginatedHeaders(
headers: Headers | Record<string, unknown> | null | undefined
): boolean {
const getHeader = (name: string): string => {
if (headers instanceof Headers) {
return headers.get(name)?.toLowerCase() ?? "";
}
if (headers && typeof headers === "object") {
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
if (key.toLowerCase() === name && typeof value === "string") {
return value.toLowerCase();
}
}
}
return "";
};
if (getHeader("originator").startsWith("codex")) return true;
return getHeader("user-agent").startsWith("codex");
}
export function applyCodexClientMetadata(
body: Record<string, unknown>,
identity?: CodexClientIdentity | null
): void {
if (!identity) return;
const existing =
body.client_metadata &&
typeof body.client_metadata === "object" &&
!Array.isArray(body.client_metadata)
? (body.client_metadata as Record<string, unknown>)
: {};
body.client_metadata = {
...existing,
"x-codex-installation-id": identity.installationId,
};
}