melchizedek-agents 0.9.6 → 0.16.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 (246) hide show
  1. package/.env.example +272 -0
  2. package/AGENT_SETUP.md +39 -23
  3. package/CHANGELOG.md +623 -0
  4. package/DOCUMENTATION.md +429 -109
  5. package/QUICKSTART.md +92 -18
  6. package/README.md +45 -5
  7. package/config/agents/README.md +46 -1
  8. package/config/agents/examples/ares.yaml +117 -40
  9. package/config/agents/examples/assistant.yaml +159 -0
  10. package/config/agents/examples/augustin.yaml +154 -51
  11. package/config/agents/examples/cartographers.yaml +91 -23
  12. package/config/agents/examples/claude.yaml +45 -6
  13. package/config/agents/examples/council.yaml +23 -8
  14. package/config/agents/examples/critic.yaml +57 -21
  15. package/config/agents/examples/delegation.yaml +76 -9
  16. package/config/agents/examples/hierarchical.yaml +55 -8
  17. package/config/agents/examples/image_production.yaml +54 -16
  18. package/config/agents/examples/librarian.yaml +70 -14
  19. package/config/agents/examples/model_zoo.yaml +105 -29
  20. package/config/agents/examples/patient_advocate.yaml +102 -34
  21. package/config/agents/examples/research.yaml +250 -0
  22. package/config/agents/examples/scribe.yaml +242 -0
  23. package/config/agents/examples/scriptorium.yaml +87 -24
  24. package/config/agents/examples/style_council.yaml +31 -9
  25. package/config/agents/examples/syndicate.yaml +29 -10
  26. package/config/agents/examples/tutor.yaml +10 -3
  27. package/config/agents/syndicateSchema.yaml +44 -9
  28. package/config/agents/templates/README.md +58 -0
  29. package/config/agents/templates/account_memory.yaml +153 -0
  30. package/config/agents/templates/case_desk.yaml +188 -0
  31. package/config/agents/templates/conversational.yaml +110 -0
  32. package/config/agents/templates/draft_review.yaml +130 -0
  33. package/config/agents/templates/intake_extractor.yaml +152 -0
  34. package/config/agents/templates/research_brief.yaml +169 -0
  35. package/config/agents/templates/research_desk.yaml +152 -0
  36. package/config/agents/templates/review_panel.yaml +188 -0
  37. package/config/agents/templates/support_triage.yaml +273 -0
  38. package/config/agents/templates/systems_operator.yaml +157 -0
  39. package/db/hardening.sql +202 -0
  40. package/db/memory_v2.sql +77 -0
  41. package/db/migrations/0001_base.sql +167 -0
  42. package/db/migrations/0002_erase_scope.sql +172 -0
  43. package/db/migrations/0003_postgres_storage.sql +58 -0
  44. package/db/migrations/0004_usage.sql +94 -0
  45. package/db/migrations/0005_agent_registry.sql +200 -0
  46. package/db/migrations/0006_task_leases.sql +25 -0
  47. package/db/migrations/0007_memory_commit.sql +134 -0
  48. package/db/migrations/0008_memory_retention.sql +37 -0
  49. package/db/migrations/0009_task_queue.sql +51 -0
  50. package/db/migrations/0010_erase_complete.sql +180 -0
  51. package/db/telemetry.sql +401 -0
  52. package/demo/README.md +77 -0
  53. package/demo/a2a_demo.mjs +95 -0
  54. package/dist/lib/a2a/app.d.ts +253 -0
  55. package/dist/lib/a2a/app.js +751 -0
  56. package/dist/lib/a2a/executor.d.ts +167 -0
  57. package/dist/lib/a2a/executor.js +548 -0
  58. package/dist/lib/a2a/identity.d.ts +122 -0
  59. package/dist/lib/a2a/identity.js +254 -0
  60. package/dist/lib/a2a/policy.d.ts +110 -0
  61. package/dist/lib/a2a/policy.js +178 -0
  62. package/dist/lib/a2a/redisLimits.d.ts +26 -0
  63. package/dist/lib/a2a/redisLimits.js +31 -0
  64. package/dist/lib/a2a/remoteAgent.d.ts +91 -0
  65. package/dist/lib/a2a/remoteAgent.js +268 -0
  66. package/dist/lib/a2a/turnLock.d.ts +28 -0
  67. package/dist/lib/a2a/turnLock.js +76 -0
  68. package/dist/lib/compile.d.ts +11 -1
  69. package/dist/lib/compile.js +109 -8
  70. package/dist/lib/config.d.ts +7 -7
  71. package/dist/lib/config.js +7 -7
  72. package/dist/lib/doctor.d.ts +145 -0
  73. package/dist/lib/doctor.js +517 -0
  74. package/dist/lib/grounding.d.ts +2 -2
  75. package/dist/lib/grounding.js +2 -2
  76. package/dist/lib/guards/index.d.ts +37 -0
  77. package/dist/lib/guards/index.js +66 -0
  78. package/dist/lib/guards/science.d.ts +117 -0
  79. package/dist/lib/guards/science.js +444 -0
  80. package/dist/lib/index.d.ts +37 -2
  81. package/dist/lib/index.js +35 -4
  82. package/dist/lib/loadEnv.d.ts +23 -11
  83. package/dist/lib/loadEnv.js +77 -29
  84. package/dist/lib/loadSyndicate.d.ts +97 -5
  85. package/dist/lib/loadSyndicate.js +176 -76
  86. package/dist/lib/memory/erase.d.ts +24 -0
  87. package/dist/lib/memory/erase.js +47 -0
  88. package/dist/lib/memory/namespace.d.ts +37 -0
  89. package/dist/lib/memory/namespace.js +74 -0
  90. package/dist/lib/memory/providers.d.ts +99 -0
  91. package/dist/lib/memory/providers.js +240 -0
  92. package/dist/lib/memory/store.d.ts +74 -0
  93. package/dist/lib/memory/store.js +108 -0
  94. package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
  95. package/dist/lib/memory/supabaseMemoryService.js +239 -125
  96. package/dist/lib/models/capabilities.d.ts +103 -0
  97. package/dist/lib/models/capabilities.js +291 -0
  98. package/dist/lib/models/claudeLlm.d.ts +10 -1
  99. package/dist/lib/models/claudeLlm.js +57 -27
  100. package/dist/lib/models/endpoints.d.ts +100 -0
  101. package/dist/lib/models/endpoints.js +282 -0
  102. package/dist/lib/models/gateway.d.ts +107 -0
  103. package/dist/lib/models/gateway.js +157 -0
  104. package/dist/lib/models/gatewayLlm.d.ts +57 -0
  105. package/dist/lib/models/gatewayLlm.js +105 -0
  106. package/dist/lib/models/gptLlm.d.ts +51 -2
  107. package/dist/lib/models/gptLlm.js +169 -26
  108. package/dist/lib/models/grokLlm.d.ts +19 -10
  109. package/dist/lib/models/grokLlm.js +29 -14
  110. package/dist/lib/models/ollamaLlm.d.ts +26 -0
  111. package/dist/lib/models/ollamaLlm.js +43 -1
  112. package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
  113. package/dist/lib/models/openAiCompatibleLlm.js +200 -37
  114. package/dist/lib/models/providerMap.js +9 -2
  115. package/dist/lib/models/registry.d.ts +42 -5
  116. package/dist/lib/models/registry.js +140 -29
  117. package/dist/lib/models/retry.d.ts +146 -0
  118. package/dist/lib/models/retry.js +305 -0
  119. package/dist/lib/models/schemaNormalize.d.ts +20 -0
  120. package/dist/lib/models/schemaNormalize.js +43 -0
  121. package/dist/lib/net/addressGuard.d.ts +60 -0
  122. package/dist/lib/net/addressGuard.js +216 -0
  123. package/dist/lib/observability/embeddings.d.ts +11 -2
  124. package/dist/lib/observability/embeddings.js +18 -14
  125. package/dist/lib/observability/metrics.d.ts +36 -0
  126. package/dist/lib/observability/metrics.js +86 -0
  127. package/dist/lib/observability/redact.d.ts +29 -0
  128. package/dist/lib/observability/redact.js +102 -0
  129. package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
  130. package/dist/lib/observability/supabaseSpanExporter.js +22 -6
  131. package/dist/lib/observability/tracer.d.ts +10 -0
  132. package/dist/lib/observability/tracer.js +70 -3
  133. package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
  134. package/dist/lib/persistence/supabaseProvider.js +32 -6
  135. package/dist/lib/registry.d.ts +87 -0
  136. package/dist/lib/registry.js +146 -0
  137. package/dist/lib/runtime/approvals.d.ts +44 -0
  138. package/dist/lib/runtime/approvals.js +67 -0
  139. package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
  140. package/dist/lib/runtime/syndicateTurn.js +597 -0
  141. package/dist/lib/runtime/turnControl.d.ts +90 -0
  142. package/dist/lib/runtime/turnControl.js +131 -0
  143. package/dist/lib/session/supabaseSessionService.js +1 -1
  144. package/dist/lib/session/transcript.d.ts +9 -1
  145. package/dist/lib/session/transcript.js +15 -7
  146. package/dist/lib/skills.d.ts +50 -0
  147. package/dist/lib/skills.js +138 -0
  148. package/dist/lib/storage/postgres/index.d.ts +98 -0
  149. package/dist/lib/storage/postgres/index.js +146 -0
  150. package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
  151. package/dist/lib/storage/postgres/memoryStore.js +82 -0
  152. package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
  153. package/dist/lib/storage/postgres/sessionService.js +225 -0
  154. package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
  155. package/dist/lib/storage/postgres/taskQueue.js +101 -0
  156. package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
  157. package/dist/lib/storage/postgres/taskStore.js +168 -0
  158. package/dist/lib/storage/schema.d.ts +27 -0
  159. package/dist/lib/storage/schema.js +50 -0
  160. package/dist/lib/storage/schemaVersion.d.ts +25 -0
  161. package/dist/lib/storage/schemaVersion.js +46 -0
  162. package/dist/lib/syndicateSchema.d.ts +275 -0
  163. package/dist/lib/syndicateSchema.js +534 -0
  164. package/dist/lib/toolRegistry.d.ts +15 -0
  165. package/dist/lib/toolRegistry.js +59 -10
  166. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  167. package/dist/lib/tools/mcpServe.d.ts +45 -0
  168. package/dist/lib/tools/mcpServe.js +88 -0
  169. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  170. package/dist/lib/tools/mcpToolFactory.js +41 -21
  171. package/dist/lib/tools/science/corrections.d.ts +42 -0
  172. package/dist/lib/tools/science/corrections.js +115 -0
  173. package/dist/lib/tools/science/europepmc.d.ts +51 -0
  174. package/dist/lib/tools/science/europepmc.js +127 -0
  175. package/dist/lib/tools/science/http.d.ts +31 -0
  176. package/dist/lib/tools/science/http.js +104 -0
  177. package/dist/lib/tools/science/openalex.d.ts +30 -0
  178. package/dist/lib/tools/science/openalex.js +81 -0
  179. package/dist/lib/tools/science/trials.d.ts +58 -0
  180. package/dist/lib/tools/science/trials.js +203 -0
  181. package/dist/lib/tools/science/types.d.ts +69 -0
  182. package/dist/lib/tools/science/types.js +48 -0
  183. package/dist/lib/tools/scienceTools.d.ts +100 -0
  184. package/dist/lib/tools/scienceTools.js +277 -0
  185. package/dist/lib/tools/taskTools.d.ts +204 -0
  186. package/dist/lib/tools/taskTools.js +374 -0
  187. package/dist/lib/tools/toolContract.d.ts +17 -4
  188. package/dist/lib/tools/toolContract.js +13 -5
  189. package/dist/lib/tools/webExtractTool.d.ts +13 -10
  190. package/dist/lib/tools/webExtractTool.js +23 -55
  191. package/dist/lib/tools/webSearchTool.d.ts +2 -1
  192. package/dist/lib/tools/webSearchTool.js +4 -3
  193. package/dist/lib/tools/wikiTools.d.ts +2 -2
  194. package/dist/lib/tools/wikiTools.js +12 -12
  195. package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
  196. package/dist/lib/tools/xApiSearchTool.js +583 -0
  197. package/dist/lib/tools/xSearchTool.js +1 -1
  198. package/dist/lib/wiki/agentRun.js +1 -1
  199. package/dist/lib/wiki/builder.js +21 -6
  200. package/dist/lib/wiki/dive.js +2 -2
  201. package/dist/lib/wiki/entities.d.ts +2 -2
  202. package/dist/lib/wiki/entities.js +7 -7
  203. package/dist/lib/wiki/extract.js +5 -1
  204. package/dist/lib/wiki/fill.js +5 -5
  205. package/dist/lib/wiki/format.d.ts +7 -5
  206. package/dist/lib/wiki/format.js +2 -2
  207. package/dist/lib/wiki/graph.js +2 -2
  208. package/dist/lib/wiki/lint.js +2 -2
  209. package/dist/lib/wiki/markdown.d.ts +1 -1
  210. package/dist/lib/wiki/markdown.js +6 -1
  211. package/dist/lib/wiki/pipeline.js +2 -2
  212. package/dist/lib/wiki/vault.js +2 -2
  213. package/dist/scripts/a2a_server.d.ts +17 -11
  214. package/dist/scripts/a2a_server.js +405 -1028
  215. package/dist/scripts/assistant_worker.d.ts +33 -0
  216. package/dist/scripts/assistant_worker.js +193 -0
  217. package/dist/scripts/db.d.ts +26 -0
  218. package/dist/scripts/db.js +179 -0
  219. package/dist/scripts/doctor.d.ts +23 -0
  220. package/dist/scripts/doctor.js +90 -0
  221. package/dist/scripts/init.d.ts +43 -0
  222. package/dist/scripts/init.js +152 -0
  223. package/dist/scripts/registry.d.ts +25 -0
  224. package/dist/scripts/registry.js +182 -0
  225. package/dist/scripts/skills_install.d.ts +7 -0
  226. package/dist/scripts/skills_install.js +137 -0
  227. package/dist/scripts/syndicate_chat.js +159 -276
  228. package/package.json +90 -16
  229. package/skills/README.md +74 -0
  230. package/skills/briefs/README.md +16 -0
  231. package/skills/briefs/_shared.md +16 -0
  232. package/skills/briefs/melchizedek-author.md +60 -0
  233. package/skills/briefs/melchizedek-memory.md +47 -0
  234. package/skills/briefs/melchizedek-models.md +47 -0
  235. package/skills/briefs/melchizedek-scribe.md +37 -0
  236. package/skills/briefs/melchizedek-serve.md +56 -0
  237. package/skills/briefs/melchizedek.md +69 -0
  238. package/skills/briefs/skills-readme.md +33 -0
  239. package/skills/melchizedek/SKILL.md +159 -0
  240. package/skills/melchizedek-author/SKILL.md +99 -0
  241. package/skills/melchizedek-author/templates/minimal.yaml +60 -0
  242. package/skills/melchizedek-memory/SKILL.md +138 -0
  243. package/skills/melchizedek-models/SKILL.md +137 -0
  244. package/skills/melchizedek-scribe/SKILL.md +72 -0
  245. package/skills/melchizedek-scribe/templates/brief.md +27 -0
  246. package/skills/melchizedek-serve/SKILL.md +119 -0
@@ -71,9 +71,19 @@
71
71
  * - generate_image / inspect_image call Gemini image models directly and
72
72
  * still require a Gemini key regardless of the inference model.
73
73
  * - thinkingConfig is Gemini-only; qwen3 thinks on its own schedule.
74
+ * generateContentConfig.reasoningEffort travels as reasoning_effort, and
75
+ * on Ollama 0.31 only "none" changes anything (it turns thinking off);
76
+ * "low" does not bound a qwen3.5 scratchpad, and `think: false` is
77
+ * ignored on this path.
78
+ * - Context window: 4,096 tokens unless the Modelfile (PARAMETER num_ctx)
79
+ * or OLLAMA_CONTEXT_LENGTH on the server says otherwise — /v1 ignores
80
+ * num_ctx. A thinking model that fills it, or stops after thinking, is
81
+ * retried once with thinking off (retriesWithoutThinking); only if that
82
+ * fails too does the turn end with OLLAMA_MAX_TOKENS or
83
+ * OLLAMA_EMPTY_RESPONSE, never an empty reply (see noAnswerError).
74
84
  */
75
85
  import { LLMRegistry } from '@google/adk';
76
- import { OpenAiCompatibleLlm } from "./openAiCompatibleLlm.js";
86
+ import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.js';
77
87
  const DEFAULT_BASE_URL = 'http://localhost:11434/v1';
78
88
  // ── OllamaLlm ─────────────────────────────────────────────────────────────────
79
89
  export class OllamaLlm extends OpenAiCompatibleLlm {
@@ -119,9 +129,41 @@ export class OllamaLlm extends OpenAiCompatibleLlm {
119
129
  'Is it running? Start the Ollama app or run: ollama serve',
120
130
  };
121
131
  }
132
+ /**
133
+ * Out of tokens on Ollama almost always means the CONTEXT WINDOW, not
134
+ * max_tokens: Ollama loads a model with a 4,096-token window unless told
135
+ * otherwise, the prompt and the scratchpad share it, and this /v1 path
136
+ * ignores num_ctx (and `options` entirely). A qwen3.5 explainer prompt can
137
+ * think for 3,700+ tokens and hit the ceiling before the answer starts.
138
+ * The two levers that work are named in the message.
139
+ */
140
+ noAnswerError(truncated) {
141
+ if (!truncated)
142
+ return super.noAnswerError(truncated);
143
+ const name = this.wireModelName();
144
+ return {
145
+ errorCode: 'OLLAMA_MAX_TOKENS',
146
+ errorMessage: `${name} filled its context window while thinking and never wrote a reply ` +
147
+ '(finish_reason "length"). Ollama defaults to a 4,096-token window and its ' +
148
+ 'OpenAI-compatible endpoint ignores num_ctx, so either set ' +
149
+ 'generateContentConfig.reasoningEffort: "none" on the agent to skip thinking, ' +
150
+ `or give the model a larger window: a Modelfile with "FROM ${name}" and ` +
151
+ '"PARAMETER num_ctx 32768" (ollama create), or OLLAMA_CONTEXT_LENGTH=32768 ' +
152
+ 'on the ollama serve process.',
153
+ };
154
+ }
122
155
  extraBodyFields(_llmRequest) {
123
156
  return {};
124
157
  }
158
+ /**
159
+ * A local reasoning model that thinks its way to no answer is asked once
160
+ * more with thinking off ("none" is the one reasoning_effort Ollama 0.31
161
+ * honors), so the turn answers instead of failing. Off with
162
+ * OLLAMA_RETRY_WITHOUT_THINKING=false.
163
+ */
164
+ retriesWithoutThinking() {
165
+ return process.env.OLLAMA_RETRY_WITHOUT_THINKING?.trim().toLowerCase() !== 'false';
166
+ }
125
167
  }
126
168
  // ── Registration helper ───────────────────────────────────────────────────────
127
169
  /**
@@ -23,6 +23,8 @@
23
23
  * surfaced as a { text, thought: true } part in a partial response —
24
24
  * printers display it dimmed; it never enters session history.
25
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`.
26
28
  * - web_search handling: subclasses with native search return its body
27
29
  * fields from webSearchBodyFields(); those without return null and the
28
30
  * base omits the tool with a one-time warning + span attribute.
@@ -61,7 +63,13 @@ type OpenAiMessage = {
61
63
  tool_call_id: string;
62
64
  content: string;
63
65
  };
64
- /** Splits "<think>…</think>" scratchpad from the reply. */
66
+ /**
67
+ * Splits "<think>…</think>" scratchpad from the reply.
68
+ *
69
+ * A block the model never closed is scratchpad too: it means the model ran
70
+ * out of budget mid-thought, so everything after the opening tag is
71
+ * reasoning — never reply text with a raw "<think>" in it.
72
+ */
65
73
  export declare function splitThinkBlocks(text: string): {
66
74
  reasoning: string;
67
75
  answer: string;
@@ -95,6 +103,7 @@ export declare class ThinkStreamSplitter {
95
103
  }
96
104
  export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
97
105
  private webSearchWarned;
106
+ private retryWarned;
98
107
  /** Provider id for telemetry, e.g. 'ollama', 'xai'. */
99
108
  protected abstract providerId(): string;
100
109
  /** Full chat-completions URL, e.g. "https://api.x.ai/v1/chat/completions". */
@@ -103,6 +112,13 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
103
112
  protected abstract headers(): Record<string, string>;
104
113
  /** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
105
114
  protected wireModelName(): string;
115
+ /**
116
+ * How the request reaches the provider: 'direct' (the provider's own
117
+ * endpoint) or 'gateway:<id>' (lib/models/gatewayLlm.ts). Recorded on the
118
+ * llm.request span as llm.transport so the ledger can tell a native call
119
+ * from a proxied one while llm.provider keeps the upstream attribution.
120
+ */
121
+ protected transport(): string;
106
122
  /** Provider-specific request body fields (merged last). */
107
123
  /** Whether the endpoint accepts response_format json_schema (strict). */
108
124
  protected supportsJsonSchemaFormat(): boolean;
@@ -135,10 +151,32 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
135
151
  protected missingRequirement(): LlmResponse | undefined;
136
152
  /** Error response for a non-2xx HTTP status. */
137
153
  protected httpError(status: number, detail: string): LlmResponse;
154
+ /**
155
+ * Whether a turn that ended with no answer after thinking (noAnswerError)
156
+ * is sent once more with reasoning_effort "none". Default: never. An
157
+ * adapter for local reasoning models turns it on (OllamaLlm).
158
+ */
159
+ protected retriesWithoutThinking(): boolean;
160
+ /**
161
+ * Error response for a turn that produced reasoning or ran out of tokens
162
+ * but no reply and no tool call. `truncated` is true when the provider
163
+ * reported finish_reason "length" (the token budget or context window
164
+ * filled up); false when it stopped after thinking with nothing to say.
165
+ */
166
+ protected noAnswerError(truncated: boolean): LlmResponse;
138
167
  /** Error response when the endpoint can't be reached at all. */
139
168
  protected unreachable(message: string): LlmResponse;
140
169
  generateContentAsync(llmRequest: LlmRequest, stream?: boolean): AsyncGenerator<LlmResponse, void>;
170
+ /**
171
+ * One request, retried once without thinking when the adapter opts in and
172
+ * the model thought its way to no answer (ADR 0027): a local reasoning
173
+ * model can spend its whole context window on the scratchpad. The first
174
+ * attempt's error is held back, never yielded; what it spent is added to
175
+ * the retry's usage, so budgets and the ledger still count it. An agent
176
+ * that already runs with reasoningEffort "none" is not retried.
177
+ */
141
178
  private generateInner;
179
+ private attempt;
142
180
  /**
143
181
  * SSE path: display-only partials as the tokens land, then ONE final
144
182
  * non-partial response carrying the complete text, tool calls and usage.
@@ -154,6 +192,22 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
154
192
  * being replayed to the model on the next turn.
155
193
  */
156
194
  private streamResponse;
195
+ /**
196
+ * The one non-partial response that closes a turn, on both paths.
197
+ *
198
+ * WHY an empty reply becomes an error: ADK drops a final response with no
199
+ * parts and no errorCode (llm_agent: "empty content → return"), so the
200
+ * last event left is the thought partial — ADK logs "The last event is
201
+ * partial, which is not expected." and the turn ends with empty text and
202
+ * no reason. That is exactly what a thinking model does when its
203
+ * scratchpad fills the budget (finish_reason "length"): the answer never
204
+ * starts. Naming it lets the caller see why, and still carries the usage
205
+ * so the tokens spent thinking are counted.
206
+ *
207
+ * A reply that was cut short but did start keeps its text and is marked
208
+ * finishReason MAX_TOKENS.
209
+ */
210
+ private finalResponse;
157
211
  /** ADK Contents (Google GenAI format) → OpenAI-compatible chat messages. */
158
212
  protected buildMessages(llmRequest: LlmRequest): OpenAiMessage[];
159
213
  /** ADK toolsDict → OpenAI function-tool definitions (lowercase schemas). */
@@ -164,7 +218,18 @@ export declare abstract class OpenAiCompatibleLlm extends BaseLlm {
164
218
  */
165
219
  connect(_llmRequest: LlmRequest): Promise<BaseLlmConnection>;
166
220
  }
167
- /** OpenAI-style usage → GenAI usageMetadata (undefined when absent). */
221
+ /**
222
+ * Stamps an HTTP error response with its numeric status and whether that
223
+ * status is transient, so callers above (and the ledger, via the tracer's
224
+ * llm.payload.response) can tell a rate limit from a bad request without
225
+ * parsing the message. Applied in the base so a subclass's httpError
226
+ * override (Ollama's hint, the gateway's GATEWAY_HTTP_ERROR) keeps its
227
+ * errorCode and wording and still gets the fields.
228
+ */
229
+ export declare function withHttpStatus(resp: LlmResponse, status: number): LlmResponse;
230
+ type Usage = NonNullable<ReturnType<typeof mapUsage>>;
231
+ /** Two usage records summed, field by field. */
232
+ export declare function addUsage(a: Usage | undefined, b: Usage | undefined): Usage | undefined;
168
233
  export declare function mapUsage(usage: any): {
169
234
  promptTokenCount?: number;
170
235
  candidatesTokenCount?: number;
@@ -23,27 +23,43 @@
23
23
  * surfaced as a { text, thought: true } part in a partial response —
24
24
  * printers display it dimmed; it never enters session history.
25
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`.
26
28
  * - web_search handling: subclasses with native search return its body
27
29
  * fields from webSearchBodyFields(); those without return null and the
28
30
  * base omits the tool with a one-time warning + span attribute.
29
31
  */
30
32
  import { BaseLlm } from '@google/adk';
31
- import { traceLlmGeneration, setLlmSpanAttribute, } from "../observability/tracer.js";
32
- import { wantsWebSearch, isWebSearchSentinel, } from "../tools/webSearchTool.js";
33
- import { toLowercaseJsonSchema, toStrictJsonSchema } from "./schemaNormalize.js";
34
- /** Splits "<think>…</think>" scratchpad from the reply. */
33
+ 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>';
41
+ /**
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.
47
+ */
35
48
  export function splitThinkBlocks(text) {
36
49
  const blocks = [];
37
- const answer = text
38
- .replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
50
+ let answer = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
39
51
  blocks.push(inner.trim());
40
52
  return '';
41
- })
42
- .trimStart();
43
- return { reasoning: blocks.join('\n\n'), answer };
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() };
44
62
  }
45
- const OPEN_THINK = '<think>';
46
- const CLOSE_THINK = '</think>';
47
63
  /** Length of the longest suffix of `s` that is a proper prefix of `tag`. */
48
64
  function partialTagSuffix(s, tag) {
49
65
  const max = Math.min(s.length, tag.length - 1);
@@ -140,10 +156,20 @@ async function* sseChunks(res) {
140
156
  // ── OpenAiCompatibleLlm ──────────────────────────────────────────────────────
141
157
  export class OpenAiCompatibleLlm extends BaseLlm {
142
158
  webSearchWarned = false;
159
+ retryWarned = false;
143
160
  /** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
144
161
  wireModelName() {
145
162
  return this.model;
146
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
+ }
147
173
  /** Provider-specific request body fields (merged last). */
148
174
  /** Whether the endpoint accepts response_format json_schema (strict). */
149
175
  supportsJsonSchemaFormat() {
@@ -191,6 +217,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
191
217
  errorMessage: `${this.providerId()} returned ${status}: ${detail.slice(0, 4000)}`,
192
218
  };
193
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
+ };
247
+ }
194
248
  /** Error response when the endpoint can't be reached at all. */
195
249
  unreachable(message) {
196
250
  return {
@@ -202,12 +256,49 @@ export class OpenAiCompatibleLlm extends BaseLlm {
202
256
  async *generateContentAsync(llmRequest, stream = false) {
203
257
  yield* traceLlmGeneration({ provider: this.providerId(), model: this.model, llmRequest }, this.generateInner(llmRequest, stream));
204
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
+ */
205
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) {
206
296
  const missing = this.missingRequirement();
207
297
  if (missing) {
208
298
  yield missing;
209
299
  return;
210
300
  }
301
+ setLlmSpanAttribute('llm.transport', this.transport());
211
302
  const messages = this.buildMessages(llmRequest);
212
303
  const openAiTools = this.buildTools(llmRequest);
213
304
  const cfg = llmRequest.config ?? {};
@@ -229,8 +320,8 @@ export class OpenAiCompatibleLlm extends BaseLlm {
229
320
  // and return an empty turn. Ollama's OpenAI-compatible endpoint honors
230
321
  // `reasoning_effort` and IGNORES the native `think` field, so this is
231
322
  // the only lever on this path. Opt-in per agent; omitted = unchanged.
232
- ...(cfg.reasoningEffort !== undefined
233
- ? { reasoning_effort: cfg.reasoningEffort }
323
+ ...((reasoningEffort ?? cfg.reasoningEffort) !== undefined
324
+ ? { reasoning_effort: reasoningEffort ?? cfg.reasoningEffort }
234
325
  : {}),
235
326
  ...(openAiTools.length > 0 ? { tools: openAiTools } : {}),
236
327
  // Structured output: an ADK outputSchema becomes a strict json_schema
@@ -253,22 +344,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
253
344
  }
254
345
  else {
255
346
  setLlmSpanAttribute('llm.web_search.omitted', true);
347
+ setLlmSpanAttribute('llm.capability.dropped', 'web_search');
256
348
  if (!this.webSearchWarned) {
257
349
  this.webSearchWarned = true;
258
- console.warn(`⚠ web_search requested but ${this.model} has no native web search — ` +
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 — ` +
259
354
  'tool omitted (the agent runs without search).');
260
355
  }
261
356
  }
262
357
  }
263
358
  try {
264
- const res = await fetch(this.endpointUrl(), {
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(), {
265
364
  method: 'POST',
266
365
  headers: { 'Content-Type': 'application/json', ...this.headers() },
267
366
  body: JSON.stringify(body),
367
+ signal,
368
+ }, {
369
+ signal,
370
+ onRetry: ({ retries }) => setLlmSpanAttribute('llm.retries', retries),
268
371
  });
269
372
  if (!res.ok) {
270
373
  const detail = await res.text().catch(() => '');
271
- yield this.httpError(res.status, detail);
374
+ yield withHttpStatus(this.httpError(res.status, detail), res.status);
272
375
  return;
273
376
  }
274
377
  if (stream) {
@@ -276,7 +379,8 @@ export class OpenAiCompatibleLlm extends BaseLlm {
276
379
  return;
277
380
  }
278
381
  const data = await res.json();
279
- const message = data.choices?.[0]?.message ?? {};
382
+ const choice = data.choices?.[0] ?? {};
383
+ const message = choice.message ?? {};
280
384
  const usageMetadata = mapUsage(data.usage);
281
385
  const { reasoning, answer } = this.extractReasoning(message);
282
386
  if (reasoning) {
@@ -308,11 +412,7 @@ export class OpenAiCompatibleLlm extends BaseLlm {
308
412
  },
309
413
  });
310
414
  }
311
- yield {
312
- content: { role: 'model', parts },
313
- turnComplete: true,
314
- ...(usageMetadata ? { usageMetadata } : {}),
315
- };
415
+ yield this.finalResponse(parts, choice.finish_reason, Boolean(reasoning), usageMetadata);
316
416
  }
317
417
  catch (err) {
318
418
  const msg = err instanceof Error ? err.message : String(err);
@@ -338,20 +438,29 @@ export class OpenAiCompatibleLlm extends BaseLlm {
338
438
  const toolCalls = new Map();
339
439
  let answer = '';
340
440
  let answerStarted = false;
441
+ let sawReasoning = false;
442
+ let finishReason;
341
443
  let usage;
342
- const partial = (text, thought) => ({
343
- content: {
344
- role: 'model',
345
- parts: [thought ? { text, thought: true } : { text }],
346
- },
347
- partial: true,
348
- });
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
+ };
349
455
  // The scratchpad is usually trailed by blank lines; the reply must not
350
456
  // open with them, but only the FIRST answer text may be trimmed.
351
457
  const openAnswer = (text) => answerStarted ? text : text.trimStart();
352
458
  for await (const chunk of sseChunks(res)) {
353
459
  if (chunk.usage)
354
460
  usage = chunk.usage;
461
+ const finish = chunk.choices?.[0]?.finish_reason;
462
+ if (finish)
463
+ finishReason = finish;
355
464
  const delta = chunk.choices?.[0]?.delta;
356
465
  if (!delta)
357
466
  continue;
@@ -409,11 +518,38 @@ export class OpenAiCompatibleLlm extends BaseLlm {
409
518
  }
410
519
  parts.push({ functionCall: { name: acc.name, args, id: acc.id } });
411
520
  }
412
- const usageMetadata = mapUsage(usage);
413
- yield {
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 {
414
549
  content: { role: 'model', parts },
415
550
  turnComplete: true,
416
- ...(usageMetadata ? { usageMetadata } : {}),
551
+ ...(truncated ? { finishReason: FinishReason.MAX_TOKENS } : {}),
552
+ ...usage,
417
553
  };
418
554
  }
419
555
  // ── Translation helpers ────────────────────────────────────────────────────
@@ -512,14 +648,14 @@ export class OpenAiCompatibleLlm extends BaseLlm {
512
648
  for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
513
649
  if (isWebSearchSentinel(tool))
514
650
  continue; // handled via body fields
515
- const t = tool;
516
- if (t.name && t.description) {
651
+ const decl = toolDeclarationFor(tool);
652
+ if (decl) {
517
653
  openAiTools.push({
518
654
  type: 'function',
519
655
  function: {
520
- name: t.name,
521
- description: t.description,
522
- parameters: toLowercaseJsonSchema(t.parameters ?? { type: 'object', properties: {} }),
656
+ name: decl.name,
657
+ description: decl.description,
658
+ parameters: decl.parameters,
523
659
  },
524
660
  });
525
661
  }
@@ -535,7 +671,34 @@ export class OpenAiCompatibleLlm extends BaseLlm {
535
671
  'Use a Gemini model for live/streaming sessions.');
536
672
  }
537
673
  }
674
+ /**
675
+ * 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.
681
+ */
682
+ export function withHttpStatus(resp, status) {
683
+ setLlmSpanAttribute('llm.http_status', status);
684
+ return { ...resp, status, retryable: isRetryableStatus(status) };
685
+ }
538
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. */
690
+ export function addUsage(a, b) {
691
+ if (!a)
692
+ return b;
693
+ if (!b)
694
+ return a;
695
+ const out = {};
696
+ for (const k of ['promptTokenCount', 'candidatesTokenCount', 'thoughtsTokenCount', 'totalTokenCount']) {
697
+ if (a[k] !== undefined || b[k] !== undefined)
698
+ out[k] = (a[k] ?? 0) + (b[k] ?? 0);
699
+ }
700
+ return out;
701
+ }
539
702
  export function mapUsage(usage) {
540
703
  if (!usage || typeof usage !== 'object')
541
704
  return undefined;
@@ -39,7 +39,14 @@ export function providerKeyPresent(provider) {
39
39
  return true;
40
40
  // Gemini historically accepts either env name.
41
41
  if (provider === 'gemini') {
42
- return !!(process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY);
42
+ return realValue(process.env.GOOGLE_GENAI_API_KEY) || realValue(process.env.GEMINI_API_KEY);
43
43
  }
44
- return !!process.env[keyEnv];
44
+ return realValue(process.env[keyEnv]);
45
+ }
46
+ /** Set, and not a `.env.example` placeholder (`your_..._here`, `<...>`).
47
+ * Inlined rather than imported: this module stays a dependency-free leaf. */
48
+ function realValue(v) {
49
+ if (!v || !v.trim())
50
+ return false;
51
+ return !/^your[_-].*[_-]here$/i.test(v.trim()) && !/^<[^>]*>$/.test(v.trim());
45
52
  }
@@ -23,22 +23,51 @@
23
23
  * resolution (LRU), so late registration can be masked by stale cache hits.
24
24
  */
25
25
  import { Gemini } from '@google/adk';
26
- import type { BaseLlm, LlmRequest, LlmResponse } from '@google/adk';
26
+ import type { BaseLlm, GeminiParams, LlmRequest, LlmResponse } from '@google/adk';
27
+ import type { ProviderEndpoint } from './endpoints.ts';
28
+ import { GatewayLlm } from './gatewayLlm.ts';
27
29
  import { PROVIDERS, providerForModel, providerKeyPresent } from './providerMap.ts';
28
30
  import type { ProviderId } from './providerMap.ts';
29
31
  export { providerForModel, providerKeyPresent, PROVIDERS };
32
+ export { endpointFromEnv, endpointLabel, endpointProblems, PLATFORMS_FOR, platformFromEnv, providerReady, } from './endpoints.ts';
33
+ export type { Platform, ProviderEndpoint } from './endpoints.ts';
30
34
  export type { ProviderId };
35
+ export { gatewayConfig, gatewayProblem, gatewayUsable, planTransport, GATEWAYS } from './gateway.ts';
36
+ export type { GatewayId, GatewayInfo, TransportPlan } from './gateway.ts';
37
+ export { describeCapabilities, capabilitySummary } from './capabilities.ts';
38
+ export type { CapabilityReport } from './capabilities.ts';
39
+ export { GatewayLlm };
31
40
  export declare class TracedGemini extends Gemini {
32
41
  static readonly supportedModels: Array<string | RegExp>;
33
- generateContentAsync(llmRequest: LlmRequest, stream?: boolean): AsyncGenerator<LlmResponse, void>;
42
+ /**
43
+ * The Gemini platform (ADR 0023) applies to every construction, the
44
+ * LLMRegistry's included: on Vertex AI the client authenticates with
45
+ * Google Application Default Credentials for the configured project and
46
+ * location, and an AI Studio key (the environment's or a caller's) is not
47
+ * sent. `endpoint` overrides the environment's (the credentials plug point).
48
+ */
49
+ constructor(params?: GeminiParams & {
50
+ endpoint?: ProviderEndpoint;
51
+ });
52
+ generateContentAsync(llmRequest: LlmRequest, stream?: boolean, abortSignal?: AbortSignal): AsyncGenerator<LlmResponse, void>;
34
53
  /** Tags the llm.request span (web_search = Gemini grounding), then
35
- * delegates to ADK's Gemini. Runs inside the span context. */
54
+ * delegates to ADK's Gemini. Runs inside the span context. The abort
55
+ * signal is forwarded so a canceled turn stops the request in flight
56
+ * (ADK's Gemini puts it on the genai request config). */
36
57
  private tagAndGenerate;
37
58
  }
38
59
  export interface ProviderStatus {
39
60
  provider: ProviderId;
40
61
  label: string;
41
62
  available: boolean;
63
+ /**
64
+ * How this provider's ids will be served: its own endpoint, or the
65
+ * configured gateway standing in because the direct key is absent
66
+ * (lib/models/gateway.ts). Absent when unavailable.
67
+ */
68
+ transport?: 'direct' | 'gateway';
69
+ /** Gateway id when transport is 'gateway'. */
70
+ gateway?: string;
42
71
  /** Why the provider is unavailable (e.g. which env var is missing). */
43
72
  reason?: string;
44
73
  }
@@ -48,8 +77,10 @@ export declare function providerStatuses(): ProviderStatus[];
48
77
  * Registers every adapter whose credentials exist into the ADK LLMRegistry
49
78
  * (Ollama unconditionally — local needs no key; Gemini always, since the
50
79
  * registry needs a fallback class, and a missing Gemini key surfaces as a
51
- * clear API error at call time). Returns the per-provider statuses so
52
- * entrypoints can log which models are routable.
80
+ * clear API error at call time). When MODEL_GATEWAY is configured, a
81
+ * provider whose direct key is ABSENT is served by the gateway stand-in
82
+ * instead (lib/models/gateway.ts — the fallback rule). Returns the
83
+ * per-provider statuses so entrypoints can log which models are routable.
53
84
  */
54
85
  export declare function registerAvailableProviders(log?: (msg: string) => void): ProviderStatus[];
55
86
  export interface ResolveModelOptions {
@@ -61,6 +92,12 @@ export interface ResolveModelOptions {
61
92
  * YAML always wins.
62
93
  */
63
94
  defaultProvider?: string;
95
+ /**
96
+ * Where this request goes and how it authenticates (ADR 0023), merged over
97
+ * the environment's endpoint for the model's provider: the credentials plug
98
+ * point's per-request answer.
99
+ */
100
+ endpoint?: Partial<ProviderEndpoint>;
64
101
  }
65
102
  /**
66
103
  * Resolves a model id (from YAML) to a provider adapter INSTANCE, injecting