@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/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,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
- /** 将 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
+ });
2448
2113
  }
2449
2114
  function mapReasoningInput(item, index) {
2450
- const text = mapper$8.textFromBlocks(mapper$8.ensureReasoningBlocks(item.content, "reasoning content"), "reasoning content");
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: messageContentAsString(item.content, `input message (${item.role}) 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$8.textFromBlocks(item.content, `tool_result ${item.callId} content`);
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$8.mapInstructions(request.instructions);
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$8.mapToolsIfPresent(request.tools, (t) => ({
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$8.mapToolChoice(request.toolChoice, {
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$7 = new NormalizedRequestMapper("messages");
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 canonicalToMessagesBlock(b) {
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
- throw new AIRequestError(`messages does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
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$7.assertNoServerTools(request.serverTools);
3211
+ mapper$6.assertNoServerTools(request.serverTools);
3475
3212
  const messages = [];
3476
3213
  let systemPrompt;
3477
3214
  let pendingToolResultMessage;
3478
- if (request.instructions) systemPrompt = mapper$7.mapInstructions(request.instructions);
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 role = item.role === "user" ? "user" : "assistant";
3484
- const supportedContent = mapper$7.ensureTextBlocks(item.content, `input message (${item.role}) content`);
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$7.parseToolArguments(item)
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$7.textFromBlocks(item.content, `tool_result ${item.callId} content`);
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$7.ensureReasoningBlocks(item.content, "reasoning content"))
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$7.rollbackTrailingAssistantMessages(messages);
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$7.mapToolsIfPresent(request.tools, (t) => ({
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$7.mapToolChoice(request.toolChoice, {
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$5 = new NormalizedRequestMapper("chat-completions");
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
- if (!(msg.content === null || typeof msg.content === "string")) return false;
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$5.assertNoServerTools(request.serverTools);
3696
+ mapper$4.assertNoServerTools(request.serverTools);
3863
3697
  const messages = [];
3864
3698
  if (request.instructions) messages.push({
3865
3699
  role: "system",
3866
- content: mapper$5.mapInstructions(request.instructions)
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: text || null
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$5.textFromBlocks(item.content, `tool_result ${item.callId} content`)
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$5.textFromBlocks(item.content, "reasoning content")
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$5.rollbackTrailingAssistantMessages(messages);
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$5.mapToolsIfPresent(request.tools, mapOpenAiFunctionTool);
3928
- body.tool_choice = mapper$5.mapToolChoice(request.toolChoice, {
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
- 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
- }
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 role = item.role;
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, `input message (${item.role}) 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
- for (const part of textPartsFromBlocks(item.content, `input message (${item.role}) content`)) appendPart(contents, role, part);
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":