titan-agent 5.6.2 → 5.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -231,11 +231,25 @@ class AnthropicProvider extends LLMProvider {
231
231
  }
232
232
  async listModels() {
233
233
  const FALLBACK = [
234
+ // Claude 4.x family (2025-2026)
235
+ "claude-opus-4-7",
236
+ "claude-opus-4-6",
237
+ "claude-opus-4-5",
234
238
  "claude-opus-4-0",
239
+ "claude-sonnet-4-6",
240
+ "claude-sonnet-4-5",
235
241
  "claude-sonnet-4-20250514",
242
+ "claude-haiku-4-6",
243
+ "claude-haiku-4-5-20251001",
236
244
  "claude-haiku-4-20250414",
245
+ // Claude 3.7 / 3.5 / 3.0 (still production-supported)
246
+ "claude-3-7-sonnet-20250219",
237
247
  "claude-3-5-sonnet-20241022",
238
- "claude-3-5-haiku-20241022"
248
+ "claude-3-5-sonnet-20240620",
249
+ "claude-3-5-haiku-20241022",
250
+ "claude-3-opus-20240229",
251
+ "claude-3-sonnet-20240229",
252
+ "claude-3-haiku-20240307"
239
253
  ];
240
254
  if (!this.apiKey) return FALLBACK;
241
255
  try {
@@ -258,6 +272,9 @@ class AnthropicProvider extends LLMProvider {
258
272
  return FALLBACK;
259
273
  }
260
274
  }
275
+ isConfigured() {
276
+ return !!this.apiKey;
277
+ }
261
278
  async healthCheck() {
262
279
  try {
263
280
  if (!this.apiKey) return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `Anthropic API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded fallback used when no key is configured or the live\n // discovery call fails. Keep this in step with the latest stable\n // generation so a fresh-clone TITAN without a key still picks a\n // reasonable default.\n const FALLBACK = [\n 'claude-opus-4-0',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-20250414',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-haiku-20241022',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery — Anthropic's /v1/models is paginated (page size 1000)\n // and returns objects shaped like `{ id, type: 'model', display_name, created_at }`.\n try {\n const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {\n headers: {\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(Boolean);\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/anthropic.ts"],"sourcesContent":["/**\n * TITAN — Anthropic/Claude Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Anthropic';\n\nexport class AnthropicProvider extends LLMProvider {\n readonly name = 'anthropic';\n readonly displayName = 'Anthropic (Claude)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.anthropic;\n return resolveApiKey('anthropic', p.authProfiles || [], p.apiKey || '', 'ANTHROPIC_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.anthropic.baseUrl || 'https://api.anthropic.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Anthropic API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n // TITAN pattern: prompt cache splitting\n // Place cache_control breakpoint on system prompt to cache it across turns.\n // This reduces input costs by ~75% for subsequent messages in the same session.\n body.system = [\n {\n type: 'text',\n text: systemMessage.content,\n cache_control: { type: 'ephemeral' },\n },\n ];\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n // Force at least one tool call on first round when task requires it.\n // Cannot combine tool_choice:any with extended thinking — skip if thinking enabled.\n if (options.forceToolUse && !options.thinking) {\n body.tool_choice = { type: 'any' };\n }\n }\n\n if (options.temperature !== undefined) {\n body.temperature = options.temperature;\n }\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Anthropic API', response, errorText, { provider: 'anthropic', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const content = data.content as Array<Record<string, unknown>> | undefined;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (!content || !Array.isArray(content)) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n for (const block of content) {\n if (block.type === 'text') {\n textContent += block.text as string;\n } else if (block.type === 'tool_use') {\n toolCalls.push({\n id: block.id as string,\n type: 'function',\n function: {\n name: block.name as string,\n arguments: JSON.stringify(block.input),\n },\n });\n }\n }\n\n const usage = data.usage as { input_tokens: number; output_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.input_tokens,\n completionTokens: usage.output_tokens,\n totalTokens: usage.input_tokens + usage.output_tokens,\n }\n : undefined,\n finishReason: (() => {\n const sr = data.stop_reason as string | undefined;\n if (sr === 'max_tokens') return 'length';\n if (sr === 'tool_use') return 'tool_calls';\n return toolCalls.length > 0 ? 'tool_calls' : 'stop';\n })(),\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'claude-sonnet-4-20250514';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Anthropic API key not configured' }; return; }\n\n const systemMessage = options.messages.find((m) => m.role === 'system');\n const nonSystemMessages = options.messages.filter((m) => m.role !== 'system');\n\n const body: Record<string, unknown> = {\n model: model.replace('anthropic/', ''),\n max_tokens: clampMaxTokens(model, options.maxTokens),\n stream: true,\n messages: nonSystemMessages.map((m) => ({\n role: m.role === 'tool' ? 'user' : m.role,\n content: m.role === 'tool'\n ? [{ type: 'tool_result', tool_use_id: m.toolCallId, content: m.content }]\n : m.content,\n })),\n };\n\n if (systemMessage) {\n body.system = [{ type: 'text', text: systemMessage.content, cache_control: { type: 'ephemeral' } }];\n }\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n input_schema: t.function.parameters,\n }));\n }\n if (options.temperature !== undefined) body.temperature = options.temperature;\n\n // Extended thinking support\n if (options.thinking) {\n const budgetMap: Record<string, number> = { low: 1024, medium: 4096, high: 16384 };\n const budgetTokens = budgetMap[options.thinkingLevel || 'medium'] || 4096;\n body.thinking = { type: 'enabled', budget_tokens: budgetTokens };\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'anthropic-beta': 'prompt-caching-2024-07-31',\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `Anthropic API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentToolId = '';\n let currentToolName = '';\n let toolArgsBuffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]' || !json) continue;\n\n try {\n const event = JSON.parse(json);\n if (event.type === 'content_block_delta') {\n const delta = event.delta;\n if (delta.type === 'text_delta' && delta.text) {\n yield { type: 'text', content: delta.text };\n } else if (delta.type === 'input_json_delta' && delta.partial_json) {\n toolArgsBuffer += delta.partial_json;\n }\n } else if (event.type === 'content_block_start') {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n currentToolId = block.id;\n currentToolName = block.name;\n toolArgsBuffer = '';\n }\n } else if (event.type === 'content_block_stop') {\n if (currentToolId) {\n yield {\n type: 'tool_call',\n toolCall: {\n id: currentToolId,\n type: 'function',\n function: { name: currentToolName, arguments: toolArgsBuffer || '{}' },\n },\n };\n currentToolId = '';\n toolArgsBuffer = '';\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or the live discovery call fails. Covers every Claude API model\n // ID currently in production as of 2026-05. Keeps the picker\n // useful even on a fresh-clone TITAN with no key — the user can\n // browse and choose, then configure the key. Live discovery (when\n // a key IS configured) will replace this with whatever the upstream\n // /v1/models returns, which may include models the key doesn't\n // have access to.\n const FALLBACK = [\n // Claude 4.x family (2025-2026)\n 'claude-opus-4-7',\n 'claude-opus-4-6',\n 'claude-opus-4-5',\n 'claude-opus-4-0',\n 'claude-sonnet-4-6',\n 'claude-sonnet-4-5',\n 'claude-sonnet-4-20250514',\n 'claude-haiku-4-6',\n 'claude-haiku-4-5-20251001',\n 'claude-haiku-4-20250414',\n // Claude 3.7 / 3.5 / 3.0 (still production-supported)\n 'claude-3-7-sonnet-20250219',\n 'claude-3-5-sonnet-20241022',\n 'claude-3-5-sonnet-20240620',\n 'claude-3-5-haiku-20241022',\n 'claude-3-opus-20240229',\n 'claude-3-sonnet-20240229',\n 'claude-3-haiku-20240307',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery — Anthropic's /v1/models is paginated (page size 1000)\n // and returns objects shaped like `{ id, type: 'model', display_name, created_at }`.\n try {\n const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {\n headers: {\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(Boolean);\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/messages`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': this.apiKey,\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-haiku-4-20250414',\n max_tokens: 1,\n messages: [{ role: 'user', content: 'ping' }],\n }),\n });\n return response.ok || response.status === 400; // 400 = valid auth but bad request\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -5,6 +5,23 @@ function isClaudeCodeAllowed(options) {
5
5
  return options.allowClaudeCode === true;
6
6
  }
7
7
  class LLMProvider {
8
+ /**
9
+ * Whether this provider has the credentials it needs to make a real
10
+ * request. Returning `false` here causes the router's chat()/chatStream()
11
+ * to fail FAST with a clear "configure key for {provider}" error,
12
+ * BEFORE it would otherwise burn circuit-breaker budget on a request
13
+ * that has zero chance of succeeding.
14
+ *
15
+ * Default implementation: assume the provider doesn't need credentials.
16
+ * Override in providers that do (the cloud ones).
17
+ *
18
+ * Note: this is intentionally synchronous and cheap. It must NOT make
19
+ * network calls. If the provider's credential store is async (very
20
+ * unusual), keep a cached snapshot.
21
+ */
22
+ isConfigured() {
23
+ return true;
24
+ }
8
25
  /** Get the provider identifier from a model string like "anthropic/claude-3" */
9
26
  static parseModelId(modelId) {
10
27
  if (!modelId || !modelId.trim()) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n /**\n * Out-of-band retry status — emitted while the router is retrying a\n * transient failure on the same provider. Stream consumers MUST NOT\n * forward `content` to user-visible output; it's a signal for UI\n * status indicators (spinners, toasts) and structured logs only.\n *\n * Pre-fix (v5.4.x): the router yielded a `text` chunk like\n * \"[Retrying request (1/4) due to rate_limit...]\" which leaked into\n * model responses. The dedicated chunk type isolates that signal.\n */\n type: 'retry';\n /** Which retry this is (1-based). */\n attempt: number;\n /** Configured maximum number of retries before failover. */\n maxRetries: number;\n /** Classified failure reason (e.g. \"rate_limit\", \"server_error\"). */\n reason: string;\n /** Provider being retried. */\n provider: string;\n /** Model being retried. */\n model: string;\n /** Backoff delay in ms before the next attempt. */\n delayMs: number;\n }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AA2FO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAkEO,MAAe,YAAY;AAAA;AAAA,EAiB9B,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/providers/base.ts"],"sourcesContent":["/**\n * TITAN LLM Provider — Base Interface\n * All LLM providers implement this interface for a unified API.\n */\n\n/** A single message in a conversation */\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n name?: string;\n toolCallId?: string;\n toolCalls?: ToolCall[];\n}\n\n/** A tool call requested by the LLM */\nexport interface ToolCall {\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n /**\n * v4.13: Gemini 3 thinking models (via Ollama's Gemini proxy) emit a\n * `thought_signature` on each function_call part. It MUST be carried\n * through to the next turn or Gemini rejects the request with:\n * \"Function call is missing a thought_signature in functionCall parts\"\n * Other providers ignore this field.\n */\n thoughtSignature?: string;\n}\n\n/** A tool definition for the LLM */\nexport interface ToolDefinition {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n };\n}\n\n/** Options for a chat completion request */\nexport interface ChatOptions {\n model?: string;\n messages: ChatMessage[];\n tools?: ToolDefinition[];\n maxTokens?: number;\n temperature?: number;\n stream?: boolean;\n thinking?: boolean;\n thinkingLevel?: 'off' | 'low' | 'medium' | 'high';\n /** Force the model to call a tool on this turn (tool_choice: required/any).\n * Only set to true on the first round when the task clearly requires tool use.\n * Subsequent rounds always use auto (model decides). */\n forceToolUse?: boolean;\n /** Disable all fallback/failover behavior — fail the request if the resolved\n * target model/provider cannot serve it. Used by the empirical model probe,\n * which must hit the exact target model or report a clean failure. Without\n * this, a silent fallback would poison the capabilities registry with data\n * from whichever model happened to answer. */\n noFallback?: boolean;\n /** Ollama-native structured output. Pass a JSON schema to constrain the\n * model's output to match it, or the string 'json' for loose JSON mode.\n * Only the Ollama provider honours this today — other providers ignore it. */\n format?: Record<string, unknown> | 'json';\n /**\n * Provider-specific options bag. Keeps ChatOptions slim while letting\n * individual providers accept flags without bloating the shared type.\n * Each provider documents which keys it reads.\n *\n * Known keys today:\n * - `allowClaudeCode: boolean` — required true for ClaudeCodeProvider\n * to accept a call. All autonomous paths (autopilot, goal driver,\n * specialists, self-mod) leave it unset; Claude Code rejects\n * anything without it. Only user-initiated UI/API chat should set\n * it, after the user explicitly picks a claude-code model.\n */\n providerOptions?: Record<string, unknown>;\n\n /**\n * @deprecated v4.12 — use `providerOptions.allowClaudeCode` instead.\n * Read as a fallback for one release cycle; will be removed in v5.0.\n */\n allowClaudeCode?: boolean;\n}\n\n/**\n * Read the Claude Code opt-in flag from either the new providerOptions\n * bag (preferred) or the deprecated top-level allowClaudeCode field.\n */\nexport function isClaudeCodeAllowed(options: ChatOptions): boolean {\n const po = options.providerOptions;\n if (po && typeof po === 'object' && po.allowClaudeCode === true) return true;\n return options.allowClaudeCode === true;\n}\n\n/** Response from a chat completion */\nexport interface ChatResponse {\n id: string;\n content: string;\n toolCalls?: ToolCall[];\n usage?: {\n promptTokens: number;\n completionTokens: number;\n totalTokens: number;\n };\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n model: string;\n}\n\n/**\n * Streaming chunk from a chat completion.\n *\n * Discriminated union keyed on `type` (v4.12). Consumers switch on `type`\n * and TypeScript narrows the shape — no more optional-everything objects\n * where you have to remember which fields exist for which variant.\n */\nexport type ChatStreamChunk =\n | { type: 'text'; content: string }\n | { type: 'tool_call'; toolCall: ToolCall }\n | { type: 'done' }\n | { type: 'error'; error: string }\n | {\n /**\n * Out-of-band retry status — emitted while the router is retrying a\n * transient failure on the same provider. Stream consumers MUST NOT\n * forward `content` to user-visible output; it's a signal for UI\n * status indicators (spinners, toasts) and structured logs only.\n *\n * Pre-fix (v5.4.x): the router yielded a `text` chunk like\n * \"[Retrying request (1/4) due to rate_limit...]\" which leaked into\n * model responses. The dedicated chunk type isolates that signal.\n */\n type: 'retry';\n /** Which retry this is (1-based). */\n attempt: number;\n /** Configured maximum number of retries before failover. */\n maxRetries: number;\n /** Classified failure reason (e.g. \"rate_limit\", \"server_error\"). */\n reason: string;\n /** Provider being retried. */\n provider: string;\n /** Model being retried. */\n model: string;\n /** Backoff delay in ms before the next attempt. */\n delayMs: number;\n }\n | {\n type: 'failover';\n /** The provider that the request fell over to. */\n content?: string;\n /** The original provider that failed before failover. */\n originalProvider: string;\n /** The original model that failed before failover. */\n originalModel: string;\n /** The error message from the original provider. */\n error?: string;\n };\n\n/** Abstract LLM Provider interface */\nexport abstract class LLMProvider {\n abstract readonly name: string;\n abstract readonly displayName: string;\n\n /** Send a chat completion request */\n abstract chat(options: ChatOptions): Promise<ChatResponse>;\n\n /** Send a streaming chat completion request */\n abstract chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk>;\n\n /** List available models */\n abstract listModels(): Promise<string[]>;\n\n /** Check if the provider is configured and reachable */\n abstract healthCheck(): Promise<boolean>;\n\n /**\n * Whether this provider has the credentials it needs to make a real\n * request. Returning `false` here causes the router's chat()/chatStream()\n * to fail FAST with a clear \"configure key for {provider}\" error,\n * BEFORE it would otherwise burn circuit-breaker budget on a request\n * that has zero chance of succeeding.\n *\n * Default implementation: assume the provider doesn't need credentials.\n * Override in providers that do (the cloud ones).\n *\n * Note: this is intentionally synchronous and cheap. It must NOT make\n * network calls. If the provider's credential store is async (very\n * unusual), keep a cached snapshot.\n */\n isConfigured(): boolean {\n return true;\n }\n\n /** Get the provider identifier from a model string like \"anthropic/claude-3\" */\n static parseModelId(modelId: string): { provider: string; model: string } {\n // E3: Guard against empty/whitespace model IDs\n if (!modelId || !modelId.trim()) {\n return { provider: 'anthropic', model: 'claude-sonnet-4-20250514' };\n }\n const parts = modelId.split('/');\n if (parts.length >= 2 && parts[0] && parts[1]) {\n return { provider: parts[0], model: parts.slice(1).join('/') };\n }\n return { provider: 'anthropic', model: modelId };\n }\n}\n"],"mappings":";AA2FO,SAAS,oBAAoB,SAA+B;AAC/D,QAAM,KAAK,QAAQ;AACnB,MAAI,MAAM,OAAO,OAAO,YAAY,GAAG,oBAAoB,KAAM,QAAO;AACxE,SAAO,QAAQ,oBAAoB;AACvC;AAkEO,MAAe,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8B9B,eAAwB;AACpB,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,OAAO,aAAa,SAAsD;AAEtE,QAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,GAAG;AAC7B,aAAO,EAAE,UAAU,aAAa,OAAO,2BAA2B;AAAA,IACtE;AACA,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,UAAU,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AAC3C,aAAO,EAAE,UAAU,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE;AAAA,IACjE;AACA,WAAO,EAAE,UAAU,aAAa,OAAO,QAAQ;AAAA,EACnD;AACJ;","names":[]}
@@ -251,7 +251,28 @@ class GoogleProvider extends LLMProvider {
251
251
  }
252
252
  }
253
253
  async listModels() {
254
- const FALLBACK = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"];
254
+ const FALLBACK = [
255
+ // Gemini 3.x (2026)
256
+ "gemini-3.1-pro",
257
+ "gemini-3.1-flash",
258
+ "gemini-3.1-flash-lite",
259
+ "gemini-3.0-pro",
260
+ "gemini-3.0-flash",
261
+ // Gemini 2.5 / 2.0
262
+ "gemini-2.5-pro",
263
+ "gemini-2.5-flash",
264
+ "gemini-2.5-flash-lite",
265
+ "gemini-2.0-flash",
266
+ "gemini-2.0-flash-lite",
267
+ "gemini-2.0-flash-thinking-exp",
268
+ "gemini-2.0-pro-exp",
269
+ // Gemini 1.5 (still production-supported)
270
+ "gemini-1.5-pro",
271
+ "gemini-1.5-pro-002",
272
+ "gemini-1.5-flash",
273
+ "gemini-1.5-flash-002",
274
+ "gemini-1.5-flash-8b"
275
+ ];
255
276
  if (!this.apiKey) return FALLBACK;
256
277
  try {
257
278
  const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models?pageSize=200", {
@@ -271,6 +292,9 @@ class GoogleProvider extends LLMProvider {
271
292
  return FALLBACK;
272
293
  }
273
294
  }
295
+ isConfigured() {
296
+ return !!this.apiKey;
297
+ }
274
298
  async healthCheck() {
275
299
  try {
276
300
  if (!this.apiKey) return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/google.ts"],"sourcesContent":["/**\n * TITAN — Google Gemini Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatMessage,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\nimport { mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst COMPONENT = 'Google';\n\n/**\n * When true, every Gemini request body that fails serialization-validation OR\n * gets a non-2xx response is dumped to ~/.titan/debug/gemini-requests/ for\n * post-mortem. Toggled via `GOOGLE_DUMP_REQUEST_BODY=1` env var or the\n * provider's `dumpRequestBody` config flag — keeps it off by default since\n * each dump is a JSON file with full prompt content.\n */\nfunction shouldDumpRequestBody(): boolean {\n if (process.env.GOOGLE_DUMP_REQUEST_BODY === '1' || process.env.GOOGLE_DUMP_REQUEST_BODY === 'true') {\n return true;\n }\n try {\n const cfg = loadConfig();\n const p = (cfg.providers as Record<string, unknown> | undefined)?.google as\n | { dumpRequestBody?: boolean }\n | undefined;\n return Boolean(p?.dumpRequestBody);\n } catch {\n return false;\n }\n}\n\nconst GEMINI_DEBUG_DIR = join(homedir(), '.titan', 'debug', 'gemini-requests');\n\nfunction dumpRequestBody(reason: string, body: unknown, extra?: Record<string, unknown>): void {\n if (!shouldDumpRequestBody()) return;\n try {\n mkdirSync(GEMINI_DEBUG_DIR, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const path = join(GEMINI_DEBUG_DIR, `${stamp}-${reason}.json`);\n writeFileSync(path, JSON.stringify({ reason, body, ...(extra ?? {}) }, null, 2));\n logger.info(COMPONENT, `Dumped Gemini request body → ${path}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to dump Gemini request body: ${(err as Error).message}`);\n }\n}\n\n/**\n * Build the Gemini `contents[]` array from TITAN ChatMessages, with strict\n * pre-serialization validation of `tool` messages.\n *\n * Why this matters:\n * Gemini's `functionResponse` requires a non-empty `name` field paired with\n * a valid `tool_call_id` from a prior assistant turn. If the agent loop\n * ever emits a tool result whose corresponding tool call cannot be located\n * in conversation history, Gemini rejects the whole request with a 400 and\n * the error message is opaque (\"function_response without function_call\").\n *\n * Rather than push the malformed message and let Gemini blow up, we:\n * 1. Build a map of every tool_call.id → name from prior assistant messages.\n * 2. For each `tool` message, ensure (a) the name is non-empty (use the\n * toolCallId map as a backstop) and (b) the toolCallId references a\n * known prior call.\n * 3. Drop or relabel messages that fail validation, with a warning that\n * names the offending message so it shows up in logs.\n * 4. If `dumpRequestBody` is enabled, write the full pre-validation body\n * to disk for inspection before any silent corrections.\n */\nfunction buildContents(messages: ChatMessage[]): { contents: Array<Record<string, unknown>>; corrections: number } {\n // Pass 1: build a lookup of valid tool_call_id → function name from\n // every prior assistant turn that emitted toolCalls.\n const toolCallNameById = new Map<string, string>();\n for (const m of messages) {\n if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {\n for (const tc of m.toolCalls) {\n if (tc.id && tc.function?.name) {\n toolCallNameById.set(tc.id, tc.function.name);\n }\n }\n }\n }\n\n let corrections = 0;\n const contents: Array<Record<string, unknown>> = [];\n\n for (const m of messages.filter((x) => x.role !== 'system')) {\n if (m.role === 'tool') {\n // Validation: name must be non-empty AND toolCallId must reference\n // a known prior call. Either failure → log + best-effort repair.\n const callId = m.toolCallId || '';\n const recordedName = callId ? toolCallNameById.get(callId) : undefined;\n const claimedName = (m.name || '').trim();\n\n if (!recordedName) {\n logger.warn(\n COMPONENT,\n `Malformed tool message: tool_call_id=\"${callId}\" has no matching prior tool_call. ` +\n `name=\"${claimedName}\". Dropping to prevent Gemini 400.`,\n );\n corrections++;\n continue;\n }\n\n const finalName = claimedName || recordedName;\n if (!claimedName) {\n logger.warn(\n COMPONENT,\n `Tool message missing name for tool_call_id=\"${callId}\"; ` +\n `inferred \"${finalName}\" from assistant history.`,\n );\n corrections++;\n } else if (claimedName !== recordedName) {\n logger.warn(\n COMPONENT,\n `Tool message name mismatch for tool_call_id=\"${callId}\": ` +\n `claimed \"${claimedName}\" but tool_call recorded \"${recordedName}\". Using recorded.`,\n );\n corrections++;\n }\n\n contents.push({\n role: 'function' as const,\n parts: [{ functionResponse: { name: recordedName, response: { result: m.content } } }],\n });\n continue;\n }\n\n contents.push({\n role: (m.role === 'assistant' ? 'model' : 'user') as string,\n parts: [{ text: m.content }],\n });\n }\n\n return { contents, corrections };\n}\n\nexport class GoogleProvider extends LLMProvider {\n readonly name = 'google';\n readonly displayName = 'Google (Gemini)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.google;\n return resolveApiKey('google', p.authProfiles || [], p.apiKey || '', 'GOOGLE_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Google API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before sending to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n if (systemInstruction) {\n body.systemInstruction = { parts: [{ text: systemInstruction }] };\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = [{\n functionDeclarations: options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n })),\n }];\n }\n\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-goog-api-key': apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Dump body when the API rejected it so post-mortem has full context\n dumpRequestBody(`http-${response.status}`, body, { errorText, model });\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Google API', response, errorText, { provider: 'google', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const candidates = data.candidates as Array<Record<string, unknown>>;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) {\n textContent += part.text as string;\n }\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fc.name as string,\n arguments: JSON.stringify(fc.args),\n },\n });\n }\n }\n }\n\n const usageMeta = data.usageMetadata as { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined;\n\n return {\n id: uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usageMeta\n ? {\n promptTokens: usageMeta.promptTokenCount || 0,\n completionTokens: usageMeta.candidatesTokenCount || 0,\n totalTokens: usageMeta.totalTokenCount || 0,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `google/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Google API key not configured' }; return; }\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before streaming to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: { maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens), temperature: options.temperature ?? 0.7 },\n };\n if (systemInstruction) body.systemInstruction = { parts: [{ text: systemInstruction }] };\n if (options.tools && options.tools.length > 0) {\n body.tools = [{ functionDeclarations: options.tools.map((t) => ({ name: t.function.name, description: t.function.description, parameters: t.function.parameters })) }];\n }\n\n try {\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });\n yield { type: 'error', error: `Google API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const candidates = chunk.candidates as Array<Record<string, unknown>> | undefined;\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) yield { type: 'text', content: part.text as string };\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fc.name as string, arguments: JSON.stringify(fc.args) } },\n };\n }\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded fallback used when no key is configured or live discovery fails.\n const FALLBACK = ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro'];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1beta/models. Response shape:\n // { models: [{ name: \"models/gemini-2.5-pro\", supportedGenerationMethods: [\"generateContent\", ...] }] }\n // Filter to models that support generateContent (chat) — skip embedding-only,\n // image-gen, and aqa models so the picker stays useful.\n try {\n const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=200', {\n headers: { 'x-goog-api-key': this.apiKey },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1beta/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as {\n models?: Array<{ name: string; supportedGenerationMethods?: string[] }>;\n };\n const ids = (data.models || [])\n .filter((m) => (m.supportedGenerationMethods || []).includes('generateContent'))\n .map((m) => m.name.replace(/^models\\//, ''))\n .filter((id) => /gemini/i.test(id) && !/embedding|tts|image/i.test(id));\n // Sort: newest gemini families first (2.5 > 2.0 > 1.5).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const url = `https://generativelanguage.googleapis.com/v1beta/models`;\n const response = await fetch(url, {\n headers: { 'x-goog-api-key': this.apiKey },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/google.ts"],"sourcesContent":["/**\n * TITAN — Google Gemini Provider\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatMessage,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\nimport { mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst COMPONENT = 'Google';\n\n/**\n * When true, every Gemini request body that fails serialization-validation OR\n * gets a non-2xx response is dumped to ~/.titan/debug/gemini-requests/ for\n * post-mortem. Toggled via `GOOGLE_DUMP_REQUEST_BODY=1` env var or the\n * provider's `dumpRequestBody` config flag — keeps it off by default since\n * each dump is a JSON file with full prompt content.\n */\nfunction shouldDumpRequestBody(): boolean {\n if (process.env.GOOGLE_DUMP_REQUEST_BODY === '1' || process.env.GOOGLE_DUMP_REQUEST_BODY === 'true') {\n return true;\n }\n try {\n const cfg = loadConfig();\n const p = (cfg.providers as Record<string, unknown> | undefined)?.google as\n | { dumpRequestBody?: boolean }\n | undefined;\n return Boolean(p?.dumpRequestBody);\n } catch {\n return false;\n }\n}\n\nconst GEMINI_DEBUG_DIR = join(homedir(), '.titan', 'debug', 'gemini-requests');\n\nfunction dumpRequestBody(reason: string, body: unknown, extra?: Record<string, unknown>): void {\n if (!shouldDumpRequestBody()) return;\n try {\n mkdirSync(GEMINI_DEBUG_DIR, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const path = join(GEMINI_DEBUG_DIR, `${stamp}-${reason}.json`);\n writeFileSync(path, JSON.stringify({ reason, body, ...(extra ?? {}) }, null, 2));\n logger.info(COMPONENT, `Dumped Gemini request body → ${path}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to dump Gemini request body: ${(err as Error).message}`);\n }\n}\n\n/**\n * Build the Gemini `contents[]` array from TITAN ChatMessages, with strict\n * pre-serialization validation of `tool` messages.\n *\n * Why this matters:\n * Gemini's `functionResponse` requires a non-empty `name` field paired with\n * a valid `tool_call_id` from a prior assistant turn. If the agent loop\n * ever emits a tool result whose corresponding tool call cannot be located\n * in conversation history, Gemini rejects the whole request with a 400 and\n * the error message is opaque (\"function_response without function_call\").\n *\n * Rather than push the malformed message and let Gemini blow up, we:\n * 1. Build a map of every tool_call.id → name from prior assistant messages.\n * 2. For each `tool` message, ensure (a) the name is non-empty (use the\n * toolCallId map as a backstop) and (b) the toolCallId references a\n * known prior call.\n * 3. Drop or relabel messages that fail validation, with a warning that\n * names the offending message so it shows up in logs.\n * 4. If `dumpRequestBody` is enabled, write the full pre-validation body\n * to disk for inspection before any silent corrections.\n */\nfunction buildContents(messages: ChatMessage[]): { contents: Array<Record<string, unknown>>; corrections: number } {\n // Pass 1: build a lookup of valid tool_call_id → function name from\n // every prior assistant turn that emitted toolCalls.\n const toolCallNameById = new Map<string, string>();\n for (const m of messages) {\n if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {\n for (const tc of m.toolCalls) {\n if (tc.id && tc.function?.name) {\n toolCallNameById.set(tc.id, tc.function.name);\n }\n }\n }\n }\n\n let corrections = 0;\n const contents: Array<Record<string, unknown>> = [];\n\n for (const m of messages.filter((x) => x.role !== 'system')) {\n if (m.role === 'tool') {\n // Validation: name must be non-empty AND toolCallId must reference\n // a known prior call. Either failure → log + best-effort repair.\n const callId = m.toolCallId || '';\n const recordedName = callId ? toolCallNameById.get(callId) : undefined;\n const claimedName = (m.name || '').trim();\n\n if (!recordedName) {\n logger.warn(\n COMPONENT,\n `Malformed tool message: tool_call_id=\"${callId}\" has no matching prior tool_call. ` +\n `name=\"${claimedName}\". Dropping to prevent Gemini 400.`,\n );\n corrections++;\n continue;\n }\n\n const finalName = claimedName || recordedName;\n if (!claimedName) {\n logger.warn(\n COMPONENT,\n `Tool message missing name for tool_call_id=\"${callId}\"; ` +\n `inferred \"${finalName}\" from assistant history.`,\n );\n corrections++;\n } else if (claimedName !== recordedName) {\n logger.warn(\n COMPONENT,\n `Tool message name mismatch for tool_call_id=\"${callId}\": ` +\n `claimed \"${claimedName}\" but tool_call recorded \"${recordedName}\". Using recorded.`,\n );\n corrections++;\n }\n\n contents.push({\n role: 'function' as const,\n parts: [{ functionResponse: { name: recordedName, response: { result: m.content } } }],\n });\n continue;\n }\n\n contents.push({\n role: (m.role === 'assistant' ? 'model' : 'user') as string,\n parts: [{ text: m.content }],\n });\n }\n\n return { contents, corrections };\n}\n\nexport class GoogleProvider extends LLMProvider {\n readonly name = 'google';\n readonly displayName = 'Google (Gemini)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.google;\n return resolveApiKey('google', p.authProfiles || [], p.apiKey || '', 'GOOGLE_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('Google API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before sending to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n if (systemInstruction) {\n body.systemInstruction = { parts: [{ text: systemInstruction }] };\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = [{\n functionDeclarations: options.tools.map((t) => ({\n name: t.function.name,\n description: t.function.description,\n parameters: t.function.parameters,\n })),\n }];\n }\n\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;\n const response = await fetchWithRetry(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-goog-api-key': apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Dump body when the API rejected it so post-mortem has full context\n dumpRequestBody(`http-${response.status}`, body, { errorText, model });\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Google API', response, errorText, { provider: 'google', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const candidates = data.candidates as Array<Record<string, unknown>>;\n\n let textContent = '';\n const toolCalls: ToolCall[] = [];\n\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) {\n textContent += part.text as string;\n }\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fc.name as string,\n arguments: JSON.stringify(fc.args),\n },\n });\n }\n }\n }\n\n const usageMeta = data.usageMetadata as { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number } | undefined;\n\n return {\n id: uuid(),\n content: textContent,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usageMeta\n ? {\n promptTokens: usageMeta.promptTokenCount || 0,\n completionTokens: usageMeta.candidatesTokenCount || 0,\n totalTokens: usageMeta.totalTokenCount || 0,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `google/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'gemini-2.0-flash').replace('google/', '');\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'Google API key not configured' }; return; }\n\n const systemInstruction = options.messages.find((m) => m.role === 'system')?.content;\n const { contents, corrections } = buildContents(options.messages);\n if (corrections > 0) {\n logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before streaming to Gemini.`);\n }\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: { maxOutputTokens: clampMaxTokens(options.model || 'google/gemini-2.0-flash', options.maxTokens), temperature: options.temperature ?? 0.7 },\n };\n if (systemInstruction) body.systemInstruction = { parts: [{ text: systemInstruction }] };\n if (options.tools && options.tools.length > 0) {\n body.tools = [{ functionDeclarations: options.tools.map((t) => ({ name: t.function.name, description: t.function.description, parameters: t.function.parameters })) }];\n }\n\n try {\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });\n yield { type: 'error', error: `Google API error (${response.status}): ${errorText}` };\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const candidates = chunk.candidates as Array<Record<string, unknown>> | undefined;\n if (candidates && candidates.length > 0) {\n const parts = (candidates[0].content as Record<string, unknown>)?.parts as Array<Record<string, unknown>> || [];\n for (const part of parts) {\n if (part.text) yield { type: 'text', content: part.text as string };\n if (part.functionCall) {\n const fc = part.functionCall as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fc.name as string, arguments: JSON.stringify(fc.args) } },\n };\n }\n }\n }\n } catch { /* skip malformed SSE lines */ }\n }\n }\n yield { type: 'done' };\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or live discovery fails. Covers Gemini families currently in\n // production as of 2026-05.\n const FALLBACK = [\n // Gemini 3.x (2026)\n 'gemini-3.1-pro',\n 'gemini-3.1-flash',\n 'gemini-3.1-flash-lite',\n 'gemini-3.0-pro',\n 'gemini-3.0-flash',\n // Gemini 2.5 / 2.0\n 'gemini-2.5-pro',\n 'gemini-2.5-flash',\n 'gemini-2.5-flash-lite',\n 'gemini-2.0-flash',\n 'gemini-2.0-flash-lite',\n 'gemini-2.0-flash-thinking-exp',\n 'gemini-2.0-pro-exp',\n // Gemini 1.5 (still production-supported)\n 'gemini-1.5-pro',\n 'gemini-1.5-pro-002',\n 'gemini-1.5-flash',\n 'gemini-1.5-flash-002',\n 'gemini-1.5-flash-8b',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1beta/models. Response shape:\n // { models: [{ name: \"models/gemini-2.5-pro\", supportedGenerationMethods: [\"generateContent\", ...] }] }\n // Filter to models that support generateContent (chat) — skip embedding-only,\n // image-gen, and aqa models so the picker stays useful.\n try {\n const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models?pageSize=200', {\n headers: { 'x-goog-api-key': this.apiKey },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1beta/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as {\n models?: Array<{ name: string; supportedGenerationMethods?: string[] }>;\n };\n const ids = (data.models || [])\n .filter((m) => (m.supportedGenerationMethods || []).includes('generateContent'))\n .map((m) => m.name.replace(/^models\\//, ''))\n .filter((id) => /gemini/i.test(id) && !/embedding|tts|image/i.test(id));\n // Sort: newest gemini families first (2.5 > 2.0 > 1.5).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const url = `https://generativelanguage.googleapis.com/v1beta/models`;\n const response = await fetch(url, {\n headers: { 'x-goog-api-key': this.apiKey },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -222,7 +222,34 @@ class OpenAIProvider extends LLMProvider {
222
222
  yield { type: "done" };
223
223
  }
224
224
  async listModels() {
225
- const FALLBACK = ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"];
225
+ const FALLBACK = [
226
+ // GPT-5 family (2026)
227
+ "gpt-5",
228
+ "gpt-5-mini",
229
+ "gpt-5-nano",
230
+ // GPT-4.5 / 4o family
231
+ "gpt-4.5-preview",
232
+ "gpt-4o",
233
+ "gpt-4o-2024-11-20",
234
+ "gpt-4o-2024-08-06",
235
+ "gpt-4o-mini",
236
+ "gpt-4o-mini-2024-07-18",
237
+ "gpt-4-turbo",
238
+ "gpt-4-turbo-2024-04-09",
239
+ "gpt-4",
240
+ "chatgpt-4o-latest",
241
+ // o-series reasoning models
242
+ "o4-mini",
243
+ "o3",
244
+ "o3-mini",
245
+ "o3-pro",
246
+ "o1",
247
+ "o1-pro",
248
+ "o1-mini",
249
+ "o1-preview",
250
+ // Legacy still in production
251
+ "gpt-3.5-turbo"
252
+ ];
226
253
  if (!this.apiKey) return FALLBACK;
227
254
  const isChatModel = (id) => {
228
255
  if (/^(text-embedding|whisper|tts|dall-e|moderation|davinci-002|babbage-002|computer-use|omni-moderation)/.test(id)) return false;
@@ -246,6 +273,9 @@ class OpenAIProvider extends LLMProvider {
246
273
  return FALLBACK;
247
274
  }
248
275
  }
276
+ isConfigured() {
277
+ return !!this.apiKey;
278
+ }
249
279
  async healthCheck() {
250
280
  try {
251
281
  if (!this.apiKey) return false;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/openai.ts"],"sourcesContent":["/**\n * TITAN — OpenAI Provider (GPT-4, o-series)\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'OpenAI';\n\nexport class OpenAIProvider extends LLMProvider {\n readonly name = 'openai';\n readonly displayName = 'OpenAI (GPT)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.openai;\n return resolveApiKey('openai', p.authProfiles || [], p.apiKey || '', 'OPENAI_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.openai.baseUrl || 'https://api.openai.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'gpt-4o';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('OpenAI API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const cleanModel = model.replace('openai/', '');\n const isReasoningModel = /^(o1|o3|o4)/.test(cleanModel);\n\n const body: Record<string, unknown> = {\n model: cleanModel,\n messages: options.messages.map((m) => {\n if (m.role === 'tool') {\n return { role: 'tool', content: m.content, tool_call_id: m.toolCallId };\n }\n if (m.role === 'assistant' && m.toolCalls) {\n return {\n role: 'assistant',\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: 'function',\n function: { name: tc.function.name, arguments: tc.function.arguments },\n })),\n };\n }\n // o-series reasoning models use 'developer' role instead of 'system'\n if (m.role === 'system' && isReasoningModel) {\n return { role: 'developer', content: m.content };\n }\n return { role: m.role, content: m.content };\n }),\n };\n\n // o-series models require max_completion_tokens, not max_tokens\n if (isReasoningModel) {\n body.max_completion_tokens = clampMaxTokens(model, options.maxTokens);\n } else {\n body.max_tokens = clampMaxTokens(model, options.maxTokens);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools;\n // Force at least one tool call on first round when task requires it.\n // Use \"auto\" for o-series (they manage tool use internally via reasoning).\n if (options.forceToolUse && !isReasoningModel) {\n body.tool_choice = 'required';\n }\n }\n\n // o-series models reject the temperature parameter\n if (options.temperature !== undefined && !isReasoningModel) {\n body.temperature = options.temperature;\n }\n\n // Reasoning effort for o-series models\n if (options.thinking && isReasoningModel) {\n const effortMap: Record<string, string> = { low: 'low', medium: 'medium', high: 'high' };\n body.reasoning_effort = effortMap[options.thinkingLevel || 'medium'] || 'medium';\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('OpenAI API', response, errorText, { provider: 'openai', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const choices = data.choices as Array<Record<string, unknown>> | undefined;\n\n if (!choices || choices.length === 0) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n const choice = choices[0];\n const message = choice.message as Record<string, unknown>;\n\n const toolCalls: ToolCall[] = [];\n if (message.tool_calls) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, string>;\n toolCalls.push({\n id: tc.id as string,\n type: 'function',\n function: { name: fn.name, arguments: fn.arguments },\n });\n }\n }\n\n const usage = data.usage as { prompt_tokens: number; completion_tokens: number; total_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: (message.content as string) || '',\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.prompt_tokens,\n completionTokens: usage.completion_tokens,\n totalTokens: usage.total_tokens,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : (choice.finish_reason as 'stop' | 'length') || 'stop',\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'gpt-4o';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'OpenAI API key not configured' }; return; }\n\n const cleanModel = model.replace('openai/', '');\n const isReasoningModel = /^(o1|o3|o4)/.test(cleanModel);\n\n const body: Record<string, unknown> = {\n model: cleanModel,\n stream: true,\n messages: options.messages.map((m) => {\n if (m.role === 'tool') return { role: 'tool', content: m.content, tool_call_id: m.toolCallId };\n if (m.role === 'assistant' && m.toolCalls) {\n return {\n role: 'assistant', content: m.content || null,\n tool_calls: m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } })),\n };\n }\n if (m.role === 'system' && isReasoningModel) return { role: 'developer', content: m.content };\n return { role: m.role, content: m.content };\n }),\n };\n\n if (isReasoningModel) { body.max_completion_tokens = options.maxTokens || 8192; }\n else { body.max_tokens = clampMaxTokens(model, options.maxTokens); }\n if (options.tools && options.tools.length > 0) body.tools = options.tools;\n if (options.temperature !== undefined && !isReasoningModel) body.temperature = options.temperature;\n\n // Reasoning effort for o-series models\n if (options.thinking && isReasoningModel) {\n const effortMap: Record<string, string> = { low: 'low', medium: 'medium', high: 'high' };\n body.reasoning_effort = effortMap[options.thinkingLevel || 'medium'] || 'medium';\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `OpenAI API error (${response.status}): ${errorText}` };\n return;\n }\n\n const toolCalls = new Map<number, { id: string; name: string; args: string }>();\n yield* this.parseOpenAISSE(response.body, toolCalls);\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n /** Parse OpenAI-format SSE stream and yield ChatStreamChunks */\n private async *parseOpenAISSE(\n body: ReadableStream<Uint8Array>,\n toolCalls: Map<number, { id: string; name: string; args: string }>,\n ): AsyncGenerator<ChatStreamChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') { break; }\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const delta = chunk.choices?.[0]?.delta;\n if (!delta) continue;\n\n if (delta.content) {\n yield { type: 'text', content: delta.content };\n }\n if (delta.tool_calls) {\n for (const tc of delta.tool_calls) {\n const idx = tc.index ?? 0;\n if (!toolCalls.has(idx)) {\n toolCalls.set(idx, { id: tc.id || '', name: '', args: '' });\n }\n const entry = toolCalls.get(idx)!;\n if (tc.id) entry.id = tc.id;\n if (tc.function?.name) entry.name = tc.function.name;\n if (tc.function?.arguments) entry.args += tc.function.arguments;\n }\n }\n } catch { /* skip malformed lines */ }\n }\n }\n\n // Emit accumulated tool calls\n for (const [, tc] of toolCalls) {\n if (tc.id && tc.name) {\n yield { type: 'tool_call', toolCall: { id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args || '{}' } } };\n }\n }\n yield { type: 'done' };\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded fallback used when no key is configured or live discovery fails.\n const FALLBACK = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1/models. The response includes embeddings,\n // image-gen, audio, moderation, etc. — filter to chat-capable\n // generation models so the picker doesn't drown in noise.\n const isChatModel = (id: string): boolean => {\n if (/^(text-embedding|whisper|tts|dall-e|moderation|davinci-002|babbage-002|computer-use|omni-moderation)/.test(id)) return false;\n return /^(gpt|o1|o3|o4|chatgpt|ft:gpt)/.test(id);\n };\n try {\n const response = await fetch(`${this.baseUrl}/v1/models`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(isChatModel);\n // Sort newest first by name (alphabetical desc tends to bubble new generations up).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/models`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/openai.ts"],"sourcesContent":["/**\n * TITAN — OpenAI Provider (GPT-4, o-series)\n */\nimport {\n LLMProvider,\n type ChatOptions,\n type ChatResponse,\n type ChatStreamChunk,\n type ToolCall,\n} from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { fetchWithRetry } from '../utils/helpers.js';\nimport { resolveApiKey } from './authResolver.js';\nimport { v4 as uuid } from 'uuid';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'OpenAI';\n\nexport class OpenAIProvider extends LLMProvider {\n readonly name = 'openai';\n readonly displayName = 'OpenAI (GPT)';\n\n private get apiKey(): string {\n const config = loadConfig();\n const p = config.providers.openai;\n return resolveApiKey('openai', p.authProfiles || [], p.apiKey || '', 'OPENAI_API_KEY', p.rotationStrategy, p.credentialCooldownMs);\n }\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.openai.baseUrl || 'https://api.openai.com';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = options.model || 'gpt-4o';\n const apiKey = this.apiKey;\n if (!apiKey) throw new Error('OpenAI API key not configured');\n\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n const cleanModel = model.replace('openai/', '');\n const isReasoningModel = /^(o1|o3|o4)/.test(cleanModel);\n\n const body: Record<string, unknown> = {\n model: cleanModel,\n messages: options.messages.map((m) => {\n if (m.role === 'tool') {\n return { role: 'tool', content: m.content, tool_call_id: m.toolCallId };\n }\n if (m.role === 'assistant' && m.toolCalls) {\n return {\n role: 'assistant',\n content: m.content || null,\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: 'function',\n function: { name: tc.function.name, arguments: tc.function.arguments },\n })),\n };\n }\n // o-series reasoning models use 'developer' role instead of 'system'\n if (m.role === 'system' && isReasoningModel) {\n return { role: 'developer', content: m.content };\n }\n return { role: m.role, content: m.content };\n }),\n };\n\n // o-series models require max_completion_tokens, not max_tokens\n if (isReasoningModel) {\n body.max_completion_tokens = clampMaxTokens(model, options.maxTokens);\n } else {\n body.max_tokens = clampMaxTokens(model, options.maxTokens);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools;\n // Force at least one tool call on first round when task requires it.\n // Use \"auto\" for o-series (they manage tool use internally via reasoning).\n if (options.forceToolUse && !isReasoningModel) {\n body.tool_choice = 'required';\n }\n }\n\n // o-series models reject the temperature parameter\n if (options.temperature !== undefined && !isReasoningModel) {\n body.temperature = options.temperature;\n }\n\n // Reasoning effort for o-series models\n if (options.thinking && isReasoningModel) {\n const effortMap: Record<string, string> = { low: 'low', medium: 'medium', high: 'high' };\n body.reasoning_effort = effortMap[options.thinkingLevel || 'medium'] || 'medium';\n }\n\n const response = await fetchWithRetry(`${this.baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Hunt Finding #37: attach status + Retry-After so the router can respect backoff\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('OpenAI API', response, errorText, { provider: 'openai', model });\n }\n\n const data = await response.json() as Record<string, unknown>;\n const choices = data.choices as Array<Record<string, unknown>> | undefined;\n\n if (!choices || choices.length === 0) {\n return {\n id: (data.id as string) || uuid(),\n content: '',\n usage: undefined,\n finishReason: 'stop',\n model,\n };\n }\n\n const choice = choices[0];\n const message = choice.message as Record<string, unknown>;\n\n const toolCalls: ToolCall[] = [];\n if (message.tool_calls) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, string>;\n toolCalls.push({\n id: tc.id as string,\n type: 'function',\n function: { name: fn.name, arguments: fn.arguments },\n });\n }\n }\n\n const usage = data.usage as { prompt_tokens: number; completion_tokens: number; total_tokens: number } | undefined;\n\n return {\n id: (data.id as string) || uuid(),\n content: (message.content as string) || '',\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: usage\n ? {\n promptTokens: usage.prompt_tokens,\n completionTokens: usage.completion_tokens,\n totalTokens: usage.total_tokens,\n }\n : undefined,\n finishReason: toolCalls.length > 0 ? 'tool_calls' : (choice.finish_reason as 'stop' | 'length') || 'stop',\n model,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = options.model || 'gpt-4o';\n const apiKey = this.apiKey;\n if (!apiKey) { yield { type: 'error', error: 'OpenAI API key not configured' }; return; }\n\n const cleanModel = model.replace('openai/', '');\n const isReasoningModel = /^(o1|o3|o4)/.test(cleanModel);\n\n const body: Record<string, unknown> = {\n model: cleanModel,\n stream: true,\n messages: options.messages.map((m) => {\n if (m.role === 'tool') return { role: 'tool', content: m.content, tool_call_id: m.toolCallId };\n if (m.role === 'assistant' && m.toolCalls) {\n return {\n role: 'assistant', content: m.content || null,\n tool_calls: m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments } })),\n };\n }\n if (m.role === 'system' && isReasoningModel) return { role: 'developer', content: m.content };\n return { role: m.role, content: m.content };\n }),\n };\n\n if (isReasoningModel) { body.max_completion_tokens = options.maxTokens || 8192; }\n else { body.max_tokens = clampMaxTokens(model, options.maxTokens); }\n if (options.tools && options.tools.length > 0) body.tools = options.tools;\n if (options.temperature !== undefined && !isReasoningModel) body.temperature = options.temperature;\n\n // Reasoning effort for o-series models\n if (options.thinking && isReasoningModel) {\n const effortMap: Record<string, string> = { low: 'low', medium: 'medium', high: 'high' };\n body.reasoning_effort = effortMap[options.thinkingLevel || 'medium'] || 'medium';\n }\n\n try {\n const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },\n body: JSON.stringify(body),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n yield { type: 'error', error: `OpenAI API error (${response.status}): ${errorText}` };\n return;\n }\n\n const toolCalls = new Map<number, { id: string; name: string; args: string }>();\n yield* this.parseOpenAISSE(response.body, toolCalls);\n } catch (error) {\n yield { type: 'error', error: (error as Error).message };\n }\n }\n\n /** Parse OpenAI-format SSE stream and yield ChatStreamChunks */\n private async *parseOpenAISSE(\n body: ReadableStream<Uint8Array>,\n toolCalls: Map<number, { id: string; name: string; args: string }>,\n ): AsyncGenerator<ChatStreamChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') { break; }\n if (!json) continue;\n\n try {\n const chunk = JSON.parse(json);\n const delta = chunk.choices?.[0]?.delta;\n if (!delta) continue;\n\n if (delta.content) {\n yield { type: 'text', content: delta.content };\n }\n if (delta.tool_calls) {\n for (const tc of delta.tool_calls) {\n const idx = tc.index ?? 0;\n if (!toolCalls.has(idx)) {\n toolCalls.set(idx, { id: tc.id || '', name: '', args: '' });\n }\n const entry = toolCalls.get(idx)!;\n if (tc.id) entry.id = tc.id;\n if (tc.function?.name) entry.name = tc.function.name;\n if (tc.function?.arguments) entry.args += tc.function.arguments;\n }\n }\n } catch { /* skip malformed lines */ }\n }\n }\n\n // Emit accumulated tool calls\n for (const [, tc] of toolCalls) {\n if (tc.id && tc.name) {\n yield { type: 'tool_call', toolCall: { id: tc.id, type: 'function', function: { name: tc.name, arguments: tc.args || '{}' } } };\n }\n }\n yield { type: 'done' };\n }\n\n async listModels(): Promise<string[]> {\n // Hardcoded comprehensive fallback used when no key is configured\n // or live discovery fails. Covers OpenAI's chat-capable production\n // models as of 2026-05. Live discovery (when a key IS set) replaces\n // this with the user's actual catalogue.\n const FALLBACK = [\n // GPT-5 family (2026)\n 'gpt-5',\n 'gpt-5-mini',\n 'gpt-5-nano',\n // GPT-4.5 / 4o family\n 'gpt-4.5-preview',\n 'gpt-4o',\n 'gpt-4o-2024-11-20',\n 'gpt-4o-2024-08-06',\n 'gpt-4o-mini',\n 'gpt-4o-mini-2024-07-18',\n 'gpt-4-turbo',\n 'gpt-4-turbo-2024-04-09',\n 'gpt-4',\n 'chatgpt-4o-latest',\n // o-series reasoning models\n 'o4-mini',\n 'o3',\n 'o3-mini',\n 'o3-pro',\n 'o1',\n 'o1-pro',\n 'o1-mini',\n 'o1-preview',\n // Legacy still in production\n 'gpt-3.5-turbo',\n ];\n if (!this.apiKey) return FALLBACK;\n\n // Live discovery via /v1/models. The response includes embeddings,\n // image-gen, audio, moderation, etc. — filter to chat-capable\n // generation models so the picker doesn't drown in noise.\n const isChatModel = (id: string): boolean => {\n if (/^(text-embedding|whisper|tts|dall-e|moderation|davinci-002|babbage-002|computer-use|omni-moderation)/.test(id)) return false;\n return /^(gpt|o1|o3|o4|chatgpt|ft:gpt)/.test(id);\n };\n try {\n const response = await fetch(`${this.baseUrl}/v1/models`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) {\n logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);\n return FALLBACK;\n }\n const data = await response.json() as { data?: Array<{ id: string }> };\n const ids = (data.data || []).map((m) => m.id).filter(isChatModel);\n // Sort newest first by name (alphabetical desc tends to bubble new generations up).\n ids.sort().reverse();\n return ids.length > 0 ? ids : FALLBACK;\n } catch (err) {\n logger.debug(COMPONENT, `listModels failed: ${(err as Error).message}, using fallback`);\n return FALLBACK;\n }\n }\n\n isConfigured(): boolean {\n return !!this.apiKey;\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n if (!this.apiKey) return false;\n const response = await fetch(`${this.baseUrl}/v1/models`, {\n headers: { Authorization: `Bearer ${this.apiKey}` },\n });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -210,24 +210,34 @@ class OpenAICompatProvider extends LLMProvider {
210
210
  }
211
211
  }
212
212
  async listModels() {
213
- if (!this.config.supportsModelList || !this.apiKey) {
213
+ if (!this.config.supportsModelList) {
214
+ return this.config.knownModels;
215
+ }
216
+ const isPublic = this.config.publicModelList === true;
217
+ if (!isPublic && !this.apiKey) {
214
218
  return this.config.knownModels;
215
219
  }
216
220
  try {
221
+ const headers = {
222
+ ...this.config.extraHeaders || {}
223
+ };
224
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
217
225
  const response = await fetch(`${this.baseUrl}/models`, {
218
- headers: {
219
- "Authorization": `Bearer ${this.apiKey}`,
220
- ...this.config.extraHeaders || {}
221
- },
222
- signal: AbortSignal.timeout(5e3)
226
+ headers,
227
+ signal: AbortSignal.timeout(8e3)
223
228
  });
224
229
  if (!response.ok) return this.config.knownModels;
225
230
  const data = await response.json();
226
- return (data.data || []).map((m) => m.id);
231
+ const ids = (data.data || []).map((m) => m.id).filter(Boolean);
232
+ ids.sort();
233
+ return ids.length > 0 ? ids : this.config.knownModels;
227
234
  } catch {
228
235
  return this.config.knownModels;
229
236
  }
230
237
  }
238
+ isConfigured() {
239
+ return !!this.apiKey;
240
+ }
231
241
  async healthCheck() {
232
242
  try {
233
243
  if (!this.apiKey) return false;
@@ -559,6 +569,11 @@ const PROVIDER_PRESETS = [
559
569
  "nvidia/llama-3.1-nemotron-70b-instruct"
560
570
  ],
561
571
  supportsModelList: true,
572
+ // OpenRouter's /api/v1/models is publicly accessible — TITAN can
573
+ // pull the full ~365-model catalogue even before the user has
574
+ // configured an OPENROUTER_API_KEY. Lets the picker show every
575
+ // available model so the user can choose first, then add the key.
576
+ publicModelList: true,
562
577
  extraHeaders: {
563
578
  "HTTP-Referer": "https://titan.local",
564
579
  "X-Title": "TITAN"