From 2d543ac9994e94f05c0a85fdf9842ec00ae41733 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:16:40 +0200 Subject: [PATCH] feat(agent): move every model call to Sonnet 5 (#1218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(agent): move every model call to Sonnet 5 Sonnet 5 is verified enabled on our Bedrock account already: a live probe of eu.anthropic.claude-sonnet-5 in eu-north-1 answered normally, so no model-access request was needed. The bare anthropic.claude-sonnet-5 is rejected (on-demand throughput needs the cross-region inference profile), so the eu. prefix we already use stays. This is not a model-string swap. Sonnet 5 REJECTS the fixed thinking budget outright: thinking {type:'enabled', budget_tokens} returns 400 "not supported for this model. Use thinking.type.adaptive and output_config.effort". Every chat intent set a budget, so the assistant would have failed on the first turn after a bare ID change. Reasoning depth is now an effort level (STANDARD high, DEEP xhigh), and max_tokens is explicit per tier rather than derived from a budget that no longer exists. display:'summarized' is load-bearing, not cosmetic. The default is 'omitted', which still emits thinking blocks but with empty text. Measured on our own account at xhigh effort: summarized returned ~1k characters of reasoning, the default returned none. Without it the collapsible "Tänker ..." block in the chat would have gone silently empty, which no mocked test would have caught. Ceilings are raised (16k standard, 24k deep) because Sonnet 5's tokenizer produces roughly 30% more tokens for the same text and max_tokens now caps thinking and the visible reply together. Also resolves the Opus 4.7 landmine recorded in the readiness doc: the composer comment told ops to flip BEDROCK_OPUS_MODEL_ID to Opus 4.7, which would have 400d every thinking intent against the legacy budget shape. Both model constants now point at Sonnet 5 and the stale instruction is gone. Checked but deliberately unchanged: forced tool_choice in atom-selection. The Sonnet 5 docs require thinking:{type:'disabled'} alongside a forced tool_choice on Bedrock; probed against our account, the forced call succeeds without it, so no change was made rather than adding a guard we cannot show is needed. Other call sites moved too: invoice-inbox extraction, document extraction, the compliance config, and the CI/CD workflows (pr-agent MODEL and MODEL_WEAK, swedish-compliance-review, compliance-swarm). Verified: 11315 tests pass, lint and tsc clean on every touched file, guards pass. Co-Authored-By: Claude Opus 5 * fix(agent): review triage: keep the no-thinking output ceiling, finish the model sweep max_tokens now caps thinking and the visible reply together, so collapsing the two tiers into one made every non-thinking intent inherit a 16000 ceiling where it used to have 4096. Give it its own MAX_TOKENS_NO_THINKING instead, set to the old 4096 scaled ~30% for Sonnet 5's tokenizer so the effective reply length is unchanged rather than quietly cut. scripts/swedish-compliance-review.mjs still fell back to Sonnet 4.6 when REVIEW_MODEL was unset, so a manual run silently used the old model. The initial sweep only covered .ts and .yml. pr-agent's FALLBACK_MODELS listed the primary model as its own fallback, which is not a fallback; dropped it and rewrote the surrounding comments, which still described Opus 4.8 and a 200k window. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- .compliance/config.yml | 2 +- .github/workflows/compliance-swarm.yml | 2 +- .github/workflows/pr-agent.yml | 17 +++--- .../workflows/swedish-compliance-review.yml | 2 +- .../general/document-extraction/index.ts | 2 +- .../lib/extract-invoice-fields.ts | 4 +- .../chat/__tests__/run-turn-memory.test.ts | 7 ++- .../chat/__tests__/run-turn-thinking.test.ts | 45 +++++++++++--- .../chat/__tests__/system-prompt.test.ts | 2 +- lib/agent/chat/run-turn.ts | 34 +++++++++-- lib/agent/composer/client.ts | 58 ++++++++++++------- lib/agent/intents/general-help.ts | 4 +- lib/agent/intents/inbox-bulk-book.ts | 4 +- lib/agent/intents/invoice-draft.ts | 4 +- lib/agent/intents/supplier-invoice-review.ts | 4 +- .../intents/transaction-categorization.ts | 4 +- lib/agent/intents/types.ts | 12 ++-- lib/agent/intents/vat-review.ts | 4 +- lib/agent/intents/verifikation-draft.ts | 4 +- scripts/swedish-compliance-review.mjs | 2 +- 20 files changed, 143 insertions(+), 74 deletions(-) diff --git a/.compliance/config.yml b/.compliance/config.yml index bf5b9139..74e5f53e 100644 --- a/.compliance/config.yml +++ b/.compliance/config.yml @@ -15,7 +15,7 @@ asvs_level: L2 soc2_categories: [security, confidentiality] severity_threshold_to_block: critical llm_provider: bedrock -llm_model: eu.anthropic.claude-sonnet-4-6 +llm_model: eu.anthropic.claude-sonnet-5 # Add suppressions here as triage progresses. Each rule needs a justification # and (for ISO/SOC 2 controls) a risk_id pointing at a Risk Register entry. diff --git a/.github/workflows/compliance-swarm.yml b/.github/workflows/compliance-swarm.yml index 8a1082a0..fdb39b81 100644 --- a/.github/workflows/compliance-swarm.yml +++ b/.github/workflows/compliance-swarm.yml @@ -54,4 +54,4 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Override default model if desired: - # COMPLIANCE_BEDROCK_MODEL: 'eu.anthropic.claude-sonnet-4-6' + # COMPLIANCE_BEDROCK_MODEL: 'eu.anthropic.claude-sonnet-5' diff --git a/.github/workflows/pr-agent.yml b/.github/workflows/pr-agent.yml index e07ce2d5..2ff01e60 100644 --- a/.github/workflows/pr-agent.yml +++ b/.github/workflows/pr-agent.yml @@ -61,16 +61,17 @@ jobs: AWS_REGION_NAME: ${{ secrets.PR_AGENT_AWS_REGION }} AWS_REGION: ${{ secrets.PR_AGENT_AWS_REGION }} - # ── Model: Claude Opus 4.8 via the EU Bedrock inference profile, with - # Sonnet 4.6 as fallback if the Opus profile isn't enabled for these - # creds. custom_model_max_tokens is required because these ids are not - # in PR-Agent's built-in token map. - CONFIG.MODEL: "bedrock/eu.anthropic.claude-opus-4-8" - CONFIG.MODEL_WEAK: "bedrock/eu.anthropic.claude-sonnet-4-6" - CONFIG.FALLBACK_MODELS: '["bedrock/eu.anthropic.claude-sonnet-4-6"]' + # ── Model: Claude Sonnet 5 via the EU Bedrock inference profile. The + # strong and weak slots both point at it: this account has no larger + # model enabled, so a second id would only be the same model under + # another name. No FALLBACK_MODELS for the same reason: a fallback + # list naming the primary is not a fallback. custom_model_max_tokens + # is required because this id is not in PR-Agent's built-in map. + CONFIG.MODEL: "bedrock/eu.anthropic.claude-sonnet-5" + CONFIG.MODEL_WEAK: "bedrock/eu.anthropic.claude-sonnet-5" CONFIG.CUSTOM_MODEL_MAX_TOKENS: "200000" # Input window PR-Agent prunes the diff to fit. Default (~32k) truncated - # large PRs; raise it so the whole diff is reviewed (Sonnet 4.6 = 200k ctx). + # large PRs; raise it so the whole diff is reviewed (Sonnet 5 = 1M ctx). CONFIG.MAX_MODEL_TOKENS: "64000" LITELLM.DROP_PARAMS: "true" diff --git a/.github/workflows/swedish-compliance-review.yml b/.github/workflows/swedish-compliance-review.yml index d41f5e4a..9fae7762 100644 --- a/.github/workflows/swedish-compliance-review.yml +++ b/.github/workflows/swedish-compliance-review.yml @@ -79,7 +79,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION || 'eu-north-1' }} - REVIEW_MODEL: eu.anthropic.claude-sonnet-4-6 + REVIEW_MODEL: eu.anthropic.claude-sonnet-5 # Two-stage mode: read the diff from the artifact instead of git-diffing. DIFF_FILE: diff.patch FILES_FILE: files.txt diff --git a/extensions/general/document-extraction/index.ts b/extensions/general/document-extraction/index.ts index c1f99234..4217897c 100644 --- a/extensions/general/document-extraction/index.ts +++ b/extensions/general/document-extraction/index.ts @@ -170,7 +170,7 @@ async function extractAndPersist( return } extractedData = data as unknown as Record - model = process.env.BEDROCK_MODEL_ID || 'eu.anthropic.claude-sonnet-4-6' + model = process.env.BEDROCK_MODEL_ID || 'eu.anthropic.claude-sonnet-5' } catch (err) { log.warn('extraction threw', { doc: document.id, diff --git a/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts b/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts index 2de640ae..eb7ded11 100644 --- a/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts +++ b/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts @@ -20,9 +20,9 @@ const log = createLogger('invoice-inbox-extract') // Both overridable via env vars so ops can swap models / raise token caps // without a code deploy. Defaults match what's expected to be set in -// production (eu.anthropic.claude-sonnet-4-6 in eu-north-1, 8192 tokens: +// production (eu.anthropic.claude-sonnet-5 in eu-north-1, 8192 tokens: // enough headroom for invoices with 20+ line items). -const MODEL = process.env.BEDROCK_MODEL_ID || 'eu.anthropic.claude-sonnet-4-6' +const MODEL = process.env.BEDROCK_MODEL_ID || 'eu.anthropic.claude-sonnet-5' const MAX_TOKENS = (() => { const parsed = Number(process.env.BEDROCK_MAX_TOKENS) // Use the env value only if it's a positive number: `||` would also diff --git a/lib/agent/chat/__tests__/run-turn-memory.test.ts b/lib/agent/chat/__tests__/run-turn-memory.test.ts index 001f6dd2..a677cacb 100644 --- a/lib/agent/chat/__tests__/run-turn-memory.test.ts +++ b/lib/agent/chat/__tests__/run-turn-memory.test.ts @@ -21,7 +21,10 @@ vi.mock('@/lib/agent/composer/client', () => ({ }, }, }), - SONNET_MODEL: 'claude-sonnet-4-6', + SONNET_MODEL: 'claude-sonnet-5', + MAX_TOKENS_NO_THINKING: 5400, + MAX_TOKENS_STANDARD: 16000, + MAX_TOKENS_DEEP: 24000, })) // system-prompt builder: return a minimal valid shape. @@ -66,7 +69,7 @@ function makeIntent(): AgentIntent { sheetTitle: 'x', atoms: { mode: 'progressive', horizontal: [], includeCompanyVertical: false, includeCompanyModifiers: false }, tools: ['gnubok_remember_fact'], - model: 'claude-sonnet-4-6', + model: 'claude-sonnet-5', capture: async () => ({}), promptTemplate: () => '', } diff --git a/lib/agent/chat/__tests__/run-turn-thinking.test.ts b/lib/agent/chat/__tests__/run-turn-thinking.test.ts index b403956a..4055714c 100644 --- a/lib/agent/chat/__tests__/run-turn-thinking.test.ts +++ b/lib/agent/chat/__tests__/run-turn-thinking.test.ts @@ -18,9 +18,16 @@ vi.mock('@/lib/agent/composer/client', () => ({ }, }, }), - SONNET_MODEL: 'claude-sonnet-4-6', + SONNET_MODEL: 'claude-sonnet-5', + MAX_TOKENS_NO_THINKING: 5400, + MAX_TOKENS_STANDARD: 16000, + MAX_TOKENS_DEEP: 24000, })) +const MAX_TOKENS_NO_THINKING = 5400 +const MAX_TOKENS_STANDARD = 16000 +const MAX_TOKENS_DEEP = 24000 + vi.mock('../system-prompt', () => ({ buildSystemPrompt: vi.fn().mockResolvedValue({ blocks: [], @@ -84,7 +91,11 @@ async function runWith(intent: AgentIntent) { emit: () => true, }) // The args object the stream was invoked with. - return messagesCreate.mock.calls[0][0] as { thinking?: unknown; max_tokens?: number } + return messagesCreate.mock.calls[0][0] as { + thinking?: unknown + output_config?: unknown + max_tokens?: number + } } beforeEach(() => { @@ -92,17 +103,33 @@ beforeEach(() => { }) describe('runChatTurn: extended thinking wiring', () => { - it('passes a thinking config and bumps max_tokens when the intent opts in', async () => { - const args = await runWith({ ...baseIntent(), thinking: { budgetTokens: 2000 } }) - expect(args.thinking).toEqual({ type: 'enabled', budget_tokens: 2000 }) - // budget must be strictly below max_tokens: we add the normal output budget. - expect(args.max_tokens).toBe(2000 + 4096) + it('requests adaptive thinking with a summarized display when the intent opts in', async () => { + const args = await runWith({ ...baseIntent(), thinking: { effort: 'high' } }) + // Sonnet 5 rejects { type: 'enabled', budget_tokens } outright. + expect(args.thinking).toEqual({ type: 'adaptive', display: 'summarized' }) + // display:'summarized' is what makes reasoning text actually arrive; the + // default 'omitted' streams empty thinking blocks and the chat's + // "Tänker …" block would never populate. + expect(args.output_config).toEqual({ effort: 'high' }) + expect(args.max_tokens).toBe(MAX_TOKENS_STANDARD) }) - it('omits thinking and keeps the default budget when the intent does not opt in', async () => { + it('raises the output ceiling for the deep-reasoning tier', async () => { + const args = await runWith({ ...baseIntent(), thinking: { effort: 'xhigh' } }) + expect(args.output_config).toEqual({ effort: 'xhigh' }) + // max_tokens now caps thinking AND the visible reply together. + expect(args.max_tokens).toBe(MAX_TOKENS_DEEP) + }) + + it('omits thinking and effort when the intent does not opt in', async () => { const args = await runWith(baseIntent()) expect(args.thinking).toBeUndefined() - expect(args.max_tokens).toBe(4096) + expect(args.output_config).toBeUndefined() + // A non-thinking intent keeps its own reply-sized ceiling: max_tokens now + // covers thinking too, so inheriting the reasoning tier's 16000 would let a + // plain answer run several times longer than it ever did before. + expect(args.max_tokens).toBe(MAX_TOKENS_NO_THINKING) + expect(args.max_tokens).toBeLessThan(MAX_TOKENS_STANDARD) }) }) diff --git a/lib/agent/chat/__tests__/system-prompt.test.ts b/lib/agent/chat/__tests__/system-prompt.test.ts index 03207070..1dbfc52f 100644 --- a/lib/agent/chat/__tests__/system-prompt.test.ts +++ b/lib/agent/chat/__tests__/system-prompt.test.ts @@ -23,7 +23,7 @@ const intent: AgentIntent = { sheetTitle: 'Fråga din assistent', atoms: { mode: 'progressive', horizontal: [], includeCompanyVertical: false, includeCompanyModifiers: false }, tools: [], - model: 'claude-sonnet-4-6', + model: 'claude-sonnet-5', capture: async () => ({}), promptTemplate: () => '', } diff --git a/lib/agent/chat/run-turn.ts b/lib/agent/chat/run-turn.ts index d045a39a..c49f3358 100644 --- a/lib/agent/chat/run-turn.ts +++ b/lib/agent/chat/run-turn.ts @@ -1,5 +1,11 @@ import type { SupabaseClient } from '@supabase/supabase-js' -import { getAnthropic, SONNET_MODEL } from '@/lib/agent/composer/client' +import { + getAnthropic, + MAX_TOKENS_DEEP, + MAX_TOKENS_NO_THINKING, + MAX_TOKENS_STANDARD, + SONNET_MODEL, +} from '@/lib/agent/composer/client' import type { AgentIntent } from '@/lib/agent/intents/types' import { agentToolRegistry } from '@/lib/agent/tools/registry' import type { AgentTool, AgentActorContext, StagedOperationResult } from '@/lib/agent/tools/types' @@ -242,13 +248,28 @@ export async function runChatTurn(args: RunTurnArgs): Promise { // Extended thinking ("tänka längre"): when the intent opts in, every model // call in the loop gets a reasoning channel so the agent reasons BEFORE it // answers or commits to a tool, instead of narrating its steps in the - // visible reply. budget_tokens must be ≥ 1024 and strictly below max_tokens, - // so the normal 4096 output budget is added on top. The reasoning streams to - // the client as reasoning_delta and renders in a collapsible "Tänkte…" block. + // visible reply. The reasoning streams to the client as reasoning_delta and + // renders in a collapsible "Tänkte…" block. + // + // display:'summarized' is load-bearing, not cosmetic. The default is + // 'omitted', which still emits thinking blocks but with empty text: measured + // on this account at xhigh effort, summarized returned ~1k characters of + // reasoning and the default returned none. Without it the collapsible + // "Tänker …" block in the chat would silently never populate. + // + // max_tokens now covers thinking and the reply together, so the ceiling + // follows what the intent opted into. An intent with no thinking keeps its + // reply-sized cap: giving it the reasoning tier's headroom would let a plain + // answer run four times longer for no reason. const thinking = intent.thinking - ? { type: 'enabled' as const, budget_tokens: intent.thinking.budgetTokens } + ? { type: 'adaptive' as const, display: 'summarized' as const } : undefined - const maxTokens = (intent.thinking?.budgetTokens ?? 0) + 4096 + const outputConfig = intent.thinking ? { effort: intent.thinking.effort } : undefined + const maxTokens = !intent.thinking + ? MAX_TOKENS_NO_THINKING + : intent.thinking.effort === 'xhigh' || intent.thinking.effort === 'max' + ? MAX_TOKENS_DEEP + : MAX_TOKENS_STANDARD // 4 + 5 + 6: iterate until the model stops requesting tools. while (iterations < MAX_TOOL_ITERATIONS) { @@ -266,6 +287,7 @@ export async function runChatTurn(args: RunTurnArgs): Promise { messages, tools: tools.length > 0 ? tools.map(toAnthropicTool) : undefined, ...(thinking ? { thinking } : {}), + ...(outputConfig ? { output_config: outputConfig } : {}), }) stream.on('text', (delta) => { diff --git a/lib/agent/composer/client.ts b/lib/agent/composer/client.ts index 71ffff1b..eb8e01e6 100644 --- a/lib/agent/composer/client.ts +++ b/lib/agent/composer/client.ts @@ -59,27 +59,41 @@ export function getAnthropic(): AnthropicBedrock { return cached } -// Bedrock model IDs. Region prefix `eu.` keeps inference inside eu-north-1. -// Both are env-overridable so ops can swap models without a code deploy. +// Bedrock model IDs. Region prefix `eu.` keeps inference inside eu-north-1 +// (a bare `anthropic.claude-sonnet-5` is rejected: on-demand throughput needs +// the cross-region inference profile). Both are env-overridable so ops can +// swap models without a code deploy. // -// Per plan §14 the composer's atom-selection call should run on Opus 4.7 for -// the higher-stakes selection reasoning. Opus 4.7 is not yet enabled on this -// AWS Bedrock account (403 "not available for this account": request access -// on the AWS console under Bedrock → Model access). For now we point OPUS at -// Sonnet 4.6 so the composer still works; atom selection on Sonnet is still -// good: it's a structured-output call via tool_use forcing, not deep -// reasoning. Flip BEDROCK_OPUS_MODEL_ID back to eu.anthropic.claude-opus-4-7 -// once Opus access lands. -export const OPUS_MODEL = process.env.BEDROCK_OPUS_MODEL_ID || 'eu.anthropic.claude-sonnet-4-6' -export const SONNET_MODEL = process.env.BEDROCK_SONNET_MODEL_ID || 'eu.anthropic.claude-sonnet-4-6' +// Both point at Sonnet 5, verified enabled on this Bedrock account. The two +// names are kept because the intents split on them: OPUS_MODEL marks the +// heavy-reasoning intents (supplier-invoice review, VAT review, bokslut) so +// that split survives if a genuinely larger model is enabled here later. +export const OPUS_MODEL = process.env.BEDROCK_OPUS_MODEL_ID || 'eu.anthropic.claude-sonnet-5' +export const SONNET_MODEL = process.env.BEDROCK_SONNET_MODEL_ID || 'eu.anthropic.claude-sonnet-5' -// Extended-thinking budgets (budget_tokens) for the chat intents. These are -// ceilings, not floors: the model spends only what a turn needs, so a generous -// cap improves hard turns (multi-source VAT synthesis, anomaly detection) -// without taxing simple ones. run-turn derives max_tokens = budget + 4096, so -// raising these is safe: no manual max_tokens bookkeeping. Tiered to match the -// model split: DEEP for the Opus / heavy-reasoning intents, STANDARD for the -// rest. Early-stage default favours reasoning quality over token cost; dial -// down here in one place if latency/cost ever bites. -export const THINKING_BUDGET_STANDARD = 6000 -export const THINKING_BUDGET_DEEP = 12000 +// Reasoning depth for the chat intents. +// +// Sonnet 5 removed the fixed thinking budget: `thinking: {type:'enabled', +// budget_tokens}` is rejected outright ("not supported for this model. Use +// thinking.type.adaptive and output_config.effort"). Depth is now a qualitative +// effort level and the model spends what a turn actually needs. +// +// Levels are `low | medium | high | xhigh | max`. Measured on this account: +// at `medium` a multi-step Swedish VAT question produced no reasoning at all, +// at `xhigh` it produced ~1k characters. Since the point of enabling thinking +// on these intents is that the agent reasons BEFORE it answers rather than +// narrating in the reply, DEEP uses xhigh and STANDARD high. +export const EFFORT_STANDARD = 'high' as const +export const EFFORT_DEEP = 'xhigh' as const + +// Output ceilings per tier. Previously derived as budget + 4096; with no budget +// to derive from these are explicit, and there are now three of them because +// max_tokens caps thinking AND the visible reply together: an intent that does +// not think must not inherit a ceiling sized for one that does. +// +// NO_THINKING is the old 4096 reply cap scaled by ~30% for Sonnet 5's new +// tokenizer, which spends that much more on the same Swedish text, so the +// effective reply length is unchanged rather than quietly cut. +export const MAX_TOKENS_NO_THINKING = 5400 +export const MAX_TOKENS_STANDARD = 16000 +export const MAX_TOKENS_DEEP = 24000 diff --git a/lib/agent/intents/general-help.ts b/lib/agent/intents/general-help.ts index 75974506..3b9cab7b 100644 --- a/lib/agent/intents/general-help.ts +++ b/lib/agent/intents/general-help.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { SONNET_MODEL, THINKING_BUDGET_STANDARD } from '@/lib/agent/composer/client' +import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client' import { renderAgentGroundRules } from './shared-rules' // general.help: always-present "Fråga min assistent" from the top nav. @@ -98,7 +98,7 @@ export const generalHelp = defineAgentIntent ({ route: route ?? null }), diff --git a/lib/agent/intents/inbox-bulk-book.ts b/lib/agent/intents/inbox-bulk-book.ts index 6c34fbb5..17e15954 100644 --- a/lib/agent/intents/inbox-bulk-book.ts +++ b/lib/agent/intents/inbox-bulk-book.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { SONNET_MODEL, THINKING_BUDGET_STANDARD } from '@/lib/agent/composer/client' +import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client' // inbox.bulk-book: "Fråga assistenten" on a multi-selection in the Underlag // view (Dokumentinkorgen). Unlike transaction.categorization (which keys off the @@ -116,7 +116,7 @@ export const inboxBulkBook = defineAgentIntent { const ids = Array.isArray(item_ids) ? item_ids.filter((x): x is string => typeof x === 'string') : [] diff --git a/lib/agent/intents/invoice-draft.ts b/lib/agent/intents/invoice-draft.ts index 4f087689..c5b91777 100644 --- a/lib/agent/intents/invoice-draft.ts +++ b/lib/agent/intents/invoice-draft.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { SONNET_MODEL, THINKING_BUDGET_STANDARD } from '@/lib/agent/composer/client' +import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client' import { renderAgentGroundRules } from './shared-rules' // invoice.draft: "Fråga om denna faktura" from the invoice form. @@ -83,7 +83,7 @@ export const invoiceDraft = defineAgentIntent { // Resolve the effective customer_id. When the FAB lands here from diff --git a/lib/agent/intents/supplier-invoice-review.ts b/lib/agent/intents/supplier-invoice-review.ts index 952876c1..7155f436 100644 --- a/lib/agent/intents/supplier-invoice-review.ts +++ b/lib/agent/intents/supplier-invoice-review.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { OPUS_MODEL, THINKING_BUDGET_DEEP } from '@/lib/agent/composer/client' +import { OPUS_MODEL, EFFORT_DEEP } from '@/lib/agent/composer/client' import { renderAgentGroundRules } from './shared-rules' // supplier_invoice.review: "Fråga din assistent" from a supplier invoice @@ -171,7 +171,7 @@ export const supplierInvoiceReview = defineAgentIntent< // so the visible reply is a single conclusion after the booking is staged: // not a pre-tool analysis echoed again post-tool. Matches the always-on // prompt's promise that reasoning happens in the (separately shown) tankekanal. - thinking: { budgetTokens: THINKING_BUDGET_DEEP }, + thinking: { effort: EFFORT_DEEP }, capture: async ({ supplier_invoice_id }, { supabase, companyId }) => { const { data: invoice } = await supabase diff --git a/lib/agent/intents/transaction-categorization.ts b/lib/agent/intents/transaction-categorization.ts index f5f0ae3c..cb01568d 100644 --- a/lib/agent/intents/transaction-categorization.ts +++ b/lib/agent/intents/transaction-categorization.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { SONNET_MODEL, THINKING_BUDGET_STANDARD } from '@/lib/agent/composer/client' +import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client' // transaction.categorization: "Fråga om denna transaktion" on a transaction // row. @@ -78,7 +78,7 @@ export const transactionCategorization = defineAgentIntent< // Reason before proposing: read underlag + history and work out the VAT // treatment in the thinking channel, so the visible reply is one short // motivation, not a play-by-play of each tool call. - thinking: { budgetTokens: THINKING_BUDGET_STANDARD }, + thinking: { effort: EFFORT_STANDARD }, capture: async ({ transaction_id }, { supabase, companyId }) => { const { data: tx } = await supabase diff --git a/lib/agent/intents/types.ts b/lib/agent/intents/types.ts index f805200e..f4cf9245 100644 --- a/lib/agent/intents/types.ts +++ b/lib/agent/intents/types.ts @@ -36,11 +36,13 @@ export interface AgentIntent, Captured = unknown> // override to Opus. model: string - // Extended-thinking budget. When set, run-turn enables a reasoning channel - // (thinking: { type: 'enabled', budget_tokens }) on every model call in the - // loop, so the agent reasons before it answers instead of narrating its - // steps in the visible reply. Omit to disable. budget_tokens must be ≥ 1024. - thinking?: { budgetTokens: number } + // Reasoning depth. When set, run-turn enables adaptive thinking on every + // model call in the loop, so the agent reasons before it answers instead of + // narrating its steps in the visible reply. Omit to disable. + // + // Sonnet 5 rejects the old fixed budget outright, so this is an effort level + // (EFFORT_STANDARD / EFFORT_DEEP), not a token count. + thinking?: { effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' } // Captures the page-context object the prompt template needs. Runs server- // side after the user clicks the button. Failures bubble up to the route. diff --git a/lib/agent/intents/vat-review.ts b/lib/agent/intents/vat-review.ts index 833202e9..bc58a32c 100644 --- a/lib/agent/intents/vat-review.ts +++ b/lib/agent/intents/vat-review.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { OPUS_MODEL, THINKING_BUDGET_DEEP } from '@/lib/agent/composer/client' +import { OPUS_MODEL, EFFORT_DEEP } from '@/lib/agent/composer/client' import { renderAgentGroundRules } from './shared-rules' // vat.review: "Fråga [namn]" from the VAT declaration preview. @@ -62,7 +62,7 @@ export const vatReview = defineAgentIntent({ // Reason over the period figures + Rutor in the thinking channel, so the // visible reply is one conclusion, not a running commentary of each read // followed by a restated summary. Parity with the other reasoning intents. - thinking: { budgetTokens: THINKING_BUDGET_DEEP }, + thinking: { effort: EFFORT_DEEP }, capture: async ({ period_type, year, period }, { supabase, companyId }) => { const { data: settings } = await supabase diff --git a/lib/agent/intents/verifikation-draft.ts b/lib/agent/intents/verifikation-draft.ts index 113c4e63..3db470f3 100644 --- a/lib/agent/intents/verifikation-draft.ts +++ b/lib/agent/intents/verifikation-draft.ts @@ -1,5 +1,5 @@ import { defineAgentIntent } from './types' -import { SONNET_MODEL, THINKING_BUDGET_STANDARD } from '@/lib/agent/composer/client' +import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client' import { renderAgentGroundRules } from './shared-rules' import { resolvePeriodStatusForDate } from '@/lib/core/bookkeeping/period-service' @@ -102,7 +102,7 @@ export const verifikationDraft = defineAgentIntent< // analysis before the tool call and a near-identical answer after it. The // always-on prompt promises "resonemang sker i tankekanalen"; without this // that channel doesn't exist and the reasoning spills into the visible reply. - thinking: { budgetTokens: THINKING_BUDGET_STANDARD }, + thinking: { effort: EFFORT_STANDARD }, capture: async ({ journal_entry_id, description }, { supabase, companyId }) => { let entry: CapturedVerifikationDraft['entry'] = null diff --git a/scripts/swedish-compliance-review.mjs b/scripts/swedish-compliance-review.mjs index 550a29cd..966e25f3 100644 --- a/scripts/swedish-compliance-review.mjs +++ b/scripts/swedish-compliance-review.mjs @@ -11,7 +11,7 @@ import path from 'node:path'; const SKILLS_DIR = '.claude/skills'; const ALWAYS_LOAD = 'swedish-accounting-compliance'; -const MODEL = process.env.REVIEW_MODEL || 'eu.anthropic.claude-sonnet-4-6'; +const MODEL = process.env.REVIEW_MODEL || 'eu.anthropic.claude-sonnet-5'; const MAX_DIFF_CHARS = 180_000; const OUTPUT_FILE = 'review.md'; const COMMENT_MARKER = '';