fix(models): antigravity opus 4.6 availability follow-up (#12845)

* fix(models): antigravity opus 4.6 availability follow-up

* chore(format): apply updated oxfmt config to models files

* fix(models): retain zai glm-5 forward-compat fallback after extraction

* chore(format): apply updated oxfmt config

* fix(models): fail fast on unknown auth login provider

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vincent Koc
2026-02-13 15:54:46 -08:00
committed by GitHub
parent 23e8f3a20a
commit a0cbf9002d
10 changed files with 1853 additions and 343 deletions

View File

@@ -26,8 +26,6 @@ import { isRemoteEnvironment } from "../oauth-env.js";
import { createVpsAwareOAuthHandlers } from "../oauth-flow.js";
import { applyAuthProfileConfig } from "../onboard-auth.js";
import { openUrl } from "../onboard-helpers.js";
import { OPENAI_CODEX_DEFAULT_MODEL } from "../openai-codex-model-default.js";
import { loginOpenAICodexOAuth } from "../openai-codex-oauth.js";
import { updateConfig } from "./shared.js";
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
@@ -260,6 +258,28 @@ function resolveProviderMatch(
);
}
export function resolveRequestedLoginProviderOrThrow(
providers: ProviderPlugin[],
rawProvider?: string,
): ProviderPlugin | null {
const requested = rawProvider?.trim();
if (!requested) {
return null;
}
const matched = resolveProviderMatch(providers, requested);
if (matched) {
return matched;
}
const available = providers
.map((provider) => provider.id)
.filter(Boolean)
.toSorted((a, b) => a.localeCompare(b));
const availableText = available.length > 0 ? available.join(", ") : "(none)";
throw new Error(
`Unknown provider "${requested}". Loaded providers: ${availableText}. Verify plugins via \`${formatCliCommand("openclaw plugins list --json")}\`.`,
);
}
function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null {
const raw = rawMethod?.trim();
if (!raw) {
@@ -344,59 +364,6 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
const workspaceDir =
resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir();
const prompter = createClackPrompter();
const requestedProvider = opts.provider ? normalizeProviderId(opts.provider) : null;
if (requestedProvider === "openai-codex") {
const method = opts.method?.trim().toLowerCase();
if (method && method !== "oauth") {
throw new Error('OpenAI Codex auth only supports --method "oauth".');
}
const creds = await loginOpenAICodexOAuth({
prompter,
runtime,
isRemote: isRemoteEnvironment(),
openUrl: async (url) => {
await openUrl(url);
},
});
if (!creds) {
return;
}
const profileId = "openai-codex:default";
upsertAuthProfile({
profileId,
credential: {
type: "oauth",
provider: "openai-codex",
...creds,
},
agentDir,
});
await updateConfig((cfg) => {
let next = applyAuthProfileConfig(cfg, {
profileId,
provider: "openai-codex",
mode: "oauth",
});
if (opts.setDefault) {
next = applyDefaultModel(next, OPENAI_CODEX_DEFAULT_MODEL);
}
return next;
});
logConfigUpdated(runtime);
runtime.log(`Auth profile: ${profileId} (openai-codex/oauth)`);
runtime.log(
opts.setDefault
? `Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`
: `Default model available: ${OPENAI_CODEX_DEFAULT_MODEL} (use --set-default to apply)`,
);
return;
}
const providers = resolvePluginProviders({ config, workspaceDir });
if (providers.length === 0) {
throw new Error(
@@ -404,8 +371,10 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
);
}
const prompter = createClackPrompter();
const requestedProvider = resolveRequestedLoginProviderOrThrow(providers, opts.provider);
const selectedProvider =
resolveProviderMatch(providers, opts.provider) ??
requestedProvider ??
(await prompter
.select({
message: "Select a provider",

View File

@@ -0,0 +1,16 @@
export const MODEL_AVAILABILITY_UNAVAILABLE_CODE = "MODEL_AVAILABILITY_UNAVAILABLE";
export function formatErrorWithStack(err: unknown): string {
if (err instanceof Error) {
return err.stack ?? `${err.name}: ${err.message}`;
}
return String(err);
}
export function shouldFallbackToAuthHeuristics(err: unknown): boolean {
if (!(err instanceof Error)) {
return false;
}
const code = (err as { code?: unknown }).code;
return code === MODEL_AVAILABILITY_UNAVAILABLE_CODE;
}

View File

@@ -3,9 +3,9 @@ import type { RuntimeEnv } from "../../runtime.js";
import type { ModelRow } from "./list.types.js";
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
import { parseModelRef } from "../../agents/model-selection.js";
import { resolveModel } from "../../agents/pi-embedded-runner/model.js";
import { loadConfig } from "../../config/config.js";
import { resolveConfiguredEntries } from "./list.configured.js";
import { formatErrorWithStack } from "./list.errors.js";
import { loadModelRegistry, toModelRow } from "./list.registry.js";
import { printModelTable } from "./list.table.js";
import { DEFAULT_PROVIDER, ensureFlagCompatibility, modelKey } from "./shared.js";
@@ -34,12 +34,21 @@ export async function modelsListCommand(
let models: Model<Api>[] = [];
let availableKeys: Set<string> | undefined;
let availabilityErrorMessage: string | undefined;
try {
const loaded = await loadModelRegistry(cfg);
models = loaded.models;
availableKeys = loaded.availableKeys;
availabilityErrorMessage = loaded.availabilityErrorMessage;
} catch (err) {
runtime.error(`Model registry unavailable: ${String(err)}`);
runtime.error(`Model registry unavailable:\n${formatErrorWithStack(err)}`);
process.exitCode = 1;
return;
}
if (availabilityErrorMessage !== undefined) {
runtime.error(
`Model availability lookup failed; falling back to auth heuristics for discovered models: ${availabilityErrorMessage}`,
);
}
const modelByKey = new Map(models.map((model) => [modelKey(model.provider, model.id), model]));
@@ -100,13 +109,7 @@ export async function modelsListCommand(
if (providerFilter && entry.ref.provider.toLowerCase() !== providerFilter) {
continue;
}
let model = modelByKey.get(entry.key);
if (!model) {
const resolved = resolveModel(entry.ref.provider, entry.ref.model, undefined, cfg);
if (resolved.model && !resolved.error) {
model = resolved.model;
}
}
const model = modelByKey.get(entry.key);
if (opts.local && model && !isLocalBaseUrl(model.baseUrl)) {
continue;
}

View File

@@ -1,5 +1,6 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import type { AuthProfileStore } from "../../agents/auth-profiles.js";
import type { ModelRegistry } from "../../agents/pi-model-discovery.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { ModelRow } from "./list.types.js";
import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
@@ -9,9 +10,14 @@ import {
resolveAwsSdkEnvVarName,
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import { resolveForwardCompatModel } from "../../agents/model-forward-compat.js";
import { ensureOpenClawModelsJson } from "../../agents/models-config.js";
import { ensurePiAuthJsonFromAuthProfiles } from "../../agents/pi-auth-json.js";
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
import {
formatErrorWithStack,
MODEL_AVAILABILITY_UNAVAILABLE_CODE,
shouldFallbackToAuthHeuristics,
} from "./list.errors.js";
import { modelKey } from "./shared.js";
const isLocalBaseUrl = (baseUrl: string) => {
@@ -30,7 +36,14 @@ const isLocalBaseUrl = (baseUrl: string) => {
}
};
const hasAuthForProvider = (provider: string, cfg: OpenClawConfig, authStore: AuthProfileStore) => {
const hasAuthForProvider = (
provider: string,
cfg?: OpenClawConfig,
authStore?: AuthProfileStore,
) => {
if (!cfg || !authStore) {
return false;
}
if (listProfilesForProvider(authStore, provider).length > 0) {
return true;
}
@@ -46,16 +59,150 @@ const hasAuthForProvider = (provider: string, cfg: OpenClawConfig, authStore: Au
return false;
};
function createAvailabilityUnavailableError(message: string): Error {
const err = new Error(message);
(err as { code?: string }).code = MODEL_AVAILABILITY_UNAVAILABLE_CODE;
return err;
}
function normalizeAvailabilityError(err: unknown): Error {
if (shouldFallbackToAuthHeuristics(err) && err instanceof Error) {
return err;
}
return createAvailabilityUnavailableError(
`Model availability unavailable: getAvailable() failed.\n${formatErrorWithStack(err)}`,
);
}
function validateAvailableModels(availableModels: unknown): Model<Api>[] {
if (!Array.isArray(availableModels)) {
throw createAvailabilityUnavailableError(
"Model availability unavailable: getAvailable() returned a non-array value.",
);
}
for (const model of availableModels) {
if (
!model ||
typeof model !== "object" ||
typeof (model as { provider?: unknown }).provider !== "string" ||
typeof (model as { id?: unknown }).id !== "string"
) {
throw createAvailabilityUnavailableError(
"Model availability unavailable: getAvailable() returned invalid model entries.",
);
}
}
return availableModels as Model<Api>[];
}
function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
let availableModels: unknown;
try {
availableModels = registry.getAvailable();
} catch (err) {
throw normalizeAvailabilityError(err);
}
try {
return validateAvailableModels(availableModels);
} catch (err) {
throw normalizeAvailabilityError(err);
}
}
export async function loadModelRegistry(cfg: OpenClawConfig) {
await ensureOpenClawModelsJson(cfg);
const agentDir = resolveOpenClawAgentDir();
await ensurePiAuthJsonFromAuthProfiles(agentDir);
const authStorage = discoverAuthStorage(agentDir);
const registry = discoverModels(authStorage, agentDir);
const models = registry.getAll();
const availableModels = registry.getAvailable();
const availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id)));
return { registry, models, availableKeys };
const appended = appendAntigravityForwardCompatModels(registry.getAll(), registry);
const models = appended.models;
const synthesizedForwardCompat = appended.synthesizedForwardCompat;
let availableKeys: Set<string> | undefined;
let availabilityErrorMessage: string | undefined;
try {
const availableModels = loadAvailableModels(registry);
availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id)));
for (const synthesized of synthesizedForwardCompat) {
if (hasAvailableTemplate(availableKeys, synthesized.templatePrefixes)) {
availableKeys.add(synthesized.key);
}
}
} catch (err) {
if (!shouldFallbackToAuthHeuristics(err)) {
throw err;
}
// Some providers can report model-level availability as unavailable.
// Fall back to provider-level auth heuristics when availability is undefined.
availableKeys = undefined;
if (!availabilityErrorMessage) {
availabilityErrorMessage = formatErrorWithStack(err);
}
}
return { registry, models, availableKeys, availabilityErrorMessage };
}
type SynthesizedForwardCompat = {
key: string;
templatePrefixes: string[];
};
function appendAntigravityForwardCompatModels(
models: Model<Api>[],
modelRegistry: ModelRegistry,
): { models: Model<Api>[]; synthesizedForwardCompat: SynthesizedForwardCompat[] } {
const candidates = [
{
id: "claude-opus-4-6-thinking",
templatePrefixes: [
"google-antigravity/claude-opus-4-5-thinking",
"google-antigravity/claude-opus-4.5-thinking",
],
},
{
id: "claude-opus-4-6",
templatePrefixes: [
"google-antigravity/claude-opus-4-5",
"google-antigravity/claude-opus-4.5",
],
},
];
const nextModels = [...models];
const synthesizedForwardCompat: SynthesizedForwardCompat[] = [];
for (const candidate of candidates) {
const key = modelKey("google-antigravity", candidate.id);
const hasForwardCompat = nextModels.some((model) => modelKey(model.provider, model.id) === key);
if (hasForwardCompat) {
continue;
}
const fallback = resolveForwardCompatModel("google-antigravity", candidate.id, modelRegistry);
if (!fallback) {
continue;
}
nextModels.push(fallback);
synthesizedForwardCompat.push({
key,
templatePrefixes: candidate.templatePrefixes,
});
}
return { models: nextModels, synthesizedForwardCompat };
}
function hasAvailableTemplate(availableKeys: Set<string>, templatePrefixes: string[]): boolean {
for (const key of availableKeys) {
if (templatePrefixes.some((prefix) => key.startsWith(prefix))) {
return true;
}
}
return false;
}
export function toModelRow(params: {
@@ -83,10 +230,14 @@ export function toModelRow(params: {
const input = model.input.join("+") || "text";
const local = isLocalBaseUrl(model.baseUrl);
// Prefer model-level registry availability when present.
// Fall back to provider-level auth heuristics only if registry availability isn't available.
const available =
cfg && authStore
? hasAuthForProvider(model.provider, cfg, authStore)
: (availableKeys?.has(modelKey(model.provider, model.id)) ?? false);
availableKeys !== undefined
? availableKeys.has(modelKey(model.provider, model.id))
: cfg && authStore
? hasAuthForProvider(model.provider, cfg, authStore)
: false;
const aliasTags = aliases.length > 0 ? [`alias:${aliases.join(",")}`] : [];
const mergedTags = new Set(tags);
if (aliasTags.length > 0) {