yingclaw 2.5.20 → 2.5.26

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/index.js CHANGED
@@ -4,5 +4,7 @@ module.exports = {
4
4
  ...require('./lib/desktop'),
5
5
  ...require('./lib/gateway'),
6
6
  ...require('./lib/install'),
7
+ ...require('./lib/openai'),
7
8
  ...require('./lib/panel'),
9
+ ...require('./lib/vscode'),
8
10
  };
package/lib/config.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const { spawnSync } = require('child_process');
5
+ const { normalizeOpenAiBaseUrl, openAiChatCompletionsUrl } = require('./openai');
5
6
 
6
7
  const CONFIG_FILE = path.join(os.homedir(), '.clawai.json');
7
8
  const WINDOWS_ENV_LABEL = 'Windows 用户环境变量';
@@ -15,10 +16,17 @@ const CLAUDE_ENV_KEYS = [
15
16
  'CLAUDE_CODE_SUBAGENT_MODEL',
16
17
  'CLAUDE_CODE_EFFORT_LEVEL',
17
18
  ];
19
+ const VSCODE_CLAUDE_ENV_KEYS = [
20
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION',
21
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME',
22
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION',
23
+ 'ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES',
24
+ 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY',
25
+ ];
18
26
  const LEGACY_CLAUDE_ENV_KEYS = [
19
27
  'ANTHROPIC_API_KEY',
20
28
  ];
21
- const CLEAR_CLAUDE_ENV_KEYS = [...new Set([...CLAUDE_ENV_KEYS, ...LEGACY_CLAUDE_ENV_KEYS])];
29
+ const CLEAR_CLAUDE_ENV_KEYS = [...new Set([...CLAUDE_ENV_KEYS, ...VSCODE_CLAUDE_ENV_KEYS, ...LEGACY_CLAUDE_ENV_KEYS])];
22
30
 
23
31
  const PROVIDERS = {
24
32
  deepseek: {
@@ -33,13 +41,27 @@ const PROVIDERS = {
33
41
  },
34
42
  kimi: {
35
43
  name: 'Kimi / Moonshot',
36
- baseUrl: 'https://api.moonshot.ai/anthropic',
37
- modelsUrl: 'https://api.moonshot.ai/v1/models',
44
+ protocol: 'openai',
45
+ baseUrl: 'https://api.moonshot.cn/v1',
46
+ modelsUrl: 'https://api.moonshot.cn/v1/models',
38
47
  fastModel: 'kimi-k2.5',
39
48
  models: [
40
49
  { name: 'Kimi K2.5(代码)', value: 'kimi-k2.5' },
41
50
  ],
42
51
  },
52
+ volcengine: {
53
+ name: '火山方舟 Coding Plan',
54
+ baseUrl: 'https://ark.cn-beijing.volces.com/api/coding',
55
+ modelsUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3/models',
56
+ fastModel: 'ark-code-latest',
57
+ models: [
58
+ { name: 'Ark Code Latest(控制台选择)', value: 'ark-code-latest' },
59
+ { name: 'Doubao Seed Code', value: 'doubao-seed-code' },
60
+ { name: 'GLM 4.7', value: 'glm-4.7' },
61
+ { name: 'DeepSeek V3.2', value: 'deepseek-v3.2' },
62
+ { name: 'Kimi K2.5', value: 'kimi-k2.5' },
63
+ ],
64
+ },
43
65
  qwen: {
44
66
  name: '阿里云百炼 (Qwen)',
45
67
  baseUrl: 'https://dashscope.aliyuncs.com/apps/anthropic',
@@ -87,9 +109,76 @@ const PROVIDERS = {
87
109
  { name: 'MiMo V2 Flash(快速)', value: 'mimo-v2-flash' },
88
110
  ],
89
111
  },
112
+ bai: {
113
+ name: 'B.AI',
114
+ baseUrl: 'https://api.b.ai',
115
+ modelsUrl: 'https://api.b.ai/v1/models',
116
+ fastModel: 'claude-haiku-4.5',
117
+ models: [
118
+ { name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4.6' },
119
+ { name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4.7' },
120
+ { name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4.5' },
121
+ { name: 'DeepSeek V4 Pro', value: 'deepseek-v4-pro' },
122
+ { name: 'GLM-5.1', value: 'glm-5.1' },
123
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
124
+ ],
125
+ },
126
+ opencode_zen: {
127
+ name: 'OpenCode Zen(按量)',
128
+ protocol: 'openai',
129
+ baseUrl: 'https://opencode.ai/zen',
130
+ modelsUrl: 'https://opencode.ai/zen/v1/models',
131
+ fastModel: 'deepseek-v4-flash-free',
132
+ models: [
133
+ { name: 'Claude Sonnet 4.6(推荐)', value: 'claude-sonnet-4-6' },
134
+ { name: 'Claude Opus 4.7(强力)', value: 'claude-opus-4-7' },
135
+ { name: 'Claude Haiku 4.5(快速)', value: 'claude-haiku-4-5' },
136
+ { name: 'GLM 5.1', value: 'glm-5.1' },
137
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
138
+ { name: 'Kimi K2.5', value: 'kimi-k2.5' },
139
+ { name: 'DeepSeek V4 Flash(免费)', value: 'deepseek-v4-flash-free' },
140
+ ],
141
+ },
142
+ opencode_go: {
143
+ name: 'OpenCode Go(订阅制)',
144
+ protocol: 'openai',
145
+ baseUrl: 'https://opencode.ai/zen/go',
146
+ modelsUrl: 'https://opencode.ai/zen/go/v1/models',
147
+ fastModel: 'deepseek-v4-flash',
148
+ models: [
149
+ { name: 'GLM-5.1', value: 'glm-5.1' },
150
+ { name: 'Kimi K2.5', value: 'kimi-k2.5' },
151
+ { name: 'DeepSeek V4 Pro(强力)', value: 'deepseek-v4-pro' },
152
+ { name: 'DeepSeek V4 Flash(快速)', value: 'deepseek-v4-flash' },
153
+ { name: 'MiniMax M2.7', value: 'minimax-m2.7' },
154
+ { name: 'MiniMax M2.5', value: 'minimax-m2.5' },
155
+ { name: 'MiMo V2.5 Pro', value: 'mimo-v2.5-pro' },
156
+ { name: 'Qwen3.6 Plus', value: 'qwen3.6-plus' },
157
+ ],
158
+ },
159
+ openrouter: {
160
+ name: 'OpenRouter',
161
+ protocol: 'openai',
162
+ baseUrl: 'https://openrouter.ai/api/v1',
163
+ modelsUrl: 'https://openrouter.ai/api/v1/models',
164
+ fastModel: 'deepseek/deepseek-chat-v3-0324',
165
+ models: [
166
+ { name: 'DeepSeek R1 0528(推理)', value: 'deepseek/deepseek-r1-0528' },
167
+ { name: 'DeepSeek Chat V3(快速)', value: 'deepseek/deepseek-chat-v3-0324' },
168
+ { name: 'Gemini 2.5 Pro(Google)', value: 'google/gemini-2.5-pro' },
169
+ { name: 'Claude Sonnet 4.5(Anthropic)', value: 'anthropic/claude-sonnet-4-5' },
170
+ ],
171
+ },
90
172
  custom: {
91
173
  name: '自定义 Anthropic 兼容接口',
92
174
  custom: true,
175
+ protocol: 'anthropic',
176
+ models: [],
177
+ },
178
+ custom_openai: {
179
+ name: '自定义 OpenAI 兼容接口',
180
+ custom: true,
181
+ protocol: 'openai',
93
182
  models: [],
94
183
  },
95
184
  };
@@ -99,6 +188,8 @@ const PREFERRED_MODEL_IDS = {
99
188
  minimax: ['MiniMax-M2.7', 'MiniMax-M2.7-Turbo', 'MiniMax-M2.5'],
100
189
  glm: ['GLM-4.7', 'GLM-5.1', 'GLM-5-Turbo', 'GLM-4.5-Air'],
101
190
  mimo: ['mimo-v2.5-pro', 'mimo-v2.5', 'mimo-v2-flash'],
191
+ opencode_zen: ['claude-sonnet-4-6', 'claude-opus-4-7', 'claude-haiku-4-5', 'deepseek-v4-flash-free'],
192
+ opencode_go: ['glm-5.1', 'deepseek-v4-pro', 'deepseek-v4-flash'],
102
193
  };
103
194
 
104
195
  function sortPreferredModelIds(providerKey, ids) {
@@ -110,11 +201,28 @@ function sortPreferredModelIds(providerKey, ids) {
110
201
  ];
111
202
  }
112
203
 
204
+ function isChatModelId(model) {
205
+ const value = String(model || '');
206
+ return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
207
+ && !/(^|[-_])(i2v|t2v|t2i|i2i|v2v|video-generation|image-generation)([-_]|$)/i.test(value)
208
+ && !/(cogview|cogvideo|wanx|^wan\d|seedance|seedream|seededit|vision|hyper3d|hitem3d|seed3d|flux|sdxl|stable-diffusion|dall-e|whisper)/i.test(value);
209
+ }
210
+
113
211
  function normalizeModelIds(providerKey, ids) {
114
212
  if (PREFERRED_MODEL_IDS[providerKey]) {
115
213
  return sortPreferredModelIds(providerKey, ids);
116
214
  }
117
215
 
216
+ if (providerKey === 'kimi') {
217
+ const preferred = PROVIDERS.kimi.models.map((item) => item.value);
218
+ const merged = [...new Set([...preferred, ...ids])];
219
+ return [
220
+ ...preferred.filter((id) => merged.includes(id)),
221
+ ...merged.filter((id) => !preferred.includes(id) && /^kimi-/i.test(id)),
222
+ ...merged.filter((id) => !preferred.includes(id) && !/^kimi-/i.test(id)),
223
+ ];
224
+ }
225
+
118
226
  if (providerKey !== 'deepseek') return ids;
119
227
 
120
228
  const mapped = ids.map((id) => {
@@ -132,7 +240,16 @@ function normalizeModelIds(providerKey, ids) {
132
240
  function parseModelIdsResponse(providerKey, data) {
133
241
  const parsed = typeof data === 'string' ? JSON.parse(data) : data;
134
242
  const list = parsed.data || parsed.models || [];
135
- const ids = list.map(m => m.id || m.model || m.name).filter(Boolean);
243
+ const availableList = providerKey === 'volcengine'
244
+ ? list.filter((model) => !/^shutdown$/i.test(String(model.status || '')))
245
+ : list;
246
+ const ids = availableList.map(m => m.id || m.model || m.name).filter(Boolean);
247
+ if (providerKey === 'volcengine') {
248
+ return normalizeModelIds(providerKey, ids.filter(isChatModelId));
249
+ }
250
+ if (providerKey === 'bai') {
251
+ return normalizeModelIds(providerKey, ids.filter(id => !id.includes('/')));
252
+ }
136
253
  return normalizeModelIds(providerKey, ids);
137
254
  }
138
255
 
@@ -155,7 +272,7 @@ function normalizeAnthropicBaseUrl(baseUrl) {
155
272
 
156
273
  function buildModelUrlCandidates(baseUrl) {
157
274
  let url;
158
- try { url = new URL(normalizeAnthropicBaseUrl(baseUrl)); } catch { return []; }
275
+ try { url = new URL(normalizeAnthropicBaseUrl(normalizeOpenAiBaseUrl(baseUrl))); } catch { return []; }
159
276
 
160
277
  const pathname = url.pathname.replace(/\/+$/, '');
161
278
  const candidates = [];
@@ -209,10 +326,7 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
209
326
  const res = await fetch(modelsUrl, {
210
327
  method: 'GET',
211
328
  signal: controller.signal,
212
- headers: {
213
- authorization: `Bearer ${apiKey}`,
214
- 'api-key': apiKey, // MiMo 用这个 header
215
- },
329
+ headers: buildProviderAuthHeaders(providerKey, apiKey),
216
330
  });
217
331
  if (!res.ok) return null;
218
332
 
@@ -225,9 +339,22 @@ async function fetchModels(providerKey, apiKey, modelsUrlOverride) {
225
339
  }
226
340
  }
227
341
 
228
- function loadConfig() {
342
+ function migrateConfig(config) {
343
+ if (!config || typeof config !== 'object') return config;
344
+ if (config.provider === 'kimi') {
345
+ return {
346
+ ...config,
347
+ protocol: 'openai',
348
+ baseUrl: PROVIDERS.kimi.baseUrl,
349
+ modelsUrl: PROVIDERS.kimi.modelsUrl,
350
+ };
351
+ }
352
+ return config;
353
+ }
354
+
355
+ function loadConfig(options = {}) {
229
356
  try {
230
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
357
+ return migrateConfig(JSON.parse(fs.readFileSync(options.configFile || CONFIG_FILE, 'utf8')));
231
358
  } catch {
232
359
  return null;
233
360
  }
@@ -259,16 +386,54 @@ function providerKeyFromBaseUrl(baseUrl) {
259
386
  return Object.entries(PROVIDERS).find(([, provider]) => provider.baseUrl === baseUrl)?.[0];
260
387
  }
261
388
 
389
+ function getProviderProtocol(config = {}) {
390
+ return config.protocol || PROVIDERS[config.provider]?.protocol || 'anthropic';
391
+ }
392
+
393
+ function buildProviderAuthHeaders(providerKey, apiKey) {
394
+ const headers = { authorization: `Bearer ${apiKey}` };
395
+ if (getProviderProtocol({ provider: providerKey }) !== 'openai') {
396
+ headers['api-key'] = apiKey; // MiMo 用这个 header
397
+ }
398
+ return headers;
399
+ }
400
+
262
401
  function resolveFastModel(provider, model) {
263
402
  if (/flash|turbo|haiku|air|lite/i.test(model)) return model;
264
403
  return provider?.fastModel || model;
265
404
  }
266
405
 
267
- function buildClaudeEnv({ provider, baseUrl, apiKey, model, fastModel }) {
406
+ function getModelFamily(model) {
407
+ const value = String(model || '').trim().toLowerCase();
408
+ if (!value) return '';
409
+ const normalized = value
410
+ .replace(/^anthropic\//, '')
411
+ .replace(/^\w+\//, '');
412
+ return normalized.split(/[-_.]/).filter(Boolean)[0] || normalized;
413
+ }
414
+
415
+ function sortRelatedModelIds(ids, mainModel) {
416
+ const unique = [...new Set(ids.filter(Boolean))];
417
+ const family = getModelFamily(mainModel);
418
+ return [
419
+ ...unique.filter((id) => id === mainModel),
420
+ ...unique.filter((id) => id !== mainModel && getModelFamily(id) === family),
421
+ ...unique.filter((id) => id !== mainModel && getModelFamily(id) !== family),
422
+ ];
423
+ }
424
+
425
+ function buildGatewayBaseUrlForEnv(config) {
426
+ const port = Number.parseInt(config.desktopGatewayPort || 18080, 10);
427
+ return `http://127.0.0.1:${port}/yingclaw`;
428
+ }
429
+
430
+ function buildClaudeEnv(config) {
431
+ const { provider, baseUrl, apiKey, model, fastModel } = config;
268
432
  const resolvedFastModel = fastModel || resolveFastModel(PROVIDERS[provider], model);
433
+ const useLocalGateway = getProviderProtocol(config) === 'openai' && config.desktopGatewayKey;
269
434
  return {
270
- ANTHROPIC_BASE_URL: baseUrl,
271
- ANTHROPIC_AUTH_TOKEN: apiKey,
435
+ ANTHROPIC_BASE_URL: useLocalGateway ? buildGatewayBaseUrlForEnv(config) : baseUrl,
436
+ ANTHROPIC_AUTH_TOKEN: useLocalGateway ? config.desktopGatewayKey : apiKey,
272
437
  ANTHROPIC_MODEL: model,
273
438
  ANTHROPIC_DEFAULT_OPUS_MODEL: model,
274
439
  ANTHROPIC_DEFAULT_SONNET_MODEL: model,
@@ -303,15 +468,19 @@ function runWindowsEnvCommands(commands, runner = spawnSync, { ignoreErrors = fa
303
468
 
304
469
  function classifyValidationStatus(statusCode) {
305
470
  if (statusCode >= 200 && statusCode < 300) return true;
471
+ if (statusCode === 429) return true; // 限流 = Key 有效
306
472
  if (statusCode === 401 || statusCode === 403) return false;
307
473
  return null;
308
474
  }
309
475
 
310
476
  // 发一次最小的 /v1/messages 请求验证 Key(true=有效, false=无效, null=网络/服务异常)
311
477
  async function validateKey(config, options = {}) {
478
+ const protocol = getProviderProtocol(config);
312
479
  let url;
313
480
  try {
314
- url = `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
481
+ url = protocol === 'openai'
482
+ ? openAiChatCompletionsUrl(config.baseUrl)
483
+ : `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
315
484
  new URL(url);
316
485
  } catch {
317
486
  return null;
@@ -325,14 +494,12 @@ async function validateKey(config, options = {}) {
325
494
  signal: controller.signal,
326
495
  headers: {
327
496
  'content-type': 'application/json',
328
- authorization: `Bearer ${config.apiKey}`,
329
- 'x-api-key': config.apiKey,
330
- 'api-key': config.apiKey,
331
- 'anthropic-version': '2023-06-01',
497
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
498
+ ...(protocol === 'anthropic' ? { 'x-api-key': config.apiKey, 'anthropic-version': '2023-06-01' } : {}),
332
499
  },
333
500
  body: JSON.stringify({
334
501
  model: config.model,
335
- max_tokens: 1,
502
+ max_tokens: 16,
336
503
  messages: [{ role: 'user', content: 'hi' }],
337
504
  }),
338
505
  });
@@ -465,6 +632,11 @@ module.exports = {
465
632
  buildModelUrlCandidates,
466
633
  fetchModelsFromBaseUrl,
467
634
  resolveFastModel,
635
+ getModelFamily,
636
+ sortRelatedModelIds,
637
+ isChatModelId,
638
+ buildProviderAuthHeaders,
639
+ getProviderProtocol,
468
640
  providerKeyFromBaseUrl,
469
641
  buildClaudeEnv,
470
642
  buildEnvBlock,
@@ -473,5 +645,6 @@ module.exports = {
473
645
  PROVIDERS,
474
646
  CONFIG_FILE,
475
647
  CLAUDE_ENV_KEYS,
648
+ VSCODE_CLAUDE_ENV_KEYS,
476
649
  CLEAR_CLAUDE_ENV_KEYS,
477
650
  };
package/lib/doctor.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  CLAUDE_ENV_KEYS,
13
13
  } = require('./config');
14
14
  const { isDesktopConfigured } = require('./desktop');
15
+ const { checkClaudeCodeSettingsEnv } = require('./vscode');
15
16
 
16
17
  const STATUS_OK = 'ok';
17
18
  const STATUS_FAIL = 'fail';
@@ -104,7 +105,16 @@ async function runDoctorChecks(options = {}) {
104
105
  : (platform === 'win32' ? '重新打开 PowerShell / CMD' : '运行 source ~/.zshrc 或重开终端'),
105
106
  });
106
107
 
107
- // 6. API Key(顺便确认网络可达)
108
+ // 6. VS Code / Claude Code 扩展共享设置
109
+ const vscodeSettings = checkClaudeCodeSettingsEnv(config, options);
110
+ checks.push({
111
+ name: 'VS Code Claude Code',
112
+ status: vscodeSettings.configured ? STATUS_OK : STATUS_WARN,
113
+ message: vscodeSettings.configured ? 'VS Code 扩展设置已配置' : `${vscodeSettings.missing.length} 项配置未写入 VS Code 扩展设置`,
114
+ fix: vscodeSettings.configured ? null : '运行 claw vscode,并重启 VS Code',
115
+ });
116
+
117
+ // 7. API Key(顺便确认网络可达)
108
118
  const valid = await validateKey(config, { timeoutMs: 6000 });
109
119
  if (valid === true) {
110
120
  checks.push({ name: 'API Key', status: STATUS_OK, message: '校验通过' });
@@ -124,7 +134,7 @@ async function runDoctorChecks(options = {}) {
124
134
  });
125
135
  }
126
136
 
127
- // 7. Claude 桌面应用接入状态
137
+ // 8. Claude 桌面应用接入状态
128
138
  const desktopConfigured = isDesktopConfigured();
129
139
  checks.push({
130
140
  name: 'Claude 桌面应用',
@@ -132,7 +142,7 @@ async function runDoctorChecks(options = {}) {
132
142
  message: desktopConfigured ? '已通过 yingclaw 接入' : '未接入(如需运行 claw desktop)',
133
143
  });
134
144
 
135
- // 8. DeepSeek 旧模型名提醒
145
+ // 9. DeepSeek 旧模型名提醒
136
146
  if (config.provider === 'deepseek' && (
137
147
  config.model === 'deepseek-v4-pro' ||
138
148
  config.model === 'deepseek-v4-flash' ||
package/lib/gateway.js CHANGED
@@ -1,6 +1,18 @@
1
1
  const crypto = require('crypto');
2
2
  const http = require('http');
3
- const { loadConfig: defaultLoadConfig, normalizeAnthropicBaseUrl } = require('./config');
3
+ const {
4
+ loadConfig: defaultLoadConfig,
5
+ normalizeAnthropicBaseUrl,
6
+ buildProviderAuthHeaders,
7
+ getProviderProtocol,
8
+ isChatModelId,
9
+ } = require('./config');
10
+ const {
11
+ anthropicToOpenAiChatRequest,
12
+ openAiChatCompletionsUrl,
13
+ openAiStreamChunkToAnthropicEvents,
14
+ openAiToAnthropicMessage,
15
+ } = require('./openai');
4
16
 
5
17
  const DEFAULT_DESKTOP_GATEWAY_PORT = 18080;
6
18
  const YINGCLAW_GATEWAY_PREFIX = '/yingclaw';
@@ -59,9 +71,26 @@ function desktopRouteLabel(routeId) {
59
71
  }
60
72
 
61
73
  function isDesktopChatModel(model) {
62
- const value = String(model || '');
63
- return !/(^|[-_])(tts|voice|voiceclone|voicedesign|speech|audio|image|video|embedding|embed|rerank|moderation)([-_]|$)/i.test(value)
64
- && !/(cogview|cogvideo|wanx|flux|sdxl|stable-diffusion)/i.test(value);
74
+ return isChatModelId(model);
75
+ }
76
+
77
+ function isNativeClaudeModel(modelId) {
78
+ return /^claude-/i.test(String(modelId || ''));
79
+ }
80
+
81
+ function stripThinkingFromBody(body) {
82
+ const cleaned = { ...body };
83
+ delete cleaned.thinking;
84
+ if (Array.isArray(cleaned.messages)) {
85
+ cleaned.messages = cleaned.messages.map((msg) => {
86
+ if (!Array.isArray(msg.content)) return msg;
87
+ const content = msg.content.filter(
88
+ (block) => block.type !== 'thinking' && block.type !== 'redacted_thinking',
89
+ );
90
+ return { ...msg, content: content.length > 0 ? content : [{ type: 'text', text: '' }] };
91
+ });
92
+ }
93
+ return cleaned;
65
94
  }
66
95
 
67
96
  function buildDesktopGatewayRoutes(config) {
@@ -93,6 +122,13 @@ function buildDesktopGatewayMappingRows(config) {
93
122
 
94
123
  function mapDesktopRouteToUpstream(config, routeId) {
95
124
  const requested = stripOneMContextSuffix(routeId);
125
+ const configuredModels = [
126
+ config.model,
127
+ config.fastModel || config.model,
128
+ ...(Array.isArray(config.availableModels) ? config.availableModels : []),
129
+ ].filter(Boolean);
130
+ if (configuredModels.includes(routeId)) return routeId;
131
+
96
132
  const route = buildDesktopGatewayRoutes(config).find((item) => stripOneMContextSuffix(item.id) === requested);
97
133
  if (!route) {
98
134
  throw new Error(`未配置的 Claude Desktop 模型路由: ${routeId}`);
@@ -147,6 +183,10 @@ function providerMessagesUrl(config) {
147
183
  return `${normalizeAnthropicBaseUrl(config.baseUrl)}/v1/messages`;
148
184
  }
149
185
 
186
+ function providerOpenAiChatUrl(config) {
187
+ return openAiChatCompletionsUrl(config.baseUrl);
188
+ }
189
+
150
190
  function buildDesktopGatewayUrl(config = {}) {
151
191
  const port = Number.parseInt(config.desktopGatewayPort || DEFAULT_DESKTOP_GATEWAY_PORT, 10);
152
192
  return {
@@ -203,14 +243,22 @@ async function proxyMessages(req, res, config) {
203
243
  return;
204
244
  }
205
245
 
246
+ if (!isNativeClaudeModel(body.model)) {
247
+ body = stripThinkingFromBody(body);
248
+ }
249
+
250
+ if (getProviderProtocol(config) === 'openai') {
251
+ await proxyOpenAiMessages(res, config, body);
252
+ return;
253
+ }
254
+
206
255
  const upstream = await fetch(providerMessagesUrl(config), {
207
256
  method: 'POST',
208
257
  headers: {
209
258
  'content-type': 'application/json',
210
259
  accept: body.stream ? 'text/event-stream' : 'application/json',
211
- authorization: `Bearer ${config.apiKey}`,
260
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
212
261
  'x-api-key': config.apiKey,
213
- 'api-key': config.apiKey,
214
262
  'anthropic-version': '2023-06-01',
215
263
  },
216
264
  body: JSON.stringify(body),
@@ -231,6 +279,89 @@ async function proxyMessages(req, res, config) {
231
279
  res.end();
232
280
  }
233
281
 
282
+ async function proxyOpenAiMessages(res, config, body) {
283
+ const openAiBody = anthropicToOpenAiChatRequest(body);
284
+ const upstream = await fetch(providerOpenAiChatUrl(config), {
285
+ method: 'POST',
286
+ headers: {
287
+ 'content-type': 'application/json',
288
+ accept: body.stream ? 'text/event-stream' : 'application/json',
289
+ ...buildProviderAuthHeaders(config.provider, config.apiKey),
290
+ },
291
+ body: JSON.stringify(openAiBody),
292
+ });
293
+
294
+ if (!body.stream) {
295
+ const text = await upstream.text();
296
+ if (!upstream.ok) {
297
+ res.writeHead(upstream.status, {
298
+ 'content-type': upstream.headers.get('content-type') || 'application/json',
299
+ });
300
+ res.end(text);
301
+ return;
302
+ }
303
+
304
+ let parsed;
305
+ try {
306
+ parsed = text ? JSON.parse(text) : {};
307
+ } catch {
308
+ sendJson(res, 502, { error: { message: 'OpenAI 兼容接口返回了无效 JSON' } });
309
+ return;
310
+ }
311
+ sendJson(res, upstream.status, openAiToAnthropicMessage(parsed, body.model));
312
+ return;
313
+ }
314
+
315
+ res.writeHead(upstream.status, {
316
+ 'content-type': upstream.ok ? 'text/event-stream' : (upstream.headers.get('content-type') || 'application/json'),
317
+ 'cache-control': 'no-cache',
318
+ });
319
+ if (!upstream.body) {
320
+ res.end();
321
+ return;
322
+ }
323
+ if (!upstream.ok) {
324
+ for await (const chunk of upstream.body) res.write(chunk);
325
+ res.end();
326
+ return;
327
+ }
328
+
329
+ const state = { model: body.model, started: false, finished: false };
330
+ let buffer = '';
331
+ const decoder = new TextDecoder();
332
+
333
+ function processFrames(frames) {
334
+ for (const frame of frames) {
335
+ const dataLine = frame.split(/\n/).find((line) => line.startsWith('data:'));
336
+ if (!dataLine) continue;
337
+ const payload = dataLine.slice('data:'.length).trim();
338
+ if (!payload || payload === '[DONE]') continue;
339
+ let parsed;
340
+ try {
341
+ parsed = JSON.parse(payload);
342
+ } catch {
343
+ continue;
344
+ }
345
+ res.write(openAiStreamChunkToAnthropicEvents(parsed, state));
346
+ }
347
+ }
348
+
349
+ for await (const chunk of upstream.body) {
350
+ buffer += decoder.decode(chunk, { stream: true });
351
+ const frames = buffer.split(/\n\n/);
352
+ buffer = frames.pop() || '';
353
+ processFrames(frames);
354
+ }
355
+ // flush any remaining buffer content not terminated by \n\n
356
+ if (buffer.trim()) {
357
+ processFrames([buffer]);
358
+ }
359
+ if (!state.finished) {
360
+ res.write(openAiStreamChunkToAnthropicEvents({ model: body.model, choices: [{ delta: {}, finish_reason: 'stop' }] }, state));
361
+ }
362
+ res.end();
363
+ }
364
+
234
365
  function createGatewayServer(options = {}) {
235
366
  const loadConfig = options.loadConfig || defaultLoadConfig;
236
367
  return http.createServer(async (req, res) => {