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 +10 -11
- package/README.md +10 -11
- package/index.js +94 -17
- package/package.json +3 -3
- package/src/proxy.js +72 -48
- package/src/routerlab.js +55 -36
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
|
|
71
|
-
- `routerlab` expose aussi `claude-gpt`, `claude-
|
|
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-
|
|
78
|
-
- `claude-gpt` : mappe les requêtes Claude vers la famille `claude-gpt`
|
|
79
|
-
`claude-gpt-5.5
|
|
80
|
-
- `claude-gpt-special` : sur `--service llm`, force toutes les requêtes vers `claude-gpt-5.4-sp`
|
|
81
|
-
- `deepseek-v4-beta` :
|
|
82
|
-
- `claude-
|
|
83
|
-
- `claude-
|
|
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`,
|
|
71
|
-
- `routerlab` also exposes `claude-gpt`, `claude-
|
|
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-
|
|
78
|
-
- `claude-gpt`: map Claude requests to the `claude-gpt` family
|
|
79
|
-
`claude-gpt-5.5
|
|
80
|
-
- `claude-gpt-special`: on `--service llm`, force all requests to `claude-gpt-5.4-sp`
|
|
81
|
-
- `deepseek-v4-beta`:
|
|
82
|
-
- `claude-
|
|
83
|
-
- `claude-
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
573
|
-
? 'Skipped: stored token
|
|
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:
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
51
|
-
"globals": "^17.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
240
|
-
res.
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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 (
|
|
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
|
|
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-
|
|
130
|
-
name: '
|
|
131
|
-
description: 'Forces all requests to claude-
|
|
132
|
-
selectionDescription: 'Forces all requests to claude-
|
|
133
|
-
mappedModels: ['claude-
|
|
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
|
|
166
|
+
let payload;
|
|
167
167
|
try {
|
|
168
168
|
payload = await response.json();
|
|
169
169
|
} catch {
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|