omnirush 0.8.0 → 0.8.2
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/assets/extensions/omnirush/agents.ts +1 -1
- package/assets/extensions/omnirush/capture/workspace-collector.ts +60 -11
- package/assets/extensions/omnirush/collector.ts +22 -2
- package/assets/extensions/omnirush/pi-engine.ts +16 -5
- package/assets/extensions/omnirush/sota.ts +11 -1
- package/assets/models.json +1 -1
- package/package.json +2 -2
- package/src/bin.js +62 -11
- package/src/lib.js +86 -7
|
@@ -41,7 +41,7 @@ const SpawnAgentsParams = Type.Object({
|
|
|
41
41
|
}),
|
|
42
42
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
43
43
|
model: Type.Optional(Type.String({
|
|
44
|
-
description: 'Cross-model child: gateway model id for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "gpt-6-astra", "gpt-5.6-sol"). Omit to inherit this session\'s model',
|
|
44
|
+
description: 'Cross-model child: gateway model id for THIS task (e.g. "muse-spark-1.3", "muse-spark-1.2-contributor" for cheap swarm workers, "meta-muse-spark", "muse-spark-1.1", "gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol"). Omit to inherit this session\'s model',
|
|
45
45
|
})),
|
|
46
46
|
}),
|
|
47
47
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
@@ -89,6 +89,9 @@ const ENVELOPE_WRITE_CHUNK_BYTES = 64 * 1024;
|
|
|
89
89
|
const MAX_CHANGE_JOURNAL_ENTRIES = 512;
|
|
90
90
|
const MAX_CHANGE_JOURNAL_BYTES = 768 * 1024;
|
|
91
91
|
const MAX_SESSION_LEDGER_ENTRIES = 512;
|
|
92
|
+
const SESSION_LEDGER_LOCK_TIMEOUT_MS = 5_000;
|
|
93
|
+
const SESSION_LEDGER_LOCK_STALE_MS = 60_000;
|
|
94
|
+
const SESSION_LEDGER_LOCK_RETRY_MS = 25;
|
|
92
95
|
const MAX_TOUCHED_PATHS = 128;
|
|
93
96
|
const MAX_GIT_STATUS_ENTRIES = 500;
|
|
94
97
|
const MAX_GIT_RECENT_COMMITS = 50;
|
|
@@ -2977,6 +2980,44 @@ async function writeFileAtomic(path: string, data: Buffer | string): Promise<voi
|
|
|
2977
2980
|
}
|
|
2978
2981
|
}
|
|
2979
2982
|
|
|
2983
|
+
async function withSessionLedgerLock<T>(lockPath: string, task: () => Promise<T>): Promise<T> {
|
|
2984
|
+
const started = Date.now();
|
|
2985
|
+
await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
2986
|
+
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
2987
|
+
while (!handle) {
|
|
2988
|
+
try {
|
|
2989
|
+
handle = await open(lockPath, "wx", 0o600);
|
|
2990
|
+
await handle.writeFile(`${process.pid}\n`);
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
await handle?.close().catch(() => undefined);
|
|
2993
|
+
handle = null;
|
|
2994
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
2995
|
+
? (error as { code?: string }).code
|
|
2996
|
+
: undefined;
|
|
2997
|
+
if (code !== "EEXIST") throw error;
|
|
2998
|
+
try {
|
|
2999
|
+
const info = await stat(lockPath);
|
|
3000
|
+
if (Date.now() - info.mtimeMs > SESSION_LEDGER_LOCK_STALE_MS) {
|
|
3001
|
+
await rm(lockPath, { force: true });
|
|
3002
|
+
continue;
|
|
3003
|
+
}
|
|
3004
|
+
} catch {
|
|
3005
|
+
// The owner may have released the lock between EEXIST and stat.
|
|
3006
|
+
}
|
|
3007
|
+
if (Date.now() - started >= SESSION_LEDGER_LOCK_TIMEOUT_MS) {
|
|
3008
|
+
throw new Error("timed out waiting for the session ledger lock");
|
|
3009
|
+
}
|
|
3010
|
+
await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, SESSION_LEDGER_LOCK_RETRY_MS));
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
return await task();
|
|
3015
|
+
} finally {
|
|
3016
|
+
await handle.close().catch(() => undefined);
|
|
3017
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
|
|
2980
3021
|
function parseSpoolMeta(value: unknown): SpoolMeta | null {
|
|
2981
3022
|
if (!value || typeof value !== "object") return null;
|
|
2982
3023
|
const record = value as Partial<SpoolMeta>;
|
|
@@ -3105,19 +3146,27 @@ export class WorkspaceCollector {
|
|
|
3105
3146
|
this.ledger = await readSessionLedger(this.ledgerPath);
|
|
3106
3147
|
}
|
|
3107
3148
|
|
|
3108
|
-
private async saveLedger(): Promise<void> {
|
|
3149
|
+
private async saveLedger(sessionId: string): Promise<void> {
|
|
3109
3150
|
if (!this.ledgerPath) return;
|
|
3110
3151
|
await this.ledgerReady;
|
|
3111
|
-
const
|
|
3112
|
-
.
|
|
3113
|
-
|
|
3114
|
-
this.ledger.sessions = Object.fromEntries(entries);
|
|
3115
|
-
const snapshot = JSON.stringify(this.ledger);
|
|
3152
|
+
const record = this.ledger.sessions[sessionId]
|
|
3153
|
+
? JSON.parse(JSON.stringify(this.ledger.sessions[sessionId])) as SessionLedgerRecord
|
|
3154
|
+
: undefined;
|
|
3116
3155
|
this.ledgerWriteTail = this.ledgerWriteTail
|
|
3117
3156
|
.catch(() => undefined)
|
|
3118
3157
|
.then(async () => {
|
|
3119
|
-
await
|
|
3120
|
-
|
|
3158
|
+
await withSessionLedgerLock(`${this.ledgerPath!}.lock`, async () => {
|
|
3159
|
+
// Reload while holding the lock so concurrent processes merge their
|
|
3160
|
+
// session record instead of overwriting one another's progress.
|
|
3161
|
+
const merged = await readSessionLedger(this.ledgerPath);
|
|
3162
|
+
if (record) merged.sessions[sessionId] = record;
|
|
3163
|
+
const entries = Object.entries(merged.sessions)
|
|
3164
|
+
.sort(([, left], [, right]) => right.lastSeenAt.localeCompare(left.lastSeenAt))
|
|
3165
|
+
.slice(0, MAX_SESSION_LEDGER_ENTRIES);
|
|
3166
|
+
merged.sessions = Object.fromEntries(entries);
|
|
3167
|
+
this.ledger = merged;
|
|
3168
|
+
await writeFileAtomic(this.ledgerPath!, JSON.stringify(merged));
|
|
3169
|
+
});
|
|
3121
3170
|
});
|
|
3122
3171
|
await this.ledgerWriteTail;
|
|
3123
3172
|
}
|
|
@@ -3158,13 +3207,13 @@ export class WorkspaceCollector {
|
|
|
3158
3207
|
this.appendTrace(state, "session.resumed", { session_segment: state.segment, previous_segment: previous?.segment ?? null });
|
|
3159
3208
|
}
|
|
3160
3209
|
this.ledger.sessions[state.id] = this.ledgerRecord(state);
|
|
3161
|
-
await this.saveLedger();
|
|
3210
|
+
await this.saveLedger(state.id);
|
|
3162
3211
|
}
|
|
3163
3212
|
|
|
3164
3213
|
private async persistSession(state: SessionState): Promise<void> {
|
|
3165
3214
|
await this.ledgerReady;
|
|
3166
3215
|
this.ledger.sessions[state.id] = this.ledgerRecord(state);
|
|
3167
|
-
await this.saveLedger();
|
|
3216
|
+
await this.saveLedger(state.id);
|
|
3168
3217
|
}
|
|
3169
3218
|
|
|
3170
3219
|
private async recordLedgerOutcome(sessionId: string, outcome: "success" | "failure"): Promise<void> {
|
|
@@ -3188,7 +3237,7 @@ export class WorkspaceCollector {
|
|
|
3188
3237
|
record.lastFailureAt = now;
|
|
3189
3238
|
}
|
|
3190
3239
|
record.lastSeenAt = now;
|
|
3191
|
-
await this.saveLedger();
|
|
3240
|
+
await this.saveLedger(sessionId);
|
|
3192
3241
|
}
|
|
3193
3242
|
|
|
3194
3243
|
async sessionCheckpoint(sessionId: string): Promise<{ resumed: boolean; segment?: number; lastMessageId?: string }> {
|
|
@@ -315,6 +315,10 @@ export default function (pi: any) {
|
|
|
315
315
|
lastMessageId: string | undefined;
|
|
316
316
|
toolStarts: Map<string, number>;
|
|
317
317
|
seenFiles: Map<string, string>;
|
|
318
|
+
/** Prompts sent in this process with the model and effort they went with, until their user entry is found. */
|
|
319
|
+
pendingPrompts: Array<{ at: number; model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
320
|
+
/** User entry id -> what its prompt was sent with (pi-engine promptSettings). */
|
|
321
|
+
promptSettings: Map<string, { model: { providerID: string; modelID: string } | null; variant: string | null }>;
|
|
318
322
|
} | null = null;
|
|
319
323
|
let settling: Promise<void> = Promise.resolve();
|
|
320
324
|
|
|
@@ -338,7 +342,18 @@ export default function (pi: any) {
|
|
|
338
342
|
|
|
339
343
|
const messagesOf = (state: NonNullable<typeof session>): EngineMessage[] => {
|
|
340
344
|
const entries = state.ctx?.sessionManager?.getBranch?.() ?? [];
|
|
341
|
-
|
|
345
|
+
// Each prompt of this process is the first user entry written at or
|
|
346
|
+
// after it went out (pi stamps the message when it appends it).
|
|
347
|
+
for (const pending of state.pendingPrompts.splice(0)) {
|
|
348
|
+
const entry = entries.find((candidate: any) =>
|
|
349
|
+
candidate?.type === "message" && candidate.message?.role === "user" && typeof candidate.id === "string"
|
|
350
|
+
&& !state.promptSettings.has(candidate.id) && Number(candidate.message.timestamp ?? Date.parse(candidate.timestamp)) >= pending.at - 1_000);
|
|
351
|
+
if (entry) state.promptSettings.set(entry.id, { model: pending.model, variant: pending.variant });
|
|
352
|
+
// Not written yet (read right as the prompt went out); a prompt that
|
|
353
|
+
// never wrote a user entry (handled by an extension) is let go.
|
|
354
|
+
else if (pending.at > Date.now() - 60_000) state.pendingPrompts.push(pending);
|
|
355
|
+
}
|
|
356
|
+
return engineMessagesFromEntries(entries, { sessionId: state.id, agent: ROOT_AGENT, cwd: state.root, toolStarts: state.toolStarts, promptSettings: state.promptSettings });
|
|
342
357
|
};
|
|
343
358
|
|
|
344
359
|
/**
|
|
@@ -351,7 +366,7 @@ export default function (pi: any) {
|
|
|
351
366
|
if (!id) return null;
|
|
352
367
|
if (session?.id !== id) {
|
|
353
368
|
const root = ctx?.cwd || process.cwd();
|
|
354
|
-
session = { id, root, ctx, started: false, running: false, lastMessageId: undefined, toolStarts: new Map(), seenFiles: new Map() };
|
|
369
|
+
session = { id, root, ctx, started: false, running: false, lastMessageId: undefined, toolStarts: new Map(), seenFiles: new Map(), pendingPrompts: [], promptSettings: new Map() };
|
|
355
370
|
}
|
|
356
371
|
session.ctx = ctx;
|
|
357
372
|
// Idempotent: a session already started in this process keeps its segment.
|
|
@@ -496,6 +511,11 @@ export default function (pi: any) {
|
|
|
496
511
|
if (!state) return;
|
|
497
512
|
state.running = true;
|
|
498
513
|
const model = ctx?.model;
|
|
514
|
+
state.pendingPrompts.push({
|
|
515
|
+
at: Date.now(),
|
|
516
|
+
model: model && typeof model.provider === "string" && typeof model.id === "string" ? { providerID: model.provider, modelID: model.id } : null,
|
|
517
|
+
variant: typeof ctx?.thinkingLevel === "string" && ctx.thinkingLevel !== "off" ? ctx.thinkingLevel : null,
|
|
518
|
+
});
|
|
499
519
|
const images = Array.isArray(event?.images) ? event.images : [];
|
|
500
520
|
// The prompt as the desktop records its engine request: text, attached
|
|
501
521
|
// files (inline bytes replaced by a marker), model, variant, agent and
|
|
@@ -74,6 +74,12 @@ export type EngineConvertOptions = {
|
|
|
74
74
|
initialModel?: { providerID: string; modelID: string } | null;
|
|
75
75
|
/** The session's thinking level before its first thinking_level_change entry, if known. */
|
|
76
76
|
initialThinkingLevel?: string | null;
|
|
77
|
+
/**
|
|
78
|
+
* What a prompt was really sent with, by its user entry id, as the live
|
|
79
|
+
* session knew it: pi records no model or thinking-level change for the
|
|
80
|
+
* `--model <id>:<effort>` flag of a resumed session.
|
|
81
|
+
*/
|
|
82
|
+
promptSettings?: ReadonlyMap<string, { model?: { providerID: string; modelID: string } | null; variant?: string | null }>;
|
|
77
83
|
};
|
|
78
84
|
|
|
79
85
|
/** The shell-command marker the engine writes on a user message holding a command the user ran (public_trace.py `_USER_SHELL_TEXT`). */
|
|
@@ -265,8 +271,12 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
265
271
|
let lastUserId: string | null = null;
|
|
266
272
|
const variant = () => (thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : null);
|
|
267
273
|
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
// The effort of the prompt the current turn answers (a live setting when known).
|
|
275
|
+
let turnVariant: string | null = null;
|
|
276
|
+
const userMessage = (id: string, created: number | null, parts: Array<Record<string, unknown>>, index: number, extraInfo: Record<string, unknown> = {}): string | null => {
|
|
277
|
+
const live = typeof entries[index]?.id === "string" ? options.promptSettings?.get(entries[index]!.id as string) : undefined;
|
|
278
|
+
const messageModel = nextAssistantModel.get(index) ?? live?.model ?? model ?? null;
|
|
279
|
+
const messageVariant = live && live.variant !== undefined ? live.variant : variant();
|
|
270
280
|
const info: Record<string, unknown> = {
|
|
271
281
|
id,
|
|
272
282
|
sessionID: sessionId,
|
|
@@ -274,7 +284,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
274
284
|
time: { created: created ?? 0 },
|
|
275
285
|
agent,
|
|
276
286
|
...(messageModel ? { model: { providerID: messageModel.providerID, modelID: messageModel.modelID } } : {}),
|
|
277
|
-
...(
|
|
287
|
+
...(messageVariant ? { variant: messageVariant } : {}),
|
|
278
288
|
...extraInfo,
|
|
279
289
|
};
|
|
280
290
|
out.push({
|
|
@@ -282,6 +292,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
282
292
|
parts: parts.map((part, partIndex) => ({ id: partId(id, partIndex), sessionID: sessionId, messageID: id, ...part })),
|
|
283
293
|
});
|
|
284
294
|
lastUserId = id;
|
|
295
|
+
return messageVariant ?? null;
|
|
285
296
|
};
|
|
286
297
|
|
|
287
298
|
entries.forEach((entry, index) => {
|
|
@@ -339,7 +350,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
339
350
|
switch (message.role) {
|
|
340
351
|
case "user": {
|
|
341
352
|
const parts = contentParts(message.content);
|
|
342
|
-
if (parts.length > 0) userMessage(id, created, parts, index);
|
|
353
|
+
if (parts.length > 0) turnVariant = userMessage(id, created, parts, index);
|
|
343
354
|
return;
|
|
344
355
|
}
|
|
345
356
|
case "custom": {
|
|
@@ -463,7 +474,7 @@ export function engineMessagesFromEntries(entries: readonly PiEntry[], options:
|
|
|
463
474
|
model = model ?? { providerID: message.provider, modelID: message.model };
|
|
464
475
|
}
|
|
465
476
|
const error = assistantError(message);
|
|
466
|
-
const effort = typeof message.providerThinkingLevel === "string" && message.providerThinkingLevel ? message.providerThinkingLevel : variant();
|
|
477
|
+
const effort = typeof message.providerThinkingLevel === "string" && message.providerThinkingLevel ? message.providerThinkingLevel : turnVariant ?? variant();
|
|
467
478
|
out.push({
|
|
468
479
|
info: {
|
|
469
480
|
id,
|
|
@@ -57,7 +57,7 @@ const STRICT_EXIT_CODE = 3;
|
|
|
57
57
|
const CATALOG_MODELS = [
|
|
58
58
|
{
|
|
59
59
|
id: "gpt-6-astra",
|
|
60
|
-
name: "GPT
|
|
60
|
+
name: "GPT 6 Astra",
|
|
61
61
|
api: "openai-responses",
|
|
62
62
|
reasoning: true,
|
|
63
63
|
input: ["text", "image"],
|
|
@@ -65,6 +65,16 @@ const CATALOG_MODELS = [
|
|
|
65
65
|
contextWindow: 400000,
|
|
66
66
|
compat: { supportsMaxOutputTokens: false },
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
id: "gpt-6-sol",
|
|
70
|
+
name: "GPT 6 Sol",
|
|
71
|
+
api: "openai-responses",
|
|
72
|
+
reasoning: true,
|
|
73
|
+
input: ["text", "image"],
|
|
74
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
75
|
+
contextWindow: 400000,
|
|
76
|
+
compat: { supportsMaxOutputTokens: false },
|
|
77
|
+
},
|
|
68
78
|
{
|
|
69
79
|
id: "gpt-5.6-sol",
|
|
70
80
|
name: "GPT-5.6 Sol",
|
package/assets/models.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnirush",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Omnirush \u2014 free daily tokens for the most powerful coding model on earth.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"minimatch": "^10.2.6",
|
|
42
42
|
"zod": "^4.6.5"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
package/src/bin.js
CHANGED
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
// OMNIRUSH_GATEWAY_URL provider base URL override ({origin}/v1)
|
|
19
19
|
// OMNIRUSH_DIR state dir override (default ~/.omnirush)
|
|
20
20
|
// OMNIRUSH_MODEL model override for plain runs (default
|
|
21
|
-
// gpt-6-astra; gpt-5.6-sol
|
|
21
|
+
// gpt-6-astra; gpt-6-sol, gpt-5.6-sol and the Muse
|
|
22
|
+
// models also available; user
|
|
22
23
|
// --model still wins)
|
|
23
24
|
//
|
|
24
25
|
// Agent runs default to `--provider omnirush --model gpt-6-astra`; the
|
|
25
|
-
// catalog also carries gpt-
|
|
26
|
-
// OMNIRUSH_MODEL=gpt-
|
|
26
|
+
// catalog also carries gpt-6-sol, gpt-5.6-sol, meta-muse-spark and
|
|
27
|
+
// muse-spark-1.1 (`--model gpt-6-sol` or OMNIRUSH_MODEL=gpt-6-sol), refreshed
|
|
28
|
+
// from the gateway's GET /v1/models at launch. The sota extension watches every response,
|
|
27
29
|
// refreshes the device token single-flight on 401 (retry once), and
|
|
28
30
|
// warns on stderr if the gateway serves a different model than
|
|
29
31
|
// requested; `--strict-sota` makes that violation fatal (exit 3).
|
|
@@ -39,6 +41,7 @@ import { fileURLToPath } from "node:url";
|
|
|
39
41
|
|
|
40
42
|
import {
|
|
41
43
|
DEFAULT_ORIGIN,
|
|
44
|
+
catalogFromGateway,
|
|
42
45
|
modelsConfig,
|
|
43
46
|
resolveGatewayUrl,
|
|
44
47
|
withModelDefaults,
|
|
@@ -147,7 +150,53 @@ function installDefaultThinkingLevel() {
|
|
|
147
150
|
// Merge our provider into the user's models.json without clobbering
|
|
148
151
|
// unrelated providers. Ours is authoritative: this file is how the
|
|
149
152
|
// product ships its provider. Other providers untouched.
|
|
150
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The model list for this run, as the desktop app gets it: the gateway's
|
|
155
|
+
* catalog (GET <gateway>/models, bounded to a couple of seconds), cached in
|
|
156
|
+
* the state dir so an offline start still shows the last list; the shipped
|
|
157
|
+
* list (src/lib.js MODELS) when neither is available. New models then
|
|
158
|
+
* appear without a CLI release. OMNIRUSH_STATIC_MODELS=1 skips the fetch.
|
|
159
|
+
*/
|
|
160
|
+
async function launchModels(gatewayUrl, accessToken) {
|
|
161
|
+
const cacheFile = path.join(OMNI_DIR, "model-catalog.json");
|
|
162
|
+
const base = String(gatewayUrl).replace(/\/+$/, "");
|
|
163
|
+
if (process.env.OMNIRUSH_STATIC_MODELS !== "1" && accessToken) {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(`${base}/models`, {
|
|
166
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/json" },
|
|
167
|
+
signal: AbortSignal.timeout(2500),
|
|
168
|
+
});
|
|
169
|
+
if (response.ok) {
|
|
170
|
+
const payload = await response.json();
|
|
171
|
+
const models = catalogFromGateway(payload);
|
|
172
|
+
if (models) {
|
|
173
|
+
try {
|
|
174
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ gateway: base, fetched_at: new Date().toISOString(), payload }), { mode: 0o600 });
|
|
175
|
+
} catch {
|
|
176
|
+
/* the cache is a convenience */
|
|
177
|
+
}
|
|
178
|
+
return models;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
await response.body?.cancel().catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
/* offline, slow or refused: the cached or shipped list */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
|
|
189
|
+
if (cached && cached.gateway === base) {
|
|
190
|
+
const models = catalogFromGateway(cached.payload);
|
|
191
|
+
if (models) return models;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
/* no cache yet */
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function installModelsJson(gatewayUrl, models) {
|
|
151
200
|
let current = {};
|
|
152
201
|
try {
|
|
153
202
|
current = JSON.parse(fs.readFileSync(PI_MODELS_JSON, "utf8"));
|
|
@@ -157,7 +206,7 @@ function installModelsJson(gatewayUrl) {
|
|
|
157
206
|
if (typeof current !== "object" || current === null) current = {};
|
|
158
207
|
current.providers = {
|
|
159
208
|
...(current.providers || {}),
|
|
160
|
-
...modelsConfig({ gatewayUrl }).providers,
|
|
209
|
+
...modelsConfig({ gatewayUrl, ...(models ? { models } : {}) }).providers,
|
|
161
210
|
};
|
|
162
211
|
fs.writeFileSync(PI_MODELS_JSON, JSON.stringify(current, null, 2) + "\n");
|
|
163
212
|
}
|
|
@@ -505,7 +554,7 @@ async function cmdDoctor() {
|
|
|
505
554
|
/* update check is advisory only */
|
|
506
555
|
}
|
|
507
556
|
|
|
508
|
-
installModelsJson(gateway);
|
|
557
|
+
installModelsJson(gateway, await launchModels(gateway, null));
|
|
509
558
|
installExtensions();
|
|
510
559
|
try {
|
|
511
560
|
installManagedTools({ agentDir: PI_AGENT_DIR, runtimeRoot: path.resolve(__dirname, "..", ".runtime") });
|
|
@@ -528,7 +577,7 @@ async function cmdDoctor() {
|
|
|
528
577
|
} else {
|
|
529
578
|
console.log(
|
|
530
579
|
`WARN agent core — installed core is v${coreVersion}, this omnirush expects v${own} ` +
|
|
531
|
-
"(stale install:
|
|
580
|
+
"(stale install: agent update banners, missing omnirush provider). Fix: omnirush update",
|
|
532
581
|
);
|
|
533
582
|
}
|
|
534
583
|
}
|
|
@@ -732,7 +781,7 @@ function spawnAgent(cmd, args, opts) {
|
|
|
732
781
|
async function cmdRun() {
|
|
733
782
|
ensureDirs();
|
|
734
783
|
const auth = requireAuth();
|
|
735
|
-
installModelsJson(agentGatewayUrl(auth));
|
|
784
|
+
installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
|
|
736
785
|
installExtensions();
|
|
737
786
|
const entry = piEntry();
|
|
738
787
|
const { cmd, args } = await runtimeFor(entry);
|
|
@@ -774,7 +823,8 @@ Usage:
|
|
|
774
823
|
omnirush --version show the omnirush version
|
|
775
824
|
|
|
776
825
|
Options:
|
|
777
|
-
--model <model[:effort]> model override (gpt-6-astra, gpt-
|
|
826
|
+
--model <model[:effort]> model override (gpt-6-astra, gpt-6-sol,
|
|
827
|
+
gpt-5.6-sol, meta-muse-spark, muse-spark-1.1;
|
|
778
828
|
effort: minimal..max)
|
|
779
829
|
--thinking <level> reasoning effort override
|
|
780
830
|
--strict-sota exit non-zero if the served model is not
|
|
@@ -792,7 +842,8 @@ Session commands (inside the agent):
|
|
|
792
842
|
|
|
793
843
|
Environment:
|
|
794
844
|
OMNIRUSH_ORIGIN manager origin override
|
|
795
|
-
OMNIRUSH_MODEL default model override (e.g. gpt-
|
|
845
|
+
OMNIRUSH_MODEL default model override (e.g. gpt-6-sol)
|
|
846
|
+
OMNIRUSH_STATIC_MODELS=1 use the shipped model list (no GET /v1/models)
|
|
796
847
|
OMNIRUSH_DIR state dir override (default ~/.omnirush)
|
|
797
848
|
OMNIRUSH_DEBUG=1 verbose diagnostics (raw gateway errors on failure)
|
|
798
849
|
|
|
@@ -816,7 +867,7 @@ else {
|
|
|
816
867
|
// duplicated and pi admin subcommands pass through untouched.
|
|
817
868
|
ensureDirs();
|
|
818
869
|
const auth = requireAuth();
|
|
819
|
-
installModelsJson(agentGatewayUrl(auth));
|
|
870
|
+
installModelsJson(agentGatewayUrl(auth), await launchModels(agentGatewayUrl(auth), auth.accessToken));
|
|
820
871
|
installExtensions();
|
|
821
872
|
const entry = piEntry();
|
|
822
873
|
const { cmd: bin, args } = await runtimeFor(entry);
|
package/src/lib.js
CHANGED
|
@@ -68,7 +68,7 @@ const VALUE_FLAGS = new Set([
|
|
|
68
68
|
/**
|
|
69
69
|
* Model defaults for plain `omnirush` runs. OMNIRUSH_MODEL overrides the
|
|
70
70
|
* default model — any catalog id below, optionally with a thinking level
|
|
71
|
-
* like "gpt-6-astra:high" or "gpt-
|
|
71
|
+
* like "gpt-6-astra:high" or "gpt-6-sol:xhigh".
|
|
72
72
|
*/
|
|
73
73
|
export function resolveModelDefaults(env = process.env) {
|
|
74
74
|
return {
|
|
@@ -77,8 +77,11 @@ export function resolveModelDefaults(env = process.env) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// The product's shipped catalog
|
|
81
|
-
//
|
|
80
|
+
// The product's shipped catalog, in the desktop app's order (the backend
|
|
81
|
+
// catalog, GET /v1/models): gpt-6-astra (default), gpt-6-sol, gpt-5.6-sol,
|
|
82
|
+
// then the Muse models. gpt-6-sol added 2026-09-26 (the gateway serves it
|
|
83
|
+
// as gpt-6-sol). At launch the list is refreshed from GET /v1/models
|
|
84
|
+
// (catalogFromGateway); this one is the fallback. Live-verified 2026-09-22 against the production
|
|
82
85
|
// gateway: GET /v1/models lists both ids and POST /v1/responses with
|
|
83
86
|
// model "gpt-5.6-sol" is served as gpt-5.6-sol (x-omnirush-model
|
|
84
87
|
// response header + the model named in every SSE frame), so the client
|
|
@@ -87,7 +90,7 @@ export function resolveModelDefaults(env = process.env) {
|
|
|
87
90
|
export const MODELS = [
|
|
88
91
|
{
|
|
89
92
|
id: "gpt-6-astra",
|
|
90
|
-
name: "GPT
|
|
93
|
+
name: "GPT 6 Astra",
|
|
91
94
|
reasoning: true,
|
|
92
95
|
input: ["text", "image"],
|
|
93
96
|
cost: { input: 10, output: 50, cacheRead: 2.5, cacheWrite: 12.5 },
|
|
@@ -105,6 +108,25 @@ export const MODELS = [
|
|
|
105
108
|
max: "max",
|
|
106
109
|
},
|
|
107
110
|
},
|
|
111
|
+
{
|
|
112
|
+
id: "gpt-6-sol",
|
|
113
|
+
name: "GPT 6 Sol",
|
|
114
|
+
reasoning: true,
|
|
115
|
+
input: ["text", "image"],
|
|
116
|
+
// No published list pricing: pi shows zeros (as for gpt-5.6-sol).
|
|
117
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
118
|
+
contextWindow: 400000,
|
|
119
|
+
compat: { supportsMaxOutputTokens: false },
|
|
120
|
+
// Same Codex route as astra: the gateway accepts every effort level.
|
|
121
|
+
thinkingLevelMap: {
|
|
122
|
+
minimal: "minimal",
|
|
123
|
+
low: "low",
|
|
124
|
+
medium: "medium",
|
|
125
|
+
high: "high",
|
|
126
|
+
xhigh: "xhigh",
|
|
127
|
+
max: "max",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
108
130
|
{
|
|
109
131
|
id: "gpt-5.6-sol",
|
|
110
132
|
name: "GPT-5.6 Sol",
|
|
@@ -205,8 +227,53 @@ export const MODELS = [
|
|
|
205
227
|
},
|
|
206
228
|
];
|
|
207
229
|
|
|
230
|
+
/** Every effort the gateway accepts from a client (backend catalog ACCEPTED_EFFORTS, pi spellings). */
|
|
231
|
+
const GATEWAY_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The model list from the gateway's catalog (GET /v1/models, the list the
|
|
235
|
+
* desktop app shows), in its order, for pi's models.json: a model the CLI
|
|
236
|
+
* ships keeps its entry (pricing, effort map) under the gateway's display
|
|
237
|
+
* name; a model it does not know gets a generic entry from the catalog's
|
|
238
|
+
* fields (context limit, image input, reasoning). Shipped models the
|
|
239
|
+
* gateway does not list (sub-agent-only Muse ids) follow, so
|
|
240
|
+
* `spawn_agents` can still name them. Null when the payload is not a
|
|
241
|
+
* usable catalog: the caller keeps the shipped list.
|
|
242
|
+
*/
|
|
243
|
+
export function catalogFromGateway(payload, shipped = MODELS) {
|
|
244
|
+
const data = payload && Array.isArray(payload.data) ? payload.data : null;
|
|
245
|
+
if (!data) return null;
|
|
246
|
+
const known = new Map(shipped.map((model) => [model.id, model]));
|
|
247
|
+
const listed = [];
|
|
248
|
+
for (const entry of data) {
|
|
249
|
+
const id = entry && typeof entry.id === "string" ? entry.id.trim() : "";
|
|
250
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id) || listed.some((model) => model.id === id)) continue;
|
|
251
|
+
const name = typeof entry.display_name === "string" && entry.display_name.trim() ? entry.display_name.trim() : null;
|
|
252
|
+
const base = known.get(id);
|
|
253
|
+
if (base) {
|
|
254
|
+
listed.push({ ...base, ...(name ? { name } : {}) });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const caps = entry.capabilities && typeof entry.capabilities === "object" ? entry.capabilities : {};
|
|
258
|
+
const context = entry.limits && Number.isSafeInteger(entry.limits.context) && entry.limits.context > 0 ? entry.limits.context : 400000;
|
|
259
|
+
const reasoning = caps.reasoning !== false;
|
|
260
|
+
listed.push({
|
|
261
|
+
id,
|
|
262
|
+
name: name ?? id,
|
|
263
|
+
reasoning,
|
|
264
|
+
input: caps.image_input === false ? ["text"] : ["text", "image"],
|
|
265
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
266
|
+
contextWindow: context,
|
|
267
|
+
compat: { supportsMaxOutputTokens: false },
|
|
268
|
+
...(reasoning ? { thinkingLevelMap: Object.fromEntries(GATEWAY_EFFORTS.map((level) => [level, level])) } : {}),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (listed.length === 0) return null;
|
|
272
|
+
return [...listed, ...shipped.filter((model) => !listed.some((entry) => entry.id === model.id)).map((model) => ({ ...model }))];
|
|
273
|
+
}
|
|
274
|
+
|
|
208
275
|
/**
|
|
209
|
-
* Prepend
|
|
276
|
+
* Prepend the product's provider/model/scope defaults for agent runs, without
|
|
210
277
|
* duplicating flags the user already specified. Consumption rules mirror
|
|
211
278
|
* pi's own parseArgs so a value token is never mistaken for a flag (e.g.
|
|
212
279
|
* `omnirush -p "how do I use --model flags"` must not suppress defaults).
|
|
@@ -222,6 +289,7 @@ export function withModelDefaults(args, defaults) {
|
|
|
222
289
|
}
|
|
223
290
|
let hasProvider = false;
|
|
224
291
|
let hasModel = false;
|
|
292
|
+
let hasModelsScope = false;
|
|
225
293
|
for (let i = 0; i < args.length; i++) {
|
|
226
294
|
const arg = args[i];
|
|
227
295
|
if (arg === "--") break;
|
|
@@ -238,10 +306,17 @@ export function withModelDefaults(args, defaults) {
|
|
|
238
306
|
hasModel = true;
|
|
239
307
|
i++;
|
|
240
308
|
}
|
|
309
|
+
} else if (arg === "--models") {
|
|
310
|
+
if (i + 1 < args.length) {
|
|
311
|
+
hasModelsScope = true;
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
241
314
|
} else if (arg.startsWith("--provider=")) {
|
|
242
315
|
hasProvider = true;
|
|
243
316
|
} else if (arg.startsWith("--model=")) {
|
|
244
317
|
hasModel = true;
|
|
318
|
+
} else if (arg.startsWith("--models=")) {
|
|
319
|
+
hasModelsScope = true;
|
|
245
320
|
} else if (arg === "--print" || arg === "-p") {
|
|
246
321
|
const next = args[i + 1];
|
|
247
322
|
if (
|
|
@@ -267,6 +342,10 @@ export function withModelDefaults(args, defaults) {
|
|
|
267
342
|
}
|
|
268
343
|
}
|
|
269
344
|
const out = [];
|
|
345
|
+
// The upstream core merges every configured provider into its registry.
|
|
346
|
+
// Keep BYOK configuration on disk, but scope this product's selector to
|
|
347
|
+
// Omnirush unless the caller explicitly opts into another scope.
|
|
348
|
+
if (!hasModelsScope) out.push("--models", `${DEFAULT_PROVIDER}/*`);
|
|
270
349
|
if (!hasProvider) out.push("--provider", provider);
|
|
271
350
|
if (!hasModel) out.push("--model", model);
|
|
272
351
|
return [...out, ...args];
|
|
@@ -284,7 +363,7 @@ export function withModelDefaults(args, defaults) {
|
|
|
284
363
|
* `compat.supportsMaxOutputTokens: false` and ships no maxTokens — pi would
|
|
285
364
|
* otherwise clamp a default and break every request.
|
|
286
365
|
*/
|
|
287
|
-
export function modelsConfig({ gatewayUrl } = {}) {
|
|
366
|
+
export function modelsConfig({ gatewayUrl, models = MODELS } = {}) {
|
|
288
367
|
return {
|
|
289
368
|
providers: {
|
|
290
369
|
[DEFAULT_PROVIDER]: {
|
|
@@ -292,7 +371,7 @@ export function modelsConfig({ gatewayUrl } = {}) {
|
|
|
292
371
|
baseUrl: gatewayUrl ?? DEFAULT_GATEWAY_URL,
|
|
293
372
|
api: "openai-responses",
|
|
294
373
|
apiKey: "$OMNIRUSH_TOKEN",
|
|
295
|
-
models:
|
|
374
|
+
models: models.map((model) => ({ ...model })),
|
|
296
375
|
},
|
|
297
376
|
},
|
|
298
377
|
};
|