titan-agent 5.6.1 → 5.6.2

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.
@@ -230,13 +230,33 @@ class AnthropicProvider extends LLMProvider {
230
230
  }
231
231
  }
232
232
  async listModels() {
233
- return [
233
+ const FALLBACK = [
234
234
  "claude-opus-4-0",
235
235
  "claude-sonnet-4-20250514",
236
236
  "claude-haiku-4-20250414",
237
237
  "claude-3-5-sonnet-20241022",
238
238
  "claude-3-5-haiku-20241022"
239
239
  ];
240
+ if (!this.apiKey) return FALLBACK;
241
+ try {
242
+ const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {
243
+ headers: {
244
+ "x-api-key": this.apiKey,
245
+ "anthropic-version": "2023-06-01"
246
+ },
247
+ signal: AbortSignal.timeout(5e3)
248
+ });
249
+ if (!response.ok) {
250
+ logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);
251
+ return FALLBACK;
252
+ }
253
+ const data = await response.json();
254
+ const ids = (data.data || []).map((m) => m.id).filter(Boolean);
255
+ return ids.length > 0 ? ids : FALLBACK;
256
+ } catch (err) {
257
+ logger.debug(COMPONENT, `listModels failed: ${err.message}, using fallback`);
258
+ return FALLBACK;
259
+ }
240
260
  }
241
261
  async healthCheck() {
242
262
  try {
@@ -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 return [\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 }\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 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":[]}
@@ -251,7 +251,25 @@ class GoogleProvider extends LLMProvider {
251
251
  }
252
252
  }
253
253
  async listModels() {
254
- return ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"];
254
+ const FALLBACK = ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"];
255
+ if (!this.apiKey) return FALLBACK;
256
+ try {
257
+ const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models?pageSize=200", {
258
+ headers: { "x-goog-api-key": this.apiKey },
259
+ signal: AbortSignal.timeout(5e3)
260
+ });
261
+ if (!response.ok) {
262
+ logger.debug(COMPONENT, `listModels: ${response.status} from /v1beta/models, using fallback`);
263
+ return FALLBACK;
264
+ }
265
+ const data = await response.json();
266
+ const ids = (data.models || []).filter((m) => (m.supportedGenerationMethods || []).includes("generateContent")).map((m) => m.name.replace(/^models\//, "")).filter((id) => /gemini/i.test(id) && !/embedding|tts|image/i.test(id));
267
+ ids.sort().reverse();
268
+ return ids.length > 0 ? ids : FALLBACK;
269
+ } catch (err) {
270
+ logger.debug(COMPONENT, `listModels failed: ${err.message}, using fallback`);
271
+ return FALLBACK;
272
+ }
255
273
  }
256
274
  async healthCheck() {
257
275
  try {
@@ -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 return ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro'];\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 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":[]}
@@ -222,7 +222,29 @@ class OpenAIProvider extends LLMProvider {
222
222
  yield { type: "done" };
223
223
  }
224
224
  async listModels() {
225
- return ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"];
225
+ const FALLBACK = ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"];
226
+ if (!this.apiKey) return FALLBACK;
227
+ const isChatModel = (id) => {
228
+ if (/^(text-embedding|whisper|tts|dall-e|moderation|davinci-002|babbage-002|computer-use|omni-moderation)/.test(id)) return false;
229
+ return /^(gpt|o1|o3|o4|chatgpt|ft:gpt)/.test(id);
230
+ };
231
+ try {
232
+ const response = await fetch(`${this.baseUrl}/v1/models`, {
233
+ headers: { Authorization: `Bearer ${this.apiKey}` },
234
+ signal: AbortSignal.timeout(5e3)
235
+ });
236
+ if (!response.ok) {
237
+ logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);
238
+ return FALLBACK;
239
+ }
240
+ const data = await response.json();
241
+ const ids = (data.data || []).map((m) => m.id).filter(isChatModel);
242
+ ids.sort().reverse();
243
+ return ids.length > 0 ? ids : FALLBACK;
244
+ } catch (err) {
245
+ logger.debug(COMPONENT, `listModels failed: ${err.message}, using fallback`);
246
+ return FALLBACK;
247
+ }
226
248
  }
227
249
  async healthCheck() {
228
250
  try {
@@ -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 return ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'];\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 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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
- const TITAN_VERSION = "5.6.1";
4
+ const TITAN_VERSION = "5.6.2";
5
5
  const TITAN_CODENAME = "Spacewalk";
6
6
  const TITAN_NAME = "TITAN";
7
7
  const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.6.1';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.6.2';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titan-agent",
3
- "version": "5.6.1",
3
+ "version": "5.6.2",
4
4
  "description": "TITAN — Autonomous AI agent framework with self-improvement, multi-agent orchestration, 36 LLM providers, 16 channel adapters, GPU VRAM management, mesh networking, LiveKit voice, TITAN-Soma homeostatic drives, and a React Mission Control dashboard. Open-source, TypeScript, MIT licensed.",
5
5
  "author": "Tony Elliott (https://github.com/Djtony707)",
6
6
  "repository": {
package/ui/dist/sw.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * but a default falls back to the source-controlled value here.
21
21
  */
22
22
 
23
- const CACHE_NAME = 'titan-' + ('1778459534110');
23
+ const CACHE_NAME = 'titan-' + ('1778460366675');
24
24
  const ASSETS_PREFIX = '/assets/';
25
25
 
26
26
  self.addEventListener('install', (event) => {