@jini-ai/agent-runtime 0.3.10 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/README.md +34 -17
- package/dist/acp-model-probe.d.ts +6 -4
- package/dist/acp-model-probe.js +3 -3
- package/dist/agent-protocol/acp/account-failure.d.ts +4 -2
- package/dist/agent-protocol/acp/account-failure.js +1 -1
- package/dist/agent-protocol/acp/json.d.ts +6 -6
- package/dist/agent-protocol/acp/json.js +3 -3
- package/dist/agent-protocol/acp/models.d.ts +10 -5
- package/dist/agent-protocol/acp/models.js +32 -31
- package/dist/agent-protocol/acp/session-params.d.ts +3 -1
- package/dist/agent-protocol/acp/session-params.js +2 -1
- package/dist/agent-protocol/acp/session.d.ts +28 -22
- package/dist/agent-protocol/acp/session.js +181 -147
- package/dist/agent-protocol/acp/text-suppression.d.ts +1 -1
- package/dist/agent-protocol/acp/text-suppression.js +1 -1
- package/dist/agent-protocol/acp/types.d.ts +3 -2
- package/dist/agent-protocol/acp/updates.d.ts +11 -11
- package/dist/agent-protocol/acp/updates.js +2 -2
- package/dist/agent-protocol/core/json-line-stream.d.ts +24 -8
- package/dist/agent-protocol/core/json-line-stream.js +8 -8
- package/dist/agent-protocol/pi-rpc/events.d.ts +5 -1
- package/dist/agent-protocol/pi-rpc/events.js +36 -36
- package/dist/agent-protocol/pi-rpc/internal.d.ts +4 -1
- package/dist/agent-protocol/pi-rpc/models.d.ts +3 -1
- package/dist/agent-protocol/pi-rpc/models.js +1 -1
- package/dist/agent-protocol/pi-rpc/session.d.ts +1 -1
- package/dist/agent-protocol/pi-rpc/session.js +33 -32
- package/dist/amr-model-cache.d.ts +12 -4
- package/dist/amr-model-cache.js +17 -21
- package/dist/amr-profile-resolver.d.ts +3 -1
- package/dist/anthropic-live-models.js +0 -0
- package/dist/artifact-taxonomy.d.ts +6 -2
- package/dist/auth.d.ts +52 -15
- package/dist/auth.js +36 -36
- package/dist/claude-code-models.js +2 -2
- package/dist/claude-stream.d.ts +22 -3
- package/dist/claude-stream.js +24 -8
- package/dist/copilot-stream.d.ts +7 -3
- package/dist/copilot-stream.js +3 -3
- package/dist/defs/aider.d.ts +8 -1
- package/dist/defs/aider.js +2 -2
- package/dist/defs/amp.d.ts +8 -1
- package/dist/defs/amp.js +2 -2
- package/dist/defs/amr.d.ts +22 -6
- package/dist/defs/amr.js +23 -23
- package/dist/defs/antigravity.d.ts +15 -4
- package/dist/defs/antigravity.js +9 -8
- package/dist/defs/auggie.d.ts +4 -1
- package/dist/defs/auggie.js +1 -1
- package/dist/defs/claude.d.ts +12 -2
- package/dist/defs/claude.js +5 -5
- package/dist/defs/cline.d.ts +4 -1
- package/dist/defs/cline.js +1 -1
- package/dist/defs/codebuddy.d.ts +8 -1
- package/dist/defs/codebuddy.js +2 -2
- package/dist/defs/codex.d.ts +18 -4
- package/dist/defs/codex.js +7 -7
- package/dist/defs/copilot.d.ts +8 -1
- package/dist/defs/copilot.js +2 -2
- package/dist/defs/crush.d.ts +8 -1
- package/dist/defs/crush.js +1 -1
- package/dist/defs/cursor-agent.d.ts +14 -3
- package/dist/defs/cursor-agent.js +5 -5
- package/dist/defs/deepseek.d.ts +8 -1
- package/dist/defs/deepseek.js +2 -2
- package/dist/defs/devin.d.ts +12 -2
- package/dist/defs/devin.js +3 -3
- package/dist/defs/droid.d.ts +8 -1
- package/dist/defs/droid.js +1 -1
- package/dist/defs/gemini.d.ts +8 -1
- package/dist/defs/gemini.js +1 -1
- package/dist/defs/goose.d.ts +4 -1
- package/dist/defs/goose.js +1 -1
- package/dist/defs/grok-build.d.ts +11 -2
- package/dist/defs/grok-build.js +3 -3
- package/dist/defs/hermes.d.ts +4 -1
- package/dist/defs/hermes.js +2 -2
- package/dist/defs/kilo.d.ts +4 -1
- package/dist/defs/kilo.js +2 -2
- package/dist/defs/kimi.d.ts +4 -1
- package/dist/defs/kimi.js +2 -2
- package/dist/defs/kiro.d.ts +4 -1
- package/dist/defs/kiro.js +2 -2
- package/dist/defs/mimo.d.ts +8 -1
- package/dist/defs/mimo.js +2 -2
- package/dist/defs/opencode.d.ts +9 -2
- package/dist/defs/opencode.js +2 -2
- package/dist/defs/pi.d.ts +12 -2
- package/dist/defs/pi.js +9 -9
- package/dist/defs/qoder.d.ts +8 -1
- package/dist/defs/qoder.js +2 -2
- package/dist/defs/qwen.d.ts +8 -1
- package/dist/defs/qwen.js +2 -2
- package/dist/defs/reasonix.d.ts +4 -1
- package/dist/defs/reasonix.js +3 -3
- package/dist/defs/shared.d.ts +4 -2
- package/dist/defs/shared.js +2 -2
- package/dist/defs/trae-cli.d.ts +12 -2
- package/dist/defs/trae-cli.js +3 -3
- package/dist/defs/vibe.d.ts +4 -1
- package/dist/defs/vibe.js +2 -2
- package/dist/detection.d.ts +19 -4
- package/dist/detection.js +33 -33
- package/dist/diagnostics.d.ts +14 -3
- package/dist/diagnostics.js +5 -5
- package/dist/env.d.ts +16 -4
- package/dist/env.js +7 -7
- package/dist/executables.d.ts +24 -8
- package/dist/executables.js +13 -13
- package/dist/index.d.ts +228 -4
- package/dist/index.js +230 -6
- package/dist/invocation.d.ts +6 -1
- package/dist/invocation.js +2 -2
- package/dist/json-event-stream.d.ts +7 -2
- package/dist/json-event-stream.js +3 -3
- package/dist/launch.d.ts +12 -2
- package/dist/launch.js +3 -3
- package/dist/log-file.d.ts +4 -1
- package/dist/log-file.js +2 -2
- package/dist/mcp.d.ts +6 -3
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +7 -3
- package/dist/metadata.js +1 -1
- package/dist/mmd-routes.d.ts +22 -6
- package/dist/mmd-routes.js +14 -14
- package/dist/model-catalog-cache.d.ts +50 -0
- package/dist/model-catalog-cache.js +61 -0
- package/dist/model-loading-state.d.ts +10 -0
- package/dist/model-loading-state.js +23 -0
- package/dist/model-registry.d.ts +28 -7
- package/dist/model-registry.js +11 -8
- package/dist/models.d.ts +31 -6
- package/dist/models.js +6 -6
- package/dist/opencode-log.d.ts +12 -4
- package/dist/opencode-log.js +8 -8
- package/dist/paths.d.ts +6 -2
- package/dist/paths.js +3 -3
- package/dist/pi-models.d.ts +3 -1
- package/dist/pi-models.js +2 -2
- package/dist/prompt-budget.d.ts +17 -4
- package/dist/prompt-budget.js +3 -3
- package/dist/prompt-file.d.ts +5 -1
- package/dist/prompt-file.js +1 -1
- package/dist/providers/aihubmix.d.ts +34 -9
- package/dist/providers/aihubmix.js +26 -26
- package/dist/providers/anthropic-messages.d.ts +77 -20
- package/dist/providers/anthropic-messages.js +53 -50
- package/dist/providers/azure-chat.d.ts +21 -7
- package/dist/providers/azure-chat.js +18 -16
- package/dist/providers/connection-guard.d.ts +45 -31
- package/dist/providers/connection-guard.js +40 -143
- package/dist/providers/connection-test.d.ts +1 -1
- package/dist/providers/connection-test.js +13 -30
- package/dist/providers/elevenlabs.d.ts +7 -2
- package/dist/providers/elevenlabs.js +4 -4
- package/dist/providers/google-messages.d.ts +44 -10
- package/dist/providers/google-messages.js +36 -34
- package/dist/providers/google-schema.d.ts +26 -0
- package/dist/providers/google-schema.js +253 -0
- package/dist/providers/google.d.ts +21 -6
- package/dist/providers/google.js +10 -10
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.js +3 -2
- package/dist/providers/model-catalog.d.ts +1 -1
- package/dist/providers/model-catalog.js +14 -27
- package/dist/providers/oauth-callback-server.d.ts +1 -1
- package/dist/providers/oauth-callback-server.js +3 -2
- package/dist/providers/oauth-credentials.d.ts +7 -1
- package/dist/providers/oauth-credentials.js +5 -5
- package/dist/providers/oauth-provider.d.ts +2 -2
- package/dist/providers/oauth-provider.js +20 -25
- package/dist/providers/oauth-tokens.d.ts +26 -6
- package/dist/providers/oauth-tokens.js +9 -9
- package/dist/providers/ollama-chat.d.ts +66 -31
- package/dist/providers/ollama-chat.js +64 -61
- package/dist/providers/openai-chat.d.ts +77 -22
- package/dist/providers/openai-chat.js +71 -67
- package/dist/providers/pkce.d.ts +33 -20
- package/dist/providers/pkce.js +75 -146
- package/dist/providers/sse-decode.d.ts +3 -1
- package/dist/providers/sse-decode.js +1 -1
- package/dist/providers/token-params.d.ts +16 -5
- package/dist/providers/token-params.js +6 -6
- package/dist/providers/tool-turn-types.d.ts +94 -0
- package/dist/providers/tool-turn-types.js +2 -0
- package/dist/providers/tool-turn.d.ts +10 -0
- package/dist/providers/tool-turn.js +258 -0
- package/dist/qoder-stream.d.ts +7 -3
- package/dist/qoder-stream.js +3 -3
- package/dist/registry.d.ts +6 -2
- package/dist/registry.js +4 -4
- package/dist/resolution.d.ts +5 -1
- package/dist/resolution.js +3 -3
- package/dist/role-marker-guard.d.ts +9 -5
- package/dist/role-marker-guard.js +5 -5
- package/dist/terminal-launch.d.ts +6 -1
- package/dist/terminal-launch.js +2 -2
- package/dist/types.d.ts +22 -6
- 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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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(
|
|
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 },
|