From ffded1eb0f090f5813df71bdc14f55ab8350e105 Mon Sep 17 00:00:00 2001 From: Brian Sparker Date: Sun, 4 Oct 2026 15:50:53 -0400 Subject: [PATCH] Move Worker to Claude Sonnet 5.5 Sonnet 5.5 rejects thinking:disabled, so use between_tools (its thinking-off setting) at low effort. Enable server-side refusal fallback, and strip non-text content blocks so shipped app versions that read content[0] keep working. Co-Authored-By: Claude Opus 5.5 --- server/src/index.ts | 25 ++++++++++++++++++++++--- server/tests/helpers.test.ts | 23 ++++++++++++++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 5d2a095..6193d4c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -40,7 +40,7 @@ const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages"; const ANTHROPIC_VERSION = "2023-06-01"; const ANTHROPIC_TIMEOUT_MS = 60_000; -const MODEL = "claude-sonnet-5"; +const MODEL = "claude-sonnet-5-5"; const MAX_TOKENS = 2048; const TRIAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days @@ -303,6 +303,20 @@ async function validateLicense(key: string, env: Env): Promise { } // --- Main handler --- +// Shipped app versions read content[0] and require it to be a text block. +// Newer models can lead with a thinking block, so pass through text only. +export function textBlocksOnly(body: string, ok: boolean): string { + if (!ok) return body; + try { + const parsed = JSON.parse(body); + if (!Array.isArray(parsed?.content)) return body; + parsed.content = parsed.content.filter((block: { type?: string }) => block?.type === "text"); + return JSON.stringify(parsed); + } catch { + return body; + } +} + export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); @@ -400,8 +414,12 @@ export default { max_tokens: MAX_TOKENS, system: buildSystemPrompt(pasteReq), // Low-latency, no-thinking profile — the product is a hotkey, not a chat. - thinking: { type: "disabled" }, + // Sonnet 5.5 rejects {type: "disabled"}; "between_tools" is its + // thinking-off setting (allowed at effort "high" or below). + thinking: { type: "between_tools" }, output_config: { effort: "low" }, + // On a cyber/frontier_llm refusal, Anthropic retries on Sonnet 5 server-side. + fallbacks: "default", messages: [ { role: "user", @@ -438,6 +456,7 @@ export default { "Content-Type": "application/json", "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": ANTHROPIC_VERSION, + "anthropic-beta": "server-side-fallback-2026-07-01", }, body: JSON.stringify(anthropicBody), signal: AbortSignal.timeout(ANTHROPIC_TIMEOUT_MS), @@ -453,7 +472,7 @@ export default { // (Requests rejected before the upstream call still cost nothing.) ctx.waitUntil(Promise.all([bumpCounter(deviceUsageKey, env), bumpCounter(globalUsageKey, env)])); - const responseBody = await anthropicResponse.text(); + const responseBody = textBlocksOnly(await anthropicResponse.text(), anthropicResponse.ok); return new Response(responseBody, { status: anthropicResponse.status, headers: { "Content-Type": "application/json" }, diff --git a/server/tests/helpers.test.ts b/server/tests/helpers.test.ts index ecd909d..ad251f0 100644 --- a/server/tests/helpers.test.ts +++ b/server/tests/helpers.test.ts @@ -300,4 +300,25 @@ describe('validatePasteRequest', () => { }); expect(result.ok).toBe(true); }); -}); \ No newline at end of file +}); +import { textBlocksOnly } from '../src/index'; + +describe('textBlocksOnly', () => { + it('drops thinking blocks so content[0] is text', () => { + const body = JSON.stringify({ + content: [ + { type: 'thinking', thinking: '' }, + { type: 'text', text: 'hello' }, + ], + stop_reason: 'end_turn', + }); + const out = JSON.parse(textBlocksOnly(body, true)); + expect(out.content).toEqual([{ type: 'text', text: 'hello' }]); + expect(out.stop_reason).toBe('end_turn'); + }); + + it('passes error and non-JSON bodies through unchanged', () => { + expect(textBlocksOnly('{"error":{}}', false)).toBe('{"error":{}}'); + expect(textBlocksOnly('not json', true)).toBe('not json'); + }); +});