mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 11:12:17 +03:00
feat(codex): add OAuth fingerprint convergence modes (#10243)
* fix(deps): bump nanoid, dompurify for 2 new Dependabot alerts (#189, #190) Bumps: nanoid ^3.3.17 (was transitive, now overridden), dompurify ^3.4.13 (with monaco-editor scoped override). Closes Dependabot #189, #190. Remaining #182-#188 (js-yaml + mermaid) already closed by #9651 merge — awaiting Dependabot re-scan. npm audit → 0 vulnerabilities. * fix(repo): harden .gitignore to also ignore a _tasks symlink (/_tasks) _tasks is a SEPARATE nested git repo (gitignored). The pattern _tasks/ (trailing slash) ignores only a directory, not a SYMLINK named _tasks. A self-referential _tasks symlink can slip in via git add -A and, once pulled, checkout materializes it over the real _tasks repo (destroying plans/specs/hands-off). Anchored /_tasks ignores the symlink too, preventing re-capture. * feat(codex): converge OAuth fingerprints * test(codex): preserve identity assertions * fix(codex): preserve explicit off identity * fix(codex): close fingerprint transport gaps --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com> Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_rebaseline_2026_08_13_10243_codex_fingerprint_merge": "PR #10243 (xz-dev, Codex OAuth fingerprint convergence) merge into release/v3.8.50: src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts crossed the 1000-line new-file cap for the first time (974 on base, 997 on the PR's own branch, 1013 after merging + prettier reflow) purely from combining two independent, already-legitimate feature additions that landed on the same shared UI-helper file — this PR's own Codex fingerprint-mode select/toggle wiring (CODEX_FINGERPRINT_MODE_VALUES, getCodexFingerprintModeLabel, CodexFingerprintModeValue) plus #8949's unrelated Codex account-service-tier helpers merged concurrently on release/v3.8.50. Neither addition alone crosses the cap; git's line-level auto-merge does not detect a threshold crossing. Not modularized as part of this conflict-resolution merge commit (out of scope — this is a merge, not a feature change). Covered by the PR's own tests/unit/codex-fingerprint-convergence.test.ts, tests/unit/executor-codex.test.ts, tests/unit/provider-specific-data-schema.test.ts (all passing post-merge).",
|
||||
"_rebaseline_2026_08_09_8984_api_key_cache_mode": "PR #8984 own growth during the 2026-08-09 rebase: src/lib/db/apiKeys.ts 1529->1545 (+16 = the per-key apiKeys.cacheDefaultMode column + its row parsers and cascade wiring; additive at the existing connection write/read chokepoints). Covered by tests/unit/chatcore-semantic-cache.test.ts. (chatCore.ts stays at the pre-existing base-red ceiling — upstream tip already exceeds the frozen 5042, this PR only adds +2 on top; not re-bumped per the no-inherit-ratchet rule.)",
|
||||
"_rebaseline_2026_08_09_9207_breaker_halfopen_recovery": "PR #9207 own growth during the 2026-08-09 rebase: open-sse/services/accountFallback.ts 1978->2020 (+42 = recordProviderSuccess now also transitions the provider circuit breaker from HALF_OPEN to CLOSED when a request succeeds, so the breaker is not stuck half-open after repeated failures; the transition and its reset wiring grow the existing provider-success path, not extractable). Covered by tests/unit/provider-breaker-halfopen-recovery.test.ts.",
|
||||
"_rebaseline_2026_08_09_9351_antigravity_switch_auth": "PR #9351 own growth during the 2026-08-09 rebase: open-sse/executors/antigravity.ts 1528->1536 (+8 = switchAuth threaded out of tryResolveRetryFromErrorBody into handleAntigravityRateLimit's short-retry guard, so a decide429 switch decision beats the 60s same-account sleep; cohesive at the existing resolve chokepoint, not extractable). Covered by tests/unit/antigravity-429-switch-auth.test.ts.",
|
||||
@@ -444,7 +445,8 @@
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1250,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"src/lib/modelCapabilities.ts": 1006
|
||||
"src/lib/modelCapabilities.ts": 1006,
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
|
||||
@@ -3,81 +3,410 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { normalizeCodexSessionId } from "./codexClient.ts";
|
||||
|
||||
const CODEX_INSTALLATION_SALT = "omniroute-codex-installation";
|
||||
const CODEX_SESSION_SEED_PREFIX = "omniroute:codex-session-id:v1:";
|
||||
const CODEX_THREAD_SEED_PREFIX = "omniroute:codex-thread-id:v1:";
|
||||
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 const CODEX_FINGERPRINT_MODES = ["off", "device", "session", "full"] as const;
|
||||
export type CodexFingerprintMode = (typeof CODEX_FINGERPRINT_MODES)[number];
|
||||
export const CODEX_FINGERPRINT_MODE_KEY = "codexFingerprintMode";
|
||||
|
||||
export type CodexClientIdentity = {
|
||||
mode: CodexFingerprintMode;
|
||||
installationId: string;
|
||||
sessionId: string;
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
windowId: string;
|
||||
installationId: string;
|
||||
turnStartedAtUnixMs: number;
|
||||
};
|
||||
|
||||
type CodexIdentityOptions = {
|
||||
mode?: CodexFingerprintMode;
|
||||
accountKey?: string | null;
|
||||
isOAuth?: boolean;
|
||||
};
|
||||
|
||||
function normalizeUuid(value: unknown): string | null {
|
||||
return typeof value === "string" && UUID_PATTERN.test(value.trim()) ? value.trim() : null;
|
||||
}
|
||||
|
||||
function uuidFromStableValue(value: string): string {
|
||||
function nonEmptyString(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const normalized = value.trim();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
/** Keep the historical installation-id layout so existing accounts stay stable. */
|
||||
function uuidFromLegacyInstallationValue(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)}`;
|
||||
}
|
||||
|
||||
/** RFC4122 v4 from SHA-256. Same seed → same UUID. */
|
||||
export function deriveStableUUIDv4(seed: string): string {
|
||||
const digest = createHash("sha256").update(seed).digest();
|
||||
const bytes = Buffer.from(digest.subarray(0, 16));
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
return [
|
||||
bytes.subarray(0, 4).toString("hex"),
|
||||
bytes.subarray(4, 6).toString("hex"),
|
||||
bytes.subarray(6, 8).toString("hex"),
|
||||
bytes.subarray(8, 10).toString("hex"),
|
||||
bytes.subarray(10, 16).toString("hex"),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
function accountSeed(
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
accountKey?: string | null
|
||||
): string {
|
||||
return (
|
||||
nonEmptyString(accountKey) ||
|
||||
nonEmptyString(providerSpecificData?.connectionId) ||
|
||||
nonEmptyString(providerSpecificData?.workspaceId) ||
|
||||
nonEmptyString(providerSpecificData?.accountId) ||
|
||||
nonEmptyString(providerSpecificData?.email) ||
|
||||
"default"
|
||||
);
|
||||
}
|
||||
|
||||
function readNamedHeader(
|
||||
headers: Headers | Record<string, unknown> | null | undefined,
|
||||
name: string
|
||||
): string {
|
||||
if (!headers) return "";
|
||||
if (headers instanceof Headers) return headers.get(name)?.trim() || "";
|
||||
const wanted = name.toLowerCase();
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === wanted && typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function isCodexOAuthCredentials(
|
||||
credentials?: {
|
||||
accessToken?: unknown;
|
||||
refreshToken?: unknown;
|
||||
} | null
|
||||
): boolean {
|
||||
return Boolean(
|
||||
nonEmptyString(credentials?.accessToken) || nonEmptyString(credentials?.refreshToken)
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodexFingerprintMode(
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
isOAuth = true
|
||||
): CodexFingerprintMode {
|
||||
if (!isOAuth) return "off";
|
||||
const raw = (
|
||||
nonEmptyString(providerSpecificData?.[CODEX_FINGERPRINT_MODE_KEY]) ||
|
||||
nonEmptyString(providerSpecificData?.codex_fingerprint_mode) ||
|
||||
""
|
||||
).toLowerCase();
|
||||
return (CODEX_FINGERPRINT_MODES as readonly string[]).includes(raw)
|
||||
? (raw as CodexFingerprintMode)
|
||||
: "session";
|
||||
}
|
||||
|
||||
export function getCodexInstallationId(
|
||||
providerSpecificData?: Record<string, unknown> | null
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
accountKey?: string | 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";
|
||||
const legacyStableSource =
|
||||
nonEmptyString(providerSpecificData?.workspaceId) ||
|
||||
nonEmptyString(providerSpecificData?.accountId) ||
|
||||
nonEmptyString(providerSpecificData?.email);
|
||||
if (legacyStableSource) {
|
||||
return uuidFromLegacyInstallationValue(`${CODEX_INSTALLATION_SALT}:${legacyStableSource}`);
|
||||
}
|
||||
|
||||
return uuidFromStableValue(`${CODEX_INSTALLATION_SALT}:${stableSource}`);
|
||||
return deriveStableUUIDv4(
|
||||
`${CODEX_INSTALLATION_SALT}:${accountSeed(providerSpecificData, accountKey)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodexConvergedSessionId(
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
accountKey?: string | null
|
||||
): string {
|
||||
return deriveStableUUIDv4(
|
||||
`${CODEX_SESSION_SEED_PREFIX}${accountSeed(providerSpecificData, accountKey)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodexConvergedThreadId(
|
||||
clientSessionId: string | null,
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
accountKey?: string | null
|
||||
): string {
|
||||
if (!nonEmptyString(clientSessionId)) return "";
|
||||
return deriveStableUUIDv4(
|
||||
`${CODEX_THREAD_SEED_PREFIX}${accountSeed(providerSpecificData, accountKey)}:${clientSessionId}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodexClientSessionId(
|
||||
headers: Headers | Record<string, unknown> | null | undefined
|
||||
): string | null {
|
||||
return (
|
||||
normalizeCodexSessionId(readNamedHeader(headers, "session-id")) ||
|
||||
normalizeCodexSessionId(readNamedHeader(headers, "session_id")) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One identity object for every carrier in one upstream turn.
|
||||
* accountKey may be the OmniRoute connection id; it is never sent upstream.
|
||||
*/
|
||||
export function createCodexClientIdentity(
|
||||
sessionId: string | null,
|
||||
providerSpecificData?: Record<string, unknown> | null
|
||||
clientSessionId: string | null,
|
||||
providerSpecificData?: Record<string, unknown> | null,
|
||||
options: CodexIdentityOptions = {}
|
||||
): CodexClientIdentity | null {
|
||||
const normalizedSessionId = normalizeCodexSessionId(sessionId);
|
||||
if (!normalizedSessionId) return null;
|
||||
const mode =
|
||||
options.mode ?? getCodexFingerprintMode(providerSpecificData, options.isOAuth ?? true);
|
||||
if (mode === "off") return null;
|
||||
|
||||
const installationId = getCodexInstallationId(providerSpecificData, options.accountKey);
|
||||
if (mode === "device") {
|
||||
return {
|
||||
mode,
|
||||
installationId,
|
||||
sessionId: "",
|
||||
threadId: "",
|
||||
turnId: "",
|
||||
windowId: "",
|
||||
turnStartedAtUnixMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionId = getCodexConvergedSessionId(providerSpecificData, options.accountKey);
|
||||
const threadId =
|
||||
mode === "full"
|
||||
? sessionId
|
||||
: getCodexConvergedThreadId(clientSessionId, providerSpecificData, options.accountKey) ||
|
||||
sessionId;
|
||||
|
||||
return {
|
||||
sessionId: normalizedSessionId,
|
||||
mode,
|
||||
installationId,
|
||||
sessionId,
|
||||
threadId,
|
||||
turnId: randomUUID(),
|
||||
windowId: `${normalizedSessionId}:0`,
|
||||
installationId: getCodexInstallationId(providerSpecificData),
|
||||
windowId: `${threadId}:0`,
|
||||
turnStartedAtUnixMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function isCompactRequestEndpoint(path: unknown): boolean {
|
||||
if (typeof path !== "string") return false;
|
||||
const normalized = path.trim().toLowerCase().replace(/\\/g, "/");
|
||||
return normalized === "/compact" || /(?:^|\/)responses\/compact(?:\/|$)/.test(normalized);
|
||||
}
|
||||
|
||||
const CODEX_IDENTITY_HEADER_NAMES = [
|
||||
"session-id",
|
||||
"session_id",
|
||||
"thread-id",
|
||||
"thread_id",
|
||||
"x-client-request-id",
|
||||
"x-codex-installation-id",
|
||||
"x-codex-window-id",
|
||||
"x-codex-turn-metadata",
|
||||
] as const;
|
||||
|
||||
type CodexCredentialIdentityInput = {
|
||||
connectionId?: string;
|
||||
requestEndpointPath?: string;
|
||||
accessToken?: unknown;
|
||||
refreshToken?: unknown;
|
||||
providerSpecificData?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export function resolveCodexOriginalIdentityHeaders(input: {
|
||||
credentials?: CodexCredentialIdentityInput | null;
|
||||
clientHeaders?: Headers | Record<string, unknown> | null;
|
||||
}): Record<string, string> | null {
|
||||
const credentials = input.credentials;
|
||||
if (!credentials || isCompactRequestEndpoint(credentials.requestEndpointPath)) return null;
|
||||
const providerSpecificData = credentials.providerSpecificData ?? null;
|
||||
if (
|
||||
!isCodexOAuthCredentials(credentials) ||
|
||||
getCodexFingerprintMode(providerSpecificData, true) !== "off"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const name of CODEX_IDENTITY_HEADER_NAMES) {
|
||||
const value = readNamedHeader(input.clientHeaders, name);
|
||||
if (value) result[name] = value;
|
||||
}
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/** One identity for headers, body, nested metadata, and WS payload. Compact skips. */
|
||||
export function resolveCodexFingerprintIdentity(input: {
|
||||
credentials?: CodexCredentialIdentityInput | null;
|
||||
clientHeaders?: Headers | Record<string, unknown> | null;
|
||||
body?: unknown;
|
||||
}): CodexClientIdentity | null {
|
||||
const credentials = input.credentials;
|
||||
if (!credentials || isCompactRequestEndpoint(credentials.requestEndpointPath)) return null;
|
||||
|
||||
const providerSpecificData = credentials.providerSpecificData ?? null;
|
||||
const isOAuth = isCodexOAuthCredentials(credentials);
|
||||
if (getCodexFingerprintMode(providerSpecificData, isOAuth) === "off") return null;
|
||||
|
||||
return createCodexClientIdentity(
|
||||
getCodexClientSessionId(input.clientHeaders),
|
||||
providerSpecificData,
|
||||
{
|
||||
accountKey: credentials.connectionId ?? null,
|
||||
isOAuth,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function withCodexFingerprintCredentials<T extends CodexCredentialIdentityInput>(
|
||||
credentials: T,
|
||||
clientHeaders?: Headers | Record<string, unknown> | null,
|
||||
body?: unknown
|
||||
): T {
|
||||
const identity = resolveCodexFingerprintIdentity({ credentials, clientHeaders, body });
|
||||
const original = resolveCodexOriginalIdentityHeaders({ credentials, clientHeaders });
|
||||
if (!identity && !original) return credentials;
|
||||
return {
|
||||
...credentials,
|
||||
providerSpecificData: {
|
||||
...(credentials.providerSpecificData || {}),
|
||||
...(identity ? { codexClientIdentity: identity } : {}),
|
||||
...(original ? { codexOriginalIdentityHeaders: original } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mergeTurnMetadata(
|
||||
raw: unknown,
|
||||
identity: CodexClientIdentity,
|
||||
includeSessionFields: boolean
|
||||
): string {
|
||||
let metadata: Record<string, unknown> = {};
|
||||
let hadExisting = false;
|
||||
if (typeof raw === "string" && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
metadata = parsed as Record<string, unknown>;
|
||||
hadExisting = true;
|
||||
}
|
||||
} catch {
|
||||
// Keep non-JSON metadata only when we do not need a complete carrier.
|
||||
}
|
||||
}
|
||||
|
||||
if (!hadExisting && includeSessionFields) {
|
||||
metadata.thread_source = "user";
|
||||
metadata.sandbox = "none";
|
||||
}
|
||||
|
||||
metadata.installation_id = identity.installationId;
|
||||
if (includeSessionFields) {
|
||||
metadata.session_id = identity.sessionId;
|
||||
metadata.thread_id = identity.threadId || identity.sessionId;
|
||||
metadata.turn_id = identity.turnId;
|
||||
metadata.window_id = identity.windowId;
|
||||
metadata.turn_started_at_unix_ms = identity.turnStartedAtUnixMs;
|
||||
}
|
||||
return JSON.stringify(metadata);
|
||||
}
|
||||
|
||||
export function applyCodexOriginalIdentityHeaders(
|
||||
headers: Record<string, string>,
|
||||
original?: Record<string, string> | null
|
||||
): void {
|
||||
if (!original) return;
|
||||
for (const name of CODEX_IDENTITY_HEADER_NAMES) {
|
||||
const value = original[name];
|
||||
if (typeof value === "string" && value) headers[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCodexClientIdentityHeaders(
|
||||
headers: Record<string, string>,
|
||||
identity?: CodexClientIdentity | null
|
||||
): void {
|
||||
if (!identity) return;
|
||||
|
||||
headers["x-codex-installation-id"] = identity.installationId;
|
||||
if (identity.mode === "device") {
|
||||
if (headers["x-codex-turn-metadata"] !== undefined) {
|
||||
headers["x-codex-turn-metadata"] = mergeTurnMetadata(
|
||||
headers["x-codex-turn-metadata"],
|
||||
identity,
|
||||
false
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
headers["session-id"] = identity.sessionId;
|
||||
headers["session_id"] = identity.sessionId;
|
||||
headers["x-client-request-id"] = identity.sessionId;
|
||||
headers["thread-id"] = identity.threadId || identity.sessionId;
|
||||
headers["x-client-request-id"] = identity.threadId || 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",
|
||||
});
|
||||
headers["x-codex-turn-metadata"] = mergeTurnMetadata(
|
||||
headers["x-codex-turn-metadata"],
|
||||
identity,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
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>) }
|
||||
: {};
|
||||
existing["x-codex-installation-id"] = identity.installationId;
|
||||
|
||||
if (identity.mode !== "device") {
|
||||
existing.session_id = identity.sessionId;
|
||||
existing.thread_id = identity.threadId || identity.sessionId;
|
||||
existing.turn_id = identity.turnId;
|
||||
existing["x-codex-window-id"] = identity.windowId;
|
||||
}
|
||||
|
||||
if (existing["x-codex-turn-metadata"] !== undefined) {
|
||||
existing["x-codex-turn-metadata"] = mergeTurnMetadata(
|
||||
existing["x-codex-turn-metadata"],
|
||||
identity,
|
||||
identity.mode !== "device"
|
||||
);
|
||||
}
|
||||
|
||||
body.client_metadata = existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* #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
|
||||
@@ -132,20 +461,3 @@ export function isVerifiedNativeCodexRequest(
|
||||
): boolean {
|
||||
return isCodexOriginatedHeaders(headers) && hasNativeCodexTurnBinding(body);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ import {
|
||||
import {
|
||||
applyCodexClientIdentityHeaders,
|
||||
applyCodexClientMetadata,
|
||||
createCodexClientIdentity,
|
||||
applyCodexOriginalIdentityHeaders,
|
||||
type CodexClientIdentity,
|
||||
withCodexFingerprintCredentials,
|
||||
} from "../config/codexIdentity.ts";
|
||||
import { getAccessToken } from "../services/tokenRefresh.ts";
|
||||
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
|
||||
@@ -765,23 +766,11 @@ export class CodexExecutor extends BaseExecutor {
|
||||
input.model
|
||||
);
|
||||
const requestInput = requestBody === input.body ? input : { ...input, body: requestBody };
|
||||
const sessionId = this.getPromptCacheSessionId(
|
||||
const credentials = withCodexFingerprintCredentials(
|
||||
requestInput.credentials,
|
||||
requestInput.body as Record<string, unknown> | null
|
||||
requestInput.clientHeaders,
|
||||
requestInput.body
|
||||
);
|
||||
const identity = createCodexClientIdentity(
|
||||
sessionId,
|
||||
requestInput.credentials?.providerSpecificData ?? null
|
||||
);
|
||||
const credentials = identity
|
||||
? {
|
||||
...requestInput.credentials,
|
||||
providerSpecificData: {
|
||||
...(requestInput.credentials?.providerSpecificData || {}),
|
||||
codexClientIdentity: identity,
|
||||
},
|
||||
}
|
||||
: requestInput.credentials;
|
||||
const nextInput = { ...requestInput, credentials };
|
||||
|
||||
if (!isCodexResponsesWebSocketRequired(nextInput.model, nextInput.credentials)) {
|
||||
@@ -1054,6 +1043,8 @@ export class CodexExecutor extends BaseExecutor {
|
||||
}
|
||||
const clientIdentity = credentials?.providerSpecificData?.codexClientIdentity as
|
||||
CodexClientIdentity | null | undefined;
|
||||
const originalIdentityHeaders = credentials?.providerSpecificData
|
||||
?.codexOriginalIdentityHeaders as Record<string, string> | null | undefined;
|
||||
|
||||
// Originator header — identifies the client type to the Codex backend.
|
||||
// Ref: openai/codex login/src/auth/default_client.rs DEFAULT_ORIGINATOR = "codex_cli_rs"
|
||||
@@ -1066,6 +1057,7 @@ export class CodexExecutor extends BaseExecutor {
|
||||
if (cacheSessionId) {
|
||||
headers["session_id"] = cacheSessionId;
|
||||
}
|
||||
applyCodexOriginalIdentityHeaders(headers, originalIdentityHeaders);
|
||||
applyCodexClientIdentityHeaders(headers, clientIdentity);
|
||||
|
||||
return headers;
|
||||
|
||||
@@ -317,6 +317,17 @@ function getAuthHeaders(requestUrl, requestHeaders) {
|
||||
if (isText(requestHeaders["x-forwarded-for"])) {
|
||||
headers["x-forwarded-for"] = requestHeaders["x-forwarded-for"];
|
||||
}
|
||||
for (const key of [
|
||||
"session-id",
|
||||
"session_id",
|
||||
"x-codex-installation-id",
|
||||
"x-codex-window-id",
|
||||
"x-codex-turn-metadata",
|
||||
"originator",
|
||||
"user-agent",
|
||||
]) {
|
||||
if (isText(requestHeaders[key])) headers[key] = requestHeaders[key];
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,18 @@ function getForwardHeaders(requestUrl, requestHeaders) {
|
||||
headers.origin = origin;
|
||||
}
|
||||
|
||||
for (const key of [
|
||||
"session-id",
|
||||
"session_id",
|
||||
"x-codex-installation-id",
|
||||
"x-codex-window-id",
|
||||
"x-codex-turn-metadata",
|
||||
"originator",
|
||||
"user-agent",
|
||||
]) {
|
||||
if (isText(requestHeaders[key])) headers[key] = requestHeaders[key];
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Select, Toggle } from "@/shared/components";
|
||||
import { type CodexServiceTier } from "@/lib/providers/requestDefaults";
|
||||
import {
|
||||
CODEX_ACCOUNT_SERVICE_TIER_VALUES,
|
||||
CODEX_FINGERPRINT_MODE_VALUES,
|
||||
CODEX_REASONING_STRENGTH_OPTIONS,
|
||||
getCodexFingerprintModeLabel,
|
||||
getCodexServiceTierLabel,
|
||||
providerText,
|
||||
type CodexFingerprintModeValue,
|
||||
} from "../../providerPageHelpers";
|
||||
|
||||
type Translator = Parameters<typeof getCodexFingerprintModeLabel>[0];
|
||||
|
||||
export function CodexConnectionFields({
|
||||
t,
|
||||
reasoningEffort,
|
||||
serviceTier,
|
||||
fingerprintMode,
|
||||
openaiStoreEnabled,
|
||||
showFingerprintMode,
|
||||
onChange,
|
||||
}: {
|
||||
t: Translator;
|
||||
reasoningEffort: string;
|
||||
serviceTier: CodexServiceTier;
|
||||
fingerprintMode: CodexFingerprintModeValue;
|
||||
openaiStoreEnabled: boolean;
|
||||
showFingerprintMode: boolean;
|
||||
onChange: (patch: {
|
||||
codexReasoningEffort?: string;
|
||||
codexServiceTier?: CodexServiceTier;
|
||||
codexFingerprintMode?: CodexFingerprintModeValue;
|
||||
codexOpenaiStoreEnabled?: boolean;
|
||||
}) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
<Select
|
||||
label={t("defaultThinkingStrengthLabel")}
|
||||
value={reasoningEffort}
|
||||
options={CODEX_REASONING_STRENGTH_OPTIONS}
|
||||
onChange={(e) => onChange({ codexReasoningEffort: e.target.value })}
|
||||
hint={t("defaultThinkingStrengthHint")}
|
||||
/>
|
||||
<Select
|
||||
label={providerText(t, "codexServiceTierLabel", "Codex service tier")}
|
||||
value={serviceTier}
|
||||
options={CODEX_ACCOUNT_SERVICE_TIER_VALUES.map((value) => ({
|
||||
value,
|
||||
label: getCodexServiceTierLabel(t, value),
|
||||
}))}
|
||||
onChange={(event) => onChange({ codexServiceTier: event.target.value as CodexServiceTier })}
|
||||
hint={providerText(
|
||||
t,
|
||||
"codexServiceTierDescription",
|
||||
"Default uses the normal Codex tier. Priority shows as Fast; Flex uses the flex service tier when available."
|
||||
)}
|
||||
/>
|
||||
{showFingerprintMode && (
|
||||
<Select
|
||||
label={providerText(t, "codexFingerprintModeLabel", "Codex fingerprint mode")}
|
||||
value={fingerprintMode}
|
||||
options={CODEX_FINGERPRINT_MODE_VALUES.map((mode) => ({
|
||||
value: mode,
|
||||
label: getCodexFingerprintModeLabel(t, mode),
|
||||
}))}
|
||||
onChange={(event) =>
|
||||
onChange({ codexFingerprintMode: event.target.value as CodexFingerprintModeValue })
|
||||
}
|
||||
hint={providerText(
|
||||
t,
|
||||
"codexFingerprintModeDescription",
|
||||
"Default Session converges one device and session per account. Off passes client IDs through."
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Toggle
|
||||
checked={openaiStoreEnabled}
|
||||
onChange={(checked) => onChange({ codexOpenaiStoreEnabled: checked })}
|
||||
label={t("openaiResponsesStoreLabel")}
|
||||
description={t("openaiResponsesStoreDescription")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button, Badge, Input, Modal, Toggle, Select } from "@/shared/components";
|
||||
import {
|
||||
@@ -37,10 +38,9 @@ import {
|
||||
getWebSessionCredentialCheckLabel,
|
||||
getLocalProviderMetadata,
|
||||
normalizeAndValidateHttpBaseUrl,
|
||||
CODEX_REASONING_STRENGTH_OPTIONS,
|
||||
CODEX_ACCOUNT_SERVICE_TIER_VALUES,
|
||||
getCodexServiceTierLabel,
|
||||
getCodexFingerprintMode,
|
||||
getCodexRequestDefaults,
|
||||
type CodexFingerprintModeValue,
|
||||
getClaudeCodeCompatibleRequestDefaults,
|
||||
providerText,
|
||||
ERROR_TYPE_LABELS,
|
||||
@@ -50,6 +50,7 @@ import { getWebSessionCredentialRequirement } from "../../webSessionCredentials"
|
||||
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
|
||||
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
|
||||
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
|
||||
import { CodexConnectionFields } from "./CodexFingerprintFields";
|
||||
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
|
||||
import { isM365TierCapableProvider, normalizeM365TierValue, type M365TierValue } from "./m365Tier";
|
||||
import ProviderTierField from "./ProviderTierField";
|
||||
@@ -123,6 +124,8 @@ export default function EditConnectionModal({
|
||||
accountId: "",
|
||||
codexReasoningEffort: "medium",
|
||||
codexServiceTier: "default" as CodexServiceTier,
|
||||
codexFingerprintMode: "session" as CodexFingerprintModeValue,
|
||||
codexOpenaiStoreEnabled: false,
|
||||
openaiResponsesStoreEnabled: false,
|
||||
preserveEncryptedReasoning: false,
|
||||
consoleApiKey: "",
|
||||
@@ -244,14 +247,6 @@ export default function EditConnectionModal({
|
||||
: apiKeyOptional
|
||||
? t("apiKeyOptionalHint")
|
||||
: t("leaveBlankKeepCurrentApiKey");
|
||||
const codexAccountServiceTierOptions = useMemo(
|
||||
() =>
|
||||
CODEX_ACCOUNT_SERVICE_TIER_VALUES.map((value) => ({
|
||||
value,
|
||||
label: getCodexServiceTierLabel(t, value),
|
||||
})),
|
||||
[t]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isOpen && connection) {
|
||||
const effectiveProvider = connection.provider || providerId;
|
||||
@@ -330,6 +325,8 @@ export default function EditConnectionModal({
|
||||
accountId: existingAccountId,
|
||||
codexReasoningEffort: codexRequestDefaults.reasoningEffort,
|
||||
codexServiceTier: codexRequestDefaults.serviceTier ?? "default",
|
||||
codexFingerprintMode: getCodexFingerprintMode(connection.providerSpecificData),
|
||||
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
|
||||
openaiResponsesStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
|
||||
preserveEncryptedReasoning:
|
||||
connection.providerSpecificData?.preserveEncryptedReasoning === true,
|
||||
@@ -599,6 +596,7 @@ export default function EditConnectionModal({
|
||||
updates.providerSpecificData = {
|
||||
...(connection.providerSpecificData || {}),
|
||||
...(validationPsd || {}),
|
||||
...(isCodex ? { codexFingerprintMode: null, codex_fingerprint_mode: null } : {}),
|
||||
};
|
||||
assignEditApiKeyProviderSpecificData({
|
||||
provider,
|
||||
@@ -634,6 +632,9 @@ export default function EditConnectionModal({
|
||||
? { serviceTier: formData.codexServiceTier }
|
||||
: {}),
|
||||
};
|
||||
updates.providerSpecificData.openaiStoreEnabled =
|
||||
formData.codexOpenaiStoreEnabled === true;
|
||||
updates.providerSpecificData.codexFingerprintMode = formData.codexFingerprintMode;
|
||||
}
|
||||
if (isAntigravityFamily) {
|
||||
updates.providerSpecificData.projectId = trimmedCloudCodeProjectId || null;
|
||||
@@ -707,9 +708,7 @@ export default function EditConnectionModal({
|
||||
const openaiResponsesStoreToggle = isResponsesConnection ? (
|
||||
<Toggle
|
||||
checked={formData.openaiResponsesStoreEnabled}
|
||||
onChange={(checked) =>
|
||||
setFormData({ ...formData, openaiResponsesStoreEnabled: checked })
|
||||
}
|
||||
onChange={(checked) => setFormData({ ...formData, openaiResponsesStoreEnabled: checked })}
|
||||
label={t("openaiResponsesStoreLabel")}
|
||||
description={t("openaiResponsesStoreDescription")}
|
||||
/>
|
||||
@@ -745,31 +744,15 @@ export default function EditConnectionModal({
|
||||
hint={t("excludedModelsHint")}
|
||||
/>
|
||||
{isCodex && (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
<Select
|
||||
label={t("defaultThinkingStrengthLabel")}
|
||||
value={formData.codexReasoningEffort}
|
||||
options={CODEX_REASONING_STRENGTH_OPTIONS}
|
||||
onChange={(e) => setFormData({ ...formData, codexReasoningEffort: e.target.value })}
|
||||
hint={t("defaultThinkingStrengthHint")}
|
||||
/>
|
||||
<Select
|
||||
label={providerText(t, "codexServiceTierLabel", "Codex service tier")}
|
||||
value={formData.codexServiceTier}
|
||||
options={codexAccountServiceTierOptions}
|
||||
onChange={(event) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
codexServiceTier: event.target.value as CodexServiceTier,
|
||||
})
|
||||
}
|
||||
hint={providerText(
|
||||
t,
|
||||
"codexServiceTierDescription",
|
||||
"Default uses the normal Codex tier. Priority shows as Fast; Flex uses the flex service tier when available."
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<CodexConnectionFields
|
||||
t={t}
|
||||
reasoningEffort={formData.codexReasoningEffort}
|
||||
serviceTier={formData.codexServiceTier}
|
||||
fingerprintMode={formData.codexFingerprintMode}
|
||||
openaiStoreEnabled={formData.codexOpenaiStoreEnabled}
|
||||
showFingerprintMode={isOAuth}
|
||||
onChange={(patch) => setFormData({ ...formData, ...patch })}
|
||||
/>
|
||||
)}
|
||||
{isClaude && (
|
||||
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
|
||||
@@ -167,7 +167,9 @@ export function evaluateTestAllEntry(
|
||||
autoHideFailed: boolean
|
||||
): TestAllModelOutcome {
|
||||
const ok = entry?.status === "ok";
|
||||
const transient = [entry?.rateLimited, entry?.isTimeout, entry?.isTransient, entry?.isQuota].some(Boolean);
|
||||
const transient = [entry?.rateLimited, entry?.isTimeout, entry?.isTransient, entry?.isQuota].some(
|
||||
Boolean
|
||||
);
|
||||
return {
|
||||
status: ok ? "ok" : "error",
|
||||
// #9511: quota errors (isQuota) are surfaced on the icon but kept visible
|
||||
@@ -628,6 +630,43 @@ export const CODEX_ACCOUNT_SERVICE_TIER_VALUES: CodexServiceTier[] = [
|
||||
"flex",
|
||||
];
|
||||
|
||||
export const CODEX_FINGERPRINT_MODE_VALUES = ["off", "device", "session", "full"] as const;
|
||||
export type CodexFingerprintModeValue = (typeof CODEX_FINGERPRINT_MODE_VALUES)[number];
|
||||
|
||||
export function getCodexFingerprintMode(providerSpecificData: unknown): CodexFingerprintModeValue {
|
||||
const data =
|
||||
providerSpecificData &&
|
||||
typeof providerSpecificData === "object" &&
|
||||
!Array.isArray(providerSpecificData)
|
||||
? (providerSpecificData as Record<string, unknown>)
|
||||
: undefined;
|
||||
const raw = data?.codexFingerprintMode ?? data?.codex_fingerprint_mode;
|
||||
const normalized = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
return (CODEX_FINGERPRINT_MODE_VALUES as readonly string[]).includes(normalized)
|
||||
? (normalized as CodexFingerprintModeValue)
|
||||
: "session";
|
||||
}
|
||||
|
||||
export function getCodexFingerprintModeLabel(
|
||||
t: ProviderMessageTranslator,
|
||||
value: CodexFingerprintModeValue
|
||||
): string {
|
||||
if (value === "off") {
|
||||
return providerText(t, "codexFingerprintModeOff", "Off — pass client IDs through");
|
||||
}
|
||||
if (value === "device") {
|
||||
return providerText(t, "codexFingerprintModeDevice", "Device — one installation ID");
|
||||
}
|
||||
if (value === "full") {
|
||||
return providerText(t, "codexFingerprintModeFull", "Full — one device, session, and thread");
|
||||
}
|
||||
return providerText(
|
||||
t,
|
||||
"codexFingerprintModeSession",
|
||||
"Session — one device and session, thread per client"
|
||||
);
|
||||
}
|
||||
|
||||
export const CODEX_GLOBAL_SERVICE_MODE_VALUES: CodexGlobalServiceMode[] = [
|
||||
"none",
|
||||
...CODEX_ACCOUNT_SERVICE_TIER_VALUES,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { logger } from "@omniroute/open-sse/utils/logger.ts";
|
||||
import { resolveProxy } from "@omniroute/open-sse/utils/networkProxy.ts";
|
||||
import { withCodexFingerprintCredentials } from "@omniroute/open-sse/config/codexIdentity.ts";
|
||||
import { proxyConfigToUrl } from "@omniroute/open-sse/utils/proxyDispatcher.ts";
|
||||
import {
|
||||
attachReasoningRuleDirective,
|
||||
@@ -434,6 +435,7 @@ async function resolveCodexRequestContext(body: JsonRecord) {
|
||||
apiKey,
|
||||
responseBody,
|
||||
requestedModel,
|
||||
clientHeaders: Object.fromEntries(authRequest.headers.entries()),
|
||||
metadata,
|
||||
allowedConnections,
|
||||
...reasoningRoute,
|
||||
@@ -542,17 +544,22 @@ async function prepare(body: JsonRecord) {
|
||||
model,
|
||||
requestId: randomUUID(),
|
||||
});
|
||||
const credentialsWithFingerprint = withCodexFingerprintCredentials(
|
||||
refreshedCredentials,
|
||||
context.clientHeaders,
|
||||
responseBodyWithMemory
|
||||
);
|
||||
const transformed = (await executor.transformRequest(
|
||||
model,
|
||||
responseBodyWithMemory,
|
||||
true,
|
||||
refreshedCredentials
|
||||
credentialsWithFingerprint
|
||||
)) as JsonRecord;
|
||||
transformed.model = model;
|
||||
delete transformed.stream;
|
||||
delete transformed.stream_options;
|
||||
|
||||
const headers = normalizeUpstreamHeaders(executor.buildHeaders(refreshedCredentials, true));
|
||||
const headers = normalizeUpstreamHeaders(executor.buildHeaders(credentialsWithFingerprint, true));
|
||||
|
||||
// #5611: apply the configured Global/provider proxy to the upstream Codex
|
||||
// Responses WebSocket too. The downstream client→OmniRoute hop works, but the
|
||||
|
||||
@@ -193,6 +193,11 @@ export function normalizeProviderSpecificData(
|
||||
delete normalized.openaiStoreEnabled;
|
||||
}
|
||||
|
||||
if (provider === "codex") {
|
||||
if (normalized.codexFingerprintMode === null) delete normalized.codexFingerprintMode;
|
||||
if (normalized.codex_fingerprint_mode === null) delete normalized.codex_fingerprint_mode;
|
||||
}
|
||||
|
||||
if (
|
||||
"preserveEncryptedReasoning" in normalized &&
|
||||
typeof normalized.preserveEncryptedReasoning !== "boolean"
|
||||
|
||||
@@ -15,6 +15,7 @@ function isHttpUrl(value: string): boolean {
|
||||
|
||||
const CODEX_REASONING_EFFORT_VALUES = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
|
||||
const REQUEST_DEFAULT_SERVICE_TIER_VALUES = new Set(["default", "priority", "fast", "flex"]);
|
||||
const CODEX_FINGERPRINT_MODE_VALUES = new Set(["off", "device", "session", "full"]);
|
||||
const CACHE_PASSTHROUGH_VALUES = new Set(["strip", "openai-format", "claude-format"]);
|
||||
|
||||
// #6880 — per-connection prompt-cache capability override, extracted so
|
||||
@@ -154,6 +155,20 @@ export function validateProviderSpecificData(
|
||||
});
|
||||
}
|
||||
|
||||
const codexFingerprintMode = data.codexFingerprintMode;
|
||||
if (codexFingerprintMode !== undefined && codexFingerprintMode !== null) {
|
||||
const normalized =
|
||||
typeof codexFingerprintMode === "string" ? codexFingerprintMode.trim().toLowerCase() : "";
|
||||
if (normalized && !CODEX_FINGERPRINT_MODE_VALUES.has(normalized)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"providerSpecificData.codexFingerprintMode must be one of off, device, session, full",
|
||||
path: ["codexFingerprintMode"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const preserveEncryptedReasoning = data.preserveEncryptedReasoning;
|
||||
if (preserveEncryptedReasoning !== undefined && typeof preserveEncryptedReasoning !== "boolean") {
|
||||
ctx.addIssue({
|
||||
|
||||
@@ -595,11 +595,10 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
|
||||
assert.equal(callLog.tokens.reasoning, 13);
|
||||
});
|
||||
|
||||
test("chat pipeline applies global Codex priority service tier inside combos", async () => {
|
||||
await seedConnection("codex", { apiKey: "sk-codex-combo-priority" });
|
||||
await settingsDb.updateSettings({
|
||||
codexServiceTier: { enabled: true, tier: "priority" },
|
||||
});
|
||||
test("chat pipeline applies Codex OAuth fingerprint and priority tier inside combos", async () => {
|
||||
setCliCompatProviders(["codex"]);
|
||||
await seedConnection("codex", { authType: "oauth", accessToken: "codex-combo-oauth-token" });
|
||||
await settingsDb.updateSettings({ codexServiceTier: { enabled: true, tier: "priority" } });
|
||||
await combosDb.createCombo({
|
||||
name: "codex-priority-combo",
|
||||
strategy: "priority",
|
||||
@@ -607,10 +606,8 @@ test("chat pipeline applies global Codex priority service tier inside combos", a
|
||||
models: ["codex/gpt-5.5"],
|
||||
});
|
||||
const fetchCalls = [];
|
||||
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
globalThis.fetch = async (_url, init: RequestInit = {}) => {
|
||||
fetchCalls.push({
|
||||
url: String(url),
|
||||
headers: toPlainHeaders(init.headers),
|
||||
body: init.body ? JSON.parse(String(init.body)) : null,
|
||||
});
|
||||
@@ -619,21 +616,24 @@ test("chat pipeline applies global Codex priority service tier inside combos", a
|
||||
|
||||
const response = await handleChat(
|
||||
buildRequest({
|
||||
url: "http://localhost/v1/responses",
|
||||
headers: { "session-id": "combo-client-session" },
|
||||
body: {
|
||||
model: "codex-priority-combo",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "Use Codex combo priority" }],
|
||||
input: "Use Codex combo priority",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const json = (await response.json()) as any;
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(json.object, "response");
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.match(fetchCalls[0].url, /\/responses$/);
|
||||
assert.equal(fetchCalls[0].headers.Authorization, "Bearer sk-codex-combo-priority");
|
||||
assert.equal(fetchCalls[0].body.service_tier, "priority");
|
||||
assert.equal(json.choices[0].message.content, "combo priority ok");
|
||||
const [call] = fetchCalls;
|
||||
assert.equal(call.headers.Authorization, "Bearer codex-combo-oauth-token");
|
||||
assert.notEqual(call.headers["session-id"], "combo-client-session");
|
||||
assert.equal(call.headers["session-id"], call.body.client_metadata.session_id);
|
||||
assert.equal(call.body.service_tier, "priority");
|
||||
});
|
||||
|
||||
test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests", async () => {
|
||||
@@ -696,7 +696,11 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
|
||||
call.headers["User-Agent"],
|
||||
`codex-cli/${getCodexClientVersion()} (Windows 10.0.26200; x64)`
|
||||
);
|
||||
assert.equal(call.headers["x-codex-window-id"], "conv_codex_fingerprint:0");
|
||||
// Session convergence derives a fresh session/thread id instead of passing the
|
||||
// client's raw conversation_id straight through, so the window id must be derived
|
||||
// from the (converged) request id header, not the original client-supplied literal.
|
||||
assert.notEqual(call.headers["session_id"], "conv_codex_fingerprint");
|
||||
assert.equal(call.headers["x-codex-window-id"], `${call.headers["x-client-request-id"]}:0`);
|
||||
assert.ok(call.headers["x-client-request-id"], "expected Codex request id header");
|
||||
assert.ok(call.headers["x-codex-turn-metadata"], "expected Codex turn metadata header");
|
||||
|
||||
|
||||
@@ -40,9 +40,8 @@ process.env.JWT_SECRET = "test-jwt-secret-codex-edit-6562";
|
||||
process.env.INITIAL_PASSWORD = "admin-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createProviderConnection, getProviderConnectionById } = await import(
|
||||
"../../src/lib/db/providers.ts"
|
||||
);
|
||||
const { createProviderConnection, getProviderConnectionById } =
|
||||
await import("../../src/lib/db/providers.ts");
|
||||
const providerByIdRoute = await import("../../src/app/api/providers/[id]/route.ts");
|
||||
|
||||
function resetDb() {
|
||||
@@ -60,7 +59,11 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function createCodexConnection(priority: number) {
|
||||
async function createCodexConnection(
|
||||
priority: number,
|
||||
authType = "oauth",
|
||||
providerSpecificData: Record<string, unknown> = {}
|
||||
) {
|
||||
// Mirrors createConnectionFromAuthFile()'s real Codex-import shape
|
||||
// (src/lib/oauth/utils/codexAuthImport.ts) — an OAuth connection whose
|
||||
// providerSpecificData already carries a normalized `requestDefaults`
|
||||
@@ -70,7 +73,7 @@ async function createCodexConnection(priority: number) {
|
||||
// Nth bulk-imported Codex account would already carry.
|
||||
return createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
authType,
|
||||
name: "Codex (imported)",
|
||||
email: "user@example.com",
|
||||
priority,
|
||||
@@ -85,6 +88,7 @@ async function createCodexConnection(priority: number) {
|
||||
chatgptUserId: "user-123",
|
||||
importedAt: new Date().toISOString(),
|
||||
requestDefaults: { reasoningEffort: "medium", serviceTier: "fast" },
|
||||
...providerSpecificData,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -155,6 +159,37 @@ test("PUT /api/providers/[id] persists a Codex OAuth edit when priority already
|
||||
assert.deepEqual(persistedPsd.requestDefaults, { reasoningEffort: "high" });
|
||||
});
|
||||
|
||||
test("PUT /api/providers/[id] removes fingerprint mode from Codex API-key connections", async () => {
|
||||
const connection = (await createCodexConnection(5, "apikey", {
|
||||
codexFingerprintMode: "full",
|
||||
codex_fingerprint_mode: "device",
|
||||
})) as Record<string, unknown>;
|
||||
const existingPsd = connection.providerSpecificData as Record<string, unknown>;
|
||||
assert.equal(existingPsd.codexFingerprintMode, "full");
|
||||
assert.equal(existingPsd.codex_fingerprint_mode, "device");
|
||||
|
||||
const payload = buildCodexEditPayload(connection);
|
||||
payload.providerSpecificData.codexFingerprintMode = null;
|
||||
payload.providerSpecificData.codex_fingerprint_mode = null;
|
||||
|
||||
const request = await makeManagementSessionRequest(
|
||||
`http://localhost/api/providers/${connection.id}`,
|
||||
{ method: "PUT", body: payload }
|
||||
);
|
||||
const response = await providerByIdRoute.PUT(request, {
|
||||
params: Promise.resolve({ id: connection.id as string }),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const persisted = (await getProviderConnectionById(connection.id as string)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const persistedPsd = persisted.providerSpecificData as Record<string, unknown>;
|
||||
assert.equal(persistedPsd.codexFingerprintMode, undefined);
|
||||
assert.equal(persistedPsd.codex_fingerprint_mode, undefined);
|
||||
});
|
||||
|
||||
test("PUT /api/providers/[id] still rejects a genuinely invalid priority (control)", async () => {
|
||||
const connection = (await createCodexConnection(5)) as Record<string, unknown>;
|
||||
|
||||
|
||||
373
tests/unit/codex-fingerprint-convergence.test.ts
Normal file
373
tests/unit/codex-fingerprint-convergence.test.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
applyCodexClientIdentityHeaders,
|
||||
applyCodexClientMetadata,
|
||||
applyCodexOriginalIdentityHeaders,
|
||||
createCodexClientIdentity,
|
||||
getCodexClientSessionId,
|
||||
getCodexConvergedSessionId,
|
||||
getCodexConvergedThreadId,
|
||||
getCodexFingerprintMode,
|
||||
getCodexInstallationId,
|
||||
resolveCodexFingerprintIdentity,
|
||||
resolveCodexOriginalIdentityHeaders,
|
||||
withCodexFingerprintCredentials,
|
||||
} from "../../open-sse/config/codexIdentity.ts";
|
||||
|
||||
const oauthCredentials = {
|
||||
accessToken: "oauth-token",
|
||||
connectionId: "connection-42",
|
||||
providerSpecificData: { workspaceId: "workspace-42" },
|
||||
};
|
||||
|
||||
test("Codex fingerprint mode defaults to session and only explicit off disables it", () => {
|
||||
assert.equal(getCodexFingerprintMode(undefined), "session");
|
||||
assert.equal(getCodexFingerprintMode({ codexFingerprintMode: "invalid" }), "session");
|
||||
assert.equal(getCodexFingerprintMode({ codexFingerprintMode: "off" }), "off");
|
||||
assert.equal(
|
||||
resolveCodexFingerprintIdentity({
|
||||
credentials: { ...oauthCredentials, providerSpecificData: { codexFingerprintMode: "off" } },
|
||||
clientHeaders: { "session-id": "client-session" },
|
||||
body: {},
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("Codex off mode preserves original OAuth identity headers", () => {
|
||||
const clientHeaders = {
|
||||
"session-id": "client-session",
|
||||
"thread-id": "client-thread",
|
||||
"x-client-request-id": "client-request",
|
||||
"x-codex-window-id": "client-thread:0",
|
||||
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
|
||||
};
|
||||
const credentials = {
|
||||
...oauthCredentials,
|
||||
providerSpecificData: { codexFingerprintMode: "off" },
|
||||
};
|
||||
const original = resolveCodexOriginalIdentityHeaders({
|
||||
credentials,
|
||||
clientHeaders,
|
||||
});
|
||||
const wrapped = withCodexFingerprintCredentials(credentials, clientHeaders, {});
|
||||
assert.deepEqual(wrapped.providerSpecificData.codexOriginalIdentityHeaders, original);
|
||||
assert.equal(wrapped.providerSpecificData.codexClientIdentity, undefined);
|
||||
|
||||
const resolvedAgain = resolveCodexOriginalIdentityHeaders({
|
||||
credentials,
|
||||
clientHeaders,
|
||||
});
|
||||
assert.ok(resolvedAgain);
|
||||
|
||||
const headers: Record<string, string> = { session_id: "generated-session" };
|
||||
applyCodexOriginalIdentityHeaders(headers, resolvedAgain);
|
||||
assert.equal(headers["session-id"], "client-session");
|
||||
assert.equal(headers["thread-id"], "client-thread");
|
||||
assert.equal(headers["x-client-request-id"], "client-request");
|
||||
assert.equal(headers["x-codex-window-id"], "client-thread:0");
|
||||
assert.equal(headers["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
|
||||
assert.equal(
|
||||
resolveCodexOriginalIdentityHeaders({
|
||||
credentials: {
|
||||
...oauthCredentials,
|
||||
requestEndpointPath: "/responses/compact",
|
||||
providerSpecificData: { codexFingerprintMode: "off" },
|
||||
},
|
||||
clientHeaders: { "session-id": "compact-session" },
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("Codex device/session/full modes preserve their Sub2API convergence boundaries", () => {
|
||||
const providerSpecificData = { workspaceId: "workspace-42" };
|
||||
const device = createCodexClientIdentity("client-session-a", providerSpecificData, {
|
||||
mode: "device",
|
||||
});
|
||||
const sessionA = createCodexClientIdentity("client-session-a", providerSpecificData, {
|
||||
mode: "session",
|
||||
accountKey: "connection-42",
|
||||
});
|
||||
const sessionB = createCodexClientIdentity("client-session-b", providerSpecificData, {
|
||||
mode: "session",
|
||||
accountKey: "connection-42",
|
||||
});
|
||||
const fullA = createCodexClientIdentity("client-session-a", providerSpecificData, {
|
||||
mode: "full",
|
||||
accountKey: "connection-42",
|
||||
});
|
||||
const fullB = createCodexClientIdentity("client-session-b", providerSpecificData, {
|
||||
mode: "full",
|
||||
accountKey: "connection-42",
|
||||
});
|
||||
|
||||
assert.ok(device && sessionA && sessionB && fullA && fullB);
|
||||
assert.equal(device.installationId, getCodexInstallationId(providerSpecificData, undefined));
|
||||
assert.notEqual(
|
||||
getCodexInstallationId({}, "connection-a"),
|
||||
getCodexInstallationId({}, "connection-b")
|
||||
);
|
||||
assert.equal(device.sessionId, "");
|
||||
assert.equal(sessionA.sessionId, sessionB.sessionId);
|
||||
assert.notEqual(sessionA.threadId, sessionB.threadId);
|
||||
assert.equal(sessionA.windowId, `${sessionA.threadId}:0`);
|
||||
assert.equal(fullA.sessionId, fullB.sessionId);
|
||||
assert.equal(fullA.threadId, fullA.sessionId);
|
||||
assert.equal(fullA.threadId, fullB.threadId);
|
||||
assert.notEqual(sessionA.turnId, sessionB.turnId);
|
||||
assert.equal(
|
||||
getCodexConvergedSessionId(providerSpecificData, "connection-42"),
|
||||
sessionA.sessionId
|
||||
);
|
||||
assert.equal(
|
||||
getCodexConvergedThreadId("client-session-a", providerSpecificData, "connection-42"),
|
||||
sessionA.threadId
|
||||
);
|
||||
});
|
||||
|
||||
test("Codex client session extraction prefers hyphenated session-id and rejects unsafe values", () => {
|
||||
assert.equal(
|
||||
getCodexClientSessionId({ "session-id": "hyphen", session_id: "underscore" }),
|
||||
"hyphen"
|
||||
);
|
||||
assert.equal(getCodexClientSessionId({ session_id: "underscore" }), "underscore");
|
||||
assert.equal(getCodexClientSessionId({ "session-id": "bad\\r\\nheader" }), null);
|
||||
});
|
||||
|
||||
test("One Codex identity is shared by headers, body metadata, and nested turn metadata", () => {
|
||||
const identity = resolveCodexFingerprintIdentity({
|
||||
credentials: oauthCredentials,
|
||||
clientHeaders: { "session-id": "client-session" },
|
||||
body: {},
|
||||
});
|
||||
assert.ok(identity);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
applyCodexClientIdentityHeaders(headers, identity);
|
||||
const body: Record<string, unknown> = {
|
||||
client_metadata: { "x-codex-turn-metadata": '{"sandbox":"none"}' },
|
||||
};
|
||||
applyCodexClientMetadata(body, identity);
|
||||
|
||||
const headerMetadata = JSON.parse(headers["x-codex-turn-metadata"]);
|
||||
const clientMetadata = body.client_metadata as Record<string, unknown>;
|
||||
const bodyMetadata = JSON.parse(clientMetadata["x-codex-turn-metadata"] as string);
|
||||
|
||||
assert.equal(headers["session-id"], clientMetadata.session_id);
|
||||
assert.equal(headers["thread-id"], clientMetadata.thread_id);
|
||||
assert.equal(headers["x-codex-window-id"], clientMetadata["x-codex-window-id"]);
|
||||
assert.equal(identity.installationId, headers["x-codex-installation-id"]);
|
||||
assert.equal(identity.turnId, clientMetadata.turn_id);
|
||||
assert.equal(identity.turnId, headerMetadata.turn_id);
|
||||
assert.equal(identity.turnId, bodyMetadata.turn_id);
|
||||
assert.equal(bodyMetadata.sandbox, "none");
|
||||
});
|
||||
|
||||
test("Codex compact requests do not resolve a fingerprint identity", () => {
|
||||
assert.equal(
|
||||
resolveCodexFingerprintIdentity({
|
||||
credentials: {
|
||||
...oauthCredentials,
|
||||
requestEndpointPath: "/responses/compact",
|
||||
},
|
||||
clientHeaders: { "session-id": "client-session" },
|
||||
body: {},
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("Codex HTTP off mode preserves original identity headers and body metadata", async () => {
|
||||
const { CodexExecutor } = await import("../../open-sse/executors/codex.ts");
|
||||
const executor = new CodexExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let upstreamHeaders = new Headers();
|
||||
let upstreamBody: Record<string, unknown> = {};
|
||||
globalThis.fetch = async (_url, init) => {
|
||||
upstreamHeaders = new Headers(init?.headers);
|
||||
upstreamBody = JSON.parse(String(init?.body || "{}"));
|
||||
return new Response(JSON.stringify({ id: "resp-off", object: "response" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await executor.execute({
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
model: "gpt-5.5",
|
||||
input: [{ role: "user", content: "hello" }],
|
||||
client_metadata: {
|
||||
session_id: "client-session",
|
||||
thread_id: "client-thread",
|
||||
turn_id: "client-turn",
|
||||
"x-codex-window-id": "client-thread:0",
|
||||
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
|
||||
},
|
||||
_nativeCodexPassthrough: true,
|
||||
},
|
||||
stream: true,
|
||||
clientHeaders: {
|
||||
"session-id": "client-session",
|
||||
"thread-id": "client-thread",
|
||||
"x-client-request-id": "client-request",
|
||||
"x-codex-window-id": "client-thread:0",
|
||||
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
|
||||
},
|
||||
credentials: {
|
||||
accessToken: "codex-token",
|
||||
connectionId: "conn-http-off",
|
||||
providerSpecificData: { workspaceId: "http-off", codexFingerprintMode: "off" },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
const metadata = upstreamBody.client_metadata as Record<string, unknown>;
|
||||
assert.equal(upstreamHeaders.get("session-id"), "client-session");
|
||||
assert.equal(upstreamHeaders.get("thread-id"), "client-thread");
|
||||
assert.equal(upstreamHeaders.get("x-client-request-id"), "client-request");
|
||||
assert.equal(upstreamHeaders.get("x-codex-window-id"), "client-thread:0");
|
||||
assert.equal(upstreamHeaders.get("x-codex-turn-metadata"), '{"turn_id":"client-turn"}');
|
||||
assert.equal(metadata.session_id, "client-session");
|
||||
assert.equal(metadata.thread_id, "client-thread");
|
||||
assert.equal(metadata.turn_id, "client-turn");
|
||||
assert.equal(metadata["x-codex-window-id"], "client-thread:0");
|
||||
assert.equal(metadata["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
|
||||
});
|
||||
|
||||
test("Codex websocket off mode preserves original identity headers and body metadata", async () => {
|
||||
const { CodexExecutor, __setCodexWebSocketTransportForTesting } =
|
||||
await import("../../open-sse/executors/codex.ts");
|
||||
const executor = new CodexExecutor();
|
||||
let sent: string | null = null;
|
||||
let wsHeaders: Record<string, string> = {};
|
||||
__setCodexWebSocketTransportForTesting(async (_url, opts) => {
|
||||
wsHeaders = (opts?.headers as Record<string, string>) || {};
|
||||
return {
|
||||
send(data: string) {
|
||||
sent = data;
|
||||
queueMicrotask(() => {
|
||||
this.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
type: "response.completed",
|
||||
response: { status: "completed" },
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
close() {},
|
||||
onmessage: null,
|
||||
onerror: null,
|
||||
onclose: null,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
model: "gpt-5.5",
|
||||
input: [{ role: "user", content: "hello" }],
|
||||
client_metadata: {
|
||||
session_id: "client-session",
|
||||
thread_id: "client-thread",
|
||||
turn_id: "client-turn",
|
||||
},
|
||||
},
|
||||
stream: true,
|
||||
clientHeaders: {
|
||||
"session-id": "client-session",
|
||||
"thread-id": "client-thread",
|
||||
"x-client-request-id": "client-request",
|
||||
"x-codex-window-id": "client-thread:0",
|
||||
"x-codex-turn-metadata": '{"turn_id":"client-turn"}',
|
||||
},
|
||||
credentials: {
|
||||
accessToken: "codex-token",
|
||||
connectionId: "conn-ws-off",
|
||||
providerSpecificData: {
|
||||
workspaceId: "ws-off",
|
||||
codexTransport: "websocket",
|
||||
codexFingerprintMode: "off",
|
||||
},
|
||||
},
|
||||
});
|
||||
await result.response.text();
|
||||
} finally {
|
||||
__setCodexWebSocketTransportForTesting(undefined);
|
||||
}
|
||||
|
||||
assert.ok(sent);
|
||||
const payload = JSON.parse(sent as string) as Record<string, unknown>;
|
||||
const metadata = payload.client_metadata as Record<string, unknown>;
|
||||
assert.equal(wsHeaders["session-id"], "client-session");
|
||||
assert.equal(wsHeaders["thread-id"], "client-thread");
|
||||
assert.equal(wsHeaders["x-client-request-id"], "client-request");
|
||||
assert.equal(wsHeaders["x-codex-window-id"], "client-thread:0");
|
||||
assert.equal(wsHeaders["x-codex-turn-metadata"], '{"turn_id":"client-turn"}');
|
||||
assert.equal(metadata.session_id, "client-session");
|
||||
assert.equal(metadata.thread_id, "client-thread");
|
||||
assert.equal(metadata.turn_id, "client-turn");
|
||||
});
|
||||
|
||||
test("Codex websocket headers and payload share one fingerprint identity", async () => {
|
||||
const { CodexExecutor, __setCodexWebSocketTransportForTesting } =
|
||||
await import("../../open-sse/executors/codex.ts");
|
||||
const executor = new CodexExecutor();
|
||||
let sent: string | null = null;
|
||||
let wsHeaders: Record<string, string> = {};
|
||||
__setCodexWebSocketTransportForTesting(async (_url, opts) => {
|
||||
wsHeaders = (opts?.headers as Record<string, string>) || {};
|
||||
return {
|
||||
send(data: string) {
|
||||
sent = data;
|
||||
queueMicrotask(() => {
|
||||
this.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
type: "response.completed",
|
||||
response: { status: "completed" },
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
close() {},
|
||||
onmessage: null,
|
||||
onerror: null,
|
||||
onclose: null,
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gpt-5.5",
|
||||
body: {
|
||||
model: "gpt-5.5",
|
||||
session_id: "client-ws",
|
||||
input: [{ role: "user", content: "hello" }],
|
||||
},
|
||||
stream: true,
|
||||
credentials: {
|
||||
accessToken: "codex-token",
|
||||
connectionId: "conn-ws",
|
||||
providerSpecificData: { workspaceId: "ws-ws", codexTransport: "websocket" },
|
||||
},
|
||||
});
|
||||
await result.response.text();
|
||||
} finally {
|
||||
__setCodexWebSocketTransportForTesting(undefined);
|
||||
}
|
||||
|
||||
assert.ok(sent);
|
||||
const payload = JSON.parse(sent as string) as Record<string, unknown>;
|
||||
const metadata = (payload.client_metadata as Record<string, unknown>) || {};
|
||||
assert.equal(wsHeaders.session_id, metadata.session_id);
|
||||
assert.equal(wsHeaders["x-client-request-id"], metadata.thread_id);
|
||||
assert.equal(wsHeaders["x-codex-window-id"], metadata["x-codex-window-id"]);
|
||||
assert.equal(payload.type, "response.create");
|
||||
});
|
||||
59
tests/unit/codex-responses-ws-fingerprint.test.ts
Normal file
59
tests/unit/codex-responses-ws-fingerprint.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-ws-fingerprint-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.APP_LOG_TO_FILE = "false";
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET = "bridge-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createProviderConnection } = await import("../../src/lib/db/providers.ts");
|
||||
const { POST } = await import("../../src/app/api/internal/codex-responses-ws/route.ts");
|
||||
|
||||
function resetDb() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(resetDb);
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("Codex internal websocket bridge prepare preserves original OAuth identity in off mode", async () => {
|
||||
await createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
name: "Codex WS off",
|
||||
accessToken: "oauth-token",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: { codexFingerprintMode: "off" },
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
new Request("http://omniroute.local/api/internal/codex-responses-ws", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-omniroute-ws-bridge-secret": "bridge-secret",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "prepare",
|
||||
requestUrl: "http://omniroute.local/v1/responses",
|
||||
headers: { "session-id": "client-session", "thread-id": "client-thread" },
|
||||
response: { model: "codex/gpt-5.5", input: "hello" },
|
||||
}),
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200, JSON.stringify(body));
|
||||
assert.equal(body.headers["session-id"], "client-session");
|
||||
assert.equal(body.headers["thread-id"], "client-thread");
|
||||
});
|
||||
@@ -871,34 +871,6 @@ test("CodexExecutor.transformRequest passes GPT 5.6 Luna xhigh reasoning through
|
||||
assert.equal(sanitized.reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest merges Codex installation metadata", () => {
|
||||
const executor = new CodexExecutor();
|
||||
const result = executor.transformRequest(
|
||||
"gpt-5.5",
|
||||
{
|
||||
model: "gpt-5.5",
|
||||
input: [],
|
||||
client_metadata: { existing: "keep" },
|
||||
},
|
||||
true,
|
||||
{
|
||||
providerSpecificData: {
|
||||
codexClientIdentity: {
|
||||
sessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
windowId: "session-1:0",
|
||||
installationId: "11111111-1111-4111-a111-111111111111",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(result.client_metadata, {
|
||||
existing: "keep",
|
||||
"x-codex-installation-id": "11111111-1111-4111-a111-111111111111",
|
||||
});
|
||||
});
|
||||
|
||||
test("CodexExecutor.transformRequest omits client metadata for compact requests", () => {
|
||||
const executor = new CodexExecutor();
|
||||
const result = executor.transformRequest(
|
||||
@@ -1043,20 +1015,25 @@ test("CodexExecutor.execute adds CLI-like session identity headers without chang
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.response.status, 200);
|
||||
assert.equal(capturedHeaders?.get("session_id"), "conversation-1");
|
||||
assert.equal(capturedHeaders?.get("x-client-request-id"), "conversation-1");
|
||||
assert.equal(capturedHeaders?.get("x-codex-window-id"), "conversation-1:0");
|
||||
const meta = (capturedBody?.client_metadata as Record<string, unknown>) || {};
|
||||
const turnMetadata = JSON.parse(capturedHeaders?.get("x-codex-turn-metadata") || "{}");
|
||||
assert.equal(turnMetadata.session_id, "conversation-1");
|
||||
assert.equal(result.response.status, 200);
|
||||
assert.notEqual(capturedHeaders?.get("session_id"), "conversation-1");
|
||||
assert.equal(capturedHeaders?.get("session-id"), capturedHeaders?.get("session_id"));
|
||||
assert.equal(capturedHeaders?.get("thread-id"), capturedHeaders?.get("x-client-request-id"));
|
||||
assert.equal(
|
||||
capturedHeaders?.get("x-codex-window-id"),
|
||||
`${capturedHeaders?.get("x-client-request-id")}:0`
|
||||
);
|
||||
assert.equal(turnMetadata.session_id, capturedHeaders?.get("session_id"));
|
||||
assert.equal(turnMetadata.thread_id, capturedHeaders?.get("thread-id"));
|
||||
assert.equal(turnMetadata.turn_id, meta.turn_id);
|
||||
assert.equal(turnMetadata.window_id, capturedHeaders?.get("x-codex-window-id"));
|
||||
assert.equal(turnMetadata.thread_source, "user");
|
||||
assert.equal(turnMetadata.sandbox, "none");
|
||||
assert.equal(typeof turnMetadata.turn_id, "string");
|
||||
assert.equal(capturedBody?.prompt_cache_key, "conversation-1");
|
||||
assert.equal(
|
||||
(capturedBody?.client_metadata as Record<string, unknown>)?.["x-codex-installation-id"],
|
||||
"7f06a8ee-2981-4c81-a4ca-e443b5400a63"
|
||||
);
|
||||
assert.equal(meta["x-codex-installation-id"], "7f06a8ee-2981-4c81-a4ca-e443b5400a63");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -1087,9 +1064,11 @@ test("CodexExecutor.execute skips identity headers for unsafe session ids", asyn
|
||||
credentials: { accessToken: "codex-token" },
|
||||
});
|
||||
|
||||
assert.equal(capturedHeaders?.get("x-client-request-id"), null);
|
||||
assert.equal(capturedHeaders?.get("x-codex-window-id"), null);
|
||||
assert.equal(capturedHeaders?.get("x-codex-turn-metadata"), null);
|
||||
assert.notEqual(capturedHeaders?.get("session_id"), "bad\r\nheader");
|
||||
assert.ok(capturedHeaders?.get("session_id"));
|
||||
assert.ok(capturedHeaders?.get("x-client-request-id"));
|
||||
assert.ok(capturedHeaders?.get("x-codex-window-id"));
|
||||
assert.ok(capturedHeaders?.get("x-codex-turn-metadata"));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,27 @@ test("provider schemas accept max but reject ultra as a server-side Codex defaul
|
||||
assert.equal(ultra.success, false);
|
||||
});
|
||||
|
||||
test("provider schemas accept Codex fingerprint modes and reject unknown values", () => {
|
||||
for (const mode of ["off", "device", "session", "full"]) {
|
||||
const created = createProviderSchema.safeParse({
|
||||
provider: "codex",
|
||||
apiKey: "token",
|
||||
name: "Codex",
|
||||
providerSpecificData: { codexFingerprintMode: mode },
|
||||
});
|
||||
const updated = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { codexFingerprintMode: mode },
|
||||
});
|
||||
assert.equal(created.success, true, mode);
|
||||
assert.equal(updated.success, true, mode);
|
||||
}
|
||||
|
||||
const rejected = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { codexFingerprintMode: "aggressive" },
|
||||
});
|
||||
assert.equal(rejected.success, false);
|
||||
});
|
||||
|
||||
test("provider schemas reject unknown Codex service tiers", () => {
|
||||
const created = createProviderSchema.safeParse({
|
||||
provider: "codex",
|
||||
|
||||
Reference in New Issue
Block a user