@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/README.md +9 -1
- package/dist/index.d.mts +35 -3
- package/dist/index.mjs +417 -526
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 / ≤
|
|
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$
|
|
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
|
-
/**
|
|
2415
|
-
|
|
2416
|
-
|
|
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$
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
|
|
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$
|
|
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
|
|
3450
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
3825
|
-
|
|
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$
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3891
|
-
body.tool_choice = mapper$
|
|
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
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
};
|