mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(models): unify full catalog auth discovery
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "./plugin-model-catalog.js";
|
||||
import {
|
||||
createPreparedModelCatalogWorkerInput,
|
||||
getPreparedModelFullCatalogAuth,
|
||||
runPreparedModelCatalogWorker,
|
||||
} from "./prepared-model-catalog-worker.js";
|
||||
import { copyPreparedModelRuntimeAuthState } from "./prepared-model-runtime-auth.js";
|
||||
@@ -37,6 +38,7 @@ const REF_ONLY_API_ENV = "OPENCLAW_WORKER_REF_ONLY_API_KEY";
|
||||
const REF_ONLY_TOKEN_PROVIDER_ID = `${PROVIDER_ID}-ref-token`;
|
||||
const REF_ONLY_TOKEN_ENV = "OPENCLAW_WORKER_REF_ONLY_TOKEN";
|
||||
const DURABLE_AUTH_PROVIDER_ID = `${PROVIDER_ID}-durable-auth`;
|
||||
const DURABLE_AUTH_KEY = "post-startup-durable-key-not-real";
|
||||
const tempDirs = useAutoCleanupTempDirTracker((cleanup) => {
|
||||
afterEach(() => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
@@ -68,6 +70,7 @@ module.exports = {
|
||||
run(context) {
|
||||
const refOnlyApi = context.resolveProviderApiKey(${JSON.stringify(REF_ONLY_API_PROVIDER_ID)}).apiKey;
|
||||
const refOnlyToken = context.resolveProviderApiKey(${JSON.stringify(REF_ONLY_TOKEN_PROVIDER_ID)}).apiKey;
|
||||
const durableAuth = context.resolveProviderApiKey(${JSON.stringify(DURABLE_AUTH_PROVIDER_ID)}).apiKey;
|
||||
const hasRefOnlyApi = refOnlyApi === ${JSON.stringify(REF_ONLY_API_ENV)} || refOnlyApi === process.env[${JSON.stringify(REF_ONLY_API_ENV)}];
|
||||
const hasRefOnlyToken = refOnlyToken === ${JSON.stringify(REF_ONLY_TOKEN_ENV)} || refOnlyToken === process.env[${JSON.stringify(REF_ONLY_TOKEN_ENV)}];
|
||||
return { provider: {
|
||||
@@ -79,6 +82,9 @@ module.exports = {
|
||||
id: \`ref-proof-api-\${hasRefOnlyApi}-token-\${hasRefOnlyToken}\`,
|
||||
name: "Ref-only worker proof",
|
||||
},
|
||||
...(durableAuth === ${JSON.stringify(DURABLE_AUTH_KEY)}
|
||||
? [{ id: "post-startup-auth-model", name: "Post-startup auth model" }]
|
||||
: []),
|
||||
],
|
||||
} };
|
||||
},
|
||||
@@ -211,6 +217,42 @@ async function waitForMarker(marker: string): Promise<void> {
|
||||
}
|
||||
|
||||
describe("prepared model catalog worker boundary", () => {
|
||||
it("refreshes durable auth before provider hooks decide catalog membership", async () => {
|
||||
const fixture = await createStaticSnapshot(0);
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[`${DURABLE_AUTH_PROVIDER_ID}:default`]: {
|
||||
type: "api_key",
|
||||
provider: DURABLE_AUTH_PROVIDER_ID,
|
||||
key: DURABLE_AUTH_KEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
fixture.agentDir,
|
||||
);
|
||||
|
||||
const catalog = await fixture.snapshot.loadFullModelCatalog?.();
|
||||
expect(catalog?.entries).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
provider: PROVIDER_ID,
|
||||
id: "post-startup-auth-model",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(getPreparedModelFullCatalogAuth(catalog!)).toMatchObject({
|
||||
authStore: {
|
||||
profiles: {
|
||||
[`${DURABLE_AUTH_PROVIDER_ID}:default`]: expect.objectContaining({
|
||||
key: DURABLE_AUTH_KEY,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes durable auth profiles added, updated, and removed after startup", async () => {
|
||||
const fixture = await createStaticSnapshot(0);
|
||||
const route = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { resolveInstalledManifestRegistryIndexFingerprint } from "../plugins/manifest-registry-installed.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { PreparedAgentCredentialModes } from "./agent-auth-credential-modes.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import type { ModelCatalogSnapshot } from "./model-catalog.types.js";
|
||||
import { PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors.js";
|
||||
@@ -13,14 +14,12 @@ import {
|
||||
type PreparedModelRuntimeAgentFacts,
|
||||
} from "./prepared-model-runtime.facts.js";
|
||||
import type { PreparedModelRuntimeInput } from "./prepared-model-runtime.types.js";
|
||||
import type { AuthStorageData } from "./sessions/auth-storage.js";
|
||||
|
||||
export type PreparedModelCatalogWorkerInput = Readonly<{
|
||||
kind: "catalog";
|
||||
generationFingerprint: string;
|
||||
input: PreparedModelRuntimeInput;
|
||||
authStore: AuthProfileStore;
|
||||
credentials: Readonly<AuthStorageData>;
|
||||
providerIds: readonly string[];
|
||||
}>;
|
||||
|
||||
@@ -41,6 +40,8 @@ export type PreparedModelWorkerResult =
|
||||
kind: "catalog";
|
||||
generationFingerprint: string;
|
||||
snapshot: ModelCatalogSnapshot;
|
||||
authStore: AuthProfileStore;
|
||||
authModes: PreparedAgentCredentialModes;
|
||||
}>
|
||||
| Readonly<{
|
||||
status: "ok";
|
||||
@@ -50,6 +51,22 @@ export type PreparedModelWorkerResult =
|
||||
}>
|
||||
| Readonly<{ status: "failed"; error: string }>;
|
||||
|
||||
const authByFullCatalog = new WeakMap<
|
||||
object,
|
||||
Readonly<{ authStore: AuthProfileStore; authModes: PreparedAgentCredentialModes }>
|
||||
>();
|
||||
|
||||
export function setPreparedModelFullCatalogAuth(
|
||||
modelCatalog: object,
|
||||
auth: Readonly<{ authStore: AuthProfileStore; authModes: PreparedAgentCredentialModes }>,
|
||||
): void {
|
||||
authByFullCatalog.set(modelCatalog, auth);
|
||||
}
|
||||
|
||||
export function getPreparedModelFullCatalogAuth(modelCatalog: object) {
|
||||
return authByFullCatalog.get(modelCatalog);
|
||||
}
|
||||
|
||||
// Cold source/plugin loading can take well over a minute. Three minutes preserves exact full-view
|
||||
// discovery while bounding a wedged provider; expiry rejects and never returns partial results.
|
||||
const PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS = 180_000;
|
||||
@@ -68,14 +85,12 @@ function fingerprintPreparedModelCatalogPlugins(snapshot: PluginMetadataSnapshot
|
||||
export function fingerprintPreparedModelCatalogGeneration(params: {
|
||||
input: PreparedModelRuntimeInput;
|
||||
authStore: AuthProfileStore;
|
||||
credentials: Readonly<AuthStorageData>;
|
||||
providerIds: readonly string[];
|
||||
pluginMetadataSnapshot: PluginMetadataSnapshot;
|
||||
}): string {
|
||||
return fingerprintPreparedRuntimeFacts({
|
||||
input: params.input,
|
||||
authStore: params.authStore,
|
||||
credentials: params.credentials,
|
||||
providerIds: params.providerIds,
|
||||
pluginFingerprint: fingerprintPreparedModelCatalogPlugins(params.pluginMetadataSnapshot),
|
||||
});
|
||||
@@ -110,7 +125,7 @@ export function createPreparedModelCatalogWorkerInput(params: {
|
||||
const input: PreparedModelRuntimeInput = {
|
||||
...(source.agentId ? { agentId: source.agentId } : {}),
|
||||
agentDir: source.agentDir,
|
||||
inheritedAuthDir: source.agentDir,
|
||||
inheritedAuthDir: source.inheritedAuthDir ?? source.agentDir,
|
||||
...(source.workspaceDir ? { workspaceDir: source.workspaceDir } : {}),
|
||||
...(source.readOnly ? { readOnly: true } : {}),
|
||||
skipCredentials: true,
|
||||
@@ -122,20 +137,17 @@ export function createPreparedModelCatalogWorkerInput(params: {
|
||||
config: source.config,
|
||||
};
|
||||
const authStore = projectWorkerAuthStore(params.agentFacts.authStore);
|
||||
const credentials = { ...params.agentFacts.credentials };
|
||||
const providerIds = [...params.agentFacts.providerIds];
|
||||
return {
|
||||
kind: "catalog",
|
||||
generationFingerprint: fingerprintPreparedModelCatalogGeneration({
|
||||
input,
|
||||
authStore,
|
||||
credentials,
|
||||
providerIds,
|
||||
pluginMetadataSnapshot: params.pluginMetadataSnapshot,
|
||||
}),
|
||||
input,
|
||||
authStore,
|
||||
credentials,
|
||||
providerIds,
|
||||
};
|
||||
}
|
||||
@@ -304,7 +316,12 @@ export function runPreparedModelCatalogWorker(params: {
|
||||
if (message.kind !== "catalog") {
|
||||
throw new Error("prepared model catalog worker returned an auth refresh result");
|
||||
}
|
||||
return markPreparedModelCatalogFull(message.snapshot);
|
||||
const modelCatalog = markPreparedModelCatalogFull(message.snapshot);
|
||||
setPreparedModelFullCatalogAuth(modelCatalog, {
|
||||
authStore: message.authStore,
|
||||
authModes: message.authModes,
|
||||
});
|
||||
return modelCatalog;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ vi.mock("./prepared-model-runtime.scoped-catalog.js", () => ({
|
||||
prepareScopedReadOnlyModelCatalog: (...args: unknown[]) => mocks.prepareScopedCatalog(...args),
|
||||
}));
|
||||
|
||||
import { setPreparedModelFullCatalogAuth } from "./prepared-model-catalog-worker.js";
|
||||
import { PreparedModelCatalogConfigReplacedError } from "./prepared-model-catalog.errors.js";
|
||||
import {
|
||||
getPublishedPreparedModelCatalogOwnerSnapshot,
|
||||
@@ -198,8 +199,9 @@ describe("prepared model catalog access", () => {
|
||||
entries: [{ provider: "test", id: "discovered", name: "Discovered" }],
|
||||
routeVariants: [],
|
||||
};
|
||||
const loadFullModelCatalog = vi.fn(async () => discoveredCatalog);
|
||||
const { authStore, ...snapshotFacts } = fullSnapshot;
|
||||
setPreparedModelFullCatalogAuth(discoveredCatalog, { authStore, authModes: {} });
|
||||
const loadFullModelCatalog = vi.fn(async () => discoveredCatalog);
|
||||
const snapshot = {
|
||||
...snapshotFacts,
|
||||
modelCatalog: configuredCatalog,
|
||||
|
||||
@@ -11,9 +11,14 @@ import {
|
||||
import { resolveLegacyInheritedAuthDir } from "./legacy-inherited-auth-dir.js";
|
||||
import type { ModelCatalogEntry, ModelCatalogSnapshot } from "./model-catalog.types.js";
|
||||
import { resolvePublishedModelCatalogOwner } from "./prepared-model-catalog-owner.js";
|
||||
import { getPreparedModelFullCatalogAuth } from "./prepared-model-catalog-worker.js";
|
||||
import { PreparedModelCatalogConfigReplacedError } from "./prepared-model-catalog.errors.js";
|
||||
import type { ResolvedPublishedModelCatalogOwner } from "./prepared-model-catalog.types.js";
|
||||
import { copyPreparedModelRuntimeAuthState } from "./prepared-model-runtime-auth.js";
|
||||
import {
|
||||
getPreparedModelRuntimeAuthMaterializations,
|
||||
setPreparedModelRuntimeAuthMaterializations,
|
||||
setPreparedModelRuntimeAuthStore,
|
||||
} from "./prepared-model-runtime-auth.js";
|
||||
import { isPreparedModelCatalogFull } from "./prepared-model-runtime.facts.js";
|
||||
import {
|
||||
acquireAgentRunPreparedModelRuntime,
|
||||
@@ -63,11 +68,22 @@ async function materializeRequestedModelCatalog(
|
||||
return snapshot;
|
||||
}
|
||||
const modelCatalog = await snapshot.loadFullModelCatalog();
|
||||
if (modelCatalog === snapshot.modelCatalog) {
|
||||
return snapshot;
|
||||
const fullAuth = getPreparedModelFullCatalogAuth(modelCatalog);
|
||||
if (!fullAuth) {
|
||||
throw new Error("prepared full model catalog omitted its auth generation");
|
||||
}
|
||||
const materialized = Object.freeze({ ...snapshot, modelCatalog });
|
||||
copyPreparedModelRuntimeAuthState(snapshot, materialized);
|
||||
const materialized = Object.freeze({
|
||||
...snapshot,
|
||||
authModes: fullAuth.authModes,
|
||||
modelCatalog,
|
||||
});
|
||||
// A materialized full read owns the worker's refreshed auth generation. Do not copy the
|
||||
// original loader or Gateway projection would start a second, split refresh after discovery.
|
||||
setPreparedModelRuntimeAuthStore(materialized, fullAuth.authStore);
|
||||
setPreparedModelRuntimeAuthMaterializations(
|
||||
materialized,
|
||||
getPreparedModelRuntimeAuthMaterializations(snapshot),
|
||||
);
|
||||
return materialized;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/** Worker-thread entrypoint for complete model-catalog discovery. */
|
||||
import { parentPort, workerData } from "node:worker_threads";
|
||||
import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js";
|
||||
import {
|
||||
resolveAgentCredentialMapFromStore,
|
||||
resolveUsableAgentCredentialModes,
|
||||
} from "./agent-auth-credentials.js";
|
||||
import { resolveAmbientAgentCredentialsForDiscovery } from "./agent-auth-discovery.js";
|
||||
import { overlayExternalAuthProfiles } from "./auth-profiles/external-auth.js";
|
||||
import { replaceRuntimeAuthProfileStoreSnapshots } from "./auth-profiles/runtime-snapshots.js";
|
||||
import {
|
||||
@@ -14,66 +20,114 @@ import {
|
||||
} from "./prepared-model-catalog-worker.js";
|
||||
import { AuthStorage } from "./sessions/auth-storage.js";
|
||||
|
||||
function refreshAuthStore(params: {
|
||||
agentDir: string;
|
||||
inheritedAuthDir?: string;
|
||||
authStore: PreparedModelCatalogWorkerInput["authStore"];
|
||||
config: PreparedModelCatalogWorkerInput["input"]["config"];
|
||||
env: NodeJS.ProcessEnv;
|
||||
providerIds?: readonly string[];
|
||||
}) {
|
||||
const durable = preserveResolvedSecretBackedCredentials({
|
||||
next: loadAuthProfileStoreWithoutExternalProfiles(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
...(params.inheritedAuthDir ? { inheritedAuthDir: params.inheritedAuthDir } : {}),
|
||||
}),
|
||||
existing: params.authStore,
|
||||
});
|
||||
const persistedProfileIds = new Set(params.authStore.runtimePersistedProfileIds ?? []);
|
||||
const externalProfileIds = new Set(params.authStore.runtimeExternalProfileIds ?? []);
|
||||
for (const [profileId, credential] of Object.entries(params.authStore.profiles)) {
|
||||
if (
|
||||
!persistedProfileIds.has(profileId) &&
|
||||
!externalProfileIds.has(profileId) &&
|
||||
durable.profiles[profileId] === undefined
|
||||
) {
|
||||
durable.profiles[profileId] = credential;
|
||||
}
|
||||
}
|
||||
return overlayExternalAuthProfiles(durable, {
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
...(params.providerIds ? { externalCliProviderIds: params.providerIds } : {}),
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runPreparedModelCatalogWorkerInput(
|
||||
value: PreparedModelCatalogWorkerInput | PreparedModelAuthRefreshWorkerInput,
|
||||
): Promise<PreparedModelWorkerResult> {
|
||||
try {
|
||||
if (value.kind === "auth-refresh") {
|
||||
// Durable profiles may be changed by another CLI process after this generation was built.
|
||||
// Reload them before adding current external overlays, while retaining only literals whose
|
||||
// unchanged SecretRefs were materialized by the owning generation.
|
||||
const authStore = preserveResolvedSecretBackedCredentials({
|
||||
next: loadAuthProfileStoreWithoutExternalProfiles(value.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
...(value.inheritedAuthDir ? { inheritedAuthDir: value.inheritedAuthDir } : {}),
|
||||
}),
|
||||
existing: value.authStore,
|
||||
});
|
||||
return {
|
||||
status: "ok",
|
||||
kind: "auth-refresh",
|
||||
generationFingerprint: value.generationFingerprint,
|
||||
authStore: overlayExternalAuthProfiles(authStore, {
|
||||
authStore: refreshAuthStore({
|
||||
agentDir: value.agentDir,
|
||||
inheritedAuthDir: value.inheritedAuthDir,
|
||||
authStore: value.authStore,
|
||||
config: value.config,
|
||||
env: value.env,
|
||||
externalCliProviderIds: value.providerIds,
|
||||
allowKeychainPrompt: false,
|
||||
providerIds: value.providerIds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const { prepareAgentCatalogSource, prepareFullCatalogFacts, prepareWorkspaceBuildGroup } =
|
||||
await import("./prepared-model-runtime.facts.js");
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{ agentDir: value.input.agentDir, store: value.authStore },
|
||||
]);
|
||||
const prepared = await prepareWorkspaceBuildGroup([value.input], "live");
|
||||
const agentFacts = prepared.agentFacts[0];
|
||||
if (!agentFacts) {
|
||||
throw new Error("prepared model catalog worker produced no agent facts");
|
||||
}
|
||||
const exactAgentFacts = {
|
||||
...agentFacts,
|
||||
authStore: value.authStore,
|
||||
templateAuthStorage: AuthStorage.inMemory({ ...value.credentials }),
|
||||
credentials: value.credentials,
|
||||
providerIds: [...value.providerIds],
|
||||
};
|
||||
const reconstructedFingerprint = fingerprintPreparedModelCatalogGeneration({
|
||||
input: value.input,
|
||||
authStore: value.authStore,
|
||||
credentials: value.credentials,
|
||||
providerIds: value.providerIds,
|
||||
pluginMetadataSnapshot: prepared.pluginGeneration.pluginMetadataSnapshot,
|
||||
});
|
||||
if (reconstructedFingerprint !== value.generationFingerprint) {
|
||||
throw new Error("prepared model catalog worker reconstructed a different runtime generation");
|
||||
}
|
||||
// Full discovery is one point-in-time operation: refresh first, then let every provider hook
|
||||
// and the returned availability projection consume the same exact store.
|
||||
const authStore = refreshAuthStore({
|
||||
agentDir: value.input.agentDir,
|
||||
inheritedAuthDir: value.input.inheritedAuthDir,
|
||||
authStore: value.authStore,
|
||||
config: value.input.config,
|
||||
env: value.input.env ?? process.env,
|
||||
});
|
||||
replaceRuntimeAuthProfileStoreSnapshots([{ agentDir: value.input.agentDir, store: authStore }]);
|
||||
const ambientCredentials = withPluginRuntimeRegistryScope(
|
||||
prepared.pluginGeneration.pluginRegistry,
|
||||
() =>
|
||||
resolveAmbientAgentCredentialsForDiscovery({
|
||||
config: value.input.config,
|
||||
env: value.input.env,
|
||||
...(value.input.workspaceDir ? { workspaceDir: value.input.workspaceDir } : {}),
|
||||
}),
|
||||
);
|
||||
const credentials = {
|
||||
...ambientCredentials,
|
||||
...resolveAgentCredentialMapFromStore(authStore, { config: value.input.config }),
|
||||
};
|
||||
const exactAgentFacts = {
|
||||
...agentFacts,
|
||||
authStore,
|
||||
templateAuthStorage: AuthStorage.inMemory(credentials),
|
||||
credentials,
|
||||
providerIds: [...new Set([...value.providerIds, ...Object.keys(credentials)])].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
),
|
||||
};
|
||||
const source = await prepareAgentCatalogSource(
|
||||
exactAgentFacts,
|
||||
prepared.pluginGeneration,
|
||||
"live",
|
||||
false,
|
||||
{ authStore: value.authStore },
|
||||
{ authStore },
|
||||
);
|
||||
const facts = await prepareFullCatalogFacts(
|
||||
exactAgentFacts,
|
||||
@@ -86,6 +140,8 @@ export async function runPreparedModelCatalogWorkerInput(
|
||||
kind: "catalog",
|
||||
generationFingerprint: value.generationFingerprint,
|
||||
snapshot: facts.modelCatalog,
|
||||
authStore,
|
||||
authModes: resolveUsableAgentCredentialModes(credentials),
|
||||
};
|
||||
} catch (error) {
|
||||
return { status: "failed", error: error instanceof Error ? error.message : String(error) };
|
||||
|
||||
@@ -118,9 +118,7 @@ function createFullModelCatalogAccess(params: {
|
||||
pluginGeneration: PreparedModelRuntimePluginGeneration;
|
||||
agentBuildCompletions: Map<string, Promise<void>>;
|
||||
isCurrent: () => boolean;
|
||||
eagerCatalog?: ModelCatalogSnapshot;
|
||||
}): PreparedModelRuntimeCatalogAccess {
|
||||
const eagerCatalog = params.eagerCatalog;
|
||||
// Concurrent readers share discovery, but completed results are discarded so
|
||||
// refreshable providers can publish changed inventory on the next explicit read.
|
||||
let pending: Promise<ModelCatalogSnapshot> | undefined;
|
||||
@@ -168,9 +166,6 @@ function createFullModelCatalogAccess(params: {
|
||||
return promise;
|
||||
},
|
||||
loadFullModelCatalog: () => {
|
||||
if (eagerCatalog) {
|
||||
return Promise.resolve(eagerCatalog);
|
||||
}
|
||||
if (!pending) {
|
||||
pending = runSerializedPreparedModelRuntimeTask({
|
||||
agentDir: params.agentFacts.input.agentDir,
|
||||
@@ -501,7 +496,6 @@ async function buildSnapshotBatch(
|
||||
pluginGeneration,
|
||||
agentBuildCompletions,
|
||||
isCurrent: generationGuards.get(input) ?? (() => false),
|
||||
...(catalogMode === "live" ? { eagerCatalog: catalogFacts.modelCatalog } : {}),
|
||||
}),
|
||||
),
|
||||
pluginGeneration,
|
||||
|
||||
@@ -582,7 +582,7 @@ describe("prepared model runtime owner selection", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("serializes on-demand full catalogs while preserving agent credentials", async () => {
|
||||
it("serializes on-demand full catalogs across prepared owners", async () => {
|
||||
mocks.configuredAgentIds = ["agent-a", "agent-b"];
|
||||
mocks.configuredWorkspaces.set("agent-a", "/tmp/shared-prepared-runtime-workspace");
|
||||
mocks.configuredWorkspaces.set("agent-b", "/tmp/shared-prepared-runtime-workspace");
|
||||
@@ -624,16 +624,6 @@ describe("prepared model runtime owner selection", () => {
|
||||
expect(mocks.ensureOpenClawModelsJson).not.toHaveBeenCalled();
|
||||
expect(mocks.runPreparedModelCatalogWorker).toHaveBeenCalledTimes(2);
|
||||
expect(peakActivePlans).toBe(1);
|
||||
expect(
|
||||
mocks.runPreparedModelCatalogWorker.mock.calls.map((call) => {
|
||||
const credential = (call[0] as { input: { credentials: Record<string, unknown> } }).input
|
||||
.credentials.custom as { type?: string; key?: string } | undefined;
|
||||
if (credential?.type !== "api_key") {
|
||||
throw new Error("expected prepared custom API key");
|
||||
}
|
||||
return credential.key;
|
||||
}),
|
||||
).toEqual(["test-key:/tmp/configured-agent-a", "test-key:/tmp/configured-agent-b"]);
|
||||
});
|
||||
|
||||
it("serializes a lazy catalog plan before a superseding generation", async () => {
|
||||
|
||||
@@ -102,7 +102,6 @@ vi.mock("./prepared-model-catalog-worker.js", () => ({
|
||||
agentFacts: {
|
||||
input: unknown;
|
||||
authStore: unknown;
|
||||
credentials: unknown;
|
||||
providerIds: unknown;
|
||||
};
|
||||
}) => ({
|
||||
@@ -110,7 +109,6 @@ vi.mock("./prepared-model-catalog-worker.js", () => ({
|
||||
generationFingerprint: "test-generation",
|
||||
input: agentFacts.input,
|
||||
authStore: agentFacts.authStore,
|
||||
credentials: agentFacts.credentials,
|
||||
providerIds: agentFacts.providerIds,
|
||||
}),
|
||||
runPreparedModelCatalogWorker: (...args: unknown[]) =>
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
} from "../../agents/agent-auth-credentials.js";
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js";
|
||||
import { setPreparedModelRuntimeAuthStore } from "../../agents/prepared-model-runtime-auth.js";
|
||||
import { setPreparedModelFullCatalogAuth } from "../../agents/prepared-model-catalog-worker.js";
|
||||
import {
|
||||
getPreparedModelRuntimeAuthStore,
|
||||
setPreparedModelRuntimeAuthStore,
|
||||
} from "../../agents/prepared-model-runtime-auth.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../../agents/prepared-model-runtime.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js";
|
||||
@@ -490,10 +494,15 @@ describe("gateway chat metadata runtime", () => {
|
||||
"openai",
|
||||
"openai-chatgpt-responses",
|
||||
);
|
||||
const loadFullModelCatalog = vi.fn(async () => ({
|
||||
const fullCatalog = {
|
||||
...owner.modelCatalog,
|
||||
providerOutcomes: [{ provider: "openai", status: "auth-rejected" as const }],
|
||||
}));
|
||||
};
|
||||
setPreparedModelFullCatalogAuth(fullCatalog, {
|
||||
authStore: getPreparedModelRuntimeAuthStore(owner)!,
|
||||
authModes: owner.authModes,
|
||||
});
|
||||
const loadFullModelCatalog = vi.fn(async () => fullCatalog);
|
||||
harness.setOwner({
|
||||
...owner,
|
||||
loadFullModelCatalog,
|
||||
|
||||
Reference in New Issue
Block a user