From f8fbc9237f0cab110d271e332e7018243fca5218 Mon Sep 17 00:00:00 2001 From: Kevin Rajan Date: Tue, 15 Sep 2026 18:51:00 +0000 Subject: [PATCH] fix(core): treat empty apiKey as unset Empty string apiKey must not enable a provider or suppress Auth.none. Fixes #42790. --- packages/core/src/catalog.ts | 2 +- packages/core/src/session/runner/model.ts | 2 +- packages/core/test/catalog.test.ts | 36 +++++++++++++++++ .../core/test/session-runner-model.test.ts | 40 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 1945024b54e8..55cacad0c4e0 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -70,7 +70,7 @@ const layer = Layer.effect( const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { if (provider.disabled) return false - if (typeof provider.request.body.apiKey === "string") return true + if (typeof provider.request.body.apiKey === "string" && provider.request.body.apiKey !== "") return true if (integration?.connections.length) return true return provider.integrationID === undefined && !integration } diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 74e78120c20e..fb6a00414f06 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -84,7 +84,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { if (credential?.type === "key") return Auth.value(credential.key) if (credential?.type === "oauth") return Auth.value(credential.access) const value = model.request.body.apiKey ?? model.api.settings?.apiKey - if (typeof value === "string") return Auth.value(value) + if (typeof value === "string" && value !== "") return Auth.value(value) } const withDefaults = (model: ModelV2.Info, route: AnyRoute) => { diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 6c736cde1e15..dc3244f22c47 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -334,6 +334,42 @@ describe("CatalogV2", () => { }), ) + it.effect("treats missing and empty apiKey as unavailable without credentials", () => { + const integrationID = Integration.ID.make("gateway") + const providerID = ProviderV2.ID.make("remote") + const localCatalogLayer = Layer.fresh( + AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [ + [Location.node, locationLayer], + ]), + ) + + return Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* (yield* Integration.Service).transform((editor) => editor.update(integrationID, () => {})) + + yield* catalog.transform((editor) => + editor.provider.update(providerID, (provider) => { + provider.integrationID = integrationID + }), + ) + expect((yield* catalog.provider.available()).map((provider) => provider.id)).not.toContain(providerID) + + yield* catalog.transform((editor) => + editor.provider.update(providerID, (provider) => { + provider.request.body.apiKey = "" + }), + ) + expect((yield* catalog.provider.available()).map((provider) => provider.id)).not.toContain(providerID) + + yield* catalog.transform((editor) => + editor.provider.update(providerID, (provider) => { + provider.request.body.apiKey = "secret" + }), + ) + expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID) + }).pipe(Effect.provide(localCatalogLayer)) + }) + it.effect("removes providers denied by policy after loading", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index 49bbce95a381..f62effc92e8c 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -313,6 +313,46 @@ describe("SessionRunnerModel", () => { }), ) + it.effect("treats empty configured apiKey as Auth.none", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + ModelV2.Info.make({ + ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + request: { headers: {}, body: { apiKey: "" } }, + }), + ) + const headers = yield* resolved.route.auth.apply({ + request: LLM.request({ model: resolved, prompt: "Hello" }), + method: "POST", + url: "https://openai.example/v1/responses", + body: "{}", + headers: Headers.empty, + }) + + expect(headers.authorization).toBeUndefined() + }), + ) + + it.effect("treats omitted apiKey as Auth.none", () => + Effect.gen(function* () { + const resolved = yield* SessionRunnerModel.fromCatalogModel( + ModelV2.Info.make({ + ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), + request: { headers: {}, body: {} }, + }), + ) + const headers = yield* resolved.route.auth.apply({ + request: LLM.request({ model: resolved, prompt: "Hello" }), + method: "POST", + url: "https://openai.example/v1/responses", + body: "{}", + headers: Headers.empty, + }) + + expect(headers.authorization).toBeUndefined() + }), + ) + it.effect("rejects catalog APIs without a native route", () => Effect.gen(function* () { const failure = yield* SessionRunnerModel.fromCatalogModel(