@xelauvas/xela-cli 0.2.2 → 0.3.0

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
@@ -1,27 +1,32 @@
1
1
  # Xela CLI
2
2
 
3
- AI coding assistant that works with **any model** — free or paid. Use OpenRouter, Groq, Ollama, DeepSeek, OpenAI, and more. One tool, any brain.
3
+ [![npm](https://img.shields.io/npm/v/@xelauvas/xela-cli)](https://www.npmjs.com/package/@xelauvas/xela-cli)
4
+ [![GitHub stars](https://img.shields.io/github/stars/xelauvas/codeclau?style=social)](https://github.com/xelauvas/codeclau)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **Free AI coding assistant for your terminal.** Works with any model — OpenRouter, Groq, Ollama, DeepSeek, OpenAI, and more. Like Cursor/Claude Code, but free and open source.
4
8
 
5
9
  ```
6
- ▐▛███▜▌ Xela CLI
7
- ▝▜█████▛▘ AI coding assistant
8
- ▘▘ ▝▝ powered by any model
10
+ ╲ ╳ ╱ Xela
11
+ ╱ ╳ ╲ AI coding assistant
12
+ XELA powered by any model
9
13
  ```
10
14
 
11
15
  ## Install
12
16
 
13
- **npm (recommended):**
14
17
  ```bash
15
18
  npm install -g @xelauvas/xela-cli
16
19
  ```
17
20
 
18
- **One-liner:**
21
+ On first run, Xela walks you through setup — pick a provider, paste a key, choose a model. No config files to edit.
22
+
23
+ **Other install methods:**
24
+
19
25
  ```bash
26
+ # One-liner
20
27
  curl -fsSL https://raw.githubusercontent.com/xelauvas/codeclau/main/install.sh | bash
21
- ```
22
28
 
23
- **From source:**
24
- ```bash
29
+ # From source
25
30
  git clone https://github.com/xelauvas/codeclau.git /opt/xela
26
31
  cd /opt/xela && npm install && npm link
27
32
  ```
@@ -29,10 +34,7 @@ cd /opt/xela && npm install && npm link
29
34
  ## Quick Start
30
35
 
31
36
  ```bash
32
- # First run creates ~/.xela/config — edit it with your API key
33
- xela
34
-
35
- # Interactive mode
37
+ # Interactive setup + launch
36
38
  xela
37
39
 
38
40
  # With a prompt
@@ -41,160 +43,170 @@ xela "fix the login bug in auth.ts"
41
43
  # Non-interactive (pipe-friendly)
42
44
  xela -p "explain this function"
43
45
 
44
- # Override model on the fly
45
- xela -m nvidia/nemotron-3-super-120b-a12b:free "review this code"
46
- ```
46
+ # Override model for one session
47
+ xela -m google/gemini-2.5-pro-exp-03-25:free "review this code"
48
+ ```
49
+
50
+ ## What It Can Do
51
+
52
+ - **Edit files** — reads your code, makes changes with your permission
53
+ - **Run commands** — executes shell commands, runs tests, builds projects
54
+ - **Agentic coding** — plans, executes, and iterates on code changes
55
+ - **Multi-file refactoring** — rename, restructure, migrate across your codebase
56
+ - **Web search** — looks up docs, APIs, error messages
57
+ - **MCP support** — extensible via Model Context Protocol servers
58
+ - **Project memory** — run `/init` to create `XELA.md` with project instructions
59
+ - **Auto mode** — let Xela handle permissions automatically for long tasks
60
+
61
+ ## Slash Commands
62
+
63
+ | Command | Description |
64
+ |---|---|
65
+ | `/init` | Create a `XELA.md` file with project instructions |
66
+ | `/model` | Switch model |
67
+ | `/config` | Open settings |
68
+ | `/help` | Show help |
69
+ | `/clear` | Clear conversation |
70
+ | `/compact` | Compress conversation to save context |
71
+ | `/cost` | Show token usage |
72
+ | `/mcp` | Manage MCP servers |
73
+
74
+ ## Keyboard Shortcuts
75
+
76
+ | Key | Action |
77
+ |---|---|
78
+ | `Enter` | Submit prompt |
79
+ | `Escape` | Cancel current request |
80
+ | `Ctrl+C` | Exit |
81
+ | `Tab` | Accept autocomplete |
82
+ | `Shift+Tab` | Toggle auto mode |
83
+ | `Up/Down` | Navigate history |
47
84
 
48
85
  ## Free Providers
49
86
 
50
- You don't need to pay anything to use Xela. Here are free options:
51
-
52
- ### OpenRouter (Free Models)
53
- Sign up at [openrouter.ai](https://openrouter.ai) — no credit card needed.
54
-
55
- ```bash
56
- # ~/.xela/config
57
- XELA_PROVIDER=openrouter
58
- OPENAI_API_KEY=sk-or-your-key
59
- OPENAI_MODEL=qwen/qwen3.6-plus-preview:free
60
- ```
87
+ You don't need to pay anything. Here are free options:
61
88
 
62
- Free models available:
63
- | Model | ID | Context |
64
- |---|---|---|
65
- | Qwen 3.6 Plus | `qwen/qwen3.6-plus-preview:free` | 1M |
66
- | Nemotron 3 Super 120B | `nvidia/nemotron-3-super-120b-a12b:free` | 262K |
67
- | Step 3.5 Flash | `stepfun/step-3.5-flash:free` | 256K |
68
- | Nemotron 3 Nano | `nvidia/nemotron-3-nano-30b-a3b:free` | 256K |
89
+ ### OpenRouter (recommended)
90
+ Sign up at [openrouter.ai/keys](https://openrouter.ai/keys) — no credit card needed.
69
91
 
70
- ### Groq (Free Tier)
71
- Sign up at [console.groq.com](https://console.groq.com) — blazing fast inference.
92
+ Available free models:
93
+ | Model | ID |
94
+ |---|---|
95
+ | Qwen 3.6 Plus (1M context) | `qwen/qwen3.6-plus-preview:free` |
96
+ | DeepSeek Chat V3 | `deepseek/deepseek-chat-v3-0324:free` |
97
+ | Gemini 2.5 Pro | `google/gemini-2.5-pro-exp-03-25:free` |
98
+ | Llama 4 Maverick | `meta-llama/llama-4-maverick:free` |
99
+ | Nemotron Ultra 253B | `nvidia/llama-3.1-nemotron-ultra-253b:free` |
72
100
 
73
- ```bash
74
- # ~/.xela/config
75
- XELA_PROVIDER=groq
76
- OPENAI_API_KEY=gsk_your-key
77
- OPENAI_MODEL=qwen-qwq-32b
78
- ```
101
+ ### Groq (free tier)
102
+ Sign up at [console.groq.com/keys](https://console.groq.com/keys) — blazing fast inference.
79
103
 
80
- ### Ollama (Local, Free Forever)
81
- Install from [ollama.com](https://ollama.com) — runs on your machine.
104
+ ### Ollama (local, free forever)
105
+ Install from [ollama.com](https://ollama.com) — runs on your machine, fully private.
82
106
 
83
107
  ```bash
84
108
  ollama pull qwen2.5-coder:7b
85
-
86
- # ~/.xela/config
87
- XELA_PROVIDER=ollama
88
- OPENAI_API_KEY=ollama
89
- OPENAI_MODEL=qwen2.5-coder:7b
109
+ # Then pick "ollama" as provider during xela setup
90
110
  ```
91
111
 
92
- ## Paid Providers
112
+ ### Other free tiers
113
+ - **Cerebras** — fast inference, free tier
114
+ - **SambaNova** — free tier available
93
115
 
94
- For better quality and speed:
116
+ ## Paid Providers
95
117
 
96
- ### OpenRouter (Paid Models)
97
- ```bash
98
- XELA_PROVIDER=openrouter
99
- OPENAI_API_KEY=sk-or-your-key
100
- OPENAI_MODEL=openai/gpt-5.3-codex # $0.00175/M input
101
- # OPENAI_MODEL=anthropic/claude-opus-4.6 # $0.005/M input
102
- # OPENAI_MODEL=google/gemini-3.1-pro-preview # $0.002/M input
103
- ```
118
+ For better quality and higher rate limits:
104
119
 
105
- ### DeepSeek
106
- ```bash
107
- XELA_PROVIDER=deepseek
108
- OPENAI_API_KEY=sk-your-key
109
- OPENAI_MODEL=deepseek-chat
110
- ```
111
-
112
- ### OpenAI Direct
113
- ```bash
114
- XELA_PROVIDER=openai
115
- OPENAI_API_KEY=sk-your-key
116
- OPENAI_MODEL=gpt-4o
117
- ```
120
+ | Provider | Setup |
121
+ |---|---|
122
+ | **DeepSeek** | [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys) — very cheap |
123
+ | **OpenAI** | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
124
+ | **OpenRouter** (paid models) | Same key, just pick a paid model |
118
125
 
119
126
  ## Configuration
120
127
 
121
- Config file: `~/.xela/config` (created on first run)
128
+ Config is stored at `~/.xela/config.json` (created during first-run setup).
122
129
 
123
- ```bash
124
- # ~/.xela/config
125
- XELA_PROVIDER=openrouter
126
- OPENAI_API_KEY=sk-or-your-key-here
127
- OPENAI_MODEL=qwen/qwen3.6-plus-preview:free
128
- # OPENAI_BASE_URL= # auto-detected from provider
130
+ ```json
131
+ {
132
+ "provider": "openrouter",
133
+ "apiKey": "sk-or-v1-...",
134
+ "model": "qwen/qwen3.6-plus-preview:free",
135
+ "baseUrl": ""
136
+ }
129
137
  ```
130
138
 
131
- ### Supported Providers
139
+ ### Re-run setup
132
140
 
133
- | Provider | `XELA_PROVIDER` | Key Prefix | Free? |
134
- |---|---|---|---|
135
- | OpenRouter | `openrouter` | `sk-or-` | Free models available |
136
- | Groq | `groq` | `gsk_` | Free tier |
137
- | Ollama | `ollama` | `ollama` | Free (local) |
138
- | DeepSeek | `deepseek` | `sk-` | Cheap |
139
- | OpenAI | `openai` | `sk-` | Paid |
140
- | Cerebras | `cerebras` | — | Free tier |
141
- | SambaNova | `sambanova` | — | Free tier |
141
+ ```bash
142
+ rm ~/.xela/config.json && xela
143
+ ```
142
144
 
143
- ### Model Override
145
+ ### Change model
144
146
 
145
147
  ```bash
146
- # Override for a single session
147
- xela -m openai/gpt-5.4-pro "architect a microservice"
148
+ # Edit config directly
149
+ nano ~/.xela/config.json
148
150
 
149
- # Or set in config permanently
150
- nano ~/.xela/config
151
+ # Or override per-session
152
+ xela -m model-id
151
153
  ```
152
154
 
153
- ## Features
155
+ ### Supported Providers
154
156
 
155
- - **Multi-provider** — Switch between any OpenAI-compatible API
156
- - **Auto-detection** — Detects provider from API key prefix
157
- - **Free by default** — Works with free models out of the box
158
- - **Full CLI** — Interactive REPL, non-interactive mode, piping support
159
- - **Tool use** — File editing, bash commands, web search, and more
160
- - **Agentic coding** — Plans, executes, and iterates on code changes
161
- - **MCP support** — Extensible via Model Context Protocol servers
157
+ | Provider | Default Model | Free? |
158
+ |---|---|---|
159
+ | OpenRouter | `qwen/qwen3.6-plus-preview:free` | Free models available |
160
+ | Groq | `qwen-qwq-32b` | Free tier |
161
+ | Ollama | `qwen2.5-coder:7b` | Free (local) |
162
+ | DeepSeek | `deepseek-chat` | Cheap |
163
+ | OpenAI | `gpt-4o` | Paid |
164
+ | Cerebras | `llama-3.3-70b` | Free tier |
165
+ | SambaNova | `Meta-Llama-3.3-70B-Instruct` | Free tier |
162
166
 
163
167
  ## Self-Hosting Models
164
168
 
165
- For privacy or unlimited usage, run models locally:
169
+ For privacy or unlimited usage, run models locally with Ollama:
166
170
 
167
- ### Small Server (16GB RAM, no GPU)
168
171
  ```bash
172
+ # Small (16GB RAM, no GPU) — ~10-30 tok/s
169
173
  ollama pull qwen2.5-coder:3b
170
- # ~10-30 tokens/sec on CPU
171
- ```
172
174
 
173
- ### GPU Server (24GB+ VRAM)
174
- ```bash
175
+ # GPU (24GB+ VRAM) — ~100 tok/s
175
176
  ollama pull qwen2.5-coder:32b
176
- # 100+ tokens/sec
177
+
178
+ # Multi-GPU — full power
179
+ ollama pull deepseek-v3.2
177
180
  ```
178
181
 
179
- ### Multi-GPU Server
182
+ ## Project Instructions (XELA.md)
183
+
184
+ Run `/init` inside any project to generate a `XELA.md` file. This file tells Xela about your project — build commands, architecture, conventions. It's loaded automatically every session.
185
+
180
186
  ```bash
181
- ollama pull deepseek-v3.2
182
- # Full power
187
+ cd my-project
188
+ xela
189
+ # then type /init
183
190
  ```
184
191
 
192
+ Xela also reads `XELA.local.md` for personal project instructions (add it to `.gitignore`).
193
+
185
194
  ## Uninstall
186
195
 
187
196
  ```bash
188
- # npm install
197
+ # Remove CLI
189
198
  npm uninstall -g @xelauvas/xela-cli
190
199
 
191
- # curl install
192
- curl -fsSL https://raw.githubusercontent.com/xelauvas/codeclau/main/uninstall.sh | bash
193
-
194
- # Config (optional)
200
+ # Remove config (optional)
195
201
  rm -rf ~/.xela
196
202
  ```
197
203
 
204
+ ## Links
205
+
206
+ - **GitHub**: [github.com/xelauvas/codeclau](https://github.com/xelauvas/codeclau)
207
+ - **npm**: [@xelauvas/xela-cli](https://www.npmjs.com/package/@xelauvas/xela-cli)
208
+ - **Issues**: [github.com/xelauvas/codeclau/issues](https://github.com/xelauvas/codeclau/issues)
209
+
198
210
  ## License
199
211
 
200
212
  MIT
package/bin/xela.js CHANGED
@@ -165,6 +165,24 @@ for (let i = 0; i < args.length; i++) {
165
165
  }
166
166
  }
167
167
 
168
+ // Show star prompt after first few uses
169
+ const STAR_FILE = join(XELA_HOME, '.star-shown');
170
+ const USAGE_FILE = join(XELA_HOME, '.usage-count');
171
+ try {
172
+ let count = 0;
173
+ try { count = parseInt(readFileSync(USAGE_FILE, 'utf-8')) || 0; } catch {}
174
+ count++;
175
+ writeFileSync(USAGE_FILE, String(count));
176
+ if (count === 3 && !existsSync(STAR_FILE)) {
177
+ console.log('');
178
+ console.log(' \x1b[36m╲ ╳ ╱\x1b[0m Enjoying Xela? Give it a star!');
179
+ console.log(' \x1b[36m╱ ╳ ╲\x1b[0m \x1b[4mhttps://github.com/xelauvas/codeclau\x1b[0m');
180
+ console.log(' \x1b[36m XELA \x1b[0m It helps others discover it too.');
181
+ console.log('');
182
+ writeFileSync(STAR_FILE, '1');
183
+ }
184
+ } catch {}
185
+
168
186
  // Launch node with the TSX loader shim
169
187
  try {
170
188
  execFileSync(process.execPath, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xelauvas/xela-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Xela — AI coding assistant powered by any model (OpenRouter, Groq, Ollama, DeepSeek, OpenAI)",
5
5
  "author": "xelauvas",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "start": "node --import ./src/_shims/register.js ./src/entrypoints/cli.tsx",
23
- "postinstall": "echo '\\n Xela installed! Run: xela\\n Config: ~/.xela/config\\n'"
23
+ "postinstall": "echo '\\n \\x1b[36m╲ ╳ ╱\\x1b[0m Xela installed!\\n \\x1b[36m╱ ╳ ╲\\x1b[0m Run: xela\\n \\x1b[36m XELA \\x1b[0m Star: github.com/xelauvas/codeclau\\n'"
24
24
  },
25
25
  "dependencies": {
26
26
  "@alcalzone/ansi-tokenize": "^0.3.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 = 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 = 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 && !modelOptions.some(opt => opt.value === initial)) {
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 = [...modelOptions, t6];
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 = modelOptions;
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>;
@@ -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
- if (specifiedModel && !isModelAllowed(specifiedModel)) {
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, return the OpenAI model name directly
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, always use the configured model
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 {
@@ -1,168 +0,0 @@
1
- // Universal stub — exports all known names for unresolved modules
2
- export default {};
3
-
4
- // --- Arrays (names ending in S, LIST, TOOLS, etc.) ---
5
- export const BROWSER_TOOLS = [];
6
- export const DEFAULT_SESSION_TIMEOUT_MS = [];
7
- export const RARITY_COLORS = [];
8
- export const EYES = [];
9
- export const HATS = [];
10
- export const RARITIES = [];
11
- export const RARITY_WEIGHTS = [];
12
- export const SPECIES = [];
13
- export const STAT_NAMES = [];
14
- export const AGENT_PATHS = [];
15
- export const DEFAULT_GRANT_FLAGS = [];
16
- export const MEMORY_TYPE_VALUES = [];
17
- export const SYNC_KEYS = [];
18
- export const CUSTOMIZATION_SURFACES = [];
19
- export const FIND_KEYS = [];
20
- export const OPERATORS = [];
21
- export const SIMPLE_MOTIONS = [];
22
- export const TEXT_OBJ_SCOPES = [];
23
- export const TEXT_OBJ_TYPES = [];
24
- export const SOURCES = [];
25
- export const FRAME_INTERVAL_MS = [];
26
- export const ONE_SHOT_BUILTIN_AGENT_TYPES = [];
27
- export const REPL_ONLY_TOOLS = [];
28
- export const SETTING_SOURCES = [];
29
- export const MDM_SUBPROCESS_TIMEOUT_MS = [];
30
-
31
- // --- Functions (is/has/get/set/create/handle...) ---
32
- export const isBackgroundTask = () => undefined;
33
- export const isInProcessTeammateTask = () => undefined;
34
- export const isMcpServerCommandEntry = () => undefined;
35
- export const isMcpServerNameEntry = () => undefined;
36
- export const isMcpServerUrlEntry = () => undefined;
37
- export const isPaneBackend = () => undefined;
38
- export const isOperatorKey = () => undefined;
39
- export const isTextObjScopeKey = () => undefined;
40
- export const isReplModeEnabled = () => undefined;
41
- export const isSettingSourceEnabled = () => undefined;
42
- export const getSourceDisplayName = () => undefined;
43
- export const getSettingSourceName = () => undefined;
44
- export const getSwarmSocketName = () => undefined;
45
- export const getSettingSourceDisplayNameLowercase = () => undefined;
46
- export const getSettingSourceDisplayNameCapitalized = () => undefined;
47
- export const getEnabledSettingSources = () => undefined;
48
- export const getMacOSPlistPaths = () => undefined;
49
- export const getSyntaxTheme = () => undefined;
50
- export const createClaudeForChromeMcpServer = () => undefined;
51
- export const createInitialPersistentState = () => undefined;
52
- export const createInitialVimState = () => undefined;
53
- export const parseSettingSourcesFlag = () => undefined;
54
- export const reduceAnsiCodes = () => undefined;
55
-
56
- // --- Constants/schemas/values ---
57
- export const BRIDGE_LOGIN_INSTRUCTION = undefined;
58
- export const BRIDGE_LOGIN_ERROR = undefined;
59
- export const axolotl = undefined;
60
- export const blob = undefined;
61
- export const cactus = undefined;
62
- export const capybara = undefined;
63
- export const cat = undefined;
64
- export const chonk = undefined;
65
- export const dragon = undefined;
66
- export const duck = undefined;
67
- export const ghost = undefined;
68
- export const goose = undefined;
69
- export const mushroom = undefined;
70
- export const octopus = undefined;
71
- export const owl = undefined;
72
- export const penguin = undefined;
73
- export const rabbit = undefined;
74
- export const robot = undefined;
75
- export const snail = undefined;
76
- export const turtle = undefined;
77
- export const ElicitRequestSchema = undefined;
78
- export const ElicitationCompleteNotificationSchema = undefined;
79
- export const REMOTE_CONTROL_DISCONNECTED_MSG = undefined;
80
- export const CallToolRequestSchema = undefined;
81
- export const ListToolsRequestSchema = undefined;
82
- export const defaultStyle = undefined;
83
- export const connectResponseSchema = undefined;
84
- export const CallToolResultSchema = undefined;
85
- export const ErrorCode = undefined;
86
- export const ListPromptsResultSchema = undefined;
87
- export const ListResourcesResultSchema = undefined;
88
- export const ListRootsRequestSchema = undefined;
89
- export const ListToolsResultSchema = undefined;
90
- export const McpError = undefined;
91
- export const McpJsonConfigSchema = undefined;
92
- export const McpServerConfigSchema = undefined;
93
- export const PromptListChangedNotificationSchema = undefined;
94
- export const ResourceListChangedNotificationSchema = undefined;
95
- export const ToolListChangedNotificationSchema = undefined;
96
- export const ConfigScopeSchema = undefined;
97
- export const PolicyLimitsResponseSchema = undefined;
98
- export const SettingsSchema = undefined;
99
- export const RemoteManagedSettingsResponseSchema = undefined;
100
- export const UserSyncDataSchema = undefined;
101
- export const TeamMemoryDataSchema = undefined;
102
- export const TeamMemoryTooManyEntriesSchema = undefined;
103
- export const HooksSchema = undefined;
104
- export const appendCappedMessage = undefined;
105
- export const inputSchema = undefined;
106
- export const outputSchema = undefined;
107
- export const gitDiffSchema = undefined;
108
- export const hunkSchema = undefined;
109
- export const ReadResourceResultSchema = undefined;
110
- export const TodoListSchema = undefined;
111
- export const McpStdioServerConfigSchema = undefined;
112
- export const DEFAULT_UPLOAD_CONCURRENCY = undefined;
113
- export const FILE_COUNT_LIMIT = undefined;
114
- export const OUTPUTS_SUBDIR = undefined;
115
- export const JSONRPCMessageSchema = undefined;
116
- export const MAX_VIM_COUNT = undefined;
117
- export const AGENT_TOOL_NAME = undefined;
118
- export const LEGACY_AGENT_TOOL_NAME = undefined;
119
- export const EXIT_PLAN_MODE_V2_TOOL_NAME = undefined;
120
- export const TEAM_CREATE_TOOL_NAME = undefined;
121
- export const CLAUDE_FOLDER_PERMISSION_PATTERN = undefined;
122
- export const FILE_EDIT_TOOL_NAME = undefined;
123
- export const GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN = undefined;
124
- export const SKILL_TOOL_NAME = undefined;
125
- export const TEAM_LEAD_NAME = undefined;
126
- export const TMUX_COMMAND = undefined;
127
- export const VERIFICATION_AGENT_TYPE = undefined;
128
- export const TODO_WRITE_TOOL_NAME = undefined;
129
- export const TASK_CREATE_TOOL_NAME = undefined;
130
- export const TASK_OUTPUT_TOOL_NAME = undefined;
131
- export const ENTER_PLAN_MODE_TOOL_NAME = undefined;
132
- export const NOTEBOOK_EDIT_TOOL_NAME = undefined;
133
- export const SEND_MESSAGE_TOOL_NAME = undefined;
134
- export const TASK_GET_TOOL_NAME = undefined;
135
- export const TASK_LIST_TOOL_NAME = undefined;
136
- export const TASK_UPDATE_TOOL_NAME = undefined;
137
- export const ENTER_WORKTREE_TOOL_NAME = undefined;
138
- export const EXIT_WORKTREE_TOOL_NAME = undefined;
139
- export const WORKFLOW_TOOL_NAME = undefined;
140
- export const TEAM_DELETE_TOOL_NAME = undefined;
141
- export const ContinuousEventPriority = undefined;
142
- export const DefaultEventPriority = undefined;
143
- export const DiscreteEventPriority = undefined;
144
- export const NoEventPriority = undefined;
145
- export const ConcurrentRoot = undefined;
146
- export const LegacyRoot = undefined;
147
- export const REPL_TOOL_NAME = undefined;
148
- export const EXIT_PLAN_MODE_TOOL_NAME = undefined;
149
- export const CONFIG_TOOL_NAME = undefined;
150
- export const FILE_UNEXPECTEDLY_MODIFIED_ERROR = undefined;
151
- export const TOOL_SEARCH_TOOL_NAME = undefined;
152
- export const SWARM_SESSION_NAME = undefined;
153
- export const TEAMMATE_COMMAND_ENV_VAR = undefined;
154
- export const PLUTIL_ARGS_PREFIX = undefined;
155
- export const PLUTIL_PATH = undefined;
156
- export const WINDOWS_REGISTRY_KEY_PATH_HKCU = undefined;
157
- export const WINDOWS_REGISTRY_KEY_PATH_HKLM = undefined;
158
- export const WINDOWS_REGISTRY_VALUE_NAME = undefined;
159
- export const CLAUDE_CODE_SETTINGS_SCHEMA_URL = undefined;
160
- export const HIDDEN_SESSION_NAME = undefined;
161
- export const SWARM_VIEW_WINDOW_NAME = undefined;
162
- export const ColorDiff = undefined;
163
- export const ColorFile = undefined;
164
- export const ansiCodesToString = undefined;
165
- export const diffAnsiCodes = undefined;
166
- export const styledCharsFromTokens = undefined;
167
- export const tokenize = undefined;
168
- export const undoAnsiCodes = undefined;
@@ -1 +0,0 @@
1
- export default '';