@codehz/ai 0.4.3 → 0.4.5
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/.github/workflows/publish.yml +56 -0
- package/README.md +32 -4
- package/dist/index.d.mts +133 -32
- package/dist/index.mjs +209 -52
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -1
- package/src/adapters/chat-completions.ts +24 -5
- package/src/adapters/messages.ts +24 -7
- package/src/adapters/mock.ts +7 -3
- package/src/adapters/ollama.ts +19 -2
- package/src/adapters/responses.ts +189 -55
- package/src/core/validation.ts +13 -0
- package/src/helpers/index.ts +14 -0
- package/src/helpers/provider-request-options.ts +25 -0
- package/src/helpers/reasoning-level.ts +85 -0
- package/src/types/index.ts +1 -1
- package/src/types/request.ts +11 -0
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
openProviderJsonStream,
|
|
28
28
|
iterateProviderStreamBatches,
|
|
29
29
|
createCompletionGate,
|
|
30
|
+
mergeProviderHeaders,
|
|
31
|
+
applyExtraBody,
|
|
32
|
+
mapResponsesReasoning,
|
|
30
33
|
} from "../helpers/index.js";
|
|
31
34
|
|
|
32
35
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
@@ -38,9 +41,17 @@ export type ResponsesAdapterOptions = {
|
|
|
38
41
|
baseUrl?: string;
|
|
39
42
|
/** 可注入自定义 fetch 实现(用于测试/代理) */
|
|
40
43
|
fetch?: FetchFn;
|
|
44
|
+
/** 额外请求头;后写覆盖内置 Authorization / Content-Type */
|
|
45
|
+
headers?: Record<string, string>;
|
|
46
|
+
/** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
|
|
47
|
+
extraBody?: Record<string, unknown>;
|
|
41
48
|
};
|
|
42
49
|
|
|
43
|
-
// ── Responses API
|
|
50
|
+
// ── Responses API 请求类型(对齐 OpenAI Responses schema)────
|
|
51
|
+
//
|
|
52
|
+
// input 是 untagged enum ModelInput = string | InputItem[]。
|
|
53
|
+
// 每个 InputItem 也必须命中官方 variant,否则会 422:
|
|
54
|
+
// "data did not match any variant of untagged enum ModelInput"
|
|
44
55
|
|
|
45
56
|
type ResponsesAPIRequest = {
|
|
46
57
|
model: string;
|
|
@@ -51,27 +62,72 @@ type ResponsesAPIRequest = {
|
|
|
51
62
|
metadata?: Record<string, string>;
|
|
52
63
|
temperature?: number;
|
|
53
64
|
max_output_tokens?: number;
|
|
65
|
+
/** Portable reasoningLevel → effort;summary 等特化字段不在此层 */
|
|
66
|
+
reasoning?: { effort: string };
|
|
67
|
+
/** 服务端多轮续写;opaque replay 的 response id 映射到此字段,而非 item_reference */
|
|
68
|
+
previous_response_id?: string;
|
|
54
69
|
stream: true;
|
|
55
70
|
};
|
|
56
71
|
|
|
72
|
+
/** EasyInputMessage:content 可为 string,或 input_* content parts */
|
|
73
|
+
type ResponsesEasyMessage = {
|
|
74
|
+
type: "message";
|
|
75
|
+
role: "user" | "assistant" | "system" | "developer";
|
|
76
|
+
content: string | ResponsesInputContentPart[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ResponsesInputContentPart =
|
|
80
|
+
| { type: "input_text"; text: string }
|
|
81
|
+
| { type: "input_image"; image_url: string; detail?: "auto" | "low" | "high" }
|
|
82
|
+
| { type: "input_file"; file_url?: string; file_id?: string; filename?: string };
|
|
83
|
+
|
|
84
|
+
/** function_call:call_id 必填;id 是可选的 item id */
|
|
85
|
+
type ResponsesFunctionCall = {
|
|
86
|
+
type: "function_call";
|
|
87
|
+
call_id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
arguments: string;
|
|
90
|
+
id?: string;
|
|
91
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type ResponsesFunctionCallOutput = {
|
|
95
|
+
type: "function_call_output";
|
|
96
|
+
call_id: string;
|
|
97
|
+
output: string;
|
|
98
|
+
id?: string;
|
|
99
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** reasoning:id + summary/content/encrypted_content,不是任意 content blocks */
|
|
103
|
+
type ResponsesReasoningInput = {
|
|
104
|
+
type: "reasoning";
|
|
105
|
+
id: string;
|
|
106
|
+
summary: Array<{ type: "summary_text"; text: string }>;
|
|
107
|
+
content?: Array<{ type: "reasoning_text"; text: string }>;
|
|
108
|
+
encrypted_content?: string | null;
|
|
109
|
+
status?: "in_progress" | "completed" | "incomplete";
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** 引用既有 item(不是 response id) */
|
|
113
|
+
type ResponsesItemReference = {
|
|
114
|
+
type: "item_reference";
|
|
115
|
+
id: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
57
118
|
type ResponsesInputItem =
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| { type: "item_reference"; id: string };
|
|
64
|
-
|
|
65
|
-
type ResponsesContentBlock =
|
|
66
|
-
| { type: "text"; text: string }
|
|
67
|
-
| { type: "reasoning"; text: string }
|
|
68
|
-
| { type: "refusal"; refusal: string };
|
|
119
|
+
| ResponsesEasyMessage
|
|
120
|
+
| ResponsesFunctionCall
|
|
121
|
+
| ResponsesFunctionCallOutput
|
|
122
|
+
| ResponsesReasoningInput
|
|
123
|
+
| ResponsesItemReference;
|
|
69
124
|
|
|
70
125
|
type ResponsesTool = {
|
|
71
126
|
type: "function";
|
|
72
127
|
name: string;
|
|
73
128
|
description?: string;
|
|
74
129
|
parameters: Record<string, unknown>;
|
|
130
|
+
strict?: boolean | null;
|
|
75
131
|
};
|
|
76
132
|
|
|
77
133
|
const mapper = new NormalizedRequestMapper("responses");
|
|
@@ -209,11 +265,12 @@ type ResponsesAPIOutputItem = {
|
|
|
209
265
|
id: string;
|
|
210
266
|
type: "message" | "reasoning" | "function_call" | string;
|
|
211
267
|
role?: string;
|
|
212
|
-
content?:
|
|
268
|
+
content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
213
269
|
summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
|
|
214
270
|
encrypted_content?: string | null;
|
|
215
271
|
name?: string;
|
|
216
272
|
arguments?: string;
|
|
273
|
+
call_id?: string;
|
|
217
274
|
status?: string;
|
|
218
275
|
[key: string]: unknown;
|
|
219
276
|
};
|
|
@@ -232,15 +289,56 @@ function extractFailureMessage(response: ResponsesAPIResponse): string {
|
|
|
232
289
|
return response.error?.message ?? response.failure?.message ?? "unknown";
|
|
233
290
|
}
|
|
234
291
|
|
|
235
|
-
|
|
292
|
+
function readNonEmptyString(value: unknown, maxLen = 256): string | undefined {
|
|
293
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return undefined;
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
|
|
298
|
+
function messageContentAsString(
|
|
299
|
+
blocks: import("../index.js").ContentBlock[],
|
|
300
|
+
field: string,
|
|
301
|
+
): string {
|
|
302
|
+
return mapper.textFromBlocks(blocks, field);
|
|
303
|
+
}
|
|
236
304
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`responses does not support content block type "${b.type}" in canonical mapping`,
|
|
242
|
-
"UNSUPPORTED_CONTENT_BLOCK",
|
|
305
|
+
function mapReasoningInput(item: import("../index.js").ReasoningItem, index: number): ResponsesReasoningInput {
|
|
306
|
+
const text = mapper.textFromBlocks(
|
|
307
|
+
mapper.ensureReasoningBlocks(item.content, "reasoning content"),
|
|
308
|
+
"reasoning content",
|
|
243
309
|
);
|
|
310
|
+
const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
|
|
311
|
+
|
|
312
|
+
if (item.visibility === "full") {
|
|
313
|
+
return {
|
|
314
|
+
type: "reasoning",
|
|
315
|
+
id,
|
|
316
|
+
summary: [],
|
|
317
|
+
content: text ? [{ type: "reasoning_text", text }] : undefined,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// summary / redacted / opaque:公开可回传的是 summary_text
|
|
322
|
+
return {
|
|
323
|
+
type: "reasoning",
|
|
324
|
+
id,
|
|
325
|
+
summary: text ? [{ type: "summary_text", text }] : [],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function extractOpaqueContinuationId(payload: Record<string, unknown>): {
|
|
330
|
+
previousResponseId?: string;
|
|
331
|
+
itemReferenceId?: string;
|
|
332
|
+
} {
|
|
333
|
+
// 优先显式 previous_response_id;历史 payload 用 id 存 response 续写句柄
|
|
334
|
+
const previousResponseId =
|
|
335
|
+
readNonEmptyString(payload.previous_response_id) ??
|
|
336
|
+
(typeof payload.item_id === "string" ? undefined : readNonEmptyString(payload.id));
|
|
337
|
+
|
|
338
|
+
// 仅在显式给出 item_id 时使用 item_reference(引用的是 item,不是 response)
|
|
339
|
+
const itemReferenceId = readNonEmptyString(payload.item_id);
|
|
340
|
+
|
|
341
|
+
return { previousResponseId, itemReferenceId };
|
|
244
342
|
}
|
|
245
343
|
|
|
246
344
|
// ── Adapter ───────────────────────────────────────────────────
|
|
@@ -252,48 +350,46 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
252
350
|
private apiKey: string;
|
|
253
351
|
private baseUrl: string;
|
|
254
352
|
private fetchFn: FetchFn;
|
|
353
|
+
private headers: Record<string, string> | undefined;
|
|
354
|
+
private extraBody: Record<string, unknown> | undefined;
|
|
255
355
|
|
|
256
356
|
constructor(options: ResponsesAdapterOptions) {
|
|
257
357
|
super();
|
|
258
358
|
this.apiKey = options.apiKey;
|
|
259
359
|
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
260
360
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
361
|
+
this.headers = options.headers;
|
|
362
|
+
this.extraBody = options.extraBody;
|
|
261
363
|
}
|
|
262
364
|
|
|
263
365
|
// ── buildRequest ──────────────────────────────────────────
|
|
264
366
|
|
|
265
367
|
protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
|
|
266
368
|
const input: ResponsesInputItem[] = [];
|
|
369
|
+
let previousResponseId: string | undefined;
|
|
370
|
+
let reasoningIndex = 0;
|
|
267
371
|
|
|
268
372
|
for (const item of request.input) {
|
|
269
373
|
switch (item.type) {
|
|
270
374
|
case "message": {
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
input.push({
|
|
279
|
-
type: "message",
|
|
280
|
-
role: item.role,
|
|
281
|
-
content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
|
|
282
|
-
});
|
|
283
|
-
}
|
|
375
|
+
// EasyInputMessage:string content 对 user/assistant 都合法,且最不易触发 ModelInput 反序列化失败。
|
|
376
|
+
// 切勿发送 { type: "text" } —— 官方 content part 是 input_text / output_text。
|
|
377
|
+
input.push({
|
|
378
|
+
type: "message",
|
|
379
|
+
role: item.role,
|
|
380
|
+
content: messageContentAsString(item.content, `input message (${item.role}) content`),
|
|
381
|
+
});
|
|
284
382
|
break;
|
|
285
383
|
}
|
|
286
384
|
case "reasoning": {
|
|
287
|
-
|
|
288
|
-
.ensureReasoningBlocks(item.content, "reasoning content")
|
|
289
|
-
.map((b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }));
|
|
290
|
-
input.push({ type: "reasoning", content: blocks });
|
|
385
|
+
input.push(mapReasoningInput(item, reasoningIndex++));
|
|
291
386
|
break;
|
|
292
387
|
}
|
|
293
388
|
case "tool_call": {
|
|
389
|
+
// call_id 必填;canonical ToolCallItem.id 即 call_id(流里会优先取 call_id)
|
|
294
390
|
input.push({
|
|
295
391
|
type: "function_call",
|
|
296
|
-
|
|
392
|
+
call_id: item.id,
|
|
297
393
|
name: item.name,
|
|
298
394
|
arguments: item.argumentsText,
|
|
299
395
|
});
|
|
@@ -309,20 +405,28 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
309
405
|
break;
|
|
310
406
|
}
|
|
311
407
|
case "opaque": {
|
|
312
|
-
// Canonical replay
|
|
313
|
-
//
|
|
408
|
+
// Canonical replay 优先;否则用 previous_response_id 做服务端续写。
|
|
409
|
+
// 注意:response id 不能塞进 item_reference(那是 item id)。
|
|
314
410
|
if (item.source !== "responses" || item.purpose !== "replay") break;
|
|
315
411
|
assertOpaqueReplayEnvelope(item.payload);
|
|
316
412
|
const payload = item.payload as Record<string, unknown>;
|
|
317
|
-
|
|
318
|
-
|
|
413
|
+
|
|
414
|
+
// 显式字段校验:id / previous_response_id / item_id 若存在必须是合法 string
|
|
415
|
+
for (const key of ["id", "previous_response_id", "item_id"] as const) {
|
|
416
|
+
if (key in payload && (typeof payload[key] !== "string" || payload[key].length === 0 || payload[key].length > 256)) {
|
|
319
417
|
throw new AIRequestError(
|
|
320
|
-
|
|
418
|
+
`Invalid opaque replay payload: ${key} must be a non-empty string (max 256)`,
|
|
321
419
|
"INVALID_OPAQUE_REPLAY",
|
|
322
420
|
);
|
|
323
421
|
}
|
|
324
|
-
|
|
325
|
-
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const { previousResponseId: prevId, itemReferenceId } = extractOpaqueContinuationId(payload);
|
|
425
|
+
if (!hasReplayCanonicalInput(input)) {
|
|
426
|
+
if (prevId && !previousResponseId) {
|
|
427
|
+
previousResponseId = prevId;
|
|
428
|
+
} else if (itemReferenceId) {
|
|
429
|
+
input.push({ type: "item_reference", id: itemReferenceId });
|
|
326
430
|
}
|
|
327
431
|
}
|
|
328
432
|
break;
|
|
@@ -336,6 +440,10 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
336
440
|
stream: true,
|
|
337
441
|
};
|
|
338
442
|
|
|
443
|
+
if (previousResponseId) {
|
|
444
|
+
body.previous_response_id = previousResponseId;
|
|
445
|
+
}
|
|
446
|
+
|
|
339
447
|
if (request.instructions) {
|
|
340
448
|
body.instructions = mapper.mapInstructions(request.instructions);
|
|
341
449
|
}
|
|
@@ -362,8 +470,11 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
362
470
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
363
471
|
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
364
472
|
if (request.metadata) body.metadata = request.metadata;
|
|
473
|
+
if (request.reasoningLevel !== undefined) {
|
|
474
|
+
body.reasoning = mapResponsesReasoning(request.reasoningLevel);
|
|
475
|
+
}
|
|
365
476
|
|
|
366
|
-
return body;
|
|
477
|
+
return applyExtraBody(body, this.extraBody);
|
|
367
478
|
}
|
|
368
479
|
|
|
369
480
|
// ── runStream ─────────────────────────────────────────────
|
|
@@ -379,10 +490,13 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
379
490
|
const { reader } = await openProviderJsonStream({
|
|
380
491
|
fetchFn: this.fetchFn,
|
|
381
492
|
url: `${this.baseUrl}/responses`,
|
|
382
|
-
headers:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
493
|
+
headers: mergeProviderHeaders(
|
|
494
|
+
{
|
|
495
|
+
"Content-Type": "application/json",
|
|
496
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
497
|
+
},
|
|
498
|
+
this.headers,
|
|
499
|
+
),
|
|
386
500
|
body: providerRequest,
|
|
387
501
|
signal: request.signal,
|
|
388
502
|
});
|
|
@@ -392,9 +506,14 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
392
506
|
let completedResponse: ResponsesAPIResponse | undefined;
|
|
393
507
|
let unknownEventsWarned = false;
|
|
394
508
|
const messageItemsWithDelta = new Set<string>();
|
|
509
|
+
/** item_id → function name */
|
|
395
510
|
const toolCallNames = new Map<string, string>();
|
|
511
|
+
/** item_id → call_id(canonical ToolCallItem.id / function_call_output.call_id) */
|
|
512
|
+
const toolCallIds = new Map<string, string>();
|
|
396
513
|
const reasoningStates = new Map<string, ReasoningStreamState>();
|
|
397
514
|
|
|
515
|
+
const resolveToolCallId = (itemId: string): string => toolCallIds.get(itemId) ?? itemId;
|
|
516
|
+
|
|
398
517
|
const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
|
|
399
518
|
let state = reasoningStates.get(itemId);
|
|
400
519
|
if (!state) {
|
|
@@ -449,8 +568,12 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
449
568
|
}
|
|
450
569
|
case "function_call": {
|
|
451
570
|
const name = typeof item.name === "string" ? item.name : "unknown";
|
|
571
|
+
// Responses 用 call_id 关联 function_call_output;item.id 是 fc_* item id
|
|
572
|
+
const callId =
|
|
573
|
+
typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : item.id;
|
|
452
574
|
toolCallNames.set(item.id, name);
|
|
453
|
-
|
|
575
|
+
toolCallIds.set(item.id, callId);
|
|
576
|
+
yield factory.toolCallStarted(callId, name);
|
|
454
577
|
break;
|
|
455
578
|
}
|
|
456
579
|
}
|
|
@@ -569,14 +692,19 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
569
692
|
|
|
570
693
|
if (sseEvent.type === "response.function_call_arguments.delta") {
|
|
571
694
|
const data = sseEvent.data as { item_id: string; delta: string };
|
|
572
|
-
if (data.delta)
|
|
695
|
+
if (data.delta) {
|
|
696
|
+
yield factory.toolCallDelta(resolveToolCallId(data.item_id), { argumentsText: data.delta });
|
|
697
|
+
}
|
|
573
698
|
continue;
|
|
574
699
|
}
|
|
575
700
|
|
|
576
701
|
if (sseEvent.type === "response.function_call_arguments.done") {
|
|
577
702
|
const data = sseEvent.data as { item_id: string; arguments: string };
|
|
578
|
-
const
|
|
579
|
-
|
|
703
|
+
const callId = resolveToolCallId(data.item_id);
|
|
704
|
+
// 若 added 事件缺失,done 时仍尽量从 completed payload 之外兜底 call_id
|
|
705
|
+
if (!toolCallIds.has(data.item_id)) toolCallIds.set(data.item_id, callId);
|
|
706
|
+
const tcItem = toolCallItem(callId, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
|
|
707
|
+
yield factory.toolCallCompleted(callId);
|
|
580
708
|
output.push(tcItem);
|
|
581
709
|
continue;
|
|
582
710
|
}
|
|
@@ -640,7 +768,13 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
640
768
|
|
|
641
769
|
const replay = [...replayFromOutput(output)];
|
|
642
770
|
if (completedResponse?.id) {
|
|
643
|
-
|
|
771
|
+
// 同时保留 id(向后兼容)与 previous_response_id(语义明确)
|
|
772
|
+
replay.push(
|
|
773
|
+
opaqueItem("responses", "replay", {
|
|
774
|
+
id: completedResponse.id,
|
|
775
|
+
previous_response_id: completedResponse.id,
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
644
778
|
}
|
|
645
779
|
|
|
646
780
|
const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;
|
package/src/core/validation.ts
CHANGED
|
@@ -18,6 +18,7 @@ const MESSAGE_ROLES = new Set(["user", "assistant"]);
|
|
|
18
18
|
const REASONING_VISIBILITIES = new Set(["full", "summary", "redacted", "opaque"]);
|
|
19
19
|
const TOOL_RESULT_OUTCOMES = new Set(["success", "error", "rejected"]);
|
|
20
20
|
const INCLUDE_MODES = new Set(["off", "best_effort"]);
|
|
21
|
+
const REASONING_LEVELS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
|
|
21
22
|
|
|
22
23
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23
24
|
return typeof value === "object" && value !== null;
|
|
@@ -333,6 +334,18 @@ export function validateRequest(request: AIRequest): ValidationIssue[] {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
// reasoningLevel 枚举
|
|
338
|
+
if (request.reasoningLevel !== undefined) {
|
|
339
|
+
if (typeof request.reasoningLevel !== "string" || !REASONING_LEVELS.has(request.reasoningLevel)) {
|
|
340
|
+
pushIssue(
|
|
341
|
+
issues,
|
|
342
|
+
"reasoningLevel",
|
|
343
|
+
"REASONING_LEVEL_INVALID",
|
|
344
|
+
'reasoningLevel must be one of: none, minimal, low, medium, high, xhigh',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
336
349
|
if (request.include !== undefined) {
|
|
337
350
|
validateInclude(request.include, issues);
|
|
338
351
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -70,4 +70,18 @@ export type {
|
|
|
70
70
|
ProviderStreamBatchOptions,
|
|
71
71
|
} from "./provider-stream.js";
|
|
72
72
|
|
|
73
|
+
export { mergeProviderHeaders, applyExtraBody } from "./provider-request-options.js";
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
REASONING_LEVELS,
|
|
77
|
+
REASONING_LEVEL_SET,
|
|
78
|
+
assertSupportedReasoningLevel,
|
|
79
|
+
mapResponsesReasoning,
|
|
80
|
+
mapChatCompletionsReasoningEffort,
|
|
81
|
+
mapMessagesThinkingBudget,
|
|
82
|
+
mapMessagesThinking,
|
|
83
|
+
mapOllamaThink,
|
|
84
|
+
} from "./reasoning-level.js";
|
|
85
|
+
export type { OpenAIReasoningEffort, MessagesThinkingConfig, OllamaThinkValue } from "./reasoning-level.js";
|
|
86
|
+
|
|
73
87
|
export { NormalizedRequestMapper } from "./request-mapper.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 请求 headers / body 扩展合并
|
|
3
|
+
*
|
|
4
|
+
* 供真实后端 adapter 构造选项 `headers` / `extraBody` 使用:
|
|
5
|
+
* - headers:内置鉴权头为基,自定义后写覆盖
|
|
6
|
+
* - extraBody:已构建 body 为基,额外字段浅层 spread,同名顶层键可覆盖
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 合并内置 headers 与自定义 headers;自定义后写覆盖同名键。 */
|
|
10
|
+
export function mergeProviderHeaders(
|
|
11
|
+
base: Record<string, string>,
|
|
12
|
+
custom?: Record<string, string>,
|
|
13
|
+
): Record<string, string> {
|
|
14
|
+
if (!custom) return base;
|
|
15
|
+
return { ...base, ...custom };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 将构造期 extraBody 浅层合并到已构建的 provider body。
|
|
20
|
+
* 无 extraBody 时原样返回;有则允许覆盖同名顶层键。
|
|
21
|
+
*/
|
|
22
|
+
export function applyExtraBody<T extends object>(body: T, extraBody?: Record<string, unknown>): T {
|
|
23
|
+
if (!extraBody) return body;
|
|
24
|
+
return { ...body, ...extraBody };
|
|
25
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable reasoningLevel → provider wire 字段映射
|
|
3
|
+
*
|
|
4
|
+
* 第一版只处理 level 枚举;budget/summary 等特化字段不在此层。
|
|
5
|
+
* 无法映射的 level 抛 AIRequestError(UNSUPPORTED_REASONING_LEVEL)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AIRequestError } from "../core/errors.js";
|
|
9
|
+
import type { ReasoningLevel } from "../types/request.js";
|
|
10
|
+
|
|
11
|
+
export const REASONING_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const satisfies readonly ReasoningLevel[];
|
|
12
|
+
|
|
13
|
+
export const REASONING_LEVEL_SET: ReadonlySet<string> = new Set(REASONING_LEVELS);
|
|
14
|
+
|
|
15
|
+
const MESSAGES_BUDGET_RATIOS: Record<Exclude<ReasoningLevel, "none">, number> = {
|
|
16
|
+
minimal: 0.02,
|
|
17
|
+
low: 0.1,
|
|
18
|
+
medium: 0.3,
|
|
19
|
+
high: 0.6,
|
|
20
|
+
xhigh: 0.9,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const OLLAMA_SUPPORTED = new Set<ReasoningLevel>(["none", "low", "medium", "high"]);
|
|
24
|
+
|
|
25
|
+
export type OpenAIReasoningEffort = ReasoningLevel;
|
|
26
|
+
|
|
27
|
+
export type MessagesThinkingConfig =
|
|
28
|
+
| { type: "disabled" }
|
|
29
|
+
| { type: "enabled"; budget_tokens: number };
|
|
30
|
+
|
|
31
|
+
export type OllamaThinkValue = false | "low" | "medium" | "high";
|
|
32
|
+
|
|
33
|
+
/** 若 level 不在 supported 集合内则抛 AIRequestError。 */
|
|
34
|
+
export function assertSupportedReasoningLevel(
|
|
35
|
+
level: ReasoningLevel,
|
|
36
|
+
supported: ReadonlySet<ReasoningLevel>,
|
|
37
|
+
adapterKind: string,
|
|
38
|
+
): void {
|
|
39
|
+
if (supported.has(level)) return;
|
|
40
|
+
throw new AIRequestError(
|
|
41
|
+
`reasoningLevel "${level}" is not supported by the ${adapterKind} adapter`,
|
|
42
|
+
"UNSUPPORTED_REASONING_LEVEL",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Responses API:`reasoning: { effort }` */
|
|
47
|
+
export function mapResponsesReasoning(level: ReasoningLevel): { effort: OpenAIReasoningEffort } {
|
|
48
|
+
return { effort: level };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Chat Completions:顶层 `reasoning_effort` */
|
|
52
|
+
export function mapChatCompletionsReasoningEffort(level: ReasoningLevel): OpenAIReasoningEffort {
|
|
53
|
+
return level;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Messages thinking budget。
|
|
58
|
+
* 基于 maxTokens 按比例推导,clamp 到 [1024, max(1024, maxTokens - 1)],
|
|
59
|
+
* 满足 Anthropic budget_tokens < max_tokens。
|
|
60
|
+
*/
|
|
61
|
+
export function mapMessagesThinkingBudget(level: Exclude<ReasoningLevel, "none">, maxTokens: number): number {
|
|
62
|
+
const ratio = MESSAGES_BUDGET_RATIOS[level];
|
|
63
|
+
const raw = Math.round(maxTokens * ratio);
|
|
64
|
+
const upper = Math.max(1024, maxTokens - 1);
|
|
65
|
+
return Math.min(Math.max(raw, 1024), upper);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Messages API:`thinking` 字段 */
|
|
69
|
+
export function mapMessagesThinking(level: ReasoningLevel, maxTokens: number): MessagesThinkingConfig {
|
|
70
|
+
if (level === "none") {
|
|
71
|
+
return { type: "disabled" };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
type: "enabled",
|
|
75
|
+
budget_tokens: mapMessagesThinkingBudget(level, maxTokens),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Ollama:`think` 字段;minimal/xhigh 不支持 */
|
|
80
|
+
export function mapOllamaThink(level: ReasoningLevel): OllamaThinkValue {
|
|
81
|
+
assertSupportedReasoningLevel(level, OLLAMA_SUPPORTED, "ollama");
|
|
82
|
+
if (level === "none") return false;
|
|
83
|
+
// narrow after assert: only low|medium|high remain
|
|
84
|
+
return level as Exclude<OllamaThinkValue, false>;
|
|
85
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type {
|
|
|
21
21
|
} from "./items.js";
|
|
22
22
|
|
|
23
23
|
// 请求模型
|
|
24
|
-
export type { AIRequest, ToolDefinition, ToolChoice, IncludeSettings } from "./request.js";
|
|
24
|
+
export type { AIRequest, ToolDefinition, ToolChoice, IncludeSettings, ReasoningLevel } from "./request.js";
|
|
25
25
|
|
|
26
26
|
// 响应模型
|
|
27
27
|
export type { AIResponse, StopReason, Usage, BillingInfo, AuxiliaryInfo, BackendTrace } from "./response.js";
|
package/src/types/request.ts
CHANGED
|
@@ -25,6 +25,11 @@ export type IncludeSettings = {
|
|
|
25
25
|
providerMetadata?: "off" | "best_effort";
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// ── reasoning level ───────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Portable reasoning / thinking effort. Mapped per-adapter to provider wire fields. */
|
|
31
|
+
export type ReasoningLevel = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
32
|
+
|
|
28
33
|
// ── 统一请求 ──────────────────────────────────────────────────
|
|
29
34
|
|
|
30
35
|
export type AIRequest = {
|
|
@@ -36,6 +41,12 @@ export type AIRequest = {
|
|
|
36
41
|
metadata?: Record<string, string>;
|
|
37
42
|
temperature?: number;
|
|
38
43
|
maxOutputTokens?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Portable reasoning effort. Adapters map this to provider-native fields
|
|
46
|
+
* (e.g. Responses `reasoning.effort`, Chat Completions `reasoning_effort`,
|
|
47
|
+
* Messages `thinking`, Ollama `think`). Unsupported levels throw.
|
|
48
|
+
*/
|
|
49
|
+
reasoningLevel?: ReasoningLevel;
|
|
39
50
|
/** AbortSignal 用于打断请求。abort 时 fetch 调用会被取消,流迭代器抛出 AbortError。 */
|
|
40
51
|
signal?: AbortSignal;
|
|
41
52
|
};
|