claude-scionos 4.4.0 → 4.6.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.
package/README.fr.md CHANGED
@@ -67,21 +67,20 @@ npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
67
67
  - `--service llm` bascule le lanceur vers `https://llm.routerlab.ch`
68
68
  - `llm` est prévu pour un accès sur invitation
69
69
  - les tokens enregistrés avec `auth login --service llm` sont stockés séparément du token RouterLab par défaut
70
- - `llm` expose pour l'instant `claude`, `claude-gpt`, `claude-gpt-special` et `deepseek-v4-beta`
71
- - `routerlab` expose aussi `claude-gpt`, `claude-minimax-m2.7` et `claude-glm-5.1`
70
+ - `llm` expose pour l'instant `claude`, `claude-gpt`, `claude-gpt-special`, `deepseek-v4-beta` et `claude-glm-5.1`
71
+ - `routerlab` expose aussi `aws`, `claude-gpt`, `deepseek-v4-beta`, `claude-kimi-k2.6` et `claude-glm-5.1`
72
72
 
73
73
  ## Stratégies
74
74
 
75
75
  - `default` : utilise Claude Code normalement sans proxy local
76
- - `aws` : remappe les familles de modèles Claude vers les variantes Claude AWS de RouterLab
77
- - `claude` : remappe les familles de modèles Claude sur `--service llm` vers les variantes Claude standard `claude-haiku-4-5-20251001`, `claude-sonnet-4-6` et `claude-opus-4-6`
78
- - `claude-gpt` : mappe les requêtes Claude vers la famille `claude-gpt`
79
- `claude-gpt-5.5 ==> claude-opus-4.7`, `claude-gpt-5.4 ==> claude-sonnet-4.6`, `claude-gpt-5.4-mini ==> claude-gpt-5.4-mini`
80
- - `claude-gpt-special` : sur `--service llm`, force toutes les requêtes vers `claude-gpt-5.4-sp`
81
- - `deepseek-v4-beta` : sur `--service llm`, mappe les requêtes Claude vers `claude-deepseek-v4-pro` pour opus ou sonnet et `claude-deepseek-v4-flash` pour haiku
82
- - `claude-qwen3.6-plus` : force toutes les requêtes vers `claude-qwen3.6-plus`
83
- - `claude-minimax-m2.7` : force toutes les requêtes vers `claude-minimax-m2.7`
84
- - `claude-glm-5.1` : force toutes les requêtes vers `claude-glm-5.1`
76
+ - `aws` : remappe les familles de modèles Claude vers les variantes Claude AWS de RouterLab
77
+ - `claude` : remappe les familles de modèles Claude sur `--service llm` vers les variantes Claude standard `claude-haiku-4-5-20251001`, `claude-sonnet-4-6` et `claude-opus-4-7`
78
+ - `claude-gpt` : mappe les requêtes Claude vers la famille `claude-gpt`
79
+ les requêtes Opus sont mappées vers `claude-gpt-5.5`, les requêtes Sonnet vers `claude-gpt-5.4` et les requêtes Haiku vers `claude-gpt-5.4-mini`
80
+ - `claude-gpt-special` : sur `--service llm`, force toutes les requêtes vers `claude-gpt-5.4-sp`
81
+ - `deepseek-v4-beta` : mappe les requêtes Claude vers `claude-deepseek-v4-pro` pour opus ou sonnet et `claude-deepseek-v4-flash` pour haiku
82
+ - `claude-kimi-k2.6` : force toutes les requêtes vers `claude-kimi-k2.6`
83
+ - `claude-glm-5.1` : force toutes les requêtes vers `claude-glm-5.1`
85
84
 
86
85
  Utilise `--list-strategies` pour voir les stratégies disponibles pour le service choisi et leur disponibilité réelle quand un token est disponible.
87
86
 
package/README.md CHANGED
@@ -67,21 +67,20 @@ npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
67
67
  - `--service llm` switches the launcher to `https://llm.routerlab.ch`
68
68
  - `llm` is intended for invitation-only access
69
69
  - Tokens stored with `auth login --service llm` are kept separate from the default RouterLab token
70
- - `llm` currently exposes `claude`, `claude-gpt`, `claude-gpt-special`, and `deepseek-v4-beta`
71
- - `routerlab` also exposes `claude-gpt`, `claude-minimax-m2.7`, and `claude-glm-5.1`
70
+ - `llm` currently exposes `claude`, `claude-gpt`, `claude-gpt-special`, `deepseek-v4-beta`, and `claude-glm-5.1`
71
+ - `routerlab` also exposes `aws`, `claude-gpt`, `deepseek-v4-beta`, `claude-kimi-k2.6`, and `claude-glm-5.1`
72
72
 
73
73
  ## Strategies
74
74
 
75
75
  - `default`: use Claude Code normally without the local proxy
76
- - `aws`: remap Claude model families to RouterLab AWS-backed Claude variants
77
- - `claude`: remap Claude model families on `--service llm` to the standard Claude variants `claude-haiku-4-5-20251001`, `claude-sonnet-4-6`, and `claude-opus-4-6`
78
- - `claude-gpt`: map Claude requests to the `claude-gpt` family
79
- `claude-gpt-5.5 ==> claude-opus-4.7`, `claude-gpt-5.4 ==> claude-sonnet-4.6`, `claude-gpt-5.4-mini ==> claude-gpt-5.4-mini`
80
- - `claude-gpt-special`: on `--service llm`, force all requests to `claude-gpt-5.4-sp`
81
- - `deepseek-v4-beta`: on `--service llm`, map Claude requests to `claude-deepseek-v4-pro` for opus or sonnet and `claude-deepseek-v4-flash` for haiku
82
- - `claude-qwen3.6-plus`: force all requests to `claude-qwen3.6-plus`
83
- - `claude-minimax-m2.7`: force all requests to `claude-minimax-m2.7`
84
- - `claude-glm-5.1`: force all requests to `claude-glm-5.1`
76
+ - `aws`: remap Claude model families to RouterLab AWS-backed Claude variants
77
+ - `claude`: remap Claude model families on `--service llm` to the standard Claude variants `claude-haiku-4-5-20251001`, `claude-sonnet-4-6`, and `claude-opus-4-7`
78
+ - `claude-gpt`: map Claude requests to the `claude-gpt` family
79
+ Opus requests map to `claude-gpt-5.5`, Sonnet requests map to `claude-gpt-5.4`, and Haiku requests map to `claude-gpt-5.4-mini`
80
+ - `claude-gpt-special`: on `--service llm`, force all requests to `claude-gpt-5.4-sp`
81
+ - `deepseek-v4-beta`: map Claude requests to `claude-deepseek-v4-pro` for opus or sonnet and `claude-deepseek-v4-flash` for haiku
82
+ - `claude-kimi-k2.6`: force all requests to `claude-kimi-k2.6`
83
+ - `claude-glm-5.1`: force all requests to `claude-glm-5.1`
85
84
 
86
85
  Use `--list-strategies` to see the strategies available for the selected service and their live availability when a token is available.
87
86
 
package/index.js CHANGED
@@ -52,6 +52,23 @@ import { startProxyServer } from './src/proxy.js';
52
52
 
53
53
  const require = createRequire(import.meta.url);
54
54
  const pkg = require('./package.json');
55
+ const SIGNAL_EXIT_CODES = {
56
+ SIGHUP: 129,
57
+ SIGINT: 130,
58
+ SIGQUIT: 131,
59
+ SIGILL: 132,
60
+ SIGTRAP: 133,
61
+ SIGABRT: 134,
62
+ SIGBUS: 135,
63
+ SIGFPE: 136,
64
+ SIGKILL: 137,
65
+ SIGUSR1: 138,
66
+ SIGSEGV: 139,
67
+ SIGUSR2: 140,
68
+ SIGPIPE: 141,
69
+ SIGALRM: 142,
70
+ SIGTERM: 143
71
+ };
55
72
 
56
73
  function normalizeEntrypointPath(candidate) {
57
74
  if (!candidate) {
@@ -68,6 +85,40 @@ function normalizeEntrypointPath(candidate) {
68
85
  }
69
86
  }
70
87
 
88
+ function shouldSpawnWithShell(commandPath) {
89
+ if (process.platform !== 'win32') {
90
+ return false;
91
+ }
92
+
93
+ const extension = path.extname(commandPath || '').toLowerCase();
94
+ return extension === '.cmd' || extension === '.bat';
95
+ }
96
+
97
+ function resolveProcessExitCode(code, signal) {
98
+ if (typeof code === 'number') {
99
+ return code;
100
+ }
101
+
102
+ if (signal) {
103
+ return SIGNAL_EXIT_CODES[signal] ?? 1;
104
+ }
105
+
106
+ return 1;
107
+ }
108
+
109
+ function createCleanupOnce(cleanup) {
110
+ let cleaned = false;
111
+
112
+ return () => {
113
+ if (cleaned) {
114
+ return;
115
+ }
116
+
117
+ cleaned = true;
118
+ cleanup();
119
+ };
120
+ }
121
+
71
122
  // --- UTILS ---
72
123
 
73
124
  function showBanner() {
@@ -184,6 +235,23 @@ function resolveSelectedService(serviceValue) {
184
235
  throw new Error(`Unknown service "${serviceValue}". Supported values: ${supported}.`);
185
236
  }
186
237
 
238
+ function getRequiredOptionValue(argv, index, optionName) {
239
+ const value = argv[index + 1];
240
+ if (!value || value.startsWith('--')) {
241
+ throw new Error(`${optionName} requires a value.`);
242
+ }
243
+
244
+ return value;
245
+ }
246
+
247
+ function getRequiredInlineOptionValue(value, optionName) {
248
+ if (!value) {
249
+ throw new Error(`${optionName} requires a value.`);
250
+ }
251
+
252
+ return value;
253
+ }
254
+
187
255
  function parseWrapperArgs(argv) {
188
256
  const parsed = {
189
257
  authAction: 'status',
@@ -201,6 +269,11 @@ function parseWrapperArgs(argv) {
201
269
  for (let index = 0; index < argv.length; index += 1) {
202
270
  const arg = argv[index];
203
271
 
272
+ if (arg === '--') {
273
+ parsed.claudeArgs.push(...argv.slice(index + 1));
274
+ break;
275
+ }
276
+
204
277
  if ((arg === 'doctor' || arg === 'auth') && parsed.command === null && parsed.claudeArgs.length === 0) {
205
278
  parsed.command = arg;
206
279
  if (arg === 'auth') {
@@ -224,24 +297,24 @@ function parseWrapperArgs(argv) {
224
297
  }
225
298
 
226
299
  if (arg === '--strategy') {
227
- parsed.strategy = normalizeStrategyValue(argv[index + 1]);
300
+ parsed.strategy = normalizeStrategyValue(getRequiredOptionValue(argv, index, '--strategy'));
228
301
  index += 1;
229
302
  continue;
230
303
  }
231
304
 
232
305
  if (arg === '--service') {
233
- parsed.service = normalizeServiceValue(argv[index + 1]);
306
+ parsed.service = normalizeServiceValue(getRequiredOptionValue(argv, index, '--service'));
234
307
  index += 1;
235
308
  continue;
236
309
  }
237
310
 
238
311
  if (arg.startsWith('--strategy=')) {
239
- parsed.strategy = normalizeStrategyValue(arg.slice('--strategy='.length));
312
+ parsed.strategy = normalizeStrategyValue(getRequiredInlineOptionValue(arg.slice('--strategy='.length), '--strategy'));
240
313
  continue;
241
314
  }
242
315
 
243
316
  if (arg.startsWith('--service=')) {
244
- parsed.service = normalizeServiceValue(arg.slice('--service='.length));
317
+ parsed.service = normalizeServiceValue(getRequiredInlineOptionValue(arg.slice('--service='.length), '--service'));
245
318
  continue;
246
319
  }
247
320
 
@@ -255,7 +328,7 @@ function parseWrapperArgs(argv) {
255
328
  continue;
256
329
  }
257
330
 
258
- if (arg === '--scionos-debug') {
331
+ if (arg === '--scionos-debug' || arg === '--scionos') {
259
332
  parsed.debug = true;
260
333
  continue;
261
334
  }
@@ -562,15 +635,12 @@ async function runDoctor(serviceConfig) {
562
635
  showStatus('Env token', getEnvironmentToken() ? 'ok' : 'warn', getEnvironmentToken() ? 'available' : 'not set');
563
636
  console.log('');
564
637
 
565
- const envToken = getEnvironmentToken();
566
- const candidate = envToken
567
- ? { token: envToken, source: 'environment' }
568
- : { token: null, source: storedStatus.stored ? 'secure-store' : 'none' };
638
+ const candidate = getAvailableTokenCandidate(serviceConfig.value);
569
639
  let validation = null;
570
640
 
571
641
  if (!candidate.token) {
572
- showStatus(`${serviceConfig.tokenPromptLabel} auth`, 'warn', storedStatus.stored
573
- ? 'Skipped: stored token available but not validated during doctor on Windows'
642
+ showStatus(`${serviceConfig.tokenPromptLabel} auth`, 'warn', candidate.source === 'secure-store-error'
643
+ ? 'Skipped: stored token is present but could not be read'
574
644
  : 'Skipped: no environment or stored token available');
575
645
  } else {
576
646
  validation = await validateToken(candidate.token, {
@@ -714,6 +784,9 @@ async function main() {
714
784
  proxyServer = proxyInfo.server;
715
785
  finalBaseUrl = proxyInfo.url; // e.g. http://127.0.0.1:54321
716
786
  if (interactive && isDebug) console.log(chalk.gray(`✓ Proxy listening on ${finalBaseUrl}`));
787
+ if (interactive && isDebug && !proxyInfo.authenticated) {
788
+ console.log(chalk.yellow('WARN Proxy authentication is not enabled because Claude Code header injection is not verified; binding remains limited to 127.0.0.1.'));
789
+ }
717
790
  }
718
791
 
719
792
  const env = {
@@ -737,19 +810,19 @@ async function main() {
737
810
  const child = spawn(claudeStatus.cliPath, parsed.claudeArgs, {
738
811
  stdio: 'inherit',
739
812
  env: env,
740
- shell: process.platform === 'win32'
813
+ shell: shouldSpawnWithShell(claudeStatus.cliPath)
741
814
  });
742
815
 
743
- const cleanup = () => {
816
+ const cleanup = createCleanupOnce(() => {
744
817
  if (proxyServer) {
745
818
  if (interactive && isDebug) console.log(chalk.gray('\nStopping proxy server...'));
746
819
  proxyServer.close();
747
820
  }
748
- };
821
+ });
749
822
 
750
- child.on('exit', (code) => {
823
+ child.on('exit', (code, signal) => {
751
824
  cleanup();
752
- process.exit(code ?? 0);
825
+ process.exit(resolveProcessExitCode(code, signal));
753
826
  });
754
827
 
755
828
  child.on('error', (err) => {
@@ -772,7 +845,7 @@ async function main() {
772
845
  process.on('SIGTERM', () => {
773
846
  if (child) child.kill('SIGTERM');
774
847
  cleanup();
775
- process.exit(0);
848
+ process.exit(resolveProcessExitCode(null, 'SIGTERM'));
776
849
  });
777
850
  }
778
851
 
@@ -787,9 +860,13 @@ if (isEntrypoint) {
787
860
 
788
861
  export {
789
862
  canProceedWithValidation,
863
+ createCleanupOnce,
790
864
  installClaudeCode,
791
865
  main,
792
866
  normalizeEntrypointPath,
867
+ parseWrapperArgs,
868
+ resolveProcessExitCode,
793
869
  resolveLaunchToken,
870
+ shouldSpawnWithShell,
794
871
  validateToken
795
872
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scionos",
3
- "version": "4.4.0",
3
+ "version": "4.6.0",
4
4
  "description": "RouterLab launcher, strategy proxy and secure token wrapper for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -47,8 +47,8 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "eslint": "^10.2.1",
51
- "globals": "^17.5.0",
50
+ "eslint": "^10.3.0",
51
+ "globals": "^17.6.0",
52
52
  "vitest": "^4.1.5"
53
53
  }
54
54
  }
package/src/proxy.js CHANGED
@@ -17,6 +17,8 @@ const HOP_BY_HOP_HEADERS = new Set([
17
17
  ]);
18
18
  const PROXY_AUTH_HEADER = 'x-scionos-proxy-secret';
19
19
  const MESSAGES_PATH = '/v1/messages';
20
+ const COUNT_TOKENS_PATH = '/v1/messages/count_tokens';
21
+ const MODELS_PATH = '/v1/models';
20
22
  const REASONING_CONTENT_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking']);
21
23
  const REASONING_DELTA_TYPES = new Set(['thinking_delta', 'signature_delta']);
22
24
 
@@ -128,17 +130,17 @@ function getPreferredClaudeGptModel(requestedModel = '') {
128
130
  return 'claude-gpt-5.4';
129
131
  }
130
132
 
131
- function getPreferredClaudeModel(requestedModel = '') {
132
- if (requestedModel.includes('haiku') || requestedModel.includes('mini')) {
133
- return 'claude-haiku-4-5-20251001';
134
- }
135
-
136
- if (requestedModel.includes('opus')) {
137
- return 'claude-opus-4-6';
138
- }
139
-
140
- return 'claude-sonnet-4-6';
141
- }
133
+ function getPreferredClaudeModel(requestedModel = '') {
134
+ if (requestedModel.includes('haiku') || requestedModel.includes('mini')) {
135
+ return 'claude-haiku-4-5-20251001';
136
+ }
137
+
138
+ if (requestedModel.includes('opus')) {
139
+ return 'claude-opus-4-7';
140
+ }
141
+
142
+ return 'claude-sonnet-4-6';
143
+ }
142
144
 
143
145
  function getPreferredDeepseekV4Model(requestedModel = '') {
144
146
  if (requestedModel.includes('opus') || requestedModel.includes('sonnet')) {
@@ -159,15 +161,15 @@ function resolveMappedModel(targetModel, requestedModel = '', availableModels =
159
161
  return preferredModel;
160
162
  }
161
163
 
162
- if (availableClaudeModels.includes(preferredModel)) {
163
- return preferredModel;
164
- }
165
-
166
- return (
167
- availableClaudeModels.find((model) => model === 'claude-sonnet-4-6')
168
- ?? availableClaudeModels[0]
169
- ?? preferredModel
170
- );
164
+ if (availableClaudeModels.includes(preferredModel)) {
165
+ return preferredModel;
166
+ }
167
+
168
+ return (
169
+ availableClaudeModels.find((model) => model === 'claude-sonnet-4-6')
170
+ ?? availableClaudeModels[0]
171
+ ?? preferredModel
172
+ );
171
173
  }
172
174
 
173
175
  if (targetModel === 'claude-gpt-special') {
@@ -224,21 +226,25 @@ function resolveMappedModel(targetModel, requestedModel = '', availableModels =
224
226
  return 'aws-claude-haiku-4-5-20251001';
225
227
  }
226
228
 
227
- if (requestedModel.includes('opus')) {
228
- return 'aws-claude-opus-4-6';
229
- }
229
+ if (requestedModel.includes('opus')) {
230
+ return 'aws-claude-opus-4-7';
231
+ }
230
232
 
231
233
  return 'aws-claude-sonnet-4-6';
232
234
  }
233
235
 
234
- function writeJsonError(res, statusCode, payload) {
235
- if (res.headersSent) {
236
- return;
237
- }
238
-
239
- res.writeHead(statusCode);
240
- res.end(JSON.stringify(payload));
241
- }
236
+ function writeJsonError(res, statusCode, payload) {
237
+ if (res.headersSent) {
238
+ return;
239
+ }
240
+
241
+ const body = JSON.stringify(payload);
242
+ res.writeHead(statusCode, {
243
+ 'content-type': 'application/json; charset=utf-8',
244
+ 'content-length': String(Buffer.byteLength(body)),
245
+ });
246
+ res.end(body);
247
+ }
242
248
 
243
249
  function getRequestPath(req) {
244
250
  return new URL(req.url, 'http://127.0.0.1').pathname;
@@ -252,9 +258,13 @@ function isAuthorizedProxyRequest(req, proxySecret) {
252
258
  return req.headers[PROXY_AUTH_HEADER] === proxySecret;
253
259
  }
254
260
 
255
- function isAllowedProxyRoute(req) {
256
- return req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH;
257
- }
261
+ function isAllowedProxyRoute(req) {
262
+ const path = getRequestPath(req);
263
+ return (
264
+ (req.method === 'POST' && (path === MESSAGES_PATH || path === COUNT_TOKENS_PATH))
265
+ || (req.method === 'GET' && path === MODELS_PATH)
266
+ );
267
+ }
258
268
 
259
269
  async function handleMessageRequest(req, res, options) {
260
270
  const {availableModels = [], baseUrl, debug, onDebug, onError, targetModel, validToken} = options;
@@ -607,7 +617,7 @@ function startProxyServer(targetModel, validToken, options = {}) {
607
617
  } = options;
608
618
 
609
619
  return new Promise((resolve, reject) => {
610
- const server = http.createServer((req, res) => {
620
+ const server = http.createServer((req, res) => {
611
621
  if (!isAuthorizedProxyRequest(req, proxySecret)) {
612
622
  writeJsonError(res, 403, {error: {message: 'Forbidden'}});
613
623
  return;
@@ -618,21 +628,33 @@ function startProxyServer(targetModel, validToken, options = {}) {
618
628
  return;
619
629
  }
620
630
 
621
- handleMessageRequest(req, res, {
622
- availableModels,
623
- baseUrl,
624
- debug,
625
- onDebug,
626
- onError,
627
- targetModel,
628
- validToken,
629
- });
631
+ if (req.method === 'GET' && getRequestPath(req) === MODELS_PATH) {
632
+ forwardRequest(req, res, {
633
+ baseUrl,
634
+ debug,
635
+ onDebug,
636
+ onError,
637
+ timeout: 30000,
638
+ validToken,
639
+ });
640
+ return;
641
+ }
642
+
643
+ handleMessageRequest(req, res, {
644
+ availableModels,
645
+ baseUrl,
646
+ debug,
647
+ onDebug,
648
+ onError,
649
+ targetModel,
650
+ validToken,
651
+ });
630
652
  });
631
653
 
632
- server.listen(0, '127.0.0.1', () => {
633
- const address = server.address();
634
- resolve({server, url: `http://127.0.0.1:${address.port}`});
635
- });
654
+ server.listen(0, '127.0.0.1', () => {
655
+ const address = server.address();
656
+ resolve({server, url: `http://127.0.0.1:${address.port}`, authenticated: Boolean(proxySecret)});
657
+ });
636
658
 
637
659
  server.on('error', (error) => reject(error));
638
660
  });
@@ -640,6 +662,8 @@ function startProxyServer(targetModel, validToken, options = {}) {
640
662
 
641
663
  export {
642
664
  buildProxyRequestOptions,
665
+ isAuthorizedProxyRequest,
666
+ isAllowedProxyRoute,
643
667
  normalizeProxyHeaders,
644
668
  PROXY_AUTH_HEADER,
645
669
  resolveMappedModel,
package/src/routerlab.js CHANGED
@@ -15,7 +15,7 @@ const SERVICES = {
15
15
  secureStorageAccount: 'routerlab-token',
16
16
  secureStorageLabel: 'RouterLab Token',
17
17
  secureStorageFileName: 'routerlab-token.secure.txt',
18
- strategyValues: ['default', 'aws', 'claude-gpt', 'claude-minimax-m2.7', 'claude-glm-5.1'],
18
+ strategyValues: ['default', 'aws', 'claude-gpt', 'deepseek-v4-beta', 'claude-kimi-k2.6', 'claude-glm-5.1'],
19
19
  },
20
20
  llm: {
21
21
  value: 'llm',
@@ -28,7 +28,7 @@ const SERVICES = {
28
28
  secureStorageAccount: 'routerlab-llm-token',
29
29
  secureStorageLabel: 'RouterLab LLM Token',
30
30
  secureStorageFileName: 'routerlab-llm-token.secure.txt',
31
- strategyValues: ['claude', 'claude-gpt', 'claude-gpt-special', 'deepseek-v4-beta'],
31
+ strategyValues: ['claude', 'claude-gpt', 'claude-gpt-special', 'deepseek-v4-beta', 'claude-glm-5.1'],
32
32
  },
33
33
  };
34
34
  const DEFAULT_SERVICE = 'routerlab';
@@ -44,12 +44,12 @@ const SECURE_STORAGE_ACCOUNT = SERVICES[DEFAULT_SERVICE].secureStorageAccount;
44
44
  const DEFAULT_CLAUDE_MODELS = [
45
45
  'claude-haiku-4-5-20251001',
46
46
  'claude-sonnet-4-6',
47
- 'claude-opus-4-6',
47
+ 'claude-opus-4-7',
48
48
  ];
49
49
  const AWS_CLAUDE_MODELS = [
50
50
  'aws-claude-haiku-4-5-20251001',
51
51
  'aws-claude-sonnet-4-6',
52
- 'aws-claude-opus-4-6',
52
+ 'aws-claude-opus-4-7',
53
53
  ];
54
54
 
55
55
  const STRATEGIES = [
@@ -57,7 +57,7 @@ const STRATEGIES = [
57
57
  value: 'default',
58
58
  name: 'Claude Native',
59
59
  description: 'Uses Claude natively without a local proxy.',
60
- selectionName: 'Claude Native (Opus 4.7)',
60
+ selectionName: 'Claude Native',
61
61
  selectionDescription: 'Standard behavior. Claude decides which model to use.',
62
62
  requiredModels: DEFAULT_CLAUDE_MODELS,
63
63
  },
@@ -65,7 +65,7 @@ const STRATEGIES = [
65
65
  value: 'aws',
66
66
  name: 'Claude via AWS',
67
67
  description: 'Maps Claude requests to AWS-backed Claude variants.',
68
- selectionName: 'Claude via AWS (Opus 4.6, -50%)',
68
+ selectionName: '💸 Claude via AWS (-50%)',
69
69
  selectionDescription: 'Map models to aws-claude-haiku, aws-claude-sonnet, aws-claude-opus.',
70
70
  requiredModels: AWS_CLAUDE_MODELS,
71
71
  mappedModels: AWS_CLAUDE_MODELS,
@@ -88,7 +88,7 @@ const STRATEGIES = [
88
88
  value: 'claude',
89
89
  name: 'Claude',
90
90
  description: 'Maps Claude requests to standard Claude variants via a local proxy.',
91
- selectionName: 'Claude (Opus 4.6)',
91
+ selectionName: 'Claude',
92
92
  selectionDescription: 'Map models to claude-haiku, claude-sonnet, claude-opus.',
93
93
  requiredModels: DEFAULT_CLAUDE_MODELS,
94
94
  mappedModels: DEFAULT_CLAUDE_MODELS,
@@ -126,11 +126,11 @@ const STRATEGIES = [
126
126
  mappedModels: ['claude-qwen3.6-plus'],
127
127
  },
128
128
  {
129
- value: 'claude-minimax-m2.7',
130
- name: 'MiniMax M2.7',
131
- description: 'Forces all requests to claude-minimax-m2.7.',
132
- selectionDescription: 'Forces all requests to claude-minimax-m2.7.',
133
- mappedModels: ['claude-minimax-m2.7'],
129
+ value: 'claude-kimi-k2.6',
130
+ name: 'Kimi K2.6',
131
+ description: 'Forces all requests to claude-kimi-k2.6.',
132
+ selectionDescription: 'Forces all requests to claude-kimi-k2.6.',
133
+ mappedModels: ['claude-kimi-k2.6'],
134
134
  },
135
135
  {
136
136
  value: 'claude-glm-5.1',
@@ -149,9 +149,10 @@ async function fetchModels(apiKey, options = {}) {
149
149
  timeoutMs = 30000,
150
150
  } = options;
151
151
 
152
+ const controller = new AbortController();
153
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
154
+
152
155
  try {
153
- const controller = new AbortController();
154
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
155
156
  const response = await fetch(`${baseUrl}/v1/models`, {
156
157
  method: 'GET',
157
158
  headers: {
@@ -160,19 +161,33 @@ async function fetchModels(apiKey, options = {}) {
160
161
  },
161
162
  signal: controller.signal,
162
163
  });
163
- clearTimeout(timeoutId);
164
164
 
165
165
  if (response.ok) {
166
- let payload = null;
166
+ let payload;
167
167
  try {
168
168
  payload = await response.json();
169
169
  } catch {
170
- payload = null;
170
+ return {
171
+ valid: false,
172
+ reason: 'invalid_response',
173
+ message: 'Model list response is not valid JSON',
174
+ };
175
+ }
176
+
177
+ const models = extractModelIds(payload);
178
+ if (models.length === 0) {
179
+ return {
180
+ valid: false,
181
+ reason: 'models_unavailable',
182
+ message: 'Model list response did not include any model ids',
183
+ models,
184
+ };
171
185
  }
172
186
 
173
187
  return {
174
188
  valid: true,
175
- models: payload ? extractModelIds(payload) : null,
189
+ models,
190
+ modelsVerified: true,
176
191
  };
177
192
  }
178
193
 
@@ -193,6 +208,8 @@ async function fetchModels(apiKey, options = {}) {
193
208
  }
194
209
 
195
210
  return {valid: false, reason: 'network_error', message: error.message};
211
+ } finally {
212
+ clearTimeout(timeoutId);
196
213
  }
197
214
  }
198
215
 
@@ -271,10 +288,6 @@ function hasNonEmptyWindowsTokenFile(tokenFile) {
271
288
  }
272
289
  }
273
290
 
274
- function encodeTokenForPowerShell(token) {
275
- return Buffer.from(token, 'utf8').toString('base64');
276
- }
277
-
278
291
  function getServiceStrategies(serviceValue = DEFAULT_SERVICE) {
279
292
  const service = getServiceConfig(serviceValue);
280
293
  if (!service?.strategyValues?.length) {
@@ -532,6 +545,21 @@ function runCommand(command, args, options = {}) {
532
545
  return result;
533
546
  }
534
547
 
548
+ function storeMacOSToken(service, token) {
549
+ // `security add-generic-password` requires `-w` for non-interactive writes.
550
+ // Keep this branch isolated so a safer native macOS path can replace it later.
551
+ return runCommand('security', [
552
+ 'add-generic-password',
553
+ '-U',
554
+ '-a',
555
+ service.secureStorageAccount,
556
+ '-s',
557
+ SECURE_STORAGE_SERVICE,
558
+ '-w',
559
+ token,
560
+ ]);
561
+ }
562
+
535
563
  function storeToken(token, serviceValue = DEFAULT_SERVICE) {
536
564
  const service = getServiceConfig(serviceValue);
537
565
  if (!service) {
@@ -545,10 +573,10 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
545
573
 
546
574
  if (process.platform === 'win32') {
547
575
  const tokenFile = getWindowsTokenFile(service.value);
548
- const encodedToken = encodeTokenForPowerShell(token);
549
576
  fs.mkdirSync(path.dirname(tokenFile), {recursive: true});
550
577
  const encrypted = runPowerShell(
551
- `$token = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedToken}')); if ([string]::IsNullOrEmpty($token)) { throw "Token input is empty" }; $secure = ConvertTo-SecureString $token -AsPlainText -Force; ConvertFrom-SecureString $secure`,
578
+ '$token = [Console]::In.ReadToEnd(); if ([string]::IsNullOrEmpty($token)) { throw "Token input is empty" }; $secure = ConvertTo-SecureString $token -AsPlainText -Force; ConvertFrom-SecureString $secure',
579
+ {input: token},
552
580
  );
553
581
  fs.writeFileSync(tokenFile, encrypted, 'utf8');
554
582
 
@@ -560,16 +588,7 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
560
588
  }
561
589
 
562
590
  if (process.platform === 'darwin') {
563
- const result = runCommand('security', [
564
- 'add-generic-password',
565
- '-U',
566
- '-a',
567
- service.secureStorageAccount,
568
- '-s',
569
- SECURE_STORAGE_SERVICE,
570
- '-w',
571
- token,
572
- ]);
591
+ const result = storeMacOSToken(service, token);
573
592
 
574
593
  if (result.status !== 0) {
575
594
  throw new Error((result.stderr || result.stdout || 'Unable to store token in Keychain').trim());
@@ -622,9 +641,9 @@ function getStoredToken(serviceValue = DEFAULT_SERVICE) {
622
641
  return null;
623
642
  }
624
643
 
625
- const encodedEncrypted = encodeTokenForPowerShell(encrypted);
626
644
  const token = runPowerShell(
627
- `$secure = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedEncrypted}')) | ConvertTo-SecureString; $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try { [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }`,
645
+ '$encrypted = [Console]::In.ReadToEnd(); if ([string]::IsNullOrWhiteSpace($encrypted)) { throw "Encrypted token input is empty" }; $secure = $encrypted | ConvertTo-SecureString; $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try { [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }',
646
+ {input: encrypted},
628
647
  );
629
648
  return token || null;
630
649
  }