@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/README.md +34 -38
- package/dist/index.d.mts +18 -40
- package/dist/index.mjs +88 -139
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +4 -22
- package/src/adapters/messages.ts +10 -28
- package/src/adapters/mock.ts +9 -12
- package/src/adapters/ollama.ts +26 -53
- package/src/adapters/responses.ts +17 -35
- package/src/core/client.ts +15 -2
- package/src/core/validation.ts +19 -0
- package/src/helpers/adapter-base.ts +6 -3
- package/src/helpers/index.ts +0 -1
- package/src/helpers/mapping.ts +1 -2
- package/src/helpers/request-mapper.ts +18 -25
- package/src/index.ts +1 -1
- package/src/types/adapter.ts +3 -12
- package/src/types/index.ts +1 -9
- package/src/types/items.ts +0 -1
- package/src/types/request.ts +2 -0
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1555
|
-
constructor(
|
|
1556
|
-
this.
|
|
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,
|
|
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,
|
|
1585
|
+
return this.ensureBlocks(blocks, field, ["text"], "reasoning only supports text blocks");
|
|
1566
1586
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
throw new AIRequestError(`${this.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1866
|
+
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
1859
1867
|
const data = sseEvent.data;
|
|
1860
|
-
if (data.delta
|
|
1868
|
+
if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
|
|
1861
1869
|
continue;
|
|
1862
1870
|
}
|
|
1863
|
-
if (sseEvent.type === "response.
|
|
1871
|
+
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
1864
1872
|
const data = sseEvent.data;
|
|
1865
|
-
const tcItem = toolCallItem(data.item_id, data.
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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) {
|