@orbit-intelligence/orbit-agent 0.3.12 → 0.3.14
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/dist/prompts/system.js +6 -1
- package/dist/src/cli/run.js +10 -2
- package/dist/src/config/config-schema.js +11 -1
- package/dist/src/core/agent/agent-loop.js +5 -0
- package/dist/src/core/llm/http.js +13 -0
- package/dist/src/core/llm/index.js +23 -1
- package/dist/src/core/llm/models.js +175 -58
- package/dist/src/core/llm/providers/anthropic.js +237 -0
- package/dist/src/core/llm/providers/ollama.js +86 -0
- package/dist/src/core/llm/secrets.js +13 -3
- package/dist/src/core/orchestration/runtime.js +1 -0
- package/dist/src/setup/wizard.js +7 -1
- package/dist/src/tui/InkApp.js +2 -2
- package/dist/src/tui/app.js +86 -11
- package/dist/src/tui/components/ModelPicker.js +17 -12
- package/dist/src/tui/picker.js +74 -0
- package/dist/src/tui/store.js +4 -2
- package/package.json +1 -1
package/dist/prompts/system.js
CHANGED
|
@@ -56,6 +56,11 @@ ${skillsBlock}
|
|
|
56
56
|
4. Verify. Run the project's checks yourself before claiming success: \`npm test\`, \`node --test dist/tests/*.test.js\`, \`npm run typecheck\`, \`npm run build\` — as applicable. NEVER assume test commands; find them in package.json.
|
|
57
57
|
5. Reflect. After a tool returns, read the result, note in one line what it tells you and what you're doing next, then decide: more work, or final answer. Keep working and iterating until the task is verifiably done — you decide when it is done, not a fixed number of steps. If a step yields unexpected results, adapt and continue.
|
|
58
58
|
|
|
59
|
+
# Narration order (output protocol)
|
|
60
|
+
- Every message that calls a tool MUST begin with a short narration BEFORE the first tool call, in the same message — one sentence stating what the imminent call will do and why. Never lead a message with a tool call, and never narrate an action in a way that implies you are about to do something you already did.
|
|
61
|
+
- Say the intent, then act. For example: "I'll list the directory to see the layout." then call list_dir. Do not list the directory first and explain afterwards.
|
|
62
|
+
- Batch: when several calls are truly independent, put them in one message instead of one call per round trip. Fewer round trips = faster for the user.
|
|
63
|
+
|
|
59
64
|
# Tool discipline
|
|
60
65
|
- Explore with glob/grep before reading whole files. Use absolute paths with file tools.
|
|
61
66
|
- run_shell: one focused command per call. Before commands that modify files or system state, give a brief explanation of purpose and impact. Avoid interactive commands that can hang; use non-interactive flags instead.
|
|
@@ -65,7 +70,7 @@ ${skillsBlock}
|
|
|
65
70
|
|
|
66
71
|
# Output style
|
|
67
72
|
- Be concise and direct. Keep responses short unless the user asks for detail.
|
|
68
|
-
- Show your working across steps:
|
|
73
|
+
- Show your working across steps: after a tool result, give a one-line finding/next step. Do not recap the whole session at the end — just give the final answer.
|
|
69
74
|
- Use Markdown for structure. Quote exact paths (path:line) when referring to code.
|
|
70
75
|
- No emojis unless asked. Do not add code explanations unless asked.
|
|
71
76
|
|
package/dist/src/cli/run.js
CHANGED
|
@@ -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 { resolvePickerRows } 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
|
-
|
|
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: () => resolvePickerRows(),
|
|
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 = [
|
|
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'];
|
|
@@ -149,6 +149,11 @@ export class AgentLoop {
|
|
|
149
149
|
return { turns: this.turnCount, error: err?.message ?? String(err) };
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
// The narration is fully streamed and tools haven't executed yet. Let the
|
|
153
|
+
// TUI reveal the whole message (snap the typewriter to the end) so the
|
|
154
|
+
// text sits above the tool rows BEFORE the tools start running — the
|
|
155
|
+
// user should see "intent first, then action".
|
|
156
|
+
bus.emit('onAssistantGenerationDone', asstMsg);
|
|
152
157
|
asstMsg.streaming = false;
|
|
153
158
|
asstMsg.reasoningOpen = (asstMsg.reasoning?.length ?? 0) > 0;
|
|
154
159
|
asstMsg.toolCalls = toolCalls;
|
|
@@ -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,21 @@
|
|
|
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';
|
|
4
6
|
import { PROVIDER_CATALOGS, ORBITX_SERVE, modelsOf } from './models.js';
|
|
5
7
|
export { PROVIDER_CATALOGS as PROVIDER_SPECS, ORBITX_SERVE, modelsOf };
|
|
6
8
|
const ENDPOINTS = {
|
|
7
9
|
groq: 'https://api.groq.com/openai/v1',
|
|
8
10
|
openrouter: 'https://openrouter.ai/api/v1',
|
|
11
|
+
openai: 'https://api.openai.com/v1',
|
|
12
|
+
grok: 'https://api.x.ai/v1',
|
|
13
|
+
deepseek: 'https://api.deepseek.com',
|
|
14
|
+
};
|
|
15
|
+
const REASONING_STYLES = {
|
|
16
|
+
groq: 'effort',
|
|
17
|
+
grok: 'effort',
|
|
18
|
+
deepseek: 'deepseek',
|
|
9
19
|
};
|
|
10
20
|
/**
|
|
11
21
|
* Build providers available in the current environment.
|
|
@@ -42,13 +52,19 @@ export function buildProviders(config) {
|
|
|
42
52
|
baseUrl,
|
|
43
53
|
apiKey: sec.keys[0],
|
|
44
54
|
models: spec ? modelsOf(spec) : [],
|
|
45
|
-
reasoningStyle: id
|
|
55
|
+
reasoningStyle: REASONING_STYLES[id] ?? 'object',
|
|
46
56
|
});
|
|
47
57
|
}
|
|
48
58
|
const geminiSec = getProviderSecrets('gemini');
|
|
49
59
|
if (geminiSec && geminiSec.keys[0]) {
|
|
50
60
|
providers.gemini = createGeminiProvider(geminiSec.keys[0]);
|
|
51
61
|
}
|
|
62
|
+
const anthropicSec = getProviderSecrets('anthropic');
|
|
63
|
+
if (anthropicSec && anthropicSec.keys[0]) {
|
|
64
|
+
providers.anthropic = createAnthropicProvider(anthropicSec.keys[0]);
|
|
65
|
+
}
|
|
66
|
+
// Local Ollama is always offered: no key required, auto-detects the server.
|
|
67
|
+
providers.ollama = createOllamaProvider();
|
|
52
68
|
return providers;
|
|
53
69
|
}
|
|
54
70
|
/** Candidate model ids (provider-prefixed) for the router. */
|
|
@@ -89,5 +105,11 @@ export async function resolveCandidates(config, providers) {
|
|
|
89
105
|
push(m);
|
|
90
106
|
}
|
|
91
107
|
}
|
|
108
|
+
// Always surface local Ollama models so /model can switch to them from any
|
|
109
|
+
// provider. listModels() returns [] fast when the server is unreachable.
|
|
110
|
+
if (providers.ollama) {
|
|
111
|
+
for (const m of await providers.ollama.listModels())
|
|
112
|
+
push(m);
|
|
113
|
+
}
|
|
92
114
|
return candidates;
|
|
93
115
|
}
|
|
@@ -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: '
|
|
18
|
-
label: '
|
|
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
|
|
25
|
-
label: 'Qwen3
|
|
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: '
|
|
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: '
|
|
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.
|
|
59
|
-
label: 'Gemini 3.
|
|
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-
|
|
64
|
-
label: 'Gemini 3
|
|
65
|
-
reasoning: r('level', ['low', '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('
|
|
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: '
|
|
105
|
-
label: '
|
|
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: '
|
|
110
|
-
label: '
|
|
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-
|
|
125
|
-
{ id: 'google/gemini-
|
|
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-
|
|
134
|
-
label: 'DeepSeek
|
|
135
|
-
reasoning: r('effort', ['low', 'medium', '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: '
|
|
139
|
-
label: '
|
|
140
|
-
reasoning: r('effort', ['low', 'medium', 'high'], '
|
|
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: '
|
|
144
|
-
label: '
|
|
165
|
+
id: 'minimax/minimax-m3',
|
|
166
|
+
label: 'MiniMax M3',
|
|
145
167
|
reasoning: r('effort', ['low', 'medium', 'high'], 'medium'),
|
|
146
168
|
},
|
|
147
|
-
{ id: '
|
|
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: '
|
|
156
|
-
label: '
|
|
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: '
|
|
162
|
-
label: '
|
|
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: '
|
|
169
|
-
label: '
|
|
187
|
+
id: 'openai/gpt-oss-20b:free',
|
|
188
|
+
label: 'GPT-OSS 20B (free)',
|
|
170
189
|
free: true,
|
|
171
|
-
reasoning: r('effort', ['low', 'medium', '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) {
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { ANTHROPIC_CATALOG } from '../models.js';
|
|
2
|
+
/**
|
|
3
|
+
* Anthropic native streaming provider (Messages API).
|
|
4
|
+
* HTTP: POST https://api.anthropic.com/v1/messages with `x-api-key` +
|
|
5
|
+
* `anthropic-version` headers, SSE stream.
|
|
6
|
+
*
|
|
7
|
+
* Event mapping:
|
|
8
|
+
* text block → token
|
|
9
|
+
* thinking block → reasoning
|
|
10
|
+
* tool_use block → tool_call_start / tool_call_args / tool_call_end
|
|
11
|
+
*/
|
|
12
|
+
const ANTHROPIC_ENDPOINT = 'https://api.anthropic.com/v1/messages';
|
|
13
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
14
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
15
|
+
const THINKING_BUDGET = { low: 2048, medium: 8192, high: 32768 };
|
|
16
|
+
const ANTHROPIC_MODELS = ANTHROPIC_CATALOG.map((m) => m.id);
|
|
17
|
+
export function createAnthropicProvider(apiKey) {
|
|
18
|
+
return {
|
|
19
|
+
id: 'anthropic',
|
|
20
|
+
modelCount: ANTHROPIC_MODELS.length,
|
|
21
|
+
listModels() {
|
|
22
|
+
return ANTHROPIC_MODELS.map((m) => `anthropic/${m}`);
|
|
23
|
+
},
|
|
24
|
+
async *stream(llmOpts) {
|
|
25
|
+
let model = llmOpts.model;
|
|
26
|
+
if (model.startsWith('anthropic/'))
|
|
27
|
+
model = model.slice('anthropic/'.length);
|
|
28
|
+
if (!model)
|
|
29
|
+
model = 'claude-sonnet-5';
|
|
30
|
+
const body = {
|
|
31
|
+
model,
|
|
32
|
+
max_tokens: llmOpts.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
33
|
+
messages: toAnthropicMessages(llmOpts.messages),
|
|
34
|
+
stream: true,
|
|
35
|
+
};
|
|
36
|
+
const sys = llmOpts.messages.find((m) => m.role === 'system');
|
|
37
|
+
if (sys?.content)
|
|
38
|
+
body.system = sys.content;
|
|
39
|
+
if (llmOpts.tools?.length)
|
|
40
|
+
body.tools = llmOpts.tools.map(toAnthropicTool);
|
|
41
|
+
if (llmOpts.temperature != null)
|
|
42
|
+
body.temperature = llmOpts.temperature;
|
|
43
|
+
const thinking = anthropicThinkingConfig(llmOpts.reasoning);
|
|
44
|
+
if (thinking)
|
|
45
|
+
body.thinking = thinking;
|
|
46
|
+
const res = await fetch(ANTHROPIC_ENDPOINT, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'x-api-key': apiKey,
|
|
51
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
signal: llmOpts.signal,
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const text = await res.text().catch(() => 'unknown');
|
|
58
|
+
throw new Error(`Anthropic HTTP ${res.status}: ${text.slice(0, 300)}`);
|
|
59
|
+
}
|
|
60
|
+
const reader = res.body?.getReader();
|
|
61
|
+
if (!reader)
|
|
62
|
+
throw new Error('No readable body from Anthropic');
|
|
63
|
+
const usage = { input: 0, output: 0 };
|
|
64
|
+
const blocks = new Map();
|
|
65
|
+
let sentDone = false;
|
|
66
|
+
const decoder = new TextDecoder();
|
|
67
|
+
let buffer = '';
|
|
68
|
+
let eventType = '';
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const { done, value } = await reader.read();
|
|
72
|
+
if (done)
|
|
73
|
+
break;
|
|
74
|
+
buffer += decoder.decode(value, { stream: true });
|
|
75
|
+
while (true) {
|
|
76
|
+
const nl = buffer.indexOf('\n');
|
|
77
|
+
if (nl === -1)
|
|
78
|
+
break;
|
|
79
|
+
const line = buffer.slice(0, nl);
|
|
80
|
+
buffer = buffer.slice(nl + 1);
|
|
81
|
+
if (line.startsWith('event:')) {
|
|
82
|
+
eventType = line.slice(6).trim();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!line.startsWith('data:'))
|
|
86
|
+
continue;
|
|
87
|
+
const data = line.slice(5).trim();
|
|
88
|
+
if (!data)
|
|
89
|
+
continue;
|
|
90
|
+
const parsed = jsonSafe(data);
|
|
91
|
+
if (!parsed)
|
|
92
|
+
continue;
|
|
93
|
+
for (const ev of handleAnthropicEvent(parsed, eventType, blocks, usage)) {
|
|
94
|
+
yield ev;
|
|
95
|
+
if (ev.type === 'done')
|
|
96
|
+
sentDone = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
}
|
|
104
|
+
if (!sentDone)
|
|
105
|
+
yield { type: 'done', usage: { inputTokens: usage.input, outputTokens: usage.output } };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function toAnthropicMessages(messages) {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const m of messages) {
|
|
112
|
+
if (m.role === 'system')
|
|
113
|
+
continue;
|
|
114
|
+
if (m.role === 'assistant') {
|
|
115
|
+
const blocks = [];
|
|
116
|
+
if (m.content)
|
|
117
|
+
blocks.push({ type: 'text', text: m.content });
|
|
118
|
+
for (const tc of m.toolCalls ?? []) {
|
|
119
|
+
blocks.push({ type: 'tool_use', id: tc.id, name: tc.name, input: safeJson(tc.args) });
|
|
120
|
+
}
|
|
121
|
+
out.push({ role: 'assistant', content: blocks });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (m.toolResults && m.toolResults.length > 0) {
|
|
125
|
+
const blocks = [];
|
|
126
|
+
for (const r of m.toolResults) {
|
|
127
|
+
blocks.push({ type: 'tool_result', tool_use_id: r.toolCallId, content: r.content });
|
|
128
|
+
}
|
|
129
|
+
out.push({ role: 'user', content: blocks });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
out.push({ role: 'user', content: m.content });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function toAnthropicTool(t) {
|
|
137
|
+
return {
|
|
138
|
+
name: t.function.name,
|
|
139
|
+
description: t.function.description,
|
|
140
|
+
input_schema: t.function.parameters ?? { type: 'object', properties: {} },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function anthropicThinkingConfig(reasoning) {
|
|
144
|
+
if (!reasoning)
|
|
145
|
+
return undefined;
|
|
146
|
+
const effort = reasoning.effort ?? 'medium';
|
|
147
|
+
if (reasoning.enabled === false || effort === 'none')
|
|
148
|
+
return { type: 'disabled' };
|
|
149
|
+
return { type: 'enabled', budget_tokens: THINKING_BUDGET[effort] ?? THINKING_BUDGET['medium'] };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Handle a single Anthropic SSE data payload, mutating block/usage state and
|
|
153
|
+
* returning the StreamEvents to surface. Empty for non-generating events.
|
|
154
|
+
*/
|
|
155
|
+
function* handleAnthropicEvent(parsed, _eventType, blocks, usage) {
|
|
156
|
+
const evt = parsed.type;
|
|
157
|
+
if (evt === 'message_start') {
|
|
158
|
+
const msg = parsed.message;
|
|
159
|
+
usage.input = msg?.usage?.input_tokens ?? 0;
|
|
160
|
+
usage.output = msg?.usage?.output_tokens ?? 0;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (evt === 'content_block_start') {
|
|
164
|
+
const index = parsed.index ?? 0;
|
|
165
|
+
const block = parsed.content_block;
|
|
166
|
+
const type = block?.type;
|
|
167
|
+
if (type === 'tool_use') {
|
|
168
|
+
const id = block?.id ?? `claude_${Date.now()}_${block?.name ?? 'tool'}`;
|
|
169
|
+
blocks.set(index, { kind: 'tool_use', id, name: block?.name ?? '', args: '' });
|
|
170
|
+
yield { type: 'tool_call_start', id, name: block?.name ?? '' };
|
|
171
|
+
}
|
|
172
|
+
else if (type === 'text' || type === 'thinking') {
|
|
173
|
+
blocks.set(index, { kind: type, id: '', name: '', args: '' });
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (evt === 'content_block_delta') {
|
|
178
|
+
const index = parsed.index ?? 0;
|
|
179
|
+
const delta = parsed.delta;
|
|
180
|
+
const dtype = delta?.type;
|
|
181
|
+
if (dtype === 'text_delta' && typeof delta?.text === 'string') {
|
|
182
|
+
yield { type: 'token', text: delta.text };
|
|
183
|
+
}
|
|
184
|
+
else if (dtype === 'thinking_delta' && typeof delta?.thinking === 'string') {
|
|
185
|
+
yield { type: 'reasoning', text: delta.thinking };
|
|
186
|
+
}
|
|
187
|
+
else if (dtype === 'input_json_delta' && typeof delta?.partial_json === 'string') {
|
|
188
|
+
const block = blocks.get(index);
|
|
189
|
+
if (block && block.kind === 'tool_use') {
|
|
190
|
+
block.args += delta.partial_json;
|
|
191
|
+
if (block.id)
|
|
192
|
+
yield { type: 'tool_call_args', id: block.id, args: delta.partial_json };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (evt === 'content_block_stop') {
|
|
198
|
+
const index = parsed.index ?? 0;
|
|
199
|
+
const block = blocks.get(index);
|
|
200
|
+
if (block && block.kind === 'tool_use') {
|
|
201
|
+
yield { type: 'tool_call_end', id: block.id, name: block.name, args: block.args };
|
|
202
|
+
}
|
|
203
|
+
blocks.delete(index);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (evt === 'message_delta') {
|
|
207
|
+
const mdUsage = parsed.usage;
|
|
208
|
+
if (mdUsage?.output_tokens != null)
|
|
209
|
+
usage.output = mdUsage.output_tokens;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (evt === 'message_stop') {
|
|
213
|
+
yield { type: 'done', usage: { inputTokens: usage.input, outputTokens: usage.output } };
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (evt === 'error') {
|
|
217
|
+
const err = parsed.error;
|
|
218
|
+
throw new Error(`Anthropic: ${err?.message ?? JSON.stringify(parsed.error) ?? 'stream error'}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function safeJson(s) {
|
|
222
|
+
try {
|
|
223
|
+
const v = JSON.parse(s);
|
|
224
|
+
return v && typeof v === 'object' ? v : {};
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function jsonSafe(s) {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(s);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { streamOpenAi } from '../http.js';
|
|
2
|
+
import { OLLAMA_CATALOG } from '../models.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ollama (local, OpenAI-compatible) provider.
|
|
5
|
+
*
|
|
6
|
+
* No API key: Ollama accepts both a dummy key and an empty Authorization.
|
|
7
|
+
* Default base http://127.0.0.1:11434 (Android/Termux), overridable via the
|
|
8
|
+
* OLLAMA_HOST env var. Installed models are discovered by probing
|
|
9
|
+
* GET /api/tags (async, short-cached, fast-fail when the server is down).
|
|
10
|
+
*/
|
|
11
|
+
export const OLLAMA_DEFAULT_BASE = 'http://127.0.0.1:11434';
|
|
12
|
+
const TAGS_CACHE_MS = 5000;
|
|
13
|
+
const TAGS_TIMEOUT_MS = 4000;
|
|
14
|
+
let tagsCache = null;
|
|
15
|
+
export function ollamaBaseUrl(explicit) {
|
|
16
|
+
return (explicit?.trim() || process.env.OLLAMA_HOST?.trim() || OLLAMA_DEFAULT_BASE).replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
async function probeTags(baseUrl) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (tagsCache && now - tagsCache.at < TAGS_CACHE_MS)
|
|
21
|
+
return tagsCache.names;
|
|
22
|
+
try {
|
|
23
|
+
const ctrl = new AbortController();
|
|
24
|
+
const timer = setTimeout(() => ctrl.abort(new Error('ollama probe timed out')), TAGS_TIMEOUT_MS);
|
|
25
|
+
const res = await fetch(`${baseUrl}/api/tags`, { signal: ctrl.signal });
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
throw new Error(`HTTP ${res.status}`);
|
|
29
|
+
const json = (await res.json());
|
|
30
|
+
const names = dedupeStable((json.models ?? []).map((m) => m.name).filter(Boolean));
|
|
31
|
+
tagsCache = { at: Date.now(), names };
|
|
32
|
+
return names;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
tagsCache = { at: Date.now(), names: [] };
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Collapse `foo:latest` → `foo` while preserving distinct non-latest tags. */
|
|
40
|
+
function dedupeStable(list) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const n of list) {
|
|
44
|
+
const clean = n.endsWith(':latest') ? n.slice(0, n.length - 7) : n;
|
|
45
|
+
if (!seen.has(clean)) {
|
|
46
|
+
seen.add(clean);
|
|
47
|
+
out.push(clean);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function createOllamaProvider(opts = {}) {
|
|
53
|
+
const baseUrl = ollamaBaseUrl(opts.baseUrl);
|
|
54
|
+
const catalog = opts.models && opts.models.length > 0 ? opts.models : OLLAMA_CATALOG.map((m) => m.id);
|
|
55
|
+
return {
|
|
56
|
+
id: 'ollama',
|
|
57
|
+
modelCount: catalog.length,
|
|
58
|
+
async listModels() {
|
|
59
|
+
const names = await probeTags(baseUrl);
|
|
60
|
+
// Empty when the server is unreachable so routing/pickers don't list
|
|
61
|
+
// phantom models; the static catalog is only a wizard fallback.
|
|
62
|
+
return names.map((n) => `ollama/${n}`);
|
|
63
|
+
},
|
|
64
|
+
async *stream(llmOpts) {
|
|
65
|
+
let model = llmOpts.model;
|
|
66
|
+
if (model.startsWith('ollama/'))
|
|
67
|
+
model = model.slice('ollama/'.length);
|
|
68
|
+
if (!model)
|
|
69
|
+
model = catalog[0] ?? 'llama3.2';
|
|
70
|
+
yield* streamOpenAi({
|
|
71
|
+
baseUrl: `${baseUrl}/v1`,
|
|
72
|
+
apiKey: '',
|
|
73
|
+
model,
|
|
74
|
+
messages: llmOpts.messages,
|
|
75
|
+
tools: llmOpts.tools,
|
|
76
|
+
temperature: llmOpts.temperature,
|
|
77
|
+
maxTokens: llmOpts.maxTokens,
|
|
78
|
+
signal: llmOpts.signal,
|
|
79
|
+
reasoning: llmOpts.reasoning,
|
|
80
|
+
// Modern Ollama accepts `thinking` + `reasoning_effort` in the OpenAI
|
|
81
|
+
// endpoint; unknown extra keys are ignored by older builds.
|
|
82
|
+
reasoningStyle: 'deepseek',
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -12,6 +12,10 @@ const ENV_LABELS = {
|
|
|
12
12
|
groq: ['GROQ_API_KEY'],
|
|
13
13
|
gemini: ['GEMINI_API_KEY'],
|
|
14
14
|
openrouter: ['OPENROUTER_API_KEY'],
|
|
15
|
+
openai: ['OPENAI_API_KEY'],
|
|
16
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
17
|
+
grok: ['XAI_API_KEY', 'GROK_API_KEY'],
|
|
18
|
+
deepseek: ['DEEPSEEK_API_KEY'],
|
|
15
19
|
};
|
|
16
20
|
// The setup wizard calls providers three times in quick succession; don't
|
|
17
21
|
// re-read + re-parse + re-chmod the keys file on every lookup.
|
|
@@ -65,7 +69,13 @@ function fromEnv(provider) {
|
|
|
65
69
|
return keys;
|
|
66
70
|
}
|
|
67
71
|
export function isImportableDirectly(p) {
|
|
68
|
-
return p === 'groq' ||
|
|
72
|
+
return (p === 'groq' ||
|
|
73
|
+
p === 'gemini' ||
|
|
74
|
+
p === 'openrouter' ||
|
|
75
|
+
p === 'openai' ||
|
|
76
|
+
p === 'anthropic' ||
|
|
77
|
+
p === 'grok' ||
|
|
78
|
+
p === 'deepseek');
|
|
69
79
|
}
|
|
70
80
|
export function getProviderSecrets(provider) {
|
|
71
81
|
const envKeys = fromEnv(provider);
|
|
@@ -78,7 +88,7 @@ export function getProviderSecrets(provider) {
|
|
|
78
88
|
return null;
|
|
79
89
|
}
|
|
80
90
|
export function hasAnySecrets() {
|
|
81
|
-
for (const p of ['orbitx', 'groq', 'gemini', 'openrouter']) {
|
|
91
|
+
for (const p of ['orbitx', 'groq', 'gemini', 'openrouter', 'openai', 'anthropic', 'grok', 'deepseek']) {
|
|
82
92
|
if (getProviderSecrets(p))
|
|
83
93
|
return true;
|
|
84
94
|
}
|
|
@@ -91,7 +101,7 @@ export function maskSecret(key) {
|
|
|
91
101
|
}
|
|
92
102
|
export function describeProvidersAvailable() {
|
|
93
103
|
const out = [];
|
|
94
|
-
for (const p of ['orbitx', 'groq', 'gemini', 'openrouter']) {
|
|
104
|
+
for (const p of ['orbitx', 'groq', 'gemini', 'openrouter', 'openai', 'anthropic', 'grok', 'deepseek']) {
|
|
95
105
|
const s = getProviderSecrets(p);
|
|
96
106
|
if (s && s.keys[0])
|
|
97
107
|
out.push({ provider: p, masked: maskSecret(s.keys[0]), source: s.source });
|
package/dist/src/setup/wizard.js
CHANGED
|
@@ -252,7 +252,13 @@ function contentLines(state, theme, width) {
|
|
|
252
252
|
lines.push('');
|
|
253
253
|
PROVIDER_SPECS.forEach((p, i) => {
|
|
254
254
|
const av = describeProvidersAvailable().some((a) => a.provider === p.id);
|
|
255
|
-
const flag = p.id === 'orbitx'
|
|
255
|
+
const flag = p.id === 'orbitx'
|
|
256
|
+
? ' free · auto-routes Groq/Gemini/OpenRouter'
|
|
257
|
+
: p.id === 'ollama'
|
|
258
|
+
? ' local · auto-detect http://127.0.0.1:11434'
|
|
259
|
+
: av
|
|
260
|
+
? ' ✓ available'
|
|
261
|
+
: ` ${dimSgr}(keys not set)${reset}`;
|
|
256
262
|
if (i === state.providerSelected)
|
|
257
263
|
lines.push(`${accentSgr}▸ ${p.label}${reset}${dimSgr}${flag}${reset}`);
|
|
258
264
|
else
|
package/dist/src/tui/InkApp.js
CHANGED
|
@@ -33,7 +33,7 @@ export function InkApp({ controller }) {
|
|
|
33
33
|
const theme = store.theme ?? buildTheme('tokyonight');
|
|
34
34
|
const menuing = store.input.currentBuffer().startsWith('/');
|
|
35
35
|
const menuLines = menuing ? store.slashMatches().length + 1 : 0;
|
|
36
|
-
const modelLines = store.modelPicker.open ? Math.min(store.
|
|
36
|
+
const modelLines = store.modelPicker.open ? Math.min(store.pickRows.length, 12) + 3 : 0;
|
|
37
37
|
const dockLines = store.dockOpen
|
|
38
38
|
? store.agents.size === 0
|
|
39
39
|
? 2
|
|
@@ -63,5 +63,5 @@ export function InkApp({ controller }) {
|
|
|
63
63
|
revealChars: store.revealChars,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
-
return (_jsx(StoreContext.Provider, { value: store, children: _jsxs(Box, { width: cols, height: bodyRows, flexDirection: "column", overflow: "hidden", children: [_jsx(Header, { width: cols }), _jsx(Text, { dimColor: true, children: 'Tip: /help for commands · /theme to switch' }), _jsx(Transcript, { rows: msgRows, viewport: viewport }), working && _jsx(WorkingStatus, {}), store.pendingAsk && _jsx(PermissionModal, { cols: cols }), store.dockOpen && _jsx(AgentDock, { width: cols, height: dockLines }), menuing && _jsx(SlashMenu, { width: cols }), store.modelPicker.open && _jsx(ModelPicker, {
|
|
66
|
+
return (_jsx(StoreContext.Provider, { value: store, children: _jsxs(Box, { width: cols, height: bodyRows, flexDirection: "column", overflow: "hidden", children: [_jsx(Header, { width: cols }), _jsx(Text, { dimColor: true, children: 'Tip: /help for commands · /theme to switch' }), _jsx(Transcript, { rows: msgRows, viewport: viewport }), working && _jsx(WorkingStatus, {}), store.pendingAsk && _jsx(PermissionModal, { cols: cols }), store.dockOpen && _jsx(AgentDock, { width: cols, height: dockLines }), menuing && _jsx(SlashMenu, { width: cols }), store.modelPicker.open && _jsx(ModelPicker, { height: modelLines }), _jsx(Box, { height: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate-end", children: '─'.repeat(Math.max(1, cols)) }) }), _jsx(Composer, {}), _jsx(StatusLine, { width: cols })] }) }));
|
|
67
67
|
}
|
package/dist/src/tui/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { providerNames } from '../config/config-schema.js';
|
|
|
4
4
|
import { makeTheme, THEME_NAMES } from './themes/index.js';
|
|
5
5
|
import { AppStore, ASK_OPTIONS } from './store.js';
|
|
6
6
|
import { InkApp } from './InkApp.js';
|
|
7
|
+
import { findPickerIndex, isPickerHeader } from './picker.js';
|
|
7
8
|
export class TuiApp {
|
|
8
9
|
store = new AppStore();
|
|
9
10
|
theme;
|
|
@@ -13,6 +14,7 @@ export class TuiApp {
|
|
|
13
14
|
config;
|
|
14
15
|
bus;
|
|
15
16
|
models;
|
|
17
|
+
pickBuilder = null;
|
|
16
18
|
instance = null;
|
|
17
19
|
done = false;
|
|
18
20
|
/** Last dispatched key (sig + timestamp) for de-duping keyboard repeats. */
|
|
@@ -26,6 +28,7 @@ export class TuiApp {
|
|
|
26
28
|
this.onCommand = opts.onCommand ?? (async () => null);
|
|
27
29
|
this.version = opts.version;
|
|
28
30
|
this.models = opts.models ?? [];
|
|
31
|
+
this.pickBuilder = opts.pickRows ?? null;
|
|
29
32
|
this.theme = makeTheme(opts.config.theme);
|
|
30
33
|
this.store.version = opts.version;
|
|
31
34
|
this.store.cwd = process.cwd();
|
|
@@ -79,6 +82,46 @@ export class TuiApp {
|
|
|
79
82
|
getMessages() {
|
|
80
83
|
return this.store.messages;
|
|
81
84
|
}
|
|
85
|
+
/** Push the current routed model list (after rebuild) into the app/UI. */
|
|
86
|
+
setModels(list) {
|
|
87
|
+
this.models = [...list];
|
|
88
|
+
this.store.models = [...list];
|
|
89
|
+
}
|
|
90
|
+
/** Rebuild the grouped picker rows (e.g. after a provider/model switch). */
|
|
91
|
+
async refreshModelPicker() {
|
|
92
|
+
if (!this.pickBuilder)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const rows = await this.pickBuilder();
|
|
96
|
+
this.store.pickRows = rows;
|
|
97
|
+
if (rows.length === 0)
|
|
98
|
+
this.store.modelPicker.index = 0;
|
|
99
|
+
else if (this.store.modelPicker.index >= rows.length)
|
|
100
|
+
this.store.modelPicker.index = rows.length - 1;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
this.store.pickRows = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Open the /model overlay, refreshing rows first so the list is current. */
|
|
107
|
+
async openModelPicker() {
|
|
108
|
+
await this.refreshModelPicker();
|
|
109
|
+
const store = this.store;
|
|
110
|
+
if (store.pickRows.length === 0 && store.models.length > 0) {
|
|
111
|
+
// Fallback when no builder: derive item rows from the routed models.
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
for (const m of store.models) {
|
|
114
|
+
const provider = m.split('/')[0] ?? 'orbitx';
|
|
115
|
+
if (seen.has(m))
|
|
116
|
+
continue;
|
|
117
|
+
seen.add(m);
|
|
118
|
+
store.pickRows.push({ kind: 'item', label: m, id: m, provider });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
store.modelPicker.index = findPickerIndex(store.pickRows, store.route?.model, this.config.model.primary);
|
|
122
|
+
store.modelPicker.open = true;
|
|
123
|
+
store.refresh();
|
|
124
|
+
}
|
|
82
125
|
ask(prompt) {
|
|
83
126
|
return new Promise((resolve) => {
|
|
84
127
|
this.store.pendingAsk = { prompt, index: 0, resolve };
|
|
@@ -126,6 +169,13 @@ export class TuiApp {
|
|
|
126
169
|
});
|
|
127
170
|
this.bus.on('onToolUpdate', (call) => this.applyToolState(call));
|
|
128
171
|
this.bus.on('onToolEnd', (call) => this.applyToolState(call));
|
|
172
|
+
// Narration for this message is fully streamed and tools have not run yet.
|
|
173
|
+
// Flush the reveal so the text is fully visible above the tool rows before
|
|
174
|
+
// the tools start — users see the intent, then the action.
|
|
175
|
+
this.bus.on('onAssistantGenerationDone', () => {
|
|
176
|
+
this.store.revealAll();
|
|
177
|
+
this.store.refresh();
|
|
178
|
+
});
|
|
129
179
|
this.bus.on('onAssistantEnd', (msg) => {
|
|
130
180
|
this.store.streaming = false;
|
|
131
181
|
this.store.cancelArmed = false;
|
|
@@ -469,8 +519,8 @@ export class TuiApp {
|
|
|
469
519
|
}
|
|
470
520
|
handleModelPickerKey(input, key) {
|
|
471
521
|
const store = this.store;
|
|
472
|
-
const
|
|
473
|
-
if (
|
|
522
|
+
const rows = store.pickRows;
|
|
523
|
+
if (rows.length === 0) {
|
|
474
524
|
store.modelPicker.open = false;
|
|
475
525
|
store.refresh();
|
|
476
526
|
return;
|
|
@@ -481,26 +531,53 @@ export class TuiApp {
|
|
|
481
531
|
return;
|
|
482
532
|
}
|
|
483
533
|
if (key.upArrow || (key.tab && key.shift)) {
|
|
484
|
-
store.modelPicker.index = (store.modelPicker.index -
|
|
534
|
+
store.modelPicker.index = this.movePickerIndex(store.modelPicker.index, -1);
|
|
485
535
|
store.refresh();
|
|
486
536
|
return;
|
|
487
537
|
}
|
|
488
538
|
if (key.downArrow || key.tab) {
|
|
489
|
-
store.modelPicker.index = (store.modelPicker.index
|
|
539
|
+
store.modelPicker.index = this.movePickerIndex(store.modelPicker.index, 1);
|
|
490
540
|
store.refresh();
|
|
491
541
|
return;
|
|
492
542
|
}
|
|
493
543
|
if (key.return) {
|
|
494
|
-
const
|
|
544
|
+
const row = rows[store.modelPicker.index];
|
|
545
|
+
if (!row || isPickerHeader(row)) {
|
|
546
|
+
store.modelPicker.open = false;
|
|
547
|
+
store.refresh();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
495
550
|
store.modelPicker.open = false;
|
|
496
551
|
store.refresh();
|
|
497
|
-
|
|
498
|
-
void this.switchModel(chosen);
|
|
552
|
+
void this.selectPickerModel(row);
|
|
499
553
|
return;
|
|
500
554
|
}
|
|
501
555
|
if (input && !key.ctrl && !key.meta)
|
|
502
556
|
store.refresh();
|
|
503
557
|
}
|
|
558
|
+
/** Step over the row list, skipping non-selectable header rows. */
|
|
559
|
+
movePickerIndex(index, delta) {
|
|
560
|
+
const rows = this.store.pickRows;
|
|
561
|
+
if (rows.length === 0)
|
|
562
|
+
return 0;
|
|
563
|
+
let i = index;
|
|
564
|
+
for (let steps = 0; steps < rows.length; steps++) {
|
|
565
|
+
i = (i + delta + rows.length) % rows.length;
|
|
566
|
+
const row = rows[i];
|
|
567
|
+
if (row && !isPickerHeader(row))
|
|
568
|
+
return i;
|
|
569
|
+
}
|
|
570
|
+
return index;
|
|
571
|
+
}
|
|
572
|
+
async selectPickerModel(item) {
|
|
573
|
+
// Switching provider first lets the same Enter pick a model from a
|
|
574
|
+
// provider that isn't active (e.g. gemini model while on openrouter).
|
|
575
|
+
const currentProvider = this.store.route?.provider;
|
|
576
|
+
if (item.provider !== currentProvider) {
|
|
577
|
+
await this.switchProvider(item.provider);
|
|
578
|
+
}
|
|
579
|
+
await this.switchModel(item.id);
|
|
580
|
+
}
|
|
504
581
|
handleTabAutocomplete() {
|
|
505
582
|
const buf = this.store.input.currentBuffer();
|
|
506
583
|
const token = buf.split(/\s+/)[0] ?? '';
|
|
@@ -571,7 +648,7 @@ export class TuiApp {
|
|
|
571
648
|
`/clear clear the conversation`,
|
|
572
649
|
`/help show this help`,
|
|
573
650
|
`/model [id] pick a model (bare: interactive selector)`,
|
|
574
|
-
`/provider <id> switch provider (orbitx · groq · gemini · openrouter)`,
|
|
651
|
+
`/provider <id> switch provider (orbitx · groq · gemini · openrouter · openai · anthropic · grok · deepseek · ollama)`,
|
|
575
652
|
`/plan <task> plan first: propose a plan, approve with /run`,
|
|
576
653
|
`/run execute the proposed plan`,
|
|
577
654
|
`/reject discard the proposed plan`,
|
|
@@ -663,9 +740,7 @@ export class TuiApp {
|
|
|
663
740
|
}
|
|
664
741
|
case 'model':
|
|
665
742
|
if (!arg) {
|
|
666
|
-
this.
|
|
667
|
-
this.store.modelPicker.index = Math.max(0, this.models.findIndex((m) => m === this.store.route?.model || m.endsWith(`/${this.config.model.primary}`)));
|
|
668
|
-
this.store.refresh();
|
|
743
|
+
void this.openModelPicker();
|
|
669
744
|
return;
|
|
670
745
|
}
|
|
671
746
|
await this.switchModel(arg);
|
|
@@ -2,22 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { useStore } from '../context.js';
|
|
4
4
|
import { themeColor } from '../colors.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
export function ModelPicker({
|
|
5
|
+
import { isPickerHeader } from '../picker.js';
|
|
6
|
+
/** Model selector overlay (opened by /model). Height includes the border. */
|
|
7
|
+
export function ModelPicker({ height }) {
|
|
8
8
|
const store = useStore();
|
|
9
|
-
const list = store.models;
|
|
10
9
|
const accent = store.theme ? themeColor(store.theme, 'accent') : '#7aa2f7';
|
|
11
10
|
const dim = store.theme ? themeColor(store.theme, 'dim') : '#565f89';
|
|
12
11
|
const muted = store.theme ? themeColor(store.theme, 'muted') : '#969cbc';
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const attention = store.theme ? themeColor(store.theme, 'attention') : '#c3a332';
|
|
13
|
+
const rows = store.pickRows;
|
|
14
|
+
const idx = rows.length === 0 ? 0 : Math.min(Math.max(store.modelPicker.index, 0), rows.length - 1);
|
|
15
|
+
const innerH = Math.max(1, height - 3); // border(2) + footer(1)
|
|
16
|
+
if (rows.length === 0) {
|
|
17
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: dim, width: "100%", height: height, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: dim, children: "no models available \u2014 configure a provider key first" }), _jsx(Text, { color: muted, children: "Esc close" })] }));
|
|
15
18
|
}
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
return (_jsxs(Box, { width: width, flexDirection: "column", children: [start > 0 && _jsxs(Text, { color: muted, children: ["\u25B2 ", (start).toLocaleString(), " more\u2026"] }), window.map((m, i) => {
|
|
19
|
+
const start = Math.max(0, Math.min(idx, rows.length - innerH));
|
|
20
|
+
const windowRows = rows.slice(start, start + innerH);
|
|
21
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: accent, width: "100%", height: height, flexDirection: "column", paddingX: 1, children: [start > 0 && (_jsxs(Text, { color: muted, children: ["\u25B2 ", start, " more\u2026"] })), windowRows.map((r, i) => {
|
|
20
22
|
const at = start + i;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
if (isPickerHeader(r)) {
|
|
24
|
+
return (_jsx(Box, { children: _jsx(Text, { color: attention, bold: true, wrap: "truncate-end", children: r.label }) }, `h-${at}`));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: at === idx ? accent : dim, children: at === idx ? '❯ ' : ' ' }), _jsx(Text, { color: at === idx ? accent : dim, wrap: "truncate-end", children: r.label })] }, r.id));
|
|
27
|
+
}), start + innerH < rows.length && (_jsxs(Text, { color: muted, children: ["\u25BC ", rows.length - start - innerH, " more\u2026"] })), _jsx(Text, { color: muted, children: "\u2191\u2193 select \u00B7 Enter switch \u00B7 Esc close" })] }));
|
|
23
28
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PROVIDER_SPECS, ORBITX_SERVE } from '../core/llm/index.js';
|
|
2
|
+
import { describeProvidersAvailable } from '../core/llm/secrets.js';
|
|
3
|
+
import { createOllamaProvider } from '../core/llm/providers/ollama.js';
|
|
4
|
+
export function isPickerHeader(row) {
|
|
5
|
+
return row.kind === 'header';
|
|
6
|
+
}
|
|
7
|
+
export async function resolvePickerRows() {
|
|
8
|
+
const rows = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
const avail = describeProvidersAvailable();
|
|
11
|
+
for (const spec of PROVIDER_SPECS) {
|
|
12
|
+
if (spec.id === 'ollama') {
|
|
13
|
+
const ollama = createOllamaProvider();
|
|
14
|
+
const local = await ollama.listModels();
|
|
15
|
+
if (local.length === 0) {
|
|
16
|
+
rows.push({ kind: 'header', label: 'Ollama (local) — server offline, run ollama serve' });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
rows.push({ kind: 'header', label: `Ollama (local) — ${local.length} installed` });
|
|
20
|
+
for (const full of local) {
|
|
21
|
+
const name = full.slice('ollama/'.length);
|
|
22
|
+
if (seen.has(full))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(full);
|
|
25
|
+
rows.push({ kind: 'item', label: name, id: full, provider: 'ollama' });
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (spec.id === 'orbitx') {
|
|
30
|
+
rows.push({ kind: 'header', label: 'Orbit X (auto-routes Groq · Gemini · OpenRouter)' });
|
|
31
|
+
for (const bare of ORBITX_SERVE) {
|
|
32
|
+
const full = `orbitx/${bare}`;
|
|
33
|
+
if (seen.has(full))
|
|
34
|
+
continue;
|
|
35
|
+
seen.add(full);
|
|
36
|
+
rows.push({
|
|
37
|
+
kind: 'item',
|
|
38
|
+
label: bare === 'auto' ? 'auto (backend routes across providers)' : bare,
|
|
39
|
+
id: full,
|
|
40
|
+
provider: 'orbitx',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Other providers: only when a key is configured.
|
|
46
|
+
const available = avail.some((a) => a.provider === spec.id);
|
|
47
|
+
if (!available)
|
|
48
|
+
continue;
|
|
49
|
+
rows.push({ kind: 'header', label: spec.label });
|
|
50
|
+
for (const m of spec.models) {
|
|
51
|
+
const full = `${spec.id}/${m.id}`;
|
|
52
|
+
if (seen.has(full))
|
|
53
|
+
continue;
|
|
54
|
+
seen.add(full);
|
|
55
|
+
rows.push({ kind: 'item', label: m.label ?? m.id, id: full, provider: spec.id });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
/** Row index whose item matches the current route/model, else the first item. */
|
|
61
|
+
export function findPickerIndex(rows, routeModel, configPrimary) {
|
|
62
|
+
if (rows.length === 0)
|
|
63
|
+
return 0;
|
|
64
|
+
const cur = routeModel || configPrimary || '';
|
|
65
|
+
for (let i = 0; i < rows.length; i++) {
|
|
66
|
+
const r = rows[i];
|
|
67
|
+
if (r && r.kind === 'item' && (r.id === cur || (configPrimary && r.id?.endsWith(`/${configPrimary.split('/').pop()}`)))) {
|
|
68
|
+
return i;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Fall back to the first item row.
|
|
72
|
+
const firstItem = rows.findIndex((r) => r.kind === 'item');
|
|
73
|
+
return firstItem >= 0 ? firstItem : 0;
|
|
74
|
+
}
|
package/dist/src/tui/store.js
CHANGED
|
@@ -22,8 +22,8 @@ const SLASH_COMMANDS = [
|
|
|
22
22
|
// Word-to-word reveal pacing. Servers stream sentence-sized chunks; the reveal
|
|
23
23
|
// cursor films their contents out at a steady character rate, word-aligned, so
|
|
24
24
|
// output reads "word by word" instead of "sentence by sentence".
|
|
25
|
-
const REVEAL_CPS =
|
|
26
|
-
const REVEAL_LAG_CAP =
|
|
25
|
+
const REVEAL_CPS = 60; // chars/sec baseline (~12 words/sec) — brisk typing feel
|
|
26
|
+
const REVEAL_LAG_CAP = 200; // caught-up threshold -> above it, drain 3x faster
|
|
27
27
|
export class AppStore {
|
|
28
28
|
messages = [];
|
|
29
29
|
streaming = false;
|
|
@@ -39,6 +39,8 @@ export class AppStore {
|
|
|
39
39
|
cwd = '';
|
|
40
40
|
models = [];
|
|
41
41
|
modelPicker = { open: false, index: 0 };
|
|
42
|
+
/** Grouped rows for the /model overlay (see picker.ts). */
|
|
43
|
+
pickRows = [];
|
|
42
44
|
agents = new Map();
|
|
43
45
|
dockOpen = false;
|
|
44
46
|
yolo = false;
|
package/package.json
CHANGED