diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 64c5e75c8e..bbdc9d16f0 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -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).", diff --git a/open-sse/config/codexIdentity.ts b/open-sse/config/codexIdentity.ts index 7a52f5a0f1..bc45fa1cf6 100644 --- a/open-sse/config/codexIdentity.ts +++ b/open-sse/config/codexIdentity.ts @@ -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 | 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 | 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 | 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 | null + providerSpecificData?: Record | 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 | null, + accountKey?: string | null +): string { + return deriveStableUUIDv4( + `${CODEX_SESSION_SEED_PREFIX}${accountSeed(providerSpecificData, accountKey)}` + ); +} + +export function getCodexConvergedThreadId( + clientSessionId: string | null, + providerSpecificData?: Record | 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 | 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 | null + clientSessionId: string | null, + providerSpecificData?: Record | 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 | null; +}; + +export function resolveCodexOriginalIdentityHeaders(input: { + credentials?: CodexCredentialIdentityInput | null; + clientHeaders?: Headers | Record | null; +}): Record | 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 = {}; + 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 | 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( + credentials: T, + clientHeaders?: Headers | Record | 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 = {}; + 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; + 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, + original?: Record | 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, 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, + 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) } + : {}; + 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 | null | undefined @@ -132,20 +461,3 @@ export function isVerifiedNativeCodexRequest( ): boolean { return isCodexOriginatedHeaders(headers) && hasNativeCodexTurnBinding(body); } - -export function applyCodexClientMetadata( - body: Record, - 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) - : {}; - body.client_metadata = { - ...existing, - "x-codex-installation-id": identity.installationId, - }; -} diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index c2e1f6b199..93e5a2fe16 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -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 | 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 | 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; diff --git a/scripts/dev/responses-ws-proxy.mjs b/scripts/dev/responses-ws-proxy.mjs index 7e2a5fbc15..9255d268ff 100644 --- a/scripts/dev/responses-ws-proxy.mjs +++ b/scripts/dev/responses-ws-proxy.mjs @@ -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; } diff --git a/scripts/dev/v1-ws-bridge.mjs b/scripts/dev/v1-ws-bridge.mjs index 3653bd159f..05d9e31010 100644 --- a/scripts/dev/v1-ws-bridge.mjs +++ b/scripts/dev/v1-ws-bridge.mjs @@ -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; } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/CodexFingerprintFields.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/CodexFingerprintFields.tsx new file mode 100644 index 0000000000..9d13462cfb --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/CodexFingerprintFields.tsx @@ -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[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 ( +
+ ({ + 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 && ( + setFormData({ ...formData, codexReasoningEffort: e.target.value })} - hint={t("defaultThinkingStrengthHint")} - /> -