scalemax 0.1.18 → 0.1.20
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 +13 -4
- package/dist/clients.js +3 -0
- package/dist/commands/configure.js +4 -1
- package/dist/configure/api-code.js +1 -1
- package/dist/configure/cherry-studio.js +2 -1
- package/dist/configure/codex.js +11 -6
- package/dist/configure/hermes.js +64 -32
- package/dist/configure/models.d.ts +14 -0
- package/dist/configure/models.js +52 -5
- package/dist/configure/openclaw.js +105 -45
- package/dist/configure/opencode.js +9 -7
- package/dist/configure/paths.js +2 -0
- package/dist/configure/roo-code.d.ts +4 -0
- package/dist/configure/roo-code.js +133 -0
- package/dist/configure/templates.js +1 -1
- package/dist/configure/trae-solo.js +1 -1
- package/dist/detect/codex.js +13 -1
- package/dist/detect/common.js +3 -3
- package/dist/detect/roo-code.d.ts +2 -0
- package/dist/detect/roo-code.js +36 -0
- package/dist/types/client.d.ts +1 -1
- package/dist/utils/api.d.ts +2 -0
- package/dist/utils/api.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ SCALEMAX_API_KEY=sm_live_xxx scalemax models
|
|
|
104
104
|
|
|
105
105
|
- Codex
|
|
106
106
|
- Cursor
|
|
107
|
+
- Roo Code (VS Code)
|
|
107
108
|
- TRAE SOLO
|
|
108
109
|
- Claude Code
|
|
109
110
|
- OpenCode
|
|
@@ -114,21 +115,29 @@ SCALEMAX_API_KEY=sm_live_xxx scalemax models
|
|
|
114
115
|
|
|
115
116
|
For clients without a stable official config file, the installer writes a ScaleMax setup guide under `~/.scalemax/` instead of modifying private app storage.
|
|
116
117
|
|
|
118
|
+
### Codex
|
|
119
|
+
|
|
120
|
+
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 Codex/OpenAI-family models are eligible, the generated files are private, and verification performs a minimal `/v1/responses` request before reporting success.
|
|
121
|
+
|
|
117
122
|
### Claude Code
|
|
118
123
|
|
|
119
124
|
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. The Anthropic base URL intentionally has no `/v1` suffix because Claude Code adds `/v1/messages` itself. The CLI preserves unrelated settings, selects a live main model from the authenticated catalog (preferring `claude-opus-4-8`), pins authenticated Haiku for lightweight background work when available, and sets `CLAUDE_CODE_DISABLE_THINKING=1` because third-party Anthropic-compatible providers do not support Claude Code's proprietary extended-thinking control envelope. 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-4-8[1m]` are normalized by the gateway.
|
|
120
125
|
|
|
121
126
|
### OpenCode
|
|
122
127
|
|
|
123
|
-
ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.json` (or the platform-equivalent config directory). It uses `https://api.scalemax.pro/v1`, reads its credential from the private `~/.config/opencode/scalemax.key` file through OpenCode's `{file:...}` substitution, keeps unrelated providers and the user's non-ScaleMax default model, and
|
|
128
|
+
ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.json` (or the platform-equivalent config directory). It uses `https://api.scalemax.pro/v1`, reads its credential from the private `~/.config/opencode/scalemax.key` file through OpenCode's `{file:...}` substitution, keeps unrelated providers and the user's non-ScaleMax default model, and exposes only authenticated Codex/OpenAI-family models. A fresh ScaleMax setup uses an authenticated GPT mini model as `small_model` when available. Each generated model has an explicit output limit of at most 4096 tokens so OpenCode does not send an unsupported default limit.
|
|
124
129
|
|
|
125
130
|
### OpenClaw
|
|
126
131
|
|
|
127
|
-
ScaleMax
|
|
132
|
+
ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`) and `anthropic-messages` (`scalemax-claude`, origin) providers. The API key and compatibility User-Agent are OpenClaw file-backed SecretRefs stored in private `0600` files under `~/.openclaw/secrets/`; neither value is embedded in the main provider JSON. Migration removes the obsolete mixed provider, preserves the active model family, and scrubs plaintext credentials from generated ScaleMax agent registries even when they contain an older key. Output is capped at 4096 tokens, private-network routing is denied for the public gateway, and unrelated providers and agent settings are preserved.
|
|
128
133
|
|
|
129
134
|
### Hermes
|
|
130
135
|
|
|
131
|
-
ScaleMax
|
|
136
|
+
ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and stores one credential in `~/.hermes/.env` with private permissions. `scalemax-codex` uses Hermes's `codex_responses` transport at `/v1`; `scalemax-claude` uses `anthropic_messages` at the gateway origin. Both use a scoped `ScaleMax-Hermes/1.0` user agent and a 4096-token output cap. Existing non-ScaleMax providers and defaults are preserved, and the obsolete mixed provider is migrated safely.
|
|
137
|
+
|
|
138
|
+
### Roo Code (VS Code)
|
|
139
|
+
|
|
140
|
+
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. Reload VS Code once so Roo imports the Codex/OpenAI-only profile into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
|
|
132
141
|
|
|
133
142
|
### Cursor BYOK routing on macOS
|
|
134
143
|
|
|
@@ -281,7 +290,7 @@ Never publish over an existing npm version. Use semantic versioning for every re
|
|
|
281
290
|
- Invalid JSON/JSON5 client files fail closed instead of being replaced; valid JSON5 values are merged safely.
|
|
282
291
|
- Backup directories are user-only and backup files are written with private permissions.
|
|
283
292
|
- API keys are masked in terminal output.
|
|
284
|
-
- Claude Code, OpenCode, OpenClaw, and
|
|
293
|
+
- Codex, Claude Code, OpenCode, OpenClaw, Hermes, and the Roo Code import handoff keep generated client credentials in private files rather than shared provider configuration blocks.
|
|
285
294
|
- The published npm package contains only runtime files: `dist/`, `README.md`, `LICENSE`, and `package.json`.
|
|
286
295
|
- Tests, TypeScript source, scripts, local caches, `.env` files, tarballs, and `node_modules` are excluded from the published package.
|
|
287
296
|
|
package/dist/clients.js
CHANGED
|
@@ -2,6 +2,8 @@ import * as codexDetect from './detect/codex.js';
|
|
|
2
2
|
import * as codexConfig from './configure/codex.js';
|
|
3
3
|
import * as cursorDetect from './detect/cursor.js';
|
|
4
4
|
import * as cursorConfig from './configure/cursor.js';
|
|
5
|
+
import * as rooCodeDetect from './detect/roo-code.js';
|
|
6
|
+
import * as rooCodeConfig from './configure/roo-code.js';
|
|
5
7
|
import * as traeSoloDetect from './detect/trae-solo.js';
|
|
6
8
|
import * as traeSoloConfig from './configure/trae-solo.js';
|
|
7
9
|
import * as claudeDetect from './detect/claude.js';
|
|
@@ -19,6 +21,7 @@ import * as apiCodeConfig from './configure/api-code.js';
|
|
|
19
21
|
export const clients = [
|
|
20
22
|
{ id: 'codex', name: 'Codex', detect: codexDetect.detect, install: codexConfig.install, configure: codexConfig.configure, verify: codexConfig.verify, launch: codexConfig.launch, remove: codexConfig.remove },
|
|
21
23
|
{ id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, verify: cursorConfig.verify, remove: cursorConfig.remove },
|
|
24
|
+
{ id: 'roo-code', name: 'Roo Code (VS Code)', detect: rooCodeDetect.detect, configure: rooCodeConfig.configure, verify: rooCodeConfig.verify, remove: rooCodeConfig.remove },
|
|
22
25
|
{ id: 'trae-solo', name: 'TRAE SOLO', detect: traeSoloDetect.detect, configure: traeSoloConfig.configure, remove: traeSoloConfig.remove },
|
|
23
26
|
{ id: 'claude', name: 'Claude Code', detect: claudeDetect.detect, configure: claudeConfig.configure, verify: claudeConfig.verify, remove: claudeConfig.remove },
|
|
24
27
|
{ id: 'opencode', name: 'OpenCode', detect: opencodeDetect.detect, configure: opencodeConfig.configure, verify: opencodeConfig.verify, remove: opencodeConfig.remove },
|
|
@@ -42,7 +42,10 @@ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL)
|
|
|
42
42
|
export function clientAllowedForModels(clientId, models) {
|
|
43
43
|
if (!models.length)
|
|
44
44
|
return false;
|
|
45
|
-
|
|
45
|
+
// These clients use either a native family-specific adapter or the gateway's
|
|
46
|
+
// normalized OpenAI-compatible Chat Completions route, which supports both
|
|
47
|
+
// authenticated model families. Codex and TRAE remain OpenAI-only.
|
|
48
|
+
const crossProtocolClients = new Set(['claude', 'cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
46
49
|
if (crossProtocolClients.has(clientId))
|
|
47
50
|
return true;
|
|
48
51
|
return models.some(model => !isAnthropicModel(model));
|
|
@@ -5,7 +5,7 @@ const id = 'api-code';
|
|
|
5
5
|
const name = 'API Code';
|
|
6
6
|
export async function configure(ctx) {
|
|
7
7
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
8
|
-
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, {
|
|
8
|
+
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { environment: { SCALEMAX_API_KEY: 'your ScaleMax API key', SCALEMAX_BASE_URL: ctx.baseUrl }, node: `import OpenAI from 'openai';\nconst client = new OpenAI({ apiKey: process.env.SCALEMAX_API_KEY, baseURL: '${ctx.baseUrl}' });`, python: `from openai import OpenAI\nclient = OpenAI(api_key='...', base_url='${ctx.baseUrl}')` });
|
|
9
9
|
const result = await writeManagedConfig(ctx, id, name, file, content);
|
|
10
10
|
return { ...result, message: 'API code examples written' };
|
|
11
11
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { clientConfigPath } from './paths.js';
|
|
2
2
|
import { writeManagedConfig, removeManagedConfig } from './common.js';
|
|
3
3
|
import { jsonConfig } from './templates.js';
|
|
4
|
+
import { preferredChatCompletionsModel } from './models.js';
|
|
4
5
|
const id = 'cherry-studio';
|
|
5
6
|
const name = 'Cherry Studio';
|
|
6
7
|
export async function configure(ctx) {
|
|
7
8
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
8
|
-
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { "officialSetup": ["Settings > Model Service / Model Providers", "Provider type: OpenAI or OpenAI Compatible",
|
|
9
|
+
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { "officialSetup": ["Settings > Model Service / Model Providers", "Provider type: OpenAI or OpenAI Compatible", `API host/base URL: ${ctx.baseUrl}`, `Add only model IDs returned by ${ctx.baseUrl}/models`], "recommendedModel": preferredChatCompletionsModel(ctx) });
|
|
9
10
|
const result = await writeManagedConfig(ctx, id, name, file, content);
|
|
10
11
|
return { ...result, message: 'setup guide written; enter these values in Cherry Studio model provider settings' };
|
|
11
12
|
}
|
package/dist/configure/codex.js
CHANGED
|
@@ -5,17 +5,16 @@ import { codexAuthPath, codexConfigPath } from './paths.js';
|
|
|
5
5
|
import { atomicWrite, backupFile, listBackups } from '../utils/fs.js';
|
|
6
6
|
import { GENERATED_HEADER } from '../constants.js';
|
|
7
7
|
import { commandExists } from '../utils/exec.js';
|
|
8
|
+
import { preferredOpenAiModel } from './models.js';
|
|
8
9
|
const id = 'codex';
|
|
9
10
|
const name = 'Codex';
|
|
10
11
|
const provider = 'scalemax';
|
|
11
12
|
const fallbackModel = 'gpt-5.5';
|
|
12
13
|
const reasoningEffort = 'medium';
|
|
13
14
|
function selectedModel(ctx) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return candidate;
|
|
18
|
-
return ctx.models?.[0]?.id || fallbackModel;
|
|
15
|
+
if (!ctx.models)
|
|
16
|
+
return fallbackModel;
|
|
17
|
+
return preferredOpenAiModel(ctx);
|
|
19
18
|
}
|
|
20
19
|
function scalemaxCodexBlock(baseUrl, model = fallbackModel) {
|
|
21
20
|
return `# ${GENERATED_HEADER}\nmodel = "${model}"\nmodel_provider = "${provider}"\nmodel_reasoning_effort = "${reasoningEffort}"\n# ScaleMax models are loaded from the authenticated live model catalog.\n\n[model_providers.${provider}]\nname = "ScaleMax"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
|
|
@@ -195,7 +194,7 @@ export async function verifyCodexConfiguration(ctx) {
|
|
|
195
194
|
const res = await fetch(`${ctx.baseUrl}/responses`, {
|
|
196
195
|
method: 'POST',
|
|
197
196
|
headers: { Authorization: `Bearer ${ctx.apiKey}`, 'Content-Type': 'application/json' },
|
|
198
|
-
body: JSON.stringify({ model, input: '
|
|
197
|
+
body: JSON.stringify({ model, input: 'Reply K', stream: false, max_output_tokens: 8 }),
|
|
199
198
|
signal: AbortSignal.timeout(15000)
|
|
200
199
|
});
|
|
201
200
|
if (res.status === 404)
|
|
@@ -204,6 +203,12 @@ export async function verifyCodexConfiguration(ctx) {
|
|
|
204
203
|
errors.push(`ScaleMax rejected the API key on /v1/responses with HTTP ${res.status}`);
|
|
205
204
|
else if (!res.ok)
|
|
206
205
|
warnings.push(`ScaleMax /v1/responses probe returned HTTP ${res.status}`);
|
|
206
|
+
else {
|
|
207
|
+
const body = await res.json().catch(() => undefined);
|
|
208
|
+
const hasOutput = typeof body?.output_text === 'string' || (Array.isArray(body?.output) && body.output.length > 0);
|
|
209
|
+
if (!body?.id || !hasOutput)
|
|
210
|
+
errors.push('ScaleMax /v1/responses returned a success status without a valid Responses payload.');
|
|
211
|
+
}
|
|
207
212
|
}
|
|
208
213
|
catch (err) {
|
|
209
214
|
warnings.push(`Could not probe ScaleMax /v1/responses: ${err instanceof Error ? err.message : String(err)}`);
|
package/dist/configure/hermes.js
CHANGED
|
@@ -4,11 +4,14 @@ import { parseDocument } from 'yaml';
|
|
|
4
4
|
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
5
5
|
import { clientConfigPath } from './paths.js';
|
|
6
6
|
import { isScaleMaxManagedText } from './common.js';
|
|
7
|
-
import {
|
|
7
|
+
import { anthropicModelIds, anthropicOrigin, openAiBaseUrl, openAiModelIds, preferredAnthropicModel, preferredOpenAiModel } from './models.js';
|
|
8
8
|
const id = 'hermes';
|
|
9
9
|
const name = 'Hermes';
|
|
10
10
|
const envKey = 'SCALEMAX_HERMES_API_KEY';
|
|
11
11
|
const envMarker = '# ScaleMax managed Hermes API key';
|
|
12
|
+
const legacyProviderName = 'scalemax';
|
|
13
|
+
const codexProviderName = 'scalemax-codex';
|
|
14
|
+
const claudeProviderName = 'scalemax-claude';
|
|
12
15
|
function asRecord(value) {
|
|
13
16
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
17
|
}
|
|
@@ -33,8 +36,8 @@ function parseYaml(text) {
|
|
|
33
36
|
doc.contents = doc.createNode({});
|
|
34
37
|
return doc;
|
|
35
38
|
}
|
|
36
|
-
function modelMetadata(ctx) {
|
|
37
|
-
return Object.fromEntries(
|
|
39
|
+
function modelMetadata(ctx, ids) {
|
|
40
|
+
return Object.fromEntries(ids.map(modelId => {
|
|
38
41
|
const model = (ctx.models || []).find(candidate => candidate.id === modelId);
|
|
39
42
|
const metadata = {};
|
|
40
43
|
if (model?.context_window)
|
|
@@ -61,21 +64,39 @@ export async function configure(ctx) {
|
|
|
61
64
|
const originalEnv = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
62
65
|
const doc = parseYaml(originalConfig);
|
|
63
66
|
const data = asRecord(doc.toJS());
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
const codexIds = openAiModelIds(ctx);
|
|
68
|
+
const claudeIds = anthropicModelIds(ctx);
|
|
69
|
+
const managedNames = new Set([legacyProviderName, codexProviderName, claudeProviderName]);
|
|
70
|
+
const providers = Array.isArray(data.custom_providers) ? data.custom_providers.filter((provider) => !managedNames.has(provider?.name)) : [];
|
|
71
|
+
if (codexIds.length)
|
|
72
|
+
providers.push({
|
|
73
|
+
name: codexProviderName,
|
|
74
|
+
base_url: openAiBaseUrl(ctx.baseUrl),
|
|
75
|
+
key_env: envKey,
|
|
76
|
+
api_mode: 'codex_responses',
|
|
77
|
+
extra_headers: { 'User-Agent': 'ScaleMax-Hermes/1.0' },
|
|
78
|
+
max_output_tokens: 4096,
|
|
79
|
+
models: modelMetadata(ctx, codexIds)
|
|
80
|
+
});
|
|
81
|
+
if (claudeIds.length)
|
|
82
|
+
providers.push({
|
|
83
|
+
name: claudeProviderName,
|
|
84
|
+
base_url: anthropicOrigin(ctx.baseUrl),
|
|
85
|
+
key_env: envKey,
|
|
86
|
+
api_mode: 'anthropic_messages',
|
|
87
|
+
extra_headers: { 'User-Agent': 'ScaleMax-Hermes/1.0' },
|
|
88
|
+
max_output_tokens: 4096,
|
|
89
|
+
models: modelMetadata(ctx, claudeIds)
|
|
90
|
+
});
|
|
74
91
|
doc.set('custom_providers', providers);
|
|
75
92
|
const currentModel = asRecord(data.model);
|
|
76
|
-
const
|
|
93
|
+
const currentProvider = String(currentModel.provider || '');
|
|
94
|
+
const activatesScaleMax = !currentProvider || currentProvider.startsWith('custom:scalemax');
|
|
77
95
|
if (activatesScaleMax) {
|
|
78
|
-
|
|
96
|
+
const keepClaude = currentProvider === `custom:${claudeProviderName}` || claudeIds.includes(String(currentModel.default || ''));
|
|
97
|
+
const activeName = codexIds.length && !keepClaude ? codexProviderName : claudeProviderName;
|
|
98
|
+
const activeModel = activeName === codexProviderName ? preferredOpenAiModel(ctx) : preferredAnthropicModel(ctx);
|
|
99
|
+
doc.set('model', { ...currentModel, provider: `custom:${activeName}`, default: activeModel, max_tokens: 4096 });
|
|
79
100
|
}
|
|
80
101
|
const nextConfig = String(doc);
|
|
81
102
|
const nextEnv = managedEnv(originalEnv, ctx.apiKey);
|
|
@@ -83,7 +104,7 @@ export async function configure(ctx) {
|
|
|
83
104
|
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
84
105
|
await fs.chmod(secrets, 0o600).catch(() => undefined);
|
|
85
106
|
await removeLegacyArtifact(ctx);
|
|
86
|
-
return { id, name, changed: false, path: file, message: `already current with ${
|
|
107
|
+
return { id, name, changed: false, path: file, message: `already current with ${codexIds.length} Codex/OpenAI and ${claudeIds.length} Claude models and a private credential` };
|
|
87
108
|
}
|
|
88
109
|
const backups = [await backupFile(file, ctx.home), await backupFile(secrets, ctx.home)].filter(Boolean);
|
|
89
110
|
try {
|
|
@@ -102,25 +123,33 @@ export async function configure(ctx) {
|
|
|
102
123
|
await fs.remove(secrets).catch(() => undefined);
|
|
103
124
|
throw error;
|
|
104
125
|
}
|
|
105
|
-
return { id, name, changed: true, path: file, message: `configured
|
|
126
|
+
return { id, name, changed: true, path: file, message: `configured family-specific ScaleMax providers with ${codexIds.length} Codex/OpenAI and ${claudeIds.length} Claude models${backups.length ? ' (backups created)' : ''}` };
|
|
106
127
|
}
|
|
107
128
|
export async function verify(ctx) {
|
|
108
129
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
109
130
|
const secrets = envPath(ctx.home);
|
|
110
131
|
const doc = parseYaml(await fs.readFile(file, 'utf8'));
|
|
111
132
|
const data = asRecord(doc.toJS());
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
const providers = Array.isArray(data.custom_providers) ? data.custom_providers : [];
|
|
134
|
+
const codexIds = openAiModelIds(ctx);
|
|
135
|
+
const claudeIds = anthropicModelIds(ctx);
|
|
136
|
+
const codex = providers.find((candidate) => candidate?.name === codexProviderName);
|
|
137
|
+
const claude = providers.find((candidate) => candidate?.name === claudeProviderName);
|
|
138
|
+
const valid = (provider, baseUrl, apiMode, ids) => provider
|
|
139
|
+
&& provider.base_url === baseUrl && provider.key_env === envKey && provider.api_mode === apiMode
|
|
140
|
+
&& provider.extra_headers?.['User-Agent'] === 'ScaleMax-Hermes/1.0'
|
|
141
|
+
&& Object.keys(asRecord(provider.models)).length === ids.length
|
|
142
|
+
&& ids.every(model => Object.hasOwn(asRecord(provider.models), model));
|
|
143
|
+
if ((codexIds.length ? !valid(codex, openAiBaseUrl(ctx.baseUrl), 'codex_responses', codexIds) : Boolean(codex))
|
|
144
|
+
|| (claudeIds.length ? !valid(claude, anthropicOrigin(ctx.baseUrl), 'anthropic_messages', claudeIds) : Boolean(claude))) {
|
|
145
|
+
throw new Error('Hermes ScaleMax family-specific custom provider settings are incomplete.');
|
|
115
146
|
}
|
|
116
|
-
const configured = Object.keys(asRecord(provider.models));
|
|
117
|
-
const missing = modelIds(ctx).filter(model => !configured.includes(model));
|
|
118
147
|
const env = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
119
|
-
if (
|
|
148
|
+
if (!env.includes(envMarker) || !env.includes(`${envKey}=${ctx.apiKey}`))
|
|
120
149
|
throw new Error('Hermes is missing authenticated ScaleMax models or credentials.');
|
|
121
|
-
if (data?.model?.provider
|
|
150
|
+
if (String(data?.model?.provider || '').startsWith('custom:scalemax') && data?.model?.max_tokens !== 4096)
|
|
122
151
|
throw new Error('Hermes active ScaleMax model must use max_tokens 4096.');
|
|
123
|
-
return { id, name, changed: false, path: file, message: `verified ${
|
|
152
|
+
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Codex/OpenAI and ${claudeIds.length} Claude models` };
|
|
124
153
|
}
|
|
125
154
|
export async function remove(ctx) {
|
|
126
155
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -131,20 +160,23 @@ export async function remove(ctx) {
|
|
|
131
160
|
const doc = parseYaml(text);
|
|
132
161
|
const data = asRecord(doc.toJS());
|
|
133
162
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers : [];
|
|
134
|
-
const
|
|
135
|
-
|
|
163
|
+
const managedNames = new Set([legacyProviderName, codexProviderName, claudeProviderName]);
|
|
164
|
+
const managedProviders = providers.filter((provider) => managedNames.has(provider?.name)
|
|
165
|
+
&& (provider?.base_url === openAiBaseUrl(ctx.baseUrl) || provider?.base_url === anthropicOrigin(ctx.baseUrl)));
|
|
166
|
+
if (!managedProviders.length)
|
|
136
167
|
return { id, name, changed: false, path: file, message: 'left unmanaged configuration unchanged' };
|
|
137
|
-
const managedModels = Object.keys(asRecord(
|
|
138
|
-
const remaining = providers.filter((provider) => provider
|
|
168
|
+
const managedModels = managedProviders.flatMap((provider) => Object.keys(asRecord(provider.models)));
|
|
169
|
+
const remaining = providers.filter((provider) => !managedProviders.includes(provider));
|
|
139
170
|
if (remaining.length)
|
|
140
171
|
doc.set('custom_providers', remaining);
|
|
141
172
|
else
|
|
142
173
|
doc.delete('custom_providers');
|
|
143
|
-
|
|
174
|
+
const activeManaged = String(data?.model?.provider || '').startsWith('custom:scalemax');
|
|
175
|
+
if (activeManaged)
|
|
144
176
|
doc.deleteIn(['model', 'provider']);
|
|
145
|
-
if (
|
|
177
|
+
if (activeManaged && managedModels.includes(String(data?.model?.default || '')))
|
|
146
178
|
doc.deleteIn(['model', 'default']);
|
|
147
|
-
if (
|
|
179
|
+
if (activeManaged && data?.model?.max_tokens === 4096)
|
|
148
180
|
doc.deleteIn(['model', 'max_tokens']);
|
|
149
181
|
const currentEnv = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
150
182
|
await backupFile(file, ctx.home);
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { ConfigureContext } from '../types/client.js';
|
|
2
2
|
export declare function modelIds(ctx: ConfigureContext): string[];
|
|
3
|
+
export declare function anthropicModelIds(ctx: ConfigureContext): string[];
|
|
4
|
+
export declare function openAiModelIds(ctx: ConfigureContext): string[];
|
|
5
|
+
export declare function preferredAnthropicModel(ctx: ConfigureContext): string;
|
|
6
|
+
export declare function preferredOpenAiModel(ctx: ConfigureContext): string;
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI-compatible chat clients can use the gateway's normalized chat route
|
|
9
|
+
* for either family. Keep an OpenAI model as the default when one is present
|
|
10
|
+
* so existing mixed-catalog configurations do not unexpectedly switch model
|
|
11
|
+
* families; a Claude-only catalog naturally selects its preferred Claude model.
|
|
12
|
+
*/
|
|
13
|
+
export declare function preferredChatCompletionsModel(ctx: ConfigureContext): string;
|
|
3
14
|
export declare function preferredModel(ctx: ConfigureContext): string;
|
|
4
15
|
export declare function fastModel(ctx: ConfigureContext): string | undefined;
|
|
16
|
+
export declare function fastOpenAiModel(ctx: ConfigureContext): string | undefined;
|
|
17
|
+
/** The lightweight model for an OpenAI-compatible chat client, if one exists. */
|
|
18
|
+
export declare function fastChatCompletionsModel(ctx: ConfigureContext): string | undefined;
|
|
5
19
|
export declare function modelName(ctx: ConfigureContext, id: string): string;
|
|
6
20
|
export declare function anthropicOrigin(baseUrl: string): string;
|
|
7
21
|
export declare function openAiBaseUrl(baseUrl: string): string;
|
package/dist/configure/models.js
CHANGED
|
@@ -1,18 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
import { isAnthropicModel, isRetiredModelId } from '../utils/api.js';
|
|
2
|
+
const preferredAnthropic = [
|
|
2
3
|
'claude-opus-4-8',
|
|
3
4
|
'claude-sonnet-5',
|
|
4
5
|
'claude-opus-4-7',
|
|
5
6
|
'claude-opus-4-6',
|
|
6
|
-
'claude-haiku-4-5-20251001'
|
|
7
|
+
'claude-haiku-4-5-20251001'
|
|
8
|
+
];
|
|
9
|
+
const preferredOpenAi = [
|
|
10
|
+
'gpt-5.6-terra',
|
|
11
|
+
'gpt-5.6-luna',
|
|
12
|
+
'gpt-5.5',
|
|
13
|
+
'gpt-5.4',
|
|
14
|
+
'gpt-5.4-mini',
|
|
15
|
+
'gpt-5.3-codex',
|
|
7
16
|
'fable-5',
|
|
8
|
-
'
|
|
17
|
+
'codex-auto-review'
|
|
9
18
|
];
|
|
10
19
|
export function modelIds(ctx) {
|
|
11
|
-
return [...new Set((ctx.models || []).map(model => model.id.trim()).filter(
|
|
20
|
+
return [...new Set((ctx.models || []).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
21
|
+
}
|
|
22
|
+
export function anthropicModelIds(ctx) {
|
|
23
|
+
return [...new Set((ctx.models || []).filter(isAnthropicModel).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
24
|
+
}
|
|
25
|
+
export function openAiModelIds(ctx) {
|
|
26
|
+
return [...new Set((ctx.models || []).filter(model => !isAnthropicModel(model)).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
27
|
+
}
|
|
28
|
+
function preferredFrom(ids, candidates, family) {
|
|
29
|
+
for (const candidate of candidates)
|
|
30
|
+
if (ids.includes(candidate))
|
|
31
|
+
return candidate;
|
|
32
|
+
if (ids.length)
|
|
33
|
+
return ids[0];
|
|
34
|
+
throw new Error(`No compatible ${family} models are available for this API key; the client was not changed.`);
|
|
35
|
+
}
|
|
36
|
+
export function preferredAnthropicModel(ctx) {
|
|
37
|
+
return preferredFrom(anthropicModelIds(ctx), preferredAnthropic, 'Claude');
|
|
38
|
+
}
|
|
39
|
+
export function preferredOpenAiModel(ctx) {
|
|
40
|
+
return preferredFrom(openAiModelIds(ctx), preferredOpenAi, 'Codex/OpenAI');
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* OpenAI-compatible chat clients can use the gateway's normalized chat route
|
|
44
|
+
* for either family. Keep an OpenAI model as the default when one is present
|
|
45
|
+
* so existing mixed-catalog configurations do not unexpectedly switch model
|
|
46
|
+
* families; a Claude-only catalog naturally selects its preferred Claude model.
|
|
47
|
+
*/
|
|
48
|
+
export function preferredChatCompletionsModel(ctx) {
|
|
49
|
+
return openAiModelIds(ctx).length ? preferredOpenAiModel(ctx) : preferredAnthropicModel(ctx);
|
|
12
50
|
}
|
|
13
51
|
export function preferredModel(ctx) {
|
|
14
52
|
const ids = modelIds(ctx);
|
|
15
|
-
for (const candidate of
|
|
53
|
+
for (const candidate of [...preferredAnthropic, ...preferredOpenAi])
|
|
16
54
|
if (ids.includes(candidate))
|
|
17
55
|
return candidate;
|
|
18
56
|
if (ids.length)
|
|
@@ -24,6 +62,15 @@ export function fastModel(ctx) {
|
|
|
24
62
|
return ids.find(id => id === 'claude-haiku-4-5-20251001')
|
|
25
63
|
|| ids.find(id => /(^|-)haiku([-.]|$)/i.test(id));
|
|
26
64
|
}
|
|
65
|
+
export function fastOpenAiModel(ctx) {
|
|
66
|
+
const ids = openAiModelIds(ctx);
|
|
67
|
+
return ids.find(id => id === 'gpt-5.4-mini')
|
|
68
|
+
|| ids.find(id => /(^|[-.])(mini|nano)([-.]|$)/i.test(id));
|
|
69
|
+
}
|
|
70
|
+
/** The lightweight model for an OpenAI-compatible chat client, if one exists. */
|
|
71
|
+
export function fastChatCompletionsModel(ctx) {
|
|
72
|
+
return fastOpenAiModel(ctx) || fastModel(ctx);
|
|
73
|
+
}
|
|
27
74
|
export function modelName(ctx, id) {
|
|
28
75
|
const model = (ctx.models || []).find(candidate => candidate.id === id);
|
|
29
76
|
if (model?.display_name)
|
|
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
4
4
|
import { clientConfigPath } from './paths.js';
|
|
5
5
|
import { isScaleMaxManagedText, writeMergedJsonConfig } from './common.js';
|
|
6
|
-
import { anthropicOrigin, fastModel,
|
|
6
|
+
import { anthropicModelIds, anthropicOrigin, fastModel, fastOpenAiModel, modelName, openAiBaseUrl, openAiModelIds, preferredAnthropicModel, preferredOpenAiModel } from './models.js';
|
|
7
7
|
import { removeClientSecret, rollbackClientSecret, verifyClientSecret, writeClientSecret } from './client-secret.js';
|
|
8
8
|
const id = 'openclaw';
|
|
9
9
|
const name = 'OpenClaw';
|
|
@@ -14,22 +14,33 @@ const userAgentProviderId = 'scalemax_cli_user_agent';
|
|
|
14
14
|
const userAgentConfigPath = '~/.openclaw/secrets/scalemax-user-agent';
|
|
15
15
|
const userAgentValue = 'ScaleMax-OpenClaw/1.0';
|
|
16
16
|
const userAgentReference = { source: 'file', provider: userAgentProviderId, id: 'value' };
|
|
17
|
+
const legacyProviderName = 'scalemax';
|
|
18
|
+
const codexProviderName = 'scalemax-codex';
|
|
19
|
+
const claudeProviderName = 'scalemax-claude';
|
|
17
20
|
function secretPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-api-key'); }
|
|
18
21
|
function userAgentPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-user-agent'); }
|
|
19
22
|
function userAgentContext(ctx) { return { ...ctx, apiKey: userAgentValue }; }
|
|
20
23
|
function asRecord(value) {
|
|
21
24
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
22
25
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
26
|
+
function isScaleMaxBaseUrl(value) {
|
|
27
|
+
try {
|
|
28
|
+
return new URL(String(value)).hostname === 'api.scalemax.pro';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function entries(ctx, ids, family) {
|
|
35
|
+
return ids.map(modelId => {
|
|
25
36
|
const metadata = (ctx.models || []).find(model => model.id === modelId);
|
|
26
37
|
const result = {
|
|
27
38
|
id: modelId,
|
|
28
39
|
name: modelName(ctx, modelId),
|
|
29
|
-
input:
|
|
40
|
+
input: family === 'anthropic' ? ['text', 'image'] : ['text'],
|
|
30
41
|
maxTokens: Math.min(metadata?.max_output_tokens || 4096, 4096)
|
|
31
42
|
};
|
|
32
|
-
if (metadata?.context_window ||
|
|
43
|
+
if (metadata?.context_window || family === 'anthropic')
|
|
33
44
|
result.contextWindow = metadata?.context_window || 200000;
|
|
34
45
|
return result;
|
|
35
46
|
});
|
|
@@ -65,10 +76,14 @@ async function scrubGeneratedPlaintextCredentials(ctx) {
|
|
|
65
76
|
const roots = [asRecord(data.providers), asRecord(asRecord(data.models).providers)];
|
|
66
77
|
let scrubbed = false;
|
|
67
78
|
for (const providers of roots) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
for (const [providerName, rawProvider] of Object.entries(providers)) {
|
|
80
|
+
const provider = asRecord(rawProvider);
|
|
81
|
+
const isScaleMax = [legacyProviderName, codexProviderName, claudeProviderName].includes(providerName)
|
|
82
|
+
&& isScaleMaxBaseUrl(provider.baseUrl);
|
|
83
|
+
if (isScaleMax && typeof provider.apiKey === 'string') {
|
|
84
|
+
delete provider.apiKey;
|
|
85
|
+
scrubbed = true;
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
if (!scrubbed)
|
|
@@ -81,8 +96,12 @@ async function scrubGeneratedPlaintextCredentials(ctx) {
|
|
|
81
96
|
}
|
|
82
97
|
export async function configure(ctx) {
|
|
83
98
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
84
|
-
const
|
|
85
|
-
const
|
|
99
|
+
const codexIds = openAiModelIds(ctx);
|
|
100
|
+
const claudeIds = anthropicModelIds(ctx);
|
|
101
|
+
const codexPrimary = codexIds.length ? `${codexProviderName}/${preferredOpenAiModel(ctx)}` : undefined;
|
|
102
|
+
const claudePrimary = claudeIds.length ? `${claudeProviderName}/${preferredAnthropicModel(ctx)}` : undefined;
|
|
103
|
+
const codexQuick = fastOpenAiModel(ctx);
|
|
104
|
+
const claudeQuick = fastModel(ctx);
|
|
86
105
|
let secret = { changed: false };
|
|
87
106
|
let userAgentSecret = { changed: false };
|
|
88
107
|
let result;
|
|
@@ -97,16 +116,52 @@ export async function configure(ctx) {
|
|
|
97
116
|
const agents = asRecord(existing.agents);
|
|
98
117
|
const defaults = asRecord(agents.defaults);
|
|
99
118
|
const allowlist = asRecord(defaults.models);
|
|
100
|
-
for (const
|
|
101
|
-
|
|
119
|
+
for (const key of Object.keys(allowlist)) {
|
|
120
|
+
if (key.startsWith(`${legacyProviderName}/`) || key.startsWith(`${codexProviderName}/`) || key.startsWith(`${claudeProviderName}/`))
|
|
121
|
+
delete allowlist[key];
|
|
122
|
+
}
|
|
123
|
+
for (const modelId of codexIds)
|
|
124
|
+
allowlist[`${codexProviderName}/${modelId}`] = {};
|
|
125
|
+
for (const modelId of claudeIds)
|
|
126
|
+
allowlist[`${claudeProviderName}/${modelId}`] = {};
|
|
102
127
|
const currentModel = defaults.model;
|
|
103
128
|
const currentPrimary = typeof currentModel === 'string' ? currentModel : asRecord(currentModel).primary;
|
|
104
|
-
const activatesScaleMax = !currentPrimary || String(currentPrimary).startsWith('scalemax
|
|
129
|
+
const activatesScaleMax = !currentPrimary || String(currentPrimary).startsWith('scalemax');
|
|
130
|
+
const currentId = String(currentPrimary || '').split('/').slice(1).join('/');
|
|
131
|
+
const keepClaude = String(currentPrimary || '').startsWith(`${claudeProviderName}/`) || claudeIds.includes(currentId);
|
|
132
|
+
const primary = keepClaude ? claudePrimary || codexPrimary : codexPrimary || claudePrimary;
|
|
133
|
+
const usesClaude = Boolean(primary?.startsWith(`${claudeProviderName}/`));
|
|
105
134
|
const nextModel = activatesScaleMax
|
|
106
|
-
? { ...asRecord(currentModel), primary }
|
|
135
|
+
? { ...asRecord(currentModel), ...(primary ? { primary } : {}) }
|
|
107
136
|
: currentModel;
|
|
108
137
|
const currentUtility = typeof defaults.utilityModel === 'string' ? defaults.utilityModel : undefined;
|
|
109
|
-
const
|
|
138
|
+
const quick = usesClaude ? claudeQuick : codexQuick;
|
|
139
|
+
const utilityProvider = usesClaude ? claudeProviderName : codexProviderName;
|
|
140
|
+
const utilityModel = quick && (activatesScaleMax || currentUtility?.startsWith('scalemax')) ? `${utilityProvider}/${quick}` : currentUtility;
|
|
141
|
+
const nextProviders = { ...providers };
|
|
142
|
+
delete nextProviders[legacyProviderName];
|
|
143
|
+
if (codexIds.length)
|
|
144
|
+
nextProviders[codexProviderName] = {
|
|
145
|
+
baseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
146
|
+
apiKey: secretReference,
|
|
147
|
+
api: 'openai-responses',
|
|
148
|
+
maxTokens: 4096,
|
|
149
|
+
request: { headers: { 'User-Agent': userAgentReference }, allowPrivateNetwork: false },
|
|
150
|
+
models: entries(ctx, codexIds, 'openai')
|
|
151
|
+
};
|
|
152
|
+
else
|
|
153
|
+
delete nextProviders[codexProviderName];
|
|
154
|
+
if (claudeIds.length)
|
|
155
|
+
nextProviders[claudeProviderName] = {
|
|
156
|
+
baseUrl: anthropicOrigin(ctx.baseUrl),
|
|
157
|
+
apiKey: secretReference,
|
|
158
|
+
api: 'anthropic-messages',
|
|
159
|
+
maxTokens: 4096,
|
|
160
|
+
request: { headers: { 'User-Agent': userAgentReference }, allowPrivateNetwork: false },
|
|
161
|
+
models: entries(ctx, claudeIds, 'anthropic')
|
|
162
|
+
};
|
|
163
|
+
else
|
|
164
|
+
delete nextProviders[claudeProviderName];
|
|
110
165
|
return JSON.stringify({
|
|
111
166
|
...existing,
|
|
112
167
|
secrets: {
|
|
@@ -120,20 +175,7 @@ export async function configure(ctx) {
|
|
|
120
175
|
models: {
|
|
121
176
|
...modelsRoot,
|
|
122
177
|
mode: modelsRoot.mode || 'merge',
|
|
123
|
-
providers:
|
|
124
|
-
...providers,
|
|
125
|
-
scalemax: {
|
|
126
|
-
baseUrl: anthropicOrigin(ctx.baseUrl),
|
|
127
|
-
apiKey: secretReference,
|
|
128
|
-
api: 'anthropic-messages',
|
|
129
|
-
maxTokens: 4096,
|
|
130
|
-
request: {
|
|
131
|
-
headers: { 'User-Agent': userAgentReference },
|
|
132
|
-
allowPrivateNetwork: false
|
|
133
|
-
},
|
|
134
|
-
models: entries(ctx)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
178
|
+
providers: nextProviders
|
|
137
179
|
},
|
|
138
180
|
agents: {
|
|
139
181
|
...agents,
|
|
@@ -149,25 +191,37 @@ export async function configure(ctx) {
|
|
|
149
191
|
}
|
|
150
192
|
await removeLegacyArtifact(ctx);
|
|
151
193
|
const scrubbed = await scrubGeneratedPlaintextCredentials(ctx);
|
|
152
|
-
return { ...result, changed: result.changed || secret.changed || userAgentSecret.changed || scrubbed > 0, message: `configured native
|
|
194
|
+
return { ...result, changed: result.changed || secret.changed || userAgentSecret.changed || scrubbed > 0, message: `configured native Responses (${codexIds.length}) and Anthropic Messages (${claudeIds.length}) providers with file-backed SecretRefs${scrubbed ? `; scrubbed ${scrubbed} generated credential ${scrubbed === 1 ? 'copy' : 'copies'}` : ''}` };
|
|
153
195
|
}
|
|
154
196
|
export async function verify(ctx) {
|
|
155
197
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
156
198
|
const config = await fs.readJson(file);
|
|
157
|
-
const
|
|
199
|
+
const codex = config?.models?.providers?.[codexProviderName];
|
|
200
|
+
const claude = config?.models?.providers?.[claudeProviderName];
|
|
158
201
|
const secretProvider = config?.secrets?.providers?.[secretProviderId];
|
|
159
202
|
const userAgentProvider = config?.secrets?.providers?.[userAgentProviderId];
|
|
160
|
-
const
|
|
161
|
-
|
|
203
|
+
const codexIds = openAiModelIds(ctx);
|
|
204
|
+
const claudeIds = anthropicModelIds(ctx);
|
|
205
|
+
const valid = (provider, baseUrl, api, ids) => {
|
|
206
|
+
const userAgent = provider?.request?.headers?.['User-Agent'];
|
|
207
|
+
const configured = new Set((Array.isArray(provider?.models) ? provider.models : []).map((model) => model?.id));
|
|
208
|
+
return provider?.baseUrl === baseUrl && provider?.apiKey?.source === secretReference.source
|
|
209
|
+
&& provider?.apiKey?.provider === secretReference.provider && provider?.apiKey?.id === secretReference.id
|
|
210
|
+
&& provider?.api === api && provider?.request?.allowPrivateNetwork === false
|
|
211
|
+
&& userAgent?.source === userAgentReference.source && userAgent?.provider === userAgentReference.provider
|
|
212
|
+
&& userAgent?.id === userAgentReference.id && configured.size === ids.length && ids.every(model => configured.has(model));
|
|
213
|
+
};
|
|
214
|
+
if ((codexIds.length ? !valid(codex, openAiBaseUrl(ctx.baseUrl), 'openai-responses', codexIds) : Boolean(codex))
|
|
215
|
+
|| (claudeIds.length ? !valid(claude, anthropicOrigin(ctx.baseUrl), 'anthropic-messages', claudeIds) : Boolean(claude))
|
|
216
|
+
|| secretProvider?.source !== 'file' || secretProvider?.path !== secretConfigPath || secretProvider?.mode !== 'singleValue'
|
|
217
|
+
|| userAgentProvider?.source !== 'file' || userAgentProvider?.path !== userAgentConfigPath || userAgentProvider?.mode !== 'singleValue') {
|
|
162
218
|
throw new Error('OpenClaw ScaleMax provider settings are incomplete.');
|
|
163
219
|
}
|
|
164
220
|
await verifyClientSecret(secretPath(ctx.home), ctx);
|
|
165
221
|
await verifyClientSecret(userAgentPath(ctx.home), userAgentContext(ctx));
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
throw new Error(`OpenClaw is missing ${missing.length} authenticated model(s).`);
|
|
170
|
-
return { id, name, changed: false, path: file, message: `verified ${configured.size} ScaleMax models` };
|
|
222
|
+
if (config?.models?.providers?.[legacyProviderName])
|
|
223
|
+
throw new Error('OpenClaw still contains the obsolete mixed ScaleMax provider.');
|
|
224
|
+
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Codex/OpenAI and ${claudeIds.length} Claude models` };
|
|
171
225
|
}
|
|
172
226
|
export async function remove(ctx) {
|
|
173
227
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -177,10 +231,16 @@ export async function remove(ctx) {
|
|
|
177
231
|
return { id, name, changed: removed || removedUserAgent, path: file, message: removed || removedUserAgent ? 'removed orphaned ScaleMax OpenClaw credentials' : 'no ScaleMax configuration found' };
|
|
178
232
|
}
|
|
179
233
|
const config = await fs.readJson(file).catch(() => undefined);
|
|
180
|
-
|
|
234
|
+
const providerRoot = asRecord(config?.models?.providers);
|
|
235
|
+
const managedNames = [legacyProviderName, codexProviderName, claudeProviderName].filter(providerName => {
|
|
236
|
+
const provider = asRecord(providerRoot[providerName]);
|
|
237
|
+
return [anthropicOrigin(ctx.baseUrl), openAiBaseUrl(ctx.baseUrl)].includes(String(provider.baseUrl || ''));
|
|
238
|
+
});
|
|
239
|
+
if (!managedNames.length) {
|
|
181
240
|
return { id, name, changed: false, path: file, message: 'left unmanaged configuration unchanged' };
|
|
182
241
|
}
|
|
183
|
-
|
|
242
|
+
for (const providerName of managedNames)
|
|
243
|
+
delete config.models.providers[providerName];
|
|
184
244
|
if (!Object.keys(config.models.providers).length)
|
|
185
245
|
delete config.models.providers;
|
|
186
246
|
if (Object.keys(config.models).length === 1 && config.models.mode === 'merge')
|
|
@@ -188,19 +248,19 @@ export async function remove(ctx) {
|
|
|
188
248
|
const defaults = config?.agents?.defaults;
|
|
189
249
|
if (defaults?.models && typeof defaults.models === 'object') {
|
|
190
250
|
for (const key of Object.keys(defaults.models))
|
|
191
|
-
if (key.startsWith('scalemax
|
|
251
|
+
if (key.startsWith('scalemax'))
|
|
192
252
|
delete defaults.models[key];
|
|
193
253
|
if (!Object.keys(defaults.models).length)
|
|
194
254
|
delete defaults.models;
|
|
195
255
|
}
|
|
196
|
-
if (typeof defaults?.model === 'string' && defaults.model.startsWith('scalemax
|
|
256
|
+
if (typeof defaults?.model === 'string' && defaults.model.startsWith('scalemax'))
|
|
197
257
|
delete defaults.model;
|
|
198
|
-
if (defaults?.model?.primary?.startsWith?.('scalemax
|
|
258
|
+
if (defaults?.model?.primary?.startsWith?.('scalemax')) {
|
|
199
259
|
delete defaults.model.primary;
|
|
200
260
|
if (!Object.keys(defaults.model).length)
|
|
201
261
|
delete defaults.model;
|
|
202
262
|
}
|
|
203
|
-
if (typeof defaults?.utilityModel === 'string' && defaults.utilityModel.startsWith('scalemax
|
|
263
|
+
if (typeof defaults?.utilityModel === 'string' && defaults.utilityModel.startsWith('scalemax'))
|
|
204
264
|
delete defaults.utilityModel;
|
|
205
265
|
if (defaults && !Object.keys(defaults).length)
|
|
206
266
|
delete config.agents.defaults;
|
|
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
4
4
|
import { clientConfigPath } from './paths.js';
|
|
5
5
|
import { writeMergedJsonConfig } from './common.js';
|
|
6
|
-
import {
|
|
6
|
+
import { fastChatCompletionsModel, modelIds, modelName, openAiBaseUrl, preferredChatCompletionsModel } from './models.js';
|
|
7
7
|
import { removeClientSecret, rollbackClientSecret, verifyClientSecret, writeClientSecret } from './client-secret.js';
|
|
8
8
|
const id = 'opencode';
|
|
9
9
|
const name = 'OpenCode';
|
|
@@ -32,8 +32,8 @@ function providerConfig(ctx) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function configure(ctx) {
|
|
34
34
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
35
|
-
const selected = `scalemax/${
|
|
36
|
-
const quick =
|
|
35
|
+
const selected = `scalemax/${preferredChatCompletionsModel(ctx)}`;
|
|
36
|
+
const quick = fastChatCompletionsModel(ctx);
|
|
37
37
|
const secret = await writeClientSecret(secretPath(ctx.home), ctx);
|
|
38
38
|
let result;
|
|
39
39
|
try {
|
|
@@ -55,7 +55,7 @@ export async function configure(ctx) {
|
|
|
55
55
|
await rollbackClientSecret(secretPath(ctx.home), ctx, secret);
|
|
56
56
|
throw error;
|
|
57
57
|
}
|
|
58
|
-
return { ...result, changed: result.changed || secret.changed, message: `${result.message}; file-backed credential${quick ? `; ${quick} handles lightweight work` : ''}` };
|
|
58
|
+
return { ...result, changed: result.changed || secret.changed, message: `${result.message}; file-backed credential and ${modelIds(ctx).length} authenticated chat model(s)${quick ? `; ${quick} handles lightweight work` : ''}` };
|
|
59
59
|
}
|
|
60
60
|
export async function verify(ctx) {
|
|
61
61
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -66,9 +66,11 @@ export async function verify(ctx) {
|
|
|
66
66
|
}
|
|
67
67
|
await verifyClientSecret(secretPath(ctx.home), ctx);
|
|
68
68
|
const configured = Object.keys(asRecord(provider.models));
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const expected = modelIds(ctx);
|
|
70
|
+
const missing = expected.filter(model => !configured.includes(model));
|
|
71
|
+
const unexpected = configured.filter(model => !expected.includes(model));
|
|
72
|
+
if (missing.length || unexpected.length)
|
|
73
|
+
throw new Error(`OpenCode model catalog is not restricted to the ${expected.length} authenticated chat model(s).`);
|
|
72
74
|
return { id, name, changed: false, path: file, message: `verified ${configured.length} ScaleMax models` };
|
|
73
75
|
}
|
|
74
76
|
export async function remove(ctx) {
|
package/dist/configure/paths.js
CHANGED
|
@@ -9,6 +9,7 @@ export function clientConfigPath(id, home, platform, env = process.env) {
|
|
|
9
9
|
switch (id) {
|
|
10
10
|
case 'codex': return codexConfigPath(home);
|
|
11
11
|
case 'cursor': return platform === 'win32' ? path.join(app, 'Cursor', 'User', 'settings.json') : platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json') : path.join(linux, 'Cursor', 'User', 'settings.json');
|
|
12
|
+
case 'roo-code': return platform === 'win32' ? path.join(app, 'Code', 'User', 'settings.json') : platform === 'darwin' ? path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json') : path.join(linux, 'Code', 'User', 'settings.json');
|
|
12
13
|
case 'trae-solo': return path.join(home, '.scalemax', 'trae-solo.setup.json');
|
|
13
14
|
case 'claude': return path.join(home, '.claude', 'settings.json');
|
|
14
15
|
case 'opencode': return path.join(linux, 'opencode', 'opencode.json');
|
|
@@ -24,6 +25,7 @@ export function installHints(id, home, platform, env = process.env) {
|
|
|
24
25
|
switch (id) {
|
|
25
26
|
case 'codex': return [codexDir(home), path.join(linux, 'codex')];
|
|
26
27
|
case 'cursor': return [platform === 'darwin' ? '/Applications/Cursor.app' : path.join(app, 'Cursor'), path.join(linux, 'Cursor')];
|
|
28
|
+
case 'roo-code': return [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
|
|
27
29
|
case 'trae-solo': return [platform === 'darwin' ? '/Applications/Trae.app' : path.join(app, 'Trae'), path.join(linux, 'Trae')];
|
|
28
30
|
case 'claude': return [path.join(home, '.claude')];
|
|
29
31
|
case 'opencode': return [path.join(linux, 'opencode')];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ConfigureContext, ConfigureResult } from '../types/client.js';
|
|
2
|
+
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
3
|
+
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
4
|
+
export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
5
|
+
import { clientConfigPath } from './paths.js';
|
|
6
|
+
import { openAiBaseUrl, preferredChatCompletionsModel } from './models.js';
|
|
7
|
+
import { isAnthropicModel } from '../utils/api.js';
|
|
8
|
+
const id = 'roo-code';
|
|
9
|
+
const name = 'Roo Code (VS Code)';
|
|
10
|
+
const importSetting = 'roo-cline.autoImportSettingsPath';
|
|
11
|
+
const importSettingValue = '~/.scalemax/roo-code-import.json';
|
|
12
|
+
const codexProfileName = 'ScaleMax Codex';
|
|
13
|
+
const claudeProfileName = 'ScaleMax Claude';
|
|
14
|
+
const codexProfileId = 'scalemax-codex';
|
|
15
|
+
const claudeProfileId = 'scalemax-claude';
|
|
16
|
+
function importPath(home) { return path.join(home, '.scalemax', 'roo-code-import.json'); }
|
|
17
|
+
function profileFor(ctx) {
|
|
18
|
+
const model = preferredChatCompletionsModel(ctx);
|
|
19
|
+
const metadata = (ctx.models || []).find(candidate => candidate.id === model) || { id: model };
|
|
20
|
+
return isAnthropicModel(metadata)
|
|
21
|
+
? { name: claudeProfileName, id: claudeProfileId, model }
|
|
22
|
+
: { name: codexProfileName, id: codexProfileId, model };
|
|
23
|
+
}
|
|
24
|
+
function parseSettings(text, file) {
|
|
25
|
+
if (!text)
|
|
26
|
+
return {};
|
|
27
|
+
try {
|
|
28
|
+
const value = JSON5.parse(text);
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
throw new Error('root value must be an object');
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`Roo Code cannot update invalid VS Code settings at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function handoff(ctx) {
|
|
38
|
+
const profile = profileFor(ctx);
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
providerProfiles: {
|
|
41
|
+
currentApiConfigName: profile.name,
|
|
42
|
+
apiConfigs: {
|
|
43
|
+
[profile.name]: {
|
|
44
|
+
id: profile.id,
|
|
45
|
+
apiProvider: 'openai',
|
|
46
|
+
openAiBaseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
47
|
+
openAiApiKey: ctx.apiKey,
|
|
48
|
+
openAiModelId: profile.model,
|
|
49
|
+
openAiStreamingEnabled: true,
|
|
50
|
+
includeMaxTokens: true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
modeApiConfigs: {}
|
|
54
|
+
},
|
|
55
|
+
globalSettings: {}
|
|
56
|
+
}, null, 2) + '\n';
|
|
57
|
+
}
|
|
58
|
+
export async function configure(ctx) {
|
|
59
|
+
const settingsFile = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
60
|
+
const profileFile = importPath(ctx.home);
|
|
61
|
+
const originalSettings = await fs.readFile(settingsFile, 'utf8').catch(() => '');
|
|
62
|
+
const originalProfile = await fs.readFile(profileFile, 'utf8').catch(() => '');
|
|
63
|
+
const settings = parseSettings(originalSettings, settingsFile);
|
|
64
|
+
const nextSettings = JSON.stringify({ ...settings, [importSetting]: importSettingValue }, null, 2) + '\n';
|
|
65
|
+
const nextProfile = handoff(ctx);
|
|
66
|
+
if (originalSettings === nextSettings && originalProfile === nextProfile) {
|
|
67
|
+
await fs.chmod(profileFile, 0o600).catch(() => undefined);
|
|
68
|
+
return { id, name, changed: false, path: profileFile, message: 'supported Roo Code auto-import handoff is already current; reload VS Code to consume it' };
|
|
69
|
+
}
|
|
70
|
+
const originals = new Map([[settingsFile, originalSettings], [profileFile, originalProfile]]);
|
|
71
|
+
await backupFile(settingsFile, ctx.home);
|
|
72
|
+
await backupFile(profileFile, ctx.home);
|
|
73
|
+
try {
|
|
74
|
+
await atomicWrite(profileFile, nextProfile, 0o600, ctx.platform);
|
|
75
|
+
await atomicWrite(settingsFile, nextSettings, 0o600, ctx.platform);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
for (const [file, original] of originals) {
|
|
79
|
+
if (original)
|
|
80
|
+
await atomicWrite(file, original, 0o600, ctx.platform);
|
|
81
|
+
else
|
|
82
|
+
await fs.remove(file).catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return { id, name, changed: true, path: profileFile, message: 'created a private, supported auto-import handoff; reload VS Code so Roo Code imports the profile into SecretStorage' };
|
|
87
|
+
}
|
|
88
|
+
export async function verify(ctx) {
|
|
89
|
+
const settingsFile = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
90
|
+
const profileFile = importPath(ctx.home);
|
|
91
|
+
const settings = parseSettings(await fs.readFile(settingsFile, 'utf8'), settingsFile);
|
|
92
|
+
const profile = await fs.readJson(profileFile).catch(() => undefined);
|
|
93
|
+
const expected = profileFor(ctx);
|
|
94
|
+
const config = profile?.providerProfiles?.apiConfigs?.[expected.name];
|
|
95
|
+
if (settings[importSetting] !== importSettingValue || profile?.providerProfiles?.currentApiConfigName !== expected.name
|
|
96
|
+
|| config?.id !== expected.id || config?.apiProvider !== 'openai' || config?.openAiBaseUrl !== openAiBaseUrl(ctx.baseUrl)
|
|
97
|
+
|| config?.openAiApiKey !== ctx.apiKey || config?.openAiModelId !== expected.model || config?.openAiStreamingEnabled !== true
|
|
98
|
+
|| config?.includeMaxTokens !== true) {
|
|
99
|
+
throw new Error('Roo Code auto-import handoff is incomplete.');
|
|
100
|
+
}
|
|
101
|
+
if (ctx.platform !== 'win32' && ((await fs.stat(profileFile)).mode & 0o077) !== 0)
|
|
102
|
+
throw new Error('Roo Code import handoff permissions are too broad.');
|
|
103
|
+
return { id, name, changed: false, path: profileFile, message: 'verified supported auto-import handoff (runtime activation requires a VS Code reload)' };
|
|
104
|
+
}
|
|
105
|
+
export async function remove(ctx) {
|
|
106
|
+
const settingsFile = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
107
|
+
const profileFile = importPath(ctx.home);
|
|
108
|
+
let changed = false;
|
|
109
|
+
if (await fs.pathExists(settingsFile)) {
|
|
110
|
+
const text = await fs.readFile(settingsFile, 'utf8');
|
|
111
|
+
const settings = parseSettings(text, settingsFile);
|
|
112
|
+
if (settings[importSetting] === importSettingValue) {
|
|
113
|
+
delete settings[importSetting];
|
|
114
|
+
await backupFile(settingsFile, ctx.home);
|
|
115
|
+
await atomicWrite(settingsFile, JSON.stringify(settings, null, 2) + '\n', 0o600, ctx.platform);
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (await fs.pathExists(profileFile)) {
|
|
120
|
+
const profile = await fs.readJson(profileFile).catch(() => undefined);
|
|
121
|
+
const configs = profile?.providerProfiles?.apiConfigs;
|
|
122
|
+
const managed = [
|
|
123
|
+
configs?.[codexProfileName]?.id === codexProfileId,
|
|
124
|
+
configs?.[claudeProfileName]?.id === claudeProfileId
|
|
125
|
+
].some(Boolean);
|
|
126
|
+
if (managed) {
|
|
127
|
+
await backupFile(profileFile, ctx.home);
|
|
128
|
+
await fs.remove(profileFile);
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { id, name, changed, path: profileFile, message: changed ? 'removed only the ScaleMax Roo Code auto-import handoff' : 'no ScaleMax Roo Code handoff found' };
|
|
133
|
+
}
|
|
@@ -11,7 +11,7 @@ export function jsonConfig(client, apiKey, baseUrl, extra = {}) {
|
|
|
11
11
|
}, null, 2) + '\n';
|
|
12
12
|
}
|
|
13
13
|
export function continueConfig(apiKey, baseUrl) {
|
|
14
|
-
const models = ['fable-5', '
|
|
14
|
+
const models = ['fable-5', 'gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'codex-auto-review'];
|
|
15
15
|
return JSON.stringify({
|
|
16
16
|
models: models.map(model => ({
|
|
17
17
|
title: `ScaleMax ${model}`,
|
|
@@ -5,7 +5,7 @@ const id = 'trae-solo';
|
|
|
5
5
|
const name = 'TRAE SOLO';
|
|
6
6
|
export async function configure(ctx) {
|
|
7
7
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
8
|
-
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { "officialSetup": ["Settings > Models > Add Model", "API format: OpenAI Chat Completions",
|
|
8
|
+
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { "officialSetup": ["Settings > Models > Add Model", "API format: OpenAI Chat Completions", `Request URL: ${ctx.baseUrl}/chat/completions or base URL ${ctx.baseUrl}`, "Use only a model ID returned by the authenticated catalog"], "recommendedModel": "fable-5" });
|
|
9
9
|
const result = await writeManagedConfig(ctx, id, name, file, content);
|
|
10
10
|
return { ...result, message: 'setup guide written; enter these values in TRAE SOLO custom model settings' };
|
|
11
11
|
}
|
package/dist/detect/codex.js
CHANGED
|
@@ -15,5 +15,17 @@ export async function detect(ctx) {
|
|
|
15
15
|
paths.push(auth);
|
|
16
16
|
if (!paths.length && await fs.pathExists(codexDir))
|
|
17
17
|
paths.push(codexDir);
|
|
18
|
-
|
|
18
|
+
let configured = false;
|
|
19
|
+
if (await fs.pathExists(config) && await fs.pathExists(auth)) {
|
|
20
|
+
const configText = await fs.readFile(config, 'utf8').catch(() => '');
|
|
21
|
+
const authJson = await fs.readJson(auth).catch(() => ({}));
|
|
22
|
+
configured = /^\s*model_provider\s*=\s*["']scalemax["']/m.test(configText)
|
|
23
|
+
&& /\[model_providers\.scalemax\]/.test(configText)
|
|
24
|
+
&& /^\s*wire_api\s*=\s*["']responses["']/m.test(configText)
|
|
25
|
+
&& /^\s*base_url\s*=\s*["']https:\/\/api\.scalemax\.pro\/(?:token\/)?v1\/?["']/m.test(configText)
|
|
26
|
+
&& authJson.auth_mode === 'apikey'
|
|
27
|
+
&& typeof authJson.OPENAI_API_KEY === 'string'
|
|
28
|
+
&& /^sm_(?:live|dev|trial)_/.test(authJson.OPENAI_API_KEY);
|
|
29
|
+
}
|
|
30
|
+
return { id: 'codex', name: 'Codex', installed: executable, reason: executable ? undefined : (paths.length ? 'configuration exists but codex executable is missing' : 'not detected'), paths, executable, configured, installable: true };
|
|
19
31
|
}
|
package/dist/detect/common.js
CHANGED
|
@@ -3,7 +3,7 @@ import { commandExists } from '../utils/exec.js';
|
|
|
3
3
|
import { clientConfigPath, installHints } from '../configure/paths.js';
|
|
4
4
|
import { GENERATED_HEADER } from '../constants.js';
|
|
5
5
|
const commands = {
|
|
6
|
-
codex: ['codex'], cursor: ['cursor'], 'trae-solo': ['trae', 'trae-solo'], claude: ['claude'], opencode: ['opencode'], openclaw: ['openclaw'], hermes: ['hermes'], 'cherry-studio': ['cherry-studio', 'cherrystudio'], 'api-code': []
|
|
6
|
+
codex: ['codex'], cursor: ['cursor'], 'roo-code': ['code'], 'trae-solo': ['trae', 'trae-solo'], claude: ['claude'], opencode: ['opencode'], openclaw: ['openclaw'], hermes: ['hermes'], 'cherry-studio': ['cherry-studio', 'cherrystudio'], 'api-code': []
|
|
7
7
|
};
|
|
8
8
|
const guiPathDetection = new Set(['trae-solo', 'cherry-studio']);
|
|
9
9
|
function hasNativeScaleMaxConfig(id, text) {
|
|
@@ -12,13 +12,13 @@ function hasNativeScaleMaxConfig(id, text) {
|
|
|
12
12
|
if (!text.includes('api.scalemax.pro'))
|
|
13
13
|
return false;
|
|
14
14
|
if (id === 'hermes')
|
|
15
|
-
return /(?:^|\n)\s*-?\s*name:\s*["']?scalemax["']?\s*(?:#.*)?(?:\n|$)/i.test(text);
|
|
15
|
+
return /(?:^|\n)\s*-?\s*name:\s*["']?scalemax(?:-(?:codex|claude))?["']?\s*(?:#.*)?(?:\n|$)/i.test(text);
|
|
16
16
|
if (id === 'opencode' || id === 'openclaw') {
|
|
17
17
|
try {
|
|
18
18
|
const config = JSON.parse(text);
|
|
19
19
|
return id === 'opencode'
|
|
20
20
|
? config?.provider?.scalemax?.name === 'ScaleMax'
|
|
21
|
-
: typeof config?.models?.providers?.
|
|
21
|
+
: ['scalemax', 'scalemax-codex', 'scalemax-claude'].some(provider => typeof config?.models?.providers?.[provider]?.baseUrl === 'string');
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
24
|
return false;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
import { commandExists } from '../utils/exec.js';
|
|
5
|
+
import { clientConfigPath } from '../configure/paths.js';
|
|
6
|
+
const extensionPattern = /^rooveterinaryinc\.roo-cline-/i;
|
|
7
|
+
async function extensionPaths(home) {
|
|
8
|
+
const roots = [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
|
|
9
|
+
const hits = [];
|
|
10
|
+
for (const root of roots) {
|
|
11
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
12
|
+
for (const entry of entries)
|
|
13
|
+
if (entry.isDirectory() && extensionPattern.test(entry.name))
|
|
14
|
+
hits.push(path.join(root, entry.name));
|
|
15
|
+
}
|
|
16
|
+
return hits;
|
|
17
|
+
}
|
|
18
|
+
export async function detect(ctx) {
|
|
19
|
+
const paths = await extensionPaths(ctx.home);
|
|
20
|
+
const executable = await commandExists('code', ctx.env);
|
|
21
|
+
if (executable)
|
|
22
|
+
paths.unshift('command:code');
|
|
23
|
+
const settingsPath = clientConfigPath('roo-code', ctx.home, ctx.platform, ctx.env);
|
|
24
|
+
let configured = false;
|
|
25
|
+
if (await fs.pathExists(settingsPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const settings = JSON5.parse(await fs.readFile(settingsPath, 'utf8'));
|
|
28
|
+
configured = settings?.['roo-cline.autoImportSettingsPath'] === '~/.scalemax/roo-code-import.json';
|
|
29
|
+
if (configured)
|
|
30
|
+
paths.push(settingsPath);
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
}
|
|
34
|
+
const installed = paths.some(entry => entry === 'command:code' || extensionPattern.test(path.basename(entry)));
|
|
35
|
+
return { id: 'roo-code', name: 'Roo Code (VS Code)', installed, configured, executable, reason: installed ? undefined : 'Roo Code extension was not detected', paths };
|
|
36
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ClientId = 'codex' | 'cursor' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
|
|
1
|
+
export type ClientId = 'codex' | 'cursor' | 'roo-code' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
|
|
2
2
|
export type ProviderScope = 'openai' | 'anthropic' | 'both';
|
|
3
3
|
export interface ClientDefinition {
|
|
4
4
|
id: ClientId;
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ApiValidation {
|
|
|
22
22
|
error?: string;
|
|
23
23
|
surfaceMismatch?: boolean;
|
|
24
24
|
}
|
|
25
|
+
export declare const RETIRED_MODEL_IDS: Set<string>;
|
|
26
|
+
export declare function isRetiredModelId(id: string): boolean;
|
|
25
27
|
export declare function isAnthropicModel(model: ModelInfo): boolean;
|
|
26
28
|
export declare function filterModelsForScope(models: ModelInfo[], scope: ProviderScope): ModelInfo[];
|
|
27
29
|
export declare function validateApiKey(apiKey: string): Promise<ApiValidation>;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SCALEMAX_BASE_URL, SCALEMAX_TOKEN_BASE_URL } from '../constants.js';
|
|
2
2
|
import { redactApiKey, sanitizeApiKey } from './apiKey.js';
|
|
3
|
+
// These aliases are intentionally unavailable because their upstream routes
|
|
4
|
+
// were retired after repeated 5xx responses. Keep a stale catalog response
|
|
5
|
+
// from selecting or advertising a model that cannot complete requests.
|
|
6
|
+
export const RETIRED_MODEL_IDS = new Set(['gpt-5.6-sol', 'fable-5-ultra']);
|
|
7
|
+
export function isRetiredModelId(id) {
|
|
8
|
+
return RETIRED_MODEL_IDS.has(id.trim());
|
|
9
|
+
}
|
|
3
10
|
export function isAnthropicModel(model) {
|
|
4
11
|
const provider = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
5
12
|
if (provider)
|
|
@@ -7,9 +14,8 @@ export function isAnthropicModel(model) {
|
|
|
7
14
|
return model.id.startsWith('claude-') && !model.id.startsWith('claude-fable-');
|
|
8
15
|
}
|
|
9
16
|
export function filterModelsForScope(models, scope) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return models.filter(model => scope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
|
|
17
|
+
return models.filter(model => !isRetiredModelId(model.id)
|
|
18
|
+
&& (scope === 'both' || (scope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model))));
|
|
13
19
|
}
|
|
14
20
|
async function friendlyError(res) {
|
|
15
21
|
let body = '';
|