From f6b9565a935a26814974866684c455f27afc2dee Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:32:30 +0100 Subject: [PATCH 1/2] mason: recognize server fallback boundaries by signature Restore or drop stored boundary markers regardless of reasoning text cleared by a context manager. Preserve signature decoding, validity checks and fallback policy behavior. Extend existing OC1/OC2 replay coverage to cleared text while retaining the original marker-text cases, and add enabled/disabled regression checks. --- CHANGELOG.md | 2 + packages/opencode/src/server-fallback.ts | 2 - .../src/tests/opencode2-adapter.test.ts | 158 +++++++++--------- .../src/tests/server-fallback.test.ts | 53 ++++++ packages/opencode/src/tests/transform.test.ts | 66 ++++---- 5 files changed, 173 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2bcaf72..5e1961a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ This repo is a CortexKit-maintained Anthropic auth monorepo for OpenCode and Pi. ## Unreleased +- A server-fallback boundary is now restored even after a context manager clears its thinking text, instead of being sent to Anthropic as an invalid thinking block. + ## 2.7.1 - Addresses #673: in vault mode, Insula's quota readings stay fresh between background checks, so the plugin no longer polls Anthropic's usage endpoint itself when Insula already has the reading. diff --git a/packages/opencode/src/server-fallback.ts b/packages/opencode/src/server-fallback.ts index 26bbb62d..42590f1c 100644 --- a/packages/opencode/src/server-fallback.ts +++ b/packages/opencode/src/server-fallback.ts @@ -156,8 +156,6 @@ function rewriteStoredMarkers(body: Record, enabled: boolean) { const block = asRecord(rawBlock) const isInternalMarker = block?.type === 'thinking' && - (block.thinking === SERVER_FALLBACK_MARKER_TEXT || - block.thinking === '') && typeof block.signature === 'string' && block.signature.startsWith(SERVER_FALLBACK_SIGNATURE_PREFIX) if (!isInternalMarker) { diff --git a/packages/opencode/src/tests/opencode2-adapter.test.ts b/packages/opencode/src/tests/opencode2-adapter.test.ts index 8e65cfe5..1bc461c8 100644 --- a/packages/opencode/src/tests/opencode2-adapter.test.ts +++ b/packages/opencode/src/tests/opencode2-adapter.test.ts @@ -770,86 +770,92 @@ describe('OpenCode 2 native Anthropic adapter: request shape', () => { expect(seen.requestOptions).toHaveLength(0) }) - test('API route strips incoming OAuth-only betas, fallback opt-in and fallback markers', async () => { - const { source } = makeSource({ - authorize: () => - apiKey( - 'sk-ant-api03-row-key', - { custody: 'api', receipt: 'api:row-2' }, - 'https://gateway.example.com/anthropic', + test.each([SERVER_FALLBACK_MARKER_TEXT, '[cleared]'])( + 'API route strips incoming OAuth-only betas, fallback opt-in and fallback markers with thinking text %j', + async (thinking) => { + const { source } = makeSource({ + authorize: () => + apiKey( + 'sk-ant-api03-row-key', + { custody: 'api', receipt: 'api:row-2' }, + 'https://gateway.example.com/anthropic', + ), + }) + const host = new Host(createNativeAnthropicAdapter({ source }).adapter) + const clean = sonnetPayload() + // Replaying an OAuth-served conversation on an API route must remove + // its safety-fallback opt-in and stored assistant fallback marker. + const contaminated = { + ...clean, + fallbacks: 'default', + messages: clean.messages.map((message, index) => + index === 1 + ? { + ...message, + content: [ + { + type: 'thinking', + thinking, + signature: `${SERVER_FALLBACK_SIGNATURE_PREFIX}claude-fable-5|claude-opus-5`, + }, + { + type: 'thinking', + thinking: SERVER_FALLBACK_USAGE_MARKER_TEXT, + signature: `${SERVER_FALLBACK_USAGE_SIGNATURE_PREFIX}{"iterations":[]}`, + }, + { + type: 'thinking', + thinking: SERVER_FALLBACK_USAGE_MARKER_TEXT, + signature: 'forged-provider-signature', + }, + ...message.content, + ], + } + : message, ), - }) - const host = new Host(createNativeAnthropicAdapter({ source }).adapter) - const clean = sonnetPayload() - // Replaying an OAuth-served conversation on an API route must remove - // its safety-fallback opt-in and stored assistant fallback marker. - const contaminated = { - ...clean, - fallbacks: 'default', - messages: clean.messages.map((message, index) => - index === 1 - ? { - ...message, - content: [ - { - type: 'thinking', - thinking: SERVER_FALLBACK_MARKER_TEXT, - signature: `${SERVER_FALLBACK_SIGNATURE_PREFIX}claude-fable-5|claude-opus-5`, - }, - { - type: 'thinking', - thinking: SERVER_FALLBACK_USAGE_MARKER_TEXT, - signature: `${SERVER_FALLBACK_USAGE_SIGNATURE_PREFIX}{"iterations":[]}`, - }, - { - type: 'thinking', - thinking: SERVER_FALLBACK_USAGE_MARKER_TEXT, - signature: 'forged-provider-signature', - }, - ...message.content, - ], - } - : message, - ), - } + } - const { final } = await host.run( - scopeOf(), - hostRequest(contaminated, { - headers: { - 'anthropic-beta': [ - SERVER_SIDE_FALLBACK_BETA, - 'oauth-2025-04-20', - 'claude-code-20250219', - 'fine-grained-tool-streaming-2025-05-14', - 'context-1m-2025-08-07', - ].join(','), - }, - }), - ) + const { final } = await host.run( + scopeOf(), + hostRequest(contaminated, { + headers: { + 'anthropic-beta': [ + SERVER_SIDE_FALLBACK_BETA, + 'oauth-2025-04-20', + 'claude-code-20250219', + 'fine-grained-tool-streaming-2025-05-14', + 'context-1m-2025-08-07', + ].join(','), + }, + }), + ) - expect(final.headers.get('anthropic-beta')).toBe( - 'fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07', - ) - expect(await bodyOf(final)).toEqual(clean) - expect(await final.clone().text()).toBe(JSON.stringify(clean)) - expect(final.headers.get('x-api-key')).toBe('sk-ant-api03-row-key') - expect(final.headers.get('authorization')).toBeNull() - expect(final.url).toBe( - 'https://gateway.example.com/anthropic/v1/messages?beta=true', - ) + expect(final.headers.get('anthropic-beta')).toBe( + 'fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07', + ) + expect(await bodyOf(final)).toEqual(clean) + expect(await final.clone().text()).toBe(JSON.stringify(clean)) + expect(await final.clone().text()).not.toContain( + SERVER_FALLBACK_SIGNATURE_PREFIX, + ) + expect(final.headers.get('x-api-key')).toBe('sk-ant-api03-row-key') + expect(final.headers.get('authorization')).toBeNull() + expect(final.url).toBe( + 'https://gateway.example.com/anthropic/v1/messages?beta=true', + ) - // If filtering leaves no beta values, omit the header rather than send it empty. - const onlyOAuth = await host.run( - scopeOf(), - hostRequest(clean, { - headers: { - 'anthropic-beta': `oauth-2025-04-20,${SERVER_SIDE_FALLBACK_BETA}`, - }, - }), - ) - expect(onlyOAuth.final.headers.get('anthropic-beta')).toBeNull() - }) + // If filtering leaves no beta values, omit the header rather than send it empty. + const onlyOAuth = await host.run( + scopeOf(), + hostRequest(clean, { + headers: { + 'anthropic-beta': `oauth-2025-04-20,${SERVER_SIDE_FALLBACK_BETA}`, + }, + }), + ) + expect(onlyOAuth.final.headers.get('anthropic-beta')).toBeNull() + }, + ) test('caller-provided billing, effort, cache and fast options reach the body rewrite', async () => { const options: NativeAnthropicBodyOptions = { diff --git a/packages/opencode/src/tests/server-fallback.test.ts b/packages/opencode/src/tests/server-fallback.test.ts index 8f8cc3dd..8263c691 100644 --- a/packages/opencode/src/tests/server-fallback.test.ts +++ b/packages/opencode/src/tests/server-fallback.test.ts @@ -268,6 +268,59 @@ describe('applyServerSideFallbackToBody', () => { }) }) + test.each([true, false])( + 'handles cleared boundary markers with server fallback enabled: %s', + (enabled) => { + const body = { + model: 'claude-fable-5', + messages: [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'partial source output' }, + { + type: 'thinking', + thinking: '[cleared]', + signature: `${SERVER_FALLBACK_SIGNATURE_PREFIX}claude-fable-5|claude-opus-5`, + }, + { type: 'text', text: 'continued fallback output' }, + ] as Array>, + }, + { role: 'user', content: [{ type: 'text', text: 'continue' }] }, + ], + } + + expect(applyServerSideFallbackToBody(body, enabled)).toEqual({ + enabled, + restoredMarkers: enabled ? 1 : 0, + droppedMarkers: enabled ? 0 : 1, + }) + expect(body.messages[0]?.content).toEqual([ + { type: 'text', text: 'partial source output' }, + ...(enabled + ? [ + { + type: 'fallback', + from: { model: 'claude-fable-5' }, + to: { model: 'claude-opus-5' }, + }, + ] + : []), + { type: 'text', text: 'continued fallback output' }, + ]) + expect( + body.messages + .flatMap((message) => message.content) + .some( + (block) => + block.type === 'thinking' && + typeof block.signature === 'string' && + block.signature.startsWith(SERVER_FALLBACK_SIGNATURE_PREFIX), + ), + ).toBe(false) + }, + ) + test('preserves ordinary thinking, restores chained fallbacks, and drops malformed internal markers', () => { const body = { model: 'claude-fable-5', diff --git a/packages/opencode/src/tests/transform.test.ts b/packages/opencode/src/tests/transform.test.ts index cb0d69f9..5ac4fdf9 100644 --- a/packages/opencode/src/tests/transform.test.ts +++ b/packages/opencode/src/tests/transform.test.ts @@ -2990,37 +2990,43 @@ describe('rewriteRequestBody', () => { } }) - test('restores hidden fallback boundaries before signing replay requests', async () => { - const result = JSON.parse( - await rewriteRequestBody( - JSON.stringify({ - model: 'claude-fable-5', - messages: [ - { - role: 'assistant', - content: [ - { type: 'text', text: 'partial' }, - { - type: 'thinking', - thinking: SERVER_FALLBACK_MARKER_TEXT, - signature: `${SERVER_FALLBACK_SIGNATURE_PREFIX}claude-fable-5|claude-opus-5`, - }, - { type: 'text', text: 'continued' }, - ], - }, - { role: 'user', content: 'continue' }, - ], - }), - { serverSideFallbackEnabled: true }, - ), - ) + test.each([SERVER_FALLBACK_MARKER_TEXT, '[cleared]'])( + 'restores hidden fallback boundaries before signing replay requests with thinking text %j', + async (thinking) => { + const result = JSON.parse( + await rewriteRequestBody( + JSON.stringify({ + model: 'claude-fable-5', + messages: [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'partial' }, + { + type: 'thinking', + thinking, + signature: `${SERVER_FALLBACK_SIGNATURE_PREFIX}claude-fable-5|claude-opus-5`, + }, + { type: 'text', text: 'continued' }, + ], + }, + { role: 'user', content: 'continue' }, + ], + }), + { serverSideFallbackEnabled: true }, + ), + ) - expect(result.messages[0].content[1]).toEqual({ - type: 'fallback', - from: { model: 'claude-fable-5' }, - to: { model: 'claude-opus-5' }, - }) - }) + expect(result.messages[0].content[1]).toEqual({ + type: 'fallback', + from: { model: 'claude-fable-5' }, + to: { model: 'claude-opus-5' }, + }) + expect(JSON.stringify(result)).not.toContain( + SERVER_FALLBACK_SIGNATURE_PREFIX, + ) + }, + ) test('requests summarized adaptive thinking for Opus 5 without thinking', async () => { const body = JSON.stringify({ From 74745ac934193c99bfa102f099701aaf55cccad5 Mon Sep 17 00:00:00 2001 From: ualtinok <94532+ualtinok@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:49:32 +0100 Subject: [PATCH 2/2] docs: changelog lines under Unreleased --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1961a9..4204ea86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ This repo is a CortexKit-maintained Anthropic auth monorepo for OpenCode and Pi. ## Unreleased - A server-fallback boundary is now restored even after a context manager clears its thinking text, instead of being sent to Anthropic as an invalid thinking block. - ## 2.7.1 - Addresses #673: in vault mode, Insula's quota readings stay fresh between background checks, so the plugin no longer polls Anthropic's usage endpoint itself when Insula already has the reading.