fix(gateway): refresh model availability from persisted auth (#102289)

This commit is contained in:
Jason (Json)
2026-07-08 14:45:09 -06:00
committed by GitHub
parent b81666ca6a
commit 5b52e2490a
2 changed files with 87 additions and 16 deletions
@@ -8,7 +8,7 @@ import {
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import {
ensureAuthProfileStoreWithoutExternalProfiles,
loadAuthProfileStoreWithoutExternalProfiles,
resolveAuthProfileOrder,
type AuthProfileCredential,
type AuthProfileStore,
@@ -170,10 +170,10 @@ function createModelsListProviderAuthChecker(params: {
workspaceDir: string;
}): ModelsListProviderAuthChecker {
const agentDir = resolveAgentDir(params.cfg, params.agentId);
const store = ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
// Auth refreshes can be persisted by another CLI process while the gateway
// keeps an older execution snapshot, so browse availability reads SQLite.
const store = loadAuthProfileStoreWithoutExternalProfiles(agentDir, {
allowKeychainPrompt: false,
readOnly: true,
syncExternalCli: false,
});
return createInFlightProviderAuthChecker(
(provider, modelApi) =>
+83 -12
View File
@@ -2,6 +2,10 @@
// validation errors, and protocol response shapes.
import { describe, expect, it, vi } from "vitest";
import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
replaceRuntimeAuthProfileStoreSnapshots,
} from "../../agents/auth-profiles.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { createDeferred } from "../../test-utils/deferred.js";
import { withEnvAsync } from "../../test-utils/env.js";
@@ -21,6 +25,21 @@ const withoutOpenAIEnvAuth = async <T>(run: () => Promise<T>): Promise<T> =>
run,
);
function createDemoOAuthStore(params: { access: string; expires: number }) {
return {
version: 1 as const,
profiles: {
"demo-provider:oauth": {
type: "oauth" as const,
provider: "demo-provider",
access: params.access,
refresh: "refresh-token",
expires: params.expires,
},
},
};
}
function requestModelsList(params: {
view: "configured" | "all";
respond?: ReturnType<typeof vi.fn>;
@@ -488,18 +507,12 @@ describe("models.list", () => {
agentEnv: "main",
},
async (state) => {
await state.writeAuthProfiles({
version: 1,
profiles: {
"demo-provider:expired": {
type: "oauth",
provider: "demo-provider",
access: "expired-access",
refresh: "refresh-token",
expires: Date.now() - 60_000,
},
},
});
await state.writeAuthProfiles(
createDemoOAuthStore({
access: "expired-access",
expires: Date.now() - 60_000,
}),
);
const { request, respond } = requestModelsList({
view: "all",
@@ -528,6 +541,64 @@ describe("models.list", () => {
);
});
it("uses refreshed persisted OAuth when the runtime auth snapshot is stale", async () => {
await withOpenClawTestState(
{
layout: "state-only",
prefix: "openclaw-models-list-stale-runtime-profile-",
agentEnv: "main",
},
async (state) => {
const agentDir = state.agentDir();
await state.writeAuthProfiles(
createDemoOAuthStore({
access: "refreshed-access",
expires: Date.now() + 60 * 60_000,
}),
);
replaceRuntimeAuthProfileStoreSnapshots([
{
agentDir,
store: createDemoOAuthStore({
access: "expired-access",
expires: Date.now() - 60_000,
}),
},
]);
try {
const { request, respond } = requestModelsList({
view: "all",
loadGatewayModelCatalog: vi.fn(() =>
Promise.resolve([
{ id: "demo-model", name: "Demo Model", provider: "demo-provider" },
]),
),
reqId: "req-models-list-stale-runtime-profile",
});
await request;
expect(respond).toHaveBeenCalledWith(
true,
{
models: [
{
id: "demo-model",
name: "Demo Model",
provider: "demo-provider",
available: true,
},
],
},
undefined,
);
} finally {
clearRuntimeAuthProfileStoreSnapshots();
}
},
);
});
it("marks env SecretRef-backed auth profiles available", async () => {
await withOpenClawTestState(
{