melchizedek-agents 0.18.0 → 0.20.0

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 (293) hide show
  1. package/.env.example +12 -0
  2. package/CHANGELOG.md +1001 -0
  3. package/DOCUMENTATION.md +252 -65
  4. package/QUICKSTART.md +9 -6
  5. package/README.md +42 -4
  6. package/config/agents/examples/harness.yaml +3 -1
  7. package/config/agents/examples/model_zoo.yaml +10 -9
  8. package/config/agents/syndicateSchema.yaml +53 -16
  9. package/config/agents/templates/account_memory.yaml +2 -6
  10. package/config/agents/templates/case_desk.yaml +3 -9
  11. package/config/agents/templates/draft_review.yaml +3 -9
  12. package/config/agents/templates/intake_extractor.yaml +1 -3
  13. package/config/agents/templates/research_brief.yaml +3 -9
  14. package/config/agents/templates/research_desk.yaml +3 -9
  15. package/config/agents/templates/review_panel.yaml +4 -12
  16. package/config/agents/templates/support_triage.yaml +4 -12
  17. package/config/agents/templates/systems_operator.yaml +2 -6
  18. package/db/hardening.sql +7 -1
  19. package/db/migrations/0013_tool_credentials.sql +260 -0
  20. package/dist/lib/a2a/app.d.ts +70 -6
  21. package/dist/lib/a2a/app.js +107 -2
  22. package/dist/lib/a2a/executor.d.ts +20 -3
  23. package/dist/lib/a2a/executor.js +66 -7
  24. package/dist/lib/a2a/remoteAgent.d.ts +24 -9
  25. package/dist/lib/a2a/remoteAgent.js +31 -14
  26. package/dist/lib/adkPeer.d.ts +63 -0
  27. package/dist/lib/adkPeer.js +240 -0
  28. package/dist/lib/compile.d.ts +159 -30
  29. package/dist/lib/compile.js +215 -134
  30. package/dist/lib/compileAdk.d.ts +32 -0
  31. package/dist/lib/compileAdk.js +142 -0
  32. package/dist/lib/compileNative.d.ts +111 -0
  33. package/dist/lib/compileNative.js +278 -0
  34. package/dist/lib/config.d.ts +6 -5
  35. package/dist/lib/config.js +6 -5
  36. package/dist/lib/doctor.d.ts +28 -1
  37. package/dist/lib/doctor.js +64 -2
  38. package/dist/lib/index.d.ts +6 -1
  39. package/dist/lib/index.js +7 -1
  40. package/dist/lib/loadSyndicate.d.ts +18 -0
  41. package/dist/lib/memory/erase.d.ts +5 -3
  42. package/dist/lib/memory/erase.js +5 -2
  43. package/dist/lib/memory/namespace.d.ts +8 -1
  44. package/dist/lib/memory/namespace.js +12 -6
  45. package/dist/lib/memory/providers.js +2 -1
  46. package/dist/lib/memory/supabaseMemoryService.d.ts +37 -3
  47. package/dist/lib/memory/supabaseMemoryService.js +42 -2
  48. package/dist/lib/model.d.ts +47 -0
  49. package/dist/lib/model.js +37 -0
  50. package/dist/lib/models/adapterResolver.d.ts +115 -0
  51. package/dist/lib/models/adapterResolver.js +137 -0
  52. package/dist/lib/models/adkGeminiAdapter.d.ts +90 -0
  53. package/dist/lib/models/adkGeminiAdapter.js +369 -0
  54. package/dist/lib/models/adkShim.d.ts +111 -0
  55. package/dist/lib/models/adkShim.js +136 -0
  56. package/dist/lib/models/capabilities.d.ts +3 -0
  57. package/dist/lib/models/capabilities.js +33 -23
  58. package/dist/lib/models/chatCompletionsAdapter.d.ts +279 -0
  59. package/dist/lib/models/chatCompletionsAdapter.js +854 -0
  60. package/dist/lib/models/circuitBreaker.d.ts +42 -0
  61. package/dist/lib/models/circuitBreaker.js +70 -0
  62. package/dist/lib/models/claudeAdapter.d.ts +103 -0
  63. package/dist/lib/models/claudeAdapter.js +669 -0
  64. package/dist/lib/models/claudeLlm.d.ts +39 -62
  65. package/dist/lib/models/claudeLlm.js +45 -350
  66. package/dist/lib/models/claudeModels.d.ts +127 -0
  67. package/dist/lib/models/claudeModels.js +237 -0
  68. package/dist/lib/models/contract.d.ts +395 -0
  69. package/dist/lib/models/contract.js +39 -0
  70. package/dist/lib/models/endpoints.js +2 -1
  71. package/dist/lib/models/errorResponse.d.ts +50 -0
  72. package/dist/lib/models/errorResponse.js +48 -0
  73. package/dist/lib/models/fallback.d.ts +18 -11
  74. package/dist/lib/models/fallback.js +54 -43
  75. package/dist/lib/models/fallbackAdapter.d.ts +55 -0
  76. package/dist/lib/models/fallbackAdapter.js +62 -0
  77. package/dist/lib/models/gateway.js +2 -1
  78. package/dist/lib/models/gatewayAdapter.d.ts +51 -0
  79. package/dist/lib/models/gatewayAdapter.js +81 -0
  80. package/dist/lib/models/gatewayLlm.d.ts +12 -33
  81. package/dist/lib/models/gatewayLlm.js +16 -77
  82. package/dist/lib/models/geminiAdapter.d.ts +125 -0
  83. package/dist/lib/models/geminiAdapter.js +854 -0
  84. package/dist/lib/models/geminiState.d.ts +46 -0
  85. package/dist/lib/models/geminiState.js +51 -0
  86. package/dist/lib/models/genaiMapping.d.ts +283 -0
  87. package/dist/lib/models/genaiMapping.js +965 -0
  88. package/dist/lib/models/gptAdapter.d.ts +240 -0
  89. package/dist/lib/models/gptAdapter.js +804 -0
  90. package/dist/lib/models/gptLlm.d.ts +63 -123
  91. package/dist/lib/models/gptLlm.js +89 -603
  92. package/dist/lib/models/grokAdapter.d.ts +86 -0
  93. package/dist/lib/models/grokAdapter.js +142 -0
  94. package/dist/lib/models/grokLlm.d.ts +22 -45
  95. package/dist/lib/models/grokLlm.js +24 -69
  96. package/dist/lib/models/kimiAdapter.d.ts +157 -0
  97. package/dist/lib/models/kimiAdapter.js +203 -0
  98. package/dist/lib/models/kimiLlm.d.ts +15 -107
  99. package/dist/lib/models/kimiLlm.js +19 -155
  100. package/dist/lib/models/ollamaAdapter.d.ts +125 -0
  101. package/dist/lib/models/ollamaAdapter.js +155 -0
  102. package/dist/lib/models/ollamaLlm.d.ts +11 -107
  103. package/dist/lib/models/ollamaLlm.js +15 -153
  104. package/dist/lib/models/openAiCompatibleLlm.d.ts +73 -216
  105. package/dist/lib/models/openAiCompatibleLlm.js +109 -671
  106. package/dist/lib/models/providerState.d.ts +63 -0
  107. package/dist/lib/models/providerState.js +61 -0
  108. package/dist/lib/models/reasoning.d.ts +26 -0
  109. package/dist/lib/models/reasoning.js +59 -0
  110. package/dist/lib/models/registry.d.ts +62 -35
  111. package/dist/lib/models/registry.js +100 -124
  112. package/dist/lib/models/retry.d.ts +2 -1
  113. package/dist/lib/models/retry.js +3 -2
  114. package/dist/lib/models/schemaNormalize.d.ts +98 -0
  115. package/dist/lib/models/schemaNormalize.js +321 -1
  116. package/dist/lib/models/tracedGemini.d.ts +41 -0
  117. package/dist/lib/models/tracedGemini.js +98 -0
  118. package/dist/lib/models/urls.d.ts +11 -0
  119. package/dist/lib/models/urls.js +16 -0
  120. package/dist/lib/net/redirects.d.ts +21 -2
  121. package/dist/lib/net/redirects.js +11 -34
  122. package/dist/lib/observability/audit.d.ts +5 -1
  123. package/dist/lib/observability/lineage.d.ts +14 -0
  124. package/dist/lib/observability/lineage.js +33 -0
  125. package/dist/lib/observability/supabaseSpanExporter.d.ts +10 -4
  126. package/dist/lib/observability/supabaseSpanExporter.js +17 -9
  127. package/dist/lib/observability/tracer.d.ts +26 -8
  128. package/dist/lib/observability/tracer.js +43 -18
  129. package/dist/lib/runtime/adkMemoryBridge.d.ts +46 -0
  130. package/dist/lib/runtime/adkMemoryBridge.js +68 -0
  131. package/dist/lib/runtime/adkSessionBridge.d.ts +78 -0
  132. package/dist/lib/runtime/adkSessionBridge.js +176 -0
  133. package/dist/lib/runtime/approvals.d.ts +5 -0
  134. package/dist/lib/runtime/approvals.js +39 -8
  135. package/dist/lib/runtime/credentials.d.ts +61 -0
  136. package/dist/lib/runtime/credentials.js +67 -0
  137. package/dist/lib/runtime/events.d.ts +229 -0
  138. package/dist/lib/runtime/events.js +274 -0
  139. package/dist/lib/runtime/logging.d.ts +32 -0
  140. package/dist/lib/runtime/logging.js +55 -0
  141. package/dist/lib/runtime/memoryService.d.ts +72 -0
  142. package/dist/lib/runtime/memoryService.js +25 -0
  143. package/dist/lib/runtime/native/agentLoop.d.ts +209 -0
  144. package/dist/lib/runtime/native/agentLoop.js +705 -0
  145. package/dist/lib/runtime/native/compaction.d.ts +103 -0
  146. package/dist/lib/runtime/native/compaction.js +191 -0
  147. package/dist/lib/runtime/native/delegate.d.ts +151 -0
  148. package/dist/lib/runtime/native/delegate.js +256 -0
  149. package/dist/lib/runtime/native/history.d.ts +83 -0
  150. package/dist/lib/runtime/native/history.js +312 -0
  151. package/dist/lib/runtime/native/interrupts.d.ts +129 -0
  152. package/dist/lib/runtime/native/interrupts.js +310 -0
  153. package/dist/lib/runtime/native/request.d.ts +215 -0
  154. package/dist/lib/runtime/native/request.js +585 -0
  155. package/dist/lib/runtime/native/selfCorrection.d.ts +164 -0
  156. package/dist/lib/runtime/native/selfCorrection.js +327 -0
  157. package/dist/lib/runtime/native/step.d.ts +156 -0
  158. package/dist/lib/runtime/native/step.js +263 -0
  159. package/dist/lib/runtime/native/taskMode.d.ts +64 -0
  160. package/dist/lib/runtime/native/taskMode.js +126 -0
  161. package/dist/lib/runtime/native/telemetry.d.ts +75 -0
  162. package/dist/lib/runtime/native/telemetry.js +299 -0
  163. package/dist/lib/runtime/native/tempState.d.ts +31 -0
  164. package/dist/lib/runtime/native/tempState.js +56 -0
  165. package/dist/lib/runtime/nativeTurn.d.ts +86 -0
  166. package/dist/lib/runtime/nativeTurn.js +107 -0
  167. package/dist/lib/runtime/questions.d.ts +29 -7
  168. package/dist/lib/runtime/questions.js +47 -32
  169. package/dist/lib/runtime/runtimeFlag.d.ts +38 -0
  170. package/dist/lib/runtime/runtimeFlag.js +58 -0
  171. package/dist/lib/runtime/sessions.d.ts +166 -0
  172. package/dist/lib/runtime/sessions.js +201 -0
  173. package/dist/lib/runtime/syndicateTurn.d.ts +46 -5
  174. package/dist/lib/runtime/syndicateTurn.js +233 -36
  175. package/dist/lib/runtime/valueDepth.d.ts +23 -0
  176. package/dist/lib/runtime/valueDepth.js +41 -0
  177. package/dist/lib/session/supabaseSessionService.d.ts +65 -18
  178. package/dist/lib/session/supabaseSessionService.js +148 -105
  179. package/dist/lib/session/transcript.d.ts +69 -23
  180. package/dist/lib/session/transcript.js +105 -29
  181. package/dist/lib/skills.js +33 -7
  182. package/dist/lib/storage/postgres/credentialStore.d.ts +20 -0
  183. package/dist/lib/storage/postgres/credentialStore.js +82 -0
  184. package/dist/lib/storage/postgres/index.d.ts +12 -0
  185. package/dist/lib/storage/postgres/index.js +6 -1
  186. package/dist/lib/storage/postgres/sessionService.d.ts +44 -16
  187. package/dist/lib/storage/postgres/sessionService.js +128 -66
  188. package/dist/lib/syndicateSchema.d.ts +49 -0
  189. package/dist/lib/syndicateSchema.js +99 -9
  190. package/dist/lib/toolRegistry.d.ts +26 -2
  191. package/dist/lib/toolRegistry.js +73 -17
  192. package/dist/lib/tools/adkTool.d.ts +81 -0
  193. package/dist/lib/tools/adkTool.js +268 -0
  194. package/dist/lib/tools/auth.d.ts +118 -0
  195. package/dist/lib/tools/auth.js +89 -0
  196. package/dist/lib/tools/collectionsSearchTool.d.ts +8 -7
  197. package/dist/lib/tools/collectionsSearchTool.js +12 -27
  198. package/dist/lib/tools/credentialCipher.d.ts +58 -0
  199. package/dist/lib/tools/credentialCipher.js +116 -0
  200. package/dist/lib/tools/credentialStore.d.ts +77 -0
  201. package/dist/lib/tools/credentialStore.js +246 -0
  202. package/dist/lib/tools/examples.d.ts +43 -0
  203. package/dist/lib/tools/examples.js +62 -0
  204. package/dist/lib/tools/generateImageTool.d.ts +15 -6
  205. package/dist/lib/tools/generateImageTool.js +19 -29
  206. package/dist/lib/tools/inspectImageTool.d.ts +9 -6
  207. package/dist/lib/tools/inspectImageTool.js +13 -17
  208. package/dist/lib/tools/mcpToolFactory.d.ts +42 -1
  209. package/dist/lib/tools/mcpToolFactory.js +46 -47
  210. package/dist/lib/tools/memoryTools.d.ts +61 -0
  211. package/dist/lib/tools/memoryTools.js +119 -0
  212. package/dist/lib/tools/nativeTools.d.ts +38 -0
  213. package/dist/lib/tools/nativeTools.js +43 -0
  214. package/dist/lib/tools/oauthConsent.d.ts +181 -0
  215. package/dist/lib/tools/oauthConsent.js +330 -0
  216. package/dist/lib/tools/openapi/call.d.ts +108 -0
  217. package/dist/lib/tools/openapi/call.js +295 -0
  218. package/dist/lib/tools/openapi/parse.d.ts +147 -0
  219. package/dist/lib/tools/openapi/parse.js +570 -0
  220. package/dist/lib/tools/openapiTools.d.ts +25 -17
  221. package/dist/lib/tools/openapiTools.js +124 -121
  222. package/dist/lib/tools/scienceTools.d.ts +14 -14
  223. package/dist/lib/tools/skillToolset.d.ts +58 -87
  224. package/dist/lib/tools/skillToolset.js +61 -142
  225. package/dist/lib/tools/skills/env.d.ts +58 -0
  226. package/dist/lib/tools/skills/env.js +165 -0
  227. package/dist/lib/tools/skills/executor.d.ts +114 -0
  228. package/dist/lib/tools/skills/executor.js +331 -0
  229. package/dist/lib/tools/skills/frontmatter.d.ts +79 -0
  230. package/dist/lib/tools/skills/frontmatter.js +215 -0
  231. package/dist/lib/tools/skills/loader.d.ts +91 -0
  232. package/dist/lib/tools/skills/loader.js +257 -0
  233. package/dist/lib/tools/skills/tools.d.ts +187 -0
  234. package/dist/lib/tools/skills/tools.js +386 -0
  235. package/dist/lib/tools/taskTools.d.ts +15 -15
  236. package/dist/lib/tools/tool.d.ts +354 -0
  237. package/dist/lib/tools/tool.js +304 -0
  238. package/dist/lib/tools/toolContract.d.ts +89 -33
  239. package/dist/lib/tools/toolContract.js +104 -82
  240. package/dist/lib/tools/urlContextTool.d.ts +4 -1
  241. package/dist/lib/tools/urlContextTool.js +6 -2
  242. package/dist/lib/tools/webExtractTool.d.ts +3 -3
  243. package/dist/lib/tools/webExtractTool.js +2 -1
  244. package/dist/lib/tools/webSearchTool.d.ts +7 -10
  245. package/dist/lib/tools/webSearchTool.js +15 -60
  246. package/dist/lib/tools/wikiTools.d.ts +19 -19
  247. package/dist/lib/tools/wikiTools.js +13 -9
  248. package/dist/lib/tools/xApiSearchTool.d.ts +3 -3
  249. package/dist/lib/tools/xApiSearchTool.js +2 -1
  250. package/dist/lib/tools/xSearchTool.d.ts +8 -10
  251. package/dist/lib/tools/xSearchTool.js +12 -63
  252. package/dist/lib/tools/xaiSearchParams.d.ts +41 -0
  253. package/dist/lib/tools/xaiSearchParams.js +126 -0
  254. package/dist/lib/wiki/agentRun.d.ts +12 -0
  255. package/dist/lib/wiki/agentRun.js +74 -35
  256. package/dist/lib/wiki/entities.d.ts +12 -0
  257. package/dist/lib/wiki/entities.js +45 -3
  258. package/dist/lib/workflow/agentNode.d.ts +153 -0
  259. package/dist/lib/workflow/agentNode.js +365 -0
  260. package/dist/lib/workflow/graph.d.ts +178 -0
  261. package/dist/lib/workflow/graph.js +468 -0
  262. package/dist/lib/workflow/nodeEvents.d.ts +50 -0
  263. package/dist/lib/workflow/nodeEvents.js +101 -0
  264. package/dist/lib/workflow/pause.d.ts +122 -0
  265. package/dist/lib/workflow/pause.js +180 -0
  266. package/dist/lib/workflow/resume.d.ts +166 -0
  267. package/dist/lib/workflow/resume.js +441 -0
  268. package/dist/lib/workflow/route.d.ts +58 -0
  269. package/dist/lib/workflow/route.js +69 -0
  270. package/dist/lib/workflow/scheduler.d.ts +418 -0
  271. package/dist/lib/workflow/scheduler.js +756 -0
  272. package/dist/lib/workflow/toolNode.d.ts +127 -0
  273. package/dist/lib/workflow/toolNode.js +236 -0
  274. package/dist/lib/workflow/turn.d.ts +118 -0
  275. package/dist/lib/workflow/turn.js +216 -0
  276. package/dist/lib/workflow.d.ts +33 -11
  277. package/dist/lib/workflow.js +49 -27
  278. package/dist/lib/workflowConfig.d.ts +7 -2
  279. package/dist/lib/workflowConfig.js +7 -12
  280. package/dist/scripts/a2a_server.js +4 -3
  281. package/dist/scripts/assistant_worker.d.ts +2 -1
  282. package/dist/scripts/assistant_worker.js +9 -4
  283. package/dist/scripts/doctor.d.ts +3 -1
  284. package/dist/scripts/doctor.js +4 -2
  285. package/dist/scripts/syndicate_chat.js +19 -10
  286. package/package.json +9 -3
  287. package/skills/briefs/melchizedek-author.md +3 -2
  288. package/skills/briefs/melchizedek-models.md +13 -9
  289. package/skills/briefs/melchizedek.md +3 -3
  290. package/skills/melchizedek/SKILL.md +2 -1
  291. package/skills/melchizedek-author/SKILL.md +2 -1
  292. package/skills/melchizedek-author/assets/minimal.yaml +2 -0
  293. package/skills/melchizedek-models/SKILL.md +23 -6
@@ -1,692 +1,130 @@
1
1
  /**
2
- * lib/models/openAiCompatibleLlm.ts — shared base class for every provider
3
- * that speaks the OpenAI chat-completions wire format.
2
+ * lib/models/openAiCompatibleLlm.ts — the ADK shim base for every
3
+ * chat-completions adapter (Ollama, Kimi, the gateway).
4
4
  *
5
5
  * WHY this file exists:
6
- * Separates the OpenAI chat-completions wire translation (ADK Content →
7
- * chat messages, tool-definition building, tool_call parsing, usage
8
- * accounting, reasoning surfacing) from provider specifics, so any
9
- * /chat/completions-speaking provider is a small subclass supplying only
10
- * the endpoint, auth headers, wire model name, and extra body fields.
11
- * Current subclass: Ollama (local, keyless). xAI Grok used to live here
12
- * too, but xAI's Agent Tools API is Responses-shaped, so GrokLlm now
13
- * subclasses GptLlm instead (see lib/models/grokLlm.ts).
6
+ * The chat-completions translation lives on the engine's own model
7
+ * contract, in lib/models/chatCompletionsAdapter.ts and one adapter per
8
+ * provider (ollamaAdapter.ts, kimiAdapter.ts, gatewayAdapter.ts). ADK
9
+ * still runs every turn (ADR 0045), so each adapter runs behind an ADK
10
+ * BaseLlm: OllamaLlm, KimiLlm and GatewayLlm, subclasses of this class,
11
+ * which is an AdkShim (lib/models/adkShim.ts, ADR 0053). The shim charges
12
+ * the turn, opens the llm.request span and maps the request and responses;
13
+ * the adapter does the provider's work.
14
14
  *
15
- * WHAT THE BASE PROVIDES (uniformly, for every subclass):
16
- * - Tool schemas normalized to lowercase JSON-Schema types
17
- * (lib/models/schemaNormalize.ts — the ADK/Gemini dialect is uppercase).
18
- * - usage → LlmResponse.usageMetadata mapping (prompt_tokens →
19
- * promptTokenCount, completion_tokens → candidatesTokenCount,
20
- * completion_tokens_details.reasoning_tokens → thoughtsTokenCount),
21
- * so traceAgentRun / traceLlmGeneration count tokens for every provider.
22
- * - Reasoning ("<think>…</think>" blocks, reasoning_content fields)
23
- * surfaced as a { text, thought: true } part in a partial response —
24
- * printers display it dimmed; it never enters session history.
25
- * - A per-request `llm.request` OpenTelemetry span (lib/observability).
26
- * - Retries of transient failures (lib/models/retry.ts) before any byte
27
- * is yielded, and HTTP errors carrying `status` + `retryable`.
28
- * - web_search handling: subclasses with native search return its body
29
- * fields from webSearchBodyFields(); those without return null and the
30
- * base omits the tool with a one-time warning + span attribute.
15
+ * WHAT THIS CLASS ADDS TO THE SHIM (ADR 0057): the ADK path stays what it
16
+ * was before the adapters moved onto the contract.
17
+ * - The request: an agent's older generateContentConfig spelling that the
18
+ * contract leaves out rides beside it (olderSpellingOf): an effort word
19
+ * that is no contract level (Kimi K3's `max`). JSON mode without a
20
+ * schema is the contract's `outputFormat` (ADR 0061).
21
+ * - The response: the final keeps the shape these classes always yielded,
22
+ * which the ledger and FallbackLlm read. usageMetadata counts the
23
+ * reasoning inside candidatesTokenCount, as the provider's
24
+ * completion_tokens does, so llm.tokens.output and the turn's charge are
25
+ * unchanged (the shim's default is Gemini's meaning, which excludes it).
26
+ * finishReason is set only for a reply cut short (MAX_TOKENS); a final
27
+ * with no parts keeps its empty content; an HTTP failure keeps its
28
+ * top-level `status` and `retryable` beside the verdict in
29
+ * customMetadata.
30
+ *
31
+ * A /chat/completions provider of your own is a ChatCompletionsAdapter
32
+ * subclass (the endpoint, headers, wire name, reasoning field and error
33
+ * wording), run under ADK as `adkShim(adapter)` or as a subclass of this
34
+ * class.
31
35
  */
32
- import { BaseLlm } from '@google/adk';
33
36
  import { FinishReason } from '@google/genai';
34
- import { traceLlmGeneration, setLlmSpanAttribute, } from '../observability/tracer.js';
35
- import { wantsWebSearch, isWebSearchSentinel, } from '../tools/webSearchTool.js';
36
- import { currentTurnSignal } from '../runtime/turnControl.js';
37
- import { toStrictJsonSchema, toolDeclarationFor } from './schemaNormalize.js';
38
- import { fetchWithRetry, isRetryableStatus } from './retry.js';
39
- const OPEN_THINK = '<think>';
40
- const CLOSE_THINK = '</think>';
37
+ import { AdkShim } from './adkShim.js';
38
+ import { setLlmSpanAttribute } from '../observability/tracer.js';
39
+ import { isRetryableStatus } from './retry.js';
40
+ import { statusDecision, withRetryVerdict } from './errorResponse.js';
41
+ export { REASONING_CONTENT_KIND, splitThinkBlocks, ThinkStreamSplitter } from './chatCompletionsAdapter.js';
42
+ /** The effort words a contract ReasoningSetting carries (genaiMapping's reasoningOf); any other rides as the older spelling. */
43
+ const CONTRACT_EFFORT_WORDS = new Set(['none', 'minimal', 'low', 'medium', 'high']);
41
44
  /**
42
- * Splits "<think>…</think>" scratchpad from the reply.
43
- *
44
- * A block the model never closed is scratchpad too: it means the model ran
45
- * out of budget mid-thought, so everything after the opening tag is
46
- * reasoning — never reply text with a raw "<think>" in it.
45
+ * What a generateContentConfig asks of a chat-completions provider that the
46
+ * contract has no field for, or undefined when it asks nothing more:
47
+ * `reasoningEffort` when it is a word no ReasoningSetting carries (`max`,
48
+ * `xhigh`), sent as written. JSON mode without a schema is no longer here:
49
+ * the genai mapping reads it as `outputFormat: 'json'` (ADR 0061).
47
50
  */
48
- export function splitThinkBlocks(text) {
49
- const blocks = [];
50
- let answer = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
51
- blocks.push(inner.trim());
52
- return '';
53
- });
54
- const unclosed = answer.indexOf(OPEN_THINK);
55
- if (unclosed !== -1) {
56
- const inner = answer.slice(unclosed + OPEN_THINK.length).trim();
57
- if (inner)
58
- blocks.push(inner);
59
- answer = answer.slice(0, unclosed).trimEnd();
60
- }
61
- return { reasoning: blocks.join('\n\n'), answer: answer.trimStart() };
62
- }
63
- /** Length of the longest suffix of `s` that is a proper prefix of `tag`. */
64
- function partialTagSuffix(s, tag) {
65
- const max = Math.min(s.length, tag.length - 1);
66
- for (let n = max; n > 0; n--) {
67
- if (s.endsWith(tag.slice(0, n)))
68
- return n;
69
- }
70
- return 0;
51
+ export function olderSpellingOf(config) {
52
+ const cfg = (config ?? {});
53
+ const out = {};
54
+ const effort = cfg.reasoningEffort;
55
+ if (typeof effort === 'string' && !CONTRACT_EFFORT_WORDS.has(effort))
56
+ out.reasoningEffort = effort;
57
+ return Object.keys(out).length > 0 ? out : undefined;
71
58
  }
72
59
  /**
73
- * Incremental counterpart to splitThinkBlocks, for the SSE path.
74
- *
75
- * WHY a state machine rather than the regex: on a stream the closing
76
- * `</think>` has usually NOT arrived yet, so the regex matches nothing and
77
- * the whole scratchpad would be printed as the reply. A tag can also be
78
- * split mid-delta ("<thi" + "nk>"), so any tail that could still grow into
79
- * a tag is held back instead of being emitted as answer text.
80
- *
81
- * Providers that report reasoning in a discrete field (Ollama's
82
- * `delta.reasoning`, others' `reasoning_content`) never open a think block
83
- * and pass straight through this untouched.
60
+ * Usage as the chat-completions classes have always reported it on the ADK
61
+ * path: candidatesTokenCount is the provider's completion_tokens, reasoning
62
+ * included, and thoughtsTokenCount the reasoning part of it.
84
63
  */
85
- export class ThinkStreamSplitter {
86
- inThink = false;
87
- held = '';
88
- /** Routes one content delta into reasoning/answer text. */
89
- push(chunk) {
90
- let buf = this.held + chunk;
91
- let reasoning = '';
92
- let answer = '';
93
- for (;;) {
94
- const tag = this.inThink ? CLOSE_THINK : OPEN_THINK;
95
- const at = buf.indexOf(tag);
96
- if (at === -1)
97
- break;
98
- if (this.inThink)
99
- reasoning += buf.slice(0, at);
100
- else
101
- answer += buf.slice(0, at);
102
- buf = buf.slice(at + tag.length);
103
- this.inThink = !this.inThink;
104
- }
105
- // Hold back only what could still become the tag we're watching for.
106
- const pending = this.inThink ? CLOSE_THINK : OPEN_THINK;
107
- const keep = partialTagSuffix(buf, pending);
108
- this.held = keep > 0 ? buf.slice(buf.length - keep) : '';
109
- const emit = keep > 0 ? buf.slice(0, buf.length - keep) : buf;
110
- if (this.inThink)
111
- reasoning += emit;
112
- else
113
- answer += emit;
114
- return { reasoning, answer };
115
- }
116
- /** Releases the held tail; the stream ended, so it was never a tag. */
117
- flush() {
118
- const rest = this.held;
119
- this.held = '';
120
- return this.inThink
121
- ? { reasoning: rest, answer: '' }
122
- : { reasoning: '', answer: rest };
123
- }
124
- }
125
- /** Yields parsed `data:` payloads from an SSE response body. */
126
- async function* sseChunks(res) {
127
- const reader = res.body?.getReader?.();
128
- if (!reader)
129
- return;
130
- const decoder = new TextDecoder();
131
- let buf = '';
132
- for (;;) {
133
- const { value, done } = await reader.read();
134
- if (done)
135
- break;
136
- buf += decoder.decode(value, { stream: true });
137
- let nl;
138
- while ((nl = buf.indexOf('\n')) !== -1) {
139
- const line = buf.slice(0, nl).trim();
140
- buf = buf.slice(nl + 1);
141
- // Blank separators and ":" keep-alive comments carry no payload.
142
- if (!line.startsWith('data:'))
143
- continue;
144
- const payload = line.slice('data:'.length).trim();
145
- if (payload === '[DONE]')
146
- return;
147
- try {
148
- yield JSON.parse(payload);
149
- }
150
- catch {
151
- /* a chunk that isn't valid JSON is not worth killing the turn for */
152
- }
153
- }
154
- }
64
+ export function chatUsageMetadata(usage) {
65
+ return {
66
+ promptTokenCount: usage.inputTokens,
67
+ candidatesTokenCount: usage.outputTokens,
68
+ ...(usage.thinkingTokens !== undefined ? { thoughtsTokenCount: usage.thinkingTokens } : {}),
69
+ totalTokenCount: usage.inputTokens + usage.outputTokens,
70
+ };
155
71
  }
156
72
  // ── OpenAiCompatibleLlm ──────────────────────────────────────────────────────
157
- export class OpenAiCompatibleLlm extends BaseLlm {
158
- webSearchWarned = false;
159
- retryWarned = false;
160
- /** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
161
- wireModelName() {
162
- return this.model;
163
- }
164
- /**
165
- * How the request reaches the provider: 'direct' (the provider's own
166
- * endpoint) or 'gateway:<id>' (lib/models/gatewayLlm.ts). Recorded on the
167
- * llm.request span as llm.transport so the ledger can tell a native call
168
- * from a proxied one while llm.provider keeps the upstream attribution.
169
- */
170
- transport() {
171
- return 'direct';
172
- }
173
- /** Provider-specific request body fields (merged last). */
174
- /** Whether the endpoint accepts response_format json_schema (strict). */
175
- supportsJsonSchemaFormat() {
176
- return true;
177
- }
178
- /**
179
- * Whether the endpoint honors stream_options.include_usage. Without it an
180
- * SSE turn reports no token counts at all; Ollama and xAI both support it.
181
- */
182
- supportsStreamUsage() {
183
- return true;
184
- }
185
- extraBodyFields(_llmRequest) {
186
- return {};
187
- }
188
- /**
189
- * Body fields enabling the provider's NATIVE web search, or null when the
190
- * provider has none (the base then omits the tool and warns once).
191
- */
192
- webSearchBodyFields() {
193
- return null;
194
- }
195
- /**
196
- * Extracts reasoning from the response message. Default: "<think>" blocks
197
- * in content plus the reasoning_content / reasoning fields emitted by
198
- * OpenAI-compatible reasoning models.
199
- */
200
- extractReasoning(message) {
201
- const { reasoning: thinkReasoning, answer } = splitThinkBlocks(message.content ?? '');
202
- const fieldReasoning = message.reasoning_content ?? message.reasoning ?? '';
203
- return {
204
- reasoning: [fieldReasoning, thinkReasoning].filter(Boolean).join('\n\n'),
205
- answer,
206
- };
73
+ export class OpenAiCompatibleLlm extends AdkShim {
74
+ constructor(adapter, options = {}) {
75
+ super(adapter, options);
207
76
  }
208
- /** Yielded before the HTTP call when a precondition is missing (e.g. no
209
- * API key). Return undefined when ready to call. */
210
- missingRequirement() {
211
- return undefined;
212
- }
213
- /** Error response for a non-2xx HTTP status. */
214
- httpError(status, detail) {
215
- return {
216
- errorCode: `${this.providerId().toUpperCase()}_HTTP_ERROR`,
217
- errorMessage: `${this.providerId()} returned ${status}: ${detail.slice(0, 4000)}`,
218
- };
219
- }
220
- /**
221
- * Whether a turn that ended with no answer after thinking (noAnswerError)
222
- * is sent once more with reasoning_effort "none". Default: never. An
223
- * adapter for local reasoning models turns it on (OllamaLlm).
224
- */
225
- retriesWithoutThinking() {
226
- return false;
227
- }
228
- /**
229
- * Error response for a turn that produced reasoning or ran out of tokens
230
- * but no reply and no tool call. `truncated` is true when the provider
231
- * reported finish_reason "length" (the token budget or context window
232
- * filled up); false when it stopped after thinking with nothing to say.
233
- */
234
- noAnswerError(truncated) {
235
- const id = this.providerId();
236
- return truncated
237
- ? {
238
- errorCode: `${id.toUpperCase()}_MAX_TOKENS`,
239
- errorMessage: `${this.model} ran out of tokens before it wrote a reply ` +
240
- '(finish_reason "length"). Raise max_tokens, or lower ' +
241
- 'generateContentConfig.reasoningEffort so less goes to thinking.',
242
- }
243
- : {
244
- errorCode: `${id.toUpperCase()}_EMPTY_RESPONSE`,
245
- errorMessage: `${this.model} finished thinking but returned no reply.`,
246
- };
77
+ /** Provider id for telemetry: the adapter's (a gateway reports the upstream provider). */
78
+ providerId() {
79
+ return this.adapter.provider;
247
80
  }
248
- /** Error response when the endpoint can't be reached at all. */
249
- unreachable(message) {
250
- return {
251
- errorCode: `${this.providerId().toUpperCase()}_UNREACHABLE`,
252
- errorMessage: `Could not reach ${this.endpointUrl()} (${message}).`,
253
- };
254
- }
255
- // ── Generation ─────────────────────────────────────────────────────────────
256
- async *generateContentAsync(llmRequest, stream = false) {
257
- yield* traceLlmGeneration({ provider: this.providerId(), model: this.model, llmRequest }, this.generateInner(llmRequest, stream));
258
- }
259
- /**
260
- * One request, retried once without thinking when the adapter opts in and
261
- * the model thought its way to no answer (ADR 0027): a local reasoning
262
- * model can spend its whole context window on the scratchpad. The first
263
- * attempt's error is held back, never yielded; what it spent is added to
264
- * the retry's usage, so budgets and the ledger still count it. An agent
265
- * that already runs with reasoningEffort "none" is not retried.
266
- */
267
- async *generateInner(llmRequest, stream) {
268
- const cfg = llmRequest.config ?? {};
269
- const mayRetry = this.retriesWithoutThinking() && cfg.reasoningEffort !== 'none';
270
- let held;
271
- for await (const r of this.attempt(llmRequest, stream)) {
272
- if (mayRetry && NO_ANSWER.has(r)) {
273
- held = r;
274
- continue;
275
- }
276
- yield r;
277
- }
278
- if (!held)
279
- return;
280
- setLlmSpanAttribute('llm.retry_without_thinking', held.errorCode ?? true);
281
- if (!this.retryWarned) {
282
- this.retryWarned = true;
283
- console.warn(`⚠ ${this.model} thought without answering (${held.errorCode}); retrying once with thinking off.`);
284
- }
285
- let carried = held.usageMetadata;
286
- for await (const r of this.attempt(llmRequest, stream, 'none')) {
287
- if (carried && !r.partial && (r.turnComplete || r.errorCode)) {
288
- yield { ...r, usageMetadata: addUsage(carried, r.usageMetadata) };
289
- carried = undefined;
290
- continue;
291
- }
292
- yield r;
293
- }
294
- }
295
- async *attempt(llmRequest, stream, reasoningEffort) {
296
- const missing = this.missingRequirement();
297
- if (missing) {
298
- yield missing;
299
- return;
300
- }
301
- setLlmSpanAttribute('llm.transport', this.transport());
302
- const messages = this.buildMessages(llmRequest);
303
- const openAiTools = this.buildTools(llmRequest);
304
- const cfg = llmRequest.config ?? {};
305
- const body = {
306
- model: this.wireModelName(),
307
- messages,
308
- stream,
309
- // SSE reports token usage only if asked, in a final choices-less chunk.
310
- ...(stream && this.supportsStreamUsage()
311
- ? { stream_options: { include_usage: true } }
312
- : {}),
313
- ...(cfg.temperature !== undefined ? { temperature: cfg.temperature } : {}),
314
- ...(cfg.topP !== undefined ? { top_p: cfg.topP } : {}),
315
- ...(cfg.maxOutputTokens !== undefined
316
- ? { max_tokens: cfg.maxOutputTokens }
317
- : {}),
318
- // Reasoning budget. Qwen3-family models think before answering, and on
319
- // a constraint-dense prompt they can spend the whole budget doing it
320
- // and return an empty turn. Ollama's OpenAI-compatible endpoint honors
321
- // `reasoning_effort` and IGNORES the native `think` field, so this is
322
- // the only lever on this path. Opt-in per agent; omitted = unchanged.
323
- ...((reasoningEffort ?? cfg.reasoningEffort) !== undefined
324
- ? { reasoning_effort: reasoningEffort ?? cfg.reasoningEffort }
325
- : {}),
326
- ...(openAiTools.length > 0 ? { tools: openAiTools } : {}),
327
- // Structured output: an ADK outputSchema becomes a strict json_schema
328
- // response_format where the endpoint supports it (xAI does; Ollama's
329
- // OpenAI-compatible endpoint does not, see supportsJsonSchemaFormat),
330
- // else plain JSON mode.
331
- ...(cfg.responseSchema && this.supportsJsonSchemaFormat()
332
- ? { response_format: { type: 'json_schema', json_schema: { name: 'response', strict: true, schema: toStrictJsonSchema(cfg.responseSchema) } } }
333
- : cfg.responseMimeType === 'application/json' || cfg.responseSchema
334
- ? { response_format: { type: 'json_object' } }
335
- : {}),
336
- ...this.extraBodyFields(llmRequest),
337
- };
338
- // web_search: enable the provider's native search, or omit with a warning.
339
- if (wantsWebSearch(llmRequest)) {
340
- const searchFields = this.webSearchBodyFields();
341
- if (searchFields) {
342
- Object.assign(body, searchFields);
343
- setLlmSpanAttribute('llm.web_search.native', true);
344
- }
345
- else {
346
- setLlmSpanAttribute('llm.web_search.omitted', true);
347
- setLlmSpanAttribute('llm.capability.dropped', 'web_search');
348
- if (!this.webSearchWarned) {
349
- this.webSearchWarned = true;
350
- const via = this.transport().startsWith('gateway')
351
- ? ` through ${this.transport()} (a gateway cannot enable upstream native search)`
352
- : '';
353
- console.warn(`⚠ web_search requested but ${this.model}${via} has no native web search — ` +
354
- 'tool omitted (the agent runs without search).');
355
- }
356
- }
357
- }
358
- try {
359
- // Transient failures (429/5xx, connection resets) are retried here, on
360
- // the REQUEST only — the body is read after, so a stream that dies
361
- // half-way is reported, never replayed (lib/models/retry.ts).
362
- const signal = currentTurnSignal();
363
- const { response: res } = await fetchWithRetry(this.endpointUrl(), {
364
- method: 'POST',
365
- headers: { 'Content-Type': 'application/json', ...this.headers() },
366
- body: JSON.stringify(body),
367
- signal,
368
- }, {
369
- signal,
370
- onRetry: ({ retries }) => setLlmSpanAttribute('llm.retries', retries),
371
- });
372
- if (!res.ok) {
373
- const detail = await res.text().catch(() => '');
374
- yield withHttpStatus(this.httpError(res.status, detail), res.status);
375
- return;
376
- }
377
- if (stream) {
378
- yield* this.streamResponse(res);
379
- return;
380
- }
381
- const data = await res.json();
382
- const choice = data.choices?.[0] ?? {};
383
- const message = choice.message ?? {};
384
- const usageMetadata = mapUsage(data.usage);
385
- const { reasoning, answer } = this.extractReasoning(message);
386
- if (reasoning) {
387
- // Scratchpad, not reply: shown by printers, kept out of history.
388
- yield {
389
- content: {
390
- role: 'model',
391
- parts: [{ text: reasoning, thought: true }],
392
- },
393
- partial: true,
394
- };
395
- }
396
- const parts = [];
397
- if (answer)
398
- parts.push({ text: answer });
399
- for (const call of message.tool_calls ?? []) {
400
- let args = {};
401
- try {
402
- args = JSON.parse(call.function?.arguments ?? '{}');
403
- }
404
- catch {
405
- args = { raw: call.function?.arguments };
406
- }
407
- parts.push({
408
- functionCall: {
409
- name: call.function?.name,
410
- args,
411
- id: call.id,
412
- },
413
- });
414
- }
415
- yield this.finalResponse(parts, choice.finish_reason, Boolean(reasoning), usageMetadata);
416
- }
417
- catch (err) {
418
- const msg = err instanceof Error ? err.message : String(err);
419
- yield this.unreachable(msg);
420
- }
421
- }
422
- /**
423
- * SSE path: display-only partials as the tokens land, then ONE final
424
- * non-partial response carrying the complete text, tool calls and usage.
425
- *
426
- * WHY the final repeats text already streamed: ADK's runner persists only
427
- * non-partial events (`if (!event.partial) appendEvent(...)`), so a final
428
- * without text would show the reply on screen and lose it from session
429
- * history. Printers therefore skip text on a turnComplete event whose
430
- * partials they already rendered — see scripts/syndicate_chat.ts.
431
- *
432
- * Thinking is deliberately NOT carried on the final: the scratchpad is
433
- * display-only and stays out of history, which is what keeps it from
434
- * being replayed to the model on the next turn.
435
- */
436
- async *streamResponse(res) {
437
- const splitter = new ThinkStreamSplitter();
438
- const toolCalls = new Map();
439
- let answer = '';
440
- let answerStarted = false;
441
- let sawReasoning = false;
442
- let finishReason;
443
- let usage;
444
- const partial = (text, thought) => {
445
- if (thought)
446
- sawReasoning = true;
447
- return {
448
- content: {
449
- role: 'model',
450
- parts: [thought ? { text, thought: true } : { text }],
451
- },
452
- partial: true,
453
- };
454
- };
455
- // The scratchpad is usually trailed by blank lines; the reply must not
456
- // open with them, but only the FIRST answer text may be trimmed.
457
- const openAnswer = (text) => answerStarted ? text : text.trimStart();
458
- for await (const chunk of sseChunks(res)) {
459
- if (chunk.usage)
460
- usage = chunk.usage;
461
- const finish = chunk.choices?.[0]?.finish_reason;
462
- if (finish)
463
- finishReason = finish;
464
- const delta = chunk.choices?.[0]?.delta;
465
- if (!delta)
466
- continue;
467
- // Reasoning as a discrete field (Ollama `reasoning`, others
468
- // `reasoning_content`) — already separated, no tag parsing needed.
469
- const fieldReasoning = delta.reasoning ?? delta.reasoning_content ?? '';
470
- if (fieldReasoning)
471
- yield partial(fieldReasoning, true);
472
- if (typeof delta.content === 'string' && delta.content) {
473
- const split = splitter.push(delta.content);
474
- if (split.reasoning)
475
- yield partial(split.reasoning, true);
476
- if (split.answer) {
477
- const text = openAnswer(split.answer);
478
- if (text) {
479
- answerStarted = true;
480
- answer += text;
481
- yield partial(text, false);
482
- }
483
- }
484
- }
485
- // Tool arguments arrive as fragments keyed by index, not whole.
486
- for (const call of delta.tool_calls ?? []) {
487
- const index = call.index ?? 0;
488
- const acc = toolCalls.get(index) ?? { args: '' };
489
- if (call.id)
490
- acc.id = call.id;
491
- if (call.function?.name)
492
- acc.name = call.function.name;
493
- if (call.function?.arguments)
494
- acc.args += call.function.arguments;
495
- toolCalls.set(index, acc);
496
- }
497
- }
498
- const tail = splitter.flush();
499
- if (tail.reasoning)
500
- yield partial(tail.reasoning, true);
501
- if (tail.answer) {
502
- const text = openAnswer(tail.answer);
503
- if (text) {
504
- answer += text;
505
- yield partial(text, false);
506
- }
507
- }
508
- const parts = [];
509
- if (answer)
510
- parts.push({ text: answer });
511
- for (const [, acc] of [...toolCalls.entries()].sort((a, b) => a[0] - b[0])) {
512
- let args = {};
513
- try {
514
- args = JSON.parse(acc.args || '{}');
515
- }
516
- catch {
517
- args = { raw: acc.args };
518
- }
519
- parts.push({ functionCall: { name: acc.name, args, id: acc.id } });
520
- }
521
- yield this.finalResponse(parts, finishReason, sawReasoning, mapUsage(usage));
522
- }
523
- /**
524
- * The one non-partial response that closes a turn, on both paths.
525
- *
526
- * WHY an empty reply becomes an error: ADK drops a final response with no
527
- * parts and no errorCode (llm_agent: "empty content → return"), so the
528
- * last event left is the thought partial — ADK logs "The last event is
529
- * partial, which is not expected." and the turn ends with empty text and
530
- * no reason. That is exactly what a thinking model does when its
531
- * scratchpad fills the budget (finish_reason "length"): the answer never
532
- * starts. Naming it lets the caller see why, and still carries the usage
533
- * so the tokens spent thinking are counted.
534
- *
535
- * A reply that was cut short but did start keeps its text and is marked
536
- * finishReason MAX_TOKENS.
537
- */
538
- finalResponse(parts, finishReason, sawReasoning, usageMetadata) {
539
- if (finishReason)
540
- setLlmSpanAttribute('llm.finish_reason', finishReason);
541
- const truncated = finishReason === 'length';
542
- const usage = usageMetadata ? { usageMetadata } : {};
543
- if (parts.length === 0 && (truncated || sawReasoning)) {
544
- const noAnswer = { ...this.noAnswerError(truncated), turnComplete: true, ...usage };
545
- NO_ANSWER.add(noAnswer);
546
- return noAnswer;
547
- }
548
- return {
549
- content: { role: 'model', parts },
550
- turnComplete: true,
551
- ...(truncated ? { finishReason: FinishReason.MAX_TOKENS } : {}),
552
- ...usage,
553
- };
81
+ /** How the request reaches the provider: 'direct' or 'gateway:<id>' (llm.transport). */
82
+ transport() {
83
+ return this.adapter.transport();
554
84
  }
555
- // ── Translation helpers ────────────────────────────────────────────────────
556
- /** ADK Contents (Google GenAI format) → OpenAI-compatible chat messages. */
85
+ /** The chat messages the adapter sends for this LlmRequest. */
557
86
  buildMessages(llmRequest) {
558
- const systemParts = [];
559
- const messages = [];
560
- for (const content of llmRequest.contents) {
561
- // ADK injects the system instruction as a 'system' role content.
562
- if (content.role === 'system') {
563
- const text = content.parts
564
- ?.filter((p) => p.text)
565
- .map((p) => p.text)
566
- .join('\n');
567
- if (text)
568
- systemParts.push(text);
569
- continue;
570
- }
571
- const isModel = content.role === 'model';
572
- const textChunks = [];
573
- const imageParts = [];
574
- const toolCalls = [];
575
- for (const part of content.parts ?? []) {
576
- const p = part;
577
- if (p.thought) {
578
- // Prior-turn scratchpad is display-only; never replay it.
579
- continue;
580
- }
581
- if (p.text) {
582
- textChunks.push(p.text);
583
- }
584
- else if (p.inlineData?.data) {
585
- // ADK inline image → OpenAI image_url data URI (vision models)
586
- const mime = p.inlineData.mimeType ?? 'image/png';
587
- imageParts.push({
588
- type: 'image_url',
589
- image_url: { url: `data:${mime};base64,${p.inlineData.data}` },
590
- });
591
- }
592
- else if (p.functionCall) {
593
- // ADK function_call → OpenAI assistant tool_calls entry
594
- toolCalls.push({
595
- id: p.functionCall.id ?? `call_${toolCalls.length}`,
596
- type: 'function',
597
- function: {
598
- name: p.functionCall.name,
599
- arguments: JSON.stringify(p.functionCall.args ?? {}),
600
- },
601
- });
602
- }
603
- else if (p.functionResponse) {
604
- // ADK function_response → OpenAI 'tool' role message
605
- messages.push({
606
- role: 'tool',
607
- tool_call_id: p.functionResponse.id ?? '',
608
- content: JSON.stringify(p.functionResponse.response ?? {}),
609
- });
610
- }
611
- }
612
- const text = textChunks.join('\n');
613
- if (isModel) {
614
- if (text || toolCalls.length > 0) {
615
- messages.push({
616
- role: 'assistant',
617
- content: text || null,
618
- ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
619
- });
620
- }
621
- }
622
- else if (text || imageParts.length > 0) {
623
- messages.push({
624
- role: 'user',
625
- content: imageParts.length > 0
626
- ? [...(text ? [{ type: 'text', text }] : []), ...imageParts]
627
- : text,
628
- });
629
- }
630
- }
631
- // Also pull system instruction from generateContentConfig if present
632
- const configSystem = llmRequest.config?.systemInstruction;
633
- if (configSystem) {
634
- const text = typeof configSystem === 'string'
635
- ? configSystem
636
- : configSystem.parts?.map((p) => p.text).join('\n') ?? '';
637
- if (text)
638
- systemParts.unshift(text);
639
- }
640
- if (systemParts.length > 0) {
641
- messages.unshift({ role: 'system', content: systemParts.join('\n\n') });
642
- }
643
- return messages;
87
+ return this.adapter.messagesFor(this.toModelRequest(llmRequest, { model: this.model }));
644
88
  }
645
- /** ADK toolsDict → OpenAI function-tool definitions (lowercase schemas). */
89
+ /** The function tools the adapter sends for this LlmRequest (lowercase schemas). */
646
90
  buildTools(llmRequest) {
647
- const openAiTools = [];
648
- for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
649
- if (isWebSearchSentinel(tool))
650
- continue; // handled via body fields
651
- const decl = toolDeclarationFor(tool);
652
- if (decl) {
653
- openAiTools.push({
654
- type: 'function',
655
- function: {
656
- name: decl.name,
657
- description: decl.description,
658
- parameters: decl.parameters,
659
- },
660
- });
661
- }
662
- }
663
- return openAiTools;
664
- }
665
- /**
666
- * Live/bidirectional streaming — not part of the OpenAI-compatible
667
- * chat-completions surface. Throws to surface this clearly.
668
- */
669
- async connect(_llmRequest) {
670
- throw new Error(`${this.constructor.name} does not support live bidirectional connections. ` +
671
- 'Use a Gemini model for live/streaming sessions.');
91
+ return this.adapter.toolsFor(this.toModelRequest(llmRequest, { model: this.model }));
92
+ }
93
+ /** The genai mapping's request, with the older spelling the contract leaves out. */
94
+ toModelRequest(llmRequest, options) {
95
+ const request = super.toModelRequest(llmRequest, options);
96
+ const olderSpelling = olderSpellingOf(llmRequest.config);
97
+ return olderSpelling ? { ...request, olderSpelling } : request;
98
+ }
99
+ /** The genai mapping's response, in the shape these classes have always yielded (see the header). */
100
+ toLlmResponse(response) {
101
+ const mapped = super.toLlmResponse(response);
102
+ if (response.partial)
103
+ return mapped;
104
+ const { finishReason: _finish, usageMetadata: _usage, ...rest } = mapped;
105
+ const error = response.error;
106
+ return {
107
+ ...(error ? {} : { content: { role: 'model', parts: [] } }),
108
+ ...rest,
109
+ ...(!error && response.finishReason === 'max_tokens' ? { finishReason: FinishReason.MAX_TOKENS } : {}),
110
+ ...(response.usage ? { usageMetadata: chatUsageMetadata(response.usage) } : {}),
111
+ ...(error?.status !== undefined ? { status: error.status, retryable: error.retryable } : {}),
112
+ };
672
113
  }
673
114
  }
115
+ // ── Kept for callers of the ADK-path helpers ─────────────────────────────────
674
116
  /**
675
117
  * Stamps an HTTP error response with its numeric status and whether that
676
- * status is transient, so callers above (and the ledger, via the tracer's
677
- * llm.payload.response) can tell a rate limit from a bad request without
678
- * parsing the message. Applied in the base so a subclass's httpError
679
- * override (Ollama's hint, the gateway's GATEWAY_HTTP_ERROR) keeps its
680
- * errorCode and wording and still gets the fields.
118
+ * status is transient, and the same verdict in customMetadata
119
+ * ('error.retryable', 'error.status'), where FallbackLlm reads it
120
+ * (lib/models/errorResponse.ts). The chat-completions shims take these
121
+ * fields from their adapter's error final instead (toLlmResponse).
681
122
  */
682
123
  export function withHttpStatus(resp, status) {
683
124
  setLlmSpanAttribute('llm.http_status', status);
684
- return { ...resp, status, retryable: isRetryableStatus(status) };
125
+ return { ...withRetryVerdict(resp, statusDecision(status)), status, retryable: isRetryableStatus(status) };
685
126
  }
686
- /** OpenAI-style usage → GenAI usageMetadata (undefined when absent). */
687
- /** Responses built by noAnswerError: the ones a retry without thinking may replace. */
688
- const NO_ANSWER = new WeakSet();
689
- /** Two usage records summed, field by field. */
127
+ /** Two genai usage records summed, field by field. */
690
128
  export function addUsage(a, b) {
691
129
  if (!a)
692
130
  return b;
@@ -699,20 +137,20 @@ export function addUsage(a, b) {
699
137
  }
700
138
  return out;
701
139
  }
140
+ /**
141
+ * OpenAI-style usage → GenAI usageMetadata in the ADK-path meaning
142
+ * (completion_tokens, reasoning included, as candidatesTokenCount), or
143
+ * undefined when absent. The contract's meaning is chatUsage in
144
+ * lib/models/chatCompletionsAdapter.ts.
145
+ */
702
146
  export function mapUsage(usage) {
703
147
  if (!usage || typeof usage !== 'object')
704
148
  return undefined;
705
149
  const reasoning = usage.completion_tokens_details?.reasoning_tokens;
706
150
  return {
707
- ...(usage.prompt_tokens !== undefined
708
- ? { promptTokenCount: usage.prompt_tokens }
709
- : {}),
710
- ...(usage.completion_tokens !== undefined
711
- ? { candidatesTokenCount: usage.completion_tokens }
712
- : {}),
151
+ ...(usage.prompt_tokens !== undefined ? { promptTokenCount: usage.prompt_tokens } : {}),
152
+ ...(usage.completion_tokens !== undefined ? { candidatesTokenCount: usage.completion_tokens } : {}),
713
153
  ...(reasoning !== undefined ? { thoughtsTokenCount: reasoning } : {}),
714
- ...(usage.total_tokens !== undefined
715
- ? { totalTokenCount: usage.total_tokens }
716
- : {}),
154
+ ...(usage.total_tokens !== undefined ? { totalTokenCount: usage.total_tokens } : {}),
717
155
  };
718
156
  }