aicodeswitch 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,7 +66,7 @@ const compact_1 = require("./conversions/compact");
66
66
  const coding_plan_1 = require("./coding-plan");
67
67
  const coding_plan_headers_1 = require("./coding-plan-headers");
68
68
  const auth_1 = require("./auth");
69
- const SUPPORTED_TARGETS = ['claude-code', 'codex'];
69
+ const SUPPORTED_TARGETS = ['claude-code', 'codex', 'opencode'];
70
70
  /**
71
71
  * Fallback(回退原始配置)路径的虚拟供应商归属。
72
72
  * 该路径转发的请求不属于任何用户配置的供应商/服务,故用一组固定 ID + 名称
@@ -129,6 +129,19 @@ function apiPathToClientFormat(apiPath) {
129
129
  case '/v1/models': return null;
130
130
  }
131
131
  }
132
+ /**
133
+ * 根据客户端工具类型推断其原生 API 格式(Format)。
134
+ * - claude-code → claude(Anthropic Messages)
135
+ * - codex → responses(OpenAI Responses)
136
+ * - opencode → completions(OpenAI Chat Completions,经由 @ai-sdk/openai-compatible)
137
+ */
138
+ function clientFormatForTool(tool) {
139
+ if (tool === 'codex')
140
+ return 'responses';
141
+ if (tool === 'opencode')
142
+ return 'completions';
143
+ return 'claude';
144
+ }
132
145
  class ClaudeCompactResponseSanitizer extends stream_1.Transform {
133
146
  constructor() {
134
147
  super({ objectMode: true });
@@ -379,6 +392,9 @@ class ProxyServer {
379
392
  if (path === '/codex' || path.startsWith('/codex/')) {
380
393
  return 'codex';
381
394
  }
395
+ if (path === '/opencode' || path.startsWith('/opencode/')) {
396
+ return 'opencode';
397
+ }
382
398
  return undefined;
383
399
  }
384
400
  inferToolFromRequest(req) {
@@ -387,6 +403,8 @@ class ProxyServer {
387
403
  return 'claude-code';
388
404
  if (path.startsWith('/codex'))
389
405
  return 'codex';
406
+ if (path.startsWith('/opencode'))
407
+ return 'opencode';
390
408
  return 'claude-code';
391
409
  }
392
410
  buildRelayTags(relayed, useOriginalConfig = false) {
@@ -747,7 +765,7 @@ class ProxyServer {
747
765
  tags: this.buildRelayTags(hasRelayAttempt),
748
766
  });
749
767
  // 确定目标类型
750
- const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
768
+ const targetType = this.inferToolFromRequest(req);
751
769
  // 记录错误日志 - 包含请求详情和最后失败的服务信息
752
770
  const _lastFailedVendor = lastFailedService ? this.dbManager.getVendorByServiceId(lastFailedService.id) : undefined;
753
771
  yield this.dbManager.addErrorLog({
@@ -813,7 +831,7 @@ class ProxyServer {
813
831
  }
814
832
  // Add error log - 包含请求详情
815
833
  if (!accessKeyCtx) {
816
- const targetType = req.path.startsWith('/claude-code/') ? 'claude-code' : 'codex';
834
+ const targetType = this.inferToolFromRequest(req);
817
835
  yield this.dbManager.addErrorLog({
818
836
  timestamp: Date.now(),
819
837
  method: req.method,
@@ -855,6 +873,8 @@ class ProxyServer {
855
873
  this.app.use('/claude-code', this.createFixedRouteHandler('claude-code'));
856
874
  this.app.use('/codex/', this.createFixedRouteHandler('codex'));
857
875
  this.app.use('/codex', this.createFixedRouteHandler('codex'));
876
+ this.app.use('/opencode/', this.createFixedRouteHandler('opencode'));
877
+ this.app.use('/opencode', this.createFixedRouteHandler('opencode'));
858
878
  }
859
879
  createFixedRouteHandler(targetType) {
860
880
  return (req, res) => __awaiter(this, void 0, void 0, function* () {
@@ -1184,6 +1204,9 @@ class ProxyServer {
1184
1204
  else if (req.path.startsWith('/codex/')) {
1185
1205
  tool = 'codex';
1186
1206
  }
1207
+ else if (req.path.startsWith('/opencode/')) {
1208
+ tool = 'opencode';
1209
+ }
1187
1210
  if (!tool)
1188
1211
  return undefined;
1189
1212
  // 优先检查会话级路由绑定
@@ -1224,6 +1247,9 @@ class ProxyServer {
1224
1247
  else if (req.path.startsWith('/codex/')) {
1225
1248
  targetType = 'codex';
1226
1249
  }
1250
+ else if (req.path.startsWith('/opencode/')) {
1251
+ targetType = 'opencode';
1252
+ }
1227
1253
  if (!targetType) {
1228
1254
  return false;
1229
1255
  }
@@ -1279,7 +1305,7 @@ class ProxyServer {
1279
1305
  apiUrl: originalConfig.apiUrl,
1280
1306
  apiKey: originalConfig.apiKey,
1281
1307
  authType: originalConfig.authType,
1282
- sourceType: originalConfig.sourceType || (targetType === 'claude-code' ? 'claude' : 'openai'),
1308
+ sourceType: originalConfig.sourceType || (targetType === 'claude-code' ? 'claude' : targetType === 'opencode' ? 'openai-chat' : 'openai'),
1283
1309
  createdAt: Date.now(),
1284
1310
  updatedAt: Date.now(),
1285
1311
  };
@@ -3110,6 +3136,16 @@ class ProxyServer {
3110
3136
  return sessionId[0] || null;
3111
3137
  }
3112
3138
  }
3139
+ else if (type === 'opencode') {
3140
+ // OpenCode 经 @ai-sdk/openai-compatible 发送 chat completions,尝试从 headers 提取 session 标识
3141
+ const sessionId = request.headers['session-id'] || request.headers['session_id'] || request.headers['x-session-id'];
3142
+ if (typeof sessionId === 'string') {
3143
+ return sessionId;
3144
+ }
3145
+ if (Array.isArray(sessionId)) {
3146
+ return sessionId[0] || null;
3147
+ }
3148
+ }
3113
3149
  return null;
3114
3150
  }
3115
3151
  /**
@@ -3299,6 +3335,14 @@ class ProxyServer {
3299
3335
  if (tool === 'codex' && this.isClaudeSource(source)) {
3300
3336
  return `${apiUrl}/v1/messages`;
3301
3337
  }
3338
+ // opencode 请求 claude 类型接口,使用 claude messages 接口处理
3339
+ if (tool === 'opencode' && this.isClaudeSource(source)) {
3340
+ return `${apiUrl}/v1/messages`;
3341
+ }
3342
+ // opencode 请求 openai(responses) 类型接口,使用 responses 接口处理
3343
+ if (tool === 'opencode' && this.isOpenAISource(source)) {
3344
+ return `${apiUrl}/v1/responses`;
3345
+ }
3302
3346
  // 透传路径
3303
3347
  return `${apiUrl}${originalPath}`;
3304
3348
  }
@@ -3312,7 +3356,7 @@ class ProxyServer {
3312
3356
  * @returns 转换后往服务商API接口的数据
3313
3357
  */
3314
3358
  transformRequestToUpstream(tool, source, payloadData, targetModel, providerConfig, serverToolConfig, sanitizeBody) {
3315
- const clientFormat = tool === 'codex' ? 'responses' : 'claude';
3359
+ const clientFormat = clientFormatForTool(tool);
3316
3360
  const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(source);
3317
3361
  const result = (0, index_1.transformRequest)({ fromFormat: clientFormat, toFormat: upstreamFormat, body: payloadData, providerConfig, serverToolConfig, sanitizeBody });
3318
3362
  const body = result.body;
@@ -3332,7 +3376,7 @@ class ProxyServer {
3332
3376
  * @param responseData
3333
3377
  */
3334
3378
  transformResponseToTool(tool, source, responseData) {
3335
- const clientFormat = tool === 'codex' ? 'responses' : 'claude';
3379
+ const clientFormat = clientFormatForTool(tool);
3336
3380
  const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(source);
3337
3381
  return (0, index_1.transformResponse)({ fromFormat: upstreamFormat, toFormat: clientFormat, response: responseData });
3338
3382
  }
@@ -3343,7 +3387,7 @@ class ProxyServer {
3343
3387
  * @returns 转换器实例和相关信息
3344
3388
  */
3345
3389
  transformSSEToTool(targetType, sourceType) {
3346
- const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
3390
+ const clientFormat = clientFormatForTool(targetType);
3347
3391
  const upstreamFormat = (0, source_type_mapping_1.sourceTypeToFormat)(sourceType);
3348
3392
  if (upstreamFormat === clientFormat) {
3349
3393
  return { converter: null };
@@ -3514,7 +3558,7 @@ class ProxyServer {
3514
3558
  let logged = false;
3515
3559
  const extraTagsForLog = [];
3516
3560
  // 编程套餐限制检查
3517
- const clientFormat = targetType === 'codex' ? 'responses' : 'claude';
3561
+ const clientFormat = clientFormatForTool(targetType);
3518
3562
  if (!this.checkCodingPlan(req, res, service, clientFormat))
3519
3563
  return;
3520
3564
  // Compact 请求消息清理:确保 tool_use/tool_result 配对完整
@@ -4077,6 +4121,9 @@ class ProxyServer {
4077
4121
  else if (targetType === 'codex' && req.path.startsWith('/codex')) {
4078
4122
  pathToRequest = req.path.slice('/codex'.length);
4079
4123
  }
4124
+ else if (targetType === 'opencode' && req.path.startsWith('/opencode')) {
4125
+ pathToRequest = req.path.slice('/opencode'.length);
4126
+ }
4080
4127
  // 使用 mapRequestPathToUpstreamUrl 统一构建上游 URL
4081
4128
  const model = rule.targetModel || (requestBody === null || requestBody === void 0 ? void 0 : requestBody.model);
4082
4129
  const apiUrl = this.resolveEffectiveApiUrl(service);
@@ -276,6 +276,7 @@ function generateMigrationPrompt(content, targetTool) {
276
276
  const toolLabels = {
277
277
  'claude-code': 'Claude Code',
278
278
  'codex': 'Codex',
279
+ 'opencode': 'OpenCode',
279
280
  };
280
281
  const sourceLabel = toolLabels[content.sourceTool];
281
282
  const targetLabel = toolLabels[targetTool];