@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,174 @@
1
+ import type { ResponsesFetch } from './openai-responses.js';
2
+
3
+ /**
4
+ * 「推理正文走 `reasoning_text`」这一类 Responses 兼容端点的 HTTP 层垫片——把它们
5
+ * 与 OpenAI 线格式的**三处差异**在响应侧抹平,让 `@ai-sdk/openai` 的 responses
6
+ * 模型原样可用。首个使用者是 DeepSeek(本文件前身叫 `deepseek-responses-fetch.ts`),
7
+ * 2026-08-27 智谱探针抓到的事件序列与它**逐字段一致**,于是升成协议级共享件:
8
+ * 这不是 DeepSeek 的私有怪癖,而是「非 OpenAI 厂商实现 Responses」的通行做法——
9
+ * 它们回的是推理**正文**而 OpenAI 只回摘要,字段名自然不同。
10
+ *
11
+ * 差异(2026-08-27 对 `deepseek-v4-flash` / `glm-5.2` 真机探针,见 providers.md):
12
+ *
13
+ * 1. 推理**正文**以 reasoning item 的 `content: [{type:'reasoning_text'}]` 返回,
14
+ * `summary` 恒空——OpenAI 官方只回 `summary`(摘要),SDK 的 item schema
15
+ * 也只读 `summary`/`encrypted_content`(4.0.46 dist:3603、6790)。
16
+ * 2. 流式事件是 `response.reasoning_text.delta / .done`;SDK 的 chunk union 里
17
+ * **没有**这两个名字(0 命中,GitHub main 亦然),未知事件按 loose schema
18
+ * 静默丢弃——推理过程在对话页整段消失。
19
+ * 3. `encrypted_content` 是 `<response_id>-<n>` 形状的不透明串,文档说不支持、
20
+ * 实际会返回;对我们无害(SDK 原样塞进 providerMetadata)。
21
+ *
22
+ * **请求侧刻意不改写**。探针把六种回传形状(原样 item / 只带 encrypted_content /
23
+ * 假 encrypted_content / 假 id / 无 id / `item_reference`)全部打过一遍:全部
24
+ * 200,且 `input_tokens` 分毫不差(多轮 29、工具回环 387)——DeepSeek 根本不消费
25
+ * 发回去的 reasoning,SDK 默认(`store` 未设=true)发出的 `item_reference`
26
+ * 它也照收。没有可走私的东西,也就没有请求侧。
27
+ *
28
+ * 映射:`reasoning_text.delta` → `reasoning_summary_text.delta`(`summary_index`
29
+ * 恒 0——SDK 在 `output_item.added` 时已把第 0 段标成 active,多段 `content_index`
30
+ * 合并进它就够了,另开段要先发 `summary_part.added`,徒增状态机);
31
+ * `reasoning_text.done` → `reasoning_summary_part.done`;`output_item.added/done`、
32
+ * `response.completed/incomplete/failed` 与非流式响应体里的 reasoning item 一律
33
+ * `content` → `summary`。`content` 保留不删,SDK schema 会剥。
34
+ *
35
+ * 只认 OpenAI 公开的线格式,不碰 SDK 内部结构——SDK 升级不影响它。契约由
36
+ * `deepseek-responses.sdk-contract.spec.ts` 与 `zhipu.sdk-contract.spec.ts` 用真实
37
+ * SDK 钉住(后者回放的是带 `event:` 行的原始 SSE——智谱多发这一行,SDK 不看)。
38
+ */
39
+ export function withReasoningTextResponsesShim(
40
+ base?: ResponsesFetch
41
+ ): NonNullable<ResponsesFetch> {
42
+ return async (url, init) => {
43
+ // 延迟到调用时再取全局 fetch:测试用 vi.stubGlobal 换掉的才是这一个。
44
+ const res = await (base ?? globalThis.fetch)(url, init);
45
+ if (!res.ok || !res.body) return res;
46
+ const contentType = res.headers.get('content-type') ?? '';
47
+ if (contentType.includes('text/event-stream')) {
48
+ return new Response(res.body.pipeThrough(sseRewriter()), {
49
+ status: res.status,
50
+ statusText: res.statusText,
51
+ headers: withoutLength(res.headers),
52
+ });
53
+ }
54
+ if (contentType.includes('application/json')) {
55
+ const body = (await res.json()) as Record<string, unknown>;
56
+ rewriteResponseObject(body);
57
+ return new Response(JSON.stringify(body), {
58
+ status: res.status,
59
+ statusText: res.statusText,
60
+ headers: withoutLength(res.headers),
61
+ });
62
+ }
63
+ return res;
64
+ };
65
+ }
66
+
67
+ /** 改写过的 body 长度变了,且已解压;带着原头会让下游按错的长度截断。 */
68
+ export function withoutLength(headers: Headers): Headers {
69
+ const out = new Headers(headers);
70
+ out.delete('content-length');
71
+ out.delete('content-encoding');
72
+ return out;
73
+ }
74
+
75
+ type JsonObject = Record<string, unknown>;
76
+
77
+ function isObject(v: unknown): v is JsonObject {
78
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
79
+ }
80
+
81
+ /**
82
+ * reasoning item:`content[].reasoning_text` → `summary[].summary_text`。
83
+ * 已有非空 `summary` 就不动(那是真 OpenAI 形状,别覆盖)。
84
+ */
85
+ export function rewriteReasoningItem(item: unknown): void {
86
+ if (!isObject(item) || item.type !== 'reasoning') return;
87
+ if (Array.isArray(item.summary) && item.summary.length > 0) return;
88
+ const content = Array.isArray(item.content) ? item.content : [];
89
+ item.summary = content.flatMap(part =>
90
+ isObject(part) &&
91
+ part.type === 'reasoning_text' &&
92
+ typeof part.text === 'string'
93
+ ? [{ type: 'summary_text', text: part.text }]
94
+ : []
95
+ );
96
+ }
97
+
98
+ /** 完整的 response 对象(非流式 body,或 `response.completed` 事件里的那份)。 */
99
+ export function rewriteResponseObject(body: unknown): void {
100
+ if (!isObject(body) || !Array.isArray(body.output)) return;
101
+ for (const item of body.output) rewriteReasoningItem(item);
102
+ }
103
+
104
+ /** 一条 SSE 事件对象。返回改写后的对象(可能是同一引用)。 */
105
+ export function rewriteEvent(event: unknown): unknown {
106
+ if (!isObject(event) || typeof event.type !== 'string') return event;
107
+ switch (event.type) {
108
+ case 'response.reasoning_text.delta':
109
+ return {
110
+ type: 'response.reasoning_summary_text.delta',
111
+ item_id: event.item_id,
112
+ output_index: event.output_index,
113
+ summary_index: 0,
114
+ delta: event.delta,
115
+ sequence_number: event.sequence_number,
116
+ };
117
+ case 'response.reasoning_text.done':
118
+ return {
119
+ type: 'response.reasoning_summary_part.done',
120
+ item_id: event.item_id,
121
+ output_index: event.output_index,
122
+ summary_index: 0,
123
+ sequence_number: event.sequence_number,
124
+ };
125
+ case 'response.output_item.added':
126
+ case 'response.output_item.done':
127
+ rewriteReasoningItem(event.item);
128
+ return event;
129
+ case 'response.completed':
130
+ case 'response.incomplete':
131
+ case 'response.failed':
132
+ rewriteResponseObject(event.response);
133
+ return event;
134
+ default:
135
+ return event;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 按行处理 SSE:只动 `data:` 行且只在它是合法 JSON 时改写,其余字节原样透传。
141
+ * 半行留在缓冲里等下一块,flush 时把尾巴吐掉。
142
+ */
143
+ function sseRewriter(): TransformStream<Uint8Array, Uint8Array> {
144
+ const decoder = new TextDecoder();
145
+ const encoder = new TextEncoder();
146
+ let buffer = '';
147
+ const rewriteLine = (line: string): string => {
148
+ if (!line.startsWith('data:')) return line;
149
+ const payload = line.slice(5).trim();
150
+ if (!payload || payload === '[DONE]') return line;
151
+ let parsed: unknown;
152
+ try {
153
+ parsed = JSON.parse(payload);
154
+ } catch {
155
+ return line;
156
+ }
157
+ return `data: ${JSON.stringify(rewriteEvent(parsed))}`;
158
+ };
159
+ return new TransformStream({
160
+ transform(chunk, controller) {
161
+ buffer += decoder.decode(chunk, { stream: true });
162
+ const lines = buffer.split('\n');
163
+ buffer = lines.pop() ?? '';
164
+ if (lines.length === 0) return;
165
+ controller.enqueue(
166
+ encoder.encode(lines.map(rewriteLine).join('\n') + '\n')
167
+ );
168
+ },
169
+ flush(controller) {
170
+ buffer += decoder.decode();
171
+ if (buffer) controller.enqueue(encoder.encode(rewriteLine(buffer)));
172
+ },
173
+ });
174
+ }
@@ -0,0 +1,294 @@
1
+ import type { JSONValue, LanguageModel } from 'ai';
2
+
3
+ import { ProtocolUnavailableError } from '../errors.js';
4
+ import type {
5
+ LlmCapability,
6
+ LlmProtocol,
7
+ ReasoningEffort,
8
+ VendorKind,
9
+ } from '../types.js';
10
+
11
+ /**
12
+ * providerOptions 的形状是 `Record<string, JSONObject>`(@ai-sdk/provider 的
13
+ * `SharedV3ProviderOptions`,`ai` 包本身不导出这个类型名,只导出组成它的
14
+ * `JSONValue`——故本地重建一个结构等价的别名)。
15
+ */
16
+ export type ProviderOptions = Record<string, Record<string, JSONValue>>;
17
+
18
+ /**
19
+ * 采样参数支持度。**默认全支持,禁止的家覆盖**——Kimi 全系禁止 temperature/top_p,
20
+ * 传参直接返回参数非法(见 `referenes/kimi-request.md` 的避坑提示第 1 条),
21
+ * 而 AI SDK 不会替我们挡(实测 `generateText({temperature})` 原样发出)。
22
+ */
23
+ export interface SamplingSupport {
24
+ temperature: boolean;
25
+ topP: boolean;
26
+ }
27
+
28
+ /** 调用方给的采样参数;`filterSampling` 按支持度剥。 */
29
+ export interface SamplingParams {
30
+ temperature?: number;
31
+ topP?: number;
32
+ }
33
+
34
+ /**
35
+ * 档位的 SSOT 在 `types.ts`(`REASONING_EFFORTS`)——这里只 re-export,
36
+ * 让各子类从同一处 import。**它是严格 union 而非 `string`**:加一个档位时,
37
+ * 凡是对档位做穷举分支的家会在编译期报错,而不是在真机上撞 vendor 的枚举校验。
38
+ */
39
+ export type { ReasoningEffort };
40
+
41
+ /** `createModel` 的入参:一条已选定的 (协议, 端点) + 凭证 + 厂商侧模型名。 */
42
+ export interface CreateModelInput {
43
+ protocol: LlmProtocol;
44
+ baseUrl: string;
45
+ apiKey: string;
46
+ modelId: string;
47
+ /**
48
+ * 出站 fetch:中心模式下是 `createCenterFetch` 造的(改写到中心、换鉴权头),直连模式下是调用方给的
49
+ * (网络代理)或省略 = 全局 fetch。**这家的每一个出站请求都要用它**:SDK 工厂的 `fetch` 选项、responses 垫片的
50
+ * base、自己手写的 `fetch`。漏一处不报错,只是那一处悄悄直连——在国内表现成
51
+ * 超时。`vendors.spec` 逐家逐协议钉着句柄确实用了它。
52
+ */
53
+ fetch?: VendorFetch;
54
+ }
55
+
56
+ /** 厂商出站请求用的 fetch;与全局 `fetch` 同签名。 */
57
+ export type VendorFetch = typeof globalThis.fetch;
58
+
59
+ /**
60
+ * 厂商 Files API 的最小面:传一份字节上去、拿回它在厂商侧的 id。
61
+ *
62
+ * `referenceKey` 是 `FilePart.data.reference` 里的命名空间 key,**跟承载包走不跟
63
+ * 厂商走**(同 `reasoning()` 的 `optionsKey`):DeepSeek 的 chat-completions 由
64
+ * `@ai-sdk/deepseek` 承载、key 是 `deepseek`;responses 由 `@ai-sdk/openai` 承载、
65
+ * key 是 `openai`。给错不报错——包解析引用时找不到自己的 key 才抛,那时错误信息
66
+ * 只说「missing reference」。
67
+ */
68
+ export interface VendorFileStore {
69
+ referenceKey: string;
70
+ upload(input: {
71
+ data: Uint8Array;
72
+ mediaType: string;
73
+ filename?: string;
74
+ /** 省略 = 厂商默认(DeepSeek 是永久)。 */
75
+ ttlSeconds?: number;
76
+ }): Promise<{ fileId: string; expiresAt: Date | null }>;
77
+ /** 尽力而为的删除;缓存失效时顺手清厂商侧副本。 */
78
+ delete(fileId: string): Promise<void>;
79
+ }
80
+
81
+ /** `chooseEndpoint` 看的模型字段:只要协议声明,签名上刻意不带「这一轮要什么」。 */
82
+ export interface EndpointModelView {
83
+ modelId: string;
84
+ protocols: string[];
85
+ }
86
+
87
+ /**
88
+ * `adviseModel` 看的模型字段。`effectiveProtocol` 是解析链此刻会选的协议
89
+ * (`chooseEndpoint` 对同一批端点算出来的;调不通时为 null)——提醒要按「实际会
90
+ * 走哪条」说话,而不是按声明的第一个。
91
+ */
92
+ export interface AdviceModelView {
93
+ modelId: string;
94
+ protocols: readonly LlmProtocol[];
95
+ capabilities: readonly LlmCapability[];
96
+ effectiveProtocol: LlmProtocol | null;
97
+ }
98
+
99
+ /** `chooseEndpoint` 看的端点字段。 */
100
+ export interface EndpointView {
101
+ protocol: string;
102
+ baseUrl: string;
103
+ /** 省略 = 启用(中心目录里只有启用的端点)。 */
104
+ isActive?: boolean;
105
+ }
106
+
107
+ /** 配置提醒:`warn` = 照这样配会出错或静默降质,`info` = 值得知道。 */
108
+ export interface ModelAdvice {
109
+ severity: 'warn' | 'info';
110
+ message: string;
111
+ }
112
+
113
+ /**
114
+ * 一家 LLM **厂商**的全部差异都收在它自己的子类里。
115
+ *
116
+ * 前身叫 `LlmProtocolAdapter`,但它的 `protocol` 字段装的一直是厂商标识——真正的
117
+ * 线协议(openai-chat-completions / openai-responses / anthropic-messages)是另一个
118
+ * 维度:一家厂商可以同时开多个协议端点(`createDeepSeek()(id)` 走 chat-completions、
119
+ * `createOpenAI({baseURL}).responses(id)` 走 responses,两者都是 DeepSeek 一家的事)。
120
+ * `kind` 对应中心的 `vendor.kind`。
121
+ *
122
+ * 从 agent-base 搬来时去掉了中心那一侧的事:显示名、接入提示、推荐模型、默认端点(在 `@pc/shared` 的
123
+ * `VENDOR_KIND_INFOS`)与余额 / 目录接口(在中心 api)。适配器只管「怎么造句柄、怎么翻译推理与采样」。
124
+ *
125
+ * 换掉早先那两个"协议无关"纯函数的理由不是代码整洁,是那个形状**在事实层面站不住**:
126
+ *
127
+ * - "接不接受采样参数"是各家自己的规矩(Kimi 禁止 temperature/top_p,DeepSeek 可调)。
128
+ * - "怎么开关推理"甚至是**模型级**的:`kimi-k3` 不支持 `thinking` 参数、传入报错,
129
+ * 只认顶层 `reasoning_effort`;而 `kimi-k2.x` 恰好相反。DeepSeek 各版本对档位的
130
+ * 支持度也不同(v4-pro 仅 high/max,low 自动映射 high)。
131
+ *
132
+ * 所以 `reasoning()` 收 modelId——家内差异真实存在,判据放在各家自己的子类里,
133
+ * 模型名变了只改那一个文件。**不**给 `llm_model` 加一列让运营选"推理控制方式":
134
+ * 那是个技术概念,运营答不上来,答错了还是静默出错。
135
+ *
136
+ * 加一家厂商 = 新增一个子类 + 进 `VENDOR_ADAPTERS` 数组 + 中心 `VENDOR_KIND_INFOS` 加一条
137
+ * (`vendors.spec` 钉着两边的 kind 与 `supportedProtocols` 一致)。
138
+ */
139
+ export abstract class LlmVendorAdapter {
140
+ abstract readonly kind: VendorKind;
141
+
142
+ /**
143
+ * 本适配器**已实现**的协议分支。取值域是 `LLM_PROTOCOLS`(外部事实,三种全占),
144
+ * 但实现是我们的活:模型声明了、厂商端点也有、而这里没写分支的协议,
145
+ * `chooseEndpoint` 会跳过它而不是让 `createModel` 在造句柄时炸。
146
+ */
147
+ readonly supportedProtocols: readonly LlmProtocol[] = [
148
+ 'openai-chat-completions',
149
+ ];
150
+
151
+ /**
152
+ * 按型号的配置提醒(模型表单实时显示、详情页体检汇总)。默认没有;子类按自己
153
+ * 探出来的坑写——判据与 `reasoning()` / `chooseEndpoint` 住在同一个文件里,
154
+ * 改一边就看得见另一边。
155
+ */
156
+ adviseModel(_model: AdviceModelView): ModelAdvice[] {
157
+ return [];
158
+ }
159
+
160
+ /** 造 AI SDK 句柄。 */
161
+ abstract createModel(input: CreateModelInput): LanguageModel;
162
+
163
+ /**
164
+ * 这家厂商在该 (协议, 端点, 凭证) 下的 Files API。**默认 null = 没有**,图片
165
+ * 递送层据此退到下一档(公网 URL / base64)。「有没有」由子类有没有覆盖决定,
166
+ * 不另加布尔字段让两处声明漂移。
167
+ */
168
+ createFileStore(_input: CreateModelInput): VendorFileStore | null {
169
+ return null;
170
+ }
171
+
172
+ readonly sampling: SamplingSupport = { temperature: true, topP: true };
173
+
174
+ /**
175
+ * 工具调用的 `providerMetadata` 里要随 part 落库、重放时原样发回的命名空间。
176
+ * **默认空 = 一个字节都不存**:别家的 metadata 原样回放会改变请求形状(openai
177
+ * responses 的 `itemId` 会让 SDK 发 `item_reference`)。只有「不带回去下一步就
178
+ * 400 / 降质」的家才声明(google 的思考签名)。
179
+ */
180
+ readonly replayedMetadataKeys: readonly string[] = [];
181
+
182
+ /**
183
+ * 从该模型支持的协议与厂商的可用端点里选一个。默认实现:按 `model.protocols`
184
+ * 的顺序取第一个「厂商有活跃端点 且 本适配器已实现」的协议,所以那个数组的
185
+ * 顺序有语义。各厂商真要接第二个协议时在子类里定自己的策略。
186
+ *
187
+ * **入参刻意只有 model 与 endpoints,没有「这一轮要什么能力」**——同一个会话里
188
+ * 协议必须稳定:按轮次切协议会让请求体形状完全不同(前缀缓存全线 miss,DeepSeek
189
+ * 命中与未命中差 30 倍),reasoning 的还原路径也不一致。签名上就堵死那种写法。
190
+ *
191
+ * 交集为空明确报 422,不静默退回某个协议。
192
+ */
193
+ chooseEndpoint(
194
+ model: EndpointModelView,
195
+ endpoints: readonly EndpointView[]
196
+ ): { protocol: LlmProtocol; baseUrl: string } {
197
+ for (const protocol of model.protocols) {
198
+ if (!this.supportedProtocols.includes(protocol as LlmProtocol)) continue;
199
+ const hit = endpoints.find(
200
+ e => e.isActive !== false && e.protocol === protocol
201
+ );
202
+ if (hit)
203
+ return { protocol: protocol as LlmProtocol, baseUrl: hit.baseUrl };
204
+ }
205
+ throw new ProtocolUnavailableError(
206
+ model.modelId,
207
+ model.protocols,
208
+ this.kind
209
+ );
210
+ }
211
+
212
+ /**
213
+ * providerOptions 的命名空间 key。默认取 kind——`@ai-sdk/deepseek`、
214
+ * `@ai-sdk/moonshotai`、`@ai-sdk/alibaba` 都由包内 provider 名首段
215
+ * (`moonshotai.chat` / `alibaba.chat` …)写死,恰好等于我们的 kind 值。
216
+ * 唯一的例外是 generic:SDK 3.x 把 openai-compatible 的 key 固定成驼峰
217
+ * `openaiCompatible`,故那个子类覆盖此 getter。**写错不会报错,只是推理开关
218
+ * 静默不下发。**
219
+ */
220
+ protected get optionsKey(): string {
221
+ return this.kind;
222
+ }
223
+
224
+ /**
225
+ * 推理控制。默认实现=在自己的命名空间下发 `reasoningEffort` 档位字符串,
226
+ * `off` 不发任何东西(没有统一的关闭语义可发明)。
227
+ *
228
+ * 覆盖它的家要记住一条:**`off` 要发的是真正的关闭指令,而不是什么都不发**
229
+ * ——"不发"会让 provider 落到自己的默认值,而 deepseek 默认开着推理
230
+ * (已用真实 API 实测,见 `.superpowers/sdd/fix-reasoning-off-report.md`)。
231
+ * 只有当该家**根本没有关闭开关**时(如 kimi-k3),不发才是如实反映。
232
+ *
233
+ * **`protocol` 是必填的**,因为 providerOptions 的命名空间 key 跟着协议走、
234
+ * 不跟着厂商走:同一家 DeepSeek,chat-completions 由 `@ai-sdk/deepseek` 承载、
235
+ * key 是 `deepseek`;responses 由 `@ai-sdk/openai` 承载、key 是 `openai`。
236
+ * 不知道协议就选不对 key,而选错 key 不报错、只是档位静默不下发。resolver
237
+ * 用 `chooseEndpoint` 算出协议,造句柄与翻译推理用的是同一个结果。
238
+ */
239
+ reasoning(
240
+ _modelId: string,
241
+ effort: ReasoningEffort,
242
+ _protocol: LlmProtocol
243
+ ): ProviderOptions | undefined {
244
+ if (effort === 'off') return undefined;
245
+ return { [this.optionsKey]: { reasoningEffort: effort } };
246
+ }
247
+
248
+ /**
249
+ * 按本家支持度剥掉不能发的采样参数。返回的对象直接展开进 `generateText`/
250
+ * `generateObject` 的入参。
251
+ *
252
+ * 剥掉时**不报错**——调用方很可能只是换了模型,让整个节点失败太重;由调用方
253
+ * 记一条 warn(它知道自己是谁、在跑哪个节点)。
254
+ */
255
+ filterSampling(params: SamplingParams): {
256
+ accepted: SamplingParams;
257
+ dropped: (keyof SamplingParams)[];
258
+ } {
259
+ const accepted: SamplingParams = {};
260
+ const dropped: (keyof SamplingParams)[] = [];
261
+ for (const key of ['temperature', 'topP'] as const) {
262
+ const value = params[key];
263
+ if (value === undefined) continue;
264
+ if (this.sampling[key]) accepted[key] = value;
265
+ else dropped.push(key);
266
+ }
267
+ return { accepted, dropped };
268
+ }
269
+ }
270
+
271
+ /**
272
+ * 没实现某协议分支的家共用的守卫:`chooseEndpoint` 已经把别的协议筛掉,
273
+ * 这里再钉一道是为了让「有人绕过 chooseEndpoint 直接调 createModel」响亮失败,
274
+ * 而不是把 responses 的 baseUrl 喂给 chat-completions 的包。放在 `createModel`
275
+ * 的 `switch` 的 `default` 分支里;`reasoning()` 里也一样用它兜底。
276
+ */
277
+ export function unsupportedProtocol(
278
+ adapter: LlmVendorAdapter,
279
+ protocol: LlmProtocol
280
+ ): Error {
281
+ return new Error(
282
+ `vendor adapter "${adapter.kind}" has no ${protocol} implementation`
283
+ );
284
+ }
285
+
286
+ /** 只实现了 chat-completions 的家在 `createModel` 开头调一下即可。 */
287
+ export function assertChatCompletions(
288
+ adapter: LlmVendorAdapter,
289
+ protocol: LlmProtocol
290
+ ): void {
291
+ if (protocol !== 'openai-chat-completions') {
292
+ throw unsupportedProtocol(adapter, protocol);
293
+ }
294
+ }
@@ -0,0 +1,166 @@
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 { withArkResponsesShim } from './ark-responses-shim.js';
6
+ import {
7
+ COMPATIBLE_RESPONSES_OPTIONS,
8
+ createResponsesModel,
9
+ responsesDefaultsMiddleware,
10
+ } from './openai-responses.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(`volcengine.chat`)与 providerOptions 的命名空间
21
+ * key(单词无连字符,`name` 与 `toCamelCase(name)` 相同),恰好等于 `kind`,
22
+ * 基类默认的 `optionsKey` 直接可用。
23
+ */
24
+ const FACTORY_NAME = 'volcengine';
25
+
26
+ /**
27
+ * responses 协议下每次调用都必须带上的两项 providerOptions——不带就是 400,
28
+ * 且**与有没有推理无关**,所以不能只放在 `reasoning()` 里(没标 `reasoning`
29
+ * 能力的模型行根本不会被问推理选项),而是由 `createModel` 挂的中间件兜底:
30
+ *
31
+ * - **`store: false`**:SDK 在 `store` 为 true(默认)时把上一轮带 itemId 的
32
+ * assistant 正文 / reasoning 发成 `{type:'item_reference', id}`,方舟对它回
33
+ * 400「The parameter `input` … not valid: `<nil>`」——多步工具回环第二步必炸。
34
+ * `store:false` 让 SDK 改发完整 item;探针里 `store:false` 本身方舟 200。
35
+ * - **`reasoningSummary: null`**:SDK 对推理模型默认附带 `reasoning.summary:
36
+ * 'detailed'`,方舟回 400「json: unknown field "summary"」。传 null 让 SDK
37
+ * 整个不发这个键(4.0.46 dist:6491 `!== undefined` 判断)。
38
+ *
39
+ * 调用方显式给的同名选项优先(spread 在后);挂法见 `responsesDefaultsMiddleware`。
40
+ */
41
+ export const ARK_RESPONSES_DEFAULTS = {
42
+ store: false,
43
+ reasoningSummary: null,
44
+ } as const;
45
+
46
+ /**
47
+ * 火山引擎方舟(Ark,豆包 Doubao-Seed 系)。
48
+ *
49
+ * **AI SDK 没有它的语言模型 provider**:`@ai-sdk/bytedance`(2.0.39,2026-09-02)
50
+ * 只做 Seedream 图像 / Seedance 视频,`languageModel()` 直接抛 `NoSuchModelError`;
51
+ * npm 也没有别的官方或社区包。承载包与智谱同一套:chat-completions 走
52
+ * `@ai-sdk/openai-compatible`(私有的 `thinking` 靠它把未知键展开进请求体,
53
+ * `reasoning_content` 是它原生读的字段),responses 走 `@ai-sdk/openai`。
54
+ *
55
+ * 全部判据来自 2026-09-07 对 `doubao-seed-2-1-pro-260628` / `doubao-seed-2-0-lite-260428`
56
+ * 的真 key 探针(方舟文档站是纯客户端渲染、抓不到正文;类型定义核对的是
57
+ * `volcengine-python-sdk` 的 `volcenginesdkarkruntime`):
58
+ *
59
+ * - 推理开关是**模型无关**的两个参数:`thinking:{type:'enabled'|'disabled'}`
60
+ * (`auto` 在 Seed 2.x 上 400「Unsupported thinking type」,永远不发)与
61
+ * `reasoning_effort: minimal|low|medium|high`(`minimal` = 不思考;`xhigh`/`max`
62
+ * 也 200 但不在 SDK 类型里,不赌)。**默认开着推理**(不发任何参数时
63
+ * `reasoning_tokens` 1154),「不发 ≠ 关闭」在这家同样成立。
64
+ * `thinking:disabled` 与非 minimal 的 effort 同发 → 400「Invalid combination」。
65
+ * 不按 modelId 分支——方舟的模型 id 可能是接入点 `ep-…`,按名字判什么都判不出来。
66
+ * - 响应侧:非流式 `message.reasoning_content` / 流式 `delta.reasoning_content`,
67
+ * 另有一个 `encrypted_content`(SDK 忽略)。工具回环不要求回传推理;SDK
68
+ * (openai-compatible)会把 `reasoning_content` 发回去,方舟收并计费。
69
+ * - `temperature` 范围 `[0, 2]`(2.5 → 400),`top_p` 收。
70
+ * - `GET /models` 存在,但返回的是**全平台目录**(含 `status: Shutdown` 的死
71
+ * 模型),不是账号已开通清单;账号未开通的模型调用回 404 `ModelNotOpen`。
72
+ * - 余额接口 `QueryBalanceAcct` 走 `open.volcengineapi.com` 的 AK/SK 签名体系,
73
+ * 不是 Ark API key,中心的凭证模型做不了。
74
+ *
75
+ * 只关注文本模型;Seedream / Seedance 归 media-provider,不在这里。
76
+ */
77
+ export class VolcengineAdapter extends LlmVendorAdapter {
78
+ readonly kind = 'volcengine' as const;
79
+
80
+ override readonly supportedProtocols: readonly LlmProtocol[] = [
81
+ 'openai-chat-completions',
82
+ 'openai-responses',
83
+ ];
84
+
85
+ createModel(input: CreateModelInput): LanguageModel {
86
+ switch (input.protocol) {
87
+ case 'openai-chat-completions': {
88
+ const { baseUrl, apiKey, modelId, fetch } = input;
89
+ return createOpenAICompatible({
90
+ name: FACTORY_NAME,
91
+ baseURL: baseUrl,
92
+ apiKey,
93
+ fetch,
94
+ })(modelId);
95
+ }
96
+ case 'openai-responses':
97
+ return wrapLanguageModel({
98
+ model: createResponsesModel(
99
+ input,
100
+ withArkResponsesShim(input.fetch)
101
+ ) as Exclude<LanguageModel, string>,
102
+ middleware: responsesDefaultsMiddleware(ARK_RESPONSES_DEFAULTS),
103
+ });
104
+ default:
105
+ throw unsupportedProtocol(this, input.protocol);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * **chat-completions**:`off` 发显式的 `thinking:{type:'disabled'}`(探针
111
+ * `reasoning_tokens: 0`;它默认开着推理);非 off 发 `thinking:enabled` +
112
+ * `reasoning_effort`。我们的 `minimal` 映到它的 `low`——方舟的 `minimal`
113
+ * 是「不思考」,与我们「最少思考」语义不同;`max` 映到 `high`(它文档化的
114
+ * 最高档)。
115
+ *
116
+ * **responses**:只发 `reasoning.effort`,`off`→`none`(探针 `reasoning_tokens:
117
+ * 0`、不出 reasoning item,是真正的关闭指令);`minimal`/`max` 同上映射。
118
+ * key 随承载包换成 `openai`,带 `COMPATIBLE_RESPONSES_OPTIONS`(不带则 SDK
119
+ * 按模型名判成非推理模型、`reasoning` 整个不发,而它默认开着推理;system 与
120
+ * developer 角色实测都收,只保证 `system`)。`store:false` / `reasoningSummary:null`
121
+ * 由中间件兜底,这里不重复。
122
+ */
123
+ override reasoning(
124
+ _modelId: string,
125
+ effort: ReasoningEffort,
126
+ protocol: LlmProtocol
127
+ ): ProviderOptions {
128
+ switch (protocol) {
129
+ case 'openai-chat-completions':
130
+ if (effort === 'off') {
131
+ return { [this.optionsKey]: { thinking: { type: 'disabled' } } };
132
+ }
133
+ return {
134
+ [this.optionsKey]: {
135
+ thinking: { type: 'enabled' },
136
+ reasoningEffort: toArkEffort(effort),
137
+ },
138
+ };
139
+ case 'openai-responses':
140
+ return {
141
+ openai: {
142
+ reasoningEffort: effort === 'off' ? 'none' : toArkEffort(effort),
143
+ ...COMPATIBLE_RESPONSES_OPTIONS,
144
+ },
145
+ };
146
+ default:
147
+ throw unsupportedProtocol(this, protocol);
148
+ }
149
+ }
150
+ }
151
+
152
+ /** 我们的档位 → 方舟文档化的三档 `low/medium/high`。 */
153
+ function toArkEffort(
154
+ effort: Exclude<ReasoningEffort, 'off'>
155
+ ): 'low' | 'medium' | 'high' {
156
+ switch (effort) {
157
+ case 'minimal':
158
+ case 'low':
159
+ return 'low';
160
+ case 'medium':
161
+ return 'medium';
162
+ case 'high':
163
+ case 'max':
164
+ return 'high';
165
+ }
166
+ }