tokenmix 0.4.12 → 0.4.14
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 +8 -0
- package/dist/api/client.js +33 -12
- package/dist/commands/welcome.js +28 -0
- package/dist/i18n/messages.js +43 -0
- package/dist/program.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,14 @@ TOKENMIX_DEFAULT_MODEL=qwen-flash npx tokenmix aider
|
|
|
102
102
|
|
|
103
103
|
(`tokenmix claude` and `tokenmix codex` speak the Anthropic / Responses protocols, so they need a Claude-family model; the other agents accept any chat model.)
|
|
104
104
|
|
|
105
|
+
## Slow or restricted networks
|
|
106
|
+
|
|
107
|
+
Requests auto-retry transient network failures (so a single hiccup won't fail a command) and time out after 20s. On a slow, proxied, or firewalled connection, raise the timeout with `TOKENMIX_TIMEOUT_MS`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
TOKENMIX_TIMEOUT_MS=60000 npx tokenmix login
|
|
111
|
+
```
|
|
112
|
+
|
|
105
113
|
## Configuration Location
|
|
106
114
|
|
|
107
115
|
Your TokenMix credentials are stored locally at:
|
package/dist/api/client.js
CHANGED
|
@@ -22,7 +22,28 @@ function handleAxios(err) {
|
|
|
22
22
|
e.message;
|
|
23
23
|
throw new ApiError(e.response.status, msg);
|
|
24
24
|
}
|
|
25
|
-
throw new ApiError(0, `Could not reach the TokenMix API (${e.message || 'network error'}). Check your internet connection.`);
|
|
25
|
+
throw new ApiError(0, `Could not reach the TokenMix API (${e.message || 'network error'}). Check your internet connection or proxy.`);
|
|
26
|
+
}
|
|
27
|
+
// Network resilience for flaky / slow / GFW'd connections: a generous default
|
|
28
|
+
// timeout (override with TOKENMIX_TIMEOUT_MS) and automatic retry of transient
|
|
29
|
+
// TRANSPORT failures (no HTTP response) with exponential backoff. HTTP errors
|
|
30
|
+
// (4xx/5xx) are real answers and are NEVER retried.
|
|
31
|
+
export const REQUEST_TIMEOUT_MS = Number(process.env.TOKENMIX_TIMEOUT_MS) || 20000;
|
|
32
|
+
const MAX_RETRIES = 2;
|
|
33
|
+
export async function withRetry(fn) {
|
|
34
|
+
let lastErr;
|
|
35
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
36
|
+
try {
|
|
37
|
+
return await fn();
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err.response || attempt === MAX_RETRIES)
|
|
41
|
+
throw err;
|
|
42
|
+
lastErr = err;
|
|
43
|
+
await new Promise((r) => setTimeout(r, 300 * 2 ** attempt));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw lastErr;
|
|
26
47
|
}
|
|
27
48
|
// Public endpoint, no auth required.
|
|
28
49
|
// Note: backend pagination uses `per_page` (NOT `page_size`); max 500 (anything >500 falls back to 20).
|
|
@@ -30,10 +51,10 @@ function handleAxios(err) {
|
|
|
30
51
|
export async function listPublicModels(cfg) {
|
|
31
52
|
const c = cfg || (await readConfig());
|
|
32
53
|
try {
|
|
33
|
-
const r = await axios.get(`${apiBaseUrl(c)}/api/models`, {
|
|
54
|
+
const r = await withRetry(() => axios.get(`${apiBaseUrl(c)}/api/models`, {
|
|
34
55
|
params: { per_page: 500 },
|
|
35
|
-
timeout:
|
|
36
|
-
});
|
|
56
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
57
|
+
}));
|
|
37
58
|
const list = unwrap(r);
|
|
38
59
|
return Array.isArray(list) ? list : list?.list ?? [];
|
|
39
60
|
}
|
|
@@ -49,11 +70,11 @@ export async function verifyApiKey(apiKey, baseUrl) {
|
|
|
49
70
|
// then raises a clear "could not reach the API" ApiError, letting callers tell a
|
|
50
71
|
// network problem apart from a genuinely bad key.
|
|
51
72
|
try {
|
|
52
|
-
const r = await axios.get(`${baseUrl || DEFAULT_API_BASE}/v1/models`, {
|
|
73
|
+
const r = await withRetry(() => axios.get(`${baseUrl || DEFAULT_API_BASE}/v1/models`, {
|
|
53
74
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
54
|
-
timeout:
|
|
75
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
55
76
|
validateStatus: () => true,
|
|
56
|
-
});
|
|
77
|
+
}));
|
|
57
78
|
return r.status === 200;
|
|
58
79
|
}
|
|
59
80
|
catch (err) {
|
|
@@ -62,10 +83,10 @@ export async function verifyApiKey(apiKey, baseUrl) {
|
|
|
62
83
|
}
|
|
63
84
|
export async function fetchWallet(apiKey, baseUrl) {
|
|
64
85
|
try {
|
|
65
|
-
const r = await axios.get(`${baseUrl || DEFAULT_API_BASE}/v1/wallet`, {
|
|
86
|
+
const r = await withRetry(() => axios.get(`${baseUrl || DEFAULT_API_BASE}/v1/wallet`, {
|
|
66
87
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
67
|
-
timeout:
|
|
68
|
-
});
|
|
88
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
89
|
+
}));
|
|
69
90
|
return unwrap(r);
|
|
70
91
|
}
|
|
71
92
|
catch (err) {
|
|
@@ -81,7 +102,7 @@ export class DeviceFlowError extends Error {
|
|
|
81
102
|
}
|
|
82
103
|
export async function startDeviceAuthorization(baseUrl, clientName = 'tokenmix-cli') {
|
|
83
104
|
try {
|
|
84
|
-
const r = await axios.post(`${baseUrl}/api/auth/device/code`, { client_name: clientName }, { timeout:
|
|
105
|
+
const r = await withRetry(() => axios.post(`${baseUrl}/api/auth/device/code`, { client_name: clientName }, { timeout: REQUEST_TIMEOUT_MS }));
|
|
85
106
|
return unwrap(r);
|
|
86
107
|
}
|
|
87
108
|
catch (err) {
|
|
@@ -106,7 +127,7 @@ export async function pollDeviceToken(baseUrl, auth, onTick) {
|
|
|
106
127
|
onTick(Math.max(0, Math.round((deadline - Date.now()) / 1000)));
|
|
107
128
|
}
|
|
108
129
|
try {
|
|
109
|
-
const r = await axios.post(`${baseUrl}/api/auth/device/token`, { device_code: auth.device_code }, { timeout:
|
|
130
|
+
const r = await axios.post(`${baseUrl}/api/auth/device/token`, { device_code: auth.device_code }, { timeout: REQUEST_TIMEOUT_MS, validateStatus: () => true });
|
|
110
131
|
if (r.status === 200 && r.data?.code === 0) {
|
|
111
132
|
const body = r.data.data;
|
|
112
133
|
return {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readConfig } from '../config/store.js';
|
|
3
|
+
import { t } from '../i18n/index.js';
|
|
4
|
+
// Shown for a bare `tokenmix` (no command) — friendly, localized onboarding for
|
|
5
|
+
// first-timers instead of commander's raw help. `tokenmix --help` still prints the
|
|
6
|
+
// full command reference. Adapts to whether the user is logged in yet.
|
|
7
|
+
export async function welcomeCommand() {
|
|
8
|
+
const cfg = await readConfig();
|
|
9
|
+
const loggedIn = Boolean(cfg.apiKey);
|
|
10
|
+
const row = (cmd, desc) => ' ' + chalk.cyan(cmd.padEnd(21)) + chalk.dim(desc);
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(' ' + chalk.bold.cyan('TokenMix') + chalk.dim(' — ' + t('welcome.tagline')));
|
|
13
|
+
console.log();
|
|
14
|
+
if (loggedIn) {
|
|
15
|
+
console.log(' ' + chalk.green('✓') + ' ' + t('welcome.loggedIn'));
|
|
16
|
+
console.log(row('tokenmix opencode', t('welcome.s3')));
|
|
17
|
+
console.log(row('tokenmix list', t('welcome.s2')));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(' ' + chalk.bold(t('welcome.start')));
|
|
21
|
+
console.log(row('1. tokenmix login', t('welcome.s1')));
|
|
22
|
+
console.log(row('2. tokenmix list', t('welcome.s2')));
|
|
23
|
+
console.log(row('3. tokenmix opencode', t('welcome.s3')));
|
|
24
|
+
}
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(' ' + chalk.dim(t('welcome.more')));
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
package/dist/i18n/messages.js
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
// `{name}`-style placeholders are filled by t() at call time.
|
|
7
7
|
export const en = {
|
|
8
8
|
'common.notLoggedIn': 'Not logged in. Run `tokenmix login` first.',
|
|
9
|
+
// welcome screen (bare `tokenmix`, no args — onboarding for first-time users)
|
|
10
|
+
'welcome.tagline': 'one account, 160+ models, every open-source coding agent',
|
|
11
|
+
'welcome.start': 'Get started:',
|
|
12
|
+
'welcome.s1': 'log in (opens a browser, ~10s)',
|
|
13
|
+
'welcome.s2': 'see all 11 supported agents',
|
|
14
|
+
'welcome.s3': 'configure & launch one',
|
|
15
|
+
'welcome.loggedIn': "You're logged in — launch an agent:",
|
|
16
|
+
'welcome.more': 'More: `tokenmix --help` for all commands · `tokenmix balance` for your balance',
|
|
9
17
|
// login
|
|
10
18
|
'login.keyMustStart': 'API key should start with sk-tm-',
|
|
11
19
|
'login.verifying': 'Verifying API key against {baseUrl} ...',
|
|
@@ -175,6 +183,13 @@ export const en = {
|
|
|
175
183
|
};
|
|
176
184
|
export const zh = {
|
|
177
185
|
'common.notLoggedIn': '未登录,请先运行 `tokenmix login`。',
|
|
186
|
+
'welcome.tagline': '一个账户、160+ 模型、对接所有开源编程 agent',
|
|
187
|
+
'welcome.start': '快速开始:',
|
|
188
|
+
'welcome.s1': '登录(打开浏览器,约 10 秒)',
|
|
189
|
+
'welcome.s2': '查看支持的 11 个 agent',
|
|
190
|
+
'welcome.s3': '配置并启动一个',
|
|
191
|
+
'welcome.loggedIn': '你已登录 —— 启动一个 agent:',
|
|
192
|
+
'welcome.more': '更多:`tokenmix --help` 查看全部命令 · `tokenmix balance` 查看余额',
|
|
178
193
|
// login
|
|
179
194
|
'login.keyMustStart': 'API 密钥应以 sk-tm- 开头',
|
|
180
195
|
'login.verifying': '正在通过 {baseUrl} 校验 API 密钥 ...',
|
|
@@ -337,6 +352,13 @@ export const zh = {
|
|
|
337
352
|
};
|
|
338
353
|
export const ja = {
|
|
339
354
|
'common.notLoggedIn': 'ログインしていません。まず `tokenmix login` を実行してください。',
|
|
355
|
+
'welcome.tagline': '1 つのアカウントで 160+ モデル、あらゆるオープンソース・コーディング agent に対応',
|
|
356
|
+
'welcome.start': 'はじめに:',
|
|
357
|
+
'welcome.s1': 'ログイン(ブラウザが開きます、約 10 秒)',
|
|
358
|
+
'welcome.s2': 'サポートされている 11 の agent を表示',
|
|
359
|
+
'welcome.s3': '設定して起動',
|
|
360
|
+
'welcome.loggedIn': 'ログイン済みです —— agent を起動:',
|
|
361
|
+
'welcome.more': 'その他:`tokenmix --help` で全コマンド · `tokenmix balance` で残高',
|
|
340
362
|
'login.keyMustStart': 'API キーは sk-tm- で始まる必要があります',
|
|
341
363
|
'login.verifying': '{baseUrl} で API キーを検証しています ...',
|
|
342
364
|
'login.verifyFailed': 'API キーの検証に失敗しました。https://tokenmix.ai/dashboard/keys で確認してください',
|
|
@@ -484,6 +506,13 @@ export const ja = {
|
|
|
484
506
|
};
|
|
485
507
|
export const ko = {
|
|
486
508
|
'common.notLoggedIn': '로그인되어 있지 않습니다. 먼저 `tokenmix login`을 실행하세요.',
|
|
509
|
+
'welcome.tagline': '하나의 계정으로 160+ 모델, 모든 오픈소스 코딩 agent 지원',
|
|
510
|
+
'welcome.start': '시작하기:',
|
|
511
|
+
'welcome.s1': '로그인 (브라우저가 열립니다, 약 10초)',
|
|
512
|
+
'welcome.s2': '지원되는 11개 agent 보기',
|
|
513
|
+
'welcome.s3': '설정하고 실행',
|
|
514
|
+
'welcome.loggedIn': '로그인되었습니다 —— agent를 실행하세요:',
|
|
515
|
+
'welcome.more': '더 보기: `tokenmix --help` 모든 명령 · `tokenmix balance` 잔액',
|
|
487
516
|
'login.keyMustStart': 'API 키는 sk-tm- 로 시작해야 합니다',
|
|
488
517
|
'login.verifying': '{baseUrl} 에서 API 키를 확인하는 중 ...',
|
|
489
518
|
'login.verifyFailed': 'API 키 확인에 실패했습니다. https://tokenmix.ai/dashboard/keys 에서 확인하세요',
|
|
@@ -631,6 +660,13 @@ export const ko = {
|
|
|
631
660
|
};
|
|
632
661
|
export const es = {
|
|
633
662
|
'common.notLoggedIn': 'No has iniciado sesión. Ejecuta `tokenmix login` primero.',
|
|
663
|
+
'welcome.tagline': 'una cuenta, 160+ modelos, todos los agentes de programación de código abierto',
|
|
664
|
+
'welcome.start': 'Empezar:',
|
|
665
|
+
'welcome.s1': 'inicia sesión (abre el navegador, ~10 s)',
|
|
666
|
+
'welcome.s2': 've los 11 agentes compatibles',
|
|
667
|
+
'welcome.s3': 'configura y lanza uno',
|
|
668
|
+
'welcome.loggedIn': 'Has iniciado sesión: lanza un agente:',
|
|
669
|
+
'welcome.more': 'Más: `tokenmix --help` para todos los comandos · `tokenmix balance` para tu saldo',
|
|
634
670
|
'login.keyMustStart': 'La clave de API debe empezar por sk-tm-',
|
|
635
671
|
'login.verifying': 'Verificando la clave de API en {baseUrl} ...',
|
|
636
672
|
'login.verifyFailed': 'Falló la verificación de la clave de API. Compruébala en https://tokenmix.ai/dashboard/keys',
|
|
@@ -778,6 +814,13 @@ export const es = {
|
|
|
778
814
|
};
|
|
779
815
|
export const fr = {
|
|
780
816
|
'common.notLoggedIn': 'Non connecté. Exécutez d’abord `tokenmix login`.',
|
|
817
|
+
'welcome.tagline': 'un seul compte, 160+ modèles, tous les agents de codage open source',
|
|
818
|
+
'welcome.start': 'Démarrer :',
|
|
819
|
+
'welcome.s1': 'connectez-vous (ouvre un navigateur, ~10 s)',
|
|
820
|
+
'welcome.s2': 'voir les 11 agents pris en charge',
|
|
821
|
+
'welcome.s3': 'configurer et lancer un agent',
|
|
822
|
+
'welcome.loggedIn': 'Vous êtes connecté — lancez un agent :',
|
|
823
|
+
'welcome.more': 'Plus : `tokenmix --help` pour toutes les commandes · `tokenmix balance` pour votre solde',
|
|
781
824
|
'login.keyMustStart': 'La clé API doit commencer par sk-tm-',
|
|
782
825
|
'login.verifying': 'Vérification de la clé API sur {baseUrl} ...',
|
|
783
826
|
'login.verifyFailed': 'Échec de la vérification de la clé API. Vérifiez sur https://tokenmix.ai/dashboard/keys',
|
package/dist/program.js
CHANGED
|
@@ -7,6 +7,7 @@ import { topupCommand } from './commands/topup.js';
|
|
|
7
7
|
import { modelsCommand } from './commands/models.js';
|
|
8
8
|
import { listCommand } from './commands/list.js';
|
|
9
9
|
import { doctorCommand } from './commands/doctor.js';
|
|
10
|
+
import { welcomeCommand } from './commands/welcome.js';
|
|
10
11
|
import { registerAgentCommands } from './commands/agent-runner.js';
|
|
11
12
|
import { t } from './i18n/index.js';
|
|
12
13
|
// Read version from package.json so we never have to bump it in two places.
|
|
@@ -22,7 +23,9 @@ export function buildProgram(deps = {}) {
|
|
|
22
23
|
program
|
|
23
24
|
.name('tokenmix')
|
|
24
25
|
.description(t('cmd.program'))
|
|
25
|
-
.version(pkg.version)
|
|
26
|
+
.version(pkg.version)
|
|
27
|
+
// Bare `tokenmix` (no command) shows a friendly onboarding screen, not raw help.
|
|
28
|
+
.action(welcomeCommand);
|
|
26
29
|
program
|
|
27
30
|
.command('login')
|
|
28
31
|
.description(t('cmd.login'))
|