@qjp88995/provider-client 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/center-fetch.d.ts +29 -0
  2. package/dist/center-fetch.d.ts.map +1 -0
  3. package/dist/center-fetch.js +45 -0
  4. package/dist/center-fetch.js.map +1 -0
  5. package/dist/direct-resolver.d.ts +29 -0
  6. package/dist/direct-resolver.d.ts.map +1 -0
  7. package/dist/direct-resolver.js +47 -0
  8. package/dist/direct-resolver.js.map +1 -0
  9. package/dist/errors.d.ts +38 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +57 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/model-handle.d.ts +92 -0
  18. package/dist/model-handle.d.ts.map +1 -0
  19. package/dist/model-handle.js +72 -0
  20. package/dist/model-handle.js.map +1 -0
  21. package/dist/provider-client.d.ts +40 -0
  22. package/dist/provider-client.d.ts.map +1 -0
  23. package/dist/provider-client.js +96 -0
  24. package/dist/provider-client.js.map +1 -0
  25. package/dist/to-error.d.ts +12 -0
  26. package/dist/to-error.d.ts.map +1 -0
  27. package/dist/to-error.js +33 -0
  28. package/dist/to-error.js.map +1 -0
  29. package/dist/types.d.ts +78 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +75 -0
  32. package/dist/types.js.map +1 -0
  33. package/dist/vendors/alibaba.adapter.d.ts +47 -0
  34. package/dist/vendors/alibaba.adapter.d.ts.map +1 -0
  35. package/dist/vendors/alibaba.adapter.js +79 -0
  36. package/dist/vendors/alibaba.adapter.js.map +1 -0
  37. package/dist/vendors/ark-responses-shim.d.ts +25 -0
  38. package/dist/vendors/ark-responses-shim.d.ts.map +1 -0
  39. package/dist/vendors/ark-responses-shim.js +60 -0
  40. package/dist/vendors/ark-responses-shim.js.map +1 -0
  41. package/dist/vendors/deepseek.adapter.d.ts +50 -0
  42. package/dist/vendors/deepseek.adapter.d.ts.map +1 -0
  43. package/dist/vendors/deepseek.adapter.js +123 -0
  44. package/dist/vendors/deepseek.adapter.js.map +1 -0
  45. package/dist/vendors/dots.adapter.d.ts +32 -0
  46. package/dist/vendors/dots.adapter.d.ts.map +1 -0
  47. package/dist/vendors/dots.adapter.js +53 -0
  48. package/dist/vendors/dots.adapter.js.map +1 -0
  49. package/dist/vendors/generic.adapter.d.ts +33 -0
  50. package/dist/vendors/generic.adapter.d.ts.map +1 -0
  51. package/dist/vendors/generic.adapter.js +82 -0
  52. package/dist/vendors/generic.adapter.js.map +1 -0
  53. package/dist/vendors/google-signature.d.ts +17 -0
  54. package/dist/vendors/google-signature.d.ts.map +1 -0
  55. package/dist/vendors/google-signature.js +69 -0
  56. package/dist/vendors/google-signature.js.map +1 -0
  57. package/dist/vendors/google.adapter.d.ts +41 -0
  58. package/dist/vendors/google.adapter.d.ts.map +1 -0
  59. package/dist/vendors/google.adapter.js +164 -0
  60. package/dist/vendors/google.adapter.js.map +1 -0
  61. package/dist/vendors/index.d.ts +11 -0
  62. package/dist/vendors/index.d.ts.map +1 -0
  63. package/dist/vendors/index.js +39 -0
  64. package/dist/vendors/index.js.map +1 -0
  65. package/dist/vendors/minimax.adapter.d.ts +98 -0
  66. package/dist/vendors/minimax.adapter.d.ts.map +1 -0
  67. package/dist/vendors/minimax.adapter.js +163 -0
  68. package/dist/vendors/minimax.adapter.js.map +1 -0
  69. package/dist/vendors/moonshotai.adapter.d.ts +31 -0
  70. package/dist/vendors/moonshotai.adapter.d.ts.map +1 -0
  71. package/dist/vendors/moonshotai.adapter.js +57 -0
  72. package/dist/vendors/moonshotai.adapter.js.map +1 -0
  73. package/dist/vendors/openai-responses.d.ts +49 -0
  74. package/dist/vendors/openai-responses.d.ts.map +1 -0
  75. package/dist/vendors/openai-responses.js +64 -0
  76. package/dist/vendors/openai-responses.js.map +1 -0
  77. package/dist/vendors/openai.adapter.d.ts +27 -0
  78. package/dist/vendors/openai.adapter.d.ts.map +1 -0
  79. package/dist/vendors/openai.adapter.js +196 -0
  80. package/dist/vendors/openai.adapter.js.map +1 -0
  81. package/dist/vendors/responses-reasoning-text-shim.d.ts +50 -0
  82. package/dist/vendors/responses-reasoning-text-shim.d.ts.map +1 -0
  83. package/dist/vendors/responses-reasoning-text-shim.js +170 -0
  84. package/dist/vendors/responses-reasoning-text-shim.js.map +1 -0
  85. package/dist/vendors/vendor-adapter.d.ts +214 -0
  86. package/dist/vendors/vendor-adapter.d.ts.map +1 -0
  87. package/dist/vendors/vendor-adapter.js +150 -0
  88. package/dist/vendors/vendor-adapter.js.map +1 -0
  89. package/dist/vendors/volcengine.adapter.d.ts +74 -0
  90. package/dist/vendors/volcengine.adapter.d.ts.map +1 -0
  91. package/dist/vendors/volcengine.adapter.js +139 -0
  92. package/dist/vendors/volcengine.adapter.js.map +1 -0
  93. package/dist/vendors/zhipu.adapter.d.ts +81 -0
  94. package/dist/vendors/zhipu.adapter.d.ts.map +1 -0
  95. package/dist/vendors/zhipu.adapter.js +133 -0
  96. package/dist/vendors/zhipu.adapter.js.map +1 -0
  97. package/package.json +53 -3
  98. package/src/center-fetch.ts +69 -0
  99. package/src/direct-resolver.ts +90 -0
  100. package/src/errors.ts +88 -0
  101. package/src/index.ts +54 -0
  102. package/src/model-handle.ts +197 -0
  103. package/src/provider-client.ts +136 -0
  104. package/src/to-error.ts +39 -0
  105. package/src/types.ts +126 -0
  106. package/src/vendors/alibaba.adapter.ts +101 -0
  107. package/src/vendors/ark-responses-shim.ts +60 -0
  108. package/src/vendors/deepseek.adapter.ts +155 -0
  109. package/src/vendors/dots.adapter.ts +69 -0
  110. package/src/vendors/generic.adapter.ts +103 -0
  111. package/src/vendors/google-signature.ts +87 -0
  112. package/src/vendors/google.adapter.ts +197 -0
  113. package/src/vendors/index.ts +56 -0
  114. package/src/vendors/minimax.adapter.ts +193 -0
  115. package/src/vendors/moonshotai.adapter.ts +84 -0
  116. package/src/vendors/openai-responses.ts +82 -0
  117. package/src/vendors/openai.adapter.ts +235 -0
  118. package/src/vendors/responses-reasoning-text-shim.ts +174 -0
  119. package/src/vendors/vendor-adapter.ts +294 -0
  120. package/src/vendors/volcengine.adapter.ts +166 -0
  121. package/src/vendors/zhipu.adapter.ts +169 -0
  122. package/README.md +0 -3
@@ -0,0 +1,193 @@
1
+ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
2
+ import { type LanguageModel, wrapLanguageModel } from 'ai';
3
+
4
+ import type { LlmProtocol } from '../types.js';
5
+ import {
6
+ COMPATIBLE_RESPONSES_OPTIONS,
7
+ createResponsesModel,
8
+ responsesDefaultsMiddleware,
9
+ } from './openai-responses.js';
10
+ import { withReasoningTextResponsesShim } from './responses-reasoning-text-shim.js';
11
+ import {
12
+ type CreateModelInput,
13
+ LlmVendorAdapter,
14
+ type ProviderOptions,
15
+ type ReasoningEffort,
16
+ unsupportedProtocol,
17
+ } from './vendor-adapter.js';
18
+
19
+ /**
20
+ * 工厂 `name`。决定 provider id(`minimax.chat`)与 providerOptions 的命名空间
21
+ * key(单词无连字符,`name` 与 `toCamelCase(name)` 相同),恰好等于 `kind`,
22
+ * 基类默认的 `optionsKey` 直接可用。
23
+ */
24
+ const FACTORY_NAME = 'minimax';
25
+
26
+ /**
27
+ * responses 协议下每次调用都必须带上的两项 providerOptions(挂法与方舟同一个
28
+ * 中间件,理由见 `responsesDefaultsMiddleware`):
29
+ *
30
+ * - **`store: false`**:文档的 `InputItem.type` 只有 `message / function_call /
31
+ * function_call_output / reasoning` 四种,**没有 `item_reference`**;SDK 在
32
+ * `store` 为 true(默认)时把上一轮带 itemId 的 assistant 正文发成
33
+ * `{type:'item_reference', id}`,多步工具回环第二步就会撞上它。
34
+ * - **`reasoningSummary: null`**:文档的 `reasoning` 对象只有 `effort` 一个键;
35
+ * SDK 对推理模型默认附带 `reasoning.summary:'detailed'`。它认不认未知键
36
+ * 没有探过,方舟对同一个键是 400——传 null 让 SDK 整个不发,反正推理正文
37
+ * 由垫片从 `content[{reasoning_text}]` 里取,`summary` 我们不需要。
38
+ */
39
+ export const MINIMAX_RESPONSES_DEFAULTS = {
40
+ store: false,
41
+ reasoningSummary: null,
42
+ } as const;
43
+
44
+ /**
45
+ * chat-completions 每次请求都必须带 `reasoning_split: true`。
46
+ *
47
+ * MiniMax 的 OpenAI 兼容面默认把思考**塞进 `content`**、用 `<think>…</think>`
48
+ * 包着(文档 guides/text-m3-function-call「原生格式」一节):SDK 会把它当正文吐成
49
+ * text part,对话页里整段思考混在回答里、存下来的产出也会带着标签,而且投影层回传历史
50
+ * 时那段也跟着当正文发回去。开了这个开关,思考走 `reasoning_content`(SDK 原生
51
+ * 读的字段)+ `reasoning_details`(结构化,SDK 忽略)。**它不开关思考本身**,
52
+ * 只决定输出格式,所以与档位无关、与模型行有没有标 `reasoning` 也无关——
53
+ * 放在工厂的 `transformRequestBody` 里,而不是 `reasoning()`。
54
+ */
55
+ export function withReasoningSplit(
56
+ body: Record<string, unknown>
57
+ ): Record<string, unknown> {
58
+ return { ...body, reasoning_split: true };
59
+ }
60
+
61
+ /**
62
+ * MiniMax(M 系:MiniMax-M3 / M2.7 / M2.5 / M2.1 / M2 及各 highspeed 版)。
63
+ *
64
+ * **AI SDK 没有它的官方 provider**(社区包跟不上大版本,不接)。承载包与智谱、方舟
65
+ * 同一套:chat-completions 走 `@ai-sdk/openai-compatible`(私有的 `thinking` /
66
+ * `reasoning_split` 靠它把未知键展开进请求体,`reasoning_content` 是它原生读的
67
+ * 字段),responses 走 `@ai-sdk/openai` + 「推理正文走 `reasoning_text`」的共享垫片。
68
+ * 文档还推荐 Anthropic 兼容面(`/anthropic`,支持 thinking 块与 interleaved
69
+ * thinking),本仓尚无 `anthropic-messages` 分支,先不接。
70
+ *
71
+ * 全部判据来自 2026-09-10 抓的官方文档(`platform.minimaxi.com/docs/llms.txt`
72
+ * 下 api-reference/text-chat-openai、responses-create、models/openai/list-models、
73
+ * guides/text-m3-function-call),**没有真 key 探针**——文档没写明的地方在各处注释
74
+ * 里标了「未探」,接真 key 后先跑一遍 `minimax.sdk-contract.spec.ts` 里列的探针项:
75
+ *
76
+ * - **chat 的推理开关只有开/关,没有档位**:`thinking:{type:'disabled'|'adaptive'}`,
77
+ * 省略 = adaptive(默认开着推理,「不发 ≠ 关闭」在这家同样成立)。M3 才关得掉,
78
+ * M2.x「thinking 无法关闭」——但 `disabled` 对 M2.x 是**被接受后忽略**(文档
79
+ * 原话「仍会保持开启」),不是 400,所以不按模型名分支。chat 面没有
80
+ * `reasoning_effort` 参数,我们的五个非 off 档位一律 adaptive(如实反映:
81
+ * 这家不提供推理深度控制)。
82
+ * - **responses 的默认值与 chat 相反**:`reasoning.effort` 省略 = `none`(关),
83
+ * `minimal/low/medium/high` 都收、但「不会调节推理深度」;M2.x 同样关不掉。
84
+ * `off` → `none` 是真正的关闭指令;`max` → `high`(它的枚举没有 max)。
85
+ * - 响应侧:chat 开了 `reasoning_split` 后是 `message.reasoning_content` /
86
+ * `delta.reasoning_content`(文档只给了非流式的字段说明,流式 delta 里叫什么
87
+ * **未探**,按各家通例假定同名);responses 的 reasoning item 是
88
+ * `content:[{type:'reasoning_text'}]` + `summary`(文档 OutputItem),与
89
+ * DeepSeek / 智谱同形,流式事件名文档没给(未探),垫片对原生 summary 事件
90
+ * 本就不动,两种形状都落得了位。
91
+ * - **交错思维链的回传**:文档要求把 `reasoning_details` 完整回传给下一轮,否则
92
+ * 思维链「被打断」;SDK 回传的是 `reasoning_content`(3.0.35 dist:284)。这家
93
+ * 认不认 `reasoning_content` 作为输入**未探**——认则零改动,不认也只是降质
94
+ * (文档说的是「发挥最佳性能」),不是失败。探明不认再在 `transformRequestBody`
95
+ * 里把它改写成 `reasoning_details:[{type:'reasoning.text', text}]`。
96
+ * - `temperature` 范围 chat `[0, 2]`、responses `(0, 1]`;`top_p` `[0, 1]`。
97
+ * `sampling` 只有「发不发」的粒度,responses 那条上限靠运营录参数时自觉。
98
+ * - `max_tokens` 已弃用、改 `max_completion_tokens`;本仓刻意不传输出上限,
99
+ * SDK 也就不会发旧参数。
100
+ * - `GET /v1/models` 是标准 OpenAI 形状(`{object:'list', data:[{id}]}`),
101
+ * 默认目录逻辑直接可用;文档没有余额接口(账户 FAQ 只讲充值与预警),
102
+ * 中心不查余额。
103
+ * - 错误在 200 里也可能带 `base_resp.status_code`(1004 鉴权 / 1008 余额不足 /
104
+ * 2013 参数错误…)——**未探**它对 OpenAI 兼容面是否改用标准 HTTP 状态码;
105
+ * 若真是 200 + base_resp,SDK 会当成一条空回复静默过去,那时要加响应垫片。
106
+ */
107
+ export class MiniMaxAdapter extends LlmVendorAdapter {
108
+ readonly kind = 'minimax' as const;
109
+
110
+ override readonly supportedProtocols: readonly LlmProtocol[] = [
111
+ 'openai-chat-completions',
112
+ 'openai-responses',
113
+ ];
114
+
115
+ createModel(input: CreateModelInput): LanguageModel {
116
+ switch (input.protocol) {
117
+ case 'openai-chat-completions': {
118
+ const { baseUrl, apiKey, modelId, fetch } = input;
119
+ return createOpenAICompatible({
120
+ name: FACTORY_NAME,
121
+ baseURL: baseUrl,
122
+ apiKey,
123
+ fetch,
124
+ transformRequestBody: withReasoningSplit,
125
+ })(modelId);
126
+ }
127
+ case 'openai-responses':
128
+ return wrapLanguageModel({
129
+ model: createResponsesModel(
130
+ input,
131
+ withReasoningTextResponsesShim(input.fetch)
132
+ ) as Exclude<LanguageModel, string>,
133
+ middleware: responsesDefaultsMiddleware(MINIMAX_RESPONSES_DEFAULTS),
134
+ });
135
+ default:
136
+ throw unsupportedProtocol(this, input.protocol);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * **chat-completions**:`off` 发显式的 `thinking:{type:'disabled'}`(它默认
142
+ * adaptive,「不发 ≠ 关闭」);非 off 一律 `thinking:{type:'adaptive'}`——这家
143
+ * 没有档位参数,五档压成一档是如实反映而不是漏了映射。绝不发 `reasoning_effort`:
144
+ * 它不在文档的参数表里,2013「参数错误」的风险不值得赌。
145
+ *
146
+ * **responses**:只发 `reasoning.effort`,`off`→`none`(文档:关闭推理输出,
147
+ * 也是它的默认值);`minimal/low/medium/high` 原生都收、直通;`max`→`high`。
148
+ * key 随承载包换成 `openai`,带 `COMPATIBLE_RESPONSES_OPTIONS`(不带则 SDK 按
149
+ * 模型名判成非推理模型、`reasoning` 整个不发——这家 responses 默认是关的,
150
+ * 所以后果是「档位调了没反应」而不是「关不掉」,同样静默)。`store:false` /
151
+ * `reasoningSummary:null` 由中间件兜底,这里不重复。
152
+ */
153
+ override reasoning(
154
+ _modelId: string,
155
+ effort: ReasoningEffort,
156
+ protocol: LlmProtocol
157
+ ): ProviderOptions {
158
+ switch (protocol) {
159
+ case 'openai-chat-completions':
160
+ return {
161
+ [this.optionsKey]: {
162
+ thinking: { type: effort === 'off' ? 'disabled' : 'adaptive' },
163
+ },
164
+ };
165
+ case 'openai-responses':
166
+ return {
167
+ openai: {
168
+ reasoningEffort: toMiniMaxEffort(effort),
169
+ ...COMPATIBLE_RESPONSES_OPTIONS,
170
+ },
171
+ };
172
+ default:
173
+ throw unsupportedProtocol(this, protocol);
174
+ }
175
+ }
176
+ }
177
+
178
+ /** 我们的六档 → 它 responses 面文档化的 `none/minimal/low/medium/high`。 */
179
+ function toMiniMaxEffort(
180
+ effort: ReasoningEffort
181
+ ): 'none' | 'minimal' | 'low' | 'medium' | 'high' {
182
+ switch (effort) {
183
+ case 'off':
184
+ return 'none';
185
+ case 'minimal':
186
+ case 'low':
187
+ case 'medium':
188
+ case 'high':
189
+ return effort;
190
+ case 'max':
191
+ return 'high';
192
+ }
193
+ }
@@ -0,0 +1,84 @@
1
+ import { createMoonshotAI } from '@ai-sdk/moonshotai';
2
+ import type { LanguageModel } from 'ai';
3
+
4
+ import type { LlmProtocol } from '../types.js';
5
+ import {
6
+ assertChatCompletions,
7
+ type CreateModelInput,
8
+ LlmVendorAdapter,
9
+ type ProviderOptions,
10
+ type ReasoningEffort,
11
+ type SamplingSupport,
12
+ } from './vendor-adapter.js';
13
+
14
+ /**
15
+ * Kimi 的推理开关是**模型级**的三种互斥形态,判据只住在这里
16
+ * (`referenes/kimi-request.md` 的分模型对照表):
17
+ *
18
+ * - `effort`:K3 专属。不支持 `thinking`,传入报错;只认顶层 `reasoning_effort`。
19
+ * - `forced`:k2.7-code 系列。思考强制开启,传 `thinking:{type:'disabled'}` 报错。
20
+ * - `toggle`:k2.5 / k2.6。`thinking` 的 `enabled`/`disabled` 都合法。
21
+ *
22
+ * 模型名换了就改这一个函数。`moonshot-v1-*` 那批老模型本就没有推理能力,
23
+ * 走不到这里(resolver 只对标了 `reasoning` 能力的模型问推理选项)。
24
+ */
25
+ type ThinkingStyle = 'effort' | 'forced' | 'toggle';
26
+
27
+ export function moonshotThinkingStyle(modelId: string): ThinkingStyle {
28
+ const id = modelId.toLowerCase();
29
+ if (id.startsWith('kimi-k3')) return 'effort';
30
+ if (id.includes('k2.7-code')) return 'forced';
31
+ return 'toggle';
32
+ }
33
+
34
+ export class MoonshotAiAdapter extends LlmVendorAdapter {
35
+ readonly kind = 'moonshotai' as const;
36
+
37
+ /**
38
+ * **Kimi 全系禁止 temperature/top_p,传参直接返回参数非法**
39
+ * (`referenes/kimi-request.md` 避坑提示第 1 条;实际值固定 1.0,
40
+ * k2.5/k2.6 非思考模式为 0.6)。AI SDK 不会替我们挡——实测
41
+ * `generateText({temperature:0.7})` 会原样发出 `"temperature":0.7`,
42
+ * 所以这道声明就是唯一的防线。
43
+ */
44
+ readonly sampling: SamplingSupport = { temperature: false, topP: false };
45
+
46
+ createModel(input: CreateModelInput): LanguageModel {
47
+ assertChatCompletions(this, input.protocol);
48
+ const { baseUrl, apiKey, modelId, fetch } = input;
49
+ return createMoonshotAI({ baseURL: baseUrl, apiKey, fetch })(modelId);
50
+ }
51
+
52
+ override reasoning(
53
+ modelId: string,
54
+ effort: ReasoningEffort,
55
+ _protocol: LlmProtocol
56
+ ): ProviderOptions | undefined {
57
+ switch (moonshotThinkingStyle(modelId)) {
58
+ case 'effort':
59
+ // K3 **没有关闭开关**(`reasoning_effort` 只有 low/high/max,默认 max),
60
+ // 故 off 只能不发——这是如实反映厂商限制,不是漏了关闭指令。
61
+ if (effort === 'off') return undefined;
62
+ return { moonshotai: { reasoningEffort: toK3Effort(effort) } };
63
+ case 'forced':
64
+ // 思考关不掉,而"开"就是默认值——发什么都是多余,发 disabled 还会报错。
65
+ return undefined;
66
+ case 'toggle':
67
+ return {
68
+ moonshotai: {
69
+ thinking: { type: effort === 'off' ? 'disabled' : 'enabled' },
70
+ },
71
+ };
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 我们的档位 → K3 的原生三档 `low/high/max`(默认 max)。`max` 直通——
78
+ * 用户选了最高档就该拿到最高档,别替他降一级。
79
+ */
80
+ function toK3Effort(effort: ReasoningEffort): string {
81
+ if (effort === 'minimal' || effort === 'low') return 'low';
82
+ if (effort === 'max') return 'max';
83
+ return 'high';
84
+ }
@@ -0,0 +1,82 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import type { JSONValue, LanguageModel, LanguageModelMiddleware } from 'ai';
3
+
4
+ import type { CreateModelInput } from './vendor-adapter.js';
5
+
6
+ /**
7
+ * `createOpenAI` 接受的自定义 fetch。类型从工厂签名上取,不为此多引一个
8
+ * `@ai-sdk/provider-utils` 依赖——它是 `@ai-sdk/openai` 的内部依赖,版本跟着走。
9
+ */
10
+ export type ResponsesFetch = NonNullable<
11
+ Parameters<typeof createOpenAI>[0]
12
+ >['fetch'];
13
+
14
+ /**
15
+ * `openai-responses` 协议的句柄,三家(openai / generic / deepseek)共用:
16
+ * 线格式由 OpenAI 定义,别家只是「兼容」,所以承载它的包也只有 `@ai-sdk/openai`
17
+ * 这一个——`@ai-sdk/deepseek` 3.0.x 只有 chat-completions(已核 dist:路径里只有
18
+ * `/chat/completions`,`DeepSeekProvider` 也只暴露 `chat()`),别去那里找开关。
19
+ *
20
+ * 兼容厂商的差异用 `fetch` 垫片在 HTTP 层抹平(见 `responses-reasoning-text-shim.ts`),
21
+ * SDK 一行不动。
22
+ */
23
+ export function createResponsesModel(
24
+ input: CreateModelInput,
25
+ fetch?: ResponsesFetch
26
+ ): LanguageModel {
27
+ const { baseUrl, apiKey, modelId } = input;
28
+ // 垫片自己以 `input.fetch` 为 base(见各家 createModel),没有垫片时直接用它。
29
+ return createOpenAI({
30
+ baseURL: baseUrl,
31
+ apiKey,
32
+ fetch: fetch ?? input.fetch,
33
+ }).responses(modelId);
34
+ }
35
+
36
+ /**
37
+ * 兼容厂商走 responses 协议时 `providerOptions.openai` 里必带的两项。
38
+ *
39
+ * - **`forceReasoning: true`**:`@ai-sdk/openai` 的 responses 模型按**模型名**
40
+ * 判断「是不是推理模型」(`o` 系 / `gpt-5+`),不是就**整个不发 `reasoning`
41
+ * 参数**——`deepseek-v4-flash` 这种名字下档位与关闭指令会静默丢光,而
42
+ * DeepSeek 默认开着推理,「不发 ≠ 关闭」在这里再次成立。已核 4.0.46 dist
43
+ * (`isReasoningModel = forceReasoning ?? modelCapabilities.isReasoningModel`)。
44
+ * 代价:SDK 在推理模型上会剥掉 `temperature`/`top_p` 并打 warning——DeepSeek
45
+ * 文档本就声明思考模式下二者无效,可接受;`off` 时也会被剥,记为已知限制。
46
+ * - **`systemMessageMode: 'system'`**:推理模型默认把 system 消息发成
47
+ * `developer` 角色,那是 OpenAI 自家的规矩,兼容厂商只保证认 `system`。
48
+ *
49
+ * OpenAI 官方**不**用这组:它的模型名 SDK 认得,强制反而会给 `gpt-4.1` 这类
50
+ * 非推理模型发 `reasoning` 参数而 400。
51
+ */
52
+ export const COMPATIBLE_RESPONSES_OPTIONS = {
53
+ forceReasoning: true,
54
+ systemMessageMode: 'system',
55
+ } as const;
56
+
57
+ /**
58
+ * 给 responses 句柄挂一层「每次调用必带的 `providerOptions.openai` 默认值」。
59
+ *
60
+ * 用它而不是把默认值放进 `reasoning()`:那条只对标了 `reasoning` 能力的模型行
61
+ * 才会被问,而这些默认值**与有没有推理无关**(方舟不带 `store:false` 多步工具
62
+ * 回环第二步就 400,MiniMax 的 InputItem 根本没有 `item_reference` 这一型)。
63
+ * 调用方显式给的同名选项优先(spread 在后)。首个使用者是方舟
64
+ * (`ARK_RESPONSES_DEFAULTS`),MiniMax 用同一组值。
65
+ */
66
+ export function responsesDefaultsMiddleware(
67
+ defaults: Readonly<Record<string, JSONValue>>
68
+ ): LanguageModelMiddleware {
69
+ return {
70
+ specificationVersion: 'v4',
71
+ transformParams: ({ params }) => {
72
+ const openai = params.providerOptions?.openai ?? {};
73
+ return Promise.resolve({
74
+ ...params,
75
+ providerOptions: {
76
+ ...params.providerOptions,
77
+ openai: { ...defaults, ...openai },
78
+ },
79
+ });
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,235 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import { type LanguageModel, wrapLanguageModel } from 'ai';
3
+
4
+ import type { LlmProtocol } from '../types.js';
5
+ import {
6
+ createResponsesModel,
7
+ responsesDefaultsMiddleware,
8
+ } from './openai-responses.js';
9
+ import {
10
+ type AdviceModelView,
11
+ type CreateModelInput,
12
+ LlmVendorAdapter,
13
+ type ModelAdvice,
14
+ type ProviderOptions,
15
+ type ReasoningEffort,
16
+ unsupportedProtocol,
17
+ } from './vendor-adapter.js';
18
+
19
+ /** OpenAI 线上的档位取值(`reasoning_effort` / `reasoning.effort` 同一套)。 */
20
+ type OpenAiEffort =
21
+ 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
22
+
23
+ /** 所有推理模型都认的三档;认不出的模型只发这几个。 */
24
+ const COMMON: readonly OpenAiEffort[] = ['low', 'medium', 'high'];
25
+
26
+ /** `gpt-<major>[.<minor>][-<variant>]`(SDK 同一条正则)拆出来的样子。 */
27
+ interface GptName {
28
+ major: number;
29
+ /** 没写小版本号时为 0(gpt-5 = 5.0)。 */
30
+ minor: number;
31
+ variant: string;
32
+ /** gpt-5+ 且不是 chat 别名(`gpt-5-chat-latest`)就是推理模型。 */
33
+ reasoning: boolean;
34
+ }
35
+
36
+ function parseGpt(modelId: string): GptName | null {
37
+ const m = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/.exec(modelId);
38
+ if (!m) return null;
39
+ const major = Number(m[1]);
40
+ const variant = m[3] ?? '';
41
+ const chatAlias =
42
+ major === 5 && m[2] === undefined && variant.startsWith('chat');
43
+ return {
44
+ major,
45
+ minor: m[2] === undefined ? 0 : Number(m[2]),
46
+ variant,
47
+ reasoning: major >= 5 && !chatAlias,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Chat Completions 带工具时只认 `none` 的家族:gpt-5.4 起(含 gpt-6 系)。探针:
53
+ * gpt-5.4 / 5.4-mini / 5.5 开任何推理档都 400「Function tools with reasoning_effort
54
+ * are not supported … use /v1/responses or set reasoning_effort to 'none'」,
55
+ * gpt-5.6 与 gpt-6-sol 连不发都 400(默认 medium),gpt-5.2 及更早、o 系不受限。
56
+ */
57
+ function chatToolsNeedNone(modelId: string): boolean {
58
+ const gpt = parseGpt(modelId);
59
+ if (!gpt?.reasoning) return false;
60
+ return gpt.major >= 6 || gpt.minor >= 4;
61
+ }
62
+
63
+ /**
64
+ * 这个模型在**服务端**认哪些档位(不含 `max` 的协议差异,见 `effortsFor`)。
65
+ * `null` = 不是推理模型(`gpt-4.1` 这类收到任何推理参数都 400)。
66
+ *
67
+ * 全部来自 2026-10-23 真 key 探针(16 个文本模型 × 7 档 × 两种协议 + nano /
68
+ * codex / pro 变体,两种协议的结论一致):
69
+ *
70
+ * - o 系(o3 / o4-mini):只有 low / medium / high。
71
+ * - gpt-5 / -mini / -nano:`minimal` 有、`none` 没有、`xhigh` 没有。
72
+ * - gpt-5.1:`none` 有、`minimal` 没了、`xhigh` 还没有。
73
+ * - gpt-5.2 ~ 5.5(含 -mini / -nano / 5.3-codex):none + 三档 + xhigh。
74
+ * - gpt-5.6-* 与 gpt-6-sol / gpt-6-luna:再加 `max`。
75
+ * - 其余 gpt-6+(gpt-6-astra、gpt-6.1-sol):没有 `none`(官方文档也这么写)。
76
+ * - pro:gpt-5-pro 只认 high;更新的 pro(5.5-pro 探过)认 medium / high / xhigh。
77
+ *
78
+ * 名字按 SDK 同一条正则拆(`gpt-<major>[.<minor>][-<variant>]`,带日期后缀的
79
+ * 快照也落在同一档);拆不出的 gpt 名字与未来的家族按 `COMMON` 走——猜成
80
+ * 「认」是 400,猜成「不认」只是退到相邻一档。
81
+ */
82
+ function serverEfforts(modelId: string): readonly OpenAiEffort[] | null {
83
+ if (/^o\d+(?:-|$)/.test(modelId)) return COMMON;
84
+ const gpt = parseGpt(modelId);
85
+ if (!gpt) return COMMON;
86
+ const { major, minor, variant } = gpt;
87
+ if (!gpt.reasoning) return null;
88
+ if (variant.startsWith('pro')) {
89
+ return major === 5 && minor === 0 ? ['high'] : ['medium', 'high', 'xhigh'];
90
+ }
91
+ if (major >= 6) {
92
+ const withNone = modelId === 'gpt-6-sol' || modelId === 'gpt-6-luna';
93
+ return [
94
+ ...(withNone ? (['none'] as const) : []),
95
+ ...COMMON,
96
+ 'xhigh',
97
+ 'max',
98
+ ];
99
+ }
100
+ if (minor === 0) return ['minimal', ...COMMON];
101
+ if (minor === 1) return ['none', ...COMMON];
102
+ if (minor <= 5) return ['none', ...COMMON, 'xhigh'];
103
+ return ['none', ...COMMON, 'xhigh', 'max'];
104
+ }
105
+
106
+ /**
107
+ * 某协议下实际能发的档位:**Chat Completions 在任何模型上都不认 `max`**(连
108
+ * gpt-6 系也 400,Responses 才收)。SDK 4.0.101 对 gpt-6 系把 `max` 列为支持,
109
+ * 走 chat 时会原样发出去,所以这里先剔掉。
110
+ */
111
+ function effortsFor(
112
+ modelId: string,
113
+ protocol: LlmProtocol
114
+ ): readonly OpenAiEffort[] | null {
115
+ const efforts = serverEfforts(modelId);
116
+ if (!efforts || protocol === 'openai-responses') return efforts;
117
+ return efforts.filter(e => e !== 'max');
118
+ }
119
+
120
+ /**
121
+ * 我们的档位 → 按偏好依次尝试的 OpenAI 档位,取第一个这个模型认的。`off` 先找
122
+ * 真关闭(`none`),没有就退到最少的那档——没有关闭开关的模型(gpt-5、gpt-6-astra、
123
+ * o 系)只能做到这一步;`max` 先找原生 `max`,再退 `xhigh` / `high`。
124
+ */
125
+ const PREFERENCE: Record<ReasoningEffort, readonly OpenAiEffort[]> = {
126
+ off: ['none', 'minimal', 'low', 'medium', 'high'],
127
+ minimal: ['minimal', 'low', 'medium', 'high'],
128
+ low: ['low', 'medium', 'high'],
129
+ medium: ['medium', 'high', 'low'],
130
+ high: ['high', 'xhigh', 'medium'],
131
+ max: ['max', 'xhigh', 'high', 'medium'],
132
+ };
133
+
134
+ /**
135
+ * Responses 每次调用必带 `store: false`。不发时服务端按默认把请求与响应存 30 天,
136
+ * 而我们每轮发全量历史、从不引用存档(`previous_response_id` / `item_reference`)
137
+ * ——存了只是让对话内容平白留在 OpenAI 那边。探针:功能不变,提示词缓存照样
138
+ * 命中(缓存与 `store` 无关)。放中间件而不是 `reasoning()`:与推理无关,没标
139
+ * `reasoning` 能力的模型行也要带。
140
+ */
141
+ const OPENAI_RESPONSES_DEFAULTS = { store: false } as const;
142
+
143
+ export class OpenAiAdapter extends LlmVendorAdapter {
144
+ readonly kind = 'openai' as const;
145
+ override readonly supportedProtocols: readonly LlmProtocol[] = [
146
+ 'openai-chat-completions',
147
+ 'openai-responses',
148
+ ];
149
+
150
+ createModel(input: CreateModelInput): LanguageModel {
151
+ switch (input.protocol) {
152
+ case 'openai-chat-completions': {
153
+ const { baseUrl, apiKey, modelId, fetch } = input;
154
+ // **必须 `.chat()`**:`@ai-sdk/openai` 4.x 里直接调用工厂造的是
155
+ // Responses 模型——这一行曾写成 `createOpenAI(...)(modelId)`,后台选了
156
+ // Chat Completions 的模型实际一直在打 `/responses`(2026-10-23 契约
157
+ // 测试回放 chat 抓包时 SDK 报「Received a Chat Completions stream while
158
+ // using the OpenAI Responses API」才发现)。
159
+ return createOpenAI({ baseURL: baseUrl, apiKey, fetch }).chat(modelId);
160
+ }
161
+ case 'openai-responses':
162
+ return wrapLanguageModel({
163
+ model: createResponsesModel(input) as Exclude<LanguageModel, string>,
164
+ middleware: responsesDefaultsMiddleware(OPENAI_RESPONSES_DEFAULTS),
165
+ });
166
+ default:
167
+ throw unsupportedProtocol(this, input.protocol);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 判据与 `reasoning()` 同源(`parseGpt` / `serverEfforts` / `chatToolsNeedNone`):
173
+ * 实际走 chat 而型号在 chat 下带不了工具(报错原文让改用 Responses)、非推理模型
174
+ * 标了推理(档位不会下发)、推理模型没标推理(档位不会下发,按默认档跑)。
175
+ */
176
+ override adviseModel(model: AdviceModelView): ModelAdvice[] {
177
+ const out: ModelAdvice[] = [];
178
+ const id = model.modelId;
179
+ if (
180
+ model.effectiveProtocol === 'openai-chat-completions' &&
181
+ chatToolsNeedNone(id)
182
+ ) {
183
+ out.push({
184
+ severity: 'warn',
185
+ message: serverEfforts(id)?.includes('none')
186
+ ? `「${id}」在 Chat Completions 下带工具时只能关闭推理(none),而 Agent 每轮都带工具,请改用 Responses。`
187
+ : `「${id}」在 Chat Completions 下不支持工具(需 none 档,而它没有),请改用 Responses。`,
188
+ });
189
+ }
190
+ const known = /^o\d+(?:-|$)/.test(id) || parseGpt(id) !== null;
191
+ const reasoningModel = serverEfforts(id) !== null;
192
+ const marked = model.capabilities.includes('reasoning');
193
+ if (known && !reasoningModel && marked) {
194
+ out.push({
195
+ severity: 'info',
196
+ message: `「${id}」不是推理模型,标记「推理」也不会下发推理档位。`,
197
+ });
198
+ }
199
+ if (known && reasoningModel && !marked) {
200
+ out.push({
201
+ severity: 'info',
202
+ message: `「${id}」是推理模型,未标记「推理」时推理档位不会下发,按模型默认档。`,
203
+ });
204
+ }
205
+ return out;
206
+ }
207
+
208
+ /**
209
+ * **档位是模型级的**(判据见 `serverEfforts`):同一个「关闭」,gpt-5.5 发
210
+ * `none`、gpt-5 只能发 `minimal`、gpt-6-astra 只能发 `low`;同一个「最高」,
211
+ * gpt-5.2 发 `xhigh`、gpt-6 走 responses 发 `max`。发了模型不认的档位,
212
+ * 服务端 400;gpt-6 系还有一层 SDK 自己的校验,不认的档位**静默丢掉**只打
213
+ * warning,模型按默认档跑——两种都不报到我们这边来,所以在这里按模型算好。
214
+ *
215
+ * 不是推理模型(`gpt-4.1`)时什么都不发:后台把它误标成 reasoning 也不该 400。
216
+ * 两种协议的 key 都是 `openai`;**不**带 `forceReasoning`:SDK 认得自家模型名,
217
+ * 强制会给非推理模型发 `reasoning` 而 400。
218
+ */
219
+ override reasoning(
220
+ modelId: string,
221
+ effort: ReasoningEffort,
222
+ protocol: LlmProtocol
223
+ ): ProviderOptions | undefined {
224
+ if (
225
+ protocol !== 'openai-chat-completions' &&
226
+ protocol !== 'openai-responses'
227
+ ) {
228
+ throw unsupportedProtocol(this, protocol);
229
+ }
230
+ const supported = effortsFor(modelId, protocol);
231
+ if (!supported) return undefined;
232
+ const sent = PREFERENCE[effort].find(e => supported.includes(e));
233
+ return sent ? { openai: { reasoningEffort: sent } } : undefined;
234
+ }
235
+ }