tokenmix 0.4.13 → 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 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:
@@ -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: 15000,
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: 15000,
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: 15000,
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: 15000 });
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: 15000, validateStatus: () => true });
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmix",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "Zero-config CLI to use any open-source coding agent with TokenMix as the unified LLM backend.",
5
5
  "type": "module",
6
6
  "bin": {