fix(gateway): avoid resolving auth during models list

This commit is contained in:
Ayaan Zaidi
2026-06-05 16:09:45 +05:30
parent e404ce98f5
commit cec5e36a39
2 changed files with 95 additions and 14 deletions

View File

@@ -7,10 +7,15 @@ import {
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { ensureAuthProfileStoreWithoutExternalProfiles } from "../../agents/auth-profiles.js";
import {
ensureAuthProfileStoreWithoutExternalProfiles,
resolveAuthProfileOrder,
type AuthProfileCredential,
type AuthProfileStore,
} from "../../agents/auth-profiles.js";
import { DEFAULT_PROVIDER } from "../../agents/defaults.js";
import { NON_ENV_SECRETREF_MARKER } from "../../agents/model-auth-markers.js";
import { hasAvailableAuthForProvider } from "../../agents/model-auth.js";
import { hasRuntimeAvailableProviderAuth } from "../../agents/model-auth.js";
import {
loadModelCatalogForBrowse,
type ModelCatalogBrowseView,
@@ -30,6 +35,7 @@ type ModelsListView = ModelCatalogBrowseView;
type ModelsListEntry = ModelCatalogEntry & { available?: boolean };
let loggedSlowModelsListCatalog = false;
const OAUTH_REFRESH_MARGIN_MS = 5 * 60 * 1000;
// Unknown views are rejected by protocol validation first; this helper keeps the
// handler default explicit for older clients that omit the field.
@@ -74,6 +80,72 @@ function createInFlightProviderAuthChecker(
};
}
function hasLiteralSecret(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function profileModeAllowedForModel(
provider: string,
modelApi: string | undefined,
mode: AuthProfileCredential["type"],
): boolean {
return (
normalizeProviderId(provider) !== "openai" ||
modelApi === undefined ||
modelApi === "openai-chatgpt-responses" ||
mode === "api_key"
);
}
function profileHasReadOnlyAvailableAuth(params: {
credential: AuthProfileCredential;
provider: string;
modelApi?: string;
now: number;
}): boolean {
if (!profileModeAllowedForModel(params.provider, params.modelApi, params.credential.type)) {
return false;
}
if (params.credential.type === "api_key") {
return hasLiteralSecret(params.credential.key);
}
if (params.credential.type === "token") {
return (
hasLiteralSecret(params.credential.token) &&
(params.credential.expires === undefined || params.credential.expires > params.now)
);
}
return (
hasLiteralSecret(params.credential.access) &&
params.credential.expires > params.now + OAUTH_REFRESH_MARGIN_MS
);
}
function hasReadOnlyAvailableProfileAuth(params: {
provider: string;
modelApi?: string;
cfg: OpenClawConfig;
store: AuthProfileStore;
}): boolean {
const now = Date.now();
return resolveAuthProfileOrder({
cfg: params.cfg,
store: params.store,
provider: params.provider,
}).some((profileId) => {
const credential = params.store.profiles[profileId];
return (
credential !== undefined &&
profileHasReadOnlyAvailableAuth({
credential,
provider: params.provider,
modelApi: params.modelApi,
now,
})
);
});
}
function createModelsListProviderAuthChecker(params: {
cfg: OpenClawConfig;
agentId: string;
@@ -82,16 +154,24 @@ function createModelsListProviderAuthChecker(params: {
const agentDir = resolveAgentDir(params.cfg, params.agentId);
const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
allowKeychainPrompt: false,
readOnly: true,
syncExternalCli: false,
});
return createInFlightProviderAuthChecker((provider, modelApi) =>
hasAvailableAuthForProvider({
provider,
modelApi,
cfg: params.cfg,
agentDir,
workspaceDir: params.workspaceDir,
store,
}),
return createInFlightProviderAuthChecker(
(provider, modelApi) =>
hasRuntimeAvailableProviderAuth({
provider,
modelApi,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
allowPluginSyntheticAuth: false,
}) ||
hasReadOnlyAvailableProfileAuth({
provider,
modelApi,
cfg: params.cfg,
store,
}),
);
}

View File

@@ -333,7 +333,7 @@ describe("models.list", () => {
);
});
it("does not mark catalog rows available from expired provider profiles", async () => {
it("does not mark catalog rows available from expired OAuth profiles", async () => {
await withOpenClawTestState(
{
layout: "state-only",
@@ -345,9 +345,10 @@ describe("models.list", () => {
version: 1,
profiles: {
"demo-provider:expired": {
type: "token",
type: "oauth",
provider: "demo-provider",
token: "expired-token",
access: "expired-access",
refresh: "refresh-token",
expires: Date.now() - 60_000,
},
},