@codehz/ai 0.2.3 → 0.3.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`);
@@ -313,15 +319,29 @@ function normalizeRequest(request, options) {
313
319
  //#endregion
314
320
  //#region src/core/client.ts
315
321
  function createAIClient(options) {
316
- const { adapter, model, defaults } = options;
322
+ const { adapter, model, defaults, signal: defaultSignal } = options;
317
323
  return { stream(request) {
318
- const normalized = normalizeRequest(request, {
324
+ const signal = mergeAbortSignals(defaultSignal, request.signal);
325
+ const normalized = normalizeRequest({
326
+ ...request,
327
+ signal
328
+ }, {
319
329
  model,
320
330
  defaults
321
331
  });
322
332
  return adapter.stream(normalized);
323
333
  } };
324
334
  }
335
+ /**
336
+ * 合并多个 AbortSignal:任一 signal abort 即触发。
337
+ * 如果没有 signal 需要合并则返回 undefined。
338
+ */
339
+ function mergeAbortSignals(...signals) {
340
+ const valid = signals.filter((s) => s != null);
341
+ if (valid.length === 0) return void 0;
342
+ if (valid.length === 1) return valid[0];
343
+ return AbortSignal.any(valid);
344
+ }
325
345
  //#endregion
326
346
  //#region src/core/event-factory.ts
327
347
  function timestamp() {
@@ -806,13 +826,12 @@ function reasoningItem(content, visibility = "full", id) {
806
826
  content
807
827
  };
808
828
  }
809
- function toolCallItem(id, name, argumentsText, argumentsJson) {
829
+ function toolCallItem(id, name, argumentsText) {
810
830
  return {
811
831
  type: "tool_call",
812
832
  id,
813
833
  name,
814
- argumentsText,
815
- argumentsJson
834
+ argumentsText
816
835
  };
817
836
  }
818
837
  function toolResultItem(callId, toolName, outcome, content) {
@@ -1076,11 +1095,12 @@ var AdapterBase = class {
1076
1095
  * 3. 委托 runStream 发射全部流事件(含 response.completed)
1077
1096
  */
1078
1097
  async *stream(request) {
1098
+ request.signal?.throwIfAborted();
1079
1099
  const factory = createEventFactory({
1080
1100
  responseId: request.requestId,
1081
1101
  backend: {
1082
1102
  kind: this.kind,
1083
- isSynthetic: this.capabilities.textStreaming === "synthetic"
1103
+ isSynthetic: this.isSyntheticStream
1084
1104
  }
1085
1105
  });
1086
1106
  yield factory.responseStarted(request.model);
@@ -1132,7 +1152,7 @@ var AdapterBase = class {
1132
1152
  requestId: request.requestId,
1133
1153
  rawResponseId: result.rawResponseId,
1134
1154
  adapter: this.kind,
1135
- isSyntheticStream: this.capabilities.textStreaming === "synthetic",
1155
+ isSyntheticStream: this.isSyntheticStream,
1136
1156
  metadataSources: result.metadataSources,
1137
1157
  warnings
1138
1158
  }
@@ -1551,25 +1571,25 @@ function splitSSEFrames(buffer, allowEOF) {
1551
1571
  //#endregion
1552
1572
  //#region src/helpers/request-mapper.ts
1553
1573
  var NormalizedRequestMapper = class {
1554
- profile;
1555
- constructor(profile) {
1556
- this.profile = profile;
1574
+ kind;
1575
+ constructor(kind) {
1576
+ this.kind = kind;
1557
1577
  }
1558
1578
  mapInstructions(instructions) {
1559
1579
  return typeof instructions === "string" ? instructions : contentBlocksToText(this.ensureTextBlocks(instructions, "instructions"));
1560
1580
  }
1561
1581
  ensureTextBlocks(blocks, field) {
1562
- return this.ensureBlocks(blocks, field, this.profile.supportedBlockTypes, "only text/json blocks are supported");
1582
+ return this.ensureBlocks(blocks, field, ["text", "json"], "only text/json blocks are supported");
1563
1583
  }
1564
1584
  ensureReasoningBlocks(blocks, field) {
1565
- return this.ensureBlocks(blocks, field, this.profile.reasoningBlockTypes, "reasoning only supports text blocks");
1585
+ return this.ensureBlocks(blocks, field, ["text"], "reasoning only supports text blocks");
1566
1586
  }
1567
- assertToolResultOutcome(outcome) {
1568
- if (this.profile.capabilities.toolResultOutcomes.includes(outcome)) return;
1569
- const outcomes = this.profile.capabilities.toolResultOutcomes;
1570
- const supported = outcomes.map((value) => `"${value}"`).join(" and ");
1571
- const verb = outcomes.length > 1 ? "are" : "is";
1572
- throw new AIRequestError(`${this.profile.kind} does not preserve tool_result outcome "${outcome}"; only ${supported} ${verb} supported`, "UNSUPPORTED_TOOL_RESULT_OUTCOME");
1587
+ parseToolArguments(item) {
1588
+ try {
1589
+ const parsed = JSON.parse(item.argumentsText);
1590
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1591
+ } catch {}
1592
+ throw new AIRequestError(`${this.kind} requires tool_call argumentsText to be a valid JSON object`, "TOOL_CALL_ARGUMENTS_INVALID");
1573
1593
  }
1574
1594
  rollbackTrailingAssistantMessages(messages) {
1575
1595
  while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") messages.pop();
@@ -1577,7 +1597,7 @@ var NormalizedRequestMapper = class {
1577
1597
  ensureBlocks(blocks, field, supportedTypes, description) {
1578
1598
  for (let i = 0; i < blocks.length; i++) {
1579
1599
  const block = blocks[i];
1580
- 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");
1600
+ if (block && !supportedTypes.includes(block.type)) throw new AIRequestError(`${this.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`, "UNSUPPORTED_CONTENT_BLOCK");
1581
1601
  }
1582
1602
  return blocks;
1583
1603
  }
@@ -1594,21 +1614,7 @@ var NormalizedRequestMapper = class {
1594
1614
  *
1595
1615
  * 支持消息流 / reasoning 流 / tool_call 流及高保真 replay。
1596
1616
  */
1597
- const profile$3 = {
1598
- kind: "responses",
1599
- instructionsMode: "instructions_field",
1600
- supportedBlockTypes: ["text", "json"],
1601
- reasoningBlockTypes: ["text"],
1602
- capabilities: {
1603
- textStreaming: "native",
1604
- reasoningStreaming: "native",
1605
- toolCallStreaming: "native",
1606
- replay: "opaque",
1607
- usage: "final",
1608
- toolResultOutcomes: ["success"]
1609
- }
1610
- };
1611
- const mapper$3 = new NormalizedRequestMapper(profile$3);
1617
+ const mapper$3 = new NormalizedRequestMapper("responses");
1612
1618
  /** 已处理或可安全忽略的 Responses SSE 类型(未知类型会 warning 一次)。 */
1613
1619
  const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1614
1620
  "response.output_item.added",
@@ -1617,8 +1623,6 @@ const KNOWN_RESPONSES_SSE_TYPES = /* @__PURE__ */ new Set([
1617
1623
  "response.output_text.done",
1618
1624
  "response.reasoning.delta",
1619
1625
  "response.reasoning.done",
1620
- "response.tool_call.delta",
1621
- "response.tool_call.done",
1622
1626
  "response.function_call_arguments.delta",
1623
1627
  "response.function_call_arguments.done",
1624
1628
  "response.content_part.added",
@@ -1654,7 +1658,7 @@ function canonicalToResponsesBlock(b) {
1654
1658
  }
1655
1659
  var ResponsesAdapter = class extends AdapterBase {
1656
1660
  kind = "responses";
1657
- capabilities = profile$3.capabilities;
1661
+ isSyntheticStream = false;
1658
1662
  apiKey;
1659
1663
  baseUrl;
1660
1664
  fetchFn;
@@ -1701,7 +1705,6 @@ var ResponsesAdapter = class extends AdapterBase {
1701
1705
  });
1702
1706
  break;
1703
1707
  case "tool_result": {
1704
- mapper$3.assertToolResultOutcome(item.outcome);
1705
1708
  const output = mapper$3.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
1706
1709
  input.push({
1707
1710
  type: "function_call_output",
@@ -1759,7 +1762,8 @@ var ResponsesAdapter = class extends AdapterBase {
1759
1762
  "Content-Type": "application/json",
1760
1763
  Authorization: `Bearer ${this.apiKey}`
1761
1764
  },
1762
- body: JSON.stringify(providerRequest)
1765
+ body: JSON.stringify(providerRequest),
1766
+ signal: request.signal
1763
1767
  });
1764
1768
  } catch (err) {
1765
1769
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -1798,6 +1802,7 @@ var ResponsesAdapter = class extends AdapterBase {
1798
1802
  let completedEmitted = false;
1799
1803
  let unknownEventsWarned = false;
1800
1804
  const messageItemsWithDelta = /* @__PURE__ */ new Set();
1805
+ const toolCallNames = /* @__PURE__ */ new Map();
1801
1806
  try {
1802
1807
  while (true) {
1803
1808
  const { done, value } = await reader.read().catch((err) => {
@@ -1825,9 +1830,12 @@ var ResponsesAdapter = class extends AdapterBase {
1825
1830
  case "reasoning":
1826
1831
  yield factory.reasoningStarted(item.id, "full");
1827
1832
  break;
1828
- case "function_call":
1829
- yield factory.toolCallStarted(item.id, item.name ?? "unknown");
1833
+ case "function_call": {
1834
+ const name = typeof item.name === "string" ? item.name : "unknown";
1835
+ toolCallNames.set(item.id, name);
1836
+ yield factory.toolCallStarted(item.id, name);
1830
1837
  break;
1838
+ }
1831
1839
  }
1832
1840
  continue;
1833
1841
  }
@@ -1855,14 +1863,14 @@ var ResponsesAdapter = class extends AdapterBase {
1855
1863
  output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
1856
1864
  continue;
1857
1865
  }
1858
- if (sseEvent.type === "response.tool_call.delta") {
1866
+ if (sseEvent.type === "response.function_call_arguments.delta") {
1859
1867
  const data = sseEvent.data;
1860
- if (data.delta.arguments) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
1868
+ if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
1861
1869
  continue;
1862
1870
  }
1863
- if (sseEvent.type === "response.tool_call.done") {
1871
+ if (sseEvent.type === "response.function_call_arguments.done") {
1864
1872
  const data = sseEvent.data;
1865
- const tcItem = toolCallItem(data.item_id, data.name ?? "unknown", data.arguments ?? "");
1873
+ const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
1866
1874
  yield factory.toolCallCompleted(data.item_id);
1867
1875
  output.push(tcItem);
1868
1876
  continue;
@@ -1959,21 +1967,7 @@ var ResponsesAdapter = class extends AdapterBase {
1959
1967
  * - 高保真 replay(含 opaque continuation)
1960
1968
  * - 能力降级 warning
1961
1969
  */
1962
- const profile$2 = {
1963
- kind: "messages",
1964
- instructionsMode: "system_message",
1965
- supportedBlockTypes: ["text", "json"],
1966
- reasoningBlockTypes: ["text"],
1967
- capabilities: {
1968
- textStreaming: "native",
1969
- reasoningStreaming: "native",
1970
- toolCallStreaming: "synthetic",
1971
- replay: "opaque",
1972
- usage: "stream",
1973
- toolResultOutcomes: ["success", "error"]
1974
- }
1975
- };
1976
- const mapper$2 = new NormalizedRequestMapper(profile$2);
1970
+ const mapper$2 = new NormalizedRequestMapper("messages");
1977
1971
  function isMessagesReplayContentBlock(value) {
1978
1972
  if (!value || typeof value !== "object" || !("type" in value)) return false;
1979
1973
  const block = value;
@@ -1999,10 +1993,10 @@ function assertMessagesReplayContent(content) {
1999
1993
  function synthesizeItemId(kind, blockIndex, responseId) {
2000
1994
  return `${kind}-${blockIndex}-${responseId}`;
2001
1995
  }
2002
- function parseToolUseInput(input) {
1996
+ function parseProviderToolUseInput(input) {
2003
1997
  try {
2004
1998
  const parsed = JSON.parse(input);
2005
- return parsed && typeof parsed === "object" ? parsed : {};
1999
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2006
2000
  } catch {
2007
2001
  return {};
2008
2002
  }
@@ -2043,7 +2037,7 @@ function buildStreamMetadata(options) {
2043
2037
  }
2044
2038
  var MessagesAdapter = class extends AdapterBase {
2045
2039
  kind = "messages";
2046
- capabilities = profile$2.capabilities;
2040
+ isSyntheticStream = false;
2047
2041
  apiKey;
2048
2042
  apiVersion;
2049
2043
  baseUrl;
@@ -2082,7 +2076,7 @@ var MessagesAdapter = class extends AdapterBase {
2082
2076
  type: "tool_use",
2083
2077
  id: item.id,
2084
2078
  name: item.name,
2085
- input: item.argumentsJson ?? parseToolUseInput(item.argumentsText)
2079
+ input: mapper$2.parseToolArguments(item)
2086
2080
  };
2087
2081
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
2088
2082
  else messages.push({
@@ -2092,13 +2086,12 @@ var MessagesAdapter = class extends AdapterBase {
2092
2086
  break;
2093
2087
  }
2094
2088
  case "tool_result": {
2095
- mapper$2.assertToolResultOutcome(item.outcome);
2096
2089
  const content = mapper$2.ensureTextBlocks(item.content, `tool_result ${item.callId} content`).map(blockToText).join("\n");
2097
2090
  const block = {
2098
2091
  type: "tool_result",
2099
2092
  tool_use_id: item.callId,
2100
2093
  content,
2101
- is_error: item.outcome === "error"
2094
+ is_error: item.outcome !== "success"
2102
2095
  };
2103
2096
  if (pendingToolResultMessage && typeof pendingToolResultMessage.content !== "string") pendingToolResultMessage.content.push(block);
2104
2097
  else {
@@ -2124,7 +2117,7 @@ var MessagesAdapter = class extends AdapterBase {
2124
2117
  break;
2125
2118
  }
2126
2119
  case "opaque": {
2127
- if (item.purpose !== "replay") break;
2120
+ if (item.source !== "messages" || item.purpose !== "replay") break;
2128
2121
  assertOpaqueReplayEnvelope(item.payload);
2129
2122
  const payload = item.payload;
2130
2123
  if (payload.role === "assistant" && "content" in payload) {
@@ -2175,7 +2168,8 @@ var MessagesAdapter = class extends AdapterBase {
2175
2168
  "x-api-key": this.apiKey,
2176
2169
  "anthropic-version": this.apiVersion
2177
2170
  },
2178
- body: JSON.stringify(providerRequest)
2171
+ body: JSON.stringify(providerRequest),
2172
+ signal: request.signal
2179
2173
  });
2180
2174
  } catch (err) {
2181
2175
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2352,7 +2346,7 @@ var MessagesAdapter = class extends AdapterBase {
2352
2346
  type: "tool_use",
2353
2347
  id: currentItemId,
2354
2348
  name: currentToolName,
2355
- input: parseToolUseInput(currentArgsText || argsBuffer)
2349
+ input: parseProviderToolUseInput(currentArgsText || argsBuffer)
2356
2350
  });
2357
2351
  }
2358
2352
  currentItemType = null;
@@ -2437,21 +2431,7 @@ var MessagesAdapter = class extends AdapterBase {
2437
2431
  * - replay fidelity 依赖 provider 是否暴露可回放的 assistant turn 字段
2438
2432
  */
2439
2433
  const REASONING_FIELDS = ["reasoning_content", "reasoning"];
2440
- const profile$1 = {
2441
- kind: "chat-completions",
2442
- instructionsMode: "system_message",
2443
- supportedBlockTypes: ["text", "json"],
2444
- reasoningBlockTypes: ["text"],
2445
- capabilities: {
2446
- textStreaming: "native",
2447
- reasoningStreaming: "native",
2448
- toolCallStreaming: "native",
2449
- replay: "opaque",
2450
- usage: "final",
2451
- toolResultOutcomes: ["success"]
2452
- }
2453
- };
2454
- const mapper$1 = new NormalizedRequestMapper(profile$1);
2434
+ const mapper$1 = new NormalizedRequestMapper("chat-completions");
2455
2435
  function extractReasoningText(value) {
2456
2436
  if (typeof value === "string") return value;
2457
2437
  if (Array.isArray(value)) return value.map(extractReasoningText).join("");
@@ -2528,7 +2508,7 @@ function buildAssistantReplayMessage(params) {
2528
2508
  }
2529
2509
  var ChatCompletionsAdapter = class extends AdapterBase {
2530
2510
  kind = "chat-completions";
2531
- capabilities = profile$1.capabilities;
2511
+ isSyntheticStream = false;
2532
2512
  apiKey;
2533
2513
  baseUrl;
2534
2514
  fetchFn;
@@ -2573,7 +2553,6 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2573
2553
  break;
2574
2554
  }
2575
2555
  case "tool_result":
2576
- mapper$1.assertToolResultOutcome(item.outcome);
2577
2556
  messages.push({
2578
2557
  role: "tool",
2579
2558
  tool_call_id: item.callId,
@@ -2588,7 +2567,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2588
2567
  });
2589
2568
  break;
2590
2569
  case "opaque": {
2591
- if (item.purpose !== "replay") break;
2570
+ if (item.source !== "chat.completions" || item.purpose !== "replay") break;
2592
2571
  assertOpaqueReplayEnvelope(item.payload);
2593
2572
  const payload = item.payload;
2594
2573
  if (payload.role === "assistant" && typeof payload.content === "string") messages.push({
@@ -2643,7 +2622,8 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2643
2622
  "Content-Type": "application/json",
2644
2623
  Authorization: `Bearer ${this.apiKey}`
2645
2624
  },
2646
- body: JSON.stringify(providerRequest)
2625
+ body: JSON.stringify(providerRequest),
2626
+ signal: request.signal
2647
2627
  });
2648
2628
  } catch (err) {
2649
2629
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -2902,29 +2882,7 @@ var ChatCompletionsAdapter = class extends AdapterBase {
2902
2882
  * - tool_call 不支持逐 token 流式
2903
2883
  * - replay 保真度低(无 opaque continuation 机制)
2904
2884
  */
2905
- const profile = {
2906
- kind: "ollama",
2907
- instructionsMode: "system_message",
2908
- supportedBlockTypes: ["text", "json"],
2909
- reasoningBlockTypes: ["text"],
2910
- capabilities: {
2911
- textStreaming: "native",
2912
- reasoningStreaming: "none",
2913
- toolCallStreaming: "synthetic",
2914
- replay: "opaque",
2915
- usage: "final",
2916
- toolResultOutcomes: ["success"]
2917
- }
2918
- };
2919
- const mapper = new NormalizedRequestMapper(profile);
2920
- function parseOllamaToolArguments(item) {
2921
- if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) return item.argumentsJson;
2922
- try {
2923
- const parsed = JSON.parse(item.argumentsText);
2924
- if (parsed && typeof parsed === "object") return parsed;
2925
- } catch {}
2926
- throw new AIRequestError("ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent", "TOOL_CALL_ARGUMENTS_INVALID");
2927
- }
2885
+ const mapper = new NormalizedRequestMapper("ollama");
2928
2886
  function isOllamaReplayToolCalls(value) {
2929
2887
  return Array.isArray(value) && value.every((entry) => {
2930
2888
  if (!entry || typeof entry !== "object" || !("function" in entry)) return false;
@@ -2942,7 +2900,7 @@ function toWireOllamaToolCalls(toolCalls) {
2942
2900
  }
2943
2901
  var OllamaAdapter = class extends AdapterBase {
2944
2902
  kind = "ollama";
2945
- capabilities = profile.capabilities;
2903
+ isSyntheticStream = false;
2946
2904
  baseUrl;
2947
2905
  apiKey;
2948
2906
  fetchFn;
@@ -2953,7 +2911,6 @@ var OllamaAdapter = class extends AdapterBase {
2953
2911
  this.fetchFn = options.fetch ?? globalThis.fetch;
2954
2912
  }
2955
2913
  buildRequest(request) {
2956
- if (request.toolChoice && request.toolChoice !== "auto") throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
2957
2914
  const messages = [];
2958
2915
  /** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
2959
2916
  const callIdsByName = /* @__PURE__ */ new Map();
@@ -2974,7 +2931,7 @@ var OllamaAdapter = class extends AdapterBase {
2974
2931
  const lastAssistant = messages.findLast((m) => m.role === "assistant");
2975
2932
  const tc = { function: {
2976
2933
  name: item.name,
2977
- arguments: parseOllamaToolArguments(item)
2934
+ arguments: mapper.parseToolArguments(item)
2978
2935
  } };
2979
2936
  const queue = callIdsByName.get(item.name) ?? [];
2980
2937
  queue.push(item.id);
@@ -2988,7 +2945,6 @@ var OllamaAdapter = class extends AdapterBase {
2988
2945
  break;
2989
2946
  }
2990
2947
  case "tool_result": {
2991
- mapper.assertToolResultOutcome(item.outcome);
2992
2948
  const queue = callIdsByName.get(item.toolName);
2993
2949
  if (queue && queue.length > 0) queue.shift();
2994
2950
  messages.push({
@@ -3035,7 +2991,9 @@ var OllamaAdapter = class extends AdapterBase {
3035
2991
  messages,
3036
2992
  stream: true
3037
2993
  };
3038
- if (request.tools && request.tools.length > 0) body.tools = request.tools.map((t) => ({
2994
+ const toolChoice = request.toolChoice;
2995
+ const selectedTools = toolChoice === "none" ? [] : toolChoice && typeof toolChoice === "object" ? request.tools?.filter((tool) => tool.name === toolChoice.name) : request.tools;
2996
+ if (selectedTools && selectedTools.length > 0) body.tools = selectedTools.map((t) => ({
3039
2997
  type: "function",
3040
2998
  function: {
3041
2999
  name: t.name,
@@ -3053,6 +3011,7 @@ var OllamaAdapter = class extends AdapterBase {
3053
3011
  async *runStream(providerRequest, factory, request) {
3054
3012
  const auxiliary = this.createAuxiliaryState(request);
3055
3013
  let completedEmitted = false;
3014
+ 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);
3056
3015
  if (request.metadata) yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
3057
3016
  const headers = { "Content-Type": "application/json" };
3058
3017
  if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
@@ -3061,7 +3020,8 @@ var OllamaAdapter = class extends AdapterBase {
3061
3020
  response = await this.fetchFn(`${this.baseUrl}/api/chat`, {
3062
3021
  method: "POST",
3063
3022
  headers,
3064
- body: JSON.stringify(providerRequest)
3023
+ body: JSON.stringify(providerRequest),
3024
+ signal: request.signal
3065
3025
  });
3066
3026
  } catch (err) {
3067
3027
  throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
@@ -3109,7 +3069,7 @@ var OllamaAdapter = class extends AdapterBase {
3109
3069
  id: tc.id,
3110
3070
  function: {
3111
3071
  name: tc.name,
3112
- arguments: tc.argumentsJson
3072
+ arguments: JSON.parse(tc.argumentsText)
3113
3073
  }
3114
3074
  }))
3115
3075
  }));
@@ -3170,8 +3130,7 @@ var OllamaAdapter = class extends AdapterBase {
3170
3130
  pendingToolCalls.push({
3171
3131
  id: tcId,
3172
3132
  name: tc.function.name,
3173
- argumentsText: argsText,
3174
- argumentsJson: tc.function.arguments
3133
+ argumentsText: argsText
3175
3134
  });
3176
3135
  }
3177
3136
  if (chunk.done) {
@@ -3187,7 +3146,7 @@ var OllamaAdapter = class extends AdapterBase {
3187
3146
  }
3188
3147
  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);
3189
3148
  for (const pending of pendingToolCalls) {
3190
- const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
3149
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
3191
3150
  yield factory.toolCallStarted(pending.id, pending.name);
3192
3151
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
3193
3152
  yield factory.toolCallCompleted(pending.id);
@@ -3229,7 +3188,7 @@ var OllamaAdapter = class extends AdapterBase {
3229
3188
  }
3230
3189
  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);
3231
3190
  for (const pending of pendingToolCalls) {
3232
- const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText, pending.argumentsJson);
3191
+ const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
3233
3192
  yield factory.toolCallStarted(pending.id, pending.name);
3234
3193
  yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
3235
3194
  yield factory.toolCallCompleted(pending.id);
@@ -3270,18 +3229,7 @@ function assertMockRequest(request, expectation, context) {
3270
3229
  }
3271
3230
  var MockAdapter = class extends AdapterBase {
3272
3231
  kind = "mock";
3273
- capabilities = {
3274
- textStreaming: "synthetic",
3275
- reasoningStreaming: "synthetic",
3276
- toolCallStreaming: "synthetic",
3277
- replay: "canonical",
3278
- usage: "final",
3279
- toolResultOutcomes: [
3280
- "success",
3281
- "error",
3282
- "rejected"
3283
- ]
3284
- };
3232
+ isSyntheticStream = true;
3285
3233
  handler;
3286
3234
  providerMetadata;
3287
3235
  cursor = 0;
@@ -3296,7 +3244,7 @@ var MockAdapter = class extends AdapterBase {
3296
3244
  }
3297
3245
  async buildRequest(request) {
3298
3246
  const turnIndex = this.cursor;
3299
- const context = this.buildHandlerContext(turnIndex);
3247
+ const context = this.buildHandlerContext(turnIndex, request.signal);
3300
3248
  const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
3301
3249
  const handlerResult = this.handler(request, context);
3302
3250
  this.cursor += 1;
@@ -3315,6 +3263,7 @@ var MockAdapter = class extends AdapterBase {
3315
3263
  const output = [];
3316
3264
  let stepCount = 0;
3317
3265
  for await (const step of mockRequest.handlerResult) {
3266
+ if (request.signal?.aborted) return;
3318
3267
  stepCount += 1;
3319
3268
  switch (step.type) {
3320
3269
  case "warning":
@@ -3434,7 +3383,7 @@ var MockAdapter = class extends AdapterBase {
3434
3383
  rawResponseId: completion.rawResponseId
3435
3384
  }, factory);
3436
3385
  }
3437
- buildHandlerContext(turnIndex) {
3386
+ buildHandlerContext(turnIndex, signal) {
3438
3387
  return {
3439
3388
  turnIndex,
3440
3389
  previousReplay: this.previousReplay.map(cloneItem),
@@ -3443,7 +3392,8 @@ var MockAdapter = class extends AdapterBase {
3443
3392
  ...record,
3444
3393
  replay: record.replay.map(cloneItem),
3445
3394
  toolCalls: record.toolCalls.map(cloneItem)
3446
- }))
3395
+ })),
3396
+ signal
3447
3397
  };
3448
3398
  }
3449
3399
  };
@@ -3487,8 +3437,7 @@ function createToolCallFromStep(step) {
3487
3437
  type: "tool_call",
3488
3438
  id: step.id,
3489
3439
  name: step.name,
3490
- argumentsText: step.argumentsText,
3491
- argumentsJson: step.argumentsJson
3440
+ argumentsText: step.argumentsText
3492
3441
  };
3493
3442
  }
3494
3443
  function normalizeBlocks(content) {