@oh-my-pi/pi-coding-agent 15.5.6 → 15.5.7
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/CHANGELOG.md +21 -1
- package/dist/types/config/settings-schema.d.ts +50 -2
- package/dist/types/lib/xai-http.d.ts +40 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +16 -0
- package/dist/types/tools/image-gen.d.ts +6 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/plan-mode-guard.d.ts +5 -6
- package/dist/types/tools/tts.d.ts +18 -0
- package/package.json +8 -8
- package/src/config/model-registry.ts +41 -9
- package/src/config/settings-schema.ts +43 -2
- package/src/lib/xai-http.ts +124 -0
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/sdk.ts +15 -9
- package/src/session/agent-session.ts +30 -3
- package/src/tools/fetch.ts +52 -24
- package/src/tools/image-gen.ts +205 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/plan-mode-guard.ts +14 -6
- package/src/tools/search.ts +2 -2
- package/src/tools/tts.ts +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.7] - 2026-05-27
|
|
6
|
+
### Added
|
|
7
|
+
- `providers.openrouterVariant` setting (Settings → Providers → "OpenRouter Routing") to default OpenRouter requests to a routing-variant suffix (`:nitro`, `:floor`, `:online`, `:exacto`). Selectors that already name a variant (e.g. `openrouter/anthropic/claude-haiku:nitro`) keep precedence.
|
|
8
|
+
|
|
9
|
+
- `generate_image` supports xAI Grok Imagine via `providers.image=xai`. Supports `grok-imagine-image` (default) and `grok-imagine-image-quality` at aspect ratios `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`. Uses the xAI Grok OAuth credential when available, otherwise `XAI_API_KEY`.
|
|
10
|
+
- New `tts` tool synthesises speech via xAI Grok Voice behind the disabled-by-default `tts.enabled` setting. Built-in voices `ara`, `eve` (default), `leo`, `rex`, `sal`; custom voice IDs also accepted. Output codec inferred from the `output_path` suffix (`.wav` → `wav`, else `mp3`). Up to 15,000 characters per request.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed plan-mode re-entry after approval reopening a fresh `local://PLAN.md` instead of the approved titled plan artifact, which could duplicate plan content and fail approval on an existing destination.
|
|
15
|
+
- Fixed `read` URL reader mode aborting after a stalled Jina request instead of falling back to trafilatura/lynx/native: Jina (and Parallel extract) now have their own per-attempt sub-budget capped at 10s, the catch handler honours only real user cancellation, and the in-process native renderer is always attempted on already-loaded HTML ([#1449](https://github.com/can1357/oh-my-pi/issues/1449))
|
|
16
|
+
|
|
5
17
|
## [15.5.6] - 2026-05-27
|
|
6
18
|
### Added
|
|
7
19
|
|
|
@@ -57,6 +69,10 @@
|
|
|
57
69
|
|
|
58
70
|
- Fixed `omp` startup and `/changelog` reading the host project's `CHANGELOG.md` as omp's — `getPackageDir()` no longer falls back to the user's `cwd` when no owning `package.json` is locatable, preventing spurious `lastChangelogVersion` writes ([#1423](https://github.com/can1357/oh-my-pi/issues/1423))
|
|
59
71
|
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Fixed hashline session-chain replay silently overwriting in-session edits when the model re-targeted a previously rewritten line with a stale file hash; replay now refuses unless every edit's anchor line content matches between the snapshot and the current file ([#1422](https://github.com/can1357/oh-my-pi/pull/1422))
|
|
75
|
+
|
|
60
76
|
## [15.5.3] - 2026-05-27
|
|
61
77
|
### Breaking Changes
|
|
62
78
|
|
|
@@ -66,6 +82,10 @@
|
|
|
66
82
|
|
|
67
83
|
- Warned when legacy inline `LINE:TEXT` lines are accepted as payload continuations only when inside a pending multi-line `A-B:` replacement
|
|
68
84
|
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- Fixed runtime model registry refresh and cache loading so providers with authoritative dynamic catalogs, including Synthetic, do not re-add deprecated bundled model IDs after discovery ([#1417](https://github.com/can1357/oh-my-pi/issues/1417)).
|
|
88
|
+
|
|
69
89
|
## [15.5.2] - 2026-05-26
|
|
70
90
|
### Breaking Changes
|
|
71
91
|
|
|
@@ -8867,4 +8887,4 @@ Initial public release.
|
|
|
8867
8887
|
- Git branch display in footer
|
|
8868
8888
|
- Message queueing during streaming responses
|
|
8869
8889
|
- OAuth integration for Gmail and Google Calendar access
|
|
8870
|
-
- HTML export with syntax highlighting and collapsible sections
|
|
8890
|
+
- HTML export with syntax highlighting and collapsible sections
|
|
@@ -2393,6 +2393,15 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2393
2393
|
readonly description: "Enable the calculator tool for basic calculations";
|
|
2394
2394
|
};
|
|
2395
2395
|
};
|
|
2396
|
+
readonly "tts.enabled": {
|
|
2397
|
+
readonly type: "boolean";
|
|
2398
|
+
readonly default: false;
|
|
2399
|
+
readonly ui: {
|
|
2400
|
+
readonly tab: "tools";
|
|
2401
|
+
readonly label: "Text-to-Speech";
|
|
2402
|
+
readonly description: "Enable the tts tool for xAI Grok Voice speech synthesis";
|
|
2403
|
+
};
|
|
2404
|
+
};
|
|
2396
2405
|
readonly "recipe.enabled": {
|
|
2397
2406
|
readonly type: "boolean";
|
|
2398
2407
|
readonly default: true;
|
|
@@ -3118,7 +3127,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
3118
3127
|
};
|
|
3119
3128
|
readonly "providers.image": {
|
|
3120
3129
|
readonly type: "enum";
|
|
3121
|
-
readonly values: readonly ["auto", "openai", "gemini", "openrouter"];
|
|
3130
|
+
readonly values: readonly ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"];
|
|
3122
3131
|
readonly default: "auto";
|
|
3123
3132
|
readonly ui: {
|
|
3124
3133
|
readonly tab: "providers";
|
|
@@ -3127,11 +3136,19 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
3127
3136
|
readonly options: readonly [{
|
|
3128
3137
|
readonly value: "auto";
|
|
3129
3138
|
readonly label: "Auto";
|
|
3130
|
-
readonly description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini";
|
|
3139
|
+
readonly description: "Priority: GPT model image tool > Antigravity > xAI > OpenRouter > Gemini";
|
|
3131
3140
|
}, {
|
|
3132
3141
|
readonly value: "openai";
|
|
3133
3142
|
readonly label: "OpenAI";
|
|
3134
3143
|
readonly description: "Uses the active GPT Responses/Codex model";
|
|
3144
|
+
}, {
|
|
3145
|
+
readonly value: "antigravity";
|
|
3146
|
+
readonly label: "Antigravity";
|
|
3147
|
+
readonly description: "Requires google-antigravity OAuth";
|
|
3148
|
+
}, {
|
|
3149
|
+
readonly value: "xai";
|
|
3150
|
+
readonly label: "xAI Grok Imagine";
|
|
3151
|
+
readonly description: "Requires xAI Grok OAuth or XAI_API_KEY";
|
|
3135
3152
|
}, {
|
|
3136
3153
|
readonly value: "gemini";
|
|
3137
3154
|
readonly label: "Gemini";
|
|
@@ -3185,6 +3202,37 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
3185
3202
|
}];
|
|
3186
3203
|
};
|
|
3187
3204
|
};
|
|
3205
|
+
readonly "providers.openrouterVariant": {
|
|
3206
|
+
readonly type: "enum";
|
|
3207
|
+
readonly values: readonly ["default", "nitro", "floor", "online", "exacto"];
|
|
3208
|
+
readonly default: "default";
|
|
3209
|
+
readonly ui: {
|
|
3210
|
+
readonly tab: "providers";
|
|
3211
|
+
readonly label: "OpenRouter Routing";
|
|
3212
|
+
readonly description: "Default routing-variant suffix appended to OpenRouter model IDs (overridden when the selector already names a variant)";
|
|
3213
|
+
readonly options: readonly [{
|
|
3214
|
+
readonly value: "default";
|
|
3215
|
+
readonly label: "Default";
|
|
3216
|
+
readonly description: "No suffix; use OpenRouter's default routing";
|
|
3217
|
+
}, {
|
|
3218
|
+
readonly value: "nitro";
|
|
3219
|
+
readonly label: ":nitro";
|
|
3220
|
+
readonly description: "Prioritize throughput / lowest latency";
|
|
3221
|
+
}, {
|
|
3222
|
+
readonly value: "floor";
|
|
3223
|
+
readonly label: ":floor";
|
|
3224
|
+
readonly description: "Prioritize cheapest available provider";
|
|
3225
|
+
}, {
|
|
3226
|
+
readonly value: "online";
|
|
3227
|
+
readonly label: ":online";
|
|
3228
|
+
readonly description: "Enable OpenRouter's web-search plugin";
|
|
3229
|
+
}, {
|
|
3230
|
+
readonly value: "exacto";
|
|
3231
|
+
readonly label: ":exacto";
|
|
3232
|
+
readonly description: "Cherry-picked high-quality providers (only defined for select models)";
|
|
3233
|
+
}];
|
|
3234
|
+
};
|
|
3235
|
+
};
|
|
3188
3236
|
readonly "providers.parallelFetch": {
|
|
3189
3237
|
readonly type: "boolean";
|
|
3190
3238
|
readonly default: true;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
2
|
+
interface XAICredentials {
|
|
3
|
+
provider: "xai-oauth" | "xai";
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseURL: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function ohMyPiXAIUserAgent(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve xAI credentials for HTTP tool calls.
|
|
10
|
+
*
|
|
11
|
+
* Credential priority:
|
|
12
|
+
* 1. xai-oauth — only when a *dedicated* xai-oauth source exists. Composed
|
|
13
|
+
* of two checks against the registry layer:
|
|
14
|
+
* a. `authStorage.hasNonEnvCredential("xai-oauth")` covers stored
|
|
15
|
+
* credentials (OAuth or api_key), runtime overrides (CLI
|
|
16
|
+
* `--api-key` for xai-oauth), config overrides (models.yml
|
|
17
|
+
* `providers.xai-oauth.apiKey`), and fallback resolvers.
|
|
18
|
+
* b. `$env.XAI_OAUTH_TOKEN` covers the xai-oauth-specific env var.
|
|
19
|
+
* `XAI_API_KEY` is intentionally NOT a signal here, even though the
|
|
20
|
+
* env-fallback map (`stream.ts: "xai-oauth"`) lets xai-oauth borrow it
|
|
21
|
+
* as a back-compat convenience: the borrow lets API-key-only setups
|
|
22
|
+
* satisfy the xai-oauth branch and then resolve baseUrl under
|
|
23
|
+
* xai-oauth instead of xai, silently bypassing `providers.xai.baseUrl`
|
|
24
|
+
* overrides for image/TTS traffic. The gate routes the borrow case to
|
|
25
|
+
* step 2 while preserving every dedicated xai-oauth path.
|
|
26
|
+
* 2. xai (plain API key). Delegates to ModelRegistry.getApiKeyForProvider
|
|
27
|
+
* which runs AuthStorage.getApiKey's full cascade: runtime override →
|
|
28
|
+
* models.yml config override → stored api_key credential → OAuth
|
|
29
|
+
* resolution → XAI_API_KEY env var → custom fallback resolver.
|
|
30
|
+
*
|
|
31
|
+
* baseURL: see `resolveXAIBaseURL` above. Resolved AFTER the credential
|
|
32
|
+
* decision so the scoped (provider, id) lookup is unambiguous. `modelId`
|
|
33
|
+
* is optional; probes / tool-availability checks pass `undefined` and fall
|
|
34
|
+
* through to env/default.
|
|
35
|
+
*
|
|
36
|
+
* Returns null when neither credential is available. Caller is responsible
|
|
37
|
+
* for surfacing an actionable error message in that case.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveXAIHttpCredentials(modelRegistry: ModelRegistry, modelId?: string): Promise<XAICredentials | null>;
|
|
40
|
+
export {};
|
|
@@ -454,6 +454,7 @@ export declare class AgentSession {
|
|
|
454
454
|
get goalRuntime(): GoalRuntime;
|
|
455
455
|
markPlanReferenceSent(): void;
|
|
456
456
|
setPlanReferencePath(path: string): void;
|
|
457
|
+
getPlanReferencePath(): string;
|
|
457
458
|
get clientBridge(): ClientBridge | undefined;
|
|
458
459
|
setClientBridge(bridge: ClientBridge | undefined): void;
|
|
459
460
|
getCheckpointState(): CheckpointState | undefined;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { type Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import type { Settings } from "../config/settings";
|
|
3
4
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
4
5
|
import { type Theme } from "../modes/theme/theme";
|
|
5
6
|
import type { ToolSession } from "../sdk";
|
|
7
|
+
import type { AgentStorage } from "../session/agent-storage";
|
|
6
8
|
import { type OutputMeta } from "./output-meta";
|
|
7
9
|
import { type LineRange } from "./path-utils";
|
|
8
10
|
export declare function isReadableUrlPath(value: string): boolean;
|
|
@@ -15,6 +17,20 @@ export interface ParsedReadUrlTarget {
|
|
|
15
17
|
ranges?: readonly LineRange[];
|
|
16
18
|
}
|
|
17
19
|
export declare function parseReadUrlTarget(readPath: string): ParsedReadUrlTarget | null;
|
|
20
|
+
/**
|
|
21
|
+
* Render HTML to markdown using Parallel, jina, trafilatura, lynx, then the
|
|
22
|
+
* in-process native converter. The overall `timeout` budget bounds the call,
|
|
23
|
+
* but remote reader requests are additionally capped at `REMOTE_READER_MAX_MS`
|
|
24
|
+
* so that a hung remote endpoint cannot prevent local fallbacks from running.
|
|
25
|
+
* Only a real `userSignal` cancellation aborts the chain — remote per-attempt
|
|
26
|
+
* timeouts and the overall reader-mode timeout still allow later renderers
|
|
27
|
+
* (especially the purely-local native converter) to be tried.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderHtmlToText(url: string, html: string, timeout: number, settings: Settings, userSignal: AbortSignal | undefined, storage: AgentStorage | null): Promise<{
|
|
30
|
+
content: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
method: string;
|
|
33
|
+
}>;
|
|
18
34
|
interface FetchImagePayload {
|
|
19
35
|
data: string;
|
|
20
36
|
mimeType: string;
|
|
@@ -2,7 +2,8 @@ import { type Model } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
3
|
import { type ModelRegistry } from "../config/model-registry";
|
|
4
4
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
5
|
-
type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter";
|
|
5
|
+
export type ImageProvider = "antigravity" | "gemini" | "openai" | "openai-codex" | "openrouter" | "xai";
|
|
6
|
+
export type ImageProviderPreference = Exclude<ImageProvider, "openai-codex"> | "auto";
|
|
6
7
|
declare const responseModalitySchema: z.ZodEnum<{
|
|
7
8
|
IMAGE: "IMAGE";
|
|
8
9
|
TEXT: "TEXT";
|
|
@@ -19,6 +20,8 @@ export declare const imageGenSchema: z.ZodObject<{
|
|
|
19
20
|
aspect_ratio: z.ZodOptional<z.ZodEnum<{
|
|
20
21
|
"16:9": "16:9";
|
|
21
22
|
"1:1": "1:1";
|
|
23
|
+
"2:3": "2:3";
|
|
24
|
+
"3:2": "3:2";
|
|
22
25
|
"3:4": "3:4";
|
|
23
26
|
"4:3": "4:3";
|
|
24
27
|
"9:16": "9:16";
|
|
@@ -70,8 +73,9 @@ interface InlineImageData {
|
|
|
70
73
|
data: string;
|
|
71
74
|
mimeType: string;
|
|
72
75
|
}
|
|
76
|
+
export declare function isImageProviderPreference(value: unknown): value is ImageProviderPreference;
|
|
73
77
|
/** Set the preferred image provider from settings */
|
|
74
|
-
export declare function setPreferredImageProvider(provider:
|
|
78
|
+
export declare function setPreferredImageProvider(provider: ImageProviderPreference): void;
|
|
75
79
|
export declare const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails>;
|
|
76
80
|
export declare function getImageGenTools(modelRegistry?: ModelRegistry, activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
|
|
77
81
|
export declare function getImageGenToolsWithRegistry(modelRegistry: ModelRegistry, activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
|
|
@@ -2,12 +2,11 @@ import type { ToolSession } from ".";
|
|
|
2
2
|
/**
|
|
3
3
|
* Resolve a write/edit target to its absolute filesystem path.
|
|
4
4
|
*
|
|
5
|
-
* In plan mode, transparently redirects
|
|
6
|
-
* plan file's basename
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* file the plan-mode guard would otherwise reject.
|
|
5
|
+
* In plan mode, transparently redirects `PLAN.md` aliases and targets whose
|
|
6
|
+
* basename matches the plan file's basename to the canonical plan file
|
|
7
|
+
* location at `state.planFilePath`. This lets `write` and `edit` accept the
|
|
8
|
+
* habitual plan filename after approval even when the active artifact has a
|
|
9
|
+
* titled path such as `local://APPROVED.md`.
|
|
11
10
|
*
|
|
12
11
|
* Outside plan mode (or when the basename does not match) this is a no-op.
|
|
13
12
|
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
3
|
+
type TtsCodec = "mp3" | "wav";
|
|
4
|
+
declare const ttsSchema: z.ZodObject<{
|
|
5
|
+
text: z.ZodString;
|
|
6
|
+
voice_id: z.ZodDefault<z.ZodString>;
|
|
7
|
+
language: z.ZodDefault<z.ZodString>;
|
|
8
|
+
output_path: z.ZodString;
|
|
9
|
+
sample_rate: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
bit_rate: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
interface TtsToolDetails {
|
|
13
|
+
bytes: number;
|
|
14
|
+
voiceId: string;
|
|
15
|
+
codec: TtsCodec;
|
|
16
|
+
}
|
|
17
|
+
export declare const ttsTool: CustomTool<typeof ttsSchema, TtsToolDetails>;
|
|
18
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "15.5.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
56
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
50
|
+
"@oh-my-pi/hashline": "15.5.7",
|
|
51
|
+
"@oh-my-pi/omp-stats": "15.5.7",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "15.5.7",
|
|
53
|
+
"@oh-my-pi/pi-ai": "15.5.7",
|
|
54
|
+
"@oh-my-pi/pi-natives": "15.5.7",
|
|
55
|
+
"@oh-my-pi/pi-tui": "15.5.7",
|
|
56
|
+
"@oh-my-pi/pi-utils": "15.5.7",
|
|
57
57
|
"@puppeteer/browsers": "^2.13.0",
|
|
58
58
|
"@types/turndown": "5.0.6",
|
|
59
59
|
"@xterm/headless": "^6.0.0",
|
|
@@ -291,6 +291,12 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
291
291
|
return model;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
const AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS = new Set<string>(
|
|
295
|
+
PROVIDER_DESCRIPTORS.filter(descriptor => descriptor.dynamicModelsAuthoritative).map(
|
|
296
|
+
descriptor => descriptor.providerId,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
|
|
294
300
|
function isAuthoritativeProjectCatalogModel(model: Model<Api>): boolean {
|
|
295
301
|
return (
|
|
296
302
|
model.provider === "google-vertex" &&
|
|
@@ -323,6 +329,11 @@ interface DiscoveryProviderConfig {
|
|
|
323
329
|
optional?: boolean;
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
interface BuiltInDiscoveryResult {
|
|
333
|
+
models: Model<Api>[];
|
|
334
|
+
authoritativeProviders: Set<string>;
|
|
335
|
+
}
|
|
336
|
+
|
|
326
337
|
export type ProviderDiscoveryStatus = "idle" | "ok" | "empty" | "cached" | "unavailable" | "unauthenticated";
|
|
327
338
|
|
|
328
339
|
export interface ProviderDiscoveryState {
|
|
@@ -914,6 +925,11 @@ export class ModelRegistry {
|
|
|
914
925
|
cachedAuthoritativeProviders.add(provider);
|
|
915
926
|
}
|
|
916
927
|
}
|
|
928
|
+
for (const provider of cachedStandardResult.authoritativeFreshProviders) {
|
|
929
|
+
if (AUTHORITATIVE_RUNTIME_CATALOG_PROVIDERS.has(provider)) {
|
|
930
|
+
cachedAuthoritativeProviders.add(provider);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
917
933
|
if (cachedAuthoritativeProviders.size > 0) {
|
|
918
934
|
builtInModels = dropProviderModels(builtInModels, cachedAuthoritativeProviders);
|
|
919
935
|
}
|
|
@@ -1253,12 +1269,12 @@ export class ModelRegistry {
|
|
|
1253
1269
|
: Promise.all(
|
|
1254
1270
|
selectedDiscoverableProviders.map(provider => this.#discoverProviderModels(provider, strategy)),
|
|
1255
1271
|
).then(results => results.flat());
|
|
1256
|
-
const [configuredDiscovered,
|
|
1272
|
+
const [configuredDiscovered, builtInDiscovery] = await Promise.all([
|
|
1257
1273
|
configuredDiscoveriesPromise,
|
|
1258
1274
|
this.#discoverBuiltInProviderModels(strategy, providerFilter),
|
|
1259
1275
|
]);
|
|
1260
|
-
const discovered = [...configuredDiscovered, ...
|
|
1261
|
-
if (discovered.length === 0) {
|
|
1276
|
+
const discovered = [...configuredDiscovered, ...builtInDiscovery.models];
|
|
1277
|
+
if (discovered.length === 0 && builtInDiscovery.authoritativeProviders.size === 0) {
|
|
1262
1278
|
return;
|
|
1263
1279
|
}
|
|
1264
1280
|
const discoveredModels = this.#applyHardcodedModelPolicies(
|
|
@@ -1271,6 +1287,9 @@ export class ModelRegistry {
|
|
|
1271
1287
|
),
|
|
1272
1288
|
);
|
|
1273
1289
|
const authoritativeProviders = providersWithAuthoritativeProjectCatalog(discoveredModels);
|
|
1290
|
+
for (const provider of builtInDiscovery.authoritativeProviders) {
|
|
1291
|
+
authoritativeProviders.add(provider);
|
|
1292
|
+
}
|
|
1274
1293
|
const baseModels =
|
|
1275
1294
|
authoritativeProviders.size > 0 ? dropProviderModels(this.#models, authoritativeProviders) : this.#models;
|
|
1276
1295
|
const resolved = this.#mergeResolvedModels(baseModels, discoveredModels);
|
|
@@ -1385,7 +1404,7 @@ export class ModelRegistry {
|
|
|
1385
1404
|
async #discoverBuiltInProviderModels(
|
|
1386
1405
|
strategy: ModelRefreshStrategy,
|
|
1387
1406
|
providerFilter?: ReadonlySet<string>,
|
|
1388
|
-
): Promise<
|
|
1407
|
+
): Promise<BuiltInDiscoveryResult> {
|
|
1389
1408
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
1390
1409
|
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
|
|
1391
1410
|
const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
|
|
@@ -1395,12 +1414,20 @@ export class ModelRegistry {
|
|
|
1395
1414
|
return providerFilter ? providerFilter.has(opts.providerId) : true;
|
|
1396
1415
|
});
|
|
1397
1416
|
if (managerOptions.length === 0) {
|
|
1398
|
-
return [];
|
|
1417
|
+
return { models: [], authoritativeProviders: new Set() };
|
|
1399
1418
|
}
|
|
1400
1419
|
const discoveries = await Promise.all(
|
|
1401
1420
|
managerOptions.map(options => this.#discoverWithModelManager(options, strategy)),
|
|
1402
1421
|
);
|
|
1403
|
-
|
|
1422
|
+
const authoritativeProviders = new Set<string>();
|
|
1423
|
+
const models: Model<Api>[] = [];
|
|
1424
|
+
for (const discovery of discoveries) {
|
|
1425
|
+
models.push(...discovery.models);
|
|
1426
|
+
for (const provider of discovery.authoritativeProviders) {
|
|
1427
|
+
authoritativeProviders.add(provider);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return { models, authoritativeProviders };
|
|
1404
1431
|
}
|
|
1405
1432
|
|
|
1406
1433
|
async #collectBuiltInModelManagerOptions(): Promise<ModelManagerOptions<Api>[]> {
|
|
@@ -1482,19 +1509,24 @@ export class ModelRegistry {
|
|
|
1482
1509
|
async #discoverWithModelManager(
|
|
1483
1510
|
options: ModelManagerOptions<Api>,
|
|
1484
1511
|
strategy: ModelRefreshStrategy,
|
|
1485
|
-
): Promise<
|
|
1512
|
+
): Promise<BuiltInDiscoveryResult> {
|
|
1486
1513
|
try {
|
|
1487
1514
|
const manager = createModelManager({ ...options, cacheDbPath: this.#cacheDbPath });
|
|
1488
1515
|
const result = await manager.refresh(strategy);
|
|
1489
|
-
|
|
1516
|
+
const models = result.models.map(model =>
|
|
1490
1517
|
model.provider === options.providerId ? model : { ...model, provider: options.providerId },
|
|
1491
1518
|
);
|
|
1519
|
+
const authoritativeProviders = new Set<string>();
|
|
1520
|
+
if (options.dynamicModelsAuthoritative && !result.stale) {
|
|
1521
|
+
authoritativeProviders.add(options.providerId);
|
|
1522
|
+
}
|
|
1523
|
+
return { models, authoritativeProviders };
|
|
1492
1524
|
} catch (error) {
|
|
1493
1525
|
logger.warn("model discovery failed for provider", {
|
|
1494
1526
|
provider: options.providerId,
|
|
1495
1527
|
error: error instanceof Error ? error.message : String(error),
|
|
1496
1528
|
});
|
|
1497
|
-
return [];
|
|
1529
|
+
return { models: [], authoritativeProviders: new Set() };
|
|
1498
1530
|
}
|
|
1499
1531
|
}
|
|
1500
1532
|
|
|
@@ -2036,6 +2036,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
2036
2036
|
},
|
|
2037
2037
|
},
|
|
2038
2038
|
|
|
2039
|
+
"tts.enabled": {
|
|
2040
|
+
type: "boolean",
|
|
2041
|
+
default: false,
|
|
2042
|
+
ui: {
|
|
2043
|
+
tab: "tools",
|
|
2044
|
+
label: "Text-to-Speech",
|
|
2045
|
+
description: "Enable the tts tool for xAI Grok Voice speech synthesis",
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2039
2048
|
"recipe.enabled": {
|
|
2040
2049
|
type: "boolean",
|
|
2041
2050
|
default: true,
|
|
@@ -2698,7 +2707,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2698
2707
|
},
|
|
2699
2708
|
"providers.image": {
|
|
2700
2709
|
type: "enum",
|
|
2701
|
-
values: ["auto", "openai", "gemini", "openrouter"] as const,
|
|
2710
|
+
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
2702
2711
|
default: "auto",
|
|
2703
2712
|
ui: {
|
|
2704
2713
|
tab: "providers",
|
|
@@ -2708,9 +2717,19 @@ export const SETTINGS_SCHEMA = {
|
|
|
2708
2717
|
{
|
|
2709
2718
|
value: "auto",
|
|
2710
2719
|
label: "Auto",
|
|
2711
|
-
description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
|
|
2720
|
+
description: "Priority: GPT model image tool > Antigravity > xAI > OpenRouter > Gemini",
|
|
2712
2721
|
},
|
|
2713
2722
|
{ value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
|
|
2723
|
+
{
|
|
2724
|
+
value: "antigravity",
|
|
2725
|
+
label: "Antigravity",
|
|
2726
|
+
description: "Requires google-antigravity OAuth",
|
|
2727
|
+
},
|
|
2728
|
+
{
|
|
2729
|
+
value: "xai",
|
|
2730
|
+
label: "xAI Grok Imagine",
|
|
2731
|
+
description: "Requires xAI Grok OAuth or XAI_API_KEY",
|
|
2732
|
+
},
|
|
2714
2733
|
{ value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
|
|
2715
2734
|
{ value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
|
|
2716
2735
|
],
|
|
@@ -2748,6 +2767,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
2748
2767
|
},
|
|
2749
2768
|
},
|
|
2750
2769
|
|
|
2770
|
+
"providers.openrouterVariant": {
|
|
2771
|
+
type: "enum",
|
|
2772
|
+
values: ["default", "nitro", "floor", "online", "exacto"] as const,
|
|
2773
|
+
default: "default",
|
|
2774
|
+
ui: {
|
|
2775
|
+
tab: "providers",
|
|
2776
|
+
label: "OpenRouter Routing",
|
|
2777
|
+
description:
|
|
2778
|
+
"Default routing-variant suffix appended to OpenRouter model IDs (overridden when the selector already names a variant)",
|
|
2779
|
+
options: [
|
|
2780
|
+
{ value: "default", label: "Default", description: "No suffix; use OpenRouter's default routing" },
|
|
2781
|
+
{ value: "nitro", label: ":nitro", description: "Prioritize throughput / lowest latency" },
|
|
2782
|
+
{ value: "floor", label: ":floor", description: "Prioritize cheapest available provider" },
|
|
2783
|
+
{ value: "online", label: ":online", description: "Enable OpenRouter's web-search plugin" },
|
|
2784
|
+
{
|
|
2785
|
+
value: "exacto",
|
|
2786
|
+
label: ":exacto",
|
|
2787
|
+
description: "Cherry-picked high-quality providers (only defined for select models)",
|
|
2788
|
+
},
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
},
|
|
2751
2792
|
"providers.parallelFetch": {
|
|
2752
2793
|
type: "boolean",
|
|
2753
2794
|
default: true,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Ported from NousResearch/hermes-agent (MIT) — tools/xai_http.py.
|
|
2
|
+
|
|
3
|
+
import { getBundledModels } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BASE_URL = "https://api.x.ai/v1";
|
|
8
|
+
|
|
9
|
+
interface XAICredentials {
|
|
10
|
+
provider: "xai-oauth" | "xai";
|
|
11
|
+
apiKey: string;
|
|
12
|
+
baseURL: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ohMyPiXAIUserAgent(): string {
|
|
16
|
+
return "oh-my-pi/xai";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type XAIProvider = "xai-oauth" | "xai";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the HTTP base URL for an xAI tool call.
|
|
23
|
+
*
|
|
24
|
+
* Precedence:
|
|
25
|
+
* 1. `model.baseUrl` from the registry IF the user pinned a per-model
|
|
26
|
+
* override — i.e. `merged.baseUrl` differs from the seeded/bundled
|
|
27
|
+
* default for the (provider, id) pair. Mirrors the chat path's per-model
|
|
28
|
+
* contract (`openai-responses.ts: model.baseUrl`).
|
|
29
|
+
* 2. `ModelRegistry.getProviderBaseUrl(provider)` — provider-level override
|
|
30
|
+
* (e.g. `providers.xai-oauth.baseUrl` from models.yml). Reached when the
|
|
31
|
+
* modelId does not appear in the registry under this provider, which
|
|
32
|
+
* happens for tool-only ids like `grok-imagine-image` that
|
|
33
|
+
* `applyXAIOAuthCuration` filters out via `XAI_NON_CHAT_PREFIXES`.
|
|
34
|
+
* Without this leg, a registry-configured proxy is silently bypassed for
|
|
35
|
+
* image/TTS traffic.
|
|
36
|
+
* 3. `XAI_BASE_URL` env var (legacy global override, preserved).
|
|
37
|
+
* 4. `DEFAULT_BASE_URL = "https://api.x.ai/v1"`.
|
|
38
|
+
*
|
|
39
|
+
* The override gate at step 1 uses `bundled?.baseUrl ?? DEFAULT_BASE_URL` as
|
|
40
|
+
* the canonical default sentinel. For xai (which has bundled entries) this
|
|
41
|
+
* compares against the bundled value; for xai-oauth (no bundled entries —
|
|
42
|
+
* models.json carries no xai-oauth records when the seed is absent, the
|
|
43
|
+
* picker is seeded statically from `xaiOAuthModelManagerOptions` with
|
|
44
|
+
* `baseUrl: DEFAULT_BASE_URL`) the sentinel falls back to DEFAULT_BASE_URL
|
|
45
|
+
* so the env leg remains reachable. Without that fallback, every xai-oauth
|
|
46
|
+
* model id forces `!bundled === true` and short-circuits XAI_BASE_URL
|
|
47
|
+
* silently. Lookup is scoped to (provider, id); matching by id alone would
|
|
48
|
+
* let xai-oauth entries hijack a xai tool call (or vice versa) when the
|
|
49
|
+
* same model id ships under both descriptors.
|
|
50
|
+
*/
|
|
51
|
+
function resolveXAIBaseURL(modelRegistry: ModelRegistry, provider: XAIProvider, modelId: string | undefined): string {
|
|
52
|
+
if (modelId) {
|
|
53
|
+
const merged = modelRegistry.getAll().find(m => m.id === modelId && m.provider === provider);
|
|
54
|
+
if (merged?.baseUrl) {
|
|
55
|
+
const bundled = getBundledModels(provider as Parameters<typeof getBundledModels>[0]).find(
|
|
56
|
+
m => m.id === modelId,
|
|
57
|
+
);
|
|
58
|
+
const providerDefault = bundled?.baseUrl ?? DEFAULT_BASE_URL;
|
|
59
|
+
if (merged.baseUrl !== providerDefault) {
|
|
60
|
+
return merged.baseUrl.replace(/\/$/, "");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const providerBaseUrl = modelRegistry.getProviderBaseUrl(provider);
|
|
65
|
+
if (providerBaseUrl) {
|
|
66
|
+
const normalized = providerBaseUrl.replace(/\/$/, "");
|
|
67
|
+
if (normalized !== DEFAULT_BASE_URL) return normalized;
|
|
68
|
+
}
|
|
69
|
+
return ($env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve xAI credentials for HTTP tool calls.
|
|
74
|
+
*
|
|
75
|
+
* Credential priority:
|
|
76
|
+
* 1. xai-oauth — only when a *dedicated* xai-oauth source exists. Composed
|
|
77
|
+
* of two checks against the registry layer:
|
|
78
|
+
* a. `authStorage.hasNonEnvCredential("xai-oauth")` covers stored
|
|
79
|
+
* credentials (OAuth or api_key), runtime overrides (CLI
|
|
80
|
+
* `--api-key` for xai-oauth), config overrides (models.yml
|
|
81
|
+
* `providers.xai-oauth.apiKey`), and fallback resolvers.
|
|
82
|
+
* b. `$env.XAI_OAUTH_TOKEN` covers the xai-oauth-specific env var.
|
|
83
|
+
* `XAI_API_KEY` is intentionally NOT a signal here, even though the
|
|
84
|
+
* env-fallback map (`stream.ts: "xai-oauth"`) lets xai-oauth borrow it
|
|
85
|
+
* as a back-compat convenience: the borrow lets API-key-only setups
|
|
86
|
+
* satisfy the xai-oauth branch and then resolve baseUrl under
|
|
87
|
+
* xai-oauth instead of xai, silently bypassing `providers.xai.baseUrl`
|
|
88
|
+
* overrides for image/TTS traffic. The gate routes the borrow case to
|
|
89
|
+
* step 2 while preserving every dedicated xai-oauth path.
|
|
90
|
+
* 2. xai (plain API key). Delegates to ModelRegistry.getApiKeyForProvider
|
|
91
|
+
* which runs AuthStorage.getApiKey's full cascade: runtime override →
|
|
92
|
+
* models.yml config override → stored api_key credential → OAuth
|
|
93
|
+
* resolution → XAI_API_KEY env var → custom fallback resolver.
|
|
94
|
+
*
|
|
95
|
+
* baseURL: see `resolveXAIBaseURL` above. Resolved AFTER the credential
|
|
96
|
+
* decision so the scoped (provider, id) lookup is unambiguous. `modelId`
|
|
97
|
+
* is optional; probes / tool-availability checks pass `undefined` and fall
|
|
98
|
+
* through to env/default.
|
|
99
|
+
*
|
|
100
|
+
* Returns null when neither credential is available. Caller is responsible
|
|
101
|
+
* for surfacing an actionable error message in that case.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveXAIHttpCredentials(
|
|
104
|
+
modelRegistry: ModelRegistry,
|
|
105
|
+
modelId?: string,
|
|
106
|
+
): Promise<XAICredentials | null> {
|
|
107
|
+
const hasDedicatedXaiOAuth =
|
|
108
|
+
modelRegistry.authStorage.hasNonEnvCredential("xai-oauth") || Boolean($env.XAI_OAUTH_TOKEN);
|
|
109
|
+
if (hasDedicatedXaiOAuth) {
|
|
110
|
+
const oauthKey = await modelRegistry.getApiKeyForProvider("xai-oauth");
|
|
111
|
+
if (oauthKey) {
|
|
112
|
+
const baseURL = resolveXAIBaseURL(modelRegistry, "xai-oauth", modelId);
|
|
113
|
+
return { provider: "xai-oauth", apiKey: oauthKey, baseURL };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const apiKey = await modelRegistry.getApiKeyForProvider("xai");
|
|
118
|
+
if (apiKey) {
|
|
119
|
+
const baseURL = resolveXAIBaseURL(modelRegistry, "xai", modelId);
|
|
120
|
+
return { provider: "xai", apiKey, baseURL };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|