@codehz/ai 0.4.4 → 0.4.6
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 +88 -10
- package/dist/index.mjs +146 -15
- 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 +24 -5
- 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 +86 -0
- package/src/types/index.ts +1 -1
- package/src/types/request.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codehz/ai",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
"types": "./dist/index.d.mts"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/codehz/nano-ai.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
12
20
|
"scripts": {
|
|
13
21
|
"typecheck": "tsc --noEmit",
|
|
14
22
|
"lint": "oxlint",
|
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
openProviderJsonStream,
|
|
28
28
|
iterateProviderStreamBatches,
|
|
29
29
|
createCompletionGate,
|
|
30
|
+
mergeProviderHeaders,
|
|
31
|
+
applyExtraBody,
|
|
32
|
+
mapChatCompletionsReasoningEffort,
|
|
30
33
|
} from "../helpers/index.js";
|
|
31
34
|
|
|
32
35
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
|
|
@@ -37,6 +40,10 @@ export type ChatCompletionsAdapterOptions = {
|
|
|
37
40
|
apiKey: string;
|
|
38
41
|
baseUrl?: string;
|
|
39
42
|
fetch?: FetchFn;
|
|
43
|
+
/** 额外请求头;后写覆盖内置 Authorization / Content-Type */
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
/** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
|
|
46
|
+
extraBody?: Record<string, unknown>;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
// ── Chat API 请求类型 ─────────────────────────────────────────
|
|
@@ -49,6 +56,8 @@ type ChatRequest = {
|
|
|
49
56
|
metadata?: Record<string, string>;
|
|
50
57
|
temperature?: number;
|
|
51
58
|
max_tokens?: number;
|
|
59
|
+
/** Portable reasoningLevel → reasoning_effort */
|
|
60
|
+
reasoning_effort?: string;
|
|
52
61
|
stream: true;
|
|
53
62
|
n: 1;
|
|
54
63
|
};
|
|
@@ -242,12 +251,16 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
242
251
|
private apiKey: string;
|
|
243
252
|
private baseUrl: string;
|
|
244
253
|
private fetchFn: FetchFn;
|
|
254
|
+
private headers: Record<string, string> | undefined;
|
|
255
|
+
private extraBody: Record<string, unknown> | undefined;
|
|
245
256
|
|
|
246
257
|
constructor(options: ChatCompletionsAdapterOptions) {
|
|
247
258
|
super();
|
|
248
259
|
this.apiKey = options.apiKey;
|
|
249
260
|
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
250
261
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
262
|
+
this.headers = options.headers;
|
|
263
|
+
this.extraBody = options.extraBody;
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
// ── buildRequest ──────────────────────────────────────────
|
|
@@ -356,8 +369,11 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
356
369
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
357
370
|
if (request.maxOutputTokens !== undefined) body.max_tokens = request.maxOutputTokens;
|
|
358
371
|
if (request.metadata) body.metadata = request.metadata;
|
|
372
|
+
if (request.reasoningLevel !== undefined) {
|
|
373
|
+
body.reasoning_effort = mapChatCompletionsReasoningEffort(request.reasoningLevel);
|
|
374
|
+
}
|
|
359
375
|
|
|
360
|
-
return body;
|
|
376
|
+
return applyExtraBody(body, this.extraBody);
|
|
361
377
|
}
|
|
362
378
|
|
|
363
379
|
// ── runStream ─────────────────────────────────────────────
|
|
@@ -373,10 +389,13 @@ export class ChatCompletionsAdapter extends AdapterBase {
|
|
|
373
389
|
const { reader } = await openProviderJsonStream({
|
|
374
390
|
fetchFn: this.fetchFn,
|
|
375
391
|
url: `${this.baseUrl}/chat/completions`,
|
|
376
|
-
headers:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
headers: mergeProviderHeaders(
|
|
393
|
+
{
|
|
394
|
+
"Content-Type": "application/json",
|
|
395
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
396
|
+
},
|
|
397
|
+
this.headers,
|
|
398
|
+
),
|
|
380
399
|
body: providerRequest,
|
|
381
400
|
signal: request.signal,
|
|
382
401
|
});
|
package/src/adapters/messages.ts
CHANGED
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
openProviderJsonStream,
|
|
31
31
|
iterateProviderStreamBatches,
|
|
32
32
|
createCompletionGate,
|
|
33
|
+
mergeProviderHeaders,
|
|
34
|
+
applyExtraBody,
|
|
35
|
+
mapMessagesThinking,
|
|
33
36
|
} from "../helpers/index.js";
|
|
34
37
|
|
|
35
38
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
|
|
@@ -42,6 +45,10 @@ export type MessagesAdapterOptions = {
|
|
|
42
45
|
baseUrl?: string;
|
|
43
46
|
/** 可注入自定义 fetch 实现(用于测试/代理) */
|
|
44
47
|
fetch?: FetchFn;
|
|
48
|
+
/** 额外请求头;后写覆盖内置 x-api-key / Content-Type / anthropic-version */
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
/** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
|
|
51
|
+
extraBody?: Record<string, unknown>;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
// ── Messages API 请求类型 ────────────────────────────────────
|
|
@@ -54,7 +61,7 @@ type MessagesAPIRequest = {
|
|
|
54
61
|
tools?: MessagesAPITool[];
|
|
55
62
|
tool_choice?: { type: "auto" | "none" } | { type: "tool"; name: string };
|
|
56
63
|
temperature?: number;
|
|
57
|
-
thinking?: { type: "enabled"; budget_tokens: number };
|
|
64
|
+
thinking?: { type: "enabled"; budget_tokens: number } | { type: "disabled" };
|
|
58
65
|
stream: true;
|
|
59
66
|
};
|
|
60
67
|
|
|
@@ -243,6 +250,8 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
243
250
|
private apiVersion: string;
|
|
244
251
|
private baseUrl: string;
|
|
245
252
|
private fetchFn: FetchFn;
|
|
253
|
+
private headers: Record<string, string> | undefined;
|
|
254
|
+
private extraBody: Record<string, unknown> | undefined;
|
|
246
255
|
|
|
247
256
|
constructor(options: MessagesAdapterOptions) {
|
|
248
257
|
super();
|
|
@@ -250,6 +259,8 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
250
259
|
this.apiVersion = options.apiVersion ?? "2023-06-01";
|
|
251
260
|
this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
|
|
252
261
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
262
|
+
this.headers = options.headers;
|
|
263
|
+
this.extraBody = options.extraBody;
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
// ── buildRequest ──────────────────────────────────────────
|
|
@@ -369,8 +380,11 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
369
380
|
});
|
|
370
381
|
|
|
371
382
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
383
|
+
if (request.reasoningLevel !== undefined) {
|
|
384
|
+
body.thinking = mapMessagesThinking(request.reasoningLevel, body.max_tokens);
|
|
385
|
+
}
|
|
372
386
|
|
|
373
|
-
return body;
|
|
387
|
+
return applyExtraBody(body, this.extraBody);
|
|
374
388
|
}
|
|
375
389
|
|
|
376
390
|
// ── runStream ─────────────────────────────────────────────
|
|
@@ -393,11 +407,14 @@ export class MessagesAdapter extends AdapterBase {
|
|
|
393
407
|
const { reader, headers } = await openProviderJsonStream({
|
|
394
408
|
fetchFn: this.fetchFn,
|
|
395
409
|
url: `${this.baseUrl}/messages`,
|
|
396
|
-
headers:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
410
|
+
headers: mergeProviderHeaders(
|
|
411
|
+
{
|
|
412
|
+
"Content-Type": "application/json",
|
|
413
|
+
"x-api-key": this.apiKey,
|
|
414
|
+
"anthropic-version": this.apiVersion,
|
|
415
|
+
},
|
|
416
|
+
this.headers,
|
|
417
|
+
),
|
|
401
418
|
body: providerRequest,
|
|
402
419
|
signal: request.signal,
|
|
403
420
|
});
|
package/src/adapters/mock.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
MessageItem,
|
|
25
25
|
NormalizedRequest,
|
|
26
26
|
OutputItem,
|
|
27
|
+
ReasoningLevel,
|
|
27
28
|
ReplayItem,
|
|
28
29
|
StopReason,
|
|
29
30
|
ToolCallItem,
|
|
@@ -70,6 +71,8 @@ export type MockHandlerContext = {
|
|
|
70
71
|
history: readonly MockHistoryRecord[];
|
|
71
72
|
/** 请求的 AbortSignal,handler 可检查 signal.aborted 提前退出。 */
|
|
72
73
|
signal?: AbortSignal;
|
|
74
|
+
/** 当前请求的 portable reasoningLevel(若设置)。 */
|
|
75
|
+
reasoningLevel?: ReasoningLevel;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
export type MockWarningStep = {
|
|
@@ -284,7 +287,7 @@ export class MockAdapter extends AdapterBase {
|
|
|
284
287
|
|
|
285
288
|
protected async buildRequest(request: NormalizedRequest): Promise<MockProviderRequest> {
|
|
286
289
|
const turnIndex = this.cursor;
|
|
287
|
-
const context = this.buildHandlerContext(turnIndex, request
|
|
290
|
+
const context = this.buildHandlerContext(turnIndex, request);
|
|
288
291
|
const remainingPendingToolCalls = consumePendingToolCalls(this.pendingToolCalls, request.input);
|
|
289
292
|
const handlerResult = this.handler(request, context);
|
|
290
293
|
|
|
@@ -476,7 +479,7 @@ export class MockAdapter extends AdapterBase {
|
|
|
476
479
|
);
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
private buildHandlerContext(turnIndex: number,
|
|
482
|
+
private buildHandlerContext(turnIndex: number, request: NormalizedRequest): MockHandlerContext {
|
|
480
483
|
return {
|
|
481
484
|
turnIndex,
|
|
482
485
|
previousReplay: this.previousReplay.map(cloneItem),
|
|
@@ -486,7 +489,8 @@ export class MockAdapter extends AdapterBase {
|
|
|
486
489
|
replay: record.replay.map(cloneItem),
|
|
487
490
|
toolCalls: record.toolCalls.map(cloneItem),
|
|
488
491
|
})),
|
|
489
|
-
signal,
|
|
492
|
+
signal: request.signal,
|
|
493
|
+
reasoningLevel: request.reasoningLevel,
|
|
490
494
|
};
|
|
491
495
|
}
|
|
492
496
|
}
|
package/src/adapters/ollama.ts
CHANGED
|
@@ -34,6 +34,9 @@ import {
|
|
|
34
34
|
openProviderJsonStream,
|
|
35
35
|
iterateProviderStreamBatches,
|
|
36
36
|
createCompletionGate,
|
|
37
|
+
mergeProviderHeaders,
|
|
38
|
+
applyExtraBody,
|
|
39
|
+
mapOllamaThink,
|
|
37
40
|
} from "../helpers/index.js";
|
|
38
41
|
|
|
39
42
|
import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn, StopReason } from "../index.js";
|
|
@@ -47,6 +50,10 @@ export type OllamaAdapterOptions = {
|
|
|
47
50
|
apiKey?: string;
|
|
48
51
|
/** 可注入自定义 fetch 实现 */
|
|
49
52
|
fetch?: FetchFn;
|
|
53
|
+
/** 额外请求头;后写覆盖内置 Content-Type / Authorization */
|
|
54
|
+
headers?: Record<string, string>;
|
|
55
|
+
/** 额外 body 顶层字段;浅层合并,同名键可覆盖 */
|
|
56
|
+
extraBody?: Record<string, unknown>;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
// ── Ollama Chat API 类型 ──────────────────────────────────────
|
|
@@ -56,6 +63,8 @@ type OllamaChatRequest = {
|
|
|
56
63
|
messages: OllamaMessage[];
|
|
57
64
|
stream: true;
|
|
58
65
|
tools?: OllamaTool[];
|
|
66
|
+
/** Portable reasoningLevel → think;minimal/xhigh/max 不支持 */
|
|
67
|
+
think?: boolean | "low" | "medium" | "high";
|
|
59
68
|
options?: {
|
|
60
69
|
temperature?: number;
|
|
61
70
|
num_predict?: number;
|
|
@@ -151,12 +160,16 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
151
160
|
private baseUrl: string;
|
|
152
161
|
private apiKey: string | undefined;
|
|
153
162
|
private fetchFn: FetchFn;
|
|
163
|
+
private headers: Record<string, string> | undefined;
|
|
164
|
+
private extraBody: Record<string, unknown> | undefined;
|
|
154
165
|
|
|
155
166
|
constructor(options: OllamaAdapterOptions = {}) {
|
|
156
167
|
super();
|
|
157
168
|
this.baseUrl = options.baseUrl ?? "http://localhost:11434";
|
|
158
169
|
this.apiKey = options.apiKey;
|
|
159
170
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
171
|
+
this.headers = options.headers;
|
|
172
|
+
this.extraBody = options.extraBody;
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
// ── buildRequest ──────────────────────────────────────────
|
|
@@ -291,7 +304,11 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
291
304
|
if (request.maxOutputTokens !== undefined) body.options.num_predict = request.maxOutputTokens;
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
|
|
307
|
+
if (request.reasoningLevel !== undefined) {
|
|
308
|
+
body.think = mapOllamaThink(request.reasoningLevel);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return applyExtraBody(body, this.extraBody);
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
// ── runStream ─────────────────────────────────────────────
|
|
@@ -326,7 +343,7 @@ export class OllamaAdapter extends AdapterBase {
|
|
|
326
343
|
const { reader } = await openProviderJsonStream({
|
|
327
344
|
fetchFn: this.fetchFn,
|
|
328
345
|
url: `${this.baseUrl}/api/chat`,
|
|
329
|
-
headers,
|
|
346
|
+
headers: mergeProviderHeaders(headers, this.headers),
|
|
330
347
|
body: providerRequest,
|
|
331
348
|
signal: request.signal,
|
|
332
349
|
});
|
|
@@ -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,6 +41,10 @@ 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
50
|
// ── Responses API 请求类型(对齐 OpenAI Responses schema)────
|
|
@@ -55,6 +62,8 @@ type ResponsesAPIRequest = {
|
|
|
55
62
|
metadata?: Record<string, string>;
|
|
56
63
|
temperature?: number;
|
|
57
64
|
max_output_tokens?: number;
|
|
65
|
+
/** Portable reasoningLevel → effort;summary 等特化字段不在此层 */
|
|
66
|
+
reasoning?: { effort: string };
|
|
58
67
|
/** 服务端多轮续写;opaque replay 的 response id 映射到此字段,而非 item_reference */
|
|
59
68
|
previous_response_id?: string;
|
|
60
69
|
stream: true;
|
|
@@ -341,12 +350,16 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
341
350
|
private apiKey: string;
|
|
342
351
|
private baseUrl: string;
|
|
343
352
|
private fetchFn: FetchFn;
|
|
353
|
+
private headers: Record<string, string> | undefined;
|
|
354
|
+
private extraBody: Record<string, unknown> | undefined;
|
|
344
355
|
|
|
345
356
|
constructor(options: ResponsesAdapterOptions) {
|
|
346
357
|
super();
|
|
347
358
|
this.apiKey = options.apiKey;
|
|
348
359
|
this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
|
|
349
360
|
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
361
|
+
this.headers = options.headers;
|
|
362
|
+
this.extraBody = options.extraBody;
|
|
350
363
|
}
|
|
351
364
|
|
|
352
365
|
// ── buildRequest ──────────────────────────────────────────
|
|
@@ -457,8 +470,11 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
457
470
|
if (request.temperature !== undefined) body.temperature = request.temperature;
|
|
458
471
|
if (request.maxOutputTokens !== undefined) body.max_output_tokens = request.maxOutputTokens;
|
|
459
472
|
if (request.metadata) body.metadata = request.metadata;
|
|
473
|
+
if (request.reasoningLevel !== undefined) {
|
|
474
|
+
body.reasoning = mapResponsesReasoning(request.reasoningLevel);
|
|
475
|
+
}
|
|
460
476
|
|
|
461
|
-
return body;
|
|
477
|
+
return applyExtraBody(body, this.extraBody);
|
|
462
478
|
}
|
|
463
479
|
|
|
464
480
|
// ── runStream ─────────────────────────────────────────────
|
|
@@ -474,10 +490,13 @@ export class ResponsesAdapter extends AdapterBase {
|
|
|
474
490
|
const { reader } = await openProviderJsonStream({
|
|
475
491
|
fetchFn: this.fetchFn,
|
|
476
492
|
url: `${this.baseUrl}/responses`,
|
|
477
|
-
headers:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
493
|
+
headers: mergeProviderHeaders(
|
|
494
|
+
{
|
|
495
|
+
"Content-Type": "application/json",
|
|
496
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
497
|
+
},
|
|
498
|
+
this.headers,
|
|
499
|
+
),
|
|
481
500
|
body: providerRequest,
|
|
482
501
|
signal: request.signal,
|
|
483
502
|
});
|
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", "max"]);
|
|
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, max",
|
|
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,86 @@
|
|
|
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", "max"] 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
|
+
max: 0.95,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const OLLAMA_SUPPORTED = new Set<ReasoningLevel>(["none", "low", "medium", "high"]);
|
|
25
|
+
|
|
26
|
+
export type OpenAIReasoningEffort = ReasoningLevel;
|
|
27
|
+
|
|
28
|
+
export type MessagesThinkingConfig =
|
|
29
|
+
| { type: "disabled" }
|
|
30
|
+
| { type: "enabled"; budget_tokens: number };
|
|
31
|
+
|
|
32
|
+
export type OllamaThinkValue = false | "low" | "medium" | "high";
|
|
33
|
+
|
|
34
|
+
/** 若 level 不在 supported 集合内则抛 AIRequestError。 */
|
|
35
|
+
export function assertSupportedReasoningLevel(
|
|
36
|
+
level: ReasoningLevel,
|
|
37
|
+
supported: ReadonlySet<ReasoningLevel>,
|
|
38
|
+
adapterKind: string,
|
|
39
|
+
): void {
|
|
40
|
+
if (supported.has(level)) return;
|
|
41
|
+
throw new AIRequestError(
|
|
42
|
+
`reasoningLevel "${level}" is not supported by the ${adapterKind} adapter`,
|
|
43
|
+
"UNSUPPORTED_REASONING_LEVEL",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Responses API:`reasoning: { effort }` */
|
|
48
|
+
export function mapResponsesReasoning(level: ReasoningLevel): { effort: OpenAIReasoningEffort } {
|
|
49
|
+
return { effort: level };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Chat Completions:顶层 `reasoning_effort` */
|
|
53
|
+
export function mapChatCompletionsReasoningEffort(level: ReasoningLevel): OpenAIReasoningEffort {
|
|
54
|
+
return level;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Messages thinking budget。
|
|
59
|
+
* 基于 maxTokens 按比例推导,clamp 到 [1024, max(1024, maxTokens - 1)],
|
|
60
|
+
* 满足 Anthropic budget_tokens < max_tokens。
|
|
61
|
+
*/
|
|
62
|
+
export function mapMessagesThinkingBudget(level: Exclude<ReasoningLevel, "none">, maxTokens: number): number {
|
|
63
|
+
const ratio = MESSAGES_BUDGET_RATIOS[level];
|
|
64
|
+
const raw = Math.round(maxTokens * ratio);
|
|
65
|
+
const upper = Math.max(1024, maxTokens - 1);
|
|
66
|
+
return Math.min(Math.max(raw, 1024), upper);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Messages API:`thinking` 字段 */
|
|
70
|
+
export function mapMessagesThinking(level: ReasoningLevel, maxTokens: number): MessagesThinkingConfig {
|
|
71
|
+
if (level === "none") {
|
|
72
|
+
return { type: "disabled" };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: "enabled",
|
|
76
|
+
budget_tokens: mapMessagesThinkingBudget(level, maxTokens),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Ollama:`think` 字段;minimal/xhigh/max 不支持 */
|
|
81
|
+
export function mapOllamaThink(level: ReasoningLevel): OllamaThinkValue {
|
|
82
|
+
assertSupportedReasoningLevel(level, OLLAMA_SUPPORTED, "ollama");
|
|
83
|
+
if (level === "none") return false;
|
|
84
|
+
// narrow after assert: only low|medium|high remain
|
|
85
|
+
return level as Exclude<OllamaThinkValue, false>;
|
|
86
|
+
}
|
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" | "max";
|
|
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
|
};
|