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
9 changes: 9 additions & 0 deletions docs/releases/0.31.6.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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}$/;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion packages/backend/src/routing/proxy/provider-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/routing/proxy/provider-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/thinker/package.json
Original file line number Diff line number Diff line change
@@ -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."
}
Loading