Skip to content

Commit 7bcead1

Browse files
committed
Memory: preserve Ollama env fallback semantics
1 parent 73a6f78 commit 7bcead1

File tree

3 files changed

+77
-21
lines changed

3 files changed

+77
-21
lines changed

src/memory/embeddings-ollama.test.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe("embeddings-ollama", () => {
4444
providers: {
4545
ollama: {
4646
baseUrl: "http://127.0.0.1:11434/v1",
47-
apiKey: "ollama-local",
47+
apiKey: "ollama-\nlocal\r\n",
4848
headers: {
4949
"X-Provider-Header": "provider",
5050
},
@@ -87,24 +87,43 @@ describe("embeddings-ollama", () => {
8787
).rejects.toThrow(/agents\.\*\.memorySearch\.remote\.apiKey: unresolved SecretRef/i);
8888
});
8989

90-
it("fails fast when models.providers.ollama.apiKey is an unresolved SecretRef", async () => {
91-
await expect(
92-
createOllamaEmbeddingProvider({
93-
config: {
94-
models: {
95-
providers: {
96-
ollama: {
97-
baseUrl: "http://127.0.0.1:11434/v1",
98-
apiKey: { source: "env", provider: "default", id: "OLLAMA_API_KEY" },
99-
models: [],
100-
},
90+
it("falls back to env key when models.providers.ollama.apiKey is an unresolved SecretRef", async () => {
91+
const fetchMock = vi.fn(
92+
async () =>
93+
new Response(JSON.stringify({ embedding: [1, 0] }), {
94+
status: 200,
95+
headers: { "content-type": "application/json" },
96+
}),
97+
);
98+
globalThis.fetch = fetchMock as unknown as typeof fetch;
99+
vi.stubEnv("OLLAMA_API_KEY", "ollama-env");
100+
101+
const { provider } = await createOllamaEmbeddingProvider({
102+
config: {
103+
models: {
104+
providers: {
105+
ollama: {
106+
baseUrl: "http://127.0.0.1:11434/v1",
107+
apiKey: { source: "env", provider: "default", id: "OLLAMA_API_KEY" },
108+
models: [],
101109
},
102110
},
103-
} as unknown as OpenClawConfig,
104-
provider: "ollama",
105-
model: "nomic-embed-text",
106-
fallback: "none",
111+
},
112+
} as unknown as OpenClawConfig,
113+
provider: "ollama",
114+
model: "nomic-embed-text",
115+
fallback: "none",
116+
});
117+
118+
await provider.embedQuery("hello");
119+
120+
expect(fetchMock).toHaveBeenCalledWith(
121+
"http://127.0.0.1:11434/api/embeddings",
122+
expect.objectContaining({
123+
headers: expect.objectContaining({
124+
Authorization: "Bearer ollama-env",
125+
}),
107126
}),
108-
).rejects.toThrow(/models\.providers\.ollama\.apiKey: unresolved SecretRef/i);
127+
);
109128
});
110129
});

src/memory/embeddings-ollama.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { resolveEnvApiKey } from "../agents/model-auth.js";
22
import { formatErrorMessage } from "../infra/errors.js";
33
import type { SsrFPolicy } from "../infra/net/ssrf.js";
4+
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
45
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
56
import { buildRemoteBaseUrlPolicy, withRemoteHttpResponse } from "./remote-http.js";
67
import { resolveMemorySecretInputString } from "./secret-input.js";
@@ -53,10 +54,9 @@ function resolveOllamaApiKey(options: EmbeddingProviderOptions): string | undefi
5354
if (remoteApiKey) {
5455
return remoteApiKey;
5556
}
56-
const providerApiKey = resolveMemorySecretInputString({
57-
value: options.config.models?.providers?.ollama?.apiKey,
58-
path: "models.providers.ollama.apiKey",
59-
});
57+
const providerApiKey = normalizeOptionalSecretInput(
58+
options.config.models?.providers?.ollama?.apiKey,
59+
);
6060
if (providerApiKey) {
6161
return providerApiKey;
6262
}

src/memory/embeddings.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,43 @@ describe("embedding provider remote overrides", () => {
210210
expect(headers["Content-Type"]).toBe("application/json");
211211
});
212212

213+
it("fails fast when Gemini remote apiKey is an unresolved SecretRef", async () => {
214+
await expect(
215+
createEmbeddingProvider({
216+
config: {} as never,
217+
provider: "gemini",
218+
remote: {
219+
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" },
220+
},
221+
model: "text-embedding-004",
222+
fallback: "openai",
223+
}),
224+
).rejects.toThrow(/agents\.\*\.memorySearch\.remote\.apiKey:/i);
225+
});
226+
227+
it("uses GEMINI_API_KEY env indirection for Gemini remote apiKey", async () => {
228+
const fetchMock = createGeminiFetchMock();
229+
vi.stubGlobal("fetch", fetchMock);
230+
vi.stubEnv("GEMINI_API_KEY", "env-gemini-key");
231+
232+
const result = await createEmbeddingProvider({
233+
config: {} as never,
234+
provider: "gemini",
235+
remote: {
236+
apiKey: "GEMINI_API_KEY",
237+
},
238+
model: "text-embedding-004",
239+
fallback: "openai",
240+
});
241+
242+
const provider = requireProvider(result);
243+
await provider.embedQuery("hello");
244+
245+
const { init } = readFirstFetchRequest(fetchMock);
246+
const headers = (init?.headers ?? {}) as Record<string, string>;
247+
expect(headers["x-goog-api-key"]).toBe("env-gemini-key");
248+
});
249+
213250
it("builds Mistral embeddings requests with bearer auth", async () => {
214251
const fetchMock = createFetchMock();
215252
vi.stubGlobal("fetch", fetchMock);

0 commit comments

Comments
 (0)