@jini-ai/agent-runtime 0.3.10 → 0.4.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 (200) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +34 -17
  3. package/dist/acp-model-probe.d.ts +6 -4
  4. package/dist/acp-model-probe.js +3 -3
  5. package/dist/agent-protocol/acp/account-failure.d.ts +4 -2
  6. package/dist/agent-protocol/acp/account-failure.js +1 -1
  7. package/dist/agent-protocol/acp/json.d.ts +6 -6
  8. package/dist/agent-protocol/acp/json.js +3 -3
  9. package/dist/agent-protocol/acp/models.d.ts +10 -5
  10. package/dist/agent-protocol/acp/models.js +32 -31
  11. package/dist/agent-protocol/acp/session-params.d.ts +3 -1
  12. package/dist/agent-protocol/acp/session-params.js +2 -1
  13. package/dist/agent-protocol/acp/session.d.ts +28 -22
  14. package/dist/agent-protocol/acp/session.js +181 -147
  15. package/dist/agent-protocol/acp/text-suppression.d.ts +1 -1
  16. package/dist/agent-protocol/acp/text-suppression.js +1 -1
  17. package/dist/agent-protocol/acp/types.d.ts +3 -2
  18. package/dist/agent-protocol/acp/updates.d.ts +11 -11
  19. package/dist/agent-protocol/acp/updates.js +2 -2
  20. package/dist/agent-protocol/core/json-line-stream.d.ts +24 -8
  21. package/dist/agent-protocol/core/json-line-stream.js +8 -8
  22. package/dist/agent-protocol/pi-rpc/events.d.ts +5 -1
  23. package/dist/agent-protocol/pi-rpc/events.js +36 -36
  24. package/dist/agent-protocol/pi-rpc/internal.d.ts +4 -1
  25. package/dist/agent-protocol/pi-rpc/models.d.ts +3 -1
  26. package/dist/agent-protocol/pi-rpc/models.js +1 -1
  27. package/dist/agent-protocol/pi-rpc/session.d.ts +1 -1
  28. package/dist/agent-protocol/pi-rpc/session.js +33 -32
  29. package/dist/amr-model-cache.d.ts +12 -4
  30. package/dist/amr-model-cache.js +17 -21
  31. package/dist/amr-profile-resolver.d.ts +3 -1
  32. package/dist/anthropic-live-models.js +0 -0
  33. package/dist/artifact-taxonomy.d.ts +6 -2
  34. package/dist/auth.d.ts +52 -15
  35. package/dist/auth.js +36 -36
  36. package/dist/claude-code-models.js +2 -2
  37. package/dist/claude-stream.d.ts +22 -3
  38. package/dist/claude-stream.js +24 -8
  39. package/dist/copilot-stream.d.ts +7 -3
  40. package/dist/copilot-stream.js +3 -3
  41. package/dist/defs/aider.d.ts +8 -1
  42. package/dist/defs/aider.js +2 -2
  43. package/dist/defs/amp.d.ts +8 -1
  44. package/dist/defs/amp.js +2 -2
  45. package/dist/defs/amr.d.ts +22 -6
  46. package/dist/defs/amr.js +23 -23
  47. package/dist/defs/antigravity.d.ts +15 -4
  48. package/dist/defs/antigravity.js +9 -8
  49. package/dist/defs/auggie.d.ts +4 -1
  50. package/dist/defs/auggie.js +1 -1
  51. package/dist/defs/claude.d.ts +12 -2
  52. package/dist/defs/claude.js +5 -5
  53. package/dist/defs/cline.d.ts +4 -1
  54. package/dist/defs/cline.js +1 -1
  55. package/dist/defs/codebuddy.d.ts +8 -1
  56. package/dist/defs/codebuddy.js +2 -2
  57. package/dist/defs/codex.d.ts +18 -4
  58. package/dist/defs/codex.js +7 -7
  59. package/dist/defs/copilot.d.ts +8 -1
  60. package/dist/defs/copilot.js +2 -2
  61. package/dist/defs/crush.d.ts +8 -1
  62. package/dist/defs/crush.js +1 -1
  63. package/dist/defs/cursor-agent.d.ts +14 -3
  64. package/dist/defs/cursor-agent.js +5 -5
  65. package/dist/defs/deepseek.d.ts +8 -1
  66. package/dist/defs/deepseek.js +2 -2
  67. package/dist/defs/devin.d.ts +12 -2
  68. package/dist/defs/devin.js +3 -3
  69. package/dist/defs/droid.d.ts +8 -1
  70. package/dist/defs/droid.js +1 -1
  71. package/dist/defs/gemini.d.ts +8 -1
  72. package/dist/defs/gemini.js +1 -1
  73. package/dist/defs/goose.d.ts +4 -1
  74. package/dist/defs/goose.js +1 -1
  75. package/dist/defs/grok-build.d.ts +11 -2
  76. package/dist/defs/grok-build.js +3 -3
  77. package/dist/defs/hermes.d.ts +4 -1
  78. package/dist/defs/hermes.js +2 -2
  79. package/dist/defs/kilo.d.ts +4 -1
  80. package/dist/defs/kilo.js +2 -2
  81. package/dist/defs/kimi.d.ts +4 -1
  82. package/dist/defs/kimi.js +2 -2
  83. package/dist/defs/kiro.d.ts +4 -1
  84. package/dist/defs/kiro.js +2 -2
  85. package/dist/defs/mimo.d.ts +8 -1
  86. package/dist/defs/mimo.js +2 -2
  87. package/dist/defs/opencode.d.ts +9 -2
  88. package/dist/defs/opencode.js +2 -2
  89. package/dist/defs/pi.d.ts +12 -2
  90. package/dist/defs/pi.js +9 -9
  91. package/dist/defs/qoder.d.ts +8 -1
  92. package/dist/defs/qoder.js +2 -2
  93. package/dist/defs/qwen.d.ts +8 -1
  94. package/dist/defs/qwen.js +2 -2
  95. package/dist/defs/reasonix.d.ts +4 -1
  96. package/dist/defs/reasonix.js +3 -3
  97. package/dist/defs/shared.d.ts +4 -2
  98. package/dist/defs/shared.js +2 -2
  99. package/dist/defs/trae-cli.d.ts +12 -2
  100. package/dist/defs/trae-cli.js +3 -3
  101. package/dist/defs/vibe.d.ts +4 -1
  102. package/dist/defs/vibe.js +2 -2
  103. package/dist/detection.d.ts +19 -4
  104. package/dist/detection.js +33 -33
  105. package/dist/diagnostics.d.ts +14 -3
  106. package/dist/diagnostics.js +5 -5
  107. package/dist/env.d.ts +16 -4
  108. package/dist/env.js +7 -7
  109. package/dist/executables.d.ts +24 -8
  110. package/dist/executables.js +13 -13
  111. package/dist/index.d.ts +228 -4
  112. package/dist/index.js +230 -6
  113. package/dist/invocation.d.ts +6 -1
  114. package/dist/invocation.js +2 -2
  115. package/dist/json-event-stream.d.ts +7 -2
  116. package/dist/json-event-stream.js +3 -3
  117. package/dist/launch.d.ts +12 -2
  118. package/dist/launch.js +3 -3
  119. package/dist/log-file.d.ts +4 -1
  120. package/dist/log-file.js +2 -2
  121. package/dist/mcp.d.ts +6 -3
  122. package/dist/mcp.js +1 -1
  123. package/dist/metadata.d.ts +7 -3
  124. package/dist/metadata.js +1 -1
  125. package/dist/mmd-routes.d.ts +22 -6
  126. package/dist/mmd-routes.js +14 -14
  127. package/dist/model-catalog-cache.d.ts +50 -0
  128. package/dist/model-catalog-cache.js +61 -0
  129. package/dist/model-loading-state.d.ts +10 -0
  130. package/dist/model-loading-state.js +23 -0
  131. package/dist/model-registry.d.ts +28 -7
  132. package/dist/model-registry.js +11 -8
  133. package/dist/models.d.ts +31 -6
  134. package/dist/models.js +6 -6
  135. package/dist/opencode-log.d.ts +12 -4
  136. package/dist/opencode-log.js +8 -8
  137. package/dist/paths.d.ts +6 -2
  138. package/dist/paths.js +3 -3
  139. package/dist/pi-models.d.ts +3 -1
  140. package/dist/pi-models.js +2 -2
  141. package/dist/prompt-budget.d.ts +17 -4
  142. package/dist/prompt-budget.js +3 -3
  143. package/dist/prompt-file.d.ts +5 -1
  144. package/dist/prompt-file.js +1 -1
  145. package/dist/providers/aihubmix.d.ts +34 -9
  146. package/dist/providers/aihubmix.js +26 -26
  147. package/dist/providers/anthropic-messages.d.ts +77 -20
  148. package/dist/providers/anthropic-messages.js +53 -50
  149. package/dist/providers/azure-chat.d.ts +21 -7
  150. package/dist/providers/azure-chat.js +18 -16
  151. package/dist/providers/connection-guard.d.ts +45 -31
  152. package/dist/providers/connection-guard.js +40 -143
  153. package/dist/providers/connection-test.d.ts +1 -1
  154. package/dist/providers/connection-test.js +13 -30
  155. package/dist/providers/elevenlabs.d.ts +7 -2
  156. package/dist/providers/elevenlabs.js +4 -4
  157. package/dist/providers/google-messages.d.ts +44 -10
  158. package/dist/providers/google-messages.js +36 -34
  159. package/dist/providers/google-schema.d.ts +26 -0
  160. package/dist/providers/google-schema.js +253 -0
  161. package/dist/providers/google.d.ts +21 -6
  162. package/dist/providers/google.js +10 -10
  163. package/dist/providers/index.d.ts +3 -2
  164. package/dist/providers/index.js +3 -2
  165. package/dist/providers/model-catalog.d.ts +1 -1
  166. package/dist/providers/model-catalog.js +14 -27
  167. package/dist/providers/oauth-callback-server.d.ts +1 -1
  168. package/dist/providers/oauth-callback-server.js +3 -2
  169. package/dist/providers/oauth-credentials.d.ts +7 -1
  170. package/dist/providers/oauth-credentials.js +5 -5
  171. package/dist/providers/oauth-provider.d.ts +2 -2
  172. package/dist/providers/oauth-provider.js +20 -25
  173. package/dist/providers/oauth-tokens.d.ts +26 -6
  174. package/dist/providers/oauth-tokens.js +9 -9
  175. package/dist/providers/ollama-chat.d.ts +66 -31
  176. package/dist/providers/ollama-chat.js +64 -61
  177. package/dist/providers/openai-chat.d.ts +77 -22
  178. package/dist/providers/openai-chat.js +71 -67
  179. package/dist/providers/pkce.d.ts +33 -20
  180. package/dist/providers/pkce.js +75 -146
  181. package/dist/providers/sse-decode.d.ts +3 -1
  182. package/dist/providers/sse-decode.js +1 -1
  183. package/dist/providers/token-params.d.ts +16 -5
  184. package/dist/providers/token-params.js +6 -6
  185. package/dist/providers/tool-turn-types.d.ts +89 -0
  186. package/dist/providers/tool-turn-types.js +2 -0
  187. package/dist/providers/tool-turn.d.ts +10 -0
  188. package/dist/providers/tool-turn.js +252 -0
  189. package/dist/qoder-stream.d.ts +7 -3
  190. package/dist/qoder-stream.js +3 -3
  191. package/dist/registry.d.ts +6 -2
  192. package/dist/registry.js +4 -4
  193. package/dist/resolution.d.ts +5 -1
  194. package/dist/resolution.js +3 -3
  195. package/dist/role-marker-guard.d.ts +9 -5
  196. package/dist/role-marker-guard.js +5 -5
  197. package/dist/terminal-launch.d.ts +6 -1
  198. package/dist/terminal-launch.js +2 -2
  199. package/dist/types.d.ts +22 -6
  200. package/package.json +43 -4
@@ -12,29 +12,29 @@
12
12
  * `/api/proxy/ollama/stream` handler):
13
13
  *
14
14
  * - **Endpoint**: `{baseUrl}/api/chat` — Ollama's own native chat API, not
15
- * the OpenAI-compatible shim. `baseUrl` has any trailing `/api` stripped
16
- * before appending, matching OD's `.replace(/\/api\/?$/, '')`.
15
+ * the OpenAI-compatible shim. `baseUrl` has any trailing `/api` stripped
16
+ * before appending, matching OD's `.replace(\/api\/?$/, '')`.
17
17
  * - **Wire format**: newline-delimited JSON (NDJSON), one JSON object per
18
- * line, terminated by a line with `"done": true` — NOT Server-Sent
19
- * Events. `text_delta`s come from `message.content`; the terminal line's
20
- * `done: true` ends the stream (see `runSingleOllamaRequest` below).
18
+ * line, terminated by a line with `"done": true` — NOT Server-Sent
19
+ * Events. `text_delta`s come from `message.content`; the terminal line's
20
+ * `done: true` ends the stream (see `runSingleOllamaRequest` below).
21
21
  * - **`apiKey` is required**, like every other provider — OD's real handler
22
- * rejects a request with no `apiKey` (`if (!apiKey || !model)`), because
23
- * its default target is Ollama Cloud (see next point), not a bare local
24
- * install.
22
+ * rejects a request with no `apiKey` (`if (!apiKey || !model)`), because
23
+ * its default target is Ollama Cloud (see next point), not a bare local
24
+ * install.
25
25
  * - **Default `baseUrl` is `https://ollama.com`** (Ollama Cloud), matching
26
- * OD's `effectiveBaseUrl = baseUrl || 'https://ollama.com'` — not a local
27
- * loopback address. A caller running a real local Ollama install still
28
- * just passes `baseUrl: 'http://localhost:11434'` explicitly; the
29
- * loopback carve-out in `connection-guard.ts` (documented there as
30
- * existing "for local LLM servers like Ollama") still applies when they
31
- * do.
26
+ * OD's `effectiveBaseUrl = baseUrl || 'https://ollama.com'` — not a local
27
+ * loopback address. A caller running a real local Ollama install still
28
+ * just passes `baseUrl: 'http://localhost:11434'` explicitly; the
29
+ * loopback carve-out in `connection-guard.ts` (documented there as
30
+ * existing "for local LLM servers like Ollama") still applies when they
31
+ * do.
32
32
  * - **Token limit**: `options.num_predict` (Ollama's native token-limit
33
- * field), sent only when `maxTokens` is a positive number — matches OD's
34
- * `if (typeof maxTokens === 'number' && maxTokens > 0) payload.options = {
35
- * num_predict: maxTokens }`.
33
+ * field), sent only when `maxTokens` is a positive number — matches OD's
34
+ * `if (typeof maxTokens === 'number' && maxTokens > 0) payload.options = {
35
+ * num_predict: maxTokens }`.
36
36
  *
37
- * **Round-4 external audit fix (`AUD-R4-002`)**: the first port of this
37
+ * **Round-4 external audit fix **: the first port of this
38
38
  * module's tool-call loop kept the OpenAI-compatible wire shape for both the
39
39
  * emitted lifecycle event and the continuation request — `tool_use` was
40
40
  * never emitted, and the assistant/tool continuation messages used a
@@ -101,6 +101,7 @@
101
101
  * as an error the model can see, which is impossible without it. Wired
102
102
  * through as `result.isError ?? false`, so every existing caller that never
103
103
  * set `isError` keeps its exact prior behavior.
104
+ * See docs/decisions/DR-001-provider-native-tool-lifecycle.md.
104
105
  */
105
106
  import { createRoleMarkerGuard } from '../role-marker-guard.js';
106
107
  import { type DnsLookupFn, type PinnedFetch } from './connection-guard.js';
@@ -120,10 +121,11 @@ export interface OllamaFunctionToolDef {
120
121
  * no `id`/`type` field at all (confirmed against Ollama's own documented
121
122
  * "Chat request (No streaming, with tools)" example,
122
123
  * `github.com/ollama/ollama/blob/main/docs/api.md`). Round-4 external audit
123
- * (`AUD-R4-002`) found the first port had kept the OpenAI shape here, which
124
+ * found the first port had kept the OpenAI shape here, which
124
125
  * a strict Ollama server can reject. Synthetic per-call `id`s are still
125
126
  * generated (see `runSingleOllamaRequest`) but stay purely internal to this
126
127
  * module's own event stream — they are never put on the wire.
128
+ * See docs/decisions/DR-001-provider-native-tool-lifecycle.md.
127
129
  */
128
130
  export interface OllamaToolCallParam {
129
131
  readonly function: {
@@ -240,17 +242,29 @@ export interface OllamaStreamState {
240
242
  finishReason: string | null;
241
243
  }
242
244
  /** Parses one `function.arguments` value: Ollama's native tool-call payload documents it as a native JSON object, but a strict/older server can still send a stringified blob — this tolerates either, falling back to the raw string on malformed JSON. */
243
- export declare function parseOllamaToolCallArguments(args: unknown): unknown;
245
+ export declare function parseOllamaToolCallArguments({ args }: {
246
+ args: unknown;
247
+ }): unknown;
244
248
  /** Resolves one raw `message.tool_calls[]` entry into an `OllamaToolCall`, or `null` when the entry is missing its `function`/`name` (malformed, so skipped rather than surfaced). `index` seeds the synthetic id fallback — see `OllamaToolCallParam`'s doc for why a call id is generated locally rather than trusted off the wire. */
245
- export declare function resolveOllamaToolCall(rawCall: unknown, index: number): OllamaToolCall | null;
249
+ export declare function resolveOllamaToolCall({ rawCall, index }: {
250
+ rawCall: unknown;
251
+ index: number;
252
+ }): OllamaToolCall | null;
246
253
  /** Resolves every entry in `message.tool_calls` into `state.toolCalls`, then marks `finishReason: 'tool_calls'` once at least one resolved — mirrors the original inline loop's behavior of setting `finishReason` after the whole batch rather than per-call. */
247
- export declare function handleOllamaToolCallsField(state: OllamaStreamState, message: Record<string, unknown>): void;
254
+ export declare function handleOllamaToolCallsField({ state, message }: {
255
+ state: OllamaStreamState;
256
+ message: Record<string, unknown>;
257
+ }): void;
248
258
  /**
249
259
  * Feeds one `message.content` delta through the role-marker guard and emits the safe portion.
250
260
  * Returns `'break'` once the guard flags contamination, otherwise `'continue'` — same contract as
251
261
  * `anthropic-messages.ts#handleAnthropicTextDelta`/`openai-chat.ts#handleOpenAiTextContentDelta`.
252
262
  */
253
- export declare function handleOllamaTextContent(state: OllamaStreamState, content: string, onEvent: (event: OllamaTurnEvent) => void): 'continue' | 'break';
263
+ export declare function handleOllamaTextContent({ state, content, onEvent }: {
264
+ state: OllamaStreamState;
265
+ content: string;
266
+ onEvent: (event: OllamaTurnEvent) => void;
267
+ }): 'continue' | 'break';
254
268
  /**
255
269
  * Reduces one already-parsed NDJSON line into `state`, reporting whether the caller's line loop
256
270
  * should stop and, if so, why. `'break'` calls `emitEnd('contaminated')` itself (mirroring the
@@ -259,37 +273,58 @@ export declare function handleOllamaTextContent(state: OllamaStreamState, conten
259
273
  * the caller (`runSingleOllamaRequest`) still owns the normal-completion `emitEnd`, exactly like
260
274
  * every sibling turn-runner's `emitEnd` contract.
261
275
  */
262
- export declare function processOllamaLine(state: OllamaStreamState, line: Record<string, unknown>, onEvent: (event: OllamaTurnEvent) => void, emitEnd: (reason: OllamaTurnEndReason) => void): 'continue' | 'break' | 'done';
276
+ export declare function processOllamaLine({ state, line, onEvent, emitEnd }: {
277
+ state: OllamaStreamState;
278
+ line: Record<string, unknown>;
279
+ onEvent: (event: OllamaTurnEvent) => void;
280
+ emitEnd: (reason: OllamaTurnEndReason) => void;
281
+ }): 'continue' | 'break' | 'done';
263
282
  /** Parses one NDJSON line, returning `undefined` for a malformed/empty line — JSON has no `undefined` literal, so a successful parse is never confused with a failed one. Extracted so the generator below never nests a `try`/`catch` inside its loops (see that function's doc). */
264
- export declare function parseNdjsonLine(line: string): unknown;
283
+ export declare function parseNdjsonLine({ line }: {
284
+ line: string;
285
+ }): unknown;
265
286
  /** Splits already-accumulated buffer content into complete newline-terminated lines (newline stripped) plus the remaining partial-line `remainder`. Pure — no I/O — so this buffering/splitting logic is exercised as plain value-in/value-out assertions instead of through the full streaming generator. */
266
- export declare function splitNdjsonLines(buffer: string): {
287
+ export declare function splitNdjsonLines({ buffer }: {
288
+ buffer: string;
289
+ }): {
267
290
  readonly lines: readonly string[];
268
291
  readonly remainder: string;
269
292
  };
270
293
  /** Trims and parses each complete line in `rawLines`, yielding only the ones that parsed to something other than `undefined` — a plain (non-async) generator so `decodeNdjsonStream` below can delegate to it with `yield*` instead of nesting a `for`/`if`/`if` inside its own `for await` loop. */
271
- export declare function parseNdjsonLines(rawLines: readonly string[]): Generator<unknown>;
294
+ export declare function parseNdjsonLines({ rawLines }: {
295
+ rawLines: readonly string[];
296
+ }): Generator<unknown>;
272
297
  /**
273
298
  * Decides why the tool loop should stop after one request, or returns `null` when it should
274
299
  * instead proceed to execute the pending tool calls — mirrors
275
300
  * `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
276
301
  */
277
- export declare function ollamaLoopExitReason(outcome: SingleRequestOutcome, toolTurns: number, maxToolTurns: number): OllamaTurnEndReason | null;
302
+ export declare function ollamaLoopExitReason({ outcome, toolTurns, maxToolTurns }: {
303
+ outcome: SingleRequestOutcome;
304
+ toolTurns: number;
305
+ maxToolTurns: number;
306
+ }): OllamaTurnEndReason | null;
278
307
  /** Builds the assistant continuation's `tool_calls` field — Ollama's native shape has no `id`/`type` on the wire (see `OllamaToolCallParam`'s doc). */
279
- export declare function buildOllamaAssistantToolCalls(toolCalls: readonly OllamaToolCall[]): OllamaToolCallParam[];
308
+ export declare function buildOllamaAssistantToolCalls({ toolCalls }: {
309
+ toolCalls: readonly OllamaToolCall[];
310
+ }): OllamaToolCallParam[];
280
311
  /**
281
312
  * Runs every pending tool call in order, applying `guardToolResult` and reducing the results into
282
313
  * the `tool`-role continuation messages Ollama's native `tool_name`-association scheme expects —
283
314
  * see module doc's "Design decision: native `tool`-role images" section for why no separate
284
315
  * follow-up message is needed here, unlike `openai-chat.ts`/`azure-chat.ts`.
285
316
  */
286
- export declare function executeOllamaToolCalls(executeTool: OllamaToolExecutor, calls: readonly OllamaToolCall[], onEvent: (event: OllamaTurnEvent) => void): Promise<OllamaMessageParam[]>;
317
+ export declare function executeOllamaToolCalls({ executeTool, calls, onEvent }: {
318
+ executeTool: OllamaToolExecutor;
319
+ calls: readonly OllamaToolCall[];
320
+ onEvent: (event: OllamaTurnEvent) => void;
321
+ }): Promise<OllamaMessageParam[]>;
287
322
  /**
288
323
  * Runs a full Ollama native `/api/chat` turn, including the tool-execution
289
324
  * loop when `options.executeTool` is supplied and the model requests a
290
325
  * function call. See `anthropic-messages.ts#runAnthropicToolTurn`'s doc for
291
326
  * the shared event-stream/`ended`-flag contract this mirrors exactly.
292
327
  */
293
- export declare function runOllamaToolTurn(options: OllamaTurnOptions): Promise<OllamaTurnResult>;
328
+ export declare function runOllamaToolTurn(requiredArgs: Pick<OllamaTurnOptions, "apiKey" | "model" | "messages" | "onEvent">, optionalArgs?: Omit<OllamaTurnOptions, "apiKey" | "model" | "messages" | "onEvent">): Promise<OllamaTurnResult>;
294
329
  export {};
295
330
  //# sourceMappingURL=ollama-chat.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { redactSecrets } from '@jini-ai/core';
1
2
  /**
2
3
  * @module providers/ollama-chat
3
4
  *
@@ -12,29 +13,29 @@
12
13
  * `/api/proxy/ollama/stream` handler):
13
14
  *
14
15
  * - **Endpoint**: `{baseUrl}/api/chat` — Ollama's own native chat API, not
15
- * the OpenAI-compatible shim. `baseUrl` has any trailing `/api` stripped
16
- * before appending, matching OD's `.replace(/\/api\/?$/, '')`.
16
+ * the OpenAI-compatible shim. `baseUrl` has any trailing `/api` stripped
17
+ * before appending, matching OD's `.replace(\/api\/?$/, '')`.
17
18
  * - **Wire format**: newline-delimited JSON (NDJSON), one JSON object per
18
- * line, terminated by a line with `"done": true` — NOT Server-Sent
19
- * Events. `text_delta`s come from `message.content`; the terminal line's
20
- * `done: true` ends the stream (see `runSingleOllamaRequest` below).
19
+ * line, terminated by a line with `"done": true` — NOT Server-Sent
20
+ * Events. `text_delta`s come from `message.content`; the terminal line's
21
+ * `done: true` ends the stream (see `runSingleOllamaRequest` below).
21
22
  * - **`apiKey` is required**, like every other provider — OD's real handler
22
- * rejects a request with no `apiKey` (`if (!apiKey || !model)`), because
23
- * its default target is Ollama Cloud (see next point), not a bare local
24
- * install.
23
+ * rejects a request with no `apiKey` (`if (!apiKey || !model)`), because
24
+ * its default target is Ollama Cloud (see next point), not a bare local
25
+ * install.
25
26
  * - **Default `baseUrl` is `https://ollama.com`** (Ollama Cloud), matching
26
- * OD's `effectiveBaseUrl = baseUrl || 'https://ollama.com'` — not a local
27
- * loopback address. A caller running a real local Ollama install still
28
- * just passes `baseUrl: 'http://localhost:11434'` explicitly; the
29
- * loopback carve-out in `connection-guard.ts` (documented there as
30
- * existing "for local LLM servers like Ollama") still applies when they
31
- * do.
27
+ * OD's `effectiveBaseUrl = baseUrl || 'https://ollama.com'` — not a local
28
+ * loopback address. A caller running a real local Ollama install still
29
+ * just passes `baseUrl: 'http://localhost:11434'` explicitly; the
30
+ * loopback carve-out in `connection-guard.ts` (documented there as
31
+ * existing "for local LLM servers like Ollama") still applies when they
32
+ * do.
32
33
  * - **Token limit**: `options.num_predict` (Ollama's native token-limit
33
- * field), sent only when `maxTokens` is a positive number — matches OD's
34
- * `if (typeof maxTokens === 'number' && maxTokens > 0) payload.options = {
35
- * num_predict: maxTokens }`.
34
+ * field), sent only when `maxTokens` is a positive number — matches OD's
35
+ * `if (typeof maxTokens === 'number' && maxTokens > 0) payload.options = {
36
+ * num_predict: maxTokens }`.
36
37
  *
37
- * **Round-4 external audit fix (`AUD-R4-002`)**: the first port of this
38
+ * **Round-4 external audit fix **: the first port of this
38
39
  * module's tool-call loop kept the OpenAI-compatible wire shape for both the
39
40
  * emitted lifecycle event and the continuation request — `tool_use` was
40
41
  * never emitted, and the assistant/tool continuation messages used a
@@ -101,9 +102,10 @@
101
102
  * as an error the model can see, which is impossible without it. Wired
102
103
  * through as `result.isError ?? false`, so every existing caller that never
103
104
  * set `isError` keeps its exact prior behavior.
105
+ * See docs/decisions/DR-001-provider-native-tool-lifecycle.md.
104
106
  */
105
107
  import { createRoleMarkerGuard } from '../role-marker-guard.js';
106
- import { defaultDnsLookup, pinnedFetch, redactSecrets, validateBaseUrlResolved } from './connection-guard.js';
108
+ import { defaultDnsLookup, pinnedFetch, validateBaseUrlResolved } from './connection-guard.js';
107
109
  import { createTurnEndGuard } from './turn-end-guard.js';
108
110
  const DEFAULT_OLLAMA_BASE_URL = 'https://ollama.com';
109
111
  const DEFAULT_MAX_TOOL_TURNS = 8;
@@ -199,7 +201,7 @@ function guardToolResult(result) {
199
201
  return result;
200
202
  }
201
203
  /** Parses one `function.arguments` value: Ollama's native tool-call payload documents it as a native JSON object, but a strict/older server can still send a stringified blob — this tolerates either, falling back to the raw string on malformed JSON. */
202
- export function parseOllamaToolCallArguments(args) {
204
+ export function parseOllamaToolCallArguments({ args }) {
203
205
  if (typeof args !== 'string')
204
206
  return args;
205
207
  try {
@@ -210,22 +212,22 @@ export function parseOllamaToolCallArguments(args) {
210
212
  }
211
213
  }
212
214
  /** Resolves one raw `message.tool_calls[]` entry into an `OllamaToolCall`, or `null` when the entry is missing its `function`/`name` (malformed, so skipped rather than surfaced). `index` seeds the synthetic id fallback — see `OllamaToolCallParam`'s doc for why a call id is generated locally rather than trusted off the wire. */
213
- export function resolveOllamaToolCall(rawCall, index) {
215
+ export function resolveOllamaToolCall({ rawCall, index }) {
214
216
  if (!isRecord(rawCall) || !isRecord(rawCall.function))
215
217
  return null;
216
218
  const name = typeof rawCall.function.name === 'string' ? rawCall.function.name : '';
217
219
  if (!name)
218
220
  return null;
219
- const input = parseOllamaToolCallArguments(rawCall.function.arguments);
221
+ const input = parseOllamaToolCallArguments({ args: rawCall.function.arguments });
220
222
  const id = typeof rawCall.id === 'string' && rawCall.id ? rawCall.id : `ollama-tool-${index}`;
221
223
  return { id, name, input };
222
224
  }
223
225
  /** Resolves every entry in `message.tool_calls` into `state.toolCalls`, then marks `finishReason: 'tool_calls'` once at least one resolved — mirrors the original inline loop's behavior of setting `finishReason` after the whole batch rather than per-call. */
224
- export function handleOllamaToolCallsField(state, message) {
226
+ export function handleOllamaToolCallsField({ state, message }) {
225
227
  if (!Array.isArray(message.tool_calls))
226
228
  return;
227
229
  for (const rawCall of message.tool_calls) {
228
- const call = resolveOllamaToolCall(rawCall, state.toolCalls.length);
230
+ const call = resolveOllamaToolCall({ rawCall: rawCall, index: state.toolCalls.length });
229
231
  if (call)
230
232
  state.toolCalls.push(call);
231
233
  }
@@ -237,8 +239,8 @@ export function handleOllamaToolCallsField(state, message) {
237
239
  * Returns `'break'` once the guard flags contamination, otherwise `'continue'` — same contract as
238
240
  * `anthropic-messages.ts#handleAnthropicTextDelta`/`openai-chat.ts#handleOpenAiTextContentDelta`.
239
241
  */
240
- export function handleOllamaTextContent(state, content, onEvent) {
241
- const safe = state.guard.feedText(content);
242
+ export function handleOllamaTextContent({ state, content, onEvent }) {
243
+ const safe = state.guard.feedText({ text: content });
242
244
  if (safe.length > 0) {
243
245
  state.fullText += safe;
244
246
  onEvent({ type: 'text_delta', delta: safe });
@@ -258,17 +260,17 @@ export function handleOllamaTextContent(state, content, onEvent) {
258
260
  * the caller (`runSingleOllamaRequest`) still owns the normal-completion `emitEnd`, exactly like
259
261
  * every sibling turn-runner's `emitEnd` contract.
260
262
  */
261
- export function processOllamaLine(state, line, onEvent, emitEnd) {
263
+ export function processOllamaLine({ state, line, onEvent, emitEnd }) {
262
264
  const message = isRecord(line.message) ? line.message : null;
263
265
  if (message && typeof message.content === 'string' && message.content.length > 0) {
264
- if (handleOllamaTextContent(state, message.content, onEvent) === 'break') {
266
+ if (handleOllamaTextContent({ state: state, content: message.content, onEvent: onEvent }) === 'break') {
265
267
  state.finishReason = 'contaminated';
266
268
  emitEnd('contaminated');
267
269
  return 'break';
268
270
  }
269
271
  }
270
272
  if (message)
271
- handleOllamaToolCallsField(state, message);
273
+ handleOllamaToolCallsField({ state: state, message: message });
272
274
  if (line.done === true) {
273
275
  if (state.finishReason !== 'contaminated' && state.finishReason !== 'tool_calls')
274
276
  state.finishReason = 'stop';
@@ -276,7 +278,7 @@ export function processOllamaLine(state, line, onEvent, emitEnd) {
276
278
  }
277
279
  return 'continue';
278
280
  }
279
- /** Emitted for every resolved call as soon as the stream ends, independent of whether the caller actually supplied an `executeTool` — see AUD-R4-002 fix note in the module doc. */
281
+ /** Emitted for every resolved call as soon as the stream ends, independent of whether the caller actually supplied an `executeTool` — see fix note in the module doc. See docs/decisions/DR-001-provider-native-tool-lifecycle.md. */
280
282
  function emitPendingOllamaToolUseEvents(toolCalls, onEvent) {
281
283
  for (const call of toolCalls) {
282
284
  onEvent({ type: 'tool_use', id: call.id, name: call.name, input: call.input });
@@ -291,7 +293,7 @@ function emitPendingOllamaToolUseEvents(toolCalls, onEvent) {
291
293
  */
292
294
  async function openOllamaResponseStream(options, messages, emitEnd) {
293
295
  const { onEvent } = options;
294
- const baseUrlCheck = await validateBaseUrlResolved(options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL, options.dnsLookup ?? defaultDnsLookup);
296
+ const baseUrlCheck = await validateBaseUrlResolved({ baseUrl: options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL, lookup: options.dnsLookup ?? defaultDnsLookup });
295
297
  if (baseUrlCheck.error) {
296
298
  onEvent({ type: 'error', message: baseUrlCheck.error });
297
299
  emitEnd('error');
@@ -299,22 +301,22 @@ async function openOllamaResponseStream(options, messages, emitEnd) {
299
301
  }
300
302
  let response;
301
303
  try {
302
- response = await (options.fetchImpl ?? pinnedFetch)(ollamaRequestUrl(options.baseUrl), {
303
- method: 'POST',
304
- headers: ollamaHeaders(options),
305
- body: JSON.stringify(ollamaRequestBody(options, messages)),
306
- // Unlike the other three providers, this call never set `redirect: 'error'` even before
307
- // this pinning change — the reviewer's redirect-rebinding finding was only reported (and
308
- // fixed) for `openai-chat.ts`. `pinnedFetch` closes it here regardless, the same way it
309
- // does everywhere else (`node:http`'s `request` never auto-follows); the flag is added for
310
- // self-documentation, matching the other three call sites.
311
- redirect: 'error',
312
- ...(options.signal ? { signal: options.signal } : {}),
313
- }, baseUrlCheck.pinnedAddress);
304
+ response = await (options.fetchImpl ?? pinnedFetch)({ url: ollamaRequestUrl(options.baseUrl), init: {
305
+ method: 'POST',
306
+ headers: ollamaHeaders(options),
307
+ body: JSON.stringify(ollamaRequestBody(options, messages)),
308
+ // Unlike the other three providers, this call never set `redirect: 'error'` even before
309
+ // this pinning change — the reviewer's redirect-rebinding finding was only reported (and
310
+ // fixed) for `openai-chat.ts`. `pinnedFetch` closes it here regardless, the same way it
311
+ // does everywhere else (`node:http`'s `request` never auto-follows); the flag is added for
312
+ // self-documentation, matching the other three call sites.
313
+ redirect: 'error',
314
+ ...(options.signal ? { signal: options.signal } : {}),
315
+ }, pinnedAddress: baseUrlCheck.pinnedAddress });
314
316
  }
315
317
  catch (error) {
316
318
  const message = error instanceof Error ? error.message : String(error);
317
- onEvent({ type: 'error', message: redactSecrets(message, [options.apiKey]) });
319
+ onEvent({ type: 'error', message: redactSecrets({ input: message }, { exactSecrets: [options.apiKey] }) });
318
320
  emitEnd('error');
319
321
  return null;
320
322
  }
@@ -322,7 +324,7 @@ async function openOllamaResponseStream(options, messages, emitEnd) {
322
324
  const rawText = await response.text();
323
325
  onEvent({
324
326
  type: 'error',
325
- message: redactSecrets(extractOllamaErrorDetail(rawText), [options.apiKey]),
327
+ message: redactSecrets({ input: extractOllamaErrorDetail(rawText) }, { exactSecrets: [options.apiKey] }),
326
328
  code: String(response.status),
327
329
  });
328
330
  emitEnd('error');
@@ -336,7 +338,7 @@ async function openOllamaResponseStream(options, messages, emitEnd) {
336
338
  return response.body;
337
339
  }
338
340
  /** Parses one NDJSON line, returning `undefined` for a malformed/empty line — JSON has no `undefined` literal, so a successful parse is never confused with a failed one. Extracted so the generator below never nests a `try`/`catch` inside its loops (see that function's doc). */
339
- export function parseNdjsonLine(line) {
341
+ export function parseNdjsonLine({ line }) {
340
342
  try {
341
343
  return JSON.parse(line);
342
344
  }
@@ -345,7 +347,7 @@ export function parseNdjsonLine(line) {
345
347
  }
346
348
  }
347
349
  /** Splits already-accumulated buffer content into complete newline-terminated lines (newline stripped) plus the remaining partial-line `remainder`. Pure — no I/O — so this buffering/splitting logic is exercised as plain value-in/value-out assertions instead of through the full streaming generator. */
348
- export function splitNdjsonLines(buffer) {
350
+ export function splitNdjsonLines({ buffer }) {
349
351
  const lines = [];
350
352
  let rest = buffer;
351
353
  let newlineIndex;
@@ -356,12 +358,12 @@ export function splitNdjsonLines(buffer) {
356
358
  return { lines, remainder: rest };
357
359
  }
358
360
  /** Trims and parses each complete line in `rawLines`, yielding only the ones that parsed to something other than `undefined` — a plain (non-async) generator so `decodeNdjsonStream` below can delegate to it with `yield*` instead of nesting a `for`/`if`/`if` inside its own `for await` loop. */
359
- export function* parseNdjsonLines(rawLines) {
361
+ export function* parseNdjsonLines({ rawLines }) {
360
362
  for (const rawLine of rawLines) {
361
363
  const line = rawLine.trim();
362
364
  if (!line)
363
365
  continue; // tolerate a malformed/empty keep-alive line
364
- const parsed = parseNdjsonLine(line);
366
+ const parsed = parseNdjsonLine({ line: line });
365
367
  if (parsed !== undefined)
366
368
  yield parsed;
367
369
  }
@@ -379,14 +381,14 @@ async function* decodeNdjsonStream(body) {
379
381
  let buffer = '';
380
382
  for await (const chunk of body) {
381
383
  buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
382
- const { lines, remainder } = splitNdjsonLines(buffer);
384
+ const { lines, remainder } = splitNdjsonLines({ buffer: buffer });
383
385
  buffer = remainder;
384
- yield* parseNdjsonLines(lines);
386
+ yield* parseNdjsonLines({ rawLines: lines });
385
387
  }
386
388
  const trailing = buffer.trim();
387
389
  if (!trailing)
388
390
  return;
389
- const parsed = parseNdjsonLine(trailing);
391
+ const parsed = parseNdjsonLine({ line: trailing });
390
392
  if (parsed !== undefined)
391
393
  yield parsed; // else: final partial line was never completed — nothing usable to yield
392
394
  }
@@ -399,7 +401,7 @@ async function runSingleOllamaRequest(options, messages, emitEnd) {
399
401
  }
400
402
  onEvent({ type: 'status', label: 'requesting' });
401
403
  const state = {
402
- guard: createRoleMarkerGuard('ollama-turn'),
404
+ guard: createRoleMarkerGuard({ messageId: 'ollama-turn' }),
403
405
  toolCalls: [],
404
406
  fullText: '',
405
407
  finishReason: null,
@@ -407,7 +409,7 @@ async function runSingleOllamaRequest(options, messages, emitEnd) {
407
409
  for await (const line of decodeNdjsonStream(body)) {
408
410
  if (!isRecord(line))
409
411
  continue;
410
- const result = processOllamaLine(state, line, onEvent, emitEnd);
412
+ const result = processOllamaLine({ state: state, line: line, onEvent: onEvent, emitEnd: emitEnd });
411
413
  if (result === 'break' || result === 'done')
412
414
  break;
413
415
  }
@@ -421,7 +423,7 @@ async function runSingleOllamaRequest(options, messages, emitEnd) {
421
423
  * instead proceed to execute the pending tool calls — mirrors
422
424
  * `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
423
425
  */
424
- export function ollamaLoopExitReason(outcome, toolTurns, maxToolTurns) {
426
+ export function ollamaLoopExitReason({ outcome, toolTurns, maxToolTurns }) {
425
427
  if (outcome.finishReason !== 'tool_calls' || outcome.toolCalls.length === 0)
426
428
  return 'stop';
427
429
  if (toolTurns >= maxToolTurns)
@@ -429,7 +431,7 @@ export function ollamaLoopExitReason(outcome, toolTurns, maxToolTurns) {
429
431
  return null;
430
432
  }
431
433
  /** Builds the assistant continuation's `tool_calls` field — Ollama's native shape has no `id`/`type` on the wire (see `OllamaToolCallParam`'s doc). */
432
- export function buildOllamaAssistantToolCalls(toolCalls) {
434
+ export function buildOllamaAssistantToolCalls({ toolCalls }) {
433
435
  return toolCalls.map((call) => ({ function: { name: call.name, arguments: call.input } }));
434
436
  }
435
437
  /**
@@ -438,7 +440,7 @@ export function buildOllamaAssistantToolCalls(toolCalls) {
438
440
  * see module doc's "Design decision: native `tool`-role images" section for why no separate
439
441
  * follow-up message is needed here, unlike `openai-chat.ts`/`azure-chat.ts`.
440
442
  */
441
- export async function executeOllamaToolCalls(executeTool, calls, onEvent) {
443
+ export async function executeOllamaToolCalls({ executeTool, calls, onEvent }) {
442
444
  const toolResultMessages = [];
443
445
  for (const call of calls) {
444
446
  const rawResult = await executeTool(call);
@@ -465,7 +467,8 @@ export async function executeOllamaToolCalls(executeTool, calls, onEvent) {
465
467
  * function call. See `anthropic-messages.ts#runAnthropicToolTurn`'s doc for
466
468
  * the shared event-stream/`ended`-flag contract this mirrors exactly.
467
469
  */
468
- export async function runOllamaToolTurn(options) {
470
+ export async function runOllamaToolTurn(requiredArgs, optionalArgs = {}) {
471
+ const options = { ...optionalArgs, ...requiredArgs };
469
472
  const maxToolTurns = options.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
470
473
  const executeTool = options.executeTool;
471
474
  const endGuard = createTurnEndGuard(options.onEvent, (reason) => ({ type: 'end', reason }));
@@ -478,7 +481,7 @@ export async function runOllamaToolTurn(options) {
478
481
  lastFinishReason = outcome.finishReason;
479
482
  if (endGuard.hasEnded())
480
483
  break;
481
- const exitReason = ollamaLoopExitReason(outcome, toolTurns, maxToolTurns);
484
+ const exitReason = ollamaLoopExitReason({ outcome: outcome, toolTurns: toolTurns, maxToolTurns: maxToolTurns });
482
485
  if (exitReason) {
483
486
  emitEnd(exitReason);
484
487
  break;
@@ -488,8 +491,8 @@ export async function runOllamaToolTurn(options) {
488
491
  break;
489
492
  }
490
493
  toolTurns += 1;
491
- const assistantToolCalls = buildOllamaAssistantToolCalls(outcome.toolCalls);
492
- const toolResultMessages = await executeOllamaToolCalls(executeTool, outcome.toolCalls, options.onEvent);
494
+ const assistantToolCalls = buildOllamaAssistantToolCalls({ toolCalls: outcome.toolCalls });
495
+ const toolResultMessages = await executeOllamaToolCalls({ executeTool: executeTool, calls: outcome.toolCalls, onEvent: options.onEvent });
493
496
  messages = [
494
497
  ...messages,
495
498
  { role: 'assistant', content: outcome.text || null, tool_calls: assistantToolCalls },