refactor(core): simplify integration test fixtures (#33292)
This commit is contained in:
parent
cdc6d01c5a
commit
2bb4311042
76 changed files with 2865 additions and 1600 deletions
48
packages/core/test/plugin/fixture.ts
Normal file
48
packages/core/test/plugin/fixture.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||
import { Ripgrep } from "@opencode-ai/core/ripgrep"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { tempLocationLayer } from "../fixture/location"
|
||||
|
||||
export const PluginTestLayer = Layer.mergeAll(
|
||||
AgentV2.locationLayer,
|
||||
CommandV2.locationLayer,
|
||||
Catalog.locationLayer,
|
||||
FileSystem.locationLayer,
|
||||
PluginV2.locationLayer,
|
||||
Reference.locationLayer,
|
||||
SkillV2.locationLayer,
|
||||
).pipe(
|
||||
Layer.provideMerge(
|
||||
Layer.mergeAll(
|
||||
Credential.defaultLayer,
|
||||
EventV2.defaultLayer,
|
||||
FSUtil.defaultLayer,
|
||||
Global.defaultLayer,
|
||||
Layer.succeed(
|
||||
Npm.Service,
|
||||
Npm.Service.of({
|
||||
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
}),
|
||||
),
|
||||
RepositoryCache.defaultLayer,
|
||||
SkillDiscovery.defaultLayer,
|
||||
Ripgrep.defaultLayer,
|
||||
tempLocationLayer,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -24,11 +24,7 @@ const locationLayer = Layer.succeed(
|
|||
)
|
||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
||||
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
||||
const connections = Credential.layer.pipe(
|
||||
Layer.fresh,
|
||||
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
||||
Layer.provide(events),
|
||||
)
|
||||
const connections = Credential.defaultLayer.pipe(Layer.fresh)
|
||||
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
||||
const catalog = Catalog.layer.pipe(
|
||||
Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections, integrations)),
|
||||
|
|
|
|||
|
|
@ -3,17 +3,35 @@ import { createAlibaba } from "@ai-sdk/alibaba"
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: AlibabaPlugin.id, effect: AlibabaPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("AlibabaPlugin", () => {
|
||||
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AlibabaPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/alibaba",
|
||||
options: { name: "alibaba" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -23,10 +41,17 @@ describe("AlibabaPlugin", () => {
|
|||
it.effect("ignores non-Alibaba SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AlibabaPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
|
||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "alibaba" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -36,11 +61,14 @@ describe("AlibabaPlugin", () => {
|
|||
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AlibabaPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-alibaba", "qwen"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
|
||||
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/alibaba",
|
||||
options: { name: "custom-alibaba", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -56,8 +84,11 @@ describe("AlibabaPlugin", () => {
|
|||
it.effect("uses the old default languageModel(api.id) behavior", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AlibabaPlugin)
|
||||
const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
|
||||
yield* addPlugin()
|
||||
const item = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
|
||||
const language = result.sdk?.languageModel(item.api.id)
|
||||
expect(language?.modelId).toBe("qwen-plus")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,61 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: AmazonBedrockPlugin.id, effect: AmazonBedrockPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
fx,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
|
||||
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
|
||||
|
|
@ -28,11 +79,10 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
|
|||
describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("moves endpoint option to api URL", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const bedrock = provider("amazon-bedrock", {
|
||||
const bedrock = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
|
||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
|
||||
request: {
|
||||
headers: {},
|
||||
|
|
@ -44,6 +94,7 @@ describe("AmazonBedrockPlugin", () => {
|
|||
item.request = bedrock.request
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
|
||||
expect(result.api).toEqual({
|
||||
type: "aisdk",
|
||||
|
|
@ -58,11 +109,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: {
|
||||
name: "amazon-bedrock",
|
||||
|
|
@ -83,11 +137,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: {
|
||||
name: "amazon-bedrock",
|
||||
|
|
@ -117,11 +174,21 @@ describe("AmazonBedrockPlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.amazonBedrock,
|
||||
ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: { name: "amazon-bedrock" },
|
||||
},
|
||||
|
|
@ -137,11 +204,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: { name: "amazon-bedrock", region: "eu-west-1" },
|
||||
},
|
||||
|
|
@ -156,11 +226,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: { name: "amazon-bedrock" },
|
||||
},
|
||||
|
|
@ -175,11 +248,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: { name: "amazon-bedrock" },
|
||||
},
|
||||
|
|
@ -195,11 +271,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: {
|
||||
name: "amazon-bedrock",
|
||||
|
|
@ -224,11 +303,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: {
|
||||
name: "amazon-bedrock",
|
||||
|
|
@ -252,12 +334,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "openai.gpt-5.5", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("openai.gpt-5.5"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
||||
options: {
|
||||
|
|
@ -281,12 +368,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "openai.gpt-5.5", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("openai.gpt-5.5"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
||||
},
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
|
||||
|
|
@ -296,8 +388,16 @@ describe("AmazonBedrockPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "openai.gpt-oss-safeguard-120b", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.amazonBedrock,
|
||||
ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/amazon-bedrock/mantle",
|
||||
},
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: { region: "us-east-1" },
|
||||
|
|
@ -311,12 +411,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||
it.effect("ignores other Bedrock provider subpaths", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/anthropic" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/amazon-bedrock/anthropic",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock/anthropic",
|
||||
options: { name: "amazon-bedrock" },
|
||||
|
|
@ -340,11 +445,21 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.amazonBedrock,
|
||||
ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/amazon-bedrock",
|
||||
options: {
|
||||
name: "amazon-bedrock",
|
||||
|
|
@ -371,11 +486,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -384,7 +502,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: "eu-west-1" },
|
||||
},
|
||||
|
|
@ -393,7 +514,17 @@ describe("AmazonBedrockPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "global.anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.amazonBedrock,
|
||||
ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: "eu-west-1" },
|
||||
},
|
||||
|
|
@ -402,7 +533,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: "ap-northeast-1" },
|
||||
},
|
||||
|
|
@ -411,7 +545,10 @@ describe("AmazonBedrockPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: "ap-southeast-2" },
|
||||
},
|
||||
|
|
@ -432,11 +569,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -517,12 +657,15 @@ describe("AmazonBedrockPlugin", () => {
|
|||
expected: "au.anthropic.claude-sonnet-4-5",
|
||||
},
|
||||
]
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
for (const item of cases) {
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("amazon-bedrock", item.modelID),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
|
||||
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: item.region },
|
||||
},
|
||||
|
|
@ -537,11 +680,14 @@ describe("AmazonBedrockPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AmazonBedrockPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("openai", "anthropic.claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: { region: "eu-west-1" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +1,34 @@
|
|||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, it, model, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: AnthropicPlugin.id, effect: AnthropicPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
describe("AnthropicPlugin", () => {
|
||||
it.effect("applies legacy beta headers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, AnthropicPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("anthropic", {
|
||||
const item = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
|
||||
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||
request: { headers: { Existing: "1" }, body: {} },
|
||||
})
|
||||
|
|
@ -22,6 +37,7 @@ describe("AnthropicPlugin", () => {
|
|||
draft.request = item.request
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
|
||||
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
|
||||
)
|
||||
|
|
@ -31,10 +47,9 @@ describe("AnthropicPlugin", () => {
|
|||
|
||||
it.effect("ignores non-Anthropic providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, AnthropicPlugin)
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
|
||||
yield* addPlugin()
|
||||
expect(
|
||||
required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
|
||||
).toBeUndefined()
|
||||
|
|
@ -44,54 +59,43 @@ describe("AnthropicPlugin", () => {
|
|||
it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, AnthropicPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-anthropic", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("custom-anthropic"),
|
||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||
}),
|
||||
package: "@ai-sdk/anthropic",
|
||||
options: { name: "custom-anthropic", apiKey: "test" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(providers).toEqual(["custom-anthropic"])
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, AnthropicPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("anthropic-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("anthropic", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
|
||||
}),
|
||||
package: "@ai-sdk/anthropic",
|
||||
options: { name: "anthropic", apiKey: "test" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(providers).toEqual(["anthropic"])
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,23 +1,73 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: AzureCognitiveServicesPlugin.id, effect: AzureCognitiveServicesPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
fx,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("AzureCognitiveServicesPlugin", () => {
|
||||
it.effect("maps the resource env var to the Azure SDK baseURL", () =>
|
||||
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: "cognitive" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
|
||||
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
||||
expect(result.api).toEqual({
|
||||
type: "aisdk",
|
||||
|
|
@ -33,14 +83,16 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
it.effect("leaves baseURL unset without resource env and ignores other providers", () =>
|
||||
withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const azure = provider("azure-cognitive-services", {
|
||||
const azure = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
||||
})
|
||||
const openai = provider("openai")
|
||||
const openai = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
catalog.provider.update(azure.id, (item) => {
|
||||
item.api = azure.api
|
||||
})
|
||||
|
|
@ -48,6 +100,7 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
item.api = openai.api
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
|
||||
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
|
||||
expect(azure.request.body.baseURL).toBeUndefined()
|
||||
|
|
@ -62,11 +115,17 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure-cognitive-services", "deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("azure-cognitive-services"),
|
||||
ModelV2.ID.make("deployment"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: { useCompletionUrls: true },
|
||||
},
|
||||
|
|
@ -80,15 +139,32 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("azure-cognitive-services"),
|
||||
ModelV2.ID.make("deployment"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual(["responses:deployment"])
|
||||
|
|
@ -101,11 +177,17 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
const sdk = fakeSelectorSdk(calls)
|
||||
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure-cognitive-services", "messages-deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("azure-cognitive-services"),
|
||||
ModelV2.ID.make("messages-deployment"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -114,7 +196,13 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure-cognitive-services", "chat-deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("azure-cognitive-services"),
|
||||
ModelV2.ID.make("chat-deployment"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -123,7 +211,13 @@ describe("AzureCognitiveServicesPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure-cognitive-services", "language-deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("azure-cognitive-services"),
|
||||
ModelV2.ID.make("language-deployment"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: sdk.languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,23 +1,73 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: AzurePlugin.id, effect: AzurePlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
fx,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
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* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.azure, (item) => {
|
||||
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
|
||||
})
|
||||
})
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||
}),
|
||||
),
|
||||
|
|
@ -26,10 +76,10 @@ describe("AzurePlugin", () => {
|
|||
it.effect("keeps explicit resourceName over env and ignores other providers", () =>
|
||||
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
const azure = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
request: { headers: {}, body: { resourceName: "from-config" } },
|
||||
})
|
||||
|
|
@ -39,7 +89,7 @@ describe("AzurePlugin", () => {
|
|||
})
|
||||
catalog.provider.update(ProviderV2.ID.openai, () => {})
|
||||
})
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
|
||||
}),
|
||||
|
|
@ -49,10 +99,10 @@ describe("AzurePlugin", () => {
|
|||
it.effect("falls back to env when configured resourceName is blank", () =>
|
||||
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
const azure = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
request: { headers: {}, body: { resourceName: "" } },
|
||||
})
|
||||
|
|
@ -61,7 +111,7 @@ describe("AzurePlugin", () => {
|
|||
item.request = azure.request
|
||||
})
|
||||
})
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||
}),
|
||||
),
|
||||
|
|
@ -70,10 +120,10 @@ describe("AzurePlugin", () => {
|
|||
it.effect("falls back to env when configured resourceName is whitespace", () =>
|
||||
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) => {
|
||||
const azure = provider("azure", {
|
||||
const azure = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.azure),
|
||||
api: { type: "aisdk", package: "@ai-sdk/azure" },
|
||||
request: { headers: {}, body: { resourceName: " " } },
|
||||
})
|
||||
|
|
@ -82,7 +132,7 @@ describe("AzurePlugin", () => {
|
|||
item.request = azure.request
|
||||
})
|
||||
})
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
|
||||
}),
|
||||
),
|
||||
|
|
@ -92,11 +142,14 @@ describe("AzurePlugin", () => {
|
|||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("azure", "deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/azure",
|
||||
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
|
||||
},
|
||||
|
|
@ -111,11 +164,18 @@ describe("AzurePlugin", () => {
|
|||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
const exit = yield* plugin
|
||||
.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("azure", "deployment"), package: "@ai-sdk/azure", options: { name: "azure" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/azure",
|
||||
options: { name: "azure" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
.pipe(Effect.exit)
|
||||
|
|
@ -128,10 +188,17 @@ describe("AzurePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: { useCompletionUrls: true },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual(["chat:deployment"])
|
||||
|
|
@ -142,10 +209,17 @@ describe("AzurePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: { useCompletionUrls: true },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual(["chat:deployment"])
|
||||
|
|
@ -156,11 +230,13 @@ describe("AzurePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure", "deployment", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
request: { headers: {}, body: { useCompletionUrls: true } },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
|
|
@ -176,15 +252,29 @@ describe("AzurePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
|
||||
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual(["responses:deployment"])
|
||||
|
|
@ -200,11 +290,14 @@ describe("AzurePlugin", () => {
|
|||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" }
|
||||
}
|
||||
yield* addPlugin(plugin, AzurePlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("azure", "messages-deployment"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
|
||||
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -212,7 +305,14 @@ describe("AzurePlugin", () => {
|
|||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("azure", "language-deployment"), sdk: { languageModel: make("languageModel") }, options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
|
||||
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: make("languageModel") },
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import { describe, expect, mock } 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, it, model, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const cerebrasOptions: Record<string, unknown>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: CerebrasPlugin.id, effect: CerebrasPlugin.effect(host) })
|
||||
})
|
||||
|
||||
void mock.module("@ai-sdk/cerebras", () => ({
|
||||
createCerebras: (options: Record<string, unknown>) => {
|
||||
|
|
@ -21,16 +31,15 @@ void mock.module("@ai-sdk/cerebras", () => ({
|
|||
describe("CerebrasPlugin", () => {
|
||||
it.effect("applies the legacy integration header", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, CerebrasPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
|
||||
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
|
||||
item.request.headers.Existing = "1"
|
||||
})
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({
|
||||
Existing: "1",
|
||||
"X-Cerebras-3rd-Party-Integration": "opencode",
|
||||
})
|
||||
|
|
@ -39,11 +48,10 @@ describe("CerebrasPlugin", () => {
|
|||
|
||||
it.effect("ignores non-Cerebras providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, CerebrasPlugin)
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -51,11 +59,21 @@ describe("CerebrasPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CerebrasPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("custom-cerebras"),
|
||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/cerebras",
|
||||
options: { name: "custom-cerebras", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -70,11 +88,21 @@ describe("CerebrasPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CerebrasPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("custom-cerebras"),
|
||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/cerebras",
|
||||
options: { name: "configured-cerebras", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -88,11 +116,21 @@ describe("CerebrasPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CerebrasPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("custom-cerebras"),
|
||||
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/groq",
|
||||
options: { name: "custom-cerebras", apiKey: "test" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,41 @@
|
|||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
|
||||
import { addPlugin, it, model, withEnv } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: CloudflareAIGatewayPlugin.id, effect: CloudflareAIGatewayPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
fx,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const aiGatewayCalls: Record<string, unknown>[] = []
|
||||
const unifiedCalls: string[] = []
|
||||
|
|
@ -78,11 +111,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
@ -98,12 +137,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: {
|
||||
name: "cloudflare-ai-gateway",
|
||||
|
|
@ -142,12 +187,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: {
|
||||
name: "cloudflare-ai-gateway",
|
||||
|
|
@ -171,12 +222,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: {
|
||||
name: "cloudflare-ai-gateway",
|
||||
|
|
@ -208,12 +265,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: {
|
||||
name: "cloudflare-ai-gateway",
|
||||
|
|
@ -239,12 +302,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
@ -261,12 +330,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
@ -284,12 +359,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
@ -313,12 +394,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
|
||||
},
|
||||
|
|
@ -336,12 +423,22 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "anthropic/claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "ai-gateway-provider",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
@ -364,12 +461,18 @@ describe("CloudflareAIGatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-ai-gateway", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("cloudflare-ai-gateway"),
|
||||
ModelV2.ID.make("openai/gpt-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-ai-gateway" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,9 +3,59 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: CloudflareWorkersAIPlugin.id, effect: CloudflareWorkersAIPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() =>
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
|
||||
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
|
||||
|
|
@ -37,12 +87,15 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
provider.api = { type: "aisdk", package: "test-provider" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "@cf/model", { api: provider.api }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
|
||||
},
|
||||
|
|
@ -61,14 +114,13 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
it.effect("preserves a configured endpoint URL instead of deriving one from account ID", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
|
|
@ -82,12 +134,18 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "@cf/model", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("@cf/model"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://proxy.example/v1",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
|
||||
|
|
@ -102,7 +160,6 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
it.effect("uses env account ID over configured account ID", () =>
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
|
|
@ -110,7 +167,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
provider.request.body.accountId = "configured-acct"
|
||||
}),
|
||||
)
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
|
|
@ -124,12 +181,18 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "@cf/model", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("@cf/model"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://proxy.example/v1",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
|
|
@ -153,12 +216,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "@cf/model", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("@cf/model"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
|
|
@ -183,11 +248,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "alias", { api: { id: ModelV2.ID.make("@cf/api-model") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -202,12 +270,18 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "@cf/model", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/anthropic", url: "https://proxy.example/v1" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("@cf/model"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/anthropic",
|
||||
url: "https://proxy.example/v1",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/anthropic",
|
||||
options: { name: "cloudflare-workers-ai" },
|
||||
|
|
|
|||
|
|
@ -2,10 +2,34 @@ import { describe, expect, mock } from "bun:test"
|
|||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
|
||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const cohereOptions: Record<string, any>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: CoherePlugin.id, effect: CoherePlugin.effect(host) })
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
void mock.module("@ai-sdk/cohere", () => ({
|
||||
createCohere: (options: Record<string, any>) => {
|
||||
|
|
@ -24,18 +48,32 @@ describe("CoherePlugin", () => {
|
|||
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CoherePlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("cohere", "command"), package: "@ai-sdk/openai-compatible", options: { name: "cohere" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
||||
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cohere" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(ignored.sdk).toBeUndefined()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("cohere", "command"), package: "@ai-sdk/cohere", options: { name: "cohere" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
|
||||
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/cohere",
|
||||
options: { name: "cohere" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -45,11 +83,14 @@ describe("CoherePlugin", () => {
|
|||
it.effect("uses the model provider ID as the bundled SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, CoherePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-cohere", "command-r-plus"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
|
||||
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/cohere",
|
||||
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
|
||||
},
|
||||
|
|
@ -70,10 +111,17 @@ describe("CoherePlugin", () => {
|
|||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
const sdk = fakeSelectorSdk(calls)
|
||||
yield* addPlugin(plugin, CoherePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk,
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const itAISDK = testEffect(
|
||||
Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
|
||||
)
|
||||
const deepinfraOptions: Record<string, any>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const deepinfraOptions: Record<string, unknown>[] = []
|
||||
const deepinfraLanguageModels: string[] = []
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: DeepInfraPlugin.id, effect: DeepInfraPlugin.effect(host) })
|
||||
})
|
||||
|
||||
void mock.module("@ai-sdk/deepinfra", () => ({
|
||||
createDeepInfra: (options: Record<string, any>) => {
|
||||
createDeepInfra: (options: Record<string, unknown>) => {
|
||||
const captured = { ...options }
|
||||
deepinfraOptions.push(captured)
|
||||
return {
|
||||
|
|
@ -36,10 +41,17 @@ describe("DeepInfraPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
package: "@ai-sdk/deepinfra",
|
||||
options: { name: "deepinfra" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -50,11 +62,14 @@ describe("DeepInfraPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-deepinfra", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
package: "@ai-sdk/deepinfra",
|
||||
options: { name: "custom-deepinfra", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -69,11 +84,14 @@ describe("DeepInfraPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("deepinfra", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
package: "@ai-sdk/deepinfra",
|
||||
options: { name: "deepinfra", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -88,7 +106,7 @@ describe("DeepInfraPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
||||
yield* addPlugin()
|
||||
const packages = [
|
||||
"unmatched-package",
|
||||
"@ai-sdk/deepinfra-compatible",
|
||||
|
|
@ -98,7 +116,14 @@ describe("DeepInfraPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("deepinfra", "model"), package: item, options: { name: "deepinfra" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
package: item,
|
||||
options: { name: "deepinfra" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(ignored.sdk).toBeUndefined()
|
||||
|
|
@ -106,7 +131,14 @@ describe("DeepInfraPlugin", () => {
|
|||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
package: "@ai-sdk/deepinfra",
|
||||
options: { name: "deepinfra" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -114,17 +146,36 @@ describe("DeepInfraPlugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
itAISDK.effect("uses the default languageModel selection for DeepInfra models", () =>
|
||||
it.effect("uses the default languageModel selection for DeepInfra models", () =>
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(plugin, DeepInfraPlugin)
|
||||
const language = yield* aisdk.language(
|
||||
model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
|
||||
}),
|
||||
yield* addPlugin()
|
||||
const sdkEvent = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("deepinfra"),
|
||||
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/deepinfra",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/deepinfra",
|
||||
options: { name: "deepinfra" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options },
|
||||
{},
|
||||
)
|
||||
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
|
||||
expect(language.provider).toBe("deepinfra.chat")
|
||||
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,43 +1,40 @@
|
|||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Cause, Effect, Layer, Option } from "effect"
|
||||
import { Cause, Effect, Layer } from "effect"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { host } from "./host"
|
||||
import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
|
||||
const fixtureProviderPath = fileURLToPath(fixtureProvider)
|
||||
const itWithAISDK = testEffect(
|
||||
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
|
||||
)
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const itWithAISDK = testEffect(AISDK.layer.pipe(Layer.provideMerge(PluginTestLayer)))
|
||||
|
||||
function npmEntrypointLayer(entrypoint?: string) {
|
||||
return Layer.succeed(
|
||||
Npm.Service,
|
||||
Npm.Service.of({
|
||||
add: () => Effect.succeed({ directory: "", entrypoint }),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
}),
|
||||
)
|
||||
function npmEntrypoint(entrypoint?: string) {
|
||||
return Npm.Service.of({
|
||||
add: () => Effect.succeed({ directory: "", entrypoint }),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
})
|
||||
}
|
||||
|
||||
function dynamicPlugin(layer = npmLayer) {
|
||||
return {
|
||||
const addPlugin = Effect.fn(function* (npm?: Npm.Interface) {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({
|
||||
id: DynamicProviderPlugin.id,
|
||||
effect: Effect.gen(function* () {
|
||||
yield* DynamicProviderPlugin.effect(host({ npm: yield* Npm.Service }))
|
||||
}).pipe(Effect.provide(layer)),
|
||||
}
|
||||
}
|
||||
effect: DynamicProviderPlugin.effect(npm ? { ...host, npm } : host),
|
||||
})
|
||||
})
|
||||
|
||||
function tempEntrypoint(source: string) {
|
||||
return Effect.acquireRelease(
|
||||
|
|
@ -55,11 +52,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
it.effect("creates an SDK from a provider factory export", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(dynamicPlugin())
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom", "test-model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
||||
}),
|
||||
package: fixtureProvider,
|
||||
options: { name: "custom", marker: "dynamic" },
|
||||
},
|
||||
|
|
@ -74,11 +74,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const sdk = { marker: "existing" }
|
||||
yield* plugin.add(dynamicPlugin())
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom", "test-model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
|
||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
||||
}),
|
||||
package: fixtureProvider,
|
||||
options: { name: "custom", marker: "dynamic" },
|
||||
},
|
||||
|
|
@ -91,11 +94,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
it.effect("injects the provider ID as the SDK factory name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(dynamicPlugin())
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-provider", "test-model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
|
||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
|
||||
}),
|
||||
package: fixtureProvider,
|
||||
options: { name: "custom-provider", marker: "dynamic" },
|
||||
},
|
||||
|
|
@ -108,11 +114,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
it.effect("loads npm packages through their resolved import entrypoint", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(fixtureProviderPath)))
|
||||
yield* addPlugin(npmEntrypoint(fixtureProviderPath))
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("npm-provider", "test-model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
|
||||
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
|
||||
}),
|
||||
package: "fixture-provider",
|
||||
options: { name: "npm-provider", marker: "npm" },
|
||||
},
|
||||
|
|
@ -126,9 +135,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer()))
|
||||
yield* addPlugin(npmEntrypoint())
|
||||
const exit = yield* aisdk
|
||||
.language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
||||
.language(
|
||||
new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.exit)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
|
||||
|
|
@ -139,10 +153,13 @@ describe("DynamicProviderPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* plugin.add(dynamicPlugin())
|
||||
yield* addPlugin()
|
||||
const exit = yield* aisdk
|
||||
.language(
|
||||
model("bad-import", "alias", { api: { type: "aisdk", package: "file:///missing/provider-factory.js" } }),
|
||||
new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.exit)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
|
|
@ -155,9 +172,14 @@ describe("DynamicProviderPlugin", () => {
|
|||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
|
||||
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(tmp.entrypoint)))
|
||||
yield* addPlugin(npmEntrypoint(tmp.entrypoint))
|
||||
const exit = yield* aisdk
|
||||
.language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
|
||||
.language(
|
||||
new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.exit)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
|
||||
|
|
@ -168,9 +190,10 @@ describe("DynamicProviderPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* plugin.add(dynamicPlugin())
|
||||
yield* addPlugin()
|
||||
const language = yield* aisdk.language(
|
||||
model("custom", "alias", {
|
||||
new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const gatewayCalls: Record<string, unknown>[] = []
|
||||
const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GatewayPlugin.id, effect: GatewayPlugin.effect(host) })
|
||||
})
|
||||
|
||||
mock.module("@ai-sdk/gateway", () => ({
|
||||
createGateway(options: Record<string, unknown>) {
|
||||
|
|
@ -27,10 +38,17 @@ describe("GatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GatewayPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("gateway", "model"), package: "@ai-sdk/gateway", options: { name: "gateway" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/gateway",
|
||||
options: { name: "gateway" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -42,12 +60,22 @@ describe("GatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("vercel", "anthropic/claude-sonnet-4"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("vercel"),
|
||||
ModelV2.ID.make("anthropic/claude-sonnet-4"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/gateway",
|
||||
options: { name: "vercel", apiKey: "test-key" },
|
||||
},
|
||||
|
|
@ -63,19 +91,33 @@ describe("GatewayPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GatewayPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
for (const modelID of vercelGatewayModels) {
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("vercel", modelID), package: "@ai-sdk/vercel", options: { name: "vercel" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
||||
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/vercel",
|
||||
options: { name: "vercel" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(ignored.sdk).toBeUndefined()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("vercel", modelID), package: "@ai-sdk/gateway", options: { name: "vercel" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
|
||||
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/gateway",
|
||||
options: { name: "vercel" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
|
|||
|
|
@ -3,19 +3,51 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, required } from "./provider-helper"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GithubCopilotPlugin.id, effect: GithubCopilotPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("GithubCopilotPlugin", () => {
|
||||
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("github-copilot", "gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "github-copilot" },
|
||||
},
|
||||
|
|
@ -24,7 +56,10 @@ describe("GithubCopilotPlugin", () => {
|
|||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("github-copilot", "gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/github-copilot",
|
||||
options: { name: "github-copilot" },
|
||||
},
|
||||
|
|
@ -39,11 +74,14 @@ describe("GithubCopilotPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("github-copilot", "claude-sonnet-4"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -57,11 +95,14 @@ describe("GithubCopilotPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("github-copilot", "alias", { api: { id: ModelV2.ID.make("claude-sonnet-4") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -75,30 +116,68 @@ describe("GithubCopilotPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("github-copilot", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("github-copilot", "gpt-5.1-codex"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
|
||||
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("github-copilot", "gpt-4o"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
|
||||
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("github-copilot", "gpt-5-mini"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
|
||||
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("github-copilot", "gpt-5-mini-2025-08-07"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("github-copilot"),
|
||||
ModelV2.ID.make("gpt-5-mini-2025-08-07"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual([
|
||||
|
|
@ -115,11 +194,14 @@ describe("GithubCopilotPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("github-copilot", "default", { api: { id: ModelV2.ID.make("gpt-5") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -128,7 +210,10 @@ describe("GithubCopilotPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("github-copilot", "small", { api: { id: ModelV2.ID.make("gpt-5-mini") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
|
||||
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -137,7 +222,10 @@ describe("GithubCopilotPlugin", () => {
|
|||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("github-copilot", "sonnet", { api: { id: ModelV2.ID.make("claude-sonnet-4") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -149,13 +237,12 @@ describe("GithubCopilotPlugin", () => {
|
|||
|
||||
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* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
|
||||
.enabled,
|
||||
|
|
@ -165,13 +252,12 @@ describe("GithubCopilotPlugin", () => {
|
|||
|
||||
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* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
|
||||
.enabled,
|
||||
|
|
@ -183,10 +269,17 @@ describe("GithubCopilotPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GithubCopilotPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("openai", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual([])
|
||||
|
|
|
|||
|
|
@ -1,12 +1,43 @@
|
|||
import { describe, expect, mock } 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, it, model, required, withEnv } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const gitlabSDKOptions: Record<string, unknown>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GitLabPlugin.id, effect: GitLabPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() =>
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
void mock.module("gitlab-ai-provider", () => ({
|
||||
VERSION: "test-version",
|
||||
|
|
@ -32,10 +63,17 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "gitlab-ai-provider",
|
||||
options: { name: "gitlab" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(gitlabSDKOptions).toHaveLength(1)
|
||||
|
|
@ -65,10 +103,17 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "gitlab-ai-provider",
|
||||
options: { name: "gitlab" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example")
|
||||
|
|
@ -86,11 +131,14 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("gitlab", "claude"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "gitlab-ai-provider",
|
||||
options: {
|
||||
name: "gitlab",
|
||||
|
|
@ -127,10 +175,17 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("gitlab", "claude"), package: "@ai-sdk/openai", options: { name: "gitlab" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai",
|
||||
options: { name: "gitlab" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -142,11 +197,13 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("gitlab", "duo-workflow-custom", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
|
||||
request: {
|
||||
headers: {},
|
||||
body: { workflowRef: "ref", workflowDefinition: "definition" },
|
||||
|
|
@ -178,11 +235,14 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("gitlab", "duo-workflow-exact"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
|
||||
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: {
|
||||
workflowChat: (id: string, options: unknown) => {
|
||||
calls.push([id, options])
|
||||
|
|
@ -205,11 +265,13 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("gitlab", "duo-workflow-custom", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
|
||||
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
|
||||
request: {
|
||||
headers: {},
|
||||
body: { featureFlags: { request_flag: true } },
|
||||
|
|
@ -234,11 +296,13 @@ describe("GitLabPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin(plugin, GitLabPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("gitlab", "claude", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
|
||||
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
|
||||
request: { headers: { h: "v" }, body: {} },
|
||||
}),
|
||||
sdk: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,50 @@
|
|||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: definition.id, effect: definition.effect(host) })
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function selector(calls: string[]) {
|
||||
return (id: string) => {
|
||||
calls.push(`languageModel:${id}`)
|
||||
return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
}
|
||||
|
||||
describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("resolves legacy project and location env on provider update", () =>
|
||||
|
|
@ -19,17 +59,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
},
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
||||
}),
|
||||
)
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
|
||||
expect(provider.request.body.project).toBe("cloud-project")
|
||||
expect(provider.request.body.location).toBe("cloud-location")
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
expect(
|
||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
|
||||
).toBe("cloud-project")
|
||||
expect(
|
||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
|
||||
).toBe("cloud-location")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -37,9 +79,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
it.effect("keeps configured project and location over env fallback", () =>
|
||||
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* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
|
||||
|
|
@ -47,9 +87,13 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
provider.request.body.location = "configured-location"
|
||||
}),
|
||||
)
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
|
||||
expect(provider.request.body.project).toBe("configured-project")
|
||||
expect(provider.request.body.location).toBe("configured-location")
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
|
||||
"configured-project",
|
||||
)
|
||||
expect(
|
||||
(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
|
||||
).toBe("configured-location")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -67,11 +111,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex-anthropic", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex-anthropic" },
|
||||
},
|
||||
|
|
@ -90,11 +140,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex-anthropic", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||
ModelV2.ID.make("claude-sonnet-4-5"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex-anthropic" },
|
||||
},
|
||||
|
|
@ -110,11 +166,14 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "eu" },
|
||||
},
|
||||
|
|
@ -129,11 +188,14 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "claude-sonnet-4-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
|
||||
},
|
||||
|
|
@ -146,12 +208,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const sdkResult = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", " claude-sonnet-4-5 "),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex/anthropic",
|
||||
options: { name: "google-vertex", project: "project", location: "us" },
|
||||
},
|
||||
|
|
@ -160,7 +225,10 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
const languageResult = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("google-vertex", " claude-sonnet-4-5 "),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
|
||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: sdkResult.sdk,
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -178,12 +246,18 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("google-vertex-anthropic", " claude-sonnet-4-5 "),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("google-vertex-anthropic"),
|
||||
ModelV2.ID.make(" claude-sonnet-4-5 "),
|
||||
),
|
||||
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: selector(calls) },
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
|
|
@ -196,12 +270,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("google-vertex", "claude-sonnet-4-5"),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: selector(calls) },
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
|
|
|
|||
|
|
@ -1,13 +1,63 @@
|
|||
import { describe, expect, mock } 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const vertexOptions: Record<string, any>[] = []
|
||||
const googleAuthOptions: Record<string, any>[] = []
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GoogleVertexPlugin.id, effect: GoogleVertexPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() =>
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
void mock.module("@ai-sdk/google-vertex", () => ({
|
||||
createVertex: (options: Record<string, any>) => {
|
||||
|
|
@ -37,9 +87,7 @@ void mock.module("google-auth-library", () => ({
|
|||
describe("GoogleVertexPlugin", () => {
|
||||
it.effect("ignores OpenAI-compatible providers that are not Google Vertex", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
|
||||
provider.api = {
|
||||
|
|
@ -49,6 +97,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
|
||||
expect(provider.request.body).toEqual({})
|
||||
|
|
@ -67,9 +116,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
},
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.api = {
|
||||
|
|
@ -79,6 +126,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
expect(provider.request.body.project).toBe("google-cloud-project")
|
||||
expect(provider.request.body.location).toBe("google-vertex-location")
|
||||
|
|
@ -107,7 +155,6 @@ describe("GoogleVertexPlugin", () => {
|
|||
vertexOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.api = {
|
||||
|
|
@ -117,12 +164,18 @@ describe("GoogleVertexPlugin", () => {
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "gemini", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/google-vertex" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("gemini"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/google-vertex",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex",
|
||||
options: { name: "google-vertex" },
|
||||
|
|
@ -154,9 +207,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
},
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.api = {
|
||||
|
|
@ -168,6 +219,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
provider.request.body.location = "global"
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
expect(provider.request.body.project).toBe("config-project")
|
||||
expect(provider.request.body.location).toBe("global")
|
||||
|
|
@ -182,9 +234,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
|
||||
it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.api = {
|
||||
|
|
@ -196,6 +246,7 @@ describe("GoogleVertexPlugin", () => {
|
|||
provider.request.body.location = "eu"
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
expect(provider.api).toEqual({
|
||||
type: "aisdk",
|
||||
|
|
@ -217,15 +268,14 @@ describe("GoogleVertexPlugin", () => {
|
|||
},
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
|
||||
provider.request.body.project = "config-project"
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
expect(provider.request.body.project).toBe("config-project")
|
||||
expect(provider.request.body.location).toBe("us-central1")
|
||||
|
|
@ -243,12 +293,17 @@ describe("GoogleVertexPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
vertexOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "gemini", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/google-vertex" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("gemini"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/google-vertex",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex",
|
||||
options: { name: "google-vertex" },
|
||||
|
|
@ -268,21 +323,17 @@ describe("GoogleVertexPlugin", () => {
|
|||
googleAuthOptions.length = 0
|
||||
const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("capture-openai-compatible"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.promise(async () => {
|
||||
if (evt.model.providerID !== "google-vertex") return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
expect(typeof evt.options.fetch).toBe("function")
|
||||
await evt.options.fetch("https://vertex.example", {
|
||||
headers: { "x-test": "1" },
|
||||
})
|
||||
}),
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (evt) =>
|
||||
Effect.promise(async () => {
|
||||
if (evt.model.providerID !== "google-vertex") return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
expect(typeof evt.options.fetch).toBe("function")
|
||||
await evt.options.fetch("https://vertex.example", {
|
||||
headers: { "x-test": "1" },
|
||||
})
|
||||
}),
|
||||
})
|
||||
)
|
||||
const originalFetch = fetch
|
||||
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = (async (
|
||||
input: Parameters<typeof fetch>[0],
|
||||
|
|
@ -297,8 +348,13 @@ describe("GoogleVertexPlugin", () => {
|
|||
plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("google-vertex", "gemini", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("gemini"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "google-vertex" },
|
||||
|
|
@ -322,11 +378,14 @@ describe("GoogleVertexPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, GoogleVertexPlugin)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("google-vertex", " gemini-2.5-pro "),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
|
||||
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const itWithAISDK = testEffect(
|
||||
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
|
||||
)
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GooglePlugin.id, effect: GooglePlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("GooglePlugin", () => {
|
||||
it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GooglePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-google", "gemini"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
|
||||
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
|
||||
}),
|
||||
package: "@ai-sdk/google",
|
||||
options: { name: "custom-google", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -34,34 +41,49 @@ describe("GooglePlugin", () => {
|
|||
it.effect("ignores non-Google SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GooglePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("google", "gemini"), package: "@ai-sdk/google-vertex", options: { name: "google" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
|
||||
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
|
||||
}),
|
||||
package: "@ai-sdk/google-vertex",
|
||||
options: { name: "google" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
itWithAISDK.effect("uses default languageModel loading with provider ID parity", () =>
|
||||
it.effect("uses default languageModel loading with provider ID parity", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(plugin, GooglePlugin)
|
||||
const language = yield* aisdk.language(
|
||||
model("custom-google", "alias", {
|
||||
api: {
|
||||
id: ModelV2.ID.make("gemini-api"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/google",
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: { apiKey: "test" },
|
||||
},
|
||||
}),
|
||||
yield* addPlugin()
|
||||
const sdkEvent = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
|
||||
}),
|
||||
package: "@ai-sdk/google",
|
||||
options: { name: "custom-google", apiKey: "test" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: sdkEvent.model,
|
||||
sdk: sdkEvent.sdk,
|
||||
options: sdkEvent.options,
|
||||
},
|
||||
{},
|
||||
)
|
||||
const language = result.language ?? result.sdk.languageModel(result.model.api.id)
|
||||
expect(language.modelId).toBe("gemini-api")
|
||||
expect(language.provider).toBe("custom-google")
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,26 +1,37 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { createGroq } from "@ai-sdk/groq"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const aisdkIt = testEffect(
|
||||
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
|
||||
)
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: GroqPlugin.id, effect: GroqPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("GroqPlugin", () => {
|
||||
it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GroqPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("groq", "llama"), package: "@ai-sdk/groq", options: { name: "groq" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
||||
}),
|
||||
package: "@ai-sdk/groq",
|
||||
options: { name: "groq" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -30,10 +41,17 @@ describe("GroqPlugin", () => {
|
|||
it.effect("ignores non-Groq SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GroqPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("groq", "llama"), package: "@ai-sdk/openai-compatible", options: { name: "groq" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "groq" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -43,10 +61,17 @@ describe("GroqPlugin", () => {
|
|||
it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GroqPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("groq", "llama"), package: "@ai-sdk/groq/compat", options: { name: "groq" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
|
||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
||||
}),
|
||||
package: "@ai-sdk/groq/compat",
|
||||
options: { name: "groq" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -56,11 +81,14 @@ describe("GroqPlugin", () => {
|
|||
it.effect("matches the old bundled Groq SDK provider naming", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, GroqPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-groq", "llama"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
|
||||
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
|
||||
}),
|
||||
package: "@ai-sdk/groq",
|
||||
options: { name: "custom-groq", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -75,26 +103,32 @@ describe("GroqPlugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
aisdkIt.effect("uses the default languageModel(api.id) behavior", () =>
|
||||
it.effect("uses the default languageModel(api.id) behavior", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(plugin, GroqPlugin)
|
||||
const result = yield* aisdk.language(
|
||||
model("groq", "alias", {
|
||||
api: {
|
||||
id: ModelV2.ID.make("llama-api"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/groq",
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: { apiKey: "test" },
|
||||
},
|
||||
}),
|
||||
yield* addPlugin()
|
||||
const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
|
||||
name: string
|
||||
})
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("llama-api"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/groq",
|
||||
},
|
||||
}),
|
||||
sdk,
|
||||
options: { name: "groq", apiKey: "test" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.modelId).toBe("llama-api")
|
||||
expect(result.provider).toBe("groq.chat")
|
||||
const language = result.language ?? sdk.languageModel(result.model.api.id)
|
||||
expect(language.modelId).toBe("llama-api")
|
||||
expect(language.provider).toBe("groq.chat")
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||
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 { Integration } from "@opencode-ai/core/integration"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
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 { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { aisdkHost, catalogHost, host, integrationHost } from "./host"
|
||||
|
||||
export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
|
||||
|
||||
export function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
|
||||
export const npmLayer = Layer.succeed(
|
||||
Npm.Service,
|
||||
Npm.Service.of({
|
||||
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
}),
|
||||
)
|
||||
|
||||
export const catalogLayer = Layer.succeed(
|
||||
Catalog.Service,
|
||||
Catalog.Service.of({
|
||||
transform: (_transform) => Effect.die("unexpected catalog.transform"),
|
||||
rebuild: () => Effect.die("unexpected catalog.rebuild"),
|
||||
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(undefined),
|
||||
small: () => Effect.succeed(undefined),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const integrations = Integration.locationLayer.pipe(
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(
|
||||
Layer.mock(Credential.Service)({
|
||||
create: () => Effect.die("unexpected credential creation"),
|
||||
all: () => Effect.succeed([]),
|
||||
list: () => Effect.succeed([]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export const it = testEffect(
|
||||
Catalog.locationLayer.pipe(
|
||||
Layer.provideMerge(integrations),
|
||||
Layer.provideMerge(
|
||||
Layer.mock(Credential.Service)({
|
||||
all: () => Effect.succeed([]),
|
||||
}),
|
||||
),
|
||||
Layer.provideMerge(EventV2.defaultLayer),
|
||||
Layer.provideMerge(locationLayer),
|
||||
Layer.provideMerge(npmLayer),
|
||||
Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
|
||||
),
|
||||
)
|
||||
|
||||
export function addPlugin(plugin: PluginV2.Interface, definition: Plugin<any>) {
|
||||
return Effect.gen(function* () {
|
||||
const catalog = yield* Effect.serviceOption(Catalog.Service)
|
||||
const integration = yield* Effect.serviceOption(Integration.Service)
|
||||
const npm = yield* Effect.serviceOption(Npm.Service)
|
||||
const effect =
|
||||
typeof definition.effect === "function"
|
||||
? definition.effect(
|
||||
host({
|
||||
aisdk: aisdkHost(plugin),
|
||||
...(Option.isSome(catalog) ? { catalog: catalogHost(catalog.value) } : {}),
|
||||
...(Option.isSome(integration) ? { integration: integrationHost(integration.value) } : {}),
|
||||
...(Option.isSome(npm) ? { npm: npm.value } : {}),
|
||||
}),
|
||||
)
|
||||
: definition.effect
|
||||
yield* plugin.add({ id: definition.id, effect })
|
||||
})
|
||||
}
|
||||
|
||||
type ProviderInput = Partial<Omit<ProviderV2.Info, "api" | "request">> & {
|
||||
api?: ProviderV2.Api
|
||||
request?: ProviderV2.Request
|
||||
}
|
||||
|
||||
type ModelInput = Partial<Omit<ModelV2.Info, "api" | "request">> & {
|
||||
api?: (ProviderV2.Api & { id?: ModelV2.ID }) | { id: ModelV2.ID }
|
||||
request?: ModelV2.Info["request"]
|
||||
}
|
||||
|
||||
export function provider(providerID: string, options?: ProviderInput) {
|
||||
return new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.make(providerID)),
|
||||
api: options?.api ?? {
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
...options,
|
||||
request: {
|
||||
headers: {},
|
||||
body: {},
|
||||
...options?.request,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function model(providerID: string, modelID: string, options?: ModelInput) {
|
||||
return new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||
...options,
|
||||
api:
|
||||
options?.api && "type" in options.api
|
||||
? { id: ModelV2.ID.make(modelID), ...options.api }
|
||||
: {
|
||||
id: ModelV2.ID.make(modelID),
|
||||
...options?.api,
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
request: {
|
||||
headers: {},
|
||||
body: {},
|
||||
...options?.request,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
return previous
|
||||
}),
|
||||
() => fx(),
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
export function expectPluginRegistered(ids: string[], id: string) {
|
||||
expect(ids).toContain(PluginV2.ID.make(id))
|
||||
}
|
||||
|
|
@ -2,96 +2,98 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
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 { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: KiloPlugin.id, effect: KiloPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("KiloPlugin", () => {
|
||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||
Effect.sync(() =>
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"kilo",
|
||||
),
|
||||
),
|
||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("kilo"))),
|
||||
)
|
||||
|
||||
it.effect("applies legacy referer headers only to kilo", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, KiloPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const kilo = provider("kilo", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
request: { headers: { Existing: "value" }, body: {} },
|
||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.kilo.ai/api/gateway",
|
||||
}
|
||||
provider.request = { headers: { Existing: "value" }, body: {} }
|
||||
})
|
||||
catalog.provider.update(kilo.id, (draft) => {
|
||||
draft.api = kilo.api
|
||||
draft.request = kilo.request
|
||||
})
|
||||
catalog.provider.update(provider("openrouter").id, () => {})
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
||||
Existing: "value",
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.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* addPlugin(plugin, KiloPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("kilo", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.kilo.ai/api/gateway",
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("kilo")))
|
||||
expect(result.request.headers).toEqual({
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
})
|
||||
expect(result.request.headers).not.toHaveProperty("http-referer")
|
||||
expect(result.request.headers).not.toHaveProperty("x-title")
|
||||
expect(result.request.headers).not.toHaveProperty("X-Source")
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
|
||||
"http-referer",
|
||||
)
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.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* addPlugin(plugin, KiloPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const kilo = provider("kilo", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
|
||||
catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.kilo.ai/api/gateway",
|
||||
}
|
||||
})
|
||||
catalog.provider.update(kilo.id, (draft) => {
|
||||
draft.api = kilo.api
|
||||
})
|
||||
const custom = provider("custom-kilo", {
|
||||
api: { type: "aisdk", package: "kilo" },
|
||||
})
|
||||
catalog.provider.update(custom.id, (draft) => {
|
||||
draft.api = custom.api
|
||||
catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "kilo" }
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,36 +3,28 @@ import { Effect } from "effect"
|
|||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
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, required } from "./provider-helper"
|
||||
import { catalogHost, host, integrationHost } from "./host"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: LLMGatewayPlugin.id, effect: LLMGatewayPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("LLMGatewayPlugin", () => {
|
||||
const add = Effect.fnUntraced(function* (plugin: PluginV2.Interface) {
|
||||
const integrations = yield* Integration.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add({
|
||||
...LLMGatewayPlugin,
|
||||
effect: LLMGatewayPlugin.effect(
|
||||
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||
Effect.sync(() =>
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"llmgateway",
|
||||
),
|
||||
),
|
||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("llmgateway"))),
|
||||
)
|
||||
|
||||
it.effect("applies legacy referer headers only to enabled llmgateway", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const integrations = yield* Integration.Service
|
||||
yield* integrations.transform((editor) => {
|
||||
|
|
@ -40,43 +32,48 @@ describe("LLMGatewayPlugin", () => {
|
|||
editor.update(Integration.ID.make("openrouter"), () => {})
|
||||
})
|
||||
yield* catalog.transform((catalog) => {
|
||||
const llmgateway = provider("llmgateway", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
|
||||
request: { headers: { Existing: "value" }, body: {} },
|
||||
})
|
||||
catalog.provider.update(llmgateway.id, (draft) => {
|
||||
draft.api = llmgateway.api
|
||||
draft.request = llmgateway.request
|
||||
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.llmgateway.io/v1",
|
||||
}
|
||||
provider.request = { headers: { Existing: "value" }, body: {} }
|
||||
})
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||
})
|
||||
yield* add(plugin)
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
|
||||
Existing: "value",
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-Source": "opencode",
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.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* add(plugin)
|
||||
const integrations = yield* Integration.Service
|
||||
yield* integrations.transform((editor) => {
|
||||
editor.update(Integration.ID.make("llmgateway"), () => {})
|
||||
})
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("llmgateway", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
|
||||
provider.disabled = true
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://api.llmgateway.io/v1",
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).disabled).toBeUndefined()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,18 +1,37 @@
|
|||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
|
||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: MistralPlugin.id, effect: MistralPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("MistralPlugin", () => {
|
||||
it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, MistralPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/mistral",
|
||||
options: { name: "mistral" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -22,11 +41,14 @@ describe("MistralPlugin", () => {
|
|||
it.effect("ignores non-Mistral SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, MistralPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("mistral", "mistral-large"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "mistral" },
|
||||
},
|
||||
|
|
@ -40,19 +62,22 @@ describe("MistralPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, MistralPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("mistral-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (event) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(event.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
})
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
|
||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/mistral",
|
||||
options: { name: "mistral" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -64,20 +89,19 @@ describe("MistralPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, MistralPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("mistral-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (event) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(event.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
})
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-mistral", "mistral-large"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
|
||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/mistral",
|
||||
options: { name: "custom-mistral" },
|
||||
},
|
||||
|
|
@ -91,11 +115,23 @@ describe("MistralPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
const sdk = fakeSelectorSdk(calls)
|
||||
yield* addPlugin(plugin, MistralPlugin)
|
||||
const sdk = {
|
||||
languageModel: (id: string) => {
|
||||
calls.push(`languageModel:${id}`)
|
||||
return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
},
|
||||
}
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("mistral", "alias", { api: { id: ModelV2.ID.make("mistral-large") } }), sdk, options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk,
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const language = result.language ?? sdk.languageModel(result.model.api.id)
|
||||
|
|
|
|||
|
|
@ -2,64 +2,66 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
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 { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: NvidiaPlugin.id, effect: NvidiaPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("NvidiaPlugin", () => {
|
||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||
Effect.sync(() =>
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"nvidia",
|
||||
),
|
||||
),
|
||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("nvidia"))),
|
||||
)
|
||||
|
||||
it.effect("applies NVIDIA tracking headers only to nvidia", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, NvidiaPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const nvidia = provider("nvidia", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
request: { headers: { Existing: "value" }, body: {} },
|
||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://integrate.api.nvidia.com/v1",
|
||||
}
|
||||
provider.request = { headers: { Existing: "value" }, body: {} }
|
||||
})
|
||||
catalog.provider.update(nvidia.id, (draft) => {
|
||||
draft.api = nvidia.api
|
||||
draft.request = nvidia.request
|
||||
})
|
||||
catalog.provider.update(provider("openrouter").id, () => {})
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
||||
Existing: "value",
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.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* addPlugin(plugin, NvidiaPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("nvidia", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
request: { headers: {}, body: {} },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
draft.request = item.request
|
||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://integrate.api.nvidia.com/v1",
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
|
||||
|
|
@ -69,24 +71,23 @@ 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* addPlugin(plugin, NvidiaPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("nvidia", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
|
||||
request: {
|
||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://integrate.api.nvidia.com/v1",
|
||||
}
|
||||
provider.request = {
|
||||
headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
|
||||
body: { baseURL: "https://integrate.api.nvidia.com/v1" },
|
||||
},
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
draft.request = item.request
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
|
||||
|
|
|
|||
|
|
@ -1,23 +1,45 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible"
|
||||
import { addPlugin, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: OpenAICompatiblePlugin.id, effect: OpenAICompatiblePlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("OpenAICompatiblePlugin", () => {
|
||||
it.effect("preserves explicit includeUsage false and defaults it to true", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, OpenAICompatiblePlugin)
|
||||
yield* addPlugin()
|
||||
const defaulted = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("custom", "model"), package: "@ai-sdk/openai-compatible", options: { name: "custom" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "custom" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const disabled = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "custom", includeUsage: false },
|
||||
},
|
||||
|
|
@ -31,11 +53,14 @@ describe("OpenAICompatiblePlugin", () => {
|
|||
it.effect("defaults includeUsage for OpenAI-compatible package matches", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, OpenAICompatiblePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
|
||||
options: { name: "custom" },
|
||||
},
|
||||
|
|
@ -49,20 +74,19 @@ describe("OpenAICompatiblePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const observed: string[] = []
|
||||
yield* addPlugin(plugin, OpenAICompatiblePlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(evt.sdk.languageModel("model").provider)
|
||||
}),
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (event) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(event.sdk.languageModel("model").provider)
|
||||
}),
|
||||
})
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-provider", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "custom-provider", baseURL: "https://example.com/v1" },
|
||||
},
|
||||
|
|
@ -85,11 +109,14 @@ describe("OpenAICompatiblePlugin", () => {
|
|||
}),
|
||||
}),
|
||||
})
|
||||
yield* addPlugin(plugin, OpenAICompatiblePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("cloudflare-workers-ai", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "cloudflare-workers-ai" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,28 +1,50 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { fakeSelectorSdk, it, model, provider, required } from "./provider-helper"
|
||||
import { host, integrationHost } from "./host"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
|
||||
return plugin.add({
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
const integrations = yield* Integration.Service
|
||||
yield* plugin.add({
|
||||
id: OpenAIPlugin.id,
|
||||
effect: OpenAIPlugin.effect(host({ integration: integrationHost(integrations) })).pipe(
|
||||
Effect.provideService(Integration.Service, integrations),
|
||||
),
|
||||
effect: OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)),
|
||||
})
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("OpenAIPlugin", () => {
|
||||
it.effect("registers browser and headless ChatGPT OAuth methods", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* addPlugin()
|
||||
expect((yield* (yield* Integration.Service).get(Integration.ID.make("openai")))?.methods).toEqual([
|
||||
{
|
||||
id: Integration.MethodID.make("chatgpt-browser"),
|
||||
|
|
@ -41,11 +63,14 @@ describe("OpenAIPlugin", () => {
|
|||
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-openai", "gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai",
|
||||
options: { name: "custom-openai", apiKey: "test" },
|
||||
},
|
||||
|
|
@ -58,10 +83,17 @@ describe("OpenAIPlugin", () => {
|
|||
it.effect("ignores non-OpenAI SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("openai", "gpt-5"), package: "@ai-sdk/openai-compatible", options: { name: "openai" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "openai" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -72,11 +104,12 @@ describe("OpenAIPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("openai", "alias", {
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
|
|
@ -93,10 +126,17 @@ describe("OpenAIPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("anthropic", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
|
||||
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual([])
|
||||
|
|
@ -106,17 +146,19 @@ describe("OpenAIPlugin", () => {
|
|||
|
||||
it.effect("disables gpt-5-chat-latest during catalog transforms", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* add(plugin, yield* Integration.Service)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
|
||||
const item = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai" },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
})
|
||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
|
||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
|
||||
|
|
@ -126,14 +168,18 @@ describe("OpenAIPlugin", () => {
|
|||
|
||||
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* add(plugin, yield* Integration.Service)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("custom-openai")
|
||||
catalog.provider.update(item.id, () => {})
|
||||
const item = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
})
|
||||
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest")))
|
||||
.enabled,
|
||||
|
|
|
|||
|
|
@ -1,45 +1,72 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { location } from "../fixture/location"
|
||||
import { it, model, provider, required, withEnv } from "./provider-helper"
|
||||
import { catalogHost, host, integrationHost } from "./host"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: OpencodePlugin.id, effect: OpencodePlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() =>
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
|
||||
const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
|
||||
)
|
||||
|
||||
const pluginWithIntegrations = (catalog: Catalog.Interface, integrations: Integration.Interface) => ({
|
||||
...OpencodePlugin,
|
||||
effect: OpencodePlugin.effect(host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) })),
|
||||
})
|
||||
|
||||
describe("OpencodePlugin", () => {
|
||||
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(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(1),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
|
||||
}),
|
||||
|
|
@ -49,17 +76,23 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("keeps free models without credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
|
||||
api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(0),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
|
||||
}),
|
||||
|
|
@ -69,17 +102,23 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("treats output-only cost as free without credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
|
||||
api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(0, 1),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
|
||||
true,
|
||||
|
|
@ -91,17 +130,23 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("uses OPENCODE_API_KEY as credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: "secret" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(1),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||
}),
|
||||
|
|
@ -111,10 +156,8 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("uses configured provider env vars as credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const integrations = yield* Integration.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, integrations))
|
||||
yield* integrations.transform((editor) => {
|
||||
editor.method.update({
|
||||
integrationID: Integration.ID.make("opencode"),
|
||||
|
|
@ -122,13 +165,21 @@ describe("OpencodePlugin", () => {
|
|||
})
|
||||
})
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(1),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||
}),
|
||||
|
|
@ -138,24 +189,29 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("uses configured apiKey as credentials", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("opencode", {
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.opencode),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
request: {
|
||||
headers: {},
|
||||
body: { apiKey: "configured" },
|
||||
},
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.request = item.request
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(1),
|
||||
})
|
||||
const paid = model("opencode", "paid", { cost: cost(1) })
|
||||
catalog.model.update(item.id, paid.id, (draft) => {
|
||||
draft.cost = [...paid.cost]
|
||||
catalog.provider.update(provider.id, (draft) => {
|
||||
draft.request = provider.request
|
||||
})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||
}),
|
||||
|
|
@ -165,17 +221,23 @@ describe("OpencodePlugin", () => {
|
|||
it.effect("ignores non-opencode providers and models", () =>
|
||||
withEnv({ OPENCODE_API_KEY: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
|
||||
yield* catalog.transform((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]
|
||||
const provider = new ProviderV2.Info({
|
||||
...ProviderV2.Info.empty(ProviderV2.ID.openai),
|
||||
api: { type: "aisdk", package: "test-provider" },
|
||||
})
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
|
||||
api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
|
||||
cost: cost(1),
|
||||
})
|
||||
catalog.provider.update(provider.id, () => {})
|
||||
catalog.model.update(provider.id, model.id, (draft) => {
|
||||
draft.cost = [...model.cost]
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
|
||||
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
|
||||
}),
|
||||
|
|
@ -206,8 +268,6 @@ describe("OpencodePlugin", () => {
|
|||
const selected = yield* catalog.model.small(providerID)
|
||||
|
||||
expect(selected?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
|
||||
}).pipe(
|
||||
Effect.provide(Catalog.locationLayer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(locationLayer))),
|
||||
),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,56 +3,59 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
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 { addPlugin, expectPluginRegistered, it, model, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: OpenRouterPlugin.id, effect: OpenRouterPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("OpenRouterPlugin", () => {
|
||||
it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
|
||||
Effect.sync(() =>
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"openrouter",
|
||||
),
|
||||
),
|
||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("openrouter"))),
|
||||
)
|
||||
|
||||
it.effect("applies legacy referer headers only to openrouter", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, OpenRouterPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const openrouter = provider("openrouter", {
|
||||
api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
|
||||
request: { headers: { Existing: "value" }, body: {} },
|
||||
})
|
||||
catalog.provider.update(openrouter.id, (item) => {
|
||||
item.api = openrouter.api
|
||||
item.request = openrouter.request
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
|
||||
provider.request = { headers: { Existing: "value" }, body: {} }
|
||||
})
|
||||
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).request.headers).toEqual({
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
|
||||
Existing: "value",
|
||||
"HTTP-Referer": "https://opencode.ai/",
|
||||
"X-Title": "opencode",
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({})
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("creates an SDK only for the OpenRouter package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, OpenRouterPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("openrouter", "openai/gpt-5"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "openrouter" },
|
||||
},
|
||||
|
|
@ -62,7 +65,14 @@ describe("OpenRouterPlugin", () => {
|
|||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("custom", "openai/gpt-5"), package: "@openrouter/ai-sdk-provider", options: { name: "custom" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
|
||||
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@openrouter/ai-sdk-provider",
|
||||
options: { name: "custom" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -71,52 +81,37 @@ 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* addPlugin(plugin, OpenRouterPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const openrouter = provider("openrouter", {
|
||||
api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
|
||||
})
|
||||
catalog.provider.update(openrouter.id, (item) => {
|
||||
item.api = openrouter.api
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
|
||||
})
|
||||
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, () => {})
|
||||
}
|
||||
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat")))
|
||||
.enabled,
|
||||
).toBe(false)
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled,
|
||||
).toBe(true)
|
||||
expect(
|
||||
required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled,
|
||||
).toBe(true)
|
||||
expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe(
|
||||
false,
|
||||
)
|
||||
expect((yield* catalog.model.get(ProviderV2.ID.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 disable gpt-5-chat-latest for non-OpenRouter providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, OpenRouterPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {})
|
||||
catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
|
||||
})
|
||||
yield* addPlugin()
|
||||
expect(
|
||||
required(
|
||||
yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")),
|
||||
).enabled,
|
||||
(yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")))
|
||||
?.enabled,
|
||||
).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,50 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
|
||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: PerplexityPlugin.id, effect: PerplexityPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("PerplexityPlugin", () => {
|
||||
it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, PerplexityPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("perplexity", "sonar"), package: "@ai-sdk/perplexity", options: { name: "perplexity" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/perplexity",
|
||||
options: { name: "perplexity" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -22,11 +54,14 @@ describe("PerplexityPlugin", () => {
|
|||
it.effect("ignores packages that are not the bundled Perplexity package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, PerplexityPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("perplexity", "sonar"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/perplexity-compatible",
|
||||
options: { name: "perplexity" },
|
||||
},
|
||||
|
|
@ -39,50 +74,40 @@ describe("PerplexityPlugin", () => {
|
|||
it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, PerplexityPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("perplexity-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("sonar").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("perplexity", "sonar"), package: "@ai-sdk/perplexity", options: { name: "perplexity" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
|
||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/perplexity",
|
||||
options: { name: "perplexity" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(providers).toEqual(["perplexity"])
|
||||
expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("creates bundled Perplexity SDKs for custom provider IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin(plugin, PerplexityPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("custom-perplexity-sdk-inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(evt.sdk.languageModel("sonar").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-perplexity", "sonar"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
|
||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/perplexity",
|
||||
options: { name: "custom-perplexity" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(providers).toEqual(["perplexity"])
|
||||
expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -90,11 +115,14 @@ describe("PerplexityPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, PerplexityPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("perplexity", "alias", { api: { id: ModelV2.ID.make("sonar") } }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,54 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { SapAICorePlugin } from "@opencode-ai/core/plugin/provider/sap-ai-core"
|
||||
import { fixtureProvider, it, model, npmLayer, withEnv } from "./provider-helper"
|
||||
import { host } from "./host"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const pluginWithNpm = {
|
||||
id: SapAICorePlugin.id,
|
||||
effect: Effect.gen(function* () {
|
||||
yield* SapAICorePlugin.effect(host({ npm: yield* Npm.Service }))
|
||||
}).pipe(Effect.provide(npmLayer)),
|
||||
const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const npm = Npm.Service.of({
|
||||
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
})
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: SapAICorePlugin.id, effect: SapAICorePlugin.effect({ ...host, npm }) })
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function model(providerID: string) {
|
||||
return new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
|
||||
api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
|
||||
})
|
||||
}
|
||||
|
||||
describe("SapAICorePlugin", () => {
|
||||
|
|
@ -20,11 +58,11 @@ describe("SapAICorePlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(pluginWithNpm)
|
||||
yield* addPlugin()
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("sap-ai-core", "sap-model"),
|
||||
model: model("sap-ai-core"),
|
||||
package: fixtureProvider,
|
||||
options: { name: "sap-ai-core", serviceKey: "service-key" },
|
||||
},
|
||||
|
|
@ -46,11 +84,11 @@ describe("SapAICorePlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(pluginWithNpm)
|
||||
yield* addPlugin()
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("sap-ai-core", "sap-model"),
|
||||
model: model("sap-ai-core"),
|
||||
package: fixtureProvider,
|
||||
options: { name: "sap-ai-core", serviceKey: "option-service-key" },
|
||||
},
|
||||
|
|
@ -68,10 +106,10 @@ describe("SapAICorePlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(pluginWithNpm)
|
||||
yield* addPlugin()
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("sap-ai-core", "sap-model"), package: fixtureProvider, options: { name: "sap-ai-core" } },
|
||||
{ model: model("sap-ai-core"), package: fixtureProvider, options: { name: "sap-ai-core" } },
|
||||
{},
|
||||
)
|
||||
expect(process.env.AICORE_SERVICE_KEY).toBeUndefined()
|
||||
|
|
@ -83,7 +121,7 @@ describe("SapAICorePlugin", () => {
|
|||
it.effect("uses the callable SDK for language selection", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(pluginWithNpm)
|
||||
yield* addPlugin()
|
||||
const sdk = Object.assign((modelID: string) => ({ modelID, provider: "callable" }), {
|
||||
languageModel() {
|
||||
throw new Error("SAP AI Core should call the SDK directly")
|
||||
|
|
@ -91,7 +129,7 @@ describe("SapAICorePlugin", () => {
|
|||
})
|
||||
const language = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("sap-ai-core", "sap-model"), sdk, options: {} },
|
||||
{ model: model("sap-ai-core"), sdk, options: {} },
|
||||
{},
|
||||
)
|
||||
expect(language.language as unknown).toEqual({ modelID: "sap-model", provider: "callable" })
|
||||
|
|
@ -104,11 +142,11 @@ describe("SapAICorePlugin", () => {
|
|||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* plugin.add(pluginWithNpm)
|
||||
yield* addPlugin()
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("openai", "sap-model"),
|
||||
model: model("openai"),
|
||||
package: fixtureProvider,
|
||||
options: { name: "openai", serviceKey: "service-key" },
|
||||
},
|
||||
|
|
@ -117,7 +155,7 @@ describe("SapAICorePlugin", () => {
|
|||
const language = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("openai", "sap-model"),
|
||||
model: model("openai"),
|
||||
sdk: () => {
|
||||
throw new Error("SAP AI Core should ignore other providers")
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,48 @@
|
|||
import { describe, expect, it as bun_it } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
|
||||
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
|
||||
import { addPlugin, expectPluginRegistered, it, model, withEnv } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: SnowflakeCortexPlugin.id, effect: SnowflakeCortexPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
return Effect.acquireUseRelease(
|
||||
Effect.sync(() => {
|
||||
const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
return previous
|
||||
}),
|
||||
effect,
|
||||
(previous) =>
|
||||
Effect.sync(() => {
|
||||
Object.entries(previous).forEach(([key, value]) => {
|
||||
if (value === undefined) delete process.env[key]
|
||||
else process.env[key] = value
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe("SnowflakeCortexPlugin", () => {
|
||||
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
|
||||
Effect.sync(() => {
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"snowflake-cortex",
|
||||
)
|
||||
const ids = ProviderPlugins.map((p) => p.id as string)
|
||||
expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("snowflake-cortex"))
|
||||
const ids = ProviderPlugins.map((p) => p.id)
|
||||
expect(ids.indexOf("snowflake-cortex")).toBeLessThan(ids.indexOf("openai-compatible"))
|
||||
}),
|
||||
)
|
||||
|
|
@ -20,10 +50,17 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
it.effect("ignores non-snowflake-cortex providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("openai", "gpt-4"), package: "@ai-sdk/openai", options: { name: "openai" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
|
||||
api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai",
|
||||
options: { name: "openai" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeUndefined()
|
||||
|
|
@ -34,11 +71,17 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("snowflake-cortex", "claude-sonnet-4-6"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("snowflake-cortex"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||
},
|
||||
|
|
@ -53,11 +96,17 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("snowflake-cortex", "claude-sonnet-4-6"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("snowflake-cortex"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
name: "snowflake-cortex",
|
||||
|
|
@ -76,11 +125,17 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("snowflake-cortex", "claude-sonnet-4-6"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("snowflake-cortex"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||
},
|
||||
|
|
@ -95,11 +150,17 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("snowflake-cortex", "claude-sonnet-4-6"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("snowflake-cortex"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
name: "snowflake-cortex",
|
||||
|
|
@ -118,27 +179,23 @@ describe("SnowflakeCortexPlugin", () => {
|
|||
withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const captured: Record<string, unknown>[] = []
|
||||
yield* addPlugin(plugin, SnowflakeCortexPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
captured.push({ ...evt.options })
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* plugin.trigger(
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("snowflake-cortex", "claude-sonnet-4-6"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("snowflake-cortex"),
|
||||
ModelV2.ID.make("claude-sonnet-4-6"),
|
||||
),
|
||||
api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(captured[0]?.includeUsage).toBe(true)
|
||||
expect(result.options.includeUsage).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,50 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
|
||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: TogetherAIPlugin.id, effect: TogetherAIPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("TogetherAIPlugin", () => {
|
||||
it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, TogetherAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("togetherai", "model"), package: "@ai-sdk/togetherai", options: { name: "togetherai" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/togetherai",
|
||||
options: { name: "togetherai" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -21,12 +54,15 @@ describe("TogetherAIPlugin", () => {
|
|||
it.effect("matches the old bundled provider package exactly", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, TogetherAIPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("togetherai", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "file:///tmp/@ai-sdk/togetherai-provider.js",
|
||||
options: { name: "togetherai" },
|
||||
},
|
||||
|
|
@ -36,7 +72,14 @@ describe("TogetherAIPlugin", () => {
|
|||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("togetherai", "model"), package: "@ai-sdk/togetherai", options: { name: "togetherai" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/togetherai",
|
||||
options: { name: "togetherai" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -46,29 +89,22 @@ describe("TogetherAIPlugin", () => {
|
|||
it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const observed: string[] = []
|
||||
yield* addPlugin(plugin, TogetherAIPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(evt.sdk.languageModel("model").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-togetherai", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/togetherai",
|
||||
options: { name: "custom-togetherai" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
expect(observed).toEqual(["togetherai.chat"])
|
||||
expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -76,12 +112,22 @@ describe("TogetherAIPlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, TogetherAIPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: model("togetherai", "meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(
|
||||
ProviderV2.ID.make("togetherai"),
|
||||
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
||||
),
|
||||
api: {
|
||||
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
},
|
||||
}),
|
||||
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
|
||||
options: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,17 +1,50 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
|
||||
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: VenicePlugin.id, effect: VenicePlugin.effect(host) })
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("VenicePlugin", () => {
|
||||
it.effect("creates a Venice SDK for venice-ai-sdk-provider", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, VenicePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("venice", "model"), package: "venice-ai-sdk-provider", options: { name: "venice" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "venice-ai-sdk-provider",
|
||||
options: { name: "venice" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
|
|
@ -21,39 +54,35 @@ describe("VenicePlugin", () => {
|
|||
it.effect("uses the model provider ID as the bundled Venice SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const observed: string[] = []
|
||||
yield* addPlugin(plugin, VenicePlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("inspector"),
|
||||
effect: Effect.succeed({
|
||||
"aisdk.sdk": (evt) =>
|
||||
Effect.sync(() => {
|
||||
observed.push(evt.sdk.languageModel("model").provider)
|
||||
}),
|
||||
}),
|
||||
})
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("custom-venice", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "venice-ai-sdk-provider",
|
||||
options: { name: "custom-venice", apiKey: "test" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(result.sdk).toBeDefined()
|
||||
expect(observed).toEqual(["custom-venice.chat"])
|
||||
expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("only handles the bundled venice-ai-sdk-provider package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, VenicePlugin)
|
||||
yield* addPlugin()
|
||||
const similar = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: model("venice", "model"),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "file:///tmp/venice-ai-sdk-provider.js",
|
||||
options: { name: "venice" },
|
||||
},
|
||||
|
|
@ -61,7 +90,14 @@ describe("VenicePlugin", () => {
|
|||
)
|
||||
const other = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("venice", "model"), package: "@ai-sdk/openai-compatible", options: { name: "venice" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
|
||||
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: { name: "venice" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(similar.sdk).toBeUndefined()
|
||||
|
|
@ -73,10 +109,17 @@ describe("VenicePlugin", () => {
|
|||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(plugin, VenicePlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{ model: model("venice", "alias"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(calls).toEqual([])
|
||||
|
|
|
|||
|
|
@ -1,28 +1,34 @@
|
|||
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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { addPlugin, it, model, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: VercelPlugin.id, effect: VercelPlugin.effect(host) })
|
||||
})
|
||||
|
||||
describe("VercelPlugin", () => {
|
||||
it.effect("applies legacy lower-case referer headers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, VercelPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("vercel", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/vercel" },
|
||||
request: { headers: { Existing: "1" }, body: {} },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
draft.request = item.request
|
||||
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
|
||||
provider.request.headers.Existing = "1"
|
||||
})
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).toEqual({
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
|
||||
Existing: "1",
|
||||
"http-referer": "https://opencode.ai/",
|
||||
"x-title": "opencode",
|
||||
|
|
@ -32,19 +38,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* addPlugin(plugin, VercelPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("vercel", { api: { type: "aisdk", package: "@ai-sdk/vercel" } })
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
})
|
||||
})
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
|
||||
provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
|
||||
}),
|
||||
)
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
|
||||
"HTTP-Referer",
|
||||
)
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
|
||||
"X-Title",
|
||||
)
|
||||
}),
|
||||
|
|
@ -53,10 +57,17 @@ describe("VercelPlugin", () => {
|
|||
it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, VercelPlugin)
|
||||
yield* addPlugin()
|
||||
const event = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model: model("custom-vercel", "v0-1.0-md"), package: "@ai-sdk/vercel", options: { name: "custom-vercel" } },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
|
||||
api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
|
||||
}),
|
||||
package: "@ai-sdk/vercel",
|
||||
options: { name: "custom-vercel" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
expect(event.sdk).toBeDefined()
|
||||
|
|
@ -66,11 +77,10 @@ describe("VercelPlugin", () => {
|
|||
|
||||
it.effect("ignores non-Vercel providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, VercelPlugin)
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).request.headers).toEqual({})
|
||||
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
|
||||
yield* addPlugin()
|
||||
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,37 +1,66 @@
|
|||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { addPlugin, fakeSelectorSdk } from "./provider-helper"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const model = new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||
api: {
|
||||
id: ModelV2.ID.make("grok-4"),
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/xai",
|
||||
},
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: XAIPlugin.id, effect: XAIPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
|
||||
}
|
||||
return {
|
||||
responses: make("responses"),
|
||||
messages: make("messages"),
|
||||
chat: make("chat"),
|
||||
languageModel: make("languageModel"),
|
||||
}
|
||||
}
|
||||
|
||||
describe("XAIPlugin", () => {
|
||||
it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
yield* addPlugin(plugin, XAIPlugin)
|
||||
yield* addPlugin()
|
||||
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{ model, package: "@ai-sdk/openai-compatible", options: {} },
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
const result = yield* plugin.trigger("aisdk.sdk", { model, package: "@ai-sdk/xai", options: {} }, {})
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
|
||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
||||
}),
|
||||
package: "@ai-sdk/xai",
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
expect(ignored.sdk).toBeUndefined()
|
||||
expect(typeof result.sdk?.responses).toBe("function")
|
||||
|
|
@ -41,32 +70,22 @@ describe("XAIPlugin", () => {
|
|||
it.effect("creates xAI SDKs for custom provider IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin()
|
||||
|
||||
yield* addPlugin(plugin, XAIPlugin)
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("xai-sdk-name-observer"),
|
||||
effect: Effect.gen(function* () {
|
||||
return {
|
||||
"aisdk.sdk": Effect.fn(function* (evt) {
|
||||
if (!evt.sdk) return
|
||||
providers.push(evt.sdk.responses("grok-4").provider)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
yield* plugin.trigger(
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
model: new ModelV2.Info({ ...model, providerID: ProviderV2.ID.make("custom-xai") }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
|
||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
||||
}),
|
||||
package: "@ai-sdk/xai",
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
expect(providers).toEqual(["xai.responses"])
|
||||
expect(result.sdk.responses("grok-4").provider).toBe("xai.responses")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -75,11 +94,14 @@ describe("XAIPlugin", () => {
|
|||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
|
||||
yield* addPlugin(plugin, XAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: new ModelV2.Info({ ...model, id: ModelV2.ID.make("alias") }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
|
||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
@ -96,11 +118,14 @@ describe("XAIPlugin", () => {
|
|||
const plugin = yield* PluginV2.Service
|
||||
const calls: string[] = []
|
||||
|
||||
yield* addPlugin(plugin, XAIPlugin)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model: new ModelV2.Info({ ...model, providerID: ProviderV2.ID.openai }),
|
||||
model: new ModelV2.Info({
|
||||
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
|
||||
api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
|
||||
}),
|
||||
sdk: fakeSelectorSdk(calls),
|
||||
options: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,34 +2,44 @@ 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 { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
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 { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({ id: ZenmuxPlugin.id, effect: ZenmuxPlugin.effect(host) })
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
if (value === undefined) throw new Error("Expected value")
|
||||
return value
|
||||
}
|
||||
|
||||
describe("ZenmuxPlugin", () => {
|
||||
it.effect("is registered so legacy referer headers can be applied", () =>
|
||||
Effect.sync(() =>
|
||||
expectPluginRegistered(
|
||||
ProviderPlugins.map((item) => item.id),
|
||||
"zenmux",
|
||||
),
|
||||
),
|
||||
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("zenmux"))),
|
||||
)
|
||||
|
||||
it.effect("applies the exact legacy Zenmux headers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, ZenmuxPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://zenmux.ai/api/v1",
|
||||
}
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
|
||||
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
|
||||
expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
|
||||
|
|
@ -38,19 +48,18 @@ describe("ZenmuxPlugin", () => {
|
|||
|
||||
it.effect("merges legacy Zenmux headers with existing headers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* addPlugin(plugin, ZenmuxPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
request: { headers: { Existing: "value" }, body: {} },
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
draft.request = item.request
|
||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://zenmux.ai/api/v1",
|
||||
}
|
||||
provider.request.headers.Existing = "value"
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
|
||||
Existing: "value",
|
||||
|
|
@ -62,22 +71,18 @@ 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* addPlugin(plugin, ZenmuxPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("zenmux", {
|
||||
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
|
||||
request: {
|
||||
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
|
||||
body: {},
|
||||
},
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.api = item.api
|
||||
draft.request = item.request
|
||||
catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
|
||||
provider.api = {
|
||||
type: "aisdk",
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
url: "https://zenmux.ai/api/v1",
|
||||
}
|
||||
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
|
||||
"HTTP-Referer": "https://example.com/",
|
||||
|
|
@ -88,20 +93,13 @@ 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* addPlugin(plugin, ZenmuxPlugin)
|
||||
yield* catalog.transform((catalog) => {
|
||||
const item = provider("openrouter", {
|
||||
request: {
|
||||
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
|
||||
body: {},
|
||||
},
|
||||
})
|
||||
catalog.provider.update(item.id, (draft) => {
|
||||
draft.request = item.request
|
||||
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
|
||||
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
|
||||
})
|
||||
})
|
||||
yield* addPlugin()
|
||||
|
||||
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
|
||||
"HTTP-Referer": "https://example.com/",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue