@codehz/ai 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.d.mts +32 -8
- package/dist/index.mjs +460 -528
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -92,6 +92,60 @@ function supportsContextCompress(adapter) {
|
|
|
92
92
|
return typeof adapter.compress === "function";
|
|
93
93
|
}
|
|
94
94
|
//#endregion
|
|
95
|
+
//#region src/runtime/normalize.ts
|
|
96
|
+
const DEFAULT_INCLUDE = {
|
|
97
|
+
usage: "best_effort",
|
|
98
|
+
billing: "best_effort",
|
|
99
|
+
providerMetadata: "best_effort"
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* 归一化请求:
|
|
103
|
+
* 1. 合并 defaults
|
|
104
|
+
* 2. 填充 include 默认值
|
|
105
|
+
* 3. 生成 requestId
|
|
106
|
+
* 4. 返回 provider 映射所需的归一化请求
|
|
107
|
+
*/
|
|
108
|
+
function normalizeRequest(request, options) {
|
|
109
|
+
const { model, defaults } = options;
|
|
110
|
+
return {
|
|
111
|
+
...defaults,
|
|
112
|
+
...request,
|
|
113
|
+
include: {
|
|
114
|
+
...DEFAULT_INCLUDE,
|
|
115
|
+
...defaults?.include,
|
|
116
|
+
...request.include
|
|
117
|
+
},
|
|
118
|
+
model,
|
|
119
|
+
requestId: crypto.randomUUID()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/runtime/client.ts
|
|
124
|
+
function createAIClient(options) {
|
|
125
|
+
const { adapter, model, defaults, signal: defaultSignal } = options;
|
|
126
|
+
return { stream(request) {
|
|
127
|
+
const signal = mergeAbortSignals(defaultSignal, request.signal);
|
|
128
|
+
const normalized = normalizeRequest({
|
|
129
|
+
...request,
|
|
130
|
+
signal
|
|
131
|
+
}, {
|
|
132
|
+
model,
|
|
133
|
+
defaults
|
|
134
|
+
});
|
|
135
|
+
return adapter.stream(normalized);
|
|
136
|
+
} };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 合并多个 AbortSignal:任一 signal abort 即触发。
|
|
140
|
+
* 如果没有 signal 需要合并则返回 undefined。
|
|
141
|
+
*/
|
|
142
|
+
function mergeAbortSignals(...signals) {
|
|
143
|
+
const valid = signals.filter((s) => s != null);
|
|
144
|
+
if (valid.length === 0) return void 0;
|
|
145
|
+
if (valid.length === 1) return valid[0];
|
|
146
|
+
return AbortSignal.any(valid);
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
95
149
|
//#region src/runtime/errors.ts
|
|
96
150
|
var AIError = class extends Error {
|
|
97
151
|
code;
|
|
@@ -165,369 +219,6 @@ var AIRecoverableError = class extends AIError {
|
|
|
165
219
|
}
|
|
166
220
|
};
|
|
167
221
|
//#endregion
|
|
168
|
-
//#region src/runtime/validation.ts
|
|
169
|
-
const MESSAGE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
170
|
-
const REASONING_VISIBILITIES = /* @__PURE__ */ new Set([
|
|
171
|
-
"full",
|
|
172
|
-
"summary",
|
|
173
|
-
"redacted",
|
|
174
|
-
"opaque"
|
|
175
|
-
]);
|
|
176
|
-
const TOOL_RESULT_OUTCOMES = /* @__PURE__ */ new Set([
|
|
177
|
-
"success",
|
|
178
|
-
"error",
|
|
179
|
-
"rejected"
|
|
180
|
-
]);
|
|
181
|
-
const INCLUDE_MODES = /* @__PURE__ */ new Set(["off", "best_effort"]);
|
|
182
|
-
function isRecord(value) {
|
|
183
|
-
return typeof value === "object" && value !== null;
|
|
184
|
-
}
|
|
185
|
-
function pushIssue(issues, field, code, message) {
|
|
186
|
-
issues.push({
|
|
187
|
-
field,
|
|
188
|
-
code,
|
|
189
|
-
message
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
function validateContentBlock(block, field, issues) {
|
|
193
|
-
if (!isRecord(block) || typeof block.type !== "string") {
|
|
194
|
-
pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field} must be a valid ContentBlock`);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
switch (block.type) {
|
|
198
|
-
case "text":
|
|
199
|
-
if (typeof block.text !== "string") pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.text must be a string`);
|
|
200
|
-
return;
|
|
201
|
-
case "json":
|
|
202
|
-
if (!("json" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.json must be present`);
|
|
203
|
-
return;
|
|
204
|
-
case "image":
|
|
205
|
-
if (typeof block.imageUrl !== "string" || block.imageUrl.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.imageUrl must be a non-empty string`);
|
|
206
|
-
return;
|
|
207
|
-
case "binary_ref":
|
|
208
|
-
if (typeof block.ref !== "string" || block.ref.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.ref must be a non-empty string`);
|
|
209
|
-
return;
|
|
210
|
-
case "opaque":
|
|
211
|
-
if (!("payload" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.payload must be present`);
|
|
212
|
-
return;
|
|
213
|
-
default: pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.type "${block.type}" is not supported`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function validateContentArray(content, field, issues, code) {
|
|
217
|
-
if (!Array.isArray(content)) {
|
|
218
|
-
pushIssue(issues, field, code, `${field} must be a ContentBlock[]`);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
for (let i = 0; i < content.length; i++) validateContentBlock(content[i], `${field}[${i}]`, issues);
|
|
222
|
-
}
|
|
223
|
-
function validateInstructionArray(content, field, issues) {
|
|
224
|
-
if (!Array.isArray(content)) {
|
|
225
|
-
pushIssue(issues, field, "INSTRUCTIONS_INVALID", `${field} must be an InstructionBlock[]`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
for (let i = 0; i < content.length; i++) {
|
|
229
|
-
const block = content[i];
|
|
230
|
-
const blockField = `${field}[${i}]`;
|
|
231
|
-
validateContentBlock(block, blockField, issues);
|
|
232
|
-
if (!isRecord(block) || typeof block.type !== "string") continue;
|
|
233
|
-
if (block.type !== "text" && block.type !== "json") pushIssue(issues, blockField, "INSTRUCTIONS_INVALID", `${blockField} only supports text/json blocks`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function validateInputItem(item, field, issues) {
|
|
237
|
-
if (!isRecord(item)) {
|
|
238
|
-
pushIssue(issues, field, "INPUT_INVALID_ITEM", `${field} must be a valid InputItem`);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (typeof item.type !== "string") {
|
|
242
|
-
pushIssue(issues, field, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type must be a supported InputItem type`);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
switch (item.type) {
|
|
246
|
-
case "message":
|
|
247
|
-
if (typeof item.role !== "string" || !MESSAGE_ROLES.has(item.role)) pushIssue(issues, `${field}.role`, "MESSAGE_ROLE_INVALID", `${field}.role must be a valid message role`);
|
|
248
|
-
validateContentArray(item.content, `${field}.content`, issues, "MESSAGE_CONTENT_INVALID");
|
|
249
|
-
return;
|
|
250
|
-
case "reasoning":
|
|
251
|
-
if (typeof item.visibility !== "string" || !REASONING_VISIBILITIES.has(item.visibility)) pushIssue(issues, `${field}.visibility`, "REASONING_VISIBILITY_INVALID", `${field}.visibility must be a valid reasoning visibility`);
|
|
252
|
-
validateContentArray(item.content, `${field}.content`, issues, "REASONING_CONTENT_INVALID");
|
|
253
|
-
return;
|
|
254
|
-
case "server_tool_call":
|
|
255
|
-
if (typeof item.id !== "string" || item.id.length === 0) pushIssue(issues, `${field}.id`, "SERVER_TOOL_CALL_INVALID", `${field}.id must be a non-empty string`);
|
|
256
|
-
if (typeof item.tool !== "string" || item.tool.length === 0) pushIssue(issues, `${field}.tool`, "SERVER_TOOL_CALL_INVALID", `${field}.tool must be a non-empty string`);
|
|
257
|
-
if (item.argumentsText !== void 0 && typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "SERVER_TOOL_CALL_INVALID", `${field}.argumentsText must be a string`);
|
|
258
|
-
return;
|
|
259
|
-
case "server_tool_result":
|
|
260
|
-
if (typeof item.callId !== "string" || item.callId.length === 0) pushIssue(issues, `${field}.callId`, "SERVER_TOOL_RESULT_INVALID", `${field}.callId must be a non-empty string`);
|
|
261
|
-
if (typeof item.tool !== "string" || item.tool.length === 0) pushIssue(issues, `${field}.tool`, "SERVER_TOOL_RESULT_INVALID", `${field}.tool must be a non-empty string`);
|
|
262
|
-
if (item.outcome !== "success" && item.outcome !== "error") pushIssue(issues, `${field}.outcome`, "SERVER_TOOL_RESULT_INVALID", `${field}.outcome must be success or error`);
|
|
263
|
-
validateContentArray(item.content, `${field}.content`, issues, "SERVER_TOOL_RESULT_INVALID");
|
|
264
|
-
return;
|
|
265
|
-
case "server_tool_discovery":
|
|
266
|
-
if (typeof item.id !== "string" || item.id.length === 0) pushIssue(issues, `${field}.id`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.id must be a non-empty string`);
|
|
267
|
-
if (item.tool !== "mcp") pushIssue(issues, `${field}.tool`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.tool must be "mcp"`);
|
|
268
|
-
if (typeof item.serverLabel !== "string" || item.serverLabel.length === 0) pushIssue(issues, `${field}.serverLabel`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.serverLabel must be a non-empty string`);
|
|
269
|
-
if (!Array.isArray(item.tools)) pushIssue(issues, `${field}.tools`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.tools must be an array`);
|
|
270
|
-
return;
|
|
271
|
-
case "tool_call":
|
|
272
|
-
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`);
|
|
273
|
-
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`);
|
|
274
|
-
if (typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must be a string`);
|
|
275
|
-
return;
|
|
276
|
-
case "tool_result":
|
|
277
|
-
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`);
|
|
278
|
-
if (typeof item.toolName !== "string" || item.toolName.length === 0) pushIssue(issues, `${field}.toolName`, "TOOL_RESULT_NAME_INVALID", `${field}.toolName must be a non-empty string`);
|
|
279
|
-
if (typeof item.outcome !== "string" || !TOOL_RESULT_OUTCOMES.has(item.outcome)) pushIssue(issues, `${field}.outcome`, "TOOL_RESULT_OUTCOME_INVALID", `${field}.outcome must be success, error, or rejected`);
|
|
280
|
-
validateContentArray(item.content, `${field}.content`, issues, "TOOL_RESULT_CONTENT_INVALID");
|
|
281
|
-
return;
|
|
282
|
-
case "opaque":
|
|
283
|
-
if (typeof item.source !== "string" || item.source.length === 0) pushIssue(issues, `${field}.source`, "OPAQUE_SOURCE_INVALID", `${field}.source must be a non-empty string`);
|
|
284
|
-
if (typeof item.purpose !== "string" || item.purpose.length === 0) pushIssue(issues, `${field}.purpose`, "OPAQUE_PURPOSE_INVALID", `${field}.purpose must be a non-empty string`);
|
|
285
|
-
return;
|
|
286
|
-
default: pushIssue(issues, `${field}.type`, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type "${item.type}" is not supported`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
function validateTools(tools, issues) {
|
|
290
|
-
if (tools === void 0) return;
|
|
291
|
-
if (!Array.isArray(tools)) {
|
|
292
|
-
pushIssue(issues, "tools", "TOOLS_INVALID", "tools must be an array");
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const seenNames = /* @__PURE__ */ new Set();
|
|
296
|
-
for (let i = 0; i < tools.length; i++) {
|
|
297
|
-
const tool = tools[i];
|
|
298
|
-
const field = `tools[${i}]`;
|
|
299
|
-
if (!isRecord(tool)) {
|
|
300
|
-
pushIssue(issues, field, "TOOL_INVALID", `${field} must be a valid ToolDefinition`);
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
if (typeof tool.name !== "string" || tool.name.length === 0) pushIssue(issues, `${field}.name`, "TOOL_NAME_INVALID", `${field}.name must be a non-empty string`);
|
|
304
|
-
else {
|
|
305
|
-
if (seenNames.has(tool.name)) pushIssue(issues, `${field}.name`, "TOOLS_DUPLICATE_NAME", `tool name "${tool.name}" is duplicated`);
|
|
306
|
-
seenNames.add(tool.name);
|
|
307
|
-
}
|
|
308
|
-
if (tool.description !== void 0 && typeof tool.description !== "string") pushIssue(issues, `${field}.description`, "TOOL_DESCRIPTION_INVALID", `${field}.description must be a string`);
|
|
309
|
-
if (!isRecord(tool.inputSchema)) pushIssue(issues, `${field}.inputSchema`, "TOOL_INPUT_SCHEMA_INVALID", `${field}.inputSchema must be an object`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
const SEARCH_CONTEXT_SIZES = /* @__PURE__ */ new Set([
|
|
313
|
-
"low",
|
|
314
|
-
"medium",
|
|
315
|
-
"high"
|
|
316
|
-
]);
|
|
317
|
-
const CODE_MEMORY_LIMITS = /* @__PURE__ */ new Set([
|
|
318
|
-
"1g",
|
|
319
|
-
"4g",
|
|
320
|
-
"16g",
|
|
321
|
-
"64g"
|
|
322
|
-
]);
|
|
323
|
-
function validateStringArrayField(value, field, code, issues) {
|
|
324
|
-
if (!Array.isArray(value)) {
|
|
325
|
-
pushIssue(issues, field, code, `${field} must be a string array`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
for (let i = 0; i < value.length; i++) if (typeof value[i] !== "string" || value[i].length === 0) pushIssue(issues, `${field}[${i}]`, code, `${field}[${i}] must be a non-empty string`);
|
|
329
|
-
}
|
|
330
|
-
function validateServerTools(serverTools, issues) {
|
|
331
|
-
if (serverTools === void 0) return;
|
|
332
|
-
if (!Array.isArray(serverTools)) {
|
|
333
|
-
pushIssue(issues, "serverTools", "SERVER_TOOLS_INVALID", "serverTools must be an array");
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
for (let i = 0; i < serverTools.length; i++) {
|
|
337
|
-
const tool = serverTools[i];
|
|
338
|
-
const field = `serverTools[${i}]`;
|
|
339
|
-
if (!isRecord(tool) || typeof tool.type !== "string") {
|
|
340
|
-
pushIssue(issues, field, "SERVER_TOOL_INVALID", `${field} must be a valid ServerToolDefinition`);
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
switch (tool.type) {
|
|
344
|
-
case "web_search":
|
|
345
|
-
if (tool.allowedDomains !== void 0 && tool.blockedDomains !== void 0) pushIssue(issues, field, "SERVER_TOOL_WEB_SEARCH_DOMAINS_CONFLICT", `${field} cannot set both allowedDomains and blockedDomains`);
|
|
346
|
-
if (tool.allowedDomains !== void 0) validateStringArrayField(tool.allowedDomains, `${field}.allowedDomains`, "SERVER_TOOL_INVALID", issues);
|
|
347
|
-
if (tool.blockedDomains !== void 0) validateStringArrayField(tool.blockedDomains, `${field}.blockedDomains`, "SERVER_TOOL_INVALID", issues);
|
|
348
|
-
if (tool.searchContextSize !== void 0) {
|
|
349
|
-
if (typeof tool.searchContextSize !== "string" || !SEARCH_CONTEXT_SIZES.has(tool.searchContextSize)) pushIssue(issues, `${field}.searchContextSize`, "SERVER_TOOL_INVALID", `${field}.searchContextSize must be low, medium, or high`);
|
|
350
|
-
}
|
|
351
|
-
if (tool.userLocation !== void 0) if (!isRecord(tool.userLocation) || tool.userLocation.type !== "approximate") pushIssue(issues, `${field}.userLocation`, "SERVER_TOOL_INVALID", `${field}.userLocation must be { type: "approximate", ... }`);
|
|
352
|
-
else for (const key of [
|
|
353
|
-
"country",
|
|
354
|
-
"city",
|
|
355
|
-
"region",
|
|
356
|
-
"timezone"
|
|
357
|
-
]) {
|
|
358
|
-
const value = tool.userLocation[key];
|
|
359
|
-
if (value !== void 0 && typeof value !== "string") pushIssue(issues, `${field}.userLocation.${key}`, "SERVER_TOOL_INVALID", `${field}.userLocation.${key} must be a string`);
|
|
360
|
-
}
|
|
361
|
-
break;
|
|
362
|
-
case "code_execution":
|
|
363
|
-
if (tool.container !== void 0) if (!isRecord(tool.container) || tool.container.type !== "auto") pushIssue(issues, `${field}.container`, "SERVER_TOOL_INVALID", `${field}.container must be { type: "auto", ... }`);
|
|
364
|
-
else {
|
|
365
|
-
if (tool.container.memoryLimit !== void 0) {
|
|
366
|
-
if (typeof tool.container.memoryLimit !== "string" || !CODE_MEMORY_LIMITS.has(tool.container.memoryLimit)) pushIssue(issues, `${field}.container.memoryLimit`, "SERVER_TOOL_INVALID", `${field}.container.memoryLimit must be 1g, 4g, 16g, or 64g`);
|
|
367
|
-
}
|
|
368
|
-
if (tool.container.fileIds !== void 0) validateStringArrayField(tool.container.fileIds, `${field}.container.fileIds`, "SERVER_TOOL_INVALID", issues);
|
|
369
|
-
}
|
|
370
|
-
break;
|
|
371
|
-
case "mcp":
|
|
372
|
-
if (typeof tool.serverLabel !== "string" || tool.serverLabel.length === 0) pushIssue(issues, `${field}.serverLabel`, "SERVER_TOOL_INVALID", `${field}.serverLabel must be a non-empty string`);
|
|
373
|
-
if (typeof tool.serverUrl !== "string" || tool.serverUrl.length === 0) pushIssue(issues, `${field}.serverUrl`, "SERVER_TOOL_INVALID", `${field}.serverUrl must be a non-empty string`);
|
|
374
|
-
if (tool.serverDescription !== void 0 && typeof tool.serverDescription !== "string") pushIssue(issues, `${field}.serverDescription`, "SERVER_TOOL_INVALID", `${field}.serverDescription must be a string`);
|
|
375
|
-
if (tool.authorization !== void 0 && typeof tool.authorization !== "string") pushIssue(issues, `${field}.authorization`, "SERVER_TOOL_INVALID", `${field}.authorization must be a string`);
|
|
376
|
-
if (tool.allowedTools !== void 0) validateStringArrayField(tool.allowedTools, `${field}.allowedTools`, "SERVER_TOOL_INVALID", issues);
|
|
377
|
-
if (tool.requireApproval !== "never") pushIssue(issues, `${field}.requireApproval`, "SERVER_TOOL_MCP_APPROVAL_UNSUPPORTED", `${field}.requireApproval must be "never" in this version`);
|
|
378
|
-
break;
|
|
379
|
-
default: pushIssue(issues, `${field}.type`, "SERVER_TOOL_TYPE_UNSUPPORTED", `${field}.type "${tool.type}" is not supported`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
function validateToolChoice(toolChoice, issues) {
|
|
384
|
-
if (toolChoice === void 0) return;
|
|
385
|
-
if (toolChoice === "auto" || toolChoice === "none") return;
|
|
386
|
-
if (!isRecord(toolChoice) || toolChoice.type !== "tool" || typeof toolChoice.name !== "string" || toolChoice.name.length === 0) pushIssue(issues, "toolChoice", "TOOL_CHOICE_INVALID", "toolChoice must be auto, none, or { type: \"tool\", name }");
|
|
387
|
-
}
|
|
388
|
-
/** Validate include settings, appending issues to the given array. */
|
|
389
|
-
function validateInclude(include, issues) {
|
|
390
|
-
if (!isRecord(include)) {
|
|
391
|
-
pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (include.usage !== void 0 && (typeof include.usage !== "string" || !INCLUDE_MODES.has(include.usage))) pushIssue(issues, "include.usage", "INCLUDE_USAGE_INVALID", "include.usage must be off or best_effort");
|
|
395
|
-
if (include.billing !== void 0 && (typeof include.billing !== "string" || !INCLUDE_MODES.has(include.billing))) pushIssue(issues, "include.billing", "INCLUDE_BILLING_INVALID", "include.billing must be off or best_effort");
|
|
396
|
-
if (include.providerMetadata !== void 0 && (typeof include.providerMetadata !== "string" || !INCLUDE_MODES.has(include.providerMetadata))) pushIssue(issues, "include.providerMetadata", "INCLUDE_PROVIDER_METADATA_INVALID", "include.providerMetadata must be off or best_effort");
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* 校验 AIRequest,返回校验问题列表。
|
|
400
|
-
* 空数组表示无问题。
|
|
401
|
-
*/
|
|
402
|
-
function validateRequest(request) {
|
|
403
|
-
const issues = [];
|
|
404
|
-
if (request.instructions !== void 0) if (typeof request.instructions === "string") {} else if (Array.isArray(request.instructions)) validateInstructionArray(request.instructions, "instructions", issues);
|
|
405
|
-
else pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or InstructionBlock[]");
|
|
406
|
-
if (!Array.isArray(request.input) || request.input.length === 0) pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
|
|
407
|
-
if (Array.isArray(request.input)) for (let i = 0; i < request.input.length; i++) validateInputItem(request.input[i], `input[${i}]`, issues);
|
|
408
|
-
if (request.temperature !== void 0) {
|
|
409
|
-
if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) issues.push({
|
|
410
|
-
field: "temperature",
|
|
411
|
-
code: "TEMPERATURE_NOT_NUMBER",
|
|
412
|
-
message: "temperature must be a number"
|
|
413
|
-
});
|
|
414
|
-
else if (request.temperature < 0 || request.temperature > 2) issues.push({
|
|
415
|
-
field: "temperature",
|
|
416
|
-
code: "TEMPERATURE_OUT_OF_RANGE",
|
|
417
|
-
message: "temperature must be between 0 and 2"
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
if (request.maxOutputTokens !== void 0) {
|
|
421
|
-
if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) issues.push({
|
|
422
|
-
field: "maxOutputTokens",
|
|
423
|
-
code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
|
|
424
|
-
message: "maxOutputTokens must be a number"
|
|
425
|
-
});
|
|
426
|
-
else if (!Number.isInteger(request.maxOutputTokens) || request.maxOutputTokens < 1) issues.push({
|
|
427
|
-
field: "maxOutputTokens",
|
|
428
|
-
code: "MAX_OUTPUT_TOKENS_INVALID",
|
|
429
|
-
message: "maxOutputTokens must be a positive integer"
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
if (request.reasoningLevel !== void 0) {
|
|
433
|
-
if (typeof request.reasoningLevel !== "string" || !REASONING_LEVEL_SET.has(request.reasoningLevel)) pushIssue(issues, "reasoningLevel", "REASONING_LEVEL_INVALID", "reasoningLevel must be one of: none, minimal, low, medium, high, xhigh, max");
|
|
434
|
-
}
|
|
435
|
-
if (request.include !== void 0) validateInclude(request.include, issues);
|
|
436
|
-
if (request.metadata !== void 0) {
|
|
437
|
-
if (!isRecord(request.metadata)) pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
|
|
438
|
-
else for (const [key, value] of Object.entries(request.metadata)) if (typeof value !== "string") pushIssue(issues, `metadata.${key}`, "METADATA_VALUE_INVALID", `metadata.${key} must be a string`);
|
|
439
|
-
}
|
|
440
|
-
validateTools(request.tools, issues);
|
|
441
|
-
validateServerTools(request.serverTools, issues);
|
|
442
|
-
validateToolChoice(request.toolChoice, issues);
|
|
443
|
-
if (request.toolChoice && typeof request.toolChoice === "object" && "type" in request.toolChoice && request.toolChoice.type === "tool") {
|
|
444
|
-
const chosenName = request.toolChoice.name;
|
|
445
|
-
if (!request.tools || request.tools.length === 0) issues.push({
|
|
446
|
-
field: "toolChoice",
|
|
447
|
-
code: "TOOL_CHOICE_NO_TOOLS",
|
|
448
|
-
message: `toolChoice specifies tool "${chosenName}" but no tools are defined`
|
|
449
|
-
});
|
|
450
|
-
else if (!request.tools.some((t) => t.name === chosenName)) issues.push({
|
|
451
|
-
field: "toolChoice",
|
|
452
|
-
code: "TOOL_CHOICE_UNKNOWN_TOOL",
|
|
453
|
-
message: `toolChoice specifies tool "${chosenName}" which is not in tools array`
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
return issues;
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* 校验请求并抛出首个问题。
|
|
460
|
-
* 适用于客户端入口的快速失败检查。
|
|
461
|
-
*/
|
|
462
|
-
function assertValidRequest(request) {
|
|
463
|
-
const issues = validateRequest(request);
|
|
464
|
-
const first = issues[0];
|
|
465
|
-
if (first) throw new AIRequestError(first.message, first.code, issues);
|
|
466
|
-
}
|
|
467
|
-
//#endregion
|
|
468
|
-
//#region src/runtime/normalize.ts
|
|
469
|
-
const DEFAULT_INCLUDE = {
|
|
470
|
-
usage: "best_effort",
|
|
471
|
-
billing: "best_effort",
|
|
472
|
-
providerMetadata: "best_effort"
|
|
473
|
-
};
|
|
474
|
-
/**
|
|
475
|
-
* 归一化请求:
|
|
476
|
-
* 1. 合并 defaults
|
|
477
|
-
* 2. 填充 include 默认值
|
|
478
|
-
* 3. 生成 requestId
|
|
479
|
-
* 4. 校验请求合法性
|
|
480
|
-
*/
|
|
481
|
-
function normalizeRequest(request, options) {
|
|
482
|
-
const { model, defaults } = options;
|
|
483
|
-
const earlyIncludeIssues = [];
|
|
484
|
-
if (request.include !== void 0) validateInclude(request.include, earlyIncludeIssues);
|
|
485
|
-
if (defaults?.include !== void 0) validateInclude(defaults.include, earlyIncludeIssues);
|
|
486
|
-
const firstIncludeIssue = earlyIncludeIssues[0];
|
|
487
|
-
if (firstIncludeIssue) throw new AIRequestError(firstIncludeIssue.message, firstIncludeIssue.code, earlyIncludeIssues);
|
|
488
|
-
const merged = {
|
|
489
|
-
...defaults,
|
|
490
|
-
...request,
|
|
491
|
-
include: {
|
|
492
|
-
...DEFAULT_INCLUDE,
|
|
493
|
-
...defaults?.include,
|
|
494
|
-
...request.include
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
assertValidRequest(merged);
|
|
498
|
-
return {
|
|
499
|
-
...merged,
|
|
500
|
-
model,
|
|
501
|
-
requestId: crypto.randomUUID()
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
//#endregion
|
|
505
|
-
//#region src/runtime/client.ts
|
|
506
|
-
function createAIClient(options) {
|
|
507
|
-
const { adapter, model, defaults, signal: defaultSignal } = options;
|
|
508
|
-
return { stream(request) {
|
|
509
|
-
const signal = mergeAbortSignals(defaultSignal, request.signal);
|
|
510
|
-
const normalized = normalizeRequest({
|
|
511
|
-
...request,
|
|
512
|
-
signal
|
|
513
|
-
}, {
|
|
514
|
-
model,
|
|
515
|
-
defaults
|
|
516
|
-
});
|
|
517
|
-
return adapter.stream(normalized);
|
|
518
|
-
} };
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* 合并多个 AbortSignal:任一 signal abort 即触发。
|
|
522
|
-
* 如果没有 signal 需要合并则返回 undefined。
|
|
523
|
-
*/
|
|
524
|
-
function mergeAbortSignals(...signals) {
|
|
525
|
-
const valid = signals.filter((s) => s != null);
|
|
526
|
-
if (valid.length === 0) return void 0;
|
|
527
|
-
if (valid.length === 1) return valid[0];
|
|
528
|
-
return AbortSignal.any(valid);
|
|
529
|
-
}
|
|
530
|
-
//#endregion
|
|
531
222
|
//#region src/canonical/content.ts
|
|
532
223
|
function textBlock(text) {
|
|
533
224
|
return {
|
|
@@ -1555,70 +1246,27 @@ function applyExtraBody(body, extraBody) {
|
|
|
1555
1246
|
...extraBody
|
|
1556
1247
|
};
|
|
1557
1248
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
* - provider HTTP 错误 body 出站脱敏
|
|
1565
|
-
*/
|
|
1566
|
-
/** 默认单条 opaque payload 上限(JSON.stringify 的 UTF-16 码元长度)。 */
|
|
1567
|
-
const DEFAULT_MAX_OPAQUE_PAYLOAD_BYTES = 1 * 1024 * 1024;
|
|
1568
|
-
/** 硬顶:配置不可超过;挡住离谱 blob / DoS。 */
|
|
1569
|
-
const HARD_MAX_OPAQUE_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
1570
|
-
/**
|
|
1571
|
-
* 将调用方配置的 opaque 上限夹到合法区间。
|
|
1572
|
-
* 非有限 / <1 → 默认;> HARD → HARD。
|
|
1573
|
-
*/
|
|
1574
|
-
function clampOpaquePayloadLimit(maxBytes) {
|
|
1575
|
-
if (maxBytes === void 0 || !Number.isFinite(maxBytes)) return DEFAULT_MAX_OPAQUE_PAYLOAD_BYTES;
|
|
1576
|
-
const n = Math.floor(maxBytes);
|
|
1577
|
-
if (n < 1) return DEFAULT_MAX_OPAQUE_PAYLOAD_BYTES;
|
|
1578
|
-
return Math.min(n, HARD_MAX_OPAQUE_PAYLOAD_BYTES);
|
|
1579
|
-
}
|
|
1580
|
-
/** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
|
|
1581
|
-
function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1582
|
-
if (value === null || typeof value !== "object") return 0;
|
|
1583
|
-
if (seen.has(value)) return 0;
|
|
1584
|
-
seen.add(value);
|
|
1585
|
-
let maxChild = 0;
|
|
1586
|
-
if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
|
|
1587
|
-
else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
|
|
1588
|
-
return 1 + maxChild;
|
|
1589
|
-
}
|
|
1590
|
-
/**
|
|
1591
|
-
* Opaque replay 通用 envelope:必须是 object、体积 ≤ limit、深度 ≤ 8。
|
|
1592
|
-
* 不校验 adapter 专用字段形状。emit / accept 共用。
|
|
1593
|
-
*/
|
|
1594
|
-
function validateOpaqueReplayEnvelope(payload, options) {
|
|
1249
|
+
Number.POSITIVE_INFINITY;
|
|
1250
|
+
Number.POSITIVE_INFINITY;
|
|
1251
|
+
Number.POSITIVE_INFINITY;
|
|
1252
|
+
Number.POSITIVE_INFINITY;
|
|
1253
|
+
/** Opaque payload 必须是 object 且可被 JSON 序列化;不限制大小或深度。 */
|
|
1254
|
+
function validateOpaqueReplayEnvelope(payload, _options) {
|
|
1595
1255
|
if (typeof payload !== "object" || payload === null) return {
|
|
1596
1256
|
ok: false,
|
|
1597
1257
|
reason: "payload must be an object"
|
|
1598
1258
|
};
|
|
1599
|
-
let raw;
|
|
1600
1259
|
try {
|
|
1601
|
-
|
|
1260
|
+
if (JSON.stringify(payload) === void 0) return {
|
|
1261
|
+
ok: false,
|
|
1262
|
+
reason: "payload is not JSON-serializable"
|
|
1263
|
+
};
|
|
1602
1264
|
} catch {
|
|
1603
1265
|
return {
|
|
1604
1266
|
ok: false,
|
|
1605
1267
|
reason: "payload is not JSON-serializable"
|
|
1606
1268
|
};
|
|
1607
1269
|
}
|
|
1608
|
-
if (raw === void 0) return {
|
|
1609
|
-
ok: false,
|
|
1610
|
-
reason: "payload is not JSON-serializable"
|
|
1611
|
-
};
|
|
1612
|
-
const maxBytes = clampOpaquePayloadLimit(options?.maxBytes);
|
|
1613
|
-
if (raw.length > maxBytes) return {
|
|
1614
|
-
ok: false,
|
|
1615
|
-
reason: `opaque payload exceeds max size (${raw.length} > ${maxBytes})`
|
|
1616
|
-
};
|
|
1617
|
-
const depth = measureJsonDepth(payload);
|
|
1618
|
-
if (depth > 8) return {
|
|
1619
|
-
ok: false,
|
|
1620
|
-
reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
|
|
1621
|
-
};
|
|
1622
1270
|
return { ok: true };
|
|
1623
1271
|
}
|
|
1624
1272
|
/** envelope 失败时抛 AIRequestError(入站 accept 路径)。 */
|
|
@@ -1854,7 +1502,7 @@ var HttpAdapterBase = class extends AdapterBase {
|
|
|
1854
1502
|
fetchFn;
|
|
1855
1503
|
headers;
|
|
1856
1504
|
extraBody;
|
|
1857
|
-
/**
|
|
1505
|
+
/** Deprecated compatibility field; opaque replay is not size-limited. */
|
|
1858
1506
|
maxOpaquePayloadBytes;
|
|
1859
1507
|
constructor(options, defaults) {
|
|
1860
1508
|
super();
|
|
@@ -1863,7 +1511,7 @@ var HttpAdapterBase = class extends AdapterBase {
|
|
|
1863
1511
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
1864
1512
|
this.headers = options.headers;
|
|
1865
1513
|
this.extraBody = options.extraBody;
|
|
1866
|
-
this.maxOpaquePayloadBytes =
|
|
1514
|
+
this.maxOpaquePayloadBytes = options.maxOpaquePayloadBytes ?? Number.POSITIVE_INFINITY;
|
|
1867
1515
|
}
|
|
1868
1516
|
/** 合并内置 headers 与构造期自定义 headers。 */
|
|
1869
1517
|
mergeHeaders(base) {
|
|
@@ -2149,10 +1797,8 @@ function createNdjsonLineParser(isValid) {
|
|
|
2149
1797
|
//#region src/provider/finalize-stream-turn.ts
|
|
2150
1798
|
/**
|
|
2151
1799
|
* 收敛 incomplete / finish 后的 replay + complete 路径。
|
|
2152
|
-
* adapter 负责构造 opaque payload;本 helper
|
|
2153
|
-
*
|
|
2154
|
-
* emit 与 accept 共用 envelope 上限:超限则省略 opaque(不截断)并打 warning,
|
|
2155
|
-
* 避免写出下一轮 accept 必炸的自产毒。
|
|
1800
|
+
* adapter 负责构造 opaque payload;本 helper 负责拼接 replay 并 complete。
|
|
1801
|
+
* opaque payload 不在客户端库内截断或施加大小 / 深度限制。
|
|
2156
1802
|
*/
|
|
2157
1803
|
/**
|
|
2158
1804
|
* 从 item session 生成 canonical replay,可选追加 opaque 尾项,再 yield session.complete。
|
|
@@ -2161,13 +1807,6 @@ function createNdjsonLineParser(isValid) {
|
|
|
2161
1807
|
async function* finalizeStreamTurn(session, items, options = {}) {
|
|
2162
1808
|
const replay = [...replayFromOutput(items.completedItems())];
|
|
2163
1809
|
let opaque = options.opaque ?? null;
|
|
2164
|
-
if (opaque) {
|
|
2165
|
-
const check = validateOpaqueReplayEnvelope(opaque.payload, { maxBytes: options.maxOpaquePayloadBytes });
|
|
2166
|
-
if (!check.ok) {
|
|
2167
|
-
if (options.factory) yield options.factory.responseWarning(`Omitted opaque replay payload: ${check.reason}`, WarningCode.OPAQUE_REPLAY_OMITTED);
|
|
2168
|
-
opaque = null;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
1810
|
if (opaque) replay.push(opaque);
|
|
2172
1811
|
yield* session.complete({
|
|
2173
1812
|
replay,
|
|
@@ -2429,7 +2068,7 @@ function mapServerTools(serverTools) {
|
|
|
2429
2068
|
*
|
|
2430
2069
|
* 同时服务 stream(/responses)与 compress(/responses/compact)。
|
|
2431
2070
|
*/
|
|
2432
|
-
const mapper$
|
|
2071
|
+
const mapper$7 = new NormalizedRequestMapper("responses");
|
|
2433
2072
|
/** compact replay opaque:整份 wire output window 保真回传 */
|
|
2434
2073
|
const RESPONSES_COMPACTED_WINDOW_KIND = "compacted_window";
|
|
2435
2074
|
function isReplayCanonicalInput(item) {
|
|
@@ -2442,12 +2081,61 @@ function readNonEmptyString(value, maxLen = 256) {
|
|
|
2442
2081
|
if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return void 0;
|
|
2443
2082
|
return value;
|
|
2444
2083
|
}
|
|
2445
|
-
/**
|
|
2446
|
-
|
|
2447
|
-
|
|
2084
|
+
/**
|
|
2085
|
+
* EasyInputMessage content:
|
|
2086
|
+
* - 非 user:继续 text/json → string
|
|
2087
|
+
* - user 且含 image:input_text / input_image parts(不发 detail)
|
|
2088
|
+
* - user 纯 text/json:仍发 string,避免无谓 shape churn
|
|
2089
|
+
*/
|
|
2090
|
+
function mapResponsesMessageContent(role, blocks, field) {
|
|
2091
|
+
if (role !== "user") return mapper$7.textFromBlocks(blocks, field);
|
|
2092
|
+
mapper$7.ensureBlocks(blocks, field, [
|
|
2093
|
+
"text",
|
|
2094
|
+
"json",
|
|
2095
|
+
"image"
|
|
2096
|
+
], "only text/json/image blocks are supported");
|
|
2097
|
+
if (!blocks.some((block) => block.type === "image")) return mapper$7.textFromBlocks(blocks, field);
|
|
2098
|
+
return blocks.map((block) => {
|
|
2099
|
+
if (block.type === "text") return {
|
|
2100
|
+
type: "input_text",
|
|
2101
|
+
text: block.text
|
|
2102
|
+
};
|
|
2103
|
+
if (block.type === "json") return {
|
|
2104
|
+
type: "input_text",
|
|
2105
|
+
text: JSON.stringify(block.json)
|
|
2106
|
+
};
|
|
2107
|
+
if (block.type === "image") return {
|
|
2108
|
+
type: "input_image",
|
|
2109
|
+
image_url: block.imageUrl
|
|
2110
|
+
};
|
|
2111
|
+
throw new AIRequestError(`${mapper$7.kind} does not support ${field} block of type "${block.type}"; only text/json/image blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function mapResponsesToolResultOutput(blocks, field) {
|
|
2115
|
+
mapper$7.ensureBlocks(blocks, field, [
|
|
2116
|
+
"text",
|
|
2117
|
+
"json",
|
|
2118
|
+
"image"
|
|
2119
|
+
], "only text/json/image blocks are supported");
|
|
2120
|
+
if (!blocks.some((block) => block.type === "image")) return mapper$7.textFromBlocks(blocks, field);
|
|
2121
|
+
return blocks.map((block) => {
|
|
2122
|
+
if (block.type === "text") return {
|
|
2123
|
+
type: "input_text",
|
|
2124
|
+
text: block.text
|
|
2125
|
+
};
|
|
2126
|
+
if (block.type === "json") return {
|
|
2127
|
+
type: "input_text",
|
|
2128
|
+
text: JSON.stringify(block.json)
|
|
2129
|
+
};
|
|
2130
|
+
if (block.type === "image") return {
|
|
2131
|
+
type: "input_image",
|
|
2132
|
+
image_url: block.imageUrl
|
|
2133
|
+
};
|
|
2134
|
+
throw new AIRequestError(`${mapper$7.kind} does not support ${field} block of type "${block.type}"; only text/json/image blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
2135
|
+
});
|
|
2448
2136
|
}
|
|
2449
2137
|
function mapReasoningInput(item, index) {
|
|
2450
|
-
const text = mapper$
|
|
2138
|
+
const text = mapper$7.textFromBlocks(mapper$7.ensureReasoningBlocks(item.content, "reasoning content"), "reasoning content");
|
|
2451
2139
|
const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
|
|
2452
2140
|
if (item.visibility === "full") return {
|
|
2453
2141
|
type: "reasoning",
|
|
@@ -2506,7 +2194,7 @@ function mapResponsesCore(request, options) {
|
|
|
2506
2194
|
input.push({
|
|
2507
2195
|
type: "message",
|
|
2508
2196
|
role: item.role,
|
|
2509
|
-
content:
|
|
2197
|
+
content: mapResponsesMessageContent(item.role, item.content, `input message (${item.role}) content`)
|
|
2510
2198
|
});
|
|
2511
2199
|
break;
|
|
2512
2200
|
case "reasoning":
|
|
@@ -2521,7 +2209,7 @@ function mapResponsesCore(request, options) {
|
|
|
2521
2209
|
});
|
|
2522
2210
|
break;
|
|
2523
2211
|
case "tool_result": {
|
|
2524
|
-
const output =
|
|
2212
|
+
const output = mapResponsesToolResultOutput(item.content, `tool_result ${item.callId} content`);
|
|
2525
2213
|
input.push({
|
|
2526
2214
|
type: "function_call_output",
|
|
2527
2215
|
call_id: item.callId,
|
|
@@ -2557,7 +2245,7 @@ function mapResponsesCore(request, options) {
|
|
|
2557
2245
|
usedCompactedWindow
|
|
2558
2246
|
};
|
|
2559
2247
|
if (previousResponseId && !usedCompactedWindow) mapped.previousResponseId = previousResponseId;
|
|
2560
|
-
if (request.instructions) mapped.instructions = mapper$
|
|
2248
|
+
if (request.instructions) mapped.instructions = mapper$7.mapInstructions(request.instructions);
|
|
2561
2249
|
return mapped;
|
|
2562
2250
|
}
|
|
2563
2251
|
/** 构建 Responses 流式请求体;调用方再 `withExtraBody` 合并构造期扩展字段。 */
|
|
@@ -2570,7 +2258,7 @@ function buildResponsesRequest(request, options) {
|
|
|
2570
2258
|
};
|
|
2571
2259
|
if (core.previousResponseId) body.previous_response_id = core.previousResponseId;
|
|
2572
2260
|
if (core.instructions) body.instructions = core.instructions;
|
|
2573
|
-
const functionTools = mapper$
|
|
2261
|
+
const functionTools = mapper$7.mapToolsIfPresent(request.tools, (t) => ({
|
|
2574
2262
|
type: "function",
|
|
2575
2263
|
name: t.name,
|
|
2576
2264
|
description: t.description,
|
|
@@ -2579,7 +2267,7 @@ function buildResponsesRequest(request, options) {
|
|
|
2579
2267
|
const serverTools = mapServerTools(request.serverTools);
|
|
2580
2268
|
const tools = [...functionTools, ...serverTools];
|
|
2581
2269
|
if (tools.length > 0) body.tools = tools;
|
|
2582
|
-
body.tool_choice = mapper$
|
|
2270
|
+
body.tool_choice = mapper$7.mapToolChoice(request.toolChoice, {
|
|
2583
2271
|
auto: "auto",
|
|
2584
2272
|
none: "none",
|
|
2585
2273
|
tool: (name) => ({
|
|
@@ -3432,17 +3120,56 @@ var ResponsesAdapter = class extends HttpAdapterBase {
|
|
|
3432
3120
|
});
|
|
3433
3121
|
}
|
|
3434
3122
|
};
|
|
3123
|
+
const SUPPORTED_IMAGE_MEDIA_TYPE_SET = /* @__PURE__ */ new Set([
|
|
3124
|
+
"image/jpeg",
|
|
3125
|
+
"image/png",
|
|
3126
|
+
"image/gif",
|
|
3127
|
+
"image/webp"
|
|
3128
|
+
]);
|
|
3129
|
+
/** True when imageUrl is an http(s) URL with a non-empty host. */
|
|
3130
|
+
function isHttpOrHttpsUrl(imageUrl) {
|
|
3131
|
+
try {
|
|
3132
|
+
const url = new URL(imageUrl);
|
|
3133
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.host.length > 0;
|
|
3134
|
+
} catch {
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Parse `data:image/(jpeg|png|gif|webp);base64,<data>`.
|
|
3140
|
+
* Rejects other media types, non-base64 data URLs, and empty payloads.
|
|
3141
|
+
* Does not accept whitespace inside the base64 payload.
|
|
3142
|
+
*/
|
|
3143
|
+
function parseImageDataUrl(imageUrl) {
|
|
3144
|
+
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(imageUrl);
|
|
3145
|
+
if (!match) return null;
|
|
3146
|
+
const mediaType = match[1]?.trim().toLowerCase();
|
|
3147
|
+
const data = match[2];
|
|
3148
|
+
if (!mediaType || !data || !SUPPORTED_IMAGE_MEDIA_TYPE_SET.has(mediaType)) return null;
|
|
3149
|
+
return {
|
|
3150
|
+
mediaType,
|
|
3151
|
+
data
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3435
3154
|
//#endregion
|
|
3436
3155
|
//#region src/adapters/messages/map-request.ts
|
|
3437
3156
|
/**
|
|
3438
3157
|
* MessagesAdapter — request 映射
|
|
3439
3158
|
*/
|
|
3440
|
-
const mapper$
|
|
3159
|
+
const mapper$6 = new NormalizedRequestMapper("messages");
|
|
3441
3160
|
function isMessagesReplayContentBlock(value) {
|
|
3442
3161
|
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
3443
3162
|
const block = value;
|
|
3444
3163
|
switch (block.type) {
|
|
3445
3164
|
case "text": return typeof block.text === "string";
|
|
3165
|
+
case "image": {
|
|
3166
|
+
const source = block.source;
|
|
3167
|
+
if (!source || typeof source !== "object") return false;
|
|
3168
|
+
const imageSource = source;
|
|
3169
|
+
if (imageSource.type === "url") return typeof imageSource.url === "string" && imageSource.url.length > 0;
|
|
3170
|
+
if (imageSource.type === "base64") return typeof imageSource.media_type === "string" && typeof imageSource.data === "string" && imageSource.data.length > 0 && (imageSource.media_type === "image/jpeg" || imageSource.media_type === "image/png" || imageSource.media_type === "image/gif" || imageSource.media_type === "image/webp");
|
|
3171
|
+
return false;
|
|
3172
|
+
}
|
|
3446
3173
|
case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
|
|
3447
3174
|
case "redacted_thinking": return typeof block.data === "string";
|
|
3448
3175
|
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
|
|
@@ -3459,7 +3186,26 @@ function assertMessagesReplayContent(content) {
|
|
|
3459
3186
|
if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
|
|
3460
3187
|
for (let i = 0; i < content.length; i++) if (!isMessagesReplayContentBlock(content[i])) throw new AIRequestError(`Invalid opaque replay payload: content[${i}] is not a valid Messages content block`, "INVALID_OPAQUE_REPLAY");
|
|
3461
3188
|
}
|
|
3462
|
-
function
|
|
3189
|
+
function mapMessagesImageBlock(imageUrl, field) {
|
|
3190
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
3191
|
+
if (dataUrl) return {
|
|
3192
|
+
type: "image",
|
|
3193
|
+
source: {
|
|
3194
|
+
type: "base64",
|
|
3195
|
+
media_type: dataUrl.mediaType,
|
|
3196
|
+
data: dataUrl.data
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
if (isHttpOrHttpsUrl(imageUrl)) return {
|
|
3200
|
+
type: "image",
|
|
3201
|
+
source: {
|
|
3202
|
+
type: "url",
|
|
3203
|
+
url: imageUrl
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
throw new AIRequestError(`messages cannot map ${field} imageUrl without a http(s) URL or data:image/(jpeg|png|gif|webp);base64,... payload`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
3207
|
+
}
|
|
3208
|
+
function canonicalToMessagesBlock(b, field = "canonical mapping") {
|
|
3463
3209
|
if (b.type === "text") return {
|
|
3464
3210
|
type: "text",
|
|
3465
3211
|
text: b.text
|
|
@@ -3468,27 +3214,57 @@ function canonicalToMessagesBlock(b) {
|
|
|
3468
3214
|
type: "text",
|
|
3469
3215
|
text: JSON.stringify(b.json)
|
|
3470
3216
|
};
|
|
3471
|
-
|
|
3217
|
+
if (b.type === "image") return mapMessagesImageBlock(b.imageUrl, field);
|
|
3218
|
+
throw new AIRequestError(`messages does not support content block type "${b.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
3219
|
+
}
|
|
3220
|
+
function mapMessagesUserContent(blocks, field) {
|
|
3221
|
+
mapper$6.ensureBlocks(blocks, field, [
|
|
3222
|
+
"text",
|
|
3223
|
+
"json",
|
|
3224
|
+
"image"
|
|
3225
|
+
], "only text/json/image blocks are supported");
|
|
3226
|
+
if (!blocks.some((block) => block.type === "image")) {
|
|
3227
|
+
const supportedContent = mapper$6.ensureTextBlocks(blocks, field);
|
|
3228
|
+
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") return supportedContent[0].text;
|
|
3229
|
+
return supportedContent.map((block) => canonicalToMessagesBlock(block, field));
|
|
3230
|
+
}
|
|
3231
|
+
return blocks.map((block) => canonicalToMessagesBlock(block, field));
|
|
3232
|
+
}
|
|
3233
|
+
function mapMessagesToolResultContent(blocks, field) {
|
|
3234
|
+
mapper$6.ensureBlocks(blocks, field, [
|
|
3235
|
+
"text",
|
|
3236
|
+
"json",
|
|
3237
|
+
"image"
|
|
3238
|
+
], "only text/json/image blocks are supported");
|
|
3239
|
+
if (!blocks.some((block) => block.type === "image")) return mapper$6.textFromBlocks(blocks, field);
|
|
3240
|
+
return blocks.map((block) => canonicalToMessagesBlock(block, field));
|
|
3472
3241
|
}
|
|
3473
3242
|
function buildMessagesRequest(request, options) {
|
|
3474
|
-
mapper$
|
|
3243
|
+
mapper$6.assertNoServerTools(request.serverTools);
|
|
3475
3244
|
const messages = [];
|
|
3476
3245
|
let systemPrompt;
|
|
3477
3246
|
let pendingToolResultMessage;
|
|
3478
|
-
if (request.instructions) systemPrompt = mapper$
|
|
3247
|
+
if (request.instructions) systemPrompt = mapper$6.mapInstructions(request.instructions);
|
|
3479
3248
|
for (const item of request.input) {
|
|
3480
3249
|
if (item.type !== "tool_result") pendingToolResultMessage = void 0;
|
|
3481
3250
|
switch (item.type) {
|
|
3482
3251
|
case "message": {
|
|
3483
|
-
const
|
|
3484
|
-
|
|
3252
|
+
const field = `input message (${item.role}) content`;
|
|
3253
|
+
if (item.role === "user") {
|
|
3254
|
+
messages.push({
|
|
3255
|
+
role: "user",
|
|
3256
|
+
content: mapMessagesUserContent(item.content, field)
|
|
3257
|
+
});
|
|
3258
|
+
break;
|
|
3259
|
+
}
|
|
3260
|
+
const supportedContent = mapper$6.ensureTextBlocks(item.content, field);
|
|
3485
3261
|
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
|
|
3486
|
-
role,
|
|
3262
|
+
role: "assistant",
|
|
3487
3263
|
content: supportedContent[0].text
|
|
3488
3264
|
});
|
|
3489
3265
|
else messages.push({
|
|
3490
|
-
role,
|
|
3491
|
-
content: supportedContent.map(canonicalToMessagesBlock)
|
|
3266
|
+
role: "assistant",
|
|
3267
|
+
content: supportedContent.map((block) => canonicalToMessagesBlock(block, field))
|
|
3492
3268
|
});
|
|
3493
3269
|
break;
|
|
3494
3270
|
}
|
|
@@ -3498,7 +3274,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3498
3274
|
type: "tool_use",
|
|
3499
3275
|
id: item.id,
|
|
3500
3276
|
name: item.name,
|
|
3501
|
-
input: mapper$
|
|
3277
|
+
input: mapper$6.parseToolArguments(item)
|
|
3502
3278
|
};
|
|
3503
3279
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
|
|
3504
3280
|
else messages.push({
|
|
@@ -3508,7 +3284,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3508
3284
|
break;
|
|
3509
3285
|
}
|
|
3510
3286
|
case "tool_result": {
|
|
3511
|
-
const content =
|
|
3287
|
+
const content = mapMessagesToolResultContent(item.content, `tool_result ${item.callId} content`);
|
|
3512
3288
|
const block = {
|
|
3513
3289
|
type: "tool_result",
|
|
3514
3290
|
tool_use_id: item.callId,
|
|
@@ -3528,7 +3304,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3528
3304
|
case "reasoning": {
|
|
3529
3305
|
const block = {
|
|
3530
3306
|
type: "thinking",
|
|
3531
|
-
thinking: contentBlocksToText(mapper$
|
|
3307
|
+
thinking: contentBlocksToText(mapper$6.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
3532
3308
|
};
|
|
3533
3309
|
const lastMsg = messages[messages.length - 1];
|
|
3534
3310
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
|
|
@@ -3543,7 +3319,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3543
3319
|
if (!payload) break;
|
|
3544
3320
|
if (payload.role === "assistant" && "content" in payload) {
|
|
3545
3321
|
assertMessagesReplayContent(payload.content);
|
|
3546
|
-
mapper$
|
|
3322
|
+
mapper$6.rollbackTrailingAssistantMessages(messages);
|
|
3547
3323
|
messages.push({
|
|
3548
3324
|
role: "assistant",
|
|
3549
3325
|
content: payload.content
|
|
@@ -3560,12 +3336,12 @@ function buildMessagesRequest(request, options) {
|
|
|
3560
3336
|
stream: true
|
|
3561
3337
|
};
|
|
3562
3338
|
if (systemPrompt) body.system = systemPrompt;
|
|
3563
|
-
body.tools = mapper$
|
|
3339
|
+
body.tools = mapper$6.mapToolsIfPresent(request.tools, (t) => ({
|
|
3564
3340
|
name: t.name,
|
|
3565
3341
|
description: t.description,
|
|
3566
3342
|
input_schema: t.inputSchema
|
|
3567
3343
|
}));
|
|
3568
|
-
body.tool_choice = mapper$
|
|
3344
|
+
body.tool_choice = mapper$6.mapToolChoice(request.toolChoice, {
|
|
3569
3345
|
auto: { type: "auto" },
|
|
3570
3346
|
none: { type: "none" },
|
|
3571
3347
|
tool: (name) => ({
|
|
@@ -3831,7 +3607,51 @@ const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
|
3831
3607
|
/**
|
|
3832
3608
|
* ChatCompletionsAdapter — request 映射
|
|
3833
3609
|
*/
|
|
3834
|
-
const mapper$
|
|
3610
|
+
const mapper$4 = new NormalizedRequestMapper("chat-completions");
|
|
3611
|
+
function extractReasoningText(value) {
|
|
3612
|
+
if (typeof value === "string") return value;
|
|
3613
|
+
if (Array.isArray(value)) return value.map(extractReasoningText).join("");
|
|
3614
|
+
if (value && typeof value === "object") {
|
|
3615
|
+
const record = value;
|
|
3616
|
+
for (const key of [
|
|
3617
|
+
"text",
|
|
3618
|
+
"content",
|
|
3619
|
+
"reasoning",
|
|
3620
|
+
"reasoning_content",
|
|
3621
|
+
"thinking",
|
|
3622
|
+
"value"
|
|
3623
|
+
]) {
|
|
3624
|
+
const nested = extractReasoningText(record[key]);
|
|
3625
|
+
if (nested) return nested;
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
return "";
|
|
3629
|
+
}
|
|
3630
|
+
function extractReasoningDeltas(delta) {
|
|
3631
|
+
const deltas = [];
|
|
3632
|
+
for (const field of REASONING_FIELDS) {
|
|
3633
|
+
const text = extractReasoningText(delta[field]);
|
|
3634
|
+
if (text) deltas.push({
|
|
3635
|
+
field,
|
|
3636
|
+
text
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
return deltas;
|
|
3640
|
+
}
|
|
3641
|
+
function isChatReplayContentPart(value) {
|
|
3642
|
+
if (!value || typeof value !== "object") return false;
|
|
3643
|
+
const part = value;
|
|
3644
|
+
if (part.type === "text") return typeof part.text === "string";
|
|
3645
|
+
if (part.type === "image_url") {
|
|
3646
|
+
const imageUrl = part.image_url;
|
|
3647
|
+
if (!imageUrl || typeof imageUrl !== "object") return false;
|
|
3648
|
+
const image = imageUrl;
|
|
3649
|
+
if (typeof image.url !== "string") return false;
|
|
3650
|
+
if (image.detail !== void 0 && image.detail !== "auto" && image.detail !== "low" && image.detail !== "high") return false;
|
|
3651
|
+
return true;
|
|
3652
|
+
}
|
|
3653
|
+
return false;
|
|
3654
|
+
}
|
|
3835
3655
|
function isChatReplayToolCall(value) {
|
|
3836
3656
|
if (!value || typeof value !== "object") return false;
|
|
3837
3657
|
const entry = value;
|
|
@@ -3846,7 +3666,8 @@ function isChatReplayMessage(value) {
|
|
|
3846
3666
|
const msg = value;
|
|
3847
3667
|
const role = msg.role;
|
|
3848
3668
|
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
|
|
3849
|
-
|
|
3669
|
+
const content = msg.content;
|
|
3670
|
+
if (!(content === null || typeof content === "string" || Array.isArray(content) && content.every(isChatReplayContentPart))) return false;
|
|
3850
3671
|
if (msg.tool_calls !== void 0) {
|
|
3851
3672
|
if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
|
|
3852
3673
|
}
|
|
@@ -3858,20 +3679,87 @@ function assertChatReplayMessages(messages, field) {
|
|
|
3858
3679
|
if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
|
|
3859
3680
|
for (let i = 0; i < messages.length; i++) if (!isChatReplayMessage(messages[i])) throw new AIRequestError(`Invalid opaque replay payload: ${field}[${i}] is not a valid chat message`, "INVALID_OPAQUE_REPLAY");
|
|
3860
3681
|
}
|
|
3682
|
+
function buildAssistantReplayMessage(params) {
|
|
3683
|
+
const { content, reasoningByField, toolCalls } = params;
|
|
3684
|
+
if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
|
|
3685
|
+
const replayMessage = {
|
|
3686
|
+
role: "assistant",
|
|
3687
|
+
content: content || null
|
|
3688
|
+
};
|
|
3689
|
+
for (const [field, text] of reasoningByField) replayMessage[field] = text;
|
|
3690
|
+
if (toolCalls.length > 0) replayMessage.tool_calls = toolCalls.map((toolCall) => ({
|
|
3691
|
+
id: toolCall.id,
|
|
3692
|
+
type: "function",
|
|
3693
|
+
function: {
|
|
3694
|
+
name: toolCall.name,
|
|
3695
|
+
arguments: toolCall.args
|
|
3696
|
+
}
|
|
3697
|
+
}));
|
|
3698
|
+
return replayMessage;
|
|
3699
|
+
}
|
|
3700
|
+
function mapChatUserContent(blocks, field) {
|
|
3701
|
+
mapper$4.ensureBlocks(blocks, field, [
|
|
3702
|
+
"text",
|
|
3703
|
+
"json",
|
|
3704
|
+
"image"
|
|
3705
|
+
], "only text/json/image blocks are supported");
|
|
3706
|
+
if (!blocks.some((block) => block.type === "image")) return contentBlocksToText(blocks) || null;
|
|
3707
|
+
return blocks.map((block) => {
|
|
3708
|
+
if (block.type === "text") return {
|
|
3709
|
+
type: "text",
|
|
3710
|
+
text: block.text
|
|
3711
|
+
};
|
|
3712
|
+
if (block.type === "json") return {
|
|
3713
|
+
type: "text",
|
|
3714
|
+
text: JSON.stringify(block.json)
|
|
3715
|
+
};
|
|
3716
|
+
if (block.type === "image") return {
|
|
3717
|
+
type: "image_url",
|
|
3718
|
+
image_url: { url: block.imageUrl }
|
|
3719
|
+
};
|
|
3720
|
+
throw new AIRequestError(`${mapper$4.kind} does not support ${field} block of type "${block.type}"; only text/json/image blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
function mapChatToolResultContent(blocks, field) {
|
|
3724
|
+
mapper$4.ensureBlocks(blocks, field, [
|
|
3725
|
+
"text",
|
|
3726
|
+
"json",
|
|
3727
|
+
"image"
|
|
3728
|
+
], "only text/json/image blocks are supported");
|
|
3729
|
+
if (!blocks.some((block) => block.type === "image")) return mapper$4.textFromBlocks(blocks, field);
|
|
3730
|
+
return blocks.map((block) => {
|
|
3731
|
+
if (block.type === "text") return {
|
|
3732
|
+
type: "text",
|
|
3733
|
+
text: block.text
|
|
3734
|
+
};
|
|
3735
|
+
if (block.type === "json") return {
|
|
3736
|
+
type: "text",
|
|
3737
|
+
text: JSON.stringify(block.json)
|
|
3738
|
+
};
|
|
3739
|
+
if (block.type === "image") return {
|
|
3740
|
+
type: "image_url",
|
|
3741
|
+
image_url: { url: block.imageUrl }
|
|
3742
|
+
};
|
|
3743
|
+
throw new AIRequestError(`${mapper$4.kind} does not support ${field} block of type "${block.type}"; only text/json/image blocks are supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
function mapChatMessageContent(role, blocks, field) {
|
|
3747
|
+
if (role === "user") return mapChatUserContent(blocks, field);
|
|
3748
|
+
return mapper$4.textFromBlocks(blocks, field) || null;
|
|
3749
|
+
}
|
|
3861
3750
|
function buildChatCompletionsRequest(request, options) {
|
|
3862
|
-
mapper$
|
|
3751
|
+
mapper$4.assertNoServerTools(request.serverTools);
|
|
3863
3752
|
const messages = [];
|
|
3864
3753
|
if (request.instructions) messages.push({
|
|
3865
3754
|
role: "system",
|
|
3866
|
-
content: mapper$
|
|
3755
|
+
content: mapper$4.mapInstructions(request.instructions)
|
|
3867
3756
|
});
|
|
3868
3757
|
for (const item of request.input) switch (item.type) {
|
|
3869
3758
|
case "message": {
|
|
3870
3759
|
const role = item.role;
|
|
3871
|
-
const text = mapper$5.textFromBlocks(item.content, `input message (${item.role}) content`);
|
|
3872
3760
|
messages.push({
|
|
3873
3761
|
role,
|
|
3874
|
-
content:
|
|
3762
|
+
content: mapChatMessageContent(role, item.content, `input message (${item.role}) content`)
|
|
3875
3763
|
});
|
|
3876
3764
|
break;
|
|
3877
3765
|
}
|
|
@@ -3898,13 +3786,13 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3898
3786
|
role: "tool",
|
|
3899
3787
|
tool_call_id: item.callId,
|
|
3900
3788
|
name: item.toolName,
|
|
3901
|
-
content:
|
|
3789
|
+
content: mapChatToolResultContent(item.content, `tool_result ${item.callId} content`)
|
|
3902
3790
|
});
|
|
3903
3791
|
break;
|
|
3904
3792
|
case "reasoning":
|
|
3905
3793
|
messages.push({
|
|
3906
3794
|
role: "assistant",
|
|
3907
|
-
content: mapper$
|
|
3795
|
+
content: mapper$4.textFromBlocks(item.content, "reasoning content")
|
|
3908
3796
|
});
|
|
3909
3797
|
break;
|
|
3910
3798
|
case "opaque": {
|
|
@@ -3912,7 +3800,7 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3912
3800
|
if (!payload) break;
|
|
3913
3801
|
if ("messages" in payload) {
|
|
3914
3802
|
assertChatReplayMessages(payload.messages, "messages");
|
|
3915
|
-
mapper$
|
|
3803
|
+
mapper$4.rollbackTrailingAssistantMessages(messages);
|
|
3916
3804
|
for (const m of payload.messages) messages.push(m);
|
|
3917
3805
|
}
|
|
3918
3806
|
break;
|
|
@@ -3924,8 +3812,8 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3924
3812
|
stream: true,
|
|
3925
3813
|
n: 1
|
|
3926
3814
|
};
|
|
3927
|
-
body.tools = mapper$
|
|
3928
|
-
body.tool_choice = mapper$
|
|
3815
|
+
body.tools = mapper$4.mapToolsIfPresent(request.tools, mapOpenAiFunctionTool);
|
|
3816
|
+
body.tool_choice = mapper$4.mapToolChoice(request.toolChoice, {
|
|
3929
3817
|
auto: "auto",
|
|
3930
3818
|
none: "none",
|
|
3931
3819
|
tool: (name) => ({
|
|
@@ -3939,55 +3827,11 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3939
3827
|
if (request.reasoningLevel !== void 0) body.reasoning_effort = mapChatCompletionsReasoningEffort(request.reasoningLevel);
|
|
3940
3828
|
return body;
|
|
3941
3829
|
}
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
const record = value;
|
|
3948
|
-
for (const key of [
|
|
3949
|
-
"text",
|
|
3950
|
-
"content",
|
|
3951
|
-
"reasoning",
|
|
3952
|
-
"reasoning_content",
|
|
3953
|
-
"thinking",
|
|
3954
|
-
"value"
|
|
3955
|
-
]) {
|
|
3956
|
-
const nested = extractReasoningText(record[key]);
|
|
3957
|
-
if (nested) return nested;
|
|
3958
|
-
}
|
|
3959
|
-
}
|
|
3960
|
-
return "";
|
|
3961
|
-
}
|
|
3962
|
-
function extractReasoningDeltas(delta) {
|
|
3963
|
-
const deltas = [];
|
|
3964
|
-
for (const field of REASONING_FIELDS) {
|
|
3965
|
-
const text = extractReasoningText(delta[field]);
|
|
3966
|
-
if (text) deltas.push({
|
|
3967
|
-
field,
|
|
3968
|
-
text
|
|
3969
|
-
});
|
|
3970
|
-
}
|
|
3971
|
-
return deltas;
|
|
3972
|
-
}
|
|
3973
|
-
function buildAssistantReplayMessage(params) {
|
|
3974
|
-
const { content, reasoningByField, toolCalls } = params;
|
|
3975
|
-
if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
|
|
3976
|
-
const replayMessage = {
|
|
3977
|
-
role: "assistant",
|
|
3978
|
-
content: content || null
|
|
3979
|
-
};
|
|
3980
|
-
for (const [field, text] of reasoningByField) replayMessage[field] = text;
|
|
3981
|
-
if (toolCalls.length > 0) replayMessage.tool_calls = toolCalls.map((toolCall) => ({
|
|
3982
|
-
id: toolCall.id,
|
|
3983
|
-
type: "function",
|
|
3984
|
-
function: {
|
|
3985
|
-
name: toolCall.name,
|
|
3986
|
-
arguments: toolCall.args
|
|
3987
|
-
}
|
|
3988
|
-
}));
|
|
3989
|
-
return replayMessage;
|
|
3990
|
-
}
|
|
3830
|
+
//#endregion
|
|
3831
|
+
//#region src/adapters/chat-completions/map-stream.ts
|
|
3832
|
+
/**
|
|
3833
|
+
* ChatCompletionsAdapter — stream 映射
|
|
3834
|
+
*/
|
|
3991
3835
|
async function* mapChatCompletionsStream(host, providerRequest, factory, request) {
|
|
3992
3836
|
const session = host.beginJsonStream(factory, request);
|
|
3993
3837
|
const { auxiliary, gate } = session;
|
|
@@ -4243,6 +4087,46 @@ function toWireOllamaToolCalls(toolCalls) {
|
|
|
4243
4087
|
arguments: tc.function.arguments
|
|
4244
4088
|
} }));
|
|
4245
4089
|
}
|
|
4090
|
+
function mapOllamaImageData(imageUrl, field) {
|
|
4091
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
4092
|
+
if (!dataUrl) throw new AIRequestError(`ollama cannot map ${field} imageUrl without fetching; only data:image/(jpeg|png|gif|webp);base64,... is supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4093
|
+
return dataUrl.data;
|
|
4094
|
+
}
|
|
4095
|
+
function mapOllamaUserMessage(blocks, field) {
|
|
4096
|
+
mapper$3.ensureBlocks(blocks, field, [
|
|
4097
|
+
"text",
|
|
4098
|
+
"json",
|
|
4099
|
+
"image"
|
|
4100
|
+
], "only text/json/image blocks are supported");
|
|
4101
|
+
const images = [];
|
|
4102
|
+
const textBlocks = [];
|
|
4103
|
+
for (const block of blocks) {
|
|
4104
|
+
if (block.type === "image") {
|
|
4105
|
+
images.push(mapOllamaImageData(block.imageUrl, field));
|
|
4106
|
+
continue;
|
|
4107
|
+
}
|
|
4108
|
+
textBlocks.push(block);
|
|
4109
|
+
}
|
|
4110
|
+
return {
|
|
4111
|
+
content: contentBlocksToText(textBlocks),
|
|
4112
|
+
...images.length > 0 ? { images } : {}
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
function mapOllamaToolResultContent(blocks, field) {
|
|
4116
|
+
mapper$3.ensureBlocks(blocks, field, [
|
|
4117
|
+
"text",
|
|
4118
|
+
"json",
|
|
4119
|
+
"image"
|
|
4120
|
+
], "only text/json/image blocks are supported");
|
|
4121
|
+
const textBlocks = [];
|
|
4122
|
+
const images = [];
|
|
4123
|
+
for (const block of blocks) if (block.type === "image") images.push(mapOllamaImageData(block.imageUrl, field));
|
|
4124
|
+
else textBlocks.push(block);
|
|
4125
|
+
return {
|
|
4126
|
+
content: contentBlocksToText(textBlocks),
|
|
4127
|
+
...images.length > 0 ? { images } : {}
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4246
4130
|
function buildOllamaRequest(request, options) {
|
|
4247
4131
|
mapper$3.assertNoServerTools(request.serverTools);
|
|
4248
4132
|
const messages = [];
|
|
@@ -4254,10 +4138,17 @@ function buildOllamaRequest(request, options) {
|
|
|
4254
4138
|
});
|
|
4255
4139
|
for (const item of request.input) switch (item.type) {
|
|
4256
4140
|
case "message": {
|
|
4257
|
-
const
|
|
4141
|
+
const field = `input message (${item.role}) content`;
|
|
4142
|
+
if (item.role === "user") {
|
|
4143
|
+
messages.push({
|
|
4144
|
+
role: "user",
|
|
4145
|
+
...mapOllamaUserMessage(item.content, field)
|
|
4146
|
+
});
|
|
4147
|
+
break;
|
|
4148
|
+
}
|
|
4258
4149
|
messages.push({
|
|
4259
|
-
role,
|
|
4260
|
-
content: mapper$3.textFromBlocks(item.content,
|
|
4150
|
+
role: item.role,
|
|
4151
|
+
content: mapper$3.textFromBlocks(item.content, field)
|
|
4261
4152
|
});
|
|
4262
4153
|
break;
|
|
4263
4154
|
}
|
|
@@ -4283,7 +4174,7 @@ function buildOllamaRequest(request, options) {
|
|
|
4283
4174
|
if (queue && queue.length > 0) queue.shift();
|
|
4284
4175
|
messages.push({
|
|
4285
4176
|
role: "tool",
|
|
4286
|
-
|
|
4177
|
+
...mapOllamaToolResultContent(item.content, `tool_result ${item.callId} content`)
|
|
4287
4178
|
});
|
|
4288
4179
|
break;
|
|
4289
4180
|
}
|
|
@@ -4501,6 +4392,37 @@ function appendPart(contents, role, part) {
|
|
|
4501
4392
|
parts: [part]
|
|
4502
4393
|
});
|
|
4503
4394
|
}
|
|
4395
|
+
function mapGeminiImagePart(imageUrl, field) {
|
|
4396
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
4397
|
+
if (!dataUrl) throw new AIRequestError(`gemini cannot map ${field} imageUrl without fetching; only data:image/(jpeg|png|gif|webp);base64,... is supported`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4398
|
+
return { inlineData: {
|
|
4399
|
+
mimeType: dataUrl.mediaType,
|
|
4400
|
+
data: dataUrl.data
|
|
4401
|
+
} };
|
|
4402
|
+
}
|
|
4403
|
+
function mapGeminiToolResultContent(blocks, field) {
|
|
4404
|
+
mapper$1.ensureBlocks(blocks, field, [
|
|
4405
|
+
"text",
|
|
4406
|
+
"json",
|
|
4407
|
+
"image"
|
|
4408
|
+
], "only text/json/image blocks are supported");
|
|
4409
|
+
const textBlocks = [];
|
|
4410
|
+
const imageParts = [];
|
|
4411
|
+
for (const block of blocks) if (block.type === "image") imageParts.push(mapGeminiImagePart(block.imageUrl, field));
|
|
4412
|
+
else textBlocks.push(block);
|
|
4413
|
+
const text = contentBlocksToText(textBlocks);
|
|
4414
|
+
let response;
|
|
4415
|
+
try {
|
|
4416
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
4417
|
+
response = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: text };
|
|
4418
|
+
} catch {
|
|
4419
|
+
response = { result: text };
|
|
4420
|
+
}
|
|
4421
|
+
return {
|
|
4422
|
+
response,
|
|
4423
|
+
imageParts
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4504
4426
|
function textPartsFromBlocks(blocks, field) {
|
|
4505
4427
|
return mapper$1.ensureTextBlocks(blocks, field).map((block) => {
|
|
4506
4428
|
if (block.type === "text") return { text: block.text };
|
|
@@ -4508,6 +4430,20 @@ function textPartsFromBlocks(blocks, field) {
|
|
|
4508
4430
|
throw new AIRequestError(`gemini does not support content block type "${block.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4509
4431
|
});
|
|
4510
4432
|
}
|
|
4433
|
+
function partsFromUserBlocks(blocks, field) {
|
|
4434
|
+
mapper$1.ensureBlocks(blocks, field, [
|
|
4435
|
+
"text",
|
|
4436
|
+
"json",
|
|
4437
|
+
"image"
|
|
4438
|
+
], "only text/json/image blocks are supported");
|
|
4439
|
+
if (!blocks.some((block) => block.type === "image")) return textPartsFromBlocks(blocks, field);
|
|
4440
|
+
return blocks.map((block) => {
|
|
4441
|
+
if (block.type === "text") return { text: block.text };
|
|
4442
|
+
if (block.type === "json") return { text: JSON.stringify(block.json) };
|
|
4443
|
+
if (block.type === "image") return mapGeminiImagePart(block.imageUrl, field);
|
|
4444
|
+
throw new AIRequestError(`gemini does not support content block type "${block.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4511
4447
|
function clonePart$1(part) {
|
|
4512
4448
|
return { ...part };
|
|
4513
4449
|
}
|
|
@@ -4524,8 +4460,10 @@ function buildGeminiRequest(request, options) {
|
|
|
4524
4460
|
if (request.instructions) systemInstruction = { parts: [{ text: mapper$1.mapInstructions(request.instructions) }] };
|
|
4525
4461
|
for (const item of request.input) switch (item.type) {
|
|
4526
4462
|
case "message": {
|
|
4463
|
+
const field = `input message (${item.role}) content`;
|
|
4527
4464
|
const role = item.role === "assistant" ? "model" : "user";
|
|
4528
|
-
|
|
4465
|
+
const parts = item.role === "user" ? partsFromUserBlocks(item.content, field) : textPartsFromBlocks(item.content, field);
|
|
4466
|
+
for (const part of parts) appendPart(contents, role, part);
|
|
4529
4467
|
break;
|
|
4530
4468
|
}
|
|
4531
4469
|
case "tool_call":
|
|
@@ -4536,19 +4474,13 @@ function buildGeminiRequest(request, options) {
|
|
|
4536
4474
|
} });
|
|
4537
4475
|
break;
|
|
4538
4476
|
case "tool_result": {
|
|
4539
|
-
|
|
4540
|
-
try {
|
|
4541
|
-
const text = mapper$1.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
4542
|
-
const parsed = text ? JSON.parse(text) : {};
|
|
4543
|
-
response = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { result: text };
|
|
4544
|
-
} catch {
|
|
4545
|
-
response = { result: mapper$1.textFromBlocks(item.content, `tool_result ${item.callId} content`) };
|
|
4546
|
-
}
|
|
4477
|
+
const { response, imageParts } = mapGeminiToolResultContent(item.content, `tool_result ${item.callId} content`);
|
|
4547
4478
|
appendPart(contents, "user", { functionResponse: {
|
|
4548
4479
|
id: item.callId,
|
|
4549
4480
|
name: item.toolName,
|
|
4550
4481
|
response
|
|
4551
4482
|
} });
|
|
4483
|
+
for (const imagePart of imageParts) appendPart(contents, "user", imagePart);
|
|
4552
4484
|
break;
|
|
4553
4485
|
}
|
|
4554
4486
|
case "reasoning":
|