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