@codehz/ai 0.4.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ai",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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
- "Content-Type": "application/json",
378
- Authorization: `Bearer ${this.apiKey}`,
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
  });
@@ -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
- "Content-Type": "application/json",
398
- "x-api-key": this.apiKey,
399
- "anthropic-version": this.apiVersion,
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
  });
@@ -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.signal);
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, signal?: AbortSignal): MockHandlerContext {
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
  }
@@ -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 不支持 */
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
- return body;
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
- "Content-Type": "application/json",
479
- Authorization: `Bearer ${this.apiKey}`,
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
  });
@@ -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
  }
@@ -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
+ }
@@ -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";
@@ -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
  };