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, resolveAgentWorkspaceDir,
resolveDefaultAgentId, resolveDefaultAgentId,
} from "../../agents/agent-scope.js"; } 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 { DEFAULT_PROVIDER } from "../../agents/defaults.js";
import { NON_ENV_SECRETREF_MARKER } from "../../agents/model-auth-markers.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 { import {
loadModelCatalogForBrowse, loadModelCatalogForBrowse,
type ModelCatalogBrowseView, type ModelCatalogBrowseView,
@@ -30,6 +35,7 @@ type ModelsListView = ModelCatalogBrowseView;
type ModelsListEntry = ModelCatalogEntry & { available?: boolean }; type ModelsListEntry = ModelCatalogEntry & { available?: boolean };
let loggedSlowModelsListCatalog = false; let loggedSlowModelsListCatalog = false;
const OAUTH_REFRESH_MARGIN_MS = 5 * 60 * 1000;
// Unknown views are rejected by protocol validation first; this helper keeps the // Unknown views are rejected by protocol validation first; this helper keeps the
// handler default explicit for older clients that omit the field. // 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: { function createModelsListProviderAuthChecker(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
agentId: string; agentId: string;
@@ -82,16 +154,24 @@ function createModelsListProviderAuthChecker(params: {
const agentDir = resolveAgentDir(params.cfg, params.agentId); const agentDir = resolveAgentDir(params.cfg, params.agentId);
const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
allowKeychainPrompt: false, allowKeychainPrompt: false,
readOnly: true,
syncExternalCli: false,
}); });
return createInFlightProviderAuthChecker((provider, modelApi) => return createInFlightProviderAuthChecker(
hasAvailableAuthForProvider({ (provider, modelApi) =>
provider, hasRuntimeAvailableProviderAuth({
modelApi, provider,
cfg: params.cfg, modelApi,
agentDir, cfg: params.cfg,
workspaceDir: params.workspaceDir, workspaceDir: params.workspaceDir,
store, 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( await withOpenClawTestState(
{ {
layout: "state-only", layout: "state-only",
@@ -345,9 +345,10 @@ describe("models.list", () => {
version: 1, version: 1,
profiles: { profiles: {
"demo-provider:expired": { "demo-provider:expired": {
type: "token", type: "oauth",
provider: "demo-provider", provider: "demo-provider",
token: "expired-token", access: "expired-access",
refresh: "refresh-token",
expires: Date.now() - 60_000, expires: Date.now() - 60_000,
}, },
}, },