titan-agent 5.4.0 → 5.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/agent/agent.js +1 -1
  2. package/dist/agent/agent.js.map +1 -1
  3. package/dist/agent/agentLoop.js +77 -12
  4. package/dist/agent/agentLoop.js.map +1 -1
  5. package/dist/agent/agentWakeup.js +8 -3
  6. package/dist/agent/agentWakeup.js.map +1 -1
  7. package/dist/agent/commandPost.js +6 -1
  8. package/dist/agent/commandPost.js.map +1 -1
  9. package/dist/agent/heartbeatScheduler.js +36 -4
  10. package/dist/agent/heartbeatScheduler.js.map +1 -1
  11. package/dist/agent/toolRunner.js +30 -0
  12. package/dist/agent/toolRunner.js.map +1 -1
  13. package/dist/config/config.js +30 -8
  14. package/dist/config/config.js.map +1 -1
  15. package/dist/config/schema.js +10 -1
  16. package/dist/config/schema.js.map +1 -1
  17. package/dist/eval/record.js +1 -1
  18. package/dist/eval/record.js.map +1 -1
  19. package/dist/gateway/server.js +26 -0
  20. package/dist/gateway/server.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/skills/registry.js +176 -163
  39. package/dist/skills/registry.js.map +1 -1
  40. package/dist/telemetry/activityLog.js +1 -1
  41. package/dist/telemetry/activityLog.js.map +1 -1
  42. package/dist/utils/constants.js +2 -2
  43. package/dist/utils/constants.js.map +1 -1
  44. package/docs/AGENT-HIERARCHY.md +154 -0
  45. package/docs/superpowers/plans/2026-04-29-titan-production-fix.md +241 -0
  46. package/package.json +2 -2
  47. package/scripts/start-workers.sh +39 -0
  48. package/scripts/task-feeder.ts +38 -0
@@ -7,7 +7,89 @@ 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";
11
+ import { mkdirSync, writeFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
10
14
  const COMPONENT = "Google";
15
+ function shouldDumpRequestBody() {
16
+ if (process.env.GOOGLE_DUMP_REQUEST_BODY === "1" || process.env.GOOGLE_DUMP_REQUEST_BODY === "true") {
17
+ return true;
18
+ }
19
+ try {
20
+ const cfg = loadConfig();
21
+ const p = cfg.providers?.google;
22
+ return Boolean(p?.dumpRequestBody);
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ const GEMINI_DEBUG_DIR = join(homedir(), ".titan", "debug", "gemini-requests");
28
+ function dumpRequestBody(reason, body, extra) {
29
+ if (!shouldDumpRequestBody()) return;
30
+ try {
31
+ mkdirSync(GEMINI_DEBUG_DIR, { recursive: true });
32
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
33
+ const path = join(GEMINI_DEBUG_DIR, `${stamp}-${reason}.json`);
34
+ writeFileSync(path, JSON.stringify({ reason, body, ...extra ?? {} }, null, 2));
35
+ logger.info(COMPONENT, `Dumped Gemini request body \u2192 ${path}`);
36
+ } catch (err) {
37
+ logger.warn(COMPONENT, `Failed to dump Gemini request body: ${err.message}`);
38
+ }
39
+ }
40
+ function buildContents(messages) {
41
+ const toolCallNameById = /* @__PURE__ */ new Map();
42
+ for (const m of messages) {
43
+ if (m.role === "assistant" && Array.isArray(m.toolCalls)) {
44
+ for (const tc of m.toolCalls) {
45
+ if (tc.id && tc.function?.name) {
46
+ toolCallNameById.set(tc.id, tc.function.name);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ let corrections = 0;
52
+ const contents = [];
53
+ for (const m of messages.filter((x) => x.role !== "system")) {
54
+ if (m.role === "tool") {
55
+ const callId = m.toolCallId || "";
56
+ const recordedName = callId ? toolCallNameById.get(callId) : void 0;
57
+ const claimedName = (m.name || "").trim();
58
+ if (!recordedName) {
59
+ logger.warn(
60
+ COMPONENT,
61
+ `Malformed tool message: tool_call_id="${callId}" has no matching prior tool_call. name="${claimedName}". Dropping to prevent Gemini 400.`
62
+ );
63
+ corrections++;
64
+ continue;
65
+ }
66
+ const finalName = claimedName || recordedName;
67
+ if (!claimedName) {
68
+ logger.warn(
69
+ COMPONENT,
70
+ `Tool message missing name for tool_call_id="${callId}"; inferred "${finalName}" from assistant history.`
71
+ );
72
+ corrections++;
73
+ } else if (claimedName !== recordedName) {
74
+ logger.warn(
75
+ COMPONENT,
76
+ `Tool message name mismatch for tool_call_id="${callId}": claimed "${claimedName}" but tool_call recorded "${recordedName}". Using recorded.`
77
+ );
78
+ corrections++;
79
+ }
80
+ contents.push({
81
+ role: "function",
82
+ parts: [{ functionResponse: { name: recordedName, response: { result: m.content } } }]
83
+ });
84
+ continue;
85
+ }
86
+ contents.push({
87
+ role: m.role === "assistant" ? "model" : "user",
88
+ parts: [{ text: m.content }]
89
+ });
90
+ }
91
+ return { contents, corrections };
92
+ }
11
93
  class GoogleProvider extends LLMProvider {
12
94
  name = "google";
13
95
  displayName = "Google (Gemini)";
@@ -22,22 +104,14 @@ class GoogleProvider extends LLMProvider {
22
104
  if (!apiKey) throw new Error("Google API key not configured");
23
105
  logger.debug(COMPONENT, `Chat request: model=${model}, messages=${options.messages.length}`);
24
106
  const systemInstruction = options.messages.find((m) => m.role === "system")?.content;
25
- const contents = options.messages.filter((m) => m.role !== "system").map((m) => {
26
- if (m.role === "tool") {
27
- return {
28
- role: "function",
29
- parts: [{ functionResponse: { name: m.name || "tool", response: { result: m.content } } }]
30
- };
31
- }
32
- return {
33
- role: m.role === "assistant" ? "model" : "user",
34
- parts: [{ text: m.content }]
35
- };
36
- });
107
+ const { contents, corrections } = buildContents(options.messages);
108
+ if (corrections > 0) {
109
+ logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before sending to Gemini.`);
110
+ }
37
111
  const body = {
38
112
  contents,
39
113
  generationConfig: {
40
- maxOutputTokens: options.maxTokens || 8192,
114
+ maxOutputTokens: clampMaxTokens(options.model || "google/gemini-2.0-flash", options.maxTokens),
41
115
  temperature: options.temperature ?? 0.7
42
116
  }
43
117
  };
@@ -64,6 +138,7 @@ class GoogleProvider extends LLMProvider {
64
138
  });
65
139
  if (!response.ok) {
66
140
  const errorText = await response.text();
141
+ dumpRequestBody(`http-${response.status}`, body, { errorText, model });
67
142
  const { createProviderError } = await import("./errorTaxonomy.js");
68
143
  throw createProviderError("Google API", response, errorText, { provider: "google", model });
69
144
  }
@@ -112,15 +187,13 @@ class GoogleProvider extends LLMProvider {
112
187
  return;
113
188
  }
114
189
  const systemInstruction = options.messages.find((m) => m.role === "system")?.content;
115
- const contents = options.messages.filter((m) => m.role !== "system").map((m) => {
116
- if (m.role === "tool") {
117
- return { role: "function", parts: [{ functionResponse: { name: m.name || "tool", response: { result: m.content } } }] };
118
- }
119
- return { role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] };
120
- });
190
+ const { contents, corrections } = buildContents(options.messages);
191
+ if (corrections > 0) {
192
+ logger.warn(COMPONENT, `Applied ${corrections} tool-message correction(s) before streaming to Gemini.`);
193
+ }
121
194
  const body = {
122
195
  contents,
123
- generationConfig: { maxOutputTokens: options.maxTokens || 8192, temperature: options.temperature ?? 0.7 }
196
+ generationConfig: { maxOutputTokens: clampMaxTokens(options.model || "google/gemini-2.0-flash", options.maxTokens), temperature: options.temperature ?? 0.7 }
124
197
  };
125
198
  if (systemInstruction) body.systemInstruction = { parts: [{ text: systemInstruction }] };
126
199
  if (options.tools && options.tools.length > 0) {
@@ -135,6 +208,7 @@ class GoogleProvider extends LLMProvider {
135
208
  });
136
209
  if (!response.ok || !response.body) {
137
210
  const errorText = await response.text();
211
+ dumpRequestBody(`stream-http-${response.status}`, body, { errorText, model });
138
212
  yield { type: "error", error: `Google API error (${response.status}): ${errorText}` };
139
213
  return;
140
214
  }
@@ -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
  }