fix(gateway): require resolved auth for model availability

This commit is contained in:
Ayaan Zaidi
2026-06-05 16:02:32 +05:30
parent 30160933f0
commit e404ce98f5
2 changed files with 74 additions and 11 deletions

View File

@@ -2,12 +2,15 @@
// strips runtime-only provider params before sending the browse API payload.
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import {
resolveAgentDir,
resolveAgentEffectiveModelPrimary,
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { ensureAuthProfileStoreWithoutExternalProfiles } 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 {
loadModelCatalogForBrowse,
type ModelCatalogBrowseView,
@@ -18,7 +21,6 @@ import {
resolveVisibleModelCatalog,
} from "../../agents/model-catalog-visibility.js";
import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js";
import { createProviderAuthChecker } from "../../agents/model-provider-auth.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { isSecretRef } from "../../config/types.secrets.js";
@@ -72,6 +74,27 @@ function createInFlightProviderAuthChecker(
};
}
function createModelsListProviderAuthChecker(params: {
cfg: OpenClawConfig;
agentId: string;
workspaceDir: string;
}): ProviderAuthChecker {
const agentDir = resolveAgentDir(params.cfg, params.agentId);
const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
allowKeychainPrompt: false,
});
return createInFlightProviderAuthChecker((provider, modelApi) =>
hasAvailableAuthForProvider({
provider,
modelApi,
cfg: params.cfg,
agentDir,
workspaceDir: params.workspaceDir,
store,
}),
);
}
async function buildPublicModelsListEntry(params: {
entry: ModelCatalogEntry;
cfg: OpenClawConfig;
@@ -96,21 +119,13 @@ async function buildPublicModelsListEntries(params: {
agentId: string;
workspaceDir: string;
}): Promise<ModelsListEntry[]> {
const inFlightProviderAuthChecker = createInFlightProviderAuthChecker(
createProviderAuthChecker({
cfg: params.cfg,
agentId: params.agentId,
workspaceDir: params.workspaceDir,
allowPluginSyntheticAuth: false,
discoverExternalCliAuth: false,
}),
);
const providerAuthChecker = createModelsListProviderAuthChecker(params);
return await Promise.all(
params.catalog.map((entry) =>
buildPublicModelsListEntry({
entry,
cfg: params.cfg,
providerAuthChecker: inFlightProviderAuthChecker,
providerAuthChecker,
}),
),
);

View File

@@ -3,6 +3,7 @@
import { describe, expect, it, vi } from "vitest";
import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js";
import { createDeferred } from "../test-helpers.deferred.js";
import { expectGatewayErrorResponse } from "./gateway-response.test-helpers.js";
import { modelsHandlers } from "./models.js";
@@ -332,6 +333,53 @@ describe("models.list", () => {
);
});
it("does not mark catalog rows available from expired provider profiles", async () => {
await withOpenClawTestState(
{
layout: "state-only",
prefix: "openclaw-models-list-expired-profile-",
agentEnv: "main",
},
async (state) => {
await state.writeAuthProfiles({
version: 1,
profiles: {
"demo-provider:expired": {
type: "token",
provider: "demo-provider",
token: "expired-token",
expires: Date.now() - 60_000,
},
},
});
const { request, respond } = requestModelsList({
view: "all",
loadGatewayModelCatalog: vi.fn(() =>
Promise.resolve([{ id: "demo-model", name: "Demo Model", provider: "demo-provider" }]),
),
reqId: "req-models-list-expired-profile",
});
await request;
expect(respond).toHaveBeenCalledWith(
true,
{
models: [
{
id: "demo-model",
name: "Demo Model",
provider: "demo-provider",
available: false,
},
],
},
undefined,
);
},
);
});
it("preserves catalog load errors before the timeout fallback wins", async () => {
const { request, respond } = requestModelsList({
view: "configured",