@codehz/ai 0.3.0 → 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/dist/index.d.mts +68 -53
- package/dist/index.mjs +673 -815
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +151 -245
- package/src/adapters/messages.ts +171 -274
- package/src/adapters/ollama.ts +119 -206
- package/src/adapters/responses.ts +135 -226
- package/src/helpers/adapter-auxiliary.ts +1 -23
- package/src/helpers/adapter-base.ts +38 -4
- package/src/helpers/incremental-stream-parser.ts +58 -0
- package/src/helpers/index.ts +20 -7
- package/src/helpers/mapping.ts +0 -8
- package/src/helpers/provider-stream.ts +147 -0
- package/src/helpers/request-mapper.ts +30 -1
- package/src/helpers/usage-mapping.ts +36 -39
- 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,12 +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 {
|
|
31
|
+
import {
|
|
32
|
+
NormalizedRequestMapper,
|
|
33
|
+
createNdjsonLineParser,
|
|
34
|
+
openProviderJsonStream,
|
|
35
|
+
iterateProviderStreamBatches,
|
|
36
|
+
createCompletionGate,
|
|
37
|
+
} from "../helpers/index.js";
|
|
33
38
|
|
|
34
|
-
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
39
|
+
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
|
|
35
40
|
|
|
36
41
|
// ── 选项类型 ──────────────────────────────────────────────────
|
|
37
42
|
|
|
@@ -172,7 +177,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
172
177
|
const role = item.role;
|
|
173
178
|
messages.push({
|
|
174
179
|
role,
|
|
175
|
-
content:
|
|
180
|
+
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
176
181
|
});
|
|
177
182
|
break;
|
|
178
183
|
}
|
|
@@ -203,7 +208,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
203
208
|
}
|
|
204
209
|
messages.push({
|
|
205
210
|
role: "tool",
|
|
206
|
-
content:
|
|
211
|
+
content: mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`),
|
|
207
212
|
});
|
|
208
213
|
break;
|
|
209
214
|
}
|
|
@@ -297,7 +302,8 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
297
302
|
request: NormalizedRequest,
|
|
298
303
|
): AsyncIterable<AIStreamEvent> {
|
|
299
304
|
const auxiliary = this.createAuxiliaryState(request);
|
|
300
|
-
|
|
305
|
+
const gate = createCompletionGate();
|
|
306
|
+
|
|
301
307
|
if (request.toolChoice && request.toolChoice !== "auto") {
|
|
302
308
|
yield factory.responseWarning(
|
|
303
309
|
request.toolChoice === "none"
|
|
@@ -317,70 +323,37 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
317
323
|
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
318
324
|
}
|
|
319
325
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
body: JSON.stringify(providerRequest),
|
|
327
|
-
signal: request.signal,
|
|
328
|
-
});
|
|
329
|
-
} catch (err) {
|
|
330
|
-
throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (!response.ok) {
|
|
334
|
-
const errorBody = await response.text().catch(() => "");
|
|
335
|
-
throw providerHttpError(response.status, errorBody);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const reader = response.body?.getReader();
|
|
339
|
-
if (!reader) {
|
|
340
|
-
throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const parser = new IncrementalStreamParser<OllamaChatChunk>(splitLines, (item: string) => {
|
|
344
|
-
const trimmed = item.trim();
|
|
345
|
-
if (!trimmed) return { status: "ignored" };
|
|
346
|
-
try {
|
|
347
|
-
const parsed = JSON.parse(trimmed);
|
|
348
|
-
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
349
|
-
return { status: "parsed", value: parsed as OllamaChatChunk };
|
|
350
|
-
}
|
|
351
|
-
return { status: "malformed" };
|
|
352
|
-
} catch {
|
|
353
|
-
return { status: "malformed" };
|
|
354
|
-
}
|
|
326
|
+
const { reader } = await openProviderJsonStream({
|
|
327
|
+
fetchFn: this.fetchFn,
|
|
328
|
+
url: `${this.baseUrl}/api/chat`,
|
|
329
|
+
headers,
|
|
330
|
+
body: providerRequest,
|
|
331
|
+
signal: request.signal,
|
|
355
332
|
});
|
|
356
333
|
|
|
357
|
-
const
|
|
358
|
-
|
|
334
|
+
const parser = createNdjsonLineParser<OllamaChatChunk>(
|
|
335
|
+
(value): value is OllamaChatChunk => !!value && typeof value === "object" && "message" in value,
|
|
336
|
+
);
|
|
359
337
|
|
|
360
|
-
|
|
338
|
+
const output: OutputItem[] = [];
|
|
361
339
|
let responseId: string | undefined;
|
|
362
340
|
let accumulatedContent = "";
|
|
363
341
|
let currentMessageId = "";
|
|
364
342
|
let hasMessageStarted = false;
|
|
365
|
-
|
|
366
|
-
// tool_calls 累积(于 final chunk 到达)
|
|
367
343
|
let pendingToolCalls: Array<{ id: string; name: string; argumentsText: string }> = [];
|
|
368
344
|
let toolCallIndex = 0;
|
|
369
|
-
const buildResponse = this.buildResponse.bind(this);
|
|
370
345
|
|
|
371
346
|
const emitCompleted = async function* (
|
|
372
|
-
|
|
347
|
+
this: OllamaAdapter,
|
|
348
|
+
stopReason: StopReason | undefined,
|
|
373
349
|
rawResponseId: string | undefined,
|
|
374
350
|
): AsyncIterable<AIStreamEvent> {
|
|
375
|
-
if (
|
|
351
|
+
if (!gate.tryComplete()) {
|
|
376
352
|
yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
|
|
377
353
|
return;
|
|
378
354
|
}
|
|
379
355
|
|
|
380
|
-
completedEmitted = true;
|
|
381
|
-
|
|
382
356
|
const replay = replayFromOutput(output);
|
|
383
|
-
|
|
384
357
|
if (accumulatedContent || pendingToolCalls.length > 0) {
|
|
385
358
|
replay.push(
|
|
386
359
|
opaqueItem("ollama", "replay", {
|
|
@@ -394,178 +367,118 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
394
367
|
);
|
|
395
368
|
}
|
|
396
369
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const finalResponse = buildResponse(
|
|
403
|
-
request,
|
|
404
|
-
{
|
|
405
|
-
output,
|
|
406
|
-
replay,
|
|
407
|
-
stopReason,
|
|
408
|
-
usage: auxiliaryResult.usage,
|
|
409
|
-
billing: auxiliaryResult.billing,
|
|
410
|
-
auxiliary: auxiliaryResult.auxiliary,
|
|
411
|
-
warnings: auxiliaryResult.warnings,
|
|
412
|
-
metadataSources: auxiliaryResult.metadataSources,
|
|
413
|
-
rawResponseId,
|
|
414
|
-
},
|
|
415
|
-
factory,
|
|
416
|
-
);
|
|
417
|
-
yield factory.responseCompleted({
|
|
418
|
-
replay: finalResponse.replay,
|
|
419
|
-
stopReason: finalResponse.stopReason,
|
|
420
|
-
trace: finalResponse.backend,
|
|
421
|
-
usage: finalResponse.usage,
|
|
422
|
-
billing: finalResponse.billing,
|
|
423
|
-
auxiliary: finalResponse.auxiliary,
|
|
424
|
-
warnings: finalResponse.warnings,
|
|
370
|
+
yield* this.emitStreamCompleted(factory, request, auxiliary, {
|
|
371
|
+
output,
|
|
372
|
+
replay,
|
|
373
|
+
stopReason,
|
|
374
|
+
rawResponseId,
|
|
425
375
|
});
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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;
|
|
446
396
|
}
|
|
447
397
|
|
|
448
|
-
|
|
449
|
-
responseId = chunk.created_at;
|
|
398
|
+
const msg = chunk.message;
|
|
450
399
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
400
|
+
if (msg.content) {
|
|
401
|
+
if (!hasMessageStarted) {
|
|
402
|
+
currentMessageId = `msg-${chunk.created_at}`;
|
|
403
|
+
hasMessageStarted = true;
|
|
404
|
+
yield factory.messageStarted(currentMessageId);
|
|
456
405
|
}
|
|
406
|
+
accumulatedContent += msg.content;
|
|
407
|
+
yield factory.messageDelta(currentMessageId, textBlock(msg.content));
|
|
408
|
+
}
|
|
457
409
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
accumulatedContent += msg.content;
|
|
468
|
-
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
|
+
});
|
|
469
419
|
}
|
|
420
|
+
}
|
|
470
421
|
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
pendingToolCalls.push({
|
|
477
|
-
id: tcId,
|
|
478
|
-
name: tc.function.name,
|
|
479
|
-
argumentsText: argsText,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
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);
|
|
482
427
|
}
|
|
483
428
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (accumulatedContent
|
|
488
|
-
|
|
489
|
-
hasMessageStarted = true;
|
|
490
|
-
yield factory.messageStarted(currentMessageId);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// 完成消息(如果有累积的内容或正在进行的消息)
|
|
494
|
-
if (hasMessageStarted) {
|
|
495
|
-
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
496
|
-
yield factory.messageCompleted(currentMessageId);
|
|
497
|
-
if (accumulatedContent) {
|
|
498
|
-
output.push(message);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (pendingToolCalls.length > 0) {
|
|
503
|
-
yield factory.responseWarning(
|
|
504
|
-
`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`,
|
|
505
|
-
WarningCode.TOOL_CALL_BATCHED,
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// 发出 tool_call 完成事件
|
|
510
|
-
for (const pending of pendingToolCalls) {
|
|
511
|
-
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
|
|
512
|
-
yield factory.toolCallStarted(pending.id, pending.name);
|
|
513
|
-
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
514
|
-
yield factory.toolCallCompleted(pending.id);
|
|
515
|
-
output.push(toolCall);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// 提取 usage
|
|
519
|
-
if (
|
|
520
|
-
request.include?.usage !== "off" &&
|
|
521
|
-
(chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined)
|
|
522
|
-
) {
|
|
523
|
-
auxiliary.recordUsage(
|
|
524
|
-
usageFromOllama({
|
|
525
|
-
prompt_eval_count: chunk.prompt_eval_count,
|
|
526
|
-
eval_count: chunk.eval_count,
|
|
527
|
-
}),
|
|
528
|
-
"final",
|
|
529
|
-
{
|
|
530
|
-
prompt_eval_count: chunk.prompt_eval_count,
|
|
531
|
-
eval_count: chunk.eval_count,
|
|
532
|
-
},
|
|
533
|
-
);
|
|
429
|
+
if (hasMessageStarted) {
|
|
430
|
+
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|
|
431
|
+
yield factory.messageCompleted(currentMessageId);
|
|
432
|
+
if (accumulatedContent) {
|
|
433
|
+
output.push(message);
|
|
534
434
|
}
|
|
435
|
+
}
|
|
535
436
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
}
|
|
538
443
|
|
|
539
|
-
|
|
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
|
+
}
|
|
540
451
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
+
);
|
|
546
467
|
}
|
|
547
|
-
}
|
|
548
468
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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 = [];
|
|
552
476
|
}
|
|
553
477
|
}
|
|
554
|
-
} finally {
|
|
555
|
-
try {
|
|
556
|
-
if (!streamDone) await reader.cancel().catch(() => undefined);
|
|
557
|
-
} finally {
|
|
558
|
-
reader.releaseLock();
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (parser.getRemaining().trim().length > 0) {
|
|
563
|
-
yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
|
|
564
478
|
}
|
|
565
479
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
|
|
480
|
+
if (!gate.completed && (hasMessageStarted || pendingToolCalls.length > 0)) {
|
|
481
|
+
yield factory.responseWarning("Stream ended without a done signal", WarningCode.STREAM_INCOMPLETE);
|
|
569
482
|
|
|
570
483
|
if (hasMessageStarted) {
|
|
571
484
|
const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
|