Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/session/runner/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
36 changes: 36 additions & 0 deletions packages/core/test/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions packages/core/test/session-runner-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading