@codehz/ai 0.7.1 → 0.8.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 +9 -1
- package/dist/index.d.mts +31 -7
- package/dist/index.mjs +364 -519
- 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,38 @@ 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
|
+
});
|
|
2448
2113
|
}
|
|
2449
2114
|
function mapReasoningInput(item, index) {
|
|
2450
|
-
const text = mapper$
|
|
2115
|
+
const text = mapper$7.textFromBlocks(mapper$7.ensureReasoningBlocks(item.content, "reasoning content"), "reasoning content");
|
|
2451
2116
|
const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
|
|
2452
2117
|
if (item.visibility === "full") return {
|
|
2453
2118
|
type: "reasoning",
|
|
@@ -2506,7 +2171,7 @@ function mapResponsesCore(request, options) {
|
|
|
2506
2171
|
input.push({
|
|
2507
2172
|
type: "message",
|
|
2508
2173
|
role: item.role,
|
|
2509
|
-
content:
|
|
2174
|
+
content: mapResponsesMessageContent(item.role, item.content, `input message (${item.role}) content`)
|
|
2510
2175
|
});
|
|
2511
2176
|
break;
|
|
2512
2177
|
case "reasoning":
|
|
@@ -2521,7 +2186,7 @@ function mapResponsesCore(request, options) {
|
|
|
2521
2186
|
});
|
|
2522
2187
|
break;
|
|
2523
2188
|
case "tool_result": {
|
|
2524
|
-
const output = mapper$
|
|
2189
|
+
const output = mapper$7.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
2525
2190
|
input.push({
|
|
2526
2191
|
type: "function_call_output",
|
|
2527
2192
|
call_id: item.callId,
|
|
@@ -2557,7 +2222,7 @@ function mapResponsesCore(request, options) {
|
|
|
2557
2222
|
usedCompactedWindow
|
|
2558
2223
|
};
|
|
2559
2224
|
if (previousResponseId && !usedCompactedWindow) mapped.previousResponseId = previousResponseId;
|
|
2560
|
-
if (request.instructions) mapped.instructions = mapper$
|
|
2225
|
+
if (request.instructions) mapped.instructions = mapper$7.mapInstructions(request.instructions);
|
|
2561
2226
|
return mapped;
|
|
2562
2227
|
}
|
|
2563
2228
|
/** 构建 Responses 流式请求体;调用方再 `withExtraBody` 合并构造期扩展字段。 */
|
|
@@ -2570,7 +2235,7 @@ function buildResponsesRequest(request, options) {
|
|
|
2570
2235
|
};
|
|
2571
2236
|
if (core.previousResponseId) body.previous_response_id = core.previousResponseId;
|
|
2572
2237
|
if (core.instructions) body.instructions = core.instructions;
|
|
2573
|
-
const functionTools = mapper$
|
|
2238
|
+
const functionTools = mapper$7.mapToolsIfPresent(request.tools, (t) => ({
|
|
2574
2239
|
type: "function",
|
|
2575
2240
|
name: t.name,
|
|
2576
2241
|
description: t.description,
|
|
@@ -2579,7 +2244,7 @@ function buildResponsesRequest(request, options) {
|
|
|
2579
2244
|
const serverTools = mapServerTools(request.serverTools);
|
|
2580
2245
|
const tools = [...functionTools, ...serverTools];
|
|
2581
2246
|
if (tools.length > 0) body.tools = tools;
|
|
2582
|
-
body.tool_choice = mapper$
|
|
2247
|
+
body.tool_choice = mapper$7.mapToolChoice(request.toolChoice, {
|
|
2583
2248
|
auto: "auto",
|
|
2584
2249
|
none: "none",
|
|
2585
2250
|
tool: (name) => ({
|
|
@@ -3432,17 +3097,56 @@ var ResponsesAdapter = class extends HttpAdapterBase {
|
|
|
3432
3097
|
});
|
|
3433
3098
|
}
|
|
3434
3099
|
};
|
|
3100
|
+
const SUPPORTED_IMAGE_MEDIA_TYPE_SET = /* @__PURE__ */ new Set([
|
|
3101
|
+
"image/jpeg",
|
|
3102
|
+
"image/png",
|
|
3103
|
+
"image/gif",
|
|
3104
|
+
"image/webp"
|
|
3105
|
+
]);
|
|
3106
|
+
/** True when imageUrl is an http(s) URL with a non-empty host. */
|
|
3107
|
+
function isHttpOrHttpsUrl(imageUrl) {
|
|
3108
|
+
try {
|
|
3109
|
+
const url = new URL(imageUrl);
|
|
3110
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.host.length > 0;
|
|
3111
|
+
} catch {
|
|
3112
|
+
return false;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Parse `data:image/(jpeg|png|gif|webp);base64,<data>`.
|
|
3117
|
+
* Rejects other media types, non-base64 data URLs, and empty payloads.
|
|
3118
|
+
* Does not accept whitespace inside the base64 payload.
|
|
3119
|
+
*/
|
|
3120
|
+
function parseImageDataUrl(imageUrl) {
|
|
3121
|
+
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(imageUrl);
|
|
3122
|
+
if (!match) return null;
|
|
3123
|
+
const mediaType = match[1]?.trim().toLowerCase();
|
|
3124
|
+
const data = match[2];
|
|
3125
|
+
if (!mediaType || !data || !SUPPORTED_IMAGE_MEDIA_TYPE_SET.has(mediaType)) return null;
|
|
3126
|
+
return {
|
|
3127
|
+
mediaType,
|
|
3128
|
+
data
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3435
3131
|
//#endregion
|
|
3436
3132
|
//#region src/adapters/messages/map-request.ts
|
|
3437
3133
|
/**
|
|
3438
3134
|
* MessagesAdapter — request 映射
|
|
3439
3135
|
*/
|
|
3440
|
-
const mapper$
|
|
3136
|
+
const mapper$6 = new NormalizedRequestMapper("messages");
|
|
3441
3137
|
function isMessagesReplayContentBlock(value) {
|
|
3442
3138
|
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
3443
3139
|
const block = value;
|
|
3444
3140
|
switch (block.type) {
|
|
3445
3141
|
case "text": return typeof block.text === "string";
|
|
3142
|
+
case "image": {
|
|
3143
|
+
const source = block.source;
|
|
3144
|
+
if (!source || typeof source !== "object") return false;
|
|
3145
|
+
const imageSource = source;
|
|
3146
|
+
if (imageSource.type === "url") return typeof imageSource.url === "string" && imageSource.url.length > 0;
|
|
3147
|
+
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");
|
|
3148
|
+
return false;
|
|
3149
|
+
}
|
|
3446
3150
|
case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
|
|
3447
3151
|
case "redacted_thinking": return typeof block.data === "string";
|
|
3448
3152
|
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
|
|
@@ -3459,7 +3163,26 @@ function assertMessagesReplayContent(content) {
|
|
|
3459
3163
|
if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
|
|
3460
3164
|
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
3165
|
}
|
|
3462
|
-
function
|
|
3166
|
+
function mapMessagesImageBlock(imageUrl, field) {
|
|
3167
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
3168
|
+
if (dataUrl) return {
|
|
3169
|
+
type: "image",
|
|
3170
|
+
source: {
|
|
3171
|
+
type: "base64",
|
|
3172
|
+
media_type: dataUrl.mediaType,
|
|
3173
|
+
data: dataUrl.data
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
if (isHttpOrHttpsUrl(imageUrl)) return {
|
|
3177
|
+
type: "image",
|
|
3178
|
+
source: {
|
|
3179
|
+
type: "url",
|
|
3180
|
+
url: imageUrl
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
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");
|
|
3184
|
+
}
|
|
3185
|
+
function canonicalToMessagesBlock(b, field = "canonical mapping") {
|
|
3463
3186
|
if (b.type === "text") return {
|
|
3464
3187
|
type: "text",
|
|
3465
3188
|
text: b.text
|
|
@@ -3468,27 +3191,48 @@ function canonicalToMessagesBlock(b) {
|
|
|
3468
3191
|
type: "text",
|
|
3469
3192
|
text: JSON.stringify(b.json)
|
|
3470
3193
|
};
|
|
3471
|
-
|
|
3194
|
+
if (b.type === "image") return mapMessagesImageBlock(b.imageUrl, field);
|
|
3195
|
+
throw new AIRequestError(`messages does not support content block type "${b.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
3196
|
+
}
|
|
3197
|
+
function mapMessagesUserContent(blocks, field) {
|
|
3198
|
+
mapper$6.ensureBlocks(blocks, field, [
|
|
3199
|
+
"text",
|
|
3200
|
+
"json",
|
|
3201
|
+
"image"
|
|
3202
|
+
], "only text/json/image blocks are supported");
|
|
3203
|
+
if (!blocks.some((block) => block.type === "image")) {
|
|
3204
|
+
const supportedContent = mapper$6.ensureTextBlocks(blocks, field);
|
|
3205
|
+
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") return supportedContent[0].text;
|
|
3206
|
+
return supportedContent.map((block) => canonicalToMessagesBlock(block, field));
|
|
3207
|
+
}
|
|
3208
|
+
return blocks.map((block) => canonicalToMessagesBlock(block, field));
|
|
3472
3209
|
}
|
|
3473
3210
|
function buildMessagesRequest(request, options) {
|
|
3474
|
-
mapper$
|
|
3211
|
+
mapper$6.assertNoServerTools(request.serverTools);
|
|
3475
3212
|
const messages = [];
|
|
3476
3213
|
let systemPrompt;
|
|
3477
3214
|
let pendingToolResultMessage;
|
|
3478
|
-
if (request.instructions) systemPrompt = mapper$
|
|
3215
|
+
if (request.instructions) systemPrompt = mapper$6.mapInstructions(request.instructions);
|
|
3479
3216
|
for (const item of request.input) {
|
|
3480
3217
|
if (item.type !== "tool_result") pendingToolResultMessage = void 0;
|
|
3481
3218
|
switch (item.type) {
|
|
3482
3219
|
case "message": {
|
|
3483
|
-
const
|
|
3484
|
-
|
|
3220
|
+
const field = `input message (${item.role}) content`;
|
|
3221
|
+
if (item.role === "user") {
|
|
3222
|
+
messages.push({
|
|
3223
|
+
role: "user",
|
|
3224
|
+
content: mapMessagesUserContent(item.content, field)
|
|
3225
|
+
});
|
|
3226
|
+
break;
|
|
3227
|
+
}
|
|
3228
|
+
const supportedContent = mapper$6.ensureTextBlocks(item.content, field);
|
|
3485
3229
|
if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
|
|
3486
|
-
role,
|
|
3230
|
+
role: "assistant",
|
|
3487
3231
|
content: supportedContent[0].text
|
|
3488
3232
|
});
|
|
3489
3233
|
else messages.push({
|
|
3490
|
-
role,
|
|
3491
|
-
content: supportedContent.map(canonicalToMessagesBlock)
|
|
3234
|
+
role: "assistant",
|
|
3235
|
+
content: supportedContent.map((block) => canonicalToMessagesBlock(block, field))
|
|
3492
3236
|
});
|
|
3493
3237
|
break;
|
|
3494
3238
|
}
|
|
@@ -3498,7 +3242,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3498
3242
|
type: "tool_use",
|
|
3499
3243
|
id: item.id,
|
|
3500
3244
|
name: item.name,
|
|
3501
|
-
input: mapper$
|
|
3245
|
+
input: mapper$6.parseToolArguments(item)
|
|
3502
3246
|
};
|
|
3503
3247
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
|
|
3504
3248
|
else messages.push({
|
|
@@ -3508,7 +3252,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3508
3252
|
break;
|
|
3509
3253
|
}
|
|
3510
3254
|
case "tool_result": {
|
|
3511
|
-
const content = mapper$
|
|
3255
|
+
const content = mapper$6.textFromBlocks(item.content, `tool_result ${item.callId} content`);
|
|
3512
3256
|
const block = {
|
|
3513
3257
|
type: "tool_result",
|
|
3514
3258
|
tool_use_id: item.callId,
|
|
@@ -3528,7 +3272,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3528
3272
|
case "reasoning": {
|
|
3529
3273
|
const block = {
|
|
3530
3274
|
type: "thinking",
|
|
3531
|
-
thinking: contentBlocksToText(mapper$
|
|
3275
|
+
thinking: contentBlocksToText(mapper$6.ensureReasoningBlocks(item.content, "reasoning content"))
|
|
3532
3276
|
};
|
|
3533
3277
|
const lastMsg = messages[messages.length - 1];
|
|
3534
3278
|
if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
|
|
@@ -3543,7 +3287,7 @@ function buildMessagesRequest(request, options) {
|
|
|
3543
3287
|
if (!payload) break;
|
|
3544
3288
|
if (payload.role === "assistant" && "content" in payload) {
|
|
3545
3289
|
assertMessagesReplayContent(payload.content);
|
|
3546
|
-
mapper$
|
|
3290
|
+
mapper$6.rollbackTrailingAssistantMessages(messages);
|
|
3547
3291
|
messages.push({
|
|
3548
3292
|
role: "assistant",
|
|
3549
3293
|
content: payload.content
|
|
@@ -3560,12 +3304,12 @@ function buildMessagesRequest(request, options) {
|
|
|
3560
3304
|
stream: true
|
|
3561
3305
|
};
|
|
3562
3306
|
if (systemPrompt) body.system = systemPrompt;
|
|
3563
|
-
body.tools = mapper$
|
|
3307
|
+
body.tools = mapper$6.mapToolsIfPresent(request.tools, (t) => ({
|
|
3564
3308
|
name: t.name,
|
|
3565
3309
|
description: t.description,
|
|
3566
3310
|
input_schema: t.inputSchema
|
|
3567
3311
|
}));
|
|
3568
|
-
body.tool_choice = mapper$
|
|
3312
|
+
body.tool_choice = mapper$6.mapToolChoice(request.toolChoice, {
|
|
3569
3313
|
auto: { type: "auto" },
|
|
3570
3314
|
none: { type: "none" },
|
|
3571
3315
|
tool: (name) => ({
|
|
@@ -3831,7 +3575,51 @@ const REASONING_FIELDS = ["reasoning_content", "reasoning"];
|
|
|
3831
3575
|
/**
|
|
3832
3576
|
* ChatCompletionsAdapter — request 映射
|
|
3833
3577
|
*/
|
|
3834
|
-
const mapper$
|
|
3578
|
+
const mapper$4 = new NormalizedRequestMapper("chat-completions");
|
|
3579
|
+
function extractReasoningText(value) {
|
|
3580
|
+
if (typeof value === "string") return value;
|
|
3581
|
+
if (Array.isArray(value)) return value.map(extractReasoningText).join("");
|
|
3582
|
+
if (value && typeof value === "object") {
|
|
3583
|
+
const record = value;
|
|
3584
|
+
for (const key of [
|
|
3585
|
+
"text",
|
|
3586
|
+
"content",
|
|
3587
|
+
"reasoning",
|
|
3588
|
+
"reasoning_content",
|
|
3589
|
+
"thinking",
|
|
3590
|
+
"value"
|
|
3591
|
+
]) {
|
|
3592
|
+
const nested = extractReasoningText(record[key]);
|
|
3593
|
+
if (nested) return nested;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
return "";
|
|
3597
|
+
}
|
|
3598
|
+
function extractReasoningDeltas(delta) {
|
|
3599
|
+
const deltas = [];
|
|
3600
|
+
for (const field of REASONING_FIELDS) {
|
|
3601
|
+
const text = extractReasoningText(delta[field]);
|
|
3602
|
+
if (text) deltas.push({
|
|
3603
|
+
field,
|
|
3604
|
+
text
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
return deltas;
|
|
3608
|
+
}
|
|
3609
|
+
function isChatReplayContentPart(value) {
|
|
3610
|
+
if (!value || typeof value !== "object") return false;
|
|
3611
|
+
const part = value;
|
|
3612
|
+
if (part.type === "text") return typeof part.text === "string";
|
|
3613
|
+
if (part.type === "image_url") {
|
|
3614
|
+
const imageUrl = part.image_url;
|
|
3615
|
+
if (!imageUrl || typeof imageUrl !== "object") return false;
|
|
3616
|
+
const image = imageUrl;
|
|
3617
|
+
if (typeof image.url !== "string") return false;
|
|
3618
|
+
if (image.detail !== void 0 && image.detail !== "auto" && image.detail !== "low" && image.detail !== "high") return false;
|
|
3619
|
+
return true;
|
|
3620
|
+
}
|
|
3621
|
+
return false;
|
|
3622
|
+
}
|
|
3835
3623
|
function isChatReplayToolCall(value) {
|
|
3836
3624
|
if (!value || typeof value !== "object") return false;
|
|
3837
3625
|
const entry = value;
|
|
@@ -3846,7 +3634,8 @@ function isChatReplayMessage(value) {
|
|
|
3846
3634
|
const msg = value;
|
|
3847
3635
|
const role = msg.role;
|
|
3848
3636
|
if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
|
|
3849
|
-
|
|
3637
|
+
const content = msg.content;
|
|
3638
|
+
if (!(content === null || typeof content === "string" || Array.isArray(content) && content.every(isChatReplayContentPart))) return false;
|
|
3850
3639
|
if (msg.tool_calls !== void 0) {
|
|
3851
3640
|
if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
|
|
3852
3641
|
}
|
|
@@ -3858,20 +3647,64 @@ function assertChatReplayMessages(messages, field) {
|
|
|
3858
3647
|
if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
|
|
3859
3648
|
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
3649
|
}
|
|
3650
|
+
function buildAssistantReplayMessage(params) {
|
|
3651
|
+
const { content, reasoningByField, toolCalls } = params;
|
|
3652
|
+
if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
|
|
3653
|
+
const replayMessage = {
|
|
3654
|
+
role: "assistant",
|
|
3655
|
+
content: content || null
|
|
3656
|
+
};
|
|
3657
|
+
for (const [field, text] of reasoningByField) replayMessage[field] = text;
|
|
3658
|
+
if (toolCalls.length > 0) replayMessage.tool_calls = toolCalls.map((toolCall) => ({
|
|
3659
|
+
id: toolCall.id,
|
|
3660
|
+
type: "function",
|
|
3661
|
+
function: {
|
|
3662
|
+
name: toolCall.name,
|
|
3663
|
+
arguments: toolCall.args
|
|
3664
|
+
}
|
|
3665
|
+
}));
|
|
3666
|
+
return replayMessage;
|
|
3667
|
+
}
|
|
3668
|
+
function mapChatUserContent(blocks, field) {
|
|
3669
|
+
mapper$4.ensureBlocks(blocks, field, [
|
|
3670
|
+
"text",
|
|
3671
|
+
"json",
|
|
3672
|
+
"image"
|
|
3673
|
+
], "only text/json/image blocks are supported");
|
|
3674
|
+
if (!blocks.some((block) => block.type === "image")) return contentBlocksToText(blocks) || null;
|
|
3675
|
+
return blocks.map((block) => {
|
|
3676
|
+
if (block.type === "text") return {
|
|
3677
|
+
type: "text",
|
|
3678
|
+
text: block.text
|
|
3679
|
+
};
|
|
3680
|
+
if (block.type === "json") return {
|
|
3681
|
+
type: "text",
|
|
3682
|
+
text: JSON.stringify(block.json)
|
|
3683
|
+
};
|
|
3684
|
+
if (block.type === "image") return {
|
|
3685
|
+
type: "image_url",
|
|
3686
|
+
image_url: { url: block.imageUrl }
|
|
3687
|
+
};
|
|
3688
|
+
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");
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
function mapChatMessageContent(role, blocks, field) {
|
|
3692
|
+
if (role === "user") return mapChatUserContent(blocks, field);
|
|
3693
|
+
return mapper$4.textFromBlocks(blocks, field) || null;
|
|
3694
|
+
}
|
|
3861
3695
|
function buildChatCompletionsRequest(request, options) {
|
|
3862
|
-
mapper$
|
|
3696
|
+
mapper$4.assertNoServerTools(request.serverTools);
|
|
3863
3697
|
const messages = [];
|
|
3864
3698
|
if (request.instructions) messages.push({
|
|
3865
3699
|
role: "system",
|
|
3866
|
-
content: mapper$
|
|
3700
|
+
content: mapper$4.mapInstructions(request.instructions)
|
|
3867
3701
|
});
|
|
3868
3702
|
for (const item of request.input) switch (item.type) {
|
|
3869
3703
|
case "message": {
|
|
3870
3704
|
const role = item.role;
|
|
3871
|
-
const text = mapper$5.textFromBlocks(item.content, `input message (${item.role}) content`);
|
|
3872
3705
|
messages.push({
|
|
3873
3706
|
role,
|
|
3874
|
-
content:
|
|
3707
|
+
content: mapChatMessageContent(role, item.content, `input message (${item.role}) content`)
|
|
3875
3708
|
});
|
|
3876
3709
|
break;
|
|
3877
3710
|
}
|
|
@@ -3898,13 +3731,13 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3898
3731
|
role: "tool",
|
|
3899
3732
|
tool_call_id: item.callId,
|
|
3900
3733
|
name: item.toolName,
|
|
3901
|
-
content: mapper$
|
|
3734
|
+
content: mapper$4.textFromBlocks(item.content, `tool_result ${item.callId} content`)
|
|
3902
3735
|
});
|
|
3903
3736
|
break;
|
|
3904
3737
|
case "reasoning":
|
|
3905
3738
|
messages.push({
|
|
3906
3739
|
role: "assistant",
|
|
3907
|
-
content: mapper$
|
|
3740
|
+
content: mapper$4.textFromBlocks(item.content, "reasoning content")
|
|
3908
3741
|
});
|
|
3909
3742
|
break;
|
|
3910
3743
|
case "opaque": {
|
|
@@ -3912,7 +3745,7 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3912
3745
|
if (!payload) break;
|
|
3913
3746
|
if ("messages" in payload) {
|
|
3914
3747
|
assertChatReplayMessages(payload.messages, "messages");
|
|
3915
|
-
mapper$
|
|
3748
|
+
mapper$4.rollbackTrailingAssistantMessages(messages);
|
|
3916
3749
|
for (const m of payload.messages) messages.push(m);
|
|
3917
3750
|
}
|
|
3918
3751
|
break;
|
|
@@ -3924,8 +3757,8 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3924
3757
|
stream: true,
|
|
3925
3758
|
n: 1
|
|
3926
3759
|
};
|
|
3927
|
-
body.tools = mapper$
|
|
3928
|
-
body.tool_choice = mapper$
|
|
3760
|
+
body.tools = mapper$4.mapToolsIfPresent(request.tools, mapOpenAiFunctionTool);
|
|
3761
|
+
body.tool_choice = mapper$4.mapToolChoice(request.toolChoice, {
|
|
3929
3762
|
auto: "auto",
|
|
3930
3763
|
none: "none",
|
|
3931
3764
|
tool: (name) => ({
|
|
@@ -3939,55 +3772,11 @@ function buildChatCompletionsRequest(request, options) {
|
|
|
3939
3772
|
if (request.reasoningLevel !== void 0) body.reasoning_effort = mapChatCompletionsReasoningEffort(request.reasoningLevel);
|
|
3940
3773
|
return body;
|
|
3941
3774
|
}
|
|
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
|
-
}
|
|
3775
|
+
//#endregion
|
|
3776
|
+
//#region src/adapters/chat-completions/map-stream.ts
|
|
3777
|
+
/**
|
|
3778
|
+
* ChatCompletionsAdapter — stream 映射
|
|
3779
|
+
*/
|
|
3991
3780
|
async function* mapChatCompletionsStream(host, providerRequest, factory, request) {
|
|
3992
3781
|
const session = host.beginJsonStream(factory, request);
|
|
3993
3782
|
const { auxiliary, gate } = session;
|
|
@@ -4243,6 +4032,31 @@ function toWireOllamaToolCalls(toolCalls) {
|
|
|
4243
4032
|
arguments: tc.function.arguments
|
|
4244
4033
|
} }));
|
|
4245
4034
|
}
|
|
4035
|
+
function mapOllamaImageData(imageUrl, field) {
|
|
4036
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
4037
|
+
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");
|
|
4038
|
+
return dataUrl.data;
|
|
4039
|
+
}
|
|
4040
|
+
function mapOllamaUserMessage(blocks, field) {
|
|
4041
|
+
mapper$3.ensureBlocks(blocks, field, [
|
|
4042
|
+
"text",
|
|
4043
|
+
"json",
|
|
4044
|
+
"image"
|
|
4045
|
+
], "only text/json/image blocks are supported");
|
|
4046
|
+
const images = [];
|
|
4047
|
+
const textBlocks = [];
|
|
4048
|
+
for (const block of blocks) {
|
|
4049
|
+
if (block.type === "image") {
|
|
4050
|
+
images.push(mapOllamaImageData(block.imageUrl, field));
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
textBlocks.push(block);
|
|
4054
|
+
}
|
|
4055
|
+
return {
|
|
4056
|
+
content: contentBlocksToText(textBlocks),
|
|
4057
|
+
...images.length > 0 ? { images } : {}
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4246
4060
|
function buildOllamaRequest(request, options) {
|
|
4247
4061
|
mapper$3.assertNoServerTools(request.serverTools);
|
|
4248
4062
|
const messages = [];
|
|
@@ -4254,10 +4068,17 @@ function buildOllamaRequest(request, options) {
|
|
|
4254
4068
|
});
|
|
4255
4069
|
for (const item of request.input) switch (item.type) {
|
|
4256
4070
|
case "message": {
|
|
4257
|
-
const
|
|
4071
|
+
const field = `input message (${item.role}) content`;
|
|
4072
|
+
if (item.role === "user") {
|
|
4073
|
+
messages.push({
|
|
4074
|
+
role: "user",
|
|
4075
|
+
...mapOllamaUserMessage(item.content, field)
|
|
4076
|
+
});
|
|
4077
|
+
break;
|
|
4078
|
+
}
|
|
4258
4079
|
messages.push({
|
|
4259
|
-
role,
|
|
4260
|
-
content: mapper$3.textFromBlocks(item.content,
|
|
4080
|
+
role: item.role,
|
|
4081
|
+
content: mapper$3.textFromBlocks(item.content, field)
|
|
4261
4082
|
});
|
|
4262
4083
|
break;
|
|
4263
4084
|
}
|
|
@@ -4501,6 +4322,14 @@ function appendPart(contents, role, part) {
|
|
|
4501
4322
|
parts: [part]
|
|
4502
4323
|
});
|
|
4503
4324
|
}
|
|
4325
|
+
function mapGeminiImagePart(imageUrl, field) {
|
|
4326
|
+
const dataUrl = parseImageDataUrl(imageUrl);
|
|
4327
|
+
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");
|
|
4328
|
+
return { inlineData: {
|
|
4329
|
+
mimeType: dataUrl.mediaType,
|
|
4330
|
+
data: dataUrl.data
|
|
4331
|
+
} };
|
|
4332
|
+
}
|
|
4504
4333
|
function textPartsFromBlocks(blocks, field) {
|
|
4505
4334
|
return mapper$1.ensureTextBlocks(blocks, field).map((block) => {
|
|
4506
4335
|
if (block.type === "text") return { text: block.text };
|
|
@@ -4508,6 +4337,20 @@ function textPartsFromBlocks(blocks, field) {
|
|
|
4508
4337
|
throw new AIRequestError(`gemini does not support content block type "${block.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4509
4338
|
});
|
|
4510
4339
|
}
|
|
4340
|
+
function partsFromUserBlocks(blocks, field) {
|
|
4341
|
+
mapper$1.ensureBlocks(blocks, field, [
|
|
4342
|
+
"text",
|
|
4343
|
+
"json",
|
|
4344
|
+
"image"
|
|
4345
|
+
], "only text/json/image blocks are supported");
|
|
4346
|
+
if (!blocks.some((block) => block.type === "image")) return textPartsFromBlocks(blocks, field);
|
|
4347
|
+
return blocks.map((block) => {
|
|
4348
|
+
if (block.type === "text") return { text: block.text };
|
|
4349
|
+
if (block.type === "json") return { text: JSON.stringify(block.json) };
|
|
4350
|
+
if (block.type === "image") return mapGeminiImagePart(block.imageUrl, field);
|
|
4351
|
+
throw new AIRequestError(`gemini does not support content block type "${block.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4511
4354
|
function clonePart$1(part) {
|
|
4512
4355
|
return { ...part };
|
|
4513
4356
|
}
|
|
@@ -4524,8 +4367,10 @@ function buildGeminiRequest(request, options) {
|
|
|
4524
4367
|
if (request.instructions) systemInstruction = { parts: [{ text: mapper$1.mapInstructions(request.instructions) }] };
|
|
4525
4368
|
for (const item of request.input) switch (item.type) {
|
|
4526
4369
|
case "message": {
|
|
4370
|
+
const field = `input message (${item.role}) content`;
|
|
4527
4371
|
const role = item.role === "assistant" ? "model" : "user";
|
|
4528
|
-
|
|
4372
|
+
const parts = item.role === "user" ? partsFromUserBlocks(item.content, field) : textPartsFromBlocks(item.content, field);
|
|
4373
|
+
for (const part of parts) appendPart(contents, role, part);
|
|
4529
4374
|
break;
|
|
4530
4375
|
}
|
|
4531
4376
|
case "tool_call":
|