diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index faf364c68e9..e9cb2b76853 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -151,7 +151,7 @@ Gateway startup builds one `PluginMetadataSnapshot` for the current config snaps Plugin-aware config validation, startup auto-enable, and Gateway plugin bootstrap consume that snapshot instead of rebuilding manifest/index metadata independently. `PluginLookUpTable` is derived from the same snapshot and adds the startup plugin plan for the current runtime config. -After startup, Gateway keeps the current metadata snapshot as a replaceable runtime product. Repeated runtime provider discovery can borrow that snapshot instead of reconstructing the installed index and manifest registry for each provider-catalog pass. The snapshot is cleared or replaced on Gateway shutdown, config/plugin inventory changes, and installed index writes; callers fall back to the cold manifest/index path when no compatible current snapshot exists. +After startup, Gateway keeps the current metadata snapshot as a replaceable runtime product. Repeated runtime provider discovery can borrow that snapshot instead of reconstructing the installed index and manifest registry for each provider-catalog pass. The snapshot is cleared or replaced on Gateway shutdown, config/plugin inventory changes, and installed index writes; callers fall back to the cold manifest/index path when no compatible current snapshot exists. Compatibility checks must include plugin discovery roots such as `plugins.load.paths` and the default agent workspace, because workspace plugins are part of the metadata scope. The snapshot and lookup table keep repeated startup decisions on the fast path: diff --git a/src/agents/models-config.applies-config-env-vars.test.ts b/src/agents/models-config.applies-config-env-vars.test.ts index ba6d4d46039..789e3c7520b 100644 --- a/src/agents/models-config.applies-config-env-vars.test.ts +++ b/src/agents/models-config.applies-config-env-vars.test.ts @@ -104,6 +104,27 @@ describe("models-config", () => { expect(observedSnapshot).toBe(pluginMetadataSnapshot); }); + it("threads workspace scope into implicit provider discovery", async () => { + let observedWorkspaceDir: string | undefined; + + await resolveProvidersForModelsJsonWithDeps( + { + cfg: { models: { providers: {} } }, + agentDir: "/tmp/openclaw-models-config-env-vars-test", + env: {}, + workspaceDir: "/tmp/openclaw-workspace", + }, + { + resolveImplicitProviders: async ({ workspaceDir }) => { + observedWorkspaceDir = workspaceDir; + return {}; + }, + }, + ); + + expect(observedWorkspaceDir).toBe("/tmp/openclaw-workspace"); + }); + it("threads plugin metadata snapshots through models.json planning", async () => { const pluginMetadataSnapshot = { index: { plugins: [] }, diff --git a/src/agents/models-config.plan.ts b/src/agents/models-config.plan.ts index 34db89dca48..8e955e7795e 100644 --- a/src/agents/models-config.plan.ts +++ b/src/agents/models-config.plan.ts @@ -19,6 +19,7 @@ export type ResolveImplicitProvidersForModelsJson = (params: { agentDir: string; config: OpenClawConfig; env: NodeJS.ProcessEnv; + workspaceDir?: string; explicitProviders: Record; pluginMetadataSnapshot?: Pick; }) => Promise>; @@ -40,6 +41,7 @@ export async function resolveProvidersForModelsJsonWithDeps( cfg: OpenClawConfig; agentDir: string; env: NodeJS.ProcessEnv; + workspaceDir?: string; pluginMetadataSnapshot?: Pick; }, deps?: { @@ -53,6 +55,7 @@ export async function resolveProvidersForModelsJsonWithDeps( agentDir, config: cfg, env, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), explicitProviders, ...(params.pluginMetadataSnapshot ? { pluginMetadataSnapshot: params.pluginMetadataSnapshot } @@ -94,6 +97,7 @@ export async function planOpenClawModelsJsonWithDeps( sourceConfigForSecrets?: OpenClawConfig; agentDir: string; env: NodeJS.ProcessEnv; + workspaceDir?: string; existingRaw: string; existingParsed: unknown; pluginMetadataSnapshot?: Pick; @@ -108,6 +112,7 @@ export async function planOpenClawModelsJsonWithDeps( cfg, agentDir, env, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), ...(params.pluginMetadataSnapshot ? { pluginMetadataSnapshot: params.pluginMetadataSnapshot } : {}), diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 5535db704db..6c2c1a0b2a5 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -11,6 +11,7 @@ import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-meta import { resolveInstalledManifestRegistryIndexFingerprint } from "../plugins/manifest-registry-installed.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js"; +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.js"; import { MODELS_JSON_STATE } from "./models-config-state.js"; import { planOpenClawModelsJson } from "./models-config.plan.js"; @@ -44,6 +45,7 @@ async function buildModelsJsonFingerprint(params: { config: OpenClawConfig; sourceConfigForSecrets: OpenClawConfig; agentDir: string; + workspaceDir?: string; pluginMetadataSnapshot?: Pick; }): Promise { const authProfilesMtimeMs = await readFileMtimeMs( @@ -60,6 +62,7 @@ async function buildModelsJsonFingerprint(params: { envShape, authProfilesMtimeMs, modelsFileMtimeMs, + workspaceDir: params.workspaceDir, pluginMetadataSnapshotIndexFingerprint, }); } @@ -152,14 +155,17 @@ export async function ensureOpenClawModelsJson( ): Promise<{ agentDir: string; wrote: boolean }> { const resolved = resolveModelsConfigInput(config); const cfg = resolved.config; + const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); const pluginMetadataSnapshot = - options.pluginMetadataSnapshot ?? getCurrentPluginMetadataSnapshot({ config: cfg }); + options.pluginMetadataSnapshot ?? + getCurrentPluginMetadataSnapshot({ config: cfg, workspaceDir }); const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir(); const targetPath = path.join(agentDir, "models.json"); const fingerprint = await buildModelsJsonFingerprint({ config: cfg, sourceConfigForSecrets: resolved.sourceConfigForSecrets, agentDir, + workspaceDir, ...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}), }); const cached = MODELS_JSON_STATE.readyCache.get(targetPath); @@ -181,6 +187,7 @@ export async function ensureOpenClawModelsJson( sourceConfigForSecrets: resolved.sourceConfigForSecrets, agentDir, env, + workspaceDir, existingRaw: existingModelsFile.raw, existingParsed: existingModelsFile.parsed, ...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),