From 2a5f98e0ff42f7a03c4966c04edf6b8db8a237d1 Mon Sep 17 00:00:00 2001 From: paultancre-bt Date: Tue, 5 May 2026 15:54:43 -0700 Subject: [PATCH 1/2] feat: expose api_base on Ollama provider metadata schema Add a typed `api_base` field to the Ollama provider's metadata so it can be customized in the AI providers form (e.g. to point at a self-hosted or tunnelled Ollama instance instead of the default `http://127.0.0.1:11434/v1`). The proxy already honors `secret.metadata.api_base` generically at runtime, so this is a schema-only change. It mirrors the existing `MistralMetadataSchema` shape: - Promote `ollama` from the bulk `z.enum` to its own discriminated union arm with `OllamaMetadataSchema` metadata. - `OllamaMetadataSchema` is `BaseMetadataSchema` plus an optional `api_base` (allows valid URL, empty string, null, or undefined to preserve the default). Existing Ollama secrets continue to validate (the new schema is a strict superset), so no migration is required. --- packages/proxy/schema/secrets.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/proxy/schema/secrets.ts b/packages/proxy/schema/secrets.ts index a3dd180d..6361a1ab 100644 --- a/packages/proxy/schema/secrets.ts +++ b/packages/proxy/schema/secrets.ts @@ -129,6 +129,12 @@ export const MistralMetadataSchema = BaseMetadataSchema.merge( }), ).passthrough(); +export const OllamaMetadataSchema = BaseMetadataSchema.merge( + z.object({ + api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), + }), +).passthrough(); + export const BraintrustMetadataSchema = BaseMetadataSchema.merge( z.object({ api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), @@ -155,7 +161,6 @@ export const APISecretSchema = z.union([ "replicate", "together", "baseten", - "ollama", "groq", "lepton", "fireworks", @@ -208,6 +213,12 @@ export const APISecretSchema = z.union([ metadata: MistralMetadataSchema.nullish(), }), ).passthrough(), + APISecretBaseSchema.merge( + z.object({ + type: z.literal("ollama"), + metadata: OllamaMetadataSchema.nullish(), + }), + ).passthrough(), ]); export type APISecret = z.infer; From 095ab2c1f490c2f04e21b27ff2243cb4b4a74264 Mon Sep 17 00:00:00 2001 From: paultancre-bt Date: Tue, 5 May 2026 16:49:07 -0700 Subject: [PATCH 2/2] fix: preserve passthrough behavior for legacy Ollama api_base values Address Codex review on #522. The new `OllamaMetadataSchema` arm tightens validation on `metadata.api_base` (must be a valid URL, empty string, null, or undefined), where the previous bulk-enum arm used `BaseMetadataSchema.passthrough()` and silently accepted any value. The edge loader parses every fetched secret with `APISecretSchema.parse`, so a single stored Ollama secret with a non-URL/non-string `api_base` (e.g. `"localhost"`, a number, or a bool from a legacy direct API call) would throw and break the whole secret list lookup, instead of falling through to the runtime's existing `typeof api_base === "string"` check that uses the default base URL. Add `.catch(undefined)` to `OllamaMetadataSchema.api_base` so invalid stored values silently coerce to `undefined`, exactly matching the prior "ignore and use default" semantics. Form input still surfaces validation errors via `zodResolver` in the AI providers form, since the form parses fresh user input separately. Includes a regression test in `APISecretSchema compatibility` covering invalid strings, bare hostnames, numbers, booleans, and objects. Co-authored-by: Cursor --- packages/proxy/schema/index.test.ts | 25 +++++++++++++++++++++++++ packages/proxy/schema/secrets.ts | 5 ++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/proxy/schema/index.test.ts b/packages/proxy/schema/index.test.ts index 38636068..c5a5f193 100644 --- a/packages/proxy/schema/index.test.ts +++ b/packages/proxy/schema/index.test.ts @@ -331,4 +331,29 @@ describe("APISecretSchema compatibility", () => { }); expect(result.success).toBe(false); }); + + it("preserves passthrough behavior for legacy Ollama api_base values", () => { + const cases: Array<{ name: string; api_base: unknown }> = [ + { name: "invalid url string", api_base: "not a url" }, + { name: "bare hostname", api_base: "localhost" }, + { name: "number", api_base: 12345 }, + { name: "boolean", api_base: true }, + { name: "object", api_base: { nested: 1 } }, + ]; + + for (const { name, api_base } of cases) { + const parsed = APISecretSchema.parse({ + secret: "ollama-secret", + type: "ollama", + metadata: { api_base }, + }); + if (parsed.type !== "ollama") { + throw new Error(`Expected ollama secret for case '${name}'`); + } + expect( + parsed.metadata?.api_base, + `case '${name}' should coerce to undefined to preserve runtime fallback`, + ).toBeUndefined(); + } + }); }); diff --git a/packages/proxy/schema/secrets.ts b/packages/proxy/schema/secrets.ts index 6361a1ab..89f76d33 100644 --- a/packages/proxy/schema/secrets.ts +++ b/packages/proxy/schema/secrets.ts @@ -131,7 +131,10 @@ export const MistralMetadataSchema = BaseMetadataSchema.merge( export const OllamaMetadataSchema = BaseMetadataSchema.merge( z.object({ - api_base: z.union([z.string().url(), z.string().length(0)]).nullish(), + api_base: z + .union([z.string().url(), z.string().length(0)]) + .nullish() + .catch(undefined), }), ).passthrough();