feat(core): map providers to integrations (#33562)

This commit is contained in:
Dax 2026-06-23 20:53:31 -04:00 committed by GitHub
commit 4898263dec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 201 additions and 140 deletions

View file

@ -76,6 +76,35 @@ describe("CatalogV2", () => {
}).pipe(Effect.provide(layer))
})
it.effect("derives availability from a provider's integration", () => {
const integrationID = Integration.ID.make("gateway")
const providerID = ProviderV2.ID.make("remote")
const layer = Catalog.locationLayer.pipe(
Layer.fresh,
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
)
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()).toEqual([])
yield* (yield* Credential.Service).create({
integrationID,
value: new Credential.Key({ type: "key", key: "secret" }),
})
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID])
}).pipe(Effect.provide(layer))
})
it.effect("projects environment connections without a catalog plugin", () =>
Effect.acquireUseRelease(
Effect.sync(() => {

View file

@ -73,13 +73,34 @@ describe("OpencodePlugin", () => {
port: 0,
fetch: (request) => {
authorization.push(request.headers.get("authorization"))
const origin = new URL(request.url).origin
return Response.json({
config: {
providers: {
enterprise: { url: origin },
provider: {
remote: {
name: "Remote",
npm: "@ai-sdk/openai-compatible",
api: `${origin}/v1`,
env: ["REMOTE_API_KEY"],
options: {
apiKey: "{env:REMOTE_API_KEY}",
headers: { "x-org-id": "org" },
custom: "value",
},
models: {
model: { name: "Remote Model" },
model: {
name: "Remote Model",
family: "remote",
release_date: "2026-01-02",
tool_call: true,
modalities: { input: ["text", "image"], output: ["text"] },
options: { apiKey: "model-secret", temperature: 0.5 },
variants: { high: { apiKey: "variant-secret", temperature: 0.2 } },
cost: { input: 1, output: 2, cache_read: 0.1 },
limit: { context: 1000, output: 100 },
},
disabled: { name: "Disabled", status: "deprecated" },
},
},
},
@ -92,6 +113,11 @@ describe("OpencodePlugin", () => {
({ authorization, server }) =>
Effect.gen(function* () {
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* catalog.transform((draft) => {
draft.provider.update(ProviderV2.ID.make("remote"), () => {})
draft.model.update(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"), () => {})
})
yield* credentials.create({
integrationID: Integration.ID.make("opencode"),
value: new Credential.Key({
@ -103,11 +129,42 @@ describe("OpencodePlugin", () => {
yield* addPlugin()
const catalog = yield* Catalog.Service
expect((yield* catalog.provider.get(ProviderV2.ID.make("remote")))?.name).toBe("Remote")
expect((yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))?.name).toBe(
"Remote Model",
)
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("remote")))
expect(provider).toMatchObject({
name: "Remote",
integrationID: "opencode",
api: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: `${server.url.origin}/v1`,
},
})
expect(provider.request).toEqual({ headers: { "x-org-id": "org" }, body: { custom: "value" } })
expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined()
const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model")))
expect(model).toMatchObject({
name: "Remote Model",
family: "remote",
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }],
limit: { context: 1000, output: 100 },
})
expect(model.request).toMatchObject({ body: { custom: "value" }, generation: { temperature: 0.5 } })
expect(model.request.body).toEqual({ custom: "value" })
expect(model.variants).toEqual([
{
id: ModelV2.VariantID.make("high"),
headers: {},
body: {},
generation: { temperature: 0.2 },
options: {},
},
])
expect(
required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("disabled"))).enabled,
).toBe(false)
expect(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"))).toBeUndefined()
expect(authorization).toContain("Bearer secret")
}),
({ server }) => Effect.promise(() => server.stop(true)),

View file

@ -1,10 +1,9 @@
import { describe, expect } from "bun:test"
import { LLM } from "@opencode-ai/llm"
import { LLMClient } from "@opencode-ai/llm/route"
import { ConfigProvider, DateTime, Effect } from "effect"
import { DateTime, Effect } from "effect"
import { Headers } from "effect/unstable/http"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ProjectV2 } from "@opencode-ai/core/project"
@ -262,27 +261,23 @@ describe("SessionRunnerModel", () => {
}),
)
it.effect("preserves environment-backed bearer auth", () =>
it.effect("uses resolved credentials for bearer auth", () =>
Effect.gen(function* () {
const resolved = yield* SessionRunnerModel.fromCatalogModel(
new ModelV2.Info({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: {}, generation: {}, options: {} },
}),
{ type: "env", name: "TEST_PROVIDER_API_KEY" },
new Credential.Key({ type: "key", key: "secret" }),
)
const request = LLM.request({ model: resolved, prompt: "Hello" })
const headers = yield* resolved.route.auth
.apply({
request,
method: "POST",
url: "https://openai.example/v1/responses",
body: "{}",
headers: Headers.empty,
})
.pipe(
Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: { TEST_PROVIDER_API_KEY: "secret" } }))),
)
const headers = yield* resolved.route.auth.apply({
request,
method: "POST",
url: "https://openai.example/v1/responses",
body: "{}",
headers: Headers.empty,
})
expect(headers.authorization).toBe("Bearer secret")
}),
@ -290,18 +285,12 @@ describe("SessionRunnerModel", () => {
it.effect("prefers stored credentials over configured auth", () =>
Effect.gen(function* () {
const credential = new Credential.Info({
id: Credential.ID.create(),
integrationID: Integration.ID.make("test-provider"),
label: "Work",
value: new Credential.Key({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }),
})
const credential = new Credential.Key({ type: "key", key: "stored-secret", metadata: { tenant: "work" } })
const resolved = yield* SessionRunnerModel.fromCatalogModel(
new ModelV2.Info({
...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }),
request: { headers: {}, body: { apiKey: "configured-secret" }, generation: {}, options: {} },
}),
{ type: "credential", id: credential.id, label: credential.label },
credential,
)
const headers = yield* resolved.route.auth.apply({