scalemax 0.1.27 → 0.1.28
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 +8 -3
- package/dist/clients.js +3 -0
- package/dist/commands/configure.js +16 -2
- package/dist/commands/helper-bot.d.ts +2 -2
- package/dist/commands/helper-bot.js +29 -9
- package/dist/commands/helper-docs.js +10 -4
- package/dist/configure/codex.js +2 -2
- package/dist/configure/continue.d.ts +5 -0
- package/dist/configure/continue.js +185 -0
- package/dist/configure/hermes.js +4 -4
- package/dist/configure/models.d.ts +7 -0
- package/dist/configure/models.js +19 -1
- package/dist/configure/openclaw.js +45 -12
- package/dist/configure/paths.js +2 -0
- package/dist/configure/roo-code.js +10 -4
- package/dist/configure/templates.js +4 -3
- package/dist/detect/common.js +1 -1
- package/dist/detect/continue.d.ts +2 -0
- package/dist/detect/continue.js +45 -0
- package/dist/types/client.d.ts +2 -2
- package/dist/utils/api.d.ts +2 -1
- package/dist/utils/api.js +27 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Recognized setup, repair, and verification requests execute the CLI's allowliste
|
|
|
125
125
|
For Claude tool access, run `scalemax claude-tools`, or ask helper-bot to `install GitHub skill tools for Claude Desktop and Claude Code`. Claude Code receives Anthropic's official `example-skills@anthropic-agent-skills` plugin plus public-GitHub read permissions. Claude Desktop/Cowork receives a local MCP server with only three tools: `github_fetch`, `github_tree`, and `github_install_skill`. The MCP server accepts only public GitHub HTTPS hosts, limits downloads, rejects unsafe archive paths and symlinks, refuses to replace an existing skill, and can write only beneath `~/Claude/skills`. It does not expose a general shell, arbitrary URLs, or unrestricted filesystem access. Restart Claude Desktop after installation and use Cowork or Code; a normal Chat tab does not expose local tools.
|
|
126
126
|
Only install skills from repositories you trust: a `SKILL.md` is instruction content that Claude may follow after installation, even though the bridge itself cannot execute arbitrary repository code.
|
|
127
127
|
|
|
128
|
-
For Roo Code, helper-bot can install VS Code on Windows with the fixed Microsoft winget package, install the official Roo Code extension, write OpenAI and Claude ScaleMax profiles when the key exposes
|
|
128
|
+
For Roo Code, helper-bot can install VS Code on Windows with the fixed Microsoft winget package, install the official Roo Code extension, write OpenAI and Claude ScaleMax profiles when the key exposes those families, and verify the import handoff. For DeepSeek V4 Flash, helper-bot installs and configures Continue's Chat/Edit integration instead because the Azure deployment has no tool calling. For general troubleshooting that cannot be completed by a fixed client action, helper-bot sends a small `gpt-5.4` request grounded with bundled ScaleMax documentation about stored authentication, credit/token base URLs, provider scope, client setup, and common errors. The language model never generates or executes shell commands; only fixed ScaleMax client detection, installation, configuration, verification, and rollback operations are available to the controller.
|
|
129
129
|
|
|
130
130
|
The helper refuses unrelated requests and redacts ScaleMax-style secrets from prompts, provider output, errors, and progress notifications. Use `/status` for non-secret connection details, `/help` for commands, and `/quit` to exit.
|
|
131
131
|
|
|
@@ -136,6 +136,7 @@ The helper refuses unrelated requests and redacts ScaleMax-style secrets from pr
|
|
|
136
136
|
- Codex
|
|
137
137
|
- Cursor
|
|
138
138
|
- Roo Code (VS Code)
|
|
139
|
+
- Continue (VS Code)
|
|
139
140
|
- TRAE SOLO
|
|
140
141
|
- Claude Code
|
|
141
142
|
- OpenCode
|
|
@@ -160,7 +161,7 @@ ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.js
|
|
|
160
161
|
|
|
161
162
|
### OpenClaw
|
|
162
163
|
|
|
163
|
-
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.
|
|
164
|
+
ScaleMax partitions OpenClaw models into native `openai-responses` (`scalemax-codex`, `/v1`), OpenAI-compatible `openai-completions` (`scalemax-deepseek`, `/v1`), and `anthropic-messages` (`scalemax-claude`, origin) providers. `deepseek-v4-flash` is isolated in the Chat Completions provider with tools disabled; it is never advertised through Responses. 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.
|
|
164
165
|
|
|
165
166
|
### Hermes
|
|
166
167
|
|
|
@@ -168,7 +169,11 @@ ScaleMax creates family-specific Hermes providers in `~/.hermes/config.yaml` and
|
|
|
168
169
|
|
|
169
170
|
### Roo Code (VS Code)
|
|
170
171
|
|
|
171
|
-
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
|
|
172
|
+
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 and Claude profiles remain separate. DeepSeek V4 Flash is deliberately not offered in Roo because Roo's task-completion loop requires tool calling and this Azure deployment does not expose it. 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.
|
|
173
|
+
|
|
174
|
+
### Continue (VS Code)
|
|
175
|
+
|
|
176
|
+
Continue is the supported VS Code route for `deepseek-v4-flash`. The CLI installs the official `Continue.continue` extension when VS Code is available, merges a Chat/Edit-only OpenAI-compatible model into `~/.continue/config.yaml`, sets `useResponsesApi: false`, and keeps the authenticated ScaleMax key in private `~/.continue/.env` rather than the YAML. Existing Continue models and environment entries are preserved. Reload VS Code, select **ScaleMax DeepSeek V4 Flash**, and use Chat or Edit; Agent mode is unavailable because the deployment does not expose tool calling.
|
|
172
177
|
|
|
173
178
|
### Cursor BYOK routing on macOS
|
|
174
179
|
|
package/dist/clients.js
CHANGED
|
@@ -4,6 +4,8 @@ import * as cursorDetect from './detect/cursor.js';
|
|
|
4
4
|
import * as cursorConfig from './configure/cursor.js';
|
|
5
5
|
import * as rooCodeDetect from './detect/roo-code.js';
|
|
6
6
|
import * as rooCodeConfig from './configure/roo-code.js';
|
|
7
|
+
import * as continueDetect from './detect/continue.js';
|
|
8
|
+
import * as continueConfig from './configure/continue.js';
|
|
7
9
|
import * as traeSoloDetect from './detect/trae-solo.js';
|
|
8
10
|
import * as traeSoloConfig from './configure/trae-solo.js';
|
|
9
11
|
import * as claudeDetect from './detect/claude.js';
|
|
@@ -22,6 +24,7 @@ export const clients = [
|
|
|
22
24
|
{ id: 'codex', name: 'Codex', detect: codexDetect.detect, install: codexConfig.install, configure: codexConfig.configure, verify: codexConfig.verify, launch: codexConfig.launch, remove: codexConfig.remove },
|
|
23
25
|
{ id: 'cursor', name: 'Cursor', detect: cursorDetect.detect, configure: cursorConfig.configure, verify: cursorConfig.verify, remove: cursorConfig.remove },
|
|
24
26
|
{ id: 'roo-code', name: 'Roo Code (VS Code)', detect: rooCodeDetect.detect, configure: rooCodeConfig.configure, verify: rooCodeConfig.verify, remove: rooCodeConfig.remove },
|
|
27
|
+
{ id: 'continue', name: 'Continue (VS Code)', detect: continueDetect.detect, install: continueConfig.install, configure: continueConfig.configure, verify: continueConfig.verify, remove: continueConfig.remove },
|
|
25
28
|
{ id: 'trae-solo', name: 'TRAE SOLO', detect: traeSoloDetect.detect, configure: traeSoloConfig.configure, remove: traeSoloConfig.remove },
|
|
26
29
|
{ id: 'claude', name: 'Claude Code', detect: claudeDetect.detect, configure: claudeConfig.configure, verify: claudeConfig.verify, remove: claudeConfig.remove },
|
|
27
30
|
{ id: 'opencode', name: 'OpenCode', detect: opencodeDetect.detect, configure: opencodeConfig.configure, verify: opencodeConfig.verify, remove: opencodeConfig.remove },
|
|
@@ -6,6 +6,7 @@ import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
|
6
6
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
7
7
|
import { validateApiKey, isAnthropicModel } from '../utils/api.js';
|
|
8
8
|
import { homeDir } from '../utils/paths.js';
|
|
9
|
+
import { isDeepSeekChatModelId } from '../configure/models.js';
|
|
9
10
|
export async function detectAll(home = homeDir()) {
|
|
10
11
|
const ctx = { home, platform: process.platform, env: process.env };
|
|
11
12
|
return Promise.all(clients.map(c => c.detect(ctx)));
|
|
@@ -44,13 +45,26 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
44
45
|
return false;
|
|
45
46
|
// These clients use either a native family-specific adapter or the gateway's
|
|
46
47
|
// normalized OpenAI-compatible Chat Completions route, which supports both
|
|
47
|
-
// authenticated model families. Codex
|
|
48
|
-
|
|
48
|
+
// authenticated model families. Codex remains Responses-only; TRAE uses
|
|
49
|
+
// OpenAI-compatible Chat Completions.
|
|
50
|
+
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
51
|
+
if (clientId === 'continue')
|
|
52
|
+
return models.some(model => isDeepSeekChatModelId(model.id));
|
|
53
|
+
if (clientId === 'claude')
|
|
54
|
+
return models.some(isAnthropicModel);
|
|
55
|
+
if (clientId === 'codex')
|
|
56
|
+
return models.some(model => !isAnthropicModel(model) && !isDeepSeekChatModelId(model.id));
|
|
57
|
+
if (clientId === 'hermes' && models.every(model => isDeepSeekChatModelId(model.id)))
|
|
58
|
+
return false;
|
|
59
|
+
if (clientId === 'roo-code' && models.every(model => isDeepSeekChatModelId(model.id)))
|
|
60
|
+
return false;
|
|
49
61
|
if (crossProtocolClients.has(clientId))
|
|
50
62
|
return true;
|
|
51
63
|
return models.some(model => !isAnthropicModel(model));
|
|
52
64
|
}
|
|
53
65
|
function scopeHint(scope) {
|
|
66
|
+
if (scope === 'deepseek-v4-flash')
|
|
67
|
+
return 'DeepSeek V4 Flash only';
|
|
54
68
|
return scope === 'both' ? 'OpenAI + Claude' : scope === 'anthropic' ? 'Claude only' : 'OpenAI only';
|
|
55
69
|
}
|
|
56
70
|
export async function configureCommand(opts = {}) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ChatCompletionOptions, type ChatCompletionResult, type ChatMessage, type ModelInfo } from '../utils/api.js';
|
|
2
2
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
3
3
|
import { validateApiKey } from '../utils/api.js';
|
|
4
|
-
import type { ClientDefinition } from '../types/client.js';
|
|
4
|
+
import type { ClientDefinition, ProviderScope } from '../types/client.js';
|
|
5
5
|
export declare const SCALEMAX_HELPER_REFUSAL: string;
|
|
6
6
|
export declare const SCALEMAX_HELPER_SYSTEM_PROMPT: string;
|
|
7
7
|
export declare const SCALEMAX_HELPER_INVALID_PROVIDER_RESPONSE: string;
|
|
@@ -14,7 +14,7 @@ export interface HelperConnection {
|
|
|
14
14
|
baseUrl: string;
|
|
15
15
|
model: string;
|
|
16
16
|
platform?: NodeJS.Platform;
|
|
17
|
-
providerScope?:
|
|
17
|
+
providerScope?: ProviderScope;
|
|
18
18
|
keyMode?: string;
|
|
19
19
|
availableModels?: string[];
|
|
20
20
|
modelCatalog?: ModelInfo[];
|
|
@@ -760,7 +760,8 @@ For automatic configuration, exit with \`/quit\`, run \`scalemax update\`, then
|
|
|
760
760
|
return reply;
|
|
761
761
|
}
|
|
762
762
|
const HELPER_CLIENT_PATTERNS = [
|
|
763
|
-
{ id: '
|
|
763
|
+
{ id: 'continue', pattern: /\b(?:continue(?:\.dev)?|vscode\s+continue|vs\s+code\s+continue)\b/i },
|
|
764
|
+
{ id: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo)\b/i },
|
|
764
765
|
{ id: 'openclaw', pattern: /\bopen\s*claw\b/i },
|
|
765
766
|
{ id: 'opencode', pattern: /\bopen\s*code\b/i },
|
|
766
767
|
{ id: 'cherry-studio', pattern: /\bcherry\s*studio\b/i },
|
|
@@ -799,6 +800,8 @@ function helperActionTarget(question) {
|
|
|
799
800
|
return undefined;
|
|
800
801
|
}
|
|
801
802
|
function requestedProviderScope(question, target) {
|
|
803
|
+
if (/\bdeepseek(?:[-\s]+v4[-\s]+flash)?\b/i.test(question))
|
|
804
|
+
return 'deepseek-v4-flash';
|
|
802
805
|
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
803
806
|
return 'both';
|
|
804
807
|
if (/\b(?:claude|anthropic)\b/i.test(question))
|
|
@@ -811,17 +814,32 @@ function actionModels(connection, requestedScope) {
|
|
|
811
814
|
const catalog = connection.modelCatalog?.length
|
|
812
815
|
? connection.modelCatalog
|
|
813
816
|
: (connection.availableModels || []).map(id => ({ id }));
|
|
814
|
-
if (!requestedScope
|
|
817
|
+
if (!requestedScope)
|
|
815
818
|
return catalog;
|
|
816
|
-
|
|
819
|
+
if (requestedScope === 'deepseek-v4-flash') {
|
|
820
|
+
return catalog.filter(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
|
|
821
|
+
}
|
|
822
|
+
if (requestedScope === 'both') {
|
|
823
|
+
return catalog.filter(model => model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
824
|
+
}
|
|
825
|
+
return catalog.filter(model => requestedScope === 'anthropic'
|
|
826
|
+
? isAnthropicModel(model)
|
|
827
|
+
: !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
817
828
|
}
|
|
818
829
|
function helperClientAllowedForModels(clientId, models) {
|
|
819
830
|
if (!models.length)
|
|
820
831
|
return false;
|
|
821
|
-
const crossProtocolClients = new Set(['
|
|
832
|
+
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
833
|
+
const onlyDeepSeek = models.every(model => model.id.trim().toLowerCase() === 'deepseek-v4-flash');
|
|
834
|
+
if (clientId === 'continue')
|
|
835
|
+
return onlyDeepSeek;
|
|
836
|
+
if (clientId === 'claude')
|
|
837
|
+
return models.some(isAnthropicModel);
|
|
838
|
+
if ((clientId === 'codex' || clientId === 'hermes' || clientId === 'roo-code') && onlyDeepSeek)
|
|
839
|
+
return false;
|
|
822
840
|
if (crossProtocolClients.has(clientId))
|
|
823
841
|
return true;
|
|
824
|
-
return models.some(model => !isAnthropicModel(model));
|
|
842
|
+
return models.some(model => !isAnthropicModel(model) && model.id.trim().toLowerCase() !== 'deepseek-v4-flash');
|
|
825
843
|
}
|
|
826
844
|
function hasRooCodeExtension(paths) {
|
|
827
845
|
return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
|
|
@@ -905,9 +923,9 @@ async function installRooCodeExtension(ctx, client, write) {
|
|
|
905
923
|
async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
|
|
906
924
|
const models = actionModels(connection, requestedScope);
|
|
907
925
|
if (!models.length)
|
|
908
|
-
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
909
|
-
if (client.id === 'codex' && models.every(isAnthropicModel))
|
|
910
|
-
throw new Error('Codex requires
|
|
926
|
+
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
927
|
+
if (client.id === 'codex' && models.every(model => isAnthropicModel(model) || model.id.trim().toLowerCase() === 'deepseek-v4-flash'))
|
|
928
|
+
throw new Error('Codex requires a Responses-capable model, but this key exposes only Claude or Chat-Completions-only models. No configuration was changed.');
|
|
911
929
|
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
912
930
|
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
913
931
|
const ctx = {
|
|
@@ -978,7 +996,9 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
978
996
|
throw new Error(`${client.name} configuration was written but could not be detected afterward.`);
|
|
979
997
|
write(`✓ ${client.name} configuration detected after write.`);
|
|
980
998
|
}
|
|
981
|
-
const activation = client.id === 'roo-code'
|
|
999
|
+
const activation = client.id === 'roo-code'
|
|
1000
|
+
? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.'
|
|
1001
|
+
: client.id === 'continue' ? ' Reload VS Code once so Continue loads the verified ScaleMax profile.' : '';
|
|
982
1002
|
return `✅ Successfully completed: ${client.name} is configured and locally verified.${activation}`;
|
|
983
1003
|
}
|
|
984
1004
|
/** Execute only narrowly allowlisted ScaleMax client actions; never model-generated shell commands. */
|
|
@@ -14,14 +14,20 @@ export const HELPER_DOC_SECTIONS = [
|
|
|
14
14
|
{
|
|
15
15
|
id: 'provider-scope',
|
|
16
16
|
title: 'Provider scope and visible models',
|
|
17
|
-
keywords: ['provider', 'scope', 'openai', 'claude', 'anthropic', 'both', 'models', 'fable'],
|
|
18
|
-
content: `A ScaleMax key may be OpenAI only, Claude only, or
|
|
17
|
+
keywords: ['provider', 'scope', 'openai', 'claude', 'anthropic', 'deepseek', 'both', 'models', 'fable'],
|
|
18
|
+
content: `A ScaleMax key may be OpenAI only, Claude only, both, or the exact-model DeepSeek V4 Flash entitlement. OpenAI-only keys should configure OpenAI-compatible models only. Claude-only keys should configure Claude-compatible models only. Both-scope keys may configure both model families in clients that can switch profiles. Fable 5 is a Claude-family frontend model through ScaleMax and should only appear for Claude-capable or both-scope keys, not OpenAI-only keys. A DeepSeek V4 Flash key discovers only deepseek-v4-flash; broad OpenAI and both scopes do not inherit that entitlement. The model uses the OpenAI-compatible /v1/chat/completions route with streaming and max_tokens; it must not be placed on a Responses route or advertised as tool-capable.`
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
id: 'roo-code',
|
|
22
22
|
title: 'Roo Code setup through VS Code',
|
|
23
|
-
keywords: ['roo', 'roo code', 'vscode', 'vs code', 'extension', 'visual studio code'],
|
|
24
|
-
content: `For Roo Code, the ScaleMax CLI writes a private settings import file at ~/.scalemax/roo-code-import.json and sets roo-cline.autoImportSettingsPath in VS Code User settings. Roo imports that profile after VS Code is reloaded. The CLI can install the official Roo Code extension with code --install-extension RooVeterinaryInc.roo-cline when VS Code is detected. On Windows, helper-bot may install VS Code first with winget using the fixed Microsoft.VisualStudioCode package id, then install Roo, write the import handoff, and verify the files.`
|
|
23
|
+
keywords: ['roo', 'roo code', 'vscode', 'vs code', 'extension', 'visual studio code', 'deepseek'],
|
|
24
|
+
content: `For Roo Code, the ScaleMax CLI writes a private settings import file at ~/.scalemax/roo-code-import.json and sets roo-cline.autoImportSettingsPath in VS Code User settings. Roo imports that profile after VS Code is reloaded. Roo is not offered for a DeepSeek V4 Flash-only key because Roo's task loop requires tool calling and this Azure deployment does not expose it; use Continue Chat/Edit instead. The CLI can install the official Roo Code extension with code --install-extension RooVeterinaryInc.roo-cline when VS Code is detected. On Windows, helper-bot may install VS Code first with winget using the fixed Microsoft.VisualStudioCode package id, then install Roo, write the import handoff, and verify the files.`
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'continue-deepseek',
|
|
28
|
+
title: 'Continue setup for DeepSeek V4 Flash',
|
|
29
|
+
keywords: ['continue', 'continue dev', 'vscode', 'vs code', 'extension', 'deepseek', 'deepseek-v4-flash'],
|
|
30
|
+
content: `Continue is the supported VS Code integration for DeepSeek V4 Flash. scalemax update installs the official Continue extension when VS Code is available, writes a private ~/.continue/config.yaml Chat/Edit profile, stores the authenticated ScaleMax key in private ~/.continue/.env, and forces useResponsesApi: false. Reload VS Code after setup. Do not use Agent mode and do not request or configure a DeepSeek, Azure, or other upstream credential.`
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
id: 'codex-and-clients',
|
package/dist/configure/codex.js
CHANGED
|
@@ -5,7 +5,7 @@ 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 {
|
|
8
|
+
import { preferredOpenAiResponsesModel } from './models.js';
|
|
9
9
|
const id = 'codex';
|
|
10
10
|
const name = 'Codex';
|
|
11
11
|
const provider = 'scalemax';
|
|
@@ -14,7 +14,7 @@ const reasoningEffort = 'medium';
|
|
|
14
14
|
function selectedModel(ctx) {
|
|
15
15
|
if (!ctx.models)
|
|
16
16
|
return fallbackModel;
|
|
17
|
-
return
|
|
17
|
+
return preferredOpenAiResponsesModel(ctx);
|
|
18
18
|
}
|
|
19
19
|
function scalemaxCodexBlock(baseUrl, model = fallbackModel) {
|
|
20
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`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ConfigureContext, ConfigureResult, InstallContext } from '../types/client.js';
|
|
2
|
+
export declare function install(ctx: InstallContext): Promise<ConfigureResult>;
|
|
3
|
+
export declare function configure(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
4
|
+
export declare function verify(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
5
|
+
export declare function remove(ctx: ConfigureContext): Promise<ConfigureResult>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { parseDocument } from 'yaml';
|
|
5
|
+
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
6
|
+
import { commandExists } from '../utils/exec.js';
|
|
7
|
+
import { clientConfigPath } from './paths.js';
|
|
8
|
+
import { isDeepSeekChatModelId } from './models.js';
|
|
9
|
+
const id = 'continue';
|
|
10
|
+
const name = 'Continue (VS Code)';
|
|
11
|
+
const modelName = 'ScaleMax DeepSeek V4 Flash';
|
|
12
|
+
const modelId = 'deepseek-v4-flash';
|
|
13
|
+
const envKey = 'SCALEMAX_API_KEY';
|
|
14
|
+
const envMarker = '# ScaleMax managed Continue API key';
|
|
15
|
+
const userAgent = 'ScaleMax-Continue/1.0';
|
|
16
|
+
function envPath(home) { return path.join(home, '.continue', '.env'); }
|
|
17
|
+
function asRecord(value) {
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
19
|
+
}
|
|
20
|
+
function parseYaml(text) {
|
|
21
|
+
const doc = parseDocument(text || '{}');
|
|
22
|
+
if (doc.errors.length)
|
|
23
|
+
throw new Error(`Continue config.yaml is invalid: ${doc.errors[0].message}`);
|
|
24
|
+
if (!doc.contents)
|
|
25
|
+
doc.contents = doc.createNode({});
|
|
26
|
+
const root = doc.toJS();
|
|
27
|
+
if (!root || typeof root !== 'object' || Array.isArray(root))
|
|
28
|
+
throw new Error('Continue config.yaml must contain a YAML object.');
|
|
29
|
+
return doc;
|
|
30
|
+
}
|
|
31
|
+
function isManagedModel(model) {
|
|
32
|
+
return model?.name === modelName
|
|
33
|
+
&& ['azure', 'openai'].includes(model?.provider)
|
|
34
|
+
&& model?.model === modelId
|
|
35
|
+
&& typeof model?.apiBase === 'string'
|
|
36
|
+
&& /^https:\/\/api\.scalemax\.pro\/(?:token\/)?v1\/?$/i.test(model.apiBase);
|
|
37
|
+
}
|
|
38
|
+
function managedEnv(text, apiKey) {
|
|
39
|
+
const kept = text.split(/\r?\n/).filter(line => line.trim() !== envMarker && !line.startsWith(`${envKey}=`));
|
|
40
|
+
while (kept.length && !kept[kept.length - 1])
|
|
41
|
+
kept.pop();
|
|
42
|
+
return `${kept.length ? `${kept.join('\n')}\n` : ''}${envMarker}\n${envKey}=${apiKey}\n`;
|
|
43
|
+
}
|
|
44
|
+
function withoutManagedEnv(text) {
|
|
45
|
+
const kept = text.split(/\r?\n/).filter(line => line.trim() !== envMarker && !line.startsWith(`${envKey}=`));
|
|
46
|
+
while (kept.length && !kept[kept.length - 1])
|
|
47
|
+
kept.pop();
|
|
48
|
+
return kept.length ? `${kept.join('\n')}\n` : '';
|
|
49
|
+
}
|
|
50
|
+
function requireDeepSeek(ctx) {
|
|
51
|
+
if (!(ctx.models || []).some(model => isDeepSeekChatModelId(model.id))) {
|
|
52
|
+
throw new Error('Continue automatic setup for this integration requires a DeepSeek V4 Flash key. No configuration was changed.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function install(ctx) {
|
|
56
|
+
const extensionRoots = [path.join(ctx.home, '.vscode', 'extensions'), path.join(ctx.home, '.vscode-insiders', 'extensions')];
|
|
57
|
+
for (const root of extensionRoots) {
|
|
58
|
+
const entries = await fs.readdir(root).catch(() => []);
|
|
59
|
+
if (entries.some(entry => /^continue\.continue-/i.test(entry)))
|
|
60
|
+
return { id, name, changed: false, message: 'extension is already installed' };
|
|
61
|
+
}
|
|
62
|
+
if (!(await commandExists('code', ctx.env)))
|
|
63
|
+
throw new Error('VS Code was not detected. Install VS Code, then retry Continue setup.');
|
|
64
|
+
const result = await execa('code', ['--install-extension', 'Continue.continue', '--force'], { env: ctx.env, reject: false, all: true });
|
|
65
|
+
if (result.exitCode !== 0)
|
|
66
|
+
throw new Error(`Continue extension installation failed: ${result.all?.trim() || `code exited with ${result.exitCode}`}`);
|
|
67
|
+
return { id, name, changed: true, message: 'official Continue extension installed' };
|
|
68
|
+
}
|
|
69
|
+
export async function configure(ctx) {
|
|
70
|
+
requireDeepSeek(ctx);
|
|
71
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
72
|
+
const secrets = envPath(ctx.home);
|
|
73
|
+
const originalConfig = await fs.readFile(file, 'utf8').catch(() => '');
|
|
74
|
+
const originalEnv = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
75
|
+
const doc = parseYaml(originalConfig);
|
|
76
|
+
const data = asRecord(doc.toJS());
|
|
77
|
+
const existingModels = Array.isArray(data.models) ? data.models.filter((model) => !isManagedModel(model)) : [];
|
|
78
|
+
const model = {
|
|
79
|
+
name: modelName,
|
|
80
|
+
provider: 'azure',
|
|
81
|
+
model: modelId,
|
|
82
|
+
apiBase: ctx.baseUrl.replace(/\/$/, ''),
|
|
83
|
+
apiKey: `\${{ secrets.${envKey} }}`,
|
|
84
|
+
useResponsesApi: false,
|
|
85
|
+
roles: ['chat', 'edit'],
|
|
86
|
+
requestOptions: { headers: { 'User-Agent': userAgent } },
|
|
87
|
+
env: { apiType: 'openai-compatible' },
|
|
88
|
+
defaultCompletionOptions: { maxTokens: 4096 }
|
|
89
|
+
};
|
|
90
|
+
if (!data.name)
|
|
91
|
+
doc.set('name', 'ScaleMax');
|
|
92
|
+
if (!data.version)
|
|
93
|
+
doc.set('version', '1.0.0');
|
|
94
|
+
if (!data.schema)
|
|
95
|
+
doc.set('schema', 'v1');
|
|
96
|
+
doc.set('models', [...existingModels, model]);
|
|
97
|
+
const nextConfig = String(doc);
|
|
98
|
+
const nextEnv = managedEnv(originalEnv, ctx.apiKey);
|
|
99
|
+
if (originalConfig === nextConfig && originalEnv === nextEnv) {
|
|
100
|
+
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
101
|
+
await fs.chmod(secrets, 0o600).catch(() => undefined);
|
|
102
|
+
return { id, name, changed: false, path: file, message: 'DeepSeek V4 Flash Chat/Edit profile is already current' };
|
|
103
|
+
}
|
|
104
|
+
const backups = [await backupFile(file, ctx.home), await backupFile(secrets, ctx.home)].filter(Boolean);
|
|
105
|
+
try {
|
|
106
|
+
await atomicWrite(file, nextConfig, 0o600, ctx.platform);
|
|
107
|
+
await atomicWrite(secrets, nextEnv, 0o600, ctx.platform);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (originalConfig)
|
|
111
|
+
await atomicWrite(file, originalConfig, 0o600, ctx.platform);
|
|
112
|
+
else
|
|
113
|
+
await fs.remove(file).catch(() => undefined);
|
|
114
|
+
if (originalEnv)
|
|
115
|
+
await atomicWrite(secrets, originalEnv, 0o600, ctx.platform);
|
|
116
|
+
else
|
|
117
|
+
await fs.remove(secrets).catch(() => undefined);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return { id, name, changed: true, path: file, message: `configured a text-only DeepSeek V4 Flash Chat/Edit profile with a private credential${backups.length ? ' (backups created)' : ''}` };
|
|
121
|
+
}
|
|
122
|
+
export async function verify(ctx) {
|
|
123
|
+
requireDeepSeek(ctx);
|
|
124
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
125
|
+
const secrets = envPath(ctx.home);
|
|
126
|
+
const doc = parseYaml(await fs.readFile(file, 'utf8'));
|
|
127
|
+
const data = asRecord(doc.toJS());
|
|
128
|
+
const model = Array.isArray(data.models)
|
|
129
|
+
? data.models.find((candidate) => isManagedModel(candidate) && candidate.provider === 'azure')
|
|
130
|
+
: undefined;
|
|
131
|
+
if (!model
|
|
132
|
+
|| model.apiBase !== ctx.baseUrl.replace(/\/$/, '')
|
|
133
|
+
|| model.apiKey !== `\${{ secrets.${envKey} }}`
|
|
134
|
+
|| model.useResponsesApi !== false
|
|
135
|
+
|| JSON.stringify(model.roles) !== JSON.stringify(['chat', 'edit'])
|
|
136
|
+
|| model.requestOptions?.headers?.['User-Agent'] !== userAgent
|
|
137
|
+
|| model.env?.apiType !== 'openai-compatible'
|
|
138
|
+
|| model.defaultCompletionOptions?.maxTokens !== 4096) {
|
|
139
|
+
throw new Error('Continue DeepSeek V4 Flash Chat/Edit configuration is incomplete.');
|
|
140
|
+
}
|
|
141
|
+
const env = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
142
|
+
if (!env.includes(envMarker) || !env.includes(`${envKey}=${ctx.apiKey}`))
|
|
143
|
+
throw new Error('Continue is missing its authenticated ScaleMax credential.');
|
|
144
|
+
if (ctx.platform !== 'win32') {
|
|
145
|
+
for (const candidate of [file, secrets])
|
|
146
|
+
if (((await fs.stat(candidate)).mode & 0o077) !== 0)
|
|
147
|
+
throw new Error(`Continue file permissions are too broad: ${candidate}`);
|
|
148
|
+
}
|
|
149
|
+
return { id, name, changed: false, path: file, message: 'verified DeepSeek V4 Flash Chat/Edit configuration; reload VS Code to activate it' };
|
|
150
|
+
}
|
|
151
|
+
export async function remove(ctx) {
|
|
152
|
+
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
153
|
+
const secrets = envPath(ctx.home);
|
|
154
|
+
if (!(await fs.pathExists(file)))
|
|
155
|
+
return { id, name, changed: false, path: file, message: 'no ScaleMax Continue configuration found' };
|
|
156
|
+
const original = await fs.readFile(file, 'utf8');
|
|
157
|
+
const doc = parseYaml(original);
|
|
158
|
+
const data = asRecord(doc.toJS());
|
|
159
|
+
const models = Array.isArray(data.models) ? data.models : [];
|
|
160
|
+
const remaining = models.filter((model) => !isManagedModel(model));
|
|
161
|
+
if (remaining.length === models.length)
|
|
162
|
+
return { id, name, changed: false, path: file, message: 'left unmanaged Continue configuration unchanged' };
|
|
163
|
+
await backupFile(file, ctx.home);
|
|
164
|
+
const currentEnv = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
165
|
+
if (currentEnv)
|
|
166
|
+
await backupFile(secrets, ctx.home);
|
|
167
|
+
if (remaining.length)
|
|
168
|
+
doc.set('models', remaining);
|
|
169
|
+
else
|
|
170
|
+
doc.delete('models');
|
|
171
|
+
const nextData = asRecord(doc.toJS());
|
|
172
|
+
const generatedOnly = !remaining.length
|
|
173
|
+
&& Object.keys(nextData).every(key => ['name', 'version', 'schema'].includes(key))
|
|
174
|
+
&& data.name === 'ScaleMax';
|
|
175
|
+
if (generatedOnly)
|
|
176
|
+
await fs.remove(file);
|
|
177
|
+
else
|
|
178
|
+
await atomicWrite(file, String(doc), 0o600, ctx.platform);
|
|
179
|
+
const nextEnv = withoutManagedEnv(currentEnv);
|
|
180
|
+
if (nextEnv)
|
|
181
|
+
await atomicWrite(secrets, nextEnv, 0o600, ctx.platform);
|
|
182
|
+
else
|
|
183
|
+
await fs.remove(secrets).catch(() => undefined);
|
|
184
|
+
return { id, name, changed: true, path: file, message: 'removed only the ScaleMax DeepSeek model and managed credential' };
|
|
185
|
+
}
|
package/dist/configure/hermes.js
CHANGED
|
@@ -4,7 +4,7 @@ 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 { anthropicModelIds, anthropicOrigin, openAiBaseUrl,
|
|
7
|
+
import { anthropicModelIds, anthropicOrigin, openAiBaseUrl, openAiResponsesModelIds, preferredAnthropicModel, preferredOpenAiResponsesModel } from './models.js';
|
|
8
8
|
const id = 'hermes';
|
|
9
9
|
const name = 'Hermes';
|
|
10
10
|
const envKey = 'SCALEMAX_HERMES_API_KEY';
|
|
@@ -64,7 +64,7 @@ export async function configure(ctx) {
|
|
|
64
64
|
const originalEnv = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
65
65
|
const doc = parseYaml(originalConfig);
|
|
66
66
|
const data = asRecord(doc.toJS());
|
|
67
|
-
const codexIds =
|
|
67
|
+
const codexIds = openAiResponsesModelIds(ctx);
|
|
68
68
|
const claudeIds = anthropicModelIds(ctx);
|
|
69
69
|
const managedNames = new Set([legacyProviderName, codexProviderName, claudeProviderName]);
|
|
70
70
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers.filter((provider) => !managedNames.has(provider?.name)) : [];
|
|
@@ -95,7 +95,7 @@ export async function configure(ctx) {
|
|
|
95
95
|
if (activatesScaleMax) {
|
|
96
96
|
const keepClaude = currentProvider === `custom:${claudeProviderName}` || claudeIds.includes(String(currentModel.default || ''));
|
|
97
97
|
const activeName = codexIds.length && !keepClaude ? codexProviderName : claudeProviderName;
|
|
98
|
-
const activeModel = activeName === codexProviderName ?
|
|
98
|
+
const activeModel = activeName === codexProviderName ? preferredOpenAiResponsesModel(ctx) : preferredAnthropicModel(ctx);
|
|
99
99
|
doc.set('model', { ...currentModel, provider: `custom:${activeName}`, default: activeModel, max_tokens: 4096 });
|
|
100
100
|
}
|
|
101
101
|
const nextConfig = String(doc);
|
|
@@ -131,7 +131,7 @@ export async function verify(ctx) {
|
|
|
131
131
|
const doc = parseYaml(await fs.readFile(file, 'utf8'));
|
|
132
132
|
const data = asRecord(doc.toJS());
|
|
133
133
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers : [];
|
|
134
|
-
const codexIds =
|
|
134
|
+
const codexIds = openAiResponsesModelIds(ctx);
|
|
135
135
|
const claudeIds = anthropicModelIds(ctx);
|
|
136
136
|
const codex = providers.find((candidate) => candidate?.name === codexProviderName);
|
|
137
137
|
const claude = providers.find((candidate) => candidate?.name === claudeProviderName);
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { ConfigureContext } from '../types/client.js';
|
|
2
|
+
export declare const DEEPSEEK_V4_FLASH_MODEL_ID = "deepseek-v4-flash";
|
|
3
|
+
export declare function isDeepSeekChatModelId(id: string): boolean;
|
|
2
4
|
export declare function modelIds(ctx: ConfigureContext): string[];
|
|
3
5
|
export declare function anthropicModelIds(ctx: ConfigureContext): string[];
|
|
4
6
|
export declare function openAiModelIds(ctx: ConfigureContext): string[];
|
|
7
|
+
/** Models that must stay on the OpenAI-compatible Chat Completions route. */
|
|
8
|
+
export declare function deepSeekChatModelIds(ctx: ConfigureContext): string[];
|
|
9
|
+
/** Non-Anthropic models that may remain on existing Responses-based clients. */
|
|
10
|
+
export declare function openAiResponsesModelIds(ctx: ConfigureContext): string[];
|
|
5
11
|
export declare function preferredAnthropicModel(ctx: ConfigureContext): string;
|
|
6
12
|
export declare function preferredOpenAiModel(ctx: ConfigureContext): string;
|
|
13
|
+
export declare function preferredOpenAiResponsesModel(ctx: ConfigureContext): string;
|
|
7
14
|
/**
|
|
8
15
|
* OpenAI-compatible chat clients can use the gateway's normalized chat route
|
|
9
16
|
* for either family. Keep an OpenAI model as the default when one is present
|
package/dist/configure/models.js
CHANGED
|
@@ -14,8 +14,13 @@ const preferredOpenAi = [
|
|
|
14
14
|
'gpt-5.4',
|
|
15
15
|
'gpt-5.4-mini',
|
|
16
16
|
'gpt-5.3-codex',
|
|
17
|
-
'codex-auto-review'
|
|
17
|
+
'codex-auto-review',
|
|
18
|
+
'deepseek-v4-flash'
|
|
18
19
|
];
|
|
20
|
+
export const DEEPSEEK_V4_FLASH_MODEL_ID = 'deepseek-v4-flash';
|
|
21
|
+
export function isDeepSeekChatModelId(id) {
|
|
22
|
+
return id.trim().toLowerCase() === DEEPSEEK_V4_FLASH_MODEL_ID;
|
|
23
|
+
}
|
|
19
24
|
export function modelIds(ctx) {
|
|
20
25
|
return [...new Set((ctx.models || []).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
21
26
|
}
|
|
@@ -25,6 +30,14 @@ export function anthropicModelIds(ctx) {
|
|
|
25
30
|
export function openAiModelIds(ctx) {
|
|
26
31
|
return [...new Set((ctx.models || []).filter(model => !isAnthropicModel(model)).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
27
32
|
}
|
|
33
|
+
/** Models that must stay on the OpenAI-compatible Chat Completions route. */
|
|
34
|
+
export function deepSeekChatModelIds(ctx) {
|
|
35
|
+
return openAiModelIds(ctx).filter(isDeepSeekChatModelId);
|
|
36
|
+
}
|
|
37
|
+
/** Non-Anthropic models that may remain on existing Responses-based clients. */
|
|
38
|
+
export function openAiResponsesModelIds(ctx) {
|
|
39
|
+
return openAiModelIds(ctx).filter(id => !isDeepSeekChatModelId(id));
|
|
40
|
+
}
|
|
28
41
|
function preferredFrom(ids, candidates, family) {
|
|
29
42
|
for (const candidate of candidates)
|
|
30
43
|
if (ids.includes(candidate))
|
|
@@ -39,6 +52,9 @@ export function preferredAnthropicModel(ctx) {
|
|
|
39
52
|
export function preferredOpenAiModel(ctx) {
|
|
40
53
|
return preferredFrom(openAiModelIds(ctx), preferredOpenAi, 'Codex/OpenAI');
|
|
41
54
|
}
|
|
55
|
+
export function preferredOpenAiResponsesModel(ctx) {
|
|
56
|
+
return preferredFrom(openAiResponsesModelIds(ctx), preferredOpenAi, 'Codex/OpenAI');
|
|
57
|
+
}
|
|
42
58
|
/**
|
|
43
59
|
* OpenAI-compatible chat clients can use the gateway's normalized chat route
|
|
44
60
|
* for either family. Keep an OpenAI model as the default when one is present
|
|
@@ -75,6 +91,8 @@ export function modelName(ctx, id) {
|
|
|
75
91
|
const model = (ctx.models || []).find(candidate => candidate.id === id);
|
|
76
92
|
if (model?.display_name)
|
|
77
93
|
return model.display_name;
|
|
94
|
+
if (isDeepSeekChatModelId(id))
|
|
95
|
+
return 'DeepSeek V4 Flash';
|
|
78
96
|
return id.split('-').map(part => part ? part[0].toUpperCase() + part.slice(1) : part).join(' ');
|
|
79
97
|
}
|
|
80
98
|
export function anthropicOrigin(baseUrl) {
|
|
@@ -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 { anthropicModelIds, anthropicOrigin, fastModel, fastOpenAiModel, modelName, openAiBaseUrl,
|
|
6
|
+
import { anthropicModelIds, anthropicOrigin, deepSeekChatModelIds, fastModel, fastOpenAiModel, modelName, openAiBaseUrl, openAiResponsesModelIds, preferredAnthropicModel, preferredOpenAiResponsesModel } from './models.js';
|
|
7
7
|
import { removeClientSecret, rollbackClientSecret, verifyClientSecret, writeClientSecret } from './client-secret.js';
|
|
8
8
|
const id = 'openclaw';
|
|
9
9
|
const name = 'OpenClaw';
|
|
@@ -16,6 +16,7 @@ const userAgentValue = 'ScaleMax-OpenClaw/1.0';
|
|
|
16
16
|
const userAgentReference = { source: 'file', provider: userAgentProviderId, id: 'value' };
|
|
17
17
|
const legacyProviderName = 'scalemax';
|
|
18
18
|
const codexProviderName = 'scalemax-codex';
|
|
19
|
+
const deepSeekProviderName = 'scalemax-deepseek';
|
|
19
20
|
const claudeProviderName = 'scalemax-claude';
|
|
20
21
|
function secretPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-api-key'); }
|
|
21
22
|
function userAgentPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-user-agent'); }
|
|
@@ -40,6 +41,10 @@ function entries(ctx, ids, family) {
|
|
|
40
41
|
input: family === 'anthropic' ? ['text', 'image'] : ['text'],
|
|
41
42
|
maxTokens: Math.min(metadata?.max_output_tokens || 4096, 4096)
|
|
42
43
|
};
|
|
44
|
+
if (family === 'deepseek') {
|
|
45
|
+
result.reasoning = false;
|
|
46
|
+
result.compat = { supportsTools: false };
|
|
47
|
+
}
|
|
43
48
|
if (metadata?.context_window || family === 'anthropic')
|
|
44
49
|
result.contextWindow = metadata?.context_window || 200000;
|
|
45
50
|
return result;
|
|
@@ -78,7 +83,7 @@ async function scrubGeneratedPlaintextCredentials(ctx) {
|
|
|
78
83
|
for (const providers of roots) {
|
|
79
84
|
for (const [providerName, rawProvider] of Object.entries(providers)) {
|
|
80
85
|
const provider = asRecord(rawProvider);
|
|
81
|
-
const isScaleMax = [legacyProviderName, codexProviderName, claudeProviderName].includes(providerName)
|
|
86
|
+
const isScaleMax = [legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName].includes(providerName)
|
|
82
87
|
&& isScaleMaxBaseUrl(provider.baseUrl);
|
|
83
88
|
if (isScaleMax && typeof provider.apiKey === 'string') {
|
|
84
89
|
delete provider.apiKey;
|
|
@@ -96,11 +101,14 @@ async function scrubGeneratedPlaintextCredentials(ctx) {
|
|
|
96
101
|
}
|
|
97
102
|
export async function configure(ctx) {
|
|
98
103
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
99
|
-
const codexIds =
|
|
104
|
+
const codexIds = openAiResponsesModelIds(ctx);
|
|
105
|
+
const deepSeekIds = deepSeekChatModelIds(ctx);
|
|
100
106
|
const claudeIds = anthropicModelIds(ctx);
|
|
101
|
-
const codexPrimary = codexIds.length ? `${codexProviderName}/${
|
|
107
|
+
const codexPrimary = codexIds.length ? `${codexProviderName}/${preferredOpenAiResponsesModel(ctx)}` : undefined;
|
|
108
|
+
const deepSeekPrimary = deepSeekIds.length ? `${deepSeekProviderName}/${deepSeekIds[0]}` : undefined;
|
|
102
109
|
const claudePrimary = claudeIds.length ? `${claudeProviderName}/${preferredAnthropicModel(ctx)}` : undefined;
|
|
103
110
|
const codexQuick = fastOpenAiModel(ctx);
|
|
111
|
+
const deepSeekQuick = deepSeekIds[0];
|
|
104
112
|
const claudeQuick = fastModel(ctx);
|
|
105
113
|
let secret = { changed: false };
|
|
106
114
|
let userAgentSecret = { changed: false };
|
|
@@ -117,26 +125,34 @@ export async function configure(ctx) {
|
|
|
117
125
|
const defaults = asRecord(agents.defaults);
|
|
118
126
|
const allowlist = asRecord(defaults.models);
|
|
119
127
|
for (const key of Object.keys(allowlist)) {
|
|
120
|
-
if (key.startsWith(`${legacyProviderName}/`) || key.startsWith(`${codexProviderName}/`) || key.startsWith(`${claudeProviderName}/`))
|
|
128
|
+
if (key.startsWith(`${legacyProviderName}/`) || key.startsWith(`${codexProviderName}/`) || key.startsWith(`${deepSeekProviderName}/`) || key.startsWith(`${claudeProviderName}/`))
|
|
121
129
|
delete allowlist[key];
|
|
122
130
|
}
|
|
123
131
|
for (const modelId of codexIds)
|
|
124
132
|
allowlist[`${codexProviderName}/${modelId}`] = {};
|
|
133
|
+
for (const modelId of deepSeekIds)
|
|
134
|
+
allowlist[`${deepSeekProviderName}/${modelId}`] = {};
|
|
125
135
|
for (const modelId of claudeIds)
|
|
126
136
|
allowlist[`${claudeProviderName}/${modelId}`] = {};
|
|
127
137
|
const currentModel = defaults.model;
|
|
128
138
|
const currentPrimary = typeof currentModel === 'string' ? currentModel : asRecord(currentModel).primary;
|
|
129
139
|
const activatesScaleMax = !currentPrimary || String(currentPrimary).startsWith('scalemax');
|
|
130
140
|
const currentId = String(currentPrimary || '').split('/').slice(1).join('/');
|
|
141
|
+
const keepDeepSeek = String(currentPrimary || '').startsWith(`${deepSeekProviderName}/`) || deepSeekIds.includes(currentId);
|
|
131
142
|
const keepClaude = String(currentPrimary || '').startsWith(`${claudeProviderName}/`) || claudeIds.includes(currentId);
|
|
132
|
-
const primary =
|
|
143
|
+
const primary = keepDeepSeek
|
|
144
|
+
? deepSeekPrimary || codexPrimary || claudePrimary
|
|
145
|
+
: keepClaude
|
|
146
|
+
? claudePrimary || codexPrimary || deepSeekPrimary
|
|
147
|
+
: codexPrimary || deepSeekPrimary || claudePrimary;
|
|
133
148
|
const usesClaude = Boolean(primary?.startsWith(`${claudeProviderName}/`));
|
|
149
|
+
const usesDeepSeek = Boolean(primary?.startsWith(`${deepSeekProviderName}/`));
|
|
134
150
|
const nextModel = activatesScaleMax
|
|
135
151
|
? { ...asRecord(currentModel), ...(primary ? { primary } : {}) }
|
|
136
152
|
: currentModel;
|
|
137
153
|
const currentUtility = typeof defaults.utilityModel === 'string' ? defaults.utilityModel : undefined;
|
|
138
|
-
const quick = usesClaude ? claudeQuick : codexQuick;
|
|
139
|
-
const utilityProvider = usesClaude ? claudeProviderName : codexProviderName;
|
|
154
|
+
const quick = usesClaude ? claudeQuick : usesDeepSeek ? deepSeekQuick : codexQuick;
|
|
155
|
+
const utilityProvider = usesClaude ? claudeProviderName : usesDeepSeek ? deepSeekProviderName : codexProviderName;
|
|
140
156
|
const utilityModel = quick && (activatesScaleMax || currentUtility?.startsWith('scalemax')) ? `${utilityProvider}/${quick}` : currentUtility;
|
|
141
157
|
const nextProviders = { ...providers };
|
|
142
158
|
delete nextProviders[legacyProviderName];
|
|
@@ -151,6 +167,17 @@ export async function configure(ctx) {
|
|
|
151
167
|
};
|
|
152
168
|
else
|
|
153
169
|
delete nextProviders[codexProviderName];
|
|
170
|
+
if (deepSeekIds.length)
|
|
171
|
+
nextProviders[deepSeekProviderName] = {
|
|
172
|
+
baseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
173
|
+
apiKey: secretReference,
|
|
174
|
+
api: 'openai-completions',
|
|
175
|
+
maxTokens: 4096,
|
|
176
|
+
request: { headers: { 'User-Agent': userAgentReference }, allowPrivateNetwork: false },
|
|
177
|
+
models: entries(ctx, deepSeekIds, 'deepseek')
|
|
178
|
+
};
|
|
179
|
+
else
|
|
180
|
+
delete nextProviders[deepSeekProviderName];
|
|
154
181
|
if (claudeIds.length)
|
|
155
182
|
nextProviders[claudeProviderName] = {
|
|
156
183
|
baseUrl: anthropicOrigin(ctx.baseUrl),
|
|
@@ -191,16 +218,18 @@ export async function configure(ctx) {
|
|
|
191
218
|
}
|
|
192
219
|
await removeLegacyArtifact(ctx);
|
|
193
220
|
const scrubbed = await scrubGeneratedPlaintextCredentials(ctx);
|
|
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'}` : ''}` };
|
|
221
|
+
return { ...result, changed: result.changed || secret.changed || userAgentSecret.changed || scrubbed > 0, message: `configured native Responses (${codexIds.length}), OpenAI Chat Completions (${deepSeekIds.length}), and Anthropic Messages (${claudeIds.length}) providers with file-backed SecretRefs${scrubbed ? `; scrubbed ${scrubbed} generated credential ${scrubbed === 1 ? 'copy' : 'copies'}` : ''}` };
|
|
195
222
|
}
|
|
196
223
|
export async function verify(ctx) {
|
|
197
224
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
198
225
|
const config = await fs.readJson(file);
|
|
199
226
|
const codex = config?.models?.providers?.[codexProviderName];
|
|
227
|
+
const deepSeek = config?.models?.providers?.[deepSeekProviderName];
|
|
200
228
|
const claude = config?.models?.providers?.[claudeProviderName];
|
|
201
229
|
const secretProvider = config?.secrets?.providers?.[secretProviderId];
|
|
202
230
|
const userAgentProvider = config?.secrets?.providers?.[userAgentProviderId];
|
|
203
|
-
const codexIds =
|
|
231
|
+
const codexIds = openAiResponsesModelIds(ctx);
|
|
232
|
+
const deepSeekIds = deepSeekChatModelIds(ctx);
|
|
204
233
|
const claudeIds = anthropicModelIds(ctx);
|
|
205
234
|
const valid = (provider, baseUrl, api, ids) => {
|
|
206
235
|
const userAgent = provider?.request?.headers?.['User-Agent'];
|
|
@@ -211,7 +240,11 @@ export async function verify(ctx) {
|
|
|
211
240
|
&& userAgent?.source === userAgentReference.source && userAgent?.provider === userAgentReference.provider
|
|
212
241
|
&& userAgent?.id === userAgentReference.id && configured.size === ids.length && ids.every(model => configured.has(model));
|
|
213
242
|
};
|
|
243
|
+
const deepSeekModels = Array.isArray(deepSeek?.models) ? deepSeek.models : [];
|
|
244
|
+
const deepSeekValid = valid(deepSeek, openAiBaseUrl(ctx.baseUrl), 'openai-completions', deepSeekIds)
|
|
245
|
+
&& deepSeekModels.every((model) => model?.compat?.supportsTools === false);
|
|
214
246
|
if ((codexIds.length ? !valid(codex, openAiBaseUrl(ctx.baseUrl), 'openai-responses', codexIds) : Boolean(codex))
|
|
247
|
+
|| (deepSeekIds.length ? !deepSeekValid : Boolean(deepSeek))
|
|
215
248
|
|| (claudeIds.length ? !valid(claude, anthropicOrigin(ctx.baseUrl), 'anthropic-messages', claudeIds) : Boolean(claude))
|
|
216
249
|
|| secretProvider?.source !== 'file' || secretProvider?.path !== secretConfigPath || secretProvider?.mode !== 'singleValue'
|
|
217
250
|
|| userAgentProvider?.source !== 'file' || userAgentProvider?.path !== userAgentConfigPath || userAgentProvider?.mode !== 'singleValue') {
|
|
@@ -221,7 +254,7 @@ export async function verify(ctx) {
|
|
|
221
254
|
await verifyClientSecret(userAgentPath(ctx.home), userAgentContext(ctx));
|
|
222
255
|
if (config?.models?.providers?.[legacyProviderName])
|
|
223
256
|
throw new Error('OpenClaw still contains the obsolete mixed ScaleMax provider.');
|
|
224
|
-
return { id, name, changed: false, path: file, message: `verified ${codexIds.length}
|
|
257
|
+
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Responses, ${deepSeekIds.length} DeepSeek Chat Completions, and ${claudeIds.length} Claude models` };
|
|
225
258
|
}
|
|
226
259
|
export async function remove(ctx) {
|
|
227
260
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -232,7 +265,7 @@ export async function remove(ctx) {
|
|
|
232
265
|
}
|
|
233
266
|
const config = await fs.readJson(file).catch(() => undefined);
|
|
234
267
|
const providerRoot = asRecord(config?.models?.providers);
|
|
235
|
-
const managedNames = [legacyProviderName, codexProviderName, claudeProviderName].filter(providerName => {
|
|
268
|
+
const managedNames = [legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName].filter(providerName => {
|
|
236
269
|
const provider = asRecord(providerRoot[providerName]);
|
|
237
270
|
return [anthropicOrigin(ctx.baseUrl), openAiBaseUrl(ctx.baseUrl)].includes(String(provider.baseUrl || ''));
|
|
238
271
|
});
|
package/dist/configure/paths.js
CHANGED
|
@@ -10,6 +10,7 @@ export function clientConfigPath(id, home, platform, env = process.env) {
|
|
|
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
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');
|
|
13
|
+
case 'continue': return path.join(home, '.continue', 'config.yaml');
|
|
13
14
|
case 'trae-solo': return path.join(home, '.scalemax', 'trae-solo.setup.json');
|
|
14
15
|
case 'claude': return path.join(home, '.claude', 'settings.json');
|
|
15
16
|
case 'opencode': return path.join(linux, 'opencode', 'opencode.json');
|
|
@@ -26,6 +27,7 @@ export function installHints(id, home, platform, env = process.env) {
|
|
|
26
27
|
case 'codex': return [codexDir(home), path.join(linux, 'codex')];
|
|
27
28
|
case 'cursor': return [platform === 'darwin' ? '/Applications/Cursor.app' : path.join(app, 'Cursor'), path.join(linux, 'Cursor')];
|
|
28
29
|
case 'roo-code': return [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
|
|
30
|
+
case 'continue': return [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
|
|
29
31
|
case 'trae-solo': return [platform === 'darwin' ? '/Applications/Trae.app' : path.join(app, 'Trae'), path.join(linux, 'Trae')];
|
|
30
32
|
case 'claude': return [path.join(home, '.claude')];
|
|
31
33
|
case 'opencode': return [path.join(linux, 'opencode')];
|
|
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
4
|
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
5
5
|
import { clientConfigPath } from './paths.js';
|
|
6
|
-
import { anthropicModelIds, openAiBaseUrl,
|
|
6
|
+
import { anthropicModelIds, isDeepSeekChatModelId, openAiBaseUrl, openAiResponsesModelIds, preferredAnthropicModel, preferredChatCompletionsModel, preferredOpenAiResponsesModel } from './models.js';
|
|
7
7
|
import { isAnthropicModel } from '../utils/api.js';
|
|
8
8
|
const id = 'roo-code';
|
|
9
9
|
const name = 'Roo Code (VS Code)';
|
|
@@ -11,12 +11,16 @@ const importSetting = 'roo-cline.autoImportSettingsPath';
|
|
|
11
11
|
const importSettingValue = '~/.scalemax/roo-code-import.json';
|
|
12
12
|
const codexProfileName = 'ScaleMax Codex';
|
|
13
13
|
const claudeProfileName = 'ScaleMax Claude';
|
|
14
|
+
const deepSeekProfileName = 'ScaleMax DeepSeek';
|
|
14
15
|
const codexProfileId = 'scalemax-codex';
|
|
15
16
|
const claudeProfileId = 'scalemax-claude';
|
|
17
|
+
const deepSeekProfileId = 'scalemax-deepseek';
|
|
16
18
|
function importPath(home) { return path.join(home, '.scalemax', 'roo-code-import.json'); }
|
|
17
19
|
function fallbackProfileFor(ctx) {
|
|
18
20
|
const model = preferredChatCompletionsModel(ctx);
|
|
19
21
|
const metadata = (ctx.models || []).find(candidate => candidate.id === model) || { id: model };
|
|
22
|
+
if (isDeepSeekChatModelId(model))
|
|
23
|
+
throw new Error('Roo Code requires task-completion tool calling, which DeepSeek V4 Flash does not expose. Use Continue Chat/Edit in VS Code instead. No configuration was changed.');
|
|
20
24
|
return isAnthropicModel(metadata)
|
|
21
25
|
? { name: claudeProfileName, id: claudeProfileId, model }
|
|
22
26
|
: { name: codexProfileName, id: codexProfileId, model };
|
|
@@ -24,8 +28,9 @@ function fallbackProfileFor(ctx) {
|
|
|
24
28
|
function profilesFor(ctx) {
|
|
25
29
|
const scope = ctx.providerScope || 'both';
|
|
26
30
|
const profiles = [];
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
const codexIds = openAiResponsesModelIds(ctx);
|
|
32
|
+
if (scope !== 'anthropic' && codexIds.length) {
|
|
33
|
+
profiles.push({ name: codexProfileName, id: codexProfileId, model: preferredOpenAiResponsesModel(ctx) });
|
|
29
34
|
}
|
|
30
35
|
if (scope !== 'openai' && anthropicModelIds(ctx).length) {
|
|
31
36
|
profiles.push({ name: claudeProfileName, id: claudeProfileId, model: preferredAnthropicModel(ctx) });
|
|
@@ -144,7 +149,8 @@ export async function remove(ctx) {
|
|
|
144
149
|
const configs = profile?.providerProfiles?.apiConfigs;
|
|
145
150
|
const managed = [
|
|
146
151
|
configs?.[codexProfileName]?.id === codexProfileId,
|
|
147
|
-
configs?.[claudeProfileName]?.id === claudeProfileId
|
|
152
|
+
configs?.[claudeProfileName]?.id === claudeProfileId,
|
|
153
|
+
configs?.[deepSeekProfileName]?.id === deepSeekProfileId
|
|
148
154
|
].some(Boolean);
|
|
149
155
|
if (managed) {
|
|
150
156
|
await backupFile(profileFile, ctx.home);
|
|
@@ -11,14 +11,15 @@ export function jsonConfig(client, apiKey, baseUrl, extra = {}) {
|
|
|
11
11
|
}, null, 2) + '\n';
|
|
12
12
|
}
|
|
13
13
|
export function continueConfig(apiKey, baseUrl) {
|
|
14
|
-
const models = ['gpt-5.6-sol', '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'];
|
|
14
|
+
const models = ['gpt-5.6-sol', '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', 'deepseek-v4-flash'];
|
|
15
15
|
return JSON.stringify({
|
|
16
16
|
models: models.map(model => ({
|
|
17
|
-
title: `ScaleMax ${model}`,
|
|
17
|
+
title: model === 'deepseek-v4-flash' ? 'ScaleMax DeepSeek V4 Flash' : `ScaleMax ${model}`,
|
|
18
18
|
provider: 'openai',
|
|
19
19
|
model,
|
|
20
20
|
apiBase: baseUrl,
|
|
21
|
-
apiKey
|
|
21
|
+
apiKey,
|
|
22
|
+
...(model === 'deepseek-v4-flash' ? { useResponsesApi: false } : {})
|
|
22
23
|
})),
|
|
23
24
|
tabAutocompleteModel: { title: 'ScaleMax gpt-5.4-mini', provider: 'openai', model: 'gpt-5.4-mini', apiBase: baseUrl, apiKey },
|
|
24
25
|
contextProviders: [{ name: 'code' }, { name: 'docs' }, { name: 'diff' }, { name: 'terminal' }]
|
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'], 'roo-code': ['code'], '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'], continue: ['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) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { commandExists } from '../utils/exec.js';
|
|
4
|
+
import { clientConfigPath } from '../configure/paths.js';
|
|
5
|
+
const id = 'continue';
|
|
6
|
+
const name = 'Continue (VS Code)';
|
|
7
|
+
const extensionPattern = /^continue\.continue-/i;
|
|
8
|
+
async function extensionPaths(home) {
|
|
9
|
+
const roots = [path.join(home, '.vscode', 'extensions'), path.join(home, '.vscode-insiders', 'extensions')];
|
|
10
|
+
const hits = [];
|
|
11
|
+
for (const root of roots) {
|
|
12
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
13
|
+
for (const entry of entries)
|
|
14
|
+
if (entry.isDirectory() && extensionPattern.test(entry.name))
|
|
15
|
+
hits.push(path.join(root, entry.name));
|
|
16
|
+
}
|
|
17
|
+
return hits;
|
|
18
|
+
}
|
|
19
|
+
export async function detect(ctx) {
|
|
20
|
+
const paths = await extensionPaths(ctx.home);
|
|
21
|
+
const executable = await commandExists('code', ctx.env);
|
|
22
|
+
if (executable)
|
|
23
|
+
paths.unshift('command:code');
|
|
24
|
+
const config = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
25
|
+
const envFile = path.join(ctx.home, '.continue', '.env');
|
|
26
|
+
const configText = await fs.readFile(config, 'utf8').catch(() => '');
|
|
27
|
+
const envText = await fs.readFile(envFile, 'utf8').catch(() => '');
|
|
28
|
+
const configured = configText.includes('ScaleMax DeepSeek V4 Flash')
|
|
29
|
+
&& configText.includes('deepseek-v4-flash')
|
|
30
|
+
&& configText.includes('api.scalemax.pro')
|
|
31
|
+
&& /^SCALEMAX_API_KEY=/m.test(envText);
|
|
32
|
+
if (configured)
|
|
33
|
+
paths.push(config, envFile);
|
|
34
|
+
const installed = paths.some(entry => extensionPattern.test(path.basename(entry)));
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
name,
|
|
38
|
+
installed,
|
|
39
|
+
configured,
|
|
40
|
+
executable,
|
|
41
|
+
installable: executable,
|
|
42
|
+
reason: installed ? undefined : executable ? 'Continue extension was not detected' : 'VS Code and Continue were not detected',
|
|
43
|
+
paths
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type ClientId = 'codex' | 'cursor' | 'roo-code' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
|
|
2
|
-
export type ProviderScope = 'openai' | 'anthropic' | 'both';
|
|
1
|
+
export type ClientId = 'codex' | 'cursor' | 'roo-code' | 'continue' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
|
|
2
|
+
export type ProviderScope = 'openai' | 'anthropic' | 'both' | 'deepseek-v4-flash';
|
|
3
3
|
export interface ClientDefinition {
|
|
4
4
|
id: ClientId;
|
|
5
5
|
name: string;
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const DEEPSEEK_V4_FLASH_SCOPE: "deepseek-v4-flash";
|
|
2
|
+
export type ProviderScope = 'openai' | 'anthropic' | 'both' | typeof DEEPSEEK_V4_FLASH_SCOPE;
|
|
2
3
|
export interface ModelInfo {
|
|
3
4
|
id: string;
|
|
4
5
|
object?: string;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SCALEMAX_BASE_URL, SCALEMAX_TOKEN_BASE_URL } from '../constants.js';
|
|
2
2
|
import { redactApiKey, sanitizeApiKey } from './apiKey.js';
|
|
3
|
+
export const DEEPSEEK_V4_FLASH_SCOPE = 'deepseek-v4-flash';
|
|
3
4
|
// Keep only aliases that are no longer offered. Direct GPT-5.6 Sol is an
|
|
4
5
|
// active Codex model and must remain available to all generated clients.
|
|
5
6
|
export const RETIRED_MODEL_IDS = new Set(['fable-5-ultra']);
|
|
@@ -13,8 +14,27 @@ export function isAnthropicModel(model) {
|
|
|
13
14
|
return model.id.startsWith('claude-') && !model.id.startsWith('claude-fable-');
|
|
14
15
|
}
|
|
15
16
|
export function filterModelsForScope(models, scope) {
|
|
16
|
-
return models.filter(model =>
|
|
17
|
-
|
|
17
|
+
return models.filter(model => {
|
|
18
|
+
if (isRetiredModelId(model.id))
|
|
19
|
+
return false;
|
|
20
|
+
const isDeepSeekV4Flash = model.id.trim().toLowerCase() === DEEPSEEK_V4_FLASH_SCOPE;
|
|
21
|
+
// This scope is an exact-model entitlement, not an OpenAI-compatible
|
|
22
|
+
// provider family. Mirror the gateway's fail-closed authorization so an
|
|
23
|
+
// unexpectedly broad catalog can never widen a customer's key locally.
|
|
24
|
+
if (isDeepSeekV4Flash)
|
|
25
|
+
return scope === DEEPSEEK_V4_FLASH_SCOPE;
|
|
26
|
+
if (scope === DEEPSEEK_V4_FLASH_SCOPE)
|
|
27
|
+
return false;
|
|
28
|
+
return scope === 'both' || (scope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function providerScope(value) {
|
|
32
|
+
return value === 'openai'
|
|
33
|
+
|| value === 'anthropic'
|
|
34
|
+
|| value === 'both'
|
|
35
|
+
|| value === DEEPSEEK_V4_FLASH_SCOPE
|
|
36
|
+
? value
|
|
37
|
+
: null;
|
|
18
38
|
}
|
|
19
39
|
async function friendlyError(res) {
|
|
20
40
|
let body = '';
|
|
@@ -112,7 +132,11 @@ async function validateOnSurface(apiKey, baseUrl) {
|
|
|
112
132
|
if (!authCheck.ok)
|
|
113
133
|
return { ok: false, models: [], error: redactApiKey(await friendlyError(authCheck), apiKey), surfaceMismatch: authCheck.status === 401 };
|
|
114
134
|
const usage = await authCheck.json().catch(() => ({}));
|
|
115
|
-
const
|
|
135
|
+
const rawScope = usage?.key?.provider_scope ?? 'both';
|
|
136
|
+
const parsedScope = providerScope(rawScope);
|
|
137
|
+
if (!parsedScope)
|
|
138
|
+
return { ok: false, models: [], error: 'ScaleMax returned an unsupported API-key model scope.' };
|
|
139
|
+
const keyMeta = { provider_scope: parsedScope, mode: usage?.key?.mode, prefix: usage?.key?.prefix, label: usage?.key?.label };
|
|
116
140
|
if (!modelsRes.ok)
|
|
117
141
|
return { ok: false, models: [], key: keyMeta, error: redactApiKey(await friendlyError(modelsRes), apiKey) };
|
|
118
142
|
const data = await modelsRes.json();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scalemax",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Universal ScaleMax installer for AI coding clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"installer",
|
|
54
54
|
"codex",
|
|
55
55
|
"cursor",
|
|
56
|
+
"continue",
|
|
56
57
|
"trae-solo",
|
|
57
58
|
"claude-code",
|
|
58
59
|
"opencode",
|