titan-agent 5.3.2 → 5.4.1

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.
Files changed (48) hide show
  1. package/dist/agent/agent.js +11 -1
  2. package/dist/agent/agent.js.map +1 -1
  3. package/dist/agent/agentLoop.js +36 -1
  4. package/dist/agent/agentLoop.js.map +1 -1
  5. package/dist/agent/session.js +106 -5
  6. package/dist/agent/session.js.map +1 -1
  7. package/dist/agent/subAgent.js +62 -1
  8. package/dist/agent/subAgent.js.map +1 -1
  9. package/dist/config/config.js +30 -8
  10. package/dist/config/config.js.map +1 -1
  11. package/dist/config/schema.js +25 -2
  12. package/dist/config/schema.js.map +1 -1
  13. package/dist/gateway/server.js +32 -1
  14. package/dist/gateway/server.js.map +1 -1
  15. package/dist/memory/graph.js +49 -15
  16. package/dist/memory/graph.js.map +1 -1
  17. package/dist/memory/index.js +192 -0
  18. package/dist/memory/index.js.map +1 -0
  19. package/dist/memory/memory.js +1 -0
  20. package/dist/memory/memory.js.map +1 -1
  21. package/dist/mesh/transport.js +60 -8
  22. package/dist/mesh/transport.js.map +1 -1
  23. package/dist/providers/anthropic.js +3 -2
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js.map +1 -1
  26. package/dist/providers/google.js +94 -20
  27. package/dist/providers/google.js.map +1 -1
  28. package/dist/providers/modelCapabilities.js +59 -0
  29. package/dist/providers/modelCapabilities.js.map +1 -0
  30. package/dist/providers/ollama.js +3 -2
  31. package/dist/providers/ollama.js.map +1 -1
  32. package/dist/providers/openai.js +4 -3
  33. package/dist/providers/openai.js.map +1 -1
  34. package/dist/providers/openai_compat.js +3 -2
  35. package/dist/providers/openai_compat.js.map +1 -1
  36. package/dist/providers/router.js +63 -21
  37. package/dist/providers/router.js.map +1 -1
  38. package/dist/safety/fabricationGuard.js +140 -0
  39. package/dist/safety/fabricationGuard.js.map +1 -0
  40. package/dist/skills/builtin/gepa.js +23 -1
  41. package/dist/skills/builtin/gepa.js.map +1 -1
  42. package/dist/skills/builtin/model_trainer.js +31 -4
  43. package/dist/skills/builtin/model_trainer.js.map +1 -1
  44. package/dist/skills/builtin/self_improve.js +50 -2
  45. package/dist/skills/builtin/self_improve.js.map +1 -1
  46. package/dist/utils/constants.js +2 -2
  47. package/dist/utils/constants.js.map +1 -1
  48. package/package.json +1 -1
@@ -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 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';\n\nconst COMPONENT = 'Google';\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 = options.messages\n .filter((m) => m.role !== 'system')\n .map((m) => {\n if (m.role === 'tool') {\n return {\n role: 'function' as const,\n parts: [{ functionResponse: { name: m.name || 'tool', response: { result: m.content } } }],\n };\n }\n return {\n role: (m.role === 'assistant' ? 'model' : 'user') as string,\n parts: [{ text: m.content }],\n };\n });\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: {\n maxOutputTokens: options.maxTokens || 8192,\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 // 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 = options.messages.filter((m) => m.role !== 'system').map((m) => {\n if (m.role === 'tool') {\n return { role: 'function' as const, parts: [{ functionResponse: { name: m.name || 'tool', response: { result: m.content } } }] };\n }\n return { role: (m.role === 'assistant' ? 'model' : 'user') as string, parts: [{ text: m.content }] };\n });\n\n const body: Record<string, unknown> = {\n contents,\n generationConfig: { maxOutputTokens: options.maxTokens || 8192, 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 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 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":[]}
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from "../config/config.js";
3
+ const STATIC_TABLE = {
4
+ "anthropic/claude-sonnet-4-20250514": { contextWindow: 2e5, maxOutput: 64e3, supportsThinking: true },
5
+ "anthropic/claude-opus-4": { contextWindow: 2e5, maxOutput: 32e3, supportsThinking: true },
6
+ "anthropic/claude-3-5-sonnet-20241022": { contextWindow: 2e5, maxOutput: 8192 },
7
+ "anthropic/claude-3-5-haiku-20241022": { contextWindow: 2e5, maxOutput: 8192 },
8
+ "openai/gpt-4o": { contextWindow: 128e3, maxOutput: 16384 },
9
+ "openai/gpt-4o-mini": { contextWindow: 128e3, maxOutput: 16384 },
10
+ "openai/gpt-4.1": { contextWindow: 1e6, maxOutput: 32768 },
11
+ "openai/o1": { contextWindow: 2e5, maxOutput: 1e5, supportsThinking: true },
12
+ "openai/o1-mini": { contextWindow: 128e3, maxOutput: 65536, supportsThinking: true },
13
+ "openai/o3-mini": { contextWindow: 2e5, maxOutput: 1e5, supportsThinking: true },
14
+ "kimi/kimi-k2.6": { contextWindow: 262144, maxOutput: 128e3, supportsThinking: true },
15
+ "moonshot/kimi-k2-0905-preview": { contextWindow: 262144, maxOutput: 128e3 },
16
+ "zhipu/glm-5.1": { contextWindow: 198e3, maxOutput: 128e3 },
17
+ "ollama/qwen3.5:cloud": { contextWindow: 128e3, maxOutput: 32e3 },
18
+ "ollama/qwen3:32b": { contextWindow: 32768, maxOutput: 8192 },
19
+ "ollama/llama3.3:70b": { contextWindow: 128e3, maxOutput: 8192 },
20
+ "ollama/deepseek-v3": { contextWindow: 128e3, maxOutput: 16384 },
21
+ "mistral/mistral-large": { contextWindow: 128e3, maxOutput: 8192 },
22
+ "gemini/gemini-2.0-flash": { contextWindow: 1e6, maxOutput: 8192 },
23
+ "gemini/gemini-2.5-pro": { contextWindow: 2e6, maxOutput: 65536, supportsThinking: true },
24
+ "cohere/command-r-plus": { contextWindow: 128e3, maxOutput: 4096 },
25
+ "groq/llama-3.3-70b-versatile": { contextWindow: 128e3, maxOutput: 32768 }
26
+ };
27
+ const FAMILY_DEFAULTS = [
28
+ { pattern: /^anthropic\//, caps: { contextWindow: 2e5, maxOutput: 8192 } },
29
+ { pattern: /^openai\/o\d/, caps: { contextWindow: 2e5, maxOutput: 1e5, supportsThinking: true } },
30
+ { pattern: /^openai\//, caps: { contextWindow: 128e3, maxOutput: 16384 } },
31
+ { pattern: /^kimi\/|^moonshot\//, caps: { contextWindow: 262144, maxOutput: 128e3 } },
32
+ { pattern: /^zhipu\/|^glm/, caps: { contextWindow: 198e3, maxOutput: 128e3 } },
33
+ { pattern: /^gemini\//, caps: { contextWindow: 1e6, maxOutput: 8192 } },
34
+ { pattern: /^ollama\//, caps: { contextWindow: 32e3, maxOutput: 8192 } }
35
+ ];
36
+ const DEFAULT_FALLBACK = { contextWindow: 32e3, maxOutput: 8192 };
37
+ function getModelCapabilities(model) {
38
+ if (STATIC_TABLE[model]) return STATIC_TABLE[model];
39
+ try {
40
+ const cfg = loadConfig();
41
+ const override = cfg.providers?.modelCapabilities;
42
+ if (override?.[model]) return override[model];
43
+ } catch {
44
+ }
45
+ for (const f of FAMILY_DEFAULTS) {
46
+ if (f.pattern.test(model)) return f.caps;
47
+ }
48
+ return DEFAULT_FALLBACK;
49
+ }
50
+ function clampMaxTokens(model, requested) {
51
+ const caps = getModelCapabilities(model);
52
+ const want = requested ?? caps.maxOutput;
53
+ return Math.max(1, Math.min(want, caps.maxOutput));
54
+ }
55
+ export {
56
+ clampMaxTokens,
57
+ getModelCapabilities
58
+ };
59
+ //# sourceMappingURL=modelCapabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/modelCapabilities.ts"],"sourcesContent":["/**\n * TITAN — Per-Model Capability Registry\n *\n * Single source of truth for output-token caps, context-window sizes,\n * and feature flags per model. Providers call clampMaxTokens() so\n * DEFAULT_MAX_TOKENS can safely be a high user-preference ceiling\n * (200 K) without causing 400s on capped providers.\n */\nimport { loadConfig } from '../config/config.js';\n\nexport interface ModelCapabilities {\n contextWindow: number;\n maxOutput: number;\n supportsThinking?: boolean;\n}\n\n// Exact-match table. Keep in alphabetical order by provider / model.\nconst STATIC_TABLE: Record<string, ModelCapabilities> = {\n 'anthropic/claude-sonnet-4-20250514': { contextWindow: 200_000, maxOutput: 64_000, supportsThinking: true },\n 'anthropic/claude-opus-4': { contextWindow: 200_000, maxOutput: 32_000, supportsThinking: true },\n 'anthropic/claude-3-5-sonnet-20241022': { contextWindow: 200_000, maxOutput: 8_192 },\n 'anthropic/claude-3-5-haiku-20241022': { contextWindow: 200_000, maxOutput: 8_192 },\n 'openai/gpt-4o': { contextWindow: 128_000, maxOutput: 16_384 },\n 'openai/gpt-4o-mini': { contextWindow: 128_000, maxOutput: 16_384 },\n 'openai/gpt-4.1': { contextWindow: 1_000_000, maxOutput: 32_768 },\n 'openai/o1': { contextWindow: 200_000, maxOutput: 100_000, supportsThinking: true },\n 'openai/o1-mini': { contextWindow: 128_000, maxOutput: 65_536, supportsThinking: true },\n 'openai/o3-mini': { contextWindow: 200_000, maxOutput: 100_000, supportsThinking: true },\n 'kimi/kimi-k2.6': { contextWindow: 262_144, maxOutput: 128_000, supportsThinking: true },\n 'moonshot/kimi-k2-0905-preview': { contextWindow: 262_144, maxOutput: 128_000 },\n 'zhipu/glm-5.1': { contextWindow: 198_000, maxOutput: 128_000 },\n 'ollama/qwen3.5:cloud': { contextWindow: 128_000, maxOutput: 32_000 },\n 'ollama/qwen3:32b': { contextWindow: 32_768, maxOutput: 8_192 },\n 'ollama/llama3.3:70b': { contextWindow: 128_000, maxOutput: 8_192 },\n 'ollama/deepseek-v3': { contextWindow: 128_000, maxOutput: 16_384 },\n 'mistral/mistral-large': { contextWindow: 128_000, maxOutput: 8_192 },\n 'gemini/gemini-2.0-flash': { contextWindow: 1_000_000, maxOutput: 8_192 },\n 'gemini/gemini-2.5-pro': { contextWindow: 2_000_000, maxOutput: 65_536, supportsThinking: true },\n 'cohere/command-r-plus': { contextWindow: 128_000, maxOutput: 4_096 },\n 'groq/llama-3.3-70b-versatile': { contextWindow: 128_000, maxOutput: 32_768 },\n};\n\n// Family heuristic for unknown specific versions.\nconst FAMILY_DEFAULTS: Array<{ pattern: RegExp; caps: ModelCapabilities }> = [\n { pattern: /^anthropic\\//, caps: { contextWindow: 200_000, maxOutput: 8_192 } },\n { pattern: /^openai\\/o\\d/, caps: { contextWindow: 200_000, maxOutput: 100_000, supportsThinking: true } },\n { pattern: /^openai\\//, caps: { contextWindow: 128_000, maxOutput: 16_384 } },\n { pattern: /^kimi\\/|^moonshot\\//, caps: { contextWindow: 262_144, maxOutput: 128_000 } },\n { pattern: /^zhipu\\/|^glm/, caps: { contextWindow: 198_000, maxOutput: 128_000 } },\n { pattern: /^gemini\\//, caps: { contextWindow: 1_000_000, maxOutput: 8_192 } },\n { pattern: /^ollama\\//, caps: { contextWindow: 32_000, maxOutput: 8_192 } },\n];\n\nconst DEFAULT_FALLBACK: ModelCapabilities = { contextWindow: 32_000, maxOutput: 8_192 };\n\nexport function getModelCapabilities(model: string): ModelCapabilities {\n if (STATIC_TABLE[model]) return STATIC_TABLE[model];\n\n // Config override path\n try {\n const cfg = loadConfig();\n const override = (cfg.providers as Record<string, unknown> | undefined)?.modelCapabilities as\n Record<string, ModelCapabilities> | undefined;\n if (override?.[model]) return override[model];\n } catch { /* config not loaded yet during early boot */ }\n\n // Family fallback\n for (const f of FAMILY_DEFAULTS) {\n if (f.pattern.test(model)) return f.caps;\n }\n\n return DEFAULT_FALLBACK;\n}\n\n/**\n * Clamp a requested maxTokens to the model's actual ceiling.\n * If requested is undefined, returns the model's own maxOutput.\n * Never returns less than 1.\n */\nexport function clampMaxTokens(model: string, requested?: number): number {\n const caps = getModelCapabilities(model);\n const want = requested ?? caps.maxOutput;\n return Math.max(1, Math.min(want, caps.maxOutput));\n}\n"],"mappings":"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","names":[]}
@@ -7,6 +7,7 @@ import logger from "../utils/logger.js";
7
7
  import { fetchWithRetry } from "../utils/helpers.js";
8
8
  import { v4 as uuid } from "uuid";
9
9
  import * as fs from "fs";
10
+ import { clampMaxTokens } from "./modelCapabilities.js";
10
11
  const COMPONENT = "Ollama";
11
12
  const CLOUD_MODEL_CTX = {
12
13
  // GLM-5.1 — 198K context (newest agentic flagship, SOTA SWE-Bench Pro)
@@ -344,7 +345,7 @@ class OllamaProvider extends LLMProvider {
344
345
  // responses don't come close to that. 8K is plenty for any
345
346
  // single turn and keeps us from getting HTTP 402s when
346
347
  // credit runs low.
347
- num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),
348
+ num_predict: clampMaxTokens(options.model || "ollama/llama3.1", options.maxTokens),
348
349
  num_ctx: getModelCtx(model),
349
350
  temperature: options.temperature ?? 0.7
350
351
  }
@@ -531,7 +532,7 @@ ${msgs[firstUserIdx].content}`;
531
532
  keep_alive: "30m",
532
533
  options: {
533
534
  // v4.10.0-local (cost cap): 8K cloud cap matches non-stream path
534
- num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),
535
+ num_predict: clampMaxTokens(options.model || "ollama/llama3.1", options.maxTokens),
535
536
  num_ctx: getModelCtx(model),
536
537
  temperature: options.temperature ?? 0.7
537
538
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/ollama.ts"],"sourcesContent":["/**\n * TITAN — Ollama Provider (Local LLMs)\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 { v4 as uuid } from 'uuid';\nimport * as fs from 'fs';\n\nconst COMPONENT = 'Ollama';\n\n/**\n * Per-model context window map for Ollama cloud models.\n * Auto-configures num_ctx to each model's actual maximum to prevent truncation.\n * Sources: Ollama Cloud model cards, March 2026.\n */\nconst CLOUD_MODEL_CTX: Record<string, number> = {\n // GLM-5.1 — 198K context (newest agentic flagship, SOTA SWE-Bench Pro)\n 'glm-5.1:cloud': 198656,\n // GLM-5 — 128K context\n 'glm-5:cloud': 131072,\n // Kimi K2.5 — 256K context (native multimodal agentic, agent swarm)\n 'kimi-k2.5:cloud': 262144,\n // Kimi K2.6 — 256K context (next-gen agentic, enhanced reasoning)\n 'kimi-k2.6:cloud': 262144,\n // Qwen3 Coder Next — 262K context (massive)\n 'qwen3-coder-next:cloud': 262144,\n // Qwen3.5 397B Cloud — 256K context (all variants support 256K)\n 'qwen3.5:397b-cloud': 262144,\n // DeepSeek V3.1 — 128K context\n 'deepseek-v3.1:671b-cloud': 131072,\n // DeepSeek V3.2 — 160K context (DSA long-context optimized)\n 'deepseek-v3.2:671b-cloud': 163840,\n 'deepseek-v3.2:cloud': 163840,\n // Devstral 2 — 128K context\n 'devstral-2:cloud': 131072,\n // Devstral Small 2 (local) — 32K\n 'devstral-small-2': 32768,\n 'devstral-small-2:latest': 32768,\n // Nemotron 3 Nano — 1M native, 32K practical for local\n 'nemotron-3-nano': 32768,\n 'nemotron-3-nano:latest': 32768,\n 'nemotron-3-nano:4b': 32768,\n 'nemotron-3-nano:30b': 32768,\n // Nemotron 3 Super — 256K context (MoE 120B/12B active)\n 'nemotron-3-super:cloud': 262144,\n // Gemini 3 Flash — 1M context\n 'gemini-3-flash-preview:latest': 1048576,\n // GPT OSS — 128K\n 'gpt-oss:120b-cloud': 131072,\n // MiniMax M2.7 — 200K context (Agent Teams, dynamic tool search)\n 'minimax-m2.7:cloud': 204800,\n // Gemma 4 — 256K context (native function calling)\n 'gemma4:cloud': 262144,\n // Qwen3.5 35B local — 32K\n 'qwen3.5:35b': 32768,\n};\n\n/**\n * Model capability profiles — controls how TITAN adapts to each model's strengths.\n * Instead of blanket rules for all models, each model gets tuned behavior.\n *\n * selfSelectsTools: Model picks tools well on its own — don't force tool_choice='required'\n * thinkingWithTools: Model benefits from thinking (<think> tags) during tool calling\n * needsSystemMerge: Model ignores standalone system messages — merge into first user msg\n * toolTemperature: Optimal temperature for tool-calling tasks (null = use caller's value or 0.5 default)\n * toolTopP: Optimal top_p for tool calling (null = omit)\n * toolTopK: Optimal top_k for tool calling (null = omit)\n */\ninterface ModelCapabilities {\n selfSelectsTools: boolean;\n thinkingWithTools: boolean;\n needsSystemMerge: boolean;\n toolTemperature: number | null;\n toolTopP: number | null;\n toolTopK: number | null;\n}\n\nconst DEFAULT_CAPABILITIES: ModelCapabilities = {\n selfSelectsTools: false,\n thinkingWithTools: false,\n needsSystemMerge: true, // Conservative default: merge for unknown models\n toolTemperature: 0.5,\n toolTopP: null,\n toolTopK: null,\n};\n\n/** Heuristic: infer capabilities from model name patterns when no hardcoded\n * entry exists. Most modern models (2024+) support native tool calling and\n * handle system prompts correctly. This prevents unknown models from being\n * crippled by overly conservative defaults. */\nfunction inferCapabilitiesFromName(modelName: string): Partial<ModelCapabilities> | undefined {\n const lower = modelName.toLowerCase();\n\n // Cloud-hosted models are almost always modern and capable\n if (lower.includes(':cloud') || lower.includes('-cloud')) {\n return { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 };\n }\n\n // Large local models (30B+) are typically capable\n const sizeMatch = lower.match(/(\\d+)b/);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 30) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Known-capable families by name pattern (even if not in hardcoded map)\n const capableFamilies = ['qwen', 'glm', 'deepseek', 'kimi', 'gemma', 'nemotron', 'devstral', 'gemini', 'mistral-large', 'llama3.3', 'llama4', 'phi4', 'command-r-plus'];\n for (const family of capableFamilies) {\n if (lower.includes(family)) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Truly unknown small local models — stay conservative\n return undefined;\n}\n\nconst MODEL_CAPABILITIES: Record<string, Partial<ModelCapabilities>> = {\n // ── Qwen family — excellent tool calling, uses thinking ──\n 'qwen3.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3-coder-next': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── DeepSeek family — strong reasoning, good tool use ──\n 'deepseek-v3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.2': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── MiniMax M2.7 — XML tool format, needs special handling ──\n // Hunt Finding #05 (2026-04-14): flipped selfSelectsTools from true→false.\n // Confirmed by reproducing: a \"use shell to run uptime\" prompt returned\n // fabricated uptime text with no tool call. The model hallucinates instead\n // of calling tools when given the choice. Setting false forces the agent\n // loop's forceToolUse to fire `tool_choice: required`, which prevents this\n // class of hallucination at the API level.\n 'minimax-m2.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95, toolTopK: 40 },\n 'minimax-m2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95 },\n\n // ── Gemma family — good tool use, no thinking ──\n 'gemma4': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 1.0, toolTopP: 0.95, toolTopK: 64 },\n 'gemma-3': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── GLM family — GLM-5.1 is agentic flagship, SOTA SWE-Bench Pro, 198K ctx ──\n 'glm-5.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'glm-5': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n 'glm-4.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── Nemotron — Super is 256K MoE optimized for collaborative agents ──\n 'nemotron-3-super': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: true, toolTemperature: 0.4 },\n 'nemotron-3-nano': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Kimi K2.5 — 256K, native agentic, agent swarm decomposition ──\n 'kimi-k2.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'kimi-k2.6': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── Devstral — code-focused ──\n 'devstral-2': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.4 },\n 'devstral-small-2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Gemini — handles system messages well ──\n 'gemini-3-flash': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── Llama/Mistral — weaker tool calling ──\n 'llama3.1': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'llama3.2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'mistral': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n};\n\n/** Resolve capabilities for a model.\n *\n * Lookup order:\n * 1. Empirical probe result from capabilities registry (~/.titan/model-capabilities.json)\n * — This reflects ACTUAL behavior tested against the live model\n * 2. Hardcoded MODEL_CAPABILITIES map (this file) — matched by longest prefix\n * 3. DEFAULT_CAPABILITIES — conservative fallback for unknown models\n */\nfunction getModelCapabilities(modelName: string): ModelCapabilities {\n // Step 1: Check empirical probe registry (preferred)\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getProbeResult, isProbeStale } = require('../agent/capabilitiesRegistry.js') as typeof import('../agent/capabilitiesRegistry.js');\n const probe = getProbeResult(modelName) || getProbeResult(`ollama/${modelName}`);\n if (probe && !isProbeStale(probe)) {\n // Convert probe result to capability flags\n return {\n ...DEFAULT_CAPABILITIES,\n selfSelectsTools: probe.nativeToolCalls,\n thinkingWithTools: probe.hasThinkingMode && !probe.needsExplicitThinkFalse,\n needsSystemMerge: !probe.respectsSystemPrompt,\n toolTemperature: probe.nativeToolCalls ? 0.5 : 0.3,\n toolTopP: null,\n toolTopK: null,\n };\n }\n } catch {\n // Registry not available (e.g., during tests) — fall through\n }\n\n/** Track which unknown models we've already triggered background probes for */\nconst probeInFlight = new Set<string>();\n\n/** Trigger a background capability probe for an unknown model.\n * Fire-and-forget: the next request will pick up the result from the registry. */\nfunction triggerBackgroundProbe(modelName: string): void {\n if (probeInFlight.has(modelName)) return;\n probeInFlight.add(modelName);\n // Dynamic import to avoid circular deps at module load time\n import('../agent/modelProbe.js')\n .then(({ probeModel }) => probeModel(`ollama/${modelName}`))\n .then((result) => import('../agent/capabilitiesRegistry.js')\n .then(({ recordProbeResult }) => {\n recordProbeResult(result);\n logger.info(COMPONENT, `Background probe complete for ${modelName}: nativeTools=${result.nativeToolCalls}, respectsSystem=${result.respectsSystemPrompt}`);\n }))\n .catch((err) => logger.warn(COMPONENT, `Background probe failed for ${modelName}: ${(err as Error).message}`))\n .finally(() => probeInFlight.delete(modelName));\n}\n\n // Step 2: Hardcoded map (prefix-matched, longest wins)\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n const baseName = bare.replace(/:(cloud|latest|\\d+b(-cloud)?)$/i, '');\n\n let bestMatch: Partial<ModelCapabilities> | undefined;\n let bestLen = 0;\n for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {\n if (baseName === pattern || baseName.startsWith(pattern)) {\n if (pattern.length > bestLen) {\n bestMatch = caps;\n bestLen = pattern.length;\n }\n }\n }\n\n if (!bestMatch) {\n // Try heuristic inference from model name before falling back to defaults\n const inferred = inferCapabilitiesFromName(modelName);\n if (inferred) {\n logger.info(COMPONENT, `Model \"${modelName}\" not in hardcoded map — using inferred capabilities: ${JSON.stringify(inferred)}`);\n bestMatch = inferred;\n } else {\n logger.info(COMPONENT, `Model \"${modelName}\" not in capabilities database or registry — using conservative defaults. Triggering background probe...`);\n triggerBackgroundProbe(modelName);\n }\n }\n return { ...DEFAULT_CAPABILITIES, ...(bestMatch || {}) };\n}\n\n/** Get the optimal num_ctx for a given model name */\nfunction getModelCtx(modelName: string): number {\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n if (CLOUD_MODEL_CTX[bare]) return CLOUD_MODEL_CTX[bare];\n\n // Heuristic: modern cloud models typically have 128K+ context\n if (bare.endsWith(':cloud') || bare.endsWith('-cloud')) return 131072;\n\n // Heuristic: large local models (30B+) often support 32K-64K\n const sizeMatch = bare.match(/(\\d+)b/i);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 70) return 65536;\n if (size >= 30) return 32768;\n if (size >= 14) return 16384;\n }\n\n // Conservative fallback for tiny unknown local models\n return 8192;\n}\n\n/** Max system prompt length for cloud models with tool calling.\n * Cloud models have 128K+ context — keep this high enough to always include\n * the full descriptions of any tools actively being used in the current task.\n */\nconst CLOUD_MAX_SYSTEM_PROMPT = 8000;\n\n/** Compress a system prompt for cloud models with tool calling.\n * Preserves (in priority order):\n * 1. Tool Execution rules (ReAct loop, MUST/NEVER — highest priority)\n * 2. Active tool descriptions (tools currently in use — must not be stripped)\n * 3. Identity\n * 4. Brief capabilities + behavior reminder\n *\n * @param content The full system prompt to compress\n * @param activeTools Descriptions of tools actively in use — always preserved\n */\nfunction compressSystemPrompt(content: string, activeTools?: Array<{ name: string; description: string }>): string {\n if (content.length <= CLOUD_MAX_SYSTEM_PROMPT) return content;\n\n const sections: string[] = [];\n\n // 1. Tool Execution rules — always first, always preserved\n const toolExecMatch = content.match(/## Tool Execution — HIGHEST PRIORITY[\\s\\S]*?(?=\\n## CRITICAL)/);\n if (toolExecMatch) {\n sections.push(toolExecMatch[0].trim());\n } else {\n sections.push(`## Tool Execution — HIGHEST PRIORITY\nYou are an AI agent. Your PRIMARY function is to execute tasks using tools.\n\nReAct Loop: THINK → ACT (call tool) → OBSERVE (read result) → REPEAT until done.\n\nMUST: call web_search+web_fetch for factual questions, call write_file/edit_file to save files (NEVER output file content as text), call shell for commands, call tool_search if unsure which tool to use.\nNEVER: describe what you could do, output file content inline, generate current facts from memory, tell user to visit a URL.\n\nRight: asked to write a file → call write_file immediately.\nWrong: asked to write a file → output the content as text in your reply.`);\n }\n\n // 2. Identity (shortened)\n const identityMatch = content.match(/## CRITICAL: Your Identity[\\s\\S]*?(?=\\n## )/);\n if (identityMatch) sections.push(identityMatch[0].trim());\n\n // 3. Brief capabilities + behavior\n sections.push('## Tools Available\\nShell, file read/write/edit, web search/fetch, browser, memory, weather, code execution, gmail, gdrive, gcal_personal, gtasks, gcontacts. Use tool_search to discover any tool not listed here.');\n sections.push('## Behavior\\n- Lead with action — call tools immediately, explain briefly after\\n- Never re-plan mid-task after CONFIRM — execute directly\\n- Confirm before destructive operations');\n\n // 4. Active tool descriptions — only inject if budget allows (max 2000 chars for tools).\n // This prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (activeTools && activeTools.length > 0) {\n const TOOL_BUDGET = 2000;\n const toolLines: string[] = [];\n let toolChars = 0;\n for (const t of activeTools) {\n // Use first 150 chars of description to keep it compact\n const desc = t.description.length > 150 ? t.description.slice(0, 147) + '...' : t.description;\n const line = `- **${t.name}**: ${desc}`;\n if (toolChars + line.length > TOOL_BUDGET) break;\n toolLines.push(line);\n toolChars += line.length;\n }\n if (toolLines.length > 0) {\n sections.push(`## Active Tools\\n${toolLines.join('\\n')}`);\n }\n }\n\n const compressed = sections.join('\\n\\n');\n // Safety: never return something larger than the original\n if (compressed.length >= content.length) {\n logger.info(COMPONENT, `Compressed prompt would be larger (${compressed.length} vs ${content.length}), using truncated original`);\n return content.slice(0, CLOUD_MAX_SYSTEM_PROMPT);\n }\n logger.info(COMPONENT, `Compressed system prompt for cloud model: ${content.length} → ${compressed.length} chars`);\n return compressed;\n}\n\n/**\n * Trim messages for cloud models while preserving tool call/response pairs.\n * Naive slicing can split a tool call from its response, breaking the tool calling contract.\n * This walks backwards keeping assistant+tool pairs together.\n */\nfunction trimPreservingToolPairs(msgs: Array<Record<string, unknown>>, maxTotal: number): Array<Record<string, unknown>> {\n const systemMsgs = msgs.filter(m => m.role === 'system');\n const nonSystem = msgs.filter(m => m.role !== 'system');\n const maxNonSystem = maxTotal - systemMsgs.length;\n\n if (nonSystem.length <= maxNonSystem) return msgs;\n\n // Walk backwards, keeping tool/assistant pairs together\n const kept: Array<Record<string, unknown>> = [];\n let i = nonSystem.length - 1;\n while (i >= 0 && kept.length < maxNonSystem) {\n const msg = nonSystem[i];\n if (msg.role === 'tool') {\n // Keep this tool result and find its assistant parent\n kept.unshift(msg);\n for (let j = i - 1; j >= 0; j--) {\n if (nonSystem[j].role === 'assistant' && (nonSystem[j].tool_calls || nonSystem[j].toolCalls)) {\n kept.unshift(nonSystem[j]);\n i = j - 1;\n break;\n }\n if (nonSystem[j].role === 'tool') {\n // Sibling tool result from same batch\n kept.unshift(nonSystem[j]);\n } else {\n i = j;\n break;\n }\n }\n } else {\n kept.unshift(msg);\n i--;\n }\n }\n\n return [...systemMsgs, ...kept];\n}\n\n/** Simplify tool parameter schemas for cloud models.\n * Strips Zod artifacts ($schema, additionalProperties, etc.) that can\n * confuse cloud model tool-calling.\n */\nfunction simplifySchema(schema: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!schema) return { type: 'object', properties: {} };\n const clean: Record<string, unknown> = { type: schema.type || 'object' };\n if (schema.properties) {\n const props: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(schema.properties as Record<string, Record<string, unknown>>)) {\n // Flatten each property to just type + description\n const prop: Record<string, unknown> = { type: val.type || 'string' };\n if (val.description) prop.description = val.description;\n if (val.enum) prop.enum = val.enum;\n if (val.default !== undefined) prop.default = val.default;\n props[key] = prop;\n }\n clean.properties = props;\n }\n if (schema.required) clean.required = schema.required;\n return clean;\n}\n\nexport class OllamaProvider extends LLMProvider {\n readonly name = 'ollama';\n readonly displayName = 'Ollama (Local)';\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools to improve tool-calling compliance.\n // Pass descriptions of complex tools (>200 chars) so compression always preserves them —\n // prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => {\n let parsedArgs: Record<string, unknown> = {};\n try {\n parsedArgs = JSON.parse(tc.function.arguments || '{}');\n } catch {\n logger.warn(COMPONENT, `Malformed tool arguments for ${tc.function.name}, using empty args`);\n }\n // v4.13: Gemini's Ollama-compat adapter rejects\n // function_call.name === ''. Some models emit empty\n // names for tool_calls when the call is malformed;\n // stamp a placeholder so the whole turn isn't\n // rejected with HTTP 400 \"Name cannot be empty\".\n const fnName = (tc.function.name || '').trim() || 'unknown_tool';\n const out: Record<string, unknown> = {\n id: tc.id,\n type: tc.type || 'function',\n function: {\n name: fnName,\n arguments: parsedArgs,\n },\n };\n // v4.13: relay Gemini thought_signature through the\n // round-trip. Ollama's Gemini proxy needs it on every\n // subsequent functionCall part or rejects with\n // \"Function call is missing a thought_signature\".\n if (tc.thoughtSignature) {\n (out.function as Record<string, unknown>).thought_signature = tc.thoughtSignature;\n out.thought_signature = tc.thoughtSignature;\n }\n return out;\n });\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty.\n // Guarantee a non-empty name on every tool-role message.\n if (m.role === 'tool') {\n const toolName = (m.name || '').trim() || 'tool';\n msg.name = toolName;\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: false,\n keep_alive: '30m',\n options: {\n // Auto-configure context window per model's known maximum.\n // getModelCtx() returns the correct num_ctx for each cloud/local model.\n // v4.10.0-local (cost cap): capped cloud num_predict to 8K\n // (was 32K). OpenRouter's paid models reject requests whose\n // max_tokens exceeds the remaining credit, even though most\n // responses don't come close to that. 8K is plenty for any\n // single turn and keeps us from getting HTTP 402s when\n // credit runs low.\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: explicitly control per model capabilities.\n // Models that don't benefit from thinking (thinkingWithTools=false) get it disabled\n // to prevent content being routed to the thinking field instead of content field.\n // This is critical for models like minimax-m2.7:cloud which put ALL output in\n // the thinking field when think is unset, leaving content empty.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n // Model doesn't support thinking — disable it to prevent 400 errors\n // from Ollama (e.g. \"titan-qwen3.5:4b does not support thinking\").\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n // Otherwise: omit body.think — let the model decide\n\n // Per-turn override: when the conversation contains tool-role messages,\n // force think=false regardless of caller intent. The GLM-family\n // tool-call parser on the server (vLLM #39611, confirmed by Z.ai docs\n // for GLM-5.1) silently drops tool results when enable_thinking=true,\n // breaking the multi-turn tool loop. Z.ai's own guidance: disable\n // thinking on tool-call turns. This keeps reasoning available for\n // planning turns while preventing the drop on execution turns.\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force a tool call on the first round when the task requires it\n // Models that self-select tools well don't need forcing — it hurts them\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n logger.info(COMPONENT, `[ToolChoiceRequired] Setting tool_choice=required for ${model} (forceToolUse=true, selfSelectsTools=false)`);\n } else if (options.forceToolUse && caps.selfSelectsTools) {\n logger.info(COMPONENT, `[ToolChoiceSkipped] forceToolUse=true but selfSelectsTools=true for ${model} — NOT setting tool_choice`);\n }\n }\n\n // Ollama-native structured outputs — constrain generation to a JSON schema.\n // https://docs.ollama.com/capabilities/structured-outputs.md\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Cloud models: trim conversation history preserving tool call/response pairs.\n // With 131K context window, cloud models can handle much longer histories.\n // E1: Use >= 80 with margin (trim to 75) to prevent off-by-one at exact boundary.\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length >= 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n\n // Some models ignore standalone system messages during tool calling.\n // Only merge when the model's capability profile says it needs it.\n if (hasTools && caps.needsSystemMerge) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs[sysIdx].content) {\n const sysContent = msgs[sysIdx].content as string;\n msgs[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs[firstUserIdx].content}`;\n msgs.splice(sysIdx, 1); // Remove the standalone system message\n logger.info(COMPONENT, `Merged system prompt into first user message for cloud model compatibility`);\n }\n }\n\n const sentMessages = body.messages as Array<{role: string; content: string}>;\n const toolNames = body.tools ? (body.tools as Array<{function: {name: string}}>).map(t => t.function.name) : [];\n logger.info(COMPONENT, `Chat request: model=${model}, cloud=${isCloudModel}, tools=[${toolNames.join(',')}], think=${body.think}, messages=${sentMessages.length}`);\n \n if (process.env.DUMP_OLLAMA_BODY === '1' || model.includes('gemini')) {\n logger.error(COMPONENT, `[DUMP_BODY] Dumping failing request body for ${model} to /tmp/ollama-body-dump.json`);\n try {\n fs.writeFileSync('/tmp/ollama-body-dump.json', JSON.stringify(body, null, 2));\n } catch (e) {\n logger.error(COMPONENT, `Failed to dump body: ${e}`);\n }\n }\n\n // Cloud models routed through Ollama need longer timeouts (they proxy to remote APIs)\n const timeoutMs = isCloudModel ? 300_000 : 120_000; // 5min cloud, 2min local\n let response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling or tokenization\n // fails with tools, retry without tools. Covers Gemini proxy errors like\n // \"does not support tools\" and \"tokenization\" failures on malformed schemas.\n if (response.status === 400 && body.tools && (\n errorText.includes('does not support tools') ||\n errorText.includes('tokenization') ||\n errorText.includes('tokenize') ||\n errorText.includes('Invalid JSON')\n )) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n if (!response.ok) {\n const retryText = await response.text();\n // Hunt Finding #37 (2026-04-14): use createProviderError to\n // attach status + parsed Retry-After so the router actually\n // respects the provider's backoff hint.\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, retryText, { provider: 'ollama', model });\n }\n } else {\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, errorText, { provider: 'ollama', model });\n }\n }\n\n // v4.13 ancestor-extraction (Hermes rate_limit_tracker): capture any\n // x-ratelimit-* headers the Ollama proxy exposes. Graceful no-op when\n // the headers aren't present. Provider name is 'ollama' so the router's\n // proactive-backoff logic can consult per-provider state.\n try {\n const { recordHeaders } = await import('./rateLimitTracker.js');\n recordHeaders('ollama', response.headers);\n } catch { /* never fail the chat on tracker issues */ }\n\n const data = await response.json() as Record<string, unknown>;\n const message = data.message as Record<string, unknown>;\n logger.info(COMPONENT, `Response from ${model}: tool_calls=${JSON.stringify(message.tool_calls)}, content_length=${((message.content as string) || '').length}`);\n const toolCalls: ToolCall[] = [];\n\n // v5.0.2: Only accept tool_calls from the model if tools were actually\n // sent in the request. Prevents hallucinated tool calls when the safety\n // system has stripped all tools (activeTools = []) or for models that\n // emit tool_calls even without tool definitions.\n if (message.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown>;\n // v4.13: capture Gemini thought_signature if present — needed\n // on the round-trip back or Gemini rejects the next request.\n const thoughtSig = (tc.thought_signature as string | undefined) ??\n (tc.thoughtSignature as string | undefined) ??\n (fn.thought_signature as string | undefined) ??\n (fn.thoughtSignature as string | undefined);\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fn.name as string,\n arguments: JSON.stringify(fn.arguments),\n },\n ...(thoughtSig ? { thoughtSignature: thoughtSig } : {}),\n });\n }\n }\n\n // A2: Hallucinated tool name detection at provider level (LangGraph pattern)\n if (options.tools && toolCalls.length > 0) {\n const validNames = new Set(options.tools.map(t => t.function.name));\n const invalid = toolCalls.filter(tc => !validNames.has(tc.function.name));\n if (invalid.length > 0) {\n logger.warn(COMPONENT, `[HallucinationGuard] Model hallucinated ${invalid.length} tool name(s): ${invalid.map(tc => tc.function.name).join(', ')}. Will be caught by toolRunner with corrective feedback.`);\n }\n }\n\n // If content is empty but thinking field has content, use it as a fallback.\n // This handles models that route output to thinking field when think is\n // unset or misconfigured. The router's stripThinkingFromResponse() will\n // clean up any reasoning that leaks through, so we can be permissive here.\n let content = (message.content as string) || '';\n if (!content && message.thinking) {\n const thinking = (message.thinking as string) || '';\n if (thinking.length > 0) {\n logger.info(COMPONENT, `[ThinkingFallback] Content empty, using thinking field (${thinking.length} chars)`);\n content = thinking;\n }\n }\n // Strip leaked thinking tags from Qwen/DeepSeek models\n content = content.replace(/^[\\s\\S]*?<\\/think>\\s*/m, '').trim();\n\n return {\n id: uuid(),\n content,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: {\n promptTokens: (data.prompt_eval_count as number) || 0,\n completionTokens: (data.eval_count as number) || 0,\n totalTokens: ((data.prompt_eval_count as number) || 0) + ((data.eval_count as number) || 0),\n },\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `ollama/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools — preserve active tool descriptions\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => ({\n id: tc.id,\n type: tc.type || 'function',\n function: { name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}') }\n }));\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty\n if (m.role === 'tool') {\n msg.name = m.name || 'tool';\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: true,\n keep_alive: '30m',\n options: {\n // v4.10.0-local (cost cap): 8K cloud cap matches non-stream path\n num_predict: options.maxTokens || (isCloudModel ? 8192 : 16384),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: respect explicit setting, otherwise use model capabilities.\n // Disable for models that don't support thinking — prevents 400 errors.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n\n // Per-turn override for tool-role turns (see chat() for rationale: vLLM #39611 / Z.ai docs).\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] (stream) Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (hasTools) {\n body.tools = options.tools!.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force tool_choice when requested — skip for models that self-select well\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n }\n }\n\n // Ollama-native structured outputs (stream variant).\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Optimize: trim history preserving tool pairs (cloud models only — local models have smaller contexts)\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length > 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `[Stream] Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n // Merge system into first user message only for models that need it\n if (hasTools && caps.needsSystemMerge) {\n const msgs2 = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs2.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs2.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs2[sysIdx].content) {\n const sysContent = msgs2[sysIdx].content as string;\n msgs2[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs2[firstUserIdx].content}`;\n msgs2.splice(sysIdx, 1);\n }\n }\n\n try {\n // Cloud models need longer timeouts for streaming too\n const streamTimeoutMs = isCloudModel ? 300_000 : 120_000;\n let response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling, retry without tools\n if (response.status === 400 && errorText.includes('does not support tools') && body.tools) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n if (!response.ok || !response.body) {\n const retryText = await response.text();\n yield { type: 'error', error: `Ollama error (${response.status}): ${retryText}` };\n return;\n }\n } else {\n yield { type: 'error', error: `Ollama error (${response.status}): ${errorText}` };\n return;\n }\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let insideThink = false;\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.trim()) continue;\n try {\n const chunk = JSON.parse(line);\n // Handle thinking field for models that put content there\n // Some models (e.g. qwen3.5, nemotron-super:cloud) use the thinking field\n // even when think=false is set — treat thinking as content in that case\n if (!chunk.message?.content && chunk.message?.thinking) {\n if (body.think === false) {\n chunk.message.content = chunk.message.thinking;\n }\n }\n if (chunk.message?.content) {\n let text = chunk.message.content;\n // Strip leaked <think>...</think> blocks from Qwen/DeepSeek\n if (text.includes('<think>')) insideThink = true;\n if (insideThink) {\n if (text.includes('</think>')) {\n text = text.split('</think>').pop()?.trim() || '';\n insideThink = false;\n } else {\n continue; // suppress thinking content\n }\n }\n if (text) yield { type: 'text', content: text };\n }\n // v5.0.2: Only yield tool_calls if tools were sent in the request\n if (chunk.message?.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of chunk.message.tool_calls) {\n const fn = tc.function as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fn.name as string, arguments: JSON.stringify(fn.arguments) } },\n };\n }\n }\n if (chunk.done) break;\n } catch { /* skip malformed NDJSON 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 try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n if (!response.ok) {\n // Hunt Finding #29 (2026-04-14): consume the body even on\n // error paths so the underlying socket can return to the\n // keep-alive pool. Without this, every non-200 response\n // leaks its socket until the GC gets around to it.\n await response.body?.cancel().catch(() => {});\n return [];\n }\n const data = await response.json() as { models?: Array<{ name: string }> };\n return (data.models || []).map((m) => m.name);\n } catch {\n return [];\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n const ok = response.ok;\n // Hunt Finding #29 (2026-04-14): ALWAYS consume or cancel the\n // body. Previously we returned response.ok directly, leaving the\n // body stream dangling and the socket held open.\n await response.body?.cancel().catch(() => {});\n return ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":["modelName"]}
1
+ {"version":3,"sources":["../../src/providers/ollama.ts"],"sourcesContent":["/**\n * TITAN — Ollama Provider (Local LLMs)\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 { v4 as uuid } from 'uuid';\nimport * as fs from 'fs';\nimport { clampMaxTokens } from './modelCapabilities.js';\n\nconst COMPONENT = 'Ollama';\n\n/**\n * Per-model context window map for Ollama cloud models.\n * Auto-configures num_ctx to each model's actual maximum to prevent truncation.\n * Sources: Ollama Cloud model cards, March 2026.\n */\nconst CLOUD_MODEL_CTX: Record<string, number> = {\n // GLM-5.1 — 198K context (newest agentic flagship, SOTA SWE-Bench Pro)\n 'glm-5.1:cloud': 198656,\n // GLM-5 — 128K context\n 'glm-5:cloud': 131072,\n // Kimi K2.5 — 256K context (native multimodal agentic, agent swarm)\n 'kimi-k2.5:cloud': 262144,\n // Kimi K2.6 — 256K context (next-gen agentic, enhanced reasoning)\n 'kimi-k2.6:cloud': 262144,\n // Qwen3 Coder Next — 262K context (massive)\n 'qwen3-coder-next:cloud': 262144,\n // Qwen3.5 397B Cloud — 256K context (all variants support 256K)\n 'qwen3.5:397b-cloud': 262144,\n // DeepSeek V3.1 — 128K context\n 'deepseek-v3.1:671b-cloud': 131072,\n // DeepSeek V3.2 — 160K context (DSA long-context optimized)\n 'deepseek-v3.2:671b-cloud': 163840,\n 'deepseek-v3.2:cloud': 163840,\n // Devstral 2 — 128K context\n 'devstral-2:cloud': 131072,\n // Devstral Small 2 (local) — 32K\n 'devstral-small-2': 32768,\n 'devstral-small-2:latest': 32768,\n // Nemotron 3 Nano — 1M native, 32K practical for local\n 'nemotron-3-nano': 32768,\n 'nemotron-3-nano:latest': 32768,\n 'nemotron-3-nano:4b': 32768,\n 'nemotron-3-nano:30b': 32768,\n // Nemotron 3 Super — 256K context (MoE 120B/12B active)\n 'nemotron-3-super:cloud': 262144,\n // Gemini 3 Flash — 1M context\n 'gemini-3-flash-preview:latest': 1048576,\n // GPT OSS — 128K\n 'gpt-oss:120b-cloud': 131072,\n // MiniMax M2.7 — 200K context (Agent Teams, dynamic tool search)\n 'minimax-m2.7:cloud': 204800,\n // Gemma 4 — 256K context (native function calling)\n 'gemma4:cloud': 262144,\n // Qwen3.5 35B local — 32K\n 'qwen3.5:35b': 32768,\n};\n\n/**\n * Model capability profiles — controls how TITAN adapts to each model's strengths.\n * Instead of blanket rules for all models, each model gets tuned behavior.\n *\n * selfSelectsTools: Model picks tools well on its own — don't force tool_choice='required'\n * thinkingWithTools: Model benefits from thinking (<think> tags) during tool calling\n * needsSystemMerge: Model ignores standalone system messages — merge into first user msg\n * toolTemperature: Optimal temperature for tool-calling tasks (null = use caller's value or 0.5 default)\n * toolTopP: Optimal top_p for tool calling (null = omit)\n * toolTopK: Optimal top_k for tool calling (null = omit)\n */\ninterface ModelCapabilities {\n selfSelectsTools: boolean;\n thinkingWithTools: boolean;\n needsSystemMerge: boolean;\n toolTemperature: number | null;\n toolTopP: number | null;\n toolTopK: number | null;\n}\n\nconst DEFAULT_CAPABILITIES: ModelCapabilities = {\n selfSelectsTools: false,\n thinkingWithTools: false,\n needsSystemMerge: true, // Conservative default: merge for unknown models\n toolTemperature: 0.5,\n toolTopP: null,\n toolTopK: null,\n};\n\n/** Heuristic: infer capabilities from model name patterns when no hardcoded\n * entry exists. Most modern models (2024+) support native tool calling and\n * handle system prompts correctly. This prevents unknown models from being\n * crippled by overly conservative defaults. */\nfunction inferCapabilitiesFromName(modelName: string): Partial<ModelCapabilities> | undefined {\n const lower = modelName.toLowerCase();\n\n // Cloud-hosted models are almost always modern and capable\n if (lower.includes(':cloud') || lower.includes('-cloud')) {\n return { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 };\n }\n\n // Large local models (30B+) are typically capable\n const sizeMatch = lower.match(/(\\d+)b/);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 30) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Known-capable families by name pattern (even if not in hardcoded map)\n const capableFamilies = ['qwen', 'glm', 'deepseek', 'kimi', 'gemma', 'nemotron', 'devstral', 'gemini', 'mistral-large', 'llama3.3', 'llama4', 'phi4', 'command-r-plus'];\n for (const family of capableFamilies) {\n if (lower.includes(family)) {\n return { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 };\n }\n }\n\n // Truly unknown small local models — stay conservative\n return undefined;\n}\n\nconst MODEL_CAPABILITIES: Record<string, Partial<ModelCapabilities>> = {\n // ── Qwen family — excellent tool calling, uses thinking ──\n 'qwen3.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.7 },\n 'qwen3-coder-next': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── DeepSeek family — strong reasoning, good tool use ──\n 'deepseek-v3': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'deepseek-v3.2': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── MiniMax M2.7 — XML tool format, needs special handling ──\n // Hunt Finding #05 (2026-04-14): flipped selfSelectsTools from true→false.\n // Confirmed by reproducing: a \"use shell to run uptime\" prompt returned\n // fabricated uptime text with no tool call. The model hallucinates instead\n // of calling tools when given the choice. Setting false forces the agent\n // loop's forceToolUse to fire `tool_choice: required`, which prevents this\n // class of hallucination at the API level.\n 'minimax-m2.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95, toolTopK: 40 },\n 'minimax-m2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.8, toolTopP: 0.95 },\n\n // ── Gemma family — good tool use, no thinking ──\n 'gemma4': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 1.0, toolTopP: 0.95, toolTopK: 64 },\n 'gemma-3': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── GLM family — GLM-5.1 is agentic flagship, SOTA SWE-Bench Pro, 198K ctx ──\n 'glm-5.1': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'glm-5': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n 'glm-4.7': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.4 },\n\n // ── Nemotron — Super is 256K MoE optimized for collaborative agents ──\n 'nemotron-3-super': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: true, toolTemperature: 0.4 },\n 'nemotron-3-nano': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Kimi K2.5 — 256K, native agentic, agent swarm decomposition ──\n 'kimi-k2.5': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n 'kimi-k2.6': { selfSelectsTools: true, thinkingWithTools: true, needsSystemMerge: false, toolTemperature: 0.6 },\n\n // ── Devstral — code-focused ──\n 'devstral-2': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.4 },\n 'devstral-small-2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n\n // ── Gemini — handles system messages well ──\n 'gemini-3-flash': { selfSelectsTools: true, thinkingWithTools: false, needsSystemMerge: false, toolTemperature: 0.5 },\n\n // ── Llama/Mistral — weaker tool calling ──\n 'llama3.1': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'llama3.2': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n 'mistral': { selfSelectsTools: false, thinkingWithTools: false, needsSystemMerge: true, toolTemperature: 0.3 },\n};\n\n/** Resolve capabilities for a model.\n *\n * Lookup order:\n * 1. Empirical probe result from capabilities registry (~/.titan/model-capabilities.json)\n * — This reflects ACTUAL behavior tested against the live model\n * 2. Hardcoded MODEL_CAPABILITIES map (this file) — matched by longest prefix\n * 3. DEFAULT_CAPABILITIES — conservative fallback for unknown models\n */\nfunction getModelCapabilities(modelName: string): ModelCapabilities {\n // Step 1: Check empirical probe registry (preferred)\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { getProbeResult, isProbeStale } = require('../agent/capabilitiesRegistry.js') as typeof import('../agent/capabilitiesRegistry.js');\n const probe = getProbeResult(modelName) || getProbeResult(`ollama/${modelName}`);\n if (probe && !isProbeStale(probe)) {\n // Convert probe result to capability flags\n return {\n ...DEFAULT_CAPABILITIES,\n selfSelectsTools: probe.nativeToolCalls,\n thinkingWithTools: probe.hasThinkingMode && !probe.needsExplicitThinkFalse,\n needsSystemMerge: !probe.respectsSystemPrompt,\n toolTemperature: probe.nativeToolCalls ? 0.5 : 0.3,\n toolTopP: null,\n toolTopK: null,\n };\n }\n } catch {\n // Registry not available (e.g., during tests) — fall through\n }\n\n/** Track which unknown models we've already triggered background probes for */\nconst probeInFlight = new Set<string>();\n\n/** Trigger a background capability probe for an unknown model.\n * Fire-and-forget: the next request will pick up the result from the registry. */\nfunction triggerBackgroundProbe(modelName: string): void {\n if (probeInFlight.has(modelName)) return;\n probeInFlight.add(modelName);\n // Dynamic import to avoid circular deps at module load time\n import('../agent/modelProbe.js')\n .then(({ probeModel }) => probeModel(`ollama/${modelName}`))\n .then((result) => import('../agent/capabilitiesRegistry.js')\n .then(({ recordProbeResult }) => {\n recordProbeResult(result);\n logger.info(COMPONENT, `Background probe complete for ${modelName}: nativeTools=${result.nativeToolCalls}, respectsSystem=${result.respectsSystemPrompt}`);\n }))\n .catch((err) => logger.warn(COMPONENT, `Background probe failed for ${modelName}: ${(err as Error).message}`))\n .finally(() => probeInFlight.delete(modelName));\n}\n\n // Step 2: Hardcoded map (prefix-matched, longest wins)\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n const baseName = bare.replace(/:(cloud|latest|\\d+b(-cloud)?)$/i, '');\n\n let bestMatch: Partial<ModelCapabilities> | undefined;\n let bestLen = 0;\n for (const [pattern, caps] of Object.entries(MODEL_CAPABILITIES)) {\n if (baseName === pattern || baseName.startsWith(pattern)) {\n if (pattern.length > bestLen) {\n bestMatch = caps;\n bestLen = pattern.length;\n }\n }\n }\n\n if (!bestMatch) {\n // Try heuristic inference from model name before falling back to defaults\n const inferred = inferCapabilitiesFromName(modelName);\n if (inferred) {\n logger.info(COMPONENT, `Model \"${modelName}\" not in hardcoded map — using inferred capabilities: ${JSON.stringify(inferred)}`);\n bestMatch = inferred;\n } else {\n logger.info(COMPONENT, `Model \"${modelName}\" not in capabilities database or registry — using conservative defaults. Triggering background probe...`);\n triggerBackgroundProbe(modelName);\n }\n }\n return { ...DEFAULT_CAPABILITIES, ...(bestMatch || {}) };\n}\n\n/** Get the optimal num_ctx for a given model name */\nfunction getModelCtx(modelName: string): number {\n const bare = modelName.includes('/') ? modelName.split('/').slice(1).join('/') : modelName;\n if (CLOUD_MODEL_CTX[bare]) return CLOUD_MODEL_CTX[bare];\n\n // Heuristic: modern cloud models typically have 128K+ context\n if (bare.endsWith(':cloud') || bare.endsWith('-cloud')) return 131072;\n\n // Heuristic: large local models (30B+) often support 32K-64K\n const sizeMatch = bare.match(/(\\d+)b/i);\n if (sizeMatch) {\n const size = parseInt(sizeMatch[1], 10);\n if (size >= 70) return 65536;\n if (size >= 30) return 32768;\n if (size >= 14) return 16384;\n }\n\n // Conservative fallback for tiny unknown local models\n return 8192;\n}\n\n/** Max system prompt length for cloud models with tool calling.\n * Cloud models have 128K+ context — keep this high enough to always include\n * the full descriptions of any tools actively being used in the current task.\n */\nconst CLOUD_MAX_SYSTEM_PROMPT = 8000;\n\n/** Compress a system prompt for cloud models with tool calling.\n * Preserves (in priority order):\n * 1. Tool Execution rules (ReAct loop, MUST/NEVER — highest priority)\n * 2. Active tool descriptions (tools currently in use — must not be stripped)\n * 3. Identity\n * 4. Brief capabilities + behavior reminder\n *\n * @param content The full system prompt to compress\n * @param activeTools Descriptions of tools actively in use — always preserved\n */\nfunction compressSystemPrompt(content: string, activeTools?: Array<{ name: string; description: string }>): string {\n if (content.length <= CLOUD_MAX_SYSTEM_PROMPT) return content;\n\n const sections: string[] = [];\n\n // 1. Tool Execution rules — always first, always preserved\n const toolExecMatch = content.match(/## Tool Execution — HIGHEST PRIORITY[\\s\\S]*?(?=\\n## CRITICAL)/);\n if (toolExecMatch) {\n sections.push(toolExecMatch[0].trim());\n } else {\n sections.push(`## Tool Execution — HIGHEST PRIORITY\nYou are an AI agent. Your PRIMARY function is to execute tasks using tools.\n\nReAct Loop: THINK → ACT (call tool) → OBSERVE (read result) → REPEAT until done.\n\nMUST: call web_search+web_fetch for factual questions, call write_file/edit_file to save files (NEVER output file content as text), call shell for commands, call tool_search if unsure which tool to use.\nNEVER: describe what you could do, output file content inline, generate current facts from memory, tell user to visit a URL.\n\nRight: asked to write a file → call write_file immediately.\nWrong: asked to write a file → output the content as text in your reply.`);\n }\n\n // 2. Identity (shortened)\n const identityMatch = content.match(/## CRITICAL: Your Identity[\\s\\S]*?(?=\\n## )/);\n if (identityMatch) sections.push(identityMatch[0].trim());\n\n // 3. Brief capabilities + behavior\n sections.push('## Tools Available\\nShell, file read/write/edit, web search/fetch, browser, memory, weather, code execution, gmail, gdrive, gcal_personal, gtasks, gcontacts. Use tool_search to discover any tool not listed here.');\n sections.push('## Behavior\\n- Lead with action — call tools immediately, explain briefly after\\n- Never re-plan mid-task after CONFIRM — execute directly\\n- Confirm before destructive operations');\n\n // 4. Active tool descriptions — only inject if budget allows (max 2000 chars for tools).\n // This prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (activeTools && activeTools.length > 0) {\n const TOOL_BUDGET = 2000;\n const toolLines: string[] = [];\n let toolChars = 0;\n for (const t of activeTools) {\n // Use first 150 chars of description to keep it compact\n const desc = t.description.length > 150 ? t.description.slice(0, 147) + '...' : t.description;\n const line = `- **${t.name}**: ${desc}`;\n if (toolChars + line.length > TOOL_BUDGET) break;\n toolLines.push(line);\n toolChars += line.length;\n }\n if (toolLines.length > 0) {\n sections.push(`## Active Tools\\n${toolLines.join('\\n')}`);\n }\n }\n\n const compressed = sections.join('\\n\\n');\n // Safety: never return something larger than the original\n if (compressed.length >= content.length) {\n logger.info(COMPONENT, `Compressed prompt would be larger (${compressed.length} vs ${content.length}), using truncated original`);\n return content.slice(0, CLOUD_MAX_SYSTEM_PROMPT);\n }\n logger.info(COMPONENT, `Compressed system prompt for cloud model: ${content.length} → ${compressed.length} chars`);\n return compressed;\n}\n\n/**\n * Trim messages for cloud models while preserving tool call/response pairs.\n * Naive slicing can split a tool call from its response, breaking the tool calling contract.\n * This walks backwards keeping assistant+tool pairs together.\n */\nfunction trimPreservingToolPairs(msgs: Array<Record<string, unknown>>, maxTotal: number): Array<Record<string, unknown>> {\n const systemMsgs = msgs.filter(m => m.role === 'system');\n const nonSystem = msgs.filter(m => m.role !== 'system');\n const maxNonSystem = maxTotal - systemMsgs.length;\n\n if (nonSystem.length <= maxNonSystem) return msgs;\n\n // Walk backwards, keeping tool/assistant pairs together\n const kept: Array<Record<string, unknown>> = [];\n let i = nonSystem.length - 1;\n while (i >= 0 && kept.length < maxNonSystem) {\n const msg = nonSystem[i];\n if (msg.role === 'tool') {\n // Keep this tool result and find its assistant parent\n kept.unshift(msg);\n for (let j = i - 1; j >= 0; j--) {\n if (nonSystem[j].role === 'assistant' && (nonSystem[j].tool_calls || nonSystem[j].toolCalls)) {\n kept.unshift(nonSystem[j]);\n i = j - 1;\n break;\n }\n if (nonSystem[j].role === 'tool') {\n // Sibling tool result from same batch\n kept.unshift(nonSystem[j]);\n } else {\n i = j;\n break;\n }\n }\n } else {\n kept.unshift(msg);\n i--;\n }\n }\n\n return [...systemMsgs, ...kept];\n}\n\n/** Simplify tool parameter schemas for cloud models.\n * Strips Zod artifacts ($schema, additionalProperties, etc.) that can\n * confuse cloud model tool-calling.\n */\nfunction simplifySchema(schema: Record<string, unknown> | undefined): Record<string, unknown> {\n if (!schema) return { type: 'object', properties: {} };\n const clean: Record<string, unknown> = { type: schema.type || 'object' };\n if (schema.properties) {\n const props: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(schema.properties as Record<string, Record<string, unknown>>)) {\n // Flatten each property to just type + description\n const prop: Record<string, unknown> = { type: val.type || 'string' };\n if (val.description) prop.description = val.description;\n if (val.enum) prop.enum = val.enum;\n if (val.default !== undefined) prop.default = val.default;\n props[key] = prop;\n }\n clean.properties = props;\n }\n if (schema.required) clean.required = schema.required;\n return clean;\n}\n\nexport class OllamaProvider extends LLMProvider {\n readonly name = 'ollama';\n readonly displayName = 'Ollama (Local)';\n\n private get baseUrl(): string {\n const config = loadConfig();\n return config.providers.ollama.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools to improve tool-calling compliance.\n // Pass descriptions of complex tools (>200 chars) so compression always preserves them —\n // prevents the model from forgetting available actions mid-task (e.g. after CONFIRM).\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => {\n let parsedArgs: Record<string, unknown> = {};\n try {\n parsedArgs = JSON.parse(tc.function.arguments || '{}');\n } catch {\n logger.warn(COMPONENT, `Malformed tool arguments for ${tc.function.name}, using empty args`);\n }\n // v4.13: Gemini's Ollama-compat adapter rejects\n // function_call.name === ''. Some models emit empty\n // names for tool_calls when the call is malformed;\n // stamp a placeholder so the whole turn isn't\n // rejected with HTTP 400 \"Name cannot be empty\".\n const fnName = (tc.function.name || '').trim() || 'unknown_tool';\n const out: Record<string, unknown> = {\n id: tc.id,\n type: tc.type || 'function',\n function: {\n name: fnName,\n arguments: parsedArgs,\n },\n };\n // v4.13: relay Gemini thought_signature through the\n // round-trip. Ollama's Gemini proxy needs it on every\n // subsequent functionCall part or rejects with\n // \"Function call is missing a thought_signature\".\n if (tc.thoughtSignature) {\n (out.function as Record<string, unknown>).thought_signature = tc.thoughtSignature;\n out.thought_signature = tc.thoughtSignature;\n }\n return out;\n });\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty.\n // Guarantee a non-empty name on every tool-role message.\n if (m.role === 'tool') {\n const toolName = (m.name || '').trim() || 'tool';\n msg.name = toolName;\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: false,\n keep_alive: '30m',\n options: {\n // Auto-configure context window per model's known maximum.\n // getModelCtx() returns the correct num_ctx for each cloud/local model.\n // v4.10.0-local (cost cap): capped cloud num_predict to 8K\n // (was 32K). OpenRouter's paid models reject requests whose\n // max_tokens exceeds the remaining credit, even though most\n // responses don't come close to that. 8K is plenty for any\n // single turn and keeps us from getting HTTP 402s when\n // credit runs low.\n num_predict: clampMaxTokens(options.model || 'ollama/llama3.1', options.maxTokens),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: explicitly control per model capabilities.\n // Models that don't benefit from thinking (thinkingWithTools=false) get it disabled\n // to prevent content being routed to the thinking field instead of content field.\n // This is critical for models like minimax-m2.7:cloud which put ALL output in\n // the thinking field when think is unset, leaving content empty.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n // Model doesn't support thinking — disable it to prevent 400 errors\n // from Ollama (e.g. \"titan-qwen3.5:4b does not support thinking\").\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n // Otherwise: omit body.think — let the model decide\n\n // Per-turn override: when the conversation contains tool-role messages,\n // force think=false regardless of caller intent. The GLM-family\n // tool-call parser on the server (vLLM #39611, confirmed by Z.ai docs\n // for GLM-5.1) silently drops tool results when enable_thinking=true,\n // breaking the multi-turn tool loop. Z.ai's own guidance: disable\n // thinking on tool-call turns. This keeps reasoning available for\n // planning turns while preventing the drop on execution turns.\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (options.tools && options.tools.length > 0) {\n body.tools = options.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force a tool call on the first round when the task requires it\n // Models that self-select tools well don't need forcing — it hurts them\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n logger.info(COMPONENT, `[ToolChoiceRequired] Setting tool_choice=required for ${model} (forceToolUse=true, selfSelectsTools=false)`);\n } else if (options.forceToolUse && caps.selfSelectsTools) {\n logger.info(COMPONENT, `[ToolChoiceSkipped] forceToolUse=true but selfSelectsTools=true for ${model} — NOT setting tool_choice`);\n }\n }\n\n // Ollama-native structured outputs — constrain generation to a JSON schema.\n // https://docs.ollama.com/capabilities/structured-outputs.md\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Cloud models: trim conversation history preserving tool call/response pairs.\n // With 131K context window, cloud models can handle much longer histories.\n // E1: Use >= 80 with margin (trim to 75) to prevent off-by-one at exact boundary.\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length >= 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n\n // Some models ignore standalone system messages during tool calling.\n // Only merge when the model's capability profile says it needs it.\n if (hasTools && caps.needsSystemMerge) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs[sysIdx].content) {\n const sysContent = msgs[sysIdx].content as string;\n msgs[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs[firstUserIdx].content}`;\n msgs.splice(sysIdx, 1); // Remove the standalone system message\n logger.info(COMPONENT, `Merged system prompt into first user message for cloud model compatibility`);\n }\n }\n\n const sentMessages = body.messages as Array<{role: string; content: string}>;\n const toolNames = body.tools ? (body.tools as Array<{function: {name: string}}>).map(t => t.function.name) : [];\n logger.info(COMPONENT, `Chat request: model=${model}, cloud=${isCloudModel}, tools=[${toolNames.join(',')}], think=${body.think}, messages=${sentMessages.length}`);\n \n if (process.env.DUMP_OLLAMA_BODY === '1' || model.includes('gemini')) {\n logger.error(COMPONENT, `[DUMP_BODY] Dumping failing request body for ${model} to /tmp/ollama-body-dump.json`);\n try {\n fs.writeFileSync('/tmp/ollama-body-dump.json', JSON.stringify(body, null, 2));\n } catch (e) {\n logger.error(COMPONENT, `Failed to dump body: ${e}`);\n }\n }\n\n // Cloud models routed through Ollama need longer timeouts (they proxy to remote APIs)\n const timeoutMs = isCloudModel ? 300_000 : 120_000; // 5min cloud, 2min local\n let response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n\n if (!response.ok) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling or tokenization\n // fails with tools, retry without tools. Covers Gemini proxy errors like\n // \"does not support tools\" and \"tokenization\" failures on malformed schemas.\n if (response.status === 400 && body.tools && (\n errorText.includes('does not support tools') ||\n errorText.includes('tokenization') ||\n errorText.includes('tokenize') ||\n errorText.includes('Invalid JSON')\n )) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetchWithRetry(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, { timeoutMs });\n if (!response.ok) {\n const retryText = await response.text();\n // Hunt Finding #37 (2026-04-14): use createProviderError to\n // attach status + parsed Retry-After so the router actually\n // respects the provider's backoff hint.\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, retryText, { provider: 'ollama', model });\n }\n } else {\n const { createProviderError } = await import('./errorTaxonomy.js');\n throw createProviderError('Ollama', response, errorText, { provider: 'ollama', model });\n }\n }\n\n // v4.13 ancestor-extraction (Hermes rate_limit_tracker): capture any\n // x-ratelimit-* headers the Ollama proxy exposes. Graceful no-op when\n // the headers aren't present. Provider name is 'ollama' so the router's\n // proactive-backoff logic can consult per-provider state.\n try {\n const { recordHeaders } = await import('./rateLimitTracker.js');\n recordHeaders('ollama', response.headers);\n } catch { /* never fail the chat on tracker issues */ }\n\n const data = await response.json() as Record<string, unknown>;\n const message = data.message as Record<string, unknown>;\n logger.info(COMPONENT, `Response from ${model}: tool_calls=${JSON.stringify(message.tool_calls)}, content_length=${((message.content as string) || '').length}`);\n const toolCalls: ToolCall[] = [];\n\n // v5.0.2: Only accept tool_calls from the model if tools were actually\n // sent in the request. Prevents hallucinated tool calls when the safety\n // system has stripped all tools (activeTools = []) or for models that\n // emit tool_calls even without tool definitions.\n if (message.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of message.tool_calls as Array<Record<string, unknown>>) {\n const fn = tc.function as Record<string, unknown>;\n // v4.13: capture Gemini thought_signature if present — needed\n // on the round-trip back or Gemini rejects the next request.\n const thoughtSig = (tc.thought_signature as string | undefined) ??\n (tc.thoughtSignature as string | undefined) ??\n (fn.thought_signature as string | undefined) ??\n (fn.thoughtSignature as string | undefined);\n toolCalls.push({\n id: uuid(),\n type: 'function',\n function: {\n name: fn.name as string,\n arguments: JSON.stringify(fn.arguments),\n },\n ...(thoughtSig ? { thoughtSignature: thoughtSig } : {}),\n });\n }\n }\n\n // A2: Hallucinated tool name detection at provider level (LangGraph pattern)\n if (options.tools && toolCalls.length > 0) {\n const validNames = new Set(options.tools.map(t => t.function.name));\n const invalid = toolCalls.filter(tc => !validNames.has(tc.function.name));\n if (invalid.length > 0) {\n logger.warn(COMPONENT, `[HallucinationGuard] Model hallucinated ${invalid.length} tool name(s): ${invalid.map(tc => tc.function.name).join(', ')}. Will be caught by toolRunner with corrective feedback.`);\n }\n }\n\n // If content is empty but thinking field has content, use it as a fallback.\n // This handles models that route output to thinking field when think is\n // unset or misconfigured. The router's stripThinkingFromResponse() will\n // clean up any reasoning that leaks through, so we can be permissive here.\n let content = (message.content as string) || '';\n if (!content && message.thinking) {\n const thinking = (message.thinking as string) || '';\n if (thinking.length > 0) {\n logger.info(COMPONENT, `[ThinkingFallback] Content empty, using thinking field (${thinking.length} chars)`);\n content = thinking;\n }\n }\n // Strip leaked thinking tags from Qwen/DeepSeek models\n content = content.replace(/^[\\s\\S]*?<\\/think>\\s*/m, '').trim();\n\n return {\n id: uuid(),\n content,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n usage: {\n promptTokens: (data.prompt_eval_count as number) || 0,\n completionTokens: (data.eval_count as number) || 0,\n totalTokens: ((data.prompt_eval_count as number) || 0) + ((data.eval_count as number) || 0),\n },\n finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',\n model: `ollama/${model}`,\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const model = (options.model || 'llama3.1').replace('ollama/', '');\n\n // Cloud models (Ollama Pro): detect via -cloud suffix or :cloud tag\n const isCloudModel = model.includes('-cloud') || model.includes(':cloud');\n const hasTools = options.tools && options.tools.length > 0;\n const hasToolRoleMessages = options.messages.some(m => m.role === 'tool');\n\n const body: Record<string, unknown> = {\n model,\n messages: options.messages.map((m) => {\n const msg: Record<string, unknown> = { role: m.role };\n // Compress system prompts for cloud models with tools — preserve active tool descriptions\n if (m.role === 'system' && isCloudModel && hasTools) {\n const activeToolDescs = (options.tools ?? [])\n .filter(t => (t.function.description?.length ?? 0) > 200)\n .map(t => ({ name: t.function.name, description: t.function.description ?? '' }));\n msg.content = compressSystemPrompt(m.content, activeToolDescs.length > 0 ? activeToolDescs : undefined);\n } else {\n msg.content = m.content;\n }\n if (m.toolCalls && m.toolCalls.length > 0) {\n msg.tool_calls = m.toolCalls.map(tc => ({\n id: tc.id,\n type: tc.type || 'function',\n function: { name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}') }\n }));\n }\n if (m.toolCallId) msg.tool_call_id = m.toolCallId;\n // Cloud models (Gemini API) require function_response.name to be non-empty\n if (m.role === 'tool') {\n msg.name = m.name || 'tool';\n } else if (m.name) {\n msg.name = m.name;\n }\n return msg;\n }),\n stream: true,\n keep_alive: '30m',\n options: {\n // v4.10.0-local (cost cap): 8K cloud cap matches non-stream path\n num_predict: clampMaxTokens(options.model || 'ollama/llama3.1', options.maxTokens),\n num_ctx: getModelCtx(model),\n temperature: options.temperature ?? 0.7,\n },\n };\n\n // Model capabilities — adapts behavior per model family\n const caps = getModelCapabilities(model);\n\n // Thinking mode: respect explicit setting, otherwise use model capabilities.\n // Disable for models that don't support thinking — prevents 400 errors.\n if (options.thinking === false) {\n body.think = false;\n } else if (!caps.thinkingWithTools) {\n body.think = false;\n } else if (options.thinking === true) {\n body.think = true;\n }\n\n // Per-turn override for tool-role turns (see chat() for rationale: vLLM #39611 / Z.ai docs).\n if (hasToolRoleMessages && body.think !== false) {\n const priorIntent = body.think === undefined ? 'unset' : String(body.think);\n body.think = false;\n logger.info(COMPONENT, `[ToolTurnThinkOverride] (stream) Forcing think=false for ${model} (tool-role message present, caller intent=${priorIntent})`);\n }\n\n if (hasTools) {\n body.tools = options.tools!.map((t) => ({\n type: 'function',\n function: {\n name: t.function.name,\n description: t.function.description,\n parameters: simplifySchema(t.function.parameters),\n },\n }));\n // Per-model optimal sampling for tool calling\n (body.options as Record<string, unknown>).temperature = options.temperature ?? caps.toolTemperature ?? 0.5;\n if (caps.toolTopP) (body.options as Record<string, unknown>).top_p = caps.toolTopP;\n if (caps.toolTopK) (body.options as Record<string, unknown>).top_k = caps.toolTopK;\n\n // Force tool_choice when requested — skip for models that self-select well\n if (options.forceToolUse && !caps.selfSelectsTools) {\n body.tool_choice = 'required';\n }\n }\n\n // Ollama-native structured outputs (stream variant).\n if (options.format !== undefined) {\n body.format = options.format;\n }\n\n // Optimize: trim history preserving tool pairs (cloud models only — local models have smaller contexts)\n if (isCloudModel && hasTools) {\n const msgs = body.messages as Array<Record<string, unknown>>;\n if (msgs.length > 80) {\n const trimmed = trimPreservingToolPairs(msgs, 80);\n logger.info(COMPONENT, `[Stream] Cloud model context trim: ${msgs.length} → ${trimmed.length} messages`);\n body.messages = trimmed;\n }\n }\n // Merge system into first user message only for models that need it\n if (hasTools && caps.needsSystemMerge) {\n const msgs2 = body.messages as Array<Record<string, unknown>>;\n const sysIdx = msgs2.findIndex(m => m.role === 'system');\n const firstUserIdx = msgs2.findIndex(m => m.role === 'user');\n if (sysIdx >= 0 && firstUserIdx >= 0 && msgs2[sysIdx].content) {\n const sysContent = msgs2[sysIdx].content as string;\n msgs2[firstUserIdx].content = `[System Instructions]\\n${sysContent}\\n\\n[User Message]\\n${msgs2[firstUserIdx].content}`;\n msgs2.splice(sysIdx, 1);\n }\n }\n\n try {\n // Cloud models need longer timeouts for streaming too\n const streamTimeoutMs = isCloudModel ? 300_000 : 120_000;\n let response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n\n if (!response.ok || !response.body) {\n const errorText = await response.text();\n // Fallback: if model doesn't support native tool calling, retry without tools\n if (response.status === 400 && errorText.includes('does not support tools') && body.tools) {\n logger.warn(COMPONENT, `Model ${model} does not support native tool calling — running in chat-only mode`);\n delete body.tools;\n response = await fetch(`${this.baseUrl}/api/chat`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(streamTimeoutMs),\n });\n if (!response.ok || !response.body) {\n const retryText = await response.text();\n yield { type: 'error', error: `Ollama error (${response.status}): ${retryText}` };\n return;\n }\n } else {\n yield { type: 'error', error: `Ollama error (${response.status}): ${errorText}` };\n return;\n }\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let insideThink = false;\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.trim()) continue;\n try {\n const chunk = JSON.parse(line);\n // Handle thinking field for models that put content there\n // Some models (e.g. qwen3.5, nemotron-super:cloud) use the thinking field\n // even when think=false is set — treat thinking as content in that case\n if (!chunk.message?.content && chunk.message?.thinking) {\n if (body.think === false) {\n chunk.message.content = chunk.message.thinking;\n }\n }\n if (chunk.message?.content) {\n let text = chunk.message.content;\n // Strip leaked <think>...</think> blocks from Qwen/DeepSeek\n if (text.includes('<think>')) insideThink = true;\n if (insideThink) {\n if (text.includes('</think>')) {\n text = text.split('</think>').pop()?.trim() || '';\n insideThink = false;\n } else {\n continue; // suppress thinking content\n }\n }\n if (text) yield { type: 'text', content: text };\n }\n // v5.0.2: Only yield tool_calls if tools were sent in the request\n if (chunk.message?.tool_calls && options.tools && options.tools.length > 0) {\n for (const tc of chunk.message.tool_calls) {\n const fn = tc.function as Record<string, unknown>;\n yield {\n type: 'tool_call',\n toolCall: { id: uuid(), type: 'function', function: { name: fn.name as string, arguments: JSON.stringify(fn.arguments) } },\n };\n }\n }\n if (chunk.done) break;\n } catch { /* skip malformed NDJSON 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 try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n if (!response.ok) {\n // Hunt Finding #29 (2026-04-14): consume the body even on\n // error paths so the underlying socket can return to the\n // keep-alive pool. Without this, every non-200 response\n // leaks its socket until the GC gets around to it.\n await response.body?.cancel().catch(() => {});\n return [];\n }\n const data = await response.json() as { models?: Array<{ name: string }> };\n return (data.models || []).map((m) => m.name);\n } catch {\n return [];\n }\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n const response = await fetch(`${this.baseUrl}/api/tags`);\n const ok = response.ok;\n // Hunt Finding #29 (2026-04-14): ALWAYS consume or cancel the\n // body. Previously we returned response.ok directly, leaving the\n // body stream dangling and the socket held open.\n await response.body?.cancel().catch(() => {});\n return ok;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":["modelName"]}
@@ -7,6 +7,7 @@ import logger from "../utils/logger.js";
7
7
  import { fetchWithRetry } from "../utils/helpers.js";
8
8
  import { resolveApiKey } from "./authResolver.js";
9
9
  import { v4 as uuid } from "uuid";
10
+ import { clampMaxTokens } from "./modelCapabilities.js";
10
11
  const COMPONENT = "OpenAI";
11
12
  class OpenAIProvider extends LLMProvider {
12
13
  name = "openai";
@@ -51,9 +52,9 @@ class OpenAIProvider extends LLMProvider {
51
52
  })
52
53
  };
53
54
  if (isReasoningModel) {
54
- body.max_completion_tokens = options.maxTokens || 8192;
55
+ body.max_completion_tokens = clampMaxTokens(model, options.maxTokens);
55
56
  } else {
56
- body.max_tokens = options.maxTokens || 8192;
57
+ body.max_tokens = clampMaxTokens(model, options.maxTokens);
57
58
  }
58
59
  if (options.tools && options.tools.length > 0) {
59
60
  body.tools = options.tools;
@@ -147,7 +148,7 @@ class OpenAIProvider extends LLMProvider {
147
148
  if (isReasoningModel) {
148
149
  body.max_completion_tokens = options.maxTokens || 8192;
149
150
  } else {
150
- body.max_tokens = options.maxTokens || 8192;
151
+ body.max_tokens = clampMaxTokens(model, options.maxTokens);
151
152
  }
152
153
  if (options.tools && options.tools.length > 0) body.tools = options.tools;
153
154
  if (options.temperature !== void 0 && !isReasoningModel) body.temperature = options.temperature;