scalemax 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +58 -38
- package/dist/commands/doctor.js +24 -3
- package/dist/commands/helper-bot.js +36 -19
- package/dist/configure/claude.js +76 -45
- package/dist/configure/cline.js +3 -1
- package/dist/configure/codex.js +21 -6
- package/dist/configure/continue.js +2 -2
- package/dist/configure/cursor.js +5 -2
- package/dist/configure/hermes.js +3 -3
- package/dist/configure/models.d.ts +83 -21
- package/dist/configure/models.js +313 -55
- package/dist/configure/openclaw.js +3 -3
- package/dist/configure/opencode.js +3 -3
- package/dist/configure/roo-code.d.ts +1 -0
- package/dist/configure/roo-code.js +15 -6
- package/dist/types/client.d.ts +17 -0
- package/dist/utils/api.d.ts +53 -27
- package/dist/utils/api.js +129 -92
- package/dist/utils/modelCheck.d.ts +8 -0
- package/dist/utils/modelCheck.js +32 -0
- package/dist/utils/modelId.d.ts +8 -0
- package/dist/utils/modelId.js +35 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,11 +151,11 @@ For clients without a stable official config file, the installer writes a ScaleM
|
|
|
151
151
|
|
|
152
152
|
### Codex
|
|
153
153
|
|
|
154
|
-
ScaleMax configures the official Codex custom-provider interface in `~/.codex/config.toml` with `wire_api = "responses"`, and stores the selected key in Codex's private `~/.codex/auth.json`. Detection is semantic: unrelated or ChatGPT-authenticated Codex files are not reported as ScaleMax-configured. Only authenticated
|
|
154
|
+
ScaleMax configures the official Codex custom-provider interface in `~/.codex/config.toml` with `wire_api = "responses"`, and stores the selected key in Codex's private `~/.codex/auth.json`. Detection is semantic: unrelated or ChatGPT-authenticated Codex files are not reported as ScaleMax-configured. Only GPT models from the authenticated catalog are eligible (`gpt-5.6-terra` first, or `auto` on an Auto key); a key with no GPT model is told so and Codex is left unchanged. The generated files are private, and verification performs a minimal `/v1/responses` request before reporting success.
|
|
155
155
|
|
|
156
156
|
### Claude Code
|
|
157
157
|
|
|
158
|
-
ScaleMax updates `~/.claude/settings.json` with `ANTHROPIC_BASE_URL=https://api.scalemax.pro`, gateway model discovery, and a cached `apiKeyHelper` that reads a private `~/.claude/scalemax.key` file. The raw key is not stored in the settings JSON. Because Claude Code's gateway discovery only accepts IDs beginning with `claude` or `anthropic`, catalogs that include Fable or Auto also receive a managed custom picker entry (`ANTHROPIC_CUSTOM_MODEL_OPTION=fable-5` or `auto`, with friendly names and descriptions), making `/model fable-5` and `/model auto` selectable; any prior custom picker values are restored when ScaleMax configuration is removed. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings
|
|
158
|
+
ScaleMax updates `~/.claude/settings.json` with `ANTHROPIC_BASE_URL=https://api.scalemax.pro`, gateway model discovery, and a cached `apiKeyHelper` that reads a private `~/.claude/scalemax.key` file. The raw key is not stored in the settings JSON. Because Claude Code's gateway discovery only accepts IDs beginning with `claude` or `anthropic`, catalogs that include Fable or Auto also receive a managed custom picker entry (`ANTHROPIC_CUSTOM_MODEL_OPTION=fable-5` or `auto`, with friendly names and descriptions), making `/model fable-5` and `/model auto` selectable; any prior custom picker values are restored when ScaleMax configuration is removed. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings and selects the main model from the key's own Claude models (Opus 5.5, then Opus 5, Sonnet 5.5, Sonnet 5, older Claude; never the `claude-sonnet-4-6` Claude Desktop alias or another provider's model). The background model (`ANTHROPIC_DEFAULT_HAIKU_MODEL`) is Haiku when the key lists it, otherwise the cheapest Claude it lists, so Claude Code never falls back to a Haiku id the key cannot use. The picker entry prefers an image-capable model; Fable 5.1 is labelled text only. The CLI does not write `CLAUDE_CODE_DISABLE_THINKING`: the ScaleMax gateway strips Claude Code's `thinking` control itself and locks native Claude requests to provider-side `low` effort. Fable uses its verified translated controls and supports `low`, `medium`, `high`, and `max`, with `low` as the default. Haiku 4.5 omits the unsupported effort field. It verifies a one-token Messages request and, if verification fails, removes only the ScaleMax-managed changes. Claude Code picker aliases such as `claude-opus-5[1m]` are normalized by the gateway.
|
|
159
159
|
|
|
160
160
|
### OpenCode
|
|
161
161
|
|
|
@@ -171,7 +171,7 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
|
|
|
171
171
|
|
|
172
172
|
### Roo Code (VS Code)
|
|
173
173
|
|
|
174
|
-
Roo Code stores provider profiles and secrets in VS Code extension storage, so ScaleMax does not mutate its private database. Instead, the CLI writes a private `0600` Roo settings-import file at `~/.scalemax/roo-code-import.json` and points the supported `roo-cline.autoImportSettingsPath` VS Code setting at it. Codex, Claude, and tool-capable DeepSeek profiles remain separate. DeepSeek is offered only when authenticated discovery reports `capabilities.tools=true`; otherwise Roo fails closed and points the user to Continue Chat/Edit. Reload VS Code once so Roo imports supported profiles into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
|
|
174
|
+
Roo Code stores provider profiles and secrets in VS Code extension storage, so ScaleMax does not mutate its private database. Instead, the CLI writes a private `0600` Roo settings-import file at `~/.scalemax/roo-code-import.json` and points the supported `roo-cline.autoImportSettingsPath` VS Code setting at it. Codex, Claude, and tool-capable DeepSeek profiles remain separate; the Codex profile is a GPT model on OpenAI keys and a strong Local model on Local Models keys, never the `claude-sonnet-4-6` alias, and every profile asks for at most 32,768 output tokens. DeepSeek is offered only when authenticated discovery reports `capabilities.tools=true`; otherwise Roo fails closed and points the user to Continue Chat/Edit. Reload VS Code once so Roo imports supported profiles into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
|
|
175
175
|
|
|
176
176
|
Roo Code 3.54 (its latest release) looks for ripgrep only in folders newer VS Code releases no longer use, so on recent VS Code versions every task hangs at "API Request…" with "Could not find ripgrep binary". Configuring Roo Code also patches the installed extension's `dist/extension.js` (in the editor's per-user extensions folder, never the VS Code app) so it tries `node_modules.asar.unpacked/@vscode/ripgrep-universal/bin/<platform>-<arch>/` first and then its original folders. The untouched file is kept once as `extension.js.scalemax-orig`. Run `npx scalemax@latest fix-roo` to apply only this fix (VS Code, Insiders, Cursor, Windsurf, VSCodium, Trae, Kiro, and remote servers), `scalemax fix-roo --undo` to restore the original, and reload VS Code afterwards. `scalemax doctor` flags installs that still need it.
|
|
177
177
|
|
|
@@ -3,6 +3,8 @@ import { ClientId, ConfigureResult } from '../types/client.js';
|
|
|
3
3
|
export declare function detectAll(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
4
4
|
export declare function printDetected(home?: string): Promise<import("../types/client.js").DetectionResult[]>;
|
|
5
5
|
export declare function clientAllowedForModels(clientId: ClientId, models: ModelInfo[]): boolean;
|
|
6
|
+
/** Why a detected client is not offered for this key, when the reason is worth saying. */
|
|
7
|
+
export declare function clientUnavailableReason(clientId: ClientId, models: ModelInfo[]): string | undefined;
|
|
6
8
|
export declare function rollbackAfterVerificationFailure(result: ConfigureResult, error: unknown): Promise<never>;
|
|
7
9
|
export declare function configureCommand(opts?: {
|
|
8
10
|
all?: boolean;
|
|
@@ -4,9 +4,10 @@ import ora from 'ora';
|
|
|
4
4
|
import { clients } from '../clients.js';
|
|
5
5
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
6
6
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
7
|
-
import { validateApiKey, isAnthropicModel, isModelRouteAvailable, AUTO_MODEL_ID, probeWorkingModels
|
|
7
|
+
import { validateApiKey, isAnthropicModel, isChatModel, isModelRouteAvailable, AUTO_MODEL_ID, probeWorkingModels } from '../utils/api.js';
|
|
8
|
+
import { saveModelCheck } from '../utils/modelCheck.js';
|
|
8
9
|
import { homeDir } from '../utils/paths.js';
|
|
9
|
-
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
10
|
+
import { CLAUDE_CODE_NEEDS_CLAUDE_MESSAGE, CODEX_NEEDS_GPT_MESSAGE, codexModelIds, defaultModelIdsFor, isExactChatCompletionsModelId, modelIdKey } from '../configure/models.js';
|
|
10
11
|
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
11
12
|
export async function detectAll(home = homeDir()) {
|
|
12
13
|
const ctx = { home, platform: process.platform, env: process.env };
|
|
@@ -57,10 +58,12 @@ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL)
|
|
|
57
58
|
* for "did Anthropic make this."
|
|
58
59
|
*/
|
|
59
60
|
function reachableByClaudeCode(model) {
|
|
60
|
-
return isAnthropicModel(model) || model.id === AUTO_MODEL_ID;
|
|
61
|
+
return isAnthropicModel(model) || modelIdKey(model.id) === AUTO_MODEL_ID;
|
|
61
62
|
}
|
|
62
63
|
export function clientAllowedForModels(clientId, models) {
|
|
63
|
-
|
|
64
|
+
// Image, video, embedding and voice rows share the catalog; an image model
|
|
65
|
+
// owned by `scalemax` must not make Codex look usable on a Claude-only key.
|
|
66
|
+
models = models.filter(model => isModelRouteAvailable(model) && isChatModel(model));
|
|
64
67
|
if (!models.length)
|
|
65
68
|
return false;
|
|
66
69
|
// These clients use either a native family-specific adapter or the gateway's
|
|
@@ -72,8 +75,11 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
72
75
|
return models.some(model => isContinueEligibleModelId(model.id));
|
|
73
76
|
if (clientId === 'claude')
|
|
74
77
|
return models.some(reachableByClaudeCode);
|
|
78
|
+
// Codex runs on GPT models only (and on Auto for an Auto key). A Claude id,
|
|
79
|
+
// the `claude-sonnet-4-6` alias, DeepSeek or a free preview model failed
|
|
80
|
+
// every Codex request it was configured with.
|
|
75
81
|
if (clientId === 'codex')
|
|
76
|
-
return models.some(model =>
|
|
82
|
+
return codexModelIds({ models }).length > 0 || models.some(model => modelIdKey(model.id) === AUTO_MODEL_ID);
|
|
77
83
|
if (clientId === 'hermes' && models.every(model => isExactChatCompletionsModelId(model.id)))
|
|
78
84
|
return models.some(model => model.capabilities?.tools === true);
|
|
79
85
|
// Roo is an agent, not a text-only chat surface. Its task loop depends on
|
|
@@ -85,6 +91,16 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
85
91
|
return true;
|
|
86
92
|
return models.some(model => !isAnthropicModel(model));
|
|
87
93
|
}
|
|
94
|
+
/** Why a detected client is not offered for this key, when the reason is worth saying. */
|
|
95
|
+
export function clientUnavailableReason(clientId, models) {
|
|
96
|
+
if (clientAllowedForModels(clientId, models))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (clientId === 'codex')
|
|
99
|
+
return CODEX_NEEDS_GPT_MESSAGE.replace('was not set up', 'is not offered');
|
|
100
|
+
if (clientId === 'claude')
|
|
101
|
+
return CLAUDE_CODE_NEEDS_CLAUDE_MESSAGE.replace('was not set up', 'is not offered');
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
88
104
|
export async function rollbackAfterVerificationFailure(result, error) {
|
|
89
105
|
if (!result.changed)
|
|
90
106
|
throw error;
|
|
@@ -131,27 +147,17 @@ export async function configureCommand(opts = {}) {
|
|
|
131
147
|
throw new Error(keyInfo.error || 'ScaleMax returned no models for this API key. No client configuration was changed.');
|
|
132
148
|
const providerScope = keyInfo.key?.provider_scope || 'both';
|
|
133
149
|
const selectedBaseUrl = keyInfo.baseUrl || SCALEMAX_BASE_URL;
|
|
134
|
-
// LIVE-PROBE the key's models with a small request and keep only the ones
|
|
135
|
-
// that actually answer, so a client is configured with working models and
|
|
136
|
-
// defaults to a working one. Bounded (concurrency + total time); if nothing
|
|
137
|
-
// probes clean we KEEP the entitlement-filtered list rather than fail — a
|
|
138
|
-
// slow/degraded upstream must not leave the user with zero models.
|
|
139
|
-
try {
|
|
140
|
-
const probeSpinner = ora('Checking which models respond').start();
|
|
141
|
-
const probe = await probeWorkingModels({ apiKey, baseUrl: selectedBaseUrl, models: keyInfo.models, providerScope });
|
|
142
|
-
if (probe.models.length && !probe.fallback) {
|
|
143
|
-
keyInfo.models = probe.models;
|
|
144
|
-
probeSpinner.succeed(`${probe.models.length}/${probe.probedCount} models responded`);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
probeSpinner.warn(probe.warning || 'No model answered the probe in time; using the full authenticated catalog');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch { /* probing is best-effort; never blocks setup */ }
|
|
151
150
|
const availableClients = clients.filter(c => clientAllowedForModels(c.id, keyInfo.models));
|
|
152
151
|
console.log(chalk.dim(`API key scope: ${scopeHint(providerScope)}. Clients are filtered against the authenticated model catalog.`));
|
|
153
152
|
const detections = await printDetected(home);
|
|
154
153
|
const detectedIds = new Set(detections.filter(d => d.installed).map(d => d.id));
|
|
154
|
+
for (const client of clients) {
|
|
155
|
+
if (!detectedIds.has(client.id))
|
|
156
|
+
continue;
|
|
157
|
+
const reason = clientUnavailableReason(client.id, keyInfo.models);
|
|
158
|
+
if (reason)
|
|
159
|
+
console.log(chalk.yellow(`${client.name}: ${reason}`));
|
|
160
|
+
}
|
|
155
161
|
let chosen;
|
|
156
162
|
if (opts.all)
|
|
157
163
|
chosen = availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id);
|
|
@@ -170,6 +176,35 @@ export async function configureCommand(opts = {}) {
|
|
|
170
176
|
console.log(chalk.yellow('No detected clients to configure.'));
|
|
171
177
|
return;
|
|
172
178
|
}
|
|
179
|
+
// Check, with one small request each, the models setup is about to write
|
|
180
|
+
// as defaults for the chosen and detected clients. A default that fails is
|
|
181
|
+
// skipped (the next pick for that slot is checked instead); every entitled
|
|
182
|
+
// model stays in the lists written to clients. Bounded in count and time,
|
|
183
|
+
// and if nothing answers nothing is skipped: a slow upstream or an empty
|
|
184
|
+
// balance must not leave a client with no model.
|
|
185
|
+
let avoidModelIds = [];
|
|
186
|
+
const probeClients = [...new Set([...chosen, ...availableClients.filter(c => detectedIds.has(c.id)).map(c => c.id)])];
|
|
187
|
+
try {
|
|
188
|
+
const probeSpinner = ora('Checking the default models (one small request each)').start();
|
|
189
|
+
const probe = await probeWorkingModels({
|
|
190
|
+
apiKey,
|
|
191
|
+
baseUrl: selectedBaseUrl,
|
|
192
|
+
models: keyInfo.models,
|
|
193
|
+
providerScope,
|
|
194
|
+
targets: models => defaultModelIdsFor({ models, providerScope }, probeClients),
|
|
195
|
+
});
|
|
196
|
+
if (probe.fallback)
|
|
197
|
+
probeSpinner.warn(probe.warning || 'No default model answered in time; nothing was skipped.');
|
|
198
|
+
else if (probe.failedIds.length) {
|
|
199
|
+
avoidModelIds = probe.failedIds;
|
|
200
|
+
probeSpinner.warn(`${probe.workingCount}/${probe.probedCount} default models answered; skipped ${probe.failedIds.join(', ')} for now`);
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
probeSpinner.succeed(`${probe.workingCount}/${probe.probedCount} default models answered`);
|
|
204
|
+
// doctor reads this so it expects the same defaults setup wrote.
|
|
205
|
+
await saveModelCheck(home, apiKey, avoidModelIds).catch(() => undefined);
|
|
206
|
+
}
|
|
207
|
+
catch { /* checking is best-effort; never blocks setup */ }
|
|
173
208
|
const successful = new Set();
|
|
174
209
|
const failures = [];
|
|
175
210
|
for (const client of availableClients.filter(c => chosen.includes(c.id))) {
|
|
@@ -206,7 +241,7 @@ export async function configureCommand(opts = {}) {
|
|
|
206
241
|
}
|
|
207
242
|
const spinner = ora(`Configuring ${client.name}`).start();
|
|
208
243
|
try {
|
|
209
|
-
const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
244
|
+
const ctx = { ...baseCtx, apiKey, baseUrl: selectedBaseUrl, providerScope, models: keyInfo.models, avoidModelIds, interactive: Boolean(process.stdin.isTTY), allowAppModification };
|
|
210
245
|
const res = await client.configure(ctx);
|
|
211
246
|
spinner.succeed(`${client.name} ${res.message}`);
|
|
212
247
|
if (res.note)
|
|
@@ -233,21 +268,6 @@ export async function configureCommand(opts = {}) {
|
|
|
233
268
|
failures.push(`${client.name}: ${message}`);
|
|
234
269
|
}
|
|
235
270
|
}
|
|
236
|
-
// Pin the working default as this key's `claude-sonnet-4-6` backend so Claude
|
|
237
|
-
// Desktop (which always sends that id) routes to a model this key can serve.
|
|
238
|
-
// Best-effort: an older gateway without the endpoint, or a scope that already
|
|
239
|
-
// has a fixed default, is skipped silently. Never fails the run.
|
|
240
|
-
if (successful.size > 0) {
|
|
241
|
-
const target = claudeAliasBackendPinTarget(keyInfo.models);
|
|
242
|
-
if (target) {
|
|
243
|
-
try {
|
|
244
|
-
const pin = await pinClaudeAliasBackend({ apiKey, baseUrl: selectedBaseUrl, model: target });
|
|
245
|
-
if (pin.ok && !pin.skipped)
|
|
246
|
-
console.log(chalk.dim(`Claude Desktop's claude-sonnet-4-6 now routes to ${target} on this key.`));
|
|
247
|
-
}
|
|
248
|
-
catch { /* endpoint absent or transient; ignore */ }
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
271
|
if (opts.promptLaunch !== false && !opts.all && process.stdin.isTTY)
|
|
252
272
|
await maybeLaunchCodex(chosen, successful, selectedBaseUrl);
|
|
253
273
|
if (failures.length)
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,6 +3,8 @@ import ora from 'ora';
|
|
|
3
3
|
import { detectAll } from './configure.js';
|
|
4
4
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
5
5
|
import { validateApiKey, testStreaming } from '../utils/api.js';
|
|
6
|
+
import { claudeCodeBackgroundModel, fastChatCompletionsModel, preferredChatCompletionsModel } from '../configure/models.js';
|
|
7
|
+
import { loadModelCheck } from '../utils/modelCheck.js';
|
|
6
8
|
import { homeDir } from '../utils/paths.js';
|
|
7
9
|
import { clients } from '../clients.js';
|
|
8
10
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
@@ -30,6 +32,13 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
30
32
|
let failures = 0;
|
|
31
33
|
const key = await loadApiKey(home);
|
|
32
34
|
let models = [];
|
|
35
|
+
let baseUrl = SCALEMAX_BASE_URL;
|
|
36
|
+
// Setup passes the key's scope to every client (Roo writes its profiles by
|
|
37
|
+
// scope); without it doctor expected a different Roo profile set.
|
|
38
|
+
let providerScope;
|
|
39
|
+
// The models setup's live check skipped, so each client is checked against
|
|
40
|
+
// the same defaults setup wrote (configure/models.ts picks them).
|
|
41
|
+
const avoidModelIds = key ? await loadModelCheck(home, key) : [];
|
|
33
42
|
if (!key) {
|
|
34
43
|
console.log(`${chalk.red('✗')} API key missing`);
|
|
35
44
|
failures++;
|
|
@@ -38,11 +47,24 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
38
47
|
const s = ora('Checking API key and models').start();
|
|
39
48
|
const res = await validateApiKey(key);
|
|
40
49
|
models = res.models;
|
|
50
|
+
baseUrl = res.baseUrl || SCALEMAX_BASE_URL;
|
|
51
|
+
providerScope = res.key?.provider_scope;
|
|
41
52
|
if (res.ok && models.length) {
|
|
42
53
|
s.succeed(`API reachable, key valid, ${models.length} models loaded`);
|
|
43
54
|
console.log(`${chalk.green('✓')} Usage and key metadata accessible through the authenticated API`);
|
|
44
55
|
const st = ora('Checking streaming (small billable request)').start();
|
|
45
|
-
|
|
56
|
+
// Stream a cheap chat default: the catalog also lists image, video and
|
|
57
|
+
// embedding rows (which /chat/completions 404s) and lists the
|
|
58
|
+
// claude-sonnet-4-6 alias first.
|
|
59
|
+
const catalog = { models, avoidModelIds };
|
|
60
|
+
let streamModel;
|
|
61
|
+
try {
|
|
62
|
+
streamModel = fastChatCompletionsModel(catalog) || claudeCodeBackgroundModel(catalog) || preferredChatCompletionsModel(catalog);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
streamModel = undefined;
|
|
66
|
+
}
|
|
67
|
+
const stream = await testStreaming(key, streamModel, baseUrl);
|
|
46
68
|
if (stream.ok)
|
|
47
69
|
st.succeed('Streaming available');
|
|
48
70
|
else {
|
|
@@ -69,8 +91,7 @@ export async function doctorCommand(home = homeDir()) {
|
|
|
69
91
|
}
|
|
70
92
|
if (client.verify && key && models.length) {
|
|
71
93
|
try {
|
|
72
|
-
const
|
|
73
|
-
const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl: checked.baseUrl || SCALEMAX_BASE_URL, models });
|
|
94
|
+
const v = await client.verify({ home, platform: process.platform, env: process.env, apiKey: key, baseUrl, providerScope, models, avoidModelIds });
|
|
74
95
|
console.log(`${chalk.green('✓')} ${client.name} ${v.message}`);
|
|
75
96
|
}
|
|
76
97
|
catch (error) {
|
|
@@ -4,15 +4,16 @@ import { createInterface } from 'node:readline/promises';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { clients } from '../clients.js';
|
|
6
6
|
import { helperDocsContext } from './helper-docs.js';
|
|
7
|
-
import { rollbackAfterVerificationFailure } from './configure.js';
|
|
8
|
-
import {
|
|
7
|
+
import { clientAllowedForModels, rollbackAfterVerificationFailure } from './configure.js';
|
|
8
|
+
import { loadModelCheck } from '../utils/modelCheck.js';
|
|
9
|
+
import { createChatCompletion, filterModelsForScope, isAnthropicModel, isChatModel } from '../utils/api.js';
|
|
9
10
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
10
11
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
11
12
|
import { homeDir } from '../utils/paths.js';
|
|
12
13
|
import { validateApiKey } from '../utils/api.js';
|
|
13
14
|
import { commandExists } from '../utils/exec.js';
|
|
14
15
|
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
15
|
-
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
16
|
+
import { CODEX_NEEDS_GPT_MESSAGE, isExactChatCompletionsModelId, modelIdKey, sameModelId } from '../configure/models.js';
|
|
16
17
|
import { isContinueEligibleModelId } from '../configure/continue.js';
|
|
17
18
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
18
19
|
|
|
@@ -708,21 +709,25 @@ codex
|
|
|
708
709
|
return undefined;
|
|
709
710
|
}
|
|
710
711
|
export function selectHelperModel(models, requested) {
|
|
712
|
+
// The helper talks over /chat/completions: never pick an image, video,
|
|
713
|
+
// embedding or voice row. Ids match in any `[1m]` spelling.
|
|
714
|
+
const chat = models.filter(isChatModel);
|
|
711
715
|
if (requested) {
|
|
712
|
-
const exact =
|
|
716
|
+
const exact = chat.find(model => model.id.trim().toLowerCase() === requested.trim().toLowerCase())
|
|
717
|
+
|| chat.find(model => sameModelId(model.id, requested));
|
|
713
718
|
if (!exact)
|
|
714
719
|
throw new Error(`Model "${requested}" is not available to this API key. Run \`scalemax models\` to see available models.`);
|
|
715
720
|
return exact.id;
|
|
716
721
|
}
|
|
717
722
|
const preferred = ['gpt-5.6-luna', 'claude-haiku-4-5-20251001', 'fable-5', 'gpt-5.6-terra', 'gpt-5.6-sol'];
|
|
718
723
|
for (const id of preferred) {
|
|
719
|
-
const match =
|
|
724
|
+
const match = chat.find(model => sameModelId(model.id, id));
|
|
720
725
|
if (match)
|
|
721
726
|
return match.id;
|
|
722
727
|
}
|
|
723
|
-
if (!
|
|
728
|
+
if (!chat[0])
|
|
724
729
|
throw new Error('No ScaleMax model is available to this API key.');
|
|
725
|
-
return
|
|
730
|
+
return chat[0].id;
|
|
726
731
|
}
|
|
727
732
|
export async function requestHelperReply(question, history, connection, complete = createChatCompletion) {
|
|
728
733
|
const cleanQuestion = redactHelperSecrets(question.trim(), connection.apiKey);
|
|
@@ -832,6 +837,19 @@ function requestedProviderScope(question, target) {
|
|
|
832
837
|
return 'openai';
|
|
833
838
|
return undefined;
|
|
834
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* The GPT/Codex family. The live catalog publishes those models under the
|
|
842
|
+
* `scalemax` owner (not `openai`, which now labels only embedding rows), so
|
|
843
|
+
* the owner alone matched no chat model at all.
|
|
844
|
+
*/
|
|
845
|
+
function isOpenAiFamilyModel(model) {
|
|
846
|
+
const family = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
847
|
+
if (family === 'openai')
|
|
848
|
+
return true;
|
|
849
|
+
if (family === 'scalemax')
|
|
850
|
+
return /^(gpt-|codex-)/i.test(modelIdKey(model.id));
|
|
851
|
+
return !family && !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id);
|
|
852
|
+
}
|
|
835
853
|
function actionModels(connection, requestedScope) {
|
|
836
854
|
const rawCatalog = connection.modelCatalog?.length
|
|
837
855
|
? connection.modelCatalog
|
|
@@ -840,7 +858,9 @@ function actionModels(connection, requestedScope) {
|
|
|
840
858
|
// The authenticated catalog already includes reseller and extra-family
|
|
841
859
|
// grants. Validate the primary scope, then narrow only for explicit user
|
|
842
860
|
// intent; never try to reconstruct effective entitlement client-side.
|
|
843
|
-
|
|
861
|
+
// Client actions configure chat clients only; image, video, embedding and
|
|
862
|
+
// voice rows would otherwise be written into them as chat models.
|
|
863
|
+
const catalog = filterModelsForScope(rawCatalog, entitlement).filter(isChatModel);
|
|
844
864
|
if (!requestedScope || requestedScope === 'reseller' || requestedScope === 'local')
|
|
845
865
|
return catalog;
|
|
846
866
|
const exactModelByScope = {
|
|
@@ -851,19 +871,14 @@ function actionModels(connection, requestedScope) {
|
|
|
851
871
|
};
|
|
852
872
|
const exactModel = exactModelByScope[requestedScope];
|
|
853
873
|
if (exactModel)
|
|
854
|
-
return catalog.filter(model => model.id
|
|
874
|
+
return catalog.filter(model => modelIdKey(model.id) === exactModel);
|
|
855
875
|
if (requestedScope === 'both') {
|
|
856
|
-
return catalog.filter(model => isAnthropicModel(model)
|
|
857
|
-
|| String(model.provider || model.owned_by || '').trim().toLowerCase() === 'openai');
|
|
876
|
+
return catalog.filter(model => isAnthropicModel(model) || isOpenAiFamilyModel(model));
|
|
858
877
|
}
|
|
859
878
|
if (requestedScope === 'anthropic')
|
|
860
879
|
return catalog.filter(isAnthropicModel);
|
|
861
|
-
if (requestedScope === 'openai')
|
|
862
|
-
return catalog.filter(
|
|
863
|
-
const family = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
864
|
-
return family ? family === 'openai' : !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id);
|
|
865
|
-
});
|
|
866
|
-
}
|
|
880
|
+
if (requestedScope === 'openai')
|
|
881
|
+
return catalog.filter(isOpenAiFamilyModel);
|
|
867
882
|
return catalog.filter(model => String(model.provider || model.owned_by || '').trim().toLowerCase() === requestedScope);
|
|
868
883
|
}
|
|
869
884
|
function helperClientAllowedForModels(clientId, models) {
|
|
@@ -968,8 +983,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
968
983
|
const models = actionModels(connection, requestedScope);
|
|
969
984
|
if (!models.length)
|
|
970
985
|
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : requestedScope === 'glm-5.2' ? 'GLM 5.2' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
971
|
-
if (client.id === 'codex' &&
|
|
972
|
-
throw new Error(
|
|
986
|
+
if (client.id === 'codex' && !clientAllowedForModels('codex', models))
|
|
987
|
+
throw new Error(`${CODEX_NEEDS_GPT_MESSAGE} No configuration was changed.`);
|
|
973
988
|
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
974
989
|
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
975
990
|
const ctx = {
|
|
@@ -980,6 +995,8 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
980
995
|
baseUrl: connection.baseUrl,
|
|
981
996
|
providerScope: connection.providerScope || 'both',
|
|
982
997
|
models,
|
|
998
|
+
// The same defaults setup and doctor use for this key.
|
|
999
|
+
avoidModelIds: await loadModelCheck(home, connection.apiKey).catch(() => []),
|
|
983
1000
|
interactive: false,
|
|
984
1001
|
allowAppModification: false
|
|
985
1002
|
};
|
package/dist/configure/claude.js
CHANGED
|
@@ -6,20 +6,30 @@ import { clientConfigPath } from './paths.js';
|
|
|
6
6
|
import { writeMergedJsonConfig } from './common.js';
|
|
7
7
|
import { GENERATED_HEADER } from '../constants.js';
|
|
8
8
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
9
|
-
import { anthropicOrigin,
|
|
9
|
+
import { anthropicOrigin, claudeCodeBackgroundModel, claudeCodeCustomModelId, claudeCodeMainModel, claudeCodeModelIds, modelAcceptsImages, modelIdKey, AUTO_MODEL_ID, canonicalModelId, sameModelId } from './models.js';
|
|
10
|
+
import { contextSuffix } from '../utils/modelId.js';
|
|
10
11
|
import { removeClientSecret, verifyClientSecret, writeClientSecret } from './client-secret.js';
|
|
11
12
|
const id = 'claude';
|
|
12
13
|
const name = 'Claude Code';
|
|
13
14
|
const discoveryKey = 'CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY';
|
|
14
15
|
const fastModelKey = 'ANTHROPIC_DEFAULT_HAIKU_MODEL';
|
|
15
16
|
const helperTtlKey = 'CLAUDE_CODE_API_KEY_HELPER_TTL_MS';
|
|
16
|
-
|
|
17
|
+
// Written (as '1') by 0.1.49 and earlier, no longer managed. It is a real
|
|
18
|
+
// Claude Code switch that omits `thinking` from every request, but the
|
|
19
|
+
// gateway already strips `thinking` from Claude Code requests unless the
|
|
20
|
+
// operator turns passthrough on, so writing it only overrode that switch.
|
|
21
|
+
// Reconfiguring removes the value earlier versions wrote.
|
|
22
|
+
const legacyDisableThinkingKey = 'CLAUDE_CODE_DISABLE_THINKING';
|
|
17
23
|
// Claude Code's gateway discovery intentionally ignores model IDs that do not
|
|
18
24
|
// start with `claude` or `anthropic`. Fable and Auto both need managed custom
|
|
19
25
|
// picker entries so their public ScaleMax IDs remain selectable.
|
|
20
26
|
const customModelOptionKey = 'ANTHROPIC_CUSTOM_MODEL_OPTION';
|
|
21
27
|
const customModelOptionNameKey = 'ANTHROPIC_CUSTOM_MODEL_OPTION_NAME';
|
|
22
28
|
const customModelOptionDescriptionKey = 'ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION';
|
|
29
|
+
// Bare public ids. The value written to ANTHROPIC_CUSTOM_MODEL_OPTION carries
|
|
30
|
+
// the catalog's context suffix when it has one (`fable-5[1m]`): Claude Code
|
|
31
|
+
// reads `[1m]` as a 1M context window and strips it before calling the API,
|
|
32
|
+
// while a bare id is budgeted (and auto-compacted) at 200k.
|
|
23
33
|
const fableModelId = 'fable-5';
|
|
24
34
|
const fableModelName = 'Fable 5';
|
|
25
35
|
const fableModelDescription = 'Fable 5 via ScaleMax';
|
|
@@ -31,6 +41,8 @@ const fableModelDescription = 'Fable 5 via ScaleMax';
|
|
|
31
41
|
const fable51ModelId = 'fable-5.1';
|
|
32
42
|
const fable51ModelName = 'Fable 5.1';
|
|
33
43
|
const fable51ModelDescription = 'Fable 5.1 via ScaleMax';
|
|
44
|
+
// Fable 5.1 cannot read images; say so when it is the picker option.
|
|
45
|
+
const fable51TextOnlyDescription = 'Fable 5.1 via ScaleMax (text only, no images)';
|
|
34
46
|
const autoModelName = 'Auto';
|
|
35
47
|
const autoModelDescription = 'Auto via ScaleMax';
|
|
36
48
|
const verificationTimeoutMs = 60_000;
|
|
@@ -44,8 +56,7 @@ const managedEnvKeys = [
|
|
|
44
56
|
customModelOptionDescriptionKey,
|
|
45
57
|
discoveryKey,
|
|
46
58
|
fastModelKey,
|
|
47
|
-
helperTtlKey
|
|
48
|
-
disableThinkingKey
|
|
59
|
+
helperTtlKey
|
|
49
60
|
];
|
|
50
61
|
const managedCustomModelValues = new Set([
|
|
51
62
|
fableModelId,
|
|
@@ -54,6 +65,7 @@ const managedCustomModelValues = new Set([
|
|
|
54
65
|
fable51ModelId,
|
|
55
66
|
fable51ModelName,
|
|
56
67
|
fable51ModelDescription,
|
|
68
|
+
fable51TextOnlyDescription,
|
|
57
69
|
AUTO_MODEL_ID,
|
|
58
70
|
autoModelName,
|
|
59
71
|
autoModelDescription
|
|
@@ -68,48 +80,45 @@ function helperCommand(home, platform) {
|
|
|
68
80
|
function asRecord(value) {
|
|
69
81
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
70
82
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function hasAutoModel(ctx) {
|
|
78
|
-
return modelIds(ctx).some(id => id.trim().toLowerCase() === AUTO_MODEL_ID);
|
|
79
|
-
}
|
|
83
|
+
/**
|
|
84
|
+
* The one custom picker entry (see claudeCodeCustomModelId): Fable 5 when the
|
|
85
|
+
* key has it, otherwise an image-capable hidden model, otherwise Fable 5.1
|
|
86
|
+
* (labelled text only) or Auto. Written in the catalog's spelling, so
|
|
87
|
+
* `fable-5[1m]` keeps Claude Code's 1M window.
|
|
88
|
+
*/
|
|
80
89
|
function managedCustomModel(ctx) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
90
|
+
const pick = claudeCodeCustomModelId(ctx);
|
|
91
|
+
if (!pick)
|
|
92
|
+
return null;
|
|
93
|
+
const key = modelIdKey(pick);
|
|
94
|
+
if (key === fableModelId || key === 'claude-fable-5') {
|
|
95
|
+
return { option: `${fableModelId}${contextSuffix(pick)}`, name: fableModelName, description: fableModelDescription };
|
|
87
96
|
}
|
|
88
|
-
if (
|
|
97
|
+
if (key === fable51ModelId) {
|
|
89
98
|
return {
|
|
90
|
-
option: fable51ModelId
|
|
99
|
+
option: `${fable51ModelId}${contextSuffix(pick)}`,
|
|
91
100
|
name: fable51ModelName,
|
|
92
|
-
description: fable51ModelDescription
|
|
101
|
+
description: modelAcceptsImages(ctx, pick) ? fable51ModelDescription : fable51TextOnlyDescription,
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
|
-
if (
|
|
96
|
-
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
description: autoModelDescription
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
104
|
+
if (key === AUTO_MODEL_ID)
|
|
105
|
+
return { option: AUTO_MODEL_ID, name: autoModelName, description: autoModelDescription };
|
|
106
|
+
const label = ctx.models?.find(model => model.id.trim() === pick)?.display_name || canonicalModelId(pick);
|
|
107
|
+
return { option: pick, name: label, description: `${label} via ScaleMax` };
|
|
103
108
|
}
|
|
104
109
|
function matchesManagedCustomModel(env, customModel) {
|
|
105
|
-
|
|
110
|
+
// `fable-5` written before the catalog advertised `fable-5[1m]` still works,
|
|
111
|
+
// and so does Fable 5.1's label from before it said "text only".
|
|
112
|
+
const description = env[customModelOptionDescriptionKey];
|
|
113
|
+
return sameModelId(env[customModelOptionKey], customModel.option)
|
|
106
114
|
&& env[customModelOptionNameKey] === customModel.name
|
|
107
|
-
&&
|
|
115
|
+
&& (description === customModel.description
|
|
116
|
+
|| (customModel.description === fable51TextOnlyDescription && description === fable51ModelDescription));
|
|
108
117
|
}
|
|
109
118
|
export async function configure(ctx) {
|
|
110
119
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
111
|
-
const model =
|
|
112
|
-
const quick =
|
|
120
|
+
const model = claudeCodeMainModel(ctx);
|
|
121
|
+
const quick = claudeCodeBackgroundModel(ctx);
|
|
113
122
|
const customModel = managedCustomModel(ctx);
|
|
114
123
|
const baseUrl = anthropicOrigin(ctx.baseUrl);
|
|
115
124
|
const apiKeyHelper = helperCommand(ctx.home, ctx.platform);
|
|
@@ -132,9 +141,14 @@ export async function configure(ctx) {
|
|
|
132
141
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
133
142
|
ANTHROPIC_MODEL: model,
|
|
134
143
|
[discoveryKey]: '1',
|
|
135
|
-
[helperTtlKey]: '900000'
|
|
136
|
-
[disableThinkingKey]: '1'
|
|
144
|
+
[helperTtlKey]: '900000'
|
|
137
145
|
};
|
|
146
|
+
if (oldMetadata.managedBy === 'ScaleMax CLI' && oldMetadata.disableThinking !== undefined && oldMetadata.disableThinking === nextEnv[legacyDisableThinkingKey]) {
|
|
147
|
+
if (Object.prototype.hasOwnProperty.call(previousEnv, legacyDisableThinkingKey))
|
|
148
|
+
nextEnv[legacyDisableThinkingKey] = previousEnv[legacyDisableThinkingKey];
|
|
149
|
+
else
|
|
150
|
+
delete nextEnv[legacyDisableThinkingKey];
|
|
151
|
+
}
|
|
138
152
|
if (customModel) {
|
|
139
153
|
// These are public model IDs, not upstream/provider aliases. Claude
|
|
140
154
|
// Code's custom option bypasses its `claude*` discovery filter while
|
|
@@ -147,10 +161,15 @@ export async function configure(ctx) {
|
|
|
147
161
|
// A key can be reconfigured from a Fable/Auto catalog to one that does
|
|
148
162
|
// not include either model. Remove only the values this CLI previously
|
|
149
163
|
// managed, restoring any user values captured on the first configure.
|
|
164
|
+
const written = {
|
|
165
|
+
[customModelOptionKey]: oldMetadata.customModelOption,
|
|
166
|
+
[customModelOptionNameKey]: oldMetadata.customModelOptionName,
|
|
167
|
+
[customModelOptionDescriptionKey]: oldMetadata.customModelOptionDescription,
|
|
168
|
+
};
|
|
150
169
|
for (const key of [customModelOptionKey, customModelOptionNameKey, customModelOptionDescriptionKey]) {
|
|
151
170
|
if (Object.prototype.hasOwnProperty.call(previousEnv, key))
|
|
152
171
|
nextEnv[key] = previousEnv[key];
|
|
153
|
-
else if (managedCustomModelValues.has(
|
|
172
|
+
else if (managedCustomModelValues.has(canonicalModelId(nextEnv[key])) || (written[key] != null && nextEnv[key] === written[key]))
|
|
154
173
|
delete nextEnv[key];
|
|
155
174
|
}
|
|
156
175
|
}
|
|
@@ -172,7 +191,6 @@ export async function configure(ctx) {
|
|
|
172
191
|
gatewayDiscovery: '1',
|
|
173
192
|
fastModel: quick || null,
|
|
174
193
|
helperTtl: '900000',
|
|
175
|
-
disableThinking: '1',
|
|
176
194
|
customModelOption: customModel?.option ?? null,
|
|
177
195
|
customModelOptionName: customModel?.name ?? null,
|
|
178
196
|
customModelOptionDescription: customModel?.description ?? null,
|
|
@@ -208,13 +226,25 @@ export async function verify(ctx) {
|
|
|
208
226
|
const model = String(env.ANTHROPIC_MODEL || '');
|
|
209
227
|
const customModel = managedCustomModel(ctx);
|
|
210
228
|
const customModelMatches = !customModel || matchesManagedCustomModel(env, customModel);
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
// Any Claude model the key has is a working main model, in any spelling (a
|
|
230
|
+
// config written before the catalog advertised `[1m]` still reaches it).
|
|
231
|
+
// The `claude-sonnet-4-6` alias, a GPT model or a model the key lacks is not:
|
|
232
|
+
// 0.1.49 wrote the alias, which cannot read images.
|
|
233
|
+
const usable = claudeCodeModelIds(ctx);
|
|
234
|
+
if (!usable.some(id => sameModelId(id, model))) {
|
|
235
|
+
throw new Error(`Claude Code is set to ${model || 'no model'}, which is not a Claude model on this API key. Run \`npx scalemax@latest update\` to switch it to ${claudeCodeMainModel(ctx)}.`);
|
|
236
|
+
}
|
|
237
|
+
if (!customModelMatches || env.ANTHROPIC_BASE_URL !== anthropicOrigin(ctx.baseUrl) || settings.apiKeyHelper !== helperCommand(ctx.home, ctx.platform) || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || env[discoveryKey] !== '1') {
|
|
238
|
+
throw new Error('Claude Code settings do not match the authenticated ScaleMax model catalog. Run `npx scalemax@latest update`.');
|
|
213
239
|
}
|
|
214
240
|
await verifyClientSecret(secretPath(ctx.home), ctx);
|
|
215
|
-
const quick =
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
const quick = claudeCodeBackgroundModel(ctx);
|
|
242
|
+
const background = env[fastModelKey];
|
|
243
|
+
// Unset, Claude Code calls its built-in Haiku id for background work, which
|
|
244
|
+
// a key without Haiku refuses; set, it must be a model this key lists.
|
|
245
|
+
if (quick ? !usable.some(id => sameModelId(id, background)) : background !== undefined && !usable.some(id => sameModelId(id, background))) {
|
|
246
|
+
throw new Error(`Claude Code's background model (${background === undefined ? 'not set' : String(background)}) is not a Claude model on this API key. Run \`npx scalemax@latest update\` to set it to ${quick || 'a listed model'}.`);
|
|
247
|
+
}
|
|
218
248
|
// Verification proves that the authenticated Messages route works; it does
|
|
219
249
|
// not need to warm the user's slower default Opus model. Prefer Haiku when
|
|
220
250
|
// it is present, while keeping ANTHROPIC_MODEL unchanged for real sessions.
|
|
@@ -284,8 +314,9 @@ export async function remove(ctx) {
|
|
|
284
314
|
restore(fastModelKey);
|
|
285
315
|
if (env[helperTtlKey] === managed.helperTtl)
|
|
286
316
|
restore(helperTtlKey);
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
// Only configs written by 0.1.49 and earlier recorded this value.
|
|
318
|
+
if (managed.disableThinking !== undefined && env[legacyDisableThinkingKey] === managed.disableThinking)
|
|
319
|
+
restore(legacyDisableThinkingKey);
|
|
289
320
|
if (env[customModelOptionKey] === managed.customModelOption)
|
|
290
321
|
restore(customModelOptionKey);
|
|
291
322
|
if (env[customModelOptionNameKey] === managed.customModelOptionName)
|
package/dist/configure/cline.js
CHANGED
|
@@ -14,6 +14,7 @@ const cliVersion = '3.0.47';
|
|
|
14
14
|
const extensionVersion = '4.0.12';
|
|
15
15
|
const extensionId = 'saoudrizwan.claude-dev';
|
|
16
16
|
const cliProviderId = 'openai-native';
|
|
17
|
+
const CLINE_MAX_OUTPUT_TOKENS = 32_768;
|
|
17
18
|
const legacyProviderId = 'scalemax';
|
|
18
19
|
const extensionCredentialHashKey = 'scalemaxManagedOpenAiApiKeySha256';
|
|
19
20
|
/**
|
|
@@ -330,7 +331,8 @@ function positiveInteger(value) {
|
|
|
330
331
|
function extensionModelInfo(ctx, modelId) {
|
|
331
332
|
const source = (ctx.models || []).find(candidate => candidate.id === modelId);
|
|
332
333
|
return {
|
|
333
|
-
|
|
334
|
+
// Same output budget as Roo: the gateway holds balance for all of it.
|
|
335
|
+
maxTokens: Math.min(positiveInteger(source?.max_output_tokens) ?? -1, CLINE_MAX_OUTPUT_TOKENS),
|
|
334
336
|
contextWindow: positiveInteger(source?.context_window) ?? 128_000,
|
|
335
337
|
supportsImages: source?.capabilities?.images === true || source?.capabilities?.vision === true,
|
|
336
338
|
supportsPromptCache: source?.capabilities?.prompt_cache === true,
|