@codehz/ai 0.2.4 → 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 +3 -8
- package/dist/index.d.mts +12 -37
- package/dist/index.mjs +58 -130
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/chat-completions.ts +3 -22
- package/src/adapters/messages.ts +9 -28
- package/src/adapters/mock.ts +1 -10
- package/src/adapters/ollama.ts +25 -53
- package/src/adapters/responses.ts +16 -35
- package/src/core/validation.ts +19 -0
- package/src/helpers/adapter-base.ts +3 -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/types/adapter.ts +1 -12
- package/src/types/index.ts +1 -9
- package/src/types/items.ts +0 -1
package/package.json
CHANGED
|
@@ -24,7 +24,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
|
24
24
|
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
25
25
|
import { usageFromChatCompletions } from "../helpers/usage-mapping.js";
|
|
26
26
|
import { NormalizedRequestMapper, splitLines, IncrementalStreamParser } from "../helpers/index.js";
|
|
27
|
-
import type { ProviderProfile } from "../helpers/index.js";
|
|
28
27
|
|
|
29
28
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
30
29
|
|
|
@@ -118,24 +117,7 @@ type ReasoningFieldName = "reasoning" | "reasoning_content";
|
|
|
118
117
|
|
|
119
118
|
const REASONING_FIELDS: readonly ReasoningFieldName[] = ["reasoning_content", "reasoning"];
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const profile: ProviderProfile = {
|
|
124
|
-
kind: "chat-completions",
|
|
125
|
-
instructionsMode: "system_message",
|
|
126
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
127
|
-
reasoningBlockTypes: ["text"] as const,
|
|
128
|
-
capabilities: {
|
|
129
|
-
textStreaming: "native",
|
|
130
|
-
reasoningStreaming: "native",
|
|
131
|
-
toolCallStreaming: "native",
|
|
132
|
-
replay: "opaque",
|
|
133
|
-
usage: "final",
|
|
134
|
-
toolResultOutcomes: ["success"],
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
120
|
+
const mapper = new NormalizedRequestMapper("chat-completions");
|
|
139
121
|
|
|
140
122
|
function extractReasoningText(value: unknown): string {
|
|
141
123
|
if (typeof value === "string") return value;
|
|
@@ -251,7 +233,7 @@ function buildAssistantReplayMessage(params: {
|
|
|
251
233
|
|
|
252
234
|
export class ChatCompletionsAdapter extends AdapterBase {
|
|
253
235
|
readonly kind = "chat-completions" as const;
|
|
254
|
-
readonly
|
|
236
|
+
readonly isSyntheticStream = false;
|
|
255
237
|
|
|
256
238
|
private apiKey: string;
|
|
257
239
|
private baseUrl: string;
|
|
@@ -303,7 +285,6 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
303
285
|
break;
|
|
304
286
|
}
|
|
305
287
|
case "tool_result": {
|
|
306
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
307
288
|
messages.push({
|
|
308
289
|
role: "tool",
|
|
309
290
|
tool_call_id: item.callId,
|
|
@@ -323,7 +304,7 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
323
304
|
}
|
|
324
305
|
case "opaque": {
|
|
325
306
|
// Try to restore from opaque replay
|
|
326
|
-
if (item.purpose !== "replay") break;
|
|
307
|
+
if (item.source !== "chat.completions" || item.purpose !== "replay") break;
|
|
327
308
|
assertOpaqueReplayEnvelope(item.payload);
|
|
328
309
|
const payload = item.payload as Record<string, unknown>;
|
|
329
310
|
if (payload.role === "assistant" && typeof payload.content === "string") {
|
package/src/adapters/messages.ts
CHANGED
|
@@ -27,7 +27,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
|
27
27
|
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
28
28
|
import { usageFromAnthropicMessages } from "../helpers/usage-mapping.js";
|
|
29
29
|
import { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
|
|
30
|
-
import type { ProviderProfile } from "../helpers/index.js";
|
|
31
30
|
|
|
32
31
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
33
32
|
|
|
@@ -73,24 +72,7 @@ type MessagesAPITool = {
|
|
|
73
72
|
input_schema: Record<string, unknown>;
|
|
74
73
|
};
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const profile: ProviderProfile = {
|
|
79
|
-
kind: "messages",
|
|
80
|
-
instructionsMode: "system_message",
|
|
81
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
82
|
-
reasoningBlockTypes: ["text"] as const,
|
|
83
|
-
capabilities: {
|
|
84
|
-
textStreaming: "native",
|
|
85
|
-
reasoningStreaming: "native",
|
|
86
|
-
toolCallStreaming: "synthetic",
|
|
87
|
-
replay: "opaque",
|
|
88
|
-
usage: "stream",
|
|
89
|
-
toolResultOutcomes: ["success", "error"],
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
75
|
+
const mapper = new NormalizedRequestMapper("messages");
|
|
94
76
|
|
|
95
77
|
function isMessagesReplayContentBlock(value: unknown): value is MessagesAPIContentBlock {
|
|
96
78
|
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
@@ -177,10 +159,10 @@ function synthesizeItemId(kind: "msg" | "reason" | "reason-redacted", blockIndex
|
|
|
177
159
|
return `${kind}-${blockIndex}-${responseId}`;
|
|
178
160
|
}
|
|
179
161
|
|
|
180
|
-
function
|
|
162
|
+
function parseProviderToolUseInput(input: string): Record<string, unknown> {
|
|
181
163
|
try {
|
|
182
|
-
const parsed = JSON.parse(input);
|
|
183
|
-
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {};
|
|
164
|
+
const parsed: unknown = JSON.parse(input);
|
|
165
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
|
|
184
166
|
} catch {
|
|
185
167
|
return {};
|
|
186
168
|
}
|
|
@@ -251,7 +233,7 @@ function buildStreamMetadata(options: {
|
|
|
251
233
|
|
|
252
234
|
export class MessagesAdapter extends AdapterBase {
|
|
253
235
|
readonly kind = "messages" as const;
|
|
254
|
-
readonly
|
|
236
|
+
readonly isSyntheticStream = false;
|
|
255
237
|
|
|
256
238
|
private apiKey: string;
|
|
257
239
|
private apiVersion: string;
|
|
@@ -302,7 +284,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
302
284
|
type: "tool_use",
|
|
303
285
|
id: item.id,
|
|
304
286
|
name: item.name,
|
|
305
|
-
input:
|
|
287
|
+
input: mapper.parseToolArguments(item),
|
|
306
288
|
};
|
|
307
289
|
|
|
308
290
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") {
|
|
@@ -313,7 +295,6 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
313
295
|
break;
|
|
314
296
|
}
|
|
315
297
|
case "tool_result": {
|
|
316
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
317
298
|
const content = mapper
|
|
318
299
|
.ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
319
300
|
.map(blockToText)
|
|
@@ -322,7 +303,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
322
303
|
type: "tool_result",
|
|
323
304
|
tool_use_id: item.callId,
|
|
324
305
|
content,
|
|
325
|
-
is_error: item.outcome
|
|
306
|
+
is_error: item.outcome !== "success",
|
|
326
307
|
};
|
|
327
308
|
if (pendingToolResultMessage && typeof pendingToolResultMessage.content !== "string") {
|
|
328
309
|
pendingToolResultMessage.content.push(block);
|
|
@@ -346,7 +327,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
346
327
|
}
|
|
347
328
|
case "opaque": {
|
|
348
329
|
// 尝试从 opaque replay item 中提取 assistant message
|
|
349
|
-
if (item.purpose !== "replay") break;
|
|
330
|
+
if (item.source !== "messages" || item.purpose !== "replay") break;
|
|
350
331
|
assertOpaqueReplayEnvelope(item.payload);
|
|
351
332
|
const payload = item.payload as Record<string, unknown>;
|
|
352
333
|
if (payload.role === "assistant" && "content" in payload) {
|
|
@@ -626,7 +607,7 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
626
607
|
type: "tool_use",
|
|
627
608
|
id: currentItemId,
|
|
628
609
|
name: currentToolName,
|
|
629
|
-
input:
|
|
610
|
+
input: parseProviderToolUseInput(currentArgsText || argsBuffer),
|
|
630
611
|
});
|
|
631
612
|
}
|
|
632
613
|
|
package/src/adapters/mock.ts
CHANGED
|
@@ -120,7 +120,6 @@ export type MockToolCallStep = {
|
|
|
120
120
|
id: string;
|
|
121
121
|
name: string;
|
|
122
122
|
argumentsText: string;
|
|
123
|
-
argumentsJson?: unknown;
|
|
124
123
|
streamArguments?: boolean;
|
|
125
124
|
stream?: MockTextStreamOptions | false;
|
|
126
125
|
};
|
|
@@ -266,14 +265,7 @@ export function assertMockRequest(
|
|
|
266
265
|
|
|
267
266
|
export class MockAdapter extends AdapterBase {
|
|
268
267
|
readonly kind = "mock" as const;
|
|
269
|
-
readonly
|
|
270
|
-
textStreaming: "synthetic",
|
|
271
|
-
reasoningStreaming: "synthetic",
|
|
272
|
-
toolCallStreaming: "synthetic",
|
|
273
|
-
replay: "canonical",
|
|
274
|
-
usage: "final",
|
|
275
|
-
toolResultOutcomes: ["success", "error", "rejected"],
|
|
276
|
-
} as const;
|
|
268
|
+
readonly isSyntheticStream = true;
|
|
277
269
|
|
|
278
270
|
private readonly handler: MockHandler;
|
|
279
271
|
private readonly providerMetadata?: Record<string, unknown>;
|
|
@@ -572,7 +564,6 @@ function createToolCallFromStep(step: MockToolCallStep): ToolCallItem {
|
|
|
572
564
|
id: step.id,
|
|
573
565
|
name: step.name,
|
|
574
566
|
argumentsText: step.argumentsText,
|
|
575
|
-
argumentsJson: step.argumentsJson,
|
|
576
567
|
};
|
|
577
568
|
}
|
|
578
569
|
|
package/src/adapters/ollama.ts
CHANGED
|
@@ -30,7 +30,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
|
30
30
|
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
31
31
|
import { usageFromOllama } from "../helpers/usage-mapping.js";
|
|
32
32
|
import { NormalizedRequestMapper, splitLines, IncrementalStreamParser } from "../helpers/index.js";
|
|
33
|
-
import type { ProviderProfile } from "../helpers/index.js";
|
|
34
33
|
|
|
35
34
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
36
35
|
|
|
@@ -82,44 +81,7 @@ type OllamaTool = {
|
|
|
82
81
|
};
|
|
83
82
|
};
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const profile: ProviderProfile = {
|
|
88
|
-
kind: "ollama",
|
|
89
|
-
instructionsMode: "system_message",
|
|
90
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
91
|
-
reasoningBlockTypes: ["text"] as const,
|
|
92
|
-
capabilities: {
|
|
93
|
-
textStreaming: "native",
|
|
94
|
-
reasoningStreaming: "none",
|
|
95
|
-
toolCallStreaming: "synthetic",
|
|
96
|
-
replay: "opaque",
|
|
97
|
-
usage: "final",
|
|
98
|
-
toolResultOutcomes: ["success"],
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
103
|
-
|
|
104
|
-
function parseOllamaToolArguments(item: import("../index.js").ToolCallItem): Record<string, unknown> {
|
|
105
|
-
if (item.argumentsJson && typeof item.argumentsJson === "object" && item.argumentsJson !== null) {
|
|
106
|
-
return item.argumentsJson as Record<string, unknown>;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const parsed = JSON.parse(item.argumentsText);
|
|
111
|
-
if (parsed && typeof parsed === "object") {
|
|
112
|
-
return parsed as Record<string, unknown>;
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// fall through
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new AIRequestError(
|
|
119
|
-
"ollama tool_call argumentsText must be valid JSON object when argumentsJson is absent",
|
|
120
|
-
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
121
|
-
);
|
|
122
|
-
}
|
|
84
|
+
const mapper = new NormalizedRequestMapper("ollama");
|
|
123
85
|
|
|
124
86
|
// ── Ollama 流式 chunk ─────────────────────────────────────────
|
|
125
87
|
|
|
@@ -179,7 +141,7 @@ function toWireOllamaToolCalls(toolCalls: OllamaReplayToolCall[]): OllamaToolCal
|
|
|
179
141
|
|
|
180
142
|
export class OllamaAdapter extends AdapterBase {
|
|
181
143
|
readonly kind = "ollama" as const;
|
|
182
|
-
readonly
|
|
144
|
+
readonly isSyntheticStream = false;
|
|
183
145
|
|
|
184
146
|
private baseUrl: string;
|
|
185
147
|
private apiKey: string | undefined;
|
|
@@ -195,10 +157,6 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
195
157
|
// ── buildRequest ──────────────────────────────────────────
|
|
196
158
|
|
|
197
159
|
protected buildRequest(request: NormalizedRequest): OllamaChatRequest {
|
|
198
|
-
if (request.toolChoice && request.toolChoice !== "auto") {
|
|
199
|
-
throw new AIRequestError("ollama does not support explicit toolChoice", "UNSUPPORTED_TOOL_CHOICE");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
160
|
const messages: OllamaMessage[] = [];
|
|
203
161
|
/** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
|
|
204
162
|
const callIdsByName = new Map<string, string[]>();
|
|
@@ -224,7 +182,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
224
182
|
const tc: OllamaToolCall = {
|
|
225
183
|
function: {
|
|
226
184
|
name: item.name,
|
|
227
|
-
arguments:
|
|
185
|
+
arguments: mapper.parseToolArguments(item),
|
|
228
186
|
},
|
|
229
187
|
};
|
|
230
188
|
const queue = callIdsByName.get(item.name) ?? [];
|
|
@@ -238,7 +196,6 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
238
196
|
break;
|
|
239
197
|
}
|
|
240
198
|
case "tool_result": {
|
|
241
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
242
199
|
// Best-effort: consume matching id from name queue when present (no wire call_id)
|
|
243
200
|
const queue = callIdsByName.get(item.toolName);
|
|
244
201
|
if (queue && queue.length > 0) {
|
|
@@ -302,8 +259,16 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
302
259
|
stream: true,
|
|
303
260
|
};
|
|
304
261
|
|
|
305
|
-
|
|
306
|
-
|
|
262
|
+
const toolChoice = request.toolChoice;
|
|
263
|
+
const selectedTools =
|
|
264
|
+
toolChoice === "none"
|
|
265
|
+
? []
|
|
266
|
+
: toolChoice && typeof toolChoice === "object"
|
|
267
|
+
? request.tools?.filter((tool) => tool.name === toolChoice.name)
|
|
268
|
+
: request.tools;
|
|
269
|
+
|
|
270
|
+
if (selectedTools && selectedTools.length > 0) {
|
|
271
|
+
body.tools = selectedTools.map(
|
|
307
272
|
(t): OllamaTool => ({
|
|
308
273
|
type: "function",
|
|
309
274
|
function: {
|
|
@@ -333,6 +298,14 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
333
298
|
): AsyncIterable<AIStreamEvent> {
|
|
334
299
|
const auxiliary = this.createAuxiliaryState(request);
|
|
335
300
|
let completedEmitted = false;
|
|
301
|
+
if (request.toolChoice && request.toolChoice !== "auto") {
|
|
302
|
+
yield factory.responseWarning(
|
|
303
|
+
request.toolChoice === "none"
|
|
304
|
+
? "Ollama toolChoice none was mapped by omitting tools"
|
|
305
|
+
: `Ollama cannot force tool choice; only tool "${request.toolChoice.name}" was provided as a best-effort constraint`,
|
|
306
|
+
WarningCode.CAPABILITY_DOWNGRADE,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
336
309
|
if (request.metadata) {
|
|
337
310
|
yield factory.responseWarning("Request metadata is not supported by the Ollama adapter", "UNSUPPORTED_METADATA");
|
|
338
311
|
}
|
|
@@ -391,7 +364,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
391
364
|
let hasMessageStarted = false;
|
|
392
365
|
|
|
393
366
|
// tool_calls 累积(于 final chunk 到达)
|
|
394
|
-
let pendingToolCalls: Array<{ id: string; name: string; argumentsText: string
|
|
367
|
+
let pendingToolCalls: Array<{ id: string; name: string; argumentsText: string }> = [];
|
|
395
368
|
let toolCallIndex = 0;
|
|
396
369
|
const buildResponse = this.buildResponse.bind(this);
|
|
397
370
|
|
|
@@ -415,7 +388,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
415
388
|
content: accumulatedContent,
|
|
416
389
|
tool_calls: pendingToolCalls.map((tc) => ({
|
|
417
390
|
id: tc.id,
|
|
418
|
-
function: { name: tc.name, arguments: tc.
|
|
391
|
+
function: { name: tc.name, arguments: JSON.parse(tc.argumentsText) as Record<string, unknown> },
|
|
419
392
|
})),
|
|
420
393
|
}),
|
|
421
394
|
);
|
|
@@ -504,7 +477,6 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
504
477
|
id: tcId,
|
|
505
478
|
name: tc.function.name,
|
|
506
479
|
argumentsText: argsText,
|
|
507
|
-
argumentsJson: tc.function.arguments,
|
|
508
480
|
});
|
|
509
481
|
}
|
|
510
482
|
}
|
|
@@ -536,7 +508,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
536
508
|
|
|
537
509
|
// 发出 tool_call 完成事件
|
|
538
510
|
for (const pending of pendingToolCalls) {
|
|
539
|
-
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText
|
|
511
|
+
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
|
|
540
512
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
541
513
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
542
514
|
yield factory.toolCallCompleted(pending.id);
|
|
@@ -611,7 +583,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
611
583
|
}
|
|
612
584
|
|
|
613
585
|
for (const pending of pendingToolCalls) {
|
|
614
|
-
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText
|
|
586
|
+
const toolCall = toolCallItem(pending.id, pending.name, pending.argumentsText);
|
|
615
587
|
yield factory.toolCallStarted(pending.id, pending.name);
|
|
616
588
|
yield factory.toolCallDelta(pending.id, { argumentsText: pending.argumentsText });
|
|
617
589
|
yield factory.toolCallCompleted(pending.id);
|
|
@@ -25,7 +25,6 @@ import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
|
|
|
25
25
|
import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
|
|
26
26
|
import { usageFromOpenAIResponses } from "../helpers/usage-mapping.js";
|
|
27
27
|
import { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
|
|
28
|
-
import type { ProviderProfile } from "../helpers/index.js";
|
|
29
28
|
|
|
30
29
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
31
30
|
|
|
@@ -72,24 +71,7 @@ type ResponsesTool = {
|
|
|
72
71
|
input_schema: Record<string, unknown>;
|
|
73
72
|
};
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const profile: ProviderProfile = {
|
|
78
|
-
kind: "responses",
|
|
79
|
-
instructionsMode: "instructions_field",
|
|
80
|
-
supportedBlockTypes: ["text", "json"] as const,
|
|
81
|
-
reasoningBlockTypes: ["text"] as const,
|
|
82
|
-
capabilities: {
|
|
83
|
-
textStreaming: "native",
|
|
84
|
-
reasoningStreaming: "native",
|
|
85
|
-
toolCallStreaming: "native",
|
|
86
|
-
replay: "opaque",
|
|
87
|
-
usage: "final",
|
|
88
|
-
toolResultOutcomes: ["success"],
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const mapper = new NormalizedRequestMapper(profile);
|
|
74
|
+
const mapper = new NormalizedRequestMapper("responses");
|
|
93
75
|
|
|
94
76
|
// ── SSE 事件类型 ──────────────────────────────────────────────
|
|
95
77
|
|
|
@@ -99,8 +81,8 @@ type ResponsesSSEEvent =
|
|
|
99
81
|
| { type: "response.output_text.done"; data: { item_id: string; text: string } }
|
|
100
82
|
| { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
|
|
101
83
|
| { type: "response.reasoning.done"; data: { item_id: string; text: string } }
|
|
102
|
-
| { type: "response.
|
|
103
|
-
| { type: "response.
|
|
84
|
+
| { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
|
|
85
|
+
| { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
|
|
104
86
|
| { type: "response.completed"; data: { response: ResponsesAPIResponse } }
|
|
105
87
|
| { type: "response.failed"; data: { response: ResponsesAPIResponse } }
|
|
106
88
|
| { type: "response.incomplete"; data: { response: ResponsesAPIResponse } }
|
|
@@ -115,8 +97,6 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
|
|
|
115
97
|
"response.output_text.done",
|
|
116
98
|
"response.reasoning.delta",
|
|
117
99
|
"response.reasoning.done",
|
|
118
|
-
"response.tool_call.delta",
|
|
119
|
-
"response.tool_call.done",
|
|
120
100
|
"response.function_call_arguments.delta",
|
|
121
101
|
"response.function_call_arguments.done",
|
|
122
102
|
"response.content_part.added",
|
|
@@ -187,7 +167,7 @@ function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): Respo
|
|
|
187
167
|
|
|
188
168
|
export class ResponsesAdapter extends AdapterBase {
|
|
189
169
|
readonly kind = "responses" as const;
|
|
190
|
-
readonly
|
|
170
|
+
readonly isSyntheticStream = false;
|
|
191
171
|
|
|
192
172
|
private apiKey: string;
|
|
193
173
|
private baseUrl: string;
|
|
@@ -242,7 +222,6 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
242
222
|
break;
|
|
243
223
|
}
|
|
244
224
|
case "tool_result": {
|
|
245
|
-
mapper.assertToolResultOutcome(item.outcome);
|
|
246
225
|
const output = mapper
|
|
247
226
|
.ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
|
|
248
227
|
.map(blockToText)
|
|
@@ -370,6 +349,7 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
370
349
|
let completedEmitted = false;
|
|
371
350
|
let unknownEventsWarned = false;
|
|
372
351
|
const messageItemsWithDelta = new Set<string>();
|
|
352
|
+
const toolCallNames = new Map<string, string>();
|
|
373
353
|
|
|
374
354
|
try {
|
|
375
355
|
while (true) {
|
|
@@ -408,9 +388,12 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
408
388
|
case "reasoning":
|
|
409
389
|
yield factory.reasoningStarted(item.id, "full");
|
|
410
390
|
break;
|
|
411
|
-
case "function_call":
|
|
412
|
-
|
|
391
|
+
case "function_call": {
|
|
392
|
+
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
393
|
+
toolCallNames.set(item.id, name);
|
|
394
|
+
yield factory.toolCallStarted(item.id, name);
|
|
413
395
|
break;
|
|
396
|
+
}
|
|
414
397
|
}
|
|
415
398
|
continue;
|
|
416
399
|
}
|
|
@@ -445,17 +428,15 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
445
428
|
continue;
|
|
446
429
|
}
|
|
447
430
|
|
|
448
|
-
if (sseEvent.type === "response.
|
|
449
|
-
const data = sseEvent.data as { item_id: string; delta:
|
|
450
|
-
if (data.delta
|
|
451
|
-
yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta.arguments });
|
|
452
|
-
}
|
|
431
|
+
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
432
|
+
const data = sseEvent.data as { item_id: string; delta: string };
|
|
433
|
+
if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
|
|
453
434
|
continue;
|
|
454
435
|
}
|
|
455
436
|
|
|
456
|
-
if (sseEvent.type === "response.
|
|
457
|
-
const data = sseEvent.data as { item_id: string; arguments
|
|
458
|
-
const tcItem = toolCallItem(data.item_id, data.
|
|
437
|
+
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
438
|
+
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
439
|
+
const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
459
440
|
yield factory.toolCallCompleted(data.item_id);
|
|
460
441
|
output.push(tcItem);
|
|
461
442
|
continue;
|
package/src/core/validation.ts
CHANGED
|
@@ -136,6 +136,25 @@ function validateInputItem(item: unknown, field: string, issues: ValidationIssue
|
|
|
136
136
|
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
137
137
|
`${field}.argumentsText must be a string`,
|
|
138
138
|
);
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = JSON.parse(item.argumentsText);
|
|
142
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
143
|
+
pushIssue(
|
|
144
|
+
issues,
|
|
145
|
+
`${field}.argumentsText`,
|
|
146
|
+
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
147
|
+
`${field}.argumentsText must encode a JSON object`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
pushIssue(
|
|
152
|
+
issues,
|
|
153
|
+
`${field}.argumentsText`,
|
|
154
|
+
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
155
|
+
`${field}.argumentsText must encode a JSON object`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
139
158
|
}
|
|
140
159
|
return;
|
|
141
160
|
case "tool_result":
|
|
@@ -56,7 +56,7 @@ export type StreamResult = {
|
|
|
56
56
|
|
|
57
57
|
export abstract class AdapterBase implements BackendAdapter {
|
|
58
58
|
abstract readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
|
|
59
|
-
abstract readonly
|
|
59
|
+
abstract readonly isSyntheticStream: boolean;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* stream 模板方法:
|
|
@@ -70,7 +70,7 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
70
70
|
|
|
71
71
|
const factory = createEventFactory({
|
|
72
72
|
responseId: request.requestId,
|
|
73
|
-
backend: { kind: this.kind, isSynthetic: this.
|
|
73
|
+
backend: { kind: this.kind, isSynthetic: this.isSyntheticStream },
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
yield factory.responseStarted(request.model);
|
|
@@ -151,7 +151,7 @@ export abstract class AdapterBase implements BackendAdapter {
|
|
|
151
151
|
requestId: request.requestId,
|
|
152
152
|
rawResponseId: result.rawResponseId,
|
|
153
153
|
adapter: this.kind,
|
|
154
|
-
isSyntheticStream: this.
|
|
154
|
+
isSyntheticStream: this.isSyntheticStream,
|
|
155
155
|
metadataSources: result.metadataSources,
|
|
156
156
|
warnings,
|
|
157
157
|
},
|
package/src/helpers/index.ts
CHANGED
|
@@ -58,4 +58,3 @@ export { IncrementalStreamParser, splitLines, splitSSEFrames } from "./increment
|
|
|
58
58
|
export type { StreamSplitResult, StreamParseResult } from "./incremental-stream-parser.js";
|
|
59
59
|
|
|
60
60
|
export { NormalizedRequestMapper } from "./request-mapper.js";
|
|
61
|
-
export type { ProviderProfile } from "./request-mapper.js";
|
package/src/helpers/mapping.ts
CHANGED
|
@@ -100,13 +100,12 @@ export function reasoningItem(
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export function toolCallItem(id: string, name: string, argumentsText: string
|
|
103
|
+
export function toolCallItem(id: string, name: string, argumentsText: string): ToolCallItem {
|
|
104
104
|
return {
|
|
105
105
|
type: "tool_call",
|
|
106
106
|
id,
|
|
107
107
|
name,
|
|
108
108
|
argumentsText,
|
|
109
|
-
argumentsJson,
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { AIRequestError } from "../core/errors.js";
|
|
2
2
|
import { contentBlocksToText } from "./mapping.js";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
export type ProviderProfile = {
|
|
7
|
-
readonly kind: string;
|
|
8
|
-
readonly instructionsMode: "system_message" | "instructions_field" | "none";
|
|
9
|
-
readonly supportedBlockTypes: ReadonlyArray<ContentBlock["type"]>;
|
|
10
|
-
readonly reasoningBlockTypes: ReadonlyArray<ContentBlock["type"]>;
|
|
11
|
-
readonly capabilities: AdapterCapabilities;
|
|
12
|
-
};
|
|
4
|
+
import type { ContentBlock, InstructionBlock, ToolCallItem } from "../types/index.js";
|
|
13
5
|
|
|
14
6
|
export class NormalizedRequestMapper {
|
|
15
|
-
constructor(readonly
|
|
7
|
+
constructor(readonly kind: string) {}
|
|
16
8
|
|
|
17
9
|
mapInstructions(instructions: string | InstructionBlock[]): string {
|
|
18
10
|
return typeof instructions === "string"
|
|
@@ -21,27 +13,28 @@ export class NormalizedRequestMapper {
|
|
|
21
13
|
}
|
|
22
14
|
|
|
23
15
|
ensureTextBlocks(blocks: ContentBlock[], field: string): ContentBlock[] {
|
|
24
|
-
return this.ensureBlocks(blocks, field,
|
|
16
|
+
return this.ensureBlocks(blocks, field, ["text", "json"], "only text/json blocks are supported");
|
|
25
17
|
}
|
|
26
18
|
|
|
27
19
|
ensureReasoningBlocks(blocks: ContentBlock[], field: string): Array<Extract<ContentBlock, { type: "text" }>> {
|
|
28
|
-
return this.ensureBlocks(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.profile.reasoningBlockTypes,
|
|
32
|
-
"reasoning only supports text blocks",
|
|
33
|
-
) as Array<Extract<ContentBlock, { type: "text" }>>;
|
|
20
|
+
return this.ensureBlocks(blocks, field, ["text"], "reasoning only supports text blocks") as Array<
|
|
21
|
+
Extract<ContentBlock, { type: "text" }>
|
|
22
|
+
>;
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
parseToolArguments(item: ToolCallItem): Record<string, unknown> {
|
|
26
|
+
try {
|
|
27
|
+
const parsed: unknown = JSON.parse(item.argumentsText);
|
|
28
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
29
|
+
return parsed as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// handled below
|
|
33
|
+
}
|
|
38
34
|
|
|
39
|
-
const outcomes = this.profile.capabilities.toolResultOutcomes;
|
|
40
|
-
const supported = outcomes.map((value) => `"${value}"`).join(" and ");
|
|
41
|
-
const verb = outcomes.length > 1 ? "are" : "is";
|
|
42
35
|
throw new AIRequestError(
|
|
43
|
-
`${this.
|
|
44
|
-
"
|
|
36
|
+
`${this.kind} requires tool_call argumentsText to be a valid JSON object`,
|
|
37
|
+
"TOOL_CALL_ARGUMENTS_INVALID",
|
|
45
38
|
);
|
|
46
39
|
}
|
|
47
40
|
|
|
@@ -61,7 +54,7 @@ export class NormalizedRequestMapper {
|
|
|
61
54
|
const block = blocks[i];
|
|
62
55
|
if (block && !supportedTypes.includes(block.type)) {
|
|
63
56
|
throw new AIRequestError(
|
|
64
|
-
`${this.
|
|
57
|
+
`${this.kind} does not support ${field}[${i}] of type "${block.type}"; ${description}`,
|
|
65
58
|
"UNSUPPORTED_CONTENT_BLOCK",
|
|
66
59
|
);
|
|
67
60
|
}
|
package/src/types/adapter.ts
CHANGED
|
@@ -21,20 +21,9 @@ export type NormalizedRequest = AIRequest & {
|
|
|
21
21
|
|
|
22
22
|
// ── Adapter 接口 ──────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
export type StreamingCapability = "native" | "synthetic" | "none";
|
|
25
|
-
|
|
26
|
-
export type AdapterCapabilities = {
|
|
27
|
-
readonly textStreaming: StreamingCapability;
|
|
28
|
-
readonly reasoningStreaming: StreamingCapability;
|
|
29
|
-
readonly toolCallStreaming: StreamingCapability;
|
|
30
|
-
readonly replay: "canonical" | "opaque" | "none";
|
|
31
|
-
readonly usage: "stream" | "final" | "none";
|
|
32
|
-
readonly toolResultOutcomes: ReadonlyArray<"success" | "error" | "rejected">;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
24
|
export interface BackendAdapter {
|
|
36
25
|
readonly kind: "chat-completions" | "messages" | "responses" | "ollama" | "mock";
|
|
37
|
-
readonly
|
|
26
|
+
readonly isSyntheticStream: boolean;
|
|
38
27
|
stream(request: NormalizedRequest): AsyncIterable<AIStreamEvent>;
|
|
39
28
|
}
|
|
40
29
|
|
package/src/types/index.ts
CHANGED
|
@@ -46,12 +46,4 @@ export type {
|
|
|
46
46
|
} from "./events.js";
|
|
47
47
|
|
|
48
48
|
// Adapter 协议和 client 类型
|
|
49
|
-
export type {
|
|
50
|
-
AdapterCapabilities,
|
|
51
|
-
StreamingCapability,
|
|
52
|
-
BackendAdapter,
|
|
53
|
-
FetchFn,
|
|
54
|
-
NormalizedRequest,
|
|
55
|
-
CreateAIClientOptions,
|
|
56
|
-
AIClient,
|
|
57
|
-
} from "./adapter.js";
|
|
49
|
+
export type { BackendAdapter, FetchFn, NormalizedRequest, CreateAIClientOptions, AIClient } from "./adapter.js";
|