feat: added auth discovery broker and expand model support

- Introduced a centralized `discoverAuthStorage` mechanism across packages to unify credential retrieval and configuration resolution.
- Added support for new Gemini and Moonshot model variants while updating context window and effort configuration for existing models.
- Resolved provider-specific 400 errors for OpenRouter and GLM models by refining reasoning effort mapping and retry logic.
- Standardized credential management in both the coding-agent and model catalog by migrating to the unified authentication broker.
This commit is contained in:
can1357
2026-06-19 14:52:26 +02:00
parent f53daec09f
commit 0dfeac8a75
12 changed files with 762 additions and 240 deletions
+6
View File
@@ -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
+217
View File
@@ -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<string | undefined>;
}
export interface DiscoverAuthStorageOptions {
agentDir?: string;
configValueResolver?: (config: string) => Promise<string | undefined>;
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<string | undefined> {
if (config.startsWith("!")) return undefined;
const envValue = process.env[config];
return envValue || config;
}
async function readTokenFile(): Promise<string | null> {
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<ConfigSnapshot> {
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<string, unknown>;
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 `<agentDir>/config.yml`.
* 3. `<config-root>/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<AuthBrokerClientConfig | null> {
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<AuthStorage> {
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;
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./client";
export * from "./discover";
export * from "./refresher";
export * from "./remote-store";
export * from "./server";
+16 -1
View File
@@ -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.
+25 -14
View File
@@ -10,7 +10,8 @@ const COPILOT_PREMIUM_MULTIPLIERS: Record<string, number> = {
};
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<OAuthAccess | null> {
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<OAuth
async function fetchAntigravityModels(): Promise<ModelSpec<"google-gemini-cli">[]> {
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=<name>.");
return [];
}
try {
@@ -404,6 +413,8 @@ function extractCodexAccountId(accessToken: string): string | null {
async function fetchCodexDiscoveryModels(): Promise<ModelSpec<"openai-codex-responses">[]> {
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=<name>.");
return [];
}
try {
+31 -10
View File
@@ -161,7 +161,7 @@ function fillThinkingWireDefaults<TApi extends Api>(
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<TApi extends Api>(
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<TApi extends Api>(spec: ModelSpec<TApi>): readonly Effort[] | undefined {
function getModelDefinedEfforts<TApi extends Api>(
spec: ModelSpec<TApi>,
compat: CompatOf<TApi>,
): 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<Api>): EffortMap | undefined {
return map && Object.keys(map).length > 0 ? map : undefined;
}
function isOpenRouterThinkingFormat(compat: CompatOf<Api>): boolean {
return compat !== undefined && "thinkingFormat" in compat && compat.thinkingFormat === "openrouter";
}
function inferDetectedEffortMap<TApi extends Api>(
spec: ModelSpec<TApi>,
compat: CompatOf<TApi>,
parsedModel: ParsedModel,
mode: ThinkingConfig["mode"],
): EffortMap | undefined {
@@ -324,12 +339,18 @@ function inferDetectedEffortMap<TApi extends Api>(
? 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<TApi extends Api>(
spec: ModelSpec<TApi>,
compat: CompatOf<TApi>,
): readonly Effort[] {
const modelDefinedEfforts = getModelDefinedEfforts(spec);
const modelDefinedEfforts = getModelDefinedEfforts(spec, compat);
if (modelDefinedEfforts !== undefined) {
return modelDefinedEfforts;
}
+376 -35
View File
@@ -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",
+21 -3
View File
@@ -76,6 +76,13 @@ export interface EffortVariantFamily {
thinking: Readonly<Omit<ThinkingConfig, "effortRouting" | "suppressWhenOff">>;
/** 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<TSpec extends VariantSpecLike>(
const routing: Partial<Record<Effort | "off", string>> = {};
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<TSpec extends VariantSpecLike>(
// 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;
}
@@ -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);
+4 -1
View File
@@ -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.
+11 -100
View File
@@ -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<AuthStorage> {
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<AuthStorage> {
return discoverAuthStorageFromConfig(agentDir);
}
/**
@@ -799,7 +710,7 @@ export async function discoverContextFiles(
export async function discoverPromptTemplates(cwd?: string, agentDir?: string): Promise<PromptTemplate[]> {
return await loadPromptTemplatesInternal({
cwd: cwd ?? getProjectDir(),
agentDir: agentDir ?? getDefaultAgentDir(),
agentDir: agentDir ?? getAgentDir(),
});
}
@@ -815,7 +726,7 @@ export async function discoverSlashCommands(cwd?: string): Promise<FileSlashComm
*/
export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise<CustomCommandsLoadResult> {
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<CreateAgentSessionResult> {
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getDefaultAgentDir();
const agentDir = options.agentDir ?? getAgentDir();
const eventBus = options.eventBus ?? new EventBus();
registerSshCleanup();
@@ -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<string | null> {
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<ConfigSnapshot> {
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<string, unknown>;
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<AuthBrokerClientConfig | null> | null = null;
export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null> {
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<AuthBrokerClientConfig | null
return promise;
}
async function resolveAuthBrokerConfigUncached(): Promise<AuthBrokerClientConfig | null> {
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<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">,
): ReturnType<typeof discoverAuthStorageShared> {
return discoverAuthStorageShared({
...options,
agentDir,
configValueResolver: resolveConfigValue,
});
}