scalemax 0.1.31 → 0.1.32
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 +1 -1
- package/dist/commands/configure.js +14 -8
- package/dist/commands/helper-bot.js +29 -13
- package/dist/configure/continue.js +20 -18
- package/dist/configure/hermes.js +28 -12
- package/dist/configure/models.d.ts +23 -1
- package/dist/configure/models.js +31 -11
- package/dist/configure/openclaw.js +53 -29
- package/dist/configure/roo-code.js +71 -24
- package/dist/configure/trae-solo.js +3 -1
- package/dist/types/client.d.ts +11 -1
- package/dist/utils/api.d.ts +10 -1
- package/dist/utils/api.js +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ ScaleMax merges a named `scalemax` provider into `~/.config/opencode/opencode.js
|
|
|
161
161
|
|
|
162
162
|
### OpenClaw
|
|
163
163
|
|
|
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 always isolated in the Chat Completions provider and never advertised through Responses. Its agent flags come from authenticated catalog discovery:
|
|
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 always isolated in the Chat Completions provider and never advertised through Responses. Its agent flags come only from authenticated catalog discovery: tools, forced `tool_choice`, reasoning, reasoning effort, streamed usage, and `reasoning_content` replay are each enabled only when the exact model advertises that capability. In particular, a tool-capable route is never forced to replay reasoning content unless `capabilities.reasoning_replay=true`; a fallback route remains explicitly tool-free. 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.
|
|
165
165
|
|
|
166
166
|
### Hermes
|
|
167
167
|
|
|
@@ -4,9 +4,9 @@ import ora from 'ora';
|
|
|
4
4
|
import { clients } from '../clients.js';
|
|
5
5
|
import { SCALEMAX_BASE_URL } from '../constants.js';
|
|
6
6
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
7
|
-
import { validateApiKey, isAnthropicModel } from '../utils/api.js';
|
|
7
|
+
import { validateApiKey, isAnthropicModel, isModelRouteAvailable } from '../utils/api.js';
|
|
8
8
|
import { homeDir } from '../utils/paths.js';
|
|
9
|
-
import {
|
|
9
|
+
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
10
10
|
export async function detectAll(home = homeDir()) {
|
|
11
11
|
const ctx = { home, platform: process.platform, env: process.env };
|
|
12
12
|
return Promise.all(clients.map(c => c.detect(ctx)));
|
|
@@ -41,6 +41,7 @@ async function maybeLaunchCodex(chosen, successful, baseUrl = SCALEMAX_BASE_URL)
|
|
|
41
41
|
console.log('\nIf Codex asks for authentication,\nit should already be configured to use ScaleMax.\n\nIf you still see the official login screen,\nthe configuration is incomplete or Codex rejected the generated auth/config files.\n');
|
|
42
42
|
}
|
|
43
43
|
export function clientAllowedForModels(clientId, models) {
|
|
44
|
+
models = models.filter(isModelRouteAvailable);
|
|
44
45
|
if (!models.length)
|
|
45
46
|
return false;
|
|
46
47
|
// These clients use either a native family-specific adapter or the gateway's
|
|
@@ -49,15 +50,18 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
49
50
|
// OpenAI-compatible Chat Completions.
|
|
50
51
|
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
51
52
|
if (clientId === 'continue')
|
|
52
|
-
return models.some(model =>
|
|
53
|
+
return models.some(model => isExactChatCompletionsModelId(model.id));
|
|
53
54
|
if (clientId === 'claude')
|
|
54
55
|
return models.some(isAnthropicModel);
|
|
55
56
|
if (clientId === 'codex')
|
|
56
|
-
return models.some(model => !isAnthropicModel(model) && !
|
|
57
|
-
if (clientId === 'hermes' && models.every(model =>
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
return models.some(model => !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
|
|
58
|
+
if (clientId === 'hermes' && models.every(model => isExactChatCompletionsModelId(model.id)))
|
|
59
|
+
return models.some(model => model.capabilities?.tools === true);
|
|
60
|
+
// Roo is an agent, not a text-only chat surface. Its task loop depends on
|
|
61
|
+
// native function calls for every model family, so offer it only when the
|
|
62
|
+
// authenticated catalog confirms at least one viable route.
|
|
63
|
+
if (clientId === 'roo-code')
|
|
64
|
+
return models.some(model => model.capabilities?.tools === true);
|
|
61
65
|
if (crossProtocolClients.has(clientId))
|
|
62
66
|
return true;
|
|
63
67
|
return models.some(model => !isAnthropicModel(model));
|
|
@@ -65,6 +69,8 @@ export function clientAllowedForModels(clientId, models) {
|
|
|
65
69
|
function scopeHint(scope) {
|
|
66
70
|
if (scope === 'deepseek-v4-flash')
|
|
67
71
|
return 'DeepSeek V4 Flash only';
|
|
72
|
+
if (scope === 'glm-5.2')
|
|
73
|
+
return 'GLM 5.2 only';
|
|
68
74
|
return scope === 'both' ? 'OpenAI + Claude' : scope === 'anthropic' ? 'Claude only' : 'OpenAI only';
|
|
69
75
|
}
|
|
70
76
|
export async function configureCommand(opts = {}) {
|
|
@@ -4,13 +4,14 @@ import { createInterface } from 'node:readline/promises';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { clients } from '../clients.js';
|
|
6
6
|
import { helperDocsContext } from './helper-docs.js';
|
|
7
|
-
import { createChatCompletion, isAnthropicModel } from '../utils/api.js';
|
|
7
|
+
import { createChatCompletion, filterModelsForScope, isAnthropicModel } from '../utils/api.js';
|
|
8
8
|
import { redactApiKey } from '../utils/apiKey.js';
|
|
9
9
|
import { loadApiKey } from '../utils/keyStore.js';
|
|
10
10
|
import { homeDir } from '../utils/paths.js';
|
|
11
11
|
import { validateApiKey } from '../utils/api.js';
|
|
12
12
|
import { commandExists } from '../utils/exec.js';
|
|
13
13
|
import { installClaudeGithubTools } from '../configure/claude-tools.js';
|
|
14
|
+
import { isExactChatCompletionsModelId } from '../configure/models.js';
|
|
14
15
|
export const SCALEMAX_HELPER_REFUSAL = `I am the official ScaleMax Helper.
|
|
15
16
|
|
|
16
17
|
I can only assist with installing, configuring, repairing, verifying, and troubleshooting ScaleMax.
|
|
@@ -800,6 +801,8 @@ function helperActionTarget(question) {
|
|
|
800
801
|
return undefined;
|
|
801
802
|
}
|
|
802
803
|
function requestedProviderScope(question, target) {
|
|
804
|
+
if (/\bglm[-\s]*5[._\s]*2\b/i.test(question))
|
|
805
|
+
return 'glm-5.2';
|
|
803
806
|
if (/\bdeepseek(?:[-\s]+v4[-\s]+flash)?\b/i.test(question))
|
|
804
807
|
return 'deepseek-v4-flash';
|
|
805
808
|
if (/\b(?:both|openai\s*\+\s*claude|claude\s*\+\s*openai)\b/i.test(question))
|
|
@@ -811,35 +814,48 @@ function requestedProviderScope(question, target) {
|
|
|
811
814
|
return undefined;
|
|
812
815
|
}
|
|
813
816
|
function actionModels(connection, requestedScope) {
|
|
814
|
-
const
|
|
817
|
+
const rawCatalog = connection.modelCatalog?.length
|
|
815
818
|
? connection.modelCatalog
|
|
816
819
|
: (connection.availableModels || []).map(id => ({ id }));
|
|
820
|
+
const entitlement = connection.providerScope || 'both';
|
|
821
|
+
// Always begin with the key's verified entitlement. Natural-language intent
|
|
822
|
+
// must never widen an exact-model key into an OpenAI/Claude family key.
|
|
823
|
+
const catalog = filterModelsForScope(rawCatalog, entitlement);
|
|
817
824
|
if (!requestedScope)
|
|
818
825
|
return catalog;
|
|
819
|
-
|
|
820
|
-
|
|
826
|
+
const exactEntitlement = entitlement === 'deepseek-v4-flash' || entitlement === 'glm-5.2';
|
|
827
|
+
if (exactEntitlement && requestedScope !== entitlement)
|
|
828
|
+
return [];
|
|
829
|
+
if (requestedScope === 'deepseek-v4-flash' || requestedScope === 'glm-5.2') {
|
|
830
|
+
return requestedScope === entitlement
|
|
831
|
+
? catalog.filter(model => model.id.trim().toLowerCase() === requestedScope)
|
|
832
|
+
: [];
|
|
821
833
|
}
|
|
822
834
|
if (requestedScope === 'both') {
|
|
823
|
-
return catalog.filter(model => model.id
|
|
835
|
+
return catalog.filter(model => !isExactChatCompletionsModelId(model.id));
|
|
824
836
|
}
|
|
825
837
|
return catalog.filter(model => requestedScope === 'anthropic'
|
|
826
838
|
? isAnthropicModel(model)
|
|
827
|
-
: !isAnthropicModel(model) && model.id
|
|
839
|
+
: !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
|
|
828
840
|
}
|
|
829
841
|
function helperClientAllowedForModels(clientId, models) {
|
|
830
842
|
if (!models.length)
|
|
831
843
|
return false;
|
|
832
844
|
const crossProtocolClients = new Set(['cursor', 'roo-code', 'opencode', 'openclaw', 'hermes', 'cherry-studio', 'api-code']);
|
|
833
|
-
const
|
|
845
|
+
const onlyExactChat = models.every(model => isExactChatCompletionsModelId(model.id));
|
|
834
846
|
if (clientId === 'continue')
|
|
835
|
-
return
|
|
847
|
+
return onlyExactChat;
|
|
836
848
|
if (clientId === 'claude')
|
|
837
849
|
return models.some(isAnthropicModel);
|
|
838
|
-
if (
|
|
850
|
+
if (clientId === 'codex' && onlyExactChat)
|
|
839
851
|
return false;
|
|
852
|
+
if (clientId === 'hermes' && onlyExactChat)
|
|
853
|
+
return models.some(model => model.capabilities?.tools === true);
|
|
854
|
+
if (clientId === 'roo-code')
|
|
855
|
+
return models.some(model => model.capabilities?.tools === true);
|
|
840
856
|
if (crossProtocolClients.has(clientId))
|
|
841
857
|
return true;
|
|
842
|
-
return models.some(model => !isAnthropicModel(model) && model.id
|
|
858
|
+
return models.some(model => !isAnthropicModel(model) && !isExactChatCompletionsModelId(model.id));
|
|
843
859
|
}
|
|
844
860
|
function hasRooCodeExtension(paths) {
|
|
845
861
|
return paths.some(candidate => /^rooveterinaryinc\.roo-cline-/i.test(path.basename(candidate)));
|
|
@@ -923,8 +939,8 @@ async function installRooCodeExtension(ctx, client, write) {
|
|
|
923
939
|
async function runOneHelperClientAction(kind, client, connection, home, requestedScope, write) {
|
|
924
940
|
const models = actionModels(connection, requestedScope);
|
|
925
941
|
if (!models.length)
|
|
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
|
|
942
|
+
throw new Error(`The authenticated key does not expose ${requestedScope === 'anthropic' ? 'Claude' : requestedScope === 'openai' ? 'OpenAI' : requestedScope === 'deepseek-v4-flash' ? 'DeepSeek V4 Flash' : requestedScope === 'glm-5.2' ? 'GLM 5.2' : 'compatible'} models for ${client.name}. No configuration was changed.`);
|
|
943
|
+
if (client.id === 'codex' && models.every(model => isAnthropicModel(model) || isExactChatCompletionsModelId(model.id)))
|
|
928
944
|
throw new Error('Codex requires a Responses-capable model, but this key exposes only Claude or Chat-Completions-only models. No configuration was changed.');
|
|
929
945
|
if (client.id === 'cursor' && connection.platform !== 'darwin')
|
|
930
946
|
throw new Error(`Automatic Cursor configuration is currently verified on macOS only. ${helperPlatformName(connection.platform)} was not modified.`);
|
|
@@ -934,7 +950,7 @@ async function runOneHelperClientAction(kind, client, connection, home, requeste
|
|
|
934
950
|
env: { ...process.env },
|
|
935
951
|
apiKey: connection.apiKey,
|
|
936
952
|
baseUrl: connection.baseUrl,
|
|
937
|
-
providerScope:
|
|
953
|
+
providerScope: connection.providerScope || 'both',
|
|
938
954
|
models,
|
|
939
955
|
interactive: false,
|
|
940
956
|
allowAppModification: false
|
|
@@ -5,11 +5,9 @@ import { parseDocument } from 'yaml';
|
|
|
5
5
|
import { atomicWrite, backupFile } from '../utils/fs.js';
|
|
6
6
|
import { commandExists } from '../utils/exec.js';
|
|
7
7
|
import { clientConfigPath } from './paths.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isExactChatCompletionsModelId, modelName as displayModelName } from './models.js';
|
|
9
9
|
const id = 'continue';
|
|
10
10
|
const name = 'Continue (VS Code)';
|
|
11
|
-
const modelName = 'ScaleMax DeepSeek V4 Flash';
|
|
12
|
-
const modelId = 'deepseek-v4-flash';
|
|
13
11
|
const envKey = 'SCALEMAX_API_KEY';
|
|
14
12
|
const envMarker = '# ScaleMax managed Continue API key';
|
|
15
13
|
const userAgent = 'ScaleMax-Continue/1.0';
|
|
@@ -29,9 +27,10 @@ function parseYaml(text) {
|
|
|
29
27
|
return doc;
|
|
30
28
|
}
|
|
31
29
|
function isManagedModel(model) {
|
|
32
|
-
return model?.name ===
|
|
30
|
+
return typeof model?.name === 'string'
|
|
31
|
+
&& model.name.startsWith('ScaleMax ')
|
|
33
32
|
&& ['azure', 'openai'].includes(model?.provider)
|
|
34
|
-
&& model?.model
|
|
33
|
+
&& isExactChatCompletionsModelId(String(model?.model || ''))
|
|
35
34
|
&& typeof model?.apiBase === 'string'
|
|
36
35
|
&& /^https:\/\/api\.scalemax\.pro\/(?:token\/)?v1\/?$/i.test(model.apiBase);
|
|
37
36
|
}
|
|
@@ -47,10 +46,11 @@ function withoutManagedEnv(text) {
|
|
|
47
46
|
kept.pop();
|
|
48
47
|
return kept.length ? `${kept.join('\n')}\n` : '';
|
|
49
48
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
function exactChatModel(ctx) {
|
|
50
|
+
const model = (ctx.models || []).find(candidate => isExactChatCompletionsModelId(candidate.id));
|
|
51
|
+
if (!model)
|
|
52
|
+
throw new Error('Continue automatic setup requires an administrator-issued exact Chat Completions model key. No configuration was changed.');
|
|
53
|
+
return model.id;
|
|
54
54
|
}
|
|
55
55
|
export async function install(ctx) {
|
|
56
56
|
const extensionRoots = [path.join(ctx.home, '.vscode', 'extensions'), path.join(ctx.home, '.vscode-insiders', 'extensions')];
|
|
@@ -67,7 +67,8 @@ export async function install(ctx) {
|
|
|
67
67
|
return { id, name, changed: true, message: 'official Continue extension installed' };
|
|
68
68
|
}
|
|
69
69
|
export async function configure(ctx) {
|
|
70
|
-
|
|
70
|
+
const modelId = exactChatModel(ctx);
|
|
71
|
+
const modelName = `ScaleMax ${displayModelName(ctx, modelId)}`;
|
|
71
72
|
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
72
73
|
const secrets = envPath(ctx.home);
|
|
73
74
|
const originalConfig = await fs.readFile(file, 'utf8').catch(() => '');
|
|
@@ -77,7 +78,7 @@ export async function configure(ctx) {
|
|
|
77
78
|
const existingModels = Array.isArray(data.models) ? data.models.filter((model) => !isManagedModel(model)) : [];
|
|
78
79
|
const model = {
|
|
79
80
|
name: modelName,
|
|
80
|
-
provider: '
|
|
81
|
+
provider: 'openai',
|
|
81
82
|
model: modelId,
|
|
82
83
|
apiBase: ctx.baseUrl.replace(/\/$/, ''),
|
|
83
84
|
apiKey: `\${{ secrets.${envKey} }}`,
|
|
@@ -99,7 +100,7 @@ export async function configure(ctx) {
|
|
|
99
100
|
if (originalConfig === nextConfig && originalEnv === nextEnv) {
|
|
100
101
|
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
101
102
|
await fs.chmod(secrets, 0o600).catch(() => undefined);
|
|
102
|
-
return { id, name, changed: false, path: file, message:
|
|
103
|
+
return { id, name, changed: false, path: file, message: `${displayModelName(ctx, modelId)} Chat/Edit profile is already current` };
|
|
103
104
|
}
|
|
104
105
|
const backups = [await backupFile(file, ctx.home), await backupFile(secrets, ctx.home)].filter(Boolean);
|
|
105
106
|
try {
|
|
@@ -117,16 +118,17 @@ export async function configure(ctx) {
|
|
|
117
118
|
await fs.remove(secrets).catch(() => undefined);
|
|
118
119
|
throw error;
|
|
119
120
|
}
|
|
120
|
-
return { id, name, changed: true, path: file, message: `configured a
|
|
121
|
+
return { id, name, changed: true, path: file, message: `configured a ScaleMax ${displayModelName(ctx, modelId)} Chat/Edit profile with a private credential${backups.length ? ' (backups created)' : ''}` };
|
|
121
122
|
}
|
|
122
123
|
export async function verify(ctx) {
|
|
123
|
-
|
|
124
|
+
const modelId = exactChatModel(ctx);
|
|
125
|
+
const modelName = `ScaleMax ${displayModelName(ctx, modelId)}`;
|
|
124
126
|
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
125
127
|
const secrets = envPath(ctx.home);
|
|
126
128
|
const doc = parseYaml(await fs.readFile(file, 'utf8'));
|
|
127
129
|
const data = asRecord(doc.toJS());
|
|
128
130
|
const model = Array.isArray(data.models)
|
|
129
|
-
? data.models.find((candidate) => isManagedModel(candidate) && candidate.provider === '
|
|
131
|
+
? data.models.find((candidate) => isManagedModel(candidate) && candidate.name === modelName && candidate.model === modelId && candidate.provider === 'openai')
|
|
130
132
|
: undefined;
|
|
131
133
|
if (!model
|
|
132
134
|
|| model.apiBase !== ctx.baseUrl.replace(/\/$/, '')
|
|
@@ -136,7 +138,7 @@ export async function verify(ctx) {
|
|
|
136
138
|
|| model.requestOptions?.headers?.['User-Agent'] !== userAgent
|
|
137
139
|
|| model.env?.apiType !== 'openai-compatible'
|
|
138
140
|
|| model.defaultCompletionOptions?.maxTokens !== 4096) {
|
|
139
|
-
throw new Error(
|
|
141
|
+
throw new Error(`Continue ${displayModelName(ctx, modelId)} Chat/Edit configuration is incomplete.`);
|
|
140
142
|
}
|
|
141
143
|
const env = await fs.readFile(secrets, 'utf8').catch(() => '');
|
|
142
144
|
if (!env.includes(envMarker) || !env.includes(`${envKey}=${ctx.apiKey}`))
|
|
@@ -146,7 +148,7 @@ export async function verify(ctx) {
|
|
|
146
148
|
if (((await fs.stat(candidate)).mode & 0o077) !== 0)
|
|
147
149
|
throw new Error(`Continue file permissions are too broad: ${candidate}`);
|
|
148
150
|
}
|
|
149
|
-
return { id, name, changed: false, path: file, message:
|
|
151
|
+
return { id, name, changed: false, path: file, message: `verified ${displayModelName(ctx, modelId)} Chat/Edit configuration; reload VS Code to activate it` };
|
|
150
152
|
}
|
|
151
153
|
export async function remove(ctx) {
|
|
152
154
|
const file = clientConfigPath(id, ctx.home, ctx.platform, ctx.env);
|
|
@@ -181,5 +183,5 @@ export async function remove(ctx) {
|
|
|
181
183
|
await atomicWrite(secrets, nextEnv, 0o600, ctx.platform);
|
|
182
184
|
else
|
|
183
185
|
await fs.remove(secrets).catch(() => undefined);
|
|
184
|
-
return { id, name, changed: true, path: file, message: 'removed only the ScaleMax
|
|
186
|
+
return { id, name, changed: true, path: file, message: 'removed only the ScaleMax exact-model Chat/Edit profile and managed credential' };
|
|
185
187
|
}
|
package/dist/configure/hermes.js
CHANGED
|
@@ -12,7 +12,18 @@ const envMarker = '# ScaleMax managed Hermes API key';
|
|
|
12
12
|
const legacyProviderName = 'scalemax';
|
|
13
13
|
const codexProviderName = 'scalemax-codex';
|
|
14
14
|
const deepSeekProviderName = 'scalemax-deepseek';
|
|
15
|
+
const glmProviderName = 'scalemax-glm-5-2';
|
|
15
16
|
const claudeProviderName = 'scalemax-claude';
|
|
17
|
+
function exactChatProviderName(ids) {
|
|
18
|
+
return ids.length > 0 && ids.every(id => id.trim().toLowerCase() === 'glm-5.2')
|
|
19
|
+
? glmProviderName
|
|
20
|
+
: deepSeekProviderName;
|
|
21
|
+
}
|
|
22
|
+
function exactChatLabel(ids) {
|
|
23
|
+
return ids.length > 0 && ids.every(id => id.trim().toLowerCase() === 'glm-5.2')
|
|
24
|
+
? 'GLM 5.2'
|
|
25
|
+
: 'DeepSeek';
|
|
26
|
+
}
|
|
16
27
|
function asRecord(value) {
|
|
17
28
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
18
29
|
}
|
|
@@ -71,11 +82,13 @@ export async function configure(ctx) {
|
|
|
71
82
|
const codexIds = openAiResponsesModelIds(ctx);
|
|
72
83
|
const discoveredDeepSeekIds = deepSeekChatModelIds(ctx);
|
|
73
84
|
const deepSeekIds = toolCapableDeepSeekModelIds(ctx);
|
|
85
|
+
const chatProviderName = exactChatProviderName(discoveredDeepSeekIds);
|
|
86
|
+
const chatLabel = exactChatLabel(discoveredDeepSeekIds);
|
|
74
87
|
const claudeIds = anthropicModelIds(ctx);
|
|
75
|
-
if (ctx.providerScope === 'deepseek-v4-flash' && discoveredDeepSeekIds.length && !deepSeekIds.length) {
|
|
76
|
-
throw new Error(
|
|
88
|
+
if ((ctx.providerScope === 'deepseek-v4-flash' || ctx.providerScope === 'glm-5.2') && discoveredDeepSeekIds.length && !deepSeekIds.length) {
|
|
89
|
+
throw new Error(`Hermes agent mode requires tool calling, but the active ${chatLabel} route is text-only. Choose a tool-capable model. No configuration was changed.`);
|
|
77
90
|
}
|
|
78
|
-
const managedNames = new Set([legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName]);
|
|
91
|
+
const managedNames = new Set([legacyProviderName, codexProviderName, deepSeekProviderName, glmProviderName, claudeProviderName]);
|
|
79
92
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers.filter((provider) => !managedNames.has(provider?.name)) : [];
|
|
80
93
|
if (codexIds.length)
|
|
81
94
|
providers.push({
|
|
@@ -89,7 +102,7 @@ export async function configure(ctx) {
|
|
|
89
102
|
});
|
|
90
103
|
if (deepSeekIds.length)
|
|
91
104
|
providers.push({
|
|
92
|
-
name:
|
|
105
|
+
name: chatProviderName,
|
|
93
106
|
base_url: openAiBaseUrl(ctx.baseUrl),
|
|
94
107
|
key_env: envKey,
|
|
95
108
|
api_mode: 'chat_completions',
|
|
@@ -114,16 +127,17 @@ export async function configure(ctx) {
|
|
|
114
127
|
if (activatesScaleMax) {
|
|
115
128
|
const keepClaude = currentProvider === `custom:${claudeProviderName}` || claudeIds.includes(String(currentModel.default || ''));
|
|
116
129
|
const keepDeepSeek = currentProvider === `custom:${deepSeekProviderName}`
|
|
130
|
+
|| currentProvider === `custom:${glmProviderName}`
|
|
117
131
|
|| deepSeekIds.includes(String(currentModel.default || ''))
|
|
118
132
|
|| (!codexIds.length && !claudeIds.length && deepSeekIds.length > 0);
|
|
119
133
|
const activeName = keepDeepSeek
|
|
120
|
-
?
|
|
134
|
+
? chatProviderName
|
|
121
135
|
: codexIds.length && !keepClaude
|
|
122
136
|
? codexProviderName
|
|
123
|
-
: claudeIds.length ? claudeProviderName :
|
|
137
|
+
: claudeIds.length ? claudeProviderName : chatProviderName;
|
|
124
138
|
const activeModel = activeName === codexProviderName
|
|
125
139
|
? preferredOpenAiResponsesModel(ctx)
|
|
126
|
-
: activeName ===
|
|
140
|
+
: activeName === chatProviderName ? deepSeekIds[0] : preferredAnthropicModel(ctx);
|
|
127
141
|
doc.set('model', { ...currentModel, provider: `custom:${activeName}`, default: activeModel, max_tokens: 4096 });
|
|
128
142
|
}
|
|
129
143
|
const nextConfig = String(doc);
|
|
@@ -132,7 +146,7 @@ export async function configure(ctx) {
|
|
|
132
146
|
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
133
147
|
await fs.chmod(secrets, 0o600).catch(() => undefined);
|
|
134
148
|
await removeLegacyArtifact(ctx);
|
|
135
|
-
return { id, name, changed: false, path: file, message: `already current with ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length}
|
|
149
|
+
return { id, name, changed: false, path: file, message: `already current with ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length} ${chatLabel}, and ${claudeIds.length} Claude models and a private credential` };
|
|
136
150
|
}
|
|
137
151
|
const backups = [await backupFile(file, ctx.home), await backupFile(secrets, ctx.home)].filter(Boolean);
|
|
138
152
|
try {
|
|
@@ -151,7 +165,7 @@ export async function configure(ctx) {
|
|
|
151
165
|
await fs.remove(secrets).catch(() => undefined);
|
|
152
166
|
throw error;
|
|
153
167
|
}
|
|
154
|
-
return { id, name, changed: true, path: file, message: `configured family-specific ScaleMax providers with ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length}
|
|
168
|
+
return { id, name, changed: true, path: file, message: `configured family-specific ScaleMax providers with ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length} ${chatLabel}, and ${claudeIds.length} Claude models${backups.length ? ' (backups created)' : ''}` };
|
|
155
169
|
}
|
|
156
170
|
export async function verify(ctx) {
|
|
157
171
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -161,9 +175,11 @@ export async function verify(ctx) {
|
|
|
161
175
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers : [];
|
|
162
176
|
const codexIds = openAiResponsesModelIds(ctx);
|
|
163
177
|
const deepSeekIds = toolCapableDeepSeekModelIds(ctx);
|
|
178
|
+
const chatProviderName = exactChatProviderName(deepSeekIds);
|
|
179
|
+
const chatLabel = exactChatLabel(deepSeekIds);
|
|
164
180
|
const claudeIds = anthropicModelIds(ctx);
|
|
165
181
|
const codex = providers.find((candidate) => candidate?.name === codexProviderName);
|
|
166
|
-
const deepSeek = providers.find((candidate) => candidate?.name ===
|
|
182
|
+
const deepSeek = providers.find((candidate) => candidate?.name === chatProviderName);
|
|
167
183
|
const claude = providers.find((candidate) => candidate?.name === claudeProviderName);
|
|
168
184
|
const valid = (provider, baseUrl, apiMode, ids) => provider
|
|
169
185
|
&& provider.base_url === baseUrl && provider.key_env === envKey && provider.api_mode === apiMode
|
|
@@ -180,7 +196,7 @@ export async function verify(ctx) {
|
|
|
180
196
|
throw new Error('Hermes is missing authenticated ScaleMax models or credentials.');
|
|
181
197
|
if (String(data?.model?.provider || '').startsWith('custom:scalemax') && data?.model?.max_tokens !== 4096)
|
|
182
198
|
throw new Error('Hermes active ScaleMax model must use max_tokens 4096.');
|
|
183
|
-
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length}
|
|
199
|
+
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Codex/OpenAI, ${deepSeekIds.length} ${chatLabel}, and ${claudeIds.length} Claude models` };
|
|
184
200
|
}
|
|
185
201
|
export async function remove(ctx) {
|
|
186
202
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -191,7 +207,7 @@ export async function remove(ctx) {
|
|
|
191
207
|
const doc = parseYaml(text);
|
|
192
208
|
const data = asRecord(doc.toJS());
|
|
193
209
|
const providers = Array.isArray(data.custom_providers) ? data.custom_providers : [];
|
|
194
|
-
const managedNames = new Set([legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName]);
|
|
210
|
+
const managedNames = new Set([legacyProviderName, codexProviderName, deepSeekProviderName, glmProviderName, claudeProviderName]);
|
|
195
211
|
const managedProviders = providers.filter((provider) => managedNames.has(provider?.name)
|
|
196
212
|
&& (provider?.base_url === openAiBaseUrl(ctx.baseUrl) || provider?.base_url === anthropicOrigin(ctx.baseUrl)));
|
|
197
213
|
if (!managedProviders.length)
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { ConfigureContext } from '../types/client.js';
|
|
2
2
|
export declare const DEEPSEEK_V4_FLASH_MODEL_ID = "deepseek-v4-flash";
|
|
3
|
-
export declare
|
|
3
|
+
export declare const GLM_5_2_MODEL_ID = "glm-5.2";
|
|
4
|
+
/**
|
|
5
|
+
* OpenClaw has distinct compatibility switches for each part of an
|
|
6
|
+
* OpenAI-compatible tool loop. Do not infer one from another: a route can
|
|
7
|
+
* support ordinary function calls without supporting forced tool choice,
|
|
8
|
+
* reasoning effort, streamed usage, or reasoning-content replay.
|
|
9
|
+
*
|
|
10
|
+
* These values deliberately come only from the authenticated /models catalog.
|
|
11
|
+
* Missing capability metadata fails closed.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthenticatedChatCapabilities {
|
|
14
|
+
tools: boolean;
|
|
15
|
+
toolChoice: boolean;
|
|
16
|
+
reasoning: boolean;
|
|
17
|
+
reasoningEffort: boolean;
|
|
18
|
+
streamingUsage: boolean;
|
|
19
|
+
reasoningReplay: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function authenticatedChatCapabilities(ctx: ConfigureContext, id: string): AuthenticatedChatCapabilities;
|
|
22
|
+
/** Exact model aliases that are Chat-Completions-only, never Responses API. */
|
|
23
|
+
export declare function isExactChatCompletionsModelId(id: string): boolean;
|
|
24
|
+
/** Backward-compatible internal name for legacy client configuration code. */
|
|
25
|
+
export declare const isDeepSeekChatModelId: typeof isExactChatCompletionsModelId;
|
|
4
26
|
/** Trust only the authenticated gateway catalog for endpoint capabilities. */
|
|
5
27
|
export declare function modelSupportsTools(ctx: ConfigureContext, id: string): boolean;
|
|
6
28
|
export declare function modelIds(ctx: ConfigureContext): string[];
|
package/dist/configure/models.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAnthropicModel, isRetiredModelId } from '../utils/api.js';
|
|
1
|
+
import { isAnthropicModel, isModelRouteAvailable, isRetiredModelId } from '../utils/api.js';
|
|
2
2
|
const preferredAnthropic = [
|
|
3
3
|
'claude-opus-5',
|
|
4
4
|
'claude-opus-4-8',
|
|
@@ -16,32 +16,50 @@ const preferredOpenAi = [
|
|
|
16
16
|
'gpt-5.4-mini',
|
|
17
17
|
'gpt-5.3-codex',
|
|
18
18
|
'codex-auto-review',
|
|
19
|
+
'glm-5.2',
|
|
19
20
|
'deepseek-v4-flash'
|
|
20
21
|
];
|
|
21
22
|
export const DEEPSEEK_V4_FLASH_MODEL_ID = 'deepseek-v4-flash';
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export const GLM_5_2_MODEL_ID = 'glm-5.2';
|
|
24
|
+
export function authenticatedChatCapabilities(ctx, id) {
|
|
25
|
+
const capabilities = (ctx.models || []).find(model => model.id.trim() === id.trim())?.capabilities;
|
|
26
|
+
return {
|
|
27
|
+
tools: capabilities?.tools === true,
|
|
28
|
+
toolChoice: capabilities?.tool_choice === true,
|
|
29
|
+
reasoning: capabilities?.reasoning === true,
|
|
30
|
+
reasoningEffort: capabilities?.reasoning_effort === true,
|
|
31
|
+
streamingUsage: capabilities?.streaming_usage === true,
|
|
32
|
+
reasoningReplay: capabilities?.reasoning_replay === true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Exact model aliases that are Chat-Completions-only, never Responses API. */
|
|
36
|
+
export function isExactChatCompletionsModelId(id) {
|
|
37
|
+
const normalized = id.trim().toLowerCase();
|
|
38
|
+
return normalized === DEEPSEEK_V4_FLASH_MODEL_ID || normalized === GLM_5_2_MODEL_ID;
|
|
39
|
+
}
|
|
40
|
+
/** Backward-compatible internal name for legacy client configuration code. */
|
|
41
|
+
export const isDeepSeekChatModelId = isExactChatCompletionsModelId;
|
|
25
42
|
/** Trust only the authenticated gateway catalog for endpoint capabilities. */
|
|
26
43
|
export function modelSupportsTools(ctx, id) {
|
|
27
|
-
|
|
44
|
+
const model = (ctx.models || []).find(candidate => candidate.id.trim() === id.trim());
|
|
45
|
+
return Boolean(model && isModelRouteAvailable(model) && authenticatedChatCapabilities(ctx, id).tools);
|
|
28
46
|
}
|
|
29
47
|
export function modelIds(ctx) {
|
|
30
|
-
return [...new Set((ctx.models || []).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
48
|
+
return [...new Set((ctx.models || []).filter(isModelRouteAvailable).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
31
49
|
}
|
|
32
50
|
export function anthropicModelIds(ctx) {
|
|
33
|
-
return [...new Set((ctx.models || []).filter(isAnthropicModel).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
51
|
+
return [...new Set((ctx.models || []).filter(isModelRouteAvailable).filter(isAnthropicModel).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
34
52
|
}
|
|
35
53
|
export function openAiModelIds(ctx) {
|
|
36
|
-
return [...new Set((ctx.models || []).filter(model => !isAnthropicModel(model)).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
54
|
+
return [...new Set((ctx.models || []).filter(isModelRouteAvailable).filter(model => !isAnthropicModel(model)).map(model => model.id.trim()).filter(id => id && !isRetiredModelId(id)))];
|
|
37
55
|
}
|
|
38
56
|
/** Models that must stay on the OpenAI-compatible Chat Completions route. */
|
|
39
57
|
export function deepSeekChatModelIds(ctx) {
|
|
40
|
-
return openAiModelIds(ctx).filter(
|
|
58
|
+
return openAiModelIds(ctx).filter(isExactChatCompletionsModelId);
|
|
41
59
|
}
|
|
42
60
|
/** Non-Anthropic models that may remain on existing Responses-based clients. */
|
|
43
61
|
export function openAiResponsesModelIds(ctx) {
|
|
44
|
-
return openAiModelIds(ctx).filter(id => !
|
|
62
|
+
return openAiModelIds(ctx).filter(id => !isExactChatCompletionsModelId(id));
|
|
45
63
|
}
|
|
46
64
|
function preferredFrom(ids, candidates, family) {
|
|
47
65
|
for (const candidate of candidates)
|
|
@@ -96,7 +114,9 @@ export function modelName(ctx, id) {
|
|
|
96
114
|
const model = (ctx.models || []).find(candidate => candidate.id === id);
|
|
97
115
|
if (model?.display_name)
|
|
98
116
|
return model.display_name;
|
|
99
|
-
if (
|
|
117
|
+
if (id.trim().toLowerCase() === GLM_5_2_MODEL_ID)
|
|
118
|
+
return 'GLM 5.2';
|
|
119
|
+
if (id.trim().toLowerCase() === DEEPSEEK_V4_FLASH_MODEL_ID)
|
|
100
120
|
return 'DeepSeek V4 Flash';
|
|
101
121
|
return id.split('-').map(part => part ? part[0].toUpperCase() + part.slice(1) : part).join(' ');
|
|
102
122
|
}
|
|
@@ -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, deepSeekChatModelIds, fastModel, fastOpenAiModel, modelName,
|
|
6
|
+
import { anthropicModelIds, anthropicOrigin, authenticatedChatCapabilities, 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';
|
|
@@ -17,10 +17,21 @@ const userAgentReference = { source: 'file', provider: userAgentProviderId, id:
|
|
|
17
17
|
const legacyProviderName = 'scalemax';
|
|
18
18
|
const codexProviderName = 'scalemax-codex';
|
|
19
19
|
const deepSeekProviderName = 'scalemax-deepseek';
|
|
20
|
+
const glmProviderName = 'scalemax-glm-5-2';
|
|
20
21
|
const claudeProviderName = 'scalemax-claude';
|
|
21
22
|
function secretPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-api-key'); }
|
|
22
23
|
function userAgentPath(home) { return path.join(home, '.openclaw', 'secrets', 'scalemax-user-agent'); }
|
|
23
24
|
function userAgentContext(ctx) { return { ...ctx, apiKey: userAgentValue }; }
|
|
25
|
+
function exactChatProviderName(ids) {
|
|
26
|
+
return ids.length > 0 && ids.every(id => id.trim().toLowerCase() === 'glm-5.2')
|
|
27
|
+
? glmProviderName
|
|
28
|
+
: deepSeekProviderName;
|
|
29
|
+
}
|
|
30
|
+
function exactChatLabel(ids) {
|
|
31
|
+
return ids.length > 0 && ids.every(id => id.trim().toLowerCase() === 'glm-5.2')
|
|
32
|
+
? 'GLM 5.2 Chat Completions'
|
|
33
|
+
: 'DeepSeek Chat Completions';
|
|
34
|
+
}
|
|
24
35
|
function asRecord(value) {
|
|
25
36
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
26
37
|
}
|
|
@@ -44,18 +55,16 @@ function entries(ctx, ids, family) {
|
|
|
44
55
|
: Math.min(metadata?.max_output_tokens || 4096, 4096)
|
|
45
56
|
};
|
|
46
57
|
if (family === 'deepseek') {
|
|
47
|
-
const
|
|
48
|
-
result.reasoning =
|
|
49
|
-
result.compat =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
: { supportsTools: false };
|
|
58
|
+
const capabilities = authenticatedChatCapabilities(ctx, modelId);
|
|
59
|
+
result.reasoning = capabilities.reasoning;
|
|
60
|
+
result.compat = {
|
|
61
|
+
supportsTools: capabilities.tools,
|
|
62
|
+
supportsToolChoice: capabilities.toolChoice,
|
|
63
|
+
supportsReasoningEffort: capabilities.reasoningEffort,
|
|
64
|
+
supportsUsageInStreaming: capabilities.streamingUsage,
|
|
65
|
+
maxTokensField: 'max_tokens',
|
|
66
|
+
requiresReasoningContentOnAssistantMessages: capabilities.reasoningReplay,
|
|
67
|
+
};
|
|
59
68
|
}
|
|
60
69
|
if (metadata?.context_window || family === 'anthropic')
|
|
61
70
|
result.contextWindow = metadata?.context_window || 200000;
|
|
@@ -95,7 +104,7 @@ async function scrubGeneratedPlaintextCredentials(ctx) {
|
|
|
95
104
|
for (const providers of roots) {
|
|
96
105
|
for (const [providerName, rawProvider] of Object.entries(providers)) {
|
|
97
106
|
const provider = asRecord(rawProvider);
|
|
98
|
-
const isScaleMax = [legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName].includes(providerName)
|
|
107
|
+
const isScaleMax = [legacyProviderName, codexProviderName, deepSeekProviderName, glmProviderName, claudeProviderName].includes(providerName)
|
|
99
108
|
&& isScaleMaxBaseUrl(provider.baseUrl);
|
|
100
109
|
if (isScaleMax && typeof provider.apiKey === 'string') {
|
|
101
110
|
delete provider.apiKey;
|
|
@@ -115,9 +124,11 @@ export async function configure(ctx) {
|
|
|
115
124
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
116
125
|
const codexIds = openAiResponsesModelIds(ctx);
|
|
117
126
|
const deepSeekIds = deepSeekChatModelIds(ctx);
|
|
127
|
+
const chatProviderName = exactChatProviderName(deepSeekIds);
|
|
128
|
+
const chatLabel = exactChatLabel(deepSeekIds);
|
|
118
129
|
const claudeIds = anthropicModelIds(ctx);
|
|
119
130
|
const codexPrimary = codexIds.length ? `${codexProviderName}/${preferredOpenAiResponsesModel(ctx)}` : undefined;
|
|
120
|
-
const deepSeekPrimary = deepSeekIds.length ? `${
|
|
131
|
+
const deepSeekPrimary = deepSeekIds.length ? `${chatProviderName}/${deepSeekIds[0]}` : undefined;
|
|
121
132
|
const claudePrimary = claudeIds.length ? `${claudeProviderName}/${preferredAnthropicModel(ctx)}` : undefined;
|
|
122
133
|
const codexQuick = fastOpenAiModel(ctx);
|
|
123
134
|
const deepSeekQuick = deepSeekIds[0];
|
|
@@ -137,20 +148,20 @@ export async function configure(ctx) {
|
|
|
137
148
|
const defaults = asRecord(agents.defaults);
|
|
138
149
|
const allowlist = asRecord(defaults.models);
|
|
139
150
|
for (const key of Object.keys(allowlist)) {
|
|
140
|
-
if (key.startsWith(`${legacyProviderName}/`) || key.startsWith(`${codexProviderName}/`) || key.startsWith(`${deepSeekProviderName}/`) || key.startsWith(`${claudeProviderName}/`))
|
|
151
|
+
if (key.startsWith(`${legacyProviderName}/`) || key.startsWith(`${codexProviderName}/`) || key.startsWith(`${deepSeekProviderName}/`) || key.startsWith(`${glmProviderName}/`) || key.startsWith(`${claudeProviderName}/`))
|
|
141
152
|
delete allowlist[key];
|
|
142
153
|
}
|
|
143
154
|
for (const modelId of codexIds)
|
|
144
155
|
allowlist[`${codexProviderName}/${modelId}`] = {};
|
|
145
156
|
for (const modelId of deepSeekIds)
|
|
146
|
-
allowlist[`${
|
|
157
|
+
allowlist[`${chatProviderName}/${modelId}`] = {};
|
|
147
158
|
for (const modelId of claudeIds)
|
|
148
159
|
allowlist[`${claudeProviderName}/${modelId}`] = {};
|
|
149
160
|
const currentModel = defaults.model;
|
|
150
161
|
const currentPrimary = typeof currentModel === 'string' ? currentModel : asRecord(currentModel).primary;
|
|
151
162
|
const activatesScaleMax = !currentPrimary || String(currentPrimary).startsWith('scalemax');
|
|
152
163
|
const currentId = String(currentPrimary || '').split('/').slice(1).join('/');
|
|
153
|
-
const keepDeepSeek = String(currentPrimary || '').startsWith(`${deepSeekProviderName}/`) || deepSeekIds.includes(currentId);
|
|
164
|
+
const keepDeepSeek = String(currentPrimary || '').startsWith(`${deepSeekProviderName}/`) || String(currentPrimary || '').startsWith(`${glmProviderName}/`) || deepSeekIds.includes(currentId);
|
|
154
165
|
const keepClaude = String(currentPrimary || '').startsWith(`${claudeProviderName}/`) || claudeIds.includes(currentId);
|
|
155
166
|
const primary = keepDeepSeek
|
|
156
167
|
? deepSeekPrimary || codexPrimary || claudePrimary
|
|
@@ -158,16 +169,20 @@ export async function configure(ctx) {
|
|
|
158
169
|
? claudePrimary || codexPrimary || deepSeekPrimary
|
|
159
170
|
: codexPrimary || deepSeekPrimary || claudePrimary;
|
|
160
171
|
const usesClaude = Boolean(primary?.startsWith(`${claudeProviderName}/`));
|
|
161
|
-
const usesDeepSeek = Boolean(primary?.startsWith(`${
|
|
172
|
+
const usesDeepSeek = Boolean(primary?.startsWith(`${chatProviderName}/`));
|
|
162
173
|
const nextModel = activatesScaleMax
|
|
163
174
|
? { ...asRecord(currentModel), ...(primary ? { primary } : {}) }
|
|
164
175
|
: currentModel;
|
|
165
176
|
const currentUtility = typeof defaults.utilityModel === 'string' ? defaults.utilityModel : undefined;
|
|
166
177
|
const quick = usesClaude ? claudeQuick : usesDeepSeek ? deepSeekQuick : codexQuick;
|
|
167
|
-
const utilityProvider = usesClaude ? claudeProviderName : usesDeepSeek ?
|
|
178
|
+
const utilityProvider = usesClaude ? claudeProviderName : usesDeepSeek ? chatProviderName : codexProviderName;
|
|
168
179
|
const utilityModel = quick && (activatesScaleMax || currentUtility?.startsWith('scalemax')) ? `${utilityProvider}/${quick}` : currentUtility;
|
|
169
180
|
const nextProviders = { ...providers };
|
|
170
181
|
delete nextProviders[legacyProviderName];
|
|
182
|
+
if (chatProviderName === glmProviderName)
|
|
183
|
+
delete nextProviders[deepSeekProviderName];
|
|
184
|
+
else
|
|
185
|
+
delete nextProviders[glmProviderName];
|
|
171
186
|
if (codexIds.length)
|
|
172
187
|
nextProviders[codexProviderName] = {
|
|
173
188
|
baseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
@@ -180,7 +195,7 @@ export async function configure(ctx) {
|
|
|
180
195
|
else
|
|
181
196
|
delete nextProviders[codexProviderName];
|
|
182
197
|
if (deepSeekIds.length)
|
|
183
|
-
nextProviders[
|
|
198
|
+
nextProviders[chatProviderName] = {
|
|
184
199
|
baseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
185
200
|
apiKey: secretReference,
|
|
186
201
|
api: 'openai-completions',
|
|
@@ -188,8 +203,10 @@ export async function configure(ctx) {
|
|
|
188
203
|
request: { headers: { 'User-Agent': userAgentReference }, allowPrivateNetwork: false },
|
|
189
204
|
models: entries(ctx, deepSeekIds, 'deepseek')
|
|
190
205
|
};
|
|
191
|
-
else
|
|
206
|
+
else {
|
|
192
207
|
delete nextProviders[deepSeekProviderName];
|
|
208
|
+
delete nextProviders[glmProviderName];
|
|
209
|
+
}
|
|
193
210
|
if (claudeIds.length)
|
|
194
211
|
nextProviders[claudeProviderName] = {
|
|
195
212
|
baseUrl: anthropicOrigin(ctx.baseUrl),
|
|
@@ -230,18 +247,20 @@ export async function configure(ctx) {
|
|
|
230
247
|
}
|
|
231
248
|
await removeLegacyArtifact(ctx);
|
|
232
249
|
const scrubbed = await scrubGeneratedPlaintextCredentials(ctx);
|
|
233
|
-
return { ...result, changed: result.changed || secret.changed || userAgentSecret.changed || scrubbed > 0, message: `configured native Responses (${codexIds.length}),
|
|
250
|
+
return { ...result, changed: result.changed || secret.changed || userAgentSecret.changed || scrubbed > 0, message: `configured native Responses (${codexIds.length}), ${chatLabel} (${deepSeekIds.length}), and Anthropic Messages (${claudeIds.length}) providers with file-backed SecretRefs${scrubbed ? `; scrubbed ${scrubbed} generated credential ${scrubbed === 1 ? 'copy' : 'copies'}` : ''}` };
|
|
234
251
|
}
|
|
235
252
|
export async function verify(ctx) {
|
|
236
253
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
237
254
|
const config = await fs.readJson(file);
|
|
238
255
|
const codex = config?.models?.providers?.[codexProviderName];
|
|
239
|
-
const deepSeek = config?.models?.providers?.[deepSeekProviderName];
|
|
240
256
|
const claude = config?.models?.providers?.[claudeProviderName];
|
|
241
257
|
const secretProvider = config?.secrets?.providers?.[secretProviderId];
|
|
242
258
|
const userAgentProvider = config?.secrets?.providers?.[userAgentProviderId];
|
|
243
259
|
const codexIds = openAiResponsesModelIds(ctx);
|
|
244
260
|
const deepSeekIds = deepSeekChatModelIds(ctx);
|
|
261
|
+
const chatProviderName = exactChatProviderName(deepSeekIds);
|
|
262
|
+
const chatLabel = exactChatLabel(deepSeekIds);
|
|
263
|
+
const deepSeek = config?.models?.providers?.[chatProviderName];
|
|
245
264
|
const claudeIds = anthropicModelIds(ctx);
|
|
246
265
|
const valid = (provider, baseUrl, api, ids) => {
|
|
247
266
|
const userAgent = provider?.request?.headers?.['User-Agent'];
|
|
@@ -255,9 +274,14 @@ export async function verify(ctx) {
|
|
|
255
274
|
const deepSeekModels = Array.isArray(deepSeek?.models) ? deepSeek.models : [];
|
|
256
275
|
const deepSeekValid = valid(deepSeek, openAiBaseUrl(ctx.baseUrl), 'openai-completions', deepSeekIds)
|
|
257
276
|
&& deepSeekModels.every((model) => {
|
|
258
|
-
const expected =
|
|
259
|
-
return model?.
|
|
260
|
-
&&
|
|
277
|
+
const expected = authenticatedChatCapabilities(ctx, String(model?.id || ''));
|
|
278
|
+
return model?.reasoning === expected.reasoning
|
|
279
|
+
&& model?.compat?.supportsTools === expected.tools
|
|
280
|
+
&& model?.compat?.supportsToolChoice === expected.toolChoice
|
|
281
|
+
&& model?.compat?.supportsReasoningEffort === expected.reasoningEffort
|
|
282
|
+
&& model?.compat?.supportsUsageInStreaming === expected.streamingUsage
|
|
283
|
+
&& model?.compat?.maxTokensField === 'max_tokens'
|
|
284
|
+
&& model?.compat?.requiresReasoningContentOnAssistantMessages === expected.reasoningReplay;
|
|
261
285
|
});
|
|
262
286
|
if ((codexIds.length ? !valid(codex, openAiBaseUrl(ctx.baseUrl), 'openai-responses', codexIds) : Boolean(codex))
|
|
263
287
|
|| (deepSeekIds.length ? !deepSeekValid : Boolean(deepSeek))
|
|
@@ -270,7 +294,7 @@ export async function verify(ctx) {
|
|
|
270
294
|
await verifyClientSecret(userAgentPath(ctx.home), userAgentContext(ctx));
|
|
271
295
|
if (config?.models?.providers?.[legacyProviderName])
|
|
272
296
|
throw new Error('OpenClaw still contains the obsolete mixed ScaleMax provider.');
|
|
273
|
-
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Responses, ${deepSeekIds.length}
|
|
297
|
+
return { id, name, changed: false, path: file, message: `verified ${codexIds.length} Responses, ${deepSeekIds.length} ${chatLabel}, and ${claudeIds.length} Claude models` };
|
|
274
298
|
}
|
|
275
299
|
export async function remove(ctx) {
|
|
276
300
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
@@ -281,7 +305,7 @@ export async function remove(ctx) {
|
|
|
281
305
|
}
|
|
282
306
|
const config = await fs.readJson(file).catch(() => undefined);
|
|
283
307
|
const providerRoot = asRecord(config?.models?.providers);
|
|
284
|
-
const managedNames = [legacyProviderName, codexProviderName, deepSeekProviderName, claudeProviderName].filter(providerName => {
|
|
308
|
+
const managedNames = [legacyProviderName, codexProviderName, deepSeekProviderName, glmProviderName, claudeProviderName].filter(providerName => {
|
|
285
309
|
const provider = asRecord(providerRoot[providerName]);
|
|
286
310
|
return [anthropicOrigin(ctx.baseUrl), openAiBaseUrl(ctx.baseUrl)].includes(String(provider.baseUrl || ''));
|
|
287
311
|
});
|
|
@@ -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, deepSeekChatModelIds, isDeepSeekChatModelId, modelSupportsTools, openAiBaseUrl, openAiResponsesModelIds, preferredAnthropicModel, preferredChatCompletionsModel, preferredOpenAiResponsesModel } from './models.js';
|
|
6
|
+
import { anthropicModelIds, deepSeekChatModelIds, isDeepSeekChatModelId, modelName, modelSupportsTools, 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)';
|
|
@@ -12,37 +12,74 @@ const importSettingValue = '~/.scalemax/roo-code-import.json';
|
|
|
12
12
|
const codexProfileName = 'ScaleMax Codex';
|
|
13
13
|
const claudeProfileName = 'ScaleMax Claude';
|
|
14
14
|
const deepSeekProfileName = 'ScaleMax DeepSeek';
|
|
15
|
+
const glmProfileName = 'ScaleMax GLM 5.2';
|
|
15
16
|
const codexProfileId = 'scalemax-codex';
|
|
16
17
|
const claudeProfileId = 'scalemax-claude';
|
|
17
18
|
const deepSeekProfileId = 'scalemax-deepseek';
|
|
19
|
+
const glmProfileId = 'scalemax-glm-5-2';
|
|
20
|
+
function exactChatProfile(ctx, model) {
|
|
21
|
+
if (model.trim().toLowerCase() === 'glm-5.2')
|
|
22
|
+
return { name: glmProfileName, id: glmProfileId, model };
|
|
23
|
+
return { name: deepSeekProfileName, id: deepSeekProfileId, model };
|
|
24
|
+
}
|
|
18
25
|
function importPath(home) { return path.join(home, '.scalemax', 'roo-code-import.json'); }
|
|
26
|
+
function positiveInteger(value, fallback) {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
29
|
+
}
|
|
30
|
+
function rooModelInfo(ctx, modelId) {
|
|
31
|
+
const model = (ctx.models || []).find(candidate => candidate.id.trim() === modelId.trim());
|
|
32
|
+
const contextWindow = positiveInteger(model?.context_window, 128_000);
|
|
33
|
+
const maxTokens = Math.min(contextWindow, positiveInteger(model?.max_output_tokens, 4_096));
|
|
34
|
+
const capabilities = model?.capabilities;
|
|
35
|
+
return {
|
|
36
|
+
maxTokens,
|
|
37
|
+
contextWindow,
|
|
38
|
+
// Missing authenticated capability metadata must fail closed. Roo would
|
|
39
|
+
// otherwise accept an image or cache workflow the active route may reject.
|
|
40
|
+
supportsImages: capabilities?.images === true || capabilities?.vision === true,
|
|
41
|
+
supportsPromptCache: capabilities?.prompt_cache === true,
|
|
42
|
+
// ScaleMax performs billing centrally; Roo should not estimate a second,
|
|
43
|
+
// potentially contradictory price from the client profile.
|
|
44
|
+
inputPrice: 0,
|
|
45
|
+
outputPrice: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function toolCapableContext(ctx) {
|
|
49
|
+
return {
|
|
50
|
+
...ctx,
|
|
51
|
+
models: (ctx.models || []).filter(model => modelSupportsTools(ctx, model.id)),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
19
54
|
function fallbackProfileFor(ctx) {
|
|
20
55
|
const model = preferredChatCompletionsModel(ctx);
|
|
21
56
|
const metadata = (ctx.models || []).find(candidate => candidate.id === model) || { id: model };
|
|
22
|
-
if (
|
|
23
|
-
throw new Error(
|
|
57
|
+
if (!modelSupportsTools(ctx, model))
|
|
58
|
+
throw new Error(`Roo Code requires native task-completion tool calling, but the active ${modelName(ctx, model)} route is text-only. Use Continue Chat/Edit in VS Code or choose a tool-capable model. No configuration was changed.`);
|
|
24
59
|
if (isDeepSeekChatModelId(model))
|
|
25
|
-
return
|
|
60
|
+
return exactChatProfile(ctx, model);
|
|
26
61
|
return isAnthropicModel(metadata)
|
|
27
62
|
? { name: claudeProfileName, id: claudeProfileId, model }
|
|
28
63
|
: { name: codexProfileName, id: codexProfileId, model };
|
|
29
64
|
}
|
|
30
65
|
function profilesFor(ctx) {
|
|
66
|
+
const compatible = toolCapableContext(ctx);
|
|
67
|
+
if (!compatible.models?.length) {
|
|
68
|
+
throw new Error('Roo Code requires native task-completion tool calling, but this API key has no tool-capable models. Use Continue Chat/Edit in VS Code or choose a tool-capable model. No configuration was changed.');
|
|
69
|
+
}
|
|
31
70
|
const scope = ctx.providerScope || 'both';
|
|
32
71
|
const profiles = [];
|
|
33
|
-
const codexIds = openAiResponsesModelIds(
|
|
72
|
+
const codexIds = openAiResponsesModelIds(compatible);
|
|
34
73
|
if (scope !== 'anthropic' && codexIds.length) {
|
|
35
|
-
profiles.push({ name: codexProfileName, id: codexProfileId, model: preferredOpenAiResponsesModel(
|
|
74
|
+
profiles.push({ name: codexProfileName, id: codexProfileId, model: preferredOpenAiResponsesModel(compatible) });
|
|
36
75
|
}
|
|
37
|
-
if (scope !== 'openai' && anthropicModelIds(
|
|
38
|
-
profiles.push({ name: claudeProfileName, id: claudeProfileId, model: preferredAnthropicModel(
|
|
76
|
+
if (scope !== 'openai' && anthropicModelIds(compatible).length) {
|
|
77
|
+
profiles.push({ name: claudeProfileName, id: claudeProfileId, model: preferredAnthropicModel(compatible) });
|
|
39
78
|
}
|
|
40
|
-
for (const model of deepSeekChatModelIds(
|
|
41
|
-
|
|
42
|
-
profiles.push({ name: deepSeekProfileName, id: deepSeekProfileId, model });
|
|
43
|
-
}
|
|
79
|
+
for (const model of deepSeekChatModelIds(compatible)) {
|
|
80
|
+
profiles.push(exactChatProfile(compatible, model));
|
|
44
81
|
}
|
|
45
|
-
return profiles.length ? profiles : [fallbackProfileFor(
|
|
82
|
+
return profiles.length ? profiles : [fallbackProfileFor(compatible)];
|
|
46
83
|
}
|
|
47
84
|
function currentProfileFor(ctx, profiles = profilesFor(ctx)) {
|
|
48
85
|
if (ctx.providerScope === 'anthropic')
|
|
@@ -51,6 +88,8 @@ function currentProfileFor(ctx, profiles = profilesFor(ctx)) {
|
|
|
51
88
|
return profiles.find(profile => profile.id === codexProfileId) || profiles[0];
|
|
52
89
|
if (ctx.providerScope === 'deepseek-v4-flash')
|
|
53
90
|
return profiles.find(profile => profile.id === deepSeekProfileId) || profiles[0];
|
|
91
|
+
if (ctx.providerScope === 'glm-5.2')
|
|
92
|
+
return profiles.find(profile => profile.id === glmProfileId) || profiles[0];
|
|
54
93
|
return profiles.find(profile => profile.id === codexProfileId) || profiles[0];
|
|
55
94
|
}
|
|
56
95
|
function parseSettings(text, file) {
|
|
@@ -69,15 +108,20 @@ function parseSettings(text, file) {
|
|
|
69
108
|
function handoff(ctx) {
|
|
70
109
|
const profiles = profilesFor(ctx);
|
|
71
110
|
const current = currentProfileFor(ctx, profiles);
|
|
72
|
-
const apiConfigs = Object.fromEntries(profiles.map(profile =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
const apiConfigs = Object.fromEntries(profiles.map(profile => {
|
|
112
|
+
const modelInfo = rooModelInfo(ctx, profile.model);
|
|
113
|
+
return [profile.name, {
|
|
114
|
+
id: profile.id,
|
|
115
|
+
apiProvider: 'openai',
|
|
116
|
+
openAiBaseUrl: openAiBaseUrl(ctx.baseUrl),
|
|
117
|
+
openAiApiKey: ctx.apiKey,
|
|
118
|
+
openAiModelId: profile.model,
|
|
119
|
+
openAiCustomModelInfo: modelInfo,
|
|
120
|
+
modelMaxTokens: modelInfo.maxTokens,
|
|
121
|
+
openAiStreamingEnabled: true,
|
|
122
|
+
includeMaxTokens: true
|
|
123
|
+
}];
|
|
124
|
+
}));
|
|
81
125
|
return JSON.stringify({
|
|
82
126
|
providerProfiles: {
|
|
83
127
|
currentApiConfigName: current.name,
|
|
@@ -129,9 +173,11 @@ export async function verify(ctx) {
|
|
|
129
173
|
}
|
|
130
174
|
for (const expected of expectedProfiles) {
|
|
131
175
|
const config = profile?.providerProfiles?.apiConfigs?.[expected.name];
|
|
176
|
+
const expectedModelInfo = rooModelInfo(ctx, expected.model);
|
|
132
177
|
if (config?.id !== expected.id || config?.apiProvider !== 'openai' || config?.openAiBaseUrl !== openAiBaseUrl(ctx.baseUrl)
|
|
133
178
|
|| config?.openAiApiKey !== ctx.apiKey || config?.openAiModelId !== expected.model || config?.openAiStreamingEnabled !== true
|
|
134
|
-
|| config?.includeMaxTokens !== true
|
|
179
|
+
|| config?.includeMaxTokens !== true || config?.modelMaxTokens !== expectedModelInfo.maxTokens
|
|
180
|
+
|| JSON.stringify(config?.openAiCustomModelInfo) !== JSON.stringify(expectedModelInfo)) {
|
|
135
181
|
throw new Error('Roo Code auto-import handoff is incomplete.');
|
|
136
182
|
}
|
|
137
183
|
}
|
|
@@ -159,7 +205,8 @@ export async function remove(ctx) {
|
|
|
159
205
|
const managed = [
|
|
160
206
|
configs?.[codexProfileName]?.id === codexProfileId,
|
|
161
207
|
configs?.[claudeProfileName]?.id === claudeProfileId,
|
|
162
|
-
configs?.[deepSeekProfileName]?.id === deepSeekProfileId
|
|
208
|
+
configs?.[deepSeekProfileName]?.id === deepSeekProfileId,
|
|
209
|
+
configs?.[glmProfileName]?.id === glmProfileId
|
|
163
210
|
].some(Boolean);
|
|
164
211
|
if (managed) {
|
|
165
212
|
await backupFile(profileFile, ctx.home);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { clientConfigPath } from './paths.js';
|
|
2
2
|
import { writeManagedConfig, removeManagedConfig } from './common.js';
|
|
3
3
|
import { jsonConfig } from './templates.js';
|
|
4
|
+
import { preferredChatCompletionsModel } from './models.js';
|
|
4
5
|
const id = 'trae-solo';
|
|
5
6
|
const name = 'TRAE SOLO';
|
|
6
7
|
export async function configure(ctx) {
|
|
7
8
|
const file = clientConfigPath(id, ctx.home, ctx.platform);
|
|
8
|
-
const
|
|
9
|
+
const recommendedModel = preferredChatCompletionsModel(ctx);
|
|
10
|
+
const content = jsonConfig(name, '$SCALEMAX_API_KEY', ctx.baseUrl, { "officialSetup": ["Settings > Models > Add Model", "API format: OpenAI Chat Completions", `Request URL: ${ctx.baseUrl}/chat/completions or base URL ${ctx.baseUrl}`, "Use only a model ID returned by the authenticated catalog"], "recommendedModel": recommendedModel });
|
|
9
11
|
const result = await writeManagedConfig(ctx, id, name, file, content);
|
|
10
12
|
return { ...result, message: 'setup guide written; enter these values in TRAE SOLO custom model settings' };
|
|
11
13
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
export type ProviderScope = 'openai' | 'anthropic' | 'both' | 'deepseek-v4-flash' | 'glm-5.2';
|
|
3
3
|
export interface ClientDefinition {
|
|
4
4
|
id: ClientId;
|
|
5
5
|
name: string;
|
|
@@ -45,6 +45,16 @@ export interface ConfigureContext extends DetectContext {
|
|
|
45
45
|
max_output_tokens?: number | null;
|
|
46
46
|
capabilities?: {
|
|
47
47
|
tools?: boolean;
|
|
48
|
+
/** The model accepts an explicit OpenAI-compatible tool_choice control. */
|
|
49
|
+
tool_choice?: boolean;
|
|
50
|
+
/** The model exposes reasoning/thinking controls. */
|
|
51
|
+
reasoning?: boolean;
|
|
52
|
+
/** The model accepts an explicit reasoning_effort control. */
|
|
53
|
+
reasoning_effort?: boolean;
|
|
54
|
+
/** The model returns usage information in streaming responses. */
|
|
55
|
+
streaming_usage?: boolean;
|
|
56
|
+
/** Assistant reasoning_content must be replayed during a tool loop. */
|
|
57
|
+
reasoning_replay?: boolean;
|
|
48
58
|
[key: string]: unknown;
|
|
49
59
|
};
|
|
50
60
|
}>;
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export declare const DEEPSEEK_V4_FLASH_SCOPE: "deepseek-v4-flash";
|
|
2
|
-
export
|
|
2
|
+
export declare const GLM_5_2_SCOPE: "glm-5.2";
|
|
3
|
+
export type ProviderScope = 'openai' | 'anthropic' | 'both' | typeof DEEPSEEK_V4_FLASH_SCOPE | typeof GLM_5_2_SCOPE;
|
|
3
4
|
export interface ModelInfo {
|
|
4
5
|
id: string;
|
|
5
6
|
object?: string;
|
|
6
7
|
owned_by?: string;
|
|
7
8
|
provider?: string;
|
|
8
9
|
display_name?: string;
|
|
10
|
+
availability?: 'available' | 'unavailable';
|
|
9
11
|
context_window?: number | null;
|
|
10
12
|
max_output_tokens?: number | null;
|
|
11
13
|
capabilities?: {
|
|
12
14
|
tools?: boolean;
|
|
15
|
+
tool_choice?: boolean;
|
|
16
|
+
route_available?: boolean;
|
|
17
|
+
reasoning?: boolean;
|
|
18
|
+
reasoning_effort?: boolean;
|
|
19
|
+
streaming_usage?: boolean;
|
|
20
|
+
reasoning_replay?: boolean;
|
|
13
21
|
[key: string]: unknown;
|
|
14
22
|
};
|
|
15
23
|
}
|
|
@@ -47,6 +55,7 @@ export interface ChatCompletionResult {
|
|
|
47
55
|
}
|
|
48
56
|
export declare const RETIRED_MODEL_IDS: Set<string>;
|
|
49
57
|
export declare function isRetiredModelId(id: string): boolean;
|
|
58
|
+
export declare function isModelRouteAvailable(model: ModelInfo): boolean;
|
|
50
59
|
export declare function isAnthropicModel(model: ModelInfo): boolean;
|
|
51
60
|
export declare function filterModelsForScope(models: ModelInfo[], scope: ProviderScope): ModelInfo[];
|
|
52
61
|
export declare function createChatCompletion(options: ChatCompletionOptions): Promise<ChatCompletionResult>;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { SCALEMAX_BASE_URL, SCALEMAX_TOKEN_BASE_URL } from '../constants.js';
|
|
2
2
|
import { redactApiKey, sanitizeApiKey } from './apiKey.js';
|
|
3
3
|
export const DEEPSEEK_V4_FLASH_SCOPE = 'deepseek-v4-flash';
|
|
4
|
+
export const GLM_5_2_SCOPE = 'glm-5.2';
|
|
4
5
|
// Keep only aliases that are no longer offered. Direct GPT-5.6 Sol is an
|
|
5
6
|
// active Codex model and must remain available to all generated clients.
|
|
6
7
|
export const RETIRED_MODEL_IDS = new Set(['fable-5-ultra']);
|
|
7
8
|
export function isRetiredModelId(id) {
|
|
8
9
|
return RETIRED_MODEL_IDS.has(id.trim());
|
|
9
10
|
}
|
|
11
|
+
export function isModelRouteAvailable(model) {
|
|
12
|
+
return model.availability !== 'unavailable'
|
|
13
|
+
&& model.capabilities?.route_available !== false;
|
|
14
|
+
}
|
|
10
15
|
export function isAnthropicModel(model) {
|
|
11
16
|
const provider = String(model.provider || model.owned_by || '').trim().toLowerCase();
|
|
12
17
|
if (provider)
|
|
@@ -17,13 +22,23 @@ export function filterModelsForScope(models, scope) {
|
|
|
17
22
|
return models.filter(model => {
|
|
18
23
|
if (isRetiredModelId(model.id))
|
|
19
24
|
return false;
|
|
20
|
-
|
|
25
|
+
// Availability is independent from feature support. Preserve truthful tool
|
|
26
|
+
// metadata while refusing to generate a client profile for a route the
|
|
27
|
+
// gateway has explicitly marked unhealthy.
|
|
28
|
+
if (!isModelRouteAvailable(model))
|
|
29
|
+
return false;
|
|
30
|
+
const modelId = model.id.trim().toLowerCase();
|
|
31
|
+
const exactScope = modelId === DEEPSEEK_V4_FLASH_SCOPE
|
|
32
|
+
? DEEPSEEK_V4_FLASH_SCOPE
|
|
33
|
+
: modelId === GLM_5_2_SCOPE
|
|
34
|
+
? GLM_5_2_SCOPE
|
|
35
|
+
: null;
|
|
21
36
|
// This scope is an exact-model entitlement, not an OpenAI-compatible
|
|
22
37
|
// provider family. Mirror the gateway's fail-closed authorization so an
|
|
23
38
|
// unexpectedly broad catalog can never widen a customer's key locally.
|
|
24
|
-
if (
|
|
25
|
-
return scope ===
|
|
26
|
-
if (scope === DEEPSEEK_V4_FLASH_SCOPE)
|
|
39
|
+
if (exactScope)
|
|
40
|
+
return scope === exactScope;
|
|
41
|
+
if (scope === DEEPSEEK_V4_FLASH_SCOPE || scope === GLM_5_2_SCOPE)
|
|
27
42
|
return false;
|
|
28
43
|
return scope === 'both' || (scope === 'anthropic' ? isAnthropicModel(model) : !isAnthropicModel(model));
|
|
29
44
|
});
|
|
@@ -33,6 +48,7 @@ function providerScope(value) {
|
|
|
33
48
|
|| value === 'anthropic'
|
|
34
49
|
|| value === 'both'
|
|
35
50
|
|| value === DEEPSEEK_V4_FLASH_SCOPE
|
|
51
|
+
|| value === GLM_5_2_SCOPE
|
|
36
52
|
? value
|
|
37
53
|
: null;
|
|
38
54
|
}
|