Rename v2 auth service to account (#28260)

This commit is contained in:
Dax
2026-05-21 00:53:27 +00:00
committed by GitHub
parent bd41dac88f
commit 8643c0721e
67 changed files with 2541 additions and 1379 deletions
@@ -1,7 +1,9 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
@@ -20,27 +22,30 @@ describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to endpoint URL", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("amazon-bedrock", {
options: {
headers: {},
body: {},
aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} },
},
}),
cancel: false,
},
)
expect(result.provider.endpoint).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const bedrock = provider("amazon-bedrock", {
endpoint: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
options: {
headers: {},
body: {},
aisdk: { provider: { endpoint: "https://bedrock.example" }, request: {} },
},
})
catalog.provider.update(bedrock.id, (item) => {
item.endpoint = bedrock.endpoint
item.options = bedrock.options
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)
expect(result.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
package: "@ai-sdk/amazon-bedrock",
url: "https://bedrock.example",
})
expect(result.provider.options.aisdk.provider.endpoint).toBeUndefined()
expect(result.options.aisdk.provider.endpoint).toBeUndefined()
}),
)
@@ -1,37 +1,43 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper"
describe("AnthropicPlugin", () => {
it.effect("applies legacy beta headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("anthropic", {
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.headers["anthropic-beta"]).toBe(
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("anthropic", {
endpoint: { type: "aisdk", package: "@ai-sdk/anthropic" },
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
)
expect(result.provider.options.headers.Existing).toBe("1")
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).options.headers.Existing).toBe("1")
}),
)
it.effect("ignores non-Anthropic providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(result.provider.options.headers["anthropic-beta"]).toBeUndefined()
const load = yield* catalog.loader()
yield* load((catalog) => catalog.provider.update(provider("openai").id, () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.headers["anthropic-beta"]).toBeUndefined()
}),
)
@@ -1,7 +1,9 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
describe("AzureCognitiveServicesPlugin", () => {
@@ -9,20 +11,22 @@ describe("AzureCognitiveServicesPlugin", () => {
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: "cognitive" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("azure-cognitive-services"), cancel: false },
)
expect(result.provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
})
})
expect(result.provider.options.aisdk.provider.baseURL).toBe(
"https://cognitive.cognitiveservices.azure.com/openai",
)
expect(result.provider.options.aisdk.provider.resourceName).toBeUndefined()
const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
expect(result.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://cognitive.cognitiveservices.azure.com/openai",
})
expect(result.options.aisdk.provider.baseURL).toBeUndefined()
expect(result.options.aisdk.provider.resourceName).toBeUndefined()
}),
),
)
@@ -31,17 +35,27 @@ describe("AzureCognitiveServicesPlugin", () => {
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const azure = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("azure-cognitive-services"), cancel: false },
)
const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(azure.provider.options.aisdk.provider.baseURL).toBeUndefined()
expect(azure.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" })
expect(other.provider.options.aisdk.provider.baseURL).toBeUndefined()
expect(other.provider.endpoint).toEqual({ type: "aisdk", package: "test-provider" })
const load = yield* catalog.loader()
yield* load((catalog) => {
const azure = provider("azure-cognitive-services", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
})
const openai = provider("openai")
catalog.provider.update(azure.id, (item) => {
item.endpoint = azure.endpoint
})
catalog.provider.update(openai.id, (item) => {
item.endpoint = openai.endpoint
})
})
const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
const openai = yield* catalog.provider.get(ProviderV2.ID.openai)
expect(azure.options.aisdk.provider.baseURL).toBeUndefined()
expect(azure.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
expect(openai.options.aisdk.provider.baseURL).toBeUndefined()
expect(openai.endpoint).toEqual({ type: "aisdk", package: "test-provider" })
}),
),
)
@@ -1,22 +1,40 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AuthV2 } from "@opencode-ai/core/auth"
import { AccountV2 } from "@opencode-ai/core/account"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AuthPlugin } from "@opencode-ai/core/plugin/auth"
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer))
const itWithAccount = testEffect(
Catalog.layer.pipe(
Layer.provideMerge(PluginV2.defaultLayer),
Layer.provideMerge(AccountV2.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
Layer.provideMerge(npmLayer),
),
)
describe("AzurePlugin", () => {
it.effect("resolves resourceName from env", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false })
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
@@ -25,25 +43,29 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const azure = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } },
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) => {
const azure = provider("azure", {
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "from-config" }, request: {} } },
})
catalog.provider.update(azure.id, (item) => {
item.endpoint = azure.endpoint
item.options = azure.options
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe(
"from-config",
)
const other = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
expect(azure.provider.options.aisdk.provider.resourceName).toBe("from-config")
expect(other.provider.options.aisdk.provider.resourceName).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.resourceName).toBeUndefined()
}),
),
)
itWithAuth.effect("prefers auth resourceName over env", () =>
itWithAccount.effect("prefers account resourceName over env", () =>
withEnv(
{
AZURE_RESOURCE_NAME: "from-env",
@@ -51,23 +73,36 @@ describe("AzurePlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("azure"),
credential: new AuthV2.ApiKeyCredential({
const accounts = yield* AccountV2.Service
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
yield* accounts.create({
serviceID: AccountV2.ServiceID.make("azure"),
credential: new AccountV2.ApiKeyCredential({
type: "api",
key: "key",
metadata: { resourceName: "from-auth" },
metadata: { resourceName: "from-account" },
}),
active: true,
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
...AccountPlugin,
effect: AccountPlugin.effect.pipe(
Effect.provideService(AccountV2.Service, accounts),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(EventV2.Service, events),
Effect.provideService(PluginV2.Service, plugin),
),
})
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("azure"), cancel: false })
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-auth")
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.endpoint = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe(
"from-account",
)
}),
),
)
@@ -76,18 +111,20 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
const load = yield* catalog.loader()
yield* load((catalog) => {
const azure = provider("azure", {
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: "" }, request: {} } },
})
catalog.provider.update(azure.id, (item) => {
item.endpoint = azure.endpoint
item.options = azure.options
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
@@ -96,18 +133,20 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("azure", {
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.aisdk.provider.resourceName).toBe("from-env")
const load = yield* catalog.loader()
yield* load((catalog) => {
const azure = provider("azure", {
endpoint: { type: "aisdk", package: "@ai-sdk/azure" },
options: { headers: {}, body: {}, aisdk: { provider: { resourceName: " " }, request: {} } },
})
catalog.provider.update(azure.id, (item) => {
item.endpoint = azure.endpoint
item.options = azure.options
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).options.aisdk.provider.resourceName).toBe("from-env")
}),
),
)
@@ -1,7 +1,9 @@
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper"
const cerebrasOptions: Record<string, unknown>[] = []
@@ -20,27 +22,27 @@ describe("CerebrasPlugin", () => {
it.effect("applies the legacy integration header", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cerebras", {
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode" })
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.endpoint = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.options.headers.Existing = "1"
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).options.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode" })
}),
)
it.effect("ignores non-Cerebras providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("groq"), cancel: false })
expect(result.provider.options.headers).toEqual({})
const load = yield* catalog.loader()
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).options.headers).toEqual({})
}),
)
@@ -1,14 +1,26 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { AuthV2 } from "@opencode-ai/core/auth"
import { AccountV2 } from "@opencode-ai/core/account"
import { Catalog } from "@opencode-ai/core/catalog"
import { Location } from "@opencode-ai/core/location"
import { EventV2 } from "@opencode-ai/core/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AuthPlugin } from "@opencode-ai/core/plugin/auth"
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer))
const itWithAccount = testEffect(
Catalog.layer.pipe(
Layer.provideMerge(PluginV2.defaultLayer),
Layer.provideMerge(AccountV2.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
Layer.provideMerge(npmLayer),
),
)
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
@@ -34,22 +46,25 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("cloudflare-workers-ai"), cancel: false },
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.endpoint = { type: "aisdk", package: "test-provider" }
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))
const sdk = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("cloudflare-workers-ai", "@cf/model", { endpoint: updated.provider.endpoint }),
model: model("cloudflare-workers-ai", "@cf/model", { endpoint: provider.endpoint }),
package: "@ai-sdk/openai-compatible",
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
},
{},
)
expect(updated.provider.endpoint).toEqual({
expect(provider.endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1",
@@ -63,18 +78,15 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cloudflare-workers-ai", {
endpoint: { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" },
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.endpoint = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
}),
)
expect(result.provider.endpoint).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://proxy.example/v1",
@@ -104,7 +116,7 @@ describe("CloudflareWorkersAIPlugin", () => {
),
)
itWithAuth.effect("falls back to auth account metadata when account env is absent", () =>
itWithAccount.effect("falls back to account metadata when account env is absent", () =>
withEnv(
{
CLOUDFLARE_ACCOUNT_ID: undefined,
@@ -113,30 +125,37 @@ describe("CloudflareWorkersAIPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("cloudflare-workers-ai"),
credential: new AuthV2.ApiKeyCredential({
const accounts = yield* AccountV2.Service
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
yield* accounts.create({
serviceID: AccountV2.ServiceID.make("cloudflare-workers-ai"),
credential: new AccountV2.ApiKeyCredential({
type: "api",
key: "auth-key",
metadata: { accountId: "auth-acct" },
key: "account-key",
metadata: { accountId: "account-acct" },
}),
active: true,
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
...AccountPlugin,
effect: AccountPlugin.effect.pipe(
Effect.provideService(AccountV2.Service, accounts),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(EventV2.Service, events),
Effect.provideService(PluginV2.Service, plugin),
),
})
yield* plugin.add(CloudflareWorkersAIPlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("cloudflare-workers-ai"), cancel: false },
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.endpoint = { type: "aisdk", package: "test-provider" }
}),
)
expect(updated.provider.endpoint).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/auth-acct/ai/v1",
url: "https://api.cloudflare.com/client/v4/accounts/account-acct/ai/v1",
})
}),
),
@@ -146,18 +165,16 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("cloudflare-workers-ai", {
options: { headers: {}, body: {}, aisdk: { provider: { accountId: "configured-acct" }, request: {} } },
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.endpoint = { type: "aisdk", package: "test-provider" }
provider.options.aisdk.provider.accountId = "configured-acct"
}),
)
expect(result.provider.endpoint).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).endpoint).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model } from "./provider-helper"
describe("GithubCopilotPlugin", () => {
@@ -145,29 +147,31 @@ describe("GithubCopilotPlugin", () => {
}),
)
it.effect("filters gpt-5-chat-latest before Copilot language selection", () =>
it.effect("disables gpt-5-chat-latest before Copilot language selection", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GithubCopilotPlugin)
const result = yield* plugin.trigger(
"model.update",
{},
{ model: model("github-copilot", "gpt-5-chat-latest"), cancel: false },
)
expect(result.cancel).toBe(true)
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect((yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
}),
)
it.effect("does not filter gpt-5-chat-latest for non-Copilot providers", () =>
it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GithubCopilotPlugin)
const result = yield* plugin.trigger(
"model.update",
{},
{ model: model("custom-copilot", "gpt-5-chat-latest"), cancel: false },
)
expect(result.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect((yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(true)
}),
)
@@ -1,11 +1,15 @@
import { describe, expect, mock } from "bun:test"
import { Effect, Layer } from "effect"
import { AuthV2 } from "@opencode-ai/core/auth"
import { AccountV2 } from "@opencode-ai/core/account"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AuthPlugin } from "@opencode-ai/core/plugin/auth"
import { AccountPlugin } from "@opencode-ai/core/plugin/account"
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { it, model, npmLayer, provider, withEnv } from "./provider-helper"
import { it, model, npmLayer, withEnv } from "./provider-helper"
const gitlabSDKOptions: Record<string, unknown>[] = []
@@ -22,7 +26,15 @@ void mock.module("gitlab-ai-provider", () => ({
isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact",
}))
const itWithAuth = testEffect(Layer.mergeAll(PluginV2.defaultLayer, AuthV2.defaultLayer, npmLayer))
const itWithAccount = testEffect(
Catalog.layer.pipe(
Layer.provideMerge(PluginV2.defaultLayer),
Layer.provideMerge(AccountV2.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))),
Layer.provideMerge(npmLayer),
),
)
describe("GitLabPlugin", () => {
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
@@ -141,7 +153,7 @@ describe("GitLabPlugin", () => {
}),
)
itWithAuth.effect("uses active API auth token over GITLAB_TOKEN", () =>
itWithAccount.effect("uses active account API token over GITLAB_TOKEN", () =>
withEnv(
{
GITLAB_TOKEN: "env-token",
@@ -150,33 +162,41 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("gitlab"),
credential: new AuthV2.ApiKeyCredential({ type: "api", key: "auth-token" }),
active: true,
const accounts = yield* AccountV2.Service
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
yield* accounts.create({
serviceID: AccountV2.ServiceID.make("gitlab"),
credential: new AccountV2.ApiKeyCredential({ type: "api", key: "account-token" }),
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
...AccountPlugin,
effect: AccountPlugin.effect.pipe(
Effect.provideService(AccountV2.Service, accounts),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(EventV2.Service, events),
Effect.provideService(PluginV2.Service, plugin),
),
})
yield* plugin.add(GitLabPlugin)
const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("gitlab"), cancel: false })
const load = yield* catalog.loader()
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: updated.provider.options.aisdk.provider,
options: provider.options.aisdk.provider,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("auth-token")
expect(gitlabSDKOptions[0].apiKey).toBe("account-token")
}),
),
)
itWithAuth.effect("uses active OAuth access token when no API auth exists", () =>
itWithAccount.effect("uses active account OAuth access token when no API token exists", () =>
withEnv(
{
GITLAB_TOKEN: undefined,
@@ -185,33 +205,41 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const auth = yield* AuthV2.Service
yield* auth.create({
serviceID: AuthV2.ServiceID.make("gitlab"),
credential: new AuthV2.OAuthCredential({
const accounts = yield* AccountV2.Service
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
yield* accounts.create({
serviceID: AccountV2.ServiceID.make("gitlab"),
credential: new AccountV2.OAuthCredential({
type: "oauth",
refresh: "refresh-token",
access: "oauth-token",
access: "account-oauth-token",
expires: 9999999999999,
}),
active: true,
})
yield* plugin.add({
...AuthPlugin,
effect: AuthPlugin.effect.pipe(Effect.provideService(AuthV2.Service, auth)),
...AccountPlugin,
effect: AccountPlugin.effect.pipe(
Effect.provideService(AccountV2.Service, accounts),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(EventV2.Service, events),
Effect.provideService(PluginV2.Service, plugin),
),
})
yield* plugin.add(GitLabPlugin)
const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("gitlab"), cancel: false })
const load = yield* catalog.loader()
yield* load((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: updated.provider.options.aisdk.provider,
options: provider.options.aisdk.provider,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("oauth-token")
expect(gitlabSDKOptions[0].apiKey).toBe("account-oauth-token")
}),
),
)
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GoogleVertexAnthropicPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
describe("GoogleVertexAnthropicPlugin", () => {
it.effect("resolves legacy project and location env on provider update", () =>
@@ -18,14 +20,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("google-vertex-anthropic"), cancel: false },
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
}),
)
expect(result.provider.options.aisdk.provider.project).toBe("cloud-project")
expect(result.provider.options.aisdk.provider.location).toBe("cloud-location")
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
expect(provider.options.aisdk.provider.project).toBe("cloud-project")
expect(provider.options.aisdk.provider.location).toBe("cloud-location")
}),
),
)
@@ -34,23 +39,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
withEnv({ GOOGLE_CLOUD_PROJECT: "env-project", GOOGLE_CLOUD_LOCATION: "env-location" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("google-vertex-anthropic", {
options: {
headers: {},
body: {},
aisdk: { provider: { project: "configured-project", location: "configured-location" }, request: {} },
},
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.options.aisdk.provider.project = "configured-project"
provider.options.aisdk.provider.location = "configured-location"
}),
)
expect(result.provider.options.aisdk.provider.project).toBe("configured-project")
expect(result.provider.options.aisdk.provider.location).toBe("configured-location")
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
expect(provider.options.aisdk.provider.project).toBe("configured-project")
expect(provider.options.aisdk.provider.location).toBe("configured-location")
}),
),
)
@@ -1,8 +1,10 @@
import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
const vertexOptions: Record<string, any>[] = []
@@ -43,24 +45,22 @@ describe("GoogleVertexPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("google-vertex", {
endpoint: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
},
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
expect(result.provider.options.aisdk.provider.project).toBe("google-cloud-project")
expect(result.provider.options.aisdk.provider.location).toBe("google-vertex-location")
expect(result.provider.endpoint).toEqual({
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
expect(provider.options.aisdk.provider.project).toBe("google-cloud-project")
expect(provider.options.aisdk.provider.location).toBe("google-vertex-location")
expect(provider.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://google-vertex-location-aiplatform.googleapis.com/v1/projects/google-cloud-project/locations/google-vertex-location",
@@ -84,21 +84,19 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
vertexOptions.length = 0
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("google-vertex", {
endpoint: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
},
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
yield* plugin.trigger(
"aisdk.sdk",
{
@@ -111,8 +109,8 @@ describe("GoogleVertexPlugin", () => {
{},
)
expect(updated.provider.options.aisdk.provider.project).toBe("vertex-project")
expect(updated.provider.endpoint).toEqual({
expect(provider.options.aisdk.provider.project).toBe("vertex-project")
expect(provider.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://europe-west4-aiplatform.googleapis.com/v1/projects/vertex-project/locations/europe-west4",
@@ -136,29 +134,24 @@ describe("GoogleVertexPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("google-vertex", {
endpoint: {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
},
options: {
headers: {},
body: {},
aisdk: { provider: { project: "config-project", location: "global" }, request: {} },
},
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.endpoint = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}",
}
provider.options.aisdk.provider.project = "config-project"
provider.options.aisdk.provider.location = "global"
}),
)
expect(result.provider.options.aisdk.provider.project).toBe("config-project")
expect(result.provider.options.aisdk.provider.location).toBe("global")
expect(result.provider.endpoint).toEqual({
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
expect(provider.options.aisdk.provider.project).toBe("config-project")
expect(provider.options.aisdk.provider.location).toBe("global")
expect(provider.endpoint).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://aiplatform.googleapis.com/v1/projects/config-project/locations/global",
@@ -180,19 +173,18 @@ describe("GoogleVertexPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("google-vertex", {
options: { headers: {}, body: {}, aisdk: { provider: { project: "config-project" }, request: {} } },
}),
cancel: false,
},
const load = yield* catalog.loader()
yield* load((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.endpoint = { type: "aisdk", package: "@ai-sdk/google-vertex" }
provider.options.aisdk.provider.project = "config-project"
}),
)
expect(result.provider.options.aisdk.provider.project).toBe("config-project")
expect(result.provider.options.aisdk.provider.location).toBe("us-central1")
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
expect(provider.options.aisdk.provider.project).toBe("config-project")
expect(provider.options.aisdk.provider.location).toBe("us-central1")
}),
),
)
+32 -1
View File
@@ -2,12 +2,16 @@ import { Npm } from "@opencode-ai/core/npm"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
export const npmLayer = Layer.succeed(
Npm.Service,
@@ -18,7 +22,34 @@ export const npmLayer = Layer.succeed(
}),
)
export const it = testEffect(Layer.mergeAll(PluginV2.defaultLayer, npmLayer))
export const catalogLayer = Layer.succeed(
Catalog.Service,
Catalog.Service.of({
loader: () => Effect.die("unexpected catalog.loader"),
provider: {
get: () => Effect.die("unexpected provider.get"),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
},
model: {
get: () => Effect.die("unexpected model.get"),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(Option.none<ModelV2.Info>()),
setDefault: () => Effect.die("unexpected model.setDefault"),
small: () => Effect.succeed(Option.none<ModelV2.Info>()),
},
}),
)
export const it = testEffect(
Catalog.layer.pipe(
Layer.provideMerge(PluginV2.defaultLayer),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(npmLayer),
),
)
export function provider(providerID: string, options?: Partial<ProviderV2.Info>) {
return new ProviderV2.Info({
+48 -40
View File
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
describe("KiloPlugin", () => {
@@ -18,73 +20,79 @@ describe("KiloPlugin", () => {
it.effect("applies legacy referer headers only to kilo", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("kilo", {
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("openrouter"), cancel: false })
expect(result.provider.options.headers).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const kilo = provider("kilo", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(kilo.id, (draft) => {
draft.endpoint = kilo.endpoint
draft.options = kilo.options
})
catalog.provider.update(provider("openrouter").id, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(ignored.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({})
}),
)
it.effect("uses the exact legacy Kilo header casing and set", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("kilo"), cancel: false })
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("kilo", { endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" } })
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
})
})
expect(result.provider.options.headers).toEqual({
const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo"))
expect(result.options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(result.provider.options.headers).not.toHaveProperty("http-referer")
expect(result.provider.options.headers).not.toHaveProperty("x-title")
expect(result.provider.options.headers).not.toHaveProperty("X-Source")
expect(result.options.headers).not.toHaveProperty("http-referer")
expect(result.options.headers).not.toHaveProperty("x-title")
expect(result.options.headers).not.toHaveProperty("X-Source")
}),
)
it.effect("uses the legacy provider-id guard instead of endpoint package matching", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const matchingID = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("kilo", {
endpoint: { type: "aisdk", package: "not-kilo" },
}),
cancel: false,
},
)
const matchingPackage = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("custom-kilo", {
endpoint: { type: "aisdk", package: "kilo" },
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const kilo = provider("kilo", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
})
catalog.provider.update(kilo.id, (draft) => {
draft.endpoint = kilo.endpoint
})
const custom = provider("custom-kilo", {
endpoint: { type: "aisdk", package: "kilo" },
})
catalog.provider.update(custom.id, (draft) => {
draft.endpoint = custom.endpoint
})
})
expect(matchingID.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(matchingPackage.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).options.headers).toEqual({})
}),
)
})
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
describe("LLMGatewayPlugin", () => {
@@ -18,46 +20,52 @@ describe("LLMGatewayPlugin", () => {
it.effect("applies legacy referer headers only to enabled llmgateway", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(LLMGatewayPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("llmgateway", {
enabled: { via: "env", name: "LLMGATEWAY_API_KEY" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const ignored = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("openrouter", {
enabled: { via: "env", name: "OPENROUTER_API_KEY" },
}),
cancel: false,
},
)
expect(result.provider.options.headers).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const llmgateway = provider("llmgateway", {
enabled: { via: "env", name: "LLMGATEWAY_API_KEY" },
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(llmgateway.id, (draft) => {
draft.enabled = llmgateway.enabled
draft.endpoint = llmgateway.endpoint
draft.options = llmgateway.options
})
const openrouter = provider("openrouter", {
enabled: { via: "env", name: "OPENROUTER_API_KEY" },
})
catalog.provider.update(openrouter.id, (draft) => {
draft.enabled = openrouter.enabled
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
})
expect(ignored.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({})
}),
)
it.effect("does not apply legacy headers to a disabled llmgateway provider", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(LLMGatewayPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("llmgateway"), cancel: false })
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("llmgateway", { endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" } })
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
})
})
expect(result.provider.enabled).toBe(false)
expect(result.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).enabled).toBe(false)
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).options.headers).toEqual({})
}),
)
})
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
describe("NvidiaPlugin", () => {
@@ -18,45 +20,48 @@ describe("NvidiaPlugin", () => {
it.effect("applies NVIDIA tracking headers only to nvidia", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("nvidia", {
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("openrouter"), cancel: false })
expect(result.provider.options.headers).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const nvidia = provider("nvidia", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(nvidia.id, (draft) => {
draft.endpoint = nvidia.endpoint
draft.options = nvidia.options
})
catalog.provider.update(provider("openrouter").id, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
})
expect(ignored.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({})
}),
)
it.effect("adds billing origin for custom NVIDIA endpoints", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("nvidia", {
endpoint: { type: "aisdk", package: "test-provider", url: "http://localhost:8000/v1" },
options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("nvidia", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
options: { headers: {}, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect(result.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -67,23 +72,25 @@ describe("NvidiaPlugin", () => {
it.effect("preserves an explicit NVIDIA billing origin header", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("nvidia", {
options: {
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
body: {},
aisdk: { provider: { baseURL: "https://integrate.api.nvidia.com/v1" }, request: {} },
},
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("nvidia", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
options: {
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
body: {},
aisdk: { provider: { baseURL: "https://integrate.api.nvidia.com/v1" }, request: {} },
},
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect(result.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
@@ -1,9 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider } from "./provider-helper"
describe("OpenAIPlugin", () => {
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
@@ -70,31 +72,37 @@ describe("OpenAIPlugin", () => {
}),
)
it.effect("cancels gpt-5-chat-latest during model updates", () =>
it.effect("disables gpt-5-chat-latest during catalog transforms", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenAIPlugin)
const normal = yield* plugin.trigger("model.update", {}, { model: model("openai", "gpt-5"), cancel: false })
const filtered = yield* plugin.trigger(
"model.update",
{},
{ model: model("openai", "gpt-5-chat-latest"), cancel: false },
)
expect(normal.cancel).toBe(false)
expect(filtered.cancel).toBe(true)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("openai", { endpoint: { type: "aisdk", package: "@ai-sdk/openai" } })
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
}),
)
it.effect("does not cancel gpt-5-chat-latest for non-OpenAI providers", () =>
it.effect("does not disable gpt-5-chat-latest for non-OpenAI providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenAIPlugin)
const result = yield* plugin.trigger(
"model.update",
{},
{ model: model("custom-openai", "gpt-5-chat-latest"), cancel: false },
)
expect(result.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("custom-openai")
catalog.provider.update(item.id, () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect((yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(true)
}),
)
})
@@ -12,19 +12,23 @@ const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0,
const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
describe("OpencodePlugin", () => {
it.effect("uses a public key and cancels paid models without credentials", () =>
it.effect("uses a public key and disables paid models without credentials", () =>
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBe("public")
expect(paid.cancel).toBe(true)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const paid = model("opencode", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public")
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
}),
),
)
@@ -33,14 +37,19 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
const free = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "free", { cost: cost(0) }), cancel: false },
)
expect(free.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const free = model("opencode", "free", { cost: cost(0) })
catalog.model.update(item.id, free.id, (draft) => {
draft.cost = [...free.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public")
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
}),
),
)
@@ -49,14 +58,19 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
const outputOnly = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "output-only", { cost: cost(0, 1) }), cancel: false },
)
expect(outputOnly.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) })
catalog.model.update(item.id, outputOnly.id, (draft) => {
draft.cost = [...outputOnly.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("public")
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(true)
}),
),
)
@@ -65,15 +79,19 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: "secret" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("opencode"), cancel: false })
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
expect(paid.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const paid = model("opencode", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined()
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -82,19 +100,21 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] }), cancel: false },
)
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
expect(paid.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] })
catalog.provider.update(item.id, (draft) => {
draft.env = [...item.env]
})
const paid = model("opencode", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined()
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -103,31 +123,30 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("opencode", {
options: {
headers: {},
body: {},
aisdk: {
provider: { apiKey: "configured" },
request: {},
},
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode", {
options: {
headers: {},
body: {},
aisdk: {
provider: { apiKey: "configured" },
request: {},
},
}),
cancel: false,
},
)
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBe("configured")
expect(paid.cancel).toBe(false)
},
})
catalog.provider.update(item.id, (draft) => {
draft.options = item.options
})
const paid = model("opencode", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBe("configured")
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -136,19 +155,21 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger(
"provider.update",
{},
{ provider: provider("opencode", { enabled: { via: "auth", service: "opencode" } }), cancel: false },
)
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("opencode", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
expect(paid.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("opencode", { enabled: { via: "account", service: "opencode" } })
catalog.provider.update(item.id, (draft) => {
draft.enabled = item.enabled
})
const paid = model("opencode", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).options.aisdk.provider.apiKey).toBeUndefined()
expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -157,15 +178,19 @@ describe("OpencodePlugin", () => {
withEnv({ OPENCODE_API_KEY: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpencodePlugin)
const updated = yield* plugin.trigger("provider.update", {}, { provider: provider("openai"), cancel: false })
const paid = yield* plugin.trigger(
"model.update",
{},
{ model: model("openai", "paid", { cost: cost(1) }), cancel: false },
)
expect(updated.provider.options.aisdk.provider.apiKey).toBeUndefined()
expect(paid.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("openai")
catalog.provider.update(item.id, () => {})
const paid = model("openai", "paid", { cost: cost(1) })
catalog.model.update(item.id, paid.id, (draft) => {
draft.cost = [...paid.cost]
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).options.aisdk.provider.apiKey).toBeUndefined()
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -175,18 +200,21 @@ describe("OpencodePlugin", () => {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode
yield* catalog.provider.update(providerID, () => {})
yield* catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = cost(1, 1)
model.time.released = DateTime.makeUnsafe(Date.now())
})
yield* catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = cost(10, 10)
model.time.released = DateTime.makeUnsafe(Date.now())
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [...cost(1, 1)]
model.time.released = DateTime.makeUnsafe(Date.now())
})
catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [...cost(10, 10)]
model.time.released = DateTime.makeUnsafe(Date.now())
})
})
const selected = yield* catalog.model.small(providerID)
@@ -1,8 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, model, provider } from "./provider-helper"
describe("OpenRouterPlugin", () => {
@@ -18,24 +21,27 @@ describe("OpenRouterPlugin", () => {
it.effect("applies legacy referer headers only to openrouter", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("openrouter", {
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("nvidia"), cancel: false })
expect(result.provider.options.headers).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const openrouter = provider("openrouter", {
endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(openrouter.id, (item) => {
item.endpoint = openrouter.endpoint
item.options = openrouter.options
})
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(ignored.provider.options.headers).toEqual({})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).options.headers).toEqual({})
}),
)
@@ -67,39 +73,43 @@ describe("OpenRouterPlugin", () => {
it.effect("filters OpenRouter's gpt-5 chat alias", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const result = yield* plugin.trigger(
"model.update",
{},
{ model: model("openrouter", "openai/gpt-5-chat"), cancel: false },
)
const regular = yield* plugin.trigger(
"model.update",
{},
{ model: model("openrouter", "openai/gpt-5"), cancel: false },
)
const ignored = yield* plugin.trigger(
"model.update",
{},
{ model: model("openai", "openai/gpt-5-chat"), cancel: false },
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const openrouter = provider("openrouter", { endpoint: { type: "aisdk", package: "@openrouter/ai-sdk-provider" } })
catalog.provider.update(openrouter.id, (item) => {
item.endpoint = openrouter.endpoint
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
for (const item of [
model("openrouter", "openai/gpt-5-chat"),
model("openrouter", "openai/gpt-5"),
model("openai", "openai/gpt-5-chat"),
]) {
catalog.model.update(item.providerID, item.id, () => {})
}
})
expect(result.cancel).toBe(true)
expect(regular.cancel).toBe(false)
expect(ignored.cancel).toBe(false)
expect((yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat"))).enabled).toBe(false)
expect((yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled).toBe(true)
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled).toBe(true)
}),
)
it.effect("does not filter gpt-5-chat-latest for non-OpenRouter providers", () =>
it.effect("does not disable gpt-5-chat-latest for non-OpenRouter providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const result = yield* plugin.trigger(
"model.update",
{},
{ model: model("custom-openrouter", "gpt-5-chat-latest"), cancel: false },
)
expect(result.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {})
catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
).toBe(true)
}),
)
})
@@ -1,25 +1,29 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper"
describe("VercelPlugin", () => {
it.effect("applies legacy lower-case referer headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("vercel", {
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
expect(result.provider.options.headers).toEqual({
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("vercel", {
endpoint: { type: "aisdk", package: "@ai-sdk/vercel" },
options: { headers: { Existing: "1" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).toEqual({
Existing: "1",
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
@@ -30,10 +34,17 @@ describe("VercelPlugin", () => {
it.effect("does not add legacy upper-case referer headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("vercel"), cancel: false })
expect(result.provider.options.headers).not.toHaveProperty("HTTP-Referer")
expect(result.provider.options.headers).not.toHaveProperty("X-Title")
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("vercel", { endpoint: { type: "aisdk", package: "@ai-sdk/vercel" } })
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty("HTTP-Referer")
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).options.headers).not.toHaveProperty("X-Title")
}),
)
@@ -54,9 +65,11 @@ describe("VercelPlugin", () => {
it.effect("ignores non-Vercel providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("gateway"), cancel: false })
expect(result.provider.options.headers).toEqual({})
const load = yield* catalog.loader()
yield* load((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).options.headers).toEqual({})
}),
)
})
@@ -1,8 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
describe("ZenmuxPlugin", () => {
@@ -18,30 +20,39 @@ describe("ZenmuxPlugin", () => {
it.effect("applies the exact legacy Zenmux headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("zenmux"), cancel: false })
expect(result.provider.options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(result.provider.options.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
expect(result.cancel).toBe(false)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("zenmux", { endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" } })
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))
expect(result.options.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(result.options.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
}),
)
it.effect("merges legacy Zenmux headers with existing headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("zenmux", {
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("zenmux", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect(result.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -52,23 +63,25 @@ describe("ZenmuxPlugin", () => {
it.effect("lets configured Zenmux legacy headers override defaults", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("zenmux", {
options: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
body: {},
aisdk: { provider: {}, request: {} },
},
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("zenmux", {
endpoint: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
options: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
body: {},
aisdk: { provider: {}, request: {} },
},
})
catalog.provider.update(item.id, (draft) => {
draft.endpoint = item.endpoint
draft.options = item.options
})
})
expect(result.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).options.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
@@ -78,23 +91,23 @@ describe("ZenmuxPlugin", () => {
it.effect("guards legacy Zenmux headers to the exact zenmux provider id", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const ignored = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("openrouter", {
options: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
body: {},
aisdk: { provider: {}, request: {} },
},
}),
cancel: false,
},
)
const load = yield* catalog.loader()
yield* load((catalog) => {
const item = provider("openrouter", {
options: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
body: {},
aisdk: { provider: {}, request: {} },
},
})
catalog.provider.update(item.id, (draft) => {
draft.options = item.options
})
})
expect(ignored.provider.options.headers).toEqual({
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).options.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})