mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:18:28 +00:00
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:
@@ -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",
|
||||
|
||||
16
src/commands/models/list.errors.ts
Normal file
16
src/commands/models/list.errors.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user