From 9cb5339e6c368bc2e758c4c9096ed804cf557e20 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:49:28 +0900 Subject: [PATCH 01/21] fix(responses): retry exact Console upload rejections once Co-authored-by: Eran Co-authored-by: yxr1995-maker <257504378+yxr1995-maker@users.noreply.github.com> --- .../content/docs/reference/proxy-formats.md | 8 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/Logs.tsx | 4 +- scripts/test-layout/layout.json | 1 + src/providers/opencode-zen-rate-limit.ts | 57 +++++ src/server/responses/core.ts | 106 ++++++++- src/usage/log.ts | 2 + structure/transports/responses.md | 4 + tests/fixtures/test-layout-expected.json | 1 + .../providers/opencode-zen-rate-limit.test.ts | 72 ++++++ .../responses-console-go-upload-retry.test.ts | 210 ++++++++++++++++++ 19 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 tests/responses/responses-console-go-upload-retry.test.ts diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index ed856803ad..a91d05e0d2 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -24,6 +24,14 @@ should select among several targets. Credential-bearing model, image, video, and search requests do not automatically follow HTTP redirects, including same-origin redirects. Configure the final upstream API URL instead of a redirecting alias. A redirect does not cause the server to resend credentials or the request body to its destination. The response owner retains its existing error or relay behavior; native Responses and compact routes can return the original 3xx and `Location` to the client. Client redirect behavior is separate from this server transport policy. +## Console upload rejections + +An exact Console or Console Go `Invalid upload request.` HTTP 400 from a canonical +OpenCode Zen/Go generation endpoint receives one retry after 800 ms. The proxy reuses +the same serialized request and records the recovery in Logs. Other 400 errors, +custom destinations, cancellations and repeated upload rejections remain failures. +This does not retry filtered model responses or interrupted streams. + ## Endpoint overview | Client surface | Endpoint | Successful non-stream result | Successful stream or socket result | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index fdb18eb6ed..d92f72d2d3 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -796,6 +796,7 @@ export const de: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth ratenbegrenzt (429)", "logs.detail.attempt.recovery.image413": "Bildnutzlast zu groß (413)", "logs.detail.attempt.recovery.emptyCompletion": "Wiederholung nach leerer Antwort", + "logs.detail.attempt.recovery.consoleGoUpload": "Console-Upload erneut versucht", "logs.detail.attempt.recovery.unknown": "Unbekannter Wiederherstellungsgrund", "logs.detail.reason.usage_missing": "Nutzung wurde nicht gemeldet.", "logs.detail.reason.usage_unsupported": "Dieser Anbieter meldet keine Nutzung.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 1847a7af7e..6a9b0eac21 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -845,6 +845,7 @@ export const en = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth rate-limited (429)", "logs.detail.attempt.recovery.image413": "Image payload too large (413)", "logs.detail.attempt.recovery.emptyCompletion": "Empty completion retry", + "logs.detail.attempt.recovery.consoleGoUpload": "Console upload retry", "logs.detail.attempt.recovery.unknown": "Unknown recovery reason", "logs.detail.reason.usage_missing": "Usage was not reported.", "logs.detail.reason.usage_unsupported": "This provider does not report usage.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index e465adbb10..f2a2e6ed71 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -821,6 +821,7 @@ export const fr: Record = { "logs.detail.attempt.recovery.transient5xx": "Erreur 5xx temporaire", "logs.detail.attempt.recovery.connectionReset": "Réinitialisation de la connexion", "logs.detail.attempt.recovery.emptyCompletion": "Nouvelle tentative après une réponse vide", + "logs.detail.attempt.recovery.consoleGoUpload": "Nouvelle tentative d’envoi Console", "logs.detail.attempt.recovery.oauth401": "Réauthentification OAuth", "logs.detail.attempt.recovery.key429": "Clé soumise à une limitation de débit (429)", "logs.detail.attempt.recovery.rateLimit429": "Limitation de débit (429)", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index e9a3d9f58b..14e82062c6 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -758,6 +758,7 @@ export const ja: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth レート制限 (429)", "logs.detail.attempt.recovery.image413": "画像ペイロードが大きすぎます (413)", "logs.detail.attempt.recovery.emptyCompletion": "空の完了を再試行", + "logs.detail.attempt.recovery.consoleGoUpload": "Console アップロード再試行", "logs.detail.attempt.recovery.unknown": "不明なリカバリ理由", "logs.detail.reason.usage_missing": "使用量が報告されませんでした。", "logs.detail.reason.usage_unsupported": "このプロバイダーは使用量を報告しません。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 67ee251970..070ce4881f 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -827,6 +827,7 @@ export const ko: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth 요청 한도 초과 (429)", "logs.detail.attempt.recovery.image413": "이미지 페이로드가 너무 큼 (413)", "logs.detail.attempt.recovery.emptyCompletion": "빈 응답 재시도", + "logs.detail.attempt.recovery.consoleGoUpload": "Console 업로드 재시도", "logs.detail.attempt.recovery.unknown": "알 수 없는 복구 사유", "logs.detail.reason.usage_missing": "usage가 보고되지 않았습니다.", "logs.detail.reason.usage_unsupported": "이 프로바이더는 usage 보고를 지원하지 않습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 56d43fc301..7a7f319edd 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -813,6 +813,7 @@ export const ru: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth ограничен (429)", "logs.detail.attempt.recovery.image413": "Слишком большой размер изображения (413)", "logs.detail.attempt.recovery.emptyCompletion": "Повтор пустого завершения", + "logs.detail.attempt.recovery.consoleGoUpload": "Повтор загрузки Console", "logs.detail.attempt.recovery.unknown": "Неизвестная причина восстановления", "logs.detail.reason.usage_missing": "Данные об использовании не были сообщены.", "logs.detail.reason.usage_unsupported": "Этот провайдер не сообщает данные об использовании.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index b627813bb9..fc976f6d29 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -832,6 +832,7 @@ export const tr: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth kısıtlandı (429)", "logs.detail.attempt.recovery.image413": "Görsel boyutu çok büyük (413)", "logs.detail.attempt.recovery.emptyCompletion": "Boş tamamlama yeniden denemesi", + "logs.detail.attempt.recovery.consoleGoUpload": "Console yüklemesi yeniden denendi", "logs.detail.attempt.recovery.unknown": "Bilinmeyen kurtarma nedeni", "logs.detail.reason.usage_missing": "Kullanım bildirilmedi.", "logs.detail.reason.usage_unsupported": "Bu sağlayıcı kullanım bildirmeyebilir.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index ce06556fd4..dae33c6635 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2210,6 +2210,7 @@ export const zhTW: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth 被限流 (429)", "logs.detail.attempt.recovery.image413": "圖片承載過大 (413)", "logs.detail.attempt.recovery.emptyCompletion": "空白完成重試", + "logs.detail.attempt.recovery.consoleGoUpload": "Console 上傳重試", "logs.detail.attempt.recovery.unknown": "未知的復原原因", "logs.detail.estimate.provider_cost_overlay": "已使用供應商設定的價格覆蓋。", "logs.detail.estimate.priority_lower_bound": "無法取得已確認的 Priority 價格;目前顯示的估算是已知下限。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index ae2fdfec92..46ee7b4fde 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -808,6 +808,7 @@ export const zh: Record = { "logs.detail.attempt.recovery.anthropicOauth429": "Anthropic OAuth 被限流 (429)", "logs.detail.attempt.recovery.image413": "图片载荷过大 (413)", "logs.detail.attempt.recovery.emptyCompletion": "空完成重试", + "logs.detail.attempt.recovery.consoleGoUpload": "Console 上传重试", "logs.detail.attempt.recovery.unknown": "未知的恢复原因", "logs.detail.reason.usage_missing": "未上报 usage。", "logs.detail.reason.usage_unsupported": "该提供方不支持上报 usage。", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index 774efc455a..d7fc3ab5c4 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -113,7 +113,8 @@ type AttemptRecoveryKind = | "rate-limit-429" | "anthropic-oauth-429" | "image-413" - | "empty-completion"; + | "empty-completion" + | "console-go-upload-retry"; interface LogAttempt { ordinal: number; @@ -307,6 +308,7 @@ const RECOVERY_KIND_KEYS = { "anthropic-oauth-429": "logs.detail.attempt.recovery.anthropicOauth429", "image-413": "logs.detail.attempt.recovery.image413", "empty-completion": "logs.detail.attempt.recovery.emptyCompletion", + "console-go-upload-retry": "logs.detail.attempt.recovery.consoleGoUpload", } as const satisfies Record; /** Map a metric-unavailable reason to its i18n key. */ diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ecbbf5862a..c3cb051eb8 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1075,6 +1075,7 @@ "responses-account-label.test.ts": "responses", "responses-compaction-routing.test.ts": "responses", "responses-compaction.test.ts": "responses", + "responses-console-go-upload-retry.test.ts": "responses", "responses-context-overflow.test.ts": "responses", "responses-custom-tool-guidance.test.ts": "responses", "responses-custom-tool-repair.test.ts": "responses", diff --git a/src/providers/opencode-zen-rate-limit.ts b/src/providers/opencode-zen-rate-limit.ts index c4dbb10319..21c248275c 100644 --- a/src/providers/opencode-zen-rate-limit.ts +++ b/src/providers/opencode-zen-rate-limit.ts @@ -175,3 +175,60 @@ export function enrichOpenCodeZenUpstreamMessage( ): string { return enrichOpenCodeZenFreeTierMessage(enrichOpenCodeZenRateLimitMessage(message, opts), opts); } + +/** The effective HTTP endpoint, never the configured row name, identifies Console. */ +export function isConsoleGoDestination(outboundUrl: string | undefined): boolean { + if (!outboundUrl) return false; + try { + const url = new URL(outboundUrl); + return url.protocol === "https:" && url.hostname === "opencode.ai" + && url.port === "" && url.username === "" && url.password === "" + && /^\/zen\/(?:go\/)?v1\/(?:responses|chat\/completions|messages)$/.test(url.pathname); + } catch { + return false; + } +} + +/** + * The canonical refusal envelope, as served on both Console routes: + * {"error":{"param":null,"type":"invalid_request_error","message":"Error from provider + * (Console Go): Upstream request failed: [invalid_request_error] Invalid upload request."}} + * + * The gateway names itself Console on the Zen key route and Console Go on the Go route, so the + * anchor is the shared product name plus the refusal sentence. The message is matched whole: a + * bare string, a suffix, or any other envelope is a different refusal and must not be replayed. + * Being stricter than necessary is the safe direction: a missed match leaves the turn failing + * exactly as it does today, while a loose match spends an extra request on unrelated 400s. + */ +const CONSOLE_UPLOAD_REFUSALS = new Set([ + "Error from provider (Console Go): Upstream request failed: [invalid_request_error] Invalid upload request.", + "Error from provider (Console): Upstream request failed: [invalid_request_error] Invalid upload request.", +]); + +/** + * True only for the canonical Console upload refusal on a canonical Console destination. + * Route-gated on purpose: the message alone would let any other upstream that happens to answer + * with this English sentence trigger a second send from an unrelated provider. + */ +export function isTransientConsoleGoUploadRejection(opts: { + status: number; + errorBody: string | undefined; + outboundUrl?: string; +}): boolean { + if (opts.status !== 400 || !opts.errorBody) return false; + if (!isConsoleGoDestination(opts.outboundUrl)) return false; + let payload: unknown; + try { + payload = JSON.parse(opts.errorBody); + } catch { + return false; + } + const error = (payload as { error?: unknown } | null)?.error; + if (!error || typeof error !== "object" || Array.isArray(error)) return false; + // The whole envelope, not just the sentence: Console always answers this refusal as + // invalid_request_error with a null param, so a partial envelope is a different error. + const envelope = error as { param?: unknown; type?: unknown; message?: unknown }; + if (envelope.type !== "invalid_request_error" || envelope.param !== null) return false; + // Matched without trimming: padding means the gateway wrapped or appended something. + return typeof envelope.message === "string" && CONSOLE_UPLOAD_REFUSALS.has(envelope.message); +} diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 1f78570eae..8fc4e8066d 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -102,7 +102,10 @@ import { } from "../../lib/errors"; import { injectionDebugLog } from "../../lib/injection-debug-log"; import { resolveClientRetryAfter } from "../../lib/retry-after"; -import { enrichOpenCodeZenUpstreamMessage } from "../../providers/opencode-zen-rate-limit"; +import { + enrichOpenCodeZenUpstreamMessage, + isTransientConsoleGoUploadRejection, +} from "../../providers/opencode-zen-rate-limit"; import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types"; import type { AdapterEvent, @@ -213,6 +216,7 @@ import { fetchWithTransientRetry, isNonReplayableResponse, prepareSameTarget429Wait, + sleepWithAbort, } from "../../lib/upstream-retry"; import { ForwardAdmissionCredentialError, @@ -848,6 +852,31 @@ async function opaqueBlobRejectionBodyForRecovery( } } +/** + * Backoff for the single exact-request replay after a canonical Console upload rejection. + */ +const CONSOLE_GO_UPLOAD_RETRY_DELAY_MS = 800; + +/** + * Peek the upstream error body for the Console Go transient-400 recovery. Only a complete, + * display-safe body may drive a retry decision (same contract as + * opaqueBlobRejectionBodyForRecovery), and reading a clone leaves the original response intact + * for the caller's own error surface when no retry is taken. + */ +async function consoleGoUploadRejectionBody( + response: Response, + alreadyAttempted: boolean, + signal: AbortSignal, +): Promise { + if (isNonReplayableResponse(response) || response.status !== 400 || alreadyAttempted) return undefined; + try { + const body = await readBoundedResponseBody(response.clone(), { signal }); + return body.displaySafe && !body.truncated ? body.text : undefined; + } catch { + return undefined; + } +} + /** * Materialize an upstream error body only when the bounded reader observed a complete, * display-safe payload. Partial timeout and over-limit prefixes are attacker-controlled, @@ -5231,6 +5260,9 @@ async function handleResponsesInner( const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false }; let oauth401ReplayAttempted = false; let codex401ReplayKind: "main" | "stored" | null = null; + // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts + // moments later; at most one byte-identical replay is allowed per request. + const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false }; const rateLimitPolicy = rateLimitRetryPolicyFor(route.provider); let rateLimitRetries = 0; const rebuildAndRefetch = async ( @@ -5245,10 +5277,12 @@ async function handleResponsesInner( return { failed: formatErrorResponse(502, "upstream_error", "Recovery changed the provider wire unexpectedly") }; } try { - request = await retryAdapter.buildRequest(parsed, { - headers: selectedForwardHeaders, - translatorBudget, - }); + if (recovery !== "console-go-upload-retry") { + request = await retryAdapter.buildRequest(parsed, { + headers: selectedForwardHeaders, + translatorBudget, + }); + } refreshRoutedNamespaceToolAliases(request); recordAdapterReasoning(logCtx, request); recordAdapterTier(logCtx, request); @@ -5778,6 +5812,36 @@ async function handleResponsesInner( logCtx.terminalIncompleteReason = preflightLog.terminalIncompleteReason; } } + // Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds + // later with 400 invalid_request_error / "Invalid upload request." Replay the byte-identical + // request once after the exact gateway rejection. Single-shot guard. + // This recovery reuses the captured request; other recovery kinds still rebuild. + if (!consoleGoUploadRetryGuard.attempted) { + const uploadRejectionBody = await consoleGoUploadRejectionBody( + upstreamResponse, + consoleGoUploadRetryGuard.attempted, + upstream.signal, + ); + if (uploadRejectionBody !== undefined + && isTransientConsoleGoUploadRejection({ + status: upstreamResponse.status, + errorBody: uploadRejectionBody, + outboundUrl: request.url, + })) { + consoleGoUploadRetryGuard.attempted = true; + try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ } + if (!upstream.signal.aborted) { + try { + await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal); + } catch { return clientCancelledResponse(); } + } + if (upstream.signal.aborted) return clientCancelledResponse(); + const result = await rebuildAndRefetch("console-go-upload-retry"); + if ("failed" in result) return result.failed; + upstreamResponse = result; + continue passthroughRecovery; + } + } break; } const headers = sanitizePassthroughHeaders(upstreamResponse.headers); @@ -7267,6 +7331,9 @@ async function handleResponsesInner( // 413→429 rotation cannot silently undo the tightening. let imageRetryAttempted = false; const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false }; + // Console Go answers a transient 400 "Invalid upload request." for bodies it accepts + // moments later; at most one byte-identical replay is allowed per request. + const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false }; let oauth401ReplayAttempted = false; /** * Rebuild the request from the current parsed input (and any image-tier bias) and refetch @@ -7651,6 +7718,35 @@ async function handleResponsesInner( upstreamResponse = result; continue recovery; } + // Console Go (opencode-zen / opencode-go) intermittently rejects a body it accepts seconds + // later with 400 invalid_request_error / "Invalid upload request." Replay the + // byte-identical request once after the exact gateway rejection. + if (!consoleGoUploadRetryGuard.attempted) { + const uploadRejectionBody = await consoleGoUploadRejectionBody( + upstreamResponse, + consoleGoUploadRetryGuard.attempted, + upstream.signal, + ); + if (uploadRejectionBody !== undefined + && isTransientConsoleGoUploadRejection({ + status: upstreamResponse.status, + errorBody: uploadRejectionBody, + outboundUrl: sameTargetRequest?.url, + })) { + consoleGoUploadRetryGuard.attempted = true; + try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ } + if (!upstream.signal.aborted) { + try { + await sleepWithAbort(CONSOLE_GO_UPLOAD_RETRY_DELAY_MS, upstream.signal); + } catch { cleanupUpstreamAbort(); return clientCancelledResponse(); } + } + if (upstream.signal.aborted) { cleanupUpstreamAbort(); return clientCancelledResponse(); } + const result = await rebuildAndRefetch("console-go-upload-retry"); + if ("failed" in result) return result.failed; + upstreamResponse = result; + continue recovery; + } + } break; } if (!upstreamResponse.ok) { diff --git a/src/usage/log.ts b/src/usage/log.ts index 2944c22f9a..953195b7ca 100644 --- a/src/usage/log.ts +++ b/src/usage/log.ts @@ -70,6 +70,7 @@ export type AttemptRecoveryKind = | "anthropic-oauth-429" | "oauth-account-429" | "image-413" + | "console-go-upload-retry" | "opaque-blob-rejection" | "empty-completion"; @@ -309,6 +310,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set([ "anthropic-oauth-429", "oauth-account-429", "image-413", + "console-go-upload-retry", "opaque-blob-rejection", "empty-completion", ]); diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 2d7bd85db6..8a989a27fb 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -471,6 +471,10 @@ retried. Guarded paths: the ChatGPT passthrough and generic adapter fetch in fallback. Adapters with their own `fetchResponse` (kiro, cursor, google) keep their own retry policies; kiro imports the shared abort/sleep helpers from this module. +## Console upload rejection recovery + +`src/providers/opencode-zen-rate-limit.ts` recognizes the complete Console upload-rejection envelope only at the effective HTTPS opencode.ai Zen/Go generation endpoint. A provider row name cannot authorize another destination. The two recovery loops in `src/server/responses/core.ts` wait 800 ms and replay the captured serialized request once; cancellation, nonreplayable responses, other errors and a second upload rejection keep their failure semantics. The recovery kind is persisted as `console-go-upload-retry` and has a localized Logs label. + ## Same-provider combo quota fallback For a failover combo with multiple models on the same Codex-login OpenAI provider, a pre-stream diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index b92e85757c..1307913a4c 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -910,6 +910,7 @@ "responses-account-label.test.ts": "responses", "responses-compaction-routing.test.ts": "responses", "responses-compaction.test.ts": "responses", + "responses-console-go-upload-retry.test.ts": "responses", "responses-context-overflow.test.ts": "responses", "responses-custom-tool-guidance.test.ts": "responses", "responses-custom-tool-repair.test.ts": "responses", diff --git a/tests/providers/opencode-zen-rate-limit.test.ts b/tests/providers/opencode-zen-rate-limit.test.ts index 8751082c2a..20a59bd61a 100644 --- a/tests/providers/opencode-zen-rate-limit.test.ts +++ b/tests/providers/opencode-zen-rate-limit.test.ts @@ -6,6 +6,7 @@ import { enrichOpenCodeZenFreeTierMessage, enrichOpenCodeZenRateLimitMessage, enrichOpenCodeZenUpstreamMessage, + isTransientConsoleGoUploadRejection, isOpenCodeZenFreeTierLockIn, isOpenCodeZenRateLimitProvider, } from "../../src/providers/opencode-zen-rate-limit"; @@ -230,3 +231,74 @@ describe("opencode-free keyless tier lock-in (#4121)", () => { expect(lockedOut).not.toContain(OPENCODE_ZEN_OBSERVED_RPM_HINT); }); }); + +describe("Console Go transient upload refusal", () => { + // The observed Go-route refusal, byte-for-byte as Console serves it. + const GO_MESSAGE = "Error from provider (Console Go): Upstream request failed: [invalid_request_error] Invalid upload request."; + // The Zen key route names the same gateway without the Go suffix. + const ZEN_MESSAGE = "Error from provider (Console): Upstream request failed: [invalid_request_error] Invalid upload request."; + const envelope = (message: string) => JSON.stringify({ model: "muse-spark-1.3-contributor", error: { param: null, type: "invalid_request_error", message } }); + const GO_ROUTE = { outboundUrl: "https://opencode.ai/zen/go/v1/responses" }; + const ZEN_ROUTE = { outboundUrl: "https://opencode.ai/zen/v1/responses" }; + + test("accepts the canonical refusal on both canonical Console routes", () => { + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), ...GO_ROUTE })).toBe(true); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(ZEN_MESSAGE), ...ZEN_ROUTE })).toBe(true); + // A custom row pointed at the same destination is still Console: the base URL decides. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl: "https://opencode.ai/zen/go/v1/responses" })).toBe(true); + }); + + test("rejects the refusal text from a non-Console route", () => { + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl: "https://api.deepseek.com/v1/responses" })).toBe(false); + // opencode.ai without the /zen segment is not the Console gateway. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl: "https://opencode.ai/v1/responses" })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE) })).toBe(false); + }); + + test("requires the effective canonical HTTPS endpoint", () => { + for (const outboundUrl of [ + "http://opencode.ai/zen/go/v1/responses", + "https://opencode.ai:8443/zen/go/v1/responses", + "https://opencode.ai.evil.test/zen/go/v1/responses", + "https://opencode.ai/zen-other/v1/responses", + "https://opencode.ai/zen/go/v1/models", + "https://user:pass@opencode.ai/zen/go/v1/responses", + ]) expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl })).toBe(false); + }); + + test("rejects noncanonical envelopes, suffixes, and other statuses", () => { + // A bare string is not the structured envelope. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: JSON.stringify({ error: "Invalid upload request." }), ...GO_ROUTE })).toBe(false); + // A suffix means the gateway said something else; do not guess. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE + " Please retry."), ...GO_ROUTE })).toBe(false); + // Different status: only the gateway 400 is the flap. + expect(isTransientConsoleGoUploadRejection({ status: 500, errorBody: envelope(GO_MESSAGE), ...GO_ROUTE })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: undefined, ...GO_ROUTE })).toBe(false); + // Other 400s on the same wire are verdicts on the request, not flaps. + expect(isTransientConsoleGoUploadRejection({ + status: 400, + errorBody: envelope("Error from provider (Console Go): Upstream request failed: [invalid_request_error] reasoning_effort max requires an active Muse Code subscription for model muse-spark-1.3-contributor."), + ...GO_ROUTE, + })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ + status: 400, + errorBody: JSON.stringify({ type: "error", error: { type: "MissingSessionID", message: "Request is missing x-opencode-session" } }), + ...GO_ROUTE, + })).toBe(false); + }); + + test("rejects partial envelopes and padded messages", () => { + const withError = (error: unknown) => JSON.stringify({ model: "muse-spark-1.3-contributor", error }); + // type carries the refusal identity; a partial envelope is a different error. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ param: null, message: GO_MESSAGE }), ...GO_ROUTE })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ param: null, type: "server_error", message: GO_MESSAGE }), ...GO_ROUTE })).toBe(false); + // param must be present and null. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ type: "invalid_request_error", message: GO_MESSAGE }), ...GO_ROUTE })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ param: "input", type: "invalid_request_error", message: GO_MESSAGE }), ...GO_ROUTE })).toBe(false); + // Padding means the gateway wrapped or appended something. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ param: null, type: "invalid_request_error", message: " " + GO_MESSAGE }), ...GO_ROUTE })).toBe(false); + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: withError({ param: null, type: "invalid_request_error", message: GO_MESSAGE + "\n" }), ...GO_ROUTE })).toBe(false); + // The exact canonical envelope still matches. + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), ...GO_ROUTE })).toBe(true); + }); +}); diff --git a/tests/responses/responses-console-go-upload-retry.test.ts b/tests/responses/responses-console-go-upload-retry.test.ts new file mode 100644 index 0000000000..e475e53139 --- /dev/null +++ b/tests/responses/responses-console-go-upload-retry.test.ts @@ -0,0 +1,210 @@ +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { handleResponses } from "../../src/server/responses/core"; +import type { RequestLogContext } from "../../src/server/request-log"; +import type { OcxConfig } from "../../src/types"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +const originalFetch = globalThis.fetch; +const originalOpenCodexHome = process.env.OPENCODEX_HOME; + +/** The exact Console Go rejection for a payload it accepts moments later. */ +const UPLOAD_REFUSAL = JSON.stringify({ + model: "muse-spark-1.3-contributor", + error: { + param: null, + type: "invalid_request_error", + message: "Error from provider (Console Go): Upstream request failed: [invalid_request_error] Invalid upload request.", + }, +}); + +/** A deterministic 400 on the same wire: a verdict on the request, never a flap. */ +const EFFORT_REFUSAL = JSON.stringify({ + model: "muse-spark-1.3-contributor", + error: { + param: "reasoning.effort", + type: "invalid_request_error", + message: "Error from provider (Console Go): Upstream request failed: [invalid_request_error] reasoning_effort max requires an active Muse Code subscription for model muse-spark-1.3-contributor.", + }, +}); + +let testDir = ""; + +beforeEach(() => { + testDir = mkdtempSync(join(tmpdir(), "ocx-console-go-upload-retry-")); + process.env.OPENCODEX_HOME = testDir; +}); + +afterEach(() => { + globalThis.fetch = originalFetch; + if (originalOpenCodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = originalOpenCodexHome; + removeTreeWithRetry(testDir); +}); + +function config(): OcxConfig { + return { + defaultProvider: "go", + providers: { + go: { + adapter: "openai-responses", + baseUrl: "https://opencode.ai/zen/go/v1", + authMode: "key", + apiKey: "go-test-key", + }, + other: { + adapter: "openai-responses", + baseUrl: "https://other.example.test/v1", + authMode: "key", + apiKey: "other-test-key", + }, + }, + } as OcxConfig; +} + +function request(stream = false, provider = "go"): Request { + return new Request("http://localhost/v1/responses", { + method: "POST", + headers: { + "content-type": "application/json", + "session_id": "thread-console-go-upload-retry", + }, + body: JSON.stringify({ + model: provider + "/muse-spark-1.3-contributor", + stream, + store: false, + input: [ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + ], + }), + }); +} + +function refusal(status = 400, body = UPLOAD_REFUSAL): Response { + return new Response(body, { status, headers: { "content-type": "application/json" } }); +} + +function success(id: string): Response { + return Response.json({ id, object: "response", status: "completed", model: "muse-spark-1.3-contributor", output: [] }); +} + +describe("Console Go transient upload refusal recovery", () => { + test("replays the refusal once and serves the retry with a byte-identical body", async () => { + const outbound: string[] = []; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + outbound.push(String(init?.body)); + return outbound.length === 1 ? refusal() : success("resp-upload-retry-recovered"); + }) as typeof fetch; + const logCtx: RequestLogContext = { model: "", provider: "" }; + + const response = await handleResponses(request(), config(), logCtx); + + expect(response.status).toBe(200); + expect(outbound).toHaveLength(2); + // The replay must preserve the exact serialized request. + expect(outbound[1]).toBe(outbound[0]); + expect(logCtx.activeAttempt?.recoveryKinds).toEqual(["console-go-upload-retry"]); + }); + + test("does not replay a different 400 from the same wire", async () => { + let sends = 0; + globalThis.fetch = (async () => { + sends += 1; + return refusal(400, EFFORT_REFUSAL); + }) as typeof fetch; + + const response = await handleResponses(request(), config(), { model: "", provider: "" }); + + expect(response.status).toBe(400); + expect(sends).toBe(1); + }); + + test("keeps a repeated refusal visible after the single bounded replay", async () => { + let sends = 0; + globalThis.fetch = (async () => { + sends += 1; + return refusal(); + }) as typeof fetch; + const logCtx: RequestLogContext = { model: "", provider: "" }; + + const response = await handleResponses(request(), config(), logCtx); + + expect(response.status).toBe(400); + expect(sends).toBe(2); + expect(logCtx.activeAttempt?.recoveryKinds).toEqual(["console-go-upload-retry"]); + }); + + test("does not replay the same refusal text from a non-Console provider", async () => { + let sends = 0; + globalThis.fetch = (async () => { + sends += 1; + return refusal(); + }) as typeof fetch; + + const response = await handleResponses(request(false, "other"), config(), { model: "", provider: "" }); + + expect(response.status).toBe(400); + expect(sends).toBe(1); + }); +}); + + +describe("Console destination and translated recovery controls", () => { + test("a canonical row name cannot authorize another host", async () => { + const cfg = config(); + cfg.providers["opencode-go"] = { ...cfg.providers.go!, baseUrl: "https://other.example.test/v1" }; + let sends = 0; + globalThis.fetch = (async () => { sends++; return refusal(); }) as typeof fetch; + const response = await handleResponses(request(false, "opencode-go"), cfg, { model: "", provider: "" }); + expect(response.status).toBe(400); + expect(sends).toBe(1); + expect(await response.text()).toContain("Invalid upload request."); + }); + + for (const adapter of ["openai-responses", "openai-chat"] as const) { + for (const stream of [false, true]) { + test(`${adapter} stream=${stream} replays identical bytes once`, async () => { + const cfg = config(); + cfg.providers.go!.adapter = adapter; + const outbound: string[] = []; + globalThis.fetch = (async (_url: RequestInfo | URL, init?: RequestInit) => { + outbound.push(String(init?.body)); + if (outbound.length === 1) return refusal(); + if (adapter === "openai-responses") { + const completed = { id: "resp_fixture", object: "response", status: "completed", output: [] }; + return stream ? new Response(`event: response.completed\ndata: ${JSON.stringify({ type: "response.completed", response: completed })}\n\n`, { headers: { "content-type": "text/event-stream" } }) : Response.json(completed); + } + const chunk = { id: "chat_fixture", object: "chat.completion.chunk", choices: [{ index: 0, delta: { content: "answer" }, finish_reason: "stop" }] }; + return new Response(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`, { headers: { "content-type": "text/event-stream" } }); + }) as typeof fetch; + const logCtx: RequestLogContext = { model: "", provider: "" }; + const response = await handleResponses(request(stream), cfg, logCtx); + const body = await response.text(); + expect(response.status).toBe(200); + expect(outbound).toHaveLength(2); + expect(outbound[1]).toBe(outbound[0]); + expect(logCtx.activeAttempt?.recoveryKinds).toEqual(["console-go-upload-retry"]); + expect(body).toContain("completed"); + }); + } + test(`${adapter} abort during backoff sends no replay`, async () => { + const cfg = config(); cfg.providers.go!.adapter = adapter; + const controller = new AbortController(); + let sends = 0; + globalThis.fetch = (async () => { sends++; return refusal(); }) as typeof fetch; + const originalTimeout = globalThis.setTimeout; + const spy = spyOn(globalThis, "setTimeout").mockImplementation(((handler: TimerHandler, ms?: number, ...args: unknown[]) => { + if (ms === 800) queueMicrotask(() => controller.abort()); + return originalTimeout(handler, ms, ...args); + }) as typeof setTimeout); + try { + const response = await handleResponses(request(), cfg, { model: "", provider: "" }, { abortSignal: controller.signal }); + expect(controller.signal.aborted).toBe(true); + expect(response.status).toBe(499); + expect(sends).toBe(1); + } finally { spy.mockRestore(); } + }); + } +}); From d1494b0acfdad0c4db8cd10e84d977fc9e13d928 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:50:09 +0900 Subject: [PATCH 02/21] test(responses): cover Console nonreplayable and buffered controls --- .../responses-console-go-upload-retry.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/responses/responses-console-go-upload-retry.test.ts b/tests/responses/responses-console-go-upload-retry.test.ts index e475e53139..4ece7bd0af 100644 --- a/tests/responses/responses-console-go-upload-retry.test.ts +++ b/tests/responses/responses-console-go-upload-retry.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { markResponseNonReplayable } from "../../src/lib/upstream-retry"; import { handleResponses } from "../../src/server/responses/core"; import type { RequestLogContext } from "../../src/server/request-log"; import type { OcxConfig } from "../../src/types"; @@ -176,6 +177,7 @@ describe("Console destination and translated recovery controls", () => { const completed = { id: "resp_fixture", object: "response", status: "completed", output: [] }; return stream ? new Response(`event: response.completed\ndata: ${JSON.stringify({ type: "response.completed", response: completed })}\n\n`, { headers: { "content-type": "text/event-stream" } }) : Response.json(completed); } + if (!stream) return Response.json({ id: "chat_fixture", object: "chat.completion", choices: [{ index: 0, message: { role: "assistant", content: "answer" }, finish_reason: "stop" }] }); const chunk = { id: "chat_fixture", object: "chat.completion.chunk", choices: [{ index: 0, delta: { content: "answer" }, finish_reason: "stop" }] }; return new Response(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`, { headers: { "content-type": "text/event-stream" } }); }) as typeof fetch; @@ -208,3 +210,23 @@ describe("Console destination and translated recovery controls", () => { }); } }); + + +describe("Console nonreplayable response boundary", () => { + for (const adapter of ["openai-responses", "openai-chat"] as const) { + test(`${adapter} does not replay a marked response`, async () => { + const cfg = config(); cfg.providers.go!.adapter = adapter; + let sends = 0; + globalThis.fetch = (async () => { + sends++; + const response = refusal(); + markResponseNonReplayable(response); + return response; + }) as typeof fetch; + const response = await handleResponses(request(), cfg, { model: "", provider: "" }); + expect(response.status).toBe(400); + expect(sends).toBe(1); + expect(await response.text()).toContain("Invalid upload request."); + }); + } +}); From e6e3b5b1d8b2f59534b16102e3a1aa012dfe6911 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:36:58 +0900 Subject: [PATCH 03/21] docs: plan thinking presentation and transport carries --- .../260912_thinking_contract/000_plan.md | 31 ++ .../260912_thinking_contract/001_sources.md | 7 + .../010_presentation.md | 316 +++++++++++++++++ .../020_transport_hint.md | 319 ++++++++++++++++++ .../260912_thinking_contract/030_spark.md | 66 ++++ .../260912_thinking_contract/040_delivery.md | 7 + 6 files changed, 746 insertions(+) create mode 100644 devlog/_plan/260912_thinking_contract/000_plan.md create mode 100644 devlog/_plan/260912_thinking_contract/001_sources.md create mode 100644 devlog/_plan/260912_thinking_contract/010_presentation.md create mode 100644 devlog/_plan/260912_thinking_contract/020_transport_hint.md create mode 100644 devlog/_plan/260912_thinking_contract/030_spark.md create mode 100644 devlog/_plan/260912_thinking_contract/040_delivery.md diff --git a/devlog/_plan/260912_thinking_contract/000_plan.md b/devlog/_plan/260912_thinking_contract/000_plan.md new file mode 100644 index 0000000000..86a87f9d36 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/000_plan.md @@ -0,0 +1,31 @@ +# Preserve reasoning provenance and transport intent + +Readers: maintainers choosing whether to integrate the thinking lane. Raw reasoning must remain content, while provider-authored summaries can be displayed under an explicit provider default. The plan reconciles #4301 and #4287, separately reviews #3652 hint suppression, and carries #4130 Spark compatibility without retirement. + +Loop: satisfy-spec HOTL, triggered by authorized thinking-lane delivery. Goal: reviewable carry PRs and final-head hosted CI. Non-goals: merges, closure of source PRs, retirement #4334, releases, user service/config changes, other worktrees. All local product suites/build/typecheck/install are NOT RUN by instruction. Only available existing credentials/tools are used; no user token/time/agent ceiling was set. Stop: every source PR has a justified disposition and every delivered branch has exact-head hosted CI evidence. Outcomes: DONE on evidence, HOLD/NEEDS_HUMAN on explicit unresolved acceptance, never fake green. Escalation: real tool denial or requirement beyond scope; main reclaims after two distinct reviewer failures. Native architect selector is unavailable; inherited independent design review and reflection follow the user instruction, with a separate A audit. + +## Dependency map + +| Cycle | Artifact | Result | +| --- | --- | --- | +| roadmap | this file and all decade docs | docs-only plan lock | +| presentation | 010_presentation.md | raw/summary contract and provider opt-in | +| hint | 020_transport_hint.md | independent transport-hint disposition/carry | +| spark | 030_spark.md | independent Spark Lite carry | +| delivery | 040_delivery.md | final heads, review closure and hosted CI | + +Presentation combines two conflicting source proposals into one contract. Hint and Spark are independent and receive ordinary dev-based PRs, not artificial stack dependencies. Final review consumes all branches. No GitHub native stacks are requested. + +## Evidence and owner map + +Baseline origin/dev: 69e3dcda755a52feb1327edad6c8ea6cefd6e871. Source PR heads: #4301 5d6d1862a11da6e4d0c04eb7f35f9f48ae1285fd; #4287 fe13bdb7bf8403a2a2cdb10f258a68b649177953; #3652 13fb263778e9036e66ae86d41e29f9f47bbbed92; #4130 5d56f5461ea3d18668b85f6bb0d8a523920f2536. All open when inspected. Original authors: Robin Bially, yxr1995-maker, itismyfield, luvs01; exact Git trailers will be read from original commits before carrying. + +Current owners: src/bridge.ts:663 raw-reasoning finalization; src/adapters/google.ts:571 shared part classifier; src/server/responses/core.ts:2490 final-route normalization; src/responses/parser.ts:543 summary omission policy; src/types/request.ts:310 AdapterEvent. Reuse these boundaries; no new event enum or generic service layer. Structure INDEX maps shared areas to topical documents; main contracts are providers/chat-compat.md, providers/google.md, transports/responses.md and config.md with references from affected area owners. + +Verification: git diff --check was run at baseline and exited 0, checking diff whitespace only. GitHub ci.yml workflow_dispatch lane=all reads checkout source, typechecks, runs product suites and cross-platform jobs; NOT RUN locally. Every conditional scenario is named in decade docs and must be asserted in committed regression tests. Source inspection is not runtime proof. + +## Cycle records + +Roadmap P: requirements/source inspection and independent design review in progress. No product patch applied. + +Roadmap B: locked amended contract after independent A PASS and both design reflections ALIGNED. Product implementation starts in the next cycle. diff --git a/devlog/_plan/260912_thinking_contract/001_sources.md b/devlog/_plan/260912_thinking_contract/001_sources.md new file mode 100644 index 0000000000..f05813eae8 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/001_sources.md @@ -0,0 +1,7 @@ +# Source decisions + +Public PR diffs and latest comments are the source proposal evidence. #4130's September 11 corrections pin Lite on for nonempty additional_tools bodies and off otherwise; adopting the earlier unconditional false version loses tools. #4334 is an explicit retirement HOLD and is not carried. + +#4301 removes automatic content-to-summary conversion. #4287 tests raw DeepSeek content as a visible summary; that expectation conflicts with provenance and will be replaced, not adopted. Google thought-summary API documentation distinguishes summaries from opaque thought signatures: https://ai.google.dev/gemini-api/docs/generate-content/thinking (opened 2026-09-12). CCA generationConfig/includeThoughts behavior is contributor probe evidence, not a newly performed live-service probe. + +Searches used: reasoning_raw_delta, thinking_delta, hideThinkingSummary, googlePartTextEvent, preserveReasoningContent, and the four PR numbers. Existing bridge event types can distinguish raw content and summary without a new enum. No-code/config-only options cannot repair the existing mislabeled content; raw rewrite deletion plus existing boundaries is the smallest change. diff --git a/devlog/_plan/260912_thinking_contract/010_presentation.md b/devlog/_plan/260912_thinking_contract/010_presentation.md new file mode 100644 index 0000000000..e39e9c0bfe --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/010_presentation.md @@ -0,0 +1,316 @@ +# Presentation contract + +Class C4 public protocol contract. Depends on roadmap lock. MODIFY src/bridge.ts: closeCurrentRawReasoning and reasoning_raw_delta emit response.reasoning_text.delta/done with content_index:0; final items use summary:[] and content:[{type:reasoning_text,text}]. buildResponseJSONWithBudget mirrors this. Keep hidden txt-only replay envelopes intact. DELETE src/server/responses-reasoning-summary-rewrite.ts and its obsolete unit test; MODIFY core.ts to remove imports and SSE/JSON content-to-summary rewrites. MODIFY both layout manifests to remove that test. Adopt the exact #4301 hunks below except reporter video and historical verification record. + +MODIFY provider.ts, registry.ts, derive.ts, router.ts and auth-cors.ts to carry showThinkingSummary boolean (preserve explicit false). Seed only google-antigravity true. Creation: provider config/registry; serialization: providerConfigSeed and deriveKeyLoginMap; deserialization: config provider passthrough and management field policy; consumers: routedProviderConfig, final-route normalization, Google request builder. No new enum. + +MODIFY core.ts final-route normalization: apply provider default only when original reasoning.summary is omitted, never explicit none; recompute on each final route so fallback cannot inherit another provider default. Provider opt-in authorizes summary display, not raw-to-summary conversion. + +MODIFY google.ts shared part classifier to use existing thinking_delta only for Gemini thought summaries under verified Gemini model provenance; CCA Claude/gpt-oss thought text remains reasoning_raw_delta. Persist request-local Gemini identity using existing adapter state, used by both stream and buffered classifier calls. includeThoughts stays provider-opted, Gemini-only, non-image and explicit-hide aware. MODIFY google-wire-compiler.ts to retain only boolean true includeThoughts, independently of thinkingLevel. Do not claim raw text is an actual summary. + +MODIFY the #4287 end-to-end fixture: raw DeepSeek content remains content with empty summary even under provider opt-in; actual CCA Gemini thought parts use summary; omitted vs none vs auto, explicit provider false, saved-row enrichment, fallback reset, streaming/buffered paths. Extend existing Google tests and bridge raw tests; both layout manifests register responses-show-thinking-summary.test.ts. Update English providers docs and structure owners, keeping locale statements consistent. Source tests are authored but run only by hosted CI. + +Acceptance: raw event fixture => content delta and no summary delta; actual Gemini summary fixture => summary only when requested/provider-opted; explicit none => no synthesized summary and no includeThoughts request; false/unknown provider => no opt-in; fallback to unopted route => hidden behavior reset; replay envelope decodes same raw text and tool continuation remains valid; native Responses mixed content/summary remains byte-semantically native. No model prose synthesizer is introduced. + +## Source patch blueprint + +```diff +diff --git a/src/bridge.ts b/src/bridge.ts +index 20e7c3fe09..bc90f35b94 100644 +--- a/src/bridge.ts ++++ b/src/bridge.ts +@@ -663,16 +663,13 @@ export function bridgeToResponsesSSE( + const closeCurrentRawReasoning = () => { + if (!currentRawReasoning) return; + rawReasoningForNextToolCall = currentRawReasoning.text; +- emit("response.reasoning_summary_text.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text, +- }); +- emit("response.reasoning_summary_part.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, +- part: { type: "summary_text", text: currentRawReasoning.text }, ++ emit("response.reasoning_text.done", { ++ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text, + }); + const item = { + type: "reasoning", id: currentRawReasoning.itemId, +- summary: [{ type: "summary_text", text: currentRawReasoning.text }], ++ summary: [] as never[], ++ content: [{ type: "reasoning_text", text: currentRawReasoning.text }], + }; + emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item }); + retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning"); +@@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE( + const itemId = `rs_${uuid()}`; + const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] }; + emit("response.output_item.added", { output_index: outputIndex, item }); +- emit("response.reasoning_summary_part.added", { +- item_id: itemId, output_index: outputIndex, summary_index: 0, +- part: { type: "summary_text", text: "" }, +- }); + currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 }; + } + ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString( +@@ -1123,9 +1116,13 @@ export function bridgeToResponsesSSE( + event.text, + "reasoning", + )); +- emit("response.reasoning_summary_text.delta", { ++ // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT ++ // channel, matching native gpt-oss passthrough: Codex applies its own display ++ // policy, so the desktop band shows the "Thinking…" placeholder instead of the ++ // raw CoT (the #45 summary-channel display intent is intentionally reverted). ++ emit("response.reasoning_text.delta", { + item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, +- summary_index: 0, delta: event.text, ++ content_index: 0, delta: event.text, + }); + break; + } +@@ -1780,7 +1777,8 @@ function buildResponseJSONWithBudget( + } + pushOutput({ + type: "reasoning", id: `rs_${uuid()}`, +- summary: [{ type: "summary_text", text: currentRawReasoning }], ++ summary: [], ++ content: [{ type: "reasoning_text", text: currentRawReasoning }], + }, currentRawReasoningBytes, "reasoning"); + currentRawReasoning = ""; + currentRawReasoningBytes = 0; + +``` + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/google-wire-compiler.ts b/src/adapters/google-wire-compiler.ts +index 88c482ba7d..aa835e50b4 100644 +--- a/src/adapters/google-wire-compiler.ts ++++ b/src/adapters/google-wire-compiler.ts +@@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined { + ))].slice(0, 5); + if (stopSequences.length > 0) out.stopSequences = stopSequences; + } +- if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") { +- const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); +- const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) +- ? raw +- : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); +- if (thinkingLevel) out.thinkingConfig = { thinkingLevel }; ++ if (isObject(value.thinkingConfig)) { ++ const thinking: JsonObject = {}; ++ if (typeof value.thinkingConfig.thinkingLevel === "string") { ++ const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); ++ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) ++ ? raw ++ : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); ++ if (thinkingLevel) thinking.thinkingLevel = thinkingLevel; ++ } ++ // The one key that makes Google return `thought: true` text. Cloud Code Assist serves ++ // thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the ++ // request opts in, so dropping it here silently reinstates the missing-thinking behavior. ++ if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true; ++ if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking; + } + if (Array.isArray(value.responseModalities)) { + const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m)); +diff --git a/src/adapters/google.ts b/src/adapters/google.ts +index 7fcc88ba59..5a6675f6e4 100644 +--- a/src/adapters/google.ts ++++ b/src/adapters/google.ts +@@ -866,11 +866,27 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte + ); + antigravityModel = wireModelId; + antigravitySession = sessionId; ++ // Gemini returns no chain-of-thought TEXT unless the request opts in. Probed against CCA ++ // 2026-09-12: `gemini-3.8-flash-high` answered with thoughtsTokenCount=321 and zero ++ // `thought` parts, then 358-652 chars of genuine reasoning once includeThoughts was set. ++ // Scoped to Gemini wire ids — Claude-on-CCA accepts the flag but never returns thought ++ // parts, and gpt-oss rejects it outright (400 INVALID_ARGUMENT, which would break every ++ // gpt-oss turn). Gated on the provider's visible-thinking opt-in so a user who wants ++ // thinking hidden does not pay conversation-history tokens for text nobody renders; ++ // `hideThinkingSummary !== true` is the same per-request gate the response path uses, so ++ // a client that explicitly asked for hidden thinking is not billed for the text either. ++ const includeThoughts = provider.showThinkingSummary === true ++ && parsed.options.hideThinkingSummary !== true ++ && /^gemini-/.test(wireModelId) ++ && !isImageCapableModel(parsed.modelId); + // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig). + // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction. +- if (thinkingLevel) { ++ if (thinkingLevel || includeThoughts) { + const gc = (body.generationConfig ?? {}) as Record; +- gc.thinkingConfig = { thinkingLevel }; ++ gc.thinkingConfig = { ++ ...(thinkingLevel ? { thinkingLevel } : {}), ++ ...(includeThoughts ? { includeThoughts: true } : {}), ++ }; + body.generationConfig = gc; + } + // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity +diff --git a/src/providers/derive.ts b/src/providers/derive.ts +index 67e6c0522e..7edf28787b 100644 +--- a/src/providers/derive.ts ++++ b/src/providers/derive.ts +@@ -43,6 +43,7 @@ export interface DerivedKeyLoginProvider { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -267,6 +268,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -315,6 +317,7 @@ export function deriveKeyLoginMap(): Record { + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -567,6 +570,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig + if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels]; + if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels]; + if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames; ++ if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary; + if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional; + if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier; + if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip; +diff --git a/src/providers/registry.ts b/src/providers/registry.ts +index f72bb7650b..e483e9db23 100644 +--- a/src/providers/registry.ts ++++ b/src/providers/registry.ts +@@ -343,6 +343,10 @@ export interface ProviderRegistryEntry { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt this provider into visible thinking summaries (see OcxProviderConfig.showThinkingSummary). ++ */ ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -367,7 +371,7 @@ export type ProviderConfigSeed = Pick< + | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens" + | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat" + | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels" +- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" ++ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary" + | "googleMode" | "project" | "location" | "headers" + >; + +@@ -2045,7 +2049,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ + // path must stay RELATIVE: this row sets `allowBaseUrlOverride`, and an absolute `url` would + // retarget a user's custom base back to Google. A leading `./` is required because a bare + // `v1internal:` reads as a URL scheme and `providerModelDiscoverySpecError` rejects it. +- { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, ++ { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", showThinkingSummary: true, jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, + { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, + { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, + { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, +diff --git a/src/router.ts b/src/router.ts +index 55a0326fce..bf2b9b4b98 100644 +--- a/src/router.ts ++++ b/src/router.ts +@@ -410,6 +410,13 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider + ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined + ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent } + : {}), ++ // The request path resolves through routedProviderConfig() and never calls ++ // enrichProviderFromRegistry(), so a saved provider row written before the ++ // registry learned this flag must be backfilled here or route.provider never ++ // carries it and the showThinkingSummary opt-in stays dead. ++ ...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined ++ ? { showThinkingSummary: registryEntry.showThinkingSummary } ++ : {}), + // Registry-only client-facing repair policy (#938): fill only when the + // saved provider has no explicit policy; clone so runtime never aliases + // the registry constant. +diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts +index 3a93246cd0..93377b5573 100644 +--- a/src/server/auth-cors.ts ++++ b/src/server/auth-cors.ts +@@ -885,6 +885,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { + autoToolChoiceOnlyModels: "editor", + preserveReasoningContentModels: "editor", + requiresReasoningPlaceholderModels: "editor", ++ showThinkingSummary: "editor", + retryOn429: "editor", + transientRetryOn5xx: "editor", + reasoningSplitModels: "editor", +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index cccd942026..852cd9f8b0 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -2467,6 +2467,20 @@ async function resolveSubagentFallbackModelEligibility(args: { + }; + } + ++/** ++ * Whether the client explicitly asked for hidden thinking (`reasoning.summary: "none"`). ++ * ++ * Pinned: parseRequest collapses "omitted" and "none" into one hideThinkingSummary ++ * flag, so the raw request body is the ONLY place that still distinguishes them. ++ * Provider opt-ins like showThinkingSummary must consult this — never the flag ++ * alone — or a future caller that copies only the flag would silently unlock an ++ * explicit opt-out. ++ */ ++function clientExplicitlyHidThinking(parsed: OcxParsedRequest): boolean { ++ const rawReasoning = (parsed._rawBody as { reasoning?: { summary?: unknown } } | undefined)?.reasoning; ++ return typeof rawReasoning === "object" && rawReasoning !== null ++ && (rawReasoning as { summary?: unknown }).summary === "none"; ++} + /** + * Apply every route-dependent request mutation against the final selected route. + * Must run only after subagent fallback has settled the model/provider. +@@ -2508,6 +2522,15 @@ async function applyFinalRouteRequestNormalization(args: { + // this request will actually use (#404). + route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req)); + route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire); ++ // Provider-opted visible thinking (e.g. google-antigravity): parseRequest hides thinking ++ // whenever the client omits reasoning.summary, which is the Codex default. A provider that ++ // serves genuine user-facing reasoning opts back into the summary channel here, so thought ++ // parts (Gemini thought, content-channel reasoning_text) reach the client instead of only ++ // the hidden replay envelopes. An explicit client reasoning.summary "none" still wins. ++ if (route.provider.showThinkingSummary === true && parsed.options.hideThinkingSummary === true ++ && !clientExplicitlyHidThinking(parsed)) { ++ parsed.options.hideThinkingSummary = false; ++ } + if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId; + logCtx.model = route.modelId; + logCtx.provider = route.providerName; +diff --git a/src/types/provider.ts b/src/types/provider.ts +index e65130a4fa..b6374a991a 100644 +--- a/src/types/provider.ts ++++ b/src/types/provider.ts +@@ -746,6 +746,15 @@ export interface OcxProviderConfig { + * out explicitly (e.g. MiniMax, where low effort disables thinking). + */ + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt-in: surface upstream thinking as visible reasoning summaries even when the ++ * client did not send `reasoning.summary`. parseRequest hides thinking by default ++ * (Codex omits the field), which strands genuine reasoning — e.g. Gemini `thought` ++ * parts on the google-antigravity (Cloud Code Assist) wire — in hidden replay ++ * envelopes. An explicit client `reasoning.summary: "none"` still wins. Set `false` ++ * to opt a seeded preset back out. ++ */ ++ showThinkingSummary?: boolean; + /** + * Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only, + * openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays + +``` + +## Reflection corrections accepted + +Explicit wire reasoning.summary:"none" wins. A client that serializes configured none as omission cannot be distinguished from unspecified preference. No client config rewrite or global catalog summary default changes. Summary classification is limited to built CCA Gemini requests; unknown/uninitialized, direct Google/Vertex and CCA Claude/gpt-oss remain raw. Streaming and buffered summary-to-tool continuations assert exact Google signature on correct call; never emit Google signatures as Anthropic thinking_signature. Hidden unsigned summaries may disappear but required tool replay state survives. Exercise final assistant text and terminal order, fallback in both directions, and remove replay-comparison rewrite alongside SSE/JSON rewrite. Desktop appearance remains client-controlled; source patch comments claiming an unconditional placeholder are replaced during adoption. diff --git a/devlog/_plan/260912_thinking_contract/020_transport_hint.md b/devlog/_plan/260912_thinking_contract/020_transport_hint.md new file mode 100644 index 0000000000..335e339a00 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/020_transport_hint.md @@ -0,0 +1,319 @@ +# Optional hint suppression + +Class C4 review because client metadata policy changes. Independent of presentation; depends only on roadmap. Adopt #3652 only after independent security/transport review. Public proposal removes exactly two x-codex-safety-buffering headers, metadata.type=safety_buffering events and top-level safety_buffering fields at the client relay boundary. Default false; malformed config must remain off and candidate validation rejects nonbooleans. This suppresses optional transport hints; provider safety decisions/refusals and upstream checks are unchanged. Compact and independent WS/other-provider pathways retain existing policy unless a directly exercised shared boundary already applies. + +MODIFY src/config.ts and src/types/config.ts for validated boolean/default; src/server/relay.ts for allowlisted header removal and SSE terminal-boundary transformation; relay-eager.ts for option forwarding; core.ts to compute option only for canonical OpenAI forward destination and pass it to all relevant headers/client output boundaries; index.ts exports if needed by existing test style. Do not apply to custom gateway/key providers. Preserve errors, response.failed/incomplete and terminal sentinel handling. + +MODIFY tests/responses/passthrough-headers.test.ts, openai-responses-passthrough.test.ts and tests/server/config.test.ts. Scenarios: absent/false/true/malformed config; uppercase headers; unrelated headers; split metadata frames; actual failure carrying hint must still fail; noncanonical provider has identical fields and retains them; eager/non-eager client paths. Add missing canonical route coverage if independent review identifies it. MODIFY English/ja/ko/ru/zh-cn server configuration docs and structure owners. Avoid unsupported claims about models being weaker or provider safety bypass. + +Before/after anchor: createSseTerminalOutputBoundary() -> createSseTerminalOutputBoundary(options?: CodexSafetyBufferingFilterOptions); sanitizePassthroughHeaders(upstream) -> sanitizePassthroughHeaders(upstream, options?); canonical true => filter option, every other provider => undefined. Full public source diff is pinned by #3652 head in 000_plan.md and inspected locally; any needed correction is recorded here before B. + +## Independent design corrections + +H1 accepted: policy rewrite and hint stripping compose. Build policyFailurePayload first, then remove top-level safety_buffering from the effective emitted payload, preserving response.failed/error data and retryable:false. H2 accepted: extend current relaySseWithFailedTail fourth options object with terminalBoundary; never replace upstreamError. Core passes both existing upstreamError and new terminalBoundary; update the existing source-contract assertion to preserve its original guarantee. H3 accepted: native WebSocket codex.response.metadata.headers and /responses/compact are explicitly excluded; their hints remain unfiltered. No new WS metadata filter. Docs must not claim the old WS allowlist excludes these headers. Regression fixtures cover CRLF/split/malformed input, policy error plus hint, EOF upstreamError, canonical true and noncanonical preservation. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/config.ts b/src/config.ts +index fdcda9547c..cd0641feb2 100644 +--- a/src/config.ts ++++ b/src/config.ts +@@ -1125,6 +1125,8 @@ const configSchema = z.object({ + configRebaseProvenance: z.unknown().optional(), + // A retry can be billable, so absence and malformed hand edits both stay off. + emptyCompletionRetry: z.boolean().optional().catch(false), ++ // Header suppression changes what Codex sees, so absence and malformed edits stay off. ++ dropCodexSafetyBuffering: z.boolean().optional().catch(false), + // A malformed hand edit must not silently stop opening the browser: fall back + // to undefined, which resolves to the historical auto-open behavior. + oauthOpenBrowser: z.boolean().optional().catch(undefined), +@@ -2613,6 +2615,14 @@ function emptyCompletionRetryError(value: unknown): string | null { + return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted"; + } + ++function dropCodexSafetyBufferingError(value: unknown): string | null { ++ const raw = rawConfigRecord(value); ++ if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null; ++ const enabled = raw.dropCodexSafetyBuffering; ++ if (enabled === undefined || typeof enabled === "boolean") return null; ++ return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted"; ++} ++ + function oauthOpenBrowserError(value: unknown): string | null { + const raw = rawConfigRecord(value); + if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null; +@@ -2718,6 +2728,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx + ?? codexQuotaAutoRefreshError(value) + ?? codexAccountPickerEnabledError(value) + ?? emptyCompletionRetryError(value) ++ ?? dropCodexSafetyBufferingError(value) + ?? oauthOpenBrowserError(value) + ?? runtimeRoleError(value) + ?? remoteGuiConfigError(value) +@@ -3684,6 +3695,7 @@ export function getDefaultConfig(): OcxConfig { + return { + port: 10100, + emptyCompletionRetry: false, ++ dropCodexSafetyBuffering: false, + managementUsageMaxReadBytes: 64 * 1024 * 1024, + appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024), + // Fresh/re-initialized configs are already written in the current three-tier +diff --git a/src/server/index.ts b/src/server/index.ts +index aedd6bf236..c6ce73b2f1 100644 +--- a/src/server/index.ts ++++ b/src/server/index.ts +@@ -142,6 +142,7 @@ import { + } from "./relay"; + export { + consumeForInspection, ++ codexSafetyBufferingFilterOptions, + relaySseWithFailedTail, + relaySseWithHeartbeat, + relayWithAbort, +diff --git a/src/server/relay-eager.ts b/src/server/relay-eager.ts +index 655997b813..a6e60d3d02 100644 +--- a/src/server/relay-eager.ts ++++ b/src/server/relay-eager.ts +@@ -26,6 +26,7 @@ + + import { + adapterEofIncompleteFrame, ++ type CodexSafetyBufferingFilterOptions, + createSseTerminalOutputBoundary, + doneFrame, + failedTailFrame, +@@ -83,6 +84,8 @@ export type EagerRelayOptions = { + postCancelDrainBytes?: number; + /** Injectable clock for tests. */ + now?: () => number; ++ /** Client output boundary filters (Codex safety-buffering hints). */ ++ terminalBoundary?: CodexSafetyBufferingFilterOptions; + }; + + const DEFAULT_MAX_QUEUE_BYTES = 8 * 1024 * 1024; +@@ -111,7 +114,7 @@ export function relaySseEagerBounded( + const terminalEncoder = new TextEncoder(); + const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder); + const terminalSentinel = doneFrame(terminalEncoder); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary); + const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks + ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined); + const encodeFailedTail = (error: unknown): Uint8Array | null => { +diff --git a/src/server/relay.ts b/src/server/relay.ts +index 60b57ea025..d840b2e59c 100644 +--- a/src/server/relay.ts ++++ b/src/server/relay.ts +@@ -162,7 +162,10 @@ export type SseTerminalOutputBoundary = { + * terminal, and drops every later block/byte. A premature [DONE] is held until + * a terminal arrives so clean EOF can synthesize one terminal and one sentinel. + */ +-export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { ++export function createSseTerminalOutputBoundary( ++ options?: CodexSafetyBufferingFilterOptions, ++): SseTerminalOutputBoundary { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES); +@@ -181,6 +184,10 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + const payload = sseDataPayload(decoder.decode(frame.block)); + const isDone = payload === "[DONE]"; + const parsed = payload === null ? undefined : parseSsePayload(payload); ++ const safetyBuffering = dropSafetyBuffering && parsed !== undefined ++ ? codexSafetyBufferingBlockAction(parsed) ++ : "keep"; ++ if (safetyBuffering === "drop") continue; + const policyError = parsed !== undefined && isPolicyRewriteType(parsed) + ? cyberPolicyTerminalError(parsed) + : undefined; +@@ -189,7 +196,9 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + decoder.decode(frame.block), + policyFailurePayload(policyError, parsed), + )) +- : frame.block; ++ : safetyBuffering === "strip" ++ ? encoder.encode(stripCodexSafetyBufferingField(decoder.decode(frame.block), parsed)) ++ : frame.block; + if (isDone) { + done = true; + if (responsesTerminal) { +@@ -260,10 +269,11 @@ export function relaySseWithFailedTail( + body: ReadableStream, + upstream: AbortController, + onClientGone?: (reason?: unknown) => void, ++ boundaryOptions?: CodexSafetyBufferingFilterOptions, + ): ReadableStream { + const reader = body.getReader(); + const encoder = new TextEncoder(); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(boundaryOptions); + let closed = false; + const relayChunk = ( + controller: ReadableStreamDefaultController, +@@ -438,6 +448,29 @@ function isPolicyRewriteType(parsed: unknown): boolean { + return type === "response.failed" || type === "response.incomplete" || type === "error"; + } + ++/** ++ * Codex emits its safety-buffering hint in the SSE body as well as in headers: ++ * a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a ++ * `safety_buffering` field on another event. The metadata event is dropped whole; ++ * the field is stripped so the carrying event is otherwise relayed unchanged. ++ */ ++function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" { ++ const root = asJsonRecord(parsed); ++ if (!root) return "keep"; ++ if (root.type === "response.metadata") { ++ const metadata = asJsonRecord(root.metadata); ++ if (metadata?.type === "safety_buffering") return "drop"; ++ } ++ return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep"; ++} ++ ++function stripCodexSafetyBufferingField(block: string, parsed: unknown): string { ++ const root = asJsonRecord(parsed); ++ if (!root) return block; ++ const { safety_buffering: _safetyBuffering, ...rest } = root; ++ return replaceSseDataPayload(block, JSON.stringify(rest)); ++} ++ + function rewritePolicyTerminalBlock(block: string, payload: string): string { + const newline = block.includes("\r\n") ? "\r\n" : "\n"; + const rewritten = replaceSseDataPayload(block, payload); +@@ -1422,7 +1455,31 @@ export function consumeForResponseLogMetadata( + * body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough. + * Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim. + */ +-export function sanitizePassthroughHeaders(upstream: Headers): Headers { ++export const CODEX_SAFETY_BUFFERING_HEADERS = [ ++ "x-codex-safety-buffering-enabled", ++ "x-codex-safety-buffering-faster-model", ++] as const; ++ ++const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet = new Set(CODEX_SAFETY_BUFFERING_HEADERS); ++ ++export interface CodexSafetyBufferingFilterOptions { ++ /** ++ * Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response ++ * headers and the `safety_buffering` SSE metadata event / field. Absent and ++ * `false` relay everything unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; ++} ++ ++/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */ ++export function codexSafetyBufferingFilterOptions( ++ config: { dropCodexSafetyBuffering?: boolean }, ++): CodexSafetyBufferingFilterOptions { ++ return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true }; ++} ++ ++export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const DROP = new Set([ + "content-encoding", + "content-length", +@@ -1439,7 +1496,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers { + ]); + const out = new Headers(); + upstream.forEach((value, key) => { +- if (!DROP.has(key.toLowerCase())) out.set(key, value); ++ const lower = key.toLowerCase(); ++ if (DROP.has(lower)) return; ++ if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return; ++ out.set(key, value); + }); + return out; + } +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index 9d0eea0d76..e199917968 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -304,6 +304,7 @@ import { + markEagerRelaySseResponse, + markNativePassthroughSseResponse, + relaySseWithFailedTail, ++ codexSafetyBufferingFilterOptions, + relayWithAbort, + sanitizePassthroughHeaders, + } from "../relay"; +@@ -3850,6 +3851,9 @@ async function handleResponsesInner( + let hostAdmissionLease = pendingHostAdmissionLease; + pendingHostAdmissionLease = null; + try { ++ const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) ++ ? codexSafetyBufferingFilterOptions(config) ++ : undefined; + const imageGenCallAliases = route.provider.authMode === "forward" + ? new Map() + : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget); +@@ -4732,7 +4736,7 @@ async function handleResponsesInner( + } + break; + } +- const headers = sanitizePassthroughHeaders(upstreamResponse.headers); ++ const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + const resolvedModel = headers.get("openai-model")?.trim(); + if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; + if (isUsageDebugEnabled()) { +@@ -4824,7 +4828,7 @@ async function handleResponsesInner( + return new Response(upstreamResponse.body, { + status: upstreamResponse.status, + statusText: upstreamResponse.statusText, +- headers: sanitizePassthroughHeaders(upstreamResponse.headers), ++ headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions), + }); + } + if (!upstreamResponse.ok) { +@@ -5027,6 +5031,7 @@ async function handleResponsesInner( + onDone: () => unregisterTurn(turnAc), + }, { + clientGoneSignal: options.abortSignal, ++ terminalBoundary: codexSafetyBufferingOptions, + ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}), + }); + // When selected, this relay closes response.completed even if upstream +@@ -5110,7 +5115,8 @@ async function handleResponsesInner( + const rewrittenBody = clientBlockRewrite !== undefined + ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget) + : nativeBody; +- const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason)); ++ const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason), ++ codexSafetyBufferingOptions); + return markNativePassthroughSseResponse(new Response(clientBody, { + status: upstreamResponse.status, + headers, +@@ -5238,7 +5244,7 @@ async function handleResponsesInner( + } + throw error; + } +- const sseHeaders = sanitizePassthroughHeaders(headers); ++ const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions); + sseHeaders.set("content-type", "text/event-stream"); + sseHeaders.set("cache-control", "no-store"); + return new Response(stream, { +diff --git a/src/types/config.ts b/src/types/config.ts +index 8cf1246979..4d2c63fdf1 100644 +--- a/src/types/config.ts ++++ b/src/types/config.ts +@@ -335,6 +335,16 @@ export interface OcxConfig { + client?: OcxClientConnectionConfig; + /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */ + emptyCompletionRetry?: boolean; ++ /** ++ * Drop the Codex safety-buffering hints from a Codex Responses passthrough: the ++ * `x-codex-safety-buffering-*` response headers, `response.metadata` SSE events of ++ * type `safety_buffering`, and the `safety_buffering` field on other SSE events. ++ * The Codex TUI turns those hints into a "retry with a faster model" prompt whose ++ * default action switches the session to a weaker model, so an unattended session ++ * can lose its model to a stray keystroke. Absent and `false` relay everything ++ * unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; + /** + * Whether a login may open a browser on the machine running the proxy. + * + +``` diff --git a/devlog/_plan/260912_thinking_contract/030_spark.md b/devlog/_plan/260912_thinking_contract/030_spark.md new file mode 100644 index 0000000000..3997aaa6d7 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/030_spark.md @@ -0,0 +1,66 @@ +# Spark Lite metadata follows body shape + +Class C3 bounded compatibility. Independent of presentation/hint; depends on roadmap. MODIFY src/adapters/openai-responses.ts only inside canonical OpenAI forwarding and final wire model gpt-5.3-codex-spark. Add bodyCarriesLiteToolShape next to existing tool-shape helpers: Array.isArray(body.input) && body.input.some(item => isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.length > 0). After final Spark body construction, delete all case variants of CODEX_RESPONSES_LITE_HEADER then set it to liteShaped ? "true" : "false". Existing prepareCodexWsRequest projects it onto native frame metadata. + +Before: Spark deletes the header, allowing stale native metadata to survive. After: tool-less/top-level-tool Spark frames advertise false; nonempty Lite catalog frames advertise true despite conflicting inherited header. No retirement, no changes to model availability, no user service changes. + +MODIFY tests/codex-integration/codex-metadata-integrity.test.ts: alias resolved final model, inherited true/false/mixed-case/absent header, Lite tool body true, empty Lite group false, malformed metadata keeps HTTP fallback/body, noncanonical remains unchanged. MODIFY tests/responses/ws-upstream-reuse.test.ts: legacy true socket retires when adapter produces false, replacement same identity reused, raw request immutable. MODIFY all eight existing architecture locale pages and structure/transports/responses.md, referencing body-shape rule from shared area owners. Adopt latest #4130 source diff, preserving author; do not import historical earlier heads. + +Verification: source diff review and final-branch hosted ci.yml lane=all. Tests NOT RUN locally. Success proves framing and connection identity, not a live backend EOF fix or all tool-bearing EOF cases. Remaining acceptance: broader tool-format conversion stays out of scope. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts +index c4aa523ee6..8fbe43816d 100644 +--- a/src/adapters/openai-responses.ts ++++ b/src/adapters/openai-responses.ts +@@ -864,6 +864,21 @@ function promoteClientLoadedTools(body: unknown): unknown { + } + + const MAX_RESPONSES_CALL_ID_LENGTH = 64; ++ ++/** ++ * Whether the outgoing body still delivers tools through the responses-lite shape. ++ * ++ * Lite carries the client catalog as an `additional_tools` input item; the non-Lite wire shape ++ * expects top-level `tools`. Anything that flips the Lite advertisement has to agree with the ++ * shape actually being sent, or the destination silently loses the tool surface. ++ */ ++function bodyCarriesLiteToolShape(body: Record): boolean { ++ if (!Array.isArray(body.input)) return false; ++ return body.input.some(item => ++ isPlainObject(item) && item.type === "additional_tools" ++ && Array.isArray(item.tools) && item.tools.length > 0 ++ ); ++} + const REPAIRED_CALL_ID_PREFIX = "call_ocx_"; + const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length; + +@@ -2515,12 +2530,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): + parsed.modelId, + ); + if (isCanonicalOpenAiForwardProvider(provider)) { +- // Spark closes Responses Lite streams before a terminal completion. Select compatibility +- // from the final wire model so aliases cannot leave the caller or a static header enabled. ++ // Select Spark's Lite compatibility from the final wire model, including aliases, and ++ // let the BODY decide it. The header also overrides native WS metadata downstream, so a ++ // forwarded or statically configured value must never contradict the shape being sent. ++ // ++ // The synchronized catalog keeps `use_responses_lite: true` for Spark precisely because ++ // it selects tool delivery (`input[].additional_tools` instead of top-level `tools`), and ++ // stripSparkCompatibility filters that group in place rather than promoting it. So a ++ // Lite-shaped body is pinned back ON — otherwise an inherited `false` advertises non-Lite ++ // while the tools exist only in the Lite shape, and Spark loses the tool surface. Only a ++ // body with no Lite tool group is downgraded, which is what the stream fix needs. + if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { ++ const liteShaped = bodyCarriesLiteToolShape(finalBody); + for (const name of Object.keys(headers)) { + if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; + } ++ headers[CODEX_RESPONSES_LITE_HEADER] = liteShaped ? "true" : "false"; + } + const routingHeaders = new Headers(headers); + applyCodexRoutingHint(routingHeaders, finalBody); + +``` diff --git a/devlog/_plan/260912_thinking_contract/040_delivery.md b/devlog/_plan/260912_thinking_contract/040_delivery.md new file mode 100644 index 0000000000..a70e62bfb6 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/040_delivery.md @@ -0,0 +1,7 @@ +# Final heads and handoff + +Class C3 delivery evidence. Depends on all dispositions. MODIFY branch-owned numbered completion docs and ignored .tmp/thinking/handoff.md. Read existing .github/PULL_REQUEST_TEMPLATE.md; write every section, credits and precise NOT RUN limitation. Publish only own codex/260912-60plus-thinking* branches with git push --no-verify; PR bases dev for independent units, ordinary parent branch only for actual dependencies. No merge/auto-merge/closures. + +NEW .tmp/thinking/*-ci.json captures gh run view JSON for final SHA plus all jobs. NEW .tmp/thinking/*-review.md captures independent implementation findings with accepted/rebutted disposition. Refresh head/base, native stack membership (unknown if API unsupported), current reviews and CI before handoff. Inspect .github/workflows/ci.yml and dispatch lane=all at each final branch where needed. Existing automatic runs stay untouched. If final-head CI fails, inspect failing logs, repair scoped source or fixtures, commit/push --no-verify and validate new final tip. Do not label skipped/cancelled/old-head runs passing. + +Final handoff fields: own worktree, branch per PR, source PR disposition, exact head, PR URL, dependency order, original author trailers, remaining acceptance, unresolved reviews, CI run id/url/head/result/job conclusions, own cycle records and local tests NOT RUN. Parent performs any subsequent integration. No evidence claims from peer commentary alone. From d11f1e65b0e10cb67f494b6d04d8ee01ed66de73 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:51:39 +0900 Subject: [PATCH 04/21] feat(responses): opt in to client transport hint suppression Co-authored-by: Wonchang Oh Co-authored-by: Claude Fable 5.1 Co-authored-by: Codex --- .../020_transport_hint.md | 4 + .../docs/ja/reference/configuration/server.md | 3 + .../docs/ko/reference/configuration/server.md | 1 + .../docs/reference/configuration/server.md | 1 + .../docs/ru/reference/configuration/server.md | 3 + .../zh-cn/reference/configuration/server.md | 3 + src/config.ts | 12 +++ src/server/index.ts | 1 + src/server/relay-eager.ts | 5 +- src/server/relay.ts | 80 ++++++++++++++++--- src/server/responses/core.ts | 13 ++- src/types/config.ts | 2 + structure/adapters/registry.md | 3 + structure/catalog.md | 3 + structure/clients/claude-desktop.md | 3 + structure/config.md | 4 + structure/data-planes/images.md | 3 + structure/data-planes/inbound-compat.md | 3 + structure/gui-and-management-api.md | 3 + structure/ops/docs-and-release.md | 3 + structure/ops/service-and-sidecars.md | 3 + structure/overview.md | 3 + structure/providers/openai-tiers.md | 3 + structure/providers/xai-grok.md | 3 + structure/runtime.md | 3 + structure/subagents.md | 3 + structure/transports/inventory.md | 3 + structure/transports/responses.md | 9 +++ structure/transports/streaming-health.md | 3 + .../openai-responses-passthrough.test.ts | 77 ++++++++++++++++++ tests/responses/passthrough-abort.test.ts | 2 +- tests/responses/passthrough-headers.test.ts | 75 ++++++++++++++++- tests/responses/sse-failed-tail.test.ts | 40 ++++++++++ tests/server/config.test.ts | 13 +++ 34 files changed, 376 insertions(+), 17 deletions(-) diff --git a/devlog/_plan/260912_thinking_contract/020_transport_hint.md b/devlog/_plan/260912_thinking_contract/020_transport_hint.md index 335e339a00..abfff918c7 100644 --- a/devlog/_plan/260912_thinking_contract/020_transport_hint.md +++ b/devlog/_plan/260912_thinking_contract/020_transport_hint.md @@ -317,3 +317,7 @@ index 8cf1246979..4d2c63fdf1 100644 * ``` + +## Hint P revalidation + +Previous D: presentation source complete; final hosted CI remains in delivery. This branch starts from the common docs checkpoint bd34120180 and baseline product 69e3dcda. Original #3652 does not apply cleanly because relay upstreamError handling changed. Carry nonconflicting hunks and manually adapt relay/core/config hunks, preserving cancellation and error capture. Independent H1-H3 plan reflection ALIGNED remains applicable. diff --git a/docs-site/src/content/docs/ja/reference/configuration/server.md b/docs-site/src/content/docs/ja/reference/configuration/server.md index 0dd9cf59e4..e86387f80e 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/server.md +++ b/docs-site/src/content/docs/ja/reference/configuration/server.md @@ -13,6 +13,7 @@ description: リスナー、リモート アクセス、アドミッション | `hostname?` | `string` | `"127.0.0.1"` |バインドアドレス。非ループバック バインドには `OPENCODEX_API_AUTH_TOKEN` が必要です。 | | `proxy?` | `string` | — |送信 HTTP(S) プロキシ URL または `${ENV_VAR}`。これらの変数が設定されていない場合にのみ、`HTTP_PROXY` / `HTTPS_PROXY` に適用されます。ループバックは `NO_PROXY` に残ります。 | | `emptyCompletionRetry?` | `boolean` | `false` | テキストもツール呼び出しもない Responses ターンを、ターミナルイベント前にストリームが終了した場合も含め、同一リクエストで 1 回再試行するよう明示的に有効化します。再試行は課金対象になる場合があります。`OCX_EMPTY_COMPLETION_RETRY=0` で設定を変更せず無効化できます。combo と routed-compaction turn は対象外です。 | +| `dropCodexSafetyBuffering?` | `boolean` | `false` | Codex Responses パススルーから Codex の safety-buffering ヒントを除去します。対象は `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 応答ヘッダー、`safety_buffering` 型の `response.metadata` SSE イベント、およびその他の SSE イベントにある `safety_buffering` フィールドです。Codex TUI はこれらを、既定の操作でセッションをより弱いモデルに切り替える「より高速なモデルで再試行」プロンプトとして表示します。その他の `x-codex-*` ヘッダーと SSE イベントの内容は、そのフィールドの除去を除いて変更せずに転送されます。既定ではオフです。 | | `stallTimeoutSec?` | `number` | `300` | `response.incomplete` より前にアップストリーム データがない秒数。最小 1。 | `connectTimeoutMs?` | `number` | `200000` |試行ごとの DNS/TCP/TLS/最終ヘッダーの期限。本体が生成される前に終了します。 | | `shutdownTimeoutMs?` | `number` | `5000` |アクティブなターンが中止される前の正常な排出期限。 | @@ -171,3 +172,5 @@ Anthropic OAuth サイドカーは、opencodex の既存のクロード コー ## Codex クォータのネットワーク診断 メイン Codex アカウント行の `quotaRefresh` はクォータ取得の診断情報であり、残量やモデルへのアクセス権を示すものではありません。キャッシュ利用時や取得を行わない場合は省略されることがあります。取得には操作中のシェルではなく、実行中のプロキシサービスの環境が使われます。`proxy` 未設定では既存の環境を維持し、`"auto"` は起動時に Windows の静的プロキシ設定だけを読みます。PAC/WPAD、SOCKS のみの設定、実行中の変更は自動反映されません。TUN での成功だけでは HTTP プロキシ経路の正常性は確認できません。[コマンドと状態の説明(英語)](/reference/configuration/server/#codex-quota-network-diagnostics)を参照してください。 + +`dropCodexSafetyBuffering`: プロバイダーの安全性の適用と拒否応答は変更しません。native `codex.response.metadata.headers` WebSocket メタデータと `/responses/compact` は対象外です。 diff --git a/docs-site/src/content/docs/ko/reference/configuration/server.md b/docs-site/src/content/docs/ko/reference/configuration/server.md index 577379fed9..5f60fc7c6c 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/server.md +++ b/docs-site/src/content/docs/ko/reference/configuration/server.md @@ -13,6 +13,7 @@ description: 리스너, 원격 접근, admission 키, 타임아웃, 저장소, | `hostname?` | `string` | `"127.0.0.1"` | 바인드 주소입니다. 루프백이 아닌 바인드에는 데이터 admission 토큰이 필요하며, `OPENCODEX_API_AUTH_TOKEN` → `OCX_API_TOKEN_FILE` → 설치된 owner-only `service-api-token` 순서로 결정됩니다. 손으로 내보낼 값은 없습니다. [Remote access](#remote-access)를 보세요. | | `proxy?` | `string` | — | 송신용 HTTP(S) 프록시 URL 또는 `${ENV_VAR}`입니다. 해당 변수가 비어 있을 때만 `HTTP_PROXY` / `HTTPS_PROXY`에 적용되며, 루프백은 `NO_PROXY`에 그대로 남습니다. | | `emptyCompletionRetry?` | `boolean` | `false` | 텍스트나 도구 호출이 없는 Responses 턴을, 터미널 이벤트 전에 스트림이 종료된 경우를 포함해 동일한 요청으로 한 번 재시도하도록 선택합니다. 재시도에는 비용이 발생할 수 있습니다. `OCX_EMPTY_COMPLETION_RETRY=0`은 설정을 바꾸지 않고 비활성화하며, combo 및 routed-compaction turn은 제외됩니다. | +| `dropCodexSafetyBuffering?` | `boolean` | `false` | Canonical Codex Responses 응답의 선택적 safety-buffering 헤더 두 개와 SSE 힌트를 제거합니다. 공급자의 안전 정책이나 거절 응답은 바뀌지 않습니다. Native WS 메타데이터와 compact는 제외됩니다. | | `stallTimeoutSec?` | `number` | `300` | 업스트림 데이터가 없을 때 `response.incomplete`가 되기까지의 초 수입니다. 최소 1입니다. | | `connectTimeoutMs?` | `number` | `200000` | 시도별 DNS/TCP/TLS/최종 헤더 기한입니다. 본문 생성 전에 끝납니다. | | `shutdownTimeoutMs?` | `number` | `5000` | 진행 중인 turn을 중단하기 전에 허용하는 정상 종료 드레인 기한입니다. | diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 108d2e0925..3781d5846e 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -15,6 +15,7 @@ runs helper features around provider requests. | `proxy?` | `string` | — | Outbound HTTP(S) proxy URL, `${ENV_VAR}`, or `"auto"`. Applied to `HTTP_PROXY` / `HTTPS_PROXY` only when those variables are unset; loopback remains in `NO_PROXY`. `"auto"` reads the Windows system proxy (WinINET `ProxyEnable`/`ProxyServer`, `https=` then `http=` entry) once at process start and logs the host it chose. On other platforms, or when the system proxy is off, SOCKS-only, or unreadable, it uses direct egress and says so. PAC/WPAD and live proxy changes are not followed; restart the service after changing the system proxy. | | `noProxy?` | `string \| string[]` | — | Hosts that bypass `proxy`, merged with inherited `NO_PROXY` and loopback entries. A string may use comma-separated `NO_PROXY` syntax or `${ENV_VAR}`. | | `emptyCompletionRetry?` | `boolean` | `false` | Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. `OCX_EMPTY_COMPLETION_RETRY=0` disables it without changing config; combo and routed-compaction turns remain excluded. | +| `dropCodexSafetyBuffering?` | `boolean` | `false` | Remove optional client-facing hints from canonical Codex Responses passthrough: the two `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` response headers, `response.metadata` events whose metadata type is `safety_buffering`, and top-level `safety_buffering` fields. Other headers, response data, policy refusals and failures are preserved. This does not disable provider safety enforcement or upstream buffering. Native `codex.response.metadata.headers` WebSocket metadata and `/responses/compact` are outside this filter. | | `stallTimeoutSec?` | `number` | `300` | Seconds without upstream data before `response.incomplete`. Minimum 1. | | `oauthOpenBrowser?` | `boolean` | `true` | Whether a login may open a browser on the machine running the proxy. Absent and `true` both open, so an existing install is unchanged; only an explicit `false` declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy's machine — the login still starts and the URL is still returned and displayed. `POST /api/oauth/login` and `POST /api/codex-auth/login` accept a per-request `openBrowser` boolean that overrides this, and the dashboard exposes the same choice beside the login button. Device-code flows never open a browser either way. | | `connectTimeoutMs?` | `number` | `200000` | Per-attempt DNS/TCP/TLS/final-header deadline; it ends before body generation. | diff --git a/docs-site/src/content/docs/ru/reference/configuration/server.md b/docs-site/src/content/docs/ru/reference/configuration/server.md index 84b3caf183..000a194b5c 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/server.md +++ b/docs-site/src/content/docs/ru/reference/configuration/server.md @@ -14,6 +14,7 @@ description: Listener, удалённый доступ, admission key, тайм | `hostname?` | `string` | `"127.0.0.1"` | Адрес bind'а. Не-loopback bind требует `OPENCODEX_API_AUTH_TOKEN`. | | `proxy?` | `string` | — | URL исходящего HTTP(S)-прокси или `${ENV_VAR}`. Применяется к `HTTP_PROXY` / `HTTPS_PROXY` только когда эти переменные не заданы; loopback всегда остаётся в `NO_PROXY`. | | `emptyCompletionRetry?` | `boolean` | `false` | Явно включает один идентичный повтор Responses, если в turn нет ни текста, ни tool call, включая случай, когда stream завершается до terminal event. Повтор может тарифицироваться. `OCX_EMPTY_COMPLETION_RETRY=0` отключает его без изменения config; combo и routed-compaction turn исключены. | +| `dropCodexSafetyBuffering?` | `boolean` | `false` | Удаляет подсказки Codex safety-buffering из passthrough-ответов Codex Responses: заголовки `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model`, SSE-события `response.metadata` типа `safety_buffering` и поле `safety_buffering` в других SSE-событиях. Codex TUI отображает их как предложение повторить запрос с более быстрой моделью, действие по умолчанию в котором переключает сессию на более слабую модель. Остальные заголовки `x-codex-*` и содержимое других SSE-событий передаются без изменений, кроме удаления этого поля. По умолчанию выключено. | | `stallTimeoutSec?` | `number` | `300` | Секунды без upstream-данных до `response.incomplete`. Минимум 1. | | `connectTimeoutMs?` | `number` | `200000` | Дедлайн одной попытки DNS/TCP/TLS/final-header; он завершается до генерации тела ответа. | | `shutdownTimeoutMs?` | `number` | `5000` | Дедлайн graceful-drain до принудительного прерывания активных turn'ов. | @@ -219,3 +220,5 @@ opencodex. Перед использованием прогоните soak-test ## Сетевая диагностика квоты Codex Поле `quotaRefresh` в строке основного аккаунта Codex описывает получение квоты, а не её остаток или право доступа к модели. Оно может отсутствовать при чтении кэша или если запрос не выполнялся. Используется окружение работающего прокси-сервиса, а не текущего терминала. Если `proxy` не задан, существующее окружение сохраняется; `"auto"` читает только статические настройки прокси Windows при запуске. PAC/WPAD, настройки только SOCKS и изменения во время работы автоматически не учитываются. Успех через TUN сам по себе не подтверждает исправность пути HTTP-прокси. См. [команды и состояния на английском](/reference/configuration/server/#codex-quota-network-diagnostics). + +`dropCodexSafetyBuffering`: не меняет проверки безопасности провайдера или отказы. Native WebSocket `codex.response.metadata.headers` и `/responses/compact` не входят в область фильтра. diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md index 4031f9ff19..8e8050de64 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md @@ -14,6 +14,7 @@ description: 监听、远程访问、准入密钥、超时、存储、侧车、 | `hostname?` | `string` | `"127.0.0.1"` | 绑定地址。非回环绑定需要 `OPENCODEX_API_AUTH_TOKEN`。 | | `proxy?` | `string` | — | 出站 HTTP(S) 代理 URL,或 `${ENV_VAR}`。仅当 `HTTP_PROXY` / `HTTPS_PROXY` 未设置时才会应用;回环地址始终保留在 `NO_PROXY` 中。 | | `emptyCompletionRetry?` | `boolean` | `false` | 显式启用:当 Responses turn 既无文本也无工具调用时,使用相同请求重试一次,包括流在终止事件之前结束的情况。重试可能产生费用。`OCX_EMPTY_COMPLETION_RETRY=0` 可在不修改配置的情况下禁用;combo 与 routed-compaction turn 不参与。 | +| `dropCodexSafetyBuffering?` | `boolean` | `false` | 从 Codex Responses 透传响应中移除 Codex safety-buffering 提示:`x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 响应头、类型为 `safety_buffering` 的 `response.metadata` SSE 事件,以及其他 SSE 事件中的 `safety_buffering` 字段。Codex TUI 会将这些提示显示为“使用更快模型重试”的提示框,其默认操作会把会话切换到较弱的模型。其他 `x-codex-*` 响应头和其他所有 SSE 事件内容均保持不变,但会移除该字段。默认关闭。 | | `stallTimeoutSec?` | `number` | `300` | 在上游没有数据之前可等待的秒数,超过后返回 `response.incomplete`。最小值为 1。 | | `connectTimeoutMs?` | `number` | `200000` | 每次尝试的 DNS/TCP/TLS/最终响应头截止时间;它在正文生成之前结束。 | | `shutdownTimeoutMs?` | `number` | `5000` | 优雅停机截止时间,超过后会中止仍在进行中的请求。 | @@ -185,3 +186,5 @@ Anthropic OAuth 侧车会复用 opencodex 现有的 Claude Code OAuth 指纹。 ## Codex 额度网络诊断 主 Codex 账户行中的 `quotaRefresh` 描述额度查询结果,并不代表剩余额度或模型访问权限。读取缓存或未执行查询时,该字段可能省略。查询使用正在运行的代理服务的环境,而不是当前终端的环境。未设置 `proxy` 时保留现有环境;`"auto"` 只在启动时读取 Windows 静态代理设置,不自动处理 PAC/WPAD、仅 SOCKS 的设置或运行中的更改。TUN 测试成功并不能单独证明 HTTP 代理路径正常。命令和状态说明见[英文网络诊断章节](/reference/configuration/server/#codex-quota-network-diagnostics)。 + +`dropCodexSafetyBuffering`: 不会改变供应商安全策略或拒绝响应。原生 WebSocket `codex.response.metadata.headers` 和 `/responses/compact` 不在过滤范围内。 diff --git a/src/config.ts b/src/config.ts index 5e81a7e5f1..a4fa6d7c02 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1253,6 +1253,8 @@ const configSchema = z.object({ configRebaseProvenance: z.unknown().optional(), // A retry can be billable, so absence and malformed hand edits both stay off. emptyCompletionRetry: z.boolean().optional().catch(false), + // Header suppression changes what Codex sees, so absence and malformed edits stay off. + dropCodexSafetyBuffering: z.boolean().optional().catch(false), // A malformed hand edit must not silently stop opening the browser: fall back // to undefined, which resolves to the historical auto-open behavior. oauthOpenBrowser: z.boolean().optional().catch(undefined), @@ -2858,6 +2860,14 @@ function emptyCompletionRetryError(value: unknown): string | null { return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted"; } +function dropCodexSafetyBufferingError(value: unknown): string | null { + const raw = rawConfigRecord(value); + if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null; + const enabled = raw.dropCodexSafetyBuffering; + if (enabled === undefined || typeof enabled === "boolean") return null; + return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted"; +} + function oauthOpenBrowserError(value: unknown): string | null { const raw = rawConfigRecord(value); if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null; @@ -3001,6 +3011,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx ?? codexQuotaAutoRefreshError(value) ?? codexAccountPickerEnabledError(value) ?? emptyCompletionRetryError(value) + ?? dropCodexSafetyBufferingError(value) ?? oauthOpenBrowserError(value) ?? runtimeRoleError(value) ?? remoteGuiConfigError(value) @@ -4022,6 +4033,7 @@ export function getDefaultConfig(): OcxConfig { return { port: 10100, emptyCompletionRetry: false, + dropCodexSafetyBuffering: false, fastRows: true, managementUsageMaxReadBytes: 64 * 1024 * 1024, appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024), diff --git a/src/server/index.ts b/src/server/index.ts index 2cb11c1e9f..3e3b89b8d9 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -150,6 +150,7 @@ import { } from "./relay"; export { consumeForInspection, + codexSafetyBufferingFilterOptions, relaySseWithFailedTail, relaySseWithHeartbeat, relayWithAbort, diff --git a/src/server/relay-eager.ts b/src/server/relay-eager.ts index 7844c65886..8439aea85d 100644 --- a/src/server/relay-eager.ts +++ b/src/server/relay-eager.ts @@ -26,6 +26,7 @@ import { adapterEofIncompleteFrame, + type CodexSafetyBufferingFilterOptions, createSseTerminalOutputBoundary, doneFrame, failedTailFrame, @@ -84,6 +85,8 @@ export type EagerRelayOptions = { postCancelDrainBytes?: number; /** Last known upstream failure to preserve when EOF would otherwise become adapter_eof. */ upstreamError?: string; + /** Optional client-facing hint policy; inspection retains original frames. */ + terminalBoundary?: CodexSafetyBufferingFilterOptions; /** Injectable clock for tests. */ now?: () => number; }; @@ -114,7 +117,7 @@ export function relaySseEagerBounded( const terminalEncoder = new TextEncoder(); const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder); const terminalSentinel = doneFrame(terminalEncoder); - const terminalBoundary = createSseTerminalOutputBoundary(); + const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary); const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined); const encodeFailedTail = (error: unknown): Uint8Array | null => { diff --git a/src/server/relay.ts b/src/server/relay.ts index a483d88f20..f480ace68a 100644 --- a/src/server/relay.ts +++ b/src/server/relay.ts @@ -183,7 +183,10 @@ export type SseTerminalOutputBoundary = { * terminal, and drops every later block/byte. A premature [DONE] is held until * a terminal arrives so clean EOF can synthesize one terminal and one sentinel. */ -export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { +export function createSseTerminalOutputBoundary( + options?: CodexSafetyBufferingFilterOptions, +): SseTerminalOutputBoundary { + const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; const decoder = new TextDecoder(); const encoder = new TextEncoder(); const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES); @@ -207,15 +210,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { // behind EOF, so its log context cannot determine the outgoing terminal. const message = boundedBareUpstreamErrorMessage(parsed); if (message !== undefined) upstreamError = message; + const safetyBuffering = dropSafetyBuffering && parsed !== undefined + ? codexSafetyBufferingBlockAction(parsed) : "keep"; + if (safetyBuffering === "drop") continue; const policyError = parsed !== undefined && isPolicyRewriteType(parsed) ? cyberPolicyTerminalError(parsed) : undefined; - const outboundBlock = policyError - ? encoder.encode(rewritePolicyTerminalBlock( - decoder.decode(frame.block), - policyFailurePayload(policyError, parsed), - )) + const policyPayload = policyError ? policyFailurePayload(policyError, parsed) : undefined; + let outboundBlock = policyPayload !== undefined + ? encoder.encode(rewritePolicyTerminalBlock(decoder.decode(frame.block), policyPayload)) : frame.block; + if (safetyBuffering === "strip") { + outboundBlock = encoder.encode(stripCodexSafetyBufferingField( + decoder.decode(outboundBlock), + policyPayload !== undefined ? parseSsePayload(policyPayload) : parsed, + )); + } if (isDone) { done = true; if (responsesTerminal) { @@ -287,11 +297,11 @@ export function relaySseWithFailedTail( body: ReadableStream, upstream: AbortController, onClientGone?: (reason?: unknown) => void, - opts?: { upstreamError?: string }, + opts?: { upstreamError?: string; terminalBoundary?: CodexSafetyBufferingFilterOptions }, ): ReadableStream { const reader = body.getReader(); const encoder = new TextEncoder(); - const terminalBoundary = createSseTerminalOutputBoundary(); + const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary); let closed = false; const relayChunk = ( controller: ReadableStreamDefaultController, @@ -468,6 +478,29 @@ function isPolicyRewriteType(parsed: unknown): boolean { return type === "response.failed" || type === "response.incomplete" || type === "error"; } +/** + * Codex emits its safety-buffering hint in the SSE body as well as in headers: + * a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a + * `safety_buffering` field on another event. The metadata event is dropped whole; + * the field is stripped so the carrying event is otherwise relayed unchanged. + */ +function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" { + const root = asJsonRecord(parsed); + if (!root) return "keep"; + if (root.type === "response.metadata") { + const metadata = asJsonRecord(root.metadata); + if (metadata?.type === "safety_buffering") return "drop"; + } + return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep"; +} + +function stripCodexSafetyBufferingField(block: string, parsed: unknown): string { + const root = asJsonRecord(parsed); + if (!root) return block; + const { safety_buffering: _safetyBuffering, ...rest } = root; + return replaceSseDataPayload(block, JSON.stringify(rest)); +} + function rewritePolicyTerminalBlock(block: string, payload: string): string { const newline = block.includes("\r\n") ? "\r\n" : "\n"; const rewritten = replaceSseDataPayload(block, payload); @@ -1461,7 +1494,31 @@ export function consumeForResponseLogMetadata( * body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough. * Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim. */ -export function sanitizePassthroughHeaders(upstream: Headers): Headers { +export const CODEX_SAFETY_BUFFERING_HEADERS = [ + "x-codex-safety-buffering-enabled", + "x-codex-safety-buffering-faster-model", +] as const; + +const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet = new Set(CODEX_SAFETY_BUFFERING_HEADERS); + +export interface CodexSafetyBufferingFilterOptions { + /** + * Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response + * headers and the `safety_buffering` SSE metadata event / field. Absent and + * `false` relay everything unchanged. + */ + dropCodexSafetyBuffering?: boolean; +} + +/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */ +export function codexSafetyBufferingFilterOptions( + config: { dropCodexSafetyBuffering?: boolean }, +): CodexSafetyBufferingFilterOptions { + return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true }; +} + +export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers { + const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; const DROP = new Set([ "content-encoding", "content-length", @@ -1478,7 +1535,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers { ]); const out = new Headers(); upstream.forEach((value, key) => { - if (!DROP.has(key.toLowerCase())) out.set(key, value); + const lower = key.toLowerCase(); + if (DROP.has(lower)) return; + if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return; + out.set(key, value); }); return out; } diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index a10d97d282..cd1c93abda 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -342,6 +342,7 @@ import { markEagerRelaySseResponse, markNativePassthroughSseResponse, relaySseWithFailedTail, + codexSafetyBufferingFilterOptions, relayWithAbort, sanitizePassthroughHeaders, } from "../relay"; @@ -4745,6 +4746,9 @@ async function handleResponsesInner( let hostAdmissionLease = pendingHostAdmissionLease; pendingHostAdmissionLease = null; try { + const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) + ? codexSafetyBufferingFilterOptions(config) + : undefined; const imageGenCallAliases = route.provider.authMode === "forward" ? new Map() : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget); @@ -5818,7 +5822,7 @@ async function handleResponsesInner( } break; } - const headers = sanitizePassthroughHeaders(upstreamResponse.headers); + const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); const resolvedModel = headers.get("openai-model")?.trim(); if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; if (isUsageDebugEnabled()) { @@ -5907,7 +5911,7 @@ async function handleResponsesInner( return new Response(upstreamResponse.body, { status: upstreamResponse.status, statusText: upstreamResponse.statusText, - headers: sanitizePassthroughHeaders(upstreamResponse.headers), + headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions), }); } if (!upstreamResponse.ok) { @@ -6173,6 +6177,7 @@ async function handleResponsesInner( onDone: () => unregisterTurn(turnAc), }, { clientGoneSignal: options.abortSignal, + terminalBoundary: codexSafetyBufferingOptions, ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}), ...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }), }); @@ -6264,7 +6269,7 @@ async function handleResponsesInner( responseCompletionCancelled = true; clientGone.abort(reason); }, - { upstreamError: logCtx.upstreamError }, + { upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions }, ); return markNativePassthroughSseResponse(new Response(clientBody, { status: upstreamResponse.status, @@ -6395,7 +6400,7 @@ async function handleResponsesInner( } throw error; } - const sseHeaders = sanitizePassthroughHeaders(headers); + const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions); sseHeaders.set("content-type", "text/event-stream"); sseHeaders.set("cache-control", "no-store"); return new Response(stream, { diff --git a/src/types/config.ts b/src/types/config.ts index acfa35f868..6248cccd2c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -374,6 +374,8 @@ export interface OcxConfig { privacy?: OcxPrivacyConfig; /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */ emptyCompletionRetry?: boolean; + /** Suppress allowlisted client-facing Codex transport hints; provider enforcement is unchanged. */ + dropCodexSafetyBuffering?: boolean; /** * Whether a login may open a browser on the machine running the proxy. * diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index b0fab66633..6487e42d9d 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -63,3 +63,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/catalog.md b/structure/catalog.md index bf7ef00d38..78a3275a28 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -271,3 +271,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](transports/responses.md). diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 36511b6f00..fe9edd9dd7 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -82,3 +82,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/config.md b/structure/config.md index 48a29a7817..6ae3821141 100644 --- a/structure/config.md +++ b/structure/config.md @@ -195,3 +195,7 @@ Client connection metadata stores a stable `apiKeyId` and a non-secret rotation Codex display-cache expiry, retained main-policy evidence, and reset history follow the [quota cache contract](providers/openai-tiers.md#quota-cache-and-short-window-history). + +`dropCodexSafetyBuffering` is an optional boolean, default false. Invalid API candidates reject; +malformed persisted values stay disabled. It controls only the allowlisted client-output hints +described in [Responses transport](transports/responses.md), not upstream policy or model selection. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 01d0cd4b0f..74f02d0f07 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -76,3 +76,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 45fe1c11e7..40aadf8b70 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -96,3 +96,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 952e9bd082..fa672aba9e 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -530,3 +530,6 @@ Missing, invalid or expired evidence is unknown. `gui/src/pages/Combos.tsx` wake expiry, including a deadline crossed before effects run, rechecks activation and visibility, and refreshes quota with Combo data while preserving drafts. Each successful quota snapshot also advances the observation clock, so a retained older row cannot defer evaluation of a fresh row. + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](transports/responses.md). diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index b06c1ce393..cadbab667c 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -308,3 +308,6 @@ The Combo guides describe the distinction between display quota and single-crede The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 21bafe5b3b..8977ebb700 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -139,3 +139,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/overview.md b/structure/overview.md index da3f2dc473..c81406dd7a 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -106,3 +106,6 @@ would pass while the rule was violated. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](transports/responses.md). diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index b8d1d56675..c5136fc3c9 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -398,3 +398,6 @@ model settings, and noncanonical `openai` rows never receive that recovery path. `GET /api/codex-auth/accounts?refresh=1` treats missing main credentials, HTTP 401, and allowlisted terminal 403 codes as `needsReauth`; generic permission failures remain non-terminal, and a successful main usage refresh clears the runtime mark. + +Canonical forwarding alone can apply the optional client-output safety-buffering hint filter; +API-key and custom forward destinations preserve their metadata. See [Responses transport](../transports/responses.md). diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5c497d4084..1162967a65 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -62,3 +62,6 @@ Account-scoped OAuth quota remains display evidence for provider-level Combo sel The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/runtime.md b/structure/runtime.md index 6d733bf8b5..4f78775ea5 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -212,3 +212,6 @@ cooldowns and response-driven retry remain authoritative. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](transports/responses.md). diff --git a/structure/subagents.md b/structure/subagents.md index 662bc857d3..3ba557bc10 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -207,3 +207,6 @@ Provider-level Combo eligibility uses explicit inference evidence for the curren The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](transports/responses.md). diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index f0348c1cdd..a514981afd 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -67,3 +67,6 @@ Quota publication distinguishes display reports from explicitly supplied inferen The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 5624a2e04e..5b56e3b57f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -520,3 +520,12 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +## Optional client transport hints + +`dropCodexSafetyBuffering` defaults to false. Canonical OpenAI forward Responses can remove only +the two safety-buffering response headers, matching response.metadata events and top-level +safety_buffering fields. Pull/eager client output boundaries compose this with policy failure +normalization; refusal/error semantics, retryability, cancellation and captured EOF errors remain +intact. Internal inspection observes original upstream frames. Native codex.response.metadata.headers +WebSocket metadata and compact are excluded. This does not disable upstream safety enforcement. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index ae20c7c7ea..a765b5e7a2 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -196,3 +196,6 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Optional Codex transport-hint suppression is scoped to canonical Responses client output; +its defaults and exclusions are owned by [Responses transport](../transports/responses.md). diff --git a/tests/responses/openai-responses-passthrough.test.ts b/tests/responses/openai-responses-passthrough.test.ts index d5a49fd39e..f6b411ec4e 100644 --- a/tests/responses/openai-responses-passthrough.test.ts +++ b/tests/responses/openai-responses-passthrough.test.ts @@ -335,6 +335,55 @@ test("canonical forward providers normalize trailing slashes and let the pool ov expect(request.headers["chatgpt-account-id"]).toBe("runtime-account"); }); +test("noncanonical Responses preserves provider-owned safety-buffering hints", async () => { + const upstream = [ + 'event: response.created\ndata: {"type":"response.created","response":{"id":"resp_custom"},"safety_buffering":{"provider_owned":true}}\n\n', + 'event: response.metadata\ndata: {"type":"response.metadata","metadata":{"type":"safety_buffering","provider_owned":true}}\n\n', + 'event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_custom","status":"completed","output":[]}}\n\n', + "data: [DONE]\n\n", + ].join(""); + const savedFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response(upstream, { headers: { + "content-type": "text/event-stream", + "x-codex-safety-buffering-enabled": "provider-owned", + "x-codex-safety-buffering-faster-model": "provider-model", + } })) as typeof fetch; + try { + for (const providerConfig of [ + { + adapter: "openai-responses", + baseUrl: "https://fixture.test/v1", + authMode: "key" as const, + apiKey: "fixture-key", + }, + { + adapter: "openai-responses", + baseUrl: "https://fixture.test/v1", + authMode: "forward" as const, + headers: { authorization: "Bearer provider-static" }, + }, + ]) { + const config = { + port: 0, + defaultProvider: "fixture", + dropCodexSafetyBuffering: true, + providers: { fixture: providerConfig }, + } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "fixture/model", stream: true, input: "ping" }), + }), config, { model: "", provider: "" }); + + expect(response.headers.get("x-codex-safety-buffering-enabled")).toBe("provider-owned"); + expect(response.headers.get("x-codex-safety-buffering-faster-model")).toBe("provider-model"); + expect(await response.text()).toBe(upstream); + } + } finally { + globalThis.fetch = savedFetch; + } +}); + test("noncanonical pool-required providers use only their configured static credentials", () => { const adapter = createResponsesPassthroughAdapter({ adapter: "openai-responses", @@ -4533,3 +4582,31 @@ describe("raw usage passthrough on the forward path (#41980 parity, #37138 adjac } }); }); + + +test("canonical Responses hint suppression is opt-in at the request boundary", async () => { + const savedFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response([ + 'data: {"type":"response.created","response":{"id":"resp_hint"},"safety_buffering":true}\n\n', + 'data: {"type":"response.metadata","metadata":{"type":"safety_buffering"}}\n\n', + 'data: {"type":"response.completed","response":{"id":"resp_hint","status":"completed","output":[]}}\n\n', + ].join(""), { headers: { "content-type": "text/event-stream", + "x-codex-safety-buffering-enabled": "true", "x-codex-safety-buffering-faster-model": "fixture-model", "x-codex-turn-id": "fixture-turn" } })) as typeof fetch; + try { + for (const dropCodexSafetyBuffering of [undefined, false, true]) { + const config = { port: 0, dropCodexSafetyBuffering, providers: { openai: { + ...provider, codexAccountMode: "direct", upstreamWebsocket: false, + } } } as OcxConfig; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json", authorization: "Bearer fixture-forward-token" }, + body: JSON.stringify({ model: "openai/gpt-5.6-sol", input: "ping", stream: true }), + }), config, { model: "", provider: "" }); + expect(response.status).toBe(200); + expect(response.headers.has("x-codex-safety-buffering-enabled")).toBe(dropCodexSafetyBuffering !== true); + expect(response.headers.get("x-codex-turn-id")).toBe("fixture-turn"); + const text = await response.text(); + expect(text.includes("safety_buffering")).toBe(dropCodexSafetyBuffering !== true); + expect(text).toContain("response.completed"); + } + } finally { globalThis.fetch = savedFetch; } +}); diff --git a/tests/responses/passthrough-abort.test.ts b/tests/responses/passthrough-abort.test.ts index 46100c6902..913da3eadd 100644 --- a/tests/responses/passthrough-abort.test.ts +++ b/tests/responses/passthrough-abort.test.ts @@ -79,7 +79,7 @@ describe("passthrough relayWithAbort (RC2, passthrough path)", () => { expect(sseBranch).toContain("rewriteBlocks: clientBlockRewrite"); // Elsewhere the failed-tail relay converts mid-stream resets into a clean response.failed. expect(sseBranch).toMatch( - /relaySseWithFailedTail\(\s*rewrittenBody,\s*upstream,\s*reason\s*=>\s*\{\s*responseCompletionCancelled\s*=\s*true;\s*clientGone\.abort\(reason\);\s*\},\s*\{\s*upstreamError:\s*logCtx\.upstreamError\s*\},\s*\)/, + /relaySseWithFailedTail\(\s*rewrittenBody,\s*upstream,\s*reason\s*=>\s*\{\s*responseCompletionCancelled\s*=\s*true;\s*clientGone\.abort\(reason\);\s*\},\s*\{\s*upstreamError:\s*logCtx\.upstreamError,\s*terminalBoundary:\s*codexSafetyBufferingOptions\s*\},\s*\)/, ); expect(sseBranch).toContain("new Response(clientBody"); expect(sseBranch).toContain("markNativePassthroughSseResponse"); diff --git a/tests/responses/passthrough-headers.test.ts b/tests/responses/passthrough-headers.test.ts index 8018e77906..2cd5261609 100644 --- a/tests/responses/passthrough-headers.test.ts +++ b/tests/responses/passthrough-headers.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { sanitizePassthroughHeaders } from "../../src/server"; +import { codexSafetyBufferingFilterOptions, sanitizePassthroughHeaders } from "../../src/server"; +import { createSseTerminalOutputBoundary } from "../../src/server/relay"; describe("passthrough header sanitization (RC5 / F4)", () => { test("content-type: text/event-stream survives sanitization", () => { @@ -43,3 +44,75 @@ describe("passthrough header sanitization (RC5 / F4)", () => { expect(sanitized.get("content-type")).toBe("text/event-stream"); }); }); + +describe("codex safety-buffering hint headers", () => { + const upstream = () => new Headers({ + "content-type": "text/event-stream", + "x-codex-safety-buffering-enabled": "true", + "X-Codex-Safety-Buffering-Faster-Model": "gpt-5.6-luna", + "x-codex-primary-used-percent": "12", + "openai-model": "gpt-6-astra", + }); + + test("forwarded verbatim by default and when the option is off", () => { + for (const options of [undefined, {}, { dropCodexSafetyBuffering: false }]) { + const sanitized = sanitizePassthroughHeaders(upstream(), options); + expect(sanitized.get("x-codex-safety-buffering-enabled")).toBe("true"); + expect(sanitized.get("x-codex-safety-buffering-faster-model")).toBe("gpt-5.6-luna"); + } + }); + + test("dropped case-insensitively when opted in, other x-codex headers survive", () => { + const sanitized = sanitizePassthroughHeaders(upstream(), { dropCodexSafetyBuffering: true }); + expect(sanitized.has("x-codex-safety-buffering-enabled")).toBe(false); + expect(sanitized.has("x-codex-safety-buffering-faster-model")).toBe(false); + expect(sanitized.get("x-codex-primary-used-percent")).toBe("12"); + expect(sanitized.get("openai-model")).toBe("gpt-6-astra"); + expect(sanitized.get("content-type")).toBe("text/event-stream"); + }); + + test("codexSafetyBufferingFilterOptions only enables the drop on an explicit true", () => { + expect(codexSafetyBufferingFilterOptions({})).toEqual({ dropCodexSafetyBuffering: false }); + expect(codexSafetyBufferingFilterOptions({ dropCodexSafetyBuffering: false })) + .toEqual({ dropCodexSafetyBuffering: false }); + expect(codexSafetyBufferingFilterOptions({ dropCodexSafetyBuffering: true })) + .toEqual({ dropCodexSafetyBuffering: true }); + }); +}); + +describe("Codex safety-buffering SSE hints at the client output boundary", () => { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const frames = [ + 'event: response.created\ndata: {"type":"response.created","response":{"id":"resp_1"},"safety_buffering":{"retry_model":"gpt-5.6-luna"}}\n\n', + 'event: response.metadata\ndata: {"type":"response.metadata","metadata":{"type":"safety_buffering","retry_model":"gpt-5.6-luna"}}\n\n', + 'event: response.metadata\ndata: {"type":"response.metadata","metadata":{"type":"other","turn":1}}\n\n', + 'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"hi"}\n\n', + 'event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_1","status":"completed"}}\n\n', + ]; + const relay = (options?: { dropCodexSafetyBuffering?: boolean }): string => { + const boundary = createSseTerminalOutputBoundary(options); + let out = ""; + for (const frame of frames) out += decoder.decode(boundary.feed(encoder.encode(frame))); + out += decoder.decode(boundary.finish()); + boundary.dispose(); + return out; + }; + + test("relayed verbatim by default and when the option is off", () => { + for (const options of [undefined, {}, { dropCodexSafetyBuffering: false }]) { + expect(relay(options)).toBe(frames.join("")); + } + }); + + test("metadata event dropped and field stripped when opted in, other events untouched", () => { + const out = relay({ dropCodexSafetyBuffering: true }); + expect(out).not.toContain("safety_buffering"); + expect(out).not.toContain("gpt-5.6-luna"); + expect(out).toContain('data: {"type":"response.created","response":{"id":"resp_1"}}'); + expect(out).toContain(frames[2]); + expect(out).toContain(frames[3]); + expect(out).toContain(frames[4]); + expect(out.match(/^event: /gm)).toHaveLength(4); + }); +}); diff --git a/tests/responses/sse-failed-tail.test.ts b/tests/responses/sse-failed-tail.test.ts index f21301ffb7..0a38424f42 100644 --- a/tests/responses/sse-failed-tail.test.ts +++ b/tests/responses/sse-failed-tail.test.ts @@ -422,3 +422,43 @@ describe("relaySseWithFailedTail", () => { expect(out).not.toContain("event: response.failed"); }); }); + + +describe("optional Codex hint filtering preserves relay semantics", () => { + for (const eager of [false, true]) { + const relay = (chunks: string[], upstreamError?: string) => eager + ? relaySseEagerBounded(sourceStream(chunks), new AbortController(), parityHooks, + { upstreamError, terminalBoundary: { dropCodexSafetyBuffering: true } }) + : relaySseWithFailedTail(sourceStream(chunks), new AbortController(), undefined, + { upstreamError, terminalBoundary: { dropCodexSafetyBuffering: true } }); + test(`policy failure plus hint is composed, eager=${eager}`, async () => { + const frame = `event: error\r\ndata: ${JSON.stringify({ type: "error", safety_buffering: { enabled: true }, + error: { code: "cyber_policy", message: "blocked by upstream policy", type: "invalid_request_error" } })}\r\n\r\n`; + const text = await drain(relay([frame.slice(0, 19), frame.slice(19)])); + expect(text).not.toContain("safety_buffering"); + expect(text).toContain('"type":"response.failed"'); + expect(text).toContain('"code":"cyber_policy"'); + expect(text).toContain('"retryable":false'); + expect(text.match(/data: \[DONE\]/g)).toHaveLength(1); + }); + test(`metadata removal retains other frames and one terminal, eager=${eager}`, async () => { + const metadata = 'data: {"type":"response.metadata","metadata":{"type":"safety_buffering"}}\n\n'; + const other = 'data: {"type":"codex.response.metadata","headers":{"x-codex-safety-buffering-enabled":"true"}}\n\n'; + const malformed = 'data: {malformed}\n\n'; + const terminal = 'data: {"type":"response.completed","response":{"status":"completed"},"safety_buffering":true}\n\ndata: [DONE]\n\n'; + const text = await drain(relay([metadata.slice(0, 7), metadata.slice(7), other, malformed, terminal])); + expect(text).not.toContain('"type":"safety_buffering"'); + expect(text).not.toContain('"safety_buffering":true'); + expect(text).toContain(other); + expect(text).toContain(malformed); + expect(text).toContain('"type":"response.completed"'); + expect(text.match(/data: \[DONE\]/g)).toHaveLength(1); + }); + test(`hint-only EOF preserves captured error fallback, eager=${eager}`, async () => { + const text = await drain(relay(['data: {"type":"response.metadata","metadata":{"type":"safety_buffering"}}\n\n'], "provider unavailable")); + expect(text).toContain("provider unavailable"); + expect(text).not.toContain("adapter_eof"); + expect(text).not.toContain("safety_buffering"); + }); + } +}); diff --git a/tests/server/config.test.ts b/tests/server/config.test.ts index b096b5857c..13cc92f708 100644 --- a/tests/server/config.test.ts +++ b/tests/server/config.test.ts @@ -777,6 +777,19 @@ describe("opencodex config defaults", () => { }); }); + test("codex safety-buffering header drop is an explicit top-level opt-in", () => { + const defaults = getDefaultConfig(); + expect(defaults.dropCodexSafetyBuffering).toBe(false); + expect(validateConfigCandidate({ ...defaults, dropCodexSafetyBuffering: true })).toMatchObject({ + ok: true, + config: { dropCodexSafetyBuffering: true }, + }); + expect(validateConfigCandidate({ ...defaults, dropCodexSafetyBuffering: "yes" })).toMatchObject({ + ok: false, + error: expect.stringContaining("dropCodexSafetyBuffering"), + }); + }); + test("usage and MCP config overrides change the effective bound while defaults remain compatible", () => { const defaults = getDefaultConfig(); expect(defaults.managementUsageMaxReadBytes).toBe(64 * 1024 * 1024); From e966c9b31af5ccf40b0c9bff2c36624a57a1b365 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:36:58 +0900 Subject: [PATCH 05/21] docs: plan thinking presentation and transport carries --- .../260912_thinking_contract/000_plan.md | 31 ++ .../260912_thinking_contract/001_sources.md | 7 + .../010_presentation.md | 316 +++++++++++++++++ .../020_transport_hint.md | 319 ++++++++++++++++++ .../260912_thinking_contract/030_spark.md | 66 ++++ .../260912_thinking_contract/040_delivery.md | 7 + 6 files changed, 746 insertions(+) create mode 100644 devlog/_plan/260912_thinking_contract/000_plan.md create mode 100644 devlog/_plan/260912_thinking_contract/001_sources.md create mode 100644 devlog/_plan/260912_thinking_contract/010_presentation.md create mode 100644 devlog/_plan/260912_thinking_contract/020_transport_hint.md create mode 100644 devlog/_plan/260912_thinking_contract/030_spark.md create mode 100644 devlog/_plan/260912_thinking_contract/040_delivery.md diff --git a/devlog/_plan/260912_thinking_contract/000_plan.md b/devlog/_plan/260912_thinking_contract/000_plan.md new file mode 100644 index 0000000000..86a87f9d36 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/000_plan.md @@ -0,0 +1,31 @@ +# Preserve reasoning provenance and transport intent + +Readers: maintainers choosing whether to integrate the thinking lane. Raw reasoning must remain content, while provider-authored summaries can be displayed under an explicit provider default. The plan reconciles #4301 and #4287, separately reviews #3652 hint suppression, and carries #4130 Spark compatibility without retirement. + +Loop: satisfy-spec HOTL, triggered by authorized thinking-lane delivery. Goal: reviewable carry PRs and final-head hosted CI. Non-goals: merges, closure of source PRs, retirement #4334, releases, user service/config changes, other worktrees. All local product suites/build/typecheck/install are NOT RUN by instruction. Only available existing credentials/tools are used; no user token/time/agent ceiling was set. Stop: every source PR has a justified disposition and every delivered branch has exact-head hosted CI evidence. Outcomes: DONE on evidence, HOLD/NEEDS_HUMAN on explicit unresolved acceptance, never fake green. Escalation: real tool denial or requirement beyond scope; main reclaims after two distinct reviewer failures. Native architect selector is unavailable; inherited independent design review and reflection follow the user instruction, with a separate A audit. + +## Dependency map + +| Cycle | Artifact | Result | +| --- | --- | --- | +| roadmap | this file and all decade docs | docs-only plan lock | +| presentation | 010_presentation.md | raw/summary contract and provider opt-in | +| hint | 020_transport_hint.md | independent transport-hint disposition/carry | +| spark | 030_spark.md | independent Spark Lite carry | +| delivery | 040_delivery.md | final heads, review closure and hosted CI | + +Presentation combines two conflicting source proposals into one contract. Hint and Spark are independent and receive ordinary dev-based PRs, not artificial stack dependencies. Final review consumes all branches. No GitHub native stacks are requested. + +## Evidence and owner map + +Baseline origin/dev: 69e3dcda755a52feb1327edad6c8ea6cefd6e871. Source PR heads: #4301 5d6d1862a11da6e4d0c04eb7f35f9f48ae1285fd; #4287 fe13bdb7bf8403a2a2cdb10f258a68b649177953; #3652 13fb263778e9036e66ae86d41e29f9f47bbbed92; #4130 5d56f5461ea3d18668b85f6bb0d8a523920f2536. All open when inspected. Original authors: Robin Bially, yxr1995-maker, itismyfield, luvs01; exact Git trailers will be read from original commits before carrying. + +Current owners: src/bridge.ts:663 raw-reasoning finalization; src/adapters/google.ts:571 shared part classifier; src/server/responses/core.ts:2490 final-route normalization; src/responses/parser.ts:543 summary omission policy; src/types/request.ts:310 AdapterEvent. Reuse these boundaries; no new event enum or generic service layer. Structure INDEX maps shared areas to topical documents; main contracts are providers/chat-compat.md, providers/google.md, transports/responses.md and config.md with references from affected area owners. + +Verification: git diff --check was run at baseline and exited 0, checking diff whitespace only. GitHub ci.yml workflow_dispatch lane=all reads checkout source, typechecks, runs product suites and cross-platform jobs; NOT RUN locally. Every conditional scenario is named in decade docs and must be asserted in committed regression tests. Source inspection is not runtime proof. + +## Cycle records + +Roadmap P: requirements/source inspection and independent design review in progress. No product patch applied. + +Roadmap B: locked amended contract after independent A PASS and both design reflections ALIGNED. Product implementation starts in the next cycle. diff --git a/devlog/_plan/260912_thinking_contract/001_sources.md b/devlog/_plan/260912_thinking_contract/001_sources.md new file mode 100644 index 0000000000..f05813eae8 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/001_sources.md @@ -0,0 +1,7 @@ +# Source decisions + +Public PR diffs and latest comments are the source proposal evidence. #4130's September 11 corrections pin Lite on for nonempty additional_tools bodies and off otherwise; adopting the earlier unconditional false version loses tools. #4334 is an explicit retirement HOLD and is not carried. + +#4301 removes automatic content-to-summary conversion. #4287 tests raw DeepSeek content as a visible summary; that expectation conflicts with provenance and will be replaced, not adopted. Google thought-summary API documentation distinguishes summaries from opaque thought signatures: https://ai.google.dev/gemini-api/docs/generate-content/thinking (opened 2026-09-12). CCA generationConfig/includeThoughts behavior is contributor probe evidence, not a newly performed live-service probe. + +Searches used: reasoning_raw_delta, thinking_delta, hideThinkingSummary, googlePartTextEvent, preserveReasoningContent, and the four PR numbers. Existing bridge event types can distinguish raw content and summary without a new enum. No-code/config-only options cannot repair the existing mislabeled content; raw rewrite deletion plus existing boundaries is the smallest change. diff --git a/devlog/_plan/260912_thinking_contract/010_presentation.md b/devlog/_plan/260912_thinking_contract/010_presentation.md new file mode 100644 index 0000000000..e39e9c0bfe --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/010_presentation.md @@ -0,0 +1,316 @@ +# Presentation contract + +Class C4 public protocol contract. Depends on roadmap lock. MODIFY src/bridge.ts: closeCurrentRawReasoning and reasoning_raw_delta emit response.reasoning_text.delta/done with content_index:0; final items use summary:[] and content:[{type:reasoning_text,text}]. buildResponseJSONWithBudget mirrors this. Keep hidden txt-only replay envelopes intact. DELETE src/server/responses-reasoning-summary-rewrite.ts and its obsolete unit test; MODIFY core.ts to remove imports and SSE/JSON content-to-summary rewrites. MODIFY both layout manifests to remove that test. Adopt the exact #4301 hunks below except reporter video and historical verification record. + +MODIFY provider.ts, registry.ts, derive.ts, router.ts and auth-cors.ts to carry showThinkingSummary boolean (preserve explicit false). Seed only google-antigravity true. Creation: provider config/registry; serialization: providerConfigSeed and deriveKeyLoginMap; deserialization: config provider passthrough and management field policy; consumers: routedProviderConfig, final-route normalization, Google request builder. No new enum. + +MODIFY core.ts final-route normalization: apply provider default only when original reasoning.summary is omitted, never explicit none; recompute on each final route so fallback cannot inherit another provider default. Provider opt-in authorizes summary display, not raw-to-summary conversion. + +MODIFY google.ts shared part classifier to use existing thinking_delta only for Gemini thought summaries under verified Gemini model provenance; CCA Claude/gpt-oss thought text remains reasoning_raw_delta. Persist request-local Gemini identity using existing adapter state, used by both stream and buffered classifier calls. includeThoughts stays provider-opted, Gemini-only, non-image and explicit-hide aware. MODIFY google-wire-compiler.ts to retain only boolean true includeThoughts, independently of thinkingLevel. Do not claim raw text is an actual summary. + +MODIFY the #4287 end-to-end fixture: raw DeepSeek content remains content with empty summary even under provider opt-in; actual CCA Gemini thought parts use summary; omitted vs none vs auto, explicit provider false, saved-row enrichment, fallback reset, streaming/buffered paths. Extend existing Google tests and bridge raw tests; both layout manifests register responses-show-thinking-summary.test.ts. Update English providers docs and structure owners, keeping locale statements consistent. Source tests are authored but run only by hosted CI. + +Acceptance: raw event fixture => content delta and no summary delta; actual Gemini summary fixture => summary only when requested/provider-opted; explicit none => no synthesized summary and no includeThoughts request; false/unknown provider => no opt-in; fallback to unopted route => hidden behavior reset; replay envelope decodes same raw text and tool continuation remains valid; native Responses mixed content/summary remains byte-semantically native. No model prose synthesizer is introduced. + +## Source patch blueprint + +```diff +diff --git a/src/bridge.ts b/src/bridge.ts +index 20e7c3fe09..bc90f35b94 100644 +--- a/src/bridge.ts ++++ b/src/bridge.ts +@@ -663,16 +663,13 @@ export function bridgeToResponsesSSE( + const closeCurrentRawReasoning = () => { + if (!currentRawReasoning) return; + rawReasoningForNextToolCall = currentRawReasoning.text; +- emit("response.reasoning_summary_text.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text, +- }); +- emit("response.reasoning_summary_part.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, +- part: { type: "summary_text", text: currentRawReasoning.text }, ++ emit("response.reasoning_text.done", { ++ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text, + }); + const item = { + type: "reasoning", id: currentRawReasoning.itemId, +- summary: [{ type: "summary_text", text: currentRawReasoning.text }], ++ summary: [] as never[], ++ content: [{ type: "reasoning_text", text: currentRawReasoning.text }], + }; + emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item }); + retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning"); +@@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE( + const itemId = `rs_${uuid()}`; + const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] }; + emit("response.output_item.added", { output_index: outputIndex, item }); +- emit("response.reasoning_summary_part.added", { +- item_id: itemId, output_index: outputIndex, summary_index: 0, +- part: { type: "summary_text", text: "" }, +- }); + currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 }; + } + ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString( +@@ -1123,9 +1116,13 @@ export function bridgeToResponsesSSE( + event.text, + "reasoning", + )); +- emit("response.reasoning_summary_text.delta", { ++ // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT ++ // channel, matching native gpt-oss passthrough: Codex applies its own display ++ // policy, so the desktop band shows the "Thinking…" placeholder instead of the ++ // raw CoT (the #45 summary-channel display intent is intentionally reverted). ++ emit("response.reasoning_text.delta", { + item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, +- summary_index: 0, delta: event.text, ++ content_index: 0, delta: event.text, + }); + break; + } +@@ -1780,7 +1777,8 @@ function buildResponseJSONWithBudget( + } + pushOutput({ + type: "reasoning", id: `rs_${uuid()}`, +- summary: [{ type: "summary_text", text: currentRawReasoning }], ++ summary: [], ++ content: [{ type: "reasoning_text", text: currentRawReasoning }], + }, currentRawReasoningBytes, "reasoning"); + currentRawReasoning = ""; + currentRawReasoningBytes = 0; + +``` + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/google-wire-compiler.ts b/src/adapters/google-wire-compiler.ts +index 88c482ba7d..aa835e50b4 100644 +--- a/src/adapters/google-wire-compiler.ts ++++ b/src/adapters/google-wire-compiler.ts +@@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined { + ))].slice(0, 5); + if (stopSequences.length > 0) out.stopSequences = stopSequences; + } +- if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") { +- const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); +- const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) +- ? raw +- : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); +- if (thinkingLevel) out.thinkingConfig = { thinkingLevel }; ++ if (isObject(value.thinkingConfig)) { ++ const thinking: JsonObject = {}; ++ if (typeof value.thinkingConfig.thinkingLevel === "string") { ++ const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); ++ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) ++ ? raw ++ : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); ++ if (thinkingLevel) thinking.thinkingLevel = thinkingLevel; ++ } ++ // The one key that makes Google return `thought: true` text. Cloud Code Assist serves ++ // thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the ++ // request opts in, so dropping it here silently reinstates the missing-thinking behavior. ++ if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true; ++ if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking; + } + if (Array.isArray(value.responseModalities)) { + const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m)); +diff --git a/src/adapters/google.ts b/src/adapters/google.ts +index 7fcc88ba59..5a6675f6e4 100644 +--- a/src/adapters/google.ts ++++ b/src/adapters/google.ts +@@ -866,11 +866,27 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte + ); + antigravityModel = wireModelId; + antigravitySession = sessionId; ++ // Gemini returns no chain-of-thought TEXT unless the request opts in. Probed against CCA ++ // 2026-09-12: `gemini-3.8-flash-high` answered with thoughtsTokenCount=321 and zero ++ // `thought` parts, then 358-652 chars of genuine reasoning once includeThoughts was set. ++ // Scoped to Gemini wire ids — Claude-on-CCA accepts the flag but never returns thought ++ // parts, and gpt-oss rejects it outright (400 INVALID_ARGUMENT, which would break every ++ // gpt-oss turn). Gated on the provider's visible-thinking opt-in so a user who wants ++ // thinking hidden does not pay conversation-history tokens for text nobody renders; ++ // `hideThinkingSummary !== true` is the same per-request gate the response path uses, so ++ // a client that explicitly asked for hidden thinking is not billed for the text either. ++ const includeThoughts = provider.showThinkingSummary === true ++ && parsed.options.hideThinkingSummary !== true ++ && /^gemini-/.test(wireModelId) ++ && !isImageCapableModel(parsed.modelId); + // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig). + // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction. +- if (thinkingLevel) { ++ if (thinkingLevel || includeThoughts) { + const gc = (body.generationConfig ?? {}) as Record; +- gc.thinkingConfig = { thinkingLevel }; ++ gc.thinkingConfig = { ++ ...(thinkingLevel ? { thinkingLevel } : {}), ++ ...(includeThoughts ? { includeThoughts: true } : {}), ++ }; + body.generationConfig = gc; + } + // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity +diff --git a/src/providers/derive.ts b/src/providers/derive.ts +index 67e6c0522e..7edf28787b 100644 +--- a/src/providers/derive.ts ++++ b/src/providers/derive.ts +@@ -43,6 +43,7 @@ export interface DerivedKeyLoginProvider { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -267,6 +268,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -315,6 +317,7 @@ export function deriveKeyLoginMap(): Record { + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -567,6 +570,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig + if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels]; + if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels]; + if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames; ++ if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary; + if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional; + if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier; + if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip; +diff --git a/src/providers/registry.ts b/src/providers/registry.ts +index f72bb7650b..e483e9db23 100644 +--- a/src/providers/registry.ts ++++ b/src/providers/registry.ts +@@ -343,6 +343,10 @@ export interface ProviderRegistryEntry { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt this provider into visible thinking summaries (see OcxProviderConfig.showThinkingSummary). ++ */ ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -367,7 +371,7 @@ export type ProviderConfigSeed = Pick< + | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens" + | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat" + | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels" +- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" ++ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary" + | "googleMode" | "project" | "location" | "headers" + >; + +@@ -2045,7 +2049,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ + // path must stay RELATIVE: this row sets `allowBaseUrlOverride`, and an absolute `url` would + // retarget a user's custom base back to Google. A leading `./` is required because a bare + // `v1internal:` reads as a URL scheme and `providerModelDiscoverySpecError` rejects it. +- { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, ++ { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", showThinkingSummary: true, jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, + { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, + { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, + { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, +diff --git a/src/router.ts b/src/router.ts +index 55a0326fce..bf2b9b4b98 100644 +--- a/src/router.ts ++++ b/src/router.ts +@@ -410,6 +410,13 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider + ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined + ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent } + : {}), ++ // The request path resolves through routedProviderConfig() and never calls ++ // enrichProviderFromRegistry(), so a saved provider row written before the ++ // registry learned this flag must be backfilled here or route.provider never ++ // carries it and the showThinkingSummary opt-in stays dead. ++ ...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined ++ ? { showThinkingSummary: registryEntry.showThinkingSummary } ++ : {}), + // Registry-only client-facing repair policy (#938): fill only when the + // saved provider has no explicit policy; clone so runtime never aliases + // the registry constant. +diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts +index 3a93246cd0..93377b5573 100644 +--- a/src/server/auth-cors.ts ++++ b/src/server/auth-cors.ts +@@ -885,6 +885,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { + autoToolChoiceOnlyModels: "editor", + preserveReasoningContentModels: "editor", + requiresReasoningPlaceholderModels: "editor", ++ showThinkingSummary: "editor", + retryOn429: "editor", + transientRetryOn5xx: "editor", + reasoningSplitModels: "editor", +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index cccd942026..852cd9f8b0 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -2467,6 +2467,20 @@ async function resolveSubagentFallbackModelEligibility(args: { + }; + } + ++/** ++ * Whether the client explicitly asked for hidden thinking (`reasoning.summary: "none"`). ++ * ++ * Pinned: parseRequest collapses "omitted" and "none" into one hideThinkingSummary ++ * flag, so the raw request body is the ONLY place that still distinguishes them. ++ * Provider opt-ins like showThinkingSummary must consult this — never the flag ++ * alone — or a future caller that copies only the flag would silently unlock an ++ * explicit opt-out. ++ */ ++function clientExplicitlyHidThinking(parsed: OcxParsedRequest): boolean { ++ const rawReasoning = (parsed._rawBody as { reasoning?: { summary?: unknown } } | undefined)?.reasoning; ++ return typeof rawReasoning === "object" && rawReasoning !== null ++ && (rawReasoning as { summary?: unknown }).summary === "none"; ++} + /** + * Apply every route-dependent request mutation against the final selected route. + * Must run only after subagent fallback has settled the model/provider. +@@ -2508,6 +2522,15 @@ async function applyFinalRouteRequestNormalization(args: { + // this request will actually use (#404). + route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req)); + route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire); ++ // Provider-opted visible thinking (e.g. google-antigravity): parseRequest hides thinking ++ // whenever the client omits reasoning.summary, which is the Codex default. A provider that ++ // serves genuine user-facing reasoning opts back into the summary channel here, so thought ++ // parts (Gemini thought, content-channel reasoning_text) reach the client instead of only ++ // the hidden replay envelopes. An explicit client reasoning.summary "none" still wins. ++ if (route.provider.showThinkingSummary === true && parsed.options.hideThinkingSummary === true ++ && !clientExplicitlyHidThinking(parsed)) { ++ parsed.options.hideThinkingSummary = false; ++ } + if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId; + logCtx.model = route.modelId; + logCtx.provider = route.providerName; +diff --git a/src/types/provider.ts b/src/types/provider.ts +index e65130a4fa..b6374a991a 100644 +--- a/src/types/provider.ts ++++ b/src/types/provider.ts +@@ -746,6 +746,15 @@ export interface OcxProviderConfig { + * out explicitly (e.g. MiniMax, where low effort disables thinking). + */ + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt-in: surface upstream thinking as visible reasoning summaries even when the ++ * client did not send `reasoning.summary`. parseRequest hides thinking by default ++ * (Codex omits the field), which strands genuine reasoning — e.g. Gemini `thought` ++ * parts on the google-antigravity (Cloud Code Assist) wire — in hidden replay ++ * envelopes. An explicit client `reasoning.summary: "none"` still wins. Set `false` ++ * to opt a seeded preset back out. ++ */ ++ showThinkingSummary?: boolean; + /** + * Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only, + * openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays + +``` + +## Reflection corrections accepted + +Explicit wire reasoning.summary:"none" wins. A client that serializes configured none as omission cannot be distinguished from unspecified preference. No client config rewrite or global catalog summary default changes. Summary classification is limited to built CCA Gemini requests; unknown/uninitialized, direct Google/Vertex and CCA Claude/gpt-oss remain raw. Streaming and buffered summary-to-tool continuations assert exact Google signature on correct call; never emit Google signatures as Anthropic thinking_signature. Hidden unsigned summaries may disappear but required tool replay state survives. Exercise final assistant text and terminal order, fallback in both directions, and remove replay-comparison rewrite alongside SSE/JSON rewrite. Desktop appearance remains client-controlled; source patch comments claiming an unconditional placeholder are replaced during adoption. diff --git a/devlog/_plan/260912_thinking_contract/020_transport_hint.md b/devlog/_plan/260912_thinking_contract/020_transport_hint.md new file mode 100644 index 0000000000..335e339a00 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/020_transport_hint.md @@ -0,0 +1,319 @@ +# Optional hint suppression + +Class C4 review because client metadata policy changes. Independent of presentation; depends only on roadmap. Adopt #3652 only after independent security/transport review. Public proposal removes exactly two x-codex-safety-buffering headers, metadata.type=safety_buffering events and top-level safety_buffering fields at the client relay boundary. Default false; malformed config must remain off and candidate validation rejects nonbooleans. This suppresses optional transport hints; provider safety decisions/refusals and upstream checks are unchanged. Compact and independent WS/other-provider pathways retain existing policy unless a directly exercised shared boundary already applies. + +MODIFY src/config.ts and src/types/config.ts for validated boolean/default; src/server/relay.ts for allowlisted header removal and SSE terminal-boundary transformation; relay-eager.ts for option forwarding; core.ts to compute option only for canonical OpenAI forward destination and pass it to all relevant headers/client output boundaries; index.ts exports if needed by existing test style. Do not apply to custom gateway/key providers. Preserve errors, response.failed/incomplete and terminal sentinel handling. + +MODIFY tests/responses/passthrough-headers.test.ts, openai-responses-passthrough.test.ts and tests/server/config.test.ts. Scenarios: absent/false/true/malformed config; uppercase headers; unrelated headers; split metadata frames; actual failure carrying hint must still fail; noncanonical provider has identical fields and retains them; eager/non-eager client paths. Add missing canonical route coverage if independent review identifies it. MODIFY English/ja/ko/ru/zh-cn server configuration docs and structure owners. Avoid unsupported claims about models being weaker or provider safety bypass. + +Before/after anchor: createSseTerminalOutputBoundary() -> createSseTerminalOutputBoundary(options?: CodexSafetyBufferingFilterOptions); sanitizePassthroughHeaders(upstream) -> sanitizePassthroughHeaders(upstream, options?); canonical true => filter option, every other provider => undefined. Full public source diff is pinned by #3652 head in 000_plan.md and inspected locally; any needed correction is recorded here before B. + +## Independent design corrections + +H1 accepted: policy rewrite and hint stripping compose. Build policyFailurePayload first, then remove top-level safety_buffering from the effective emitted payload, preserving response.failed/error data and retryable:false. H2 accepted: extend current relaySseWithFailedTail fourth options object with terminalBoundary; never replace upstreamError. Core passes both existing upstreamError and new terminalBoundary; update the existing source-contract assertion to preserve its original guarantee. H3 accepted: native WebSocket codex.response.metadata.headers and /responses/compact are explicitly excluded; their hints remain unfiltered. No new WS metadata filter. Docs must not claim the old WS allowlist excludes these headers. Regression fixtures cover CRLF/split/malformed input, policy error plus hint, EOF upstreamError, canonical true and noncanonical preservation. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/config.ts b/src/config.ts +index fdcda9547c..cd0641feb2 100644 +--- a/src/config.ts ++++ b/src/config.ts +@@ -1125,6 +1125,8 @@ const configSchema = z.object({ + configRebaseProvenance: z.unknown().optional(), + // A retry can be billable, so absence and malformed hand edits both stay off. + emptyCompletionRetry: z.boolean().optional().catch(false), ++ // Header suppression changes what Codex sees, so absence and malformed edits stay off. ++ dropCodexSafetyBuffering: z.boolean().optional().catch(false), + // A malformed hand edit must not silently stop opening the browser: fall back + // to undefined, which resolves to the historical auto-open behavior. + oauthOpenBrowser: z.boolean().optional().catch(undefined), +@@ -2613,6 +2615,14 @@ function emptyCompletionRetryError(value: unknown): string | null { + return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted"; + } + ++function dropCodexSafetyBufferingError(value: unknown): string | null { ++ const raw = rawConfigRecord(value); ++ if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null; ++ const enabled = raw.dropCodexSafetyBuffering; ++ if (enabled === undefined || typeof enabled === "boolean") return null; ++ return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted"; ++} ++ + function oauthOpenBrowserError(value: unknown): string | null { + const raw = rawConfigRecord(value); + if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null; +@@ -2718,6 +2728,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx + ?? codexQuotaAutoRefreshError(value) + ?? codexAccountPickerEnabledError(value) + ?? emptyCompletionRetryError(value) ++ ?? dropCodexSafetyBufferingError(value) + ?? oauthOpenBrowserError(value) + ?? runtimeRoleError(value) + ?? remoteGuiConfigError(value) +@@ -3684,6 +3695,7 @@ export function getDefaultConfig(): OcxConfig { + return { + port: 10100, + emptyCompletionRetry: false, ++ dropCodexSafetyBuffering: false, + managementUsageMaxReadBytes: 64 * 1024 * 1024, + appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024), + // Fresh/re-initialized configs are already written in the current three-tier +diff --git a/src/server/index.ts b/src/server/index.ts +index aedd6bf236..c6ce73b2f1 100644 +--- a/src/server/index.ts ++++ b/src/server/index.ts +@@ -142,6 +142,7 @@ import { + } from "./relay"; + export { + consumeForInspection, ++ codexSafetyBufferingFilterOptions, + relaySseWithFailedTail, + relaySseWithHeartbeat, + relayWithAbort, +diff --git a/src/server/relay-eager.ts b/src/server/relay-eager.ts +index 655997b813..a6e60d3d02 100644 +--- a/src/server/relay-eager.ts ++++ b/src/server/relay-eager.ts +@@ -26,6 +26,7 @@ + + import { + adapterEofIncompleteFrame, ++ type CodexSafetyBufferingFilterOptions, + createSseTerminalOutputBoundary, + doneFrame, + failedTailFrame, +@@ -83,6 +84,8 @@ export type EagerRelayOptions = { + postCancelDrainBytes?: number; + /** Injectable clock for tests. */ + now?: () => number; ++ /** Client output boundary filters (Codex safety-buffering hints). */ ++ terminalBoundary?: CodexSafetyBufferingFilterOptions; + }; + + const DEFAULT_MAX_QUEUE_BYTES = 8 * 1024 * 1024; +@@ -111,7 +114,7 @@ export function relaySseEagerBounded( + const terminalEncoder = new TextEncoder(); + const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder); + const terminalSentinel = doneFrame(terminalEncoder); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary); + const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks + ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined); + const encodeFailedTail = (error: unknown): Uint8Array | null => { +diff --git a/src/server/relay.ts b/src/server/relay.ts +index 60b57ea025..d840b2e59c 100644 +--- a/src/server/relay.ts ++++ b/src/server/relay.ts +@@ -162,7 +162,10 @@ export type SseTerminalOutputBoundary = { + * terminal, and drops every later block/byte. A premature [DONE] is held until + * a terminal arrives so clean EOF can synthesize one terminal and one sentinel. + */ +-export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { ++export function createSseTerminalOutputBoundary( ++ options?: CodexSafetyBufferingFilterOptions, ++): SseTerminalOutputBoundary { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES); +@@ -181,6 +184,10 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + const payload = sseDataPayload(decoder.decode(frame.block)); + const isDone = payload === "[DONE]"; + const parsed = payload === null ? undefined : parseSsePayload(payload); ++ const safetyBuffering = dropSafetyBuffering && parsed !== undefined ++ ? codexSafetyBufferingBlockAction(parsed) ++ : "keep"; ++ if (safetyBuffering === "drop") continue; + const policyError = parsed !== undefined && isPolicyRewriteType(parsed) + ? cyberPolicyTerminalError(parsed) + : undefined; +@@ -189,7 +196,9 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + decoder.decode(frame.block), + policyFailurePayload(policyError, parsed), + )) +- : frame.block; ++ : safetyBuffering === "strip" ++ ? encoder.encode(stripCodexSafetyBufferingField(decoder.decode(frame.block), parsed)) ++ : frame.block; + if (isDone) { + done = true; + if (responsesTerminal) { +@@ -260,10 +269,11 @@ export function relaySseWithFailedTail( + body: ReadableStream, + upstream: AbortController, + onClientGone?: (reason?: unknown) => void, ++ boundaryOptions?: CodexSafetyBufferingFilterOptions, + ): ReadableStream { + const reader = body.getReader(); + const encoder = new TextEncoder(); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(boundaryOptions); + let closed = false; + const relayChunk = ( + controller: ReadableStreamDefaultController, +@@ -438,6 +448,29 @@ function isPolicyRewriteType(parsed: unknown): boolean { + return type === "response.failed" || type === "response.incomplete" || type === "error"; + } + ++/** ++ * Codex emits its safety-buffering hint in the SSE body as well as in headers: ++ * a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a ++ * `safety_buffering` field on another event. The metadata event is dropped whole; ++ * the field is stripped so the carrying event is otherwise relayed unchanged. ++ */ ++function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" { ++ const root = asJsonRecord(parsed); ++ if (!root) return "keep"; ++ if (root.type === "response.metadata") { ++ const metadata = asJsonRecord(root.metadata); ++ if (metadata?.type === "safety_buffering") return "drop"; ++ } ++ return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep"; ++} ++ ++function stripCodexSafetyBufferingField(block: string, parsed: unknown): string { ++ const root = asJsonRecord(parsed); ++ if (!root) return block; ++ const { safety_buffering: _safetyBuffering, ...rest } = root; ++ return replaceSseDataPayload(block, JSON.stringify(rest)); ++} ++ + function rewritePolicyTerminalBlock(block: string, payload: string): string { + const newline = block.includes("\r\n") ? "\r\n" : "\n"; + const rewritten = replaceSseDataPayload(block, payload); +@@ -1422,7 +1455,31 @@ export function consumeForResponseLogMetadata( + * body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough. + * Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim. + */ +-export function sanitizePassthroughHeaders(upstream: Headers): Headers { ++export const CODEX_SAFETY_BUFFERING_HEADERS = [ ++ "x-codex-safety-buffering-enabled", ++ "x-codex-safety-buffering-faster-model", ++] as const; ++ ++const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet = new Set(CODEX_SAFETY_BUFFERING_HEADERS); ++ ++export interface CodexSafetyBufferingFilterOptions { ++ /** ++ * Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response ++ * headers and the `safety_buffering` SSE metadata event / field. Absent and ++ * `false` relay everything unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; ++} ++ ++/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */ ++export function codexSafetyBufferingFilterOptions( ++ config: { dropCodexSafetyBuffering?: boolean }, ++): CodexSafetyBufferingFilterOptions { ++ return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true }; ++} ++ ++export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const DROP = new Set([ + "content-encoding", + "content-length", +@@ -1439,7 +1496,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers { + ]); + const out = new Headers(); + upstream.forEach((value, key) => { +- if (!DROP.has(key.toLowerCase())) out.set(key, value); ++ const lower = key.toLowerCase(); ++ if (DROP.has(lower)) return; ++ if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return; ++ out.set(key, value); + }); + return out; + } +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index 9d0eea0d76..e199917968 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -304,6 +304,7 @@ import { + markEagerRelaySseResponse, + markNativePassthroughSseResponse, + relaySseWithFailedTail, ++ codexSafetyBufferingFilterOptions, + relayWithAbort, + sanitizePassthroughHeaders, + } from "../relay"; +@@ -3850,6 +3851,9 @@ async function handleResponsesInner( + let hostAdmissionLease = pendingHostAdmissionLease; + pendingHostAdmissionLease = null; + try { ++ const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) ++ ? codexSafetyBufferingFilterOptions(config) ++ : undefined; + const imageGenCallAliases = route.provider.authMode === "forward" + ? new Map() + : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget); +@@ -4732,7 +4736,7 @@ async function handleResponsesInner( + } + break; + } +- const headers = sanitizePassthroughHeaders(upstreamResponse.headers); ++ const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + const resolvedModel = headers.get("openai-model")?.trim(); + if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; + if (isUsageDebugEnabled()) { +@@ -4824,7 +4828,7 @@ async function handleResponsesInner( + return new Response(upstreamResponse.body, { + status: upstreamResponse.status, + statusText: upstreamResponse.statusText, +- headers: sanitizePassthroughHeaders(upstreamResponse.headers), ++ headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions), + }); + } + if (!upstreamResponse.ok) { +@@ -5027,6 +5031,7 @@ async function handleResponsesInner( + onDone: () => unregisterTurn(turnAc), + }, { + clientGoneSignal: options.abortSignal, ++ terminalBoundary: codexSafetyBufferingOptions, + ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}), + }); + // When selected, this relay closes response.completed even if upstream +@@ -5110,7 +5115,8 @@ async function handleResponsesInner( + const rewrittenBody = clientBlockRewrite !== undefined + ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget) + : nativeBody; +- const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason)); ++ const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason), ++ codexSafetyBufferingOptions); + return markNativePassthroughSseResponse(new Response(clientBody, { + status: upstreamResponse.status, + headers, +@@ -5238,7 +5244,7 @@ async function handleResponsesInner( + } + throw error; + } +- const sseHeaders = sanitizePassthroughHeaders(headers); ++ const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions); + sseHeaders.set("content-type", "text/event-stream"); + sseHeaders.set("cache-control", "no-store"); + return new Response(stream, { +diff --git a/src/types/config.ts b/src/types/config.ts +index 8cf1246979..4d2c63fdf1 100644 +--- a/src/types/config.ts ++++ b/src/types/config.ts +@@ -335,6 +335,16 @@ export interface OcxConfig { + client?: OcxClientConnectionConfig; + /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */ + emptyCompletionRetry?: boolean; ++ /** ++ * Drop the Codex safety-buffering hints from a Codex Responses passthrough: the ++ * `x-codex-safety-buffering-*` response headers, `response.metadata` SSE events of ++ * type `safety_buffering`, and the `safety_buffering` field on other SSE events. ++ * The Codex TUI turns those hints into a "retry with a faster model" prompt whose ++ * default action switches the session to a weaker model, so an unattended session ++ * can lose its model to a stray keystroke. Absent and `false` relay everything ++ * unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; + /** + * Whether a login may open a browser on the machine running the proxy. + * + +``` diff --git a/devlog/_plan/260912_thinking_contract/030_spark.md b/devlog/_plan/260912_thinking_contract/030_spark.md new file mode 100644 index 0000000000..3997aaa6d7 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/030_spark.md @@ -0,0 +1,66 @@ +# Spark Lite metadata follows body shape + +Class C3 bounded compatibility. Independent of presentation/hint; depends on roadmap. MODIFY src/adapters/openai-responses.ts only inside canonical OpenAI forwarding and final wire model gpt-5.3-codex-spark. Add bodyCarriesLiteToolShape next to existing tool-shape helpers: Array.isArray(body.input) && body.input.some(item => isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.length > 0). After final Spark body construction, delete all case variants of CODEX_RESPONSES_LITE_HEADER then set it to liteShaped ? "true" : "false". Existing prepareCodexWsRequest projects it onto native frame metadata. + +Before: Spark deletes the header, allowing stale native metadata to survive. After: tool-less/top-level-tool Spark frames advertise false; nonempty Lite catalog frames advertise true despite conflicting inherited header. No retirement, no changes to model availability, no user service changes. + +MODIFY tests/codex-integration/codex-metadata-integrity.test.ts: alias resolved final model, inherited true/false/mixed-case/absent header, Lite tool body true, empty Lite group false, malformed metadata keeps HTTP fallback/body, noncanonical remains unchanged. MODIFY tests/responses/ws-upstream-reuse.test.ts: legacy true socket retires when adapter produces false, replacement same identity reused, raw request immutable. MODIFY all eight existing architecture locale pages and structure/transports/responses.md, referencing body-shape rule from shared area owners. Adopt latest #4130 source diff, preserving author; do not import historical earlier heads. + +Verification: source diff review and final-branch hosted ci.yml lane=all. Tests NOT RUN locally. Success proves framing and connection identity, not a live backend EOF fix or all tool-bearing EOF cases. Remaining acceptance: broader tool-format conversion stays out of scope. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts +index c4aa523ee6..8fbe43816d 100644 +--- a/src/adapters/openai-responses.ts ++++ b/src/adapters/openai-responses.ts +@@ -864,6 +864,21 @@ function promoteClientLoadedTools(body: unknown): unknown { + } + + const MAX_RESPONSES_CALL_ID_LENGTH = 64; ++ ++/** ++ * Whether the outgoing body still delivers tools through the responses-lite shape. ++ * ++ * Lite carries the client catalog as an `additional_tools` input item; the non-Lite wire shape ++ * expects top-level `tools`. Anything that flips the Lite advertisement has to agree with the ++ * shape actually being sent, or the destination silently loses the tool surface. ++ */ ++function bodyCarriesLiteToolShape(body: Record): boolean { ++ if (!Array.isArray(body.input)) return false; ++ return body.input.some(item => ++ isPlainObject(item) && item.type === "additional_tools" ++ && Array.isArray(item.tools) && item.tools.length > 0 ++ ); ++} + const REPAIRED_CALL_ID_PREFIX = "call_ocx_"; + const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length; + +@@ -2515,12 +2530,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): + parsed.modelId, + ); + if (isCanonicalOpenAiForwardProvider(provider)) { +- // Spark closes Responses Lite streams before a terminal completion. Select compatibility +- // from the final wire model so aliases cannot leave the caller or a static header enabled. ++ // Select Spark's Lite compatibility from the final wire model, including aliases, and ++ // let the BODY decide it. The header also overrides native WS metadata downstream, so a ++ // forwarded or statically configured value must never contradict the shape being sent. ++ // ++ // The synchronized catalog keeps `use_responses_lite: true` for Spark precisely because ++ // it selects tool delivery (`input[].additional_tools` instead of top-level `tools`), and ++ // stripSparkCompatibility filters that group in place rather than promoting it. So a ++ // Lite-shaped body is pinned back ON — otherwise an inherited `false` advertises non-Lite ++ // while the tools exist only in the Lite shape, and Spark loses the tool surface. Only a ++ // body with no Lite tool group is downgraded, which is what the stream fix needs. + if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { ++ const liteShaped = bodyCarriesLiteToolShape(finalBody); + for (const name of Object.keys(headers)) { + if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; + } ++ headers[CODEX_RESPONSES_LITE_HEADER] = liteShaped ? "true" : "false"; + } + const routingHeaders = new Headers(headers); + applyCodexRoutingHint(routingHeaders, finalBody); + +``` diff --git a/devlog/_plan/260912_thinking_contract/040_delivery.md b/devlog/_plan/260912_thinking_contract/040_delivery.md new file mode 100644 index 0000000000..a70e62bfb6 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/040_delivery.md @@ -0,0 +1,7 @@ +# Final heads and handoff + +Class C3 delivery evidence. Depends on all dispositions. MODIFY branch-owned numbered completion docs and ignored .tmp/thinking/handoff.md. Read existing .github/PULL_REQUEST_TEMPLATE.md; write every section, credits and precise NOT RUN limitation. Publish only own codex/260912-60plus-thinking* branches with git push --no-verify; PR bases dev for independent units, ordinary parent branch only for actual dependencies. No merge/auto-merge/closures. + +NEW .tmp/thinking/*-ci.json captures gh run view JSON for final SHA plus all jobs. NEW .tmp/thinking/*-review.md captures independent implementation findings with accepted/rebutted disposition. Refresh head/base, native stack membership (unknown if API unsupported), current reviews and CI before handoff. Inspect .github/workflows/ci.yml and dispatch lane=all at each final branch where needed. Existing automatic runs stay untouched. If final-head CI fails, inspect failing logs, repair scoped source or fixtures, commit/push --no-verify and validate new final tip. Do not label skipped/cancelled/old-head runs passing. + +Final handoff fields: own worktree, branch per PR, source PR disposition, exact head, PR URL, dependency order, original author trailers, remaining acceptance, unresolved reviews, CI run id/url/head/result/job conclusions, own cycle records and local tests NOT RUN. Parent performs any subsequent integration. No evidence claims from peer commentary alone. From 533d5782863d7ffb0de86243e2136d51582de77b Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:44:33 +0900 Subject: [PATCH 06/21] fix(responses): separate raw reasoning from provider summaries Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com> Co-authored-by: yxr1995-maker <257504378+yxr1995-maker@users.noreply.github.com> Co-authored-by: Eran --- .../010_presentation.md | 4 + .../docs/reference/configuration/providers.md | 1 + scripts/test-layout/layout.json | 2 +- src/adapters/google-wire-compiler.ts | 20 +- src/adapters/google.ts | 26 +- src/bridge.ts | 25 +- src/providers/derive.ts | 4 + src/providers/registry.ts | 8 +- src/router.ts | 7 + src/server/auth-cors.ts | 1 + .../responses-reasoning-summary-rewrite.ts | 178 ----------- src/server/responses/core.ts | 30 +- src/types/provider.ts | 6 + structure/adapters/registry.md | 2 + structure/catalog.md | 2 + structure/clients/claude-desktop.md | 2 + structure/data-planes/images.md | 2 + structure/data-planes/inbound-compat.md | 2 + structure/gui-and-management-api.md | 2 + structure/ops/docs-and-release.md | 2 + structure/ops/service-and-sidecars.md | 2 + structure/overview.md | 2 + structure/providers/chat-compat.md | 19 +- structure/providers/cursor.md | 2 + structure/providers/google.md | 14 +- structure/providers/xai-grok.md | 2 + structure/runtime.md | 2 + structure/subagents.md | 2 + structure/transports/inventory.md | 2 + structure/transports/responses.md | 13 +- structure/transports/streaming-health.md | 2 + .../bridge-raw-reasoning-hidden.test.ts | 14 +- tests/adapters/bridge.test.ts | 23 +- tests/adapters/google/google-adapter.test.ts | 126 +++++++- .../google/google-wire-compiler.test.ts | 24 ++ tests/fixtures/test-layout-expected.json | 2 +- tests/providers/opencode-go-luna-wire.test.ts | 15 +- ...nses-reasoning-summary-passthrough.test.ts | 23 +- ...esponses-reasoning-summary-rewrite.test.ts | 286 ------------------ .../responses-show-thinking-summary.test.ts | 172 +++++++++++ .../server/server-combo-failover-e2e.test.ts | 32 ++ ...erver-xai-chat-reasoning-streaming.test.ts | 4 +- 42 files changed, 528 insertions(+), 581 deletions(-) delete mode 100644 src/server/responses-reasoning-summary-rewrite.ts delete mode 100644 tests/responses/responses-reasoning-summary-rewrite.test.ts create mode 100644 tests/responses/responses-show-thinking-summary.test.ts diff --git a/devlog/_plan/260912_thinking_contract/010_presentation.md b/devlog/_plan/260912_thinking_contract/010_presentation.md index e39e9c0bfe..8c626aa5bd 100644 --- a/devlog/_plan/260912_thinking_contract/010_presentation.md +++ b/devlog/_plan/260912_thinking_contract/010_presentation.md @@ -314,3 +314,7 @@ index e65130a4fa..b6374a991a 100644 ## Reflection corrections accepted Explicit wire reasoning.summary:"none" wins. A client that serializes configured none as omission cannot be distinguished from unspecified preference. No client config rewrite or global catalog summary default changes. Summary classification is limited to built CCA Gemini requests; unknown/uninitialized, direct Google/Vertex and CCA Claude/gpt-oss remain raw. Streaming and buffered summary-to-tool continuations assert exact Google signature on correct call; never emit Google signatures as Anthropic thinking_signature. Hidden unsigned summaries may disappear but required tool replay state survives. Exercise final assistant text and terminal order, fallback in both directions, and remove replay-comparison rewrite alongside SSE/JSON rewrite. Desktop appearance remains client-controlled; source patch comments claiming an unconditional placeholder are replaced during adoption. + +## Presentation P revalidation + +Prior D: roadmap locked; next presentation implementation. Both source patches apply to baseline; combined application requires keeping the newer no-rewrite expectation. Shared classifier signatures remain current. Implement CCA-only provider default by recomputing parsed.options.hideThinkingSummary from raw summary each final route for inboundWire responses; other inbound types preserve their existing flag. Missing raw request leaves original hide flag authoritative. CCA Gemini classification records boolean in existing per-request adapter closure on each build; default false. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 23b7e5e92d..863b264042 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -208,6 +208,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. | | `reasoningDetailsModels?` | `string[]` | Models whose endpoint returns thinking as a structured `reasoning_details` array (MiniMax M-series with `reasoning_split`); stream deltas are cumulative snapshots that are prefix-diffed, and preserved reasoning replays as a `reasoning_details` array instead of a `reasoning_content` string. | | `requiresReasoningPlaceholderModels?` | `string[]` | Models whose upstream rejects a tool_call continuation missing `reasoning_content` (DeepSeek thinking mode); a minimal placeholder is injected when the replay cache misses. Defaults to `preserveReasoningContentModels`; set `[]` to opt out. | +| `showThinkingSummary?` | `boolean` | Display provider-authored summaries when a Responses client omits `reasoning.summary`. Explicit wire `"none"` wins; a client that serializes its preference as omission cannot be distinguished. Raw reasoning remains content and is never relabeled as a summary. The `google-antigravity` preset defaults to `true`; explicit `false` disables that default. CCA Gemini requests also opt into `generationConfig.thinkingConfig.includeThoughts` when display is enabled; image, Claude and gpt-oss requests do not. This does not change client configuration or global catalog summary defaults. | | `thinkingToggleModels?` | `string[]` | Chat models using `thinking.enabled` rather than an effort ladder. | | `thinkingBudgetModels?` | `string[]` | Chat models using integer `thinking_budget`; effort maps to a budget fraction. | | `noVisionModels?` | `string[]` | Text-only models sent through the vision sidecar; matching tolerates an Ollama `:size` tag. | diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 7241f26266..1162f987ad 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1097,10 +1097,10 @@ "responses-pool-401-refresh.test.ts": "responses", "responses-pool-refresh-attribution.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", - "responses-reasoning-summary-rewrite.test.ts": "responses", "responses-routed-web-search-fields.test.ts": "responses", "responses-self-named-namespace-scrub.test.ts": "responses", "responses-shadow-intercept.test.ts": "responses", + "responses-show-thinking-summary.test.ts": "responses", "responses-snapshot-repair-server.test.ts": "responses", "responses-snapshot-repair.test.ts": "responses", "responses-state-write-amplification.test.ts": "responses", diff --git a/src/adapters/google-wire-compiler.ts b/src/adapters/google-wire-compiler.ts index 88c482ba7d..aa835e50b4 100644 --- a/src/adapters/google-wire-compiler.ts +++ b/src/adapters/google-wire-compiler.ts @@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined { ))].slice(0, 5); if (stopSequences.length > 0) out.stopSequences = stopSequences; } - if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") { - const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); - const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) - ? raw - : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); - if (thinkingLevel) out.thinkingConfig = { thinkingLevel }; + if (isObject(value.thinkingConfig)) { + const thinking: JsonObject = {}; + if (typeof value.thinkingConfig.thinkingLevel === "string") { + const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); + const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) + ? raw + : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); + if (thinkingLevel) thinking.thinkingLevel = thinkingLevel; + } + // The one key that makes Google return `thought: true` text. Cloud Code Assist serves + // thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the + // request opts in, so dropping it here silently reinstates the missing-thinking behavior. + if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true; + if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking; } if (Array.isArray(value.responseModalities)) { const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m)); diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 7fcc88ba59..518aca3903 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -568,13 +568,15 @@ function googleToolCallMetadataFromPart( * Keep that provider visibility bit authoritative here so the streaming and buffered parsers * cannot accidentally expose the same hidden reasoning through different event types. */ -function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined { +function googlePartTextEvent(part: GoogleResponsePart, thoughtSummary = false): AdapterEvent | undefined { // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping // only this optional field preserves the rest of the part without inventing assistant output by // coercion; an empty string keeps its existing no-event behavior. if (typeof part.text !== "string" || part.text.length === 0) return undefined; return part.thought === true - ? { type: "reasoning_raw_delta", text: part.text } + ? thoughtSummary + ? { type: "thinking_delta", thinking: part.text } + : { type: "reasoning_raw_delta", text: part.text } : { type: "text_delta", text: part.text }; } @@ -721,6 +723,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest // can stash the CCA model/session for parseStream's reasoning-replay observation. let antigravityModel: string | undefined; + let returnsThoughtSummaries = false; let antigravitySession: string | undefined; // Vertex returns the same opaque Gemini thought signatures as CCA, but its replay namespace // must stay transport-scoped: a signature minted by one Google backend must never be sent to @@ -795,6 +798,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte : provider.googleMode === "vertex" ? parsed.modelId : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); + returnsThoughtSummaries = provider.googleMode === "cloud-code-assist" + && /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId); // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation. const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId; const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist" @@ -866,11 +871,20 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte ); antigravityModel = wireModelId; antigravitySession = sessionId; + // CCA Gemini exposes provider-authored thought summaries with includeThoughts. + // Other CCA model families do not share this request contract. + const includeThoughts = provider.showThinkingSummary === true + && parsed.options.hideThinkingSummary !== true + && /^gemini-/.test(wireModelId) + && !isImageCapableModel(parsed.modelId); // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig). // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction. - if (thinkingLevel) { + if (thinkingLevel || includeThoughts) { const gc = (body.generationConfig ?? {}) as Record; - gc.thinkingConfig = { thinkingLevel }; + gc.thinkingConfig = { + ...(thinkingLevel ? { thinkingLevel } : {}), + ...(includeThoughts ? { includeThoughts: true } : {}), + }; body.generationConfig = gc; } // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity @@ -1139,7 +1153,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) { pendingStreamThoughtSig = sig; } - const textEvent = googlePartTextEvent(part); + const textEvent = googlePartTextEvent(part, returnsThoughtSummaries); if (textEvent) { emittedContentEvent = true; yield textEvent; @@ -1415,7 +1429,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) { pendingThoughtSig = sig; } - const textEvent = googlePartTextEvent(part); + const textEvent = googlePartTextEvent(part, returnsThoughtSummaries); if (textEvent) events.push(textEvent); const inline = (part as { inlineData?: { mimeType?: string; data?: string } }).inlineData; if (inline && typeof inline.data === "string") { diff --git a/src/bridge.ts b/src/bridge.ts index 20e7c3fe09..a9fbb42988 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -663,16 +663,13 @@ export function bridgeToResponsesSSE( const closeCurrentRawReasoning = () => { if (!currentRawReasoning) return; rawReasoningForNextToolCall = currentRawReasoning.text; - emit("response.reasoning_summary_text.done", { - item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text, - }); - emit("response.reasoning_summary_part.done", { - item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, - part: { type: "summary_text", text: currentRawReasoning.text }, + emit("response.reasoning_text.done", { + item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text, }); const item = { type: "reasoning", id: currentRawReasoning.itemId, - summary: [{ type: "summary_text", text: currentRawReasoning.text }], + summary: [] as never[], + content: [{ type: "reasoning_text", text: currentRawReasoning.text }], }; emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item }); retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning"); @@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE( const itemId = `rs_${uuid()}`; const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] }; emit("response.output_item.added", { output_index: outputIndex, item }); - emit("response.reasoning_summary_part.added", { - item_id: itemId, output_index: outputIndex, summary_index: 0, - part: { type: "summary_text", text: "" }, - }); currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 }; } ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString( @@ -1123,9 +1116,12 @@ export function bridgeToResponsesSSE( event.text, "reasoning", )); - emit("response.reasoning_summary_text.delta", { + // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT + // channel. Clients control raw-reasoning display; this text is not a + // provider-authored summary. + emit("response.reasoning_text.delta", { item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, - summary_index: 0, delta: event.text, + content_index: 0, delta: event.text, }); break; } @@ -1780,7 +1776,8 @@ function buildResponseJSONWithBudget( } pushOutput({ type: "reasoning", id: `rs_${uuid()}`, - summary: [{ type: "summary_text", text: currentRawReasoning }], + summary: [], + content: [{ type: "reasoning_text", text: currentRawReasoning }], }, currentRawReasoningBytes, "reasoning"); currentRawReasoning = ""; currentRawReasoningBytes = 0; diff --git a/src/providers/derive.ts b/src/providers/derive.ts index 72a662aee4..3ab1d01500 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -44,6 +44,7 @@ export interface DerivedKeyLoginProvider { autoToolChoiceOnlyModels?: string[]; preserveReasoningContentModels?: string[]; requiresReasoningPlaceholderModels?: string[]; + showThinkingSummary?: boolean; reasoningSplitModels?: string[]; reasoningDetailsModels?: string[]; thinkingToggleModels?: string[]; @@ -271,6 +272,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), + ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), @@ -320,6 +322,7 @@ export function deriveKeyLoginMap(): Record { ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), + ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), @@ -574,6 +577,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels]; if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels]; if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames; + if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary; if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional; if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier; if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip; diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e2189254fc..d2bee8888c 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -356,6 +356,10 @@ export interface ProviderRegistryEntry { autoToolChoiceOnlyModels?: string[]; preserveReasoningContentModels?: string[]; requiresReasoningPlaceholderModels?: string[]; + /** + * Opt this provider into visible thinking summaries (see OcxProviderConfig.showThinkingSummary). + */ + showThinkingSummary?: boolean; reasoningSplitModels?: string[]; reasoningDetailsModels?: string[]; thinkingToggleModels?: string[]; @@ -380,7 +384,7 @@ export type ProviderConfigSeed = Pick< | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens" | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat" | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels" - | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" + | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary" | "googleMode" | "project" | "location" | "headers" >; @@ -2112,7 +2116,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // path must stay RELATIVE: this row sets `allowBaseUrlOverride`, and an absolute `url` would // retarget a user's custom base back to Google. A leading `./` is required because a bare // `v1internal:` reads as a URL scheme and `providerModelDiscoverySpecError` rejects it. - { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, + { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", showThinkingSummary: true, jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, diff --git a/src/router.ts b/src/router.ts index bd8e9dc690..70e427b74b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -413,6 +413,13 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent } : {}), + // The request path resolves through routedProviderConfig() and never calls + // enrichProviderFromRegistry(), so a saved provider row written before the + // registry learned this flag must be backfilled here or route.provider never + // carries it and the showThinkingSummary opt-in stays dead. + ...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined + ? { showThinkingSummary: registryEntry.showThinkingSummary } + : {}), // Registry-only client-facing repair policy (#938): fill only when the // saved provider has no explicit policy; clone so runtime never aliases // the registry constant. diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 0910698a0c..e59029a3e2 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -887,6 +887,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { autoToolChoiceOnlyModels: "editor", preserveReasoningContentModels: "editor", requiresReasoningPlaceholderModels: "editor", + showThinkingSummary: "editor", retryOn429: "editor", transientRetryOn5xx: "editor", reasoningSplitModels: "editor", diff --git a/src/server/responses-reasoning-summary-rewrite.ts b/src/server/responses-reasoning-summary-rewrite.ts deleted file mode 100644 index 55c6d8ae7b..0000000000 --- a/src/server/responses-reasoning-summary-rewrite.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { SsePayloadRewrite } from "./sse-payload-rewrite"; - -/** - * Route content-channel reasoning from native-Responses upstreams through the - * expandable summary channel (issue #45). - * - * Codex renders the expandable reasoning trace from the Responses reasoning - * item's `summary[]` channel. DeepSeek's native `/responses` endpoint emits - * raw thinking on the content channel instead (`response.reasoning_text.delta` - * plus items with `content: [{type: "reasoning_text", text}]` and an empty - * `summary`), so routed DeepSeek turns showed the "Worked for Xs" timer with - * nothing to expand. Native OpenAI upstreams already emit summary-channel - * events; this rewrite is a no-op for them (no reasoning_text events to - * rewrite) and only engages when the upstream produces content-channel - * reasoning. - * - * Replay compatibility: Codex echoes the reasoning item it received back into - * the next request's input. DeepSeek's Responses API accepts summary-shaped - * reasoning input items (verified live), so the rewrite round-trips. - */ - -function isPlainObject(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value); -} - -function reasoningTextOf(item: Record): string { - if (!Array.isArray(item.content)) return ""; - return item.content - .filter((part): part is Record => isPlainObject(part) && part.type === "reasoning_text") - .map(part => (typeof part.text === "string" ? part.text : "")) - .join(""); -} - -/** Move a reasoning item's content channel into the summary channel. */ -function reasoningItemToSummaryShape(item: Record): Record { - if (item.type !== "reasoning") return item; - // `encrypted_content` is opaque, state-bearing provider data, so the entire item must retain its - // upstream shape unless that backend has an explicit replay contract permitting a rewrite. This - // defensively protects content-channel backends that do issue blobs when the client replays the - // stored item. The delta rewrite can still provide the expandable trace for the live turn. - // DeepSeek — the provider this rewrite was verified against — is `statelessResponses` and issues - // no blob, so it is unaffected. - if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) return item; - const text = reasoningTextOf(item); - // Items that already use the summary channel (or carry no content text at - // all) are left untouched: rewriting them could clear a valid summary. - if (text.length === 0) return item; - const next: Record = { ...item }; - delete next.content; - next.summary = [{ type: "summary_text", text }]; - return next; -} - -/** - * Rewrite one parsed SSE payload in place of the content channel, or return - * `null` when nothing changed (caller keeps the original payload). - */ -function rewritePayload(payload: Record): Record | null { - switch (payload.type) { - case "response.reasoning_text.delta": { - const next: Record = { - type: "response.reasoning_summary_text.delta", - item_id: payload.item_id, - output_index: payload.output_index, - summary_index: 0, - delta: payload.delta, - }; - if (payload.sequence_number !== undefined) next.sequence_number = payload.sequence_number; - return next; - } - case "response.reasoning_text.done": { - const next: Record = { - type: "response.reasoning_summary_text.done", - item_id: payload.item_id, - output_index: payload.output_index, - summary_index: 0, - text: payload.text, - }; - if (payload.sequence_number !== undefined) next.sequence_number = payload.sequence_number; - return next; - } - default: { - let changed = false; - const next: Record = { ...payload }; - if (isPlainObject(next.item) && next.item.type === "reasoning") { - const rewritten = reasoningItemToSummaryShape(next.item); - if (rewritten !== next.item) { - next.item = rewritten; - changed = true; - } - } - // SSE event shape: {type: "response.completed", response: {output}}. - const response = isPlainObject(next.response) ? { ...next.response } : null; - if (response && Array.isArray(response.output)) { - const output = response.output.map(item => { - if (!isPlainObject(item) || item.type !== "reasoning") return item; - const rewritten = reasoningItemToSummaryShape(item); - if (rewritten !== item) changed = true; - return rewritten; - }); - if (changed) { - response.output = output; - next.response = response; - } - } - // Bare response document shape (non-streaming passthrough): - // {object: "response", output: [...]}. - if (Array.isArray(next.output)) { - const output = next.output.map(item => { - if (!isPlainObject(item) || item.type !== "reasoning") return item; - const rewritten = reasoningItemToSummaryShape(item); - if (rewritten !== item) changed = true; - return rewritten; - }); - if (changed) next.output = output; - } - return changed ? next : null; - } - } -} - -/** Payload rewrite for passthrough relays whose upstream emits content-channel reasoning. */ -export function createReasoningSummaryChannelPayloadRewrite(): SsePayloadRewrite { - return (payload: string): string => { - let parsed: unknown; - try { - parsed = JSON.parse(payload); - } catch { - return payload; - } - if (!isPlainObject(parsed)) return payload; - const rewritten = rewritePayload(parsed); - return rewritten !== null ? JSON.stringify(rewritten) : payload; - }; -} - -/** - * Object-level variant for the non-streaming passthrough: the bounded-JSON - * relay bypasses the SSE payload rewrite, so reasoning items inside a full - * Responses JSON document need the same normalization before plain JSON - * serialization or forced JSON-to-SSE reframing. Returns the same reference - * when nothing changed. - */ -export function rewriteReasoningSummaryInJson(value: unknown): unknown { - if (!isPlainObject(value)) return value; - const rewritten = rewritePayload(value); - return rewritten !== null ? rewritten : value; -} - -/** String-level variant of {@link rewriteReasoningSummaryInJson}. */ -export function rewriteReasoningSummaryInJsonString(json: string): string { - let parsed: unknown; - try { - parsed = JSON.parse(json); - } catch { - return json; - } - const rewritten = rewriteReasoningSummaryInJson(parsed); - return rewritten === parsed ? json : JSON.stringify(rewritten); -} - -/** - * True when a routed native-Responses provider emits content-channel reasoning - * (raw `reasoning_text`) instead of the summary channel. DeepSeek's - * `/responses` endpoint is the current example: it ships raw thinking with an - * empty `summary` and keeps `preserveReasoningContentModels` so multi-turn - * replays round-trip. - */ -export function routeUsesContentChannelReasoning( - provider: { statelessResponses?: boolean; preserveReasoningContentModels?: string[] }, - modelId: string, -): boolean { - if (provider.statelessResponses === true) return true; - const preserved = provider.preserveReasoningContentModels; - const normalizedModelId = modelId.toLowerCase(); - return Array.isArray(preserved) - && preserved.some(id => id.toLowerCase() === normalizedModelId); -} diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index a10d97d282..7e71e5d37d 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -365,12 +365,6 @@ import { hasResponsesItemIdRepair, repairResponsesJsonItemIds, } from "../responses-item-id-repair"; -import { - createReasoningSummaryChannelPayloadRewrite, - rewriteReasoningSummaryInJson, - rewriteReasoningSummaryInJsonString, - routeUsesContentChannelReasoning, -} from "../responses-reasoning-summary-rewrite"; import { createImageGenCallRestoreRewrite, imageGenToolCallAliases, @@ -2514,6 +2508,13 @@ async function applyFinalRouteRequestNormalization(args: { // this request will actually use (#404). route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req)); route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire); + // Recompute from the original wire preference on every route, including fallback. + // A provider default never converts raw reasoning into a summary. + if (inboundWire === "responses" && parsed._rawBody) { + const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } }).reasoning?.summary; + parsed.options.hideThinkingSummary = summary === "none" + || (!summary && route.provider.showThinkingSummary !== true); + } if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId; logCtx.model = route.modelId; logCtx.provider = route.providerName; @@ -5042,10 +5043,7 @@ async function handleResponsesInner( ? JSON.parse(normalizeFunctionCompletionJson(JSON.stringify(restored))) : restored) as { id?: unknown; output?: unknown; status?: unknown }; // Replay overlap compares the items the client echoes, including visible reasoning shape. - const replayResponse = parsed.options.hideThinkingSummary !== true - && routeUsesContentChannelReasoning(route.provider, route.modelId) - ? rewriteReasoningSummaryInJson(restoredResponse) as typeof restoredResponse - : restoredResponse; + const replayResponse = restoredResponse; if ( undeclaredToolGuardActive && undeclaredToolCallNameInResponse( @@ -6039,10 +6037,6 @@ async function handleResponsesInner( ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget) : undefined, responseModelRewrite, - parsed.options.hideThinkingSummary !== true - && routeUsesContentChannelReasoning(route.provider, route.modelId) - ? createReasoningSummaryChannelPayloadRewrite() - : undefined, ].filter((rewrite): rewrite is NonNullable => rewrite !== undefined); // #893: sparse-snapshot gateways get field backfills AND lifecycle event // injection at the block level, after payload rewrites. Defaults come @@ -6313,13 +6307,7 @@ async function handleResponsesInner( const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId ? rewriteResponsesModelJson(repaired, parsed._responseModelId) : repaired; - // The bounded-JSON answer bypasses the SSE payload rewrite, so content- - // channel reasoning needs the same normalization here for the plain - // JSON answer and every reframed-SSE variant built from clientJson. - return parsed.options.hideThinkingSummary !== true - && routeUsesContentChannelReasoning(route.provider, route.modelId) - ? rewriteReasoningSummaryInJsonString(modelRewritten) - : modelRewritten; + return modelRewritten; })(); // #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and // the reframed-SSE branch below are built from this body, so one check covers them. This diff --git a/src/types/provider.ts b/src/types/provider.ts index d559ffef8f..bf37ac708a 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -773,6 +773,12 @@ export interface OcxProviderConfig { * out explicitly (e.g. MiniMax, where low effort disables thinking). */ requiresReasoningPlaceholderModels?: string[]; + /** + * Default to displaying provider-authored summaries when Responses summary is omitted. + * Explicit wire summary:"none" wins; false disables a seeded provider default. + * Raw reasoning is never relabeled as a summary. + */ + showThinkingSummary?: boolean; /** * Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only, * openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index b0fab66633..514b7fede6 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -63,3 +63,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Adapter events distinguish raw reasoning content from summary-channel thinking; CCA Gemini classification is request-local. See [Google provenance](../providers/google.md). diff --git a/structure/catalog.md b/structure/catalog.md index bf7ef00d38..dffd61b353 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -271,3 +271,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Provider `showThinkingSummary` is a Responses request default; it does not rewrite catalog summary defaults or client configuration. See [Google summaries](providers/google.md). diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 36511b6f00..3a7f229f98 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -82,3 +82,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Provider summary defaults are Responses-specific and do not rewrite connected Claude Desktop profiles. See [inbound compatibility](../data-planes/inbound-compat.md). diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 01d0cd4b0f..0260413b7b 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -76,3 +76,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +CCA image-capable requests do not acquire the text-summary includeThoughts opt-in. See [Google summary boundary](../providers/google.md). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 45fe1c11e7..a7f077a5d7 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -96,3 +96,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +The provider summary default applies at Responses ingress; native Chat and Anthropic inbound preferences keep their existing handling. Raw content is never renamed to a summary. See [bridge contract](../providers/chat-compat.md). diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 952e9bd082..d3df91a6ef 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -530,3 +530,5 @@ Missing, invalid or expired evidence is unknown. `gui/src/pages/Combos.tsx` wake expiry, including a deadline crossed before effects run, rechecks activation and visibility, and refreshes quota with Combo data while preserving drafts. Each successful quota snapshot also advances the observation clock, so a retained older row cannot defer evaluation of a fresh row. + +The provider editor field policy exposes `showThinkingSummary` as a boolean provider option; it controls Responses summary defaults without a dashboard rendering change. See [Google provider](providers/google.md). diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index b06c1ce393..634c71a8b7 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -308,3 +308,5 @@ The Combo guides describe the distinction between display quota and single-crede The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Provider configuration documents distinguish actual summaries from raw reasoning content. The test layout registers the summary-default contract cases and removes the obsolete content-rewrite test with its implementation. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 21bafe5b3b..337218e478 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -139,3 +139,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Provider summary defaults are evaluated per routed Responses request without changing service lifecycle or sidecar activation. See [runtime](../runtime.md). diff --git a/structure/overview.md b/structure/overview.md index da3f2dc473..eec481ff18 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -106,3 +106,5 @@ would pass while the rule was violated. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Raw reasoning content and provider-authored summaries remain distinct on the Responses wire. See [reasoning presentation](providers/chat-compat.md). diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e0b87add2d..7a52bf477f 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -215,18 +215,13 @@ honored by BOTH reasoning paths: anthropic `thinking_delta` AND raw `reasoning_r item (`summary: []`, txt-only `ocxr1:` `encrypted_content`, no text deltas) — invisible in the Codex app, so tool cells group like native models — while the text still round-trips for `preserveReasoningContentModels` replay. Visible mode (summary "auto") keeps the raw -`content[reasoning_text]` shape. Diagnosis and codex-rs grouping evidence: -`devlog/_fin/260709_native_response_pattern/`. - -The content-to-summary channel rewrite skips any reasoning item that carries a native -`encrypted_content` blob. The blob is opaque, state-bearing provider data, so the item must -round-trip unchanged unless that backend has an explicit replay contract permitting a rewrite. -This defensively protects providers that issue blobs and later join the route through -`preserveReasoningContentModels`. The rewrite's round trip was verified against DeepSeek, which is -`statelessResponses` and issues no blob. Grok is unaffected in practice because it natively emits -summary-channel reasoning and no `reasoning_text` events, so this content-to-summary item rewrite -does not engage on its route. Only the stored item is exempt — `reasoning_text` delta events carry -no blob and still route to the summary channel, so the live expandable trace is unchanged. +`content[reasoning_text]` shape: raw deltas stream as `response.reasoning_text.delta` and the final +item carries `content: [{type: "reasoning_text", text}]`, so Codex applies its own display policy — +the desktop thinking band shows the "Thinking…" placeholder, and raw text appears only when +`show_raw_agent_reasoning` is enabled. Routing raw CoT through the summary channel instead (the +#45 display intent, intentionally reverted 260911) put unsummarized thinking in the desktop band, +which only fits native OpenAI providers that author real summaries. Diagnosis and codex-rs +grouping evidence: `devlog/_fin/260709_native_response_pattern/`. The process-local raw-reasoning fallback is fail-closed unless a request has an explicit client thread plus an exact provider destination, wire adapter, final model, and physical credential diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index be42793e0b..dd1ea597d9 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -82,3 +82,5 @@ constraints cannot widen the canonical shape. Bare shell bridge names are reject on the freeform path. Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in `tests/providers/cursor/cursor-tool-definitions.test.ts`. + +Shared raw-reasoning events retain content-channel presentation; provider-authored thinking keeps its existing summary path. See [bridge contract](chat-compat.md). diff --git a/structure/providers/google.md b/structure/providers/google.md index 26187aeebf..1c1d5648f8 100644 --- a/structure/providers/google.md +++ b/structure/providers/google.md @@ -2,12 +2,14 @@ ## Google thought-text visibility boundary -Google-family responses may represent model-internal reasoning as a text-bearing part with -`thought: true`. The Google adapter maps that text to the internal `reasoning_raw_delta` event; -only text without the marker becomes visible `text_delta`. Streaming SSE and buffered JSON share -one classifier so transport selection cannot change whether provider-declared reasoning is shown -as assistant output. Thought-signature observation still runs on the original parts before text -classification, preserving the opaque continuation state independently of display semantics. +Google-family parts with `thought: true` stay separate from assistant output. After a CCA +Gemini request is built, the shared streaming/buffered classifier emits `thinking_delta` for +these provider-authored summaries. Other Google wires, non-Gemini CCA models and uninitialized +adapters retain `reasoning_raw_delta`. Model provenance is refreshed on every build. +`showThinkingSummary` defaults on only for the Antigravity preset; explicit provider false and +explicit wire summary none win. Eligible CCA Gemini requests use `includeThoughts: true` only +when provider opt-in and per-request display both allow it. Thought signatures remain attached +to their tool calls independently; they never become Anthropic thinking signatures. > Decision record: [ADR-0055](../decisions/ADR-0055-google-thought-text-visibility-boundary.md) diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5c497d4084..f852534dda 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -62,3 +62,5 @@ Account-scoped OAuth quota remains display evidence for provider-level Combo sel The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Grok chat raw reasoning uses content-channel output with an empty summary; hidden replay envelopes retain continuation text. Native Responses content is not promoted to summaries. See [chat compatibility](chat-compat.md). diff --git a/structure/runtime.md b/structure/runtime.md index 6d733bf8b5..ed719df105 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -212,3 +212,5 @@ cooldowns and response-driven retry remain authoritative. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Responses route normalization resolves provider summary defaults from the original wire preference on every final route. See [reasoning presentation](providers/chat-compat.md) and [CCA summary provenance](providers/google.md). diff --git a/structure/subagents.md b/structure/subagents.md index 662bc857d3..907d4d0d8e 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -207,3 +207,5 @@ Provider-level Combo eligibility uses explicit inference evidence for the curren The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Final-route summary visibility is recomputed after fallback from the original Responses preference; an earlier provider opt-in does not carry into a later provider. See [reasoning presentation](providers/chat-compat.md). diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index f0348c1cdd..dcd0256588 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -67,3 +67,5 @@ Quota publication distinguishes display reports from explicitly supplied inferen The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +CCA Gemini summary provenance and request opt-in are specified in [Google provider](../providers/google.md); raw Responses content retains its wire channel. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 5624a2e04e..4a3a8eb321 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -309,14 +309,11 @@ custom result has no local call, because its original wire type cannot be establ would send an unmatched result upstream. The check resolves the selected wire protocol and the request's own tool declarations after final route selection, so stateful destinations keep their upstream-owned native function and native-only custom continuations. Explicit input still receives -orphan repair; this path asks the client to replay rather than reconstructing history. This flag also enables the existing -visible content-to-summary rewrite for SSE and JSON; summary-channel items and opaque reasoning -blobs keep their existing response handling. The shared recording callback applies the same -reasoning rewrite under the exact client-visible predicate before caching output, after tool -restoration and function normalization. This keeps full-content replay fingerprints comparable -for both full-history-plus-ID and delta continuations without weakening identity checks. Hidden -summaries and opaque blobs keep their existing cache representation. It does not change streaming selection or Chat -model routes. Go fixtures cover Luna, Grok and Muse against both response formats. +orphan repair; this path asks the client to replay rather than reconstructing history. Content-channel reasoning stays content in SSE, JSON and stored replay output; native +summary items and opaque blobs retain their upstream representation. Full-content replay +fingerprints compare the same client-visible items without content-to-summary conversion. +It does not change streaming selection or Chat model routes. Go fixtures cover Luna, Grok +and Muse against both response formats. The canonical OpenCode Go transport also derives `x-opencode-session` from the existing hashed session lane before per-model wire selection. One conversation keeps one opaque affinity value diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index ae20c7c7ea..76681dd7bb 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -196,3 +196,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Raw reasoning and provider-authored summary deltas both remain real upstream activity; visibility does not change heartbeat or terminal ownership. See [reasoning presentation](../providers/chat-compat.md). diff --git a/tests/adapters/bridge-raw-reasoning-hidden.test.ts b/tests/adapters/bridge-raw-reasoning-hidden.test.ts index 4acc66ce62..162d98b206 100644 --- a/tests/adapters/bridge-raw-reasoning-hidden.test.ts +++ b/tests/adapters/bridge-raw-reasoning-hidden.test.ts @@ -77,18 +77,19 @@ describe("hidden raw reasoning (hideThinkingSummary parity for reasoning_raw_del expect(fc).toMatchObject({ call_id: "call_1", name: "read_file" }); }); - test("streamed visible (flag off): raw reasoning rides the expandable summary channel (#2007)", async () => { + test("streamed visible (flag off): raw reasoning rides the content channel (#2007)", async () => { const frames = await collectSse(bridgeToResponsesSSE(replay([ { type: "reasoning_raw_delta", text: "visible raw" }, { type: "done" }, ]), "routed/model")); - expect(frames.some(f => f.event === "response.reasoning_summary_text.delta")).toBe(true); - expect(frames.some(f => f.event === "response.reasoning_text.delta")).toBe(false); + expect(frames.some(f => f.event === "response.reasoning_text.delta")).toBe(true); + expect(frames.some(f => f.event === "response.reasoning_summary_text.delta")).toBe(false); const completed = frames.find(f => f.event === "response.completed")?.data.response as Record; const output = completed.output as Record[]; expect(output[0]).toMatchObject({ type: "reasoning", - summary: [{ type: "summary_text", text: "visible raw" }], + summary: [], + content: [{ type: "reasoning_text", text: "visible raw" }], }); }); @@ -118,14 +119,15 @@ describe("hidden raw reasoning (hideThinkingSummary parity for reasoning_raw_del expect(decodeReasoningEnvelope(reasoning.encrypted_content as string)?.txt).toBe("quiet"); }); - test("non-streaming visible: raw reasoning lands in the summary channel (#2007)", () => { + test("non-streaming visible: raw reasoning lands on the content channel (#2007)", () => { const json = buildResponseJSON([ { type: "reasoning_raw_delta", text: "loud" }, { type: "done" }, ], "routed/model", {}); const output = (json as { output: Record[] }).output; expect(output.find(o => o.type === "reasoning")).toMatchObject({ - summary: [{ type: "summary_text", text: "loud" }], + summary: [], + content: [{ type: "reasoning_text", text: "loud" }], }); }); diff --git a/tests/adapters/bridge.test.ts b/tests/adapters/bridge.test.ts index e9c2b050b6..f283b77015 100644 --- a/tests/adapters/bridge.test.ts +++ b/tests/adapters/bridge.test.ts @@ -87,27 +87,26 @@ describe("Responses bridge reasoning and usage parity", () => { expect(firstOutputs).toBe(1); }); - test("streaming raw reasoning is routed through the expandable summary channel", async () => { + test("streaming raw reasoning rides the content channel like native gpt-oss", async () => { const frames = await collectSse(bridgeToResponsesSSE(replay([ { type: "reasoning_raw_delta", text: "raw detail" }, { type: "done", usage: { inputTokens: 10, outputTokens: 5, cachedInputTokens: 3, reasoningOutputTokens: 2 } }, ]), "routed/model")); - // Chat-completions providers (DeepSeek-style) deliver thinking as raw - // reasoning_content. Codex renders the expandable reasoning trace from the - // Responses summary channel only, so raw reasoning is routed through the - // summary channel (issue #45) instead of the content channel. - expect(frames.find(f => f.event === "response.reasoning_summary_text.delta")?.data) - .toMatchObject({ summary_index: 0, delta: "raw detail" }); - expect(frames.some(f => f.event === "response.reasoning_text.delta")).toBe(false); + // Raw reasoning_content rides the content channel so Codex applies its own display + // policy: the desktop band shows the "Thinking…" placeholder, and raw text appears + // only when show_raw_agent_reasoning is enabled — never as a fake summary. + expect(frames.find(f => f.event === "response.reasoning_text.delta")?.data) + .toMatchObject({ content_index: 0, delta: "raw detail" }); + expect(frames.some(f => f.event === "response.reasoning_summary_text.delta")).toBe(false); const completed = frames.find(f => f.event === "response.completed")?.data.response as Record; const output = completed.output as Record[]; expect(output[0]).toMatchObject({ type: "reasoning", - summary: [{ type: "summary_text", text: "raw detail" }], + summary: [], + content: [{ type: "reasoning_text", text: "raw detail" }], }); - expect((output[0] as { content?: unknown }).content).toBeUndefined(); expect(completed.usage).toMatchObject({ input_tokens: 10, input_tokens_details: { cached_tokens: 3 }, @@ -502,9 +501,9 @@ describe("Responses bridge reasoning and usage parity", () => { const output = json.output as Record[]; expect(output.map(item => item.type)).toEqual(["reasoning", "message"]); expect(output[0]).toMatchObject({ - summary: [{ type: "summary_text", text: "raw json" }], + summary: [], + content: [{ type: "reasoning_text", text: "raw json" }], }); - expect((output[0] as { content?: unknown }).content).toBeUndefined(); expect(json.usage).toMatchObject({ input_tokens: 6, input_tokens_details: { cached_tokens: 1, cache_write_tokens: 2 }, diff --git a/tests/adapters/google/google-adapter.test.ts b/tests/adapters/google/google-adapter.test.ts index 61e1d15d4b..36052d90f2 100644 --- a/tests/adapters/google/google-adapter.test.ts +++ b/tests/adapters/google/google-adapter.test.ts @@ -1,8 +1,10 @@ import { describe, expect, test } from "bun:test"; import { createGoogleAdapter } from "../../../src/adapters/google"; import { chatCompletionsToResponsesBody } from "../../../src/chat/inbound"; +import { buildResponseJSON } from "../../../src/bridge"; +import { withTestTranslatorBudget } from "../../helpers/translator-budget"; import { parseRequest } from "../../../src/responses/parser"; -import type { OcxParsedRequest } from "../../../src/types"; +import type { AdapterEvent, OcxParsedRequest } from "../../../src/types"; const provider = { adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", apiKey: "key" }; @@ -583,3 +585,125 @@ describe("google adapter — direct -tiered wire renames", () => { } }); }); + +describe("google adapter — Antigravity thought-text opt-in", () => { + // CCA keeps generating thinking either way (thoughtsTokenCount stays non-zero) but returns + // NO `thought` text unless the request sets generationConfig.thinkingConfig.includeThoughts. + // Probed 2026-09-12: gemini-3.8-flash-high answered with 0 thought parts and 321 thoughts + // tokens, then 358-652 chars of reasoning once the key was present. + const ccaProvider = { + adapter: "google", + googleMode: "cloud-code-assist", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + apiKey: "key", + project: "proj-123", + } as const; + const optedIn = { ...ccaProvider, showThinkingSummary: true } as const; + + function thoughtParsed(modelId: string, effort?: string, hideThinkingSummary?: boolean): OcxParsedRequest { + return { + modelId, + stream: false, + options: { ...(effort ? { reasoning: effort } : {}), ...(hideThinkingSummary ? { hideThinkingSummary } : {}) }, + context: { messages: [{ role: "user", content: "hi" }], tools: [] }, + } as unknown as OcxParsedRequest; + } + + async function thinkingConfig( + providerConfig: Record, + modelId: string, + effort?: string, + hideThinkingSummary?: boolean, + ): Promise | undefined> { + const { body } = await createGoogleAdapter(providerConfig as never) + .buildRequest(thoughtParsed(modelId, effort, hideThinkingSummary)); + const envelope = JSON.parse(body) as { + request: { generationConfig?: { thinkingConfig?: Record } }; + }; + return envelope.request.generationConfig?.thinkingConfig; + } + + test("asks CCA for thought text on the Gemini wire families", async () => { + // Suffix tier ids deliberately state no level — the suffix IS the effort — so the opt-in + // has to stand on its own for those. + expect(await thinkingConfig(optedIn, "gemini-3.8-flash", "high")).toEqual({ includeThoughts: true }); + expect(await thinkingConfig(optedIn, "gemini-3.8-flash-medium")).toEqual({ includeThoughts: true }); + expect(await thinkingConfig(optedIn, "gemini-3.7-flash", "high")) + .toEqual({ thinkingLevel: "high", includeThoughts: true }); + expect(await thinkingConfig(optedIn, "gemini-3.1-pro", "high")) + .toEqual({ thinkingLevel: "high", includeThoughts: true }); + }); + + test("never sends the flag to models that reject or ignore it", async () => { + // gpt-oss answers 400 INVALID_ARGUMENT with the key present, so it would break the turn. + expect(await thinkingConfig(optedIn, "gpt-oss-120b-medium")).toBeUndefined(); + // Claude-on-CCA accepts the key but returns no thought parts, so it stays off that wire. + expect(await thinkingConfig(optedIn, "claude-sonnet-4-6", "high")).toEqual({ thinkingLevel: "high" }); + }); + + test("a provider without the opt-in keeps the CCA wire unchanged", async () => { + expect(await thinkingConfig(ccaProvider, "gemini-3.8-flash", "high")).toBeUndefined(); + expect(await thinkingConfig(ccaProvider, "gemini-3.7-flash", "high")).toEqual({ thinkingLevel: "high" }); + }); + + test("an explicit client opt-out stops the thought text at the source", async () => { + // Same per-request gate the response path uses: hideThinkingSummary is set for an explicit + // reasoning.summary "none", and paying upstream for text the client refused is waste. + expect(await thinkingConfig(optedIn, "gemini-3.8-flash", "high", true)).toBeUndefined(); + expect(await thinkingConfig(optedIn, "gemini-3.7-flash", "high", true)).toEqual({ thinkingLevel: "high" }); + }); +}); + + +describe("CCA thought summary provenance and replay", () => { + const cca = { adapter: "google", googleMode: "cloud-code-assist", baseUrl: "https://daily-cloudcode-pa.googleapis.com", + apiKey: "fixture-key", project: "fixture-project", showThinkingSummary: true } as const; + const signature = "CiQAx-summary-tool-signature-0123456789abcdef"; + for (const stream of [false, true]) test(`Gemini summary retains tool signature, stream=${stream}`, async () => { + const adapter = withTestTranslatorBudget(createGoogleAdapter(cca)); + const parsed = parsedWith([{ role: "user", content: "lookup" }], [ + { name: "lookup", description: "look up", parameters: { type: "object", properties: {} } }, + ]); + parsed.modelId = "gemini-3.8-flash"; + await adapter.buildRequest(parsed); + const payload = { response: { candidates: [{ content: { parts: [ + { thought: true, text: "Provider summary", thoughtSignature: signature }, + { functionCall: { name: "lookup", args: {} } }, + { text: "Final answer" }, + ] }, finishReason: "STOP" }], usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 2 } } }; + const events: AdapterEvent[] = []; + if (stream) { + for await (const event of adapter.parseStream(new Response(`data: ${JSON.stringify(payload)}\n\n`, + { headers: { "content-type": "text/event-stream" } }))) events.push(event); + } else events.push(...await adapter.parseResponse!(Response.json(payload))); + expect(events[0]).toEqual({ type: "thinking_delta", thinking: "Provider summary" }); + expect(events.some(event => event.type === "thinking_signature")).toBe(false); + const call = events.find(event => event.type === "tool_call_start"); + expect(call?.type === "tool_call_start" && call.providerMetadata?.google?.thoughtSignature).toBe(signature); + expect(events.at(-1)?.type).toBe("done"); + const output = buildResponseJSON(events, parsed.modelId); + expect(JSON.stringify(output)).toContain('"summary_text","text":"Provider summary"'); + expect(JSON.stringify(output)).toContain("Final answer"); + const continuation = parseRequest({ model: parsed.modelId, input: [ + ...(output.output as unknown[]), { type: "function_call_output", call_id: call && "id" in call ? call.id : "", output: "result" }, + ] }); + const next = JSON.parse((await withTestTranslatorBudget(createGoogleAdapter(cca)).buildRequest(continuation)).body); + const parts = next.request.contents.flatMap((turn: { parts: unknown[] }) => turn.parts); + expect(parts).toContainEqual(expect.objectContaining({ functionCall: expect.objectContaining({ name: "lookup" }), thoughtSignature: signature })); + }); + + test("reused adapter resets Gemini summary provenance for a CCA non-Gemini model", async () => { + const adapter = withTestTranslatorBudget(createGoogleAdapter(cca)); + for (const modelId of ["gemini-3.8-flash", "gpt-oss-120b-medium"]) { + const request = parsedWith([{ role: "user", content: "hi" }]); + request.modelId = modelId; + await adapter.buildRequest(request); + const events = await adapter.parseResponse!(Response.json({ response: { candidates: [{ + content: { parts: [{ thought: true, text: "thinking" }] }, finishReason: "STOP", + }] } })); + expect(events[0]).toEqual(modelId.startsWith("gemini-") + ? { type: "thinking_delta", thinking: "thinking" } + : { type: "reasoning_raw_delta", text: "thinking" }); + } + }); +}); diff --git a/tests/adapters/google/google-wire-compiler.test.ts b/tests/adapters/google/google-wire-compiler.test.ts index 1523d11d85..482aa809e6 100644 --- a/tests/adapters/google/google-wire-compiler.test.ts +++ b/tests/adapters/google/google-wire-compiler.test.ts @@ -132,4 +132,28 @@ describe("Google wire compiler", () => { const repaired = JSON.parse(repairGoogleInvalidRequestBody(body, error)!); expect(repaired.request.generationConfig).toEqual({ maxOutputTokens: 4096 }); }); + + test("keeps the includeThoughts opt-in while still dropping unknown thinking keys", () => { + const withOptIn = compileGoogleWireBody({ + generationConfig: { + thinkingConfig: { includeThoughts: true, thinkingLevel: "max", futureThinkingField: true }, + }, + }); + expect(withOptIn.body.generationConfig).toEqual({ + thinkingConfig: { thinkingLevel: "high", includeThoughts: true }, + }); + + // The flag has to survive on its own too: suffix tier ids deliberately carry no + // thinkingLevel, so an includeThoughts-only config is the whole request. + const optInOnly = compileGoogleWireBody({ + generationConfig: { thinkingConfig: { includeThoughts: true } }, + }); + expect(optInOnly.body.generationConfig).toEqual({ thinkingConfig: { includeThoughts: true } }); + + // Non-boolean / absent values must not invent the key. + const notRequested = compileGoogleWireBody({ + generationConfig: { thinkingConfig: { includeThoughts: "yes", thinkingLevel: "high" } }, + }); + expect(notRequested.body.generationConfig).toEqual({ thinkingConfig: { thinkingLevel: "high" } }); + }); }); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 62724ffed2..7fd3c60621 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -932,10 +932,10 @@ "responses-pool-401-refresh.test.ts": "responses", "responses-pool-refresh-attribution.test.ts": "responses", "responses-reasoning-summary-passthrough.test.ts": "responses", - "responses-reasoning-summary-rewrite.test.ts": "responses", "responses-routed-web-search-fields.test.ts": "responses", "responses-self-named-namespace-scrub.test.ts": "responses", "responses-shadow-intercept.test.ts": "responses", + "responses-show-thinking-summary.test.ts": "responses", "responses-snapshot-repair-server.test.ts": "responses", "responses-snapshot-repair.test.ts": "responses", "responses-state-write-amplification.test.ts": "responses", diff --git a/tests/providers/opencode-go-luna-wire.test.ts b/tests/providers/opencode-go-luna-wire.test.ts index c0afb15553..7866ec9415 100644 --- a/tests/providers/opencode-go-luna-wire.test.ts +++ b/tests/providers/opencode-go-luna-wire.test.ts @@ -163,19 +163,17 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { const initial = { type: "message", role: "user", content: [{ type: "input_text", text: "Run probe" }] }; const first = await drive({ input: [initial] }); expect(first.document.output[0]).toEqual(reasoning[0]); - expect(first.document.output[1]).toEqual(continuation.summary === "auto" ? { - type: "reasoning", id: `rs_${prefix}_content`, status: "completed", summary: [{ type: "summary_text", text: "Visible thinking" }], - } : reasoning[1]); + // The passthrough keeps native content-channel reasoning in both display modes. + expect(first.document.output[1]).toEqual(reasoning[1]); expect(first.document.output[2]).toEqual(reasoning[2]); expect(first.document.output[3]).toMatchObject(call); expect(first.document.output[4]).toEqual(priorMessage); if (streaming) { - const channel = continuation.summary === "auto" ? "reasoning_summary_text" : "reasoning_text"; - expect(first.text).toContain(`"type":"response.${channel}.delta"`); + expect(first.text).toContain('"type":"response.reasoning_text.delta"'); } const result = { type: "function_call_output", call_id: call.call_id, output: "probe succeeded" }; // Echo exactly the client-visible history through handleResponses. An upstream-shape - // cache would prepend it again after the content-to-summary rewrite (F1). + // cache would prepend it again (F1). const nextBody = { input: continuation.fullHistory ? [initial, ...first.document.output, result] : [result], previous_response_id: first.document.id, store: true, @@ -207,9 +205,8 @@ describe("OpenCode Go stateless reasoning and continuation routes", () => { expect(replay.filter(item => item.type === "reasoning")).toHaveLength(3); expect(replay).toContainEqual(expect.objectContaining({ type: "reasoning", encrypted_content: blob })); expect(JSON.stringify(replay)).toContain("Already summarized"); - if (continuation.summary === "auto") expect(replay).toContainEqual(expect.objectContaining({ - type: "reasoning", summary: [{ type: "summary_text", text: "Visible thinking" }], - })); + // Replay sanitation strips reasoning content in both display modes (F1), so the + // visible "Visible thinking" trace does not re-enter the upstream history. expect(JSON.stringify(replay)).not.toContain("no tool result was recorded"); }); } diff --git a/tests/responses/responses-reasoning-summary-passthrough.test.ts b/tests/responses/responses-reasoning-summary-passthrough.test.ts index 5912221354..ab6e099bcc 100644 --- a/tests/responses/responses-reasoning-summary-passthrough.test.ts +++ b/tests/responses/responses-reasoning-summary-passthrough.test.ts @@ -6,10 +6,12 @@ import type { OcxConfig } from "../../src/types"; /** * The passthrough relay for DeepSeek's native /responses endpoint emits - * content-channel reasoning (reasoning_text.delta + content items). The - * summary-channel rewrite must engage only when the client did NOT ask for - * hidden thinking (hideThinkingSummary) - otherwise a client that asked to - * hide reasoning would get it surfaced as visible summary output. + * content-channel reasoning (reasoning_text.delta + content items) in BOTH + * display modes: Codex applies its own raw-reasoning display policy, so a + * requested summary must not rewrite the native passthrough shape either. + * Hidden thinking (hideThinkingSummary) and visible summary get the same + * content-channel passthrough; the hidden variant additionally arrives as an + * envelope-only item upstream when the adapter layer handles suppression. */ function deepseekSeed() { @@ -86,15 +88,16 @@ describe("passthrough reasoning summary rewrite honors hideThinkingSummary", () expect(text).toContain('"content":[{"type":"reasoning_text","text":"think"}]'); }); - test("SSE: requested summary routes raw reasoning through the summary channel", async () => { + test("SSE: requested summary keeps the native content-channel passthrough", async () => { const response = await runHandleResponses( { model: "deepseek-v4-flash", input: "ping", stream: true, reasoning: { effort: "max", summary: "detailed" } }, SSE_UPSTREAM_FRAMES.join(""), "text/event-stream", ); const text = await response.text(); - expect(text).toContain("response.reasoning_summary_text.delta"); - expect(text).toContain('"summary":[{"type":"summary_text","text":"think"}]'); + expect(text).toContain("response.reasoning_text.delta"); + expect(text).not.toContain("response.reasoning_summary_text.delta"); + expect(text).toContain('"content":[{"type":"reasoning_text","text":"think"}]'); }); test("bounded JSON: hidden thinking keeps the content shape", async () => { @@ -108,14 +111,14 @@ describe("passthrough reasoning summary rewrite honors hideThinkingSummary", () expect(text).not.toContain('"summary":[{"type":"summary_text"'); }); - test("bounded JSON: requested summary moves item content into summary", async () => { + test("bounded JSON: requested summary keeps the content shape", async () => { const response = await runHandleResponses( { model: "deepseek-v4-flash", input: "ping", stream: false, reasoning: { effort: "max", summary: "detailed" } }, JSON_UPSTREAM, "application/json", ); const text = await response.text(); - expect(text).toContain('"summary":[{"type":"summary_text","text":"think"}]'); - expect(text).not.toContain('"content":[{"type":"reasoning_text","text":"think"}]'); + expect(text).toContain('"content":[{"type":"reasoning_text","text":"think"}]'); + expect(text).not.toContain('"summary":[{"type":"summary_text"'); }); }); diff --git a/tests/responses/responses-reasoning-summary-rewrite.test.ts b/tests/responses/responses-reasoning-summary-rewrite.test.ts deleted file mode 100644 index 32940a24e7..0000000000 --- a/tests/responses/responses-reasoning-summary-rewrite.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - createReasoningSummaryChannelPayloadRewrite, - routeUsesContentChannelReasoning, - rewriteReasoningSummaryInJson, - rewriteReasoningSummaryInJsonString, -} from "../../src/server/responses-reasoning-summary-rewrite"; - -const rewrite = createReasoningSummaryChannelPayloadRewrite(); - -function apply(payload: unknown): unknown { - return JSON.parse(rewrite(JSON.stringify(payload))); -} - -describe("responses reasoning summary channel rewrite", () => { - test("routes reasoning_text.delta through the summary channel", () => { - expect(apply({ - type: "response.reasoning_text.delta", - content_index: 0, - delta: "think", - item_id: "rs_1", - output_index: 0, - sequence_number: 4, - })).toEqual({ - type: "response.reasoning_summary_text.delta", - summary_index: 0, - delta: "think", - item_id: "rs_1", - output_index: 0, - sequence_number: 4, - }); - }); - - test("routes reasoning_text.done through the summary channel", () => { - expect(apply({ - type: "response.reasoning_text.done", - content_index: 0, - text: "full thinking", - item_id: "rs_1", - output_index: 0, - })).toEqual({ - type: "response.reasoning_summary_text.done", - summary_index: 0, - text: "full thinking", - item_id: "rs_1", - output_index: 0, - }); - }); - - test("moves reasoning item content into summary on output_item.done", () => { - expect(apply({ - type: "response.output_item.done", - output_index: 0, - item: { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [{ type: "reasoning_text", text: "thinking" }], - summary: [], - }, - })).toEqual({ - type: "response.output_item.done", - output_index: 0, - item: { - type: "reasoning", - id: "rs_1", - status: "completed", - summary: [{ type: "summary_text", text: "thinking" }], - }, - }); - }); - - test("moves reasoning item content into summary inside response.completed", () => { - const payload = { - type: "response.completed", - response: { - id: "resp_1", - status: "completed", - output: [ - { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [{ type: "reasoning_text", text: "thinking" }], - summary: [], - }, - { type: "message", id: "msg_1", status: "completed", content: [{ type: "output_text", text: "OK" }] }, - ], - }, - }; - const result = apply(payload) as { response: { output: Record[] } }; - expect(result.response.output[0]).toEqual({ - type: "reasoning", - id: "rs_1", - status: "completed", - summary: [{ type: "summary_text", text: "thinking" }], - }); - expect(result.response.output[1]).toEqual(payload.response.output[1]); - }); - - test("leaves summary-channel and message events untouched", () => { - const untouched = [ - { type: "response.reasoning_summary_text.delta", summary_index: 0, delta: "s", item_id: "rs_1", output_index: 0 }, - { type: "response.output_text.delta", content_index: 0, delta: "OK", item_id: "msg_1", output_index: 1 }, - { type: "response.output_item.added", output_index: 1, item: { type: "message", id: "msg_1", status: "in_progress", content: [] } }, - ]; - for (const payload of untouched) { - expect(apply(payload)).toEqual(payload); - } - }); - - test("leaves a reasoning item without content text untouched", () => { - expect(apply({ - type: "response.output_item.done", - output_index: 0, - item: { type: "reasoning", id: "rs_1", status: "completed", content: [], summary: [] }, - })).toEqual({ - type: "response.output_item.done", - output_index: 0, - item: { type: "reasoning", id: "rs_1", status: "completed", content: [], summary: [] }, - }); - }); - - test("preserves a summary-channel reasoning item as-is", () => { - expect(apply({ - type: "response.output_item.done", - output_index: 0, - item: { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [], - summary: [{ type: "summary_text", text: "already summarized" }], - }, - })).toEqual({ - type: "response.output_item.done", - output_index: 0, - item: { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [], - summary: [{ type: "summary_text", text: "already summarized" }], - }, - }); - }); - - test("rewrites reasoning items inside a bare completed response document", () => { - const doc = { - id: "resp_1", - object: "response", - status: "completed", - output: [ - { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [{ type: "reasoning_text", text: "thinking" }], - summary: [], - }, - { type: "message", id: "msg_1", status: "completed", content: [{ type: "output_text", text: "OK" }] }, - ], - }; - const result = rewriteReasoningSummaryInJson(doc) as { output: Record[] }; - expect(result.output[0]).toEqual({ - type: "reasoning", - id: "rs_1", - status: "completed", - summary: [{ type: "summary_text", text: "thinking" }], - }); - expect(result.output[1]).toEqual(doc.output[1]); - }); - - test("rewrites reasoning items inside an SSE completed event document", () => { - const doc = { - type: "response.completed", - response: { - id: "resp_1", - status: "completed", - output: [ - { - type: "reasoning", - id: "rs_1", - status: "completed", - content: [{ type: "reasoning_text", text: "thinking" }], - summary: [], - }, - ], - }, - }; - const result = rewriteReasoningSummaryInJson(doc) as { response: { output: Record[] } }; - expect(result.response.output[0]).toEqual({ - type: "reasoning", - id: "rs_1", - status: "completed", - summary: [{ type: "summary_text", text: "thinking" }], - }); - }); - - test("string-level rewrite leaves summary-channel documents untouched", () => { - const doc = JSON.stringify({ - id: "resp_1", - output: [{ type: "reasoning", id: "rs_1", summary: [{ type: "summary_text", text: "already summarized" }] }], - }); - expect(rewriteReasoningSummaryInJsonString(doc)).toBe(doc); - }); - - test("malformed payloads pass through unchanged", () => { - expect(rewrite("not json")).toBe("not json"); - expect(rewrite("[1,2]")).toBe("[1,2]"); - }); - - // `encrypted_content` is opaque, state-bearing provider data, so preserve the complete item - // shape defensively when the client replays it. This rewrite's round-trip was verified against - // DeepSeek, which is stateless and issues no blob; providers that do issue one joined later - // through `preserveReasoningContentModels`. - describe("items carrying encrypted_content", () => { - const blobItem = { - type: "reasoning", - id: "rs_1", - status: "completed", - encrypted_content: "gAAAAAB-upstream-issued-blob", - content: [{ type: "reasoning_text", text: "thinking" }], - summary: [], - }; - - test("are returned byte-for-byte on output_item.done", () => { - const payload = { type: "response.output_item.done", output_index: 0, item: blobItem }; - expect(apply(payload)).toEqual(payload); - }); - - test("are returned byte-for-byte inside response.completed output", () => { - const payload = { - type: "response.completed", - response: { id: "resp_1", output: [blobItem] }, - }; - expect(apply(payload)).toEqual(payload); - }); - - test("are returned byte-for-byte through the non-streaming document rewrite", () => { - const doc = { id: "resp_1", object: "response", output: [blobItem] }; - expect(rewriteReasoningSummaryInJson(doc)).toBe(doc); - const json = JSON.stringify(doc); - expect(rewriteReasoningSummaryInJsonString(json)).toBe(json); - }); - - // Only the stored item is protected: the live trace Codex renders comes from the delta events, - // which carry no blob and are still routed to the summary channel. - test("do not disable the delta rewrite that renders the live trace", () => { - expect(apply({ - type: "response.reasoning_text.delta", - delta: "think", - item_id: "rs_1", - output_index: 0, - })).toMatchObject({ type: "response.reasoning_summary_text.delta", delta: "think" }); - }); - }); -}); - -describe("routeUsesContentChannelReasoning", () => { - test("statelessResponses providers use the content channel", () => { - expect(routeUsesContentChannelReasoning({ statelessResponses: true }, "deepseek-v4-flash")).toBe(true); - }); - - test("preserveReasoningContentModels lists qualify", () => { - expect(routeUsesContentChannelReasoning( - { preserveReasoningContentModels: ["deepseek-v4-flash"] }, - "deepseek-v4-flash", - )).toBe(true); - }); - - test("model matching is case-insensitive on both sides", () => { - expect(routeUsesContentChannelReasoning( - { preserveReasoningContentModels: ["DeepSeek-V4-Flash"] }, - "deepseek-v4-flash", - )).toBe(true); - expect(routeUsesContentChannelReasoning( - { preserveReasoningContentModels: ["deepseek-v4-flash"] }, - "DeepSeek-V4-Flash", - )).toBe(true); - }); - - test("other providers do not", () => { - expect(routeUsesContentChannelReasoning({}, "gpt-5.5")).toBe(false); - }); -}); diff --git a/tests/responses/responses-show-thinking-summary.test.ts b/tests/responses/responses-show-thinking-summary.test.ts new file mode 100644 index 0000000000..a12ad8dc83 --- /dev/null +++ b/tests/responses/responses-show-thinking-summary.test.ts @@ -0,0 +1,172 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { providerConfigSeed } from "../../src/providers/derive"; +import { getProviderRegistryEntry } from "../../src/providers/registry"; +import { handleResponses } from "../../src/server/responses/core"; +import type { OcxConfig, OcxProviderConfig } from "../../src/types"; + +// Provider-opted visible thinking (showThinkingSummary): a provider that serves +// genuine user-facing reasoning surfaces it on the summary channel even when the +// client omits reasoning.summary (the Codex default, which otherwise hides all +// thinking in replay-only envelopes). An explicit client summary of "none" still +// wins and keeps thinking hidden. + +function shownSeed() { + const seed = providerConfigSeed(getProviderRegistryEntry("deepseek")!); + return { ...seed, apiKey: "sk-test", showThinkingSummary: true } as OcxProviderConfig; +} + +function sseFrame(payload: unknown): string { + return "data: " + JSON.stringify(payload) + "\n\n"; +} + +const SSE_UPSTREAM = [ + sseFrame({ type: "response.created", response: { id: "resp_1", status: "in_progress", output: [] } }), + sseFrame({ type: "response.output_item.added", output_index: 0, item: { type: "reasoning", id: "rs_1", status: "in_progress", content: [], summary: [] } }), + sseFrame({ type: "response.reasoning_text.delta", content_index: 0, delta: "think", item_id: "rs_1", output_index: 0 }), + sseFrame({ type: "response.reasoning_text.done", content_index: 0, text: "think", item_id: "rs_1", output_index: 0 }), + sseFrame({ type: "response.output_item.done", output_index: 0, item: { type: "reasoning", id: "rs_1", status: "completed", content: [{ type: "reasoning_text", text: "think" }], summary: [] } }), + sseFrame({ type: "response.completed", response: { id: "resp_1", status: "completed", output: [{ type: "reasoning", id: "rs_1", status: "completed", content: [{ type: "reasoning_text", text: "think" }], summary: [] }] } }), +].join(""); + +async function runHandleResponses(body: Record, seed: OcxProviderConfig) { + const encoder = new TextEncoder(); + globalThis.fetch = (async () => new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(SSE_UPSTREAM)); + controller.close(); + }, + }), + { status: 200, headers: { "content-type": "text/event-stream" } }, + )) as typeof fetch; + const config = { providers: { deepseek: seed } } as unknown as OcxConfig; + return handleResponses( + new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }), + config, + { model: "", provider: "" }, + { abortSignal: AbortSignal.timeout(5_000) }, + ); +} + +describe("showThinkingSummary provider option", () => { + const originalFetch = globalThis.fetch; + afterEach(() => { globalThis.fetch = originalFetch; }); + + test("provider opt-in never relabels raw content as a summary", async () => { + const response = await runHandleResponses( + { model: "deepseek-v4-flash", input: "ping", stream: true }, + shownSeed(), + ); + const text = await response.text(); + expect(text).not.toContain("response.reasoning_summary_text.delta"); + expect(text).toContain('"content":[{"type":"reasoning_text","text":"think"}]'); + }); + + test("explicit client summary none keeps thinking hidden", async () => { + const response = await runHandleResponses( + { model: "deepseek-v4-flash", input: "ping", stream: true, reasoning: { summary: "none" } }, + shownSeed(), + ); + const text = await response.text(); + expect(text).not.toContain("response.reasoning_summary_text.delta"); + expect(text).toContain("response.reasoning_text.delta"); + }); + + test("without the provider option, omitted summary stays hidden", async () => { + const seed = { ...providerConfigSeed(getProviderRegistryEntry("deepseek")!), apiKey: "sk-test" } as OcxProviderConfig; + const response = await runHandleResponses( + { model: "deepseek-v4-flash", input: "ping", stream: true }, + seed, + ); + const text = await response.text(); + expect(text).not.toContain("response.reasoning_summary_text.delta"); + expect(text).toContain("response.reasoning_text.delta"); + }); + + test("google-antigravity preset opts in", () => { + expect(providerConfigSeed(getProviderRegistryEntry("google-antigravity")!).showThinkingSummary).toBe(true); + expect(providerConfigSeed(getProviderRegistryEntry("deepseek")!).showThinkingSummary).toBeUndefined(); + }); + + for (const stream of [false, true]) for (const [summary, providerFlag, visible] of [ + [undefined, undefined, true], ["none", true, false], [undefined, false, false], ["auto", false, true], + ] as const) test(`CCA summary=${summary} provider=${providerFlag} stream=${stream}`, async () => { + const home = mkdtempSync(join(tmpdir(), "ocx-show-thinking-")); + const prevHome = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = home; + writeFileSync(join(home, "auth.json"), JSON.stringify({ + "google-antigravity": { + activeAccountId: "active", + accounts: [{ + id: "active", + credential: { + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 3_600_000, + projectId: "project-id", + }, + }], + }, + })); + const seen: string[] = []; + const requests: Array<{ request: { generationConfig?: { thinkingConfig?: { includeThoughts?: boolean } } } }> = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + seen.push(String(input)); + requests.push(JSON.parse(String(init?.body))); + const payload = { + response: { + candidates: [{ + content: { parts: [{ thought: true, text: "cca-think" }, { text: "OK" }] }, + finishReason: "STOP", + }], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15, thoughtsTokenCount: 3 }, + }, + }; + return stream + ? new Response(sseFrame(payload), { headers: { "content-type": "text/event-stream" } }) + : Response.json(payload); + }) as typeof fetch; + try { + const seed = { + ...providerConfigSeed(getProviderRegistryEntry("google-antigravity")!), + liveModels: false, + models: ["gemini-3.8-flash"], + } as OcxProviderConfig; + // Simulate a saved provider row written before the registry learned the flag: + // the request path must backfill it from the registry entry (routedProviderConfig), + // enrichProviderFromRegistry never runs there. + delete seed.showThinkingSummary; + if (providerFlag !== undefined) seed.showThinkingSummary = providerFlag; + const config = { providers: { "google-antigravity": seed } } as unknown as OcxConfig; + const response = await handleResponses( + new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "google-antigravity/gemini-3.8-flash", input: "ping", stream, reasoning: { effort: "low", ...(summary ? { summary } : {}) } }), + }), + config, + { model: "", provider: "" }, + { abortSignal: AbortSignal.timeout(10_000) }, + ); + const text = await response.text(); + expect(seen).toHaveLength(1); + expect(seen[0]).toContain(stream ? "v1internal:streamGenerateContent" : "v1internal:generateContent"); + expect(text.includes('"summary":[{"type":"summary_text","text":"cca-think"}]')).toBe(visible); + expect(requests[0].request.generationConfig?.thinkingConfig?.includeThoughts === true) + .toBe(visible && providerFlag !== false); + if (stream) expect(text).toContain("response.completed"); + expect(text).toContain("OK"); + } finally { + if (prevHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = prevHome; + rmSync(home, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index 6f649c52d3..1428b8137f 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -3937,3 +3937,35 @@ describe("combo compact failover", () => { expect(await response.text()).toContain("empty summary"); }); }); + + +describe("thinking-summary defaults follow the serving combo route", () => { + for (const firstVisible of [true, false]) for (const summary of [undefined, "none", "auto"]) { + test(`fallback from ${firstVisible} with summary=${summary}`, async () => { + const observed: Array<[string, boolean | undefined]> = []; + customRunTurn = async (parsed, _incoming, emit) => { + observed.push([parsed.modelId, parsed.options.hideThinkingSummary]); + if (parsed.modelId === "m1") { + emit({ type: "error", message: "provider unavailable", status: 503, retryable: true }); + return; + } + emit({ type: "thinking_delta", thinking: "Actual provider summary" }); + emit({ type: "text_delta", text: "Final fallback answer" }); + emit({ type: "done" }); + }; + const config = comboConfig({ + a: provider("test-run-turn", "https://a.test/v1", "key-a", { showThinkingSummary: firstVisible }), + b: provider("test-run-turn", "https://b.test/v1", "key-b", { showThinkingSummary: !firstVisible }), + }); + const response = await post(config, { ...(summary ? { reasoning: { summary } } : {}) }); + const output = JSON.stringify(await response.json()); + expect(response.status).toBe(200); + expect(observed).toEqual([ + ["m1", summary === "none" || (!summary && !firstVisible)], + ["m2", summary === "none" || (!summary && firstVisible)], + ]); + expect(output.includes("Actual provider summary")).toBe(summary === "auto" || (!summary && !firstVisible)); + expect(output).toContain("Final fallback answer"); + }); + } +}); diff --git a/tests/server/server-xai-chat-reasoning-streaming.test.ts b/tests/server/server-xai-chat-reasoning-streaming.test.ts index 5019147189..badc15fa9f 100644 --- a/tests/server/server-xai-chat-reasoning-streaming.test.ts +++ b/tests/server/server-xai-chat-reasoning-streaming.test.ts @@ -151,7 +151,7 @@ describe("xAI OAuth Chat reasoning streaming", () => { let received = ""; await Promise.race([ (async () => { - while (!received.includes("response.reasoning_summary_text.delta")) { + while (!received.includes("response.reasoning_text.delta")) { const chunk = await reader!.read(); if (chunk.done) throw new Error("stream ended before the first xAI reasoning delta"); received += decoder.decode(chunk.value, { stream: true }); @@ -184,7 +184,7 @@ describe("xAI OAuth Chat reasoning streaming", () => { if (chunk.done) break; received += decoder.decode(chunk.value, { stream: true }); } - const reasoningIndex = received.indexOf("response.reasoning_summary_text.delta"); + const reasoningIndex = received.indexOf("response.reasoning_text.delta"); const contentIndex = received.indexOf("response.output_text.delta"); const completedIndex = received.indexOf("response.completed"); expect(reasoningIndex).toBeGreaterThanOrEqual(0); From 6fe87d4ca88da30f35107195ac15592ecf35540c Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:45:31 +0900 Subject: [PATCH 07/21] test(responses): exercise summary generation and hidden signature replay --- .../010_presentation.md | 2 ++ tests/adapters/google/google-adapter.test.ts | 22 +++++++++++++------ .../responses-show-thinking-summary.test.ts | 5 +++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/devlog/_plan/260912_thinking_contract/010_presentation.md b/devlog/_plan/260912_thinking_contract/010_presentation.md index 8c626aa5bd..b1503a5f34 100644 --- a/devlog/_plan/260912_thinking_contract/010_presentation.md +++ b/devlog/_plan/260912_thinking_contract/010_presentation.md @@ -318,3 +318,5 @@ Explicit wire reasoning.summary:"none" wins. A client that serializes configured ## Presentation P revalidation Prior D: roadmap locked; next presentation implementation. Both source patches apply to baseline; combined application requires keeping the newer no-rewrite expectation. Shared classifier signatures remain current. Implement CCA-only provider default by recomputing parsed.options.hideThinkingSummary from raw summary each final route for inboundWire responses; other inbound types preserve their existing flag. Missing raw request leaves original hide flag authoritative. CCA Gemini classification records boolean in existing per-request adapter closure on each build; default false. + +C review corrections: fixture summary emission now depends on includeThoughts; provider false + client auto does not request upstream summaries. Signature regression exercises actual SSE and JSON serializers, visible/hidden modes, a tool-ending first turn, exact signature and real matching functionResponse. Final-answer behavior is covered separately by bridge and combo tests. diff --git a/tests/adapters/google/google-adapter.test.ts b/tests/adapters/google/google-adapter.test.ts index 36052d90f2..f4b53960a5 100644 --- a/tests/adapters/google/google-adapter.test.ts +++ b/tests/adapters/google/google-adapter.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { createGoogleAdapter } from "../../../src/adapters/google"; import { chatCompletionsToResponsesBody } from "../../../src/chat/inbound"; -import { buildResponseJSON } from "../../../src/bridge"; +import { bridgeToResponsesSSE, buildResponseJSON } from "../../../src/bridge"; import { withTestTranslatorBudget } from "../../helpers/translator-budget"; import { parseRequest } from "../../../src/responses/parser"; import type { AdapterEvent, OcxParsedRequest } from "../../../src/types"; @@ -659,7 +659,7 @@ describe("CCA thought summary provenance and replay", () => { const cca = { adapter: "google", googleMode: "cloud-code-assist", baseUrl: "https://daily-cloudcode-pa.googleapis.com", apiKey: "fixture-key", project: "fixture-project", showThinkingSummary: true } as const; const signature = "CiQAx-summary-tool-signature-0123456789abcdef"; - for (const stream of [false, true]) test(`Gemini summary retains tool signature, stream=${stream}`, async () => { + for (const stream of [false, true]) for (const hideThinkingSummary of [false, true]) test(`Gemini signature stream=${stream} hidden=${hideThinkingSummary}`, async () => { const adapter = withTestTranslatorBudget(createGoogleAdapter(cca)); const parsed = parsedWith([{ role: "user", content: "lookup" }], [ { name: "lookup", description: "look up", parameters: { type: "object", properties: {} } }, @@ -669,7 +669,6 @@ describe("CCA thought summary provenance and replay", () => { const payload = { response: { candidates: [{ content: { parts: [ { thought: true, text: "Provider summary", thoughtSignature: signature }, { functionCall: { name: "lookup", args: {} } }, - { text: "Final answer" }, ] }, finishReason: "STOP" }], usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 2 } } }; const events: AdapterEvent[] = []; if (stream) { @@ -681,15 +680,24 @@ describe("CCA thought summary provenance and replay", () => { const call = events.find(event => event.type === "tool_call_start"); expect(call?.type === "tool_call_start" && call.providerMetadata?.google?.thoughtSignature).toBe(signature); expect(events.at(-1)?.type).toBe("done"); - const output = buildResponseJSON(events, parsed.modelId); - expect(JSON.stringify(output)).toContain('"summary_text","text":"Provider summary"'); - expect(JSON.stringify(output)).toContain("Final answer"); + let output: Record; + if (stream) { + async function* replay() { yield* events; } + const text = await new Response(bridgeToResponsesSSE(replay(), parsed.modelId, { hideThinkingSummary })).text(); + const payloads = text.split("\n").filter(line => line.startsWith("data: {")).map(line => JSON.parse(line.slice(6))); + output = payloads.find(frame => frame.type === "response.completed").response; + expect(text.includes("response.reasoning_summary_text.delta")).toBe(!hideThinkingSummary); + } else output = buildResponseJSON(events, parsed.modelId, { hideThinkingSummary }); + expect(JSON.stringify(output).includes("Provider summary")).toBe(!hideThinkingSummary); + if (!Array.isArray(output.output)) throw new Error("missing Responses output"); const continuation = parseRequest({ model: parsed.modelId, input: [ - ...(output.output as unknown[]), { type: "function_call_output", call_id: call && "id" in call ? call.id : "", output: "result" }, + ...output.output, { type: "function_call_output", call_id: call && "id" in call ? call.id : "", output: "result" }, ] }); const next = JSON.parse((await withTestTranslatorBudget(createGoogleAdapter(cca)).buildRequest(continuation)).body); const parts = next.request.contents.flatMap((turn: { parts: unknown[] }) => turn.parts); expect(parts).toContainEqual(expect.objectContaining({ functionCall: expect.objectContaining({ name: "lookup" }), thoughtSignature: signature })); + expect(parts).toContainEqual(expect.objectContaining({ functionResponse: expect.objectContaining({ name: "lookup", response: { result: "result" } }) })); + expect(JSON.stringify(next)).not.toContain("no tool result"); }); test("reused adapter resets Gemini summary provenance for a CCA non-Gemini model", async () => { diff --git a/tests/responses/responses-show-thinking-summary.test.ts b/tests/responses/responses-show-thinking-summary.test.ts index a12ad8dc83..a749518b63 100644 --- a/tests/responses/responses-show-thinking-summary.test.ts +++ b/tests/responses/responses-show-thinking-summary.test.ts @@ -96,7 +96,7 @@ describe("showThinkingSummary provider option", () => { }); for (const stream of [false, true]) for (const [summary, providerFlag, visible] of [ - [undefined, undefined, true], ["none", true, false], [undefined, false, false], ["auto", false, true], + [undefined, undefined, true], ["none", true, false], [undefined, false, false], ["auto", false, false], ["auto", true, true], ] as const) test(`CCA summary=${summary} provider=${providerFlag} stream=${stream}`, async () => { const home = mkdtempSync(join(tmpdir(), "ocx-show-thinking-")); const prevHome = process.env.OPENCODEX_HOME; @@ -120,10 +120,11 @@ describe("showThinkingSummary provider option", () => { globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { seen.push(String(input)); requests.push(JSON.parse(String(init?.body))); + const requestedThoughts = requests.at(-1)?.request.generationConfig?.thinkingConfig?.includeThoughts === true; const payload = { response: { candidates: [{ - content: { parts: [{ thought: true, text: "cca-think" }, { text: "OK" }] }, + content: { parts: [...(requestedThoughts ? [{ thought: true, text: "cca-think" }] : []), { text: "OK" }] }, finishReason: "STOP", }], usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5, totalTokenCount: 15, thoughtsTokenCount: 3 }, From 97a7d9c0f8b9ae3dae9ce87c439e4e84a5564216 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:46:14 +0900 Subject: [PATCH 08/21] test(google): pass summary visibility to SSE bridge options --- tests/adapters/google/google-adapter.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/adapters/google/google-adapter.test.ts b/tests/adapters/google/google-adapter.test.ts index f4b53960a5..1dd5a7c97a 100644 --- a/tests/adapters/google/google-adapter.test.ts +++ b/tests/adapters/google/google-adapter.test.ts @@ -683,7 +683,8 @@ describe("CCA thought summary provenance and replay", () => { let output: Record; if (stream) { async function* replay() { yield* events; } - const text = await new Response(bridgeToResponsesSSE(replay(), parsed.modelId, { hideThinkingSummary })).text(); + const text = await new Response(bridgeToResponsesSSE(replay(), parsed.modelId, + undefined, undefined, undefined, undefined, undefined, { hideThinkingSummary })).text(); const payloads = text.split("\n").filter(line => line.startsWith("data: {")).map(line => JSON.parse(line.slice(6))); output = payloads.find(frame => frame.type === "response.completed").response; expect(text.includes("response.reasoning_summary_text.delta")).toBe(!hideThinkingSummary); From 53021fb04b8088e4ce273ae4b175b7385266597f Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:36:58 +0900 Subject: [PATCH 09/21] docs: plan thinking presentation and transport carries --- .../260912_thinking_contract/000_plan.md | 31 ++ .../260912_thinking_contract/001_sources.md | 7 + .../010_presentation.md | 316 +++++++++++++++++ .../020_transport_hint.md | 319 ++++++++++++++++++ .../260912_thinking_contract/030_spark.md | 66 ++++ .../260912_thinking_contract/040_delivery.md | 7 + 6 files changed, 746 insertions(+) create mode 100644 devlog/_plan/260912_thinking_contract/000_plan.md create mode 100644 devlog/_plan/260912_thinking_contract/001_sources.md create mode 100644 devlog/_plan/260912_thinking_contract/010_presentation.md create mode 100644 devlog/_plan/260912_thinking_contract/020_transport_hint.md create mode 100644 devlog/_plan/260912_thinking_contract/030_spark.md create mode 100644 devlog/_plan/260912_thinking_contract/040_delivery.md diff --git a/devlog/_plan/260912_thinking_contract/000_plan.md b/devlog/_plan/260912_thinking_contract/000_plan.md new file mode 100644 index 0000000000..86a87f9d36 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/000_plan.md @@ -0,0 +1,31 @@ +# Preserve reasoning provenance and transport intent + +Readers: maintainers choosing whether to integrate the thinking lane. Raw reasoning must remain content, while provider-authored summaries can be displayed under an explicit provider default. The plan reconciles #4301 and #4287, separately reviews #3652 hint suppression, and carries #4130 Spark compatibility without retirement. + +Loop: satisfy-spec HOTL, triggered by authorized thinking-lane delivery. Goal: reviewable carry PRs and final-head hosted CI. Non-goals: merges, closure of source PRs, retirement #4334, releases, user service/config changes, other worktrees. All local product suites/build/typecheck/install are NOT RUN by instruction. Only available existing credentials/tools are used; no user token/time/agent ceiling was set. Stop: every source PR has a justified disposition and every delivered branch has exact-head hosted CI evidence. Outcomes: DONE on evidence, HOLD/NEEDS_HUMAN on explicit unresolved acceptance, never fake green. Escalation: real tool denial or requirement beyond scope; main reclaims after two distinct reviewer failures. Native architect selector is unavailable; inherited independent design review and reflection follow the user instruction, with a separate A audit. + +## Dependency map + +| Cycle | Artifact | Result | +| --- | --- | --- | +| roadmap | this file and all decade docs | docs-only plan lock | +| presentation | 010_presentation.md | raw/summary contract and provider opt-in | +| hint | 020_transport_hint.md | independent transport-hint disposition/carry | +| spark | 030_spark.md | independent Spark Lite carry | +| delivery | 040_delivery.md | final heads, review closure and hosted CI | + +Presentation combines two conflicting source proposals into one contract. Hint and Spark are independent and receive ordinary dev-based PRs, not artificial stack dependencies. Final review consumes all branches. No GitHub native stacks are requested. + +## Evidence and owner map + +Baseline origin/dev: 69e3dcda755a52feb1327edad6c8ea6cefd6e871. Source PR heads: #4301 5d6d1862a11da6e4d0c04eb7f35f9f48ae1285fd; #4287 fe13bdb7bf8403a2a2cdb10f258a68b649177953; #3652 13fb263778e9036e66ae86d41e29f9f47bbbed92; #4130 5d56f5461ea3d18668b85f6bb0d8a523920f2536. All open when inspected. Original authors: Robin Bially, yxr1995-maker, itismyfield, luvs01; exact Git trailers will be read from original commits before carrying. + +Current owners: src/bridge.ts:663 raw-reasoning finalization; src/adapters/google.ts:571 shared part classifier; src/server/responses/core.ts:2490 final-route normalization; src/responses/parser.ts:543 summary omission policy; src/types/request.ts:310 AdapterEvent. Reuse these boundaries; no new event enum or generic service layer. Structure INDEX maps shared areas to topical documents; main contracts are providers/chat-compat.md, providers/google.md, transports/responses.md and config.md with references from affected area owners. + +Verification: git diff --check was run at baseline and exited 0, checking diff whitespace only. GitHub ci.yml workflow_dispatch lane=all reads checkout source, typechecks, runs product suites and cross-platform jobs; NOT RUN locally. Every conditional scenario is named in decade docs and must be asserted in committed regression tests. Source inspection is not runtime proof. + +## Cycle records + +Roadmap P: requirements/source inspection and independent design review in progress. No product patch applied. + +Roadmap B: locked amended contract after independent A PASS and both design reflections ALIGNED. Product implementation starts in the next cycle. diff --git a/devlog/_plan/260912_thinking_contract/001_sources.md b/devlog/_plan/260912_thinking_contract/001_sources.md new file mode 100644 index 0000000000..f05813eae8 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/001_sources.md @@ -0,0 +1,7 @@ +# Source decisions + +Public PR diffs and latest comments are the source proposal evidence. #4130's September 11 corrections pin Lite on for nonempty additional_tools bodies and off otherwise; adopting the earlier unconditional false version loses tools. #4334 is an explicit retirement HOLD and is not carried. + +#4301 removes automatic content-to-summary conversion. #4287 tests raw DeepSeek content as a visible summary; that expectation conflicts with provenance and will be replaced, not adopted. Google thought-summary API documentation distinguishes summaries from opaque thought signatures: https://ai.google.dev/gemini-api/docs/generate-content/thinking (opened 2026-09-12). CCA generationConfig/includeThoughts behavior is contributor probe evidence, not a newly performed live-service probe. + +Searches used: reasoning_raw_delta, thinking_delta, hideThinkingSummary, googlePartTextEvent, preserveReasoningContent, and the four PR numbers. Existing bridge event types can distinguish raw content and summary without a new enum. No-code/config-only options cannot repair the existing mislabeled content; raw rewrite deletion plus existing boundaries is the smallest change. diff --git a/devlog/_plan/260912_thinking_contract/010_presentation.md b/devlog/_plan/260912_thinking_contract/010_presentation.md new file mode 100644 index 0000000000..e39e9c0bfe --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/010_presentation.md @@ -0,0 +1,316 @@ +# Presentation contract + +Class C4 public protocol contract. Depends on roadmap lock. MODIFY src/bridge.ts: closeCurrentRawReasoning and reasoning_raw_delta emit response.reasoning_text.delta/done with content_index:0; final items use summary:[] and content:[{type:reasoning_text,text}]. buildResponseJSONWithBudget mirrors this. Keep hidden txt-only replay envelopes intact. DELETE src/server/responses-reasoning-summary-rewrite.ts and its obsolete unit test; MODIFY core.ts to remove imports and SSE/JSON content-to-summary rewrites. MODIFY both layout manifests to remove that test. Adopt the exact #4301 hunks below except reporter video and historical verification record. + +MODIFY provider.ts, registry.ts, derive.ts, router.ts and auth-cors.ts to carry showThinkingSummary boolean (preserve explicit false). Seed only google-antigravity true. Creation: provider config/registry; serialization: providerConfigSeed and deriveKeyLoginMap; deserialization: config provider passthrough and management field policy; consumers: routedProviderConfig, final-route normalization, Google request builder. No new enum. + +MODIFY core.ts final-route normalization: apply provider default only when original reasoning.summary is omitted, never explicit none; recompute on each final route so fallback cannot inherit another provider default. Provider opt-in authorizes summary display, not raw-to-summary conversion. + +MODIFY google.ts shared part classifier to use existing thinking_delta only for Gemini thought summaries under verified Gemini model provenance; CCA Claude/gpt-oss thought text remains reasoning_raw_delta. Persist request-local Gemini identity using existing adapter state, used by both stream and buffered classifier calls. includeThoughts stays provider-opted, Gemini-only, non-image and explicit-hide aware. MODIFY google-wire-compiler.ts to retain only boolean true includeThoughts, independently of thinkingLevel. Do not claim raw text is an actual summary. + +MODIFY the #4287 end-to-end fixture: raw DeepSeek content remains content with empty summary even under provider opt-in; actual CCA Gemini thought parts use summary; omitted vs none vs auto, explicit provider false, saved-row enrichment, fallback reset, streaming/buffered paths. Extend existing Google tests and bridge raw tests; both layout manifests register responses-show-thinking-summary.test.ts. Update English providers docs and structure owners, keeping locale statements consistent. Source tests are authored but run only by hosted CI. + +Acceptance: raw event fixture => content delta and no summary delta; actual Gemini summary fixture => summary only when requested/provider-opted; explicit none => no synthesized summary and no includeThoughts request; false/unknown provider => no opt-in; fallback to unopted route => hidden behavior reset; replay envelope decodes same raw text and tool continuation remains valid; native Responses mixed content/summary remains byte-semantically native. No model prose synthesizer is introduced. + +## Source patch blueprint + +```diff +diff --git a/src/bridge.ts b/src/bridge.ts +index 20e7c3fe09..bc90f35b94 100644 +--- a/src/bridge.ts ++++ b/src/bridge.ts +@@ -663,16 +663,13 @@ export function bridgeToResponsesSSE( + const closeCurrentRawReasoning = () => { + if (!currentRawReasoning) return; + rawReasoningForNextToolCall = currentRawReasoning.text; +- emit("response.reasoning_summary_text.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text, +- }); +- emit("response.reasoning_summary_part.done", { +- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, +- part: { type: "summary_text", text: currentRawReasoning.text }, ++ emit("response.reasoning_text.done", { ++ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text, + }); + const item = { + type: "reasoning", id: currentRawReasoning.itemId, +- summary: [{ type: "summary_text", text: currentRawReasoning.text }], ++ summary: [] as never[], ++ content: [{ type: "reasoning_text", text: currentRawReasoning.text }], + }; + emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item }); + retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning"); +@@ -1111,10 +1108,6 @@ export function bridgeToResponsesSSE( + const itemId = `rs_${uuid()}`; + const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] }; + emit("response.output_item.added", { output_index: outputIndex, item }); +- emit("response.reasoning_summary_part.added", { +- item_id: itemId, output_index: outputIndex, summary_index: 0, +- part: { type: "summary_text", text: "" }, +- }); + currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 }; + } + ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString( +@@ -1123,9 +1116,13 @@ export function bridgeToResponsesSSE( + event.text, + "reasoning", + )); +- emit("response.reasoning_summary_text.delta", { ++ // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT ++ // channel, matching native gpt-oss passthrough: Codex applies its own display ++ // policy, so the desktop band shows the "Thinking…" placeholder instead of the ++ // raw CoT (the #45 summary-channel display intent is intentionally reverted). ++ emit("response.reasoning_text.delta", { + item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, +- summary_index: 0, delta: event.text, ++ content_index: 0, delta: event.text, + }); + break; + } +@@ -1780,7 +1777,8 @@ function buildResponseJSONWithBudget( + } + pushOutput({ + type: "reasoning", id: `rs_${uuid()}`, +- summary: [{ type: "summary_text", text: currentRawReasoning }], ++ summary: [], ++ content: [{ type: "reasoning_text", text: currentRawReasoning }], + }, currentRawReasoningBytes, "reasoning"); + currentRawReasoning = ""; + currentRawReasoningBytes = 0; + +``` + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/google-wire-compiler.ts b/src/adapters/google-wire-compiler.ts +index 88c482ba7d..aa835e50b4 100644 +--- a/src/adapters/google-wire-compiler.ts ++++ b/src/adapters/google-wire-compiler.ts +@@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined { + ))].slice(0, 5); + if (stopSequences.length > 0) out.stopSequences = stopSequences; + } +- if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") { +- const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); +- const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) +- ? raw +- : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); +- if (thinkingLevel) out.thinkingConfig = { thinkingLevel }; ++ if (isObject(value.thinkingConfig)) { ++ const thinking: JsonObject = {}; ++ if (typeof value.thinkingConfig.thinkingLevel === "string") { ++ const raw = value.thinkingConfig.thinkingLevel.toLowerCase(); ++ const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw) ++ ? raw ++ : (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined); ++ if (thinkingLevel) thinking.thinkingLevel = thinkingLevel; ++ } ++ // The one key that makes Google return `thought: true` text. Cloud Code Assist serves ++ // thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the ++ // request opts in, so dropping it here silently reinstates the missing-thinking behavior. ++ if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true; ++ if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking; + } + if (Array.isArray(value.responseModalities)) { + const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m)); +diff --git a/src/adapters/google.ts b/src/adapters/google.ts +index 7fcc88ba59..5a6675f6e4 100644 +--- a/src/adapters/google.ts ++++ b/src/adapters/google.ts +@@ -866,11 +866,27 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte + ); + antigravityModel = wireModelId; + antigravitySession = sessionId; ++ // Gemini returns no chain-of-thought TEXT unless the request opts in. Probed against CCA ++ // 2026-09-12: `gemini-3.8-flash-high` answered with thoughtsTokenCount=321 and zero ++ // `thought` parts, then 358-652 chars of genuine reasoning once includeThoughts was set. ++ // Scoped to Gemini wire ids — Claude-on-CCA accepts the flag but never returns thought ++ // parts, and gpt-oss rejects it outright (400 INVALID_ARGUMENT, which would break every ++ // gpt-oss turn). Gated on the provider's visible-thinking opt-in so a user who wants ++ // thinking hidden does not pay conversation-history tokens for text nobody renders; ++ // `hideThinkingSummary !== true` is the same per-request gate the response path uses, so ++ // a client that explicitly asked for hidden thinking is not billed for the text either. ++ const includeThoughts = provider.showThinkingSummary === true ++ && parsed.options.hideThinkingSummary !== true ++ && /^gemini-/.test(wireModelId) ++ && !isImageCapableModel(parsed.modelId); + // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig). + // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction. +- if (thinkingLevel) { ++ if (thinkingLevel || includeThoughts) { + const gc = (body.generationConfig ?? {}) as Record; +- gc.thinkingConfig = { thinkingLevel }; ++ gc.thinkingConfig = { ++ ...(thinkingLevel ? { thinkingLevel } : {}), ++ ...(includeThoughts ? { includeThoughts: true } : {}), ++ }; + body.generationConfig = gc; + } + // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity +diff --git a/src/providers/derive.ts b/src/providers/derive.ts +index 67e6c0522e..7edf28787b 100644 +--- a/src/providers/derive.ts ++++ b/src/providers/derive.ts +@@ -43,6 +43,7 @@ export interface DerivedKeyLoginProvider { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -267,6 +268,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -315,6 +317,7 @@ export function deriveKeyLoginMap(): Record { + ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}), + ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}), + ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}), ++ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}), + ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}), + ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}), + ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}), +@@ -567,6 +570,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig + if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels]; + if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels]; + if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames; ++ if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary; + if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional; + if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier; + if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip; +diff --git a/src/providers/registry.ts b/src/providers/registry.ts +index f72bb7650b..e483e9db23 100644 +--- a/src/providers/registry.ts ++++ b/src/providers/registry.ts +@@ -343,6 +343,10 @@ export interface ProviderRegistryEntry { + autoToolChoiceOnlyModels?: string[]; + preserveReasoningContentModels?: string[]; + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt this provider into visible thinking summaries (see OcxProviderConfig.showThinkingSummary). ++ */ ++ showThinkingSummary?: boolean; + reasoningSplitModels?: string[]; + reasoningDetailsModels?: string[]; + thinkingToggleModels?: string[]; +@@ -367,7 +371,7 @@ export type ProviderConfigSeed = Pick< + | "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens" + | "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat" + | "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels" +- | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" ++ | "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary" + | "googleMode" | "project" | "location" | "headers" + >; + +@@ -2045,7 +2049,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ + // path must stay RELATIVE: this row sets `allowBaseUrlOverride`, and an absolute `url` would + // retarget a user's custom base back to Google. A leading `./` is required because a bare + // `v1internal:` reads as a URL scheme and `providerModelDiscoverySpecError` rejects it. +- { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, ++ { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", showThinkingSummary: true, jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } }, + { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, + { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, + { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, +diff --git a/src/router.ts b/src/router.ts +index 55a0326fce..bf2b9b4b98 100644 +--- a/src/router.ts ++++ b/src/router.ts +@@ -410,6 +410,13 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider + ...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined + ? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent } + : {}), ++ // The request path resolves through routedProviderConfig() and never calls ++ // enrichProviderFromRegistry(), so a saved provider row written before the ++ // registry learned this flag must be backfilled here or route.provider never ++ // carries it and the showThinkingSummary opt-in stays dead. ++ ...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined ++ ? { showThinkingSummary: registryEntry.showThinkingSummary } ++ : {}), + // Registry-only client-facing repair policy (#938): fill only when the + // saved provider has no explicit policy; clone so runtime never aliases + // the registry constant. +diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts +index 3a93246cd0..93377b5573 100644 +--- a/src/server/auth-cors.ts ++++ b/src/server/auth-cors.ts +@@ -885,6 +885,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { + autoToolChoiceOnlyModels: "editor", + preserveReasoningContentModels: "editor", + requiresReasoningPlaceholderModels: "editor", ++ showThinkingSummary: "editor", + retryOn429: "editor", + transientRetryOn5xx: "editor", + reasoningSplitModels: "editor", +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index cccd942026..852cd9f8b0 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -2467,6 +2467,20 @@ async function resolveSubagentFallbackModelEligibility(args: { + }; + } + ++/** ++ * Whether the client explicitly asked for hidden thinking (`reasoning.summary: "none"`). ++ * ++ * Pinned: parseRequest collapses "omitted" and "none" into one hideThinkingSummary ++ * flag, so the raw request body is the ONLY place that still distinguishes them. ++ * Provider opt-ins like showThinkingSummary must consult this — never the flag ++ * alone — or a future caller that copies only the flag would silently unlock an ++ * explicit opt-out. ++ */ ++function clientExplicitlyHidThinking(parsed: OcxParsedRequest): boolean { ++ const rawReasoning = (parsed._rawBody as { reasoning?: { summary?: unknown } } | undefined)?.reasoning; ++ return typeof rawReasoning === "object" && rawReasoning !== null ++ && (rawReasoning as { summary?: unknown }).summary === "none"; ++} + /** + * Apply every route-dependent request mutation against the final selected route. + * Must run only after subagent fallback has settled the model/provider. +@@ -2508,6 +2522,15 @@ async function applyFinalRouteRequestNormalization(args: { + // this request will actually use (#404). + route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req)); + route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire); ++ // Provider-opted visible thinking (e.g. google-antigravity): parseRequest hides thinking ++ // whenever the client omits reasoning.summary, which is the Codex default. A provider that ++ // serves genuine user-facing reasoning opts back into the summary channel here, so thought ++ // parts (Gemini thought, content-channel reasoning_text) reach the client instead of only ++ // the hidden replay envelopes. An explicit client reasoning.summary "none" still wins. ++ if (route.provider.showThinkingSummary === true && parsed.options.hideThinkingSummary === true ++ && !clientExplicitlyHidThinking(parsed)) { ++ parsed.options.hideThinkingSummary = false; ++ } + if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId; + logCtx.model = route.modelId; + logCtx.provider = route.providerName; +diff --git a/src/types/provider.ts b/src/types/provider.ts +index e65130a4fa..b6374a991a 100644 +--- a/src/types/provider.ts ++++ b/src/types/provider.ts +@@ -746,6 +746,15 @@ export interface OcxProviderConfig { + * out explicitly (e.g. MiniMax, where low effort disables thinking). + */ + requiresReasoningPlaceholderModels?: string[]; ++ /** ++ * Opt-in: surface upstream thinking as visible reasoning summaries even when the ++ * client did not send `reasoning.summary`. parseRequest hides thinking by default ++ * (Codex omits the field), which strands genuine reasoning — e.g. Gemini `thought` ++ * parts on the google-antigravity (Cloud Code Assist) wire — in hidden replay ++ * envelopes. An explicit client `reasoning.summary: "none"` still wins. Set `false` ++ * to opt a seeded preset back out. ++ */ ++ showThinkingSummary?: boolean; + /** + * Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only, + * openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays + +``` + +## Reflection corrections accepted + +Explicit wire reasoning.summary:"none" wins. A client that serializes configured none as omission cannot be distinguished from unspecified preference. No client config rewrite or global catalog summary default changes. Summary classification is limited to built CCA Gemini requests; unknown/uninitialized, direct Google/Vertex and CCA Claude/gpt-oss remain raw. Streaming and buffered summary-to-tool continuations assert exact Google signature on correct call; never emit Google signatures as Anthropic thinking_signature. Hidden unsigned summaries may disappear but required tool replay state survives. Exercise final assistant text and terminal order, fallback in both directions, and remove replay-comparison rewrite alongside SSE/JSON rewrite. Desktop appearance remains client-controlled; source patch comments claiming an unconditional placeholder are replaced during adoption. diff --git a/devlog/_plan/260912_thinking_contract/020_transport_hint.md b/devlog/_plan/260912_thinking_contract/020_transport_hint.md new file mode 100644 index 0000000000..335e339a00 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/020_transport_hint.md @@ -0,0 +1,319 @@ +# Optional hint suppression + +Class C4 review because client metadata policy changes. Independent of presentation; depends only on roadmap. Adopt #3652 only after independent security/transport review. Public proposal removes exactly two x-codex-safety-buffering headers, metadata.type=safety_buffering events and top-level safety_buffering fields at the client relay boundary. Default false; malformed config must remain off and candidate validation rejects nonbooleans. This suppresses optional transport hints; provider safety decisions/refusals and upstream checks are unchanged. Compact and independent WS/other-provider pathways retain existing policy unless a directly exercised shared boundary already applies. + +MODIFY src/config.ts and src/types/config.ts for validated boolean/default; src/server/relay.ts for allowlisted header removal and SSE terminal-boundary transformation; relay-eager.ts for option forwarding; core.ts to compute option only for canonical OpenAI forward destination and pass it to all relevant headers/client output boundaries; index.ts exports if needed by existing test style. Do not apply to custom gateway/key providers. Preserve errors, response.failed/incomplete and terminal sentinel handling. + +MODIFY tests/responses/passthrough-headers.test.ts, openai-responses-passthrough.test.ts and tests/server/config.test.ts. Scenarios: absent/false/true/malformed config; uppercase headers; unrelated headers; split metadata frames; actual failure carrying hint must still fail; noncanonical provider has identical fields and retains them; eager/non-eager client paths. Add missing canonical route coverage if independent review identifies it. MODIFY English/ja/ko/ru/zh-cn server configuration docs and structure owners. Avoid unsupported claims about models being weaker or provider safety bypass. + +Before/after anchor: createSseTerminalOutputBoundary() -> createSseTerminalOutputBoundary(options?: CodexSafetyBufferingFilterOptions); sanitizePassthroughHeaders(upstream) -> sanitizePassthroughHeaders(upstream, options?); canonical true => filter option, every other provider => undefined. Full public source diff is pinned by #3652 head in 000_plan.md and inspected locally; any needed correction is recorded here before B. + +## Independent design corrections + +H1 accepted: policy rewrite and hint stripping compose. Build policyFailurePayload first, then remove top-level safety_buffering from the effective emitted payload, preserving response.failed/error data and retryable:false. H2 accepted: extend current relaySseWithFailedTail fourth options object with terminalBoundary; never replace upstreamError. Core passes both existing upstreamError and new terminalBoundary; update the existing source-contract assertion to preserve its original guarantee. H3 accepted: native WebSocket codex.response.metadata.headers and /responses/compact are explicitly excluded; their hints remain unfiltered. No new WS metadata filter. Docs must not claim the old WS allowlist excludes these headers. Regression fixtures cover CRLF/split/malformed input, policy error plus hint, EOF upstreamError, canonical true and noncanonical preservation. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/config.ts b/src/config.ts +index fdcda9547c..cd0641feb2 100644 +--- a/src/config.ts ++++ b/src/config.ts +@@ -1125,6 +1125,8 @@ const configSchema = z.object({ + configRebaseProvenance: z.unknown().optional(), + // A retry can be billable, so absence and malformed hand edits both stay off. + emptyCompletionRetry: z.boolean().optional().catch(false), ++ // Header suppression changes what Codex sees, so absence and malformed edits stay off. ++ dropCodexSafetyBuffering: z.boolean().optional().catch(false), + // A malformed hand edit must not silently stop opening the browser: fall back + // to undefined, which resolves to the historical auto-open behavior. + oauthOpenBrowser: z.boolean().optional().catch(undefined), +@@ -2613,6 +2615,14 @@ function emptyCompletionRetryError(value: unknown): string | null { + return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted"; + } + ++function dropCodexSafetyBufferingError(value: unknown): string | null { ++ const raw = rawConfigRecord(value); ++ if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null; ++ const enabled = raw.dropCodexSafetyBuffering; ++ if (enabled === undefined || typeof enabled === "boolean") return null; ++ return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted"; ++} ++ + function oauthOpenBrowserError(value: unknown): string | null { + const raw = rawConfigRecord(value); + if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null; +@@ -2718,6 +2728,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx + ?? codexQuotaAutoRefreshError(value) + ?? codexAccountPickerEnabledError(value) + ?? emptyCompletionRetryError(value) ++ ?? dropCodexSafetyBufferingError(value) + ?? oauthOpenBrowserError(value) + ?? runtimeRoleError(value) + ?? remoteGuiConfigError(value) +@@ -3684,6 +3695,7 @@ export function getDefaultConfig(): OcxConfig { + return { + port: 10100, + emptyCompletionRetry: false, ++ dropCodexSafetyBuffering: false, + managementUsageMaxReadBytes: 64 * 1024 * 1024, + appOwnedMemoryBudgetMb: DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES / (1024 * 1024), + // Fresh/re-initialized configs are already written in the current three-tier +diff --git a/src/server/index.ts b/src/server/index.ts +index aedd6bf236..c6ce73b2f1 100644 +--- a/src/server/index.ts ++++ b/src/server/index.ts +@@ -142,6 +142,7 @@ import { + } from "./relay"; + export { + consumeForInspection, ++ codexSafetyBufferingFilterOptions, + relaySseWithFailedTail, + relaySseWithHeartbeat, + relayWithAbort, +diff --git a/src/server/relay-eager.ts b/src/server/relay-eager.ts +index 655997b813..a6e60d3d02 100644 +--- a/src/server/relay-eager.ts ++++ b/src/server/relay-eager.ts +@@ -26,6 +26,7 @@ + + import { + adapterEofIncompleteFrame, ++ type CodexSafetyBufferingFilterOptions, + createSseTerminalOutputBoundary, + doneFrame, + failedTailFrame, +@@ -83,6 +84,8 @@ export type EagerRelayOptions = { + postCancelDrainBytes?: number; + /** Injectable clock for tests. */ + now?: () => number; ++ /** Client output boundary filters (Codex safety-buffering hints). */ ++ terminalBoundary?: CodexSafetyBufferingFilterOptions; + }; + + const DEFAULT_MAX_QUEUE_BYTES = 8 * 1024 * 1024; +@@ -111,7 +114,7 @@ export function relaySseEagerBounded( + const terminalEncoder = new TextEncoder(); + const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder); + const terminalSentinel = doneFrame(terminalEncoder); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary); + const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks + ?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined); + const encodeFailedTail = (error: unknown): Uint8Array | null => { +diff --git a/src/server/relay.ts b/src/server/relay.ts +index 60b57ea025..d840b2e59c 100644 +--- a/src/server/relay.ts ++++ b/src/server/relay.ts +@@ -162,7 +162,10 @@ export type SseTerminalOutputBoundary = { + * terminal, and drops every later block/byte. A premature [DONE] is held until + * a terminal arrives so clean EOF can synthesize one terminal and one sentinel. + */ +-export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { ++export function createSseTerminalOutputBoundary( ++ options?: CodexSafetyBufferingFilterOptions, ++): SseTerminalOutputBoundary { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES); +@@ -181,6 +184,10 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + const payload = sseDataPayload(decoder.decode(frame.block)); + const isDone = payload === "[DONE]"; + const parsed = payload === null ? undefined : parseSsePayload(payload); ++ const safetyBuffering = dropSafetyBuffering && parsed !== undefined ++ ? codexSafetyBufferingBlockAction(parsed) ++ : "keep"; ++ if (safetyBuffering === "drop") continue; + const policyError = parsed !== undefined && isPolicyRewriteType(parsed) + ? cyberPolicyTerminalError(parsed) + : undefined; +@@ -189,7 +196,9 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary { + decoder.decode(frame.block), + policyFailurePayload(policyError, parsed), + )) +- : frame.block; ++ : safetyBuffering === "strip" ++ ? encoder.encode(stripCodexSafetyBufferingField(decoder.decode(frame.block), parsed)) ++ : frame.block; + if (isDone) { + done = true; + if (responsesTerminal) { +@@ -260,10 +269,11 @@ export function relaySseWithFailedTail( + body: ReadableStream, + upstream: AbortController, + onClientGone?: (reason?: unknown) => void, ++ boundaryOptions?: CodexSafetyBufferingFilterOptions, + ): ReadableStream { + const reader = body.getReader(); + const encoder = new TextEncoder(); +- const terminalBoundary = createSseTerminalOutputBoundary(); ++ const terminalBoundary = createSseTerminalOutputBoundary(boundaryOptions); + let closed = false; + const relayChunk = ( + controller: ReadableStreamDefaultController, +@@ -438,6 +448,29 @@ function isPolicyRewriteType(parsed: unknown): boolean { + return type === "response.failed" || type === "response.incomplete" || type === "error"; + } + ++/** ++ * Codex emits its safety-buffering hint in the SSE body as well as in headers: ++ * a `response.metadata` event whose `metadata.type` is `safety_buffering`, or a ++ * `safety_buffering` field on another event. The metadata event is dropped whole; ++ * the field is stripped so the carrying event is otherwise relayed unchanged. ++ */ ++function codexSafetyBufferingBlockAction(parsed: unknown): "keep" | "drop" | "strip" { ++ const root = asJsonRecord(parsed); ++ if (!root) return "keep"; ++ if (root.type === "response.metadata") { ++ const metadata = asJsonRecord(root.metadata); ++ if (metadata?.type === "safety_buffering") return "drop"; ++ } ++ return Object.hasOwn(root, "safety_buffering") ? "strip" : "keep"; ++} ++ ++function stripCodexSafetyBufferingField(block: string, parsed: unknown): string { ++ const root = asJsonRecord(parsed); ++ if (!root) return block; ++ const { safety_buffering: _safetyBuffering, ...rest } = root; ++ return replaceSseDataPayload(block, JSON.stringify(rest)); ++} ++ + function rewritePolicyTerminalBlock(block: string, payload: string): string { + const newline = block.includes("\r\n") ? "\r\n" : "\n"; + const rewritten = replaceSseDataPayload(block, payload); +@@ -1422,7 +1455,31 @@ export function consumeForResponseLogMetadata( + * body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough. + * Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim. + */ +-export function sanitizePassthroughHeaders(upstream: Headers): Headers { ++export const CODEX_SAFETY_BUFFERING_HEADERS = [ ++ "x-codex-safety-buffering-enabled", ++ "x-codex-safety-buffering-faster-model", ++] as const; ++ ++const CODEX_SAFETY_BUFFERING_HEADER_SET: ReadonlySet = new Set(CODEX_SAFETY_BUFFERING_HEADERS); ++ ++export interface CodexSafetyBufferingFilterOptions { ++ /** ++ * Drop Codex safety-buffering hints: the `x-codex-safety-buffering-*` response ++ * headers and the `safety_buffering` SSE metadata event / field. Absent and ++ * `false` relay everything unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; ++} ++ ++/** Resolve the passthrough header policy from the loaded config (absent means "forward everything"). */ ++export function codexSafetyBufferingFilterOptions( ++ config: { dropCodexSafetyBuffering?: boolean }, ++): CodexSafetyBufferingFilterOptions { ++ return { dropCodexSafetyBuffering: config.dropCodexSafetyBuffering === true }; ++} ++ ++export function sanitizePassthroughHeaders(upstream: Headers, options?: CodexSafetyBufferingFilterOptions): Headers { ++ const dropSafetyBuffering = options?.dropCodexSafetyBuffering === true; + const DROP = new Set([ + "content-encoding", + "content-length", +@@ -1439,7 +1496,10 @@ export function sanitizePassthroughHeaders(upstream: Headers): Headers { + ]); + const out = new Headers(); + upstream.forEach((value, key) => { +- if (!DROP.has(key.toLowerCase())) out.set(key, value); ++ const lower = key.toLowerCase(); ++ if (DROP.has(lower)) return; ++ if (dropSafetyBuffering && CODEX_SAFETY_BUFFERING_HEADER_SET.has(lower)) return; ++ out.set(key, value); + }); + return out; + } +diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts +index 9d0eea0d76..e199917968 100644 +--- a/src/server/responses/core.ts ++++ b/src/server/responses/core.ts +@@ -304,6 +304,7 @@ import { + markEagerRelaySseResponse, + markNativePassthroughSseResponse, + relaySseWithFailedTail, ++ codexSafetyBufferingFilterOptions, + relayWithAbort, + sanitizePassthroughHeaders, + } from "../relay"; +@@ -3850,6 +3851,9 @@ async function handleResponsesInner( + let hostAdmissionLease = pendingHostAdmissionLease; + pendingHostAdmissionLease = null; + try { ++ const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider) ++ ? codexSafetyBufferingFilterOptions(config) ++ : undefined; + const imageGenCallAliases = route.provider.authMode === "forward" + ? new Map() + : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget); +@@ -4732,7 +4736,7 @@ async function handleResponsesInner( + } + break; + } +- const headers = sanitizePassthroughHeaders(upstreamResponse.headers); ++ const headers = sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions); + const resolvedModel = headers.get("openai-model")?.trim(); + if (resolvedModel && !logCtx.preserveResolvedModelFromRoute) logCtx.resolvedModel = resolvedModel; + if (isUsageDebugEnabled()) { +@@ -4824,7 +4828,7 @@ async function handleResponsesInner( + return new Response(upstreamResponse.body, { + status: upstreamResponse.status, + statusText: upstreamResponse.statusText, +- headers: sanitizePassthroughHeaders(upstreamResponse.headers), ++ headers: sanitizePassthroughHeaders(upstreamResponse.headers, codexSafetyBufferingOptions), + }); + } + if (!upstreamResponse.ok) { +@@ -5027,6 +5031,7 @@ async function handleResponsesInner( + onDone: () => unregisterTurn(turnAc), + }, { + clientGoneSignal: options.abortSignal, ++ terminalBoundary: codexSafetyBufferingOptions, + ...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}), + }); + // When selected, this relay closes response.completed even if upstream +@@ -5110,7 +5115,8 @@ async function handleResponsesInner( + const rewrittenBody = clientBlockRewrite !== undefined + ? relaySseWithBlockRewrite(nativeBody, clientBlockRewrite, translatorBudget) + : nativeBody; +- const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason)); ++ const clientBody = relaySseWithFailedTail(rewrittenBody, upstream, reason => clientGone.abort(reason), ++ codexSafetyBufferingOptions); + return markNativePassthroughSseResponse(new Response(clientBody, { + status: upstreamResponse.status, + headers, +@@ -5238,7 +5244,7 @@ async function handleResponsesInner( + } + throw error; + } +- const sseHeaders = sanitizePassthroughHeaders(headers); ++ const sseHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions); + sseHeaders.set("content-type", "text/event-stream"); + sseHeaders.set("cache-control", "no-store"); + return new Response(stream, { +diff --git a/src/types/config.ts b/src/types/config.ts +index 8cf1246979..4d2c63fdf1 100644 +--- a/src/types/config.ts ++++ b/src/types/config.ts +@@ -335,6 +335,16 @@ export interface OcxConfig { + client?: OcxClientConnectionConfig; + /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */ + emptyCompletionRetry?: boolean; ++ /** ++ * Drop the Codex safety-buffering hints from a Codex Responses passthrough: the ++ * `x-codex-safety-buffering-*` response headers, `response.metadata` SSE events of ++ * type `safety_buffering`, and the `safety_buffering` field on other SSE events. ++ * The Codex TUI turns those hints into a "retry with a faster model" prompt whose ++ * default action switches the session to a weaker model, so an unattended session ++ * can lose its model to a stray keystroke. Absent and `false` relay everything ++ * unchanged. ++ */ ++ dropCodexSafetyBuffering?: boolean; + /** + * Whether a login may open a browser on the machine running the proxy. + * + +``` diff --git a/devlog/_plan/260912_thinking_contract/030_spark.md b/devlog/_plan/260912_thinking_contract/030_spark.md new file mode 100644 index 0000000000..3997aaa6d7 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/030_spark.md @@ -0,0 +1,66 @@ +# Spark Lite metadata follows body shape + +Class C3 bounded compatibility. Independent of presentation/hint; depends on roadmap. MODIFY src/adapters/openai-responses.ts only inside canonical OpenAI forwarding and final wire model gpt-5.3-codex-spark. Add bodyCarriesLiteToolShape next to existing tool-shape helpers: Array.isArray(body.input) && body.input.some(item => isPlainObject(item) && item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.length > 0). After final Spark body construction, delete all case variants of CODEX_RESPONSES_LITE_HEADER then set it to liteShaped ? "true" : "false". Existing prepareCodexWsRequest projects it onto native frame metadata. + +Before: Spark deletes the header, allowing stale native metadata to survive. After: tool-less/top-level-tool Spark frames advertise false; nonempty Lite catalog frames advertise true despite conflicting inherited header. No retirement, no changes to model availability, no user service changes. + +MODIFY tests/codex-integration/codex-metadata-integrity.test.ts: alias resolved final model, inherited true/false/mixed-case/absent header, Lite tool body true, empty Lite group false, malformed metadata keeps HTTP fallback/body, noncanonical remains unchanged. MODIFY tests/responses/ws-upstream-reuse.test.ts: legacy true socket retires when adapter produces false, replacement same identity reused, raw request immutable. MODIFY all eight existing architecture locale pages and structure/transports/responses.md, referencing body-shape rule from shared area owners. Adopt latest #4130 source diff, preserving author; do not import historical earlier heads. + +Verification: source diff review and final-branch hosted ci.yml lane=all. Tests NOT RUN locally. Success proves framing and connection identity, not a live backend EOF fix or all tool-bearing EOF cases. Remaining acceptance: broader tool-format conversion stays out of scope. + +## Pinned source hunks (apply with corrections above) + +```diff +diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts +index c4aa523ee6..8fbe43816d 100644 +--- a/src/adapters/openai-responses.ts ++++ b/src/adapters/openai-responses.ts +@@ -864,6 +864,21 @@ function promoteClientLoadedTools(body: unknown): unknown { + } + + const MAX_RESPONSES_CALL_ID_LENGTH = 64; ++ ++/** ++ * Whether the outgoing body still delivers tools through the responses-lite shape. ++ * ++ * Lite carries the client catalog as an `additional_tools` input item; the non-Lite wire shape ++ * expects top-level `tools`. Anything that flips the Lite advertisement has to agree with the ++ * shape actually being sent, or the destination silently loses the tool surface. ++ */ ++function bodyCarriesLiteToolShape(body: Record): boolean { ++ if (!Array.isArray(body.input)) return false; ++ return body.input.some(item => ++ isPlainObject(item) && item.type === "additional_tools" ++ && Array.isArray(item.tools) && item.tools.length > 0 ++ ); ++} + const REPAIRED_CALL_ID_PREFIX = "call_ocx_"; + const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length; + +@@ -2515,12 +2530,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): + parsed.modelId, + ); + if (isCanonicalOpenAiForwardProvider(provider)) { +- // Spark closes Responses Lite streams before a terminal completion. Select compatibility +- // from the final wire model so aliases cannot leave the caller or a static header enabled. ++ // Select Spark's Lite compatibility from the final wire model, including aliases, and ++ // let the BODY decide it. The header also overrides native WS metadata downstream, so a ++ // forwarded or statically configured value must never contradict the shape being sent. ++ // ++ // The synchronized catalog keeps `use_responses_lite: true` for Spark precisely because ++ // it selects tool delivery (`input[].additional_tools` instead of top-level `tools`), and ++ // stripSparkCompatibility filters that group in place rather than promoting it. So a ++ // Lite-shaped body is pinned back ON — otherwise an inherited `false` advertises non-Lite ++ // while the tools exist only in the Lite shape, and Spark loses the tool surface. Only a ++ // body with no Lite tool group is downgraded, which is what the stream fix needs. + if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { ++ const liteShaped = bodyCarriesLiteToolShape(finalBody); + for (const name of Object.keys(headers)) { + if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; + } ++ headers[CODEX_RESPONSES_LITE_HEADER] = liteShaped ? "true" : "false"; + } + const routingHeaders = new Headers(headers); + applyCodexRoutingHint(routingHeaders, finalBody); + +``` diff --git a/devlog/_plan/260912_thinking_contract/040_delivery.md b/devlog/_plan/260912_thinking_contract/040_delivery.md new file mode 100644 index 0000000000..a70e62bfb6 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/040_delivery.md @@ -0,0 +1,7 @@ +# Final heads and handoff + +Class C3 delivery evidence. Depends on all dispositions. MODIFY branch-owned numbered completion docs and ignored .tmp/thinking/handoff.md. Read existing .github/PULL_REQUEST_TEMPLATE.md; write every section, credits and precise NOT RUN limitation. Publish only own codex/260912-60plus-thinking* branches with git push --no-verify; PR bases dev for independent units, ordinary parent branch only for actual dependencies. No merge/auto-merge/closures. + +NEW .tmp/thinking/*-ci.json captures gh run view JSON for final SHA plus all jobs. NEW .tmp/thinking/*-review.md captures independent implementation findings with accepted/rebutted disposition. Refresh head/base, native stack membership (unknown if API unsupported), current reviews and CI before handoff. Inspect .github/workflows/ci.yml and dispatch lane=all at each final branch where needed. Existing automatic runs stay untouched. If final-head CI fails, inspect failing logs, repair scoped source or fixtures, commit/push --no-verify and validate new final tip. Do not label skipped/cancelled/old-head runs passing. + +Final handoff fields: own worktree, branch per PR, source PR disposition, exact head, PR URL, dependency order, original author trailers, remaining acceptance, unresolved reviews, CI run id/url/head/result/job conclusions, own cycle records and local tests NOT RUN. Parent performs any subsequent integration. No evidence claims from peer commentary alone. From 2303f7883c764181989ec9d0859397b4969df804 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:57:20 +0900 Subject: [PATCH 10/21] fix(responses): align Spark Lite metadata with serialized body Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- .../260912_thinking_contract/030_spark.md | 8 + .../content/docs/fr/reference/architecture.md | 9 + .../content/docs/ja/reference/architecture.md | 9 + .../content/docs/ko/reference/architecture.md | 9 + .../content/docs/reference/architecture.md | 9 + .../content/docs/ru/reference/architecture.md | 9 + .../content/docs/tr/reference/architecture.md | 10 +- .../docs/zh-cn/reference/architecture.md | 7 + .../docs/zh-tw/reference/architecture.md | 8 + src/adapters/openai-responses.ts | 50 ++++-- structure/adapters/registry.md | 2 + structure/data-planes/inbound-compat.md | 2 + structure/ops/docs-and-release.md | 2 + structure/providers/chat-compat.md | 2 + structure/providers/cursor.md | 2 + structure/runtime.md | 2 + structure/transports/inventory.md | 2 + structure/transports/responses.md | 12 ++ .../codex-metadata-integrity.test.ts | 154 +++++++++++++++++- tests/responses/ws-upstream-reuse.test.ts | 30 ++++ 20 files changed, 319 insertions(+), 19 deletions(-) diff --git a/devlog/_plan/260912_thinking_contract/030_spark.md b/devlog/_plan/260912_thinking_contract/030_spark.md index 3997aaa6d7..3b013f75aa 100644 --- a/devlog/_plan/260912_thinking_contract/030_spark.md +++ b/devlog/_plan/260912_thinking_contract/030_spark.md @@ -64,3 +64,11 @@ index c4aa523ee6..8fbe43816d 100644 applyCodexRoutingHint(routingHeaders, finalBody); ``` + +## Spark P revalidation + +Prior D: hint source/security review PASS, final hosted tests pending. This independent branch starts from bd34120180. Latest #4130 hunks still apply cleanly. CCA summary and hint branches do not modify this adapter. Body-dependent Lite true/false, canonical final wire model and no retirement remain the acceptance contract. + +## Spark design reflection amendments + +S1 accepted: apply the existing modelSuffixBracketStrip normalization to finalBody before deciding Lite, using the same immutable object serialized later. A canonical gpt-5.3-codex-spark[1m] request that strips to Spark gets the policy; a final non-Spark model does not. S2 accepted: all eight architecture paragraphs say nonempty additional_tools tools array, not merely group presence; source PR outstanding documentation finding is addressed. S3 accepted: tests cover catalog filtered empty, surviving functions group, only top-level tools, both alias directions and preserved noncanonical configured Lite. Check both actual serialized body and WS header metadata; shape detection is not tool-support validation. diff --git a/docs-site/src/content/docs/fr/reference/architecture.md b/docs-site/src/content/docs/fr/reference/architecture.md index f197d85c11..746ea7f726 100644 --- a/docs-site/src/content/docs/fr/reference/architecture.md +++ b/docs-site/src/content/docs/fr/reference/architecture.md @@ -89,6 +89,15 @@ Par défaut, `server/index.ts` sert HTTP/SSE sur `/v1/responses`. Si Codex tente Indépendamment de ce réglage côté client, les requêtes canoniques transmises à ChatGPT avec `stream: true` à la racine peuvent utiliser le transport WebSocket en amont de Codex avec une version stable de Bun 1.4.0 ou ultérieure. La version intégrée Bun 1.3.14, les préversions et les identités de runtime impossibles à vérifier utilisent HTTP/SSE. Les réponses WS en amont qui réussissent conservent le contrat SSE en aval et contournent `tee()` au moyen d’un relais borné à lecteur unique et avide (4 MiB par trame brute/enveloppée et une file de production de 8 MiB). Le dépassement de la file ferme la connexion en amont et émet en aval un événement terminal `response.failed`, suivi de `[DONE]`. +Pour le modèle sortant final `gpt-5.3-codex-spark`, la transmission canonique à ChatGPT +désactive explicitement Responses Lite dans l’en-tête HTTP et les métadonnées natives des +trames WS, même lorsqu’un alias sélectionne Spark — uniquement si le corps sortant ne porte pas +de groupe `additional_tools` contenant un tableau `tools` non vide. Ce groupe EST la forme Lite de livraison des outils : un corps Spark +qui l’utilise conserve Lite ACTIF même si un en-tête appelant ou configuré disait l’inverse. Un changement d’identité Lite retire +l’ancien socket ; les requêtes admissibles suivantes ayant la même identité peuvent réutiliser +le nouveau socket. Les autres modèles et passerelles conservent leur politique Lite. +Des métadonnées natives mal formées entraînent toujours un repli HTTP, sans modifier le corps. + Le compactage du contexte Codex fonctionne avec les modèles routés. `server/responses/compact.ts` traite `POST /v1/responses/compact` en exécutant un tour interne de synthèse routé et en renvoyant un historique compacté, tandis que `responses/parser.ts` et `bridge.ts` traitent les tours de compactage distant v2 `compaction_trigger` en émettant exactement un élément de sortie synthétique `compaction`. ## Mise en cache et catalogue diff --git a/docs-site/src/content/docs/ja/reference/architecture.md b/docs-site/src/content/docs/ja/reference/architecture.md index cd2ce3db56..3500cbaf86 100644 --- a/docs-site/src/content/docs/ja/reference/architecture.md +++ b/docs-site/src/content/docs/ja/reference/architecture.md @@ -97,6 +97,15 @@ HTTP の境界は `server/index.ts` が担い、Responses データプレーン `server/index.ts` はデフォルトで `/v1/responses` を HTTP/SSE で提供します。`websockets` が `false` の状態で Codex が Responses WebSocket アップグレードを試みると、opencodex は `426 upgrade_required` を返し、Codex はそのセッションで HTTP にフォールバックします。`"websockets": true` を設定すると同じエンドポイントがアップグレードを受け入れ WebSocket ブリッジを使います。 +最終送信モデルが `gpt-5.3-codex-spark` の場合、canonical ChatGPT 転送は HTTP ヘッダーと +ネイティブ WS フレームのメタデータの両方で Responses Lite を明示的に無効にします。 +エイリアスで Spark を選択した場合も同様です。ただし無効化は、送信本文が空でない `tools` 配列を持つ `additional_tools` +項目を持たない場合に限ります。このグループ自体が Lite のツール受け渡し形式なので、それを +使う Spark 本文は呼び出し元や設定のヘッダーに関わらず Lite を有効のまま保ちます。Lite の識別値が変わると古いソケットは退役し、 +以後の条件を満たす同じ識別値のリクエストは新しいソケットを再利用できます。他のモデルと +ゲートウェイの Lite ポリシーは維持されます。不正なネイティブメタデータは引き続き、 +本文を変更せずに HTTP にフォールバックします。 + Codex コンテキスト compaction はルーティングされたモデルでも動作します。`server/responses/compact.ts` は `POST /v1/responses/compact` を内部ルーティング要約ターンとして扱い、圧縮されたヒストリーを返します。 `responses/parser.ts` と `bridge.ts` は remote compaction v2 の `compaction_trigger` ターンを扱い、合成 `compaction` 出力項目を正確に 1 つ送ります。 diff --git a/docs-site/src/content/docs/ko/reference/architecture.md b/docs-site/src/content/docs/ko/reference/architecture.md index fa3056fb49..d95bf391ad 100644 --- a/docs-site/src/content/docs/ko/reference/architecture.md +++ b/docs-site/src/content/docs/ko/reference/architecture.md @@ -127,6 +127,15 @@ envelope를 각각 4 MiB로 제한하고 8 MiB producer queue 상한이 있는 b relay를 거칩니다. queue overflow 시 업스트림을 닫고 downstream에는 terminal `response.failed` 이벤트와 `[DONE]`을 내보냅니다. +최종 전송 모델이 `gpt-5.3-codex-spark`이면 canonical ChatGPT forward 경로는 HTTP 헤더와 +네이티브 WS 프레임 메타데이터 모두에서 Responses Lite를 명시적으로 끕니다. 별칭으로 Spark를 +선택해도 동일합니다. 다만 이 비활성화는 전송 본문에 비어 있지 않은 `tools` 배열을 가진 `additional_tools` 항목이 없을 때만 +적용됩니다. 이 그룹 자체가 Lite의 도구 전달 형식이므로, 그것을 사용하는 Spark 본문은 호출자나 +설정 헤더가 무엇이든 Lite를 켠 상태로 유지합니다. Lite 식별값이 바뀌면 기존 소켓은 사용을 종료하며, 이후 같은 식별값으로 +재사용 조건을 충족하는 요청은 새 소켓을 재사용할 수 있습니다. 다른 모델과 게이트웨이는 기존 +Lite 정책을 유지합니다. 네이티브 메타데이터 형식이 잘못된 경우에는 본문을 바꾸지 않고 +기존처럼 HTTP로 폴백합니다. + Codex 컨텍스트 compaction은 라우팅된 모델에서도 동작합니다. `server/responses/compact.ts`는 `POST /v1/responses/compact`를 내부 라우팅 요약 턴으로 처리해 압축된 히스토리를 반환합니다. `responses/parser.ts`와 `bridge.ts`는 remote compaction v2의 `compaction_trigger` 턴을 처리해 diff --git a/docs-site/src/content/docs/reference/architecture.md b/docs-site/src/content/docs/reference/architecture.md index e0fbc8bcba..a9bdf48978 100644 --- a/docs-site/src/content/docs/reference/architecture.md +++ b/docs-site/src/content/docs/reference/architecture.md @@ -157,6 +157,15 @@ upstream WS responses keep the downstream SSE contract and bypass `tee()` throug single-reader relay (4 MiB per raw/enveloped frame and an 8 MiB producer queue). Queue overflow closes the upstream and emits a terminal downstream `response.failed` event followed by `[DONE]`. +For the final outgoing model `gpt-5.3-codex-spark`, canonical ChatGPT forwarding explicitly +disables Responses Lite in both the HTTP header and native WS frame metadata, including when +an alias selects Spark — but only when the outgoing body carries no `additional_tools` item with a nonempty `tools` array. +That group IS the Lite tool-delivery shape, so a Spark body that still uses it keeps Lite ON even +if a caller or configured header said otherwise; otherwise the frame would advertise non-Lite +while the tools exist only in the Lite shape. A changed Lite identity retires the old socket; subsequent eligible +requests with the same identity can reuse the new socket. Other models and gateways keep +their existing Lite policy. Malformed native metadata still falls back to HTTP with its body unchanged. + When a provider rejects a streaming request with HTTP 413 before SSE begins, OpenCodex emits one terminal `response.failed` event with `context_length_exceeded` instead of relaying the retryable unknown status. This lets Codex stop its reconnect loop and apply its own context-compaction policy diff --git a/docs-site/src/content/docs/ru/reference/architecture.md b/docs-site/src/content/docs/ru/reference/architecture.md index c46da773c0..cd3f776efb 100644 --- a/docs-site/src/content/docs/ru/reference/architecture.md +++ b/docs-site/src/content/docs/ru/reference/architecture.md @@ -151,6 +151,15 @@ loopback; настроенные записи `corsAllowOrigins` расширя `426 upgrade_required`; Codex тогда откатывается на HTTP для этой сессии. Когда установлено `"websockets": true`, та же конечная точка принимает апгрейд и использует WebSocket-мост. +Для итоговой исходящей модели `gpt-5.3-codex-spark` каноническая пересылка в ChatGPT явно +отключает Responses Lite в HTTP-заголовке и нативных метаданных WS-кадра, в том числе при +выборе Spark через псевдоним — но только если в исходящем теле нет элемента `additional_tools` с непустым массивом `tools`. +Эта группа и ЕСТЬ Lite-форма доставки инструментов, поэтому тело Spark, которое её использует, +сохраняет Lite ВКЛЮЧЁННЫМ независимо от заголовка вызывающего клиента или конфигурации. Изменение идентичности Lite выводит старый сокет из использования; +последующие подходящие запросы с той же идентичностью могут повторно использовать новый сокет. +Другие модели и шлюзы сохраняют прежнюю политику Lite. Некорректные нативные метаданные +по-прежнему приводят к откату на HTTP без изменения тела запроса. + Compaction контекста Codex работает для маршрутизируемых моделей. `server/responses/compact.ts` обрабатывает `POST /v1/responses/compact`, выполняя внутренний маршрутизируемый ход суммаризации и возвращая сжатую историю, а `responses/parser.ts` и `bridge.ts` обрабатывают ходы diff --git a/docs-site/src/content/docs/tr/reference/architecture.md b/docs-site/src/content/docs/tr/reference/architecture.md index 24d2bbb7aa..5dd9b99957 100644 --- a/docs-site/src/content/docs/tr/reference/architecture.md +++ b/docs-site/src/content/docs/tr/reference/architecture.md @@ -170,6 +170,15 @@ opencodex `426 upgrade_required` döndürür; Codex daha sonra bu oturum için HTTP'ye geri döner. `"websockets": true` ayarlandığında aynı uç nokta yükseltmeyi kabul eder ve WebSocket köprüsünü kullanır. +Son gönderilen model `gpt-5.3-codex-spark` olduğunda, kanonik ChatGPT iletimi HTTP başlığında +ve yerel WS çerçevesi meta verilerinde Responses Lite'ı açıkça kapatır; Spark bir takma adla +seçildiğinde de bu geçerlidir — ancak yalnızca giden gövde boş olmayan `tools` dizisine sahip bir `additional_tools` grubu +taşımıyorsa. Bu grup Lite'ın araç teslim biçiminin kendisidir; onu kullanan bir Spark gövdesi, +çağıran veya yapılandırılmış başlık ne derse desin Lite'ı AÇIK tutar. Lite kimliği değişince eski soket kullanım dışı bırakılır; +aynı kimliğe sahip sonraki uygun istekler yeni soketi yeniden kullanabilir. Diğer modeller ve +ağ geçitleri mevcut Lite politikalarını korur. Bozuk yerel meta verilerde, istek gövdesi +değiştirilmeden HTTP'ye geri dönülmeye devam edilir. + Codex bağlam sıkıştırması yönlendirilen modeller için çalışır. `server/responses/compact.ts`, dahili bir yönlendirilen özetleme turu çalıştırarak ve sıkıştırılmış geçmişi döndürerek `POST /v1/responses/compact`'ı @@ -220,4 +229,3 @@ Dahili model `types.ts` içinde yer alır: `OcxParsedRequest`, `OcxContext`, `OcxProviderConfig`). İki yardımcı yaygın olarak kullanılır: `namespacedToolName()` ve `modelInList()` (`noVisionModels` / `noReasoningModels` için toleranslı `:size` etiketi eşleştirmesi). - diff --git a/docs-site/src/content/docs/zh-cn/reference/architecture.md b/docs-site/src/content/docs/zh-cn/reference/architecture.md index 94c5eb8ea0..b1edbd1e8b 100644 --- a/docs-site/src/content/docs/zh-cn/reference/architecture.md +++ b/docs-site/src/content/docs/zh-cn/reference/architecture.md @@ -134,6 +134,13 @@ thread affinity 位于 `codex/` 下,不会出现在管理 API 响应中。请 session 中回退到 HTTP。设置 `"websockets": true` 后,同一 endpoint 会接受 upgrade 并使用 WebSocket bridge。 +当最终发送的模型为 `gpt-5.3-codex-spark` 时,canonical ChatGPT 转发会在 HTTP 请求头和 +原生 WS 帧元数据中明确关闭 Responses Lite,通过别名选择 Spark 时也一样;但这仅适用于发送正文 +不含带有非空 `tools` 数组的 `additional_tools` 分组的情况。该分组本身就是 Lite 的工具投递形态,因此仍使用它的 Spark +正文会保持 Lite 开启,无论调用方或配置的请求头如何。Lite 标识变化时, +旧 socket 会退出使用;后续标识相同且满足复用条件的请求可以复用新 socket。其他模型和网关 +保留原有 Lite 策略。原生元数据格式不合法时,仍会回退到 HTTP,并保持请求正文不变。 + Codex context compaction 同样适用于路由模型。`server/responses/compact.ts` 处理 `POST /v1/responses/compact`,运行一次内部路由 summarization turn 并返回压缩后的历史; `responses/parser.ts` 与 `bridge.ts` 则处理 remote compaction v2 的 `compaction_trigger` turn, diff --git a/docs-site/src/content/docs/zh-tw/reference/architecture.md b/docs-site/src/content/docs/zh-tw/reference/architecture.md index daf9bc9080..be0b2c5003 100644 --- a/docs-site/src/content/docs/zh-tw/reference/architecture.md +++ b/docs-site/src/content/docs/zh-tw/reference/architecture.md @@ -134,6 +134,14 @@ thread affinity 位於 `codex/` 下,不會出現在管理 API 回應中。請 session 中回退到 HTTP。設定 `"websockets": true` 後,同一 endpoint 會接受 upgrade 並使用 WebSocket bridge。 +當最終傳送的模型為 `gpt-5.3-codex-spark` 時,canonical ChatGPT 轉送會在 HTTP 請求標頭與 +原生 WS 訊框中繼資料中明確關閉 Responses Lite,透過別名選擇 Spark 時也一樣;但僅限於傳送本文 +不含帶有非空 `tools` 陣列的 `additional_tools` 群組的情況。該群組本身就是 Lite 的工具傳遞形態,因此仍使用它的 Spark +本文會保持 Lite 開啟,無論呼叫端或設定的標頭為何。Lite 識別值 +改變時,舊 socket 會停止使用;後續識別值相同且符合重用條件的請求可以重用新 socket。 +其他模型與閘道保留既有 Lite 政策。原生中繼資料格式不合法時,仍會退回 HTTP,並保持 +請求本文不變。 + Codex context compaction 同樣適用於路由模型。`server/responses/compact.ts` 處理 `POST /v1/responses/compact`,執行一次內部路由 summarization turn 並回傳壓縮後的歷史; `responses/parser.ts` 與 `bridge.ts` 則處理 remote compaction v2 的 `compaction_trigger` turn, diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 30e1c14c85..10be9c516f 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -865,6 +865,21 @@ function promoteClientLoadedTools(body: unknown): unknown { } const MAX_RESPONSES_CALL_ID_LENGTH = 64; + +/** + * Whether the outgoing body still delivers tools through the responses-lite shape. + * + * Lite carries the client catalog as an `additional_tools` input item; the non-Lite wire shape + * expects top-level `tools`. Anything that flips the Lite advertisement has to agree with the + * shape actually being sent, or the destination silently loses the tool surface. + */ +function bodyCarriesLiteToolShape(body: Record): boolean { + if (!Array.isArray(body.input)) return false; + return body.input.some(item => + isPlainObject(item) && item.type === "additional_tools" + && Array.isArray(item.tools) && item.tools.length > 0 + ); +} const REPAIRED_CALL_ID_PREFIX = "call_ocx_"; const REPAIRED_CALL_ID_DIGEST_LENGTH = MAX_RESPONSES_CALL_ID_LENGTH - REPAIRED_CALL_ID_PREFIX.length; @@ -2506,7 +2521,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ), isXaiSchemaTarget(provider), ); - const finalBody = stripDisabledVerbosity( + const unnormalizedBody = stripDisabledVerbosity( stripDisabledReasoningSummaries( normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId), provider, @@ -2515,13 +2530,32 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): provider, parsed.modelId, ); + // Normalize the wire model before deriving model-dependent transport metadata. + const finalBody = + provider.modelSuffixBracketStrip + && unnormalizedBody !== null + && typeof unnormalizedBody === "object" + && !Array.isArray(unnormalizedBody) + && typeof (unnormalizedBody as { model?: unknown }).model === "string" + ? { ...(unnormalizedBody as Record), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) } + : unnormalizedBody; if (isCanonicalOpenAiForwardProvider(provider)) { - // Spark closes Responses Lite streams before a terminal completion. Select compatibility - // from the final wire model so aliases cannot leave the caller or a static header enabled. + // Select Spark's Lite compatibility from the final wire model, including aliases, and + // let the BODY decide it. The header also overrides native WS metadata downstream, so a + // forwarded or statically configured value must never contradict the shape being sent. + // + // The synchronized catalog keeps `use_responses_lite: true` for Spark precisely because + // it selects tool delivery (`input[].additional_tools` instead of top-level `tools`), and + // stripSparkCompatibility filters that group in place rather than promoting it. So a + // Lite-shaped body is pinned back ON — otherwise an inherited `false` advertises non-Lite + // while the tools exist only in the Lite shape, and Spark loses the tool surface. Only a + // body with no Lite tool group is downgraded, which is what the stream fix needs. if (isPlainObject(finalBody) && finalBody.model === "gpt-5.3-codex-spark") { + const liteShaped = bodyCarriesLiteToolShape(finalBody); for (const name of Object.keys(headers)) { if (name.toLowerCase() === CODEX_RESPONSES_LITE_HEADER) delete headers[name]; } + headers[CODEX_RESPONSES_LITE_HEADER] = liteShaped ? "true" : "false"; } const routingHeaders = new Headers(headers); applyCodexRoutingHint(routingHeaders, finalBody); @@ -2548,15 +2582,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): // here, on the serialized body, not on the parsed selector. One place covers both the // HTTP and the WebSocket outbound, because the WS path transports this same request // instead of rebuilding it. - const body = JSON.stringify( - provider.modelSuffixBracketStrip - && finalBody !== null - && typeof finalBody === "object" - && !Array.isArray(finalBody) - && typeof (finalBody as { model?: unknown }).model === "string" - ? { ...(finalBody as Record), model: stripBracketedModelSuffix((finalBody as { model: string }).model) } - : finalBody, - ); + const body = JSON.stringify(finalBody); const releaseBodyObservation = translatorBudget.observeExternallyCapped( "passthrough_serialization", new TextEncoder().encode(body).byteLength, diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index b0fab66633..24f4e16537 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -63,3 +63,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 45fe1c11e7..401aed4b34 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -96,3 +96,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index b06c1ce393..1bf486d5d1 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -308,3 +308,5 @@ The Combo guides describe the distinction between display quota and single-crede The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e0b87add2d..1f159e3355 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -264,3 +264,5 @@ fragments are not guessed onto pending ID-only calls. parallel/colliding identities, distinct unsafe raw JSON index literals, the maximum safe-integer boundary, invalid index types, missing/null continuations and UTF-8 byte-limit boundaries. + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index be42793e0b..fee3576257 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -82,3 +82,5 @@ constraints cannot widen the canonical shape. Bare shell bridge names are reject on the freeform path. Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in `tests/providers/cursor/cursor-tool-definitions.test.ts`. + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/runtime.md b/structure/runtime.md index 6d733bf8b5..acbe3c464a 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -212,3 +212,5 @@ cooldowns and response-driven retry remain authoritative. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing-quota). + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](transports/responses.md). diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index f0348c1cdd..268248ce48 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -67,3 +67,5 @@ Quota publication distinguishes display reports from explicitly supplied inferen The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md). diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 5624a2e04e..a7c9d8b6f9 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -402,6 +402,16 @@ final outgoing model/tier. No caller identity is synthesized. Noncanonical opt-in gateways keep their own metadata policy. Oversized/unsupported-runtime HTTP fallback preserves the original HTTP body and Lite header. +For the final wire model `gpt-5.3-codex-spark`, the canonical forward adapter normalizes the +Lite header from the BODY, overriding caller/configured headers and stale native WS Lite +metadata in both directions. A body carrying a nonempty `additional_tools` input item is +pinned to `true`: that item IS the Lite tool-delivery format and the non-Lite wire shape +expects top-level `tools`, so an inherited `false` would advertise non-Lite while the tools +exist only in the Lite shape and hide the client tool surface. Any other Spark body is set to +`false`, selecting the non-Lite framing policy. A changed Lite identity retires the previous socket; +subsequent eligible Spark requests with the same identity can reuse the new socket. Malformed +native metadata retains HTTP fallback eligibility without rewriting its body. + Canonical WS quota and response metadata preceding the first Responses event are projected into bounded, allowlisted HTTP headers before the response is committed. Later quota observations update only the captured serving account; @@ -520,3 +530,5 @@ claims stored main, after terminal vision, routed vision and search exclusions. The management quota DTO keeps Combo editing aligned with scoped inference evidence; see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routing-quota). + +Spark Lite and routing metadata use the same suffix-normalized model object as serialization, including configured bracket-suffix removal. diff --git a/tests/codex-integration/codex-metadata-integrity.test.ts b/tests/codex-integration/codex-metadata-integrity.test.ts index ee70d4829e..329e90a973 100644 --- a/tests/codex-integration/codex-metadata-integrity.test.ts +++ b/tests/codex-integration/codex-metadata-integrity.test.ts @@ -208,33 +208,115 @@ describe("Codex request transport metadata", () => { expect(new Headers(dropped.headers).get(hintHeader)).toBe("model=gpt-5.6-sol"); }); - test("canonical adapter drops Lite only for the Spark wire model", async () => { + test("canonical adapter disables Spark Lite in HTTP headers and WS metadata without mutating input", async () => { + const { prepareCodexWsRequest } = await import("../../src/server/responses/codex-ws-request"); const adapter = createResponsesPassthroughAdapter({ adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", headers: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, }); for (const [model, incomingLite, expectedLite] of [ - ["gpt-5.3-codex-spark", "true", null], - ["gpt-5.3-codex-spark", undefined, null], + ["gpt-5.3-codex-spark", "true", "false"], + ["gpt-5.3-codex-spark", "false", "false"], + ["gpt-5.3-codex-spark", undefined, "false"], ["gpt-5.6-sol", "true", "true"], + ["gpt-5.6-sol", "false", "false"], + ["gpt-5.6-sol", undefined, "true"], ] as const) { const parsed = minimalParsed(); parsed.modelId = model; - parsed._rawBody = { model, input: [], stream: true }; + parsed._rawBody = { model, input: [], stream: true, + client_metadata: { [liteKey]: "true", other: "preserved" } }; + const before = JSON.stringify(parsed._rawBody); const incoming = new Headers(); if (incomingLite !== undefined) incoming.set(liteHeader, incomingLite); const request = await adapter.buildRequest(parsed, { headers: incoming, }); expect(new Headers(request.headers).get(liteHeader)).toBe(expectedLite); + const prepared = prepareCodexWsRequest(url, { body: request.body, headers: request.headers })!; + expect(JSON.parse(prepared.frameText).client_metadata).toEqual({ + [liteKey]: expectedLite, other: "preserved", + }); + expect(prepared.httpInit.body).toBe(request.body); + expect(JSON.stringify(parsed._rawBody)).toBe(before); + expect(incoming.get(liteHeader)).toBe(incomingLite ?? null); } const routed = minimalParsed(); routed.modelId = "spark-alias"; routed._rawBody = { model: "gpt-5.3-codex-spark", input: [], stream: true }; const request = await adapter.buildRequest(routed, { headers: new Headers({ [liteHeader]: "true" }) }); - expect(new Headers(request.headers).get(liteHeader)).toBeNull(); + expect(new Headers(request.headers).get(liteHeader)).toBe("false"); + const prepared = prepareCodexWsRequest(url, { body: request.body, headers: request.headers })!; + expect(JSON.parse(prepared.frameText).client_metadata[liteKey]).toBe("false"); + + routed.modelId = "gpt-5.3-codex-spark"; + routed._rawBody = { model: "gpt-5.6-sol", input: [], stream: true }; + const otherWireModel = await adapter.buildRequest(routed, { headers: new Headers({ [liteHeader]: "true" }) }); + expect(new Headers(otherWireModel.headers).get(liteHeader)).toBe("true"); + }); + + test("a Lite-shaped Spark body pins Lite back on, whatever the inherited header said", async () => { + const { prepareCodexWsRequest } = await import("../../src/server/responses/codex-ws-request"); + // The catalog keeps use_responses_lite: true for Spark because it selects tool DELIVERY: + // the client catalog rides `input[].additional_tools`, not top-level `tools`. A forwarded or + // configured `false` must not survive on such a body, or the frame advertises non-Lite while + // the tools exist only in the Lite shape and Spark loses them. + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", + headers: { "X-OpenAI-Internal-Codex-Responses-Lite": "false" }, + }); + const liteShapedInput = [ + { type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }, + { type: "additional_tools", tools: [{ type: "function", name: "shell", parameters: {} }] }, + ]; + + for (const incomingLite of ["false", "true", undefined] as const) { + const parsed = minimalParsed(); + parsed.modelId = "gpt-5.3-codex-spark"; + parsed._rawBody = { model: "gpt-5.3-codex-spark", input: liteShapedInput, stream: true, + client_metadata: { [liteKey]: "false", other: "preserved" } }; + const incoming = new Headers(); + if (incomingLite !== undefined) incoming.set(liteHeader, incomingLite); + const request = await adapter.buildRequest(parsed, { headers: incoming }); + expect(new Headers(request.headers).get(liteHeader)).toBe("true"); + const prepared = prepareCodexWsRequest(url, { body: request.body, headers: request.headers })!; + expect(JSON.parse(prepared.frameText).client_metadata).toEqual({ + [liteKey]: "true", other: "preserved", + }); + } + + // An empty group is not a Lite tool surface, so the stream fix still applies. + const toolless = minimalParsed(); + toolless.modelId = "gpt-5.3-codex-spark"; + toolless._rawBody = { model: "gpt-5.3-codex-spark", stream: true, + input: [{ type: "additional_tools", tools: [] }] }; + const downgraded = await adapter.buildRequest(toolless, { headers: new Headers() }); + expect(new Headers(downgraded.headers).get(liteHeader)).toBe("false"); + }); + + test("Spark disables Lite without configured headers and retains malformed-metadata HTTP fallback", async () => { + const { prepareCodexWsRequest } = await import("../../src/server/responses/codex-ws-request"); + const adapter = createResponsesPassthroughAdapter({ + adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", + }); + for (const client_metadata of [undefined, {}, null, [], { [liteKey]: true }]) { + const parsed = minimalParsed(); + parsed._rawBody = { model: "gpt-5.3-codex-spark", input: [], stream: true, + ...(client_metadata === undefined ? {} : { client_metadata }) }; + const before = JSON.stringify(parsed._rawBody); + const request = await adapter.buildRequest(parsed, { headers: new Headers() }); + expect(new Headers(request.headers).get(liteHeader)).toBe("false"); + const prepared = prepareCodexWsRequest(url, { body: request.body, headers: request.headers }); + if (client_metadata === undefined || JSON.stringify(client_metadata) === "{}") { + expect(JSON.parse(prepared!.frameText).client_metadata).toEqual({ [liteKey]: "false" }); + } else { + expect(prepared).toBeNull(); + expect(JSON.parse(request.body).client_metadata).toEqual(client_metadata); + } + expect(JSON.stringify(parsed._rawBody)).toBe(before); + } }); test("noncanonical adapters neither forward caller Lite nor synthesize a routing hint", async () => { @@ -243,7 +325,10 @@ describe("Codex request transport metadata", () => { adapter: "openai-responses", authMode, baseUrl: "https://gateway.example/v1", headers: { [hintHeader]: "operator-owned" }, }); - const request = await adapter.buildRequest(minimalParsed(), { + const parsed = minimalParsed(); + parsed.modelId = "gpt-5.3-codex-spark"; + parsed._rawBody = { model: parsed.modelId, input: [] }; + const request = await adapter.buildRequest(parsed, { headers: new Headers({ [liteHeader]: "true", [hintHeader]: "caller-owned" }), }); expect(new Headers(request.headers).has(liteHeader)).toBe(false); @@ -363,3 +448,60 @@ describe("Codex request transport metadata", () => { expect(headers.get(hintHeader)).toBe("model=gpt-5.6-luna"); }); }); + + +describe("Spark Lite follows serialized model and surviving tool shape", () => { + const liteHeader = "x-openai-internal-codex-responses-lite"; + const liteKey = "ws_request_header_x_openai_internal_codex_responses_lite"; + test("bracket normalization and both alias directions use the wire model", async () => { + const adapter = createResponsesPassthroughAdapter({ adapter: "openai-responses", authMode: "forward", + baseUrl: "https://chatgpt.com/backend-api/codex", modelSuffixBracketStrip: true }); + for (const [selector, model, expectedModel, expectedLite] of [ + ["alias", "gpt-5.3-codex-spark[1m]", "gpt-5.3-codex-spark", "false"], + ["gpt-5.3-codex-spark", "gpt-5.6-sol", "gpt-5.6-sol", "true"], + ]) { + const parsed = minimalParsed(); + parsed.modelId = selector; + parsed._rawBody = { model, input: [] }; + const before = JSON.stringify(parsed._rawBody); + const built = await adapter.buildRequest(parsed, { headers: new Headers({ [liteHeader]: "true" }) }); + expect(JSON.parse(built.body).model).toBe(expectedModel); + expect(new Headers(built.headers).get(liteHeader)).toBe(expectedLite); + expect(new Headers(built.headers).get("x-codex-routing-hint")).toContain(`model=${expectedModel}`); + expect(JSON.stringify(parsed._rawBody)).toBe(before); + } + }); + + for (const [name, inputTools, topTools, expectedTools, expectedLite] of [ + ["filtered empty", [{ type: "tool_search" }], undefined, [], "false"], + ["reserved functions", [{ type: "namespace", name: "functions", tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }] }], undefined, + [{ type: "namespace", name: "functions", tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }] }], "true"], + ["top-level only", undefined, [{ type: "function", name: "lookup", parameters: { type: "object" } }], undefined, "false"], + ] as const) test(`post-transform body: ${name}`, async () => { + const { prepareCodexWsRequest } = await import("../../src/server/responses/codex-ws-request"); + const adapter = createResponsesPassthroughAdapter({ adapter: "openai-responses", authMode: "forward", + baseUrl: "https://chatgpt.com/backend-api/codex", headers: { [liteHeader]: expectedLite === "true" ? "false" : "true" } }); + const parsed = minimalParsed(); + parsed.modelId = "gpt-5.3-codex-spark"; + parsed._rawBody = { model: parsed.modelId, input: inputTools ? [{ type: "additional_tools", tools: inputTools }] : [], + ...(topTools ? { tools: topTools } : {}) }; + const built = await adapter.buildRequest(parsed); + const body = JSON.parse(built.body); + expect(body.input.find((item: { type: string }) => item.type === "additional_tools")?.tools).toEqual(expectedTools); + if (topTools) expect(body.tools).toEqual(topTools); + expect(new Headers(built.headers).get(liteHeader)).toBe(expectedLite); + const prepared = prepareCodexWsRequest("https://chatgpt.com/backend-api/codex/responses", { body: built.body, headers: built.headers }); + expect(JSON.parse(prepared!.frameText).client_metadata[liteKey]).toBe(expectedLite); + }); + + test("noncanonical static Lite remains operator-owned", async () => { + for (const authMode of ["key", "forward"] as const) { + const adapter = createResponsesPassthroughAdapter({ adapter: "openai-responses", authMode, + baseUrl: "https://gateway.example/v1", headers: { [liteHeader]: "operator-owned" } }); + const parsed = minimalParsed(); + parsed._rawBody = { model: "gpt-5.3-codex-spark", input: [] }; + const built = await adapter.buildRequest(parsed, { headers: new Headers({ [liteHeader]: "true" }) }); + expect(new Headers(built.headers).get(liteHeader)).toBe("operator-owned"); + } + }); +}); diff --git a/tests/responses/ws-upstream-reuse.test.ts b/tests/responses/ws-upstream-reuse.test.ts index b957fdb317..48b720d2ea 100644 --- a/tests/responses/ws-upstream-reuse.test.ts +++ b/tests/responses/ws-upstream-reuse.test.ts @@ -3,6 +3,8 @@ import { codexWsUpstreamFetch } from "../../src/server/responses/ws-upstream"; import { runOptionalShutdownHooks } from "../../src/lib/optional-shutdown-hooks"; import { CodexWsPool, codexWsPool } from "../../src/server/responses/codex-ws-pool"; import { prepareCodexWsRequest } from "../../src/server/responses/codex-ws-request"; +import { createResponsesPassthroughAdapter } from "../../src/adapters/openai-responses"; +import { withTestTranslatorBudget } from "../helpers/translator-budget"; const URL = "https://chatgpt.com/backend-api/codex/responses"; const realWebSocket = globalThis.WebSocket; @@ -307,3 +309,31 @@ test("a Lite mode change retires the old handshake", async () => { expect(Socket.all).toHaveLength(2); expect(Socket.all[0]!.readyState).toBe(3); }); + +test("adapter Spark Lite override retires a legacy socket and reuses the disabled identity", async () => { + const liteHeader = "x-openai-internal-codex-responses-lite"; + const liteKey = "ws_request_header_x_openai_internal_codex_responses_lite"; + const options = init(); + const rawBody = { ...JSON.parse(options.body as string), model: "gpt-5.3-codex-spark", + client_metadata: { thread_id: "fixture-thread", turn_id: "fixture-turn", [liteKey]: "true" } }; + const before = JSON.stringify(rawBody); + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ + adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", + })); + const built = await adapter.buildRequest({ modelId: "spark-alias", context: { messages: [] }, + stream: true, options: {}, _rawBody: rawBody, + }, { headers: new Headers(options.headers) }); + const current = { ...options, body: built.body, headers: built.headers }; + // Keep the exact same Spark model/scope/headers; only the old delete-only Lite policy differs. + const legacyHeaders = new Headers(current.headers); + legacyHeaders.delete(liteHeader); + await drain({ ...current, headers: legacyHeaders }); + await drain(current); + await drain(current); + expect(Socket.all).toHaveLength(2); + expect(Socket.all.map(socket => socket.readyState)).toEqual([3, 1]); + expect(Socket.all.map(socket => socket.frames.map(frame => + (frame.client_metadata as Record)[liteKey]))).toEqual([["true"], ["false", "false"]]); + expect(Socket.all.flatMap(socket => socket.frames).every(frame => frame.model === rawBody.model)).toBe(true); + expect(JSON.stringify(rawBody)).toBe(before); +}); From d593599714bd218a2eb2535ad8facd13937ac2f2 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:00:54 +0900 Subject: [PATCH 11/21] docs: plan latest-dev conflict refresh for thinking carries --- devlog/_plan/260912_thinking_contract/050_refresh.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 devlog/_plan/260912_thinking_contract/050_refresh.md diff --git a/devlog/_plan/260912_thinking_contract/050_refresh.md b/devlog/_plan/260912_thinking_contract/050_refresh.md new file mode 100644 index 0000000000..79d0133c85 --- /dev/null +++ b/devlog/_plan/260912_thinking_contract/050_refresh.md @@ -0,0 +1,5 @@ +# Integration conflict repair + +Parent explicitly requests own hint branch latest-dev integration with independent resolution-only audit and no-verify push. Latest fetched dev ca5ac39124671ee05349e7873231f672824ea26c also conflicts with presentation; preserve all three independent dev-based PRs. Most collisions are adjacent structure-document additions; core received continuation recovery changes that must survive. No source PR/other worktree modifications or merges into dev. Rebase only owned branches, preserve pre-rebase refs in ignored evidence and compare range-diff; use explicit expected old remote SHA with force-with-lease plus --no-verify. This is branch refresh, not native restacking. Product tests remain NOT RUN. + +Prior D: Spark source audit PASS; hosted tests remain pending. Work-phase added per parent collision instruction, before final delivery. MODIFY conflict paths only, retaining source contracts and new dev changes. Verification: git range-diff, git diff --check, docs source validator, independent resolution-only review; hosted tests rerun only on refreshed final heads. From 4b8eaed3449b4e10dd67aefd0b6e9b0bfe06005e Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:02:45 +0900 Subject: [PATCH 12/21] docs: bind conflict refresh to active delivery cycle --- devlog/_plan/260912_thinking_contract/050_refresh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devlog/_plan/260912_thinking_contract/050_refresh.md b/devlog/_plan/260912_thinking_contract/050_refresh.md index 79d0133c85..16d4aa80dd 100644 --- a/devlog/_plan/260912_thinking_contract/050_refresh.md +++ b/devlog/_plan/260912_thinking_contract/050_refresh.md @@ -2,4 +2,4 @@ Parent explicitly requests own hint branch latest-dev integration with independent resolution-only audit and no-verify push. Latest fetched dev ca5ac39124671ee05349e7873231f672824ea26c also conflicts with presentation; preserve all three independent dev-based PRs. Most collisions are adjacent structure-document additions; core received continuation recovery changes that must survive. No source PR/other worktree modifications or merges into dev. Rebase only owned branches, preserve pre-rebase refs in ignored evidence and compare range-diff; use explicit expected old remote SHA with force-with-lease plus --no-verify. This is branch refresh, not native restacking. Product tests remain NOT RUN. -Prior D: Spark source audit PASS; hosted tests remain pending. Work-phase added per parent collision instruction, before final delivery. MODIFY conflict paths only, retaining source contracts and new dev changes. Verification: git range-diff, git diff --check, docs source validator, independent resolution-only review; hosted tests rerun only on refreshed final heads. +Prior D: Spark source audit PASS; hosted tests remain pending. Refresh is a subtask of the already-active delivery cycle; no separate cycle is claimed. MODIFY conflict paths only, retaining source contracts and new dev changes. Verification: git range-diff, git diff --check, docs source validator, independent resolution-only review; hosted tests rerun only on refreshed final heads. From 2526b6c42a9c604d05b83cfbaf950cb408787951 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:17:35 +0900 Subject: [PATCH 13/21] test(console): construct synthetic URL userinfo without email-shaped data --- tests/providers/opencode-zen-rate-limit.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/providers/opencode-zen-rate-limit.test.ts b/tests/providers/opencode-zen-rate-limit.test.ts index 20a59bd61a..5219533044 100644 --- a/tests/providers/opencode-zen-rate-limit.test.ts +++ b/tests/providers/opencode-zen-rate-limit.test.ts @@ -256,13 +256,16 @@ describe("Console Go transient upload refusal", () => { }); test("requires the effective canonical HTTPS endpoint", () => { + const userInfoUrl = new URL("https://opencode.ai/zen/go/v1/responses"); + userInfoUrl.username = "fixture-user"; + userInfoUrl.password = "fixture-password"; + expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl: userInfoUrl.href })).toBe(false); for (const outboundUrl of [ "http://opencode.ai/zen/go/v1/responses", "https://opencode.ai:8443/zen/go/v1/responses", "https://opencode.ai.evil.test/zen/go/v1/responses", "https://opencode.ai/zen-other/v1/responses", "https://opencode.ai/zen/go/v1/models", - "https://user:pass@opencode.ai/zen/go/v1/responses", ]) expect(isTransientConsoleGoUploadRejection({ status: 400, errorBody: envelope(GO_MESSAGE), outboundUrl })).toBe(false); }); From 2c63a5d4283936aa9d0525e39090afb5e492c0ec Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 16:43:02 +0900 Subject: [PATCH 14/21] fix(console): exclude custom query destinations from upload replay --- src/providers/opencode-zen-rate-limit.ts | 1 + structure/transports/responses.md | 2 ++ tests/providers/opencode-zen-rate-limit.test.ts | 2 ++ .../responses-console-go-upload-retry.test.ts | 11 +++++++++++ 4 files changed, 16 insertions(+) diff --git a/src/providers/opencode-zen-rate-limit.ts b/src/providers/opencode-zen-rate-limit.ts index 21c248275c..383c9f70ae 100644 --- a/src/providers/opencode-zen-rate-limit.ts +++ b/src/providers/opencode-zen-rate-limit.ts @@ -183,6 +183,7 @@ export function isConsoleGoDestination(outboundUrl: string | undefined): boolean const url = new URL(outboundUrl); return url.protocol === "https:" && url.hostname === "opencode.ai" && url.port === "" && url.username === "" && url.password === "" + && url.search === "" && url.hash === "" && /^\/zen\/(?:go\/)?v1\/(?:responses|chat\/completions|messages)$/.test(url.pathname); } catch { return false; diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 8a989a27fb..4d2c285c87 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -511,3 +511,5 @@ deprecated, sunset, decommissioned, or no longer available). An unrelated applic not retried. > Decision record: [ADR-0071](../decisions/ADR-0071-combo-streaming-commit-boundary.md) + +Console upload-rejection recovery excludes query-bearing and fragment-bearing destinations even when their host and generation path match the canonical endpoint. diff --git a/tests/providers/opencode-zen-rate-limit.test.ts b/tests/providers/opencode-zen-rate-limit.test.ts index 5219533044..e8e90d9416 100644 --- a/tests/providers/opencode-zen-rate-limit.test.ts +++ b/tests/providers/opencode-zen-rate-limit.test.ts @@ -263,6 +263,8 @@ describe("Console Go transient upload refusal", () => { for (const outboundUrl of [ "http://opencode.ai/zen/go/v1/responses", "https://opencode.ai:8443/zen/go/v1/responses", + "https://opencode.ai/zen/go/v1/responses?tenant=fixture", + "https://opencode.ai/zen/go/v1/responses#fragment", "https://opencode.ai.evil.test/zen/go/v1/responses", "https://opencode.ai/zen-other/v1/responses", "https://opencode.ai/zen/go/v1/models", diff --git a/tests/responses/responses-console-go-upload-retry.test.ts b/tests/responses/responses-console-go-upload-retry.test.ts index 4ece7bd0af..e4b569dabe 100644 --- a/tests/responses/responses-console-go-upload-retry.test.ts +++ b/tests/responses/responses-console-go-upload-retry.test.ts @@ -153,6 +153,17 @@ describe("Console Go transient upload refusal recovery", () => { describe("Console destination and translated recovery controls", () => { + test("a query-bearing Console destination never authorizes another POST", async () => { + const cfg = config(); + cfg.providers.go!.baseUrl = "https://opencode.ai/zen/go/v1?tenant=fixture"; + const outbound: string[] = []; + globalThis.fetch = (async input => { outbound.push(String(input)); return refusal(); }) as typeof fetch; + const response = await handleResponses(request(), cfg, { model: "", provider: "" }); + expect(response.status).toBe(400); + expect(outbound).toHaveLength(1); + expect(new URL(outbound[0]!).search).toBe("?tenant=fixture"); + }); + test("a canonical row name cannot authorize another host", async () => { const cfg = config(); cfg.providers["opencode-go"] = { ...cfg.providers.go!, baseUrl: "https://other.example.test/v1" }; From b597f5f03d5f2431b3b8cf4fb0f419cf112e25bd Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 21:11:20 +0900 Subject: [PATCH 15/21] docs: attach hosted-build Console recovery label evidence --- .../260912_stream_recovery/092_resume.md | 20 ++++++++++++++++++ .../093_console_recovery.png | Bin 0 -> 355231 bytes 2 files changed, 20 insertions(+) create mode 100644 devlog/_plan/260912_stream_recovery/092_resume.md create mode 100644 devlog/_plan/260912_stream_recovery/093_console_recovery.png diff --git a/devlog/_plan/260912_stream_recovery/092_resume.md b/devlog/_plan/260912_stream_recovery/092_resume.md new file mode 100644 index 0000000000..a6463ddfe4 --- /dev/null +++ b/devlog/_plan/260912_stream_recovery/092_resume.md @@ -0,0 +1,20 @@ +# Stream delivery verification update + +Five source fixes were delivered as independent dev-based PRs. PR4341 (terminal integrity) is merged; PR4354 (Console), PR4356 (search), PR4363 (Cursor) and PR4367 (live sideband) remain open at the 2026-09-12 verification update. This task does not merge integration branches. Runtime verification remains incomplete; shared Cline, history and Windows failures are not treated as passing baseline evidence. + +## Console label capture + +![Console recovery label with synthetic log data](093_console_recovery.png) + +The capture uses the dashboard-preview artifact from GitHub Actions run34675214816, artifact10294058486, recorded build commit f9dcd6449298821e6ba02d026037a677effe6aaa and GUI tree e0d22385336080717ad29a14d65a270a207a41e7. The artifact was built in hosted CI. No local product build, install, typecheck or test suite ran. + +The artifact GUI differs from Console source head2c63a5d4283936aa9d0525e39090afb5e492c0ec only in Combo workspace files and associated tests; Logs.tsx, its locale strings and styles are unchanged. The Logs page was served locally from the existing bundle with a synthetic API fixture. The screenshot shows the actual Console upload retry label in the request detail dialog. Values, request ID and model are synthetic, not live request or billing evidence. This is rendered label evidence, not a backend retry test. + +## Remaining acceptance + +- #3389 remains HOLD: zero observed bytes cannot establish upstream nonexecution. +- #4191 retains its native failing-stage evidence requirement; existing diagnostics are not a reproduced fix. +- #4312 has its open-tool status sub-defect addressed; the actual client nonretryable refusal contract remains unresolved. +- #3506 requires a redacted translation-fidelity exchange. No semantic-progress cutoff is added. + +Fresh source/security reviews and exact CI heads/runs are retained in the task-local handoff. Skipped, cancelled, failed and superseded runs do not certify completion. Local suites/build/typecheck/install: NOT RUN. diff --git a/devlog/_plan/260912_stream_recovery/093_console_recovery.png b/devlog/_plan/260912_stream_recovery/093_console_recovery.png new file mode 100644 index 0000000000000000000000000000000000000000..43e6fdf359d53c21739fcd995237ff4776928e65 GIT binary patch literal 355231 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 P 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

0$J$ z9sO_<;?!)MdxAqXx|cRK9DgQcz7YS~&Wgz|pFJDsu8pUJf4}%7_jH9fa+8T*ASUL< z6oOhiDIQgA6?H1WUircO`|3FRQrQuEy{3Wwe z>F8o;2aVDNRyZnjUAz-YGVoGTh3>oIqB)245w&m3q6ZTNVc(!Qol14$bQMzP!31Gs zvdg13Fg&l<%FwoLxqpy7KR(y@@d4F-Z|WeE-GakV569*2k}hx;6+@}DjRg1V)xg{qax-rli_&q{mlk8|Y@m2QSavn>tNHH%7d z>%G1D*i}tJoH?A5A?K-NWerWkkm;h%>e}QdKNfi8=$H}F5*}IZf~ng6`fcl(YA|u= ztI|I|^VNv(;EN0?)2`esZ@bst1l_-$r~0}A2*vkq0cJhq1Jl!_Z1MqnZO%}JOXYKJ zLf||xce%ta5;XLJIJS}OKGiDjRPJ2 z^^_df=AGhBxd?4M?u9#^#n#@K#!&7gv9r_F(qfn!vkOZ<-kjG}ujsAP#xg>-1QLLB zIvn}rQK~Oqm~uL$pl;6J(3a|~zU^XrU+RwZ!nbvM&L&aiy}TU)tZbnV;ekzLHCXOO z(P}qz?y<~F>~l=oY`Gn7a5@&e;d+!0sxI$S-tKg4xgS=y{vvkU8<37>1hMz?ysp&d zjEpVUB_5{Qvz^1c0+aYgJHb}<=LZJCTw;ZV^L8M;IaWDo*M&-xY=Zu-F6YX`^Mh9n zgW@IR7a$&A(nji{)&j{{3@FKM%qz(ix8=0opIT7WvYk0Uyi3jU_m)NX#hI;_ZOYS48~bv9oUQEX7@A{M^#u3C z8!kF>+crAZCAqk`J`KR!`(ip7o{D#lB}ik%rwTy7qq?`;z3`Oy0r zd$Y4SaC-ZeZtMWjtR`u`Zv(?)eMQF`?SAF%=O^ArM=PvGkM6#C>*ab2%uOGjy84kg z_~X{Gy27=@VL1*Ys^`#qM<;Pp(g`t!AZ&nUjE+MVN?Yq|AOCI=oX`(BP=;!G9;)kb zhrR1$;@#!l&BD`N_Ow;Ed|{v||4GbN&RxZS$rzMIlfQmN)Q8v^ruAXAbrZ6TFye_qa$8U`eRd zd_x9RZcmu z`a%;mp2x9(S!({V`Ni3B>+_H2viB4I#_=FjL{}N4&bFOnKX!1Q1UTlbh? zU~%WFb$4gyot|Rmos*~<756aQJrZ6v^(EN!(wA&lac^}4GQ69AnZmm z4CK2fhcP|xA?O?Q{gAE4Q2#;i8B&vgAt zDWJ)QBrx;o;;$^7po2Z|4QU>0wz9JFvZ>?fJ&d3Y2`Q;N3GN7gtz-LJYBc>NdiiC3 zqHt^hT&umqUQRAJZwr&We2l&_duEwc)sIaRt*a#=vERDBpeO}n^yW*3GkpU732J1n zr_L>E79rD!lshiD17CKMS6yCM^)|dzi>UZ}${(T(YAb1|n(njQH(VG?DZgbb5%aNa zE&p?2R9)QfMAtP^BKkF7K5f6D*)6MFORMGe%}xnPX?eNw9|S|6Sj0Z=T}YCB@-?}! zFQ}1coXMly#0gepel}*<=MF8&A`}k@hY+5gA7)Jghja9CxPKmm4sm?J`MoEpiS5w0 zXa4vBl1Lxcj;_acHX2fFAfc?CZJIvDh)e)j>7Z6vM09RARF)rS9|tm@`zGqc0Nl(k zD~x+;8K2AtumRb12%#EBfVu<99{PN60Yn|NX9_cLA+w{U>7(S`UIS6mJIn@5&f2M5 zNthU22;}Wbny%#!gGI&3;WtzI%)RIA_j9!>fS7CNLUrNyPp`H)3!KH%zZl=<;-I9j za{T?G(AY&c$i7{feb~7sc`|Ul`RImuFba%J8|lWZy?s@+dHiZ-dLDwluX9`f!7d^G zF2IKp5)vq79T?ogq4qXNPN$ttNQEfd4L7Ot!^e?mre$Y;k^NzS;Io0ptPPvk1f`4o ziFRh;+xDgL=uWU-vyV^UFJwllKat{@d4k=R_@SucL%+$s;NkDNZgwaqyl&zL<2%j4`2P#<++>w3Twn4 zgWxDeo{OKKKiajacnEQQ#op4y1mPnRo@! za54bKj-Lk#g5#DC|N(*#WCdET6Z3un2@)H~v)2Jh-jLVCQ`q zf_|0PgQTrQI>-F*)9HEt&g@~FCt0}PrBk4DAx)6SWTp&Mq`T~1}FO-IscZEto^%}elKCQ_1m{-G3{On z$%JsFdm7Ze4omW)wI0u^IC*AF7pIH3{eLI&Q9e7E@Q$C*U=zlDCFwYcPnN$ads`Ax(aa z1l1r25Sr2DY=ohxY4^OZM}E%FlcR3-B_}5=daXLbFFtBaN6~m8`sI#=scBB@@AjX9 zrA+3ZK3vly{w3n|I zv(8lJJ}U->XVsc3A5R}MH72iXe5{(Q&THI)u{8T;*`U(zp{0rqh1=ve3pP8bEZ3?YCT3=qaNyyuW%?6!gg{*_n(rSQ{ ztn5bm%r0+Vk)t}HFZGV_DLyJHDv12$6cz%Iw)po`^xayh6;&Fbr!j*R*_SW$_L)&b zjYS0x*E3Yr$CZGVRym=*At@t6d=|;J)A0ZgPg3j}|kh2>i(I`}w*nf-5Dgo_1E= z4b7ESD2V|xyIz-t?XB=ZvD?Vf!wajP?`@jfrY|XW$l7XZFFMo2X^zi4Zr7!T=ObkZwY)A{2o{~|DydL(rkHyEWbH#o!o{h&xnIb48T!pK()t?{Dn@Y2< z@34)_6v@v*f{;~tZc{N~^Nv&LiN0xaRFO~Sob&2%&WP6RPN>PsG87d7q6&EK4Q4(< z4kb0UW&hbUJJ7j-7@*{#ZhMig(ec9yi?PAc(a#asQ`$V}dbd@TcTz{KkD|X=p9j9GtggKvS++sh5TgIMjbQhe$H57GL;U(=$VzQW zD}CGN;6(HH{^4xvfD+dMC$a4xzFPSmuA9blNC<)(ENMB+k$9F3C-HXn603)nGmV@x z+W6c(8&4MAy!f^rGTr_paA!e6QpVv%){v~6Oofqv>6%GpX9iWKT33!FUDA}c%EGsY z*=k8%-8XV3pR=i`D+M&SOZ`AR*ul8lpw=bdKXX3AR9ruXUn(n-r(HdAXR2S~VUIo7 zijBZHCV>qB0lOODtaGxmXhM6}FkZi*r2ko{TUb?=M17nd5x=lyHC^MBN@fB&W^5gI z_P5{i!s!m0YS1A9Lp*z%n8u7}e*vBl_xQMoEY5R<9P!27iaj`I$X@8-hy6yW(dk*$ z|EhI?ikhL@TQ3^YCa}2m2O&X#mel!u0Ec;Y>t@UpV@kNIjcxF@B>V5iEl@FfvTOQM zc<_Dl8C45^y)VJaY#Q<%BCwOmS)yr3(*H7iw7!BqXSdwKL<_j z%SnwU{}7;OuPvw&ilHYmgM7K`uplk9_0F3ObCW_xC($*fJ2uq2V@JhwgyQ}rvU+tF2nzK41#QEw9aA8=eTN5 z!d%zOuuj%>h|`;rj=U>mR7PFaLDpunD0)@c1nK>-WQ= zYWs3(>)rUUj`hNhFXiby0h!AOUn_qodq;Qlb$4{NSCATIJrGnt{iha?X^!L7(VS=t&{}9(Ovok!UwUs??L$% zIW`i&VNpOht!E1g35khHu5)(7XfWuR``25)7uEyH^x(9YV zZ({4e+Ei*;Gd-9nt0$mOhk<59#Z*wggEu=N?uB)N31CK-!+w_b0QdHm)$?3RkXo*fo&wXcCc^e5##Cqs3k{9&x(pFUAkabciq;mk(g2td z><)ck{YydE|A1>%R8$1<1jyF`g9CS*7YX2WMx>61z7%XqrYb8bvA$VVUZYD2qGDu} ztFX$qVD1ox+#S%f5V`$8%HieWs)9)ZX9aR{q3Ms%i+c$e^c`g&Hrah$Dag2{sB=Z@ z7>llXzDlg5ghhTk<*EyTxM#$%C&*pIOM2;(NWU70JkosQ;5(vcLZ8Xg)(ei%4_suS zm4}3v^Up>^vb$2uT6F})R(P#K+=4s^qx$3C6Z^Jp{8+xW;&q3+u9Nze2%o%Z`6BwW z+NbloRW8<_RsACS`i(PvKh559Q<~K@+#6nzU!7y^jtjAnJwT~%UfPJu?dTO)DK`Jr z6mcR~-ZL^wL2F#-kzMV~F^#v_#8N6&)!3SJG_kSrVsyby)H*uyF1Kr(sjn8GoVL+q z60xaGy}9a>n#ejV#5|9omfJ|gX<+X?y|X5-{Chai%5I%z{^ogjO5B{?H?xG*DKDI- zT5~d=q11@#(6O z5OMpqH(%j{n@_kv8XP6GpmlKeTH&UwLK&Rk0IsUaiiiRB#RM{ESL{}IGqs?5CHh*PhH03Jr8T! zVnzQx%I#c@#NVWhh2&S?-}ut~NAyza_4Iw;vs=x^Q(k1Vb;ld0BeVEv1TT)*n8RW_ z-a9+HAXm{k%YMXm)-7s^c5hw!_@L;~xglwY<{b7q*4>M?McIyveh$AM*i9qYJw*EG zGqKTqWOrv#Yi5!safCc-E*|2r>b-P9LA!JrK%k|R7EU+R5&jyg%&VT7+wVikO@^AE49 zwQK`hu5UXx-6%Tv-LHVkSGUhNv}-Usk^G1D(Ua$WWfbw~9ZRL|y2691YD~*!+`N>ZW4X7V30w>ICud?_XO=;_sa|zcoeNpweON>?-Q}C= zDUzuq=!JdwgQ{?+t?IYbovcpB{PZtMb&4IRfqU}ALI&TE-kA)daV?J-Ffl|$ML|;> z65D~xNYLDz*LAsm7f3M?I>W=mCfBH90<2h9!rVT>eE{1OLRdHzOZum#NXEkf2JVz9 zS6X7!Ls%7grP`F=ZWzyNZmkanTu|4<*u5PJ{?b&L!Z|89cKG=E%!ikMV~PWDFGCYl ztvxrZP5RM#QrVqm59IVWRn`T}N3e~(-TOZAVEZmx+40ND5Ua3x&R)9w2~j#0;ZDnr zv9XZHoZVlt*cM}4xvUNqXlme9TJ-Ifxhp2W=bP?Wj;UFGQJ?nx(Kr9zZkFHvA4|)> zKg9~KP)g@`w`1A9opBueO8mG{z6!fEx#4z7QwnZ#rJIP+yVoghH-=?y4C3sR@ToK{ zv9bCeO{|oJSjAUy2Y#Z|k^dHEGbPPGMO52)+GO7ydzW>vg~yfbhUXu0n{9tm)odY@ z@2R(^b&B1+zIulunXtMykzN1I{;sntx4%({;KOw<%DP|ncy-Bh##{O3F9Yj>XT3hi zHCkjAs^cM}PNa!xa}w+O4H!%r#P0o``~BOXH=4=VmF|Ni3fzr$xq4Jhw=trCwlH2{ znE)lir9Q2;O_>8hhH(}RdK^cc9?cr`s7eiSxE%*loD?%dUYxW z-m~t*7?Mlh+SE0iDxHp~Y!a}S;#l{zB+Ib(-RK>uesq4y)$g*oKV!H_`sy7zzwAQ? zOk;W9yL@`%*OMvGHG^;o?y}N26pM%Mb=p~%v(h?zpIz@T#bR?9)m=QF@xnOFTPfEI zDIZO~-CQ?B%vb7o?Ch6pVuabWe7-yH&-o*!7wHF1;PUd1XCyiM)=4$O4X^O*PjH0f z>(VxS^3-m~9=%%S8b6+UES%PvcY{<#U)%aY1>R74npmAHnKQ7V3~nb+0O68v@^m@>R8yRt+I=t6z4Npk>})0W97fg(6CFbM1^Wy?Vv z5TX;(`bzB@LA)G@DbTR84r!RSSgL&|P-*b9GCA2>DItGCM%bWFHQHZ80!iM9oqB)g z#?NHJ%&-_dUd!lV>y*ly1{haiJyL^2|7j|T*Pr~)qAuAM+s7318=pif1e`aix7e?F zi>iG4tyS=q^Y|wnUfsoI(9p}ehT28sX~q<4XOoh{(aVH9`(;U*Q-tx8KP}N;O-42E zTjCEIPw{S`UydhRT=MxCTo8_avAuC_9eJ=!-!#c!l|XLjGP_af#v^>k@5`ifcJTIA zSvD%ZE+?D5ggTD@-mW2^IDXJ;b~c-Qnr|v!Fm^G}9}_PM&XjnlQZ+4io;_K7lTt{K^G`b-!L^Xf%-b zP2jdd)iv$s-2Op#ZVuLc7(lqQ-wWr_V$gdwY4m49$^^ngK*ikM-3@A3hN2&fD&UfZ89mLH+? zbGFW)32LyASqUL!zZQj>_B62+FJUo5Gl*08o ztDvmy-3`l@Iz7I!c;mhXU}|bZziI0an3coQ1W39x6yFodYBA<+rQEv;d)C=zYlc?* zZ3XEOuZ#|=o!u}eCa+bHYJGcYu&}=uC|#9U;QmJc)Uleti8Z#snqT(Q%o!a=qL-v( zqQT2~Iy0g?^0I9oV&=RRRZZuRMIVyQwY490p*z76qAov4-ew24|1d}lm@PHH#b++X zWIsO{yn%h)=L?h8GxejbjQcqwyB41UUjKFQ4cXY->;-`?dHImcMgVyO z{RUtFkOmD8M?wWwR`=yi8@1gcH8e$c08{R40Zut0JbZm2U1mIO3F(0K8lORN3a^BZ zqUBX@VyL8qMH{$>8ft2^)}0M%|A|4?^Clu|lXVX1*QlaA!h<*FB2Q%G1q%C?{>%hg zrR+2jni$-pdU}8!txt{X-?{qhaK?sfo3A$;RlrJf>z2nzS~4R|3bBA&dP-R<*2Jz+ zNhp68x7faKw%6nX>!DrY%UVOZtc4`b#EUcKhJM?<`_c~M-L{yDkryX@5K-emrj1uK5jUyJ&4P~EoW#}O86p!M7nQdRvqUT!{R91c$9~VYsKSTZH?_&Yut*&SZ-M3OnIn|6YfD^qC7x!bEXozL2L+0#8 zwRLg+Ci1!+Ru+;$z?dy@*2Is1U`G@du3J{bY!PB#-)-`9Y?T=_2t4Jl(%S}VRcZYI zk;5!c-TMd)@z^y_d7NdMq+N+E>cPEI1N{e4w0Qz+7tK7HIi1fh@%> zjAo7LTJoVDNhuB;S1M#;A8dBi~4S*(rHl!jN8smR4 zhIF1kM?Bg$Xb-AK!aTMDL`X+vO=;;+@(r=Np&jFg9!5~~x$zoCN>9dC^eg6GY*G zEh%tjtIE9;b~!jb`{(O|wX6!)t+oZnbu3k(;=59cH!cUtPm0~ZuGu2P-~D7a z#cgaXM@#wh)9>~QWl;xQP-6R+mr9{;!R4v3ru4x$(iQI2;nI_BS zK@(alvorLnIW)V4wVFfD%BqykH=onkDEy@c*UHLj5wp^L-EH4X z``W#VjnPYfvUS)6?i>lXBDu&X)g;v@g@{k*^sTIWlm%?Kv%LK2`GMh{B(*q+afZNw zpgW7E{gMys&gOfXclU%gM0eO8p>^3>^$VW=$)ipZyPY*g%N0$V|PI2T# z(c5n0sPR(3E$jI~v|t>_84p*g!5aaj?(*MjCV;08PX^oF{>WO%bR*N8OwXs}lrRNo%p} z0pvm3l2Jtc`Kq(v`*G2Cv&xOBKiw>!pn0l;`+-TUY83z0L42*w|Hs~2#YOeDZNn%E z2BD&Kh)5{Xog<)hNJw`hDJer5fFj+E$S9yRNSB~=BPAu$0wO~W`JJP;|NFk5XXD#= z_uk*Tc9fZ6m|5#u*L7aUd4ykG&@27+GZxa0i{q~?-AVR?F6}L~`WKB2L`Qus>t9|! z^|p*+-04<2(*=ffAp8?$H3e5Y?;Zyqb{ruR&jj6Hz3?miGTtG55bOL_5Z`W@W8X4Y z;1m9SJ1JR_!R(@nYnh*@1OW z=&3bln(FR1{le_Y23-^s-!nVMpz*zgyIcra8;~&t%Z#Evngooc(bXub_C65#xINP_9Pv?clc>jKa^6~P6i51j99VL67^RBG ztd$>x652aZ+-VWxg-JqfJqekIrf~4+X^2fBt^AJXTes?31#TkB{nNjGjGzj#bcZWm zG|{pRy~**E@=nP3^h%{=ya#=$z>|Bq&;yxFGEU$g1UM%8ocWhR+T`Q4@T6kRL>p(< zFYRWA8kFo-1!Mjw|HL1lQxU{3vW)JGp^hFqUTYfHjsv903&N=YV%ev8Vk9K{Iaf+Y(bn(81wUf1p?V!?Ok>uWOe;Q8H5&4{+K2fFN*y#btcuH zv7hPW4PI0!TXSRiVcvG)!!F_3_KdOVGe$2ix`xn{iQ(Zi++mET2beT)G;0SgaUdIy zSu?@@3iBbvk@SN*Aux!6OoKvQuM8Xv!G*32u>FZ{-m0I&%4@3Jmd)>Yfqo7k>f%?C z)WBN@dI}GojLZmZ+)#{INj4|z;p5V1`Ux&uYJGZ;cS9izkkp))WnSMdfe(ux0h)}_ zXmM|E4+z+{hY>AKi>0>%6ZNF)e&Q*j1qx&yga5^R0o;J(w~o&wcKCIam1~9P@#WzJ zhBf`*8umMBi_do`zs3f*Ao?81iErnt>PHbMOFW}RYjxS@ffQWd_+<_c%`~PP(`N4z z1r2?BnQ+{CEVIDfbf;{p#V(&BX`I}-;<<~UsHMZ%{R0MJB7w$e#x_Ie-JRBL0oyy` zE3F9@DN-{xSEeo4_vzp7oJXYz5^An*v47um0ILk-G#zI17t^`q8JH_*m5qTZ}F?1EH45i1i6cgdpn;GXid$09o zhHxp?3qfXOQca+4W(!E3Nf_e^7=2Gb$n`#(a}#ABsf*j}(`FWUQMVAtmwny^j(iCkO5Z zyyr2cI{5fRf|2GUg5ojvN3MVj#~~BJeIcVX4qoy;(I+C;PM>TpYvHWo=HbqrgOE}2 z4l*(_{8*z$^PWkc0;}I}S+;rbEoy5(&#EUKUs01g^s zVq!AHzIKo!K>U#TkMC2iuHOJ%i+PnrD8E81Iu9`R^Pa!4*TJr zPWssE%*^3ADHqT(+E|`Fgx64%m6Sh}aR1OUBPT1{+S5~B5RV%X8`J`8O~btx55EvT zH761y^73FwXX5-J{Vm+bJdBy^$FMyEZbggv!9?6iV{@Ghd@;}NJ>iSaWH5bx2k))LWx;JUcZ2Y(i+uJ&veL&~G!1>wuWQ+voi*Rr;^*~e zxaFxCmIe@*+KZa&;6l;hB+o^G!}+jMsh{f-9UX%7`^h=sk2C8L^QYk3;BXEJ5o8)g zo<3xwQxA~Nbt7_Kmg?X2mH9#;Rnvm=3c+hsXEM1=r|>p$U3v2DWR`)Nn&cfG{|r8N zTtV|Y?;sRDezk}Uv|LbG0%t=6dlP8ZB-a#z@h&Oq9r&_D@twd~sWB`#xDB$7ghVYG zfGY^~6n*7Uiop00vl9jT0$+a6OZr zLG%~>p(>StL&(dQ$$YkPyqU1vfl#)pwb>y5Ar4OS+ajt4`Wf@8h`2bWtdl-*<(8_E z8|1A1Qid!X@gF(!3XKvU>?w4sZt^bzV=0s&($8X6?(5aD4|s#)4jxbs@?B%hi!v>~ z$}F(G$)|Q77SwV%>3P&wz$IlFT-4O~nHlHokf(jk4cA8IoWuPCf5Sq=-yyjGkC|Sh=2RmY(<}W zarj{n+l>}>!nc~pl_;$?BxWK~DxZ2YB=SMX15O(*p}jVfdqjcGodA4Oe^<0VS`4aF zquG;W8rveA5kpkb^E7`Z6E-OF^I5yU9fzqPgI*RKAu9bOZ^RBHP#&^-Z=oTA=Om(( zEQc)L8_Ym8kqU=hrBVONx%3z-7VxL%fVI-MSwXHD{Duc z>J0|QH#M~sXifye$g!jBHAU`fyKl4!f>pd3`Z89lE_pidqiC${zlaAV^3})3RvEP~ zQ|?wT&xh7?j6F>3X?7-*Ucj4}>z+cj+ED#9~dH3hgA@M|RBPb6^kJt(n+% z_O`3@Wgyyoc^W&g6eIP9wjncOCYHL5{4%ur$UdvbT<} zJrbHfrP-4FKD&xHxtmqBwX-|E+OtpG>A%k|rUA`}kU33lktK^LaYoPk^~f(4DWN2smAqbruvQ3zX?eAhSFhO$yxxfAVoGn9u+ZkzbnHzL-=dVhMsu{KAb=oOY$_-K$$mX$at1<^(s ze*@YxUo%}k%9eopjDZyXX~Kq>1(vzd#x$9BJ39i1LeiUQX z!WK$`Kg;oshXz=@Tsb!BE9Z!{HL(e_({OpDonBn?@Q`tUCYuomVki@}yp%?w>0L(u z7cxW=l;6SM;RgQT-X2oP^m17twMy{Imrs2|W)n1ljjXHtZX=C=$v@L5l&jPG?pJ^4 zDX=l&_k*ehEW>R=4o;2uG2wmge4j!1;_~S}9;;v}^+3Trf?aYJ*1W({O zL25r3F7?V7bb&D;5c9mU!=q=euPE+n)ARiB4K6vG8Lz*TObNk8%WYhmTbk6;y~yfU zS*}-QZpJ(a^717deU?(K-Qq(N94%EseaM#_K>g~CKlPIjk(8V!&v*^L-jMNp|3U37 zOu!E!7^<6D$t|;kuDH=#*=#pT)aST6go|J;&yo|r%n;8zRblhSWpr^FTs0}}D@~4m zP+67+nWWv=_KbVG&blXgi0QM)(T|R4T??)0>Lnv3C4FErnx7KbF3#I&B)D(KA#Hr1 zHf)UwTM^6AObewoIcFRd-4sAAHhY0aA-lMFGQfZXpOLWe{JvzW<>#B!H^%dfLi-l& znht!5;)fYZOaeWg5q{&On)&&{`uSaB^yQ!?3dUPaCg;psIHjLA(KSskCyrToH4+kj zDeQS|nyL^HB-OU`vCMv~N?1g}vb*%>FokYxzftdmn9ts(_PLIQ;)gTRl`F(vs();B z_O6R5aDR&n{D&8i#0txn0Q>OpcGyFTyoh`^PkB-lhXH#-9=p$yWPFC{s%3lz{?ffD zRlN5Fx2u1;MS0MiiTLT`H5}*4G13tf6|azrcJ_>|OVMKwdf}}NMI-v+@&Q_yg@LZh z`;^2Xo0^)mH;P%^kv2}{-jMY*ccsFM8YSMDuGZ3P-LbU%sh@Nl5z$50%`TW%Ln{qk zpz%JmPbBfpJajvOJ+Pd%@NpnzrDP;*DnYm%}_TF z2TJh1+(MsdL$aCSBrG;t;{^F^N60>=j2yC^ER-E2^1ul?6C4`xG)H=M)Q|XD-09$@RGf1v9qN6#|Bbc zSu_kXz1e{9kEV01T6#vJe1?soe%`O*XBCJWp#`0XE`0UM7(^`lOpjb@-clAzA&>R zn1qF$wx-ltHIC=*-*I5vj+leRW-0fA@NEdAxi-88+Sh)KQ%P?`0Lxn`d?H%yxnVh) zLZsSZYmqI<&&I(VXeQW-f89p}hsl)t&tzCM1>1evH%;)=YTI9wK@H@H+n#ZMj<6p7f~!VyVh9cHOeVN zyJ{nA>KHQlBCBJSscAf&YukEj6znA<_|#l_X};{?vr=92$4wwecy0H532LIyEB0AD z!FR0!0bd6VJn?CY4& zGWjtud*2Sc{EP?vj;S`O;^WfT$s{w@1ANXsFur;=sM(I7LoZ@7A}ifjn1^8o3<*N1 zf?dNHzHJUDm&|KM7rZ}k0N7f-rvqz!k(Gv&pHx=+QhNW`W6a*RepfPnV%k)A?%GC; zOOMat)-WNDrX12~cF%qN`)3Gy_Sub;ku0&5J??8`PBjlewo-KLZ7n-iua*^~(Rz-x zZl_G`e~*9!W7m)dc@mf)oF~{OL?CH=_=wa~evcP8edZd1VqymE01;~;hvzQU6&BuW zewX(S#F}q96ozDtLiJJ0D60PGdpbHoz`PgNyJm0!n(6(Rs{wO!{dhU|ko2fPSq}k| z2oUM7PTWAh=#Y7}^XaRM-sW`z(05lec0t+ z_Y4iwY6H028H{RRjasse;+BfCGRVr0BVyT?*D+gt#x+PzaXy-`Z60h}pPUqQ)wO4p zLioS155aTNe!A{_{ai3piRIjx+N^>0wu_rF)Wfc?tWP!XN(uB8WH7J_HdWb(#9W!p z2w-5`4&Ow}u0}qX^U%t?7I=NIX2m7+;7l@SyDmji!u8UC4BOctb>&@il73^F%9wK& z_vC&1zqhnGQTnedhar%+Y1@R7CH=Z*Pikwn6Q}dFY1~?W(zldyLph7@O4|jm`9!Nm z%QsQwS0~Ov?%gkOj$^xeb^43cN6d#M*>*j3Tvydz7}m6LyganBhOwfj0~#s&uQi?! zBHb*AFyZZjBFopS-d^)N_5z!x z#Z&2Vwgr{anF0<6Gju9KbC)snzN;UGeUX!%fE84JJJ_J}St)vjKJT~5uyi+yqz0e% z(KjluHMRvekMV~s>5jGczk z;jdjiKVJ%a)#TK#L&jUjkQ}X2QdudacI`lWG5*e1QlwFeo$5wiYrGEC64z%ndz9YS zYmConZkyq8Ak7GP?$*w4M|jwWZjNTza^WbR#Fh?W@=5lRr)au!H6$ZX3eH{ymkhqh zmJwpb5^0p_3qFd*>R^kIcNrNU>)f)Mj{P(*G~?U}KUqYHC^WvPWH|_~7L!9iw|5zo zUEh)EFd&S5j!vCyxY}lt1=}f3KUcg60n-K;W3%#dzB98C39+#j0Xu};{!I%i4d%^a z1RLh0j67DBcNs5nD} zv8chkO*Wq+xSUr`>)lGfU2=8FQ>WCrZ@w#8%lp?y zhHz2mRkz^)rRxm)zd*J8!8`M4p}f9!vGIJ9Cc9QRcz(qN1e_;3T9iTb$D+?3ZDt{A zSIfpGeRd2mw^#RnfZ5cXdRzcv0=Yanb@0~2kiZDJ^v1CkGar0~mDYDL0|djhK5gNUDH0x4qM8zC%U+75XcyDxr{NZP;6h^bo5)+6Im;M z=@Y6yhBzK~fd=7V6r+Oq4!gF~J(WyvLxx_beZClV%B7x)zW2JRcR!2RsW(8!Ep=xg zopPpqhe(Xq_038OL%5*nXMDQWo<*!n)Q18ew$e^es232!?6FePGwGR?dhPyEqu){H za0G(jjw89{$_k8(*B#-A?GezQnuxtQD+E(+xZw9QwsY;VtQJzm_cS%Jc2(3)4JQIU z?GKi+(%T!sOAZ76bh)}8wRkp?qZO*ozj^4~%eVjPKZ6%He^-4BIHlT^ACYUQZ@dF!8 zel$tWt571r!@!HXH~Lr9GML?m7Wj50lrTC<{B6}kvWl2_@K!Y5ZT(hCKuk4%#U^Gm z+}WE7UGgBbzxQcLsRU6W;?}K(kGP5u7*=NR%E<;C+|r1=waI>FPs(yOAMI&_3XWFA3VCErJwN_rRKX7#Rzx@ z&AFC?Z}`PdlPZ451;kNCKu`U*Ct}+pGfy^phB*!p0O2QErN`RKhs146hhpWO-uXDQim}Ty9y}upv4g#GZiy9T8FR$Rav>%nvOo1R0 zp>trVteP?jqM3bH13J&H>twtAO+fz_)DRFfX)q_Z0GAPAsUU((YyU9BfT!0!5UE!# z5@?1U^vdVP;u6{RlF6IxrAUu*sy7la9Y0JDU4eW5Mzh#kxQt7ShpUwG7I5knnap-} z)bPG7FE11F6f1^x52;jN>7;Kd5SOB!jgiD1D3vr@IZMeM>5(OrJA1&iIML?S_dM=Y z^eq%f2O)SW{7uMon@-G!m#$0~87wuao-`krztogGVkrog^xZ(=5iXP-m$usGg{a`R zhP+nCBcHc=aIBrjX^1c7juRM4UB|l?5JC}tAXj22)26CH)aNY3MhwdK%^S;wqQ5YW zWj(^&4`ftFH=jHu(%rx;#Da#uwsaE}t-M{}Dy-={D*zHFDPpnCTjht0Fh7H4Qn%3f zV~wYaTNfBWFgnhHm7t^DgM)8RiR<*sm3>3Nfb6)>%3>I6R1!V-evVhlR~u+T9w{Oo zYqdLN+o)W>gQAvPMOCo<+%y>L^3`Ylv9< z_yQg-YhZ06K`-8e*hz7HLhbtNBic}0zsPjvtXiO{vwcQ>#dv?^nkduQt zQE#!N{%~S#*7ZBF-|o|u*XY(*kV3ZBt3BQN5UKJ53_N2xj&SQXQf25oQT?}GnfJ8r zXp+va)m48$!$hOM|Cua^&Ud#tl&XIB`^(_=_E$>d;Eq|mG4<$h3*1zWfogy05((De z60@-YGg}in3cQStWXfjgu-VDn(`byKt>4hjXGjns)!)EP?;Zpbk4T*8yM{cbNNfXN#+%N1@TKrTvtt3+)Sl9 z+M$MEq7SWB*AI=kzgxP6QOtK4gL*=>|GkWg&c#%33fI}z-@aWj(n@jyN#4BNPu|Y> zsLofP;9L)xz#$Ua>>#cQ8|b(fd}W^r|3;`F$+nKh?C@Q(m5nN!GtW)Z*a^IE+mg42 zr{g^+EiZ3Lo&^W8x9wh$>C!@ZZ6H?2LM4dCs8j1i(A{0WCDYzfkW0YWo~Z8T_CeX_ zT8FOHO0=`iLt<$B6z^;b+;%VMet${V@SJrw?+p&BSf;ysqY*r!HffjRqSRu$Cm8uU zsJ^62c|OG@XZqs7e5JkTo%9JR%aLHW0=BcGI0Pg@4ecLC`>cr8|MCJjiH#%%o`(^z z`e&AvX%ank*0C^<)r+UVJ*qvgm}bhpyRwXXelqQ3Q_@#SU$Q!{R(fKv!hkEo?%0l? zkDs=F;D8lcAf=_Phe9Fb0cs~u8rz1(@OJ7n&>|NlLVZeovu5MJPkR0ABUUYEOt-mz ze-NC$_P`uR?Vh%E0)j>N>AN$Z=oXJ6qlrEFSz6U@dd@Fp4d?2Z-QWMbklx|;KSm6bec?Chbu6-6f>t~7#V;|BzSex!yD#6 z1w-$3s;S*(Fl`{OrS+5vTwPJvPJ260u{58IFs$t7>|xJc`o+83X6Or~pO>U;Q67gu zVCo2VP8Ld`IvEp2$7{!Re&C$DfjBN%X0L9=uN}*;X7}B*8EwOyKJ5p^*r=Z4D6Sw_ z0E%Ngphj0=Gqej=%*)Nzt~Yu5dr#HyjDhC7^A=nDND&%PGt zU65F5BHYIKxly`OeAX^^NF_LTp@D1YdQ#QN6%`vCnGrp2H;WkWo0;-uh)B5 zF7bNfjg#BjvBu>D>?!QEqqNkUhhcr+a<}exH101sYTrv@@|7eXpm>jRG3pj|4ZeVJ z3+L7#BcZcNulEP-_ zh~KYGM(5o)WNr~_Eq!&Pdy}mzdLOkB-b!k5K|%Q)FJIi1IWi4H-E7umCFl%Cswv1l zeZvT-EM-#$57Edng|_ZxvDN4vtsu^=tiG=b{gyeSIQ|)L@#TcRwq6{gjS!E@6=Iru z#TImLf4aNs6{UH@Yfs*-yN@0`yX3mU=~ZT8_zURs3v+W6xM=WR1#=p9Z9p^%3#;E- zEr{+dW`Z20cnW)cL^_kMkkQqw!zCRBJB+3gz&*H2FspAw)zn0f_lOh=O*jTpY>~XU?a%OQv7RB%P6+? zQ8R^AOlQgsy744FTSMI{4PP}QU(6g>x=xZ`^xGRqM|l>r=GPzXy~>(BnDz7B*Ta0M zJqo1SJ1BkMWlg?(XBj&MmbI_Roxy+)}Gzd<>(K+Z4~5#MAG>5 zc1~&9I}6cy`96W^w?lgu%i$8uE6Y-ls>|EsO*eT$)-LHSVpBH{-&LYR~E)HOv|5SdN@OlW;kb z8uD7Ch~&5>-!jutzRs99L>IXK)GNt;hUdha~6jSZDZ@c2-|rxp20^ z@iXIfCMM8c;h*l5H=$UKNWX1+=@uq6V&)M9T!L@*!N{UbVwnv8b=Ien>@h1Yp6@Di zMpu_am{h;2LppMA?^U)hNqmo!YvMm%500NwwJBV=<~cmX9>rIjmd=IKB$0pLJVhHC zD%;d%vX5&%2?zh+8YkynZp}eNZfEgYs)kjW%n)=o^HQm{d}kmW+z&~6My=amLa={| z6)eh=z|vV2kGg@rzGNZA%R#04QaI#Zr(bSgjZ+R@^Ih{Q=_0=f29sWp{)b)KAO&D{ zqudB2&^8qj0F%%p1P|)v#*BQLELtbgS zc5broCbKxtFV4^2)^k&8#ab3!{fr8LI$nGG$M;>4llyHQV0%q9UCr_lS82$EXI|V_ z+$-1|l3)7Jn=I+}Ny5-#4Zl}T&HRis-dDatsj9-+55=)yzS{0qOJveoBHVT77F%am zS6+7ZNR`|B8vVT-^J?J}&vFGg-=Ss{nyqK0%BQa9+Rze|?DQLI2e%c5NuRkwN~$Y> zg#>M#RPKevU8Ovxr>nJ7cz5o{>qw43_rs+xi0y)W4fcYlEYQtJTk1PDgce;|U8X)M zHKsdi)_)1py4h{NM3R|&{tQoOc27nee zO&)lBw;qSK=Kgz41J}O0=v2{Zz@o)gOJ7+bVD`mY(DuJ~#$3_b)VZ(bT)h2lQS0y> zYrzW4Wq@Xss=wunpy~`R_;7R%Rg=P`R=(J6!?S;0a;lEcL@A1*A?ETK zGkN}Dfi0qMfX(bBbkq$E*=a5dw_X!dg@!Kl zzl!jk$St{*X^4jNT6aBA-F_kD6mpT131E6vt0z|{3G{*_y}R`a+l|PqBJVKno4{jz z$8c}Rz6xz4ohbR4(`&ob8RM=qbqe!Dry%MiIy8#A_^f@DchJ_dJ)vW00yH**23-Z6 zp{VF5&hZOW8*6ax!S92-iBX&jeWyZZ$(K)1OTQX0!sYv_i5d;B1UP%X;&Mk{V%_z*+&%AO z5{FE^L1^9Lnt(7LYCXjHbm;+uh|V@@MWxHd8WHe3)$(*Q!q0b69jgDa?^$V4O07%5 zwdvOo*B8~1DzoZwj`5Z!xB|NZ38{Q*oH9}nx3mkt4e2=&A~oGxfl9_c^@g>ehP#<3 z8yH+mLy<4XKEEWZ-x|`x2h;%yvTPL{vP8_|+ls7T@3&7qY9e&L?+rro`cJ26g}Y9xtRdQr^xYE=ghbbOXVA(3T=O>_oCl{kF<W5}YWeV$*W+7DOto@q!?MI9Arw5Qa7kg_*(m)W+T52OO*Oq`3oD9{ zvL!#An4HXNcI*)Sg`axnBsmU-<;{DglFBg=+PU|PQ_;Nzao)w~bvc2Tm&+Uy;|iW_ zp596{G$?75U3+84Zs+)pU>T4bIim<=BU28BRqE+yB)ASCZ_E4a_GqI`s_T&vmlV-f z*&dq|4dw!o%!+6E#C-*~u4;p-6JMOhs86UgmmA%i=Azex%e-wziB`xNjRq^ZS>}P@ z+6W`{P}|7V1``^b#;6(d-dn2ZWHlI(9q3FR`>(tPW$Zk&i@KVcq-Fe-S1CFbgNg*2 zISbyukxOd0eH8n&Ui8aiD80(iut54n-irw){F8BK+PMnPgdutO5dt3m8(HL$R?kLj zYzErH5pff&$O_vT#U%mv^yo1}yZQ=h{m*H>VDNdQ?f*xv8xJ zMW2JCNN|_+7?-Jb$3lzo1aml5;#I@)&DU^O;FT-E8cM`@5SX`7c*a-eZWP1jqx5C zFc}C8Y#24mO)qb%hxMnj(4+x1C^jDKTIAC2qbie|qLTJ&YTMh@3bqvHk`5cm&rzma z_QE{oV;cNoHFszqV&Z=3M+@(D&(sqk95YO4)i8gc()5=&cDd5>Hk$ac1eHKS(Ao3^ z313n3a%{GBj}_2iYm9U<>+7lnW?KTkY2qY2f+q89!c>31>0S4Zbucc?>iM>_{<<

vC@#i-0Kk$w4@Ai6-m+Tm+yPUVgwHP;WAID=pIGG-WDah|pcDFapTfU?+! z++4e#HJ-!=%SNhoZ0I*g@G5T<|9~~3M_}-DPf#aunqfl?*03IRKE7|t_bJ%-M; zuC1$M`G*g+P-*tZx&W&DwMhlcpcf0AoK8?b2wQrReyDh)mj zvkn$30we{C(O+mp`}*dFsco&Ym&tXFAV znAcjRmGKGcB?^k-M^PRg%}ZI1K)0`p2Bg&q1F-pv$x4<_f`Bzj<33^{p(2XP82yU{5Z*#c zfjJ6j|K#U@*)5?0>})07#q=S24O~G8%JQpmnub!8iOqYqPlnG*S{t0c5XYDvso?@} zzQq-U?nZ>Vve@9!mxnDhJ3-6&Sd}JtYOgYzLFsC0B@=Wj#k(C=F0?b@w-JDPR+q^* z4DAwxw1_Z2Jwh|epbA-*1nK&p$^21C2Ya+i(n>os0dFFsSRV!NX-nLq}&6cV`N`yV-Bu8i$XP zwy8xp4=GzvQmJe=s`s_@UyS802sC1a9 zn$mZRHL;W?ln+CGdyG#?dZn!q;zytn#AJYh1}~&^JS}z_UvD9{G-|8X+s&k_Ep^RN zW<$+Z?ZhQJ#+@F;6iweA`Rv@FXhD<6I2YSE^@YzkvtSiP-@7~#EA)Y9GrAn4AN*BW4rv5nkzbRDEw z%Bc?$N}7E^_8Z(uezJ*x!V~aBNoK3a2Rz(7$&^%DzMFm6a2%FIWo!(MPlf4SAD=^9 zVo|5BHd!{s7b?ef(cfPdv&9d%jqKDrhNQthy?hd>Kq@RE+zrrOp+$b(-UBSB-0hH1 zz#GWQ=`g_vEu`>3_YoYORm3rO+-)8{T!+k(iy9#4pMZPID#f?TdvQwWo4?r3cu7?i z63b@z;=8}<@%f4t`O}T68?3Ch*q9yYh(aq{tH3ZXeXUDif_I&J=+6l=HjVJ*sSQ=C zDnxO7!Qs^Tt+tl_2;mTv?uXsgE6xzHY~ zkC{q==+#2&cd$Tau!#-v;a$jiD4dF2U#E_ab_%`hz_i{Eaw6C(cfW(cC||!)ZK#zN z>t*e5xRGx05nNYq&9bg<7h3?ShE=i37j~8Lhu_I$Xxw7{jv~4KH>63TTAZ9{%!1xpm8^@t!0vBtezymsH8TO*jliuR=uJeG#6r7;iS}|UQAPZ@^wjM~x zz$~PV6`Dwa4$7ptLnWDiwKX%WzmzVM1+M0DnSJxO&Qt*oBqIn3b`ZaATUjmqxc^>Q zSY+p`El1a#OKt4cD`+%p0WUv4e0i*Y1|+k@sxW7FLO0=AhAnSo z0Q41O;^Kxt+HGZ}A}~r)^=E7VBln7ovB`HCXTdrT76FEn4Zr#|PDI<+38&IL)!A!4 z!?W8k&Erix?vW*`D5u2NoChZ%eKy~bBH;MF!?7HE%U)O=GostVSm34 zP6!7H=kLg@e;&c%R8~rn{T;#e&tnab1b#9Io%#091MnY`z@{?Mv?u@b(WoYJM9RM$ z=l;B0`nMg^zkf9|9wVW^S3aAQGOm{|%pdVp9HX6#vglag0-v1l#`SS9#h*{!r?J z+<$)*JlSYp0`ZXlep7#mqIm}jr|Vev`Tsbi%o-y9M@Ae$(UG+Ce}7I+ebbQtaaD{* zFh~SJ@WOvzD`V8@&n|+$3#z|=B;@9Qoj8B4Rx3p&^KXBj#{Nrg@!vn2MKF|^l3Z8) z?UZjayMdt&Blpw!*L@DJKgNmfLmbkEQYJTPDQ29+At$9Q?>nB-`sY0r9|$+_<_etM zZ#EzLJxrROV6*D}br3Y1IpwrzXB`sokTc*MjG~Ec0jbEsftNKk()6jMY<4#w*!S0Xy2V-nuETtjy08 zifs_RU`~64T>lP6_O=%8?%Q|C3|!}RtQbLoAB&j%_O#q#g0k%0A+aFn#C+{NTuMy`>|@#!S68s=+i}+PqYoH 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°7L>62e>=Ut-G7JW1usue7LEMIMAa<7_2B9G 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 z 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 Date: Sat, 12 Sep 2026 21:37:08 +0900 Subject: [PATCH 16/21] test(console): exercise the effective noncanonical endpoint --- .../094_console_fixture.md | 7 +++++ .../responses-console-go-upload-retry.test.ts | 28 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 devlog/_plan/260912_stream_recovery/094_console_fixture.md diff --git a/devlog/_plan/260912_stream_recovery/094_console_fixture.md b/devlog/_plan/260912_stream_recovery/094_console_fixture.md new file mode 100644 index 0000000000..31e4e77c5b --- /dev/null +++ b/devlog/_plan/260912_stream_recovery/094_console_fixture.md @@ -0,0 +1,7 @@ +# Effective Console destination fixture correction + +Hosted macOS control run34693025423/job103551974484 failed the canonical-row other-host fixture. The log records that routing discarded the configured other-host URL and selected the canonical Go endpoint. The test therefore never reached its intended negative condition. This is not evidence of a noncanonical replay. + +The negative case now configures an unsupported generation path on the same canonical row. Both supported adapter path fields name `/unrelated`; the assertion requires exactly one outgoing URL at that path and preserves the original refusal. Existing custom-row other-host coverage remains. A separate control records the two exact `/responses` sends produced after canonical base normalization; the exact Muse model wire default selects Responses. No production code, policy, retry count or assertion skip changes. + +Local tests/build/typecheck/install are NOT RUN. Independent source review and updated exact-head hosted CI are required. Windows pnpm/Devin failures in the same run remain separate shared repair items; no baseline runtime pass is claimed. diff --git a/tests/responses/responses-console-go-upload-retry.test.ts b/tests/responses/responses-console-go-upload-retry.test.ts index e4b569dabe..4421848bf2 100644 --- a/tests/responses/responses-console-go-upload-retry.test.ts +++ b/tests/responses/responses-console-go-upload-retry.test.ts @@ -164,14 +164,34 @@ describe("Console destination and translated recovery controls", () => { expect(new URL(outbound[0]!).search).toBe("?tenant=fixture"); }); - test("a canonical row name cannot authorize another host", async () => { + test("a canonical row name cannot authorize a noncanonical generation path", async () => { + const cfg = config(); + cfg.providers["opencode-go"] = { + ...cfg.providers.go!, + responsesPath: "/unrelated", + chatCompletionsPath: "/unrelated", + }; + const outbound: string[] = []; + globalThis.fetch = (async input => { outbound.push(String(input)); return refusal(); }) as typeof fetch; + const response = await handleResponses(request(false, "opencode-go"), cfg, { model: "", provider: "" }); + expect(response.status).toBe(400); + // Canonical row names normalize their base URL. A configured send path survives + // that normalization and reaches the effective-destination recovery gate. + expect(outbound).toEqual(["https://opencode.ai/zen/go/v1/unrelated"]); + expect(await response.text()).toContain("Invalid upload request."); + }); + + test("normalization to the canonical endpoint keeps its bounded recovery", async () => { const cfg = config(); cfg.providers["opencode-go"] = { ...cfg.providers.go!, baseUrl: "https://other.example.test/v1" }; - let sends = 0; - globalThis.fetch = (async () => { sends++; return refusal(); }) as typeof fetch; + const outbound: string[] = []; + globalThis.fetch = (async input => { outbound.push(String(input)); return refusal(); }) as typeof fetch; const response = await handleResponses(request(false, "opencode-go"), cfg, { model: "", provider: "" }); expect(response.status).toBe(400); - expect(sends).toBe(1); + expect(outbound).toEqual([ + "https://opencode.ai/zen/go/v1/responses", + "https://opencode.ai/zen/go/v1/responses", + ]); expect(await response.text()).toContain("Invalid upload request."); }); From 53702cacd6a0187017f8a27f500f459d763fcd56 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 13:43:55 +0900 Subject: [PATCH 17/21] fix(combos): normalize target reasoning without losing unknown ladders Carry #4319 with native raw-ladder distinction and regression coverage. Co-authored-by: Keito Itagaki <171206780+ke-1t@users.noreply.github.com> --- docs-site/src/content/docs/guides/combos.md | 11 ++- src/adapters/openai-chat.ts | 4 + src/combos/request.ts | 20 ++++- src/server/responses/core.ts | 1 + src/types/config.ts | 5 +- structure/catalog.md | 5 ++ structure/config.md | 2 +- structure/data-planes/inbound-compat.md | 3 + structure/providers/chat-compat.md | 4 +- structure/runtime.md | 2 + structure/transports/inventory.md | 2 +- structure/transports/responses.md | 9 +++ .../openai/openai-chat-hardening.test.ts | 39 +++++++++ tests/codex-integration/combos.test.ts | 79 ++++++++++++++++++- .../server/server-combo-failover-e2e.test.ts | 54 +++++++++++++ 15 files changed, 226 insertions(+), 14 deletions(-) diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index ef5ccde16c..c497ec918f 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -297,9 +297,12 @@ as wildcards in both modes. } ``` -The default is `"strict"`, which keeps the original behavior. This setting changes published -catalog metadata only — it does not change target order, failover policy, or which effort a given -target receives at dispatch. In the dashboard it is the **Adaptive reasoning ladder** switch in a +The default is `"strict"`, which keeps the original picker behavior. This setting does not change +target order or failover policy. At dispatch, an explicitly empty target ladder has its unsupported +effort/thinking controls removed in either mode while preserving supported non-effort reasoning fields +such as `reasoning.summary`; `"adaptive"` applies the same normalization to an unknown target +capability, while known non-empty targets keep their existing per-target effort resolution. +In the dashboard it is the **Adaptive reasoning ladder** switch in a combo's Capabilities section. ## Image / multimodal capability @@ -414,7 +417,7 @@ Combos are stored in the top-level `combos` object, keyed by combo id: | `cooldownMs` | No | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Integer from 1 to 600000. When set, applies as the per-target cooldown whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. | | `waitForCooldownMs` | No | `0` | Integer from 0 to 600000. Maximum time to wait for the earliest eligible cooling target before returning `combo_unavailable`; abort cancels the wait. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max`, or `ultra`; applied only when the caller omits effort and the target advertises support. | -| `reasoningEffortMode` | No | `"strict"` | `"strict"` intersects every known target ladder, so one target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. Metadata only; dispatch is unchanged. | +| `reasoningEffortMode` | No | `"strict"` | `"strict"` intersects every known target ladder, so one target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. At dispatch, explicit empty or adaptive unknown ladders remove unsupported effort/thinking controls while preserving supported non-effort reasoning fields such as `reasoning.summary`; known non-empty targets keep existing effort resolution. | | `imageInput` | No | `"auto"` | `"auto"` or `"disabled"`. `"auto"` publishes image support only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. | | `nativeAlias` | No | `false` | Explicitly permit a currently supported bare native `alias` to take routing and catalog precedence. Never inferred from the alias. | diff --git a/src/adapters/openai-chat.ts b/src/adapters/openai-chat.ts index e338d845ad..e4136ecb3e 100644 --- a/src/adapters/openai-chat.ts +++ b/src/adapters/openai-chat.ts @@ -130,6 +130,10 @@ export function buildOpenAIChatPassthroughRequest( for (const field of CHAT_PASSTHROUGH_FIELDS) { if (rawBody[field] !== undefined) body[field] = rawBody[field]; } + const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts; + if (modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0) { + delete body.reasoning_effort; + } const openRouterRouting = resolveOpenRouterRouting(provider, modelId); if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting); diff --git a/src/combos/request.ts b/src/combos/request.ts index abafccc525..63c5ba7fca 100644 --- a/src/combos/request.ts +++ b/src/combos/request.ts @@ -1,4 +1,4 @@ -import type { OcxComboDefaultEffort, OcxComboTarget, OcxConfig } from "../types"; +import type { OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboTarget, OcxConfig } from "../types"; import { resolveEffortAtOrBelow } from "../reasoning-effort"; import { resolveComboId } from "./types"; @@ -59,9 +59,14 @@ export function concreteComboRequestBody( target: Pick, defaultEffort: OcxComboDefaultEffort | null, targetReasoningEfforts: readonly string[] | undefined, + reasoningEffortMode: OcxComboReasoningEffortMode = "strict", ): Record { const clone = structuredClone(body) as Record; clone.model = `${target.provider}/${target.model}`; + if (targetReasoningEfforts?.length === 0 + || (reasoningEffortMode === "adaptive" && targetReasoningEfforts === undefined)) { + stripUnsupportedReasoningControls(clone); + } if (!defaultEffort) return clone; const reasoning = clone.reasoning; const needsDefault = reasoning === undefined || ( @@ -104,3 +109,16 @@ export function concreteComboRequestBody( } return clone; } + +function stripUnsupportedReasoningControls(body: Record): void { + const reasoning = body.reasoning; + if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) { + const next = { ...(reasoning as Record) }; + delete next.effort; + if (Object.keys(next).length > 0) body.reasoning = next; + else delete body.reasoning; + } + delete body.reasoning_effort; + delete body.thinking_budget; + delete body.thinking; +} diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 8b499d5111..08c561d75e 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -2927,6 +2927,7 @@ export async function handleComboResponses( pick.target, comboDefaultEffort(config, comboId), supportedLadderFor({ provider: targetRoute.provider, modelId: targetRoute.modelId }), + combo.reasoningEffortMode, ); const childHeaders = buildComboChildHeaders(req.headers); const childRequest = new Request(req.url, { diff --git a/src/types/config.ts b/src/types/config.ts index 52c4ed8b0e..a8d925188c 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -967,8 +967,9 @@ export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" * advertises no effort control (`reasoningEfforts: []`) empties the combo's picker. * `adaptive` excludes those empty ladders from the published intersection, keeping the * control usable for a mixed-capability group. Unknown (`undefined`) ladders stay - * wildcards in both modes. Dispatch is unchanged: each concrete target still resolves - * its own effort at request time. + * wildcards in both modes. An explicit empty ladder removes unsupported effort controls + * in either mode; adaptive dispatch also removes them before sending to an unknown target, + * while each known target still resolves its own effort. */ export type OcxComboReasoningEffortMode = "strict" | "adaptive"; diff --git a/structure/catalog.md b/structure/catalog.md index eda6f26e80..b9b5d35eb3 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -240,6 +240,11 @@ wire-clamps ultra/max to each model's real top rung (e.g. gpt-5.5 ultra → xhig (`src/server/effort-policy.ts`): they lower or preserve the requested effort rather than rejecting the request, and they never raise it. +Combo dispatch reads the final target ladder through the same `supportedLadderFor` authority. An +explicit empty ladder means that target receives no effort control; an unknown ladder receives no +parent effort controls only when the combo opts into `reasoningEffortMode: "adaptive"`. Known +non-empty ladders continue through the existing per-target resolution. + The `ocx effort` CLI accepts only the same canonical cap ladder before live probing or persistence. Its status output preserves unsupported legacy cap values and reports that those fields are ignored; the read does not normalize or migrate them, and an ignored subagent field does not disable a valid diff --git a/structure/config.md b/structure/config.md index 4f532e789e..4fc23a46c6 100644 --- a/structure/config.md +++ b/structure/config.md @@ -45,7 +45,7 @@ matters for maintainers is which groups exist and who resolves them: | Group | Keys | Resolution rule | | --- | --- | --- | | Listener | `port`, `hostname` | The listener owns the port; `runtime-port.json` reports where it actually landed. | -| Routing | `defaultProvider`, `providers`, per-provider `selectedModels` | Explicit `provider/model` wins over `defaultProvider`. | +| Routing | `defaultProvider`, `providers`, per-provider `selectedModels`, `combos` | Explicit `provider/model` wins over `defaultProvider`; combo dispatch uses the selected target's existing capability ladder and does not create a second catalog authority. | | Catalog | `disabledModels`, `customModels`, `modelCacheTtlMs`, `providerContextCaps`, `contextCapValue`, per-provider `modelDisplayNames`, `codexAccountNamespaces`, `codexAccountPickerEnabled` | Catalog state is derived; config only records intent. Exact provider model display names are durable display only overlays. The picker flag is an explicit visibility override, while selector mappings remain the durable exact-routing contract. | | Retained state | `appOwnedMemoryBudgetMb` | Process-wide eviction target for app-owned logs, caches, blobs, and continuation payloads. Default 256 MiB, valid 64..4096; pinned state may temporarily exceed the target, but every pin-capable store has a finite local cap and their documented aggregate stays below `APP_OWNED_WORST_CASE_PINNED_BYTES` (512 MiB). Neither value caps RSS or native runtime memory. | | Transport | stream mode, timeouts, proxy settings, `websockets`, `emptyCompletionRetry` | `streamMode` persists in config.json; Windows services need a persisted input, and macOS uses it for explicit eager-relay opt-in. Empty-completion replay is an explicit top-level opt-in because its second upstream request may be billable. | diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index ee84baebee..88a5510042 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -19,6 +19,9 @@ the native passthrough there is no canonical Fast injection and no wire mapping: and `fastMode` injects nothing here. Resolved-Fast-policy injection applies only to routes that take the Chat -> Responses -> Chat bridge below. `parallel_tool_calls` is emitted only for providers opted into parallel tools (or pinned false by the existing provider opt-out contract). +The native passthrough still applies the existing model capability authority to reasoning: an +explicit empty ladder removes caller `reasoning_effort`, while an unknown ladder remains +unclassified. This guard does not alter the separate raw service-tier contract. Combo/policy routes and requests that need Responses-only hosted tools, continuation, background, or storage semantics retain the existing Chat -> Responses -> Chat bridge. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e0b87add2d..efaac6c8e0 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -22,7 +22,9 @@ content converter after validation and imports no optional subsystem. Stateful developer-guidance injection reuses that validator for its raw insertion boundary, so parsed messages and stored raw history retain the same task/guidance order. -Native OpenAI passthrough sanitizes routed reasoning history so `reasoning` input items do not send +Native OpenAI passthrough consults the existing configured capability ladder before forwarding +`reasoning_effort`; an explicitly empty ladder removes that unsupported control while an unknown +ladder remains unclassified. It also sanitizes routed reasoning history so `reasoning` input items do not send non-empty `content` arrays to upstream models that reject them. Chat Completions bridging repairs orphan `toolResult` messages by inserting a synthetic assistant `tool_call` before tool messages. It also repairs the opposite direction (260718): an assistant `tool_calls` round left dangling — diff --git a/structure/runtime.md b/structure/runtime.md index 89276a1839..669016a8a7 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -146,6 +146,8 @@ The server exposes `POST /api/stop` which restores native Codex config, stops an | `src/providers/registry.ts` | Canonical provider presets for CLI, dashboard, OAuth, key providers, and metadata. | | `src/providers/derive.ts` | Enrichment from provider presets into user config. | | `src/oauth/` | OAuth providers, token storage, refresh, and auth-token resolution. The login callback listener binds a per-provider FIXED loopback port, so consecutive logins reuse the same number; every response it sends ends its connection (`Connection: close`, including non-callback paths such as a stray `/favicon.ico` 404). Stopping the listener does not close an established socket, so without that a pooled client would deliver the next login's callback to the retired flow, which rejects the unknown state as a CSRF mismatch while the live flow waits. | +| `src/oauth/` | OAuth providers, token storage, refresh, and auth-token resolution. | +| `src/combos/request.ts` | Clones each selected combo target request and applies the existing target capability ladder: adaptive unknown targets and explicit empty ladders receive no unsupported reasoning/thinking controls, while known ladders retain per-target resolution. | | `src/adapters/openai-responses.ts` | Native OpenAI/ChatGPT Responses passthrough. | | `src/adapters/openai-chat.ts` | OpenAI-compatible Chat Completions bridge. | | `src/adapters/anthropic.ts` | Anthropic Messages bridge. | diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index ec75ff03d4..60328ab2cb 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -14,7 +14,7 @@ surface is listed here so a maintainer can find the owner without grepping: | Adapter execution support | `src/adapters/run-turn-queue.ts`, `src/adapters/tool-catalog-nudge.ts`, `src/adapters/identity.ts`, `src/adapters/image.ts`, `src/adapters/upstream-http-error.ts` | Shared machinery: turn ordering, tool-catalog nudging, client fingerprinting, image conversion, upstream error normalization. | | Cursor (beyond the sections above) | `src/adapters/cursor/live-transport.ts`, `src/adapters/cursor/http1-bidi.ts`, `src/adapters/cursor/live-models.ts`, `src/adapters/cursor/transport-retry.ts`, `src/adapters/cursor/mcp-manager.ts`, `src/adapters/cursor/thread-continuity.ts`, `src/adapters/cursor/checkpoint-store.ts` | Thread continuity is the point: a retry must not start a new Cursor thread, and a validated checkpoint must not rebuild the full root history. HTTP/2 remains the default; an explicit `http1.1`/`h1` pin maps the bidi run onto Cursor's `RunSSE` receive stream plus sequenced `BidiAppend` sends, and applies to live discovery too. | | Claude Messages | `src/server/claude-messages.ts` | Routed translation, a native Anthropic passthrough branch, and `count_tokens`. | -| Chat Completions inbound | `src/server/chat-completions.ts`, `src/chat/` | Inbound translation onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. | +| Chat Completions inbound | `src/server/chat-completions.ts`, `src/chat/`, `src/adapters/openai-chat.ts` | Inbound translation onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. Native passthrough consults the existing model capability ladder before forwarding `reasoning_effort`. | | Hosted search relay | `src/server/search.ts` | Direct relay; distinct from the web-search sidecar loop below. | | Image/video generation loop | `src/images/loop.ts`, `src/images/plan.ts`, `src/images/fulfill.ts`, `src/images/xai-client.ts`, `src/images/xai-video-client.ts`, `src/images/artifacts.ts` | A provider-returned image URL is downloaded into a local artifact once, then served locally; warnings stay URL-free because provider CDN URLs may embed credentials. | | GitHub Copilot | `src/providers/xai-transport.ts` (`resolveProviderTransport`), `src/providers/github-copilot-transport.ts` | `resolveProviderTransport` selects the Copilot transport when the routed provider name is `github-copilot`; the Copilot module then resolves its headers and base URL, and the registry seeds the provider row and model fallback. | diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 6e975af2ac..62eb0eff07 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -489,6 +489,15 @@ combo whose remaining eligible targets use other providers. > Decision record: [ADR-0070](../decisions/ADR-0070-same-provider-combo-quota-fallback.md) +## Combo per-target reasoning controls + +`src/server/responses/core.ts` passes the combo's `reasoningEffortMode` and the final target's +`supportedLadderFor` result to `src/combos/request.ts` before adapter parsing. Explicit empty +capability ladders remove effort and thinking controls in every combo mode; adaptive mode also +removes those controls for unknown ladders and preserves `reasoning.summary`. Known non-empty +ladders retain the existing per-target effort resolution. This request normalization does not +change target order, attempt accounting, or the existing provider-400 failover classification. + ## Combo streaming commit boundary An HTTP 200 does not by itself commit a streaming combo child. The combo parent runs the child's diff --git a/tests/adapters/openai/openai-chat-hardening.test.ts b/tests/adapters/openai/openai-chat-hardening.test.ts index b051817469..3a8fe98f03 100644 --- a/tests/adapters/openai/openai-chat-hardening.test.ts +++ b/tests/adapters/openai/openai-chat-hardening.test.ts @@ -140,6 +140,23 @@ describe("AgentRouter openai-chat compatibility", () => { expect(body.messages[0]?.content.map(part => part.text)).toEqual([preamble, "responda somente: OK"]); expect(rawBody.messages[0]?.content).toBe("responda somente: OK"); }); + + test("passthrough chat drops reasoning_effort for an explicitly empty capability ladder", () => { + const rawBody = { + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "xhigh", + }; + const request = buildOpenAIChatPassthroughRequest( + provider({ reasoningEfforts: [] }), + rawBody, + "test-model", + false, + ); + const body = JSON.parse(request.body as string) as Record; + + expect(body).not.toHaveProperty("reasoning_effort"); + expect(rawBody.reasoning_effort).toBe("xhigh"); + }); }); function parsed(): OcxParsedRequest { @@ -1455,3 +1472,25 @@ test("tool-call deltas emit heartbeats so a long buffering phase is not read as expect(visible.at(-1)).toMatchObject({ type: "done" }); }); }); + + +describe("native Chat raw reasoning declarations", () => { + const raw = { messages: [{ role: "user", content: "hello" }], reasoning_effort: "enabled" }; + function wire(overrides: Partial) { + const request = buildOpenAIChatPassthroughRequest({ + adapter: "openai-chat", baseUrl: "https://example.test/v1", ...overrides, + }, raw, "target", false); + return JSON.parse(request.body as string) as Record; + } + test("preserves a nonempty wire-only ladder as unknown", () => { + expect(wire({ reasoningEfforts: ["enabled"] }).reasoning_effort).toBe("enabled"); + expect(wire({ reasoningEfforts: [], modelReasoningEfforts: { target: ["enabled"] } }).reasoning_effort).toBe("enabled"); + }); + test("honors explicit empty model overrides over a provider ladder", () => { + expect(wire({ reasoningEfforts: ["high"], modelReasoningEfforts: { target: [] } }).reasoning_effort).toBeUndefined(); + }); + test("honors noReasoningModels over a nonempty model ladder without mutating input", () => { + expect(wire({ noReasoningModels: ["target"], modelReasoningEfforts: { target: ["high"] } }).reasoning_effort).toBeUndefined(); + expect(raw.reasoning_effort).toBe("enabled"); + }); +}); diff --git a/tests/codex-integration/combos.test.ts b/tests/codex-integration/combos.test.ts index 76e4f26ca9..0bc1230e0f 100644 --- a/tests/codex-integration/combos.test.ts +++ b/tests/codex-integration/combos.test.ts @@ -297,22 +297,93 @@ describe("combo request cloning", () => { expect(concrete.input).not.toBe(raw.input); }); - test("combo default respects client-owned ignored reasoning values", () => { + test("combo target capability strips unsupported client reasoning controls", () => { expect(concreteComboRequestBody({ model: "combo/x", reasoning: null }, target, "high", []).reasoning).toBeNull(); expect(concreteComboRequestBody( { model: "combo/x", reasoning: { effort: "" } }, target, "high", [], - ).reasoning).toEqual({ effort: "" }); + ).reasoning).toBeUndefined(); expect(concreteComboRequestBody( { model: "combo/x", reasoning: { effort: "banana" } }, target, "high", [], - ).reasoning).toEqual({ effort: "banana" }); + ).reasoning).toBeUndefined(); expect(concreteComboRequestBody( { model: "combo/x", reasoning: { effort: null } }, target, "high", [], - ).reasoning).toEqual({ effort: null }); + ).reasoning).toBeUndefined(); expect(concreteComboRequestBody( { model: "combo/x", reasoning: { summary: "concise" } }, target, "high", ["high"], ).reasoning).toEqual({ summary: "concise", effort: "high" }); }); + test("adaptive normalization strips unsupported controls for an unknown target while preserving summary", () => { + const raw = { + model: "combo/x", + input: "hi", + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }; + const concrete = concreteComboRequestBody(raw, target, null, undefined, "adaptive"); + + expect(concrete).toEqual({ + model: "a/m1", + input: "hi", + reasoning: { summary: "concise" }, + }); + expect(raw).toEqual({ + model: "combo/x", + input: "hi", + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }); + }); + + test("strict normalization preserves reasoning controls for an unknown target", () => { + const raw = { + model: "combo/x", + input: "hi", + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }; + const concrete = concreteComboRequestBody(raw, target, null, undefined, "strict"); + + expect(concrete).toEqual({ + model: "a/m1", + input: "hi", + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }); + }); + + test("explicit empty ladder strips unsupported controls while preserving reasoning summary", () => { + const concrete = concreteComboRequestBody({ + model: "combo/x", + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }, target, "high", []); + + expect(concrete).toEqual({ + model: "a/m1", + reasoning: { summary: "concise" }, + }); + }); + + test("adaptive normalization preserves xhigh for a known reasoning ladder", () => { + const concrete = concreteComboRequestBody({ + model: "combo/x", + reasoning: { effort: "xhigh", summary: "concise" }, + }, target, null, ["low", "medium", "high", "xhigh"], "adaptive"); + + expect(concrete.reasoning).toEqual({ effort: "xhigh", summary: "concise" }); + }); + test("omits combo defaults for unset, no-reasoning, and unknown target capabilities", () => { expect(concreteComboRequestBody({ model: "combo/x" }, target, null, ["high"]).reasoning).toBeUndefined(); // An explicitly empty ladder is how a no-reasoning model is expressed. diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index 6f649c52d3..76b90720ba 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -1182,6 +1182,60 @@ describe("server combo failover 030 activation matrix", () => { expectMappedReceipt(hydrated[0]!); }); + test("adaptive combo normalizes unknown and empty target capability before the upstream wire", async () => { + const bodies: Array> = []; + const upstream = serve(async request => { + bodies.push(await request.json() as Record); + return chatSuccess("normalized", "m1"); + }); + const request = { + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", + thinking_budget: 8192, + thinking: { type: "enabled" }, + }; + + const unknownResponse = await post( + comboConfig( + { a: provider("openai-chat", baseUrl(upstream), "key-a") }, + undefined, + { reasoningEffortMode: "adaptive" }, + ), + request, + ); + expect(unknownResponse.status).toBe(200); + + const emptyResponse = await post( + comboConfig( + { a: provider("openai-chat", baseUrl(upstream), "key-a", { reasoningEfforts: [] }) }, + undefined, + { reasoningEffortMode: "adaptive" }, + ), + request, + ); + expect(emptyResponse.status).toBe(200); + + const knownResponse = await post( + comboConfig( + { a: provider("openai-chat", baseUrl(upstream), "key-a", { + reasoningEfforts: ["low", "medium", "high", "xhigh"], + }) }, + undefined, + { reasoningEffortMode: "adaptive" }, + ), + request, + ); + expect(knownResponse.status).toBe(200); + + expect(bodies).toHaveLength(3); + for (const body of bodies.slice(0, 2)) { + expect(body).not.toHaveProperty("reasoning_effort"); + expect(body).not.toHaveProperty("thinking_budget"); + expect(body).not.toHaveProperty("thinking"); + } + expect(bodies[2]!.reasoning_effort).toBe("xhigh"); + }); + test("all-target exhaustion promotes the final attempt reasoning wire to the logical row", async () => { const a = serve(() => Response.json({ error: { message: "first overloaded" } }, { status: 503 })); const b = serve(() => Response.json({ error: { message: "last overloaded" } }, { status: 503 })); From 07b47dd2c92c93b81c1359fda33d97fb083ff6b4 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:02:59 +0900 Subject: [PATCH 18/21] docs: synchronize all combo reasoning source owners --- structure/adapters/registry.md | 2 ++ structure/clients/claude-desktop.md | 2 ++ structure/data-planes/images.md | 2 ++ structure/gui-and-management-api.md | 2 ++ structure/ops/docs-and-release.md | 2 ++ structure/ops/service-and-sidecars.md | 2 ++ structure/providers/cursor.md | 2 ++ structure/providers/xai-grok.md | 2 ++ structure/runtime.md | 1 - structure/subagents.md | 2 ++ structure/transports/streaming-health.md | 2 ++ 11 files changed, 20 insertions(+), 1 deletion(-) diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index db26a7cb4d..735514ccc5 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -71,3 +71,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 54689b36a1..cfa1bba58f 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -95,3 +95,5 @@ Config JSON preserves the boolean; only literal true activates the role-changing The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index d1d0193048..1d74b0a354 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -80,3 +80,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 82d0ef5d20..af16f1710b 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -544,3 +544,5 @@ integration IO adapter. Its snapshot fingerprint cannot be checked against provi The existing dashboard file-client maps include Cline CLI and reuse its committed color mark. The export panel labels its download as a settings/catalog bundle; all locales explain that Undo restores both original files. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 8e31434039..29f9154fcd 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -318,3 +318,5 @@ The integrations guide documents Cline CLI as a two-file, loopback-only integrat The lightweight top-level CLI help counts Cline CLI among the fifteen registered export clients; registry parity remains covered by the client help and integration tests. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index c8ef069110..fd5cfb556b 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -143,3 +143,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index be42793e0b..1231d280c8 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -82,3 +82,5 @@ constraints cannot widen the canonical shape. Bare shell bridge names are reject on the freeform path. Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in `tests/providers/cursor/cursor-tool-definitions.test.ts`. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 5f4217b093..fe7a8b504e 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -69,3 +69,5 @@ privately to final dispatch; preliminary route selection does not inject Go-only Devin CLI credential path composition in `src/oauth/devin-cli.ts` follows the selected platform: Windows uses Win32 APPDATA paths, other platforms use POSIX XDG-data paths. The explicit absolute override remains verbatim; credential parsing and login behavior are unchanged. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](../transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/runtime.md b/structure/runtime.md index 669016a8a7..4450637be3 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -146,7 +146,6 @@ The server exposes `POST /api/stop` which restores native Codex config, stops an | `src/providers/registry.ts` | Canonical provider presets for CLI, dashboard, OAuth, key providers, and metadata. | | `src/providers/derive.ts` | Enrichment from provider presets into user config. | | `src/oauth/` | OAuth providers, token storage, refresh, and auth-token resolution. The login callback listener binds a per-provider FIXED loopback port, so consecutive logins reuse the same number; every response it sends ends its connection (`Connection: close`, including non-callback paths such as a stray `/favicon.ico` 404). Stopping the listener does not close an established socket, so without that a pooled client would deliver the next login's callback to the retired flow, which rejects the unknown state as a CSRF mismatch while the live flow waits. | -| `src/oauth/` | OAuth providers, token storage, refresh, and auth-token resolution. | | `src/combos/request.ts` | Clones each selected combo target request and applies the existing target capability ladder: adaptive unknown targets and explicit empty ladders receive no unsupported reasoning/thinking controls, while known ladders retain per-target resolution. | | `src/adapters/openai-responses.ts` | Native OpenAI/ChatGPT Responses passthrough. | | `src/adapters/openai-chat.ts` | OpenAI-compatible Chat Completions bridge. | diff --git a/structure/subagents.md b/structure/subagents.md index a80af4d990..59ebc68348 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -215,3 +215,5 @@ see [Combo editor routing quota](gui-and-management-api.md#combo-editor-routing- Claude replay carries [Go conversation affinity](data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](transports/responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 6a5574c6d2..a7a6ffccae 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -200,3 +200,5 @@ see [Combo editor routing quota](../gui-and-management-api.md#combo-editor-routi Claude replay carries [Go conversation affinity](../data-planes/inbound-compat.md#claude-affinity-at-final-go-dispatch) privately to final dispatch; preliminary route selection does not inject Go-only headers. Native Chat applies qualifying effort ceilings independently of model pins; pin selection precedes the cap and only pins or cap rewrites enter wire mapping. The [catalog effort contract](../catalog.md#ultra-reasoning-level) records the V1/compaction exemptions and caller-preservation boundary. + +Combo child requests normalize effort and thinking controls against the selected target while retaining reasoning summaries; strict unknown targets preserve caller controls. The [Responses transport owner](responses.md) documents this boundary, and native Chat removes effort only for an explicit empty declaration or no-reasoning model. From 926d4123e73af037154ca7df71d335806a9c336e Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 14:28:35 +0900 Subject: [PATCH 19/21] test(combos): verify summary preservation on the Responses wire Document reasoningEffortMode in all seven translated guides. Keep the original Chat-wire regression; Responses summary is not a Chat request field. --- .../src/content/docs/fr/guides/combos.md | 7 +++++ .../src/content/docs/ja/guides/combos.md | 7 +++++ .../src/content/docs/ko/guides/combos.md | 7 +++++ .../src/content/docs/ru/guides/combos.md | 7 +++++ .../src/content/docs/tr/guides/combos.md | 7 +++++ .../src/content/docs/zh-cn/guides/combos.md | 7 +++++ .../src/content/docs/zh-tw/guides/combos.md | 7 +++++ .../server/server-combo-failover-e2e.test.ts | 28 +++++++++++++++++++ 8 files changed, 77 insertions(+) diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md index 9073372327..5120925d6d 100644 --- a/docs-site/src/content/docs/fr/guides/combos.md +++ b/docs-site/src/content/docs/fr/guides/combos.md @@ -233,6 +233,12 @@ par défaut et laisse le comportement de la cible inchangé. Les valeurs prises `high`, `xhigh`, `max` et `ultra` ; omettez le champ ou réglez-le sur `null` pour laisser l'effort entièrement à l'appelant et la cible. +## Capacités reasoning mixtes + +`reasoningEffortMode` vaut `"strict"` par défaut : le catalogue publie l’intersection des listes effort de toutes les cibles, y compris les listes explicitement vides. `"adaptive"` exclut ces listes vides pour conserver le sélecteur dans un combo mixte. Une liste inconnue ne limite l’intersection dans aucun des deux modes. + +À l’envoi, une liste explicitement vide supprime les paramètres effort et thinking dans les deux modes ; une liste inconnue les supprime uniquement en adaptive. `reasoning.summary` et les autres champs hors effort sont conservés. La résolution des cibles connues non vides reste inchangée. Les cibles inconnues en strict et les déclarations inconnues du native Chat ordinaire conservent les paramètres de l’appelant. L’ajout d’une valeur par défaut ne remplace pas un effort existant, mais cette normalisation peut supprimer les paramètres non pris en charge. + ## Capacité d’entrée d’images / multimodale Par défaut, une combinaison publie l’**intersection** des modalités d’entrée de ses cibles : les images ne @@ -337,6 +343,7 @@ Les combos sont stockés dans l'objet `combos` de niveau supérieur, saisi par l | `strategy` | Non | `"failover"` | Valeurs autorisées : `"failover"`, `"round-robin"`, `"random"`, `"least-used"` et `"reset-window"`. | | `stickyLimit` | Non | `1` | Nombre entier de 1 à 100 requêtes réussies par sélection à tour de rôle. S’applique uniquement à `round-robin`. | | `defaultEffort` | Non | `null` | `low`, `medium`, `high`, `xhigh`, `max` ou `ultra` ; appliqué uniquement lorsque l'appelant omet ses efforts et que la cible annonce son soutien. | +| `reasoningEffortMode` | Non | `"strict"` | `strict` ou `adaptive` ; choisit l’intersection des capacités et la normalisation par cible. | | `imageInput` | Non | `"auto"` | `"auto"` ou `"disabled"`. `"auto"` publie les images uniquement si toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias` | Non | aucun | Identifiant de modèle public tronqué facultatif ; utilisez les règles d'alias ci-dessus. Une valeur vide est stockée sans alias. | | `nativeAlias` | Non | `false` | Autoriser explicitement un `alias` natif nu actuellement pris en charge à avoir la priorité sur le routage et le catalogue. Jamais déduit de l'alias. | diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md index 70c902ec6e..efb2fa6021 100644 --- a/docs-site/src/content/docs/ja/guides/combos.md +++ b/docs-site/src/content/docs/ja/guides/combos.md @@ -149,6 +149,12 @@ ocx combo set balanced \ ターゲットの機能が不明な場合、または設定されたエフォートが含まれていない場合、opencodex はデフォルトを省略し、ターゲット自体の動作を変更しないままにします。サポートされている値は、`low`、`medium`、`high`、`xhigh`、`max`、および `ultra` です。このフィールドを省略するか、`null` に設定して、呼び出し元とターゲットに作業を完全に任せます。 +## 異なる reasoning capability の組み合わせ + +`reasoningEffortMode` の既定値は `"strict"` です。明示的な空リストを含む全対象の effort リストの共通部分を公開します。`"adaptive"` は空リストを共通部分の計算から除外し、混在するコンボでも選択肢を維持します。不明なリストは、どちらのモードでもカタログの共通部分を制限しません。 + +送信時には、明示的な空リストの対象で effort と thinking の制御を両モードとも削除します。不明な対象で削除するのは adaptive のみです。`reasoning.summary` と effort 以外のフィールドは保持し、既知の空でない対象は従来どおり effort を解決します。strict の不明な対象と通常の native Chat の不明な宣言は保持します。既定値の補完は既存の effort を上書きしませんが、この正規化は非対応の制御を削除できます。 + ## 暗号化された v2 サブエージェント タスク Codex v2 サブエージェントには重要な制限が 1 つあります ([第92号](https://github.com/lidge-jun/opencodex/issues/92))。ネイティブの親は、新しく生成されたワーカーのタスクを、ネイティブ ChatGPT バックエンド用に作成された暗号文としてのみ送信できます。外部プロバイダーはそのペイロードを読み取ることができません。 @@ -237,6 +243,7 @@ ocx combo remove --yes | `cooldownMs` |いいえ | 未設定 → アップストリーム フォールバック(リクエストレート 429 コード `1302`/`1305` では 5 秒、それ以外では 60 秒) | 1 ~ 600000 の整数。設定時は、使用可能なアップストリーム `Retry-After` または Codex リセットシグナルがない場合に、リクエストレート 429 を含むターゲットごとのクールダウンとして適用されます。未設定時はアップストリーム フォールバックを使用します。 | | `waitForCooldownMs` |いいえ | `0` | 0 ~ 600000 の整数。最も早く利用可能になる冷却中のターゲットを待ってから `combo_unavailable` を返すまでの最大待機時間。中止すると待機はキャンセルされます。 | | `defaultEffort` |いいえ | `null` | `low`、`medium`、`high`、`xhigh`、`max`、または `ultra`;呼び出し元が努力を省略し、ターゲットがサポートをアドバタイズした場合にのみ適用されます。 | +| `reasoningEffortMode` | いいえ | `"strict"` | `strict` または `adaptive`。混在する capability の共通部分と対象別の制御正規化を選択します。 | | `alias` |いいえ |なし |オプションのトリミングされたパブリック モデル ID。上記のエイリアス ルールを使用します。空の値はエイリアスなしで保存されます。 | | `nativeAlias` |いいえ | `false` | 現在サポートされている bare native alias に routing/catalog の優先権を明示的に与えます。 | | `displayName` |いいえ |なし | catalog 表示専用ラベル。`nativeAlias` が true の場合は必須です。 | diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index 80eac32c2d..b7f9165ebb 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -155,6 +155,12 @@ ocx combo set balanced \ 대상 기능을 알 수 없거나 설정한 effort를 포함하지 않으면 opencodex는 기본값을 생략하고 대상의 동작은 그대로 둡니다. 지원 값은 `low`, `medium`, `high`, `xhigh`, `max`, `ultra`입니다. effort를 호출자와 대상에 완전히 맡기려면 이 필드를 생략하거나 `null`로 설정하십시오. +## 서로 다른 reasoning capability + +`reasoningEffortMode`의 기본값은 `"strict"`입니다. 모든 대상의 effort 목록을 교집합으로 계산하므로 명시적 빈 목록도 반영합니다. `"adaptive"`는 빈 목록을 교집합에서 제외해 혼합 콤보에서도 선택기를 유지합니다. 알 수 없는 목록은 두 모드 모두 카탈로그 교집합을 제한하지 않습니다. + +전송 시 명시적 빈 목록은 두 모드 모두에서 effort·thinking 제어를 제거하고, 알 수 없는 목록은 adaptive에서만 제거합니다. `reasoning.summary`와 다른 비-effort 필드는 보존하며, 알려진 비어 있지 않은 대상은 기존 방식으로 effort를 결정합니다. strict의 unknown 대상과 일반 native Chat의 unknown 선언은 그대로 유지됩니다. 기본값 주입은 기존 effort를 덮어쓰지 않지만, 이 capability 정규화는 지원되지 않는 제어를 제거할 수 있습니다. + ## 암호화된 v2 서브에이전트 작업 Codex v2 서브에이전트에는 중요한 제한이 하나 있습니다([issue #92](https://github.com/lidge-jun/opencodex/issues/92)). 네이티브 부모 프로세스는 새로 생성된 작업자에게 보낼 작업을 네이티브 ChatGPT 백엔드용으로 생성한 암호문으로만 전달할 수 있습니다. 외부 공급자는 그 페이로드를 읽을 수 없습니다. @@ -241,6 +247,7 @@ ocx combo remove --yes | `cooldownMs` | 아니요 | 미설정 → 업스트림 폴백(요청 속도 제한 429 코드 `1302`/`1305`는 5초, 그 외는 60초) | 1에서 600000 사이의 정수입니다. 설정하면 사용 가능한 업스트림 `Retry-After` 또는 Codex 재설정 신호가 없을 때 요청 속도 제한 429를 포함한 대상별 쿨다운으로 적용됩니다. 설정하지 않으면 업스트림 폴백을 사용합니다. | | `waitForCooldownMs` | 아니요 | `0` | 0에서 600000 사이의 정수입니다. `combo_unavailable`을 반환하기 전에 가장 먼저 적합해지는 쿨다운 중인 대상을 기다리는 최대 시간입니다. 중단하면 대기가 취소됩니다. | | `defaultEffort` | 아니요 | `null` | `low`, `medium`, `high`, `xhigh`, `max`, 또는 `ultra`입니다. 호출자가 effort를 생략하고 대상이 지원을 광고할 때만 적용됩니다. | +| `reasoningEffortMode` | 아니요 | `"strict"` | `strict` 또는 `adaptive`; 혼합 capability의 교집합과 대상별 제어 정규화를 선택합니다. | | `alias` | 아니요 | 없음 | 선택적으로 앞뒤 공백을 제거한 공개 모델 ID입니다. 위의 alias 규칙을 따릅니다. 빈 값은 alias 없음으로 저장됩니다. | | `nativeAlias` | 아니요 | `false` | 현재 지원되는 bare native alias가 routing/catalog 우선권을 갖도록 명시적으로 허용합니다. | | `displayName` | 아니요 | 없음 | catalog 표시 전용 label입니다. `nativeAlias`가 true이면 필수입니다. | diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index b873f4ed03..d54c03e33e 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -192,6 +192,12 @@ Failover намеренно ограничен. Он помогает при п `high`, `xhigh`, `max` и `ultra`; опустите поле или задайте `null`, чтобы полностью оставить выбор effort вызывающей стороне и цели. +## Разные возможности reasoning в одном combo + +`reasoningEffortMode` по умолчанию равен `"strict"`: публикуется пересечение списков effort всех целей, включая явно пустые списки. `"adaptive"` исключает пустые списки из пересечения, сохраняя выбор effort для смешанного combo. Неизвестные списки не ограничивают пересечение каталога в обоих режимах. + +При отправке явно пустой список удаляет параметры effort и thinking в обоих режимах; неизвестный список — только в adaptive. `reasoning.summary` и остальные поля, не задающие effort, сохраняются. Для известных непустых списков разрешение effort не меняется. Неизвестные цели в strict и неизвестные объявления обычного native Chat сохраняют параметры вызывающей стороны. Подстановка значения по умолчанию не заменяет существующий effort, но нормализация возможностей может удалить неподдерживаемые параметры. + ## Шифрованные задачи подагентов v2 Есть одно важное ограничение для подагентов Codex v2 @@ -292,6 +298,7 @@ Combo хранятся в объекте верхнего уровня `combos`, | `cooldownMs` | No | не задано → fallback upstream (5 с для rate-limit 429 с кодами `1302`/`1305`, иначе 60 с) | Целое число от 1 до 600000. Если задано, применяется как cooldown каждой цели, когда нет пригодного upstream `Retry-After` или сигнала сброса Codex, включая rate-limit 429; если не задано, используется fallback upstream. | | `waitForCooldownMs` | No | `0` | Целое число от 0 до 600000. Максимальное время ожидания самой ранней подходящей цели в cooldown перед возвратом `combo_unavailable`; отмена запроса отменяет ожидание. | | `defaultEffort` | No | `null` | `low`, `medium`, `high`, `xhigh`, `max` или `ultra`; применяется только когда вызывающая сторона не указала effort, а цель объявляет поддержку. | +| `reasoningEffortMode` | Нет | `"strict"` | `strict` или `adaptive`; задаёт пересечение возможностей и нормализацию параметров конкретной цели. | | `alias` | No | none | Необязательный обрезанный публичный id модели; используйте правила alias выше. Пустое значение хранится как отсутствие alias. | | `nativeAlias` | No | `false` | Явно разрешает поддерживаемому сейчас bare native alias перехватить приоритет routing/catalog только для неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct; маршруты с квалификатором аккаунта сохраняют свою идентичность, а provider-qualified `openai-apikey/gpt-5.6-*` использует API-ключ и никогда не переходит на native alias. | | `displayName` | No | none | Метка только для отображения в catalog; обязательна при `nativeAlias: true`. | diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md index b8cd5bad0d..ddb877cbcb 100644 --- a/docs-site/src/content/docs/tr/guides/combos.md +++ b/docs-site/src/content/docs/tr/guides/combos.md @@ -266,6 +266,12 @@ Desteklenen değerler `low`, `medium`, `high`, `xhigh`, `max` ve `ultra`'dır; çabayı tamamen arayana ve hedefe bırakmak için alanı atlayın veya `null` olarak ayarlayın. +## Farklı reasoning yetenekleri + +`reasoningEffortMode` varsayılan olarak `"strict"` kullanır: açıkça boş listeler dahil tüm hedeflerin effort listelerinin kesişimi yayımlanır. `"adaptive"`, karma kombolarda seçiciyi korumak için boş listeleri kesişimden çıkarır. Bilinmeyen listeler her iki modda da katalog kesişimini sınırlamaz. + +Gönderim sırasında açıkça boş liste her iki modda effort ve thinking denetimlerini kaldırır; bilinmeyen liste bunları yalnızca adaptive modunda kaldırır. `reasoning.summary` ve effort dışındaki alanlar korunur. Bilinen, boş olmayan hedeflerin effort çözümü değişmez. strict modundaki bilinmeyen hedefler ve normal native Chat bilinmeyen bildirimleri çağıranın denetimlerini korur. Varsayılan değer ekleme mevcut effort değerini değiştirmez; yetenek normalizasyonu desteklenmeyen denetimleri kaldırabilir. + ## Şifrelenmiş v2 alt ajan görevleri Codex v2 alt ajanları için önemli bir sınırlama vardır ([sorun @@ -373,6 +379,7 @@ saklanır: | `strategy` | Hayır | `"failover"` | İzin verilen değerler: `"failover"`, `"round-robin"`, `"random"`, `"least-used"`, `"reset-window"`. | | `stickyLimit` | Hayır | `1` | Yalnızca `round-robin` için geçerlidir; seçim başına 1 ile 100 arasında başarılı istek tam sayısı. | | `defaultEffort` | Hayır | `null` | `low`, `medium`, `high`, `xhigh`, `max` veya `ultra`; yalnızca arayan çabayı atladığında ve hedef desteği bildirdiğinde uygulanır. | +| `reasoningEffortMode` | Hayır | `"strict"` | `strict` veya `adaptive`; karma yetenek kesişimini ve hedefe özel normalizasyonu seçer. | | `alias` | Hayır | yok | İsteğe bağlı kırpılmış genel model kimliği; yukarıdaki takma ad kurallarını kullanın. Boş bir değer takma ad yok olarak saklanır. | | `nativeAlias` | Hayır | `false` | Şu anda desteklenen yalın bir yerel `alias`'ın yönlendirme ve katalog önceliği almasına açıkça izin verin. Asla takma addan çıkarılmaz. | | `displayName` | Hayır | yok | Sınırlı salt görüntüleme katalog etiketi. `nativeAlias` true olduğunda gerekli ve boş değildir. | diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md index abe32ae786..ffd670b57c 100644 --- a/docs-site/src/content/docs/zh-cn/guides/combos.md +++ b/docs-site/src/content/docs/zh-cn/guides/combos.md @@ -175,6 +175,12 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。 当目标能力未知,或者不包含配置的 effort 时,opencodex 会省略默认值,并保持目标自身行为不变。支持的值是 `low`、`medium`、`high`、`xhigh`、`max` 和 `ultra`;省略该字段或将其设为 `null`,就会把 effort 完全交给调用方和目标。 +## 混合 reasoning 能力 + +`reasoningEffortMode` 默认为 `"strict"`,发布所有目标 effort 列表的交集,包括显式空列表。`"adaptive"` 在计算交集时排除空列表,让混合 combo 保留选择器。未知列表在两种模式下都不限制目录交集。 + +发送时,显式空列表在两种模式下都会移除 effort 和 thinking 控制;未知列表仅在 adaptive 下移除这些控制。`reasoning.summary` 和其他非 effort 字段保持不变,已知非空目标继续按现有规则解析 effort。strict 的未知目标及普通 native Chat 的未知声明保留调用方控制。默认值填充不会覆盖现有 effort,但能力归一化可移除不支持的控制。 + ## 图片 / 多模态能力 默认情况下,combo 会发布其目标 **input modalities 的交集**(只有当每个目标都声明支持图片时,图片才会启用)。设置 `imageInput: "disabled"` 可在目标均支持图片时仍强制仅文本——目录会从 `inputModalities` 中去掉 `image`,带图请求会在分发前以 HTTP 400 拒绝。`"auto"`(或省略该字段)保持自动交集。 @@ -266,6 +272,7 @@ combo 会存储在顶层的 `combos` 对象中,并以 combo id 作为键: | `cooldownMs` | 否 | 未设置 → 上游回退值(请求速率限制代码为 `1302`/`1305` 的 429 为 5 秒,否则为 60 秒) | 1 到 600000 的整数。设置后,只要没有可用的上游 `Retry-After` 或 Codex 重置信号,就会作为每个目标的冷却时间应用,包括请求速率限制 429;未设置时使用上游回退值。 | | `waitForCooldownMs` | 否 | `0` | 0 到 600000 的整数。在返回 `combo_unavailable` 前等待最早恢复资格的冷却中目标的最长时间;请求中止会取消等待。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;仅当调用方省略 effort 且目标声明支持时才会应用。 | +| `reasoningEffortMode` | 否 | `"strict"` | `strict` 或 `adaptive`;选择混合能力交集和目标级控制归一化。 | | `imageInput` | 否 | `"auto"` | `"auto"` 或 `"disabled"`。`"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias` | 否 | 无 | 可选的、已修剪的公开模型 id;使用上面的别名规则。空值会以“无别名”形式存储。 | | `nativeAlias` | 否 | `false` | 显式允许当前受支持的裸原生 alias 接管路由和 catalog 优先级;绝不会根据 alias 自动推断。 | diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md index ce3ad70a94..10b30e4c28 100644 --- a/docs-site/src/content/docs/zh-tw/guides/combos.md +++ b/docs-site/src/content/docs/zh-tw/guides/combos.md @@ -187,6 +187,12 @@ Failover 是刻意受限的。它有助於目標特定的可用性、認證、 當目標能力未知或不包含設定的 effort 時,opencodex 省略預設值並保持目標自身行為不變。支援的值為 `low`、`medium`、`high`、`xhigh`、`max` 與 `ultra`;省略欄位或設為 `null` 可將 effort 完全交給呼叫者與目標。 +## 混合 reasoning 能力 + +`reasoningEffortMode` 預設為 `"strict"`,發布所有目標 effort 清單的交集,包括明確空清單。`"adaptive"` 計算交集時排除空清單,讓混合 combo 保留選擇器。未知清單在兩種模式下都不限制目錄交集。 + +傳送時,明確空清單在兩種模式下都會移除 effort 與 thinking 控制;未知清單只在 adaptive 移除這些控制。`reasoning.summary` 與其他非 effort 欄位保持不變,已知非空目標仍按現有規則解析 effort。strict 的未知目標及一般 native Chat 的未知宣告保留呼叫者控制。預設值補入不會覆寫現有 effort,但能力正規化可移除不支援的控制。 + ## 加密的 v2 子代理任務 Codex v2 子代理有一個重要限制([issue #92](https://github.com/lidge-jun/opencodex/issues/92))。原生父代只能將新生成 worker 的任務以為原生 ChatGPT 後端鑄造的密文發送。外部供應商無法讀取該 payload。 @@ -269,6 +275,7 @@ Combo 儲存於頂層 `combos` 物件中,以 combo id 為 key: | `strategy` | 否 | `"failover"` | 可用值為 `"failover"`、`"round-robin"`、`"random"`、`"least-used"`、`"reset-window"`。 | | `stickyLimit` | 否 | `1` | 僅適用於 `round-robin`:每次選擇的成功請求數,1 到 100 的整數。 | | `defaultEffort` | 否 | `null` | `low`、`medium`、`high`、`xhigh`、`max` 或 `ultra`;僅在呼叫者省略 effort 且目標宣告支援時套用。 | +| `reasoningEffortMode` | 否 | `"strict"` | `strict` 或 `adaptive`;選擇混合能力交集及目標層級控制正規化。 | | `alias` | 否 | 無 | 可選的修剪後公開模型 id;使用上述別名規則。空值儲存為無別名。 | ## 疑難排解 diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index 76b90720ba..4fe9381698 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -1236,6 +1236,34 @@ describe("server combo failover 030 activation matrix", () => { expect(bodies[2]!.reasoning_effort).toBe("xhigh"); }); + test("adaptive Responses combo preserves summary after removing unsupported controls", async () => { + const bodies: Array> = []; + const upstream = serve(async request => { + bodies.push(await request.json() as Record); + return Response.json(responsesSuccess("normalized", "m1")); + }); + for (const reasoningEfforts of [undefined, [], ["high", "xhigh"]]) { + const response = await post(comboConfig({ + a: provider("openai-responses", baseUrl(upstream), "key-a", { + ...(reasoningEfforts === undefined ? {} : { reasoningEfforts }), + modelSupportsReasoningSummaries: { m1: true }, + }), + }, undefined, { reasoningEffortMode: "adaptive" }), { + reasoning: { effort: "xhigh", summary: "concise" }, + reasoning_effort: "xhigh", thinking_budget: 8192, thinking: { type: "enabled" }, + }); + expect(response.status).toBe(200); + } + expect(bodies).toHaveLength(3); + for (const body of bodies.slice(0, 2)) { + expect(body.reasoning).toEqual({ summary: "concise" }); + expect(body).not.toHaveProperty("reasoning_effort"); + expect(body).not.toHaveProperty("thinking_budget"); + expect(body).not.toHaveProperty("thinking"); + } + expect(bodies[2]!.reasoning).toMatchObject({ effort: "xhigh", summary: "concise" }); + }); + test("all-target exhaustion promotes the final attempt reasoning wire to the logical row", async () => { const a = serve(() => Response.json({ error: { message: "first overloaded" } }, { status: 503 })); const b = serve(() => Response.json({ error: { message: "last overloaded" } }, { status: 503 })); From 37fb3812141fc2f9d8d93e4fa15d3aae65306ebd Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 21:17:15 +0900 Subject: [PATCH 20/21] docs(combos): document supported-rung default fallback across locales --- .../src/content/docs/fr/guides/combos.md | 14 ++----------- .../fr/reference/configuration/routing.md | 2 +- docs-site/src/content/docs/guides/combos.md | 14 ++----------- .../src/content/docs/ja/guides/combos.md | 9 ++------- .../ja/reference/configuration/routing.md | 2 +- .../src/content/docs/ko/guides/combos.md | 9 ++------- .../ko/reference/configuration/routing.md | 2 +- .../docs/reference/configuration/routing.md | 2 +- .../src/content/docs/ru/guides/combos.md | 14 ++----------- .../ru/reference/configuration/routing.md | 2 +- .../src/content/docs/tr/guides/combos.md | 20 ++++--------------- .../tr/reference/configuration/routing.md | 2 +- .../src/content/docs/zh-cn/guides/combos.md | 9 ++------- .../zh-cn/reference/configuration/routing.md | 2 +- .../src/content/docs/zh-tw/guides/combos.md | 9 ++------- .../zh-tw/reference/configuration/routing.md | 2 +- structure/transports/inventory.md | 2 +- 17 files changed, 27 insertions(+), 89 deletions(-) diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md index 5120925d6d..dc18f68971 100644 --- a/docs-site/src/content/docs/fr/guides/combos.md +++ b/docs-site/src/content/docs/fr/guides/combos.md @@ -218,20 +218,10 @@ Le basculement est intentionnellement limité. Il facilite la disponibilité, l' ## Effort de raisonnement par défaut -`defaultEffort` fournit `reasoning.effort` uniquement lorsque toutes ces conditions sont vraies : +`defaultEffort` complète un `reasoning.effort` absent si le combo possède une valeur par défaut non nulle et si la liste des niveaux acceptés par la cible est connue et non vide. La valeur configurée est conservée si elle est acceptée ; sinon, le niveau accepté le plus élevé ne la dépassant pas est choisi, ou le niveau le plus bas si aucun n’est inférieur. Une liste inconnue ou vide n’ajoute aucune valeur par défaut. -1. le combo a un défaut non nul ; -2. l'appelant n'a pas fait d'effort ; et -3. le catalogue de la cible sélectionnée annonce cet effort précis. +Cette étape conserve un effort existant et les autres champs reasoning. La normalisation des capacités ci-dessous peut supprimer séparément les paramètres effort/thinking non acceptés. Valeurs possibles : `low`, `medium`, `high`, `xhigh`, `max`, `ultra` ; l’absence du champ ou `null` désactive l’ajout. -Si la requête n'a pas d'objet `reasoning`, opencodex en crée un. Si `reasoning` existe sans -`effort`, il préserve les autres champs et ajoute la valeur par défaut. Un effort fourni par l’appelant n’est -jamais écrasé. - -Lorsque la capacité cible est inconnue ou n'inclut pas l'effort configuré, opencodex omet le -par défaut et laisse le comportement de la cible inchangé. Les valeurs prises en charge sont `low`, `medium`, -`high`, `xhigh`, `max` et `ultra` ; omettez le champ ou réglez-le sur `null` pour laisser l'effort entièrement à -l'appelant et la cible. ## Capacités reasoning mixtes diff --git a/docs-site/src/content/docs/fr/reference/configuration/routing.md b/docs-site/src/content/docs/fr/reference/configuration/routing.md index 110d2fcd66..d1a0107adc 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/fr/reference/configuration/routing.md @@ -58,7 +58,7 @@ Chaque clé de combinaison est un identifiant conforme à `[A-Za-z0-9][A-Za-z0-9 | `targets` | `{ provider: string; model: string; weight?: number }[]` | requis | Routes concrètes ordonnées. `weight` est compris entre 1 et 10000 et vaut `1` par défaut. | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Stratégie de sélection. L’ordre des cibles définit la priorité de `failover` ; les poids déterminent les sélections de `round-robin` et de `random` ; `least-used` suit les réussites enregistrées ; `reset-window` suit la réinitialisation de quota la plus proche. | | `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot de rotation. Plage de 1 à 100. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | non défini | Appliqué uniquement lorsque l’appelant ne précise aucun effort et que la cible sélectionnée annonce le niveau demandé. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | non défini | `defaultEffort` complète un `reasoning.effort` absent si le combo possède une valeur par défaut non nulle et si la liste des niveaux acceptés par la cible est connue et non vide. La valeur configurée est conservée si elle est acceptée ; sinon, le niveau accepté le plus élevé ne la dépassant pas est choisi, ou le niveau le plus bas si aucun n’est inférieur. Une liste inconnue ou vide n’ajoute aucune valeur par défaut. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publie les images uniquement lorsque toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias?` | `string` | — | Identifiant public facultatif du modèle, à la place du slug canonique du sélecteur. | | `nativeAlias?` | `boolean` | `false` | Permet à un identifiant natif non qualifié actuellement pris en charge de prendre la priorité uniquement pour cet identifiant. Les identifiants non qualifiés `gpt-5.6-*` utilisent les identifiants Codex Pool/Direct. Les routes qualifiées par un compte restent distinctes. Les routes qualifiées par un fournisseur, telles que `openai-apikey/gpt-5.6-*`, utilisent la route configurée avec sa clé d’API et ne passent jamais par l’alias natif. | diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index c497ec918f..979a84ffcd 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -256,20 +256,10 @@ instead of growing memory without a bound. ## Default reasoning effort -`defaultEffort` supplies `reasoning.effort` only when all of these are true: +`defaultEffort` fills an absent `reasoning.effort` when the combo has a non-null default and the selected target has a known, nonempty supported ladder. If the target supports the configured value, it is retained; otherwise the highest supported rung at or below it is used, or the lowest supported rung when none is lower. Unknown or empty ladders omit the default. -1. the combo has a non-null default; -2. the caller did not set an effort; and -3. the selected target's catalog advertises that exact effort. +The default-injection step preserves existing effort and other reasoning fields. Capability normalization can separately remove unsupported effort/thinking controls as described below. Supported defaults are `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`; omit the field or use `null` to disable default injection. -If the request has no `reasoning` object, opencodex creates one. If `reasoning` exists without an -`effort` property, it preserves the other fields and adds the default. A caller-provided effort is -never overwritten. - -When target capability is unknown or does not include the configured effort, opencodex omits the -default and leaves the target's own behavior unchanged. Supported values are `low`, `medium`, -`high`, `xhigh`, `max`, and `ultra`; omit the field or set it to `null` to leave effort entirely to -the caller and target. ### Mixed-capability groups (`reasoningEffortMode`) diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md index efb2fa6021..7b2c62a9ec 100644 --- a/docs-site/src/content/docs/ja/guides/combos.md +++ b/docs-site/src/content/docs/ja/guides/combos.md @@ -139,15 +139,10 @@ ocx combo set balanced \ ## デフォルトの推論負荷 -`defaultEffort` は、次のすべてが当てはまる場合にのみ `reasoning.effort` を提供します。 +`defaultEffort` は、コンボの既定値が null でなく、対象の対応リストが既知で空でない場合に、省略された `reasoning.effort` を補います。設定値に対応していればその値を使い、そうでなければ設定値以下で最も高い段階を選びます。それもなければ最も低い対応段階を使います。不明または空のリストでは既定値を省略します。 -1. コンボには null 以外のデフォルトがあります。 -2. 呼び出し側は努力を設定しませんでした。そして -3. 選択したターゲットのカタログは、その正確な取り組みを宣伝します。 +既定値の補完は既存の effort と他の reasoning フィールドを保持します。以下の capability 正規化は、別途、非対応の effort/thinking 制御を削除できます。設定可能な既定値は `low`、`medium`、`high`、`xhigh`、`max`、`ultra` です。省略または `null` で補完を無効にします。 -リクエストに `reasoning` オブジェクトがない場合、opencodex はオブジェクトを作成します。 `reasoning` が `effort` プロパティなしで存在する場合、他のフィールドは保持され、デフォルトが追加されます。呼び出し元が提供した努力は決し​​て上書きされません。 - -ターゲットの機能が不明な場合、または設定されたエフォートが含まれていない場合、opencodex はデフォルトを省略し、ターゲット自体の動作を変更しないままにします。サポートされている値は、`low`、`medium`、`high`、`xhigh`、`max`、および `ultra` です。このフィールドを省略するか、`null` に設定して、呼び出し元とターゲットに作業を完全に任せます。 ## 異なる reasoning capability の組み合わせ diff --git a/docs-site/src/content/docs/ja/reference/configuration/routing.md b/docs-site/src/content/docs/ja/reference/configuration/routing.md index 18b238c8bf..6cd6e81ab6 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ja/reference/configuration/routing.md @@ -69,7 +69,7 @@ picker catalog の convergence だけが保留中で routing change は失われ | `targets` | `{ provider: string; model: string; weight?: number }[]` |必須 |具体的なルートを指示しました。 `weight` は 1 ~ 10000 で、デフォルトは `1` です。 | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` |選択戦略。ターゲットの順序は `failover` の優先順位となり、`weight` は `round-robin` と `random` の抽選に影響し、`least-used` は記録された成功数に従い、`reset-window` は最も早いクォータリセットに従います。 | | `stickyLimit?` | `number` | `1` |成功したリクエストは 1 つのラウンドロビン バッチに保持されます。範囲は 1 ~ 100。 | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` |設定を解除する |呼び出し元が努力を省略し、選択されたターゲットが要求されたラングをアドバタイズする場合にのみ適用されます。 | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` |設定を解除する | `defaultEffort` は、コンボの既定値が null でなく、対象の対応リストが既知で空でない場合に、省略された `reasoning.effort` を補います。設定値に対応していればその値を使い、そうでなければ設定値以下で最も高い段階を選びます。それもなければ最も低い対応段階を使います。不明または空のリストでは既定値を省略します。 | | `alias?` | `string` | — |正規のピッカー スラグの代わりのオプションのパブリック モデル ID。 | | `nativeAlias?` | `boolean` | `false` | 現在サポートされている bare native id に限り、その未修飾 id で優先します。アカウント修飾およびプロバイダー修飾の OpenAI ルートは別のままです。 | | `displayName?` | `string` | — | catalog 表示専用ラベル。native alias では空でない値が必須です。 | diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index b7f9165ebb..e507889c6f 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -145,15 +145,10 @@ ocx combo set balanced \ ## 기본 reasoning effort -`defaultEffort`는 다음 조건이 모두 참일 때만 `reasoning.effort`를 채웁니다. +`defaultEffort`는 콤보 기본값이 null이 아니고, 선택한 대상의 지원 목록이 알려져 있으며 비어 있지 않을 때 생략된 `reasoning.effort`를 채웁니다. 설정값을 지원하면 그대로 사용합니다. 그렇지 않으면 설정값 이하의 가장 높은 지원 단계를 사용하고, 그런 단계가 없으면 가장 낮은 지원 단계를 사용합니다. 지원 목록이 없거나 비어 있으면 기본값을 생략합니다. -1. 콤보에 null이 아닌 기본값이 있습니다. -2. 호출자가 effort를 설정하지 않았습니다. -3. 선택된 대상의 카탈로그가 그 정확한 effort를 광고합니다. +기본값 주입은 기존 effort와 다른 reasoning 필드를 보존합니다. 아래의 capability 정규화는 별도로 지원되지 않는 effort·thinking 제어를 제거할 수 있습니다. 기본값은 `low`, `medium`, `high`, `xhigh`, `max`, `ultra`이며, 필드를 생략하거나 `null`로 설정하면 주입하지 않습니다. -요청에 `reasoning` 객체가 없으면 opencodex가 새로 만듭니다. `reasoning`은 있지만 `effort` 속성이 없으면 다른 필드는 그대로 두고 기본값만 추가합니다. 호출자가 준 effort는 절대 덮어쓰지 않습니다. - -대상 기능을 알 수 없거나 설정한 effort를 포함하지 않으면 opencodex는 기본값을 생략하고 대상의 동작은 그대로 둡니다. 지원 값은 `low`, `medium`, `high`, `xhigh`, `max`, `ultra`입니다. effort를 호출자와 대상에 완전히 맡기려면 이 필드를 생략하거나 `null`로 설정하십시오. ## 서로 다른 reasoning capability diff --git a/docs-site/src/content/docs/ko/reference/configuration/routing.md b/docs-site/src/content/docs/ko/reference/configuration/routing.md index cfc20d07fc..1a25960344 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ko/reference/configuration/routing.md @@ -68,7 +68,7 @@ Codex Auth 페이지에서 이 picker 동작을 opt-in할 수 있습니다. 비 | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 순서가 있는 concrete route입니다. `weight`는 1–10000이며 기본값은 `1`입니다. | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 선택 전략입니다. 대상 순서는 `failover` 우선순위이고, 가중치는 `round-robin`과 `random` 추첨 비율을 결정하며, `least-used`는 기록된 성공 횟수를 따르고, `reset-window`는 가장 가까운 할당량 재설정을 따릅니다. | | `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100입니다. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 호출자가 effort를 생략했고 선택된 대상이 요청한 rung를 광고할 때만 적용됩니다. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort`는 콤보 기본값이 null이 아니고, 선택한 대상의 지원 목록이 알려져 있으며 비어 있지 않을 때 생략된 `reasoning.effort`를 채웁니다. 설정값을 지원하면 그대로 사용합니다. 그렇지 않으면 설정값 이하의 가장 높은 지원 단계를 사용하고, 그런 단계가 없으면 가장 낮은 지원 단계를 사용합니다. 지원 목록이 없거나 비어 있으면 기본값을 생략합니다. | | `alias?` | `string` | — | 정규화된 picker slug 대신 쓰는 선택적 공개 model id입니다. | | `nativeAlias?` | `boolean` | `false` | 현재 지원되는 bare native id가 해당 비수식 id에만 우선하도록 합니다. 계정 또는 프로바이더로 수식된 OpenAI route는 별도로 유지됩니다. | | `displayName?` | `string` | — | catalog 표시 전용 label이며 native alias에서는 비어 있지 않아야 합니다. | diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 25bd32f64e..7cd038b649 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -90,7 +90,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. Applies only to round-robin. | | `cooldownMs?` | `number` | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Range 1–600000. When set, applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. Upstream signals take precedence and all cooldowns are capped at 10 minutes. | | `waitForCooldownMs?` | `number` | `0` | Maximum wait for the earliest eligible cooling target on each selection attempt before returning `combo_unavailable`. Range 0–600000; an abort cancels the wait. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Applied only when the caller omits effort and the selected target advertises the requested rung. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort` fills an absent `reasoning.effort` when the combo has a non-null default and the selected target has a known, nonempty supported ladder. If the target supports the configured value, it is retained; otherwise the highest supported rung at or below it is used, or the lowest supported rung when none is lower. Unknown or empty ladders omit the default. | | `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` intersects every known target effort ladder, so a target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. Picker metadata only; target selection and dispatch are unchanged. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index d54c03e33e..53a6b4b5f3 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -177,20 +177,10 @@ Failover намеренно ограничен. Он помогает при п ## Effort по умолчанию -`defaultEffort` подставляет `reasoning.effort` только если одновременно выполняются все условия: +`defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан ненулевой default и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. -1. у combo задано ненулевое значение по умолчанию; -2. вызывающая сторона сама не указала effort; и -3. каталог выбранной цели объявляет поддержку именно этого effort. +Подстановка сохраняет существующий effort и остальные поля reasoning. Нормализация возможностей ниже может отдельно удалить неподдерживаемые параметры effort/thinking. Значения default: `low`, `medium`, `high`, `xhigh`, `max`, `ultra`; отсутствие поля или `null` отключает подстановку. -Если в запросе нет объекта `reasoning`, opencodex создаёт его. Если `reasoning` есть, но в нём нет -свойства `effort`, остальные поля сохраняются, а значение по умолчанию добавляется. Effort, -заданный вызывающей стороной, никогда не перезаписывается. - -Если возможности цели неизвестны или не включают настроенный effort, opencodex опускает значение -по умолчанию и оставляет нативное поведение цели без изменений. Поддерживаются `low`, `medium`, -`high`, `xhigh`, `max` и `ultra`; опустите поле или задайте `null`, чтобы полностью оставить выбор -effort вызывающей стороне и цели. ## Разные возможности reasoning в одном combo diff --git a/docs-site/src/content/docs/ru/reference/configuration/routing.md b/docs-site/src/content/docs/ru/reference/configuration/routing.md index dbc7181044..029df5594e 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ru/reference/configuration/routing.md @@ -87,7 +87,7 @@ selector-qualified строки и возвращает обычные GPT-ст | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Упорядоченные конкретные маршруты. `weight` находится в диапазоне 1–10000 и по умолчанию равен `1`. | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Стратегия выбора. Порядок целей задаёт приоритет `failover`; значения `weight` определяют взвешивание выборов `round-robin` и `random`; `least-used` следует числу зарегистрированных успешных запросов; `reset-window` следует ближайшему сбросу квоты. | | `stickyLimit?` | `number` | `1` | Число успешных запросов, удерживаемых в одной партии round-robin. Диапазон 1–100. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | Применяется, только если вызывающая сторона не задала effort, а выбранная цель объявляет эту ступень. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан ненулевой default и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. | | `alias?` | `string` | — | Необязательный публичный id модели вместо канонического slug в селекторе. | | `nativeAlias?` | `boolean` | `false` | Даёт поддерживаемому bare native id приоритет только для этого неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct. Маршруты с квалификатором аккаунта остаются отдельными. Провайдер-квалифицированные маршруты, например `openai-apikey/gpt-5.6-*`, используют настроенный API-ключ и никогда не переходят на native alias. | | `displayName?` | `string` | — | Метка только для catalog; для native alias обязательна и не может быть пустой. | diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md index ddb877cbcb..4ac9e0febf 100644 --- a/docs-site/src/content/docs/tr/guides/combos.md +++ b/docs-site/src/content/docs/tr/guides/combos.md @@ -249,22 +249,10 @@ veya politika retlerini gizlemez. ## Varsayılan akıl yürütme çabası -`defaultEffort`, yalnızca bunların tümü doğru olduğunda `reasoning.effort` -sağlar: - -1. kombonun boş olmayan (non-null) bir varsayılanı vardır; -2. arayan bir çaba ayarlamamıştır; ve -3. seçilen hedefin kataloğu tam olarak bu çabayı bildirmektedir. - -İstekte bir `reasoning` nesnesi yoksa opencodex bir tane oluşturur. Bir `effort` -özelliği olmadan `reasoning` varsa diğer alanları korur ve varsayılanı ekler. -Arayan tarafından sağlanan bir çabanın üzerine asla yazılmaz. - -Hedef yeteneği bilinmediğinde veya yapılandırılan çabayı içermediğinde opencodex -varsayılanı atlar ve hedefin kendi davranışını değiştirmeden bırakır. -Desteklenen değerler `low`, `medium`, `high`, `xhigh`, `max` ve `ultra`'dır; -çabayı tamamen arayana ve hedefe bırakmak için alanı atlayın veya `null` olarak -ayarlayın. +`defaultEffort`, combo varsayılanı null değilse ve hedefin desteklenen seviye listesi bilinen ve boş olmayan bir listeyse eksik `reasoning.effort` değerini doldurur. Yapılandırılmış değer destekleniyorsa korunur; değilse bu değeri aşmayan en yüksek desteklenen seviye, böyle bir seviye yoksa en düşük desteklenen seviye kullanılır. Liste bilinmiyor veya boşsa varsayılan eklenmez. + +Varsayılan ekleme mevcut effort ve diğer reasoning alanlarını korur. Aşağıdaki yetenek normalizasyonu desteklenmeyen effort/thinking denetimlerini ayrıca kaldırabilir. Desteklenen varsayılanlar: `low`, `medium`, `high`, `xhigh`, `max`, `ultra`; alanı atlamak veya `null` kullanmak eklemeyi kapatır. + ## Farklı reasoning yetenekleri diff --git a/docs-site/src/content/docs/tr/reference/configuration/routing.md b/docs-site/src/content/docs/tr/reference/configuration/routing.md index 74d239ee14..b81f1464cd 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/tr/reference/configuration/routing.md @@ -110,7 +110,7 @@ aileleri kullanamaz. | `targets` | `{ provider: string; model: string; weight?: number }[]` | gerekli | Sıralı somut rotalar. `weight` 1–10000 arasındadır ve varsayılan olarak `1`'dir. | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Seçim stratejisi. Hedef sırası `failover` önceliğini belirler; `weight` değerleri `round-robin` ve `random` seçimlerini biçimlendirir; `least-used` kaydedilen başarılı istekleri izler; `reset-window` en yakın kota sıfırlamasını izler. | | `stickyLimit?` | `number` | `1` | Tek bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | ayarlanmamış | Yalnızca arayan çabayı atladığında ve seçilen hedef istenen basamağı bildirdiğinde uygulanır. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | ayarlanmamış | `defaultEffort`, combo varsayılanı null değilse ve hedefin desteklenen seviye listesi bilinen ve boş olmayan bir listeyse eksik `reasoning.effort` değerini doldurur. Yapılandırılmış değer destekleniyorsa korunur; değilse bu değeri aşmayan en yüksek desteklenen seviye, böyle bir seviye yoksa en düşük desteklenen seviye kullanılır. Liste bilinmiyor veya boşsa varsayılan eklenmez. | | `alias?` | `string` | — | Kurallı seçici slug'ı yerine isteğe bağlı genel model kimliği. | | `nativeAlias?` | `boolean` | `false` | Şu anda desteklenen bir yalın yerel kimliğin yalnızca o niteliksiz kimlik için öncelikli olmasına izin verin. Yalın `gpt-5.6-*` kimlikleri Codex Havuz/Direct kimlik bilgilerini kullanır. Hesap nitelikli rotalar ayrı kalır. `openai-apikey/gpt-5.6-*` gibi sağlayıcı nitelikli rotalar yapılandırılmış API anahtarı rotalarını kullanır ve asla yerel takma ada düşmez. | | `displayName?` | `string` | — | Yalnızca görüntüleme amaçlı katalog etiketi, yerel bir takma ad için gerekli ve boş olmamalıdır. | diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md index ffd670b57c..7c8c8efd63 100644 --- a/docs-site/src/content/docs/zh-cn/guides/combos.md +++ b/docs-site/src/content/docs/zh-cn/guides/combos.md @@ -165,15 +165,10 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。 ## 默认推理力度 -只有在以下所有条件都满足时,`defaultEffort` 才会提供 `reasoning.effort`: +当 combo 配置了非 null 默认值且目标支持列表已知且非空时,`defaultEffort` 会填充省略的 `reasoning.effort`。目标支持配置值时保留该值,否则选择不高于配置值的最高支持档位;若不存在更低档位,则使用最低支持档位。未知或空列表不会注入默认值。 -1. combo 有一个非空默认值; -2. 调用方没有设置 effort;并且 -3. 选中的目标目录明确声明了该精确的 effort。 +默认值注入保留已有 effort 和其他 reasoning 字段。下述能力归一化可单独移除不支持的 effort/thinking 控制。默认值支持 `low`、`medium`、`high`、`xhigh`、`max`、`ultra`;省略字段或设为 `null` 可关闭注入。 -如果请求没有 `reasoning` 对象,opencodex 会创建一个。如果 `reasoning` 存在但没有 `effort` 属性,它会保留其他字段并添加默认值。调用方提供的 effort 永远不会被覆盖。 - -当目标能力未知,或者不包含配置的 effort 时,opencodex 会省略默认值,并保持目标自身行为不变。支持的值是 `low`、`medium`、`high`、`xhigh`、`max` 和 `ultra`;省略该字段或将其设为 `null`,就会把 effort 完全交给调用方和目标。 ## 混合 reasoning 能力 diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md index 609c9ab48f..6232bc0306 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md @@ -73,7 +73,7 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏 | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | 有序的具体路由。`weight` 范围为 1–10000,默认值为 `1`。 | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 选择策略。目标顺序表示 `failover` 优先级;`weight` 决定 `round-robin` 和 `random` 的抽取权重;`least-used` 根据记录的成功次数选择;`reset-window` 跟随最近的额度重置。 | | `stickyLimit?` | `number` | `1` | 在单个轮询批次中保留的成功请求数。范围 1–100。 | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 仅在调用方省略 effort 且所选目标声明了请求的档位时应用。 | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 当 combo 配置了非 null 默认值且目标支持列表已知且非空时,`defaultEffort` 会填充省略的 `reasoning.effort`。目标支持配置值时保留该值,否则选择不高于配置值的最高支持档位;若不存在更低档位,则使用最低支持档位。未知或空列表不会注入默认值。 | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias?` | `string` | — | 可选的公开 model id,用于替代规范化的选择器 slug。 | | `nativeAlias?` | `boolean` | `false` | 仅让当前受支持的裸原生 id 对该不带限定前缀的 id 优先;带账号或提供方限定的 OpenAI 路由仍是独立路由。 | diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md index 10b30e4c28..be566c77cd 100644 --- a/docs-site/src/content/docs/zh-tw/guides/combos.md +++ b/docs-site/src/content/docs/zh-tw/guides/combos.md @@ -177,15 +177,10 @@ Failover 是刻意受限的。它有助於目標特定的可用性、認證、 ## 預設推理 effort -`defaultEffort` 僅在以下全為真時提供 `reasoning.effort`: +當 combo 設定非 null 預設值且目標支援清單已知且非空時,`defaultEffort` 會補入省略的 `reasoning.effort`。目標支援設定值時保留該值,否則選擇不高於設定值的最高支援層級;若沒有更低層級,則使用最低支援層級。未知或空清單不會注入預設值。 -1. combo 有非 null 預設值; -2. 呼叫者未設定 effort;且 -3. 所選目標的目錄宣告該精確 effort。 +預設值補入會保留既有 effort 與其他 reasoning 欄位。下述能力正規化可另外移除不支援的 effort/thinking 控制。預設值支援 `low`、`medium`、`high`、`xhigh`、`max`、`ultra`;省略欄位或設為 `null` 可關閉注入。 -若請求沒有 `reasoning` 物件,opencodex 建立一個。若 `reasoning` 存在但無 `effort` 屬性,它保留其他欄位並加入預設值。呼叫者提供的 effort 永不被覆寫。 - -當目標能力未知或不包含設定的 effort 時,opencodex 省略預設值並保持目標自身行為不變。支援的值為 `low`、`medium`、`high`、`xhigh`、`max` 與 `ultra`;省略欄位或設為 `null` 可將 effort 完全交給呼叫者與目標。 ## 混合 reasoning 能力 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md index 13a4e2622e..50ef13c16b 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md @@ -56,7 +56,7 @@ Codex Auth 頁面將此 picker 行為作為選擇加入功能暴露。停用它 | `targets` | `{ provider: string; model: string; weight?: number }[]` | 必填 | 有序的具體路由。`weight` 為 1–10000,預設 `1`。 | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 選擇策略。目標順序為 `failover` 優先序;`weight` 塑造 `round-robin` 與 `random` 抽選;`least-used` 依循已記錄的成功次數;`reset-window` 依循最早的配額重設。 | | `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100。 | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | 未設定 | 僅在呼叫者省略 effort 且所選目標廣告請求的階層時套用。 | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | 未設定 | 當 combo 設定非 null 預設值且目標支援清單已知且非空時,`defaultEffort` 會補入省略的 `reasoning.effort`。目標支援設定值時保留該值,否則選擇不高於設定值的最高支援層級;若沒有更低層級,則使用最低支援層級。未知或空清單不會注入預設值。 | | `alias?` | `string` | — | 可選的公開模型 id,取代標準 picker slug。 | | `nativeAlias?` | `boolean` | `false` | 讓目前支援的裸原生 id 僅對該未限定 id 取得優先。裸 `gpt-5.6-*` id 使用 Codex 池/Direct 憑證。帳號限定路由保持獨立。供應商限定路由(如 `openai-apikey/gpt-5.6-*`)使用其設定的 API-key 路由,且永不會落到原生別名。 | | `displayName?` | `string` | — | 僅顯示的目錄標籤,對原生別名為必填且非空。 | diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 60328ab2cb..e76cc4fb74 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -14,7 +14,7 @@ surface is listed here so a maintainer can find the owner without grepping: | Adapter execution support | `src/adapters/run-turn-queue.ts`, `src/adapters/tool-catalog-nudge.ts`, `src/adapters/identity.ts`, `src/adapters/image.ts`, `src/adapters/upstream-http-error.ts` | Shared machinery: turn ordering, tool-catalog nudging, client fingerprinting, image conversion, upstream error normalization. | | Cursor (beyond the sections above) | `src/adapters/cursor/live-transport.ts`, `src/adapters/cursor/http1-bidi.ts`, `src/adapters/cursor/live-models.ts`, `src/adapters/cursor/transport-retry.ts`, `src/adapters/cursor/mcp-manager.ts`, `src/adapters/cursor/thread-continuity.ts`, `src/adapters/cursor/checkpoint-store.ts` | Thread continuity is the point: a retry must not start a new Cursor thread, and a validated checkpoint must not rebuild the full root history. HTTP/2 remains the default; an explicit `http1.1`/`h1` pin maps the bidi run onto Cursor's `RunSSE` receive stream plus sequenced `BidiAppend` sends, and applies to live discovery too. | | Claude Messages | `src/server/claude-messages.ts` | Routed translation, a native Anthropic passthrough branch, and `count_tokens`. | -| Chat Completions inbound | `src/server/chat-completions.ts`, `src/chat/`, `src/adapters/openai-chat.ts` | Inbound translation onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. Native passthrough consults the existing model capability ladder before forwarding `reasoning_effort`. | +| Chat Completions inbound | `src/server/chat-completions.ts`, `src/server/chat-native.ts`, `src/chat/`, `src/adapters/openai-chat.ts` | Inbound translation onto the same routing pipeline. The content mapper preserves image URLs and supported detail, including screenshot-bearing tool results; target adapters own image placement on their wire. Image-free tool results stay strings. The native handler owns pin/cap normalization; the adapter wire builder removes effort only for explicit empty declarations or no-reasoning models, preserving unknown raw declarations. | | Hosted search relay | `src/server/search.ts` | Direct relay; distinct from the web-search sidecar loop below. | | Image/video generation loop | `src/images/loop.ts`, `src/images/plan.ts`, `src/images/fulfill.ts`, `src/images/xai-client.ts`, `src/images/xai-video-client.ts`, `src/images/artifacts.ts` | A provider-returned image URL is downloaded into a local artifact once, then served locally; warnings stay URL-free because provider CDN URLs may embed credentials. | | GitHub Copilot | `src/providers/xai-transport.ts` (`resolveProviderTransport`), `src/providers/github-copilot-transport.ts` | `resolveProviderTransport` selects the Copilot transport when the routed provider name is `github-copilot`; the Copilot module then resolves its headers and base URL, and the registry seeds the provider row and model fallback. | From 32121b4735871b4c8cfbc18c34176f5d3a7c6282 Mon Sep 17 00:00:00 2001 From: JUN Date: Sat, 12 Sep 2026 22:25:10 +0900 Subject: [PATCH 21/21] docs(combos): align localized routing mode and null semantics --- .../src/content/docs/fr/reference/configuration/routing.md | 1 + .../src/content/docs/ja/reference/configuration/routing.md | 1 + .../src/content/docs/ko/reference/configuration/routing.md | 1 + docs-site/src/content/docs/reference/configuration/routing.md | 2 +- docs-site/src/content/docs/ru/guides/combos.md | 2 +- .../src/content/docs/ru/reference/configuration/routing.md | 3 ++- .../src/content/docs/tr/reference/configuration/routing.md | 1 + .../src/content/docs/zh-cn/reference/configuration/routing.md | 1 + .../src/content/docs/zh-tw/reference/configuration/routing.md | 1 + 9 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/fr/reference/configuration/routing.md b/docs-site/src/content/docs/fr/reference/configuration/routing.md index d1a0107adc..64713d645c 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/fr/reference/configuration/routing.md @@ -59,6 +59,7 @@ Chaque clé de combinaison est un identifiant conforme à `[A-Za-z0-9][A-Za-z0-9 | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Stratégie de sélection. L’ordre des cibles définit la priorité de `failover` ; les poids déterminent les sélections de `round-robin` et de `random` ; `least-used` suit les réussites enregistrées ; `reset-window` suit la réinitialisation de quota la plus proche. | | `stickyLimit?` | `number` | `1` | Nombre de requêtes réussies conservées dans un même lot de rotation. Plage de 1 à 100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | non défini | `defaultEffort` complète un `reasoning.effort` absent si le combo possède une valeur par défaut non nulle et si la liste des niveaux acceptés par la cible est connue et non vide. La valeur configurée est conservée si elle est acceptée ; sinon, le niveau accepté le plus élevé ne la dépassant pas est choisi, ou le niveau le plus bas si aucun n’est inférieur. Une liste inconnue ou vide n’ajoute aucune valeur par défaut. | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` calcule l’intersection des listes connues, y compris les listes vides ; `"adaptive"` exclut les listes vides. Les listes inconnues ne limitent l’intersection dans aucun des deux modes. À l’envoi, les listes explicitement vides suppriment les paramètres effort/thinking dans les deux modes ; les listes inconnues les suppriment seulement en adaptive. `reasoning.summary` est conservé. La résolution des listes connues non vides ainsi que le choix et l’ordre des cibles restent inchangés. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publie les images uniquement lorsque toutes les cibles les prennent en charge ; `"disabled"` impose le texte seul, retire les images des modalités publiées et rejette les requêtes qui en contiennent avant leur distribution. | | `alias?` | `string` | — | Identifiant public facultatif du modèle, à la place du slug canonique du sélecteur. | | `nativeAlias?` | `boolean` | `false` | Permet à un identifiant natif non qualifié actuellement pris en charge de prendre la priorité uniquement pour cet identifiant. Les identifiants non qualifiés `gpt-5.6-*` utilisent les identifiants Codex Pool/Direct. Les routes qualifiées par un compte restent distinctes. Les routes qualifiées par un fournisseur, telles que `openai-apikey/gpt-5.6-*`, utilisent la route configurée avec sa clé d’API et ne passent jamais par l’alias natif. | diff --git a/docs-site/src/content/docs/ja/reference/configuration/routing.md b/docs-site/src/content/docs/ja/reference/configuration/routing.md index 6cd6e81ab6..c6a6d80772 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ja/reference/configuration/routing.md @@ -70,6 +70,7 @@ picker catalog の convergence だけが保留中で routing change は失われ | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` |選択戦略。ターゲットの順序は `failover` の優先順位となり、`weight` は `round-robin` と `random` の抽選に影響し、`least-used` は記録された成功数に従い、`reset-window` は最も早いクォータリセットに従います。 | | `stickyLimit?` | `number` | `1` |成功したリクエストは 1 つのラウンドロビン バッチに保持されます。範囲は 1 ~ 100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` |設定を解除する | `defaultEffort` は、コンボの既定値が null でなく、対象の対応リストが既知で空でない場合に、省略された `reasoning.effort` を補います。設定値に対応していればその値を使い、そうでなければ設定値以下で最も高い段階を選びます。それもなければ最も低い対応段階を使います。不明または空のリストでは既定値を省略します。 | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` は空リストを含む既知の対応リストの共通部分を公開し、`"adaptive"` は空リストを除外します。不明なリストは両モードで共通部分を制限しません。送信時、明示的な空リストは両モードで effort/thinking 制御を削除し、不明なリストでは adaptive のみ削除します。`reasoning.summary` は保持されます。既知の空でない対象の effort 解決と対象の選択・順序は変わりません。 | | `alias?` | `string` | — |正規のピッカー スラグの代わりのオプションのパブリック モデル ID。 | | `nativeAlias?` | `boolean` | `false` | 現在サポートされている bare native id に限り、その未修飾 id で優先します。アカウント修飾およびプロバイダー修飾の OpenAI ルートは別のままです。 | | `displayName?` | `string` | — | catalog 表示専用ラベル。native alias では空でない値が必須です。 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/routing.md b/docs-site/src/content/docs/ko/reference/configuration/routing.md index 1a25960344..2f617695ce 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ko/reference/configuration/routing.md @@ -69,6 +69,7 @@ Codex Auth 페이지에서 이 picker 동작을 opt-in할 수 있습니다. 비 | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 선택 전략입니다. 대상 순서는 `failover` 우선순위이고, 가중치는 `round-robin`과 `random` 추첨 비율을 결정하며, `least-used`는 기록된 성공 횟수를 따르고, `reset-window`는 가장 가까운 할당량 재설정을 따릅니다. | | `stickyLimit?` | `number` | `1` | 한 round-robin 배치에서 유지되는 성공 요청 수입니다. 범위는 1–100입니다. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort`는 콤보 기본값이 null이 아니고, 선택한 대상의 지원 목록이 알려져 있으며 비어 있지 않을 때 생략된 `reasoning.effort`를 채웁니다. 설정값을 지원하면 그대로 사용합니다. 그렇지 않으면 설정값 이하의 가장 높은 지원 단계를 사용하고, 그런 단계가 없으면 가장 낮은 지원 단계를 사용합니다. 지원 목록이 없거나 비어 있으면 기본값을 생략합니다. | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"`는 빈 목록을 포함한 알려진 대상 지원 목록의 교집합을 사용하고, `"adaptive"`는 빈 목록을 제외합니다. 알 수 없는 목록은 두 모드 모두 교집합을 제한하지 않습니다. 전송 시 명시적 빈 목록은 두 모드에서 effort·thinking 제어를 제거하고, 알 수 없는 목록은 adaptive에서만 제거합니다. `reasoning.summary`는 보존됩니다. 알려진 비어 있지 않은 대상의 effort 결정과 대상 선택·순서는 그대로입니다. | | `alias?` | `string` | — | 정규화된 picker slug 대신 쓰는 선택적 공개 model id입니다. | | `nativeAlias?` | `boolean` | `false` | 현재 지원되는 bare native id가 해당 비수식 id에만 우선하도록 합니다. 계정 또는 프로바이더로 수식된 OpenAI route는 별도로 유지됩니다. | | `displayName?` | `string` | — | catalog 표시 전용 label이며 native alias에서는 비어 있지 않아야 합니다. | diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 7cd038b649..2934c337a6 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -91,7 +91,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | `cooldownMs?` | `number` | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Range 1–600000. When set, applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. Upstream signals take precedence and all cooldowns are capped at 10 minutes. | | `waitForCooldownMs?` | `number` | `0` | Maximum wait for the earliest eligible cooling target on each selection attempt before returning `combo_unavailable`. Range 0–600000; an abort cancels the wait. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort` fills an absent `reasoning.effort` when the combo has a non-null default and the selected target has a known, nonempty supported ladder. If the target supports the configured value, it is retained; otherwise the highest supported rung at or below it is used, or the lowest supported rung when none is lower. Unknown or empty ladders omit the default. | -| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` intersects every known target effort ladder, so a target advertising no effort control empties the combo's picker. `"adaptive"` excludes those empty ladders from the published intersection. Picker metadata only; target selection and dispatch are unchanged. | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` intersects all known target ladders, including empty ones; `"adaptive"` excludes empty ladders. Unknown ladders are catalog wildcards in both modes. At dispatch, explicit empty ladders remove effort/thinking controls in both modes; unknown ladders do so only in adaptive. `reasoning.summary` is preserved. Known nonempty targets retain their effort resolution, and target selection/order is unchanged. | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` publishes image only when every target supports images; `"disabled"` forces text-only (drops image from published modalities and rejects image-bearing requests before dispatch). | | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | | `nativeAlias?` | `boolean` | `false` | Let a currently supported bare native id take precedence only for that unqualified id. Bare `gpt-5.6-*` ids use Codex Pool/Direct credentials. Account-qualified routes remain distinct. Provider-qualified routes such as `openai-apikey/gpt-5.6-*` use their configured API-key route and never fall through to the native alias. | diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md index 53a6b4b5f3..ddd16f6175 100644 --- a/docs-site/src/content/docs/ru/guides/combos.md +++ b/docs-site/src/content/docs/ru/guides/combos.md @@ -177,7 +177,7 @@ Failover намеренно ограничен. Он помогает при п ## Effort по умолчанию -`defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан ненулевой default и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. +`defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан `defaultEffort`, отличный от `null`, и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. Подстановка сохраняет существующий effort и остальные поля reasoning. Нормализация возможностей ниже может отдельно удалить неподдерживаемые параметры effort/thinking. Значения default: `low`, `medium`, `high`, `xhigh`, `max`, `ultra`; отсутствие поля или `null` отключает подстановку. diff --git a/docs-site/src/content/docs/ru/reference/configuration/routing.md b/docs-site/src/content/docs/ru/reference/configuration/routing.md index 029df5594e..595916aebd 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/routing.md +++ b/docs-site/src/content/docs/ru/reference/configuration/routing.md @@ -87,7 +87,8 @@ selector-qualified строки и возвращает обычные GPT-ст | `targets` | `{ provider: string; model: string; weight?: number }[]` | required | Упорядоченные конкретные маршруты. `weight` находится в диапазоне 1–10000 и по умолчанию равен `1`. | | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Стратегия выбора. Порядок целей задаёт приоритет `failover`; значения `weight` определяют взвешивание выборов `round-robin` и `random`; `least-used` следует числу зарегистрированных успешных запросов; `reset-window` следует ближайшему сбросу квоты. | | `stickyLimit?` | `number` | `1` | Число успешных запросов, удерживаемых в одной партии round-robin. Диапазон 1–100. | -| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан ненулевой default и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. | +| `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | `defaultEffort` заполняет отсутствующий `reasoning.effort`, если задан `defaultEffort`, отличный от `null`, и список поддерживаемых уровней цели известен и непуст. Поддерживаемое настроенное значение сохраняется; иначе выбирается максимальный поддерживаемый уровень не выше него, а если такого нет — минимальный поддерживаемый уровень. При неизвестном или пустом списке default не добавляется. | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` вычисляет пересечение известных списков уровней, включая пустые; `"adaptive"` исключает пустые списки. Неизвестные списки не ограничивают пересечение в обоих режимах. При отправке явно пустой список удаляет параметры effort/thinking в обоих режимах, а неизвестный — только в adaptive. `reasoning.summary` сохраняется. Разрешение effort для известных непустых списков, выбор и порядок целей не меняются. | | `alias?` | `string` | — | Необязательный публичный id модели вместо канонического slug в селекторе. | | `nativeAlias?` | `boolean` | `false` | Даёт поддерживаемому bare native id приоритет только для этого неквалифицированного id. Bare `gpt-5.6-*` использует учётные данные Codex Pool/Direct. Маршруты с квалификатором аккаунта остаются отдельными. Провайдер-квалифицированные маршруты, например `openai-apikey/gpt-5.6-*`, используют настроенный API-ключ и никогда не переходят на native alias. | | `displayName?` | `string` | — | Метка только для catalog; для native alias обязательна и не может быть пустой. | diff --git a/docs-site/src/content/docs/tr/reference/configuration/routing.md b/docs-site/src/content/docs/tr/reference/configuration/routing.md index b81f1464cd..b1cbe4b484 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/routing.md +++ b/docs-site/src/content/docs/tr/reference/configuration/routing.md @@ -111,6 +111,7 @@ aileleri kullanamaz. | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | Seçim stratejisi. Hedef sırası `failover` önceliğini belirler; `weight` değerleri `round-robin` ve `random` seçimlerini biçimlendirir; `least-used` kaydedilen başarılı istekleri izler; `reset-window` en yakın kota sıfırlamasını izler. | | `stickyLimit?` | `number` | `1` | Tek bir round-robin grubunda tutulan başarılı istekler. Aralık 1–100. | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | ayarlanmamış | `defaultEffort`, combo varsayılanı null değilse ve hedefin desteklenen seviye listesi bilinen ve boş olmayan bir listeyse eksik `reasoning.effort` değerini doldurur. Yapılandırılmış değer destekleniyorsa korunur; değilse bu değeri aşmayan en yüksek desteklenen seviye, böyle bir seviye yoksa en düşük desteklenen seviye kullanılır. Liste bilinmiyor veya boşsa varsayılan eklenmez. | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"`, boş listeler dahil bilinen hedef seviye listelerinin kesişimini alır; `"adaptive"` boş listeleri çıkarır. Bilinmeyen listeler iki modda da kesişimi sınırlamaz. Gönderimde açıkça boş listeler iki modda effort/thinking denetimlerini kaldırır; bilinmeyen listeler bunu yalnızca adaptive modunda yapar. `reasoning.summary` korunur. Bilinen boş olmayan hedeflerin effort çözümü ve hedef seçimi/sırası değişmez. | | `alias?` | `string` | — | Kurallı seçici slug'ı yerine isteğe bağlı genel model kimliği. | | `nativeAlias?` | `boolean` | `false` | Şu anda desteklenen bir yalın yerel kimliğin yalnızca o niteliksiz kimlik için öncelikli olmasına izin verin. Yalın `gpt-5.6-*` kimlikleri Codex Havuz/Direct kimlik bilgilerini kullanır. Hesap nitelikli rotalar ayrı kalır. `openai-apikey/gpt-5.6-*` gibi sağlayıcı nitelikli rotalar yapılandırılmış API anahtarı rotalarını kullanır ve asla yerel takma ada düşmez. | | `displayName?` | `string` | — | Yalnızca görüntüleme amaçlı katalog etiketi, yerel bir takma ad için gerekli ve boş olmamalıdır. | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md index 6232bc0306..fa7d04ffc5 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/routing.md @@ -74,6 +74,7 @@ Codex Auth 页面将此 picker 行为作为选择加入项。关闭它会隐藏 | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 选择策略。目标顺序表示 `failover` 优先级;`weight` 决定 `round-robin` 和 `random` 的抽取权重;`least-used` 根据记录的成功次数选择;`reset-window` 跟随最近的额度重置。 | | `stickyLimit?` | `number` | `1` | 在单个轮询批次中保留的成功请求数。范围 1–100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | unset | 当 combo 配置了非 null 默认值且目标支持列表已知且非空时,`defaultEffort` 会填充省略的 `reasoning.effort`。目标支持配置值时保留该值,否则选择不高于配置值的最高支持档位;若不存在更低档位,则使用最低支持档位。未知或空列表不会注入默认值。 | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` 对所有已知目标档位列表取交集,包括空列表;`"adaptive"` 排除空列表。未知列表在两种模式下都不限制目录交集。发送时,显式空列表在两种模式下都会移除 effort/thinking 控制;未知列表仅在 adaptive 下移除。`reasoning.summary` 保持不变。已知非空目标的 effort 解析、目标选择和顺序不变。 | | `imageInput?` | `"auto" \| "disabled"` | `"auto"` | `"auto"` 仅在每个目标都支持图片时发布图片能力;`"disabled"` 强制仅文本(从对外能力中去掉图片,并在分发前拒绝带图请求)。 | | `alias?` | `string` | — | 可选的公开 model id,用于替代规范化的选择器 slug。 | | `nativeAlias?` | `boolean` | `false` | 仅让当前受支持的裸原生 id 对该不带限定前缀的 id 优先;带账号或提供方限定的 OpenAI 路由仍是独立路由。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md index 50ef13c16b..2001cf14a9 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/routing.md @@ -57,6 +57,7 @@ Codex Auth 頁面將此 picker 行為作為選擇加入功能暴露。停用它 | `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window"` | `"failover"` | 選擇策略。目標順序為 `failover` 優先序;`weight` 塑造 `round-robin` 與 `random` 抽選;`least-used` 依循已記錄的成功次數;`reset-window` 依循最早的配額重設。 | | `stickyLimit?` | `number` | `1` | 在一個 round-robin 批次中保留的成功請求數。範圍 1–100。 | | `defaultEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max" \| "ultra" \| null` | 未設定 | 當 combo 設定非 null 預設值且目標支援清單已知且非空時,`defaultEffort` 會補入省略的 `reasoning.effort`。目標支援設定值時保留該值,否則選擇不高於設定值的最高支援層級;若沒有更低層級,則使用最低支援層級。未知或空清單不會注入預設值。 | +| `reasoningEffortMode?` | `"strict" \| "adaptive"` | `"strict"` | `"strict"` 對所有已知目標層級清單取交集,包括空清單;`"adaptive"` 排除空清單。未知清單在兩種模式下都不限制目錄交集。傳送時,明確空清單在兩種模式下都會移除 effort/thinking 控制;未知清單只在 adaptive 移除。`reasoning.summary` 保持不變。已知非空目標的 effort 解析、目標選擇及順序不變。 | | `alias?` | `string` | — | 可選的公開模型 id,取代標準 picker slug。 | | `nativeAlias?` | `boolean` | `false` | 讓目前支援的裸原生 id 僅對該未限定 id 取得優先。裸 `gpt-5.6-*` id 使用 Codex 池/Direct 憑證。帳號限定路由保持獨立。供應商限定路由(如 `openai-apikey/gpt-5.6-*`)使用其設定的 API-key 路由,且永不會落到原生別名。 | | `displayName?` | `string` | — | 僅顯示的目錄標籤,對原生別名為必填且非空。 |