omni-pi 0.1.0 → 0.2.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Omni-Pi: Guided software delivery for everyone.
4
4
 
5
- Omni-Pi is an opinionated Pi package and branded launcher that helps people move from a blank repo to a structured plan, implemented work, and explicit verification without having to assemble the workflow themselves.
5
+ Omni-Pi is an opinionated Pi package and branded launcher published on npm as `omni-pi`. It helps people move from a blank repo to a structured plan, implemented work, and explicit verification without having to assemble the workflow themselves.
6
6
 
7
7
  Requires Node.js 22 or newer.
8
8
 
@@ -19,12 +19,73 @@ Requires Node.js 22 or newer.
19
19
 
20
20
  ## Quick Start
21
21
 
22
+ Install Omni-Pi from npm, then run it in your project:
23
+
24
+ ```bash
25
+ npm install -g omni-pi
26
+ cd your-project
27
+ omni
28
+ ```
29
+
30
+ ## Install
31
+
32
+ Install the published package globally with npm:
33
+
22
34
  ```bash
23
35
  npm install -g omni-pi
36
+ ```
37
+
38
+ Confirm the launcher is available:
39
+
40
+ ```bash
41
+ omni --help
42
+ ```
43
+
44
+ Then open any project directory and start Omni-Pi:
45
+
46
+ ```bash
24
47
  cd your-project
25
48
  omni
26
49
  ```
27
50
 
51
+ To upgrade later:
52
+
53
+ ```bash
54
+ npm install -g omni-pi@latest
55
+ ```
56
+
57
+ Omni-Pi launches the bundled Pi runtime and loads the Omni-Pi package automatically, so you do not need to manually wire extensions, skills, or prompts after installing from npm.
58
+
59
+ ## Model Providers
60
+
61
+ Omni-Pi now ships the upstream provider mix needed for practical multi-provider use on top of Pi.
62
+
63
+ - Built into the underlying Pi runtime: `anthropic`, `openai`, `openai-codex`, `google`, `google-vertex`, `amazon-bedrock`, `azure-openai-responses`, `openrouter`, `xai`, `zai`, `mistral`, `groq`, `cerebras`, `huggingface`, `github-copilot`, `kimi-coding`, `minimax`, `minimax-cn`, `opencode`, `opencode-go`
64
+ - Added by Omni-Pi: `nvidia`, `together`, `synthetic`, `nanogpt`, `xiaomi`, `moonshot`, `venice`, `kilo`, `gitlab-duo`, `qwen-portal`, `qianfan`, `cloudflare-ai-gateway`
65
+ - Auto-discovered when running locally: `ollama`, `lm-studio`, `llama.cpp`, `litellm`, `vllm`
66
+
67
+ For users who do not want to rely on Anthropic OAuth inside Pi, Omni-Pi also exposes opt-in Claude Agent SDK model aliases:
68
+
69
+ - `claude-agent/claude-sonnet-4-6`
70
+ - `claude-agent/claude-opus-4-6`
71
+
72
+ These are intended for Omni-Pi's worker and expert subagents. Configure a role with `/omni-model` and Omni-Pi will run that subagent through the Claude Agent SDK instead of Pi's normal Anthropic provider path.
73
+
74
+ Common provider env vars:
75
+
76
+ - `NVIDIA_API_KEY`, `TOGETHER_API_KEY`, `SYNTHETIC_API_KEY`, `NANO_GPT_API_KEY`
77
+ - `XIAOMI_API_KEY`, `MOONSHOT_API_KEY`, `VENICE_API_KEY`, `KILO_API_KEY`
78
+ - `GITLAB_TOKEN`, `QWEN_OAUTH_TOKEN` or `QWEN_PORTAL_API_KEY`, `QIANFAN_API_KEY`
79
+ - `CLOUDFLARE_AI_GATEWAY_API_KEY` and `CLOUDFLARE_AI_GATEWAY_BASE_URL`
80
+
81
+ For local providers, Omni-Pi registers models only when the endpoint is reachable:
82
+
83
+ - `OLLAMA_BASE_URL` / `OLLAMA_API_KEY`
84
+ - `LM_STUDIO_BASE_URL` / `LM_STUDIO_API_KEY`
85
+ - `LLAMA_CPP_BASE_URL` / `LLAMA_CPP_API_KEY`
86
+ - `LITELLM_BASE_URL` / `LITELLM_API_KEY`
87
+ - `VLLM_BASE_URL` / `VLLM_API_KEY`
88
+
28
89
  ## Commands
29
90
 
30
91
  | Command | Description |
@@ -36,7 +97,7 @@ omni
36
97
  | `/omni-sync` | Update durable memory files from recent progress |
37
98
  | `/omni-skills` | Inspect installed, recommended, deferred, and rejected skills |
38
99
  | `/omni-explain` | Explain what Omni-Pi is doing in simple language |
39
- | `/omni-model` | Interactively select the model for a specific agent role |
100
+ | `/omni-model` | Interactively select the model for a specific agent role, or enter any canonical `provider/model` reference |
40
101
  | `/omni-commit` | Create a branch and commit for the last completed task |
41
102
  | `/omni-doctor` | Run diagnostic health checks and detect stuck tasks |
42
103
 
@@ -46,6 +107,8 @@ Omni-Pi follows a simple agent pipeline: Brain, Planner, Worker, Expert. The Bra
46
107
 
47
108
  When the Worker gets stuck or verification fails repeatedly, the Expert role steps in to recover the task, adapt the approach, or surface the blocker clearly instead of letting the session stall.
48
109
 
110
+ On first use inside a project, Omni-Pi creates and updates `.omni/` state so plans, task progress, verification steps, and recovery context persist across sessions.
111
+
49
112
  ## Features
50
113
 
51
114
  - Core workflow with durable `.omni/` project memory, typed planning and execution contracts, filesystem-backed init/planning/status, and retry-aware task execution.
@@ -61,7 +124,7 @@ When the Worker gets stuck or verification fails repeatedly, the Expert role ste
61
124
 
62
125
  ## Development
63
126
 
64
- For contributor setup, see [CONTRIBUTING.md](CONTRIBUTING.md).
127
+ For local checkout development, see [CONTRIBUTING.md](CONTRIBUTING.md).
65
128
 
66
129
  ```bash
67
130
  git clone https://github.com/EdGy2k/Omni-Pi.git
@@ -0,0 +1,9 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { registerOmniProviders } from "../../src/providers.js";
4
+
5
+ export default async function omniProvidersExtension(
6
+ api: ExtensionAPI,
7
+ ): Promise<void> {
8
+ await registerOmniProviders(api);
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-pi",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Opinionated Pi package for guided, beginner-friendly planning and implementation workflows.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  "extensions": [
57
57
  "./node_modules/pi-subagents/index.ts",
58
58
  "./node_modules/pi-subagents/notify.ts",
59
+ "./extensions/omni-providers/index.ts",
59
60
  "./extensions/omni-core/index.ts",
60
61
  "./extensions/omni-memory/index.ts",
61
62
  "./extensions/omni-skills/index.ts",
@@ -69,7 +70,9 @@
69
70
  ]
70
71
  },
71
72
  "dependencies": {
73
+ "@anthropic-ai/claude-agent-sdk": "0.2.84",
72
74
  "@mariozechner/pi-coding-agent": "^0.62.0",
73
- "pi-subagents": "^0.11.11"
75
+ "pi-subagents": "^0.11.11",
76
+ "zod": "^4.3.6"
74
77
  }
75
78
  }
package/src/commands.ts CHANGED
@@ -449,6 +449,7 @@ export function createOmniCommands(): AppCommandDefinition[] {
449
449
  const modelOptions = AVAILABLE_MODELS.map((model) =>
450
450
  model === currentModel ? `${model} (current)` : model,
451
451
  );
452
+ modelOptions.push("Enter custom provider/model");
452
453
 
453
454
  const selectedModelDisplay = await ui.select(
454
455
  `Select model for ${selectedAgent}:`,
@@ -458,7 +459,18 @@ export function createOmniCommands(): AppCommandDefinition[] {
458
459
  return "Model selection cancelled.";
459
460
  }
460
461
 
461
- const selectedModel = selectedModelDisplay.replace(" (current)", "");
462
+ let selectedModel = selectedModelDisplay.replace(" (current)", "");
463
+ if (selectedModel === "Enter custom provider/model") {
464
+ const customModel = await ui.input(
465
+ "Enter model as provider/model",
466
+ "e.g., openrouter/anthropic/claude-sonnet-4",
467
+ );
468
+ if (!customModel?.includes("/")) {
469
+ return "Custom model cancelled. Use the canonical provider/model format.";
470
+ }
471
+ selectedModel = customModel.trim();
472
+ }
473
+
462
474
  await updateModelConfig(cwd, selectedAgent, selectedModel);
463
475
 
464
476
  return `Updated ${selectedAgent} model to ${selectedModel}. Configuration saved to .omni/CONFIG.md`;
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import type { OmniConfig } from "./contracts.js";
5
+ import { AVAILABLE_MODELS } from "./providers.js";
5
6
 
6
7
  export const DEFAULT_CONFIG: OmniConfig = {
7
8
  models: {
@@ -138,17 +139,4 @@ export async function updateModelConfig(
138
139
  return config;
139
140
  }
140
141
 
141
- export const AVAILABLE_MODELS = [
142
- "anthropic/claude-sonnet-4-6",
143
- "anthropic/claude-opus-4-6",
144
- "anthropic/claude-sonnet-4-5",
145
- "anthropic/claude-opus-4-1",
146
- "openai/gpt-5.4",
147
- "openai/gpt-5",
148
- "openai/gpt-4.1",
149
- "openai/gpt-4o",
150
- "openai/o3-mini",
151
- "openai/o1",
152
- "google/gemini-2.5-pro",
153
- "google/gemini-2.5-flash",
154
- ];
142
+ export { AVAILABLE_MODELS };
@@ -0,0 +1,682 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ type ModelApi =
4
+ | "anthropic-messages"
5
+ | "openai-completions"
6
+ | "openai-responses";
7
+
8
+ interface OmniProviderModel {
9
+ id: string;
10
+ name: string;
11
+ api: ModelApi;
12
+ reasoning: boolean;
13
+ input: Array<"text" | "image">;
14
+ cost: {
15
+ input: number;
16
+ output: number;
17
+ cacheRead: number;
18
+ cacheWrite: number;
19
+ };
20
+ contextWindow: number;
21
+ maxTokens: number;
22
+ }
23
+
24
+ interface StaticProviderDefinition {
25
+ name: string;
26
+ apiKey: string;
27
+ models: OmniProviderModel[];
28
+ }
29
+
30
+ interface LocalDiscoveryDefinition {
31
+ name: string;
32
+ api: ModelApi;
33
+ baseUrl: string;
34
+ apiKeyEnv?: string;
35
+ discover: () => Promise<OmniProviderModel[]>;
36
+ }
37
+
38
+ const ZERO_COST = {
39
+ input: 0,
40
+ output: 0,
41
+ cacheRead: 0,
42
+ cacheWrite: 0,
43
+ } as const;
44
+
45
+ function model(
46
+ id: string,
47
+ name: string,
48
+ api: ModelApi,
49
+ reasoning: boolean,
50
+ input: Array<"text" | "image">,
51
+ contextWindow: number,
52
+ maxTokens: number,
53
+ ): OmniProviderModel {
54
+ return {
55
+ id,
56
+ name,
57
+ api,
58
+ reasoning,
59
+ input,
60
+ cost: ZERO_COST,
61
+ contextWindow,
62
+ maxTokens,
63
+ };
64
+ }
65
+
66
+ const STATIC_PROVIDERS: StaticProviderDefinition[] = [
67
+ {
68
+ name: "nvidia",
69
+ apiKey: "NVIDIA_API_KEY",
70
+ models: [
71
+ model(
72
+ "deepseek-ai/deepseek-v3.2",
73
+ "DeepSeek V3.2",
74
+ "openai-completions",
75
+ true,
76
+ ["text"],
77
+ 163840,
78
+ 65536,
79
+ ),
80
+ model(
81
+ "deepseek-ai/deepseek-r1-0528",
82
+ "DeepSeek R1 0528",
83
+ "openai-completions",
84
+ true,
85
+ ["text"],
86
+ 128000,
87
+ 4096,
88
+ ),
89
+ model(
90
+ "meta/llama-3.3-70b-instruct",
91
+ "Llama 3.3 70B Instruct",
92
+ "openai-completions",
93
+ false,
94
+ ["text"],
95
+ 128000,
96
+ 4096,
97
+ ),
98
+ ],
99
+ },
100
+ {
101
+ name: "together",
102
+ apiKey: "TOGETHER_API_KEY",
103
+ models: [
104
+ model(
105
+ "deepseek-ai/DeepSeek-R1",
106
+ "DeepSeek R1",
107
+ "openai-completions",
108
+ true,
109
+ ["text"],
110
+ 131072,
111
+ 8192,
112
+ ),
113
+ model(
114
+ "moonshotai/Kimi-K2.5",
115
+ "Kimi K2.5",
116
+ "openai-completions",
117
+ true,
118
+ ["text", "image"],
119
+ 262144,
120
+ 32768,
121
+ ),
122
+ model(
123
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
124
+ "Llama 3.3 70B Instruct Turbo",
125
+ "openai-completions",
126
+ false,
127
+ ["text"],
128
+ 131072,
129
+ 8192,
130
+ ),
131
+ ],
132
+ },
133
+ {
134
+ name: "synthetic",
135
+ apiKey: "SYNTHETIC_API_KEY",
136
+ models: [
137
+ model(
138
+ "hf:deepseek-ai/DeepSeek-V3.2",
139
+ "DeepSeek V3.2",
140
+ "openai-completions",
141
+ false,
142
+ ["text"],
143
+ 162816,
144
+ 8192,
145
+ ),
146
+ model(
147
+ "hf:moonshotai/Kimi-K2-Instruct-0905",
148
+ "Kimi K2 Instruct 0905",
149
+ "openai-completions",
150
+ false,
151
+ ["text"],
152
+ 262144,
153
+ 8192,
154
+ ),
155
+ model(
156
+ "hf:meta-llama/Llama-3.3-70B-Instruct",
157
+ "Llama 3.3 70B Instruct",
158
+ "openai-completions",
159
+ false,
160
+ ["text"],
161
+ 131072,
162
+ 8192,
163
+ ),
164
+ ],
165
+ },
166
+ {
167
+ name: "nanogpt",
168
+ apiKey: "NANO_GPT_API_KEY",
169
+ models: [
170
+ model(
171
+ "anthropic/claude-sonnet-4.6",
172
+ "Claude Sonnet 4.6",
173
+ "openai-completions",
174
+ true,
175
+ ["text"],
176
+ 222222,
177
+ 8888,
178
+ ),
179
+ model(
180
+ "anthropic/claude-opus-4.6",
181
+ "Claude Opus 4.6",
182
+ "openai-completions",
183
+ true,
184
+ ["text"],
185
+ 222222,
186
+ 8888,
187
+ ),
188
+ model(
189
+ "baseten/Kimi-K2-Instruct-FP4",
190
+ "Kimi K2 Instruct FP4",
191
+ "openai-completions",
192
+ false,
193
+ ["text"],
194
+ 222222,
195
+ 8888,
196
+ ),
197
+ ],
198
+ },
199
+ {
200
+ name: "xiaomi",
201
+ apiKey: "XIAOMI_API_KEY",
202
+ models: [
203
+ model(
204
+ "mimo-v2-flash",
205
+ "MiMo-V2-Flash",
206
+ "anthropic-messages",
207
+ true,
208
+ ["text"],
209
+ 256000,
210
+ 64000,
211
+ ),
212
+ model(
213
+ "mimo-v2-omni",
214
+ "MiMo-V2-Omni",
215
+ "anthropic-messages",
216
+ true,
217
+ ["text", "image"],
218
+ 256000,
219
+ 128000,
220
+ ),
221
+ model(
222
+ "mimo-v2-pro",
223
+ "MiMo-V2-Pro",
224
+ "anthropic-messages",
225
+ true,
226
+ ["text"],
227
+ 1000000,
228
+ 128000,
229
+ ),
230
+ ],
231
+ },
232
+ {
233
+ name: "moonshot",
234
+ apiKey: "MOONSHOT_API_KEY",
235
+ models: [
236
+ model(
237
+ "kimi-k2.5",
238
+ "Kimi K2.5",
239
+ "openai-completions",
240
+ true,
241
+ ["text", "image"],
242
+ 262144,
243
+ 65536,
244
+ ),
245
+ ],
246
+ },
247
+ {
248
+ name: "venice",
249
+ apiKey: "VENICE_API_KEY",
250
+ models: [
251
+ model(
252
+ "claude-sonnet-4-6",
253
+ "Claude Sonnet 4.6",
254
+ "openai-completions",
255
+ true,
256
+ ["text", "image"],
257
+ 1000000,
258
+ 64000,
259
+ ),
260
+ model(
261
+ "claude-opus-4-6",
262
+ "Claude Opus 4.6",
263
+ "openai-completions",
264
+ true,
265
+ ["text", "image"],
266
+ 1000000,
267
+ 128000,
268
+ ),
269
+ model(
270
+ "deepseek-v3.2",
271
+ "DeepSeek V3.2",
272
+ "openai-completions",
273
+ true,
274
+ ["text"],
275
+ 160000,
276
+ 8192,
277
+ ),
278
+ ],
279
+ },
280
+ {
281
+ name: "kilo",
282
+ apiKey: "KILO_API_KEY",
283
+ models: [
284
+ model(
285
+ "anthropic/claude-sonnet-4.6",
286
+ "Claude Sonnet 4.6",
287
+ "openai-completions",
288
+ true,
289
+ ["text"],
290
+ 222222,
291
+ 8888,
292
+ ),
293
+ model(
294
+ "deepseek/deepseek-r1",
295
+ "DeepSeek R1",
296
+ "openai-completions",
297
+ false,
298
+ ["text"],
299
+ 222222,
300
+ 8888,
301
+ ),
302
+ model(
303
+ "arcee-ai/coder-large",
304
+ "Arcee Coder Large",
305
+ "openai-completions",
306
+ false,
307
+ ["text"],
308
+ 222222,
309
+ 8888,
310
+ ),
311
+ ],
312
+ },
313
+ {
314
+ name: "gitlab-duo",
315
+ apiKey: "GITLAB_TOKEN",
316
+ models: [
317
+ model(
318
+ "duo-chat-sonnet-4-6",
319
+ "Duo Chat Sonnet 4.6",
320
+ "anthropic-messages",
321
+ true,
322
+ ["text", "image"],
323
+ 200000,
324
+ 64000,
325
+ ),
326
+ model(
327
+ "duo-chat-opus-4-6",
328
+ "Duo Chat Opus 4.6",
329
+ "anthropic-messages",
330
+ true,
331
+ ["text", "image"],
332
+ 200000,
333
+ 64000,
334
+ ),
335
+ model(
336
+ "duo-chat-gpt-5-2-codex",
337
+ "Duo Chat GPT-5.2 Codex",
338
+ "openai-responses",
339
+ true,
340
+ ["text", "image"],
341
+ 272000,
342
+ 128000,
343
+ ),
344
+ ],
345
+ },
346
+ {
347
+ name: "qwen-portal",
348
+ apiKey: process.env.QWEN_OAUTH_TOKEN
349
+ ? "QWEN_OAUTH_TOKEN"
350
+ : "QWEN_PORTAL_API_KEY",
351
+ models: [
352
+ model(
353
+ "coder-model",
354
+ "Qwen Coder",
355
+ "openai-completions",
356
+ false,
357
+ ["text"],
358
+ 128000,
359
+ 8192,
360
+ ),
361
+ model(
362
+ "vision-model",
363
+ "Qwen Vision",
364
+ "openai-completions",
365
+ false,
366
+ ["text", "image"],
367
+ 128000,
368
+ 8192,
369
+ ),
370
+ ],
371
+ },
372
+ {
373
+ name: "qianfan",
374
+ apiKey: "QIANFAN_API_KEY",
375
+ models: [
376
+ model(
377
+ "deepseek-v3.2",
378
+ "DeepSeek V3.2",
379
+ "openai-completions",
380
+ false,
381
+ ["text"],
382
+ 98304,
383
+ 32768,
384
+ ),
385
+ ],
386
+ },
387
+ {
388
+ name: "cloudflare-ai-gateway",
389
+ apiKey: "CLOUDFLARE_AI_GATEWAY_API_KEY",
390
+ models: [
391
+ model(
392
+ "anthropic/claude-sonnet-4-6",
393
+ "Claude Sonnet 4.6",
394
+ "anthropic-messages",
395
+ true,
396
+ ["text", "image"],
397
+ 200000,
398
+ 64000,
399
+ ),
400
+ model(
401
+ "anthropic/claude-opus-4-6",
402
+ "Claude Opus 4.6",
403
+ "anthropic-messages",
404
+ true,
405
+ ["text", "image"],
406
+ 200000,
407
+ 32000,
408
+ ),
409
+ model(
410
+ "openai/gpt-5.1",
411
+ "GPT-5.1",
412
+ "openai-completions",
413
+ true,
414
+ ["text", "image"],
415
+ 400000,
416
+ 128000,
417
+ ),
418
+ ].map((entry) => ({
419
+ ...entry,
420
+ // Cloudflare requires the Anthropic/OpenAI provider path in the base URL.
421
+ // The canonical endpoint must be supplied by environment in real usage.
422
+ })),
423
+ },
424
+ ];
425
+
426
+ const LOCAL_PROVIDERS: LocalDiscoveryDefinition[] = [
427
+ {
428
+ name: "ollama",
429
+ api: "openai-completions",
430
+ baseUrl: withV1(process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434"),
431
+ apiKeyEnv: "OLLAMA_API_KEY",
432
+ discover: async () => discoverOllamaModels(),
433
+ },
434
+ {
435
+ name: "lm-studio",
436
+ api: "openai-completions",
437
+ baseUrl: process.env.LM_STUDIO_BASE_URL ?? "http://127.0.0.1:1234/v1",
438
+ apiKeyEnv: "LM_STUDIO_API_KEY",
439
+ discover: async () =>
440
+ discoverOpenAICompatibleModels(
441
+ "lm-studio",
442
+ process.env.LM_STUDIO_BASE_URL ?? "http://127.0.0.1:1234/v1",
443
+ "openai-completions",
444
+ ),
445
+ },
446
+ {
447
+ name: "llama.cpp",
448
+ api: "openai-responses",
449
+ baseUrl: process.env.LLAMA_CPP_BASE_URL ?? "http://127.0.0.1:8080",
450
+ apiKeyEnv: "LLAMA_CPP_API_KEY",
451
+ discover: async () =>
452
+ discoverOpenAICompatibleModels(
453
+ "llama.cpp",
454
+ process.env.LLAMA_CPP_BASE_URL ?? "http://127.0.0.1:8080",
455
+ "openai-responses",
456
+ ),
457
+ },
458
+ {
459
+ name: "litellm",
460
+ api: "openai-completions",
461
+ baseUrl: process.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1",
462
+ apiKeyEnv: "LITELLM_API_KEY",
463
+ discover: async () =>
464
+ discoverOpenAICompatibleModels(
465
+ "litellm",
466
+ process.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1",
467
+ "openai-completions",
468
+ ),
469
+ },
470
+ {
471
+ name: "vllm",
472
+ api: "openai-completions",
473
+ baseUrl: process.env.VLLM_BASE_URL ?? "http://127.0.0.1:8000/v1",
474
+ apiKeyEnv: "VLLM_API_KEY",
475
+ discover: async () =>
476
+ discoverOpenAICompatibleModels(
477
+ "vllm",
478
+ process.env.VLLM_BASE_URL ?? "http://127.0.0.1:8000/v1",
479
+ "openai-completions",
480
+ ),
481
+ },
482
+ ];
483
+
484
+ export const AVAILABLE_MODELS = [
485
+ "claude-agent/claude-sonnet-4-6",
486
+ "claude-agent/claude-opus-4-6",
487
+ "anthropic/claude-sonnet-4-6",
488
+ "anthropic/claude-opus-4-6",
489
+ "anthropic/claude-sonnet-4-5",
490
+ "anthropic/claude-opus-4-1",
491
+ "openai/gpt-5.4",
492
+ "openai/gpt-5",
493
+ "openai/gpt-4.1",
494
+ "openai/gpt-4o",
495
+ "openai/o3-mini",
496
+ "openai/o1",
497
+ "google/gemini-2.5-pro",
498
+ "google/gemini-2.5-flash",
499
+ "amazon-bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0",
500
+ "azure-openai-responses/gpt-5.2",
501
+ "openrouter/anthropic/claude-sonnet-4",
502
+ "xai/grok-code-fast-1",
503
+ "zai/glm-4.6",
504
+ "openai-codex/gpt-5-codex",
505
+ "github-copilot/claude-sonnet-4",
506
+ "google-vertex/gemini-2.5-pro",
507
+ "together/moonshotai/Kimi-K2.5",
508
+ "moonshot/kimi-k2.5",
509
+ "nvidia/deepseek-ai/deepseek-v3.2",
510
+ "venice/claude-sonnet-4-6",
511
+ "qianfan/deepseek-v3.2",
512
+ "qwen-portal/coder-model",
513
+ "cloudflare-ai-gateway/anthropic/claude-sonnet-4-6",
514
+ "gitlab-duo/duo-chat-gpt-5-2-codex",
515
+ "xiaomi/mimo-v2-pro",
516
+ "synthetic/hf:deepseek-ai/DeepSeek-V3.2",
517
+ "nanogpt/anthropic/claude-sonnet-4.6",
518
+ "kilo/anthropic/claude-sonnet-4.6",
519
+ ];
520
+
521
+ export async function registerOmniProviders(api: ExtensionAPI): Promise<void> {
522
+ for (const provider of STATIC_PROVIDERS) {
523
+ const baseUrl =
524
+ provider.name === "cloudflare-ai-gateway"
525
+ ? (process.env.CLOUDFLARE_AI_GATEWAY_BASE_URL ??
526
+ "https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic")
527
+ : undefined;
528
+
529
+ api.registerProvider(provider.name, {
530
+ ...(baseUrl ? { baseUrl } : {}),
531
+ apiKey: provider.apiKey,
532
+ models: provider.models.map((entry) => ({
533
+ ...entry,
534
+ ...(baseUrl ? { baseUrl } : {}),
535
+ })),
536
+ });
537
+ }
538
+
539
+ const discovered = await Promise.all(
540
+ LOCAL_PROVIDERS.map(async (provider) => {
541
+ const models = await provider.discover();
542
+ return { provider, models };
543
+ }),
544
+ );
545
+
546
+ for (const { provider, models } of discovered) {
547
+ if (models.length === 0) {
548
+ continue;
549
+ }
550
+
551
+ api.registerProvider(provider.name, {
552
+ baseUrl: provider.baseUrl,
553
+ apiKey: provider.apiKeyEnv ?? "omni-local",
554
+ api: provider.api,
555
+ models,
556
+ });
557
+ }
558
+ }
559
+
560
+ function withV1(baseUrl: string): string {
561
+ const trimmed = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
562
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
563
+ }
564
+
565
+ function withoutV1(baseUrl: string): string {
566
+ return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
567
+ }
568
+
569
+ async function fetchJson(
570
+ input: string,
571
+ init?: RequestInit,
572
+ ): Promise<unknown | null> {
573
+ const controller = new AbortController();
574
+ const timeout = setTimeout(() => controller.abort(), 750);
575
+
576
+ try {
577
+ const response = await fetch(input, {
578
+ ...init,
579
+ signal: controller.signal,
580
+ headers: {
581
+ Accept: "application/json",
582
+ ...(init?.headers ?? {}),
583
+ },
584
+ });
585
+
586
+ if (!response.ok) {
587
+ return null;
588
+ }
589
+
590
+ return await response.json();
591
+ } catch {
592
+ return null;
593
+ } finally {
594
+ clearTimeout(timeout);
595
+ }
596
+ }
597
+
598
+ async function discoverOllamaModels(): Promise<OmniProviderModel[]> {
599
+ const baseUrl = withV1(
600
+ process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434",
601
+ );
602
+ const nativeBaseUrl = withoutV1(baseUrl);
603
+ const payload = (await fetchJson(`${nativeBaseUrl}/api/tags`)) as {
604
+ models?: Array<{ model?: string; name?: string }>;
605
+ } | null;
606
+
607
+ return (payload?.models ?? [])
608
+ .map((entry) => {
609
+ const id = entry.model ?? entry.name;
610
+ if (!id) {
611
+ return null;
612
+ }
613
+
614
+ return model(
615
+ id,
616
+ entry.name ?? id,
617
+ "openai-completions",
618
+ inferReasoning(id),
619
+ inferInput(id),
620
+ 128000,
621
+ 8192,
622
+ );
623
+ })
624
+ .filter((entry): entry is OmniProviderModel => entry !== null)
625
+ .sort((left, right) => left.id.localeCompare(right.id));
626
+ }
627
+
628
+ async function discoverOpenAICompatibleModels(
629
+ provider: string,
630
+ baseUrl: string,
631
+ api: ModelApi,
632
+ ): Promise<OmniProviderModel[]> {
633
+ const normalizedBaseUrl = baseUrl.endsWith("/")
634
+ ? baseUrl.slice(0, -1)
635
+ : baseUrl;
636
+ const headerKey = apiKeyEnvForProvider(provider);
637
+ const headerValue = headerKey ? process.env[headerKey] : undefined;
638
+ const payload = (await fetchJson(`${normalizedBaseUrl}/models`, {
639
+ headers: headerValue
640
+ ? {
641
+ Authorization: `Bearer ${headerValue}`,
642
+ }
643
+ : undefined,
644
+ })) as { data?: Array<{ id?: string }> } | Array<{ id?: string }> | null;
645
+
646
+ const entries = Array.isArray(payload) ? payload : (payload?.data ?? []);
647
+
648
+ return entries
649
+ .map((entry) => entry.id?.trim())
650
+ .filter((id): id is string => Boolean(id))
651
+ .map((id) =>
652
+ model(id, id, api, inferReasoning(id), inferInput(id), 128000, 8192),
653
+ )
654
+ .sort((left, right) => left.id.localeCompare(right.id));
655
+ }
656
+
657
+ function inferReasoning(id: string): boolean {
658
+ return /(reason|thinking|r1|o1|o3|o4|qwq|gpt-oss|sonnet|opus|kimi-k2\.5)/iu.test(
659
+ id,
660
+ );
661
+ }
662
+
663
+ function inferInput(id: string): Array<"text" | "image"> {
664
+ return /(vision|vl|omni|llava|gemma-3|mimo-v2-omni)/iu.test(id)
665
+ ? ["text", "image"]
666
+ : ["text"];
667
+ }
668
+
669
+ function apiKeyEnvForProvider(provider: string): string | undefined {
670
+ switch (provider) {
671
+ case "lm-studio":
672
+ return "LM_STUDIO_API_KEY";
673
+ case "llama.cpp":
674
+ return "LLAMA_CPP_API_KEY";
675
+ case "litellm":
676
+ return "LITELLM_API_KEY";
677
+ case "vllm":
678
+ return "VLLM_API_KEY";
679
+ default:
680
+ return undefined;
681
+ }
682
+ }
package/src/subagents.ts CHANGED
@@ -69,6 +69,54 @@ interface SubagentDeps {
69
69
  loadRunsForAgent?: (agent: string) => RunHistoryEntry[];
70
70
  }
71
71
 
72
+ interface ClaudeAgentTextBlock {
73
+ type: string;
74
+ text?: string;
75
+ }
76
+
77
+ interface ClaudeAgentAssistantMessage {
78
+ type: "assistant";
79
+ message?: {
80
+ content?: ClaudeAgentTextBlock[];
81
+ };
82
+ }
83
+
84
+ interface ClaudeAgentResultMessage {
85
+ type: "result";
86
+ result?: string;
87
+ subtype?: string;
88
+ errors?: string[];
89
+ }
90
+
91
+ interface ClaudeAgentProgressMessage {
92
+ type: "tool_progress" | "session_state_changed";
93
+ title?: string;
94
+ data?: {
95
+ toolName?: string;
96
+ status?: string;
97
+ };
98
+ }
99
+
100
+ type ClaudeAgentMessage =
101
+ | ClaudeAgentAssistantMessage
102
+ | ClaudeAgentResultMessage
103
+ | ClaudeAgentProgressMessage
104
+ | { type: string };
105
+
106
+ interface ClaudeAgentDeps {
107
+ query: (input: {
108
+ prompt: string;
109
+ options: {
110
+ cwd: string;
111
+ model: string;
112
+ permissionMode: "bypassPermissions";
113
+ allowDangerouslySkipPermissions: boolean;
114
+ canUseTool: () => Promise<{ behavior: "allow" }>;
115
+ env: Record<string, string | undefined>;
116
+ };
117
+ }) => AsyncIterable<ClaudeAgentMessage>;
118
+ }
119
+
72
120
  export interface RunHistoryEntry {
73
121
  agent: string;
74
122
  task: string;
@@ -195,6 +243,13 @@ export async function loadSubagentDeps(
195
243
  } as SubagentDeps;
196
244
  }
197
245
 
246
+ export async function loadClaudeAgentDeps(): Promise<ClaudeAgentDeps> {
247
+ const sdkModule = await import("@anthropic-ai/claude-agent-sdk");
248
+ return {
249
+ query: sdkModule.query,
250
+ };
251
+ }
252
+
198
253
  export async function loadRunHistory(
199
254
  packageDir = omniPackageDir(),
200
255
  ): Promise<{ loadRunsForAgent: (agent: string) => RunHistoryEntry[] } | null> {
@@ -711,6 +766,145 @@ function findAgent(
711
766
  return fallback;
712
767
  }
713
768
 
769
+ function getAgentConfig(
770
+ agents: SubagentConfig[],
771
+ preferred: string,
772
+ fallback: string,
773
+ ): SubagentConfig | undefined {
774
+ return (
775
+ agents.find((agent) => agent.name === preferred) ??
776
+ agents.find((agent) => agent.name === fallback)
777
+ );
778
+ }
779
+
780
+ function isClaudeAgentModel(model: string | undefined): boolean {
781
+ return model?.startsWith("claude-agent/") ?? false;
782
+ }
783
+
784
+ function stripClaudeAgentPrefix(model: string): string {
785
+ return model.replace(/^claude-agent\//u, "");
786
+ }
787
+
788
+ function isClaudeAgentResultMessage(
789
+ message: ClaudeAgentMessage,
790
+ ): message is ClaudeAgentResultMessage {
791
+ return message.type === "result";
792
+ }
793
+
794
+ function isClaudeAgentAssistantMessage(
795
+ message: ClaudeAgentMessage,
796
+ ): message is ClaudeAgentAssistantMessage {
797
+ return message.type === "assistant";
798
+ }
799
+
800
+ function isClaudeAgentProgressMessage(
801
+ message: ClaudeAgentMessage,
802
+ ): message is ClaudeAgentProgressMessage {
803
+ return (
804
+ message.type === "tool_progress" || message.type === "session_state_changed"
805
+ );
806
+ }
807
+
808
+ function extractClaudeAgentRawOutput(messages: ClaudeAgentMessage[]): string {
809
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
810
+ const message = messages[index];
811
+ if (
812
+ isClaudeAgentResultMessage(message) &&
813
+ typeof message.result === "string" &&
814
+ message.result.trim().length > 0
815
+ ) {
816
+ return message.result;
817
+ }
818
+ }
819
+
820
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
821
+ const message = messages[index];
822
+ if (
823
+ isClaudeAgentResultMessage(message) &&
824
+ Array.isArray(message.errors) &&
825
+ message.errors.length > 0
826
+ ) {
827
+ return message.errors.join("\n");
828
+ }
829
+ }
830
+
831
+ const assistantText = messages
832
+ .flatMap((message) =>
833
+ isClaudeAgentAssistantMessage(message)
834
+ ? (message.message?.content
835
+ ?.filter(
836
+ (block): block is ClaudeAgentTextBlock & { text: string } =>
837
+ typeof block.text === "string" && block.text.trim().length > 0,
838
+ )
839
+ .map((block) => block.text) ?? [])
840
+ : [],
841
+ )
842
+ .join("\n\n")
843
+ .trim();
844
+
845
+ return assistantText;
846
+ }
847
+
848
+ async function runClaudeAgentTask(
849
+ rootDir: string,
850
+ ctx: ExtensionCommandContext,
851
+ claudeDeps: ClaudeAgentDeps,
852
+ agentName: string,
853
+ agentModel: string,
854
+ prompt: string,
855
+ ): Promise<SubagentSingleResult> {
856
+ const messages: ClaudeAgentMessage[] = [];
857
+
858
+ try {
859
+ const query = claudeDeps.query({
860
+ prompt,
861
+ options: {
862
+ cwd: rootDir,
863
+ model: stripClaudeAgentPrefix(agentModel),
864
+ permissionMode: "bypassPermissions",
865
+ allowDangerouslySkipPermissions: true,
866
+ canUseTool: async () => ({ behavior: "allow" }),
867
+ env: {
868
+ ...process.env,
869
+ CLAUDE_AGENT_SDK_CLIENT_APP: "omni-pi",
870
+ },
871
+ },
872
+ });
873
+
874
+ for await (const message of query) {
875
+ messages.push(message);
876
+ if (
877
+ isClaudeAgentProgressMessage(message) &&
878
+ message.type === "tool_progress"
879
+ ) {
880
+ const toolName = message.data?.toolName ?? message.title ?? "working";
881
+ ctx.ui.setStatus("omni", `${agentName}: ${toolName}`);
882
+ } else if (
883
+ isClaudeAgentProgressMessage(message) &&
884
+ message.type === "session_state_changed"
885
+ ) {
886
+ const status = message.data?.status;
887
+ if (status) {
888
+ ctx.ui.setStatus("omni", `${agentName}: ${status}`);
889
+ }
890
+ }
891
+ }
892
+
893
+ return {
894
+ agent: agentName,
895
+ exitCode: 0,
896
+ messages,
897
+ };
898
+ } catch (error) {
899
+ return {
900
+ agent: agentName,
901
+ exitCode: 1,
902
+ messages,
903
+ error: error instanceof Error ? error.message : String(error),
904
+ };
905
+ }
906
+ }
907
+
714
908
  const AGENT_ROLE_MAP: Record<string, keyof OmniConfig["models"]> = {
715
909
  "omni-worker": "worker",
716
910
  "omni-expert": "expert",
@@ -740,13 +934,25 @@ export async function createSubagentWorkEngine(
740
934
  ctx: ExtensionCommandContext,
741
935
  deps?: SubagentDeps,
742
936
  verificationExecutor?: VerificationExecutor,
937
+ claudeDeps?: ClaudeAgentDeps,
743
938
  ): Promise<WorkEngine> {
744
939
  const subagentDeps = deps ?? (await loadSubagentDeps());
940
+ const resolvedClaudeDeps = claudeDeps;
745
941
  const config = await readConfig(rootDir);
746
942
  const discovery = subagentDeps.discoverAgents(rootDir, "both");
747
943
  const agentsWithOverrides = applyModelOverrides(discovery.agents, config);
748
944
  const workerAgent = findAgent(agentsWithOverrides, "omni-worker", "worker");
749
945
  const expertAgent = findAgent(agentsWithOverrides, "omni-expert", "reviewer");
946
+ const workerAgentConfig = getAgentConfig(
947
+ agentsWithOverrides,
948
+ "omni-worker",
949
+ "worker",
950
+ );
951
+ const expertAgentConfig = getAgentConfig(
952
+ agentsWithOverrides,
953
+ "omni-expert",
954
+ "reviewer",
955
+ );
750
956
  const sessionDir = path.join(rootDir, ".omni", "subagent-sessions");
751
957
  const packageDir = omniPackageDir();
752
958
  const skillTriggers = await loadSkillTriggers(
@@ -768,37 +974,51 @@ export async function createSubagentWorkEngine(
768
974
  runWorkerTask: async (task, attempt) => {
769
975
  const verificationPlan = await readVerificationPlan(rootDir, task);
770
976
  const preReadContext = await gatherTaskContext(rootDir, task, 4000);
977
+ const workerPrompt = buildWorkerPrompt(
978
+ task,
979
+ verificationPlan,
980
+ getSkillContext(task),
981
+ preReadContext,
982
+ );
771
983
  ctx.ui.setStatus(
772
984
  "omni",
773
985
  `Worker ${workerAgent} is handling ${task.id} (attempt ${attempt})`,
774
986
  );
775
987
  const startTime = Date.now();
776
- const result = await subagentDeps.runSync(
777
- rootDir,
778
- agentsWithOverrides,
779
- workerAgent,
780
- buildWorkerPrompt(
781
- task,
782
- verificationPlan,
783
- getSkillContext(task),
784
- preReadContext,
785
- ),
786
- {
787
- cwd: rootDir,
788
- runId: randomUUID(),
789
- sessionDir,
790
- onUpdate: (update) => {
791
- const progress = update.details?.progress?.[0];
792
- if (progress) {
793
- ctx.ui.setStatus(
794
- "omni",
795
- `${progress.agent}: ${progress.currentTool ?? "working"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
796
- );
797
- }
798
- },
799
- },
800
- );
801
- const raw = subagentDeps.getFinalOutput(result.messages);
988
+ const result =
989
+ workerAgentConfig?.model && isClaudeAgentModel(workerAgentConfig.model)
990
+ ? await runClaudeAgentTask(
991
+ rootDir,
992
+ ctx,
993
+ resolvedClaudeDeps ?? (await loadClaudeAgentDeps()),
994
+ workerAgent,
995
+ workerAgentConfig.model,
996
+ workerPrompt,
997
+ )
998
+ : await subagentDeps.runSync(
999
+ rootDir,
1000
+ agentsWithOverrides,
1001
+ workerAgent,
1002
+ workerPrompt,
1003
+ {
1004
+ cwd: rootDir,
1005
+ runId: randomUUID(),
1006
+ sessionDir,
1007
+ onUpdate: (update) => {
1008
+ const progress = update.details?.progress?.[0];
1009
+ if (progress) {
1010
+ ctx.ui.setStatus(
1011
+ "omni",
1012
+ `${progress.agent}: ${progress.currentTool ?? "working"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
1013
+ );
1014
+ }
1015
+ },
1016
+ },
1017
+ );
1018
+ const raw =
1019
+ workerAgentConfig?.model && isClaudeAgentModel(workerAgentConfig.model)
1020
+ ? extractClaudeAgentRawOutput(result.messages as ClaudeAgentMessage[])
1021
+ : subagentDeps.getFinalOutput(result.messages);
802
1022
  const rawOutputPath = path.join(
803
1023
  rootDir,
804
1024
  ".omni",
@@ -871,34 +1091,48 @@ export async function createSubagentWorkEngine(
871
1091
  `Escalating ${task.id} to expert after ${escalation.priorAttempts} failed attempts. Failed checks: ${failedChecksSummary}`,
872
1092
  );
873
1093
  const preReadContext = await gatherTaskContext(rootDir, task, 6000);
874
- const expertStartTime = Date.now();
875
- const result = await subagentDeps.runSync(
876
- rootDir,
877
- agentsWithOverrides,
878
- expertAgent,
879
- buildExpertPrompt(
880
- task,
881
- escalation,
882
- verificationPlan,
883
- getSkillContext(task),
884
- preReadContext,
885
- ),
886
- {
887
- cwd: rootDir,
888
- runId: randomUUID(),
889
- sessionDir,
890
- onUpdate: (update) => {
891
- const progress = update.details?.progress?.[0];
892
- if (progress) {
893
- ctx.ui.setStatus(
894
- "omni",
895
- `${progress.agent}: ${progress.currentTool ?? "resolving"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
896
- );
897
- }
898
- },
899
- },
1094
+ const expertPrompt = buildExpertPrompt(
1095
+ task,
1096
+ escalation,
1097
+ verificationPlan,
1098
+ getSkillContext(task),
1099
+ preReadContext,
900
1100
  );
901
- const raw = subagentDeps.getFinalOutput(result.messages);
1101
+ const expertStartTime = Date.now();
1102
+ const result =
1103
+ expertAgentConfig?.model && isClaudeAgentModel(expertAgentConfig.model)
1104
+ ? await runClaudeAgentTask(
1105
+ rootDir,
1106
+ ctx,
1107
+ resolvedClaudeDeps ?? (await loadClaudeAgentDeps()),
1108
+ expertAgent,
1109
+ expertAgentConfig.model,
1110
+ expertPrompt,
1111
+ )
1112
+ : await subagentDeps.runSync(
1113
+ rootDir,
1114
+ agentsWithOverrides,
1115
+ expertAgent,
1116
+ expertPrompt,
1117
+ {
1118
+ cwd: rootDir,
1119
+ runId: randomUUID(),
1120
+ sessionDir,
1121
+ onUpdate: (update) => {
1122
+ const progress = update.details?.progress?.[0];
1123
+ if (progress) {
1124
+ ctx.ui.setStatus(
1125
+ "omni",
1126
+ `${progress.agent}: ${progress.currentTool ?? "resolving"}${progress.toolCount ? ` (${progress.toolCount} tools)` : ""}`,
1127
+ );
1128
+ }
1129
+ },
1130
+ },
1131
+ );
1132
+ const raw =
1133
+ expertAgentConfig?.model && isClaudeAgentModel(expertAgentConfig.model)
1134
+ ? extractClaudeAgentRawOutput(result.messages as ClaudeAgentMessage[])
1135
+ : subagentDeps.getFinalOutput(result.messages);
902
1136
  const rawOutputPath = path.join(
903
1137
  rootDir,
904
1138
  ".omni",