claude-scionos 4.1.10 → 4.3.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
@@ -54,7 +54,7 @@ npx claude-scionos auth login
54
54
  npx claude-scionos auth login --service llm
55
55
  npx claude-scionos auth test
56
56
  npx claude-scionos --strategy aws
57
- npx claude-scionos --service llm --strategy claude-glm-5
57
+ npx claude-scionos --service llm --strategy claude-gpt
58
58
  npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
59
59
  ```
60
60
 
@@ -64,8 +64,8 @@ npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
64
64
  - `--service llm` bascule le lanceur vers `https://llm.routerlab.ch`
65
65
  - `llm` est prévu pour un accès sur invitation
66
66
  - les tokens enregistrés avec `auth login --service llm` sont stockés séparément du token RouterLab par défaut
67
- - `llm` expose pour l'instant `claude-glm-5`, `claude-gpt-5.4` et `claude-qwen3.6-plus`
68
- - `routerlab` expose aussi `claude-gpt-5.4`
67
+ - `llm` expose pour l'instant `claude-gpt`, `claude-qwen3.6-plus`, `claude-minimax-m2.7` et `claude-glm-5.1`
68
+ - `routerlab` expose aussi `claude-gpt`
69
69
 
70
70
  ## Stratégies
71
71
 
@@ -73,8 +73,11 @@ npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
73
73
  - `aws` : remappe les familles de modèles Claude vers les variantes Claude AWS de RouterLab
74
74
  - `claude-glm-5` : force toutes les requêtes vers `claude-glm-5`
75
75
  - `claude-minimax-m2.5` : force toutes les requêtes vers `claude-minimax-m2.5`
76
- - `claude-gpt-5.4` : force toutes les requêtes vers `claude-gpt-5.4`
76
+ - `claude-gpt` : mappe les requêtes Claude vers la famille `claude-gpt`
77
+ `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`
77
78
  - `claude-qwen3.6-plus` : force toutes les requêtes vers `claude-qwen3.6-plus`
79
+ - `claude-minimax-m2.7` : force toutes les requêtes vers `claude-minimax-m2.7`
80
+ - `claude-glm-5.1` : force toutes les requêtes vers `claude-glm-5.1`
78
81
 
79
82
  Utilise `--list-strategies` pour voir les stratégies disponibles pour le service choisi et leur disponibilité réelle quand un token est disponible.
80
83
 
@@ -137,7 +140,7 @@ Cas courants :
137
140
  - `Claude Code CLI not found` : installer `@anthropic-ai/claude-code`
138
141
  - `Git Bash is required on Windows` : installer Git for Windows
139
142
  - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt` : définir la variable d'environnement ou stocker le token au préalable
140
- - `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.1.10` ou plus récent, puis relancer `claude-scionos auth login`
143
+ - `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.2.0` ou plus récent, puis relancer `claude-scionos auth login`
141
144
  - `Stored token` est indiqué comme absent sous Windows alors qu'un login a déjà été fait : relancer `claude-scionos auth login`, car le fichier DPAPI local peut être vide ou corrompu
142
145
  - `secret-tool not found` : installer un client Secret Service sous Linux ou utiliser la variable d'environnement
143
146
 
package/README.md CHANGED
@@ -54,7 +54,7 @@ npx claude-scionos auth login
54
54
  npx claude-scionos auth login --service llm
55
55
  npx claude-scionos auth test
56
56
  npx claude-scionos --strategy aws
57
- npx claude-scionos --service llm --strategy claude-glm-5
57
+ npx claude-scionos --service llm --strategy claude-gpt
58
58
  npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
59
59
  ```
60
60
 
@@ -64,8 +64,8 @@ npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
64
64
  - `--service llm` switches the launcher to `https://llm.routerlab.ch`
65
65
  - `llm` is intended for invitation-only access
66
66
  - Tokens stored with `auth login --service llm` are kept separate from the default RouterLab token
67
- - `llm` currently exposes `claude-glm-5`, `claude-gpt-5.4`, and `claude-qwen3.6-plus`
68
- - `routerlab` also exposes `claude-gpt-5.4`
67
+ - `llm` currently exposes `claude-gpt`, `claude-qwen3.6-plus`, `claude-minimax-m2.7`, and `claude-glm-5.1`
68
+ - `routerlab` also exposes `claude-gpt`
69
69
 
70
70
  ## Strategies
71
71
 
@@ -73,8 +73,11 @@ npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
73
73
  - `aws`: remap Claude model families to RouterLab AWS-backed Claude variants
74
74
  - `claude-glm-5`: force all requests to `claude-glm-5`
75
75
  - `claude-minimax-m2.5`: force all requests to `claude-minimax-m2.5`
76
- - `claude-gpt-5.4`: force all requests to `claude-gpt-5.4`
76
+ - `claude-gpt`: map Claude requests to the `claude-gpt` family
77
+ `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`
77
78
  - `claude-qwen3.6-plus`: force all requests to `claude-qwen3.6-plus`
79
+ - `claude-minimax-m2.7`: force all requests to `claude-minimax-m2.7`
80
+ - `claude-glm-5.1`: force all requests to `claude-glm-5.1`
78
81
 
79
82
  Use `--list-strategies` to see the strategies available for the selected service and their live availability when a token is available.
80
83
 
@@ -137,7 +140,7 @@ Common cases:
137
140
  - `Claude Code CLI not found`: install `@anthropic-ai/claude-code`
138
141
  - `Git Bash is required on Windows`: install Git for Windows
139
142
  - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt`: set the environment variable or store the token first
140
- - `Secure token file was created but no encrypted content was written`: update to `4.1.10` or later, then re-run `claude-scionos auth login`
143
+ - `Secure token file was created but no encrypted content was written`: update to `4.2.0` or later, then re-run `claude-scionos auth login`
141
144
  - `Stored token` is missing on Windows even though you already logged in: re-run `claude-scionos auth login` because the local DPAPI token file may be empty or corrupted
142
145
  - `secret-tool not found`: install a Secret Service client on Linux or rely on the environment variable
143
146
 
package/index.js CHANGED
@@ -4,23 +4,23 @@ import { styleText } from 'node:util';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  const chalk = {
7
- hex: (color) => {
8
- if (color === '#3b82f6') return (t) => styleText('blueBright', t);
9
- if (color === '#a855f7') return (t) => styleText('magentaBright', t);
10
- if (color === '#D97757') return (t) => styleText('redBright', t);
11
- return (t) => t;
12
- },
13
- white: (t) => styleText('white', t),
14
- gray: (t) => styleText('gray', t),
15
- yellow: (t) => styleText('yellow', t),
16
- red: (t) => styleText('red', t),
17
- cyan: (t) => styleText('cyan', t),
18
- redBright: (t) => styleText('redBright', t),
19
- blueBright: (t) => styleText('blueBright', t),
20
- green: (t) => styleText('green', t),
21
- magenta: (t) => styleText('magenta', t),
22
- bold: (t) => styleText('bold', t)
23
- };
7
+ hex: (color) => {
8
+ if (color === '#3b82f6') return (t) => styleText('blueBright', t);
9
+ if (color === '#a855f7') return (t) => styleText('magentaBright', t);
10
+ if (color === '#D97757') return (t) => styleText('redBright', t);
11
+ return (t) => t;
12
+ },
13
+ white: (t) => styleText('white', t),
14
+ gray: (t) => styleText('gray', t),
15
+ yellow: (t) => styleText('yellow', t),
16
+ red: (t) => styleText('red', t),
17
+ cyan: (t) => styleText('cyan', t),
18
+ redBright: (t) => styleText('redBright', t),
19
+ blueBright: (t) => styleText('blueBright', t),
20
+ green: (t) => styleText('green', t),
21
+ magenta: (t) => styleText('magenta', t),
22
+ bold: (t) => styleText('bold', t)
23
+ };
24
24
  import { password, confirm, select, Separator } from '@inquirer/prompts';
25
25
  import { spawn, spawnSync } from 'node:child_process';
26
26
  import process from 'node:process';
@@ -43,11 +43,13 @@ import {
43
43
  getStrategyChoices,
44
44
  hasVerifiedModelIds,
45
45
  listStrategies,
46
+ resolveServiceBaseUrl,
46
47
  storeToken,
47
- validateToken
48
+ validateToken,
49
+ validateTokenFormat
48
50
  } from './src/routerlab.js';
49
- import { buildProxyRequestOptions, normalizeProxyHeaders, startProxyServer } from './src/proxy.js';
50
-
51
+ import { startProxyServer } from './src/proxy.js';
52
+
51
53
  const require = createRequire(import.meta.url);
52
54
  const pkg = require('./package.json');
53
55
 
@@ -116,7 +118,7 @@ function showHelp() {
116
118
  console.log(chalk.gray("Examples"));
117
119
  console.log(` ${chalk.cyan("claude-scionos --strategy aws")}`);
118
120
  console.log(` ${chalk.cyan("claude-scionos auth login --service llm")}`);
119
- console.log(` ${chalk.cyan("claude-scionos --service llm --strategy claude-glm-5")}`);
121
+ console.log(` ${chalk.cyan("claude-scionos --service llm --strategy claude-gpt")}`);
120
122
  console.log(` ${chalk.cyan('claude-scionos --strategy aws --no-prompt -p "Summarize this repo"')}`);
121
123
  console.log(` ${chalk.cyan("claude-scionos auth test")}`);
122
124
  console.log("");
@@ -157,11 +159,15 @@ function getStrategyIndicator(strategyValue, modelIds, serviceValue) {
157
159
  }
158
160
 
159
161
  function getStrategyMenuLabel(strategyValue) {
160
- if (strategyValue === 'aws') {
162
+ const strategy = getServiceStrategies(DEFAULT_SERVICE)
163
+ .concat(getServiceStrategies('llm'))
164
+ .find((entry) => entry.value === strategyValue || entry.aliases?.includes(strategyValue));
165
+
166
+ if (strategy?.value === 'aws') {
161
167
  return '💰 aws 50%';
162
168
  }
163
169
 
164
- return strategyValue;
170
+ return strategy?.selectionName || strategy?.name || strategyValue;
165
171
  }
166
172
 
167
173
  function normalizeStrategyValue(strategy) {
@@ -292,7 +298,7 @@ function formatTokenSource(source) {
292
298
  if (source === 'manual') return 'manual input';
293
299
  return 'not available';
294
300
  }
295
-
301
+
296
302
  function installClaudeCode() {
297
303
  return spawnSync('npm', ['install', '-g', '@anthropic-ai/claude-code'], {
298
304
  stdio: 'inherit',
@@ -301,26 +307,27 @@ function installClaudeCode() {
301
307
  }
302
308
 
303
309
  async function promptAndValidateToken(promptMessage, serviceConfig) {
304
- while (true) {
305
- if (serviceConfig.tokenHelpUrl) {
306
- console.log(chalk.blueBright(`To retrieve your token, visit: ${serviceConfig.tokenHelpUrl}`));
307
- } else if (serviceConfig.tokenHelpMessage) {
308
- console.log(chalk.blueBright(serviceConfig.tokenHelpMessage));
309
- }
310
+ const serviceBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
310
311
 
312
+ while (true) {
311
313
  const token = await password({
312
314
  message: promptMessage,
313
315
  mask: '*'
314
316
  });
315
317
 
316
- console.log(chalk.gray("Validating token..."));
317
- const validation = await validateToken(token, { baseUrl: serviceConfig.baseUrl });
318
+ const formatValidation = validateTokenFormat(token);
319
+ if (!formatValidation.valid) {
320
+ console.log(chalk.red(`✗ ${formatValidation.message}\n`));
321
+ continue;
322
+ }
323
+
324
+ const validation = await validateToken(token, { baseUrl: serviceBaseUrl, serviceValue: serviceConfig.value });
318
325
  if (canProceedWithValidation(validation)) {
319
- console.log(chalk.green("✓ Token validated."));
326
+ console.log(chalk.green(`✓ ${serviceConfig.tokenPromptLabel} token validated.`));
320
327
  return { token, validation };
321
328
  }
322
329
 
323
- console.log(chalk.red(`❌ Token validation failed: ${validation.message || validation.status || validation.reason}. Try again.`));
330
+ console.log(chalk.red(`✗ Token invalid: ${validation.message || validation.status || validation.reason}\n`));
324
331
  }
325
332
  }
326
333
 
@@ -347,9 +354,27 @@ async function maybeStoreToken(token, serviceConfig, replaceExisting = false) {
347
354
 
348
355
  async function resolveLaunchToken(noPrompt, serviceConfig) {
349
356
  const candidate = getAvailableTokenCandidate(serviceConfig.value);
357
+ const serviceBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
350
358
 
351
359
  if (candidate.token) {
352
- const validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
360
+ const formatValidation = validateTokenFormat(candidate.token);
361
+ if (!formatValidation.valid) {
362
+ const sourceLabel = formatTokenSource(candidate.source);
363
+ if (noPrompt) {
364
+ throw new Error(`${sourceLabel} token is invalid: ${formatValidation.message}`);
365
+ }
366
+
367
+ console.log(chalk.yellow(`⚠ The ${sourceLabel} token is invalid: ${formatValidation.message} Please enter a new token.`));
368
+ const prompted = await promptAndValidateToken(`Please enter your ${serviceConfig.tokenPromptLabel} token:`, serviceConfig);
369
+ await maybeStoreToken(prompted.token, serviceConfig, candidate.source === 'secure-store');
370
+ return {
371
+ token: prompted.token,
372
+ source: 'manual',
373
+ validation: prompted.validation
374
+ };
375
+ }
376
+
377
+ const validation = await validateToken(candidate.token, { baseUrl: serviceBaseUrl, serviceValue: serviceConfig.value });
353
378
  if (canProceedWithValidation(validation)) {
354
379
  return {
355
380
  token: candidate.token,
@@ -398,7 +423,7 @@ async function resolveStrategyChoice(parsed, modelIds, serviceConfig) {
398
423
  if (hasVerifiedModelIds(modelIds)) {
399
424
  const availability = assessStrategy(selected, modelIds, serviceConfig.value);
400
425
  if (availability.level === 'partial') {
401
- console.log(chalk.yellow(`⚠ Strategy "${selected}" is only partially available on ${serviceConfig.availabilityLabel}.`));
426
+ console.log(chalk.yellow(`WARN Strategy "${selected}" is only partially available on ${serviceConfig.availabilityLabel}.`));
402
427
  }
403
428
  }
404
429
 
@@ -406,7 +431,9 @@ async function resolveStrategyChoice(parsed, modelIds, serviceConfig) {
406
431
  };
407
432
 
408
433
  if (parsed.strategy) {
409
- const strategy = getServiceStrategies(serviceConfig.value).find((entry) => entry.value === parsed.strategy);
434
+ const strategy = getServiceStrategies(serviceConfig.value).find((entry) => (
435
+ entry.value === parsed.strategy || entry.aliases?.includes(parsed.strategy)
436
+ ));
410
437
  if (!strategy) {
411
438
  throw new Error(`Unknown strategy "${parsed.strategy}". Use --list-strategies to inspect the supported values.`);
412
439
  }
@@ -421,12 +448,13 @@ async function resolveStrategyChoice(parsed, modelIds, serviceConfig) {
421
448
  const strategyChoices = getStrategyChoices(modelIds, serviceConfig.value).map((choice) => {
422
449
  const launchReadiness = assessStrategyLaunch(choice.value, modelIds, serviceConfig.value);
423
450
  const disabled = hasVerifiedModelIds(modelIds) && !launchReadiness.ready ? launchReadiness.note : false;
424
-
451
+ const menuLabel = getStrategyMenuLabel(choice.value);
425
452
  return {
426
453
  ...choice,
427
454
  disabled,
428
- name: `${getStrategyIndicator(choice.value, modelIds, serviceConfig.value)} ${getStrategyMenuLabel(choice.value)}`,
429
- short: getStrategyMenuLabel(choice.value)
455
+ name: `${getStrategyIndicator(choice.value, modelIds, serviceConfig.value)} ${menuLabel}`,
456
+ short: menuLabel,
457
+ description: choice.description
430
458
  };
431
459
  });
432
460
 
@@ -451,12 +479,7 @@ function showStrategies(modelIds = null, serviceConfig) {
451
479
  const strategies = listStrategies(modelIds, serviceConfig.value);
452
480
  showSection('Strategies', strategies.map((strategy) => {
453
481
  const indicator = getStrategyIndicator(strategy.value, modelIds, serviceConfig.value);
454
- const state = !hasVerifiedModelIds(modelIds)
455
- ? chalk.gray('Unknown')
456
- : assessStrategyLaunch(strategy.value, modelIds, serviceConfig.value).ready
457
- ? chalk.green('Ready')
458
- : chalk.red('Blocked');
459
- return `${indicator} ${chalk.white(strategy.name)} ${state} ${chalk.gray(`(${strategy.value})`)}\n ${strategy.description} ${strategy.availability.note}`.trimEnd();
482
+ return `${indicator} ${chalk.white(getStrategyMenuLabel(strategy.value))} ${chalk.gray(`(${strategy.value})`)}\n ${[strategy.description, strategy.availability.note].filter(Boolean).join(' ')}`;
460
483
  }));
461
484
  }
462
485
 
@@ -478,7 +501,7 @@ async function runAuthCommand(action, serviceConfig) {
478
501
 
479
502
  if (action === 'logout') {
480
503
  const removed = deleteStoredToken(serviceConfig.value);
481
- console.log(removed ? chalk.green(' Stored token removed.') : chalk.yellow(' No stored token was found.'));
504
+ console.log(removed ? chalk.green('OK Stored token removed.') : chalk.yellow('WARN No stored token was found.'));
482
505
  return;
483
506
  }
484
507
 
@@ -489,21 +512,24 @@ async function runAuthCommand(action, serviceConfig) {
489
512
 
490
513
  const prompted = await promptAndValidateToken(`Enter the ${serviceConfig.tokenPromptLabel} token to save securely:`, serviceConfig);
491
514
  storeToken(prompted.token, serviceConfig.value);
492
- console.log(chalk.green(`✓ Token saved securely in ${storage.backend}.`));
515
+ console.log(chalk.green(`OK Token saved securely in ${storage.backend}.`));
493
516
  return;
494
517
  }
495
518
 
496
519
  if (action === 'test') {
497
520
  const candidate = getAvailableTokenCandidate(serviceConfig.value);
498
521
  if (!candidate.token) {
499
- console.log(chalk.yellow(' No environment token or stored token is available.'));
522
+ console.log(chalk.yellow('WARN No environment token or stored token is available.'));
500
523
  return;
501
524
  }
502
525
 
503
- const validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
526
+ const validation = await validateToken(candidate.token, {
527
+ baseUrl: resolveServiceBaseUrl(serviceConfig.value),
528
+ serviceValue: serviceConfig.value
529
+ });
504
530
  console.log(canProceedWithValidation(validation)
505
- ? chalk.green(`✓ ${formatTokenSource(candidate.source)} token is valid.`)
506
- : chalk.red(`❌ ${formatTokenSource(candidate.source)} token is invalid: ${validation.message || validation.status || validation.reason}`));
531
+ ? chalk.green(`OK ${formatTokenSource(candidate.source)} token is valid for ${serviceConfig.label}.`)
532
+ : chalk.red(`FAIL ${formatTokenSource(candidate.source)} token is invalid for ${serviceConfig.label}: ${validation.message || validation.status || validation.reason}`));
507
533
 
508
534
  if (canProceedWithValidation(validation)) {
509
535
  showStrategies(validation.models, serviceConfig);
@@ -538,16 +564,19 @@ async function runDoctor(serviceConfig) {
538
564
  console.log('');
539
565
 
540
566
  const candidate = getAvailableTokenCandidate(serviceConfig.value);
567
+ let validation = null;
568
+
541
569
  if (!candidate.token) {
542
570
  showStatus(`${serviceConfig.tokenPromptLabel} auth`, 'warn', 'Skipped: no environment or stored token available');
543
- console.log('');
544
- return;
571
+ } else {
572
+ validation = await validateToken(candidate.token, {
573
+ baseUrl: resolveServiceBaseUrl(serviceConfig.value),
574
+ serviceValue: serviceConfig.value
575
+ });
576
+ showStatus(`${serviceConfig.tokenPromptLabel} auth`, canProceedWithValidation(validation) ? 'ok' : 'error', canProceedWithValidation(validation)
577
+ ? `validated via ${formatTokenSource(candidate.source)} token`
578
+ : validation.message || validation.status || validation.reason);
545
579
  }
546
-
547
- const validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
548
- showStatus(`${serviceConfig.tokenPromptLabel} auth`, canProceedWithValidation(validation) ? 'ok' : 'error', canProceedWithValidation(validation)
549
- ? `validated via ${formatTokenSource(candidate.source)} token`
550
- : validation.message || validation.status || validation.reason);
551
580
  console.log('');
552
581
 
553
582
  if (canProceedWithValidation(validation)) {
@@ -560,18 +589,14 @@ async function runListStrategies(serviceConfig) {
560
589
  const candidate = getAvailableTokenCandidate(serviceConfig.value);
561
590
  if (!candidate.token) {
562
591
  showStrategies(null, serviceConfig);
563
- console.log(chalk.gray(`Tip: save a token with \`claude-scionos auth login${serviceConfig.value === DEFAULT_SERVICE ? '' : ` --service ${serviceConfig.value}`}\` to verify availability live.`));
564
- return;
565
- }
566
-
567
- const validation = await validateToken(candidate.token, { baseUrl: serviceConfig.baseUrl });
568
- if (canProceedWithValidation(validation)) {
569
- showStrategies(validation.models, serviceConfig);
570
592
  return;
571
593
  }
572
594
 
573
- console.log(chalk.yellow(`⚠ Unable to verify strategy availability with the ${formatTokenSource(candidate.source)} token.`));
574
- showStrategies(null, serviceConfig);
595
+ const validation = await validateToken(candidate.token, {
596
+ baseUrl: resolveServiceBaseUrl(serviceConfig.value),
597
+ serviceValue: serviceConfig.value
598
+ });
599
+ showStrategies(canProceedWithValidation(validation) ? validation.models : null, serviceConfig);
575
600
  }
576
601
 
577
602
  async function ensureClaudeInstallation(osInfo, interactive) {
@@ -611,7 +636,7 @@ async function ensureClaudeInstallation(osInfo, interactive) {
611
636
 
612
637
  return claudeStatus;
613
638
  }
614
-
639
+
615
640
  async function main() {
616
641
  const parsed = parseWrapperArgs(process.argv.slice(2));
617
642
 
@@ -667,7 +692,7 @@ async function main() {
667
692
  }
668
693
 
669
694
  const modelChoice = await resolveStrategyChoice(parsed, validation.models, serviceConfig);
670
- let finalBaseUrl = serviceConfig.baseUrl;
695
+ let finalBaseUrl = resolveServiceBaseUrl(serviceConfig.value);
671
696
  let proxyServer = null;
672
697
 
673
698
  if (modelChoice !== 'default') {
@@ -676,7 +701,8 @@ async function main() {
676
701
  }
677
702
 
678
703
  const proxyInfo = await startProxyServer(modelChoice, token, {
679
- baseUrl: serviceConfig.baseUrl,
704
+ availableModels: validation.models,
705
+ baseUrl: resolveServiceBaseUrl(serviceConfig.value),
680
706
  debug: isDebug,
681
707
  onDebug: (message) => console.log(chalk.yellow(message)),
682
708
  onError: (message) => console.error(chalk.red(message))
@@ -689,10 +715,10 @@ async function main() {
689
715
  const env = {
690
716
  ...process.env,
691
717
  ANTHROPIC_BASE_URL: finalBaseUrl,
692
- ANTHROPIC_AUTH_TOKEN: token,
693
- ANTHROPIC_API_KEY: "" // Force empty
694
- };
695
-
718
+ ANTHROPIC_AUTH_TOKEN: token,
719
+ ANTHROPIC_API_KEY: "" // Force empty
720
+ };
721
+
696
722
  if (interactive) {
697
723
  showSection('Launch Summary', [
698
724
  `${chalk.white('Service:')} ${serviceConfig.label}`,
@@ -721,31 +747,31 @@ async function main() {
721
747
  cleanup();
722
748
  process.exit(code ?? 0);
723
749
  });
724
-
725
- child.on('error', (err) => {
726
- cleanup();
727
- console.error(chalk.red(`\n❌ Error launching Claude CLI:`));
728
- if (err.code === 'ENOENT') {
729
- console.error(chalk.yellow(` Executable not found. Try 'npm install -g @anthropic-ai/claude-code'`));
730
- } else if (err.code === 'EACCES') {
731
- console.error(chalk.yellow(` Permission denied.`));
732
- } else {
733
- console.error(chalk.yellow(` ${err.message}`));
734
- }
735
- process.exit(1);
736
- });
750
+
751
+ child.on('error', (err) => {
752
+ cleanup();
753
+ console.error(chalk.red(`\n❌ Error launching Claude CLI:`));
754
+ if (err.code === 'ENOENT') {
755
+ console.error(chalk.yellow(` Executable not found. Try 'npm install -g @anthropic-ai/claude-code'`));
756
+ } else if (err.code === 'EACCES') {
757
+ console.error(chalk.yellow(` Permission denied.`));
758
+ } else {
759
+ console.error(chalk.yellow(` ${err.message}`));
760
+ }
761
+ process.exit(1);
762
+ });
737
763
 
738
764
  process.on('SIGINT', () => {
739
765
  // Claude handles SIGINT; we keep the wrapper alive for cleanup on child exit.
740
766
  });
741
-
742
- process.on('SIGTERM', () => {
743
- if (child) child.kill('SIGTERM');
744
- cleanup();
745
- process.exit(0);
746
- });
747
- }
748
-
767
+
768
+ process.on('SIGTERM', () => {
769
+ if (child) child.kill('SIGTERM');
770
+ cleanup();
771
+ process.exit(0);
772
+ });
773
+ }
774
+
749
775
  const isEntrypoint = normalizeEntrypointPath(process.argv[1]) === normalizeEntrypointPath(fileURLToPath(import.meta.url));
750
776
 
751
777
  if (isEntrypoint) {
@@ -754,14 +780,12 @@ if (isEntrypoint) {
754
780
  process.exit(1);
755
781
  });
756
782
  }
757
-
783
+
758
784
  export {
759
- buildProxyRequestOptions,
760
785
  canProceedWithValidation,
761
786
  installClaudeCode,
762
787
  main,
763
788
  normalizeEntrypointPath,
764
- normalizeProxyHeaders,
765
- startProxyServer,
789
+ resolveLaunchToken,
766
790
  validateToken
767
791
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scionos",
3
- "version": "4.1.10",
3
+ "version": "4.3.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",
@@ -43,12 +43,12 @@
43
43
  },
44
44
  "private": false,
45
45
  "dependencies": {
46
- "@inquirer/prompts": "^8.4.1"
46
+ "@inquirer/prompts": "^8.4.2"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "eslint": "^10.2.0",
50
+ "eslint": "^10.2.1",
51
51
  "globals": "^17.5.0",
52
- "vitest": "^4.1.4"
52
+ "vitest": "^4.1.5"
53
53
  }
54
54
  }
package/src/proxy.js CHANGED
@@ -14,6 +14,8 @@ const HOP_BY_HOP_HEADERS = new Set([
14
14
  'transfer-encoding',
15
15
  'upgrade',
16
16
  ]);
17
+ const PROXY_AUTH_HEADER = 'x-scionos-proxy-secret';
18
+ const MESSAGES_PATH = '/v1/messages';
17
19
 
18
20
  function normalizeProxyHeaders(headers) {
19
21
  const normalizedHeaders = {};
@@ -32,6 +34,7 @@ function normalizeProxyHeaders(headers) {
32
34
  function buildProxyRequestOptions(url, method, upstreamHeaders, validToken, bodyLength, timeout) {
33
35
  const headers = normalizeProxyHeaders(upstreamHeaders);
34
36
  delete headers.authorization;
37
+ delete headers[PROXY_AUTH_HEADER];
35
38
  headers['x-api-key'] = validToken;
36
39
  headers['anthropic-version'] ??= DEFAULT_ANTHROPIC_VERSION;
37
40
 
@@ -49,9 +52,42 @@ function buildProxyRequestOptions(url, method, upstreamHeaders, validToken, body
49
52
  };
50
53
  }
51
54
 
52
- function resolveMappedModel(targetModel, requestedModel = '') {
55
+ function getPreferredClaudeGptModel(requestedModel = '') {
56
+ if (requestedModel.includes('haiku') || requestedModel.includes('mini')) {
57
+ return 'claude-gpt-5.4-mini';
58
+ }
59
+
60
+ if (requestedModel.includes('opus')) {
61
+ return 'claude-gpt-5.5';
62
+ }
63
+
64
+ return 'claude-gpt-5.4';
65
+ }
66
+
67
+ function resolveMappedModel(targetModel, requestedModel = '', availableModels = []) {
53
68
  if (targetModel !== 'aws') {
54
- return targetModel;
69
+ if (targetModel !== 'claude-gpt') {
70
+ return targetModel;
71
+ }
72
+
73
+ const preferredModel = getPreferredClaudeGptModel(requestedModel);
74
+ const availableClaudeGptModels = Array.isArray(availableModels)
75
+ ? availableModels.filter((model) => model.startsWith('claude-gpt-'))
76
+ : [];
77
+
78
+ if (availableClaudeGptModels.length === 0) {
79
+ return preferredModel;
80
+ }
81
+
82
+ if (availableClaudeGptModels.includes(preferredModel)) {
83
+ return preferredModel;
84
+ }
85
+
86
+ return (
87
+ availableClaudeGptModels.find((model) => model === 'claude-gpt-5.4')
88
+ ?? availableClaudeGptModels[0]
89
+ ?? preferredModel
90
+ );
55
91
  }
56
92
 
57
93
  if (requestedModel.includes('haiku')) {
@@ -74,8 +110,24 @@ function writeJsonError(res, statusCode, payload) {
74
110
  res.end(JSON.stringify(payload));
75
111
  }
76
112
 
113
+ function getRequestPath(req) {
114
+ return new URL(req.url, 'http://127.0.0.1').pathname;
115
+ }
116
+
117
+ function isAuthorizedProxyRequest(req, proxySecret) {
118
+ if (!proxySecret) {
119
+ return true;
120
+ }
121
+
122
+ return req.headers[PROXY_AUTH_HEADER] === proxySecret;
123
+ }
124
+
125
+ function isAllowedProxyRoute(req) {
126
+ return req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH;
127
+ }
128
+
77
129
  async function handleMessageRequest(req, res, options) {
78
- const {baseUrl, debug, onDebug, onError, targetModel, validToken} = options;
130
+ const {availableModels = [], baseUrl, debug, onDebug, onError, targetModel, validToken} = options;
79
131
  const chunks = [];
80
132
  const maxSize = 100 * 1024 * 1024;
81
133
  let totalSize = 0;
@@ -103,9 +155,13 @@ async function handleMessageRequest(req, res, options) {
103
155
  }
104
156
 
105
157
  if (bodyJson?.model) {
106
- const newModel = resolveMappedModel(targetModel, bodyJson.model);
158
+ const preferredModel = resolveMappedModel(targetModel, bodyJson.model);
159
+ const newModel = resolveMappedModel(targetModel, bodyJson.model, availableModels);
107
160
  if (debug) {
108
161
  onDebug(`[Proxy] Swapping model ${bodyJson.model} -> ${newModel}`);
162
+ if (preferredModel !== newModel) {
163
+ onDebug(`[Proxy] Fallback applied because ${preferredModel} is not available for this token`);
164
+ }
109
165
  }
110
166
 
111
167
  bodyJson.model = newModel;
@@ -123,7 +179,7 @@ async function handleMessageRequest(req, res, options) {
123
179
  validToken,
124
180
  });
125
181
  } catch (error) {
126
- onError(`[Proxy Error] POST /messages: ${error.message}`);
182
+ onError(`[Proxy Error] POST ${MESSAGES_PATH}: ${error.message}`);
127
183
  writeJsonError(res, 500, {
128
184
  error: {
129
185
  message: 'Scionos Proxy Error',
@@ -162,9 +218,9 @@ async function forwardRequest(req, res, options) {
162
218
  onError(`[Proxy Error] Code: ${error.code}`);
163
219
  }
164
220
 
165
- writeJsonError(res, req.method === 'POST' && req.url.includes('/messages') ? 500 : 502, {
221
+ writeJsonError(res, req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH ? 500 : 502, {
166
222
  error: {
167
- message: req.method === 'POST' && req.url.includes('/messages')
223
+ message: req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH
168
224
  ? 'Proxy Error'
169
225
  : 'Scionos Proxy Error: Failed to connect to upstream',
170
226
  details: error.message,
@@ -197,51 +253,34 @@ async function forwardRequest(req, res, options) {
197
253
 
198
254
  function startProxyServer(targetModel, validToken, options = {}) {
199
255
  const {
256
+ availableModels = [],
200
257
  baseUrl = BASE_URL,
201
258
  debug = false,
202
259
  onDebug = () => {},
203
260
  onError = () => {},
261
+ proxySecret = null,
204
262
  } = options;
205
263
 
206
264
  return new Promise((resolve, reject) => {
207
265
  const server = http.createServer((req, res) => {
208
- if (req.method === 'OPTIONS') {
209
- res.writeHead(200, {
210
- 'Access-Control-Allow-Origin': '*',
211
- 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
212
- 'Access-Control-Allow-Headers': '*',
213
- });
214
- res.end();
266
+ if (!isAuthorizedProxyRequest(req, proxySecret)) {
267
+ writeJsonError(res, 403, {error: {message: 'Forbidden'}});
215
268
  return;
216
269
  }
217
270
 
218
- if (req.method === 'POST' && req.url.includes('/messages')) {
219
- handleMessageRequest(req, res, {
220
- baseUrl,
221
- debug,
222
- onDebug,
223
- onError,
224
- targetModel,
225
- validToken,
226
- });
271
+ if (!isAllowedProxyRoute(req)) {
272
+ writeJsonError(res, 404, {error: {message: 'Not Found'}});
227
273
  return;
228
274
  }
229
275
 
230
- forwardRequest(req, res, {
276
+ handleMessageRequest(req, res, {
277
+ availableModels,
231
278
  baseUrl,
232
279
  debug,
233
280
  onDebug,
234
281
  onError,
235
- timeout: 60000,
282
+ targetModel,
236
283
  validToken,
237
- }).catch((error) => {
238
- onError(`[Proxy Error] ${req.method} ${req.url}: ${error.message}`);
239
- writeJsonError(res, 502, {
240
- error: {
241
- message: 'Scionos Proxy Error: Failed to connect to upstream',
242
- details: error.message,
243
- },
244
- });
245
284
  });
246
285
  });
247
286
 
@@ -257,6 +296,7 @@ function startProxyServer(targetModel, validToken, options = {}) {
257
296
  export {
258
297
  buildProxyRequestOptions,
259
298
  normalizeProxyHeaders,
299
+ PROXY_AUTH_HEADER,
260
300
  resolveMappedModel,
261
301
  startProxyServer,
262
302
  };
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-5.4', 'claude-glm-5', 'claude-minimax-m2.5'],
18
+ strategyValues: ['default', 'aws', 'claude-gpt', 'claude-glm-5', 'claude-minimax-m2.5'],
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-glm-5', 'claude-gpt-5.4', 'claude-qwen3.6-plus'],
31
+ strategyValues: ['claude-gpt', 'claude-qwen3.6-plus', 'claude-minimax-m2.7', 'claude-glm-5.1'],
32
32
  },
33
33
  };
34
34
  const DEFAULT_SERVICE = 'routerlab';
@@ -85,11 +85,13 @@ const STRATEGIES = [
85
85
  mappedModels: ['claude-minimax-m2.5'],
86
86
  },
87
87
  {
88
- value: 'claude-gpt-5.4',
89
- name: 'GPT-5.4',
90
- description: 'Forces all requests to claude-gpt-5.4.',
91
- selectionDescription: 'Forces all requests to claude-gpt-5.4.',
92
- mappedModels: ['claude-gpt-5.4'],
88
+ value: 'claude-gpt',
89
+ name: 'claude-gpt',
90
+ selectionName: 'claude-gpt',
91
+ description: 'Maps Claude requests to the claude-gpt family. Opus 4.7 => claude-gpt-5.5, Sonnet 4.6 => claude-gpt-5.4, Haiku => claude-gpt-5.4-mini.',
92
+ selectionDescription: 'Opus 4.7 => claude-gpt-5.5, Sonnet 4.6 => claude-gpt-5.4, Haiku => claude-gpt-5.4-mini.',
93
+ aliases: ['claude-gpt-5.4'],
94
+ verificationModels: ['claude-gpt-5.4'],
93
95
  },
94
96
  {
95
97
  value: 'claude-qwen3.6-plus',
@@ -98,11 +100,26 @@ const STRATEGIES = [
98
100
  selectionDescription: 'Forces all requests to claude-qwen3.6-plus.',
99
101
  mappedModels: ['claude-qwen3.6-plus'],
100
102
  },
103
+ {
104
+ value: 'claude-minimax-m2.7',
105
+ name: 'MiniMax M2.7',
106
+ description: 'Forces all requests to claude-minimax-m2.7.',
107
+ selectionDescription: 'Forces all requests to claude-minimax-m2.7.',
108
+ mappedModels: ['claude-minimax-m2.7'],
109
+ },
110
+ {
111
+ value: 'claude-glm-5.1',
112
+ name: 'GLM-5.1',
113
+ description: 'Forces all requests to claude-glm-5.1.',
114
+ selectionDescription: 'Forces all requests to claude-glm-5.1.',
115
+ mappedModels: ['claude-glm-5.1'],
116
+ },
101
117
  ];
102
118
 
103
119
  async function fetchModels(apiKey, options = {}) {
104
120
  const {
105
- baseUrl = BASE_URL,
121
+ serviceValue = DEFAULT_SERVICE,
122
+ baseUrl = resolveServiceBaseUrl(serviceValue),
106
123
  anthropicVersion = DEFAULT_ANTHROPIC_VERSION,
107
124
  timeoutMs = 30000,
108
125
  } = options;
@@ -190,6 +207,24 @@ function getServiceConfig(serviceValue = DEFAULT_SERVICE) {
190
207
  return SERVICES[normalizeServiceValue(serviceValue)] ?? null;
191
208
  }
192
209
 
210
+ function resolveServiceBaseUrl(serviceValue = DEFAULT_SERVICE, env = {}) {
211
+ return env.ANTHROPIC_BASE_URL?.trim() || getServiceConfig(serviceValue)?.baseUrl || BASE_URL;
212
+ }
213
+
214
+ function validateTokenFormat(apiKey) {
215
+ const token = apiKey?.trim() ?? '';
216
+
217
+ if (!token) {
218
+ return {valid: false, reason: 'missing', message: 'Token is required.'};
219
+ }
220
+
221
+ if (token.length < 20) {
222
+ return {valid: false, reason: 'too_short', message: 'Token seems invalid (too short).'};
223
+ }
224
+
225
+ return {valid: true};
226
+ }
227
+
193
228
  function getServiceLabel(serviceValue = DEFAULT_SERVICE) {
194
229
  return getServiceConfig(serviceValue)?.availabilityLabel ?? 'RouterLab';
195
230
  }
@@ -226,12 +261,19 @@ function getServiceStrategies(serviceValue = DEFAULT_SERVICE) {
226
261
  .filter(Boolean);
227
262
  }
228
263
 
264
+ function normalizeStrategyValue(strategyValue) {
265
+ return strategyValue === 'claude-gpt-5.4' ? 'claude-gpt' : strategyValue;
266
+ }
267
+
229
268
  function findStrategy(strategyValue, serviceValue = DEFAULT_SERVICE) {
230
- return getServiceStrategies(serviceValue).find((strategy) => strategy.value === strategyValue) ?? null;
269
+ const normalizedValue = normalizeStrategyValue(strategyValue);
270
+ return getServiceStrategies(serviceValue).find((strategy) => (
271
+ strategy.value === normalizedValue || strategy.aliases?.includes(strategyValue)
272
+ )) ?? null;
231
273
  }
232
274
 
233
275
  function getRequiredModels(strategy) {
234
- return strategy?.requiredModels ?? strategy?.mappedModels ?? [];
276
+ return strategy?.requiredModels ?? strategy?.verificationModels ?? strategy?.mappedModels ?? [];
235
277
  }
236
278
 
237
279
  function hasVerifiedModelIds(modelIds) {
@@ -372,17 +414,18 @@ function assessStrategyLaunch(strategyValue, modelIds = [], serviceValue = DEFAU
372
414
  }
373
415
 
374
416
  function getFallbackStrategy(strategyValue, modelIds = [], serviceValue = DEFAULT_SERVICE) {
417
+ const normalizedValue = normalizeStrategyValue(strategyValue);
375
418
  if (hasExploitableModelIds(modelIds, serviceValue)) {
376
- return assessStrategyLaunch(strategyValue, modelIds, serviceValue).ready ? strategyValue : null;
419
+ return assessStrategyLaunch(normalizedValue, modelIds, serviceValue).ready ? normalizedValue : null;
377
420
  }
378
421
 
379
- const availability = assessStrategy(strategyValue, modelIds, serviceValue);
380
- return availability.level === 'unavailable' ? null : strategyValue;
422
+ const availability = assessStrategy(normalizedValue, modelIds, serviceValue);
423
+ return availability.level === 'unavailable' ? null : normalizedValue;
381
424
  }
382
425
 
383
426
  function getStrategyChoices(modelIds = [], serviceValue = DEFAULT_SERVICE) {
384
427
  return listStrategies(modelIds, serviceValue).map((strategy) => ({
385
- name: strategy.value,
428
+ name: strategy.selectionName ?? strategy.name ?? strategy.value,
386
429
  value: strategy.value,
387
430
  description: strategy.selectionDescription ?? strategy.description,
388
431
  }));
@@ -652,6 +695,8 @@ export {
652
695
  getStrategyChoices,
653
696
  hasVerifiedModelIds,
654
697
  listStrategies,
698
+ resolveServiceBaseUrl,
655
699
  storeToken,
656
700
  validateToken,
701
+ validateTokenFormat,
657
702
  };