From 6bfe32e7edb412803882abaa8827ce5289c70a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 17:34:05 -0300 Subject: [PATCH 1/2] fix(proxy): the Anthropic connection's own base URL receives the caller's anthropic-beta flags Caller beta flags were forwarded only when the host was api.anthropic.com. An Anthropic API-key connection pointed at a proxy or a simulator lost them, so Claude Code's beta-only fields (a role "system" turn with output_config, per-turn-control) came back as 400 "Extra inputs are not permitted". Anthropic-format overrides of other providers still get no flags. Co-Authored-By: Claude Opus 5.5 --- .../__tests__/provider-client.headers.spec.ts | 42 +++++++++++++++++++ .../proxy/forward-endpoint-resolver.spec.ts | 17 ++++++++ .../proxy/forward-endpoint-resolver.ts | 1 + .../src/routing/proxy/provider-client.ts | 6 ++- .../src/routing/proxy/provider-endpoints.ts | 6 +++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/routing/proxy/__tests__/provider-client.headers.spec.ts b/packages/backend/src/routing/proxy/__tests__/provider-client.headers.spec.ts index 4455c230..7f775844 100644 --- a/packages/backend/src/routing/proxy/__tests__/provider-client.headers.spec.ts +++ b/packages/backend/src/routing/proxy/__tests__/provider-client.headers.spec.ts @@ -9,6 +9,7 @@ // fails loudly. import { ProviderClient } from '../provider-client'; +import { buildEndpointOverride } from '../provider-endpoints'; import { CodexSessionAffinity } from '../codex-session-affinity'; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; @@ -482,6 +483,47 @@ describe('ProviderClient — anthropic-beta merge', () => { expect(sentBeta()).toBe('structured-outputs-2025-11-13'); }); + it("forwards through the Anthropic connection's own base URL (a proxy or simulator)", async () => { + // Without the flags, Claude Code's beta-only fields (role "system" turns + // with output_config, per-turn-control) are refused upstream. + const customEndpoint = { + ...buildEndpointOverride('http://wwz:8000/anthropic', 'anthropic'), + requiresSsrfRevalidation: false, + }; + customEndpoint.anthropicAccountOverride = true; + await client.forward({ + provider: 'anthropic', + apiKey: 'sk-ant-key', + model: 'claude-sonnet-4-20250514', + body, + stream: false, + apiMode: 'messages', + clientAnthropicBeta: 'per-turn-control-2026-07-01', + customEndpoint, + }); + + expect(mockFetch.mock.calls[0][0]).toBe('http://wwz:8000/anthropic/v1/messages'); + expect(sentBeta()).toBe('per-turn-control-2026-07-01'); + }); + + it('does not forward beta flags to an Anthropic-format override of another provider', async () => { + await client.forward({ + provider: 'minimax', + apiKey: 'k', + model: 'MiniMax-M2', + body, + stream: false, + apiMode: 'messages', + clientAnthropicBeta: 'per-turn-control-2026-07-01', + customEndpoint: { + ...buildEndpointOverride('http://proxy:8000', 'anthropic'), + requiresSsrfRevalidation: false, + }, + }); + + expect(mockFetch.mock.calls[0][1].headers).not.toHaveProperty('anthropic-beta'); + }); + it('does not forward beta flags on a translated (non-Messages) request', async () => { // A chat-completions body reaches Anthropic through the OpenAI->Anthropic // converters, which only understand the content blocks they were written diff --git a/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts b/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts index fb1cd0a8..dd488fe2 100644 --- a/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts +++ b/packages/backend/src/routing/proxy/forward-endpoint-resolver.spec.ts @@ -366,6 +366,23 @@ describe('resolveForwardEndpoint', () => { expect(or.forwardModel).toBe('white-wizard/wwz-fast'); }); + it('marks the Anthropic connection override so caller beta flags still reach it', () => { + const out = resolveForwardEndpoint({ + provider: 'anthropic', + authType: 'api_key', + model: 'claude-sonnet-5-5', + providerBaseUrl: 'http://wwz:8000/anthropic', + }); + expect(out.customEndpoint?.anthropicAccountOverride).toBe(true); + const openai = resolveForwardEndpoint({ + provider: 'openai', + authType: 'api_key', + model: 'gpt-4o', + providerBaseUrl: 'http://wwz:8000/openai', + }); + expect(openai.customEndpoint?.anthropicAccountOverride).toBeUndefined(); + }); + it('sets no override for a custom provider whose row is missing', () => { const out = resolveForwardEndpoint({ provider: 'custom:gone', diff --git a/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts b/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts index 1d757484..0ab9c5e2 100644 --- a/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts +++ b/packages/backend/src/routing/proxy/forward-endpoint-resolver.ts @@ -168,6 +168,7 @@ export function resolveForwardEndpoint( // exactly as the native endpoint would have. forwardModel = stripModelPrefix(forwardModel, overrideKey); customEndpoint.allowPrivateDestination = allowsPrivateDestination(providerBaseUrlAllowsPrivate); + if (overrideKey === 'anthropic') customEndpoint.anthropicAccountOverride = true; } else if (resolveEndpointKey(provider) === 'bedrock' && isBedrockRegion(providerRegion)) { const bedrockEndpointKey = resolveBedrockEndpointKey(model, apiMode); customEndpoint = buildEndpointOverride( diff --git a/packages/backend/src/routing/proxy/provider-client.ts b/packages/backend/src/routing/proxy/provider-client.ts index 2615337d..be9db64a 100644 --- a/packages/backend/src/routing/proxy/provider-client.ts +++ b/packages/backend/src/routing/proxy/provider-client.ts @@ -854,7 +854,11 @@ export class ProviderClient { // type would have its output silently dropped on the way back, which // is worse than the 400 we are fixing. Native Messages responses pass // through byte-for-byte, so unknown blocks survive there. - ctx.apiMode === 'messages' && isAnthropicHost(endpoint.baseUrl) + // The Anthropic connection's own base URL counts too: the operator + // pointed the Anthropic provider there (a proxy, a simulator), and + // without the flags Claude Code's beta-only fields are refused. + ctx.apiMode === 'messages' && + (isAnthropicHost(endpoint.baseUrl) || endpoint.anthropicAccountOverride === true) ? ctx.clientAnthropicBeta : undefined, ), diff --git a/packages/backend/src/routing/proxy/provider-endpoints.ts b/packages/backend/src/routing/proxy/provider-endpoints.ts index 4518c60e..e150fae2 100644 --- a/packages/backend/src/routing/proxy/provider-endpoints.ts +++ b/packages/backend/src/routing/proxy/provider-endpoints.ts @@ -66,6 +66,12 @@ export interface ProviderEndpoint { * private-URL policy did not allow, ADR 0006 item 7). Absent = allowed. */ allowPrivateDestination?: boolean; + /** + * The Anthropic provider's own connection with an operator-set base URL + * (a proxy in front of Anthropic, a simulator). It is Anthropic as far as + * the caller's `anthropic-beta` flags go, though the host is not. + */ + anthropicAccountOverride?: boolean; /** * Upper bound of one forwarded request, overriding PROVIDER_TIMEOUT_MS. * Set per local server, whose first call may have to load the model. From d08f06c11186ffa4fbe3b4e473e3795c870a820f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 17:34:18 -0300 Subject: [PATCH 2/2] chore(release): version 0.31.6 --- docs/releases/0.31.6.md | 9 +++++++++ package-lock.json | 2 +- packages/thinker/package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/releases/0.31.6.md diff --git a/docs/releases/0.31.6.md b/docs/releases/0.31.6.md new file mode 100644 index 00000000..67069eb5 --- /dev/null +++ b/docs/releases/0.31.6.md @@ -0,0 +1,9 @@ +# Thinker 0.31.6 + +```bash +docker pull tmsoftbrasil/thinker:0.31.6 +``` + +## Mudanças + +- fix(proxy): the Anthropic connection's own base URL receives the caller's anthropic-beta flags diff --git a/package-lock.json b/package-lock.json index 47a2580e..910a54b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14768,7 +14768,7 @@ } }, "packages/thinker": { - "version": "0.31.5" + "version": "0.31.6" } } } diff --git a/packages/thinker/package.json b/packages/thinker/package.json index 4604d56d..46b1161c 100644 --- a/packages/thinker/package.json +++ b/packages/thinker/package.json @@ -1,6 +1,6 @@ { "name": "thinker", - "version": "0.31.5", + "version": "0.31.6", "private": true, "description": "Canonical release version for the Thinker Docker image. This package holds no code — the real backend and frontend live in ../backend and ../frontend. See README.md for details." }