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 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 both families, and verify the import handoff. 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.
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 the Codex/OpenAI-only profile into its SecretStorage. CLI verification intentionally reports the handoff as verified, not the extension runtime, until that reload occurs.
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 and TRAE remain OpenAI-only.
48
- const crossProtocolClients = new Set(['claude', 'cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
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?: 'openai' | 'anthropic' | 'both';
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: 'roo-code', pattern: /\b(?:roo(?:\s+code)?|roo\s+(?:extension|plugin)|vscode\s+roo|vs\s+code\s+roo|vs\s*code\s+extension|visual\s+studio\s+code\s+extension)\b/i },
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 || requestedScope === 'both')
817
+ if (!requestedScope)
815
818
  return catalog;
816
- return catalog.filter(model => requestedScope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
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(['claude', 'cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
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 an OpenAI-compatible model, but this key exposes only Claude models. No configuration was changed.');
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' ? ' Reload VS Code once so Roo Code imports the verified ScaleMax profile.' : '';
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 both. 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.`
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',
@@ -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 { preferredOpenAiModel } from './models.js';
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 preferredOpenAiModel(ctx);
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
+ }
@@ -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, openAiModelIds, preferredAnthropicModel, preferredOpenAiModel } from './models.js';
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 = openAiModelIds(ctx);
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 ? preferredOpenAiModel(ctx) : preferredAnthropicModel(ctx);
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 = openAiModelIds(ctx);
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
@@ -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, openAiModelIds, preferredAnthropicModel, preferredOpenAiModel } from './models.js';
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 = openAiModelIds(ctx);
104
+ const codexIds = openAiResponsesModelIds(ctx);
105
+ const deepSeekIds = deepSeekChatModelIds(ctx);
100
106
  const claudeIds = anthropicModelIds(ctx);
101
- const codexPrimary = codexIds.length ? `${codexProviderName}/${preferredOpenAiModel(ctx)}` : undefined;
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 = keepClaude ? claudePrimary || codexPrimary : codexPrimary || claudePrimary;
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 = openAiModelIds(ctx);
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} Codex/OpenAI and ${claudeIds.length} Claude models` };
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
  });
@@ -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, openAiModelIds, preferredAnthropicModel, preferredChatCompletionsModel, preferredOpenAiModel } from './models.js';
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
- if (scope !== 'anthropic' && openAiModelIds(ctx).length) {
28
- profiles.push({ name: codexProfileName, id: codexProfileId, model: preferredOpenAiModel(ctx) });
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' }]
@@ -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,2 @@
1
+ import { DetectContext, DetectionResult } from '../types/client.js';
2
+ export declare function detect(ctx: DetectContext): Promise<DetectionResult>;
@@ -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
+ }
@@ -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;
@@ -1,4 +1,5 @@
1
- export type ProviderScope = 'openai' | 'anthropic' | 'both';
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 => !isRetiredModelId(model.id)
17
- && (scope === 'both' || (scope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model))));
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 keyMeta = { provider_scope: usage?.key?.provider_scope || 'both', mode: usage?.key?.mode, prefix: usage?.key?.prefix, label: usage?.key?.label };
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.27",
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",