Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/server/responses/core-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,17 @@ export async function applyFinalRouteRequestNormalization(args: {
// Native passthrough keeps its snapshot-owned effort contract, including saved exclusions.
if (!shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)) {
const previousEffort = parsed.options.reasoning;
const previousThinkingEnabled = parsed.options.thinkingEnabled;
normalizeThinkingOptions(route.provider, route.modelId, parsed.options);
const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
if (previousEffort !== undefined && parsed.options.reasoning === undefined) {
const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
if (raw) raw.reasoning = stripEmptyLadderEffort(raw.reasoning, []);
}
if (previousThinkingEnabled !== undefined && parsed.options.thinkingEnabled === undefined
&& raw?.reasoning && typeof raw.reasoning === "object" && !Array.isArray(raw.reasoning)) {
delete (raw.reasoning as { enabled?: boolean }).enabled;
if (Object.keys(raw.reasoning).length === 0) delete raw.reasoning;
}
}
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
const surface = collabSurface(parsed);
Expand Down
1 change: 1 addition & 0 deletions structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ provider, lets the selected adapter speak the upstream protocol, then bridges ad
Responses-compatible streaming output. Every Responses serializer projects unreferenced hosted-search history when the final body declares no hosted search tool; with search declared, the canonical Codex ordinary serializer retains structural hosted history by default, and only selected-provider `projectResponsesLiteHostedSearchHistory === true` plus exact canonical `/responses` and explicit Lite intent enables model-independent projection under the [hosted-search history boundary](../providers-and-adapters.md#hosted-search-continuation-binding), shared by HTTP/SSE and WS. The routed provider name follows every adapter build, including retries and continuations; only `github-copilot` injects a configured `modelContextTiers` value as upstream `contextTier`, while unconfigured passthrough retains the caller field. For an opted-in key-auth provider, a hosted-search continuation stays bound to the API-key selection that served the first leg; the contract is the [hosted-search continuation binding](../providers-and-adapters.md#hosted-search-continuation-binding). The native passthrough undeclared-tool guard fails closed on the Responses inbound wire in SSE and buffered JSON before continuation state is stored. Chat and Anthropic inbounds defer to the caller's own registry except an explicit empty catalog; the [declared-tool contract](responses-wire-shapes.md#declared-tool-membership-by-inbound-wire) also covers managed native, translated, directly encoded, and final media output while preserving internal hosted operations. GitHub Copilot Auto admission rechecks the negotiated provider/model against the key scope captured at request admission (a key edited or removed mid-request cannot widen it) before route mutation or dispatch, on initial and renewal negotiation; denial returns the standard `model_not_allowed_for_key` 403 naming the caller's inbound selector without disclosing the destination. The web-search and image sidecars answer a denial during their credential rotation with that 403 instead of the original 429, and a combo child's denial keeps the same envelope. Local model-scope refusals are non-replayable. A denial during Copilot dispatch renewal propagates as an admission exception to the request boundary, never as a provider fetch response; account recovery and combo/policy fallback cannot continue after that denial. A committed continuation, including OAuth account rotation after a 429, emits the same refusal type in-stream without dispatching another inference; ordinary negotiation failures still preserve the original provider refusal.

The `openai-responses` adapter preserves caller `User-Agent` unless provider headers own it, in either auth mode.
Final route capability normalization removes rejected reasoning effort and toggle fields from the native passthrough body when those fields were parsed into options, before custom gateway-object serialization.
`forwardClientHeaders` additionally opts into `originator`, `x-client-request-id`, `x-codex-app-version`, or `user-agent`.
Provider headers win case-insensitively; arbitrary names are rejected on load/write and ignored at runtime.
Canonical forward auth retains its separate fixed credential/metadata allowlist; absent caller identity is not invented. Upstream WebSocket reasoning-inclusion headers follow the [native prelude and connection-signal contract](responses-wire-shapes.md#passthrough-sse-stream-shapes-314).
Expand Down
20 changes: 20 additions & 0 deletions tests/responses/reasoning-effort-summary-default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ describe("reasoning effort preserves visible thinking when summary is omitted",
expect(child.reasoning).toEqual({ effort: "high", summary: "auto" });
});

test("final route removes a rejected thinking toggle from native passthrough", async () => {
const config: OcxConfig = {
port: 0,
defaultProvider: "native",
providers: { native: {
adapter: "openai-responses", baseUrl: "https://example.test/v1", reasoningWireFormat: "gateway-object",
models: ["test"],
modelCapabilities: { test: { supportsReasoning: false, supportsThinkingToggle: false, supportsReasoningEffort: false } },
} },
};
const parsed = parseRequest({ model: "native/test", input: [], reasoning: { enabled: true, effort: "high", summary: "auto" } });
const route = routeModel(config, parsed.modelId);
await applyFinalRouteRequestNormalization({
parsed, route, config, req: new Request("http://localhost/v1/responses"),
logCtx: { model: parsed.modelId, provider: route.providerName }, inboundWire: "responses",
});
expect(parsed.options.thinkingEnabled).toBeUndefined();
expect((parsed._rawBody as { reasoning?: unknown }).reasoning).toEqual({ summary: "auto" });
});

test("Chat final-route normalization strips both shapes without losing summary or later-route effort", async () => {
const config: OcxConfig = {
port: 0,
Expand Down
Loading