@xelauvas/xela-cli 0.2.3 → 0.3.1
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/bin/xela.js +12 -0
- package/package.json +13 -3
- package/src/_shims/openai_adapter.ts +15 -2
- package/src/commands/model/model.tsx +23 -8
- package/src/components/ModelPicker.tsx +34 -4
- package/src/utils/model/model.ts +9 -4
- package/src/utils/model/modelOptions.ts +21 -0
- package/src/utils/model/providerModels.ts +168 -0
- package/src/utils/model/validateModel.ts +19 -0
package/bin/xela.js
CHANGED
|
@@ -183,6 +183,18 @@ try {
|
|
|
183
183
|
}
|
|
184
184
|
} catch {}
|
|
185
185
|
|
|
186
|
+
// Ensure esbuild is available (may be missing in global installs)
|
|
187
|
+
try {
|
|
188
|
+
await import('esbuild');
|
|
189
|
+
} catch {
|
|
190
|
+
try {
|
|
191
|
+
execSync('npm install esbuild --no-save --no-audit --no-fund', {
|
|
192
|
+
cwd: INSTALL_DIR,
|
|
193
|
+
stdio: 'pipe',
|
|
194
|
+
});
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
|
|
186
198
|
// Launch node with the TSX loader shim
|
|
187
199
|
try {
|
|
188
200
|
execFileSync(process.execPath, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xelauvas/xela-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Xela — AI coding assistant powered by any model (OpenRouter, Groq, Ollama, DeepSeek, OpenAI)",
|
|
5
5
|
"author": "xelauvas",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,17 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/xelauvas/codeclau.git"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"coding",
|
|
15
|
+
"assistant",
|
|
16
|
+
"cli",
|
|
17
|
+
"openrouter",
|
|
18
|
+
"groq",
|
|
19
|
+
"ollama",
|
|
20
|
+
"deepseek",
|
|
21
|
+
"xela"
|
|
22
|
+
],
|
|
13
23
|
"bin": {
|
|
14
24
|
"xela": "./bin/xela.js"
|
|
15
25
|
},
|
|
@@ -50,7 +60,7 @@
|
|
|
50
60
|
"diff": "^7.0.0",
|
|
51
61
|
"emoji-regex": "^10.4.0",
|
|
52
62
|
"env-paths": "^3.0.0",
|
|
53
|
-
"esbuild": "^0.27.
|
|
63
|
+
"esbuild": "^0.27.7",
|
|
54
64
|
"execa": "^9.5.2",
|
|
55
65
|
"figures": "^6.1.0",
|
|
56
66
|
"fuse.js": "^7.1.0",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* OPENAI_API_KEY=... OPENAI_BASE_URL=https://api.together.xyz/v1 OPENAI_MODEL=meta-llama/Llama-3.3-70B-Instruct-Turbo
|
|
36
36
|
*/
|
|
37
37
|
import OpenAI from 'openai'
|
|
38
|
+
import { isClaudeModelName } from '../utils/model/providerModels.js'
|
|
38
39
|
|
|
39
40
|
// Auto-detect provider from API key prefix or base URL
|
|
40
41
|
function detectProvider(): { baseURL?: string; model: string; maxTokens: number } {
|
|
@@ -67,6 +68,18 @@ const PROVIDER = detectProvider()
|
|
|
67
68
|
const MODEL = PROVIDER.model
|
|
68
69
|
const MAX_TOKENS = PROVIDER.maxTokens
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Resolve which model to send to the OpenAI-compatible provider.
|
|
73
|
+
* - Non-Claude names pass through directly (e.g. 'gpt-4o', 'deepseek-chat')
|
|
74
|
+
* - Claude names/aliases fall back to the provider default
|
|
75
|
+
* - Null/undefined uses the startup default
|
|
76
|
+
*/
|
|
77
|
+
function resolveModel(paramsModel: string | undefined): string {
|
|
78
|
+
if (!paramsModel) return MODEL
|
|
79
|
+
if (isClaudeModelName(paramsModel)) return MODEL
|
|
80
|
+
return paramsModel
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
let _openaiClient: OpenAI | null = null
|
|
71
84
|
|
|
72
85
|
function getOpenAI(): OpenAI {
|
|
@@ -188,7 +201,7 @@ function mapStopReason(finishReason: string | null): string {
|
|
|
188
201
|
async function* createAnthropicStyleStream(params: any, signal?: AbortSignal): AsyncGenerator<any> {
|
|
189
202
|
const openai = getOpenAI()
|
|
190
203
|
// Always use the configured MODEL — internal Claude model names don't exist on OpenAI-compatible providers
|
|
191
|
-
const model =
|
|
204
|
+
const model = resolveModel(params.model)
|
|
192
205
|
|
|
193
206
|
const openaiMessages: any[] = []
|
|
194
207
|
|
|
@@ -398,7 +411,7 @@ export function createOpenAIBackedAnthropicClient(): any {
|
|
|
398
411
|
const nonStreamingResult = (async () => {
|
|
399
412
|
const openai = getOpenAI()
|
|
400
413
|
// Always use the configured MODEL
|
|
401
|
-
const model =
|
|
414
|
+
const model = resolveModel(params.model)
|
|
402
415
|
|
|
403
416
|
const openaiMessages: any[] = []
|
|
404
417
|
if (params.system) {
|
|
@@ -14,7 +14,8 @@ import { MODEL_ALIASES } from '../../utils/model/aliases.js';
|
|
|
14
14
|
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js';
|
|
15
15
|
import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
|
|
16
16
|
import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
|
|
17
|
-
import { validateModel } from '../../utils/model/validateModel.js'
|
|
17
|
+
import { validateModel } from '../../utils/model/validateModel.js'
|
|
18
|
+
import { isOpenAIBackend, isClaudeModelName } from '../../utils/model/providerModels.js';
|
|
18
19
|
function ModelPickerWrapper(t0) {
|
|
19
20
|
const $ = _c(17);
|
|
20
21
|
const {
|
|
@@ -141,13 +142,30 @@ function SetModelAndClose({
|
|
|
141
142
|
const model = args === 'default' ? null : args;
|
|
142
143
|
React.useEffect(() => {
|
|
143
144
|
async function handleModelChange(): Promise<void> {
|
|
144
|
-
if (model && !isModelAllowed(model)) {
|
|
145
|
+
if (model && !isOpenAIBackend() && !isModelAllowed(model)) {
|
|
145
146
|
onDone(`Model '${model}' is not available. Your organization restricts model selection.`, {
|
|
146
147
|
display: 'system'
|
|
147
148
|
});
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
// Skip validation for default model
|
|
153
|
+
if (!model) {
|
|
154
|
+
setModel(null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// OpenAI backend: skip all Claude-specific checks, go straight to validation
|
|
159
|
+
if (isOpenAIBackend()) {
|
|
160
|
+
const { valid, error: validationError } = await validateModel(model);
|
|
161
|
+
if (valid) {
|
|
162
|
+
setModel(model);
|
|
163
|
+
} else {
|
|
164
|
+
onDone(validationError || `Model '${model}' not found`, { display: 'system' });
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
// @[MODEL LAUNCH]: Update check for 1M access.
|
|
152
170
|
if (model && isOpus1mUnavailable(model)) {
|
|
153
171
|
onDone(`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, {
|
|
@@ -162,12 +180,6 @@ function SetModelAndClose({
|
|
|
162
180
|
return;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
// Skip validation for default model
|
|
166
|
-
if (!model) {
|
|
167
|
-
setModel(null);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
183
|
// Skip validation for known aliases - they're predefined and should work
|
|
172
184
|
if (isKnownAlias(model)) {
|
|
173
185
|
setModel(model);
|
|
@@ -291,6 +303,9 @@ export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
|
|
|
291
303
|
return <ModelPickerWrapper onDone={onDone} />;
|
|
292
304
|
};
|
|
293
305
|
function renderModelLabel(model: string | null): string {
|
|
306
|
+
if (isOpenAIBackend() && model && !isClaudeModelName(model)) {
|
|
307
|
+
return model;
|
|
308
|
+
}
|
|
294
309
|
const rendered = renderDefaultModelSetting(model ?? getDefaultMainLoopModelSetting());
|
|
295
310
|
return model === null ? `${rendered} (default)` : rendered;
|
|
296
311
|
}
|
|
@@ -18,6 +18,7 @@ import { Byline } from './design-system/Byline.js';
|
|
|
18
18
|
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
|
19
19
|
import { Pane } from './design-system/Pane.js';
|
|
20
20
|
import { effortLevelToSymbol } from './EffortIndicator.js';
|
|
21
|
+
import { isOpenAIBackend, fetchProviderModels, getOpenAIDefaultModel } from '../utils/model/providerModels.js';
|
|
21
22
|
export type Props = {
|
|
22
23
|
initial: string | null;
|
|
23
24
|
sessionModel?: ModelSetting;
|
|
@@ -74,9 +75,38 @@ export function ModelPicker(t0) {
|
|
|
74
75
|
t3 = $[3];
|
|
75
76
|
}
|
|
76
77
|
const modelOptions = t3;
|
|
78
|
+
|
|
79
|
+
const [providerModels, setProviderModels] = useState<Array<{value: string | null, label: string, description: string}>>([]);
|
|
80
|
+
const [providerLoading, setProviderLoading] = useState(isOpenAIBackend());
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (!isOpenAIBackend()) return;
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
(async () => {
|
|
86
|
+
try {
|
|
87
|
+
const models = await fetchProviderModels();
|
|
88
|
+
if (cancelled) return;
|
|
89
|
+
const defaultModel = getOpenAIDefaultModel();
|
|
90
|
+
const options = [
|
|
91
|
+
{ value: null, label: `Default (${defaultModel})`, description: 'Current default model' },
|
|
92
|
+
...models.map(m => ({ value: m.id, label: m.id, description: m.name !== m.id ? m.name : '' })),
|
|
93
|
+
];
|
|
94
|
+
setProviderModels(options);
|
|
95
|
+
} finally {
|
|
96
|
+
if (!cancelled) setProviderLoading(false);
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
return () => { cancelled = true; };
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
// When on OpenAI backend and provider models loaded, use them instead of Claude options
|
|
103
|
+
const effectiveModelOptions = isOpenAIBackend() && providerModels.length > 0
|
|
104
|
+
? providerModels
|
|
105
|
+
: modelOptions;
|
|
106
|
+
|
|
77
107
|
let t4;
|
|
78
108
|
bb0: {
|
|
79
|
-
if (initial !== null && !
|
|
109
|
+
if (initial !== null && !effectiveModelOptions.some(opt => opt.value === initial)) {
|
|
80
110
|
let t5;
|
|
81
111
|
if ($[4] !== initial) {
|
|
82
112
|
t5 = modelDisplayString(initial);
|
|
@@ -100,7 +130,7 @@ export function ModelPicker(t0) {
|
|
|
100
130
|
}
|
|
101
131
|
let t7;
|
|
102
132
|
if ($[9] !== modelOptions || $[10] !== t6) {
|
|
103
|
-
t7 = [...
|
|
133
|
+
t7 = [...effectiveModelOptions, t6];
|
|
104
134
|
$[9] = modelOptions;
|
|
105
135
|
$[10] = t6;
|
|
106
136
|
$[11] = t7;
|
|
@@ -110,7 +140,7 @@ export function ModelPicker(t0) {
|
|
|
110
140
|
t4 = t7;
|
|
111
141
|
break bb0;
|
|
112
142
|
}
|
|
113
|
-
t4 =
|
|
143
|
+
t4 = effectiveModelOptions;
|
|
114
144
|
}
|
|
115
145
|
const optionsWithInitial = t4;
|
|
116
146
|
let t5;
|
|
@@ -265,7 +295,7 @@ export function ModelPicker(t0) {
|
|
|
265
295
|
} else {
|
|
266
296
|
t15 = $[41];
|
|
267
297
|
}
|
|
268
|
-
const t16 = headerText ?? "Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model.";
|
|
298
|
+
const t16 = headerText ?? (providerLoading ? 'Loading models from provider...' : isOpenAIBackend() ? 'Switch between provider models. Select a model or use /model <name> directly.' : "Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model.");
|
|
269
299
|
let t17;
|
|
270
300
|
if ($[42] !== t16) {
|
|
271
301
|
t17 = <Text dimColor={true}>{t16}</Text>;
|
package/src/utils/model/model.ts
CHANGED
|
@@ -74,7 +74,8 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// Ignore the user-specified model if it's not in the availableModels allowlist.
|
|
77
|
-
|
|
77
|
+
// Skip this check on OpenAI backend — the allowlist is for Claude models only.
|
|
78
|
+
if (specifiedModel && !process.env.OPENAI_API_KEY && !isModelAllowed(specifiedModel)) {
|
|
78
79
|
return undefined
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -94,8 +95,12 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
|
|
|
94
95
|
* @returns The resolved model name to use
|
|
95
96
|
*/
|
|
96
97
|
export function getMainLoopModel(): ModelName {
|
|
97
|
-
// When using OpenAI backend,
|
|
98
|
+
// When using OpenAI backend, respect user-selected model, fall back to OPENAI_MODEL
|
|
98
99
|
if (process.env.OPENAI_API_KEY) {
|
|
100
|
+
const model = getUserSpecifiedModelSetting()
|
|
101
|
+
if (model !== undefined && model !== null) {
|
|
102
|
+
return model
|
|
103
|
+
}
|
|
99
104
|
return process.env.OPENAI_MODEL || 'gpt-4o'
|
|
100
105
|
}
|
|
101
106
|
const model = getUserSpecifiedModelSetting()
|
|
@@ -160,9 +165,9 @@ export function getRuntimeMainLoopModel(params: {
|
|
|
160
165
|
}): ModelName {
|
|
161
166
|
const { permissionMode, mainLoopModel, exceeds200kTokens = false } = params
|
|
162
167
|
|
|
163
|
-
// When using OpenAI backend,
|
|
168
|
+
// When using OpenAI backend, use the passed-in model (already resolved by caller)
|
|
164
169
|
if (process.env.OPENAI_API_KEY) {
|
|
165
|
-
return process.env.OPENAI_MODEL || 'gpt-4o'
|
|
170
|
+
return mainLoopModel || process.env.OPENAI_MODEL || 'gpt-4o'
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
// opusplan uses Opus in plan mode without [1m] suffix.
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from './model.js'
|
|
33
33
|
import { has1mContext } from '../context.js'
|
|
34
34
|
import { getGlobalConfig } from '../config.js'
|
|
35
|
+
import { isOpenAIBackend, getOpenAIDefaultModel, detectOpenAIProvider } from './providerModels.js'
|
|
35
36
|
|
|
36
37
|
// @[MODEL LAUNCH]: Update all the available and default model option strings below.
|
|
37
38
|
|
|
@@ -266,6 +267,23 @@ function getOpusPlanOption(): ModelOption {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Build model options from the OpenAI-compatible provider's model list.
|
|
272
|
+
* Called synchronously with cached data if available, otherwise returns
|
|
273
|
+
* a minimal default option.
|
|
274
|
+
*/
|
|
275
|
+
function getOpenAIModelOptions(): ModelOption[] {
|
|
276
|
+
const defaultModel = getOpenAIDefaultModel()
|
|
277
|
+
const provider = detectOpenAIProvider()
|
|
278
|
+
const providerLabel = provider === 'unknown' ? 'OpenAI-compatible' : provider.charAt(0).toUpperCase() + provider.slice(1)
|
|
279
|
+
|
|
280
|
+
return [{
|
|
281
|
+
value: null,
|
|
282
|
+
label: `Default (${defaultModel})`,
|
|
283
|
+
description: `${providerLabel} default model`,
|
|
284
|
+
}]
|
|
285
|
+
}
|
|
286
|
+
|
|
269
287
|
// @[MODEL LAUNCH]: Update the model picker lists below to include/reorder options for the new model.
|
|
270
288
|
// Each user tier (ant, Max/Team Premium, Pro/Team Standard/Enterprise, PAYG 1P, PAYG 3P) has its own list.
|
|
271
289
|
function getModelOptionsBase(fastMode = false): ModelOption[] {
|
|
@@ -459,6 +477,9 @@ function getKnownModelOption(model: string): ModelOption | null {
|
|
|
459
477
|
}
|
|
460
478
|
|
|
461
479
|
export function getModelOptions(fastMode = false): ModelOption[] {
|
|
480
|
+
if (isOpenAIBackend()) {
|
|
481
|
+
return getOpenAIModelOptions()
|
|
482
|
+
}
|
|
462
483
|
const options = getModelOptionsBase(fastMode)
|
|
463
484
|
|
|
464
485
|
// Add the custom model from the ANTHROPIC_CUSTOM_MODEL_OPTION env var
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import OpenAI from 'openai'
|
|
2
|
+
import { MODEL_ALIASES } from './aliases.js'
|
|
3
|
+
|
|
4
|
+
export type ProviderModel = {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ProviderName =
|
|
10
|
+
| 'openai'
|
|
11
|
+
| 'groq'
|
|
12
|
+
| 'openrouter'
|
|
13
|
+
| 'deepseek'
|
|
14
|
+
| 'mistral'
|
|
15
|
+
| 'together'
|
|
16
|
+
| 'ollama'
|
|
17
|
+
| 'fireworks'
|
|
18
|
+
| 'moonshot'
|
|
19
|
+
| 'unknown'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if the model name is a Claude model name or alias.
|
|
23
|
+
* These don't exist on OpenAI-compatible providers, so the adapter
|
|
24
|
+
* should fall back to the provider default.
|
|
25
|
+
*/
|
|
26
|
+
export function isClaudeModelName(model: string): boolean {
|
|
27
|
+
const lower = model.toLowerCase().trim()
|
|
28
|
+
if (lower.includes('claude')) return true
|
|
29
|
+
return (MODEL_ALIASES as readonly string[]).includes(lower)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect which OpenAI-compatible provider is configured.
|
|
34
|
+
*/
|
|
35
|
+
export function detectOpenAIProvider(): ProviderName {
|
|
36
|
+
const key = process.env.OPENAI_API_KEY || ''
|
|
37
|
+
const baseURL = process.env.OPENAI_BASE_URL || ''
|
|
38
|
+
|
|
39
|
+
if (baseURL.includes('groq.com') || key.startsWith('gsk_')) return 'groq'
|
|
40
|
+
if (baseURL.includes('openrouter.ai') || key.startsWith('sk-or-')) return 'openrouter'
|
|
41
|
+
if (baseURL.includes('deepseek.com')) return 'deepseek'
|
|
42
|
+
if (baseURL.includes('mistral.ai')) return 'mistral'
|
|
43
|
+
if (baseURL.includes('together.xyz')) return 'together'
|
|
44
|
+
if (baseURL.includes('localhost') || baseURL.includes('127.0.0.1')) return 'ollama'
|
|
45
|
+
if (baseURL.includes('fireworks.ai')) return 'fireworks'
|
|
46
|
+
if (baseURL.includes('moonshot.cn') || baseURL.includes('moonshot.ai')) return 'moonshot'
|
|
47
|
+
if (!baseURL || baseURL.includes('openai.com')) return 'openai'
|
|
48
|
+
return 'unknown'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CURATED_MODELS: Record<ProviderName, ProviderModel[]> = {
|
|
52
|
+
openai: [
|
|
53
|
+
{ id: 'gpt-4o', name: 'GPT-4o' },
|
|
54
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
|
|
55
|
+
{ id: 'gpt-4.1', name: 'GPT-4.1' },
|
|
56
|
+
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini' },
|
|
57
|
+
{ id: 'o3', name: 'o3' },
|
|
58
|
+
{ id: 'o4-mini', name: 'o4 Mini' },
|
|
59
|
+
],
|
|
60
|
+
groq: [
|
|
61
|
+
{ id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B' },
|
|
62
|
+
{ id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B Instant' },
|
|
63
|
+
{ id: 'gemma2-9b-it', name: 'Gemma 2 9B' },
|
|
64
|
+
{ id: 'mixtral-8x7b-32768', name: 'Mixtral 8x7B' },
|
|
65
|
+
],
|
|
66
|
+
openrouter: [
|
|
67
|
+
{ id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash' },
|
|
68
|
+
{ id: 'openai/gpt-4o', name: 'GPT-4o' },
|
|
69
|
+
{ id: 'meta-llama/llama-3.3-70b', name: 'Llama 3.3 70B' },
|
|
70
|
+
],
|
|
71
|
+
deepseek: [
|
|
72
|
+
{ id: 'deepseek-chat', name: 'DeepSeek Chat' },
|
|
73
|
+
{ id: 'deepseek-reasoner', name: 'DeepSeek Reasoner' },
|
|
74
|
+
],
|
|
75
|
+
mistral: [
|
|
76
|
+
{ id: 'mistral-large-latest', name: 'Mistral Large' },
|
|
77
|
+
{ id: 'mistral-small-latest', name: 'Mistral Small' },
|
|
78
|
+
{ id: 'codestral-latest', name: 'Codestral' },
|
|
79
|
+
],
|
|
80
|
+
together: [
|
|
81
|
+
{ id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', name: 'Llama 3.3 70B Turbo' },
|
|
82
|
+
{ id: 'Qwen/Qwen2.5-72B-Instruct-Turbo', name: 'Qwen 2.5 72B Turbo' },
|
|
83
|
+
],
|
|
84
|
+
ollama: [
|
|
85
|
+
{ id: 'llama3.2', name: 'Llama 3.2' },
|
|
86
|
+
{ id: 'qwen2.5-coder', name: 'Qwen 2.5 Coder' },
|
|
87
|
+
{ id: 'deepseek-r1', name: 'DeepSeek R1' },
|
|
88
|
+
{ id: 'mistral', name: 'Mistral' },
|
|
89
|
+
],
|
|
90
|
+
fireworks: [
|
|
91
|
+
{ id: 'accounts/fireworks/models/llama-v3p3-70b-instruct', name: 'Llama 3.3 70B' },
|
|
92
|
+
],
|
|
93
|
+
moonshot: [
|
|
94
|
+
{ id: 'moonshot-v1-32k', name: 'Moonshot V1 32K' },
|
|
95
|
+
{ id: 'moonshot-v1-128k', name: 'Moonshot V1 128K' },
|
|
96
|
+
],
|
|
97
|
+
unknown: [],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getCuratedModels(provider: ProviderName): ProviderModel[] {
|
|
101
|
+
return CURATED_MODELS[provider] ?? []
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let cachedModels: ProviderModel[] | null = null
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fetch all available models from the provider via GET /v1/models.
|
|
108
|
+
* Results are cached in memory for the session.
|
|
109
|
+
* Returns curated fallback list on failure.
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchProviderModels(): Promise<ProviderModel[]> {
|
|
112
|
+
if (cachedModels !== null) {
|
|
113
|
+
return cachedModels
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const provider = detectOpenAIProvider()
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const client = new OpenAI({
|
|
120
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
121
|
+
...(process.env.OPENAI_BASE_URL ? { baseURL: process.env.OPENAI_BASE_URL } : {}),
|
|
122
|
+
...(provider === 'groq' && !process.env.OPENAI_BASE_URL
|
|
123
|
+
? { baseURL: 'https://api.groq.com/openai/v1' }
|
|
124
|
+
: {}),
|
|
125
|
+
...(provider === 'openrouter' && !process.env.OPENAI_BASE_URL
|
|
126
|
+
? { baseURL: 'https://openrouter.ai/api/v1' }
|
|
127
|
+
: {}),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const response = await client.models.list({}, { timeout: 5000 })
|
|
131
|
+
const models: ProviderModel[] = []
|
|
132
|
+
|
|
133
|
+
for await (const model of response) {
|
|
134
|
+
models.push({ id: model.id, name: model.id })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
models.sort((a, b) => a.id.localeCompare(b.id))
|
|
138
|
+
cachedModels = models
|
|
139
|
+
return models
|
|
140
|
+
} catch {
|
|
141
|
+
const fallback = getCuratedModels(provider)
|
|
142
|
+
cachedModels = fallback
|
|
143
|
+
return fallback
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a model ID exists in the cached provider model list.
|
|
149
|
+
* Returns false if the cache hasn't been populated yet.
|
|
150
|
+
*/
|
|
151
|
+
export function isKnownProviderModel(modelId: string): boolean {
|
|
152
|
+
if (!cachedModels) return false
|
|
153
|
+
return cachedModels.some(m => m.id === modelId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns true if the app is using an OpenAI-compatible backend.
|
|
158
|
+
*/
|
|
159
|
+
export function isOpenAIBackend(): boolean {
|
|
160
|
+
return !!process.env.OPENAI_API_KEY
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the startup default model for the current OpenAI provider.
|
|
165
|
+
*/
|
|
166
|
+
export function getOpenAIDefaultModel(): string {
|
|
167
|
+
return process.env.OPENAI_MODEL || 'gpt-4o'
|
|
168
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AuthenticationError,
|
|
11
11
|
} from '@anthropic-ai/sdk'
|
|
12
12
|
import { getModelStrings } from './modelStrings.js'
|
|
13
|
+
import { isOpenAIBackend, isKnownProviderModel, isClaudeModelName } from './providerModels.js'
|
|
13
14
|
|
|
14
15
|
// Cache valid models to avoid repeated API calls
|
|
15
16
|
const validModelCache = new Map<string, boolean>()
|
|
@@ -27,6 +28,24 @@ export async function validateModel(
|
|
|
27
28
|
return { valid: false, error: 'Model name cannot be empty' }
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// OpenAI-compatible backend: skip Anthropic validation
|
|
32
|
+
if (isOpenAIBackend()) {
|
|
33
|
+
// Claude names don't exist on OpenAI providers
|
|
34
|
+
if (isClaudeModelName(normalizedModel)) {
|
|
35
|
+
return {
|
|
36
|
+
valid: false,
|
|
37
|
+
error: `'${normalizedModel}' is a Claude model name. Use a model name from your provider (e.g. 'gpt-4o', 'deepseek-chat').`,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// If we already fetched the model list, check against it
|
|
41
|
+
if (isKnownProviderModel(normalizedModel)) {
|
|
42
|
+
return { valid: true }
|
|
43
|
+
}
|
|
44
|
+
// Otherwise, trust the user — the adapter will try it and the provider
|
|
45
|
+
// will return an error if it doesn't exist
|
|
46
|
+
return { valid: true }
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
// Check against availableModels allowlist before any API call
|
|
31
50
|
if (!isModelAllowed(normalizedModel)) {
|
|
32
51
|
return {
|