@tkpdx01/ccc 1.6.2 → 1.6.5
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/README.md +0 -2
- package/package.json +1 -1
- package/src/codex-models.js +133 -0
- package/src/commands/edit.js +7 -13
- package/src/commands/help.js +1 -1
- package/src/commands/new.js +5 -12
- package/src/profiles.js +97 -20
- package/cover.png +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# ccc — Claude Code / Codex Launcher
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
|
|
5
3
|
Manage multiple API profiles for **Claude Code** and **OpenAI Codex**. Switch between providers, keys, and endpoints instantly.
|
|
6
4
|
|
|
7
5
|
管理 **Claude Code** 和 **OpenAI Codex** 的多套 API 配置,一键切换 Provider、Key 和 Endpoint。
|
package/package.json
CHANGED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 8000;
|
|
5
|
+
const MANUAL_INPUT_VALUE = '__manual_input__';
|
|
6
|
+
|
|
7
|
+
function normalizeBaseUrl(baseUrl) {
|
|
8
|
+
const trimmed = (baseUrl || '').trim();
|
|
9
|
+
return trimmed || 'https://api.openai.com/v1';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildModelsEndpoint(baseUrl) {
|
|
13
|
+
const normalized = normalizeBaseUrl(baseUrl).replace(/\/+$/, '');
|
|
14
|
+
if (normalized.endsWith('/models')) {
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(normalized);
|
|
21
|
+
} catch {
|
|
22
|
+
return `${normalized}/models`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const path = url.pathname || '/';
|
|
26
|
+
if (path === '/' || path === '') {
|
|
27
|
+
url.pathname = '/v1/models';
|
|
28
|
+
} else {
|
|
29
|
+
url.pathname = `${path.replace(/\/+$/, '')}/models`;
|
|
30
|
+
}
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function fetchOpenAIModelIds(baseUrl, apiKey) {
|
|
35
|
+
const token = (apiKey || '').trim();
|
|
36
|
+
if (!token) {
|
|
37
|
+
throw new Error('API Key 为空');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endpoint = buildModelsEndpoint(baseUrl);
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(endpoint, {
|
|
46
|
+
method: 'GET',
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
Accept: 'application/json'
|
|
50
|
+
},
|
|
51
|
+
signal: controller.signal
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const body = await response.text();
|
|
56
|
+
const snippet = body.replace(/\s+/g, ' ').slice(0, 120);
|
|
57
|
+
throw new Error(`HTTP ${response.status}${snippet ? `: ${snippet}` : ''}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
const models = Array.isArray(data?.data) ? data.data : [];
|
|
62
|
+
|
|
63
|
+
return models
|
|
64
|
+
.map(item => (typeof item?.id === 'string' ? item.id.trim() : ''))
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.sort((a, b) => a.localeCompare(b));
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function promptCodexModel(baseUrl, apiKey, currentModel = '') {
|
|
73
|
+
const current = (currentModel || '').trim();
|
|
74
|
+
|
|
75
|
+
if (!(apiKey || '').trim()) {
|
|
76
|
+
const { model } = await inquirer.prompt([
|
|
77
|
+
{
|
|
78
|
+
type: 'input',
|
|
79
|
+
name: 'model',
|
|
80
|
+
message: 'Model (留空使用默认):',
|
|
81
|
+
default: current
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
return model;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(chalk.gray('正在获取模型列表...'));
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const modelIds = await fetchOpenAIModelIds(baseUrl, apiKey);
|
|
91
|
+
if (modelIds.length === 0) {
|
|
92
|
+
throw new Error('返回了空模型列表');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const choices = [
|
|
96
|
+
{ name: '(默认模型)', value: '' },
|
|
97
|
+
...modelIds.map(id => ({ name: id, value: id })),
|
|
98
|
+
{ name: '手动输入模型 ID', value: MANUAL_INPUT_VALUE }
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
let defaultChoice = '';
|
|
102
|
+
if (current) {
|
|
103
|
+
defaultChoice = modelIds.includes(current) ? current : MANUAL_INPUT_VALUE;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { selectedModel } = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: 'list',
|
|
109
|
+
name: 'selectedModel',
|
|
110
|
+
message: '选择模型:',
|
|
111
|
+
choices,
|
|
112
|
+
default: defaultChoice
|
|
113
|
+
}
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
if (selectedModel !== MANUAL_INPUT_VALUE) {
|
|
117
|
+
return selectedModel;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const reason = error?.name === 'AbortError' ? '请求超时' : (error?.message || '未知错误');
|
|
121
|
+
console.log(chalk.yellow(`获取模型列表失败,改为手动输入(${reason})`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { model } = await inquirer.prompt([
|
|
125
|
+
{
|
|
126
|
+
type: 'input',
|
|
127
|
+
name: 'model',
|
|
128
|
+
message: 'Model (留空使用默认):',
|
|
129
|
+
default: current
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
return model;
|
|
133
|
+
}
|
package/src/commands/edit.js
CHANGED
|
@@ -4,8 +4,6 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import {
|
|
5
5
|
getAllProfiles,
|
|
6
6
|
getDefaultProfile,
|
|
7
|
-
profileExists,
|
|
8
|
-
codexProfileExists,
|
|
9
7
|
anyProfileExists,
|
|
10
8
|
getProfilePath,
|
|
11
9
|
readProfile,
|
|
@@ -22,6 +20,7 @@ import {
|
|
|
22
20
|
createCodexProfile,
|
|
23
21
|
deleteCodexProfile
|
|
24
22
|
} from '../profiles.js';
|
|
23
|
+
import { promptCodexModel } from '../codex-models.js';
|
|
25
24
|
|
|
26
25
|
export function editCommand(program) {
|
|
27
26
|
program
|
|
@@ -67,18 +66,12 @@ export function editCommand(program) {
|
|
|
67
66
|
const { apiKey: currentApiKey, baseUrl: currentBaseUrl, model: currentModel } = getCodexProfileCredentials(profileInfo.name);
|
|
68
67
|
|
|
69
68
|
console.log(chalk.cyan(`\n当前配置 (${profileInfo.name}) ${chalk.blue('[Codex]')}:`));
|
|
70
|
-
console.log(chalk.gray(` OPENAI_API_KEY: ${currentApiKey ? currentApiKey.substring(0, 10) + '...' : '未设置'}`));
|
|
71
69
|
console.log(chalk.gray(` Base URL: ${currentBaseUrl || '未设置'}`));
|
|
70
|
+
console.log(chalk.gray(` OPENAI_API_KEY: ${currentApiKey ? currentApiKey.substring(0, 10) + '...' : '未设置'}`));
|
|
72
71
|
console.log(chalk.gray(` Model: ${currentModel || '(默认)'}`));
|
|
73
72
|
console.log();
|
|
74
73
|
|
|
75
|
-
const { apiKey, baseUrl,
|
|
76
|
-
{
|
|
77
|
-
type: 'input',
|
|
78
|
-
name: 'apiKey',
|
|
79
|
-
message: 'OPENAI_API_KEY:',
|
|
80
|
-
default: currentApiKey || ''
|
|
81
|
-
},
|
|
74
|
+
const { apiKey, baseUrl, newName } = await inquirer.prompt([
|
|
82
75
|
{
|
|
83
76
|
type: 'input',
|
|
84
77
|
name: 'baseUrl',
|
|
@@ -87,9 +80,9 @@ export function editCommand(program) {
|
|
|
87
80
|
},
|
|
88
81
|
{
|
|
89
82
|
type: 'input',
|
|
90
|
-
name: '
|
|
91
|
-
message: '
|
|
92
|
-
default:
|
|
83
|
+
name: 'apiKey',
|
|
84
|
+
message: 'OPENAI_API_KEY:',
|
|
85
|
+
default: currentApiKey || ''
|
|
93
86
|
},
|
|
94
87
|
{
|
|
95
88
|
type: 'input',
|
|
@@ -98,6 +91,7 @@ export function editCommand(program) {
|
|
|
98
91
|
default: profileInfo.name
|
|
99
92
|
}
|
|
100
93
|
]);
|
|
94
|
+
const model = await promptCodexModel(baseUrl, apiKey, currentModel || '');
|
|
101
95
|
|
|
102
96
|
if (newName && newName !== profileInfo.name) {
|
|
103
97
|
const check = anyProfileExists(newName);
|
package/src/commands/help.js
CHANGED
|
@@ -42,7 +42,7 @@ export function showHelp() {
|
|
|
42
42
|
console.log(chalk.gray(' ccc new ') + '交互式创建,选择 Claude 或 Codex 类型');
|
|
43
43
|
console.log(chalk.gray(' ccc new myprofile ') + '指定名称创建,随后选择类型并填写凭证');
|
|
44
44
|
console.log(chalk.gray(' ') + chalk.dim('Claude 需要: ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN'));
|
|
45
|
-
console.log(chalk.gray(' ') + chalk.dim('Codex 需要:
|
|
45
|
+
console.log(chalk.gray(' ') + chalk.dim('Codex 需要: Base URL + OPENAI_API_KEY + Model(可从接口拉取后选择)'));
|
|
46
46
|
console.log();
|
|
47
47
|
|
|
48
48
|
console.log(chalk.yellow(' 示例:'));
|
package/src/commands/new.js
CHANGED
|
@@ -2,10 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import {
|
|
4
4
|
ensureDirs,
|
|
5
|
-
getProfiles,
|
|
6
5
|
getAllProfiles,
|
|
7
|
-
profileExists,
|
|
8
|
-
codexProfileExists,
|
|
9
6
|
anyProfileExists,
|
|
10
7
|
createProfileFromTemplate,
|
|
11
8
|
createCodexProfile,
|
|
@@ -13,6 +10,7 @@ import {
|
|
|
13
10
|
ensureClaudeSettingsExtras
|
|
14
11
|
} from '../profiles.js';
|
|
15
12
|
import { launchClaude, launchCodex } from '../launch.js';
|
|
13
|
+
import { promptCodexModel } from '../codex-models.js';
|
|
16
14
|
|
|
17
15
|
const RESERVED_PROFILE_NAMES = [
|
|
18
16
|
'list',
|
|
@@ -104,12 +102,6 @@ export function newCommand(program) {
|
|
|
104
102
|
if (profileType === 'codex') {
|
|
105
103
|
// Codex profile 创建
|
|
106
104
|
const answers = await inquirer.prompt([
|
|
107
|
-
{
|
|
108
|
-
type: 'input',
|
|
109
|
-
name: 'apiKey',
|
|
110
|
-
message: 'OPENAI_API_KEY:',
|
|
111
|
-
default: ''
|
|
112
|
-
},
|
|
113
105
|
{
|
|
114
106
|
type: 'input',
|
|
115
107
|
name: 'baseUrl',
|
|
@@ -118,8 +110,8 @@ export function newCommand(program) {
|
|
|
118
110
|
},
|
|
119
111
|
{
|
|
120
112
|
type: 'input',
|
|
121
|
-
name: '
|
|
122
|
-
message: '
|
|
113
|
+
name: 'apiKey',
|
|
114
|
+
message: 'OPENAI_API_KEY:',
|
|
123
115
|
default: ''
|
|
124
116
|
},
|
|
125
117
|
{
|
|
@@ -130,6 +122,7 @@ export function newCommand(program) {
|
|
|
130
122
|
validate: validateProfileName
|
|
131
123
|
}
|
|
132
124
|
]);
|
|
125
|
+
const model = await promptCodexModel(answers.baseUrl, answers.apiKey, '');
|
|
133
126
|
|
|
134
127
|
const finalName = answers.finalName;
|
|
135
128
|
if (finalName !== name) {
|
|
@@ -151,7 +144,7 @@ export function newCommand(program) {
|
|
|
151
144
|
}
|
|
152
145
|
|
|
153
146
|
ensureDirs();
|
|
154
|
-
createCodexProfile(finalName, answers.apiKey, answers.baseUrl,
|
|
147
|
+
createCodexProfile(finalName, answers.apiKey, answers.baseUrl, model);
|
|
155
148
|
console.log(chalk.green(`\n✓ Codex 配置 "${finalName}" 已创建`));
|
|
156
149
|
|
|
157
150
|
const allProfiles = getAllProfiles();
|
package/src/profiles.js
CHANGED
|
@@ -336,6 +336,88 @@ export function clearDefaultProfile() {
|
|
|
336
336
|
// Codex Profile 管理
|
|
337
337
|
// ============================================================
|
|
338
338
|
|
|
339
|
+
const OPENAI_DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
340
|
+
const CCC_OPENAI_COMPAT_PROVIDER = 'ccc_openai';
|
|
341
|
+
|
|
342
|
+
function normalizeBaseUrl(baseUrl) {
|
|
343
|
+
return (baseUrl || '').trim().replace(/\/+$/, '');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isCustomOpenAIBaseUrl(baseUrl) {
|
|
347
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
348
|
+
return normalized && normalized !== normalizeBaseUrl(OPENAI_DEFAULT_BASE_URL);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function extractBaseUrlFromConfigToml(configToml) {
|
|
352
|
+
if (!configToml) return OPENAI_DEFAULT_BASE_URL;
|
|
353
|
+
const baseUrlMatch = configToml.match(/base_url\s*=\s*"([^"]+)"/);
|
|
354
|
+
return baseUrlMatch ? baseUrlMatch[1] : OPENAI_DEFAULT_BASE_URL;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function upsertTomlKey(block, key, valueLiteral) {
|
|
358
|
+
const keyPattern = new RegExp(`^\\s*${key}\\s*=\\s*.*$`, 'm');
|
|
359
|
+
if (keyPattern.test(block)) {
|
|
360
|
+
return block.replace(keyPattern, `${key} = ${valueLiteral}`);
|
|
361
|
+
}
|
|
362
|
+
return `${block.trimEnd()}\n${key} = ${valueLiteral}\n`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function ensureCodexOpenAICompatConfig(configToml, baseUrl) {
|
|
366
|
+
if (!isCustomOpenAIBaseUrl(baseUrl)) {
|
|
367
|
+
return configToml;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
371
|
+
let output = configToml || '# Codex profile managed by ccc\n';
|
|
372
|
+
const firstSectionMatch = output.match(/^\s*\[[^\]]+\]/m);
|
|
373
|
+
const firstSectionIndex = firstSectionMatch && firstSectionMatch.index !== undefined
|
|
374
|
+
? firstSectionMatch.index
|
|
375
|
+
: output.length;
|
|
376
|
+
|
|
377
|
+
let preamble = output.slice(0, firstSectionIndex);
|
|
378
|
+
const rest = output.slice(firstSectionIndex);
|
|
379
|
+
|
|
380
|
+
// 如果用户显式指定了非 openai provider,尊重用户配置,不自动覆盖
|
|
381
|
+
const providerMatch = preamble.match(/^\s*model_provider\s*=\s*"([^"]+)"/m);
|
|
382
|
+
if (providerMatch && !['openai', CCC_OPENAI_COMPAT_PROVIDER].includes(providerMatch[1])) {
|
|
383
|
+
return output;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (providerMatch) {
|
|
387
|
+
preamble = preamble.replace(
|
|
388
|
+
/^\s*model_provider\s*=\s*"([^"]+)"/m,
|
|
389
|
+
`model_provider = "${CCC_OPENAI_COMPAT_PROVIDER}"`
|
|
390
|
+
);
|
|
391
|
+
} else {
|
|
392
|
+
preamble = `${preamble.replace(/\s*$/, '\n')}model_provider = "${CCC_OPENAI_COMPAT_PROVIDER}"\n`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (rest.trim()) {
|
|
396
|
+
output = `${preamble.trimEnd()}\n\n${rest.replace(/^\s*/, '')}`;
|
|
397
|
+
} else {
|
|
398
|
+
output = `${preamble.trimEnd()}\n`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const sectionPattern = new RegExp(
|
|
402
|
+
`\\[model_providers\\.${CCC_OPENAI_COMPAT_PROVIDER}\\][\\s\\S]*?(?=\\n\\[|$)`
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (sectionPattern.test(output)) {
|
|
406
|
+
output = output.replace(sectionPattern, (section) => {
|
|
407
|
+
let next = section;
|
|
408
|
+
next = upsertTomlKey(next, 'name', '"OpenAI Compatible"');
|
|
409
|
+
next = upsertTomlKey(next, 'base_url', `"${normalizedBaseUrl}"`);
|
|
410
|
+
next = upsertTomlKey(next, 'wire_api', '"responses"');
|
|
411
|
+
next = upsertTomlKey(next, 'requires_openai_auth', 'true');
|
|
412
|
+
return next.trimEnd();
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
output = `${output.trimEnd()}\n\n[model_providers.${CCC_OPENAI_COMPAT_PROVIDER}]\nname = "OpenAI Compatible"\nbase_url = "${normalizedBaseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return `${output.trimEnd()}\n`;
|
|
419
|
+
}
|
|
420
|
+
|
|
339
421
|
// 获取 Codex profile 目录路径
|
|
340
422
|
export function getCodexProfileDir(name) {
|
|
341
423
|
return path.join(CODEX_PROFILES_DIR, name);
|
|
@@ -388,16 +470,20 @@ export function saveCodexProfile(name, auth, configToml) {
|
|
|
388
470
|
// 生成 Codex config.toml 内容
|
|
389
471
|
export function generateCodexConfigToml(baseUrl, model) {
|
|
390
472
|
let lines = ['# Codex profile managed by ccc'];
|
|
473
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) || OPENAI_DEFAULT_BASE_URL;
|
|
391
474
|
|
|
392
475
|
if (model) {
|
|
393
476
|
lines.push(`model = "${model}"`);
|
|
394
477
|
}
|
|
395
478
|
|
|
396
|
-
if (
|
|
479
|
+
if (isCustomOpenAIBaseUrl(normalizedBaseUrl)) {
|
|
480
|
+
lines.push(`model_provider = "${CCC_OPENAI_COMPAT_PROVIDER}"`);
|
|
397
481
|
lines.push('');
|
|
398
|
-
lines.push(
|
|
399
|
-
lines.push(
|
|
400
|
-
lines.push(`base_url = "${
|
|
482
|
+
lines.push(`[model_providers.${CCC_OPENAI_COMPAT_PROVIDER}]`);
|
|
483
|
+
lines.push('name = "OpenAI Compatible"');
|
|
484
|
+
lines.push(`base_url = "${normalizedBaseUrl}"`);
|
|
485
|
+
lines.push('wire_api = "responses"');
|
|
486
|
+
lines.push('requires_openai_auth = true');
|
|
401
487
|
}
|
|
402
488
|
|
|
403
489
|
lines.push('');
|
|
@@ -432,7 +518,7 @@ export function getCodexProfileCredentials(name) {
|
|
|
432
518
|
if (modelMatch) model = modelMatch[1];
|
|
433
519
|
}
|
|
434
520
|
|
|
435
|
-
return { apiKey, baseUrl: baseUrl ||
|
|
521
|
+
return { apiKey, baseUrl: baseUrl || OPENAI_DEFAULT_BASE_URL, model: model || '' };
|
|
436
522
|
}
|
|
437
523
|
|
|
438
524
|
// 删除 Codex profile
|
|
@@ -457,20 +543,6 @@ export function syncCodexProfileWithTemplate(name) {
|
|
|
457
543
|
// 保留当前 profile 的 base_url 和 model
|
|
458
544
|
const { baseUrl, model } = getCodexProfileCredentials(name);
|
|
459
545
|
|
|
460
|
-
// 在模板基础上覆盖 base_url 和 model
|
|
461
|
-
// 如果当前 profile 有自定义 base_url,追加到模板
|
|
462
|
-
if (baseUrl && baseUrl !== 'https://api.openai.com/v1') {
|
|
463
|
-
// 检查模板是否已有 [model_providers.openai] 节
|
|
464
|
-
if (templateConfig.includes('[model_providers.openai]')) {
|
|
465
|
-
templateConfig = templateConfig.replace(
|
|
466
|
-
/(\[model_providers\.openai\][^\[]*?)base_url\s*=\s*"[^"]*"/,
|
|
467
|
-
`$1base_url = "${baseUrl}"`
|
|
468
|
-
);
|
|
469
|
-
} else {
|
|
470
|
-
templateConfig += `\n[model_providers.openai]\nbase_url = "${baseUrl}"\n`;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
546
|
if (model) {
|
|
475
547
|
if (templateConfig.match(/^model\s*=/m)) {
|
|
476
548
|
templateConfig = templateConfig.replace(/^model\s*=\s*"[^"]*"/m, `model = "${model}"`);
|
|
@@ -479,6 +551,9 @@ export function syncCodexProfileWithTemplate(name) {
|
|
|
479
551
|
}
|
|
480
552
|
}
|
|
481
553
|
|
|
554
|
+
// 对第三方 base_url 自动补齐 provider 兼容配置,避免依赖 OPENAI_BASE_URL 环境变量
|
|
555
|
+
templateConfig = ensureCodexOpenAICompatConfig(templateConfig, baseUrl);
|
|
556
|
+
|
|
482
557
|
saveCodexProfile(name, current.auth, templateConfig);
|
|
483
558
|
return { auth: current.auth, configToml: templateConfig };
|
|
484
559
|
}
|
|
@@ -508,7 +583,9 @@ export function applyCodexProfile(name) {
|
|
|
508
583
|
|
|
509
584
|
// 写入 config.toml(如果有内容)
|
|
510
585
|
if (profile.configToml && profile.configToml.trim()) {
|
|
511
|
-
|
|
586
|
+
const baseUrl = extractBaseUrlFromConfigToml(profile.configToml);
|
|
587
|
+
const compatConfig = ensureCodexOpenAICompatConfig(profile.configToml, baseUrl);
|
|
588
|
+
fs.writeFileSync(path.join(CODEX_HOME_PATH, 'config.toml'), compatConfig);
|
|
512
589
|
}
|
|
513
590
|
|
|
514
591
|
return true;
|
package/cover.png
DELETED
|
Binary file
|