diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index cea18627d..f3115d9de 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,10 +1,16 @@ # Changelog ## [Unreleased] + +### Added + +- Added authentication broker discovery to sync credentials between local SQLite and remote state + ### Fixed - Added automatic fallback for unsupported OpenAI reasoning effort levels - Improved reliability when handling invalid reasoning parameter errors across OpenAI-compatible APIs +- Fixed OpenAI-compatible Chat Completions, Responses, and Azure Responses requests to retry once with the nearest provider-supported reasoning effort when an endpoint rejects `xhigh`/`minimal`-style effort values. ## [16.1.0] - 2026-06-19 diff --git a/packages/ai/src/auth-broker/discover.ts b/packages/ai/src/auth-broker/discover.ts new file mode 100644 index 000000000..857fcc904 --- /dev/null +++ b/packages/ai/src/auth-broker/discover.ts @@ -0,0 +1,217 @@ +/** + * Broker-aware auth-storage discovery used by both the coding-agent runtime and + * the catalog model generator. Keeps the precedence logic (env → config.yml → + * token file → local SQLite) in one place so build-time tooling sees the same + * credentials as the TUI. + */ +import * as path from "node:path"; +import { + getAgentDbPath, + getAgentDir, + getAuthBrokerSnapshotCachePath, + getConfigRootDir, + isEnoent, + logger, +} from "@oh-my-pi/pi-utils"; +import { YAML } from "bun"; +import { AuthStorage } from "../auth-storage"; +import { AuthBrokerClient } from "./client"; +import { RemoteAuthCredentialStore } from "./remote-store"; +import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "./snapshot-cache"; +import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types"; + +export interface AuthBrokerClientConfig { + url: string; + token: string; +} + +export interface ResolveAuthBrokerConfigOptions { + agentDir?: string; + configValueResolver?: (config: string) => Promise; +} + +export interface DiscoverAuthStorageOptions { + agentDir?: string; + configValueResolver?: (config: string) => Promise; + cachePath?: string; + sourceLabel?: string; +} + +/** Path to the local bearer token file. Created by `omp auth-broker token`. */ +export function getAuthBrokerTokenFilePath(): string { + return path.join(getConfigRootDir(), "auth-broker.token"); +} + +/** + * Default resolver for config values: checks `process.env` first, then treats + * the value as a literal. Does NOT execute `!command` syntax; such values are + * left unresolved so the caller can fall back to the token file. + */ +async function defaultResolveConfigValue(config: string): Promise { + if (config.startsWith("!")) return undefined; + const envValue = process.env[config]; + return envValue || config; +} + +async function readTokenFile(): Promise { + try { + const raw = await Bun.file(getAuthBrokerTokenFilePath()).text(); + const trimmed = raw.trim(); + return trimmed.length > 0 ? trimmed : null; + } catch (err) { + if (isEnoent(err)) return null; + logger.warn("auth-broker token file unreadable", { error: String(err) }); + return null; + } +} + +interface ConfigSnapshot { + url?: string; + token?: string; +} + +async function readConfigYaml(agentDir: string): Promise { + const configPath = path.join(agentDir, "config.yml"); + try { + const raw = await Bun.file(configPath).text(); + const parsed = YAML.parse(raw); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; + const record = parsed as Record; + const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined; + const token = + typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined; + return { url, token }; + } catch (err) { + if (isEnoent(err)) return {}; + logger.warn("auth-broker config.yml unreadable", { error: String(err) }); + return {}; + } +} + +function resolveSnapshotTtlMs(): number { + const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; + if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS; + const value = raw.trim(); + if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS; + const ttlMs = Number(value); + if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs; + logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw }); + return DEFAULT_SNAPSHOT_CACHE_TTL_MS; +} + +/** + * Resolve broker connection configuration using the same precedence as the TUI: + * + * 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars. + * 2. `auth.broker.url` / `auth.broker.token` in `/config.yml`. + * 3. `/auth-broker.token` file (paired with a URL from env/config). + * + * Returns `null` when no broker URL is configured — callers should fall back to + * the local SQLite store. Throws when a URL is configured but no token is + * available, matching the TUI behavior. + */ +export async function resolveAuthBrokerConfig( + options: ResolveAuthBrokerConfigOptions = {}, +): Promise { + const agentDir = options.agentDir ?? getAgentDir(); + const resolveConfig = options.configValueResolver ?? defaultResolveConfigValue; + + const envUrl = process.env.OMP_AUTH_BROKER_URL; + const envToken = process.env.OMP_AUTH_BROKER_TOKEN; + + let url = envUrl && envUrl.length > 0 ? envUrl : undefined; + let configToken: string | undefined; + if (!url || !envToken) { + const fromConfig = await readConfigYaml(agentDir); + if (!url && fromConfig.url) { + const resolved = await resolveConfig(fromConfig.url); + if (resolved && resolved.length > 0) url = resolved; + } + if (fromConfig.token) { + const resolved = await resolveConfig(fromConfig.token); + if (resolved && resolved.length > 0) configToken = resolved; + } + } + if (!url) return null; + + const token = + (envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined; + if (!token) { + throw new Error( + `OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` + + `Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`, + ); + } + return { url, token }; +} + +/** + * Create an AuthStorage instance, using the broker when configured and falling + * back to the local SQLite store otherwise. This is the single source of truth + * for the TUI and the catalog generator. + */ +export async function discoverAuthStorage(options: DiscoverAuthStorageOptions = {}): Promise { + const agentDir = options.agentDir ?? getAgentDir(); + const brokerConfig = await resolveAuthBrokerConfig({ + agentDir, + configValueResolver: options.configValueResolver, + }); + + if (brokerConfig) { + const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token }); + const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath(); + const ttlMs = resolveSnapshotTtlMs(); + const persist = + ttlMs > 0 + ? (snapshot: SnapshotResponse): void => { + void writeAuthBrokerSnapshotCache({ + path: cachePath, + token: brokerConfig.token, + url: brokerConfig.url, + snapshot, + }).catch(error => { + logger.debug("auth-broker snapshot cache write failed", { error: String(error) }); + }); + } + : undefined; + + let initialSnapshot: SnapshotResponse | undefined; + if (ttlMs > 0) { + initialSnapshot = + (await readAuthBrokerSnapshotCache({ + path: cachePath, + token: brokerConfig.token, + url: brokerConfig.url, + ttlMs, + }).catch(error => { + logger.debug("auth-broker snapshot cache read failed", { error: String(error) }); + return null; + })) ?? undefined; + } + if (!initialSnapshot) { + const initialResult = await client.fetchSnapshot(); + if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot"); + initialSnapshot = initialResult.snapshot; + persist?.(initialSnapshot); + } + const store = new RemoteAuthCredentialStore({ + client, + initialSnapshot, + onSnapshot: persist, + }); + const storage = new AuthStorage(store, { + configValueResolver: options.configValueResolver, + sourceLabel: options.sourceLabel ?? `broker ${brokerConfig.url}`, + }); + await storage.reload(); + return storage; + } + + const dbPath = getAgentDbPath(agentDir); + const storage = await AuthStorage.create(dbPath, { + configValueResolver: options.configValueResolver, + sourceLabel: options.sourceLabel ?? `local ${dbPath}`, + }); + await storage.reload(); + return storage; +} diff --git a/packages/ai/src/auth-broker/index.ts b/packages/ai/src/auth-broker/index.ts index 189d377c5..22497acbb 100644 --- a/packages/ai/src/auth-broker/index.ts +++ b/packages/ai/src/auth-broker/index.ts @@ -1,4 +1,5 @@ export * from "./client"; +export * from "./discover"; export * from "./refresher"; export * from "./remote-store"; export * from "./server"; diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index d7c4edbad..dac9f11a8 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -1,6 +1,21 @@ # Changelog ## [Unreleased] +### Added + +- Added support for Gemini 2.5 Flash-Lite, 3.1 Flash-Lite, and 3.5 Flash models +- Added support for Moonshot V1 model family + +### Changed + +- Updated context window and token limits for various Claude, Gemini, and GPT-OSS models +- Refined thinking mode behaviors and routing for supported LLM families + +### Fixed + +- Fixed effort mapping for GLM-5.2 and OpenRouter reasoning models to resolve top-tier 400 errors +- Maintained thinking effort routing when discovery only returns the base model ID +- Improved credential retrieval logic for Antigravity and Codex providers via auth discovery ## [16.0.9] - 2026-06-18 @@ -300,4 +315,4 @@ ### Removed -- Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads. +- Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads. \ No newline at end of file diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index cba4765d9..5f85acc02 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -10,7 +10,8 @@ const COPILOT_PREMIUM_MULTIPLIERS: Record = { }; import * as path from "node:path"; -import { AuthStorage, type OAuthAccess, SqliteAuthCredentialStore } from "@oh-my-pi/pi-ai/auth-storage"; +import { discoverAuthStorage } from "@oh-my-pi/pi-ai/auth-broker/discover"; +import type { OAuthAccess } from "@oh-my-pi/pi-ai/auth-storage"; import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types"; import { getGitLabDuoModels } from "@oh-my-pi/pi-ai/providers/gitlab-duo"; import { $env } from "@oh-my-pi/pi-utils"; @@ -69,10 +70,8 @@ async function resolveProviderApiKey(providerId: string, catalog: CatalogDiscove } try { - const store = await SqliteAuthCredentialStore.open(); - const authStorage = new AuthStorage(store); + const authStorage = await discoverAuthStorage(); try { - await authStorage.reload(); const storedApiKey = await authStorage.getApiKey(providerId); if (storedApiKey) { return storedApiKey; @@ -88,10 +87,13 @@ async function resolveProviderApiKey(providerId: string, catalog: CatalogDiscove } } } finally { - store.close(); + authStorage.close(); } - } catch { - // Ignore missing/unreadable auth storage. + } catch (err) { + console.warn( + `Warning: Failed to retrieve credentials for ${providerId}:`, + err instanceof Error ? err.message : String(err), + ); } return undefined; @@ -336,19 +338,25 @@ const ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_PRIMARY_ENDPOINT; async function getOAuthAccessFromStorage(provider: OAuthProvider): Promise { try { - const store = await SqliteAuthCredentialStore.open(); - const authStorage = new AuthStorage(store); + const authStorage = await discoverAuthStorage(); try { - await authStorage.reload(); // `getOAuthAccess` runs the full AuthStorage refresh pipeline so an // expired-but-refreshable credential gets rotated before discovery, // and identity metadata (accountId/projectId/email) flows through // for Codex/Antigravity downstream calls. - return (await authStorage.getOAuthAccess(provider)) ?? null; + let access = await authStorage.getOAuthAccess(provider); + if (!access && provider === "google-antigravity") { + access = await authStorage.getOAuthAccess("google-gemini-cli"); + } + return access ?? null; } finally { - store.close(); + authStorage.close(); } - } catch { + } catch (err) { + console.warn( + `Warning: Failed to retrieve credentials for ${provider}:`, + err instanceof Error ? err.message : String(err), + ); return null; } } @@ -360,7 +368,8 @@ async function getOAuthAccessFromStorage(provider: OAuthProvider): Promise[]> { const access = await getOAuthAccessFromStorage("google-antigravity"); if (!access) { - console.log("No Antigravity credentials found, will use previous models"); + console.log("No Antigravity or Gemini CLI credentials found, will use previous models."); + console.log("Tip: If you are logged in under a specific profile, run with OMP_PROFILE=."); return []; } try { @@ -404,6 +413,8 @@ function extractCodexAccountId(accessToken: string): string | null { async function fetchCodexDiscoveryModels(): Promise[]> { const access = await getOAuthAccessFromStorage("openai-codex"); if (!access) { + console.log("No Codex credentials found, will use previous models."); + console.log("Tip: If you are logged in under a specific profile, run with OMP_PROFILE=."); return []; } try { diff --git a/packages/catalog/src/model-thinking.ts b/packages/catalog/src/model-thinking.ts index a9bead070..e7cfdcfc3 100644 --- a/packages/catalog/src/model-thinking.ts +++ b/packages/catalog/src/model-thinking.ts @@ -161,7 +161,7 @@ function fillThinkingWireDefaults( thinking: ThinkingConfig, ): ThinkingConfig { const parsed = parseKnownModel(spec.id); - const normalizedEfforts = getModelDefinedEfforts(spec) ?? thinking.efforts; + const normalizedEfforts = getModelDefinedEfforts(spec, compat) ?? thinking.efforts; const effortsChanged = !sameEffortList(normalizedEfforts, thinking.efforts); const effortMap = thinking.effortMap === undefined @@ -248,7 +248,7 @@ function inferEffortMap( mode: ThinkingConfig["mode"], efforts: readonly Effort[], ): EffortMap | undefined { - const detected = inferDetectedEffortMap(spec, parsedModel, mode); + const detected = inferDetectedEffortMap(spec, compat, parsedModel, mode); const configured = readCompatEffortMap(compat); const merged = detected === undefined ? configured : configured === undefined ? detected : { ...detected, ...configured }; @@ -278,13 +278,23 @@ function isOpenAICompatReasoningApi(api: Api): boolean { return api === "openai-completions" || api === "openrouter"; } -function getModelDefinedEfforts(spec: ModelSpec): readonly Effort[] | undefined { +function getModelDefinedEfforts( + spec: ModelSpec, + compat: CompatOf, +): readonly Effort[] | undefined { if (isOpenAICompatReasoningApi(spec.api) && isZaiGlm52ReasoningEffortModel(spec)) { return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; } if (isOllamaCloudGlm52ReasoningEffortModel(spec)) { return GLM_52_HIGH_MAX_REASONING_EFFORTS; } + // OpenRouter fronts GLM-5.2 on its own effort scale where `xhigh` IS the max + // tier (the literal `max` value 400s). Expose `xhigh` so the top tier is + // selectable; it passes through unmapped (excluded from the GLM dialect map) + // and OpenRouter resolves it to max reasoning. + if (isOpenRouterThinkingFormat(compat) && isGlm52ReasoningEffortModelId(spec.id)) { + return DEFAULT_REASONING_EFFORTS_WITH_XHIGH; + } return isOpenAICompatReasoningApi(spec.api) && (isMinimaxM2FamilyModelId(spec.id) || isOpenAIGptOssModelId(spec.id)) ? LOW_MEDIUM_HIGH_REASONING_EFFORTS : undefined; @@ -311,8 +321,13 @@ function readCompatEffortMap(compat: CompatOf): EffortMap | undefined { return map && Object.keys(map).length > 0 ? map : undefined; } +function isOpenRouterThinkingFormat(compat: CompatOf): boolean { + return compat !== undefined && "thinkingFormat" in compat && compat.thinkingFormat === "openrouter"; +} + function inferDetectedEffortMap( spec: ModelSpec, + compat: CompatOf, parsedModel: ParsedModel, mode: ThinkingConfig["mode"], ): EffortMap | undefined { @@ -324,12 +339,18 @@ function inferDetectedEffortMap( ? ANTHROPIC_ADAPTIVE_EFFORT_MAP_5_TIER : ANTHROPIC_ADAPTIVE_EFFORT_MAP_4_TIER; } - // GLM-5.2 coding SKUs speak the GLM-native effort dialect regardless of host: - // the internal `xhigh` has no GLM equivalent (top tier is `max`), and the - // lower tiers fold to `none`/`high`. Z.ai/Zhipu, Ollama Cloud, Fireworks, - // resellers — every gateway serving genuine glm-5.2 gets the same map so the - // top tier stops 400ing. Filtered to each host's supported efforts downstream. - if (isGlm52ReasoningEffortModelId(spec.id)) { + // GLM-5.2 coding SKUs speak the GLM-native effort dialect (`none`/`high`/`max`) + // on every direct host that fronts the genuine model — Z.ai/Zhipu, Fireworks, + // resellers, Ollama Cloud. The internal `xhigh` has no GLM equivalent (top + // tier is `max`), so without this map the top tier 400s. OpenRouter is the + // exception: its API rejects `max` and treats `xhigh` AS GLM's max tier, so it + // is excluded here and passes `xhigh` through literally (the tier is exposed + // via `getModelDefinedEfforts`). Filtered to supported efforts downstream. + if ( + isGlm52ReasoningEffortModelId(spec.id) && + !isOpenRouterThinkingFormat(compat) && + (isOpenAICompatReasoningApi(spec.api) || isOllamaCloudGlm52ReasoningEffortModel(spec)) + ) { return GLM_52_REASONING_EFFORT_MAP; } if (!isOpenAICompatReasoningApi(spec.api)) { @@ -382,7 +403,7 @@ function inferSupportedEfforts( spec: ModelSpec, compat: CompatOf, ): readonly Effort[] { - const modelDefinedEfforts = getModelDefinedEfforts(spec); + const modelDefinedEfforts = getModelDefinedEfforts(spec, compat); if (modelDefinedEfforts !== undefined) { return modelDefinedEfforts; } diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index 273e0bbf2..2ce4ee016 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -14829,7 +14829,11 @@ "xhigh" ], "effortMap": { - "minimal": "none" + "minimal": "none", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "max" } } }, @@ -17775,8 +17779,9 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 1000000, + "contextWindow": 250000, "maxTokens": 64000, + "requestModelId": "claude-opus-4-6-thinking", "thinking": { "mode": "budget", "efforts": [ @@ -17786,14 +17791,12 @@ "high" ], "effortRouting": { - "off": "claude-opus-4-6", "minimal": "claude-opus-4-6-thinking", "low": "claude-opus-4-6-thinking", "medium": "claude-opus-4-6-thinking", "high": "claude-opus-4-6-thinking" } - }, - "requestModelId": "claude-opus-4-6-thinking" + } }, "claude-sonnet-4-5": { "id": "claude-sonnet-4-5", @@ -17849,8 +17852,9 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 1000000, - "maxTokens": 128000, + "contextWindow": 250000, + "maxTokens": 64000, + "requestModelId": "claude-sonnet-4-6", "thinking": { "mode": "budget", "efforts": [ @@ -17866,8 +17870,7 @@ "medium": "claude-sonnet-4-6-thinking", "high": "claude-sonnet-4-6-thinking" } - }, - "requestModelId": "claude-sonnet-4-6" + } }, "gemini-2.5-flash": { "id": "gemini-2.5-flash", @@ -17887,7 +17890,7 @@ "cacheWrite": 0 }, "contextWindow": 1048576, - "maxTokens": 65536, + "maxTokens": 65535, "thinking": { "mode": "budget", "efforts": [ @@ -17905,6 +17908,35 @@ } } }, + "gemini-2.5-flash-lite": { + "id": "gemini-2.5-flash-lite", + "name": "Gemini 2.5 Flash-Lite", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 65535, + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ] + } + }, "gemini-2.5-pro": { "id": "gemini-2.5-pro", "name": "Gemini 2.5 Pro", @@ -18013,6 +18045,55 @@ }, "requestModelId": "gemini-3-pro-low" }, + "gemini-3.1-flash-image": { + "id": "gemini-3.1-flash-image", + "name": "Gemini 3.1 Flash Image", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 200000, + "maxTokens": 64000 + }, + "gemini-3.1-flash-lite": { + "id": "gemini-3.1-flash-lite", + "name": "Gemini 3.1 Flash Lite", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 65535, + "thinking": { + "mode": "google-level", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "requiresEffort": true + } + }, "gemini-3.1-pro": { "id": "gemini-3.1-pro", "name": "Gemini 3.1 Pro Preview", @@ -18032,6 +18113,7 @@ }, "contextWindow": 1048576, "maxTokens": 65535, + "requestModelId": "gemini-3.1-pro-low", "thinking": { "mode": "budget", "efforts": [ @@ -18048,8 +18130,51 @@ "high": "gemini-pro-agent" }, "suppressWhenOff": true + } + }, + "gemini-3.5-flash": { + "id": "gemini-3.5-flash", + "name": "Gemini 3.5 Flash", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 }, - "requestModelId": "gemini-3.1-pro-low" + "contextWindow": 1048576, + "maxTokens": 65536, + "requestModelId": "gemini-3.5-flash-extra-low", + "thinking": { + "mode": "budget", + "efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "effortBudgets": { + "minimal": 1000, + "low": 1000, + "medium": 4000, + "high": 10000 + }, + "effortRouting": { + "off": "gemini-3.5-flash-extra-low", + "minimal": "gemini-3.5-flash-extra-low", + "low": "gemini-3.5-flash-extra-low", + "medium": "gemini-3.5-flash-low", + "high": "gemini-3-flash-agent" + }, + "suppressWhenOff": true + } }, "gpt-oss-120b": { "id": "gpt-oss-120b", @@ -18067,8 +18192,9 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": 114000, + "contextWindow": 131072, "maxTokens": 32768, + "requestModelId": "gpt-oss-120b-medium", "thinking": { "mode": "budget", "efforts": [ @@ -18077,8 +18203,45 @@ "medium", "high" ] + } + }, + "tab_flash_lite_preview": { + "id": "tab_flash_lite_preview", + "name": "tab_flash_lite_preview", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 }, - "requestModelId": "gpt-oss-120b-medium" + "contextWindow": 16384, + "maxTokens": 4096 + }, + "tab_jump_flash_lite_preview": { + "id": "tab_jump_flash_lite_preview", + "name": "tab_jump_flash_lite_preview", + "api": "google-gemini-cli", + "provider": "google-antigravity", + "baseUrl": "https://daily-cloudcode-pa.googleapis.com", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 16384, + "maxTokens": 4096 } }, "google-gemini-cli": { @@ -23018,8 +23181,8 @@ "cacheRead": 0, "cacheWrite": 0 }, - "contextWindow": null, - "maxTokens": null + "contextWindow": 200000, + "maxTokens": 64000 }, "google/gemini-3.1-flash-image-preview": { "id": "google/gemini-3.1-flash-image-preview", @@ -33153,6 +33316,142 @@ "high" ] } + }, + "moonshot-v1-128k": { + "id": "moonshot-v1-128k", + "name": "moonshot-v1-128k", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": null + }, + "moonshot-v1-128k-vision-preview": { + "id": "moonshot-v1-128k-vision-preview", + "name": "moonshot-v1-128k-vision-preview", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": null + }, + "moonshot-v1-32k": { + "id": "moonshot-v1-32k", + "name": "moonshot-v1-32k", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 32768, + "maxTokens": null + }, + "moonshot-v1-32k-vision-preview": { + "id": "moonshot-v1-32k-vision-preview", + "name": "moonshot-v1-32k-vision-preview", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 32768, + "maxTokens": null + }, + "moonshot-v1-8k": { + "id": "moonshot-v1-8k", + "name": "moonshot-v1-8k", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 8192, + "maxTokens": null + }, + "moonshot-v1-8k-vision-preview": { + "id": "moonshot-v1-8k-vision-preview", + "name": "moonshot-v1-8k-vision-preview", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 8192, + "maxTokens": null + }, + "moonshot-v1-auto": { + "id": "moonshot-v1-auto", + "name": "moonshot-v1-auto", + "api": "openai-completions", + "provider": "moonshot", + "baseUrl": "https://api.moonshot.ai/v1", + "reasoning": false, + "input": [ + "text" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 131072, + "maxTokens": null } }, "nanogpt": { @@ -49822,7 +50121,14 @@ "medium", "high", "xhigh" - ] + ], + "effortMap": { + "minimal": "none", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "max" + } } }, "zai-org/glm-latest": { @@ -53684,6 +53990,7 @@ "xhigh" ], "effortMap": { + "high": "high", "xhigh": "max" } } @@ -56358,7 +56665,14 @@ "medium", "high", "xhigh" - ] + ], + "effortMap": { + "minimal": "none", + "low": "high", + "medium": "high", + "high": "high", + "xhigh": "max" + } } }, "kimi-k2.5": { @@ -67541,7 +67855,8 @@ "minimal", "low", "medium", - "high" + "high", + "xhigh" ] } }, @@ -78226,14 +78541,6 @@ }, "contextWindow": 2000000, "maxTokens": 2000000, - "compat": { - "reasoningEffortMap": { - "minimal": "low" - }, - "includeEncryptedReasoning": false, - "filterReasoningHistory": true, - "omitReasoningEffort": false - }, "thinking": { "mode": "effort", "efforts": [ @@ -78246,6 +78553,14 @@ "effortMap": { "minimal": "low" } + }, + "compat": { + "reasoningEffortMap": { + "minimal": "low" + }, + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "omitReasoningEffort": false } }, "grok-4.3": { @@ -78267,14 +78582,6 @@ }, "contextWindow": 1000000, "maxTokens": 1000000, - "compat": { - "reasoningEffortMap": { - "minimal": "low" - }, - "includeEncryptedReasoning": false, - "filterReasoningHistory": true, - "omitReasoningEffort": false - }, "thinking": { "mode": "effort", "efforts": [ @@ -78287,6 +78594,14 @@ "effortMap": { "minimal": "low" } + }, + "compat": { + "reasoningEffortMap": { + "minimal": "low" + }, + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "omitReasoningEffort": false } }, "grok-build": { @@ -78297,7 +78612,8 @@ "baseUrl": "https://api.x.ai/v1", "reasoning": true, "input": [ - "text" + "text", + "image" ], "cost": { "input": 0, @@ -78317,6 +78633,31 @@ "supportsReasoningEffort": false } }, + "grok-build-0.1": { + "id": "grok-build-0.1", + "name": "Grok Build 0.1", + "api": "openai-responses", + "provider": "xai-oauth", + "baseUrl": "https://api.x.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 256000, + "maxTokens": 256000, + "compat": { + "includeEncryptedReasoning": false, + "filterReasoningHistory": true, + "omitReasoningEffort": true + } + }, "grok-composer-2.5-fast": { "id": "grok-composer-2.5-fast", "name": "Grok Composer 2.5 Fast", diff --git a/packages/catalog/src/variant-collapse.ts b/packages/catalog/src/variant-collapse.ts index 9f8bf016c..70c207ea7 100644 --- a/packages/catalog/src/variant-collapse.ts +++ b/packages/catalog/src/variant-collapse.ts @@ -76,6 +76,13 @@ export interface EffortVariantFamily { thinking: Readonly>; /** Thinking-off requests must explicitly suppress thinking on the wire. */ suppressWhenOff?: boolean; + /** + * Preserve non-off effort routes even when discovery omits the backing member. + * Used for Cloud Code Assist `X`/`X-thinking` pairs where upstream accepts + * the `-thinking` wire id but the model-list endpoint may advertise only the + * bare id. + */ + preserveAbsentEffortRoutes?: boolean; /** Retired/recycled selector ids that alias to this family without being members. */ extraAliases?: readonly string[]; } @@ -100,6 +107,7 @@ function thinkingPair(baseId: string, name: string): EffortVariantFamily { // Thinking-off routes to the non-thinking backing id, where omitting // thinkingConfig is already correct — no suppressWhenOff. thinking: { mode: "budget", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }, + preserveAbsentEffortRoutes: true, }; } @@ -517,12 +525,18 @@ export function collapseEffortVariants( const routing: Partial> = {}; let hasRouting = false; let hasEffortRoute = false; + let usedAbsentEffortRoute = false; for (const effortKey in family.routing) { const target = family.routing[effortKey as Effort | "off"]; - if (target !== undefined && presentSet.has(target) && !retired?.has(target)) { - routing[effortKey as Effort | "off"] = target; + const effort = effortKey as Effort | "off"; + const targetPresent = target !== undefined && presentSet.has(target); + const preserveAbsentEffort = + target !== undefined && effort !== "off" && family.preserveAbsentEffortRoutes === true; + if (target !== undefined && (targetPresent || preserveAbsentEffort) && !retired?.has(target)) { + routing[effort] = target; hasRouting = true; if (effortKey !== "off") hasEffortRoute = true; + if (!targetPresent && effort !== "off") usedAbsentEffortRoute = true; } } @@ -551,7 +565,11 @@ export function collapseEffortVariants( // falls back. Retired members never become the default. const defaultWireId = rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0]; if (defaultWireId === family.id) { - delete collapsed.requestModelId; + if (usedAbsentEffortRoute) { + collapsed.requestModelId = defaultWireId as string; + } else { + delete collapsed.requestModelId; + } } else { collapsed.requestModelId = defaultWireId as string; } diff --git a/packages/catalog/test/variant-collapse.test.ts b/packages/catalog/test/variant-collapse.test.ts index b6dd7489d..6cb3f46e4 100644 --- a/packages/catalog/test/variant-collapse.test.ts +++ b/packages/catalog/test/variant-collapse.test.ts @@ -151,6 +151,24 @@ describe("collapseEffortVariants", () => { }); }); + it("keeps claude -thinking routing when discovery only returns the bare id", () => { + const out = collapseEffortVariants( + [memberSpec("claude-sonnet-4-6", { maxTokens: 64_000 })], + ANTIGRAVITY_VARIANT_COLLAPSE_TABLE, + ); + + expect(out).toHaveLength(1); + expect(out[0]?.id).toBe("claude-sonnet-4-6"); + expect(out[0]?.requestModelId).toBe("claude-sonnet-4-6"); + expect(out[0]?.thinking?.effortRouting).toEqual({ + off: "claude-sonnet-4-6", + minimal: "claude-sonnet-4-6-thinking", + low: "claude-sonnet-4-6-thinking", + medium: "claude-sonnet-4-6-thinking", + high: "claude-sonnet-4-6-thinking", + }); + }); + it("keeps the thinking backing id for a -thinking-only claude family", () => { const out = collapseEffortVariants([memberSpec("claude-opus-4-6-thinking")], ANTIGRAVITY_VARIANT_COLLAPSE_TABLE); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 84c185a06..2fb620cb3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Changed + +- Refactored authentication storage discovery to share logic with other pi-ai tools ### Fixed @@ -12086,4 +12089,4 @@ Initial public release. ## [0.7.6] - 2025-11-13 -Previous releases did not maintain a changelog. +Previous releases did not maintain a changelog. \ No newline at end of file diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 6ed825a28..aa5ee2949 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -22,18 +22,7 @@ import { } from "@oh-my-pi/pi-ai/providers/openai-codex-responses"; import { FALLBACK_DIALECT, preferredDialect } from "@oh-my-pi/pi-catalog/identity"; import type { Component } from "@oh-my-pi/pi-tui"; -import { - $env, - $flag, - getAgentDbPath, - getAgentDir, - getAuthBrokerSnapshotCachePath, - getProjectDir, - logger, - postmortem, - prompt, - Snowflake, -} from "@oh-my-pi/pi-utils"; +import { $env, $flag, getAgentDir, getProjectDir, logger, postmortem, prompt, Snowflake } from "@oh-my-pi/pi-utils"; import { INTENT_FIELD } from "@oh-my-pi/pi-wire"; import { ADVISOR_READONLY_TOOL_NAMES, discoverWatchdogFiles } from "./advisor"; import { type AsyncJob, AsyncJobManager } from "./async"; @@ -56,11 +45,6 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate import { Settings, type SkillsSettings } from "./config/settings"; import { CursorExecHandlers } from "./cursor"; import "./discovery"; -import { AuthBrokerClient } from "@oh-my-pi/pi-ai/auth-broker/client"; -import { RemoteAuthCredentialStore } from "@oh-my-pi/pi-ai/auth-broker/remote-store"; -import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "@oh-my-pi/pi-ai/auth-broker/snapshot-cache"; -import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "@oh-my-pi/pi-ai/auth-broker/types"; -import { resolveConfigValue } from "./config/resolve-config-value"; import { initializeWithSettings } from "./discovery"; import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor"; import { defaultEvalSessionId } from "./eval/session-id"; @@ -119,8 +103,8 @@ import { SecretObfuscator, } from "./secrets"; import { AgentSession } from "./session/agent-session"; -import { resolveAuthBrokerConfig } from "./session/auth-broker-config"; -import { AuthStorage } from "./session/auth-storage"; +import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config"; +import type { AuthStorage } from "./session/auth-storage"; import { type CustomMessage, convertToLlm, @@ -621,21 +605,6 @@ export { // Helper Functions -function getDefaultAgentDir(): string { - return getAgentDir(); -} - -function resolveSnapshotTtlMs(): number { - const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; - if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS; - const value = raw.trim(); - if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS; - const ttlMs = Number(value); - if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs; - logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw }); - return DEFAULT_SNAPSHOT_CACHE_TTL_MS; -} - // Discovery Functions /** @@ -648,70 +617,12 @@ function resolveSnapshotTtlMs(): number { * the client receives access tokens with `refresh = "__remote__"` and calls * back into the broker through the {@link AuthStorageOptions.refreshOAuthCredential} * override to re-mint access tokens when needed. + * + * Delegates to {@link ./session/auth-broker-config} so the TUI and the catalog + * generator share the same credential-discovery logic. */ -export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise { - const brokerConfigPromise = resolveAuthBrokerConfig(); - const cachePath = getAuthBrokerSnapshotCachePath(); - // Warm the encrypted snapshot cache into the page cache while the broker - // config resolves (it may shell out for a `!command` token). Decryption - // needs the resolved token, so the real cache read cannot start earlier. - void Bun.file(cachePath) - .arrayBuffer() - .catch(() => undefined); - const brokerConfig = await brokerConfigPromise; - if (brokerConfig) { - const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token }); - const ttlMs = resolveSnapshotTtlMs(); - const persist = - ttlMs > 0 - ? (snapshot: SnapshotResponse): void => { - void writeAuthBrokerSnapshotCache({ - path: cachePath, - token: brokerConfig.token, - url: brokerConfig.url, - snapshot, - }).catch(error => { - logger.debug("auth-broker snapshot cache write failed", { error: String(error) }); - }); - } - : undefined; - - let initialSnapshot: SnapshotResponse | undefined; - if (ttlMs > 0) { - initialSnapshot = - (await readAuthBrokerSnapshotCache({ - path: cachePath, - token: brokerConfig.token, - url: brokerConfig.url, - ttlMs, - }).catch(error => { - logger.debug("auth-broker snapshot cache read failed", { error: String(error) }); - return null; - })) ?? undefined; - } - if (!initialSnapshot) { - const initialResult = await client.fetchSnapshot(); - if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot"); - initialSnapshot = initialResult.snapshot; - persist?.(initialSnapshot); - } - const store = new RemoteAuthCredentialStore({ client, initialSnapshot, onSnapshot: persist }); - // Refresh + usage hooks live on RemoteAuthCredentialStore; AuthStorage - // discovers them automatically when no explicit option overrides them. - const storage = new AuthStorage(store, { - configValueResolver: resolveConfigValue, - sourceLabel: `broker ${brokerConfig.url}`, - }); - await storage.reload(); - return storage; - } - const dbPath = getAgentDbPath(agentDir); - const storage = await AuthStorage.create(dbPath, { - configValueResolver: resolveConfigValue, - sourceLabel: `local ${dbPath}`, - }); - await storage.reload(); - return storage; +export async function discoverAuthStorage(agentDir: string = getAgentDir()): Promise { + return discoverAuthStorageFromConfig(agentDir); } /** @@ -799,7 +710,7 @@ export async function discoverContextFiles( export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise { return await loadPromptTemplatesInternal({ cwd: cwd ?? getProjectDir(), - agentDir: agentDir ?? getDefaultAgentDir(), + agentDir: agentDir ?? getAgentDir(), }); } @@ -815,7 +726,7 @@ export async function discoverSlashCommands(cwd?: string): Promise { const resolvedCwd = cwd ?? getProjectDir(); - const resolvedAgentDir = agentDir ?? getDefaultAgentDir(); + const resolvedAgentDir = agentDir ?? getAgentDir(); return loadCustomCommandsInternal({ cwd: resolvedCwd, @@ -1118,7 +1029,7 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] { */ export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise { const cwd = options.cwd ?? getProjectDir(); - const agentDir = options.agentDir ?? getDefaultAgentDir(); + const agentDir = options.agentDir ?? getAgentDir(); const eventBus = options.eventBus ?? new EventBus(); registerSshCleanup(); diff --git a/packages/coding-agent/src/session/auth-broker-config.ts b/packages/coding-agent/src/session/auth-broker-config.ts index 2c015b4cd..49b3b5d30 100644 --- a/packages/coding-agent/src/session/auth-broker-config.ts +++ b/packages/coding-agent/src/session/auth-broker-config.ts @@ -1,6 +1,11 @@ /** * Resolve auth-broker connection configuration for the local omp client. * + * This is a thin coding-agent wrapper around the shared resolver in + * `@oh-my-pi/pi-ai/auth-broker/discover` that preserves the process-lifetime + * memoization expected by the CLI and injects the full `resolveConfigValue` + * (including `!command` config indirection) from coding-agent's config layer. + * * Precedence (highest first): * 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars. * 2. `auth.broker.url` / `auth.broker.token` in `~/.omp/agent/config.yml` @@ -15,55 +20,18 @@ * `runRootCommand`, and we want hand-edited config entries to be honoured at * boot without forcing a startup reorder. */ -import * as path from "node:path"; -import { getAgentDir, getConfigRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; -import { YAML } from "bun"; + +import { + type AuthBrokerClientConfig, + type DiscoverAuthStorageOptions, + discoverAuthStorage as discoverAuthStorageShared, + getAuthBrokerTokenFilePath, + resolveAuthBrokerConfig as resolveAuthBrokerConfigShared, +} from "@oh-my-pi/pi-ai/auth-broker/discover"; +import { getAgentDir } from "@oh-my-pi/pi-utils"; import { resolveConfigValue } from "../config/resolve-config-value"; -export interface AuthBrokerClientConfig { - url: string; - token: string; -} - -/** Path to the local bearer token file. Created on the broker host by `omp auth-broker token`. */ -export function getAuthBrokerTokenFilePath(): string { - return path.join(getConfigRootDir(), "auth-broker.token"); -} - -async function readTokenFile(): Promise { - try { - const raw = await Bun.file(getAuthBrokerTokenFilePath()).text(); - const trimmed = raw.trim(); - return trimmed.length > 0 ? trimmed : null; - } catch (err) { - if (isEnoent(err)) return null; - logger.warn("auth-broker token file unreadable", { error: String(err) }); - return null; - } -} - -interface ConfigSnapshot { - url?: string; - token?: string; -} - -async function readConfigYaml(): Promise { - const configPath = path.join(getAgentDir(), "config.yml"); - try { - const raw = await Bun.file(configPath).text(); - const parsed = YAML.parse(raw); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; - const record = parsed as Record; - const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined; - const token = - typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined; - return { url, token }; - } catch (err) { - if (isEnoent(err)) return {}; - logger.warn("auth-broker config.yml unreadable", { error: String(err) }); - return {}; - } -} +export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath }; /** * Process-lifetime memo for {@link resolveAuthBrokerConfig}. Keyed on the env @@ -88,7 +56,10 @@ let cachedConfigPromise: Promise | null = null; export function resolveAuthBrokerConfig(): Promise { const key = `${process.env.OMP_AUTH_BROKER_URL ?? ""}\u0000${process.env.OMP_AUTH_BROKER_TOKEN ?? ""}\u0000${getAgentDir()}`; if (cachedConfigPromise && cachedConfigKey === key) return cachedConfigPromise; - const promise = resolveAuthBrokerConfigUncached(); + const promise = resolveAuthBrokerConfigShared({ + agentDir: getAgentDir(), + configValueResolver: resolveConfigValue, + }); cachedConfigKey = key; cachedConfigPromise = promise; promise.catch(() => { @@ -100,32 +71,21 @@ export function resolveAuthBrokerConfig(): Promise { - const envUrl = process.env.OMP_AUTH_BROKER_URL; - const envToken = process.env.OMP_AUTH_BROKER_TOKEN; - - let url = envUrl && envUrl.length > 0 ? envUrl : undefined; - let configToken: string | undefined; - if (!url || !envToken) { - const fromConfig = await readConfigYaml(); - if (!url && fromConfig.url) { - const resolved = await resolveConfigValue(fromConfig.url); - if (resolved && resolved.length > 0) url = resolved; - } - if (fromConfig.token) { - const resolved = await resolveConfigValue(fromConfig.token); - if (resolved && resolved.length > 0) configToken = resolved; - } - } - if (!url) return null; - - const token = - (envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined; - if (!token) { - throw new Error( - `OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` + - `Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`, - ); - } - return { url, token }; +/** + * Create an AuthStorage instance, using the broker when configured and falling + * back to the local SQLite store otherwise. Delegates to the shared resolver in + * pi-ai so the CLI, subagents, and the catalog generator all see the same + * credentials. + * + * Default `agentDir` is the current configured agent directory. + */ +export function discoverAuthStorage( + agentDir: string = getAgentDir(), + options?: Omit, +): ReturnType { + return discoverAuthStorageShared({ + ...options, + agentDir, + configValueResolver: resolveConfigValue, + }); }