@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/dist/index.mjs CHANGED
@@ -156,6 +156,12 @@ function validateInputItem(item, field, issues) {
156
156
  if (typeof item.id !== "string" || item.id.length === 0) pushIssue(issues, `${field}.id`, "TOOL_CALL_ID_INVALID", `${field}.id must be a non-empty string`);
157
157
  if (typeof item.name !== "string" || item.name.length === 0) pushIssue(issues, `${field}.name`, "TOOL_CALL_NAME_INVALID", `${field}.name must be a non-empty string`);
158
158
  if (typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must be a string`);
159
+ else try {
160
+ const parsed = JSON.parse(item.argumentsText);
161
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must encode a JSON object`);
162
+ } catch {
163
+ pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must encode a JSON object`);
164
+ }
159
165
  return;
160
166
  case "tool_result":
161
167
  if (typeof item.callId !== "string" || item.callId.length === 0) pushIssue(issues, `${field}.callId`, "TOOL_RESULT_CALL_ID_INVALID", `${field}.callId must be a non-empty string`);
@@ -820,13 +826,12 @@ function reasoningItem(content, visibility = "full", id) {
820
826
  content
821
827
  };
822
828
  }
823
- function toolCallItem(id, name, argumentsText, argumentsJson) {
829
+ function toolCallItem(id, name, argumentsText) {
824
830
  return {
825
831
  type: "tool_call",
826
832
  id,
827
833
  name,
828
- argumentsText,
829
- argumentsJson
834
+ argumentsText
830
835
  };
831
836
  }
832
837
  function toolResultItem(callId, toolName, outcome, content) {
@@ -878,12 +883,6 @@ function contentBlocksToText(blocks) {
878
883
  return blocks.map(blockToText).join("\n");
879
884
  }
880
885
  /**
881
- * 将 instructions(string | InstructionBlock[])归一化为纯文本。
882
- */
883
- function instructionsToText(instructions) {
884
- return typeof instructions === "string" ? instructions : contentBlocksToText(instructions);
885
- }
886
- /**
887
886
  * 从 OutputItem 数组中提取所有 message 类型 item 的文本内容。
888
887
  */
889
888
  function extractText(output) {
@@ -1069,14 +1068,6 @@ function emitMalformedStreamWarning(factory, options) {
1069
1068
  if (options.count < 1) return void 0;
1070
1069
  return factory.responseWarning(`Skipped ${options.count} malformed ${options.providerLabel} ${options.transportLabel}`, "STREAM_ERROR");
1071
1070
  }
1072
- function metadataSourceList(...groups) {
1073
- const sources = /* @__PURE__ */ new Set();
1074
- for (const group of groups) {
1075
- if (!group) continue;
1076
- for (const source of group) sources.add(source);
1077
- }
1078
- return sources.size > 0 ? [...sources] : void 0;
1079
- }
1080
1071
  function isEmptyRecord(value) {
1081
1072
  return Object.keys(value).length === 0;
1082
1073
  }
@@ -1095,7 +1086,7 @@ var AdapterBase = class {
1095
1086
  responseId: request.requestId,
1096
1087
  backend: {
1097
1088
  kind: this.kind,
1098
- isSynthetic: this.capabilities.textStreaming === "synthetic"
1089
+ isSynthetic: this.isSyntheticStream
1099
1090
  }
1100
1091
  });
1101
1092
  yield factory.responseStarted(request.model);
@@ -1129,7 +1120,7 @@ var AdapterBase = class {
1129
1120
  * 子类可在返回前自定义覆盖。
1130
1121
  */
1131
1122
  buildResponse(request, result, _factory) {
1132
- const text = this.extractText(result.output);
1123
+ const text = extractText(result.output);
1133
1124
  const warnings = mergeWarnings(result.warnings, _factory.warnings);
1134
1125
  const auxiliary = mergeAuxiliary(result.auxiliary, result.providerMetadata ? { providerMetadata: result.providerMetadata } : void 0);
1135
1126
  return {
@@ -1147,15 +1138,36 @@ var AdapterBase = class {
1147
1138
  requestId: request.requestId,
1148
1139
  rawResponseId: result.rawResponseId,
1149
1140
  adapter: this.kind,
1150
- isSyntheticStream: this.capabilities.textStreaming === "synthetic",
1141
+ isSyntheticStream: this.isSyntheticStream,
1151
1142
  metadataSources: result.metadataSources,
1152
1143
  warnings
1153
1144
  }
1154
1145
  };
1155
1146
  }
1156
- /** 从 output items 中提取文本内容。 */
1157
- extractText(output) {
1158
- return extractText(output);
1147
+ /**
1148
+ * 统一 finalize auxiliary → response.completed。
1149
+ * adapter 在调用前组装 output / replay / stopReason 等业务字段。
1150
+ */
1151
+ async *emitStreamCompleted(factory, request, auxiliary, result) {
1152
+ const auxiliaryResult = await auxiliary.finalize(factory);
1153
+ for (const event of auxiliaryResult.events) yield event;
1154
+ const finalResponse = this.buildResponse(request, {
1155
+ ...result,
1156
+ usage: result.usage ?? auxiliaryResult.usage,
1157
+ billing: result.billing ?? auxiliaryResult.billing,
1158
+ auxiliary: mergeAuxiliary(result.auxiliary, auxiliaryResult.auxiliary),
1159
+ warnings: mergeWarnings(result.warnings, auxiliaryResult.warnings),
1160
+ metadataSources: result.metadataSources ?? auxiliaryResult.metadataSources
1161
+ }, factory);
1162
+ yield factory.responseCompleted({
1163
+ replay: finalResponse.replay,
1164
+ stopReason: finalResponse.stopReason,
1165
+ trace: finalResponse.backend,
1166
+ usage: finalResponse.usage,
1167
+ billing: finalResponse.billing,
1168
+ auxiliary: finalResponse.auxiliary,
1169
+ warnings: finalResponse.warnings
1170
+ });
1159
1171
  }
1160
1172
  createAuxiliaryState(request) {
1161
1173
  return new AdapterAuxiliaryState(request);
@@ -1264,32 +1276,35 @@ function record(obj) {
1264
1276
  for (const [key, value] of Object.entries(obj)) if (value !== void 0) out[key] = value;
1265
1277
  return out;
1266
1278
  }
1267
- /** OpenAI Chat Completions `usage` */
1268
- function usageFromChatCompletions(raw) {
1269
- const inputTokens = num(raw.prompt_tokens);
1270
- const outputTokens = num(raw.completion_tokens);
1271
- const cachedInputTokens = num(raw.prompt_tokens_details?.cached_tokens);
1272
- const reasoningTokens = num(raw.completion_tokens_details?.reasoning_tokens);
1279
+ function withDerivedTotal(usage) {
1280
+ const { inputTokens, outputTokens, totalTokens, cachedInputTokens, reasoningTokens, cacheWriteInputTokens } = usage;
1273
1281
  return record({
1274
1282
  inputTokens,
1275
1283
  outputTokens,
1276
- totalTokens: num(raw.total_tokens) ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0),
1284
+ totalTokens: totalTokens ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0),
1277
1285
  cachedInputTokens,
1278
- reasoningTokens
1286
+ reasoningTokens,
1287
+ cacheWriteInputTokens
1288
+ });
1289
+ }
1290
+ /** OpenAI Chat Completions `usage` */
1291
+ function usageFromChatCompletions(raw) {
1292
+ return withDerivedTotal({
1293
+ inputTokens: num(raw.prompt_tokens),
1294
+ outputTokens: num(raw.completion_tokens),
1295
+ totalTokens: num(raw.total_tokens),
1296
+ cachedInputTokens: num(raw.prompt_tokens_details?.cached_tokens),
1297
+ reasoningTokens: num(raw.completion_tokens_details?.reasoning_tokens)
1279
1298
  });
1280
1299
  }
1281
1300
  /** OpenAI Responses API `usage` */
1282
1301
  function usageFromOpenAIResponses(raw) {
1283
- const inputTokens = num(raw.input_tokens);
1284
- const outputTokens = num(raw.output_tokens);
1285
- const cachedInputTokens = num(raw.input_tokens_details?.cached_tokens);
1286
- const reasoningTokens = num(raw.output_tokens_details?.reasoning_tokens);
1287
- return record({
1288
- inputTokens,
1289
- outputTokens,
1290
- totalTokens: num(raw.total_tokens) ?? (inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0),
1291
- cachedInputTokens,
1292
- reasoningTokens
1302
+ return withDerivedTotal({
1303
+ inputTokens: num(raw.input_tokens),
1304
+ outputTokens: num(raw.output_tokens),
1305
+ totalTokens: num(raw.total_tokens),
1306
+ cachedInputTokens: num(raw.input_tokens_details?.cached_tokens),
1307
+ reasoningTokens: num(raw.output_tokens_details?.reasoning_tokens)
1293
1308
  });
1294
1309
  }
1295
1310
  /** Anthropic Messages `usage`(message_start / message_delta) */
@@ -1303,93 +1318,21 @@ function usageFromAnthropicMessages(raw) {
1303
1318
  cacheWriteInputTokens,
1304
1319
  cachedInputTokens
1305
1320
  ].filter((n) => n !== void 0);
1306
- const inputTokens = inputParts.length > 0 ? inputParts.reduce((sum, n) => sum + n, 0) : void 0;
1307
- return record({
1308
- inputTokens,
1321
+ return withDerivedTotal({
1322
+ inputTokens: inputParts.length > 0 ? inputParts.reduce((sum, n) => sum + n, 0) : void 0,
1309
1323
  outputTokens,
1310
- totalTokens: inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0,
1311
1324
  cachedInputTokens,
1312
1325
  cacheWriteInputTokens
1313
1326
  });
1314
1327
  }
1315
1328
  /** Ollama 流式 chunk */
1316
1329
  function usageFromOllama(raw) {
1317
- const inputTokens = num(raw.prompt_eval_count);
1318
- const outputTokens = num(raw.eval_count);
1319
- return record({
1320
- inputTokens,
1321
- outputTokens,
1322
- totalTokens: inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0
1330
+ return withDerivedTotal({
1331
+ inputTokens: num(raw.prompt_eval_count),
1332
+ outputTokens: num(raw.eval_count)
1323
1333
  });
1324
1334
  }
1325
1335
  //#endregion
1326
- //#region src/helpers/sse-parser.ts
1327
- /**
1328
- * 将 SSE 文本块解析为事件数组。
1329
- * 累积事件行直到遇到空行,支持 [DONE] 标记。
1330
- * 返回已解析的事件和未处理的剩余 buffer(用于增量解析)。
1331
- *
1332
- * 关键行为:
1333
- * - 只解析完整的 event(以空行结尾)
1334
- * - 未完成的行保留在 rest 中,等待下次 chunk 补全
1335
- * - 支持跨 chunk 的 event 分片
1336
- */
1337
- function parseSSEEvents(chunk, options = {}) {
1338
- const events = [];
1339
- let eventType = "";
1340
- let dataLines = [];
1341
- let consumedUntil = 0;
1342
- let cursor = 0;
1343
- let malformedEvents = 0;
1344
- const emitEvent = (consumedCursor) => {
1345
- const dataStr = dataLines.join("\n");
1346
- if (dataStr === "[DONE]") {
1347
- eventType = "";
1348
- dataLines = [];
1349
- consumedUntil = consumedCursor;
1350
- return;
1351
- }
1352
- try {
1353
- const data = JSON.parse(dataStr);
1354
- events.push({
1355
- type: eventType,
1356
- data
1357
- });
1358
- } catch {
1359
- malformedEvents++;
1360
- }
1361
- eventType = "";
1362
- dataLines = [];
1363
- consumedUntil = consumedCursor;
1364
- };
1365
- const consumeLine = (line, consumedCursor) => {
1366
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1367
- else if (line.startsWith("data: ")) dataLines.push(line.slice(6));
1368
- else if (line === "" && eventType && dataLines.length > 0) emitEvent(consumedCursor);
1369
- else if (line === "" && !eventType && dataLines.length === 0) consumedUntil = consumedCursor;
1370
- };
1371
- while (cursor < chunk.length) {
1372
- const lineEnd = chunk.indexOf("\n", cursor);
1373
- if (lineEnd === -1) break;
1374
- let line = chunk.slice(cursor, lineEnd);
1375
- cursor = lineEnd + 1;
1376
- if (line.endsWith("\r")) line = line.slice(0, -1);
1377
- consumeLine(line, cursor);
1378
- }
1379
- if (options.allowEOF && cursor < chunk.length) {
1380
- let line = chunk.slice(cursor);
1381
- if (line.endsWith("\r")) line = line.slice(0, -1);
1382
- consumeLine(line, chunk.length);
1383
- cursor = chunk.length;
1384
- }
1385
- if (options.allowEOF && eventType && dataLines.length > 0) emitEvent(chunk.length);
1386
- return {
1387
- events,
1388
- rest: chunk.slice(consumedUntil),
1389
- malformedEvents
1390
- };
1391
- }
1392
- //#endregion
1393
1336
  //#region src/helpers/synthetic-stream.ts
1394
1337
  /**
1395
1338
  * 模拟流式 (Synthetic Streaming)
@@ -1563,36 +1506,207 @@ function splitSSEFrames(buffer, allowEOF) {
1563
1506
  rest: normalized.slice(cursor)
1564
1507
  };
1565
1508
  }
1509
+ /** 解析标准 SSE frame(event: + data:),用于 Messages / Responses。 */
1510
+ function parseSseJsonFrame(frame) {
1511
+ let eventType = "";
1512
+ let dataStr = "";
1513
+ for (const rawLine of frame.split("\n")) {
1514
+ const line = rawLine.trim();
1515
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1516
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
1517
+ }
1518
+ if (!eventType) return { status: "ignored" };
1519
+ try {
1520
+ const data = JSON.parse(dataStr);
1521
+ return {
1522
+ status: "parsed",
1523
+ value: {
1524
+ type: eventType,
1525
+ data
1526
+ }
1527
+ };
1528
+ } catch {
1529
+ return { status: "malformed" };
1530
+ }
1531
+ }
1532
+ function createSseJsonParser() {
1533
+ return new IncrementalStreamParser(splitSSEFrames, (frame) => parseSseJsonFrame(frame));
1534
+ }
1535
+ /** OpenAI Chat Completions 简化 SSE:仅 `data: ...` 行,忽略 `[DONE]`。 */
1536
+ function parseChatCompletionsDataLine(item) {
1537
+ const trimmed = item.trim();
1538
+ if (!trimmed.startsWith("data: ")) return { status: "ignored" };
1539
+ const data = trimmed.slice(6).trim();
1540
+ if (data === "[DONE]") return { status: "ignored" };
1541
+ try {
1542
+ return {
1543
+ status: "parsed",
1544
+ value: JSON.parse(data)
1545
+ };
1546
+ } catch {
1547
+ return { status: "malformed" };
1548
+ }
1549
+ }
1550
+ function createChatCompletionsSseParser() {
1551
+ return new IncrementalStreamParser(splitLines, (item) => parseChatCompletionsDataLine(item));
1552
+ }
1553
+ /** NDJSON 行解析(Ollama 等):空行忽略,JSON 失败为 malformed。 */
1554
+ function createNdjsonLineParser(isValid) {
1555
+ return new IncrementalStreamParser(splitLines, (item) => {
1556
+ const trimmed = item.trim();
1557
+ if (!trimmed) return { status: "ignored" };
1558
+ try {
1559
+ const parsed = JSON.parse(trimmed);
1560
+ if (isValid(parsed)) return {
1561
+ status: "parsed",
1562
+ value: parsed
1563
+ };
1564
+ return { status: "malformed" };
1565
+ } catch {
1566
+ return { status: "malformed" };
1567
+ }
1568
+ });
1569
+ }
1570
+ //#endregion
1571
+ //#region src/helpers/provider-stream.ts
1572
+ /**
1573
+ * Provider HTTP 流公共脚手架
1574
+ *
1575
+ * 收敛 adapter 间重复的:
1576
+ * - JSON POST + 错误映射
1577
+ * - ReadableStream reader 生命周期
1578
+ * - IncrementalStreamParser feed/flush + malformed warning
1579
+ * - 不完整尾帧 warning
1580
+ */
1581
+ /** POST JSON 并返回可读 body reader + response headers;统一网络/HTTP/空 body 错误。 */
1582
+ async function openProviderJsonStream(options) {
1583
+ const { fetchFn, url, headers, body, signal } = options;
1584
+ let response;
1585
+ try {
1586
+ response = await fetchFn(url, {
1587
+ method: "POST",
1588
+ headers,
1589
+ body: JSON.stringify(body),
1590
+ signal
1591
+ });
1592
+ } catch (err) {
1593
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
1594
+ }
1595
+ if (!response.ok) {
1596
+ const errorBody = await response.text().catch(() => "");
1597
+ throw providerHttpError(response.status, errorBody);
1598
+ }
1599
+ const bodyStream = response.body;
1600
+ if (!bodyStream) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
1601
+ return {
1602
+ reader: bodyStream.getReader(),
1603
+ headers: response.headers
1604
+ };
1605
+ }
1606
+ /**
1607
+ * 读取并解析 provider 流。
1608
+ * 每个 batch 携带本轮解析出的 items 与(可选)malformed / incomplete warning。
1609
+ * 调用方应 `for await` 消费完毕;reader 在迭代结束时 cancel/release。
1610
+ */
1611
+ async function* iterateProviderStreamBatches(options) {
1612
+ const { reader, parser, factory, providerLabel, transportLabel, incompleteMessage } = options;
1613
+ let streamDone = false;
1614
+ try {
1615
+ while (true) {
1616
+ const { done, value } = await reader.read().catch((err) => {
1617
+ throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
1618
+ });
1619
+ const { items, malformed } = done ? parser.flush() : parser.feed(value);
1620
+ const warnings = [];
1621
+ const malformedWarning = emitMalformedStreamWarning(factory, {
1622
+ count: malformed,
1623
+ providerLabel,
1624
+ transportLabel
1625
+ });
1626
+ if (malformedWarning) warnings.push(malformedWarning);
1627
+ yield {
1628
+ items,
1629
+ warnings
1630
+ };
1631
+ if (done) {
1632
+ streamDone = true;
1633
+ break;
1634
+ }
1635
+ }
1636
+ } finally {
1637
+ try {
1638
+ if (!streamDone) await reader.cancel().catch(() => void 0);
1639
+ } finally {
1640
+ reader.releaseLock();
1641
+ }
1642
+ }
1643
+ if (parser.getRemaining().trim().length > 0) yield {
1644
+ items: [],
1645
+ warnings: [factory.responseWarning(incompleteMessage, "STREAM_ERROR")]
1646
+ };
1647
+ }
1648
+ /** 一次性 complete 守卫:首次成功,后续返回 false。 */
1649
+ function createCompletionGate() {
1650
+ let completed = false;
1651
+ return {
1652
+ get completed() {
1653
+ return completed;
1654
+ },
1655
+ tryComplete() {
1656
+ if (completed) return false;
1657
+ completed = true;
1658
+ return true;
1659
+ }
1660
+ };
1661
+ }
1566
1662
  //#endregion
1567
1663
  //#region src/helpers/request-mapper.ts
1568
1664
  var NormalizedRequestMapper = class {
1569
- profile;
1570
- constructor(profile) {
1571
- this.profile = profile;
1665
+ kind;
1666
+ constructor(kind) {
1667
+ this.kind = kind;
1572
1668
  }
1573
1669
  mapInstructions(instructions) {
1574
1670
  return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
1575
1671
  }
1576
1672
  ensureTextBlocks(blocks, field) {
1577
- return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
1673
+ return this.ensureBlocks(blocks, field, ["text", "json"], "only text/json blocks are supported");
1578
1674
  }
1579
1675
  ensureReasoningBlocks(blocks, field) {
1580
- return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
1676
+ return this.ensureBlocks(blocks, field, ["text"], "reasoning only supports text blocks");
1677
+ }
1678
+ /** ensureTextBlocks + contentBlocksToText 的常见组合。 */
1679
+ textFromBlocks(blocks, field) {
1680
+ return contentBlocksToText(this.ensureTextBlocks(blocks, field));
1581
1681
  }
1582
- assertToolResultOutcome(outcome) {
1583
- if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
1584
- const outcomes = this.profile.capabilities.toolResultOutcomes;
1585
- const supported = outcomes.map((value) => `"${value}"`).join(" and ");
1586
- const verb = outcomes.length > 1 ? "are" : "is";
1587
- throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1682
+ parseToolArguments(item) {
1683
+ try {
1684
+ const parsed = JSON.parse(item.argumentsText);
1685
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1686
+ } catch {}
1687
+ throw new AIRequestError(`${this.kind} requires tool_call argumentsText to be a valid JSON object`, "TOOL_CALL_ARGUMENTS_INVALID");
1588
1688
  }
1589
1689
  rollbackTrailingAssistantMessages(messages) {
1590
1690
  while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
1591
1691
  }
1692
+ mapToolsIfPresent(tools, map) {
1693
+ if (!tools || tools.length === 0) return void 0;
1694
+ return tools.map(map);
1695
+ }
1696
+ /**
1697
+ * 将 canonical toolChoice 映射为 provider 形状。
1698
+ * 返回 undefined 表示调用方无需写入 body 字段。
1699
+ */
1700
+ mapToolChoice(toolChoice, mappers) {
1701
+ if (!toolChoice) return void 0;
1702
+ if (toolChoice === "auto") return mappers.auto;
1703
+ if (toolChoice === "none") return mappers.none;
1704
+ if (toolChoice.type === "tool") return mappers.tool(toolChoice.name);
1705
+ }
1592
1706
  ensureBlocks(blocks, field, supportedTypes, description) {
1593
1707
  for (let i = 0; i < blocks.length; i++) {
1594
1708
  const block = blocks[i];
1595
- if (block && !supportedTypes.includes(block.type)) throw new AIRequestError(`${this.profile.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`, "UNSUPPORTED_CONTENT_BLOCK");
1709
+ if (block && !supportedTypes.includes(block.type)) throw new AIRequestError(`${this.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`, "UNSUPPORTED_CONTENT_BLOCK");
1596
1710
  }
1597
1711
  return blocks;
1598
1712
  }
@@ -1609,21 +1723,7 @@ var NormalizedRequestMapper = class {
1609
1723
  *
1610
1724
  * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1611
1725
  */
1612
- const profile$3 = {
1613
- kind: "responses",
1614
- instructionsMode: "instructions_field",
1615
- supportedBlockTypes: ["text", "json"],
1616
- reasoningBlockTypes: ["text"],
1617
- capabilities: {
1618
- textStreaming: "native",
1619
- reasoningStreaming: "native",
1620
- toolCallStreaming: "native",
1621
- replay: "opaque",
1622
- usage: "final",
1623
- toolResultOutcomes: ["success"]
1624
- }
1625
- };
1626
- const mapper$3 = new NormalizedRequestMapper(profile$3);
1726
+ const mapper$3 = new NormalizedRequestMapper("responses");
1627
1727
  /** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
1628
1728
  const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1629
1729
  "response.output_item.added",
@@ -1632,8 +1732,6 @@ const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1632
1732
  "response.output_text.done",
1633
1733
  "response.reasoning.delta",
1634
1734
  "response.reasoning.done",
1635
- "response.tool_call.delta",
1636
- "response.tool_call.done",
1637
1735
  "response.function_call_arguments.delta",
1638
1736
  "response.function_call_arguments.done",
1639
1737
  "response.content_part.added",
@@ -1669,7 +1767,7 @@ function canonicalToResponsesBlock(b) {
1669
1767
  }
1670
1768
  var ResponsesAdapter = class extends AdapterBase {
1671
1769
  kind = "responses";
1672
- capabilities = profile$3.capabilities;
1770
+ isSyntheticStream = false;
1673
1771
  apiKey;
1674
1772
  baseUrl;
1675
1773
  fetchFn;
@@ -1693,7 +1791,7 @@ var ResponsesAdapter = class extends AdapterBase {
1693
1791
  } else input.push({
1694
1792
  type: "message",
1695
1793
  role: item.role,
1696
- content: contentBlocksToText(mapper$3.ensureTextBlocks(item.content, `input message (${item.role}) content`))
1794
+ content: mapper$3.textFromBlocks(item.content, `input message (${item.role}) content`)
1697
1795
  });
1698
1796
  break;
1699
1797
  case "reasoning": {
@@ -1716,8 +1814,7 @@ var ResponsesAdapter = class extends AdapterBase {
1716
1814
  });
1717
1815
  break;
1718
1816
  case "tool_result": {
1719
- mapper$3.assertToolResultOutcome(item.outcome);
1720
- const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1817
+ const output = mapper$3.textFromBlocks(item.content, `tool_result ${item.callId} content`);
1721
1818
  input.push({
1722
1819
  type: "function_call_output",
1723
1820
  call_id: item.callId,
@@ -1745,20 +1842,20 @@ var ResponsesAdapter = class extends AdapterBase {
1745
1842
  stream: true
1746
1843
  };
1747
1844
  if (request.instructions) body.instructions = mapper$3.mapInstructions(request.instructions);
1748
- if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
1845
+ body.tools = mapper$3.mapToolsIfPresent(request.tools, (t) => ({
1749
1846
  type: "function",
1750
1847
  name: t.name,
1751
1848
  description: t.description,
1752
1849
  input_schema: t.inputSchema
1753
1850
  }));
1754
- if (request.toolChoice) {
1755
- if (request.toolChoice === "auto") body.tool_choice = "auto";
1756
- else if (request.toolChoice === "none") body.tool_choice = "none";
1757
- else if (request.toolChoice.type === "tool") body.tool_choice = {
1851
+ body.tool_choice = mapper$3.mapToolChoice(request.toolChoice, {
1852
+ auto: "auto",
1853
+ none: "none",
1854
+ tool: (name) => ({
1758
1855
  type: "function",
1759
- name: request.toolChoice.name
1760
- };
1761
- }
1856
+ name
1857
+ })
1858
+ });
1762
1859
  if (request.temperature !== void 0) body.temperature = request.temperature;
1763
1860
  if (request.maxOutputTokens !== void 0) body.max_output_tokens = request.maxOutputTokens;
1764
1861
  if (request.metadata) body.metadata = request.metadata;
@@ -1766,151 +1863,108 @@ var ResponsesAdapter = class extends AdapterBase {
1766
1863
  }
1767
1864
  async *runStream(providerRequest, factory, request) {
1768
1865
  const auxiliary = this.createAuxiliaryState(request);
1769
- let response;
1770
- try {
1771
- response = await this.fetchFn(`${this.baseUrl}/responses`, {
1772
- method: "POST",
1773
- headers: {
1774
- "Content-Type": "application/json",
1775
- Authorization: `Bearer ${this.apiKey}`
1776
- },
1777
- body: JSON.stringify(providerRequest),
1778
- signal: request.signal
1779
- });
1780
- } catch (err) {
1781
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
1782
- }
1783
- if (!response.ok) {
1784
- const errorBody = await response.text().catch(() => "");
1785
- throw providerHttpError(response.status, errorBody);
1786
- }
1787
- const reader = response.body?.getReader();
1788
- if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
1789
- const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
1790
- let eventType = "";
1791
- let dataStr = "";
1792
- for (const rawLine of frame.split("\n")) {
1793
- const line = rawLine.trim();
1794
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
1795
- else if (line.startsWith("data: ")) dataStr += line.slice(6);
1796
- }
1797
- if (!eventType) return { status: "ignored" };
1798
- try {
1799
- const data = JSON.parse(dataStr);
1800
- return {
1801
- status: "parsed",
1802
- value: {
1803
- type: eventType,
1804
- data
1805
- }
1806
- };
1807
- } catch {
1808
- return { status: "malformed" };
1809
- }
1866
+ const gate = createCompletionGate();
1867
+ const { reader } = await openProviderJsonStream({
1868
+ fetchFn: this.fetchFn,
1869
+ url: `${this.baseUrl}/responses`,
1870
+ headers: {
1871
+ "Content-Type": "application/json",
1872
+ Authorization: `Bearer ${this.apiKey}`
1873
+ },
1874
+ body: providerRequest,
1875
+ signal: request.signal
1810
1876
  });
1877
+ const parser = createSseJsonParser();
1811
1878
  const output = [];
1812
- let streamDone = false;
1813
1879
  let completedResponse;
1814
- let completedEmitted = false;
1815
1880
  let unknownEventsWarned = false;
1816
1881
  const messageItemsWithDelta = /* @__PURE__ */ new Set();
1817
- try {
1818
- while (true) {
1819
- const { done, value } = await reader.read().catch((err) => {
1820
- throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
1821
- });
1822
- const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
1823
- const malformedWarning = emitMalformedStreamWarning(factory, {
1824
- count: malformedEvents,
1825
- providerLabel: "Responses",
1826
- transportLabel: "SSE event(s)"
1827
- });
1828
- if (malformedWarning) yield malformedWarning;
1829
- for (const sseEvent of events) {
1830
- if (sseEvent.type === "error") {
1831
- const data = sseEvent.data;
1832
- yield factory.responseWarning(data.message ?? "Provider error event", data.code);
1833
- continue;
1834
- }
1835
- if (sseEvent.type === "response.output_item.added") {
1836
- const item = sseEvent.data.item;
1837
- switch (item.type) {
1838
- case "message":
1839
- yield factory.messageStarted(item.id);
1840
- break;
1841
- case "reasoning":
1842
- yield factory.reasoningStarted(item.id, "full");
1843
- break;
1844
- case "function_call":
1845
- yield factory.toolCallStarted(item.id, item.name ?? "unknown");
1846
- break;
1882
+ const toolCallNames = /* @__PURE__ */ new Map();
1883
+ for await (const batch of iterateProviderStreamBatches({
1884
+ reader,
1885
+ parser,
1886
+ factory,
1887
+ providerLabel: "Responses",
1888
+ transportLabel: "SSE event(s)",
1889
+ incompleteMessage: "Stream ended with an incomplete Responses SSE frame"
1890
+ })) {
1891
+ for (const warning of batch.warnings) yield warning;
1892
+ for (const sseEvent of batch.items) {
1893
+ if (sseEvent.type === "error") {
1894
+ const data = sseEvent.data;
1895
+ yield factory.responseWarning(data.message ?? "Provider error event", data.code);
1896
+ continue;
1897
+ }
1898
+ if (sseEvent.type === "response.output_item.added") {
1899
+ const item = sseEvent.data.item;
1900
+ switch (item.type) {
1901
+ case "message":
1902
+ yield factory.messageStarted(item.id);
1903
+ break;
1904
+ case "reasoning":
1905
+ yield factory.reasoningStarted(item.id, "full");
1906
+ break;
1907
+ case "function_call": {
1908
+ const name = typeof item.name === "string" ? item.name : "unknown";
1909
+ toolCallNames.set(item.id, name);
1910
+ yield factory.toolCallStarted(item.id, name);
1911
+ break;
1847
1912
  }
1848
- continue;
1849
- }
1850
- if (sseEvent.type === "response.output_text.delta") {
1851
- const data = sseEvent.data;
1852
- yield factory.messageDelta(data.item_id, textBlock(data.delta));
1853
- messageItemsWithDelta.add(data.item_id);
1854
- continue;
1855
- }
1856
- if (sseEvent.type === "response.output_text.done") {
1857
- const data = sseEvent.data;
1858
- if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
1859
- yield factory.messageCompleted(data.item_id);
1860
- output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
1861
- continue;
1862
1913
  }
1863
- if (sseEvent.type === "response.reasoning.delta") {
1864
- const data = sseEvent.data;
1865
- yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
1866
- continue;
1867
- }
1868
- if (sseEvent.type === "response.reasoning.done") {
1869
- const data = sseEvent.data;
1870
- yield factory.reasoningCompleted(data.item_id);
1871
- output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
1872
- continue;
1873
- }
1874
- if (sseEvent.type === "response.tool_call.delta") {
1875
- const data = sseEvent.data;
1876
- if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
1877
- continue;
1878
- }
1879
- if (sseEvent.type === "response.tool_call.done") {
1880
- const data = sseEvent.data;
1881
- const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
1882
- yield factory.toolCallCompleted(data.item_id);
1883
- output.push(tcItem);
1884
- continue;
1885
- }
1886
- if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
1887
- const data = sseEvent.data;
1888
- if (completedResponse) {
1889
- yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
1890
- continue;
1891
- }
1892
- completedResponse = data.response;
1893
- if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
1914
+ continue;
1915
+ }
1916
+ if (sseEvent.type === "response.output_text.delta") {
1917
+ const data = sseEvent.data;
1918
+ yield factory.messageDelta(data.item_id, textBlock(data.delta));
1919
+ messageItemsWithDelta.add(data.item_id);
1920
+ continue;
1921
+ }
1922
+ if (sseEvent.type === "response.output_text.done") {
1923
+ const data = sseEvent.data;
1924
+ if (!messageItemsWithDelta.has(data.item_id) && data.text) yield factory.messageDelta(data.item_id, textBlock(data.text));
1925
+ yield factory.messageCompleted(data.item_id);
1926
+ output.push(messageItem([textBlock(data.text)], { id: data.item_id }));
1927
+ continue;
1928
+ }
1929
+ if (sseEvent.type === "response.reasoning.delta") {
1930
+ const data = sseEvent.data;
1931
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
1932
+ continue;
1933
+ }
1934
+ if (sseEvent.type === "response.reasoning.done") {
1935
+ const data = sseEvent.data;
1936
+ yield factory.reasoningCompleted(data.item_id);
1937
+ output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
1938
+ continue;
1939
+ }
1940
+ if (sseEvent.type === "response.function_call_arguments.delta") {
1941
+ const data = sseEvent.data;
1942
+ if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
1943
+ continue;
1944
+ }
1945
+ if (sseEvent.type === "response.function_call_arguments.done") {
1946
+ const data = sseEvent.data;
1947
+ const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
1948
+ yield factory.toolCallCompleted(data.item_id);
1949
+ output.push(tcItem);
1950
+ continue;
1951
+ }
1952
+ if (sseEvent.type === "response.completed" || sseEvent.type === "response.failed" || sseEvent.type === "response.incomplete") {
1953
+ const data = sseEvent.data;
1954
+ if (completedResponse) {
1955
+ yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
1894
1956
  continue;
1895
1957
  }
1896
- if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
1897
- unknownEventsWarned = true;
1898
- yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
1899
- }
1958
+ completedResponse = data.response;
1959
+ if (sseEvent.type === "response.failed") yield factory.responseWarning(`Response failed: ${extractFailureMessage(data.response)}`, "PROVIDER_FAILURE");
1960
+ continue;
1900
1961
  }
1901
- if (done) {
1902
- streamDone = true;
1903
- break;
1962
+ if (!KNOWN_RESPONSES_SSE_TYPES.has(sseEvent.type) && !unknownEventsWarned) {
1963
+ unknownEventsWarned = true;
1964
+ yield factory.responseWarning(`Responses API sent unknown event type "${sseEvent.type}"; this may indicate an incomplete integration`, "UNKNOWN_PROVIDER_EVENT");
1904
1965
  }
1905
1966
  }
1906
- } finally {
1907
- try {
1908
- if (!streamDone) await reader.cancel().catch(() => void 0);
1909
- } finally {
1910
- reader.releaseLock();
1911
- }
1912
1967
  }
1913
- if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Responses SSE frame", "STREAM_ERROR");
1914
1968
  let rawResponseId;
1915
1969
  if (completedResponse) {
1916
1970
  rawResponseId = completedResponse.id;
@@ -1919,31 +1973,12 @@ var ResponsesAdapter = class extends AdapterBase {
1919
1973
  const replay = [...replayFromOutput(output)];
1920
1974
  if (completedResponse?.id) replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
1921
1975
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : void 0;
1922
- const auxiliaryResult = await auxiliary.finalize(factory);
1923
- for (const event of auxiliaryResult.events) yield event;
1924
- if (!completedEmitted) {
1925
- completedEmitted = true;
1926
- const finalResponse = this.buildResponse(request, {
1927
- output,
1928
- replay,
1929
- stopReason,
1930
- usage: auxiliaryResult.usage,
1931
- billing: auxiliaryResult.billing,
1932
- auxiliary: auxiliaryResult.auxiliary,
1933
- warnings: auxiliaryResult.warnings,
1934
- metadataSources: auxiliaryResult.metadataSources,
1935
- rawResponseId
1936
- }, factory);
1937
- yield factory.responseCompleted({
1938
- replay: finalResponse.replay,
1939
- stopReason: finalResponse.stopReason,
1940
- trace: finalResponse.backend,
1941
- usage: finalResponse.usage,
1942
- billing: finalResponse.billing,
1943
- auxiliary: finalResponse.auxiliary,
1944
- warnings: finalResponse.warnings
1945
- });
1946
- }
1976
+ if (gate.tryComplete()) yield* this.emitStreamCompleted(factory, request, auxiliary, {
1977
+ output,
1978
+ replay,
1979
+ stopReason,
1980
+ rawResponseId
1981
+ });
1947
1982
  }
1948
1983
  inferStopReason(response) {
1949
1984
  if (response.status === "failed") return "error";
@@ -1975,21 +2010,7 @@ var ResponsesAdapter = class extends AdapterBase {
1975
2010
  * - 高保真 replay(含 opaque continuation)
1976
2011
  * - 能力降级 warning
1977
2012
  */
1978
- const profile$2 = {
1979
- kind: "messages",
1980
- instructionsMode: "system_message",
1981
- supportedBlockTypes: ["text", "json"],
1982
- reasoningBlockTypes: ["text"],
1983
- capabilities: {
1984
- textStreaming: "native",
1985
- reasoningStreaming: "native",
1986
- toolCallStreaming: "synthetic",
1987
- replay: "opaque",
1988
- usage: "stream",
1989
- toolResultOutcomes: ["success", "error"]
1990
- }
1991
- };
1992
- const mapper$2 = new NormalizedRequestMapper(profile$2);
2013
+ const mapper$2 = new NormalizedRequestMapper("messages");
1993
2014
  function isMessagesReplayContentBlock(value) {
1994
2015
  if (!value || typeof value !== "object" || !("type" in value)) return false;
1995
2016
  const block = value;
@@ -2015,10 +2036,10 @@ function assertMessagesReplayContent(content) {
2015
2036
  function synthesizeItemId(kind, blockIndex, responseId) {
2016
2037
  return `${kind}-${blockIndex}-${responseId}`;
2017
2038
  }
2018
- function parseToolUseInput(input) {
2039
+ function parseProviderToolUseInput(input) {
2019
2040
  try {
2020
2041
  const parsed = JSON.parse(input);
2021
- return parsed && typeof parsed === "object" ? parsed : {};
2042
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2022
2043
  } catch {
2023
2044
  return {};
2024
2045
  }
@@ -2059,7 +2080,7 @@ function buildStreamMetadata(options) {
2059
2080
  }
2060
2081
  var MessagesAdapter = class extends AdapterBase {
2061
2082
  kind = "messages";
2062
- capabilities = profile$2.capabilities;
2083
+ isSyntheticStream = false;
2063
2084
  apiKey;
2064
2085
  apiVersion;
2065
2086
  baseUrl;
@@ -2098,7 +2119,7 @@ var MessagesAdapter = class extends AdapterBase {
2098
2119
  type: "tool_use",
2099
2120
  id: item.id,
2100
2121
  name: item.name,
2101
- input: item.argumentsJson ?? parseToolUseInput(item.argumentsText)
2122
+ input: mapper$2.parseToolArguments(item)
2102
2123
  };
2103
2124
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
2104
2125
  else messages.push({
@@ -2108,13 +2129,12 @@ var MessagesAdapter = class extends AdapterBase {
2108
2129
  break;
2109
2130
  }
2110
2131
  case "tool_result": {
2111
- mapper$2.assertToolResultOutcome(item.outcome);
2112
- const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
2132
+ const content = mapper$2.textFromBlocks(item.content, `tool_result ${item.callId} content`);
2113
2133
  const block = {
2114
2134
  type: "tool_result",
2115
2135
  tool_use_id: item.callId,
2116
2136
  content,
2117
- is_error: item.outcome === "error"
2137
+ is_error: item.outcome !== "success"
2118
2138
  };
2119
2139
  if (pendingToolResultMessage && typeof pendingToolResultMessage.content !== "string") pendingToolResultMessage.content.push(block);
2120
2140
  else {
@@ -2140,7 +2160,7 @@ var MessagesAdapter = class extends AdapterBase {
2140
2160
  break;
2141
2161
  }
2142
2162
  case "opaque": {
2143
- if (item.purpose !== "replay") break;
2163
+ if (item.source !== "messages" || item.purpose !== "replay") break;
2144
2164
  assertOpaqueReplayEnvelope(item.payload);
2145
2165
  const payload = item.payload;
2146
2166
  if (payload.role === "assistant" && "content" in payload) {
@@ -2162,71 +2182,39 @@ var MessagesAdapter = class extends AdapterBase {
2162
2182
  stream: true
2163
2183
  };
2164
2184
  if (systemPrompt) body.system = systemPrompt;
2165
- if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2185
+ body.tools = mapper$2.mapToolsIfPresent(request.tools, (t) => ({
2166
2186
  name: t.name,
2167
2187
  description: t.description,
2168
2188
  input_schema: t.inputSchema
2169
2189
  }));
2170
- if (request.toolChoice) {
2171
- if (request.toolChoice === "auto") body.tool_choice = { type: "auto" };
2172
- else if (request.toolChoice === "none") body.tool_choice = { type: "none" };
2173
- else if (request.toolChoice.type === "tool") body.tool_choice = {
2190
+ body.tool_choice = mapper$2.mapToolChoice(request.toolChoice, {
2191
+ auto: { type: "auto" },
2192
+ none: { type: "none" },
2193
+ tool: (name) => ({
2174
2194
  type: "tool",
2175
- name: request.toolChoice.name
2176
- };
2177
- }
2195
+ name
2196
+ })
2197
+ });
2178
2198
  if (request.temperature !== void 0) body.temperature = request.temperature;
2179
2199
  return body;
2180
2200
  }
2181
2201
  async *runStream(providerRequest, factory, request) {
2182
2202
  const auxiliary = this.createAuxiliaryState(request);
2183
- let completedEmitted = false;
2203
+ const gate = createCompletionGate();
2184
2204
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Messages adapter", "UNSUPPORTED_METADATA");
2185
- let response;
2186
- try {
2187
- response = await this.fetchFn(`${this.baseUrl}/messages`, {
2188
- method: "POST",
2189
- headers: {
2190
- "Content-Type": "application/json",
2191
- "x-api-key": this.apiKey,
2192
- "anthropic-version": this.apiVersion
2193
- },
2194
- body: JSON.stringify(providerRequest),
2195
- signal: request.signal
2196
- });
2197
- } catch (err) {
2198
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2199
- }
2200
- if (!response.ok) {
2201
- const errorBody = await response.text().catch(() => "");
2202
- throw providerHttpError(response.status, errorBody);
2203
- }
2204
- const reader = response.body?.getReader();
2205
- if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2206
- const parser = new IncrementalStreamParser(splitSSEFrames, (frame) => {
2207
- let eventType = "";
2208
- let dataStr = "";
2209
- for (const rawLine of frame.split("\n")) {
2210
- const line = rawLine.trim();
2211
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
2212
- else if (line.startsWith("data: ")) dataStr += line.slice(6);
2213
- }
2214
- if (!eventType) return { status: "ignored" };
2215
- try {
2216
- const data = JSON.parse(dataStr);
2217
- return {
2218
- status: "parsed",
2219
- value: {
2220
- type: eventType,
2221
- data
2222
- }
2223
- };
2224
- } catch {
2225
- return { status: "malformed" };
2226
- }
2205
+ const { reader, headers } = await openProviderJsonStream({
2206
+ fetchFn: this.fetchFn,
2207
+ url: `${this.baseUrl}/messages`,
2208
+ headers: {
2209
+ "Content-Type": "application/json",
2210
+ "x-api-key": this.apiKey,
2211
+ "anthropic-version": this.apiVersion
2212
+ },
2213
+ body: providerRequest,
2214
+ signal: request.signal
2227
2215
  });
2216
+ const parser = createSseJsonParser();
2228
2217
  const output = [];
2229
- let streamDone = false;
2230
2218
  let messageResponse;
2231
2219
  let currentContentBlockIndex = -1;
2232
2220
  let currentItemType = null;
@@ -2234,7 +2222,6 @@ var MessagesAdapter = class extends AdapterBase {
2234
2222
  let currentToolName = "";
2235
2223
  let currentArgsText = "";
2236
2224
  let currentThinkingVisibility = "full";
2237
- let hasStreamedReasoning = false;
2238
2225
  const rawReplayContent = [];
2239
2226
  let textBuffer = "";
2240
2227
  let thinkingBuffer = "";
@@ -2243,160 +2230,142 @@ var MessagesAdapter = class extends AdapterBase {
2243
2230
  let stopSequence;
2244
2231
  let rawResponseId = "";
2245
2232
  if (request.include?.providerMetadata !== "off") {
2246
- const headerMetadata = pickProviderHeaders(response.headers);
2233
+ const headerMetadata = pickProviderHeaders(headers);
2247
2234
  auxiliary.recordProviderMetadata("header", Object.keys(headerMetadata).length > 0 ? { headers: headerMetadata } : void 0);
2248
2235
  }
2249
- try {
2250
- while (true) {
2251
- const { done, value } = await reader.read().catch((err) => {
2252
- throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2253
- });
2254
- const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
2255
- const malformedWarning = emitMalformedStreamWarning(factory, {
2256
- count: malformedEvents,
2257
- providerLabel: "Messages",
2258
- transportLabel: "SSE event(s)"
2259
- });
2260
- if (malformedWarning) yield malformedWarning;
2261
- for (const sseEvent of events) switch (sseEvent.type) {
2262
- case "ping": continue;
2263
- case "error": {
2264
- const err = sseEvent.data.error;
2265
- yield factory.responseWarning(err.message, err.type);
2266
- continue;
2267
- }
2268
- case "message_start":
2269
- messageResponse = sseEvent.data.message;
2270
- rawResponseId = messageResponse.id;
2271
- if (messageResponse.content.some((b) => b.type === "thinking" || b.type === "redacted_thinking")) hasStreamedReasoning = true;
2272
- continue;
2273
- case "content_block_start": {
2274
- const block = sseEvent.data.content_block;
2275
- currentContentBlockIndex = sseEvent.data.index;
2276
- switch (block.type) {
2277
- case "text":
2278
- currentItemType = "message";
2279
- currentItemId = synthesizeItemId("msg", currentContentBlockIndex, rawResponseId);
2280
- textBuffer = "";
2281
- yield factory.messageStarted(currentItemId);
2282
- break;
2283
- case "thinking":
2284
- hasStreamedReasoning = true;
2285
- currentItemType = "reasoning";
2286
- currentItemId = synthesizeItemId("reason", currentContentBlockIndex, rawResponseId);
2287
- currentThinkingVisibility = "full";
2288
- thinkingBuffer = "";
2289
- yield factory.reasoningStarted(currentItemId, "full");
2290
- break;
2291
- case "redacted_thinking": {
2292
- hasStreamedReasoning = true;
2293
- currentItemType = "reasoning";
2294
- currentItemId = synthesizeItemId("reason-redacted", currentContentBlockIndex, rawResponseId);
2295
- currentThinkingVisibility = "redacted";
2296
- const data = block.data;
2297
- yield factory.reasoningStarted(currentItemId, "redacted");
2298
- yield factory.reasoningDelta(currentItemId, textBlock(data));
2299
- const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
2300
- yield factory.reasoningCompleted(currentItemId);
2301
- output.push(redactedItem);
2302
- rawReplayContent.push({
2303
- type: "redacted_thinking",
2304
- data
2305
- });
2306
- currentItemType = null;
2307
- break;
2308
- }
2309
- case "tool_use": {
2310
- const tuBlock = block;
2311
- currentItemType = "tool_call";
2312
- currentItemId = tuBlock.id;
2313
- currentToolName = tuBlock.name;
2314
- currentArgsText = "";
2315
- argsBuffer = "";
2316
- yield factory.toolCallStarted(currentItemId, currentToolName);
2317
- break;
2318
- }
2319
- }
2320
- continue;
2321
- }
2322
- case "content_block_delta": {
2323
- const delta = sseEvent.data.delta;
2324
- switch (delta.type) {
2325
- case "text_delta":
2326
- if (currentItemType === "message" && currentItemId) {
2327
- const txt = delta.text;
2328
- textBuffer += txt;
2329
- yield factory.messageDelta(currentItemId, textBlock(txt));
2330
- }
2331
- break;
2332
- case "thinking_delta":
2333
- if (currentItemType === "reasoning" && currentItemId) {
2334
- const txt = delta.thinking;
2335
- thinkingBuffer += txt;
2336
- yield factory.reasoningDelta(currentItemId, textBlock(txt));
2337
- }
2338
- break;
2339
- case "input_json_delta":
2340
- if (currentItemType === "tool_call" && currentItemId) {
2341
- const partial = delta.partial_json;
2342
- argsBuffer += partial;
2343
- yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
2344
- }
2345
- break;
2346
- }
2347
- continue;
2348
- }
2349
- case "content_block_stop":
2350
- if (currentItemType === "message" && currentItemId) {
2351
- yield factory.messageCompleted(currentItemId);
2352
- output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
2353
- rawReplayContent.push({
2354
- type: "text",
2355
- text: textBuffer
2356
- });
2357
- } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
2236
+ for await (const batch of iterateProviderStreamBatches({
2237
+ reader,
2238
+ parser,
2239
+ factory,
2240
+ providerLabel: "Messages",
2241
+ transportLabel: "SSE event(s)",
2242
+ incompleteMessage: "Stream ended with an incomplete Messages SSE frame"
2243
+ })) {
2244
+ for (const warning of batch.warnings) yield warning;
2245
+ for (const sseEvent of batch.items) switch (sseEvent.type) {
2246
+ case "ping": continue;
2247
+ case "error": {
2248
+ const err = sseEvent.data.error;
2249
+ yield factory.responseWarning(err.message, err.type);
2250
+ continue;
2251
+ }
2252
+ case "message_start":
2253
+ messageResponse = sseEvent.data.message;
2254
+ rawResponseId = messageResponse.id;
2255
+ continue;
2256
+ case "content_block_start": {
2257
+ const block = sseEvent.data.content_block;
2258
+ currentContentBlockIndex = sseEvent.data.index;
2259
+ switch (block.type) {
2260
+ case "text":
2261
+ currentItemType = "message";
2262
+ currentItemId = synthesizeItemId("msg", currentContentBlockIndex, rawResponseId);
2263
+ textBuffer = "";
2264
+ yield factory.messageStarted(currentItemId);
2265
+ break;
2266
+ case "thinking":
2267
+ currentItemType = "reasoning";
2268
+ currentItemId = synthesizeItemId("reason", currentContentBlockIndex, rawResponseId);
2269
+ currentThinkingVisibility = "full";
2270
+ thinkingBuffer = "";
2271
+ yield factory.reasoningStarted(currentItemId, "full");
2272
+ break;
2273
+ case "redacted_thinking": {
2274
+ currentItemType = "reasoning";
2275
+ currentItemId = synthesizeItemId("reason-redacted", currentContentBlockIndex, rawResponseId);
2276
+ currentThinkingVisibility = "redacted";
2277
+ const data = block.data;
2278
+ yield factory.reasoningStarted(currentItemId, "redacted");
2279
+ yield factory.reasoningDelta(currentItemId, textBlock(data));
2280
+ const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
2358
2281
  yield factory.reasoningCompleted(currentItemId);
2359
- output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
2360
- rawReplayContent.push({
2361
- type: "thinking",
2362
- thinking: thinkingBuffer
2363
- });
2364
- } else if (currentItemType === "tool_call" && currentItemId) {
2365
- const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
2366
- yield factory.toolCallCompleted(currentItemId);
2367
- output.push(tcItem);
2282
+ output.push(redactedItem);
2368
2283
  rawReplayContent.push({
2369
- type: "tool_use",
2370
- id: currentItemId,
2371
- name: currentToolName,
2372
- input: parseToolUseInput(currentArgsText || argsBuffer)
2284
+ type: "redacted_thinking",
2285
+ data
2373
2286
  });
2287
+ currentItemType = null;
2288
+ break;
2289
+ }
2290
+ case "tool_use": {
2291
+ const tuBlock = block;
2292
+ currentItemType = "tool_call";
2293
+ currentItemId = tuBlock.id;
2294
+ currentToolName = tuBlock.name;
2295
+ currentArgsText = "";
2296
+ argsBuffer = "";
2297
+ yield factory.toolCallStarted(currentItemId, currentToolName);
2298
+ break;
2374
2299
  }
2375
- currentItemType = null;
2376
- currentItemId = "";
2377
- continue;
2378
- case "message_delta": {
2379
- stopReason = sseEvent.data.delta.stop_reason;
2380
- stopSequence = sseEvent.data.delta.stop_sequence;
2381
- const u = sseEvent.data.usage;
2382
- if (u) auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
2383
- continue;
2384
2300
  }
2385
- case "message_stop": break;
2301
+ continue;
2386
2302
  }
2387
- if (done) {
2388
- streamDone = true;
2389
- break;
2303
+ case "content_block_delta": {
2304
+ const delta = sseEvent.data.delta;
2305
+ switch (delta.type) {
2306
+ case "text_delta":
2307
+ if (currentItemType === "message" && currentItemId) {
2308
+ const txt = delta.text;
2309
+ textBuffer += txt;
2310
+ yield factory.messageDelta(currentItemId, textBlock(txt));
2311
+ }
2312
+ break;
2313
+ case "thinking_delta":
2314
+ if (currentItemType === "reasoning" && currentItemId) {
2315
+ const txt = delta.thinking;
2316
+ thinkingBuffer += txt;
2317
+ yield factory.reasoningDelta(currentItemId, textBlock(txt));
2318
+ }
2319
+ break;
2320
+ case "input_json_delta":
2321
+ if (currentItemType === "tool_call" && currentItemId) {
2322
+ const partial = delta.partial_json;
2323
+ argsBuffer += partial;
2324
+ yield factory.toolCallDelta(currentItemId, { argumentsText: partial });
2325
+ }
2326
+ break;
2327
+ }
2328
+ continue;
2390
2329
  }
2391
- }
2392
- } finally {
2393
- try {
2394
- if (!streamDone) await reader.cancel().catch(() => void 0);
2395
- } finally {
2396
- reader.releaseLock();
2330
+ case "content_block_stop":
2331
+ if (currentItemType === "message" && currentItemId) {
2332
+ yield factory.messageCompleted(currentItemId);
2333
+ output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
2334
+ rawReplayContent.push({
2335
+ type: "text",
2336
+ text: textBuffer
2337
+ });
2338
+ } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
2339
+ yield factory.reasoningCompleted(currentItemId);
2340
+ output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
2341
+ rawReplayContent.push({
2342
+ type: "thinking",
2343
+ thinking: thinkingBuffer
2344
+ });
2345
+ } else if (currentItemType === "tool_call" && currentItemId) {
2346
+ const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
2347
+ yield factory.toolCallCompleted(currentItemId);
2348
+ output.push(tcItem);
2349
+ rawReplayContent.push({
2350
+ type: "tool_use",
2351
+ id: currentItemId,
2352
+ name: currentToolName,
2353
+ input: parseProviderToolUseInput(currentArgsText || argsBuffer)
2354
+ });
2355
+ }
2356
+ currentItemType = null;
2357
+ currentItemId = "";
2358
+ continue;
2359
+ case "message_delta": {
2360
+ stopReason = sseEvent.data.delta.stop_reason;
2361
+ stopSequence = sseEvent.data.delta.stop_sequence;
2362
+ const u = sseEvent.data.usage;
2363
+ if (u) auxiliary.recordUsage(usageFromAnthropicMessages(u), "stream", u);
2364
+ continue;
2365
+ }
2366
+ case "message_stop": break;
2397
2367
  }
2398
2368
  }
2399
- if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
2400
2369
  const replay = [...replayFromOutput(output)];
2401
2370
  if (messageResponse) {
2402
2371
  const replayContent = rawReplayContent.length > 0 ? rawReplayContent : messageResponse.content;
@@ -2414,32 +2383,12 @@ var MessagesAdapter = class extends AdapterBase {
2414
2383
  stopReason,
2415
2384
  stopSequence
2416
2385
  }));
2417
- if (!hasStreamedReasoning) {}
2418
- const auxiliaryResult = await auxiliary.finalize(factory);
2419
- for (const event of auxiliaryResult.events) yield event;
2420
- if (!completedEmitted) {
2421
- completedEmitted = true;
2422
- const finalResponse = this.buildResponse(request, {
2423
- output,
2424
- replay,
2425
- stopReason: stopReason ? mapStopReason(stopReason) : void 0,
2426
- usage: auxiliaryResult.usage,
2427
- billing: auxiliaryResult.billing,
2428
- auxiliary: auxiliaryResult.auxiliary,
2429
- warnings: auxiliaryResult.warnings,
2430
- metadataSources: auxiliaryResult.metadataSources,
2431
- rawResponseId
2432
- }, factory);
2433
- yield factory.responseCompleted({
2434
- replay: finalResponse.replay,
2435
- stopReason: finalResponse.stopReason,
2436
- trace: finalResponse.backend,
2437
- usage: finalResponse.usage,
2438
- billing: finalResponse.billing,
2439
- auxiliary: finalResponse.auxiliary,
2440
- warnings: finalResponse.warnings
2441
- });
2442
- }
2386
+ if (gate.tryComplete()) yield* this.emitStreamCompleted(factory, request, auxiliary, {
2387
+ output,
2388
+ replay,
2389
+ stopReason: stopReason ? mapStopReason(stopReason) : void 0,
2390
+ rawResponseId
2391
+ });
2443
2392
  }
2444
2393
  };
2445
2394
  //#endregion
@@ -2454,21 +2403,7 @@ var MessagesAdapter = class extends AdapterBase {
2454
2403
  * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
2455
2404
  */
2456
2405
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
2457
- const profile$1 = {
2458
- kind: "chat-completions",
2459
- instructionsMode: "system_message",
2460
- supportedBlockTypes: ["text", "json"],
2461
- reasoningBlockTypes: ["text"],
2462
- capabilities: {
2463
- textStreaming: "native",
2464
- reasoningStreaming: "native",
2465
- toolCallStreaming: "native",
2466
- replay: "opaque",
2467
- usage: "final",
2468
- toolResultOutcomes: ["success"]
2469
- }
2470
- };
2471
- const mapper$1 = new NormalizedRequestMapper(profile$1);
2406
+ const mapper$1 = new NormalizedRequestMapper("chat-completions");
2472
2407
  function extractReasoningText(value) {
2473
2408
  if (typeof value === "string") return value;
2474
2409
  if (Array.isArray(value)) return value.map(extractReasoningText).join("");
@@ -2545,7 +2480,7 @@ function buildAssistantReplayMessage(params) {
2545
2480
  }
2546
2481
  var ChatCompletionsAdapter = class extends AdapterBase {
2547
2482
  kind = "chat-completions";
2548
- capabilities = profile$1.capabilities;
2483
+ isSyntheticStream = false;
2549
2484
  apiKey;
2550
2485
  baseUrl;
2551
2486
  fetchFn;
@@ -2564,7 +2499,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2564
2499
  for (const item of request.input) switch (item.type) {
2565
2500
  case "message": {
2566
2501
  const role = item.role;
2567
- const text = contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `input message (${item.role}) content`));
2502
+ const text = mapper$1.textFromBlocks(item.content, `input message (${item.role}) content`);
2568
2503
  messages.push({
2569
2504
  role,
2570
2505
  content: text || null
@@ -2590,22 +2525,21 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2590
2525
  break;
2591
2526
  }
2592
2527
  case "tool_result":
2593
- mapper$1.assertToolResultOutcome(item.outcome);
2594
2528
  messages.push({
2595
2529
  role: "tool",
2596
2530
  tool_call_id: item.callId,
2597
2531
  name: item.toolName,
2598
- content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2532
+ content: mapper$1.textFromBlocks(item.content, `tool_result ${item.callId} content`)
2599
2533
  });
2600
2534
  break;
2601
2535
  case "reasoning":
2602
2536
  messages.push({
2603
2537
  role: "assistant",
2604
- content: contentBlocksToText(mapper$1.ensureTextBlocks(item.content, "reasoning content"))
2538
+ content: mapper$1.textFromBlocks(item.content, "reasoning content")
2605
2539
  });
2606
2540
  break;
2607
2541
  case "opaque": {
2608
- if (item.purpose !== "replay") break;
2542
+ if (item.source !== "chat.completions" || item.purpose !== "replay") break;
2609
2543
  assertOpaqueReplayEnvelope(item.payload);
2610
2544
  const payload = item.payload;
2611
2545
  if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
@@ -2629,7 +2563,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2629
2563
  stream: true,
2630
2564
  n: 1
2631
2565
  };
2632
- if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2566
+ body.tools = mapper$1.mapToolsIfPresent(request.tools, (t) => ({
2633
2567
  type: "function",
2634
2568
  function: {
2635
2569
  name: t.name,
@@ -2637,14 +2571,14 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2637
2571
  parameters: t.inputSchema
2638
2572
  }
2639
2573
  }));
2640
- if (request.toolChoice) {
2641
- if (request.toolChoice === "auto") body.tool_choice = "auto";
2642
- else if (request.toolChoice === "none") body.tool_choice = "none";
2643
- else if (request.toolChoice.type === "tool") body.tool_choice = {
2574
+ body.tool_choice = mapper$1.mapToolChoice(request.toolChoice, {
2575
+ auto: "auto",
2576
+ none: "none",
2577
+ tool: (name) => ({
2644
2578
  type: "function",
2645
- function: { name: request.toolChoice.name }
2646
- };
2647
- }
2579
+ function: { name }
2580
+ })
2581
+ });
2648
2582
  if (request.temperature !== void 0) body.temperature = request.temperature;
2649
2583
  if (request.maxOutputTokens !== void 0) body.max_tokens = request.maxOutputTokens;
2650
2584
  if (request.metadata) body.metadata = request.metadata;
@@ -2652,42 +2586,19 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2652
2586
  }
2653
2587
  async *runStream(providerRequest, factory, request) {
2654
2588
  const auxiliary = this.createAuxiliaryState(request);
2655
- let response;
2656
- try {
2657
- response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
2658
- method: "POST",
2659
- headers: {
2660
- "Content-Type": "application/json",
2661
- Authorization: `Bearer ${this.apiKey}`
2662
- },
2663
- body: JSON.stringify(providerRequest),
2664
- signal: request.signal
2665
- });
2666
- } catch (err) {
2667
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
2668
- }
2669
- if (!response.ok) {
2670
- const errorBody = await response.text().catch(() => "");
2671
- throw providerHttpError(response.status, errorBody);
2672
- }
2673
- const reader = response.body?.getReader();
2674
- if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
2675
- const parser = new IncrementalStreamParser(splitLines, (item) => {
2676
- const trimmed = item.trim();
2677
- if (!trimmed.startsWith("data: ")) return { status: "ignored" };
2678
- const data = trimmed.slice(6).trim();
2679
- if (data === "[DONE]") return { status: "ignored" };
2680
- try {
2681
- return {
2682
- status: "parsed",
2683
- value: JSON.parse(data)
2684
- };
2685
- } catch {
2686
- return { status: "malformed" };
2687
- }
2589
+ const gate = createCompletionGate();
2590
+ const { reader } = await openProviderJsonStream({
2591
+ fetchFn: this.fetchFn,
2592
+ url: `${this.baseUrl}/chat/completions`,
2593
+ headers: {
2594
+ "Content-Type": "application/json",
2595
+ Authorization: `Bearer ${this.apiKey}`
2596
+ },
2597
+ body: providerRequest,
2598
+ signal: request.signal
2688
2599
  });
2600
+ const parser = createChatCompletionsSseParser();
2689
2601
  const output = [];
2690
- let streamDone = false;
2691
2602
  let responseId;
2692
2603
  let accumulatedContent = "";
2693
2604
  let accumulatedReasoning = "";
@@ -2695,9 +2606,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2695
2606
  let currentReasoningId = "";
2696
2607
  let hasMessageStarted = false;
2697
2608
  let hasReasoningStarted = false;
2698
- let completedEmitted = false;
2699
2609
  let warnedNonZeroChoice = false;
2700
- const buildResponse = this.buildResponse.bind(this);
2701
2610
  const pendingToolCalls = /* @__PURE__ */ new Map();
2702
2611
  const reasoningByField = /* @__PURE__ */ new Map();
2703
2612
  const finalizePendingTurn = () => {
@@ -2738,163 +2647,131 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2738
2647
  };
2739
2648
  };
2740
2649
  const emitCompleted = async function* (stopReason, assistantReplayMessage, rawResponseId) {
2741
- if (completedEmitted) {
2650
+ if (!gate.tryComplete()) {
2742
2651
  yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2743
2652
  return;
2744
2653
  }
2745
- completedEmitted = true;
2746
2654
  const replay = [...replayFromOutput(output)];
2747
2655
  if (assistantReplayMessage) replay.push(opaqueItem("chat.completions", "replay", {
2748
2656
  replaceCanonical: true,
2749
2657
  messages: [assistantReplayMessage]
2750
2658
  }));
2751
- const auxiliaryResult = await auxiliary.finalize(factory);
2752
- for (const event of auxiliaryResult.events) yield event;
2753
- const finalResponse = buildResponse(request, {
2659
+ yield* this.emitStreamCompleted(factory, request, auxiliary, {
2754
2660
  output,
2755
2661
  replay,
2756
2662
  stopReason,
2757
- usage: auxiliaryResult.usage,
2758
- billing: auxiliaryResult.billing,
2759
- auxiliary: auxiliaryResult.auxiliary,
2760
- warnings: auxiliaryResult.warnings,
2761
- metadataSources: auxiliaryResult.metadataSources,
2762
2663
  rawResponseId
2763
- }, factory);
2764
- yield factory.responseCompleted({
2765
- replay: finalResponse.replay,
2766
- stopReason: finalResponse.stopReason,
2767
- trace: finalResponse.backend,
2768
- usage: finalResponse.usage,
2769
- billing: finalResponse.billing,
2770
- auxiliary: finalResponse.auxiliary,
2771
- warnings: finalResponse.warnings
2772
2664
  });
2773
- };
2774
- try {
2775
- while (true) {
2776
- const { done, value } = await reader.read().catch((err) => {
2777
- throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
2778
- });
2779
- const { items: chunks, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value);
2780
- const malformedWarning = emitMalformedStreamWarning(factory, {
2781
- count: malformedEvents,
2782
- providerLabel: "Chat Completions",
2783
- transportLabel: "SSE event(s)"
2784
- });
2785
- if (malformedWarning) yield malformedWarning;
2786
- for (const chunk of chunks) {
2787
- responseId = chunk.id;
2788
- if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
2789
- for (const choice of chunk.choices) {
2790
- if (choice.index !== 0) {
2791
- if (!warnedNonZeroChoice) {
2792
- yield factory.responseWarning(`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`, "MULTIPLE_CHOICES_IGNORED");
2793
- warnedNonZeroChoice = true;
2794
- }
2795
- continue;
2665
+ }.bind(this);
2666
+ for await (const batch of iterateProviderStreamBatches({
2667
+ reader,
2668
+ parser,
2669
+ factory,
2670
+ providerLabel: "Chat Completions",
2671
+ transportLabel: "SSE event(s)",
2672
+ incompleteMessage: "Stream ended with an incomplete Chat Completions SSE frame"
2673
+ })) {
2674
+ for (const warning of batch.warnings) yield warning;
2675
+ for (const chunk of batch.items) {
2676
+ responseId = chunk.id;
2677
+ if (chunk.usage) auxiliary.recordUsage(usageFromChatCompletions(chunk.usage), "final", chunk.usage);
2678
+ for (const choice of chunk.choices) {
2679
+ if (choice.index !== 0) {
2680
+ if (!warnedNonZeroChoice) {
2681
+ yield factory.responseWarning(`Chat Completions returned choice index ${choice.index}; only the first choice (index 0) is supported. This choice is ignored.`, "MULTIPLE_CHOICES_IGNORED");
2682
+ warnedNonZeroChoice = true;
2796
2683
  }
2797
- if (completedEmitted) {
2798
- if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2799
- continue;
2684
+ continue;
2685
+ }
2686
+ if (gate.completed) {
2687
+ if (choice.finish_reason) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2688
+ continue;
2689
+ }
2690
+ const delta = choice.delta;
2691
+ const finishReason = choice.finish_reason;
2692
+ const reasoningDeltas = extractReasoningDeltas(delta);
2693
+ const ensureMessageStarted = () => {
2694
+ if (hasMessageStarted) return;
2695
+ currentMessageId = `msg-${chunk.id}`;
2696
+ hasMessageStarted = true;
2697
+ accumulatedContent = "";
2698
+ };
2699
+ if (reasoningDeltas.length > 0) {
2700
+ if (!hasReasoningStarted) {
2701
+ currentReasoningId = `reason-${chunk.id}`;
2702
+ hasReasoningStarted = true;
2703
+ accumulatedReasoning = "";
2704
+ yield factory.reasoningStarted(currentReasoningId, "full");
2800
2705
  }
2801
- const delta = choice.delta;
2802
- const finishReason = choice.finish_reason;
2803
- const reasoningDeltas = extractReasoningDeltas(delta);
2804
- const ensureMessageStarted = () => {
2805
- if (hasMessageStarted) return;
2806
- currentMessageId = `msg-${chunk.id}`;
2807
- hasMessageStarted = true;
2808
- accumulatedContent = "";
2809
- };
2810
- if (reasoningDeltas.length > 0) {
2811
- if (!hasReasoningStarted) {
2812
- currentReasoningId = `reason-${chunk.id}`;
2813
- hasReasoningStarted = true;
2814
- accumulatedReasoning = "";
2815
- yield factory.reasoningStarted(currentReasoningId, "full");
2816
- }
2817
- for (const reasoningDelta of reasoningDeltas) {
2818
- accumulatedReasoning += reasoningDelta.text;
2819
- reasoningByField.set(reasoningDelta.field, (reasoningByField.get(reasoningDelta.field) ?? "") + reasoningDelta.text);
2820
- yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
2821
- }
2706
+ for (const reasoningDelta of reasoningDeltas) {
2707
+ accumulatedReasoning += reasoningDelta.text;
2708
+ reasoningByField.set(reasoningDelta.field, (reasoningByField.get(reasoningDelta.field) ?? "") + reasoningDelta.text);
2709
+ yield factory.reasoningDelta(currentReasoningId, textBlock(reasoningDelta.text));
2822
2710
  }
2823
- if (delta.content) {
2824
- if (!hasMessageStarted) {
2825
- ensureMessageStarted();
2826
- yield factory.messageStarted(currentMessageId);
2827
- }
2828
- accumulatedContent += delta.content;
2829
- yield factory.messageDelta(currentMessageId, textBlock(delta.content));
2711
+ }
2712
+ if (delta.content) {
2713
+ if (!hasMessageStarted) {
2714
+ ensureMessageStarted();
2715
+ yield factory.messageStarted(currentMessageId);
2830
2716
  }
2831
- if (delta.tool_calls) {
2832
- if (!hasMessageStarted) {
2833
- ensureMessageStarted();
2834
- yield factory.messageStarted(currentMessageId);
2835
- }
2836
- for (const tc of delta.tool_calls) {
2837
- const idx = tc.index;
2838
- if (tc.id) {
2839
- pendingToolCalls.set(idx, {
2840
- id: tc.id,
2841
- name: tc.function?.name ?? "",
2842
- args: ""
2843
- });
2844
- yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2845
- }
2846
- if (tc.function?.arguments) {
2847
- const pending = pendingToolCalls.get(idx);
2848
- if (pending) {
2849
- pending.args += tc.function.arguments;
2850
- yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2851
- }
2852
- }
2853
- }
2717
+ accumulatedContent += delta.content;
2718
+ yield factory.messageDelta(currentMessageId, textBlock(delta.content));
2719
+ }
2720
+ if (delta.tool_calls) {
2721
+ if (!hasMessageStarted) {
2722
+ ensureMessageStarted();
2723
+ yield factory.messageStarted(currentMessageId);
2854
2724
  }
2855
- if (delta.function_call) {
2856
- if (!hasMessageStarted) {
2857
- ensureMessageStarted();
2858
- yield factory.messageStarted(currentMessageId);
2859
- }
2860
- if (delta.function_call.name) {
2861
- const fcId = `fc-${chunk.id}-0`;
2862
- pendingToolCalls.set(0, {
2863
- id: fcId,
2864
- name: delta.function_call.name,
2725
+ for (const tc of delta.tool_calls) {
2726
+ const idx = tc.index;
2727
+ if (tc.id) {
2728
+ pendingToolCalls.set(idx, {
2729
+ id: tc.id,
2730
+ name: tc.function?.name ?? "",
2865
2731
  args: ""
2866
2732
  });
2867
- yield factory.toolCallStarted(fcId, delta.function_call.name);
2733
+ yield factory.toolCallStarted(tc.id, tc.function?.name ?? "");
2868
2734
  }
2869
- if (delta.function_call.arguments) {
2870
- const pending = pendingToolCalls.get(0);
2735
+ if (tc.function?.arguments) {
2736
+ const pending = pendingToolCalls.get(idx);
2871
2737
  if (pending) {
2872
- pending.args += delta.function_call.arguments;
2873
- yield factory.toolCallDelta(pending.id, { argumentsText: delta.function_call.arguments });
2738
+ pending.args += tc.function.arguments;
2739
+ yield factory.toolCallDelta(pending.id, { argumentsText: tc.function.arguments });
2874
2740
  }
2875
2741
  }
2876
2742
  }
2877
- if (finishReason && finishReason !== null) {
2878
- const { events, assistantReplayMessage } = finalizePendingTurn();
2879
- for (const event of events) yield event;
2880
- yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
2743
+ }
2744
+ if (delta.function_call) {
2745
+ if (!hasMessageStarted) {
2746
+ ensureMessageStarted();
2747
+ yield factory.messageStarted(currentMessageId);
2748
+ }
2749
+ if (delta.function_call.name) {
2750
+ const fcId = `fc-${chunk.id}-0`;
2751
+ pendingToolCalls.set(0, {
2752
+ id: fcId,
2753
+ name: delta.function_call.name,
2754
+ args: ""
2755
+ });
2756
+ yield factory.toolCallStarted(fcId, delta.function_call.name);
2757
+ }
2758
+ if (delta.function_call.arguments) {
2759
+ const pending = pendingToolCalls.get(0);
2760
+ if (pending) {
2761
+ pending.args += delta.function_call.arguments;
2762
+ yield factory.toolCallDelta(pending.id, { argumentsText: delta.function_call.arguments });
2763
+ }
2881
2764
  }
2882
2765
  }
2766
+ if (finishReason && finishReason !== null) {
2767
+ const { events, assistantReplayMessage } = finalizePendingTurn();
2768
+ for (const event of events) yield event;
2769
+ yield* emitCompleted(mapStopReason(finishReason), assistantReplayMessage, chunk.id);
2770
+ }
2883
2771
  }
2884
- if (done) {
2885
- streamDone = true;
2886
- break;
2887
- }
2888
- }
2889
- } finally {
2890
- try {
2891
- if (!streamDone) await reader.cancel().catch(() => void 0);
2892
- } finally {
2893
- reader.releaseLock();
2894
2772
  }
2895
2773
  }
2896
- if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Chat Completions SSE frame", "STREAM_ERROR");
2897
- if (!completedEmitted && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
2774
+ if (!gate.completed && (hasMessageStarted || hasReasoningStarted || pendingToolCalls.size > 0)) {
2898
2775
  yield factory.responseWarning("Stream ended without a finish_reason", "INCOMPLETE_STREAM");
2899
2776
  const { events, assistantReplayMessage } = finalizePendingTurn();
2900
2777
  for (const event of events) yield event;
@@ -2920,29 +2797,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2920
2797
  * - tool_call 不支持逐 token 流式
2921
2798
  * - replay 保真度低(无 opaque continuation 机制)
2922
2799
  */
2923
- const profile = {
2924
- kind: "ollama",
2925
- instructionsMode: "system_message",
2926
- supportedBlockTypes: ["text", "json"],
2927
- reasoningBlockTypes: ["text"],
2928
- capabilities: {
2929
- textStreaming: "native",
2930
- reasoningStreaming: "none",
2931
- toolCallStreaming: "synthetic",
2932
- replay: "opaque",
2933
- usage: "final",
2934
- toolResultOutcomes: ["success"]
2935
- }
2936
- };
2937
- const mapper = new NormalizedRequestMapper(profile);
2938
- function parseOllamaToolArguments(item) {
2939
- if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2940
- try {
2941
- const parsed = JSON.parse(item.argumentsText);
2942
- if (parsed && typeof parsed === "object") return parsed;
2943
- } catch {}
2944
- throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2945
- }
2800
+ const mapper = new NormalizedRequestMapper("ollama");
2946
2801
  function isOllamaReplayToolCalls(value) {
2947
2802
  return Array.isArray(value) && value.every((entry) => {
2948
2803
  if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
@@ -2960,7 +2815,7 @@ function toWireOllamaToolCalls(toolCalls) {
2960
2815
  }
2961
2816
  var OllamaAdapter = class extends AdapterBase {
2962
2817
  kind = "ollama";
2963
- capabilities = profile.capabilities;
2818
+ isSyntheticStream = false;
2964
2819
  baseUrl;
2965
2820
  apiKey;
2966
2821
  fetchFn;
@@ -2971,7 +2826,6 @@ var OllamaAdapter = class extends AdapterBase {
2971
2826
  this.fetchFn = options.fetch ?? globalThis.fetch;
2972
2827
  }
2973
2828
  buildRequest(request) {
2974
- if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2975
2829
  const messages = [];
2976
2830
  /** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
2977
2831
  const callIdsByName = /* @__PURE__ */ new Map();
@@ -2984,7 +2838,7 @@ var OllamaAdapter = class extends AdapterBase {
2984
2838
  const role = item.role;
2985
2839
  messages.push({
2986
2840
  role,
2987
- content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`))
2841
+ content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`)
2988
2842
  });
2989
2843
  break;
2990
2844
  }
@@ -2992,7 +2846,7 @@ var OllamaAdapter = class extends AdapterBase {
2992
2846
  const lastAssistant = messages.findLast((m) => m.role === "assistant");
2993
2847
  const tc = { function: {
2994
2848
  name: item.name,
2995
- arguments: parseOllamaToolArguments(item)
2849
+ arguments: mapper.parseToolArguments(item)
2996
2850
  } };
2997
2851
  const queue = callIdsByName.get(item.name) ?? [];
2998
2852
  queue.push(item.id);
@@ -3006,12 +2860,11 @@ var OllamaAdapter = class extends AdapterBase {
3006
2860
  break;
3007
2861
  }
3008
2862
  case "tool_result": {
3009
- mapper.assertToolResultOutcome(item.outcome);
3010
2863
  const queue = callIdsByName.get(item.toolName);
3011
2864
  if (queue && queue.length > 0) queue.shift();
3012
2865
  messages.push({
3013
2866
  role: "tool",
3014
- content: contentBlocksToText(mapper.ensureTextBlocks(item.content, `tool_result ${item.callId} content`))
2867
+ content: mapper.textFromBlocks(item.content, `tool_result ${item.callId} content`)
3015
2868
  });
3016
2869
  break;
3017
2870
  }
@@ -3053,7 +2906,9 @@ var OllamaAdapter = class extends AdapterBase {
3053
2906
  messages,
3054
2907
  stream: true
3055
2908
  };
3056
- if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2909
+ const toolChoice = request.toolChoice;
2910
+ const selectedTools = toolChoice === "none" ? [] : toolChoice && typeof toolChoice === "object" ? request.tools?.filter((tool) => tool.name === toolChoice.name) : request.tools;
2911
+ if (selectedTools && selectedTools.length > 0) body.tools = selectedTools.map((t) => ({
3057
2912
  type: "function",
3058
2913
  function: {
3059
2914
  name: t.name,
@@ -3070,56 +2925,31 @@ var OllamaAdapter = class extends AdapterBase {
3070
2925
  }
3071
2926
  async *runStream(providerRequest, factory, request) {
3072
2927
  const auxiliary = this.createAuxiliaryState(request);
3073
- let completedEmitted = false;
2928
+ const gate = createCompletionGate();
2929
+ if (request.toolChoice && request.toolChoice !== "auto") yield factory.responseWarning(request.toolChoice === "none" ? "Ollama toolChoice none was mapped by omitting tools" : `Ollama cannot force tool choice; only tool "${request.toolChoice.name}" was provided as a best-effort constraint`, WarningCode.CAPABILITY_DOWNGRADE);
3074
2930
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
3075
2931
  const headers = { "Content-Type": "application/json" };
3076
2932
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
3077
- let response;
3078
- try {
3079
- response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3080
- method: "POST",
3081
- headers,
3082
- body: JSON.stringify(providerRequest),
3083
- signal: request.signal
3084
- });
3085
- } catch (err) {
3086
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
3087
- }
3088
- if (!response.ok) {
3089
- const errorBody = await response.text().catch(() => "");
3090
- throw providerHttpError(response.status, errorBody);
3091
- }
3092
- const reader = response.body?.getReader();
3093
- if (!reader) throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
3094
- const parser = new IncrementalStreamParser(splitLines, (item) => {
3095
- const trimmed = item.trim();
3096
- if (!trimmed) return { status: "ignored" };
3097
- try {
3098
- const parsed = JSON.parse(trimmed);
3099
- if (parsed && typeof parsed === "object" && "message" in parsed) return {
3100
- status: "parsed",
3101
- value: parsed
3102
- };
3103
- return { status: "malformed" };
3104
- } catch {
3105
- return { status: "malformed" };
3106
- }
2933
+ const { reader } = await openProviderJsonStream({
2934
+ fetchFn: this.fetchFn,
2935
+ url: `${this.baseUrl}/api/chat`,
2936
+ headers,
2937
+ body: providerRequest,
2938
+ signal: request.signal
3107
2939
  });
2940
+ const parser = createNdjsonLineParser((value) => !!value && typeof value === "object" && "message" in value);
3108
2941
  const output = [];
3109
- let streamDone = false;
3110
2942
  let responseId;
3111
2943
  let accumulatedContent = "";
3112
2944
  let currentMessageId = "";
3113
2945
  let hasMessageStarted = false;
3114
2946
  let pendingToolCalls = [];
3115
2947
  let toolCallIndex = 0;
3116
- const buildResponse = this.buildResponse.bind(this);
3117
2948
  const emitCompleted = async function* (stopReason, rawResponseId) {
3118
- if (completedEmitted) {
2949
+ if (!gate.tryComplete()) {
3119
2950
  yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3120
2951
  return;
3121
2952
  }
3122
- completedEmitted = true;
3123
2953
  const replay = replayFromOutput(output);
3124
2954
  if (accumulatedContent || pendingToolCalls.length > 0) replay.push(opaqueItem("ollama", "replay", {
3125
2955
  role: "assistant",
@@ -3128,118 +2958,86 @@ var OllamaAdapter = class extends AdapterBase {
3128
2958
  id: tc.id,
3129
2959
  function: {
3130
2960
  name: tc.name,
3131
- arguments: tc.argumentsJson
2961
+ arguments: JSON.parse(tc.argumentsText)
3132
2962
  }
3133
2963
  }))
3134
2964
  }));
3135
- const auxiliaryResult = await auxiliary.finalize(factory);
3136
- for (const event of auxiliaryResult.events) yield event;
3137
- const finalResponse = buildResponse(request, {
2965
+ yield* this.emitStreamCompleted(factory, request, auxiliary, {
3138
2966
  output,
3139
2967
  replay,
3140
2968
  stopReason,
3141
- usage: auxiliaryResult.usage,
3142
- billing: auxiliaryResult.billing,
3143
- auxiliary: auxiliaryResult.auxiliary,
3144
- warnings: auxiliaryResult.warnings,
3145
- metadataSources: auxiliaryResult.metadataSources,
3146
2969
  rawResponseId
3147
- }, factory);
3148
- yield factory.responseCompleted({
3149
- replay: finalResponse.replay,
3150
- stopReason: finalResponse.stopReason,
3151
- trace: finalResponse.backend,
3152
- usage: finalResponse.usage,
3153
- billing: finalResponse.billing,
3154
- auxiliary: finalResponse.auxiliary,
3155
- warnings: finalResponse.warnings
3156
2970
  });
3157
- };
3158
- try {
3159
- while (true) {
3160
- const { done, value } = await reader.read().catch((err) => {
3161
- throw new AIStreamError(`Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`, "STREAM_ERROR");
3162
- });
3163
- const { items: chunks, malformed: malformedLines } = done ? parser.flush() : parser.feed(value);
3164
- const malformedWarning = emitMalformedStreamWarning(factory, {
3165
- count: malformedLines,
3166
- providerLabel: "Ollama",
3167
- transportLabel: "NDJSON line(s)"
3168
- });
3169
- if (malformedWarning) yield malformedWarning;
3170
- for (const chunk of chunks) {
3171
- responseId = chunk.created_at;
3172
- if (completedEmitted) {
3173
- if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
3174
- continue;
2971
+ }.bind(this);
2972
+ for await (const batch of iterateProviderStreamBatches({
2973
+ reader,
2974
+ parser,
2975
+ factory,
2976
+ providerLabel: "Ollama",
2977
+ transportLabel: "NDJSON line(s)",
2978
+ incompleteMessage: "Stream ended with an incomplete Ollama NDJSON line"
2979
+ })) {
2980
+ for (const warning of batch.warnings) yield warning;
2981
+ for (const chunk of batch.items) {
2982
+ responseId = chunk.created_at;
2983
+ if (gate.completed) {
2984
+ if (chunk.done) yield factory.responseWarning("Duplicate finish signal ignored", "DUPLICATE_FINISH");
2985
+ continue;
2986
+ }
2987
+ const msg = chunk.message;
2988
+ if (msg.content) {
2989
+ if (!hasMessageStarted) {
2990
+ currentMessageId = `msg-${chunk.created_at}`;
2991
+ hasMessageStarted = true;
2992
+ yield factory.messageStarted(currentMessageId);
3175
2993
  }
3176
- const msg = chunk.message;
3177
- if (msg.content) {
3178
- if (!hasMessageStarted) {
3179
- currentMessageId = `msg-${chunk.created_at}`;
3180
- hasMessageStarted = true;
3181
- yield factory.messageStarted(currentMessageId);
3182
- }
3183
- accumulatedContent += msg.content;
3184
- yield factory.messageDelta(currentMessageId, textBlock(msg.content));
2994
+ accumulatedContent += msg.content;
2995
+ yield factory.messageDelta(currentMessageId, textBlock(msg.content));
2996
+ }
2997
+ if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
2998
+ const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
2999
+ const argsText = JSON.stringify(tc.function.arguments);
3000
+ pendingToolCalls.push({
3001
+ id: tcId,
3002
+ name: tc.function.name,
3003
+ argumentsText: argsText
3004
+ });
3005
+ }
3006
+ if (chunk.done) {
3007
+ if (accumulatedContent === "" && pendingToolCalls.length > 0 && !hasMessageStarted) {
3008
+ currentMessageId = `msg-${chunk.created_at}`;
3009
+ hasMessageStarted = true;
3010
+ yield factory.messageStarted(currentMessageId);
3185
3011
  }
3186
- if (msg.tool_calls && msg.tool_calls.length > 0) for (const tc of msg.tool_calls) {
3187
- const tcId = `ollama-tc-${request.requestId}-${toolCallIndex++}`;
3188
- const argsText = JSON.stringify(tc.function.arguments);
3189
- pendingToolCalls.push({
3190
- id: tcId,
3191
- name: tc.function.name,
3192
- argumentsText: argsText,
3193
- argumentsJson: tc.function.arguments
3194
- });
3012
+ if (hasMessageStarted) {
3013
+ const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
3014
+ yield factory.messageCompleted(currentMessageId);
3015
+ if (accumulatedContent) output.push(message);
3195
3016
  }
3196
- if (chunk.done) {
3197
- if (accumulatedContent === "" && pendingToolCalls.length > 0 && !hasMessageStarted) {
3198
- currentMessageId = `msg-${chunk.created_at}`;
3199
- hasMessageStarted = true;
3200
- yield factory.messageStarted(currentMessageId);
3201
- }
3202
- if (hasMessageStarted) {
3203
- const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
3204
- yield factory.messageCompleted(currentMessageId);
3205
- if (accumulatedContent) output.push(message);
3206
- }
3207
- if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
3208
- for (const pending of pendingToolCalls) {
3209
- const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
3210
- yield factory.toolCallStarted(pending.id, pending.name);
3211
- yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
3212
- yield factory.toolCallCompleted(pending.id);
3213
- output.push(toolCall);
3214
- }
3215
- if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
3216
- prompt_eval_count: chunk.prompt_eval_count,
3217
- eval_count: chunk.eval_count
3218
- }), "final", {
3219
- prompt_eval_count: chunk.prompt_eval_count,
3220
- eval_count: chunk.eval_count
3221
- });
3222
- yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
3223
- accumulatedContent = "";
3224
- currentMessageId = "";
3225
- hasMessageStarted = false;
3226
- pendingToolCalls = [];
3017
+ if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
3018
+ for (const pending of pendingToolCalls) {
3019
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
3020
+ yield factory.toolCallStarted(pending.id, pending.name);
3021
+ yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
3022
+ yield factory.toolCallCompleted(pending.id);
3023
+ output.push(toolCall);
3227
3024
  }
3228
- }
3229
- if (done) {
3230
- streamDone = true;
3231
- break;
3025
+ if (request.include?.usage !== "off" && (chunk.prompt_eval_count !== void 0 || chunk.eval_count !== void 0)) auxiliary.recordUsage(usageFromOllama({
3026
+ prompt_eval_count: chunk.prompt_eval_count,
3027
+ eval_count: chunk.eval_count
3028
+ }), "final", {
3029
+ prompt_eval_count: chunk.prompt_eval_count,
3030
+ eval_count: chunk.eval_count
3031
+ });
3032
+ yield* emitCompleted(chunk.done_reason ? mapStopReason(chunk.done_reason) : void 0, chunk.created_at);
3033
+ accumulatedContent = "";
3034
+ currentMessageId = "";
3035
+ hasMessageStarted = false;
3036
+ pendingToolCalls = [];
3232
3037
  }
3233
3038
  }
3234
- } finally {
3235
- try {
3236
- if (!streamDone) await reader.cancel().catch(() => void 0);
3237
- } finally {
3238
- reader.releaseLock();
3239
- }
3240
3039
  }
3241
- if (parser.getRemaining().trim().length > 0) yield factory.responseWarning("Stream ended with an incomplete Ollama NDJSON line", "STREAM_ERROR");
3242
- if (!completedEmitted && (hasMessageStarted || pendingToolCalls.length > 0)) {
3040
+ if (!gate.completed && (hasMessageStarted || pendingToolCalls.length > 0)) {
3243
3041
  yield factory.responseWarning("Stream ended without a done signal", "INCOMPLETE_STREAM");
3244
3042
  if (hasMessageStarted) {
3245
3043
  const message = messageItem([textBlock(accumulatedContent)], { id: currentMessageId });
@@ -3248,7 +3046,7 @@ var OllamaAdapter = class extends AdapterBase {
3248
3046
  }
3249
3047
  if (pendingToolCalls.length > 0) yield factory.responseWarning(`Ollama delivered ${pendingToolCalls.length} tool call(s) as a batch; tool_call streaming is not supported`, WarningCode.TOOL_CALL_BATCHED);
3250
3048
  for (const pending of pendingToolCalls) {
3251
- const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
3049
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
3252
3050
  yield factory.toolCallStarted(pending.id, pending.name);
3253
3051
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
3254
3052
  yield factory.toolCallCompleted(pending.id);
@@ -3289,18 +3087,7 @@ function assertMockRequest(request, expectation, context) {
3289
3087
  }
3290
3088
  var MockAdapter = class extends AdapterBase {
3291
3089
  kind = "mock";
3292
- capabilities = {
3293
- textStreaming: "synthetic",
3294
- reasoningStreaming: "synthetic",
3295
- toolCallStreaming: "synthetic",
3296
- replay: "canonical",
3297
- usage: "final",
3298
- toolResultOutcomes: [
3299
- "success",
3300
- "error",
3301
- "rejected"
3302
- ]
3303
- };
3090
+ isSyntheticStream = true;
3304
3091
  handler;
3305
3092
  providerMetadata;
3306
3093
  cursor = 0;
@@ -3508,8 +3295,7 @@ function createToolCallFromStep(step) {
3508
3295
  type: "tool_call",
3509
3296
  id: step.id,
3510
3297
  name: step.name,
3511
- argumentsText: step.argumentsText,
3512
- argumentsJson: step.argumentsJson
3298
+ argumentsText: step.argumentsText
3513
3299
  };
3514
3300
  }
3515
3301
  function normalizeBlocks(content) {
@@ -3691,6 +3477,6 @@ function cloneItem(item) {
3691
3477
  return structuredClone(item);
3692
3478
  }
3693
3479
  //#endregion
3694
- export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, IncrementalStreamParser, MAX_OPAQUE_JSON_DEPTH, MAX_OPAQUE_PAYLOAD_BYTES, MessagesAdapter, MockAdapter, NormalizedRequestMapper, OllamaAdapter, PROVIDER_ERROR_MESSAGE_MAX_LEN, PROVIDER_ERROR_RAW_BODY_THRESHOLD, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertOpaqueReplayEnvelope, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createEventFactory, emitMalformedStreamWarning, extractProviderErrorMessage, extractText, imageBlock, instructionsToText, jsonBlock, mapReasoningVisibility, mapStopReason, measureJsonDepth, messageItem, metadataSourceList, normalizeRequest, opaqueBlock, opaqueItem, parseSSEEvents, providerHttpError, reasoningItem, replayFromOutput, splitLines, splitSSEFrames, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateOpaqueReplayEnvelope, validateRequest, withMockStreaming };
3480
+ export { AIError, AIMappingError, AIProviderError, AIRequestError, AIStreamError, AdapterAuxiliaryState, AdapterBase, AuxiliaryCollector, ChatCompletionsAdapter, IncrementalStreamParser, MAX_OPAQUE_JSON_DEPTH, MAX_OPAQUE_PAYLOAD_BYTES, MessagesAdapter, MockAdapter, NormalizedRequestMapper, OllamaAdapter, PROVIDER_ERROR_MESSAGE_MAX_LEN, PROVIDER_ERROR_RAW_BODY_THRESHOLD, ResponsesAdapter, WarningCode, aggregateEvents, assertMockRequest, assertOpaqueReplayEnvelope, assertValidRequest, blockToText, collectStream, contentBlocksToText, createAIClient, createChatCompletionsSseParser, createCompletionGate, createEventFactory, createNdjsonLineParser, createSseJsonParser, emitMalformedStreamWarning, extractProviderErrorMessage, extractText, imageBlock, iterateProviderStreamBatches, jsonBlock, mapReasoningVisibility, mapStopReason, measureJsonDepth, messageItem, normalizeRequest, opaqueBlock, opaqueItem, openProviderJsonStream, parseChatCompletionsDataLine, parseSseJsonFrame, providerHttpError, reasoningItem, replayFromOutput, splitLines, splitSSEFrames, syntheticStream, textBlock, toolCallItem, toolResultItem, usageFromAnthropicMessages, usageFromChatCompletions, usageFromOllama, usageFromOpenAIResponses, validateOpaqueReplayEnvelope, validateRequest, withMockStreaming };
3695
3481
 
3696
3482
  //# sourceMappingURL=index.mjs.map