Skip to content

Commit 299ed80

Browse files
authored
fix: reuse provider auth lookup facts (#85499)
* fix: reuse provider auth lookup facts * test: update model auth mocks * fix: scope synthetic auth registry lookup
1 parent 7e12370 commit 299ed80

10 files changed

Lines changed: 233 additions & 107 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Docs: https://docs.openclaw.ai
9090
- Agents/OpenAI: preserve structured provider error code, type, and redacted body metadata on boundary-aware transport failures.
9191
- Doctor/Codex: point native Codex asset warnings at the canonical `openclaw migrate plan codex` preview command. Fixes #84948. Thanks @markoa.
9292
- CLI/models: make `capability model auth logout --agent` remove auth profiles from the selected non-default agent store. Fixes #85092. Thanks @islandpreneur007.
93+
- Gateway/models: reuse prepared provider auth metadata during model-listing auth checks so repeated lookups avoid broad plugin discovery while preserving synthetic local auth.
9394
- CLI/status: suppress systemd user-service setup hints when `openclaw status --deep` can already reach a running Gateway RPC service. Fixes #85094. Thanks @islandpreneur007.
9495
- CLI/devices: recover local approval when a same-device repair request replaces the request ID being approved.
9596
- CLI/agents: retry transient normal-close Gateway handshakes before falling back to embedded `openclaw agent` execution.

extensions/lmstudio/openclaw.plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
}
2424
},
2525
"nonSecretAuthMarkers": ["lmstudio-local"],
26+
"syntheticAuthRefs": ["lmstudio"],
2627
"providerAuthEnvVars": {
2728
"lmstudio": ["LM_API_TOKEN"]
2829
},

src/agents/model-auth-env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type EnvApiKeyResult = {
1818
source: string;
1919
};
2020

21-
type EnvApiKeyLookupOptions = {
21+
export type EnvApiKeyLookupOptions = {
2222
config?: OpenClawConfig;
2323
workspaceDir?: string;
2424
aliasMap?: Readonly<Record<string, string>>;

src/agents/model-auth.ts

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
shouldDeferProviderSyntheticProfileAuthWithPlugin,
1414
} from "../plugins/provider-runtime.js";
1515
import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js";
16-
import type { ProviderAuthEvidence } from "../secrets/provider-env-vars.js";
16+
import { resolveRuntimeSyntheticAuthProviderRefState } from "../plugins/synthetic-auth.runtime.js";
1717
import { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js";
1818
import {
1919
normalizeLowercaseStringOrEmpty,
@@ -33,7 +33,15 @@ import {
3333
resolveAuthStorePathForDisplay,
3434
} from "./auth-profiles.js";
3535
import * as cliCredentials from "./cli-credentials.js";
36-
import { resolveEnvApiKey, type EnvApiKeyResult } from "./model-auth-env.js";
36+
import {
37+
resolveProviderEnvApiKeyCandidates,
38+
resolveProviderEnvAuthEvidence,
39+
} from "./model-auth-env-vars.js";
40+
import {
41+
resolveEnvApiKey,
42+
type EnvApiKeyLookupOptions,
43+
type EnvApiKeyResult,
44+
} from "./model-auth-env.js";
3745
import {
3846
CUSTOM_LOCAL_AUTH_MARKER,
3947
isKnownEnvApiKeyMarker,
@@ -42,6 +50,7 @@ import {
4250
} from "./model-auth-markers.js";
4351
import { type ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
4452
import { normalizeProviderId } from "./model-selection.js";
53+
import { resolveProviderAuthAliasMap } from "./provider-auth-aliases.js";
4554

4655
export {
4756
ensureAuthProfileStore,
@@ -56,6 +65,11 @@ export {
5665
export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
5766
export type ProviderCredentialPrecedence = "profile-first" | "env-first";
5867

68+
export type RuntimeProviderAuthLookup = {
69+
envApiKey: Pick<EnvApiKeyLookupOptions, "aliasMap" | "candidateMap" | "authEvidenceMap">;
70+
syntheticAuthProviderRefs?: readonly string[];
71+
};
72+
5973
const log = createSubsystemLogger("model-auth");
6074

6175
function resolveConfigAwareEnvApiKey(
@@ -88,6 +102,30 @@ function resolveProviderConfig(
88102
);
89103
}
90104

105+
export function createRuntimeProviderAuthLookup(params: {
106+
cfg?: OpenClawConfig;
107+
workspaceDir?: string;
108+
env?: NodeJS.ProcessEnv;
109+
}): RuntimeProviderAuthLookup {
110+
const env = params.env ?? process.env;
111+
const lookupParams = {
112+
config: params.cfg,
113+
workspaceDir: params.workspaceDir,
114+
env,
115+
};
116+
const syntheticAuthProviderRefs = resolveRuntimeSyntheticAuthProviderRefState(lookupParams);
117+
return {
118+
envApiKey: {
119+
aliasMap: resolveProviderAuthAliasMap(lookupParams),
120+
candidateMap: resolveProviderEnvApiKeyCandidates(lookupParams),
121+
authEvidenceMap: resolveProviderEnvAuthEvidence(lookupParams),
122+
},
123+
syntheticAuthProviderRefs: syntheticAuthProviderRefs.complete
124+
? syntheticAuthProviderRefs.refs
125+
: undefined,
126+
};
127+
}
128+
91129
export function getCustomProviderApiKey(
92130
cfg: OpenClawConfig | undefined,
93131
provider: string,
@@ -344,17 +382,51 @@ export function hasSyntheticLocalProviderAuthConfig(params: {
344382
return Boolean(providerConfig.baseUrl && isLocalBaseUrl(providerConfig.baseUrl));
345383
}
346384

385+
function listProviderSyntheticAuthRefs(params: {
386+
cfg: OpenClawConfig | undefined;
387+
provider: string;
388+
modelApi?: string;
389+
}): string[] {
390+
const refs = [params.provider];
391+
const providerConfig = resolveProviderConfig(params.cfg, params.provider);
392+
if (params.modelApi) {
393+
refs.push(params.modelApi);
394+
}
395+
if (providerConfig?.api) {
396+
refs.push(providerConfig.api);
397+
}
398+
return [...new Set(refs.map((ref) => normalizeProviderId(ref)).filter(Boolean))];
399+
}
400+
401+
function shouldResolvePluginSyntheticAuth(params: {
402+
cfg: OpenClawConfig | undefined;
403+
provider: string;
404+
modelApi?: string;
405+
runtimeLookup?: RuntimeProviderAuthLookup;
406+
}): boolean {
407+
const syntheticAuthProviderRefs = params.runtimeLookup?.syntheticAuthProviderRefs;
408+
if (!syntheticAuthProviderRefs) {
409+
return true;
410+
}
411+
if (resolveProviderConfig(params.cfg, params.provider)) {
412+
return true;
413+
}
414+
const eligibleRefs = new Set(
415+
syntheticAuthProviderRefs.map((ref) => normalizeProviderId(ref)).filter(Boolean),
416+
);
417+
if (eligibleRefs.size === 0) {
418+
return false;
419+
}
420+
return listProviderSyntheticAuthRefs(params).some((ref) => eligibleRefs.has(ref));
421+
}
422+
347423
export function hasRuntimeAvailableProviderAuth(params: {
348424
provider: string;
349425
cfg?: OpenClawConfig;
350426
workspaceDir?: string;
351427
env?: NodeJS.ProcessEnv;
352428
allowPluginSyntheticAuth?: boolean;
353-
envAuthLookup?: {
354-
aliasMap?: Readonly<Record<string, string>>;
355-
candidateMap?: Readonly<Record<string, readonly string[]>>;
356-
authEvidenceMap?: Readonly<Record<string, readonly ProviderAuthEvidence[]>>;
357-
};
429+
runtimeLookup?: RuntimeProviderAuthLookup;
358430
}): boolean {
359431
const provider = normalizeProviderId(params.provider);
360432
const authOverride = resolveProviderAuthOverride(params.cfg, provider);
@@ -368,9 +440,7 @@ export function hasRuntimeAvailableProviderAuth(params: {
368440
resolveEnvApiKey(provider, params.env, {
369441
config: params.cfg,
370442
workspaceDir: params.workspaceDir,
371-
aliasMap: params.envAuthLookup?.aliasMap,
372-
candidateMap: params.envAuthLookup?.candidateMap,
373-
authEvidenceMap: params.envAuthLookup?.authEvidenceMap,
443+
...params.runtimeLookup?.envApiKey,
374444
})
375445
) {
376446
return true;
@@ -383,6 +453,11 @@ export function hasRuntimeAvailableProviderAuth(params: {
383453
}
384454
if (
385455
params.allowPluginSyntheticAuth !== false &&
456+
shouldResolvePluginSyntheticAuth({
457+
cfg: params.cfg,
458+
provider,
459+
runtimeLookup: params.runtimeLookup,
460+
}) &&
386461
resolveSyntheticLocalProviderAuth({ cfg: params.cfg, provider })
387462
) {
388463
return true;

src/agents/model-provider-auth.test.ts

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,25 @@ const modelCatalogMocks = vi.hoisted(() => ({
77
}));
88

99
const modelAuthMocks = vi.hoisted(() => ({
10+
createRuntimeProviderAuthLookup: vi.fn(() => ({
11+
envApiKey: {
12+
aliasMap: {},
13+
candidateMap: {},
14+
authEvidenceMap: {},
15+
},
16+
syntheticAuthProviderRefs: [],
17+
})),
1018
hasRuntimeAvailableProviderAuth:
1119
vi.fn<
1220
(params: {
1321
provider: string;
1422
cfg?: OpenClawConfig;
1523
workspaceDir?: string;
16-
envAuthLookup?: unknown;
24+
runtimeLookup?: unknown;
1725
}) => boolean
1826
>(),
1927
}));
2028

21-
const providerAuthAliasMocks = vi.hoisted(() => ({
22-
resolveProviderAuthAliasMap: vi.fn(() => ({ openai: "openai" })),
23-
}));
24-
25-
const modelAuthEnvVarMocks = vi.hoisted(() => ({
26-
resolveProviderEnvApiKeyCandidates: vi.fn(() => ({ openai: ["OPENAI_API_KEY"] })),
27-
resolveProviderEnvAuthEvidence: vi.fn(() => ({})),
28-
}));
29-
3029
const authProfilesMocks = vi.hoisted(() => ({
3130
ensureAuthProfileStore: vi.fn(() => ({ profiles: {} })),
3231
ensureAuthProfileStoreWithoutExternalProfiles: vi.fn(() => ({ profiles: {} })),
@@ -40,18 +39,10 @@ vi.mock("./model-catalog.js", () => ({
4039
}));
4140

4241
vi.mock("./model-auth.js", () => ({
42+
createRuntimeProviderAuthLookup: modelAuthMocks.createRuntimeProviderAuthLookup,
4343
hasRuntimeAvailableProviderAuth: modelAuthMocks.hasRuntimeAvailableProviderAuth,
4444
}));
4545

46-
vi.mock("./provider-auth-aliases.js", () => ({
47-
resolveProviderAuthAliasMap: providerAuthAliasMocks.resolveProviderAuthAliasMap,
48-
}));
49-
50-
vi.mock("./model-auth-env-vars.js", () => ({
51-
resolveProviderEnvApiKeyCandidates: modelAuthEnvVarMocks.resolveProviderEnvApiKeyCandidates,
52-
resolveProviderEnvAuthEvidence: modelAuthEnvVarMocks.resolveProviderEnvAuthEvidence,
53-
}));
54-
5546
vi.mock("./auth-profiles.js", () => ({
5647
ensureAuthProfileStore: authProfilesMocks.ensureAuthProfileStore,
5748
ensureAuthProfileStoreWithoutExternalProfiles:
@@ -81,7 +72,7 @@ describe("prepared provider auth state", () => {
8172
vi.clearAllMocks();
8273
});
8374

84-
it("reuses prepared env auth lookup data while warming providers", async () => {
75+
it("reuses prepared runtime auth lookup data while warming providers", async () => {
8576
const cfg = {} as OpenClawConfig;
8677
modelCatalogMocks.loadModelCatalog.mockResolvedValue([
8778
{ id: "gpt", name: "gpt", provider: "openai" },
@@ -91,13 +82,11 @@ describe("prepared provider auth state", () => {
9182

9283
await warmCurrentProviderAuthState(cfg);
9384

94-
expect(providerAuthAliasMocks.resolveProviderAuthAliasMap).toHaveBeenCalledTimes(1);
95-
expect(modelAuthEnvVarMocks.resolveProviderEnvApiKeyCandidates).toHaveBeenCalledTimes(1);
96-
expect(modelAuthEnvVarMocks.resolveProviderEnvAuthEvidence).toHaveBeenCalledTimes(1);
85+
expect(modelAuthMocks.createRuntimeProviderAuthLookup).toHaveBeenCalledTimes(1);
9786
const firstLookup =
98-
modelAuthMocks.hasRuntimeAvailableProviderAuth.mock.calls[0]?.[0].envAuthLookup;
87+
modelAuthMocks.hasRuntimeAvailableProviderAuth.mock.calls[0]?.[0].runtimeLookup;
9988
const secondLookup =
100-
modelAuthMocks.hasRuntimeAvailableProviderAuth.mock.calls[1]?.[0].envAuthLookup;
89+
modelAuthMocks.hasRuntimeAvailableProviderAuth.mock.calls[1]?.[0].runtimeLookup;
10190
expect(firstLookup).toBe(secondLookup);
10291
});
10392

src/agents/model-provider-auth.ts

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { hashRuntimeConfigValue } from "../config/runtime-snapshot.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
3-
import type { ProviderAuthEvidence } from "../secrets/provider-env-vars.js";
43
import {
54
listAgentIds,
65
resolveAgentDir,
@@ -16,13 +15,12 @@ import {
1615
type AuthProfileStore,
1716
} from "./auth-profiles.js";
1817
import {
19-
resolveProviderEnvApiKeyCandidates,
20-
resolveProviderEnvAuthEvidence,
21-
} from "./model-auth-env-vars.js";
22-
import { hasRuntimeAvailableProviderAuth } from "./model-auth.js";
18+
createRuntimeProviderAuthLookup,
19+
hasRuntimeAvailableProviderAuth,
20+
type RuntimeProviderAuthLookup,
21+
} from "./model-auth.js";
2322
import { loadModelCatalog } from "./model-catalog.js";
2423
import { normalizeProviderId } from "./model-selection.js";
25-
import { resolveProviderAuthAliasMap } from "./provider-auth-aliases.js";
2624
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
2725

2826
// Prepared runtime fact: which providers have available auth given the
@@ -37,12 +35,6 @@ type PreparedProviderAuthState = {
3735
providers: ReadonlyMap<string, boolean>;
3836
};
3937

40-
type ProviderEnvAuthLookup = {
41-
aliasMap?: Readonly<Record<string, string>>;
42-
candidateMap?: Readonly<Record<string, readonly string[]>>;
43-
authEvidenceMap?: Readonly<Record<string, readonly ProviderAuthEvidence[]>>;
44-
};
45-
4638
// One entry per configured agent, keyed by agentId. Populated by
4739
// warmCurrentProviderAuthState at gateway startup / on reload; consulted by
4840
// hasAuthForModelProvider on every model-listing call.
@@ -97,7 +89,8 @@ export async function hasAuthForModelProvider(params: {
9789
store?: AuthProfileStore;
9890
allowPluginSyntheticAuth?: boolean;
9991
discoverExternalCliAuth?: boolean;
100-
envAuthLookup?: ProviderEnvAuthLookup;
92+
runtimeAuthLookup?: RuntimeProviderAuthLookup;
93+
resolveRuntimeAuthLookup?: () => RuntimeProviderAuthLookup;
10194
}): Promise<boolean> {
10295
const provider = normalizeProviderId(params.provider);
10396
// The prepared map is built by warmCurrentProviderAuthState — one entry per
@@ -144,7 +137,7 @@ export async function hasAuthForModelProvider(params: {
144137
workspaceDir: params.workspaceDir,
145138
env: params.env,
146139
allowPluginSyntheticAuth: params.allowPluginSyntheticAuth,
147-
envAuthLookup: params.envAuthLookup,
140+
runtimeLookup: params.runtimeAuthLookup ?? params.resolveRuntimeAuthLookup?.(),
148141
})
149142
) {
150143
return true;
@@ -175,6 +168,7 @@ export function createProviderAuthChecker(params: {
175168
discoverExternalCliAuth?: boolean;
176169
}): (provider: string) => Promise<boolean> {
177170
const authCache = new Map<string, boolean>();
171+
let runtimeAuthLookup: RuntimeProviderAuthLookup | undefined;
178172
return async (provider: string) => {
179173
const key = normalizeProviderId(provider);
180174
const cached = authCache.get(key);
@@ -189,6 +183,12 @@ export function createProviderAuthChecker(params: {
189183
env: params.env,
190184
allowPluginSyntheticAuth: params.allowPluginSyntheticAuth,
191185
discoverExternalCliAuth: params.discoverExternalCliAuth,
186+
resolveRuntimeAuthLookup: () =>
187+
(runtimeAuthLookup ??= createRuntimeProviderAuthLookup({
188+
cfg: params.cfg,
189+
workspaceDir: params.workspaceDir,
190+
env: params.env,
191+
})),
192192
});
193193
authCache.set(key, value);
194194
return value;
@@ -225,11 +225,10 @@ export async function warmCurrentProviderAuthState(
225225
}
226226
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
227227
const agentDir = resolveAgentDir(cfg, agentId);
228-
const envAuthLookup = {
229-
aliasMap: resolveProviderAuthAliasMap({ config: cfg, workspaceDir }),
230-
candidateMap: resolveProviderEnvApiKeyCandidates({ config: cfg, workspaceDir }),
231-
authEvidenceMap: resolveProviderEnvAuthEvidence({ config: cfg, workspaceDir }),
232-
};
228+
const runtimeAuthLookup = createRuntimeProviderAuthLookup({
229+
cfg,
230+
workspaceDir,
231+
});
233232
// One AuthProfileStore scoped to every candidate provider; without this
234233
// the per-provider externalCli discovery rebuilds the store ~N times.
235234
const store = ensureAuthProfileStore(agentDir, {
@@ -250,7 +249,7 @@ export async function warmCurrentProviderAuthState(
250249
workspaceDir,
251250
agentId,
252251
store,
253-
envAuthLookup,
252+
runtimeAuthLookup,
254253
});
255254
state.set(provider, value);
256255
}

src/auto-reply/reply/directive-handling.model.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ vi.mock("../../agents/model-auth.js", () => {
8181
const hasWorkspaceCredential = (env: NodeJS.ProcessEnv = process.env) =>
8282
Boolean(env.WORKSPACE_MODEL_LIST_CREDENTIALS || env.WORKSPACE_MODEL_CREDENTIALS);
8383
return {
84+
createRuntimeProviderAuthLookup: () => ({
85+
envApiKey: {
86+
aliasMap: {},
87+
candidateMap: {},
88+
authEvidenceMap: {},
89+
},
90+
syntheticAuthProviderRefs: [],
91+
}),
8492
ensureAuthProfileStore: store,
8593
hasRuntimeAvailableProviderAuth: ({
8694
provider,

0 commit comments

Comments
 (0)