@codehz/ai 0.4.6 → 0.7.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.
Files changed (46) hide show
  1. package/README.md +221 -75
  2. package/dist/index.d.mts +670 -523
  3. package/dist/index.mjs +3677 -2207
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +19 -8
  6. package/.github/workflows/publish.yml +0 -56
  7. package/.oxfmtrc.json +0 -12
  8. package/.oxlintrc.json +0 -34
  9. package/AGENTS.md +0 -37
  10. package/src/adapters/chat-completions.ts +0 -624
  11. package/src/adapters/index.ts +0 -44
  12. package/src/adapters/messages.ts +0 -635
  13. package/src/adapters/mock.ts +0 -934
  14. package/src/adapters/ollama.ts +0 -526
  15. package/src/adapters/responses.ts +0 -818
  16. package/src/core/aggregator.ts +0 -428
  17. package/src/core/client.ts +0 -36
  18. package/src/core/collect-stream.ts +0 -19
  19. package/src/core/errors.ts +0 -105
  20. package/src/core/event-factory.ts +0 -151
  21. package/src/core/index.ts +0 -18
  22. package/src/core/merge-auxiliary.ts +0 -22
  23. package/src/core/normalize.ts +0 -65
  24. package/src/core/validation.ts +0 -404
  25. package/src/helpers/adapter-auxiliary.ts +0 -155
  26. package/src/helpers/adapter-base.ts +0 -218
  27. package/src/helpers/adapter-security.ts +0 -126
  28. package/src/helpers/auxiliary-collector.ts +0 -166
  29. package/src/helpers/incremental-stream-parser.ts +0 -142
  30. package/src/helpers/index.ts +0 -87
  31. package/src/helpers/mapping.ts +0 -192
  32. package/src/helpers/provider-request-options.ts +0 -25
  33. package/src/helpers/provider-stream.ts +0 -147
  34. package/src/helpers/reasoning-level.ts +0 -86
  35. package/src/helpers/request-mapper.ts +0 -94
  36. package/src/helpers/synthetic-stream.ts +0 -188
  37. package/src/helpers/usage-mapping.ts +0 -110
  38. package/src/index.ts +0 -17
  39. package/src/types/adapter.ts +0 -42
  40. package/src/types/content.ts +0 -15
  41. package/src/types/events.ts +0 -138
  42. package/src/types/index.ts +0 -49
  43. package/src/types/items.ts +0 -57
  44. package/src/types/request.ts +0 -52
  45. package/src/types/response.ts +0 -68
  46. package/tsdown.config.ts +0 -10
@@ -1,624 +0,0 @@
1
- /**
2
- * Chat Completions Adapter
3
- *
4
- * 接入 OpenAI Chat Completions API (chat/completions 端点)。
5
- * 弱能力兼容层:
6
- * - third-party reasoning 字段仅做 best-effort 提取
7
- * - 工具调用通常整块到达(非逐 token 流)
8
- * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
9
- */
10
-
11
- import { AdapterBase } from "../helpers/adapter-base.js";
12
- import { AIRequestError, WarningCode } from "../core/errors.js";
13
- import {
14
- textBlock,
15
- messageItem,
16
- reasoningItem,
17
- toolCallItem,
18
- opaqueItem,
19
- replayFromOutput,
20
- mapStopReason,
21
- } from "../helpers/mapping.js";
22
- import { assertOpaqueReplayEnvelope } from "../helpers/adapter-security.js";
23
- import { usageFromChatCompletions } from "../helpers/usage-mapping.js";
24
- import {
25
- NormalizedRequestMapper,
26
- createChatCompletionsSseParser,
27
- openProviderJsonStream,
28
- iterateProviderStreamBatches,
29
- createCompletionGate,
30
- mergeProviderHeaders,
31
- applyExtraBody,
32
- mapChatCompletionsReasoningEffort,
33
- } from "../helpers/index.js";
34
-
35
- import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
36
-
37
- // ── 类型 ──────────────────────────────────────────────────────
38
-
39
- export type ChatCompletionsAdapterOptions = {
40
- apiKey: string;
41
- baseUrl?: string;
42
- fetch?: FetchFn;
43
- /** 额外请求头;后写覆盖内置 Authorization / Content-Type */
44
- headers?: Record<string, string>;
45
- /** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
46
- extraBody?: Record<string, unknown>;
47
- };
48
-
49
- // ── Chat API 请求类型 ─────────────────────────────────────────
50
-
51
- type ChatRequest = {
52
- model: string;
53
- messages: ChatMessage[];
54
- tools?: ChatTool[];
55
- tool_choice?: "auto" | "none" | { type: "function"; function: { name: string } };
56
- metadata?: Record<string, string>;
57
- temperature?: number;
58
- max_tokens?: number;
59
- /** Portable reasoningLevel → reasoning_effort */
60
- reasoning_effort?: string;
61
- stream: true;
62
- n: 1;
63
- };
64
-
65
- type ChatMessage = {
66
- role: "system" | "user" | "assistant" | "tool";
67
- content: string | null;
68
- tool_calls?: ChatToolCall[];
69
- tool_call_id?: string;
70
- name?: string;
71
- [key: string]: unknown;
72
- };
73
-
74
- type ChatToolCall = {
75
- id: string;
76
- type: "function";
77
- function: { name: string; arguments: string };
78
- };
79
-
80
- type ChatTool = {
81
- type: "function";
82
- function: { name: string; description?: string; parameters: Record<string, unknown> };
83
- };
84
-
85
- // ── SSE chunk 类型 ────────────────────────────────────────────
86
-
87
- type ChatChunk = {
88
- id: string;
89
- object: string;
90
- created: number;
91
- model: string;
92
- choices: ChatChunkChoice[];
93
- usage?: {
94
- prompt_tokens: number;
95
- completion_tokens: number;
96
- total_tokens: number;
97
- prompt_tokens_details?: { cached_tokens?: number };
98
- completion_tokens_details?: { reasoning_tokens?: number };
99
- };
100
- };
101
-
102
- type ChatChunkChoice = {
103
- index: number;
104
- delta: {
105
- role?: string;
106
- content?: string | null;
107
- reasoning?: unknown;
108
- reasoning_content?: unknown;
109
- tool_calls?: ChatChunkToolCall[];
110
- function_call?: { name?: string; arguments?: string };
111
- [key: string]: unknown;
112
- };
113
- finish_reason?: string | null;
114
- };
115
-
116
- type ChatChunkToolCall = {
117
- index: number;
118
- id?: string;
119
- type?: string;
120
- function?: { name?: string; arguments?: string };
121
- };
122
-
123
- type PendingToolCall = {
124
- id: string;
125
- name: string;
126
- args: string;
127
- };
128
-
129
- type ReasoningFieldName = "reasoning" | "reasoning_content";
130
-
131
- const REASONING_FIELDS: readonly ReasoningFieldName[] = ["reasoning_content", "reasoning"];
132
-
133
- const mapper = new NormalizedRequestMapper("chat-completions");
134
-
135
- function extractReasoningText(value: unknown): string {
136
- if (typeof value === "string") return value;
137
-
138
- if (Array.isArray(value)) {
139
- return value.map(extractReasoningText).join("");
140
- }
141
-
142
- if (value && typeof value === "object") {
143
- const record = value as Record<string, unknown>;
144
- for (const key of ["text", "content", "reasoning", "reasoning_content", "thinking", "value"]) {
145
- const nested = extractReasoningText(record[key]);
146
- if (nested) return nested;
147
- }
148
- }
149
-
150
- return "";
151
- }
152
-
153
- function extractReasoningDeltas(delta: ChatChunkChoice["delta"]): Array<{ field: ReasoningFieldName; text: string }> {
154
- const deltas: Array<{ field: ReasoningFieldName; text: string }> = [];
155
-
156
- for (const field of REASONING_FIELDS) {
157
- const text = extractReasoningText(delta[field]);
158
- if (text) {
159
- deltas.push({ field, text });
160
- }
161
- }
162
-
163
- return deltas;
164
- }
165
-
166
- function isChatReplayToolCall(value: unknown): value is ChatToolCall {
167
- if (!value || typeof value !== "object") return false;
168
- const entry = value as Record<string, unknown>;
169
- if (typeof entry.id !== "string" || entry.type !== "function") return false;
170
- const fn = entry.function;
171
- if (!fn || typeof fn !== "object") return false;
172
- const f = fn as Record<string, unknown>;
173
- return typeof f.name === "string" && typeof f.arguments === "string";
174
- }
175
-
176
- function isChatReplayMessage(value: unknown): value is ChatMessage {
177
- if (!value || typeof value !== "object") return false;
178
- const msg = value as Record<string, unknown>;
179
- const role = msg.role;
180
- if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") {
181
- return false;
182
- }
183
- if (!(msg.content === null || typeof msg.content === "string")) {
184
- return false;
185
- }
186
- if (msg.tool_calls !== undefined) {
187
- if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) {
188
- return false;
189
- }
190
- }
191
- if (msg.tool_call_id !== undefined && typeof msg.tool_call_id !== "string") {
192
- return false;
193
- }
194
- if (msg.name !== undefined && typeof msg.name !== "string") {
195
- return false;
196
- }
197
- return true;
198
- }
199
-
200
- function assertChatReplayMessages(messages: unknown, field: string): asserts messages is ChatMessage[] {
201
- if (!Array.isArray(messages)) {
202
- throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
203
- }
204
- for (let i = 0; i < messages.length; i++) {
205
- if (!isChatReplayMessage(messages[i])) {
206
- throw new AIRequestError(
207
- `Invalid opaque replay payload: ${field}[${i}] is not a valid chat message`,
208
- "INVALID_OPAQUE_REPLAY",
209
- );
210
- }
211
- }
212
- }
213
-
214
- function buildAssistantReplayMessage(params: {
215
- content: string;
216
- reasoningByField: ReadonlyMap<ReasoningFieldName, string>;
217
- toolCalls: readonly PendingToolCall[];
218
- }): ChatMessage | null {
219
- const { content, reasoningByField, toolCalls } = params;
220
- if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
221
-
222
- const replayMessage: ChatMessage = {
223
- role: "assistant",
224
- content: content || null,
225
- };
226
-
227
- for (const [field, text] of reasoningByField) {
228
- replayMessage[field] = text;
229
- }
230
-
231
- if (toolCalls.length > 0) {
232
- replayMessage.tool_calls = toolCalls.map((toolCall) => ({
233
- id: toolCall.id,
234
- type: "function",
235
- function: {
236
- name: toolCall.name,
237
- arguments: toolCall.args,
238
- },
239
- }));
240
- }
241
-
242
- return replayMessage;
243
- }
244
-
245
- // ── Adapter ───────────────────────────────────────────────────
246
-
247
- export class ChatCompletionsAdapter extends AdapterBase {
248
- readonly kind = "chat-completions" as const;
249
- readonly isSyntheticStream = false;
250
-
251
- private apiKey: string;
252
- private baseUrl: string;
253
- private fetchFn: FetchFn;
254
- private headers: Record<string, string> | undefined;
255
- private extraBody: Record<string, unknown> | undefined;
256
-
257
- constructor(options: ChatCompletionsAdapterOptions) {
258
- super();
259
- this.apiKey = options.apiKey;
260
- this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
261
- this.fetchFn = options.fetch ?? globalThis.fetch;
262
- this.headers = options.headers;
263
- this.extraBody = options.extraBody;
264
- }
265
-
266
- // ── buildRequest ──────────────────────────────────────────
267
-
268
- protected buildRequest(request: NormalizedRequest): ChatRequest {
269
- const messages: ChatMessage[] = [];
270
-
271
- // handle instructions → system message
272
- if (request.instructions) {
273
- messages.push({ role: "system", content: mapper.mapInstructions(request.instructions) });
274
- }
275
-
276
- for (const item of request.input) {
277
- switch (item.type) {
278
- case "message": {
279
- const role = item.role;
280
- const text = mapper.textFromBlocks(item.content, `input message (${item.role}) content`);
281
- messages.push({ role, content: text || null });
282
- break;
283
- }
284
- case "tool_call": {
285
- // 只允许附着到尾部 assistant turn,否则新建一个
286
- const lastAssistant =
287
- messages.length > 0 && messages[messages.length - 1]?.role === "assistant"
288
- ? messages[messages.length - 1]
289
- : null;
290
- const tc: ChatToolCall = {
291
- id: item.id,
292
- type: "function",
293
- function: { name: item.name, arguments: item.argumentsText },
294
- };
295
- if (lastAssistant) {
296
- lastAssistant.tool_calls = [...(lastAssistant.tool_calls ?? []), tc];
297
- } else {
298
- messages.push({ role: "assistant", content: null, tool_calls: [tc] });
299
- }
300
- break;
301
- }
302
- case "tool_result": {
303
- messages.push({
304
- role: "tool",
305
- tool_call_id: item.callId,
306
- name: item.toolName,
307
- content: mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`),
308
- });
309
- break;
310
- }
311
- case "reasoning": {
312
- // chat.completions doesn't support reasoning items in input
313
- // Convert to a text message for best-effort
314
- messages.push({
315
- role: "assistant",
316
- content: mapper.textFromBlocks(item.content, "reasoning content"),
317
- });
318
- break;
319
- }
320
- case "opaque": {
321
- // Try to restore from opaque replay
322
- if (item.source !== "chat.completions" || item.purpose !== "replay") break;
323
- assertOpaqueReplayEnvelope(item.payload);
324
- const payload = item.payload as Record<string, unknown>;
325
- if (payload.role === "assistant" && typeof payload.content === "string") {
326
- messages.push({ role: "assistant", content: payload.content });
327
- } else if (payload.replaceCanonical === true && "messages" in payload) {
328
- assertChatReplayMessages(payload.messages, "messages");
329
- mapper.rollbackTrailingAssistantMessages(messages);
330
- for (const m of payload.messages) {
331
- messages.push(m);
332
- }
333
- } else if ("messages" in payload) {
334
- assertChatReplayMessages(payload.messages, "messages");
335
- for (const m of payload.messages) {
336
- messages.push(m);
337
- }
338
- }
339
- break;
340
- }
341
- }
342
- }
343
-
344
- const body: ChatRequest = {
345
- model: request.model,
346
- messages,
347
- stream: true,
348
- n: 1,
349
- };
350
-
351
- body.tools = mapper.mapToolsIfPresent(
352
- request.tools,
353
- (t): ChatTool => ({
354
- type: "function",
355
- function: {
356
- name: t.name,
357
- description: t.description,
358
- parameters: t.inputSchema as Record<string, unknown>,
359
- },
360
- }),
361
- );
362
-
363
- body.tool_choice = mapper.mapToolChoice<Exclude<ChatRequest["tool_choice"], undefined>>(request.toolChoice, {
364
- auto: "auto",
365
- none: "none",
366
- tool: (name) => ({ type: "function" as const, function: { name } }),
367
- });
368
-
369
- if (request.temperature !== undefined) body.temperature = request.temperature;
370
- if (request.maxOutputTokens !== undefined) body.max_tokens = request.maxOutputTokens;
371
- if (request.metadata) body.metadata = request.metadata;
372
- if (request.reasoningLevel !== undefined) {
373
- body.reasoning_effort = mapChatCompletionsReasoningEffort(request.reasoningLevel);
374
- }
375
-
376
- return applyExtraBody(body, this.extraBody);
377
- }
378
-
379
- // ── runStream ─────────────────────────────────────────────
380
-
381
- protected async *runStream(
382
- providerRequest: ChatRequest,
383
- factory: EventFactory,
384
- request: NormalizedRequest,
385
- ): AsyncIterable<AIStreamEvent> {
386
- const auxiliary = this.createAuxiliaryState(request);
387
- const gate = createCompletionGate();
388
-
389
- const { reader } = await openProviderJsonStream({
390
- fetchFn: this.fetchFn,
391
- url: `${this.baseUrl}/chat/completions`,
392
- headers: mergeProviderHeaders(
393
- {
394
- "Content-Type": "application/json",
395
- Authorization: `Bearer ${this.apiKey}`,
396
- },
397
- this.headers,
398
- ),
399
- body: providerRequest,
400
- signal: request.signal,
401
- });
402
-
403
- const parser = createChatCompletionsSseParser<ChatChunk>();
404
- const output: OutputItem[] = [];
405
-
406
- // 累积状态 — 支持多 choice,此处只取 index 0
407
- let responseId: string | undefined;
408
- let accumulatedContent = "";
409
- let accumulatedReasoning = "";
410
- let currentMessageId = "";
411
- let currentReasoningId = "";
412
- let hasMessageStarted = false;
413
- let hasReasoningStarted = false;
414
- let warnedNonZeroChoice = false;
415
-
416
- // tool_calls 累积: tool call index → { id, name, args }
417
- const pendingToolCalls = new Map<number, PendingToolCall>();
418
- const reasoningByField = new Map<ReasoningFieldName, string>();
419
-
420
- const finalizePendingTurn = (): { events: AIStreamEvent[]; assistantReplayMessage: ChatMessage | null } => {
421
- const events: AIStreamEvent[] = [];
422
- const finalizedToolCalls = [...pendingToolCalls.values()];
423
- const finalizedReasoningByField = new Map(reasoningByField);
424
-
425
- if (hasReasoningStarted && accumulatedReasoning) {
426
- const reasoning = reasoningItem([textBlock(accumulatedReasoning)], "full", currentReasoningId);
427
- events.push(factory.reasoningCompleted(currentReasoningId));
428
- output.push(reasoning);
429
- }
430
-
431
- if (hasMessageStarted) {
432
- const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
433
- events.push(factory.messageCompleted(currentMessageId));
434
- output.push(message);
435
- }
436
-
437
- for (const pending of finalizedToolCalls) {
438
- const toolCall = toolCallItem(pending.id, pending.name, pending.args);
439
- events.push(factory.toolCallCompleted(pending.id));
440
- output.push(toolCall);
441
- }
442
-
443
- const assistantReplayMessage = buildAssistantReplayMessage({
444
- content: accumulatedContent,
445
- reasoningByField: finalizedReasoningByField,
446
- toolCalls: finalizedToolCalls,
447
- });
448
-
449
- accumulatedContent = "";
450
- accumulatedReasoning = "";
451
- currentMessageId = "";
452
- currentReasoningId = "";
453
- hasMessageStarted = false;
454
- hasReasoningStarted = false;
455
- pendingToolCalls.clear();
456
- reasoningByField.clear();
457
-
458
- return { events, assistantReplayMessage };
459
- };
460
-
461
- const emitCompleted = async function* (
462
- this: ChatCompletionsAdapter,
463
- stopReason: StopReason | undefined,
464
- assistantReplayMessage: ChatMessage | null,
465
- rawResponseId: string | undefined,
466
- ): AsyncIterable<AIStreamEvent> {
467
- if (!gate.tryComplete()) {
468
- yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
469
- return;
470
- }
471
-
472
- const replay = [...replayFromOutput(output)];
473
- if (assistantReplayMessage) {
474
- replay.push(
475
- opaqueItem("chat.completions", "replay", {
476
- replaceCanonical: true,
477
- messages: [assistantReplayMessage],
478
- }),
479
- );
480
- }
481
-
482
- yield* this.emitStreamCompleted(factory, request, auxiliary, {
483
- output,
484
- replay,
485
- stopReason,
486
- rawResponseId,
487
- });
488
- }.bind(this);
489
-
490
- for await (const batch of iterateProviderStreamBatches({
491
- reader,
492
- parser,
493
- factory,
494
- providerLabel: "Chat Completions",
495
- transportLabel: "SSE event(s)",
496
- incompleteMessage: "Stream ended with an incomplete Chat Completions SSE frame",
497
- })) {
498
- for (const warning of batch.warnings) yield warning;
499
-
500
- for (const chunk of batch.items) {
501
- responseId = chunk.id;
502
-
503
- if (chunk.usage) {
504
- auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
505
- }
506
-
507
- for (const choice of chunk.choices) {
508
- if (choice.index !== 0) {
509
- if (!warnedNonZeroChoice) {
510
- yield factory.responseWarning(
511
- `Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`,
512
- "MULTIPLE_CHOICES_IGNORED",
513
- );
514
- warnedNonZeroChoice = true;
515
- }
516
- continue;
517
- }
518
-
519
- if (gate.completed) {
520
- if (choice.finish_reason) {
521
- yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
522
- }
523
- continue;
524
- }
525
-
526
- const delta = choice.delta;
527
- const finishReason = choice.finish_reason;
528
- const reasoningDeltas = extractReasoningDeltas(delta);
529
-
530
- const ensureMessageStarted = (): void => {
531
- if (hasMessageStarted) return;
532
- currentMessageId = `msg-${chunk.id}`;
533
- hasMessageStarted = true;
534
- accumulatedContent = "";
535
- };
536
-
537
- if (reasoningDeltas.length > 0) {
538
- if (!hasReasoningStarted) {
539
- currentReasoningId = `reason-${chunk.id}`;
540
- hasReasoningStarted = true;
541
- accumulatedReasoning = "";
542
- yield factory.reasoningStarted(currentReasoningId, "full");
543
- }
544
-
545
- for (const reasoningDelta of reasoningDeltas) {
546
- accumulatedReasoning += reasoningDelta.text;
547
- reasoningByField.set(
548
- reasoningDelta.field,
549
- (reasoningByField.get(reasoningDelta.field) ?? "") + reasoningDelta.text,
550
- );
551
- yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
552
- }
553
- }
554
-
555
- if (delta.content) {
556
- if (!hasMessageStarted) {
557
- ensureMessageStarted();
558
- yield factory.messageStarted(currentMessageId);
559
- }
560
- accumulatedContent += delta.content;
561
- yield factory.messageDelta(currentMessageId, textBlock(delta.content));
562
- }
563
-
564
- if (delta.tool_calls) {
565
- if (!hasMessageStarted) {
566
- ensureMessageStarted();
567
- yield factory.messageStarted(currentMessageId);
568
- }
569
-
570
- for (const tc of delta.tool_calls) {
571
- const idx = tc.index;
572
-
573
- if (tc.id) {
574
- pendingToolCalls.set(idx, { id: tc.id, name: tc.function?.name ?? "", args: "" });
575
- yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
576
- }
577
-
578
- if (tc.function?.arguments) {
579
- const pending = pendingToolCalls.get(idx);
580
- if (pending) {
581
- pending.args += tc.function.arguments;
582
- yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
583
- }
584
- }
585
- }
586
- }
587
-
588
- if (delta.function_call) {
589
- if (!hasMessageStarted) {
590
- ensureMessageStarted();
591
- yield factory.messageStarted(currentMessageId);
592
- }
593
-
594
- if (delta.function_call.name) {
595
- const fcId = `fc-${chunk.id}-0`;
596
- pendingToolCalls.set(0, { id: fcId, name: delta.function_call.name, args: "" });
597
- yield factory.toolCallStarted(fcId, delta.function_call.name);
598
- }
599
- if (delta.function_call.arguments) {
600
- const pending = pendingToolCalls.get(0);
601
- if (pending) {
602
- pending.args += delta.function_call.arguments;
603
- yield factory.toolCallDelta(pending.id, { argumentsText: delta.function_call.arguments });
604
- }
605
- }
606
- }
607
-
608
- if (finishReason && finishReason !== null) {
609
- const { events, assistantReplayMessage } = finalizePendingTurn();
610
- for (const event of events) yield event;
611
- yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
612
- }
613
- }
614
- }
615
- }
616
-
617
- if (!gate.completed && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
618
- yield factory.responseWarning("Stream ended without a finish_reason", WarningCode.STREAM_INCOMPLETE);
619
- const { events, assistantReplayMessage } = finalizePendingTurn();
620
- for (const event of events) yield event;
621
- yield* emitCompleted(undefined, assistantReplayMessage, responseId);
622
- }
623
- }
624
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * 后端适配器
3
- *
4
- * 模块边界:各类 AI 后端 adapter 实现。
5
- * - responses
6
- * - messages
7
- * - chat.completions
8
- * - ollama
9
- * - mock
10
- *
11
- * 每个 adapter 实现 BackendAdapter 内部协议。
12
- */
13
-
14
- export { ResponsesAdapter } from "./responses.js";
15
- export type { ResponsesAdapterOptions } from "./responses.js";
16
- export { MessagesAdapter } from "./messages.js";
17
- export type { MessagesAdapterOptions } from "./messages.js";
18
- export { ChatCompletionsAdapter } from "./chat-completions.js";
19
- export type { ChatCompletionsAdapterOptions } from "./chat-completions.js";
20
- export { OllamaAdapter } from "./ollama.js";
21
- export type { OllamaAdapterOptions } from "./ollama.js";
22
- export { MockAdapter } from "./mock.js";
23
- export type {
24
- MockAdapterOptions,
25
- MockHistoryRecord,
26
- MockTextStreamOptions,
27
- MockInputExpectation,
28
- MockRequestExpectation,
29
- MockHandlerContext,
30
- MockHandler,
31
- MockStaticHandler,
32
- MockWarningStep,
33
- MockAuxiliaryStep,
34
- MockMessageStep,
35
- MockReasoningStep,
36
- MockToolCallStep,
37
- MockOutputStep,
38
- MockCompleteStep,
39
- MockErrorStep,
40
- MockInterruptStep,
41
- MockThrowStep,
42
- MockStep,
43
- } from "./mock.js";
44
- export { assertMockRequest, withMockStreaming } from "./mock.js";