@codehz/ai 0.7.0 → 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
@@ -74,7 +74,9 @@ const WarningCode = {
74
74
  /** MCP 审批流不被支持 */
75
75
  MCP_APPROVAL_REQUIRED: "MCP_APPROVAL_REQUIRED",
76
76
  /** provider 侧 response.failed 等失败 */
77
- PROVIDER_FAILURE: "PROVIDER_FAILURE"
77
+ PROVIDER_FAILURE: "PROVIDER_FAILURE",
78
+ /** 出站 opaque 超限被省略(避免下一轮 accept 自产毒) */
79
+ OPAQUE_REPLAY_OMITTED: "OPAQUE_REPLAY_OMITTED"
78
80
  };
79
81
  /**
80
82
  * 去重键:以 message 为主(与旧 string[] 行为一致)。
@@ -90,6 +92,60 @@ function supportsContextCompress(adapter) {
90
92
  return typeof adapter.compress === "function";
91
93
  }
92
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
93
149
  //#region src/runtime/errors.ts
94
150
  var AIError = class extends Error {
95
151
  code;
@@ -163,369 +219,6 @@ var AIRecoverableError = class extends AIError {
163
219
  }
164
220
  };
165
221
  //#endregion
166
- //#region src/runtime/validation.ts
167
- const MESSAGE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
168
- const REASONING_VISIBILITIES = /* @__PURE__ */ new Set([
169
- "full",
170
- "summary",
171
- "redacted",
172
- "opaque"
173
- ]);
174
- const TOOL_RESULT_OUTCOMES = /* @__PURE__ */ new Set([
175
- "success",
176
- "error",
177
- "rejected"
178
- ]);
179
- const INCLUDE_MODES = /* @__PURE__ */ new Set(["off", "best_effort"]);
180
- function isRecord(value) {
181
- return typeof value === "object" && value !== null;
182
- }
183
- function pushIssue(issues, field, code, message) {
184
- issues.push({
185
- field,
186
- code,
187
- message
188
- });
189
- }
190
- function validateContentBlock(block, field, issues) {
191
- if (!isRecord(block) || typeof block.type !== "string") {
192
- pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field} must be a valid ContentBlock`);
193
- return;
194
- }
195
- switch (block.type) {
196
- case "text":
197
- if (typeof block.text !== "string") pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.text must be a string`);
198
- return;
199
- case "json":
200
- if (!("json" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.json must be present`);
201
- return;
202
- case "image":
203
- if (typeof block.imageUrl !== "string" || block.imageUrl.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.imageUrl must be a non-empty string`);
204
- return;
205
- case "binary_ref":
206
- if (typeof block.ref !== "string" || block.ref.length === 0) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.ref must be a non-empty string`);
207
- return;
208
- case "opaque":
209
- if (!("payload" in block)) pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.payload must be present`);
210
- return;
211
- default: pushIssue(issues, field, "CONTENT_BLOCK_INVALID", `${field}.type "${block.type}" is not supported`);
212
- }
213
- }
214
- function validateContentArray(content, field, issues, code) {
215
- if (!Array.isArray(content)) {
216
- pushIssue(issues, field, code, `${field} must be a ContentBlock[]`);
217
- return;
218
- }
219
- for (let i = 0; i < content.length; i++) validateContentBlock(content[i], `${field}[${i}]`, issues);
220
- }
221
- function validateInstructionArray(content, field, issues) {
222
- if (!Array.isArray(content)) {
223
- pushIssue(issues, field, "INSTRUCTIONS_INVALID", `${field} must be an InstructionBlock[]`);
224
- return;
225
- }
226
- for (let i = 0; i < content.length; i++) {
227
- const block = content[i];
228
- const blockField = `${field}[${i}]`;
229
- validateContentBlock(block, blockField, issues);
230
- if (!isRecord(block) || typeof block.type !== "string") continue;
231
- if (block.type !== "text" && block.type !== "json") pushIssue(issues, blockField, "INSTRUCTIONS_INVALID", `${blockField} only supports text/json blocks`);
232
- }
233
- }
234
- function validateInputItem(item, field, issues) {
235
- if (!isRecord(item)) {
236
- pushIssue(issues, field, "INPUT_INVALID_ITEM", `${field} must be a valid InputItem`);
237
- return;
238
- }
239
- if (typeof item.type !== "string") {
240
- pushIssue(issues, field, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type must be a supported InputItem type`);
241
- return;
242
- }
243
- switch (item.type) {
244
- case "message":
245
- 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`);
246
- validateContentArray(item.content, `${field}.content`, issues, "MESSAGE_CONTENT_INVALID");
247
- return;
248
- case "reasoning":
249
- 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`);
250
- validateContentArray(item.content, `${field}.content`, issues, "REASONING_CONTENT_INVALID");
251
- return;
252
- case "server_tool_call":
253
- 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`);
254
- 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`);
255
- if (item.argumentsText !== void 0 && typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "SERVER_TOOL_CALL_INVALID", `${field}.argumentsText must be a string`);
256
- return;
257
- case "server_tool_result":
258
- 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`);
259
- 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`);
260
- if (item.outcome !== "success" && item.outcome !== "error") pushIssue(issues, `${field}.outcome`, "SERVER_TOOL_RESULT_INVALID", `${field}.outcome must be success or error`);
261
- validateContentArray(item.content, `${field}.content`, issues, "SERVER_TOOL_RESULT_INVALID");
262
- return;
263
- case "server_tool_discovery":
264
- 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`);
265
- if (item.tool !== "mcp") pushIssue(issues, `${field}.tool`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.tool must be "mcp"`);
266
- 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`);
267
- if (!Array.isArray(item.tools)) pushIssue(issues, `${field}.tools`, "SERVER_TOOL_DISCOVERY_INVALID", `${field}.tools must be an array`);
268
- return;
269
- case "tool_call":
270
- 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`);
271
- 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`);
272
- if (typeof item.argumentsText !== "string") pushIssue(issues, `${field}.argumentsText`, "TOOL_CALL_ARGUMENTS_INVALID", `${field}.argumentsText must be a string`);
273
- return;
274
- case "tool_result":
275
- 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`);
276
- 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`);
277
- 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`);
278
- validateContentArray(item.content, `${field}.content`, issues, "TOOL_RESULT_CONTENT_INVALID");
279
- return;
280
- case "opaque":
281
- if (typeof item.source !== "string" || item.source.length === 0) pushIssue(issues, `${field}.source`, "OPAQUE_SOURCE_INVALID", `${field}.source must be a non-empty string`);
282
- if (typeof item.purpose !== "string" || item.purpose.length === 0) pushIssue(issues, `${field}.purpose`, "OPAQUE_PURPOSE_INVALID", `${field}.purpose must be a non-empty string`);
283
- return;
284
- default: pushIssue(issues, `${field}.type`, "INPUT_ITEM_UNKNOWN_TYPE", `${field}.type "${item.type}" is not supported`);
285
- }
286
- }
287
- function validateTools(tools, issues) {
288
- if (tools === void 0) return;
289
- if (!Array.isArray(tools)) {
290
- pushIssue(issues, "tools", "TOOLS_INVALID", "tools must be an array");
291
- return;
292
- }
293
- const seenNames = /* @__PURE__ */ new Set();
294
- for (let i = 0; i < tools.length; i++) {
295
- const tool = tools[i];
296
- const field = `tools[${i}]`;
297
- if (!isRecord(tool)) {
298
- pushIssue(issues, field, "TOOL_INVALID", `${field} must be a valid ToolDefinition`);
299
- continue;
300
- }
301
- if (typeof tool.name !== "string" || tool.name.length === 0) pushIssue(issues, `${field}.name`, "TOOL_NAME_INVALID", `${field}.name must be a non-empty string`);
302
- else {
303
- if (seenNames.has(tool.name)) pushIssue(issues, `${field}.name`, "TOOLS_DUPLICATE_NAME", `tool name "${tool.name}" is duplicated`);
304
- seenNames.add(tool.name);
305
- }
306
- if (tool.description !== void 0 && typeof tool.description !== "string") pushIssue(issues, `${field}.description`, "TOOL_DESCRIPTION_INVALID", `${field}.description must be a string`);
307
- if (!isRecord(tool.inputSchema)) pushIssue(issues, `${field}.inputSchema`, "TOOL_INPUT_SCHEMA_INVALID", `${field}.inputSchema must be an object`);
308
- }
309
- }
310
- const SEARCH_CONTEXT_SIZES = /* @__PURE__ */ new Set([
311
- "low",
312
- "medium",
313
- "high"
314
- ]);
315
- const CODE_MEMORY_LIMITS = /* @__PURE__ */ new Set([
316
- "1g",
317
- "4g",
318
- "16g",
319
- "64g"
320
- ]);
321
- function validateStringArrayField(value, field, code, issues) {
322
- if (!Array.isArray(value)) {
323
- pushIssue(issues, field, code, `${field} must be a string array`);
324
- return;
325
- }
326
- 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`);
327
- }
328
- function validateServerTools(serverTools, issues) {
329
- if (serverTools === void 0) return;
330
- if (!Array.isArray(serverTools)) {
331
- pushIssue(issues, "serverTools", "SERVER_TOOLS_INVALID", "serverTools must be an array");
332
- return;
333
- }
334
- for (let i = 0; i < serverTools.length; i++) {
335
- const tool = serverTools[i];
336
- const field = `serverTools[${i}]`;
337
- if (!isRecord(tool) || typeof tool.type !== "string") {
338
- pushIssue(issues, field, "SERVER_TOOL_INVALID", `${field} must be a valid ServerToolDefinition`);
339
- continue;
340
- }
341
- switch (tool.type) {
342
- case "web_search":
343
- 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`);
344
- if (tool.allowedDomains !== void 0) validateStringArrayField(tool.allowedDomains, `${field}.allowedDomains`, "SERVER_TOOL_INVALID", issues);
345
- if (tool.blockedDomains !== void 0) validateStringArrayField(tool.blockedDomains, `${field}.blockedDomains`, "SERVER_TOOL_INVALID", issues);
346
- if (tool.searchContextSize !== void 0) {
347
- 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`);
348
- }
349
- 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", ... }`);
350
- else for (const key of [
351
- "country",
352
- "city",
353
- "region",
354
- "timezone"
355
- ]) {
356
- const value = tool.userLocation[key];
357
- if (value !== void 0 && typeof value !== "string") pushIssue(issues, `${field}.userLocation.${key}`, "SERVER_TOOL_INVALID", `${field}.userLocation.${key} must be a string`);
358
- }
359
- break;
360
- case "code_execution":
361
- 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", ... }`);
362
- else {
363
- if (tool.container.memoryLimit !== void 0) {
364
- 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`);
365
- }
366
- if (tool.container.fileIds !== void 0) validateStringArrayField(tool.container.fileIds, `${field}.container.fileIds`, "SERVER_TOOL_INVALID", issues);
367
- }
368
- break;
369
- case "mcp":
370
- if (typeof tool.serverLabel !== "string" || tool.serverLabel.length === 0) pushIssue(issues, `${field}.serverLabel`, "SERVER_TOOL_INVALID", `${field}.serverLabel must be a non-empty string`);
371
- if (typeof tool.serverUrl !== "string" || tool.serverUrl.length === 0) pushIssue(issues, `${field}.serverUrl`, "SERVER_TOOL_INVALID", `${field}.serverUrl must be a non-empty string`);
372
- if (tool.serverDescription !== void 0 && typeof tool.serverDescription !== "string") pushIssue(issues, `${field}.serverDescription`, "SERVER_TOOL_INVALID", `${field}.serverDescription must be a string`);
373
- if (tool.authorization !== void 0 && typeof tool.authorization !== "string") pushIssue(issues, `${field}.authorization`, "SERVER_TOOL_INVALID", `${field}.authorization must be a string`);
374
- if (tool.allowedTools !== void 0) validateStringArrayField(tool.allowedTools, `${field}.allowedTools`, "SERVER_TOOL_INVALID", issues);
375
- if (tool.requireApproval !== "never") pushIssue(issues, `${field}.requireApproval`, "SERVER_TOOL_MCP_APPROVAL_UNSUPPORTED", `${field}.requireApproval must be "never" in this version`);
376
- break;
377
- default: pushIssue(issues, `${field}.type`, "SERVER_TOOL_TYPE_UNSUPPORTED", `${field}.type "${tool.type}" is not supported`);
378
- }
379
- }
380
- }
381
- function validateToolChoice(toolChoice, issues) {
382
- if (toolChoice === void 0) return;
383
- if (toolChoice === "auto" || toolChoice === "none") return;
384
- 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 }");
385
- }
386
- /** Validate include settings, appending issues to the given array. */
387
- function validateInclude(include, issues) {
388
- if (!isRecord(include)) {
389
- pushIssue(issues, "include", "INCLUDE_INVALID", "include must be an object");
390
- return;
391
- }
392
- 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");
393
- 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");
394
- 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");
395
- }
396
- /**
397
- * 校验 AIRequest,返回校验问题列表。
398
- * 空数组表示无问题。
399
- */
400
- function validateRequest(request) {
401
- const issues = [];
402
- if (request.instructions !== void 0) if (typeof request.instructions === "string") {} else if (Array.isArray(request.instructions)) validateInstructionArray(request.instructions, "instructions", issues);
403
- else pushIssue(issues, "instructions", "INSTRUCTIONS_INVALID", "instructions must be a string or InstructionBlock[]");
404
- if (!Array.isArray(request.input) || request.input.length === 0) pushIssue(issues, "input", "INPUT_EMPTY", "input must be a non-empty array");
405
- if (Array.isArray(request.input)) for (let i = 0; i < request.input.length; i++) validateInputItem(request.input[i], `input[${i}]`, issues);
406
- if (request.temperature !== void 0) {
407
- if (typeof request.temperature !== "number" || !Number.isFinite(request.temperature)) issues.push({
408
- field: "temperature",
409
- code: "TEMPERATURE_NOT_NUMBER",
410
- message: "temperature must be a number"
411
- });
412
- else if (request.temperature < 0 || request.temperature > 2) issues.push({
413
- field: "temperature",
414
- code: "TEMPERATURE_OUT_OF_RANGE",
415
- message: "temperature must be between 0 and 2"
416
- });
417
- }
418
- if (request.maxOutputTokens !== void 0) {
419
- if (typeof request.maxOutputTokens !== "number" || !Number.isFinite(request.maxOutputTokens)) issues.push({
420
- field: "maxOutputTokens",
421
- code: "MAX_OUTPUT_TOKENS_NOT_NUMBER",
422
- message: "maxOutputTokens must be a number"
423
- });
424
- else if (!Number.isInteger(request.maxOutputTokens) || request.maxOutputTokens < 1) issues.push({
425
- field: "maxOutputTokens",
426
- code: "MAX_OUTPUT_TOKENS_INVALID",
427
- message: "maxOutputTokens must be a positive integer"
428
- });
429
- }
430
- if (request.reasoningLevel !== void 0) {
431
- 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");
432
- }
433
- if (request.include !== void 0) validateInclude(request.include, issues);
434
- if (request.metadata !== void 0) {
435
- if (!isRecord(request.metadata)) pushIssue(issues, "metadata", "METADATA_INVALID", "metadata must be an object");
436
- 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`);
437
- }
438
- validateTools(request.tools, issues);
439
- validateServerTools(request.serverTools, issues);
440
- validateToolChoice(request.toolChoice, issues);
441
- if (request.toolChoice && typeof request.toolChoice === "object" && "type" in request.toolChoice && request.toolChoice.type === "tool") {
442
- const chosenName = request.toolChoice.name;
443
- if (!request.tools || request.tools.length === 0) issues.push({
444
- field: "toolChoice",
445
- code: "TOOL_CHOICE_NO_TOOLS",
446
- message: `toolChoice specifies tool "${chosenName}" but no tools are defined`
447
- });
448
- else if (!request.tools.some((t) => t.name === chosenName)) issues.push({
449
- field: "toolChoice",
450
- code: "TOOL_CHOICE_UNKNOWN_TOOL",
451
- message: `toolChoice specifies tool "${chosenName}" which is not in tools array`
452
- });
453
- }
454
- return issues;
455
- }
456
- /**
457
- * 校验请求并抛出首个问题。
458
- * 适用于客户端入口的快速失败检查。
459
- */
460
- function assertValidRequest(request) {
461
- const issues = validateRequest(request);
462
- const first = issues[0];
463
- if (first) throw new AIRequestError(first.message, first.code, issues);
464
- }
465
- //#endregion
466
- //#region src/runtime/normalize.ts
467
- const DEFAULT_INCLUDE = {
468
- usage: "best_effort",
469
- billing: "best_effort",
470
- providerMetadata: "best_effort"
471
- };
472
- /**
473
- * 归一化请求:
474
- * 1. 合并 defaults
475
- * 2. 填充 include 默认值
476
- * 3. 生成 requestId
477
- * 4. 校验请求合法性
478
- */
479
- function normalizeRequest(request, options) {
480
- const { model, defaults } = options;
481
- const earlyIncludeIssues = [];
482
- if (request.include !== void 0) validateInclude(request.include, earlyIncludeIssues);
483
- if (defaults?.include !== void 0) validateInclude(defaults.include, earlyIncludeIssues);
484
- const firstIncludeIssue = earlyIncludeIssues[0];
485
- if (firstIncludeIssue) throw new AIRequestError(firstIncludeIssue.message, firstIncludeIssue.code, earlyIncludeIssues);
486
- const merged = {
487
- ...defaults,
488
- ...request,
489
- include: {
490
- ...DEFAULT_INCLUDE,
491
- ...defaults?.include,
492
- ...request.include
493
- }
494
- };
495
- assertValidRequest(merged);
496
- return {
497
- ...merged,
498
- model,
499
- requestId: crypto.randomUUID()
500
- };
501
- }
502
- //#endregion
503
- //#region src/runtime/client.ts
504
- function createAIClient(options) {
505
- const { adapter, model, defaults, signal: defaultSignal } = options;
506
- return { stream(request) {
507
- const signal = mergeAbortSignals(defaultSignal, request.signal);
508
- const normalized = normalizeRequest({
509
- ...request,
510
- signal
511
- }, {
512
- model,
513
- defaults
514
- });
515
- return adapter.stream(normalized);
516
- } };
517
- }
518
- /**
519
- * 合并多个 AbortSignal:任一 signal abort 即触发。
520
- * 如果没有 signal 需要合并则返回 undefined。
521
- */
522
- function mergeAbortSignals(...signals) {
523
- const valid = signals.filter((s) => s != null);
524
- if (valid.length === 0) return void 0;
525
- if (valid.length === 1) return valid[0];
526
- return AbortSignal.any(valid);
527
- }
528
- //#endregion
529
222
  //#region src/canonical/content.ts
530
223
  function textBlock(text) {
531
224
  return {
@@ -1553,61 +1246,32 @@ function applyExtraBody(body, extraBody) {
1553
1246
  ...extraBody
1554
1247
  };
1555
1248
  }
1556
- //#endregion
1557
- //#region src/provider/security.ts
1558
- /**
1559
- * Adapter 边界安全辅助
1560
- *
1561
- * - opaque replay 入站 envelope(大小 / 深度)
1562
- * - provider HTTP 错误 body 出站脱敏
1563
- */
1564
- const MAX_OPAQUE_PAYLOAD_BYTES = 65536;
1565
- /** 测量 JSON 值嵌套深度(对象/数组);循环引用按已访问节点深度计。 */
1566
- function measureJsonDepth(value, seen = /* @__PURE__ */ new WeakSet()) {
1567
- if (value === null || typeof value !== "object") return 0;
1568
- if (seen.has(value)) return 0;
1569
- seen.add(value);
1570
- let maxChild = 0;
1571
- if (Array.isArray(value)) for (const item of value) maxChild = Math.max(maxChild, measureJsonDepth(item, seen));
1572
- else for (const key of Object.keys(value)) maxChild = Math.max(maxChild, measureJsonDepth(value[key], seen));
1573
- return 1 + maxChild;
1574
- }
1575
- /**
1576
- * Opaque replay 通用 envelope:必须是 object、体积 ≤ 64KB、深度 ≤ 8。
1577
- * 不校验 adapter 专用字段形状。
1578
- */
1579
- function validateOpaqueReplayEnvelope(payload) {
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) {
1580
1255
  if (typeof payload !== "object" || payload === null) return {
1581
1256
  ok: false,
1582
1257
  reason: "payload must be an object"
1583
1258
  };
1584
- let raw;
1585
1259
  try {
1586
- raw = JSON.stringify(payload);
1260
+ if (JSON.stringify(payload) === void 0) return {
1261
+ ok: false,
1262
+ reason: "payload is not JSON-serializable"
1263
+ };
1587
1264
  } catch {
1588
1265
  return {
1589
1266
  ok: false,
1590
1267
  reason: "payload is not JSON-serializable"
1591
1268
  };
1592
1269
  }
1593
- if (raw === void 0) return {
1594
- ok: false,
1595
- reason: "payload is not JSON-serializable"
1596
- };
1597
- if (raw.length > 65536) return {
1598
- ok: false,
1599
- reason: `opaque payload exceeds max size (${raw.length} > ${MAX_OPAQUE_PAYLOAD_BYTES})`
1600
- };
1601
- const depth = measureJsonDepth(payload);
1602
- if (depth > 8) return {
1603
- ok: false,
1604
- reason: `opaque payload nesting depth (${depth}) exceeds max (8)`
1605
- };
1606
1270
  return { ok: true };
1607
1271
  }
1608
- /** envelope 失败时抛 AIRequestError */
1609
- function assertOpaqueReplayEnvelope(payload) {
1610
- const result = validateOpaqueReplayEnvelope(payload);
1272
+ /** envelope 失败时抛 AIRequestError(入站 accept 路径)。 */
1273
+ function assertOpaqueReplayEnvelope(payload, options) {
1274
+ const result = validateOpaqueReplayEnvelope(payload, options);
1611
1275
  if (!result.ok) throw new AIRequestError(`Invalid opaque replay payload: ${result.reason}`, "INVALID_OPAQUE_REPLAY");
1612
1276
  }
1613
1277
  /**
@@ -1838,6 +1502,8 @@ var HttpAdapterBase = class extends AdapterBase {
1838
1502
  fetchFn;
1839
1503
  headers;
1840
1504
  extraBody;
1505
+ /** Deprecated compatibility field; opaque replay is not size-limited. */
1506
+ maxOpaquePayloadBytes;
1841
1507
  constructor(options, defaults) {
1842
1508
  super();
1843
1509
  this.apiKey = options.apiKey;
@@ -1845,6 +1511,7 @@ var HttpAdapterBase = class extends AdapterBase {
1845
1511
  this.fetchFn = options.fetch ?? globalThis.fetch;
1846
1512
  this.headers = options.headers;
1847
1513
  this.extraBody = options.extraBody;
1514
+ this.maxOpaquePayloadBytes = options.maxOpaquePayloadBytes ?? Number.POSITIVE_INFINITY;
1848
1515
  }
1849
1516
  /** 合并内置 headers 与构造期自定义 headers。 */
1850
1517
  mergeHeaders(base) {
@@ -2130,14 +1797,17 @@ function createNdjsonLineParser(isValid) {
2130
1797
  //#region src/provider/finalize-stream-turn.ts
2131
1798
  /**
2132
1799
  * 收敛 incomplete / finish 后的 replay + complete 路径。
2133
- * adapter 负责构造 opaque payload;本 helper 统一拼接 replay 并 complete。
1800
+ * adapter 负责构造 opaque payload;本 helper 负责拼接 replay 并 complete。
1801
+ * opaque payload 不在客户端库内截断或施加大小 / 深度限制。
2134
1802
  */
2135
1803
  /**
2136
1804
  * 从 item session 生成 canonical replay,可选追加 opaque 尾项,再 yield session.complete。
1805
+ * 超限 opaque 会被丢弃并 yield `OPAQUE_REPLAY_OMITTED` warning。
2137
1806
  */
2138
1807
  async function* finalizeStreamTurn(session, items, options = {}) {
2139
1808
  const replay = [...replayFromOutput(items.completedItems())];
2140
- if (options.opaque) replay.push(options.opaque);
1809
+ let opaque = options.opaque ?? null;
1810
+ if (opaque) replay.push(opaque);
2141
1811
  yield* session.complete({
2142
1812
  replay,
2143
1813
  stopReason: options.stopReason,
@@ -2163,7 +1833,7 @@ const OPAQUE_SOURCE = {
2163
1833
  * Opaque replay 统一协议(入站薄层)
2164
1834
  *
2165
1835
  * 过滤:仅 `source === expectedSource` 且 `purpose === "replay"` 才处理,否则忽略。
2166
- * envelope:object / ≤64KB / depth≤8;失败抛 AIRequestError / INVALID_OPAQUE_REPLAY。
1836
+ * envelope:object / ≤limit(默认 1MiB,硬顶 8MiB)/ depth≤8;失败抛 AIRequestError / INVALID_OPAQUE_REPLAY。
2167
1837
  * 写入 wire 尾部 assistant/model turn 前:先 rollbackTrailing*,再 append(responses 续写 id 除外)。
2168
1838
  * 已知 shape 非法 → 抛 INVALID_OPAQUE_REPLAY;未知 shape → 静默跳过。
2169
1839
  */
@@ -2171,9 +1841,9 @@ const OPAQUE_SOURCE = {
2171
1841
  * 接受本 adapter 的 opaque replay payload。
2172
1842
  * source/purpose 不匹配返回 null;匹配则 assert envelope 后返回 payload object。
2173
1843
  */
2174
- function acceptOpaqueReplay(item, expectedSource) {
1844
+ function acceptOpaqueReplay(item, expectedSource, options) {
2175
1845
  if (item.source !== expectedSource || item.purpose !== "replay") return null;
2176
- assertOpaqueReplayEnvelope(item.payload);
1846
+ assertOpaqueReplayEnvelope(item.payload, options);
2177
1847
  return item.payload;
2178
1848
  }
2179
1849
  //#endregion
@@ -2398,7 +2068,7 @@ function mapServerTools(serverTools) {
2398
2068
  *
2399
2069
  * 同时服务 stream(/responses)与 compress(/responses/compact)。
2400
2070
  */
2401
- const mapper$8 = new NormalizedRequestMapper("responses");
2071
+ const mapper$7 = new NormalizedRequestMapper("responses");
2402
2072
  /** compact replay opaque:整份 wire output window 保真回传 */
2403
2073
  const RESPONSES_COMPACTED_WINDOW_KIND = "compacted_window";
2404
2074
  function isReplayCanonicalInput(item) {
@@ -2411,12 +2081,38 @@ function readNonEmptyString(value, maxLen = 256) {
2411
2081
  if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return void 0;
2412
2082
  return value;
2413
2083
  }
2414
- /** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
2415
- function messageContentAsString(blocks, field) {
2416
- 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
+ });
2417
2113
  }
2418
2114
  function mapReasoningInput(item, index) {
2419
- 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");
2420
2116
  const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
2421
2117
  if (item.visibility === "full") return {
2422
2118
  type: "reasoning",
@@ -2465,7 +2161,7 @@ function appendCompactedWindow(input, payload) {
2465
2161
  * stream / compact 共享的 input + instructions + opaque 续写映射。
2466
2162
  * compact 不附带 tools / stream 等生成字段。
2467
2163
  */
2468
- function mapResponsesCore(request) {
2164
+ function mapResponsesCore(request, options) {
2469
2165
  const input = [];
2470
2166
  let previousResponseId;
2471
2167
  let usedCompactedWindow = false;
@@ -2475,7 +2171,7 @@ function mapResponsesCore(request) {
2475
2171
  input.push({
2476
2172
  type: "message",
2477
2173
  role: item.role,
2478
- content: messageContentAsString(item.content, `input message (${item.role}) content`)
2174
+ content: mapResponsesMessageContent(item.role, item.content, `input message (${item.role}) content`)
2479
2175
  });
2480
2176
  break;
2481
2177
  case "reasoning":
@@ -2490,7 +2186,7 @@ function mapResponsesCore(request) {
2490
2186
  });
2491
2187
  break;
2492
2188
  case "tool_result": {
2493
- 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`);
2494
2190
  input.push({
2495
2191
  type: "function_call_output",
2496
2192
  call_id: item.callId,
@@ -2499,7 +2195,7 @@ function mapResponsesCore(request) {
2499
2195
  break;
2500
2196
  }
2501
2197
  case "opaque": {
2502
- const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.RESPONSES);
2198
+ const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.RESPONSES, { maxBytes: options?.maxOpaquePayloadBytes });
2503
2199
  if (!payload) break;
2504
2200
  if (payload.kind === "compacted_window") {
2505
2201
  assertOptionalIdFields(payload);
@@ -2526,12 +2222,12 @@ function mapResponsesCore(request) {
2526
2222
  usedCompactedWindow
2527
2223
  };
2528
2224
  if (previousResponseId && !usedCompactedWindow) mapped.previousResponseId = previousResponseId;
2529
- if (request.instructions) mapped.instructions = mapper$8.mapInstructions(request.instructions);
2225
+ if (request.instructions) mapped.instructions = mapper$7.mapInstructions(request.instructions);
2530
2226
  return mapped;
2531
2227
  }
2532
2228
  /** 构建 Responses 流式请求体;调用方再 `withExtraBody` 合并构造期扩展字段。 */
2533
- function buildResponsesRequest(request) {
2534
- const core = mapResponsesCore(request);
2229
+ function buildResponsesRequest(request, options) {
2230
+ const core = mapResponsesCore(request, options);
2535
2231
  const body = {
2536
2232
  model: request.model,
2537
2233
  input: core.input,
@@ -2539,7 +2235,7 @@ function buildResponsesRequest(request) {
2539
2235
  };
2540
2236
  if (core.previousResponseId) body.previous_response_id = core.previousResponseId;
2541
2237
  if (core.instructions) body.instructions = core.instructions;
2542
- const functionTools = mapper$8.mapToolsIfPresent(request.tools, (t) => ({
2238
+ const functionTools = mapper$7.mapToolsIfPresent(request.tools, (t) => ({
2543
2239
  type: "function",
2544
2240
  name: t.name,
2545
2241
  description: t.description,
@@ -2548,7 +2244,7 @@ function buildResponsesRequest(request) {
2548
2244
  const serverTools = mapServerTools(request.serverTools);
2549
2245
  const tools = [...functionTools, ...serverTools];
2550
2246
  if (tools.length > 0) body.tools = tools;
2551
- body.tool_choice = mapper$8.mapToolChoice(request.toolChoice, {
2247
+ body.tool_choice = mapper$7.mapToolChoice(request.toolChoice, {
2552
2248
  auto: "auto",
2553
2249
  none: "none",
2554
2250
  tool: (name) => ({
@@ -2566,10 +2262,10 @@ function buildResponsesRequest(request) {
2566
2262
  * 构建 Responses compact 请求体(POST /responses/compact)。
2567
2263
  * 仅映射 model / input / instructions / previous_response_id;无 stream / tools。
2568
2264
  */
2569
- function buildResponsesCompactRequest(request) {
2265
+ function buildResponsesCompactRequest(request, options) {
2570
2266
  if (!request.model || typeof request.model !== "string" || request.model.length === 0) throw new AIRequestError("compress requires a non-empty model", "INPUT_EMPTY");
2571
2267
  if (!Array.isArray(request.input) || request.input.length === 0) throw new AIRequestError("compress requires a non-empty input", "INPUT_EMPTY");
2572
- const core = mapResponsesCore(request);
2268
+ const core = mapResponsesCore(request, options);
2573
2269
  const body = {
2574
2270
  model: request.model,
2575
2271
  input: core.input
@@ -3321,7 +3017,7 @@ var ResponsesAdapter = class extends HttpAdapterBase {
3321
3017
  super(options, { baseUrl: "https://api.openai.com/v1" });
3322
3018
  }
3323
3019
  buildRequest(request) {
3324
- return this.withExtraBody(buildResponsesRequest(request));
3020
+ return this.withExtraBody(buildResponsesRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
3325
3021
  }
3326
3022
  /**
3327
3023
  * 原生上下文压缩:POST /responses/compact。
@@ -3329,7 +3025,7 @@ var ResponsesAdapter = class extends HttpAdapterBase {
3329
3025
  */
3330
3026
  async compress(request) {
3331
3027
  request.signal?.throwIfAborted();
3332
- const body = this.withExtraBody(buildResponsesCompactRequest(request));
3028
+ const body = this.withExtraBody(buildResponsesCompactRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
3333
3029
  const { data } = await postProviderJson({
3334
3030
  fetchFn: this.fetchFn,
3335
3031
  url: `${this.baseUrl}/responses/compact`,
@@ -3346,6 +3042,7 @@ var ResponsesAdapter = class extends HttpAdapterBase {
3346
3042
  output: data.output
3347
3043
  };
3348
3044
  if (typeof data.id === "string" && data.id.length > 0 && data.id.length <= 256) payload.id = data.id;
3045
+ assertOpaqueReplayEnvelope(payload, { maxBytes: this.maxOpaquePayloadBytes });
3349
3046
  const result = { replay: [opaqueItem(OPAQUE_SOURCE.RESPONSES, "replay", payload)] };
3350
3047
  if (data.usage) {
3351
3048
  const usage = usageFromOpenAIResponses(data.usage);
@@ -3394,21 +3091,62 @@ var ResponsesAdapter = class extends HttpAdapterBase {
3394
3091
  }) : null,
3395
3092
  stopReason,
3396
3093
  rawResponseId,
3094
+ factory,
3095
+ maxOpaquePayloadBytes: this.maxOpaquePayloadBytes,
3397
3096
  onDuplicate: "silent"
3398
3097
  });
3399
3098
  }
3400
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
+ }
3401
3131
  //#endregion
3402
3132
  //#region src/adapters/messages/map-request.ts
3403
3133
  /**
3404
3134
  * MessagesAdapter — request 映射
3405
3135
  */
3406
- const mapper$7 = new NormalizedRequestMapper("messages");
3136
+ const mapper$6 = new NormalizedRequestMapper("messages");
3407
3137
  function isMessagesReplayContentBlock(value) {
3408
3138
  if (!value || typeof value !== "object" || !("type" in value)) return false;
3409
3139
  const block = value;
3410
3140
  switch (block.type) {
3411
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
+ }
3412
3150
  case "thinking": return typeof block.thinking === "string" && (block.signature === void 0 || typeof block.signature === "string");
3413
3151
  case "redacted_thinking": return typeof block.data === "string";
3414
3152
  case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && !!block.input && typeof block.input === "object" && !Array.isArray(block.input);
@@ -3425,7 +3163,26 @@ function assertMessagesReplayContent(content) {
3425
3163
  if (!Array.isArray(content)) throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
3426
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");
3427
3165
  }
3428
- 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") {
3429
3186
  if (b.type === "text") return {
3430
3187
  type: "text",
3431
3188
  text: b.text
@@ -3434,27 +3191,48 @@ function canonicalToMessagesBlock(b) {
3434
3191
  type: "text",
3435
3192
  text: JSON.stringify(b.json)
3436
3193
  };
3437
- throw new AIRequestError(`messages does not support content block type "${b.type}" in canonical mapping`, "UNSUPPORTED_CONTENT_BLOCK");
3438
- }
3439
- function buildMessagesRequest(request) {
3440
- mapper$7.assertNoServerTools(request.serverTools);
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));
3209
+ }
3210
+ function buildMessagesRequest(request, options) {
3211
+ mapper$6.assertNoServerTools(request.serverTools);
3441
3212
  const messages = [];
3442
3213
  let systemPrompt;
3443
3214
  let pendingToolResultMessage;
3444
- if (request.instructions) systemPrompt = mapper$7.mapInstructions(request.instructions);
3215
+ if (request.instructions) systemPrompt = mapper$6.mapInstructions(request.instructions);
3445
3216
  for (const item of request.input) {
3446
3217
  if (item.type !== "tool_result") pendingToolResultMessage = void 0;
3447
3218
  switch (item.type) {
3448
3219
  case "message": {
3449
- const role = item.role === "user" ? "user" : "assistant";
3450
- 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);
3451
3229
  if (supportedContent.length === 1 && supportedContent[0]?.type === "text") messages.push({
3452
- role,
3230
+ role: "assistant",
3453
3231
  content: supportedContent[0].text
3454
3232
  });
3455
3233
  else messages.push({
3456
- role,
3457
- content: supportedContent.map(canonicalToMessagesBlock)
3234
+ role: "assistant",
3235
+ content: supportedContent.map((block) => canonicalToMessagesBlock(block, field))
3458
3236
  });
3459
3237
  break;
3460
3238
  }
@@ -3464,7 +3242,7 @@ function buildMessagesRequest(request) {
3464
3242
  type: "tool_use",
3465
3243
  id: item.id,
3466
3244
  name: item.name,
3467
- input: mapper$7.parseToolArguments(item)
3245
+ input: mapper$6.parseToolArguments(item)
3468
3246
  };
3469
3247
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(toolBlock);
3470
3248
  else messages.push({
@@ -3474,7 +3252,7 @@ function buildMessagesRequest(request) {
3474
3252
  break;
3475
3253
  }
3476
3254
  case "tool_result": {
3477
- 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`);
3478
3256
  const block = {
3479
3257
  type: "tool_result",
3480
3258
  tool_use_id: item.callId,
@@ -3494,7 +3272,7 @@ function buildMessagesRequest(request) {
3494
3272
  case "reasoning": {
3495
3273
  const block = {
3496
3274
  type: "thinking",
3497
- thinking: contentBlocksToText(mapper$7.ensureReasoningBlocks(item.content, "reasoning content"))
3275
+ thinking: contentBlocksToText(mapper$6.ensureReasoningBlocks(item.content, "reasoning content"))
3498
3276
  };
3499
3277
  const lastMsg = messages[messages.length - 1];
3500
3278
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") lastMsg.content.push(block);
@@ -3505,11 +3283,11 @@ function buildMessagesRequest(request) {
3505
3283
  break;
3506
3284
  }
3507
3285
  case "opaque": {
3508
- const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.MESSAGES);
3286
+ const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.MESSAGES, { maxBytes: options?.maxOpaquePayloadBytes });
3509
3287
  if (!payload) break;
3510
3288
  if (payload.role === "assistant" && "content" in payload) {
3511
3289
  assertMessagesReplayContent(payload.content);
3512
- mapper$7.rollbackTrailingAssistantMessages(messages);
3290
+ mapper$6.rollbackTrailingAssistantMessages(messages);
3513
3291
  messages.push({
3514
3292
  role: "assistant",
3515
3293
  content: payload.content
@@ -3526,12 +3304,12 @@ function buildMessagesRequest(request) {
3526
3304
  stream: true
3527
3305
  };
3528
3306
  if (systemPrompt) body.system = systemPrompt;
3529
- body.tools = mapper$7.mapToolsIfPresent(request.tools, (t) => ({
3307
+ body.tools = mapper$6.mapToolsIfPresent(request.tools, (t) => ({
3530
3308
  name: t.name,
3531
3309
  description: t.description,
3532
3310
  input_schema: t.inputSchema
3533
3311
  }));
3534
- body.tool_choice = mapper$7.mapToolChoice(request.toolChoice, {
3312
+ body.tool_choice = mapper$6.mapToolChoice(request.toolChoice, {
3535
3313
  auto: { type: "auto" },
3536
3314
  none: { type: "none" },
3537
3315
  tool: (name) => ({
@@ -3743,6 +3521,8 @@ async function* mapMessagesStream(host, providerRequest, factory, request) {
3743
3521
  }) : null,
3744
3522
  stopReason: stopReason ? mapStopReason(stopReason) : void 0,
3745
3523
  rawResponseId,
3524
+ factory,
3525
+ maxOpaquePayloadBytes: host.maxOpaquePayloadBytes,
3746
3526
  onDuplicate: "silent"
3747
3527
  });
3748
3528
  }
@@ -3762,7 +3542,7 @@ var MessagesAdapter = class extends HttpAdapterBase {
3762
3542
  this.apiVersion = options.apiVersion ?? "2023-06-01";
3763
3543
  }
3764
3544
  buildRequest(request) {
3765
- return this.withExtraBody(buildMessagesRequest(request));
3545
+ return this.withExtraBody(buildMessagesRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
3766
3546
  }
3767
3547
  async *runStream(providerRequest, factory, request) {
3768
3548
  yield* mapMessagesStream({
@@ -3770,7 +3550,8 @@ var MessagesAdapter = class extends HttpAdapterBase {
3770
3550
  baseUrl: this.baseUrl,
3771
3551
  apiKey: this.apiKey,
3772
3552
  mergeHeaders: this.mergeHeaders.bind(this),
3773
- apiVersion: this.apiVersion
3553
+ apiVersion: this.apiVersion,
3554
+ maxOpaquePayloadBytes: this.maxOpaquePayloadBytes
3774
3555
  }, providerRequest, factory, request);
3775
3556
  }
3776
3557
  };
@@ -3794,7 +3575,51 @@ const REASONING_FIELDS = ["reasoning_content", "reasoning"];
3794
3575
  /**
3795
3576
  * ChatCompletionsAdapter — request 映射
3796
3577
  */
3797
- 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
+ }
3798
3623
  function isChatReplayToolCall(value) {
3799
3624
  if (!value || typeof value !== "object") return false;
3800
3625
  const entry = value;
@@ -3809,7 +3634,8 @@ function isChatReplayMessage(value) {
3809
3634
  const msg = value;
3810
3635
  const role = msg.role;
3811
3636
  if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
3812
- 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;
3813
3639
  if (msg.tool_calls !== void 0) {
3814
3640
  if (!Array.isArray(msg.tool_calls) || !msg.tool_calls.every(isChatReplayToolCall)) return false;
3815
3641
  }
@@ -3821,20 +3647,64 @@ function assertChatReplayMessages(messages, field) {
3821
3647
  if (!Array.isArray(messages)) throw new AIRequestError(`Invalid opaque replay payload: ${field} must be an array`, "INVALID_OPAQUE_REPLAY");
3822
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");
3823
3649
  }
3824
- function buildChatCompletionsRequest(request) {
3825
- mapper$5.assertNoServerTools(request.serverTools);
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
+ }
3695
+ function buildChatCompletionsRequest(request, options) {
3696
+ mapper$4.assertNoServerTools(request.serverTools);
3826
3697
  const messages = [];
3827
3698
  if (request.instructions) messages.push({
3828
3699
  role: "system",
3829
- content: mapper$5.mapInstructions(request.instructions)
3700
+ content: mapper$4.mapInstructions(request.instructions)
3830
3701
  });
3831
3702
  for (const item of request.input) switch (item.type) {
3832
3703
  case "message": {
3833
3704
  const role = item.role;
3834
- const text = mapper$5.textFromBlocks(item.content, `input message (${item.role}) content`);
3835
3705
  messages.push({
3836
3706
  role,
3837
- content: text || null
3707
+ content: mapChatMessageContent(role, item.content, `input message (${item.role}) content`)
3838
3708
  });
3839
3709
  break;
3840
3710
  }
@@ -3861,21 +3731,21 @@ function buildChatCompletionsRequest(request) {
3861
3731
  role: "tool",
3862
3732
  tool_call_id: item.callId,
3863
3733
  name: item.toolName,
3864
- content: mapper$5.textFromBlocks(item.content, `tool_result ${item.callId} content`)
3734
+ content: mapper$4.textFromBlocks(item.content, `tool_result ${item.callId} content`)
3865
3735
  });
3866
3736
  break;
3867
3737
  case "reasoning":
3868
3738
  messages.push({
3869
3739
  role: "assistant",
3870
- content: mapper$5.textFromBlocks(item.content, "reasoning content")
3740
+ content: mapper$4.textFromBlocks(item.content, "reasoning content")
3871
3741
  });
3872
3742
  break;
3873
3743
  case "opaque": {
3874
- const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.CHAT_COMPLETIONS);
3744
+ const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.CHAT_COMPLETIONS, { maxBytes: options?.maxOpaquePayloadBytes });
3875
3745
  if (!payload) break;
3876
3746
  if ("messages" in payload) {
3877
3747
  assertChatReplayMessages(payload.messages, "messages");
3878
- mapper$5.rollbackTrailingAssistantMessages(messages);
3748
+ mapper$4.rollbackTrailingAssistantMessages(messages);
3879
3749
  for (const m of payload.messages) messages.push(m);
3880
3750
  }
3881
3751
  break;
@@ -3887,8 +3757,8 @@ function buildChatCompletionsRequest(request) {
3887
3757
  stream: true,
3888
3758
  n: 1
3889
3759
  };
3890
- body.tools = mapper$5.mapToolsIfPresent(request.tools, mapOpenAiFunctionTool);
3891
- 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, {
3892
3762
  auto: "auto",
3893
3763
  none: "none",
3894
3764
  tool: (name) => ({
@@ -3902,55 +3772,11 @@ function buildChatCompletionsRequest(request) {
3902
3772
  if (request.reasoningLevel !== void 0) body.reasoning_effort = mapChatCompletionsReasoningEffort(request.reasoningLevel);
3903
3773
  return body;
3904
3774
  }
3905
- new NormalizedRequestMapper("chat-completions");
3906
- function extractReasoningText(value) {
3907
- if (typeof value === "string") return value;
3908
- if (Array.isArray(value)) return value.map(extractReasoningText).join("");
3909
- if (value && typeof value === "object") {
3910
- const record = value;
3911
- for (const key of [
3912
- "text",
3913
- "content",
3914
- "reasoning",
3915
- "reasoning_content",
3916
- "thinking",
3917
- "value"
3918
- ]) {
3919
- const nested = extractReasoningText(record[key]);
3920
- if (nested) return nested;
3921
- }
3922
- }
3923
- return "";
3924
- }
3925
- function extractReasoningDeltas(delta) {
3926
- const deltas = [];
3927
- for (const field of REASONING_FIELDS) {
3928
- const text = extractReasoningText(delta[field]);
3929
- if (text) deltas.push({
3930
- field,
3931
- text
3932
- });
3933
- }
3934
- return deltas;
3935
- }
3936
- function buildAssistantReplayMessage(params) {
3937
- const { content, reasoningByField, toolCalls } = params;
3938
- if (!content && reasoningByField.size === 0 && toolCalls.length === 0) return null;
3939
- const replayMessage = {
3940
- role: "assistant",
3941
- content: content || null
3942
- };
3943
- for (const [field, text] of reasoningByField) replayMessage[field] = text;
3944
- if (toolCalls.length > 0) replayMessage.tool_calls = toolCalls.map((toolCall) => ({
3945
- id: toolCall.id,
3946
- type: "function",
3947
- function: {
3948
- name: toolCall.name,
3949
- arguments: toolCall.args
3950
- }
3951
- }));
3952
- return replayMessage;
3953
- }
3775
+ //#endregion
3776
+ //#region src/adapters/chat-completions/map-stream.ts
3777
+ /**
3778
+ * ChatCompletionsAdapter stream 映射
3779
+ */
3954
3780
  async function* mapChatCompletionsStream(host, providerRequest, factory, request) {
3955
3781
  const session = host.beginJsonStream(factory, request);
3956
3782
  const { auxiliary, gate } = session;
@@ -4019,6 +3845,8 @@ async function* mapChatCompletionsStream(host, providerRequest, factory, request
4019
3845
  yield* finalizeStreamTurn(session, items, {
4020
3846
  stopReason,
4021
3847
  rawResponseId,
3848
+ factory,
3849
+ maxOpaquePayloadBytes: host.maxOpaquePayloadBytes,
4022
3850
  opaque: assistantReplayMessage ? opaqueItem(OPAQUE_SOURCE.CHAT_COMPLETIONS, "replay", {
4023
3851
  replaceCanonical: true,
4024
3852
  messages: [assistantReplayMessage]
@@ -4171,14 +3999,15 @@ var ChatCompletionsAdapter = class extends HttpAdapterBase {
4171
3999
  super(options, { baseUrl: "https://api.openai.com/v1" });
4172
4000
  }
4173
4001
  buildRequest(request) {
4174
- return this.withExtraBody(buildChatCompletionsRequest(request));
4002
+ return this.withExtraBody(buildChatCompletionsRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
4175
4003
  }
4176
4004
  async *runStream(providerRequest, factory, request) {
4177
4005
  yield* mapChatCompletionsStream({
4178
4006
  beginJsonStream: this.beginJsonStream.bind(this),
4179
4007
  baseUrl: this.baseUrl,
4180
4008
  apiKey: this.apiKey,
4181
- mergeHeaders: this.mergeHeaders.bind(this)
4009
+ mergeHeaders: this.mergeHeaders.bind(this),
4010
+ maxOpaquePayloadBytes: this.maxOpaquePayloadBytes
4182
4011
  }, providerRequest, factory, request);
4183
4012
  }
4184
4013
  };
@@ -4203,7 +4032,32 @@ function toWireOllamaToolCalls(toolCalls) {
4203
4032
  arguments: tc.function.arguments
4204
4033
  } }));
4205
4034
  }
4206
- function buildOllamaRequest(request) {
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
+ }
4060
+ function buildOllamaRequest(request, options) {
4207
4061
  mapper$3.assertNoServerTools(request.serverTools);
4208
4062
  const messages = [];
4209
4063
  /** Local-only name → call id queue for best-effort tool_result association (not sent to Ollama). */
@@ -4214,10 +4068,17 @@ function buildOllamaRequest(request) {
4214
4068
  });
4215
4069
  for (const item of request.input) switch (item.type) {
4216
4070
  case "message": {
4217
- 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
+ }
4218
4079
  messages.push({
4219
- role,
4220
- content: mapper$3.textFromBlocks(item.content, `input message (${item.role}) content`)
4080
+ role: item.role,
4081
+ content: mapper$3.textFromBlocks(item.content, field)
4221
4082
  });
4222
4083
  break;
4223
4084
  }
@@ -4254,7 +4115,7 @@ function buildOllamaRequest(request) {
4254
4115
  });
4255
4116
  break;
4256
4117
  case "opaque": {
4257
- const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.OLLAMA);
4118
+ const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.OLLAMA, { maxBytes: options?.maxOpaquePayloadBytes });
4258
4119
  if (!payload) break;
4259
4120
  if (payload.role === "assistant" && typeof payload.content === "string") {
4260
4121
  mapper$3.rollbackTrailingAssistantMessages(messages);
@@ -4339,6 +4200,8 @@ async function* mapOllamaStream(host, providerRequest, factory, request) {
4339
4200
  yield* finalizeStreamTurn(session, items, {
4340
4201
  stopReason,
4341
4202
  rawResponseId,
4203
+ factory,
4204
+ maxOpaquePayloadBytes: host.maxOpaquePayloadBytes,
4342
4205
  opaque: accumulatedContent || pendingToolCalls.length > 0 ? opaqueItem(OPAQUE_SOURCE.OLLAMA, "replay", {
4343
4206
  role: "assistant",
4344
4207
  content: accumulatedContent,
@@ -4417,14 +4280,15 @@ var OllamaAdapter = class extends HttpAdapterBase {
4417
4280
  super(options, { baseUrl: "http://localhost:11434" });
4418
4281
  }
4419
4282
  buildRequest(request) {
4420
- return this.withExtraBody(buildOllamaRequest(request));
4283
+ return this.withExtraBody(buildOllamaRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
4421
4284
  }
4422
4285
  async *runStream(providerRequest, factory, request) {
4423
4286
  yield* mapOllamaStream({
4424
4287
  beginJsonStream: this.beginJsonStream.bind(this),
4425
4288
  baseUrl: this.baseUrl,
4426
4289
  apiKey: this.apiKey,
4427
- mergeHeaders: this.mergeHeaders.bind(this)
4290
+ mergeHeaders: this.mergeHeaders.bind(this),
4291
+ maxOpaquePayloadBytes: this.maxOpaquePayloadBytes
4428
4292
  }, providerRequest, factory, request);
4429
4293
  }
4430
4294
  };
@@ -4458,6 +4322,14 @@ function appendPart(contents, role, part) {
4458
4322
  parts: [part]
4459
4323
  });
4460
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
+ }
4461
4333
  function textPartsFromBlocks(blocks, field) {
4462
4334
  return mapper$1.ensureTextBlocks(blocks, field).map((block) => {
4463
4335
  if (block.type === "text") return { text: block.text };
@@ -4465,6 +4337,20 @@ function textPartsFromBlocks(blocks, field) {
4465
4337
  throw new AIRequestError(`gemini does not support content block type "${block.type}" in ${field}`, "UNSUPPORTED_CONTENT_BLOCK");
4466
4338
  });
4467
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
+ }
4468
4354
  function clonePart$1(part) {
4469
4355
  return { ...part };
4470
4356
  }
@@ -4474,15 +4360,17 @@ function cloneContent(content) {
4474
4360
  parts: content.parts.map(clonePart$1)
4475
4361
  };
4476
4362
  }
4477
- function buildGeminiRequest(request) {
4363
+ function buildGeminiRequest(request, options) {
4478
4364
  mapper$1.assertNoServerTools(request.serverTools);
4479
4365
  const contents = [];
4480
4366
  let systemInstruction;
4481
4367
  if (request.instructions) systemInstruction = { parts: [{ text: mapper$1.mapInstructions(request.instructions) }] };
4482
4368
  for (const item of request.input) switch (item.type) {
4483
4369
  case "message": {
4370
+ const field = `input message (${item.role}) content`;
4484
4371
  const role = item.role === "assistant" ? "model" : "user";
4485
- 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);
4486
4374
  break;
4487
4375
  }
4488
4376
  case "tool_call":
@@ -4515,7 +4403,7 @@ function buildGeminiRequest(request) {
4515
4403
  });
4516
4404
  break;
4517
4405
  case "opaque": {
4518
- const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.GEMINI);
4406
+ const payload = acceptOpaqueReplay(item, OPAQUE_SOURCE.GEMINI, { maxBytes: options?.maxOpaquePayloadBytes });
4519
4407
  if (!payload) break;
4520
4408
  if (payload.replaceCanonical === true && "content" in payload) {
4521
4409
  assertGeminiReplayContent(payload.content, "content");
@@ -4628,6 +4516,8 @@ async function* mapGeminiStream(host, providerRequest, factory, request) {
4628
4516
  yield* finalizeStreamTurn(session, items, {
4629
4517
  stopReason: reason,
4630
4518
  rawResponseId,
4519
+ factory,
4520
+ maxOpaquePayloadBytes: host.maxOpaquePayloadBytes,
4631
4521
  opaque: replayParts.length > 0 ? opaqueItem(OPAQUE_SOURCE.GEMINI, "replay", {
4632
4522
  replaceCanonical: true,
4633
4523
  content: {
@@ -4713,14 +4603,15 @@ var GeminiAdapter = class extends HttpAdapterBase {
4713
4603
  super(options, { baseUrl: "https://generativelanguage.googleapis.com/v1beta" });
4714
4604
  }
4715
4605
  buildRequest(request) {
4716
- return this.withExtraBody(buildGeminiRequest(request));
4606
+ return this.withExtraBody(buildGeminiRequest(request, { maxOpaquePayloadBytes: this.maxOpaquePayloadBytes }));
4717
4607
  }
4718
4608
  async *runStream(providerRequest, factory, request) {
4719
4609
  yield* mapGeminiStream({
4720
4610
  beginJsonStream: this.beginJsonStream.bind(this),
4721
4611
  baseUrl: this.baseUrl,
4722
4612
  apiKey: this.apiKey,
4723
- mergeHeaders: this.mergeHeaders.bind(this)
4613
+ mergeHeaders: this.mergeHeaders.bind(this),
4614
+ maxOpaquePayloadBytes: this.maxOpaquePayloadBytes
4724
4615
  }, providerRequest, factory, request);
4725
4616
  }
4726
4617
  };