@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/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
- //#endregion
1559
- //#region src/provider/security.ts
1560
- /**
1561
- * Adapter 边界安全辅助
1562
- *
1563
- * - opaque replay envelope(大小 / 深度;emit 与 accept 共用)
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
- raw = JSON.stringify(payload);
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
- /** clamp opaque 体积上限(emit / accept 共用)。 */
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 = clampOpaquePayloadLimit(options.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 统一校验体积、拼接 replay 并 complete。
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$8 = new NormalizedRequestMapper("responses");
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
- /** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
2446
- function messageContentAsString(blocks, field) {
2447
- return mapper$8.textFromBlocks(blocks, field);
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$8.textFromBlocks(mapper$8.ensureReasoningBlocks(item.content, "reasoning content"), "reasoning content");
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: messageContentAsString(item.content, `input message (${item.role}) 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 = mapper$8.textFromBlocks(item.content, `tool_result ${item.callId} content`);
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$8.mapInstructions(request.instructions);
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$8.mapToolsIfPresent(request.tools, (t) => ({
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$8.mapToolChoice(request.toolChoice, {
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$7 = new NormalizedRequestMapper("messages");
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 canonicalToMessagesBlock(b) {
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
- throw new AIRequestError(`messages does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
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$7.assertNoServerTools(request.serverTools);
3243
+ mapper$6.assertNoServerTools(request.serverTools);
3475
3244
  const messages = [];
3476
3245
  let systemPrompt;
3477
3246
  let pendingToolResultMessage;
3478
- if (request.instructions) systemPrompt = mapper$7.mapInstructions(request.instructions);
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 role = item.role === "user" ? "user" : "assistant";
3484
- const supportedContent = mapper$7.ensureTextBlocks(item.content, `input message (${item.role}) content`);
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$7.parseToolArguments(item)
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 = mapper$7.textFromBlocks(item.content, `tool_result ${item.callId} 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$7.ensureReasoningBlocks(item.content, "reasoning content"))
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$7.rollbackTrailingAssistantMessages(messages);
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$7.mapToolsIfPresent(request.tools, (t) => ({
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$7.mapToolChoice(request.toolChoice, {
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$5 = new NormalizedRequestMapper("chat-completions");
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
- if (!(msg.content === null || typeof msg.content === "string")) return false;
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$5.assertNoServerTools(request.serverTools);
3751
+ mapper$4.assertNoServerTools(request.serverTools);
3863
3752
  const messages = [];
3864
3753
  if (request.instructions) messages.push({
3865
3754
  role: "system",
3866
- content: mapper$5.mapInstructions(request.instructions)
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: text || null
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: mapper$5.textFromBlocks(item.content, `tool_result ${item.callId} 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$5.textFromBlocks(item.content, "reasoning content")
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$5.rollbackTrailingAssistantMessages(messages);
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$5.mapToolsIfPresent(request.tools, mapOpenAiFunctionTool);
3928
- body.tool_choice = mapper$5.mapToolChoice(request.toolChoice, {
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
- new NormalizedRequestMapper("chat-completions");
3943
- function extractReasoningText(value) {
3944
- if (typeof value === "string") return value;
3945
- if (Array.isArray(value)) return value.map(extractReasoningText).join("");
3946
- if (value && typeof value === "object") {
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 role = item.role;
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, `input message (${item.role}) 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
- content: mapper$3.textFromBlocks(item.content, `tool_result ${item.callId} content`)
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
- for (const part of textPartsFromBlocks(item.content, `input message (${item.role}) content`)) appendPart(contents, role, part);
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
- let response;
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":