llm.service.ts raw
1 const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages'
2 const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models'
3 const ANTHROPIC_VERSION = '2023-06-01'
4 export const DEFAULT_MODEL = 'claude-sonnet-4-20250514'
5 const MAX_TOKENS = 4096
6
7 export type TAnthropicModel = {
8 id: string
9 display_name: string
10 }
11
12 // Cache models per API key to avoid repeated fetches
13 const modelCache = new Map<string, { models: TAnthropicModel[]; timestamp: number }>()
14 const CACHE_TTL = 30 * 60 * 1000 // 30 minutes
15
16 export async function fetchModels(apiKey: string): Promise<TAnthropicModel[]> {
17 const cached = modelCache.get(apiKey)
18 if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
19 return cached.models
20 }
21
22 const allModels: TAnthropicModel[] = []
23 let afterId: string | undefined
24
25 // Paginate through all models
26 do {
27 const url = new URL(ANTHROPIC_MODELS_URL)
28 url.searchParams.set('limit', '100')
29 if (afterId) url.searchParams.set('after_id', afterId)
30
31 const response = await fetch(url.toString(), {
32 headers: {
33 'x-api-key': apiKey,
34 'anthropic-version': ANTHROPIC_VERSION,
35 'anthropic-dangerous-direct-browser-access': 'true'
36 }
37 })
38
39 if (!response.ok) {
40 const errorBody = await response.text()
41 console.error('[LLM] Failed to fetch models:', response.status, errorBody)
42 throw new Error(`Failed to fetch models (${response.status})`)
43 }
44
45 const data = await response.json()
46 const models = (data.data ?? []) as TAnthropicModel[]
47 allModels.push(...models)
48 afterId = data.has_more ? data.last_id : undefined
49 } while (afterId)
50
51 modelCache.set(apiKey, { models: allModels, timestamp: Date.now() })
52 return allModels
53 }
54
55 export async function rewriteText(
56 apiKey: string,
57 systemPrompt: string,
58 noteText: string,
59 model?: string
60 ): Promise<string> {
61 const body = {
62 model: model || DEFAULT_MODEL,
63 max_tokens: MAX_TOKENS,
64 system: systemPrompt + '\n\nIMPORTANT: Output ONLY the rewritten text. Do not include any commentary, explanations, notes, or preamble. Your entire response must be the rewritten text and nothing else.',
65 messages: [
66 {
67 role: 'user',
68 content: noteText
69 }
70 ]
71 }
72 console.debug('[LLM] Sending request:', { model: body.model, systemPrompt: body.system })
73
74 const response = await fetch(ANTHROPIC_API_URL, {
75 method: 'POST',
76 headers: {
77 'x-api-key': apiKey,
78 'anthropic-version': ANTHROPIC_VERSION,
79 'content-type': 'application/json',
80 'anthropic-dangerous-direct-browser-access': 'true'
81 },
82 body: JSON.stringify(body)
83 })
84
85 if (!response.ok) {
86 const errorBody = await response.text()
87 console.error('[LLM] API error:', response.status, errorBody)
88 throw new Error(`Anthropic API error (${response.status}): ${errorBody}`)
89 }
90
91 const data = await response.json()
92 console.debug('[LLM] Response received:', { stopReason: data.stop_reason, model: data.model })
93
94 const textBlock = data.content?.find(
95 (block: { type: string; text?: string }) => block.type === 'text'
96 )
97 if (!textBlock?.text) {
98 throw new Error('No text content in API response')
99 }
100 return textBlock.text
101 }
102