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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.
Expand Down
2 changes: 0 additions & 2 deletions packages/opencode/src/server-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,6 @@ function rewriteStoredMarkers(body: Record<string, unknown>, 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) {
Expand Down
158 changes: 82 additions & 76 deletions packages/opencode/src/tests/opencode2-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
53 changes: 53 additions & 0 deletions packages/opencode/src/tests/server-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>>,
},
{ 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',
Expand Down
66 changes: 36 additions & 30 deletions packages/opencode/src/tests/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading