diff --git a/src/gateway/server-methods/models-list-result.ts b/src/gateway/server-methods/models-list-result.ts index 2ba9df816b89..26b0cc8b27f2 100644 --- a/src/gateway/server-methods/models-list-result.ts +++ b/src/gateway/server-methods/models-list-result.ts @@ -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 { - 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, }), ), ); diff --git a/src/gateway/server-methods/models.test.ts b/src/gateway/server-methods/models.test.ts index 155d3ba63a86..e4b33e05e909 100644 --- a/src/gateway/server-methods/models.test.ts +++ b/src/gateway/server-methods/models.test.ts @@ -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",