@codehz/ai 0.2.4 → 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.
- package/README.md +3 -8
- package/dist/index.d.mts +79 -89
- package/dist/index.mjs +717 -931
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +153 -266
- package/src/adapters/messages.ts +179 -301
- package/src/adapters/mock.ts +1 -10
- package/src/adapters/ollama.ts +142 -257
- package/src/adapters/responses.ts +141 -251
- package/src/core/validation.ts +19 -0
- package/src/helpers/adapter-auxiliary.ts +1 -23
- package/src/helpers/adapter-base.ts +41 -7
- package/src/helpers/incremental-stream-parser.ts +58 -0
- package/src/helpers/index.ts +20 -8
- package/src/helpers/mapping.ts +1 -10
- package/src/helpers/provider-stream.ts +147 -0
- package/src/helpers/request-mapper.ts +47 -25
- package/src/helpers/usage-mapping.ts +36 -39
- package/src/types/adapter.ts +1 -12
- package/src/types/index.ts +1 -9
- package/src/types/items.ts +0 -1
- package/src/helpers/sse-parser.ts +0 -113
package/src/adapters/ollama.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { AdapterBase } from "../helpers/adapter-base.js";
|
|
19
|
-
import {
|
|
19
|
+
import { AIRequestError, WarningCode } from "../core/errors.js";
|
|
20
20
|
import {
|
|
21
21
|
textBlock,
|
|
22
22
|
messageItem,
|
|
@@ -26,13 +26,17 @@ import {
|
|
|
26
26
|
mapStopReason,
|
|
27
27
|
contentBlocksToText,
|
|
28
28
|
} from "../helpers/mapping.js";
|
|
29
|
-
import {
|
|
30
|
-
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
29
|
+
import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
|
|
31
30
|
import { usageFromOllama } from "../helpers/usage-mapping.js";
|
|
32
|
-
import {
|
|
33
|
-
|
|
31
|
+
import {
|
|
32
|
+
NormalizedRequestMapper,
|
|
33
|
+
createNdjsonLineParser,
|
|
34
|
+
openProviderJsonStream,
|
|
35
|
+
iterateProviderStreamBatches,
|
|
36
|
+
createCompletionGate,
|
|
37
|
+
} from "../helpers/index.js";
|
|
34
38
|
|
|
35
|
-
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
39
|
+
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
|
|
36
40
|
|
|
37
41
|
// ── 选项类型 ──────────────────────────────────────────────────
|
|
38
42
|
|
|
@@ -82,44 +86,7 @@ type OllamaTool = {
|
|
|
82
86
|
};
|
|
83
87
|
};
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const profile: ProviderProfile = {
|
|
88
|
-
kind: "ollama",
|
|
89
|
-
instructionsMode: "system_message",
|
|
90
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
91
|
-
reasoningBlockTypes: ["text"] as const,
|
|
92
|
-
capabilities: {
|
|
93
|
-
textStreaming: "native",
|
|
94
|
-
reasoningStreaming: "none",
|
|
95
|
-
toolCallStreaming: "synthetic",
|
|
96
|
-
replay: "opaque",
|
|
97
|
-
usage: "final",
|
|
98
|
-
toolResultOutcomes: ["success"],
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
103
|
-
|
|
104
|
-
function parseOllamaToolArguments(item: import("../index.js").ToolCallItem): Record<string, unknown> {
|
|
105
|
-
if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) {
|
|
106
|
-
return item.argumentsJson as Record<string, unknown>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const parsed = JSON.parse(item.argumentsText);
|
|
111
|
-
if (parsed && typeof parsed === "object") {
|
|
112
|
-
return parsed as Record<string, unknown>;
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// fall through
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new AIRequestError(
|
|
119
|
-
"ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent",
|
|
120
|
-
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
121
|
-
);
|
|
122
|
-
}
|
|
89
|
+
const mapper = new NormalizedRequestMapper("ollama");
|
|
123
90
|
|
|
124
91
|
// ── Ollama 流式 chunk ─────────────────────────────────────────
|
|
125
92
|
|
|
@@ -179,7 +146,7 @@ function toWireOllamaToolCalls(toolCalls: OllamaReplayToolCall[]): OllamaToolCal
|
|
|
179
146
|
|
|
180
147
|
export class OllamaAdapter extends AdapterBase {
|
|
181
148
|
readonly kind = "ollama" as const;
|
|
182
|
-
readonly
|
|
149
|
+
readonly isSyntheticStream = false;
|
|
183
150
|
|
|
184
151
|
private baseUrl: string;
|
|
185
152
|
private apiKey: string | undefined;
|
|
@@ -195,10 +162,6 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
195
162
|
// ── buildRequest ──────────────────────────────────────────
|
|
196
163
|
|
|
197
164
|
protected buildRequest(request: NormalizedRequest): OllamaChatRequest {
|
|
198
|
-
if (request.toolChoice && request.toolChoice !== "auto") {
|
|
199
|
-
throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
165
|
const messages: OllamaMessage[] = [];
|
|
203
166
|
/** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
|
|
204
167
|
const callIdsByName = new Map<string, string[]>();
|
|
@@ -214,7 +177,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
214
177
|
const role = item.role;
|
|
215
178
|
messages.push({
|
|
216
179
|
role,
|
|
217
|
-
content:
|
|
180
|
+
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
218
181
|
});
|
|
219
182
|
break;
|
|
220
183
|
}
|
|
@@ -224,7 +187,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
224
187
|
const tc: OllamaToolCall = {
|
|
225
188
|
function: {
|
|
226
189
|
name: item.name,
|
|
227
|
-
arguments:
|
|
190
|
+
arguments: mapper.parseToolArguments(item),
|
|
228
191
|
},
|
|
229
192
|
};
|
|
230
193
|
const queue = callIdsByName.get(item.name) ?? [];
|
|
@@ -238,7 +201,6 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
238
201
|
break;
|
|
239
202
|
}
|
|
240
203
|
case "tool_result": {
|
|
241
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
242
204
|
// Best-effort: consume matching id from name queue when present (no wire call_id)
|
|
243
205
|
const queue = callIdsByName.get(item.toolName);
|
|
244
206
|
if (queue && queue.length > 0) {
|
|
@@ -246,7 +208,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
246
208
|
}
|
|
247
209
|
messages.push({
|
|
248
210
|
role: "tool",
|
|
249
|
-
content:
|
|
211
|
+
content: mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`),
|
|
250
212
|
});
|
|
251
213
|
break;
|
|
252
214
|
}
|
|
@@ -302,8 +264,16 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
302
264
|
stream: true,
|
|
303
265
|
};
|
|
304
266
|
|
|
305
|
-
|
|
306
|
-
|
|
267
|
+
const toolChoice = request.toolChoice;
|
|
268
|
+
const selectedTools =
|
|
269
|
+
toolChoice === "none"
|
|
270
|
+
? []
|
|
271
|
+
: toolChoice && typeof toolChoice === "object"
|
|
272
|
+
? request.tools?.filter((tool) => tool.name === toolChoice.name)
|
|
273
|
+
: request.tools;
|
|
274
|
+
|
|
275
|
+
if (selectedTools && selectedTools.length > 0) {
|
|
276
|
+
body.tools = selectedTools.map(
|
|
307
277
|
(t): OllamaTool => ({
|
|
308
278
|
type: "function",
|
|
309
279
|
function: {
|
|
@@ -332,7 +302,16 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
332
302
|
request: NormalizedRequest,
|
|
333
303
|
): AsyncIterable<AIStreamEvent> {
|
|
334
304
|
const auxiliary = this.createAuxiliaryState(request);
|
|
335
|
-
|
|
305
|
+
const gate = createCompletionGate();
|
|
306
|
+
|
|
307
|
+
if (request.toolChoice && request.toolChoice !== "auto") {
|
|
308
|
+
yield factory.responseWarning(
|
|
309
|
+
request.toolChoice === "none"
|
|
310
|
+
? "Ollama toolChoice none was mapped by omitting tools"
|
|
311
|
+
: `Ollama cannot force tool choice; only tool "${request.toolChoice.name}" was provided as a best-effort constraint`,
|
|
312
|
+
WarningCode.CAPABILITY_DOWNGRADE,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
336
315
|
if (request.metadata) {
|
|
337
316
|
yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
|
|
338
317
|
}
|
|
@@ -344,70 +323,37 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
344
323
|
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
345
324
|
}
|
|
346
325
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
body: JSON.stringify(providerRequest),
|
|
354
|
-
signal: request.signal,
|
|
355
|
-
});
|
|
356
|
-
} catch (err) {
|
|
357
|
-
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!response.ok) {
|
|
361
|
-
const errorBody = await response.text().catch(() => "");
|
|
362
|
-
throw providerHttpError(response.status, errorBody);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const reader = response.body?.getReader();
|
|
366
|
-
if (!reader) {
|
|
367
|
-
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const parser = new IncrementalStreamParser<OllamaChatChunk>(splitLines, (item: string) => {
|
|
371
|
-
const trimmed = item.trim();
|
|
372
|
-
if (!trimmed) return { status: "ignored" };
|
|
373
|
-
try {
|
|
374
|
-
const parsed = JSON.parse(trimmed);
|
|
375
|
-
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
376
|
-
return { status: "parsed", value: parsed as OllamaChatChunk };
|
|
377
|
-
}
|
|
378
|
-
return { status: "malformed" };
|
|
379
|
-
} catch {
|
|
380
|
-
return { status: "malformed" };
|
|
381
|
-
}
|
|
326
|
+
const { reader } = await openProviderJsonStream({
|
|
327
|
+
fetchFn: this.fetchFn,
|
|
328
|
+
url: `${this.baseUrl}/api/chat`,
|
|
329
|
+
headers,
|
|
330
|
+
body: providerRequest,
|
|
331
|
+
signal: request.signal,
|
|
382
332
|
});
|
|
383
333
|
|
|
384
|
-
const
|
|
385
|
-
|
|
334
|
+
const parser = createNdjsonLineParser<OllamaChatChunk>(
|
|
335
|
+
(value): value is OllamaChatChunk => !!value && typeof value === "object" && "message" in value,
|
|
336
|
+
);
|
|
386
337
|
|
|
387
|
-
|
|
338
|
+
const output: OutputItem[] = [];
|
|
388
339
|
let responseId: string | undefined;
|
|
389
340
|
let accumulatedContent = "";
|
|
390
341
|
let currentMessageId = "";
|
|
391
342
|
let hasMessageStarted = false;
|
|
392
|
-
|
|
393
|
-
// tool_calls 累积(于 final chunk 到达)
|
|
394
|
-
let pendingToolCalls: Array<{ id: string; name: string; argumentsText: string; argumentsJson?: unknown }> = [];
|
|
343
|
+
let pendingToolCalls: Array<{ id: string; name: string; argumentsText: string }> = [];
|
|
395
344
|
let toolCallIndex = 0;
|
|
396
|
-
const buildResponse = this.buildResponse.bind(this);
|
|
397
345
|
|
|
398
346
|
const emitCompleted = async function* (
|
|
399
|
-
|
|
347
|
+
this: OllamaAdapter,
|
|
348
|
+
stopReason: StopReason | undefined,
|
|
400
349
|
rawResponseId: string | undefined,
|
|
401
350
|
): AsyncIterable<AIStreamEvent> {
|
|
402
|
-
if (
|
|
351
|
+
if (!gate.tryComplete()) {
|
|
403
352
|
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
404
353
|
return;
|
|
405
354
|
}
|
|
406
355
|
|
|
407
|
-
completedEmitted = true;
|
|
408
|
-
|
|
409
356
|
const replay = replayFromOutput(output);
|
|
410
|
-
|
|
411
357
|
if (accumulatedContent || pendingToolCalls.length > 0) {
|
|
412
358
|
replay.push(
|
|
413
359
|
opaqueItem("ollama", "replay", {
|
|
@@ -415,184 +361,123 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
415
361
|
content: accumulatedContent,
|
|
416
362
|
tool_calls: pendingToolCalls.map((tc) => ({
|
|
417
363
|
id: tc.id,
|
|
418
|
-
function: { name: tc.name, arguments: tc.
|
|
364
|
+
function: { name: tc.name, arguments: JSON.parse(tc.argumentsText) as Record<string, unknown> },
|
|
419
365
|
})),
|
|
420
366
|
}),
|
|
421
367
|
);
|
|
422
368
|
}
|
|
423
369
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const finalResponse = buildResponse(
|
|
430
|
-
request,
|
|
431
|
-
{
|
|
432
|
-
output,
|
|
433
|
-
replay,
|
|
434
|
-
stopReason,
|
|
435
|
-
usage: auxiliaryResult.usage,
|
|
436
|
-
billing: auxiliaryResult.billing,
|
|
437
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
438
|
-
warnings: auxiliaryResult.warnings,
|
|
439
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
440
|
-
rawResponseId,
|
|
441
|
-
},
|
|
442
|
-
factory,
|
|
443
|
-
);
|
|
444
|
-
yield factory.responseCompleted({
|
|
445
|
-
replay: finalResponse.replay,
|
|
446
|
-
stopReason: finalResponse.stopReason,
|
|
447
|
-
trace: finalResponse.backend,
|
|
448
|
-
usage: finalResponse.usage,
|
|
449
|
-
billing: finalResponse.billing,
|
|
450
|
-
auxiliary: finalResponse.auxiliary,
|
|
451
|
-
warnings: finalResponse.warnings,
|
|
370
|
+
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
371
|
+
output,
|
|
372
|
+
replay,
|
|
373
|
+
stopReason,
|
|
374
|
+
rawResponseId,
|
|
452
375
|
});
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
376
|
+
}.bind(this);
|
|
377
|
+
|
|
378
|
+
for await (const batch of iterateProviderStreamBatches({
|
|
379
|
+
reader,
|
|
380
|
+
parser,
|
|
381
|
+
factory,
|
|
382
|
+
providerLabel: "Ollama",
|
|
383
|
+
transportLabel: "NDJSON line(s)",
|
|
384
|
+
incompleteMessage: "Stream ended with an incomplete Ollama NDJSON line",
|
|
385
|
+
})) {
|
|
386
|
+
for (const warning of batch.warnings) yield warning;
|
|
387
|
+
|
|
388
|
+
for (const chunk of batch.items) {
|
|
389
|
+
responseId = chunk.created_at;
|
|
390
|
+
|
|
391
|
+
if (gate.completed) {
|
|
392
|
+
if (chunk.done) {
|
|
393
|
+
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
394
|
+
}
|
|
395
|
+
continue;
|
|
473
396
|
}
|
|
474
397
|
|
|
475
|
-
|
|
476
|
-
responseId = chunk.created_at;
|
|
398
|
+
const msg = chunk.message;
|
|
477
399
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
400
|
+
if (msg.content) {
|
|
401
|
+
if (!hasMessageStarted) {
|
|
402
|
+
currentMessageId = `msg-${chunk.created_at}`;
|
|
403
|
+
hasMessageStarted = true;
|
|
404
|
+
yield factory.messageStarted(currentMessageId);
|
|
483
405
|
}
|
|
406
|
+
accumulatedContent += msg.content;
|
|
407
|
+
yield factory.messageDelta(currentMessageId, textBlock(msg.content));
|
|
408
|
+
}
|
|
484
409
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
accumulatedContent += msg.content;
|
|
495
|
-
yield factory.messageDelta(currentMessageId, textBlock(msg.content));
|
|
410
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
411
|
+
for (const tc of msg.tool_calls) {
|
|
412
|
+
const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
|
|
413
|
+
const argsText = JSON.stringify(tc.function.arguments);
|
|
414
|
+
pendingToolCalls.push({
|
|
415
|
+
id: tcId,
|
|
416
|
+
name: tc.function.name,
|
|
417
|
+
argumentsText: argsText,
|
|
418
|
+
});
|
|
496
419
|
}
|
|
420
|
+
}
|
|
497
421
|
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
pendingToolCalls.push({
|
|
504
|
-
id: tcId,
|
|
505
|
-
name: tc.function.name,
|
|
506
|
-
argumentsText: argsText,
|
|
507
|
-
argumentsJson: tc.function.arguments,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
422
|
+
if (chunk.done) {
|
|
423
|
+
if (accumulatedContent === "" && pendingToolCalls.length > 0 && !hasMessageStarted) {
|
|
424
|
+
currentMessageId = `msg-${chunk.created_at}`;
|
|
425
|
+
hasMessageStarted = true;
|
|
426
|
+
yield factory.messageStarted(currentMessageId);
|
|
510
427
|
}
|
|
511
428
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (accumulatedContent
|
|
516
|
-
|
|
517
|
-
hasMessageStarted = true;
|
|
518
|
-
yield factory.messageStarted(currentMessageId);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// 完成消息(如果有累积的内容或正在进行的消息)
|
|
522
|
-
if (hasMessageStarted) {
|
|
523
|
-
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
524
|
-
yield factory.messageCompleted(currentMessageId);
|
|
525
|
-
if (accumulatedContent) {
|
|
526
|
-
output.push(message);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (pendingToolCalls.length > 0) {
|
|
531
|
-
yield factory.responseWarning(
|
|
532
|
-
`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`,
|
|
533
|
-
WarningCode.TOOL_CALL_BATCHED,
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// 发出 tool_call 完成事件
|
|
538
|
-
for (const pending of pendingToolCalls) {
|
|
539
|
-
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
|
|
540
|
-
yield factory.toolCallStarted(pending.id, pending.name);
|
|
541
|
-
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
542
|
-
yield factory.toolCallCompleted(pending.id);
|
|
543
|
-
output.push(toolCall);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// 提取 usage
|
|
547
|
-
if (
|
|
548
|
-
request.include?.usage !== "off" &&
|
|
549
|
-
(chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined)
|
|
550
|
-
) {
|
|
551
|
-
auxiliary.recordUsage(
|
|
552
|
-
usageFromOllama({
|
|
553
|
-
prompt_eval_count: chunk.prompt_eval_count,
|
|
554
|
-
eval_count: chunk.eval_count,
|
|
555
|
-
}),
|
|
556
|
-
"final",
|
|
557
|
-
{
|
|
558
|
-
prompt_eval_count: chunk.prompt_eval_count,
|
|
559
|
-
eval_count: chunk.eval_count,
|
|
560
|
-
},
|
|
561
|
-
);
|
|
429
|
+
if (hasMessageStarted) {
|
|
430
|
+
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
431
|
+
yield factory.messageCompleted(currentMessageId);
|
|
432
|
+
if (accumulatedContent) {
|
|
433
|
+
output.push(message);
|
|
562
434
|
}
|
|
435
|
+
}
|
|
563
436
|
|
|
564
|
-
|
|
565
|
-
|
|
437
|
+
if (pendingToolCalls.length > 0) {
|
|
438
|
+
yield factory.responseWarning(
|
|
439
|
+
`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`,
|
|
440
|
+
WarningCode.TOOL_CALL_BATCHED,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
566
443
|
|
|
567
|
-
|
|
444
|
+
for (const pending of pendingToolCalls) {
|
|
445
|
+
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
|
|
446
|
+
yield factory.toolCallStarted(pending.id, pending.name);
|
|
447
|
+
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
448
|
+
yield factory.toolCallCompleted(pending.id);
|
|
449
|
+
output.push(toolCall);
|
|
450
|
+
}
|
|
568
451
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
452
|
+
if (
|
|
453
|
+
request.include?.usage !== "off" &&
|
|
454
|
+
(chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined)
|
|
455
|
+
) {
|
|
456
|
+
auxiliary.recordUsage(
|
|
457
|
+
usageFromOllama({
|
|
458
|
+
prompt_eval_count: chunk.prompt_eval_count,
|
|
459
|
+
eval_count: chunk.eval_count,
|
|
460
|
+
}),
|
|
461
|
+
"final",
|
|
462
|
+
{
|
|
463
|
+
prompt_eval_count: chunk.prompt_eval_count,
|
|
464
|
+
eval_count: chunk.eval_count,
|
|
465
|
+
},
|
|
466
|
+
);
|
|
574
467
|
}
|
|
575
|
-
}
|
|
576
468
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
469
|
+
const stopReason = chunk.done_reason ? mapStopReason(chunk.done_reason) : undefined;
|
|
470
|
+
yield* emitCompleted(stopReason, chunk.created_at);
|
|
471
|
+
|
|
472
|
+
accumulatedContent = "";
|
|
473
|
+
currentMessageId = "";
|
|
474
|
+
hasMessageStarted = false;
|
|
475
|
+
pendingToolCalls = [];
|
|
580
476
|
}
|
|
581
477
|
}
|
|
582
|
-
} finally {
|
|
583
|
-
try {
|
|
584
|
-
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
585
|
-
} finally {
|
|
586
|
-
reader.releaseLock();
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (parser.getRemaining().trim().length > 0) {
|
|
591
|
-
yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
|
|
592
478
|
}
|
|
593
479
|
|
|
594
|
-
|
|
595
|
-
if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
|
|
480
|
+
if (!gate.completed && (hasMessageStarted || pendingToolCalls.length > 0)) {
|
|
596
481
|
yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
|
|
597
482
|
|
|
598
483
|
if (hasMessageStarted) {
|
|
@@ -611,7 +496,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
611
496
|
}
|
|
612
497
|
|
|
613
498
|
for (const pending of pendingToolCalls) {
|
|
614
|
-
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText
|
|
499
|
+
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
|
|
615
500
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
616
501
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
617
502
|
yield factory.toolCallCompleted(pending.id);
|