profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

Use adaptive thinking and structured outputs in the Anthropic provider

commit 8b5e278

1 changed file with +8 and −8

modified server/providers/llmAnthropic.ts +8 −8
@@ -1,12 +1,11 @@
1 1 import Anthropic from '@anthropic-ai/sdk'
2 -import { betaZodOutputFormat } from '@anthropic-ai/sdk/helpers/beta/zod'
2 +import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod'
3 3 import { CATEGORY_IDS, InterviewTurnSchema, type InterviewTurn } from '../../shared/types'
4 4 import { weakestCategory } from '../engine/coverage'
5 5 import type { GenerateFileRequest, InterviewContext, InterviewLlm } from './types'
6 6
7 7 const DEFAULT_MODEL = 'claude-opus-4-8'
8 -const INTERVIEW_MAX_TOKENS = 4096
9 -const INTERVIEW_THINKING_BUDGET_TOKENS = 2048
8 +const INTERVIEW_MAX_TOKENS = 16000
10 9 const GENERATE_MAX_TOKENS = 32000
11 10
12 11 export function buildInterviewSystemPrompt(context: InterviewContext): string {
@@ -19,7 +18,7 @@export function buildInterviewSystemPrompt(context: InterviewContext): string {
19 18 ? `The weakest category right now is "${weakest}". Your next question must target it, unless a contradiction takes priority.`
20 19 : 'Every category is already "clear".',
21 20 'If the latest user answer conflicts with an earlier statement, do not ask a coverage question: set "contradiction" to describe both statements, referencing their segment ids, instead.',
22 - 'The interview ends only when the user answer is exactly "done" (case-insensitive, trimmed) — never for any other reason, even if "done" appears inside a longer answer.',
21 + 'The interview ends only when the user answer is exactly "done" (case-insensitive, trimmed), never for any other reason, even if "done" appears inside a longer answer.',
23 22 'Reply with only the InterviewTurn structure described by the output schema.',
24 23 ].join('\n')
25 24 }
@@ -45,13 +44,13 @@export function createLlmAnthropic(): InterviewLlm {
45 44
46 45 return {
47 46 async nextTurn(context: InterviewContext): Promise<InterviewTurn> {
48 - const message = await client.beta.messages.parse({
47 + const message = await client.messages.parse({
49 48 model,
50 49 max_tokens: INTERVIEW_MAX_TOKENS,
51 - thinking: { type: 'enabled', budget_tokens: INTERVIEW_THINKING_BUDGET_TOKENS },
50 + thinking: { type: 'adaptive' },
52 51 system: buildInterviewSystemPrompt(context),
53 52 messages: [{ role: 'user', content: buildInterviewUserMessage(context) }],
54 - output_format: betaZodOutputFormat(InterviewTurnSchema),
53 + output_config: { format: zodOutputFormat(InterviewTurnSchema) },
55 54 })
56 55 if (!message.parsed_output) {
57 56 throw new Error('Anthropic response did not include a parsed InterviewTurn')
@@ -60,9 +59,10 @@export function createLlmAnthropic(): InterviewLlm {
60 59 },
61 60
62 61 async generateFile(request: GenerateFileRequest): Promise<string> {
63 - const stream = client.beta.messages.stream({
62 + const stream = client.messages.stream({
64 63 model,
65 64 max_tokens: GENERATE_MAX_TOKENS,
65 + thinking: { type: 'adaptive' },
66 66 messages: [{ role: 'user', content: request.prompt }],
67 67 })
68 68 const final = await stream.finalMessage()