@codehz/ai 0.4.6 → 0.7.1

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.
Files changed (46) hide show
  1. package/README.md +221 -75
  2. package/dist/index.d.mts +670 -523
  3. package/dist/index.mjs +3677 -2207
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +19 -8
  6. package/.github/workflows/publish.yml +0 -56
  7. package/.oxfmtrc.json +0 -12
  8. package/.oxlintrc.json +0 -34
  9. package/AGENTS.md +0 -37
  10. package/src/adapters/chat-completions.ts +0 -624
  11. package/src/adapters/index.ts +0 -44
  12. package/src/adapters/messages.ts +0 -635
  13. package/src/adapters/mock.ts +0 -934
  14. package/src/adapters/ollama.ts +0 -526
  15. package/src/adapters/responses.ts +0 -818
  16. package/src/core/aggregator.ts +0 -428
  17. package/src/core/client.ts +0 -36
  18. package/src/core/collect-stream.ts +0 -19
  19. package/src/core/errors.ts +0 -105
  20. package/src/core/event-factory.ts +0 -151
  21. package/src/core/index.ts +0 -18
  22. package/src/core/merge-auxiliary.ts +0 -22
  23. package/src/core/normalize.ts +0 -65
  24. package/src/core/validation.ts +0 -404
  25. package/src/helpers/adapter-auxiliary.ts +0 -155
  26. package/src/helpers/adapter-base.ts +0 -218
  27. package/src/helpers/adapter-security.ts +0 -126
  28. package/src/helpers/auxiliary-collector.ts +0 -166
  29. package/src/helpers/incremental-stream-parser.ts +0 -142
  30. package/src/helpers/index.ts +0 -87
  31. package/src/helpers/mapping.ts +0 -192
  32. package/src/helpers/provider-request-options.ts +0 -25
  33. package/src/helpers/provider-stream.ts +0 -147
  34. package/src/helpers/reasoning-level.ts +0 -86
  35. package/src/helpers/request-mapper.ts +0 -94
  36. package/src/helpers/synthetic-stream.ts +0 -188
  37. package/src/helpers/usage-mapping.ts +0 -110
  38. package/src/index.ts +0 -17
  39. package/src/types/adapter.ts +0 -42
  40. package/src/types/content.ts +0 -15
  41. package/src/types/events.ts +0 -138
  42. package/src/types/index.ts +0 -49
  43. package/src/types/items.ts +0 -57
  44. package/src/types/request.ts +0 -52
  45. package/src/types/response.ts +0 -68
  46. package/tsdown.config.ts +0 -10
@@ -1,142 +0,0 @@
1
- export type StreamSplitResult = {
2
- items: string[];
3
- rest: string;
4
- };
5
-
6
- export type StreamParseResult<T> = { status: "parsed"; value: T } | { status: "ignored" } | { status: "malformed" };
7
-
8
- export class IncrementalStreamParser<T> {
9
- private buffer = "";
10
- private readonly decoder = new TextDecoder();
11
-
12
- constructor(
13
- private readonly split: (buffer: string, allowEOF: boolean) => StreamSplitResult,
14
- private readonly parse: (item: string) => StreamParseResult<T>,
15
- ) {}
16
-
17
- feed(value: Uint8Array): { items: T[]; malformed: number } {
18
- this.buffer += this.decoder.decode(value, { stream: true });
19
- return this.consume(false);
20
- }
21
-
22
- flush(): { items: T[]; malformed: number } {
23
- this.buffer += this.decoder.decode();
24
- return this.consume(true);
25
- }
26
-
27
- getRemaining(): string {
28
- return this.buffer;
29
- }
30
-
31
- private consume(allowEOF: boolean): { items: T[]; malformed: number } {
32
- const split = this.split(this.buffer, allowEOF);
33
- this.buffer = split.rest;
34
- const items: T[] = [];
35
- let malformed = 0;
36
-
37
- for (const rawItem of split.items) {
38
- const result = this.parse(rawItem);
39
- if (result.status === "parsed") items.push(result.value);
40
- else if (result.status === "malformed") malformed++;
41
- }
42
-
43
- return { items, malformed };
44
- }
45
- }
46
-
47
- export function splitLines(buffer: string, allowEOF: boolean): StreamSplitResult {
48
- const items: string[] = [];
49
- let cursor = 0;
50
-
51
- while (true) {
52
- const lineEnd = buffer.indexOf("\n", cursor);
53
- if (lineEnd === -1) break;
54
- items.push(buffer.slice(cursor, lineEnd));
55
- cursor = lineEnd + 1;
56
- }
57
-
58
- if (allowEOF && cursor < buffer.length) {
59
- items.push(buffer.slice(cursor));
60
- cursor = buffer.length;
61
- }
62
-
63
- return { items, rest: buffer.slice(cursor) };
64
- }
65
-
66
- export function splitSSEFrames(buffer: string, allowEOF: boolean): StreamSplitResult {
67
- const normalized = buffer.replaceAll("\r\n", "\n");
68
- const items: string[] = [];
69
- let cursor = 0;
70
-
71
- while (true) {
72
- const frameEnd = normalized.indexOf("\n\n", cursor);
73
- if (frameEnd === -1) break;
74
- items.push(normalized.slice(cursor, frameEnd));
75
- cursor = frameEnd + 2;
76
- }
77
-
78
- if (allowEOF && cursor < normalized.length) {
79
- items.push(normalized.slice(cursor));
80
- cursor = normalized.length;
81
- }
82
-
83
- return { items, rest: normalized.slice(cursor) };
84
- }
85
-
86
- // ── 常用 parse 工厂 ───────────────────────────────────────────
87
-
88
- export type SseJsonEvent = { type: string; data: unknown };
89
-
90
- /** 解析标准 SSE frame(event: + data:),用于 Messages / Responses。 */
91
- export function parseSseJsonFrame(frame: string): StreamParseResult<SseJsonEvent> {
92
- let eventType = "";
93
- let dataStr = "";
94
- for (const rawLine of frame.split("\n")) {
95
- const line = rawLine.trim();
96
- if (line.startsWith("event: ")) eventType = line.slice(7).trim();
97
- else if (line.startsWith("data: ")) dataStr += line.slice(6);
98
- }
99
- if (!eventType) return { status: "ignored" };
100
- try {
101
- const data: unknown = JSON.parse(dataStr);
102
- return { status: "parsed", value: { type: eventType, data } };
103
- } catch {
104
- return { status: "malformed" };
105
- }
106
- }
107
-
108
- export function createSseJsonParser<T extends SseJsonEvent = SseJsonEvent>(): IncrementalStreamParser<T> {
109
- return new IncrementalStreamParser(splitSSEFrames, (frame) => parseSseJsonFrame(frame) as StreamParseResult<T>);
110
- }
111
-
112
- /** OpenAI Chat Completions 简化 SSE:仅 `data: ...` 行,忽略 `[DONE]`。 */
113
- export function parseChatCompletionsDataLine(item: string): StreamParseResult<unknown> {
114
- const trimmed = item.trim();
115
- if (!trimmed.startsWith("data: ")) return { status: "ignored" };
116
- const data = trimmed.slice(6).trim();
117
- if (data === "[DONE]") return { status: "ignored" };
118
- try {
119
- return { status: "parsed", value: JSON.parse(data) as unknown };
120
- } catch {
121
- return { status: "malformed" };
122
- }
123
- }
124
-
125
- export function createChatCompletionsSseParser<T>(): IncrementalStreamParser<T> {
126
- return new IncrementalStreamParser(splitLines, (item) => parseChatCompletionsDataLine(item) as StreamParseResult<T>);
127
- }
128
-
129
- /** NDJSON 行解析(Ollama 等):空行忽略,JSON 失败为 malformed。 */
130
- export function createNdjsonLineParser<T>(isValid: (value: unknown) => value is T): IncrementalStreamParser<T> {
131
- return new IncrementalStreamParser<T>(splitLines, (item: string): StreamParseResult<T> => {
132
- const trimmed = item.trim();
133
- if (!trimmed) return { status: "ignored" };
134
- try {
135
- const parsed: unknown = JSON.parse(trimmed);
136
- if (isValid(parsed)) return { status: "parsed", value: parsed };
137
- return { status: "malformed" };
138
- } catch {
139
- return { status: "malformed" };
140
- }
141
- });
142
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * 共享工具模块
3
- *
4
- * 模块边界:adapter 间共享的映射 helper、adapter 基类、模拟流式、辅助信息采集等。
5
- */
6
-
7
- export {
8
- mapStopReason,
9
- mapReasoningVisibility,
10
- textBlock,
11
- jsonBlock,
12
- imageBlock,
13
- opaqueBlock,
14
- blockToText,
15
- contentBlocksToText,
16
- extractText,
17
- messageItem,
18
- reasoningItem,
19
- toolCallItem,
20
- toolResultItem,
21
- opaqueItem,
22
- replayFromOutput,
23
- } from "./mapping.js";
24
-
25
- export { AdapterBase } from "./adapter-base.js";
26
- export type { StreamResult } from "./adapter-base.js";
27
- export { AdapterAuxiliaryState, emitMalformedStreamWarning } from "./adapter-auxiliary.js";
28
- export type { AuxiliaryFinalizeOptions, AuxiliaryFinalizeResult, BillingPostprocessHook } from "./adapter-auxiliary.js";
29
- export { syntheticStream } from "./synthetic-stream.js";
30
- export type { SyntheticStreamOptions } from "./synthetic-stream.js";
31
- export { AuxiliaryCollector } from "./auxiliary-collector.js";
32
- export type { UsageSource, BillingSource, LookupResult } from "./auxiliary-collector.js";
33
- export {
34
- usageFromAnthropicMessages,
35
- usageFromChatCompletions,
36
- usageFromOllama,
37
- usageFromOpenAIResponses,
38
- } from "./usage-mapping.js";
39
-
40
- export {
41
- assertOpaqueReplayEnvelope,
42
- extractProviderErrorMessage,
43
- measureJsonDepth,
44
- providerHttpError,
45
- validateOpaqueReplayEnvelope,
46
- MAX_OPAQUE_JSON_DEPTH,
47
- MAX_OPAQUE_PAYLOAD_BYTES,
48
- PROVIDER_ERROR_MESSAGE_MAX_LEN,
49
- PROVIDER_ERROR_RAW_BODY_THRESHOLD,
50
- } from "./adapter-security.js";
51
- export type { OpaqueEnvelopeResult } from "./adapter-security.js";
52
-
53
- export {
54
- IncrementalStreamParser,
55
- splitLines,
56
- splitSSEFrames,
57
- parseSseJsonFrame,
58
- createSseJsonParser,
59
- parseChatCompletionsDataLine,
60
- createChatCompletionsSseParser,
61
- createNdjsonLineParser,
62
- } from "./incremental-stream-parser.js";
63
- export type { StreamSplitResult, StreamParseResult, SseJsonEvent } from "./incremental-stream-parser.js";
64
-
65
- export { openProviderJsonStream, iterateProviderStreamBatches, createCompletionGate } from "./provider-stream.js";
66
- export type {
67
- OpenProviderJsonStreamOptions,
68
- OpenedProviderStream,
69
- ProviderStreamBatch,
70
- ProviderStreamBatchOptions,
71
- } from "./provider-stream.js";
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
-
87
- export { NormalizedRequestMapper } from "./request-mapper.js";
@@ -1,192 +0,0 @@
1
- /**
2
- * Adapter 共享映射 helper
3
- *
4
- * 提供 adapter 间通用的类型映射函数:
5
- * - stop reason 映射
6
- * - content block 映射
7
- * - item 映射
8
- * - warning 记录
9
- * - replay 构造工具
10
- */
11
-
12
- import type {
13
- StopReason,
14
- ContentBlock,
15
- MessageItem,
16
- ReasoningItem,
17
- ToolCallItem,
18
- ToolResultItem,
19
- OpaqueItem,
20
- InputItem,
21
- OutputItem,
22
- ReplayItem,
23
- } from "../types/index.js";
24
-
25
- // ── Stop reason 映射 ──────────────────────────────────────────
26
-
27
- /**
28
- * 常见 provider stop_reason / finish_reason 到 canonical StopReason 的映射表。
29
- * adapter 可先查此表,未覆盖时走 fallback 规则。
30
- */
31
- const STOP_REASON_MAP: Record<string, StopReason> = {
32
- // OpenAI / Azure
33
- stop: "end_turn",
34
- length: "max_output_tokens",
35
- content_filter: "content_filter",
36
- tool_calls: "tool_call",
37
- // Anthropic
38
- end_turn: "end_turn",
39
- max_tokens: "max_output_tokens",
40
- tool_use: "tool_call",
41
- // Generic
42
- error: "error",
43
- };
44
-
45
- export function mapStopReason(providerReason: string): StopReason {
46
- return STOP_REASON_MAP[providerReason] ?? "unknown";
47
- }
48
-
49
- // ── Reasoning visibility 映射 ──────────────────────────────────
50
-
51
- export function mapReasoningVisibility(hasThinking: boolean, hasRedacted: boolean): ReasoningItem["visibility"] {
52
- if (hasRedacted) return "redacted";
53
- if (hasThinking) return "full";
54
- return "opaque";
55
- }
56
-
57
- // ── Content block 构造 helper ─────────────────────────────────
58
-
59
- export function textBlock(text: string): ContentBlock & { type: "text" } {
60
- return { type: "text", text };
61
- }
62
-
63
- export function jsonBlock(json: unknown): ContentBlock & { type: "json" } {
64
- return { type: "json", json };
65
- }
66
-
67
- export function imageBlock(imageUrl: string): ContentBlock & { type: "image" } {
68
- return { type: "image", imageUrl };
69
- }
70
-
71
- export function opaqueBlock(payload: unknown): ContentBlock & { type: "opaque" } {
72
- return { type: "opaque", payload };
73
- }
74
-
75
- // ── Item 构造 helper ──────────────────────────────────────────
76
-
77
- export function messageItem(
78
- content: ContentBlock[],
79
- overrides?: Partial<Omit<MessageItem, "type" | "content">>,
80
- ): MessageItem {
81
- return {
82
- type: "message",
83
- role: "assistant",
84
- ...overrides,
85
- content,
86
- };
87
- }
88
-
89
- export function reasoningItem(
90
- content: ContentBlock[],
91
- visibility: ReasoningItem["visibility"] = "full",
92
- id?: string,
93
- ): ReasoningItem {
94
- return {
95
- type: "reasoning",
96
- id,
97
- visibility,
98
- content,
99
- };
100
- }
101
-
102
- export function toolCallItem(id: string, name: string, argumentsText: string): ToolCallItem {
103
- return {
104
- type: "tool_call",
105
- id,
106
- name,
107
- argumentsText,
108
- };
109
- }
110
-
111
- export function toolResultItem(
112
- callId: string,
113
- toolName: string,
114
- outcome: ToolResultItem["outcome"],
115
- content: ContentBlock[],
116
- ): ToolResultItem {
117
- return {
118
- type: "tool_result",
119
- callId,
120
- toolName,
121
- outcome,
122
- content,
123
- };
124
- }
125
-
126
- export function opaqueItem(
127
- source: OpaqueItem["source"],
128
- purpose: OpaqueItem["purpose"],
129
- payload: unknown,
130
- id?: string,
131
- ): OpaqueItem {
132
- return {
133
- type: "opaque",
134
- id,
135
- source,
136
- purpose,
137
- payload,
138
- };
139
- }
140
-
141
- // ── Replay 构造工具 ──────────────────────────────────────────
142
-
143
- /**
144
- * 从 output items 构建标准 replay items。
145
- * 简单场景下 replay 与 output 一致。
146
- * 复杂场景(需要 opaque continuation)由 adapter 自行扩展。
147
- */
148
- export function replayFromOutput(output: readonly OutputItem[]): ReplayItem[] {
149
- return output.map((item): InputItem => {
150
- switch (item.type) {
151
- case "message":
152
- case "reasoning":
153
- case "tool_call":
154
- return item as InputItem;
155
- case "opaque":
156
- return item;
157
- }
158
- });
159
- }
160
-
161
- // ── Content block 提取 helper ──────────────────────────────────
162
-
163
- /**
164
- * 将单个 ContentBlock 转为纯文本。
165
- * text 块直接返回文本,json 块序列化,其余返回空串。
166
- */
167
- export function blockToText(b: ContentBlock): string {
168
- if (b.type === "text") return b.text;
169
- if (b.type === "json") return JSON.stringify(b.json);
170
- return "";
171
- }
172
-
173
- /**
174
- * 将 ContentBlock 数组拼接为纯文本,块间以换行符分隔。
175
- */
176
- export function contentBlocksToText(blocks: ContentBlock[]): string {
177
- return blocks.map(blockToText).join("\n");
178
- }
179
-
180
- // ── Output 文本提取 ───────────────────────────────────────────
181
-
182
- /**
183
- * 从 OutputItem 数组中提取所有 message 类型 item 的文本内容。
184
- */
185
- export function extractText(output: OutputItem[]): string {
186
- return output
187
- .filter((item): item is MessageItem => item.type === "message")
188
- .flatMap((m) => m.content)
189
- .filter((b): b is ContentBlock & { type: "text" } => b.type === "text")
190
- .map((b) => b.text)
191
- .join("");
192
- }
@@ -1,25 +0,0 @@
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
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Provider HTTP 流公共脚手架
3
- *
4
- * 收敛 adapter 间重复的:
5
- * - JSON POST + 错误映射
6
- * - ReadableStream reader 生命周期
7
- * - IncrementalStreamParser feed/flush + malformed warning
8
- * - 不完整尾帧 warning
9
- */
10
-
11
- import { AIProviderError, AIStreamError } from "../core/errors.js";
12
- import type { EventFactory } from "../core/event-factory.js";
13
- import type { AIStreamEvent, FetchFn } from "../types/index.js";
14
- import { emitMalformedStreamWarning } from "./adapter-auxiliary.js";
15
- import { providerHttpError } from "./adapter-security.js";
16
- import type { IncrementalStreamParser } from "./incremental-stream-parser.js";
17
-
18
- export type OpenProviderJsonStreamOptions = {
19
- fetchFn: FetchFn;
20
- url: string;
21
- headers: Record<string, string>;
22
- body: unknown;
23
- signal?: AbortSignal;
24
- };
25
-
26
- export type OpenedProviderStream = {
27
- reader: ReadableStreamDefaultReader<Uint8Array>;
28
- headers: Headers;
29
- };
30
-
31
- /** POST JSON 并返回可读 body reader + response headers;统一网络/HTTP/空 body 错误。 */
32
- export async function openProviderJsonStream(options: OpenProviderJsonStreamOptions): Promise<OpenedProviderStream> {
33
- const { fetchFn, url, headers, body, signal } = options;
34
-
35
- let response: Response;
36
- try {
37
- response = await fetchFn(url, {
38
- method: "POST",
39
- headers,
40
- body: JSON.stringify(body),
41
- signal,
42
- });
43
- } catch (err) {
44
- throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
45
- }
46
-
47
- if (!response.ok) {
48
- const errorBody = await response.text().catch(() => "");
49
- throw providerHttpError(response.status, errorBody);
50
- }
51
-
52
- const bodyStream = response.body;
53
- if (!bodyStream) {
54
- throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
55
- }
56
-
57
- // Bun/DOM ReadableStreamDefaultReader 类型略有差异,按最小接口使用
58
- return {
59
- reader: bodyStream.getReader() as ReadableStreamDefaultReader<Uint8Array>,
60
- headers: response.headers,
61
- };
62
- }
63
-
64
- export type ProviderStreamBatchOptions<T> = {
65
- reader: ReadableStreamDefaultReader<Uint8Array>;
66
- parser: IncrementalStreamParser<T>;
67
- factory: EventFactory;
68
- providerLabel: string;
69
- transportLabel: string;
70
- incompleteMessage: string;
71
- };
72
-
73
- export type ProviderStreamBatch<T> = {
74
- items: T[];
75
- warnings: AIStreamEvent[];
76
- };
77
-
78
- /**
79
- * 读取并解析 provider 流。
80
- * 每个 batch 携带本轮解析出的 items 与(可选)malformed / incomplete warning。
81
- * 调用方应 `for await` 消费完毕;reader 在迭代结束时 cancel/release。
82
- */
83
- export async function* iterateProviderStreamBatches<T>(
84
- options: ProviderStreamBatchOptions<T>,
85
- ): AsyncGenerator<ProviderStreamBatch<T>, void, undefined> {
86
- const { reader, parser, factory, providerLabel, transportLabel, incompleteMessage } = options;
87
- let streamDone = false;
88
-
89
- try {
90
- while (true) {
91
- const readResult = await reader.read().catch((err: unknown) => {
92
- throw new AIStreamError(
93
- `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
94
- "STREAM_ERROR",
95
- );
96
- });
97
- const { done, value } = readResult;
98
- const { items, malformed } = done ? parser.flush() : parser.feed(value as Uint8Array);
99
-
100
- const warnings: AIStreamEvent[] = [];
101
- const malformedWarning = emitMalformedStreamWarning(factory, {
102
- count: malformed,
103
- providerLabel,
104
- transportLabel,
105
- });
106
- if (malformedWarning) warnings.push(malformedWarning);
107
-
108
- yield { items, warnings };
109
-
110
- if (done) {
111
- streamDone = true;
112
- break;
113
- }
114
- }
115
- } finally {
116
- try {
117
- if (!streamDone) await reader.cancel().catch(() => undefined);
118
- } finally {
119
- reader.releaseLock();
120
- }
121
- }
122
-
123
- if (parser.getRemaining().trim().length > 0) {
124
- yield {
125
- items: [],
126
- warnings: [factory.responseWarning(incompleteMessage, "STREAM_ERROR")],
127
- };
128
- }
129
- }
130
-
131
- /** 一次性 complete 守卫:首次成功,后续返回 false。 */
132
- export function createCompletionGate(): {
133
- readonly completed: boolean;
134
- tryComplete(): boolean;
135
- } {
136
- let completed = false;
137
- return {
138
- get completed() {
139
- return completed;
140
- },
141
- tryComplete() {
142
- if (completed) return false;
143
- completed = true;
144
- return true;
145
- },
146
- };
147
- }
@@ -1,86 +0,0 @@
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
- }