mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
Fix Ollama cloud API key discovery (#85091)
Summary: - The branch teaches Ollama discovery to use resolved `discoveryApiKey` values for non-local cloud providers, preserves local marker auth, and adds focused provider-discovery regressions plus a changelog entry. - Reproducibility: yes. from source inspection: current main can return the `OLLAMA_API_KEY` marker instead of ... ential for documented Ollama Cloud config. I did not run executable tests because this review is read-only. Automerge notes: - PR branch already contained follow-up commit before automerge: ci: allowlist qa lab fixtures - PR branch already contained follow-up commit before automerge: Fix Ollama cloud API key discovery - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8503… Validation: - ClawSweeper review passed for headcb6b658819. - Required merge gates passed before the squash merge. Prepared head SHA:cb6b658819Review: https://github.com/openclaw/openclaw/pull/85091#issuecomment-4512647237 Co-authored-by: Anup Sharma <anupnewsmail@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
|
||||
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
|
||||
- Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.
|
||||
- Gateway/status: surface inbound delivery telemetry counters and transport-liveness warnings in `openclaw status --all`. Fixes #49577. (#72724)
|
||||
- Docker: prune package-excluded plugin source workspaces and dependency closures so runtime images do not keep packages for plugins that were not opted in.
|
||||
|
||||
@@ -57,7 +57,11 @@ describe("Ollama provider", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function runOllamaCatalog(params: { config?: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
|
||||
async function runOllamaCatalog(params: {
|
||||
config?: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
resolveProviderApiKey?: () => { apiKey: string | undefined; discoveryApiKey?: string };
|
||||
}) {
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
VITEST: "1",
|
||||
@@ -68,9 +72,11 @@ describe("Ollama provider", () => {
|
||||
config: params.config ?? {},
|
||||
agentDir: createAgentDir(),
|
||||
env,
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: env.OLLAMA_API_KEY?.trim() ? env.OLLAMA_API_KEY : undefined,
|
||||
}),
|
||||
resolveProviderApiKey:
|
||||
params.resolveProviderApiKey ??
|
||||
(() => ({
|
||||
apiKey: env.OLLAMA_API_KEY?.trim() ? env.OLLAMA_API_KEY : undefined,
|
||||
})),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: env.OLLAMA_API_KEY?.trim() ? env.OLLAMA_API_KEY : undefined,
|
||||
mode: env.OLLAMA_API_KEY?.trim() ? "api_key" : "none",
|
||||
@@ -480,6 +486,131 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses resolved discovery api key when configured cloud apiKey is an env marker", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "https://ollama.com/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 81920,
|
||||
},
|
||||
],
|
||||
apiKey: "OLLAMA_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { OLLAMA_API_KEY: "real-secret", VITEST: "", NODE_ENV: "development" },
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "OLLAMA_API_KEY",
|
||||
discoveryApiKey: "real-secret",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(provider?.baseUrl).toBe("https://ollama.com");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.apiKey).toBe("real-secret");
|
||||
expect(provider?.models).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses resolved discovery api key for configured cloud providers without apiKey", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "https://ollama.com/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 81920,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { OLLAMA_API_KEY: "real-secret", VITEST: "", NODE_ENV: "development" },
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "OLLAMA_API_KEY",
|
||||
discoveryApiKey: "real-secret",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(provider?.baseUrl).toBe("https://ollama.com");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.apiKey).toBe("real-secret");
|
||||
expect(provider?.models).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps synthetic local auth when a local provider also has a discovery key", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const provider = await runOllamaCatalog({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
baseUrl: "http://127.0.0.1:11434/v1",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-oss:20b",
|
||||
name: "GPT-OSS 20B",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8192,
|
||||
maxTokens: 81920,
|
||||
},
|
||||
],
|
||||
apiKey: "OLLAMA_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { OLLAMA_API_KEY: "real-secret", VITEST: "", NODE_ENV: "development" },
|
||||
resolveProviderApiKey: () => ({
|
||||
apiKey: "OLLAMA_API_KEY",
|
||||
discoveryApiKey: "real-secret",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(provider?.baseUrl).toBe("http://127.0.0.1:11434");
|
||||
expect(provider?.api).toBe("ollama");
|
||||
expect(provider?.apiKey).toBe(OLLAMA_LOCAL_AUTH_MARKER);
|
||||
expect(provider?.models).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve explicit apiKey from configured remote providers", async () => {
|
||||
await withoutAmbientOllamaEnv(async () => {
|
||||
const fetchMock = vi.fn(async (input: unknown) => {
|
||||
|
||||
@@ -23,7 +23,10 @@ type OllamaDiscoveryContext = {
|
||||
};
|
||||
};
|
||||
env: NodeJS.ProcessEnv;
|
||||
resolveProviderApiKey: (providerId: string) => { apiKey?: unknown };
|
||||
resolveProviderApiKey: (providerId: string) => {
|
||||
apiKey?: unknown;
|
||||
discoveryApiKey?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
@@ -40,27 +43,34 @@ function readStringValue(value: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isOllamaApiKeyMarker(value: string): boolean {
|
||||
return value === "OLLAMA_API_KEY" || value === OLLAMA_DEFAULT_API_KEY;
|
||||
}
|
||||
|
||||
function resolveOllamaDiscoveryApiKey(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
baseUrl?: string;
|
||||
explicitApiKey?: string;
|
||||
hasDeclaredApiKey?: boolean;
|
||||
resolvedApiKey?: unknown;
|
||||
resolvedDiscoveryApiKey?: unknown;
|
||||
}): string | undefined {
|
||||
const envValue = normalizeOptionalString(params.env.OLLAMA_API_KEY);
|
||||
const envApiKey = envValue ? "OLLAMA_API_KEY" : undefined;
|
||||
const resolvedApiKey = normalizeOptionalString(params.resolvedApiKey);
|
||||
const resolvedDiscoveryApiKey = normalizeOptionalString(params.resolvedDiscoveryApiKey);
|
||||
const explicitApiKey = normalizeOptionalString(params.explicitApiKey);
|
||||
if (explicitApiKey) {
|
||||
if (explicitApiKey && !isOllamaApiKeyMarker(explicitApiKey)) {
|
||||
return explicitApiKey;
|
||||
}
|
||||
if (params.hasDeclaredApiKey && resolvedApiKey) {
|
||||
return resolvedApiKey;
|
||||
}
|
||||
if (!isLocalOllamaBaseUrl(params.baseUrl)) {
|
||||
return envApiKey ?? (resolvedApiKey !== OLLAMA_DEFAULT_API_KEY ? resolvedApiKey : undefined);
|
||||
if (resolvedDiscoveryApiKey) {
|
||||
return resolvedDiscoveryApiKey;
|
||||
}
|
||||
if (resolvedApiKey && !isOllamaApiKeyMarker(resolvedApiKey)) {
|
||||
return resolvedApiKey;
|
||||
}
|
||||
return envValue && envValue !== OLLAMA_DEFAULT_API_KEY ? envValue : undefined;
|
||||
}
|
||||
if (resolvedApiKey && resolvedApiKey !== envValue && resolvedApiKey !== OLLAMA_DEFAULT_API_KEY) {
|
||||
if (resolvedApiKey && resolvedApiKey !== envValue && !isOllamaApiKeyMarker(resolvedApiKey)) {
|
||||
return resolvedApiKey;
|
||||
}
|
||||
return OLLAMA_DEFAULT_API_KEY;
|
||||
@@ -237,22 +247,23 @@ export async function resolveOllamaDiscoveryResult(params: {
|
||||
if (!hasExplicitModels && discoveryEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
const ollamaKey = params.ctx.resolveProviderApiKey(OLLAMA_PROVIDER_ID).apiKey;
|
||||
const resolvedOllamaAuth = params.ctx.resolveProviderApiKey(OLLAMA_PROVIDER_ID);
|
||||
const ollamaKey = resolvedOllamaAuth.apiKey;
|
||||
const ollamaDiscoveryKey = resolvedOllamaAuth.discoveryApiKey;
|
||||
const hasOllamaDiscoveryOptIn = typeof ollamaKey === "string" && ollamaKey.trim().length > 0;
|
||||
const hasRealOllamaKey =
|
||||
typeof ollamaKey === "string" &&
|
||||
ollamaKey.trim().length > 0 &&
|
||||
ollamaKey.trim() !== OLLAMA_DEFAULT_API_KEY;
|
||||
const explicitApiKey = readStringValue(explicit?.apiKey);
|
||||
const hasDeclaredApiKey = explicit?.apiKey !== undefined;
|
||||
if (hasExplicitModels && explicit) {
|
||||
const baseUrl = resolveOllamaApiBase(readProviderBaseUrl(explicit) ?? OLLAMA_DEFAULT_BASE_URL);
|
||||
const apiKey = resolveOllamaDiscoveryApiKey({
|
||||
env: params.ctx.env,
|
||||
baseUrl,
|
||||
explicitApiKey,
|
||||
hasDeclaredApiKey,
|
||||
resolvedApiKey: ollamaKey,
|
||||
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
|
||||
});
|
||||
return {
|
||||
provider: {
|
||||
@@ -299,8 +310,8 @@ export async function resolveOllamaDiscoveryResult(params: {
|
||||
env: params.ctx.env,
|
||||
baseUrl: provider.baseUrl ?? configuredBaseUrl,
|
||||
explicitApiKey,
|
||||
hasDeclaredApiKey,
|
||||
resolvedApiKey: ollamaKey,
|
||||
resolvedDiscoveryApiKey: ollamaDiscoveryKey,
|
||||
});
|
||||
return {
|
||||
provider: {
|
||||
|
||||
Reference in New Issue
Block a user