aicodeswitch 3.9.3 → 4.0.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.
@@ -8,79 +8,103 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const types_1 = require("../types");
11
+ const toml_1 = __importDefault(require("@iarna/toml"));
11
12
  /**
12
- * TOML 解析器(简单实现,仅用于解析 Codex config.toml)
13
+ * TOML 解析器(使用 @iarna/toml 库)
13
14
  */
14
15
  const parseToml = (content) => {
15
- const result = {};
16
- let currentSection = result;
17
- const lines = content.split('\n');
18
- for (const line of lines) {
19
- const trimmed = line.trim();
20
- // 跳过空行和注释
21
- if (!trimmed || trimmed.startsWith('#')) {
22
- continue;
23
- }
24
- // 检查是否是 section
25
- const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
26
- if (sectionMatch) {
27
- const sectionPath = sectionMatch[1].split('.');
28
- currentSection = result;
29
- for (const key of sectionPath) {
30
- if (!currentSection[key]) {
31
- currentSection[key] = {};
32
- }
33
- currentSection = currentSection[key];
34
- }
35
- continue;
36
- }
37
- // 解析键值对
38
- const kvMatch = trimmed.match(/^([^=]+)=(.*)$/);
39
- if (kvMatch) {
40
- const key = kvMatch[1].trim();
41
- let value = kvMatch[2].trim();
42
- // 移除引号
43
- if ((value.startsWith('"') && value.endsWith('"')) ||
44
- (value.startsWith("'") && value.endsWith("'"))) {
45
- value = value.slice(1, -1);
46
- }
47
- // 尝试转换为布尔值
48
- if (value === 'true') {
49
- value = true;
50
- }
51
- else if (value === 'false') {
52
- value = false;
53
- }
54
- currentSection[key] = value;
16
+ try {
17
+ return toml_1.default.parse(content);
18
+ }
19
+ catch (error) {
20
+ console.warn('Failed to parse TOML file:', error);
21
+ return {}; // 返回空对象以保持兼容性
22
+ }
23
+ };
24
+ const normalizeApiUrl = (value) => {
25
+ return value.replace(/\/+$/, '');
26
+ };
27
+ const inferSourceTypeFromBaseUrlAndWireApi = (baseUrl, wireApi) => {
28
+ const lowerBaseUrl = baseUrl.toLowerCase();
29
+ const normalizedWireApi = (wireApi || '').toLowerCase();
30
+ if (lowerBaseUrl.includes('anthropic')) {
31
+ return normalizedWireApi === 'chat' ? 'claude-chat' : 'claude';
32
+ }
33
+ if (lowerBaseUrl.includes('generativelanguage.googleapis.com') ||
34
+ lowerBaseUrl.includes('aiplatform.googleapis.com') ||
35
+ lowerBaseUrl.includes('vertexai')) {
36
+ return normalizedWireApi === 'chat' ? 'gemini-chat' : 'gemini';
37
+ }
38
+ if (lowerBaseUrl.includes('deepseek')) {
39
+ return 'deepseek-reasoning-chat';
40
+ }
41
+ if (normalizedWireApi === 'chat') {
42
+ return 'openai-chat';
43
+ }
44
+ return 'openai';
45
+ };
46
+ const inferAuthTypeFromSource = (sourceType) => {
47
+ if (sourceType === 'gemini' || sourceType === 'gemini-chat') {
48
+ return types_1.AuthType.G_API_KEY;
49
+ }
50
+ if (sourceType === 'claude' || sourceType === 'claude-chat') {
51
+ return types_1.AuthType.API_KEY;
52
+ }
53
+ return types_1.AuthType.AUTH_TOKEN;
54
+ };
55
+ const getProviderCandidateKeys = (providerName) => {
56
+ if (!providerName) {
57
+ return [];
58
+ }
59
+ const upper = providerName.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
60
+ const lower = providerName.toLowerCase().replace(/[^a-z0-9]+/g, '_');
61
+ return [
62
+ `${upper}_API_KEY`,
63
+ `${upper}_AUTH_TOKEN`,
64
+ `${lower}_api_key`,
65
+ `${lower}_auth_token`,
66
+ ];
67
+ };
68
+ const pickApiKey = (authConfig, candidates) => {
69
+ for (const key of candidates) {
70
+ const value = authConfig[key];
71
+ if (typeof value === 'string' && value.trim()) {
72
+ return value.trim();
55
73
  }
56
74
  }
57
- return result;
75
+ return '';
58
76
  };
59
77
  /**
60
78
  * 读取 Claude Code 原始配置
61
- * 从备份文件或当前配置文件中读取(如果未激活路由)
79
+ * fallback 场景下仅从备份文件读取
62
80
  */
63
81
  const readClaudeOriginalConfig = () => {
64
- var _a, _b, _c;
82
+ var _a, _b, _c, _d, _e, _f;
65
83
  try {
66
84
  const homeDir = os_1.default.homedir();
67
- const settingsPath = path_1.default.join(homeDir, '.claude/settings.json');
68
85
  const settingsBakPath = path_1.default.join(homeDir, '.claude/settings.json.aicodeswitch_backup');
69
- // 优先读取备份文件(原始配置)
70
- let configPath = settingsBakPath;
71
- if (!fs_1.default.existsSync(configPath)) {
72
- // 如果没有备份,尝试读取当前配置
73
- configPath = settingsPath;
74
- if (!fs_1.default.existsSync(configPath)) {
75
- console.log('No Claude config file found');
76
- return null;
77
- }
86
+ // fallback 只读取备份文件,确保使用真实上游配置
87
+ if (!fs_1.default.existsSync(settingsBakPath)) {
88
+ console.log('No Claude backup config file found');
89
+ return null;
78
90
  }
79
- const content = fs_1.default.readFileSync(configPath, 'utf-8');
91
+ const content = fs_1.default.readFileSync(settingsBakPath, 'utf-8');
80
92
  const config = JSON.parse(content);
81
93
  // 提取配置信息
82
- const baseUrl = ((_a = config.env) === null || _a === void 0 ? void 0 : _a.ANTHROPIC_BASE_URL) || 'https://api.anthropic.com';
83
- const apiKey = ((_b = config.env) === null || _b === void 0 ? void 0 : _b.ANTHROPIC_AUTH_TOKEN) || ((_c = config.env) === null || _c === void 0 ? void 0 : _c.ANTHROPIC_API_KEY) || '';
94
+ const baseUrlRaw = ((_a = config.env) === null || _a === void 0 ? void 0 : _a.ANTHROPIC_BASE_URL) || 'https://api.anthropic.com';
95
+ const baseUrl = normalizeApiUrl(baseUrlRaw);
96
+ const authToken = typeof ((_b = config.env) === null || _b === void 0 ? void 0 : _b.ANTHROPIC_AUTH_TOKEN) === 'string' ? config.env.ANTHROPIC_AUTH_TOKEN : '';
97
+ const apiKeyValue = typeof ((_c = config.env) === null || _c === void 0 ? void 0 : _c.ANTHROPIC_API_KEY) === 'string' ? config.env.ANTHROPIC_API_KEY : '';
98
+ const apiKey = (authToken || apiKeyValue).trim();
99
+ const defaultHaikuModel = typeof ((_d = config.env) === null || _d === void 0 ? void 0 : _d.ANTHROPIC_DEFAULT_HAIKU_MODEL) === 'string'
100
+ ? config.env.ANTHROPIC_DEFAULT_HAIKU_MODEL.trim()
101
+ : '';
102
+ const defaultSonnetModel = typeof ((_e = config.env) === null || _e === void 0 ? void 0 : _e.ANTHROPIC_DEFAULT_SONNET_MODEL) === 'string'
103
+ ? config.env.ANTHROPIC_DEFAULT_SONNET_MODEL.trim()
104
+ : '';
105
+ const defaultOpusModel = typeof ((_f = config.env) === null || _f === void 0 ? void 0 : _f.ANTHROPIC_DEFAULT_OPUS_MODEL) === 'string'
106
+ ? config.env.ANTHROPIC_DEFAULT_OPUS_MODEL.trim()
107
+ : '';
84
108
  if (!apiKey) {
85
109
  console.log('No API key found in Claude config');
86
110
  return null;
@@ -88,8 +112,13 @@ const readClaudeOriginalConfig = () => {
88
112
  return {
89
113
  apiUrl: baseUrl,
90
114
  apiKey: apiKey,
91
- authType: types_1.AuthType.AUTH_TOKEN,
115
+ authType: authToken ? types_1.AuthType.AUTH_TOKEN : types_1.AuthType.API_KEY,
92
116
  sourceType: 'claude',
117
+ claudeDefaultModels: {
118
+ haiku: defaultHaikuModel || undefined,
119
+ sonnet: defaultSonnetModel || undefined,
120
+ opus: defaultOpusModel || undefined,
121
+ },
93
122
  };
94
123
  }
95
124
  catch (error) {
@@ -100,63 +129,100 @@ const readClaudeOriginalConfig = () => {
100
129
  exports.readClaudeOriginalConfig = readClaudeOriginalConfig;
101
130
  /**
102
131
  * 读取 Codex 原始配置
103
- * 从备份文件或当前配置文件中读取(如果未激活路由)
132
+ * fallback 场景下仅从备份文件读取
104
133
  */
105
134
  const readCodexOriginalConfig = () => {
106
135
  try {
107
136
  const homeDir = os_1.default.homedir();
108
- const configPath = path_1.default.join(homeDir, '.codex/config.toml');
109
137
  const configBakPath = path_1.default.join(homeDir, '.codex/config.toml.aicodeswitch_backup');
110
- const authPath = path_1.default.join(homeDir, '.codex/auth.json');
111
- // 优先读取备份文件(原始配置)
112
- let tomlPath = configBakPath;
113
- if (!fs_1.default.existsSync(tomlPath)) {
114
- // 如果没有备份,尝试读取当前配置
115
- tomlPath = configPath;
116
- if (!fs_1.default.existsSync(tomlPath)) {
117
- console.log('No Codex config file found');
118
- return null;
119
- }
138
+ const authBakPath = path_1.default.join(homeDir, '.codex/auth.json.aicodeswitch_backup');
139
+ // fallback 只读取备份文件,确保使用真实上游配置
140
+ if (!fs_1.default.existsSync(configBakPath)) {
141
+ console.log('No Codex backup config file found');
142
+ return null;
120
143
  }
121
- const tomlContent = fs_1.default.readFileSync(tomlPath, 'utf-8');
144
+ const tomlContent = fs_1.default.readFileSync(configBakPath, 'utf-8');
122
145
  const config = parseToml(tomlContent);
123
- // 提取 base_url
146
+ // 提取 provider 配置
124
147
  let baseUrl = '';
125
- let model = config.model;
148
+ let wireApi = '';
149
+ const model = typeof config.model === 'string' ? config.model : undefined;
150
+ const providerName = typeof config.model_provider === 'string' ? config.model_provider : undefined;
126
151
  // 从 model_providers 中查找配置
127
- if (config.model_providers) {
128
- const providerName = config.model_provider;
129
- if (providerName && config.model_providers[providerName]) {
130
- baseUrl = config.model_providers[providerName].base_url;
152
+ if (config.model_providers && typeof config.model_providers === 'object') {
153
+ const providers = config.model_providers;
154
+ if (providerName && providers[providerName]) {
155
+ const providerConfig = providers[providerName];
156
+ if (typeof (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.base_url) === 'string') {
157
+ baseUrl = providerConfig.base_url;
158
+ }
159
+ if (typeof (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.wire_api) === 'string') {
160
+ wireApi = providerConfig.wire_api;
161
+ }
162
+ }
163
+ else {
164
+ const firstProvider = Object.values(providers).find(provider => typeof (provider === null || provider === void 0 ? void 0 : provider.base_url) === 'string');
165
+ if (firstProvider) {
166
+ baseUrl = firstProvider.base_url;
167
+ if (typeof firstProvider.wire_api === 'string') {
168
+ wireApi = firstProvider.wire_api;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ if (!baseUrl) {
174
+ if (typeof config.base_url === 'string' && config.base_url.trim()) {
175
+ baseUrl = config.base_url;
131
176
  }
132
177
  }
133
178
  if (!baseUrl) {
134
179
  console.log('No base_url found in Codex config');
135
180
  return null;
136
181
  }
137
- // 读取 API key(从 auth.json)
182
+ const normalizedBaseUrl = normalizeApiUrl(baseUrl);
183
+ const sourceType = inferSourceTypeFromBaseUrlAndWireApi(normalizedBaseUrl, wireApi);
184
+ // 读取 API key(从 auth.json.aicodeswitch_backup)
138
185
  let apiKey = '';
139
- if (fs_1.default.existsSync(authPath)) {
186
+ if (fs_1.default.existsSync(authBakPath)) {
140
187
  try {
141
- const authContent = fs_1.default.readFileSync(authPath, 'utf-8');
188
+ const authContent = fs_1.default.readFileSync(authBakPath, 'utf-8');
142
189
  const authConfig = JSON.parse(authContent);
143
- // Codex auth.json 可能包含多个 provider 的 key
144
- // 尝试读取常见的字段
145
- apiKey = authConfig.api_key || authConfig.openai_api_key || authConfig.key || '';
190
+ const providerCandidateKeys = getProviderCandidateKeys(providerName);
191
+ const sourceTypeKeys = sourceType === 'claude' || sourceType === 'claude-chat'
192
+ ? ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY']
193
+ : sourceType === 'gemini' || sourceType === 'gemini-chat'
194
+ ? ['GEMINI_API_KEY', 'GOOGLE_API_KEY']
195
+ : ['OPENAI_API_KEY', 'DEEPSEEK_API_KEY'];
196
+ const commonKeys = ['api_key', 'openai_api_key', 'key'];
197
+ apiKey = pickApiKey(authConfig, [
198
+ ...providerCandidateKeys,
199
+ ...sourceTypeKeys,
200
+ ...commonKeys,
201
+ ]);
146
202
  }
147
203
  catch (error) {
148
- console.error('Failed to read Codex auth.json:', error);
204
+ console.error('Failed to read Codex auth backup:', error);
205
+ }
206
+ }
207
+ else {
208
+ console.log('No Codex backup auth file found');
209
+ }
210
+ // 某些配置会把 key 内联在 provider 中
211
+ if (!apiKey && config.model_providers && providerName) {
212
+ const providerConfig = config.model_providers[providerName];
213
+ if (providerConfig && typeof providerConfig.api_key === 'string' && providerConfig.api_key.trim()) {
214
+ apiKey = providerConfig.api_key.trim();
149
215
  }
150
216
  }
151
217
  if (!apiKey) {
152
- console.log('No API key found in Codex auth.json');
218
+ console.log('No API key found in Codex backup config/auth');
153
219
  return null;
154
220
  }
155
221
  return {
156
- apiUrl: baseUrl,
222
+ apiUrl: normalizedBaseUrl,
157
223
  apiKey: apiKey,
158
- authType: types_1.AuthType.API_KEY,
159
- sourceType: 'openai',
224
+ authType: inferAuthTypeFromSource(sourceType),
225
+ sourceType,
160
226
  model: model,
161
227
  };
162
228
  }