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 +8 -5
- package/README.md +8 -5
- package/index.js +124 -100
- package/package.json +4 -4
- package/src/proxy.js +73 -33
- package/src/routerlab.js +59 -14
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-
|
|
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-
|
|
68
|
-
- `routerlab` expose aussi `claude-gpt
|
|
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
|
|
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.
|
|
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-
|
|
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-
|
|
68
|
-
- `routerlab` also exposes `claude-gpt
|
|
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
|
|
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.
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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(
|
|
326
|
+
console.log(chalk.green(`✓ ${serviceConfig.tokenPromptLabel} token validated.`));
|
|
320
327
|
return { token, validation };
|
|
321
328
|
}
|
|
322
329
|
|
|
323
|
-
console.log(chalk.red(
|
|
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
|
|
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(
|
|
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) =>
|
|
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)} ${
|
|
429
|
-
short:
|
|
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
|
-
|
|
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('
|
|
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(
|
|
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('
|
|
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, {
|
|
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(
|
|
506
|
-
: chalk.red(
|
|
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
|
-
|
|
544
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
46
|
+
"@inquirer/prompts": "^8.4.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "^10.0.1",
|
|
50
|
-
"eslint": "^10.2.
|
|
50
|
+
"eslint": "^10.2.1",
|
|
51
51
|
"globals": "^17.5.0",
|
|
52
|
-
"vitest": "^4.1.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
221
|
+
writeJsonError(res, req.method === 'POST' && getRequestPath(req) === MESSAGES_PATH ? 500 : 502, {
|
|
166
222
|
error: {
|
|
167
|
-
message: req.method === 'POST' && req
|
|
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
|
|
209
|
-
res
|
|
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
|
|
219
|
-
|
|
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
|
-
|
|
276
|
+
handleMessageRequest(req, res, {
|
|
277
|
+
availableModels,
|
|
231
278
|
baseUrl,
|
|
232
279
|
debug,
|
|
233
280
|
onDebug,
|
|
234
281
|
onError,
|
|
235
|
-
|
|
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
|
|
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-
|
|
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
|
|
89
|
-
name: '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
419
|
+
return assessStrategyLaunch(normalizedValue, modelIds, serviceValue).ready ? normalizedValue : null;
|
|
377
420
|
}
|
|
378
421
|
|
|
379
|
-
const availability = assessStrategy(
|
|
380
|
-
return availability.level === 'unavailable' ? null :
|
|
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
|
};
|