@tokenaut/opentoken 1.3.6 → 1.4.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.
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const { API_NODES, DEFAULT_BASE_URL } = require('../models');
6
+ const { fetchModelsForBaseUrl } = require('../fetch-models');
7
+ const { getCurrentConfig, applyConfig } = require('../config');
8
+ const { printLogo } = require('../logo');
9
+ const { clearScreen } = require('../screen');
10
+
11
+ let pendingConfig = { apiKey: null, baseUrl: null, model: null };
12
+
13
+ function resetPending() {
14
+ const disk = getCurrentConfig();
15
+ pendingConfig = {
16
+ apiKey: null,
17
+ baseUrl: disk.baseUrl ? null : DEFAULT_BASE_URL,
18
+ model: null,
19
+ };
20
+ }
21
+
22
+ function maskApiKey(key) {
23
+ if (!key || key.length < 8) return key || '';
24
+ return key.slice(0, 6) + '****' + key.slice(-4);
25
+ }
26
+
27
+ async function handleConfigApiKey() {
28
+ const { apiKey } = await inquirer.prompt([
29
+ {
30
+ type: 'password',
31
+ name: 'apiKey',
32
+ prefix: '',
33
+ message: '请输入 API Key:',
34
+ mask: '*',
35
+ },
36
+ ]);
37
+
38
+ if (apiKey && apiKey.trim()) {
39
+ pendingConfig.apiKey = apiKey.trim();
40
+ console.log(chalk.green(' ✓ API Key 已更新'));
41
+ } else {
42
+ console.log(chalk.gray(' (留空,保持不变)'));
43
+ }
44
+ console.log('');
45
+ }
46
+
47
+ async function handleConfigBaseUrl() {
48
+ const { baseUrl: existing } = getCurrentConfig();
49
+ const current = pendingConfig.baseUrl !== null ? pendingConfig.baseUrl : existing;
50
+
51
+ const nodeChoices = API_NODES.map((n) => ({
52
+ ...n,
53
+ name: current === n.value ? chalk.green('► ' + n.name) : ' ' + n.name,
54
+ }));
55
+
56
+ const { selected } = await inquirer.prompt([
57
+ {
58
+ type: 'list',
59
+ name: 'selected',
60
+ prefix: '',
61
+ message: '请选择 Base URL:',
62
+ choices: nodeChoices,
63
+ default: current || API_NODES[0].value,
64
+ },
65
+ ]);
66
+
67
+ if (selected === '__custom__') {
68
+ const { customUrl } = await inquirer.prompt([
69
+ {
70
+ type: 'input',
71
+ name: 'customUrl',
72
+ prefix: '',
73
+ message: '请输入自定义 Base URL:',
74
+ default: current && current !== '__custom__' ? current : DEFAULT_BASE_URL,
75
+ validate: (v) => v.trim().startsWith('http') || '请输入合法的 URL(以 http 开头)',
76
+ },
77
+ ]);
78
+ pendingConfig.baseUrl = customUrl.trim();
79
+ console.log(chalk.green(' ✓ Base URL 已更新:' + pendingConfig.baseUrl));
80
+ } else {
81
+ pendingConfig.baseUrl = selected;
82
+ console.log(chalk.green(' ✓ Base URL 已更新:' + selected));
83
+ }
84
+ console.log('');
85
+ }
86
+
87
+ function getEffectiveClaudeEnv() {
88
+ const disk = getCurrentConfig();
89
+ return {
90
+ apiKey: pendingConfig.apiKey !== null ? pendingConfig.apiKey : disk.apiKey,
91
+ baseUrl: pendingConfig.baseUrl !== null ? pendingConfig.baseUrl : disk.baseUrl,
92
+ model: pendingConfig.model !== null ? pendingConfig.model : disk.model,
93
+ };
94
+ }
95
+
96
+ function grayParen(text) {
97
+ return chalk.gray('(' + text + ')');
98
+ }
99
+
100
+ function buildClaudeMenuChoices() {
101
+ const ev = getEffectiveClaudeEnv();
102
+ const keyShow = ev.apiKey ? maskApiKey(ev.apiKey) : '未配置';
103
+ const urlShow = ev.baseUrl ? ev.baseUrl : '未配置';
104
+ const modelShow = ev.model ? ev.model : '未配置';
105
+
106
+ return [
107
+ { name: '1. 配置 Base URL' + grayParen(urlShow), value: 'apiNode' },
108
+ { name: '2. 配置 API Key' + grayParen(keyShow), value: 'apiKey' },
109
+ { name: '3. 配置模型' + grayParen(modelShow), value: 'model' },
110
+ new inquirer.Separator(),
111
+ { name: '4. 应用配置并保存', value: 'apply' },
112
+ new inquirer.Separator(),
113
+ { name: '← 返回上级菜单', value: 'back' },
114
+ ];
115
+ }
116
+
117
+ async function handleConfigModel() {
118
+ const { model: existing, baseUrl, apiKey } = getEffectiveClaudeEnv();
119
+ const current = existing || '';
120
+
121
+ if (!baseUrl) {
122
+ console.log(chalk.yellow(' 请先配置 Base URL,否则无法拉取模型列表。'));
123
+ const { modelId } = await inquirer.prompt([
124
+ {
125
+ type: 'input',
126
+ name: 'modelId',
127
+ prefix: '',
128
+ message: '或直接输入模型 ID:',
129
+ default: current,
130
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
131
+ },
132
+ ]);
133
+ pendingConfig.model = modelId.trim();
134
+ applyConfig({ model: pendingConfig.model });
135
+ console.log(chalk.green(' ✓ 模型已更新:' + pendingConfig.model));
136
+ console.log(
137
+ chalk.gray(
138
+ ' (已写入 settings.json,并合并 API_TIMEOUT_MS 等默认环境变量)',
139
+ ),
140
+ );
141
+ console.log('');
142
+ return;
143
+ }
144
+
145
+ const fetched = await fetchModelsForBaseUrl(baseUrl, apiKey, {
146
+ filterAnthropicCompatible: true,
147
+ });
148
+
149
+ if (fetched.ok && fetched.models.length > 0) {
150
+ let ids = [...fetched.models];
151
+ if (current && !ids.includes(current)) {
152
+ ids = [current, ...ids];
153
+ }
154
+
155
+ const modelChoices = ids.map((id) => ({
156
+ name: current === id ? chalk.green('► ' + id) : ' ' + id,
157
+ value: id,
158
+ }));
159
+ modelChoices.push(new inquirer.Separator());
160
+ modelChoices.push({ name: ' 手动输入其他模型 ID…', value: '__manual__' });
161
+
162
+ const defaultValue = current && ids.includes(current) ? current : ids[0];
163
+
164
+ const { model } = await inquirer.prompt([
165
+ {
166
+ type: 'list',
167
+ name: 'model',
168
+ prefix: '',
169
+ message: '请选择模型:',
170
+ choices: modelChoices,
171
+ default: defaultValue,
172
+ pageSize: 15,
173
+ },
174
+ ]);
175
+
176
+ if (model === '__manual__') {
177
+ const { modelId } = await inquirer.prompt([
178
+ {
179
+ type: 'input',
180
+ name: 'modelId',
181
+ prefix: '',
182
+ message: '请输入模型 ID:',
183
+ default: current || '',
184
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
185
+ },
186
+ ]);
187
+ pendingConfig.model = modelId.trim();
188
+ } else {
189
+ pendingConfig.model = model;
190
+ }
191
+ } else {
192
+ console.log(chalk.yellow(' 无法拉取模型列表:' + (fetched.error || '未知错误')));
193
+ console.log(chalk.gray(' 请检查 Base URL、网络或 API Key 后重试,或直接输入模型 ID。'));
194
+ const { modelId } = await inquirer.prompt([
195
+ {
196
+ type: 'input',
197
+ name: 'modelId',
198
+ prefix: '',
199
+ message: '请输入模型 ID:',
200
+ default: current,
201
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
202
+ },
203
+ ]);
204
+ pendingConfig.model = modelId.trim();
205
+ }
206
+
207
+ applyConfig({ model: pendingConfig.model });
208
+ console.log(chalk.green(' ✓ 模型已更新:' + pendingConfig.model));
209
+ console.log(
210
+ chalk.gray(
211
+ ' (已写入 settings.json,并合并 API_TIMEOUT_MS 等默认环境变量)',
212
+ ),
213
+ );
214
+ console.log('');
215
+ }
216
+
217
+ function doApplyConfig() {
218
+ const toWrite = {};
219
+ if (pendingConfig.apiKey !== null) toWrite.apiKey = pendingConfig.apiKey;
220
+ if (pendingConfig.baseUrl !== null) toWrite.baseUrl = pendingConfig.baseUrl;
221
+ if (pendingConfig.model !== null) toWrite.model = pendingConfig.model;
222
+
223
+ console.log('');
224
+ if (Object.keys(toWrite).length > 0) {
225
+ applyConfig(toWrite);
226
+ console.log(chalk.green(' ✓ 配置已成功写入'));
227
+ } else {
228
+ applyConfig({});
229
+ console.log(
230
+ chalk.gray(
231
+ ' (无 Key / URL / 模型变更;已同步 API_TIMEOUT_MS 等默认环境变量)',
232
+ ),
233
+ );
234
+ }
235
+ console.log('');
236
+ }
237
+
238
+ function paintClaudeScreen() {
239
+ clearScreen();
240
+ printLogo();
241
+ }
242
+
243
+ async function run() {
244
+ resetPending();
245
+
246
+ while (true) {
247
+ paintClaudeScreen();
248
+
249
+ const { action } = await inquirer.prompt([
250
+ {
251
+ type: 'list',
252
+ name: 'action',
253
+ prefix: '',
254
+ message: '请选择操作:',
255
+ choices: buildClaudeMenuChoices(),
256
+ pageSize: 10,
257
+ },
258
+ ]);
259
+
260
+ switch (action) {
261
+ case 'apiNode':
262
+ paintClaudeScreen();
263
+ await handleConfigBaseUrl();
264
+ break;
265
+ case 'apiKey':
266
+ paintClaudeScreen();
267
+ await handleConfigApiKey();
268
+ break;
269
+ case 'model':
270
+ paintClaudeScreen();
271
+ await handleConfigModel();
272
+ break;
273
+ case 'apply':
274
+ doApplyConfig();
275
+ return;
276
+ case 'back':
277
+ return;
278
+ }
279
+ }
280
+ }
281
+
282
+ function quickSet({ key, url, model }) {
283
+ const toWrite = {};
284
+ if (key) toWrite.apiKey = key;
285
+ if (url) toWrite.baseUrl = url;
286
+ if (model) toWrite.model = model;
287
+
288
+ if (Object.keys(toWrite).length === 0) return false;
289
+
290
+ applyConfig(toWrite);
291
+
292
+ console.log('');
293
+ console.log(chalk.bold(chalk.cyan('Claude Code 配置已更新:')));
294
+ if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
295
+ if (url) console.log(chalk.green(' ✓ Base URL : ') + url);
296
+ if (model) console.log(chalk.green(' ✓ 模型 : ') + model);
297
+ console.log('');
298
+
299
+ return true;
300
+ }
301
+
302
+ module.exports = { run, quickSet };
@@ -0,0 +1,327 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const { CODEX_OPENTOKEN_BASE_URL, CODEX_AUTH_KEY } = require('../models');
6
+ const { fetchModelsForBaseUrl } = require('../fetch-models');
7
+ const {
8
+ CONFIG_PATH,
9
+ AUTH_PATH,
10
+ getCurrentCodexConfig,
11
+ applyCodexConfig,
12
+ } = require('../codex-config');
13
+ const { printLogo } = require('../logo');
14
+ const { clearScreen } = require('../screen');
15
+
16
+ let pendingConfig = { apiKey: null, baseUrl: null, model: null, models: null };
17
+
18
+ function resetPending() {
19
+ const disk = getCurrentCodexConfig();
20
+ pendingConfig = {
21
+ apiKey: null,
22
+ baseUrl: disk.baseUrl ? null : CODEX_OPENTOKEN_BASE_URL,
23
+ model: null,
24
+ models: null,
25
+ };
26
+ }
27
+
28
+ function maskApiKey(key) {
29
+ if (!key || key.length < 8) return key || '';
30
+ return key.slice(0, 6) + '****' + key.slice(-4);
31
+ }
32
+
33
+ function getEffectiveCodexEnv() {
34
+ const disk = getCurrentCodexConfig();
35
+ return {
36
+ apiKey: pendingConfig.apiKey !== null ? pendingConfig.apiKey : disk.apiKey,
37
+ baseUrl: pendingConfig.baseUrl !== null ? pendingConfig.baseUrl : disk.baseUrl,
38
+ model: pendingConfig.model !== null ? pendingConfig.model : disk.model,
39
+ };
40
+ }
41
+
42
+ function grayParen(text) {
43
+ return chalk.gray('(' + text + ')');
44
+ }
45
+
46
+ function buildCodexMenuChoices() {
47
+ const ev = getEffectiveCodexEnv();
48
+ const keyShow = ev.apiKey ? maskApiKey(ev.apiKey) : '未配置';
49
+ const urlShow = ev.baseUrl ? ev.baseUrl : CODEX_OPENTOKEN_BASE_URL;
50
+ const modelShow = ev.model ? ev.model : '未配置';
51
+
52
+ return [
53
+ { name: '1. 配置 Base URL' + grayParen(urlShow), value: 'baseUrl' },
54
+ { name: '2. 输入 API Key' + grayParen(keyShow), value: 'apiKey' },
55
+ { name: '3. 配置模型' + grayParen(modelShow), value: 'model' },
56
+ new inquirer.Separator(),
57
+ { name: '4. 应用配置并保存', value: 'apply' },
58
+ new inquirer.Separator(),
59
+ { name: '← 返回上级菜单', value: 'back' },
60
+ ];
61
+ }
62
+
63
+ async function handleConfigBaseUrl() {
64
+ const { baseUrl: existing } = getEffectiveCodexEnv();
65
+ const { baseUrl } = await inquirer.prompt([
66
+ {
67
+ type: 'input',
68
+ name: 'baseUrl',
69
+ prefix: '',
70
+ message: '请输入 Codex Base URL(需包含 /v1):',
71
+ default: existing || CODEX_OPENTOKEN_BASE_URL,
72
+ validate: (v) => v.trim().startsWith('http') || '请输入合法的 URL(以 http 开头)',
73
+ },
74
+ ]);
75
+
76
+ pendingConfig.baseUrl = baseUrl.trim();
77
+ console.log(chalk.green(' ✓ Base URL 已更新:' + pendingConfig.baseUrl));
78
+ console.log('');
79
+ }
80
+
81
+ async function handleConfigApiKey() {
82
+ const { apiKey } = await inquirer.prompt([
83
+ {
84
+ type: 'password',
85
+ name: 'apiKey',
86
+ prefix: '',
87
+ message: '请输入 Opentoken API Key(将写入 ' + AUTH_PATH + '):',
88
+ mask: '*',
89
+ },
90
+ ]);
91
+
92
+ if (apiKey && apiKey.trim()) {
93
+ pendingConfig.apiKey = apiKey.trim();
94
+ console.log(chalk.green(' ✓ API Key 已记录(保存时将写入 auth.json)'));
95
+ } else {
96
+ console.log(chalk.gray(' (留空,保持不变)'));
97
+ }
98
+ console.log('');
99
+ }
100
+
101
+ async function handleConfigModel() {
102
+ const { model: existing, baseUrl, apiKey } = getEffectiveCodexEnv();
103
+ const current = existing || '';
104
+ const listBase = baseUrl || CODEX_OPENTOKEN_BASE_URL;
105
+
106
+ if (!apiKey) {
107
+ console.log(chalk.yellow(' 未配置 API Key,无法拉取 responses 模型列表。'));
108
+ const { modelId } = await inquirer.prompt([
109
+ {
110
+ type: 'input',
111
+ name: 'modelId',
112
+ prefix: '',
113
+ message: '或直接输入模型 ID:',
114
+ default: current,
115
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
116
+ },
117
+ ]);
118
+ pendingConfig.model = modelId.trim();
119
+ pendingConfig.models = [pendingConfig.model];
120
+ applyCodexConfig({
121
+ baseUrl: listBase,
122
+ model: pendingConfig.model,
123
+ models: pendingConfig.models,
124
+ apiKey: apiKey || undefined,
125
+ });
126
+ console.log(chalk.green(' ✓ 模型已更新:' + pendingConfig.model));
127
+ console.log(chalk.gray(' (已写入 ' + CONFIG_PATH + ')'));
128
+ console.log('');
129
+ return;
130
+ }
131
+
132
+ const fetched = await fetchModelsForBaseUrl(listBase, apiKey, {
133
+ requiredEndpointType: 'responses',
134
+ });
135
+
136
+ if (fetched.ok && fetched.models.length > 0) {
137
+ let ids = [...fetched.models];
138
+ if (current && !ids.includes(current)) {
139
+ ids = [current, ...ids];
140
+ }
141
+
142
+ const modelChoices = ids.map((id) => ({
143
+ name: current === id ? chalk.green('► ' + id) : ' ' + id,
144
+ value: id,
145
+ }));
146
+ modelChoices.push(new inquirer.Separator());
147
+ modelChoices.push({ name: ' 手动输入其他模型 ID…', value: '__manual__' });
148
+
149
+ const defaultValue = current && ids.includes(current) ? current : ids[0];
150
+
151
+ const { model } = await inquirer.prompt([
152
+ {
153
+ type: 'list',
154
+ name: 'model',
155
+ prefix: '',
156
+ message: '请选择 Codex responses 模型:',
157
+ choices: modelChoices,
158
+ default: defaultValue,
159
+ pageSize: 15,
160
+ },
161
+ ]);
162
+
163
+ if (model === '__manual__') {
164
+ const { modelId } = await inquirer.prompt([
165
+ {
166
+ type: 'input',
167
+ name: 'modelId',
168
+ prefix: '',
169
+ message: '请输入模型 ID:',
170
+ default: current || '',
171
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
172
+ },
173
+ ]);
174
+ pendingConfig.model = modelId.trim();
175
+ } else {
176
+ pendingConfig.model = model;
177
+ }
178
+ pendingConfig.models = ids;
179
+ } else {
180
+ console.log(chalk.yellow(' 无法拉取 responses 模型列表:' + (fetched.error || '未知错误')));
181
+ console.log(chalk.gray(' 请检查 Base URL、网络或 API Key 后重试,或直接输入模型 ID。'));
182
+ const { modelId } = await inquirer.prompt([
183
+ {
184
+ type: 'input',
185
+ name: 'modelId',
186
+ prefix: '',
187
+ message: '请输入模型 ID:',
188
+ default: current,
189
+ validate: (v) => (v && v.trim() ? true : '请输入模型 ID'),
190
+ },
191
+ ]);
192
+ pendingConfig.model = modelId.trim();
193
+ pendingConfig.models = [pendingConfig.model];
194
+ }
195
+
196
+ applyCodexConfig({
197
+ baseUrl: listBase,
198
+ model: pendingConfig.model,
199
+ models: pendingConfig.models,
200
+ apiKey: apiKey || undefined,
201
+ });
202
+ console.log(chalk.green(' ✓ 模型已更新:' + pendingConfig.model));
203
+ console.log(chalk.gray(' (已写入 ' + CONFIG_PATH + ')'));
204
+ console.log('');
205
+ }
206
+
207
+ function printAuthHint(hasKey) {
208
+ console.log(
209
+ chalk.gray(
210
+ ' API Key 保存在 ' +
211
+ AUTH_PATH +
212
+ '(' +
213
+ CODEX_AUTH_KEY +
214
+ ')' +
215
+ (hasKey ? ',可直接运行 codex 或使用 IDE 插件。' : ';保存配置时请填写 API Key。'),
216
+ ),
217
+ );
218
+ }
219
+
220
+ function doApplyConfig() {
221
+ const ev = getEffectiveCodexEnv();
222
+ const toWrite = {};
223
+ if (pendingConfig.baseUrl !== null) toWrite.baseUrl = pendingConfig.baseUrl;
224
+ if (pendingConfig.model !== null) toWrite.model = pendingConfig.model;
225
+ if (pendingConfig.models !== null) toWrite.models = pendingConfig.models;
226
+ if (pendingConfig.apiKey !== null && pendingConfig.apiKey) {
227
+ toWrite.apiKey = pendingConfig.apiKey;
228
+ }
229
+
230
+ console.log('');
231
+ if (Object.keys(toWrite).length > 0) {
232
+ applyCodexConfig(toWrite);
233
+ console.log(chalk.green(' ✓ 配置已成功写入'));
234
+ } else {
235
+ applyCodexConfig({});
236
+ console.log(chalk.gray(' (无 URL / 模型变更;已确保 Opentoken responses provider 存在)'));
237
+ }
238
+ console.log(chalk.gray(' ' + CONFIG_PATH));
239
+ if (toWrite.apiKey) {
240
+ console.log(chalk.gray(' ' + AUTH_PATH));
241
+ }
242
+ printAuthHint(Boolean(ev.apiKey || toWrite.apiKey));
243
+ console.log('');
244
+ }
245
+
246
+ function paintScreen() {
247
+ clearScreen();
248
+ printLogo();
249
+ }
250
+
251
+ async function run() {
252
+ resetPending();
253
+
254
+ while (true) {
255
+ paintScreen();
256
+
257
+ const { action } = await inquirer.prompt([
258
+ {
259
+ type: 'list',
260
+ name: 'action',
261
+ prefix: '',
262
+ message: 'Codex 配置(~/.codex/config.toml):',
263
+ choices: buildCodexMenuChoices(),
264
+ pageSize: 12,
265
+ },
266
+ ]);
267
+
268
+ switch (action) {
269
+ case 'baseUrl':
270
+ paintScreen();
271
+ await handleConfigBaseUrl();
272
+ break;
273
+ case 'apiKey':
274
+ paintScreen();
275
+ await handleConfigApiKey();
276
+ break;
277
+ case 'model':
278
+ paintScreen();
279
+ await handleConfigModel();
280
+ break;
281
+ case 'apply':
282
+ doApplyConfig();
283
+ return;
284
+ case 'back':
285
+ return;
286
+ }
287
+ }
288
+ }
289
+
290
+ async function quickSet({ key, url, model }) {
291
+ const toWrite = {};
292
+ if (url) toWrite.baseUrl = url;
293
+ if (model) toWrite.model = model;
294
+ if (key) toWrite.apiKey = key;
295
+
296
+ if (key) {
297
+ const listBase = url || getCurrentCodexConfig().baseUrl || CODEX_OPENTOKEN_BASE_URL;
298
+ const fetched = await fetchModelsForBaseUrl(listBase, key, {
299
+ requiredEndpointType: 'responses',
300
+ });
301
+ if (fetched.ok && fetched.models.length > 0) {
302
+ toWrite.models = fetched.models;
303
+ if (!toWrite.model) toWrite.model = fetched.models[0];
304
+ }
305
+ }
306
+
307
+ if (Object.keys(toWrite).length === 0) return false;
308
+
309
+ applyCodexConfig(toWrite);
310
+
311
+ console.log('');
312
+ console.log(chalk.bold(chalk.cyan('Codex 配置已更新:')));
313
+ if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
314
+ if (url) console.log(chalk.green(' ✓ Base URL : ') + url);
315
+ if (toWrite.model) console.log(chalk.green(' ✓ 模型 : ') + toWrite.model);
316
+ if (toWrite.models) {
317
+ console.log(chalk.gray(' (包含 ' + toWrite.models.length + ' 个 responses 模型)'));
318
+ }
319
+ console.log(chalk.gray(' ' + CONFIG_PATH));
320
+ if (key) console.log(chalk.gray(' ' + AUTH_PATH));
321
+ printAuthHint(Boolean(key || getCurrentCodexConfig().apiKey));
322
+ console.log('');
323
+
324
+ return true;
325
+ }
326
+
327
+ module.exports = { run, quickSet };