Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
6.2 KiB
JSON
1 line
6.2 KiB
JSON
{"repo": "amakedoni/novasync", "files": {"src/main/services/llm-client.ts": {"82641a7fce6501c7ac55b73b0be50b88e1c4633a": {"t": 1786067732.7813585, "n": 5949, "h": "1bdd84dc65ffeb40", "c": "/**\n * LLM Client with SSE streaming support.\n * Uses AsyncGenerator pattern — clean, testable, framework-agnostic.\n *\n * Supported providers:\n * - DeepSeek (OpenAI-compatible endpoint)\n * - OpenAI (GPT-4o-mini, etc.)\n * - Anthropic (Claude Haiku, etc.)\n */\n\ninterface Message {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface ProviderConfig {\n endpoint: string;\n headers: (apiKey: string) => Record<string, string>;\n body: (model: string, messages: Message[]) => unknown;\n parseChunk: (data: Record<string, unknown>) => string | null;\n}\n\nconst providers: Record<string, ProviderConfig> = {\n deepseek: {\n endpoint: 'https://api.deepseek.com/v1/chat/completions',\n headers: (apiKey) => ({\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n }),\n body: (model, messages) => ({\n model: model || 'deepseek-chat',\n messages,\n stream: true,\n temperature: 0.7,\n max_tokens: 4096,\n }),\n parseChunk: (data: Record<string, unknown>) => {\n // OpenAI-compatible SSE format\n const choices = data.choices as Array<{ delta?: { content?: string } }> | undefined;\n return choices?.[0]?.delta?.content || null;\n },\n },\n\n openai: {\n endpoint: 'https://api.openai.com/v1/chat/completions',\n headers: (apiKey) => ({\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n }),\n body: (model, messages) => ({\n model: model || 'gpt-4o-mini',\n messages,\n stream: true,\n temperature: 0.7,\n max_tokens: 4096,\n }),\n parseChunk: (data: Record<string, unknown>) => {\n const choices = data.choices as Array<{ delta?: { content?: string } }> | undefined;\n return choices?.[0]?.delta?.content || null;\n },\n },\n\n anthropic: {\n endpoint: 'https://api.anthropic.com/v1/messages',\n headers: (apiKey) => ({\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n }),\n body: (_model, messages) => {\n // Extract system message if present\n const systemMsg = messages.find((m) => m.role === 'system');\n const otherMsgs = messages.filter((m) => m.role !== 'system');\n\n return {\n model: 'claude-haiku-4-5',\n max_tokens: 4096,\n system: systemMsg?.content || '',\n messages: otherMsgs.map((m) => ({\n role: m.role,\n content: m.content,\n })),\n stream: true,\n };\n },\n parseChunk: (data: Record<string, unknown>) => {\n // Anthropic SSE format\n if (data.type === 'content_block_delta') {\n const delta = data.delta as { text?: string } | undefined;\n return delta?.text || null;\n }\n return null;\n },\n },\n};\n\n/**\n * Map model name to provider. Uses simple heuristics.\n */\nfunction detectProvider(model: string): string {\n const m = model.toLowerCase();\n if (m.includes('claude') || m.includes('anthropic')) return 'anthropic';\n if (m.includes('gpt') || m.includes('openai')) return 'openai';\n return 'deepseek'; // default\n}\n\n/**\n * Stream chat completion tokens from the LLM.\n * @returns AsyncGenerator yielding content tokens one at a time.\n */\nexport async function* streamChat(\n model: string,\n messages: Message[],\n apiKey: string\n): AsyncGenerator<string> {\n const provider = detectProvider(model);\n const config = providers[provider];\n\n if (!config) {\n throw new Error(`Unknown provider for model: ${model}`);\n }\n\n const response = await fetch(config.endpoint, {\n method: 'POST',\n headers: config.headers(apiKey),\n body: JSON.stringify(config.body(model, messages)),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => 'Unknown error');\n throw new Error(\n `API error (${response.status}): ${errorText.slice(0, 200)}`\n );\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error('No response body — streaming not supported');\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // OpenAI/DeepSeek format: \"data: {...}\"\n if (trimmed.startsWith('data: ')) {\n const data = trimmed.slice(6);\n if (data === '[DONE]') return;\n\n try {\n const parsed = JSON.parse(data);\n const content = config.parseChunk(parsed);\n if (content) yield content;\n } catch {\n // Skip malformed JSON lines\n }\n }\n\n // Anthropic format: \"event: ...\" / \"data: {...}\"\n if (provider === 'anthropic' && trimmed.startsWith('data: ')) {\n const data = trimmed.slice(6);\n try {\n const parsed = JSON.parse(data);\n const content = config.parseChunk(parsed);\n if (content) yield content;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n\n/**\n * Non-streaming chat completion (for simple one-shot queries).\n */\nexport async function chat(\n model: string,\n messages: Message[],\n apiKey: string\n): Promise<string> {\n const provider = detectProvider(model);\n const config = providers[provider];\n\n if (!config) {\n throw new Error(`Unknown provider for model: ${model}`);\n }\n\n const body = config.body(model, messages) as Record<string, unknown>;\n const nonStreamBody = { ...body, stream: false };\n\n const response = await fetch(config.endpoint, {\n method: 'POST',\n headers: config.headers(apiKey),\n body: JSON.stringify(nonStreamBody),\n });\n\n if (!response.ok) {\n throw new Error(`API error (${response.status})`);\n }\n\n const data = await response.json();\n\n // OpenAI-compatible response format\n return data.choices?.[0]?.message?.content || '';\n}\n"}}}} |