feat(core): map providers to integrations (#33562)
This commit is contained in:
parent
c556bddda3
commit
4898263dec
8 changed files with 201 additions and 140 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue