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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from "./client";
|
||||
export * from "./discover";
|
||||
export * from "./refresher";
|
||||
export * from "./remote-store";
|
||||
export * from "./server";
|
||||
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user