@orbit-intelligence/orbit-agent 0.3.13 → 0.3.15

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.
@@ -5,6 +5,7 @@ import { VERSION } from '../version.js';
5
5
  import { touchConfig, saveConfig } from '../config/index.js';
6
6
  import { EventBus } from '../core/events.js';
7
7
  import { buildProviders, resolveCandidates } from '../core/llm/index.js';
8
+ import { resolveModelRows, resolveProviderRows } from '../tui/picker.js';
8
9
  import { AutoRouter } from '../core/llm/router.js';
9
10
  import { ContextManager } from '../core/context/context-manager.js';
10
11
  import { ToolRegistry } from '../core/tools/registry.js';
@@ -19,7 +20,7 @@ import { createEventLog, appendEvent, readEventLog } from '../session/event-log.
19
20
  import { loadProjectContext } from '../core/project-context.js';
20
21
  import { createSkillLoaderTool } from '../core/skill-loader.js';
21
22
  const THEME_NAMES = ['tokyonight', 'catppuccin-mocha', 'catppuccin-latte', 'nord', 'gruvbox', 'monokai', 'clean-dark'];
22
- const PROVIDER_IDS = ['orbitx', 'groq', 'gemini', 'openrouter', 'openai'];
23
+ const PROVIDER_IDS = ['orbitx', 'groq', 'gemini', 'openrouter', 'openai', 'anthropic', 'grok', 'deepseek', 'ollama'];
23
24
  export async function main(argv) {
24
25
  let args;
25
26
  try {
@@ -175,6 +176,7 @@ export async function main(argv) {
175
176
  const permissions = new PermissionManager(cfg.permissions, {
176
177
  ask: (prompt) => app.ask(prompt),
177
178
  });
179
+ let app = null;
178
180
  let agent = makeAgent(cfg, router, bus, context, registry, permissions, systemPrompt, cwd);
179
181
  function rebuild() {
180
182
  return rebuildPipeline(cfg, bus).then((next) => {
@@ -183,6 +185,9 @@ export async function main(argv) {
183
185
  agent = makeAgent(cfg, router, bus, context, registry, permissions, systemPrompt, cwd);
184
186
  emitRoute(bus, router, cfg);
185
187
  saveConfig(cfg);
188
+ // Keep the TUI's model list + picker rows in sync with the new route.
189
+ app?.setModels(router.order());
190
+ void app?.refreshModelPicker();
186
191
  });
187
192
  }
188
193
  const onCommand = async (cmd) => {
@@ -236,18 +241,21 @@ export async function main(argv) {
236
241
  bus.on('onError', (err) => {
237
242
  appendEvent(session.id, { level: 'error', type: 'error', data: { message: err.message } });
238
243
  });
239
- const app = new TuiApp({
244
+ app = new TuiApp({
240
245
  config: cfg,
241
246
  bus,
242
247
  onSubmit: (text) => submitQueued(text),
243
248
  onCommand,
244
249
  version: VERSION,
245
250
  models: router.order(),
251
+ pickRows: (stage, providerId, liveModels) => stage === 'providers' ? Promise.resolve(resolveProviderRows()) : resolveModelRows(providerId ?? '', liveModels),
246
252
  });
247
253
  app.store.skills = project.skills.map((s) => ({ name: s.name, summary: s.summary }));
248
254
  const resumeMessages = session.messages.filter((m) => m.role !== 'tool');
249
255
  if (resumeMessages.length > 0)
250
256
  app.store.messages = resumeMessages;
257
+ // Warm the grouped overlay rows once at startup so /model opens instantly.
258
+ void app.refreshModelPicker();
251
259
  function makeAgent(config, r, evBus, ctx, reg, perms, sys, wd) {
252
260
  return new Orchestrator({
253
261
  bus: evBus,
@@ -4,7 +4,17 @@ import { z } from 'zod';
4
4
  * Provider keys are NEVER stored here — they live in env vars or the
5
5
  * git-ignored keys.json (0600 perms). Read via core/llm/secrets.ts.
6
6
  */
7
- export const providerNames = ['orbitx', 'groq', 'gemini', 'openrouter', 'openai'];
7
+ export const providerNames = [
8
+ 'orbitx',
9
+ 'groq',
10
+ 'gemini',
11
+ 'openrouter',
12
+ 'openai',
13
+ 'anthropic',
14
+ 'grok',
15
+ 'deepseek',
16
+ 'ollama',
17
+ ];
8
18
  export const effortLevels = ['none', 'minimal', 'low', 'medium', 'high'];
9
19
  export const reasoningDefault = { enabled: true, effort: 'medium' };
10
20
  export const routerStrategies = ['auto', 'failover', 'round-robin', 'pinned'];
@@ -1,3 +1,4 @@
1
+ import { sanitizeToken, sanitizeContent } from '../llm/sanitize.js';
1
2
  import { combineSignals } from '../../utils/signals.js';
2
3
  import { unifiedDiff, countChanges } from '../../utils/diff.js';
3
4
  import { readFile, stat } from 'node:fs/promises';
@@ -87,13 +88,15 @@ export class AgentLoop {
87
88
  try {
88
89
  for await (const ev of gen) {
89
90
  if (ev.type === 'token') {
90
- asstMsg.content += ev.text;
91
- bus.emit('onToken', ev.text);
91
+ const clean = sanitizeToken(ev.text);
92
+ asstMsg.content += clean;
93
+ bus.emit('onToken', clean);
92
94
  await throttle(this.opts.maxTokensPerSecond);
93
95
  }
94
96
  else if (ev.type === 'reasoning') {
95
- asstMsg.reasoning = (asstMsg.reasoning ?? '') + ev.text;
96
- bus.emit('onThinking', ev.text);
97
+ const clean = sanitizeToken(ev.text);
98
+ asstMsg.reasoning = (asstMsg.reasoning ?? '') + clean;
99
+ bus.emit('onThinking', clean);
97
100
  }
98
101
  else if (ev.type === 'tool_call_start') {
99
102
  const call = {
@@ -153,6 +156,8 @@ export class AgentLoop {
153
156
  // TUI reveal the whole message (snap the typewriter to the end) so the
154
157
  // text sits above the tool rows BEFORE the tools start running — the
155
158
  // user should see "intent first, then action".
159
+ asstMsg.content = sanitizeContent(asstMsg.content);
160
+ asstMsg.reasoning = sanitizeContent(asstMsg.reasoning ?? '');
156
161
  bus.emit('onAssistantGenerationDone', asstMsg);
157
162
  asstMsg.streaming = false;
158
163
  asstMsg.reasoningOpen = (asstMsg.reasoning?.length ?? 0) > 0;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Custom OpenAI-compatible endpoint registry.
3
+ *
4
+ * Lets users add named endpoints (LM Studio, vLLM, a personal gateway, …)
5
+ * with a base URL and an OPTIONAL key. Persisted to the git-ignored
6
+ * `endpoints.json` (0600) next to keys.json. Base URLs are not secrets, but
7
+ * endpoint keys are — keeping both in one 0600 file matches the keys.json
8
+ * security posture (never config.json).
9
+ */
10
+ import { readFileSync, existsSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
11
+ import { endpointsPath, configDir } from '../../utils/platform.js';
12
+ const CACHE_MS = 2000;
13
+ let cache = null;
14
+ function readFile() {
15
+ const now = Date.now();
16
+ if (cache && now - cache.at < CACHE_MS)
17
+ return cache.data;
18
+ const path = endpointsPath();
19
+ if (!existsSync(path)) {
20
+ cache = { data: [], at: now };
21
+ return [];
22
+ }
23
+ try {
24
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
25
+ if (!Array.isArray(raw))
26
+ throw new Error('not an array');
27
+ const cleaned = raw.filter((e) => e && typeof e.name === 'string' && typeof e.baseUrl === 'string');
28
+ cache = { data: cleaned, at: now };
29
+ return cleaned;
30
+ }
31
+ catch {
32
+ cache = { data: [], at: now };
33
+ return [];
34
+ }
35
+ }
36
+ function write(list) {
37
+ mkdirSync(configDir(), { recursive: true });
38
+ writeFileSync(endpointsPath(), JSON.stringify(list, null, 2), 'utf8');
39
+ try {
40
+ chmodSync(endpointsPath(), 0o600);
41
+ }
42
+ catch {
43
+ /* best-effort */
44
+ }
45
+ cache = { data: list, at: Date.now() };
46
+ }
47
+ export function loadEndpoints() {
48
+ return readFile();
49
+ }
50
+ export function findEndpoint(name) {
51
+ return readFile().find((e) => e.name === name) ?? null;
52
+ }
53
+ /** Upsert by name (case-sensitive). Returns an error string, or null on success. */
54
+ export function saveEndpoint(ep) {
55
+ const trimmed = { ...ep, name: ep.name.trim(), baseUrl: ep.baseUrl.trim() };
56
+ if (!/^[a-z0-9][a-z0-9._-]{0,39}$/.test(trimmed.name)) {
57
+ return 'name must be 1-40 chars: lowercase letters, digits, . _ -';
58
+ }
59
+ if (!/^https?:\/\/\S+$/.test(trimmed.baseUrl)) {
60
+ return 'base URL must start with http:// or https://';
61
+ }
62
+ if (!trimmed.baseUrl.endsWith('/v1') && !/\/(v1|v1beta1)\/?$/.test(trimmed.baseUrl)) {
63
+ trimmed.baseUrl = `${trimmed.baseUrl.replace(/\/+$/, '')}/v1`;
64
+ }
65
+ const list = readFile();
66
+ const idx = list.findIndex((e) => e.name === trimmed.name);
67
+ if (idx >= 0)
68
+ list[idx] = trimmed;
69
+ else
70
+ list.push(trimmed);
71
+ write(list);
72
+ return null;
73
+ }
74
+ export function deleteEndpoint(name) {
75
+ write(readFile().filter((e) => e.name !== name));
76
+ }
77
+ /** Remember a model id the user actually picked for this endpoint. */
78
+ export function rememberEndpointModel(name, model) {
79
+ const list = readFile();
80
+ const ep = list.find((e) => e.name === name);
81
+ if (!ep)
82
+ return;
83
+ const models = ep.models ?? [];
84
+ if (!models.includes(model)) {
85
+ ep.models = [...models, model].slice(-20);
86
+ write(list);
87
+ }
88
+ }
89
+ /** Probe the endpoint's live /v1/models list (fast-fail, short timeout). */
90
+ export async function listEndpointModels(name) {
91
+ const ep = findEndpoint(name);
92
+ if (!ep)
93
+ return [];
94
+ try {
95
+ const ctrl = new AbortController();
96
+ const timer = setTimeout(() => ctrl.abort(new Error('endpoint probe timed out')), 3000);
97
+ const headers = { accept: 'application/json' };
98
+ if (ep.key)
99
+ headers.authorization = `Bearer ${ep.key}`;
100
+ const res = await fetch(`${ep.baseUrl.replace(/\/+$/, '')}/models`, { signal: ctrl.signal, headers });
101
+ clearTimeout(timer);
102
+ if (!res.ok)
103
+ throw new Error(`HTTP ${res.status}`);
104
+ const json = (await res.json());
105
+ return (json.data ?? []).map((m) => m.id).filter(Boolean);
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ }
@@ -155,6 +155,19 @@ function jsonSafe(s) {
155
155
  }
156
156
  }
157
157
  function appendReasoning(body, model, reasoning, style) {
158
+ if (style === 'deepseek') {
159
+ // DeepSeek: `thinking:{type}` toggles the chain-of-thought stream;
160
+ // `reasoning_effort` (low/high/max) controls reasoning depth in newer
161
+ // v4 models. Our minimal/low → low, medium → high, high → high.
162
+ if (reasoning.enabled === false) {
163
+ body.thinking = { type: 'disabled' };
164
+ return;
165
+ }
166
+ body.thinking = { type: 'enabled' };
167
+ const effort = reasoning.effort ?? 'medium';
168
+ body.reasoning_effort = effort === 'high' ? 'high' : effort === 'minimal' || effort === 'low' ? 'low' : 'high';
169
+ return;
170
+ }
158
171
  if (style === 'effort') {
159
172
  // Groq: top-level `reasoning_effort`. `none` is valid for qwen3 family.
160
173
  const effort = reasoning.enabled === false ? 'none' : (reasoning.effort ?? 'medium');
@@ -1,11 +1,22 @@
1
1
  import { getProviderSecrets } from './secrets.js';
2
2
  import { createOpenAiProvider } from './providers/openai-compat.js';
3
3
  import { createGeminiProvider } from './providers/gemini.js';
4
+ import { createAnthropicProvider } from './providers/anthropic.js';
5
+ import { createOllamaProvider } from './providers/ollama.js';
6
+ import { loadEndpoints } from './endpoints.js';
4
7
  import { PROVIDER_CATALOGS, ORBITX_SERVE, modelsOf } from './models.js';
5
8
  export { PROVIDER_CATALOGS as PROVIDER_SPECS, ORBITX_SERVE, modelsOf };
6
9
  const ENDPOINTS = {
7
10
  groq: 'https://api.groq.com/openai/v1',
8
11
  openrouter: 'https://openrouter.ai/api/v1',
12
+ openai: 'https://api.openai.com/v1',
13
+ grok: 'https://api.x.ai/v1',
14
+ deepseek: 'https://api.deepseek.com',
15
+ };
16
+ const REASONING_STYLES = {
17
+ groq: 'effort',
18
+ grok: 'effort',
19
+ deepseek: 'deepseek',
9
20
  };
10
21
  /**
11
22
  * Build providers available in the current environment.
@@ -42,13 +53,34 @@ export function buildProviders(config) {
42
53
  baseUrl,
43
54
  apiKey: sec.keys[0],
44
55
  models: spec ? modelsOf(spec) : [],
45
- reasoningStyle: id === 'groq' ? 'effort' : 'object',
56
+ reasoningStyle: REASONING_STYLES[id] ?? 'object',
46
57
  });
47
58
  }
48
59
  const geminiSec = getProviderSecrets('gemini');
49
60
  if (geminiSec && geminiSec.keys[0]) {
50
61
  providers.gemini = createGeminiProvider(geminiSec.keys[0]);
51
62
  }
63
+ const anthropicSec = getProviderSecrets('anthropic');
64
+ if (anthropicSec && anthropicSec.keys[0]) {
65
+ providers.anthropic = createAnthropicProvider(anthropicSec.keys[0]);
66
+ }
67
+ // Local Ollama is always offered: no key required, auto-detects the server.
68
+ providers.ollama = createOllamaProvider();
69
+ // Named custom OpenAI-compatible endpoints (endpoints.json). Each is exposed
70
+ // as a provider keyed by its name so `<name>/<model>` routes to it.
71
+ for (const ep of loadEndpoints()) {
72
+ if (!ep || !ep.name || !ep.baseUrl)
73
+ continue;
74
+ const existing = providers[ep.name];
75
+ if (existing)
76
+ continue; // never shadow a real provider with a same-named endpoint
77
+ providers[ep.name] = createOpenAiProvider({
78
+ id: ep.name,
79
+ baseUrl: ep.baseUrl,
80
+ apiKey: ep.key ?? '',
81
+ models: (ep.models ?? []).filter(Boolean),
82
+ });
83
+ }
52
84
  return providers;
53
85
  }
54
86
  /** Candidate model ids (provider-prefixed) for the router. */
@@ -89,5 +121,11 @@ export async function resolveCandidates(config, providers) {
89
121
  push(m);
90
122
  }
91
123
  }
124
+ // Always surface local Ollama models so /model can switch to them from any
125
+ // provider. listModels() returns [] fast when the server is unreachable.
126
+ if (providers.ollama) {
127
+ for (const m of await providers.ollama.listModels())
128
+ push(m);
129
+ }
92
130
  return candidates;
93
131
  }
@@ -6,32 +6,38 @@ export const GROQ_CATALOG = [
6
6
  {
7
7
  id: 'openai/gpt-oss-120b',
8
8
  label: 'OpenAI GPT-OSS 120B',
9
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
9
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
10
10
  },
11
11
  {
12
12
  id: 'openai/gpt-oss-20b',
13
13
  label: 'OpenAI GPT-OSS 20B',
14
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
14
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
15
15
  },
16
16
  {
17
- id: 'qwen/qwen3-32b',
18
- label: 'Qwen3 32B',
17
+ id: 'openai/gpt-oss-safeguard-20b',
18
+ label: 'OpenAI GPT-OSS Safeguard 20B',
19
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
20
+ },
21
+ {
22
+ id: 'qwen/qwen3.8-27b',
23
+ label: 'Qwen3.8 27B',
19
24
  reasoning: r('effort', ['none', 'low', 'medium', 'high'], 'none', {
20
25
  paramValue: { none: 'none', low: 'default', medium: 'default', high: 'default' },
21
26
  }),
22
27
  },
23
28
  {
24
- id: 'qwen/qwen3.6-27b',
25
- label: 'Qwen3.6 27B',
29
+ id: 'qwen/qwen3-32b',
30
+ label: 'Qwen3 32B',
26
31
  reasoning: r('effort', ['none', 'low', 'medium', 'high'], 'none', {
27
32
  paramValue: { none: 'none', low: 'default', medium: 'default', high: 'default' },
28
33
  }),
29
34
  },
30
- { id: 'meta-llama/llama-4-maverick-17b-128e-instruct', label: 'Llama 4 Maverick 17B' },
35
+ { id: 'minimaxai/minimax-m2.7', label: 'MiniMax M2.7' },
31
36
  { id: 'meta-llama/llama-4-scout-17b-16e-instruct', label: 'Llama 4 Scout 17B' },
32
- { id: 'meta-llama/llama-3.3-70b-versatile', label: 'Llama 3.3 70B' },
33
- { id: 'meta-llama/llama-3.1-8b-instant', label: 'Llama 3.1 8B' },
34
- { id: 'moonshotai/kimi-k2-instruct-0905', label: 'Kimi K2 Instruct' },
37
+ { id: 'meta-llama/llama-3.3-70b-versatile', label: 'Llama 3.3 70B Versatile' },
38
+ { id: 'meta-llama/llama-3.1-8b-instant', label: 'Llama 3.1 8B Instant' },
39
+ { id: 'groq/compound', label: 'Groq Compound (router)' },
40
+ { id: 'groq/compound-mini', label: 'Groq Compound Mini (router)' },
35
41
  ];
36
42
  export const GEMINI_CATALOG = [
37
43
  {
@@ -55,14 +61,14 @@ export const GEMINI_CATALOG = [
55
61
  reasoning: r('level', ['minimal', 'low', 'medium', 'high'], 'medium'),
56
62
  },
57
63
  {
58
- id: 'gemini-3.1-flash-lite',
59
- label: 'Gemini 3.1 Flash-Lite',
64
+ id: 'gemini-3.5-flash-lite',
65
+ label: 'Gemini 3.5 Flash-Lite',
60
66
  reasoning: r('level', ['minimal', 'low', 'medium', 'high'], 'minimal'),
61
67
  },
62
68
  {
63
- id: 'gemini-3-pro-preview',
64
- label: 'Gemini 3 Pro',
65
- reasoning: r('level', ['low', 'high'], 'high'),
69
+ id: 'gemini-3.1-flash-lite',
70
+ label: 'Gemini 3.1 Flash-Lite',
71
+ reasoning: r('level', ['minimal', 'low', 'medium', 'high'], 'minimal'),
66
72
  },
67
73
  {
68
74
  id: 'gemini-3.1-pro-preview',
@@ -96,90 +102,196 @@ export const OPENROUTER_CATALOG = [
96
102
  {
97
103
  id: 'anthropic/claude-opus-4.8',
98
104
  label: 'Claude Opus 4.8',
99
- reasoning: r('none', ['low', 'medium', 'high'], 'medium', { switchable: false }),
105
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
100
106
  },
101
- { id: 'anthropic/claude-sonnet-4.7', label: 'Claude Sonnet 4.7' },
102
- { id: 'anthropic/claude-haiku-4.5', label: 'Claude Haiku 4.5' },
103
107
  {
104
- id: 'openai/o3',
105
- label: 'OpenAI o3',
108
+ id: 'anthropic/claude-sonnet-5',
109
+ label: 'Claude Sonnet 5',
106
110
  reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
107
111
  },
108
112
  {
109
- id: 'openai/o4-mini',
110
- label: 'OpenAI o4-mini',
113
+ id: 'anthropic/claude-sonnet-4.6',
114
+ label: 'Claude Sonnet 4.6',
115
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
116
+ },
117
+ {
118
+ id: 'anthropic/claude-opus-5',
119
+ label: 'Claude Opus 5',
120
+ reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
121
+ },
122
+ { id: 'anthropic/claude-haiku-4.5', label: 'Claude Haiku 4.5' },
123
+ {
124
+ id: 'openai/gpt-5.5',
125
+ label: 'GPT-5.5',
111
126
  reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
112
127
  },
113
128
  {
114
129
  id: 'openai/gpt-oss-120b',
115
130
  label: 'OpenAI GPT-OSS 120B',
116
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
131
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
117
132
  },
118
133
  {
119
134
  id: 'openai/gpt-oss-20b',
120
135
  label: 'OpenAI GPT-OSS 20B',
121
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
136
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
122
137
  },
123
138
  { id: 'google/gemini-3.5-flash', label: 'Gemini 3.5 Flash' },
124
- { id: 'google/gemini-3-pro-preview', label: 'Gemini 3 Pro' },
125
- { id: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
139
+ { id: 'google/gemini-3-flash-preview', label: 'Gemini 3 Flash (preview)' },
140
+ { id: 'google/gemini-3.1-flash-lite', label: 'Gemini 3.1 Flash-Lite' },
141
+ { id: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
142
+ { id: 'google/gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite' },
143
+ { id: 'google/gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro' },
126
144
  {
127
145
  id: 'deepseek/deepseek-v4-pro',
128
146
  label: 'DeepSeek V4 Pro',
129
147
  reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
130
148
  },
131
- { id: 'deepseek/deepseek-v4-flash', label: 'DeepSeek V4 Flash' },
132
149
  {
133
- id: 'deepseek/deepseek-r1',
134
- label: 'DeepSeek R1',
135
- reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
150
+ id: 'deepseek/deepseek-v4-flash',
151
+ label: 'DeepSeek V4 Flash',
152
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
136
153
  },
137
154
  {
138
- id: 'qwen/qwen3.7-max',
139
- label: 'Qwen3.7 Max',
140
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
155
+ id: 'x-ai/grok-4.3',
156
+ label: 'Grok 4.3',
157
+ reasoning: r('effort', ['none', 'low', 'medium', 'high'], 'none'),
158
+ },
159
+ {
160
+ id: 'x-ai/grok-4.6',
161
+ label: 'Grok 4.6',
162
+ reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
141
163
  },
142
164
  {
143
- id: 'x-ai/grok-build-0.1',
144
- label: 'Grok Build 0.1',
165
+ id: 'minimax/minimax-m3',
166
+ label: 'MiniMax M3',
145
167
  reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
146
168
  },
147
- { id: 'x-ai/grok-4.3', label: 'Grok 4.3' },
148
- { id: 'minimax/minimax-m3', label: 'MiniMax M3' },
169
+ { id: 'moonshotai/kimi-k3', label: 'Kimi K3' },
149
170
  { id: 'nvidia/nemotron-3-ultra', label: 'NVIDIA Nemotron 3 Ultra' },
150
- { id: 'moonshotai/kimi-k2.7-code', label: 'Kimi K2.7 Code' },
151
- { id: 'mistralai/mistral-medium-3-5', label: 'Mistral Medium 3.5' },
152
- // Free (`:free` tiers)
153
- { id: 'openrouter/free', label: 'Auto — free router', free: true },
154
171
  {
155
- id: 'thinkingmachines/inkling:free',
156
- label: 'Inkling (free)',
157
- free: true,
172
+ id: 'z-ai/glm-5.2',
173
+ label: 'GLM 5.2',
158
174
  reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
159
175
  },
176
+ { id: 'tencent/hy3', label: 'Tencent Hunyuan 3' },
177
+ { id: 'stepfun/step-3.7-flash', label: 'Step 3.7 Flash' },
178
+ { id: 'xiaomi/mimo-v2.5', label: 'Xiaomi MiMo V2.5' },
179
+ // Free (`:free` tiers)
160
180
  {
161
- id: 'thinkingmachines/inkling-small:free',
162
- label: 'Inkling Small (free)',
181
+ id: 'openai/gpt-oss-120b:free',
182
+ label: 'GPT-OSS 120B (free)',
163
183
  free: true,
164
- reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
184
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
165
185
  },
166
- { id: 'minimax/minimax-m3:free', label: 'MiniMax M3 (free)', free: true },
167
186
  {
168
- id: 'deepseek/deepseek-r1:free',
169
- label: 'DeepSeek R1 (free)',
187
+ id: 'openai/gpt-oss-20b:free',
188
+ label: 'GPT-OSS 20B (free)',
170
189
  free: true,
171
- reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
190
+ reasoning: r('effort', ['minimal', 'low', 'medium', 'high'], 'medium'),
172
191
  },
173
- { id: 'openai/gpt-oss-120b:free', label: 'GPT-OSS 120B (free)', free: true },
174
- { id: 'openai/gpt-oss-20b:free', label: 'GPT-OSS 20B (free)', free: true },
175
- { id: 'nvidia/nemotron-3-ultra:free', label: 'Nemotron 3 Ultra (free)', free: true },
176
- { id: 'nvidia/nemotron-3-super:free', label: 'Nemotron 3 Super (free)', free: true },
177
- { id: 'nvidia/nemotron-3.5-lightning:free', label: 'Nemotron 3.5 Light (free)', free: true },
178
192
  { id: 'google/gemma-4-31b:free', label: 'Gemma 4 31B (free)', free: true },
179
- { id: 'google/gemma-4-26b-a4b:free', label: 'Gemma 4 26B (free)', free: true },
180
- { id: 'cohere/north-mini-code:free', label: 'North Mini Code (free)', free: true },
181
193
  { id: 'meta-llama/llama-3.3-70b-instruct:free', label: 'Llama 3.3 70B (free)', free: true },
182
194
  { id: 'meta-llama/llama-3.1-8b-instruct:free', label: 'Llama 3.1 8B (free)', free: true },
195
+ { id: 'minimax/minimax-m3:free', label: 'MiniMax M3 (free)', free: true },
196
+ { id: 'nvidia/nemotron-3-ultra:free', label: 'Nemotron 3 Ultra (free)', free: true },
197
+ ];
198
+ export const ANTHROPIC_CATALOG = [
199
+ {
200
+ id: 'claude-fable-5-1',
201
+ label: 'Claude Fable 5.1',
202
+ reasoning: r('budget', ['low', 'medium', 'high'], 'high', { switchable: false }),
203
+ },
204
+ {
205
+ id: 'claude-opus-5',
206
+ label: 'Claude Opus 5',
207
+ reasoning: r('budget', ['low', 'medium', 'high'], 'high', { switchable: false }),
208
+ },
209
+ {
210
+ id: 'claude-sonnet-5',
211
+ label: 'Claude Sonnet 5',
212
+ reasoning: r('budget', ['low', 'medium', 'high'], 'medium', { switchable: false }),
213
+ },
214
+ {
215
+ id: 'claude-haiku-4-5',
216
+ label: 'Claude Haiku 4.5',
217
+ reasoning: r('budget', ['low', 'medium', 'high'], 'low', { switchable: true }),
218
+ },
219
+ {
220
+ id: 'claude-sonnet-4-6',
221
+ label: 'Claude Sonnet 4.6 (legacy)',
222
+ reasoning: r('budget', ['low', 'medium', 'high'], 'medium', { switchable: true }),
223
+ },
224
+ {
225
+ id: 'claude-opus-4-8',
226
+ label: 'Claude Opus 4.8 (legacy)',
227
+ reasoning: r('budget', ['low', 'medium', 'high'], 'medium', { switchable: true }),
228
+ },
229
+ ];
230
+ export const GROK_CATALOG = [
231
+ {
232
+ id: 'grok-4.6',
233
+ label: 'Grok 4.6',
234
+ reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
235
+ },
236
+ {
237
+ id: 'grok-4.5',
238
+ label: 'Grok 4.5',
239
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
240
+ },
241
+ {
242
+ id: 'grok-4.3',
243
+ label: 'Grok 4.3',
244
+ reasoning: r('effort', ['none', 'low', 'medium', 'high'], 'none'),
245
+ },
246
+ {
247
+ id: 'grok-4.20-reasoning',
248
+ label: 'Grok 4.20 Reasoning',
249
+ reasoning: r('effort', ['low', 'medium', 'high'], 'high'),
250
+ },
251
+ { id: 'grok-4.20-non-reasoning', label: 'Grok 4.20 (non-reasoning)' },
252
+ { id: 'grok-3-mini', label: 'Grok 3 Mini' },
253
+ ];
254
+ export const DEEPSEEK_CATALOG = [
255
+ {
256
+ id: 'deepseek-v4-pro',
257
+ label: 'DeepSeek V4 Pro (0813)',
258
+ reasoning: r('deepseek', ['low', 'medium', 'high'], 'medium'),
259
+ },
260
+ {
261
+ id: 'deepseek-flash',
262
+ label: 'DeepSeek V4 Flash (0731)',
263
+ reasoning: r('deepseek', ['low', 'medium', 'high'], 'low'),
264
+ },
265
+ {
266
+ id: 'deepseek-v4-flash-vision-exp',
267
+ label: 'DeepSeek V4 Flash Vision (exp)',
268
+ reasoning: r('deepseek', ['low', 'medium', 'high'], 'low'),
269
+ },
270
+ ];
271
+ export const OPENAI_CATALOG = [
272
+ {
273
+ id: 'gpt-5.6',
274
+ label: 'GPT-5.6',
275
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
276
+ },
277
+ {
278
+ id: 'gpt-5.5',
279
+ label: 'GPT-5.5',
280
+ reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
281
+ },
282
+ { id: 'gpt-4o', label: 'GPT-4o (legacy)' },
283
+ ];
284
+ export const OLLAMA_CATALOG = [
285
+ { id: 'llama3.2', label: 'Llama 3.2' },
286
+ { id: 'llama3.1', label: 'Llama 3.1' },
287
+ { id: 'qwen3', label: 'Qwen3' },
288
+ { id: 'qwen3:32b', label: 'Qwen3 32B' },
289
+ { id: 'qwen3-vl:8b', label: 'Qwen3 VL 8B' },
290
+ { id: 'gemma4', label: 'Gemma 4' },
291
+ { id: 'gemma3', label: 'Gemma 3' },
292
+ { id: 'mistral', label: 'Mistral' },
293
+ { id: 'gpt-oss:20b', label: 'GPT-OSS 20B' },
294
+ { id: 'deepseek-r1', label: 'DeepSeek R1' },
183
295
  ];
184
296
  export const ORBITX_CATALOG = [
185
297
  { id: 'auto', label: 'auto (backend routes across providers)' },
@@ -210,6 +322,11 @@ export const PROVIDER_CATALOGS = [
210
322
  { id: 'groq', label: 'Groq', requiresSecret: true, supportsCustom: false, models: GROQ_CATALOG },
211
323
  { id: 'gemini', label: 'Google Gemini', requiresSecret: true, supportsCustom: false, models: GEMINI_CATALOG },
212
324
  { id: 'openrouter', label: 'OpenRouter', requiresSecret: true, supportsCustom: true, models: OPENROUTER_CATALOG },
325
+ { id: 'openai', label: 'OpenAI', requiresSecret: true, supportsCustom: true, models: OPENAI_CATALOG },
326
+ { id: 'anthropic', label: 'Anthropic', requiresSecret: true, supportsCustom: false, models: ANTHROPIC_CATALOG },
327
+ { id: 'grok', label: 'xAI Grok', requiresSecret: true, supportsCustom: false, models: GROK_CATALOG },
328
+ { id: 'deepseek', label: 'DeepSeek', requiresSecret: true, supportsCustom: false, models: DEEPSEEK_CATALOG },
329
+ { id: 'ollama', label: 'Ollama (local)', requiresSecret: false, supportsCustom: true, models: OLLAMA_CATALOG },
213
330
  ];
214
331
  /** Find a model entry by bare id across every provider catalog. */
215
332
  export function findModel(provider, modelBare) {