mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
feat(plugin): add namespaced hook API (#33416)
This commit is contained in:
@@ -108,7 +108,7 @@ export const layer = Layer.effect(
|
||||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
get: Effect.fn("AgentV2.get")(function* (id) {
|
||||
return state.get().agents.get(id)
|
||||
}),
|
||||
|
||||
+74
-21
@@ -1,14 +1,27 @@
|
||||
export * as AISDK from "./aisdk"
|
||||
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Cause, Context, Effect, Layer, Schema } from "effect"
|
||||
import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
|
||||
import { ModelV2 } from "./model"
|
||||
import { EventV2 } from "./event"
|
||||
import { PluginV2 } from "./plugin"
|
||||
import { ProviderV2 } from "./provider"
|
||||
import { State } from "./state"
|
||||
|
||||
type SDK = any
|
||||
|
||||
export interface SDKEvent {
|
||||
readonly model: ModelV2.Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: SDK
|
||||
}
|
||||
|
||||
export interface LanguageEvent {
|
||||
readonly model: ModelV2.Info
|
||||
readonly sdk: SDK
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
|
||||
function wrapSSE(res: Response, ms: number, ctl: AbortController) {
|
||||
if (typeof ms !== "number" || ms <= 0) return res
|
||||
if (!res.body) return res
|
||||
@@ -117,19 +130,70 @@ function initError(providerID: ProviderV2.ID) {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly hook: {
|
||||
readonly sdk: (
|
||||
callback: (event: SDKEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly language: (
|
||||
callback: (event: LanguageEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
}
|
||||
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
|
||||
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
|
||||
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export const locationLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
|
||||
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
|
||||
const languages = new Map<string, LanguageModelV3>()
|
||||
const sdks = new Map<string, SDK>()
|
||||
|
||||
return Service.of({
|
||||
const register = <Event>(
|
||||
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
||||
update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
|
||||
) =>
|
||||
Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
|
||||
const scope = yield* Scope.Scope
|
||||
let active = true
|
||||
update([...hooks(), callback])
|
||||
const dispose = Effect.sync(() => {
|
||||
if (!active) return
|
||||
active = false
|
||||
update(hooks().filter((item) => item !== callback))
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
return { dispose }
|
||||
})
|
||||
|
||||
const run = Effect.fnUntraced(function* <Event>(
|
||||
hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
|
||||
event: Event,
|
||||
) {
|
||||
for (const hook of hooks) {
|
||||
const result = hook(event)
|
||||
if (Effect.isEffect(result)) yield* result
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
const service = Service.of({
|
||||
hook: {
|
||||
sdk: register(
|
||||
() => sdkHooks,
|
||||
(next) => (sdkHooks = next),
|
||||
),
|
||||
language: register(
|
||||
() => languageHooks,
|
||||
(next) => (languageHooks = next),
|
||||
),
|
||||
},
|
||||
runSDK: (event) => run(sdkHooks, event),
|
||||
runLanguage: (event) => run(languageHooks, event),
|
||||
language: Effect.fn("AISDK.language")(function* (model) {
|
||||
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
|
||||
const existing = languages.get(key)
|
||||
@@ -148,26 +212,14 @@ export const layer = Layer.effect(
|
||||
})
|
||||
const sdk =
|
||||
sdks.get(sdkKey) ??
|
||||
(yield* plugin
|
||||
.trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
|
||||
.pipe(initError(model.providerID))).sdk
|
||||
(yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
|
||||
if (!sdk)
|
||||
return yield* new InitError({
|
||||
providerID: model.providerID,
|
||||
cause: new Error("No AISDK provider plugin returned an SDK"),
|
||||
})
|
||||
sdks.set(sdkKey, sdk)
|
||||
const result = yield* plugin
|
||||
.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
model,
|
||||
sdk,
|
||||
options,
|
||||
},
|
||||
{},
|
||||
)
|
||||
.pipe(initError(model.providerID))
|
||||
const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
|
||||
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
|
||||
initError(model.providerID),
|
||||
)
|
||||
@@ -175,7 +227,8 @@ export const layer = Layer.effect(
|
||||
return language
|
||||
}),
|
||||
})
|
||||
return service
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))))
|
||||
export const defaultLayer = locationLayer
|
||||
|
||||
@@ -170,7 +170,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
const result: Interface = {
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
|
||||
provider: {
|
||||
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
transform: state.transform,
|
||||
get: Effect.fn("CommandV2.get")(function* (name) {
|
||||
return state.get().commands.get(name)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigAgentPlugin from "./agent"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { AgentV2 } from "../../agent"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigCommandPlugin from "./command"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { CommandV2 } from "../../command"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
export * as ConfigExternalPlugin from "./external"
|
||||
|
||||
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { fileURLToPath, pathToFileURL } from "url"
|
||||
import { Config } from "../../config"
|
||||
import { FSUtil } from "../../fs-util"
|
||||
import { Location } from "../../location"
|
||||
import { Npm } from "../../npm"
|
||||
import { define } from "../../plugin/internal"
|
||||
import { PluginPromise } from "../../plugin/promise"
|
||||
|
||||
const PluginModule = Schema.Struct({
|
||||
default: Schema.Union([
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
effect: Schema.declare<EffectPlugin["effect"]>(
|
||||
(input): input is EffectPlugin["effect"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
setup: Schema.declare<PromisePlugin["setup"]>(
|
||||
(input): input is PromisePlugin["setup"] => typeof input === "function",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
export const Plugin = define({
|
||||
id: "config-plugin",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const loaded: EffectPlugin[] = []
|
||||
|
||||
yield* ctx.plugin.transform((plugins) => {
|
||||
for (const plugin of loaded) plugins.add(plugin)
|
||||
})
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const configured: { package: string; options?: Record<string, any> }[] = []
|
||||
|
||||
for (const entry of yield* config.entries()) {
|
||||
if (entry.type === "document") {
|
||||
const directory = entry.path ? path.dirname(entry.path) : location.directory
|
||||
for (const item of entry.info.plugins ?? []) {
|
||||
const ref = typeof item === "string" ? { package: item } : item
|
||||
const packageName = (() => {
|
||||
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
|
||||
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
|
||||
return path.resolve(directory, ref.package)
|
||||
}
|
||||
return ref.package
|
||||
})()
|
||||
configured.push({ package: packageName, options: ref.options })
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.type === "directory") {
|
||||
const files = yield* fs
|
||||
.glob("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: entry.path,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})
|
||||
.pipe(Effect.orElseSucceed(() => []))
|
||||
files.sort()
|
||||
for (const file of files) configured.push({ package: file })
|
||||
}
|
||||
}
|
||||
|
||||
for (const ref of configured) {
|
||||
yield* Effect.gen(function* () {
|
||||
const entrypoint = path.isAbsolute(ref.package)
|
||||
? pathToFileURL(ref.package).href
|
||||
: (yield* npm.add(ref.package)).entrypoint
|
||||
if (!entrypoint) return
|
||||
|
||||
const mod = yield* Effect.promise(() => import(entrypoint))
|
||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||
loaded.push({
|
||||
id: plugin.id,
|
||||
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
|
||||
})
|
||||
}).pipe(Effect.ignoreCause)
|
||||
}
|
||||
|
||||
yield* ctx.plugin.reload()
|
||||
}).pipe(Effect.forkScoped({ startImmediately: true }))
|
||||
}),
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigProviderPlugin from "./provider"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
export * as ConfigReferencePlugin from "./reference"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ConfigReference } from "../reference"
|
||||
import { Reference } from "../../reference"
|
||||
import { AbsolutePath } from "../../schema"
|
||||
import { Global } from "../../global"
|
||||
import { Location } from "../../location"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "core/config-reference",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const global = yield* Global.Service
|
||||
yield* ctx.reference.transform(
|
||||
Effect.fn(function* (draft) {
|
||||
const entries = new Map<string, Reference.Source>()
|
||||
for (const doc of (yield* config.entries()).filter(
|
||||
(entry): entry is Config.Document => entry.type === "document",
|
||||
)) {
|
||||
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory
|
||||
const directory = doc.path ? path.dirname(doc.path) : location.directory
|
||||
for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
|
||||
if (!validAlias(name)) continue
|
||||
entries.set(
|
||||
@@ -27,7 +31,7 @@ export const Plugin = define({
|
||||
? new Reference.LocalSource({
|
||||
type: "local",
|
||||
path: AbsolutePath.make(
|
||||
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
|
||||
localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
|
||||
),
|
||||
description: typeof entry === "string" ? undefined : entry.description,
|
||||
hidden: typeof entry === "string" ? undefined : entry.hidden,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
export * as ConfigSkillPlugin from "./skill"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../../plugin/internal"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { AbsolutePath } from "../../schema"
|
||||
import { SkillV2 } from "../../skill"
|
||||
import { Global } from "../../global"
|
||||
import { Location } from "../../location"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "config-skill",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
yield* ctx.skill.transform(
|
||||
Effect.fn(function* (draft) {
|
||||
const entries = yield* config.entries()
|
||||
@@ -29,13 +33,11 @@ export const Plugin = define({
|
||||
draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
|
||||
continue
|
||||
}
|
||||
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item
|
||||
const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
|
||||
draft.source(
|
||||
new SkillV2.DirectorySource({
|
||||
type: "directory",
|
||||
path: AbsolutePath.make(
|
||||
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
|
||||
),
|
||||
path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ export const locationLayer = Layer.effect(
|
||||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
get: Effect.fn("Integration.get")(function* (id) {
|
||||
const entry = state.get().integrations.get(id)
|
||||
if (!entry) return undefined
|
||||
|
||||
+79
-176
@@ -1,12 +1,17 @@
|
||||
export * as PluginV2 from "./plugin"
|
||||
|
||||
import { createDraft, finishDraft, type Draft } from "immer"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||
import type { ModelV2 } from "./model"
|
||||
import type { Catalog } from "./catalog"
|
||||
import type { Plugin, PluginDraft } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { AISDK } from "./aisdk"
|
||||
import { Catalog } from "./catalog"
|
||||
import { CommandV2 } from "./command"
|
||||
import { EventV2 } from "./event"
|
||||
import { Integration } from "./integration"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||
import { PluginHost } from "./plugin/host"
|
||||
import { Reference } from "./reference"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { State } from "./state"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
|
||||
@@ -21,69 +26,9 @@ export const Event = {
|
||||
}),
|
||||
}
|
||||
|
||||
type HookSpec = {
|
||||
"catalog.transform": {
|
||||
input: Catalog.Draft
|
||||
output: {}
|
||||
}
|
||||
"aisdk.language": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
sdk: any
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}
|
||||
"aisdk.sdk": {
|
||||
input: {
|
||||
model: ModelV2.Info
|
||||
package: string
|
||||
options: Record<string, any>
|
||||
}
|
||||
output: {
|
||||
sdk?: any
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Hooks = {
|
||||
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
|
||||
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
|
||||
? Draft<HookSpec[Name]["output"][Field]>
|
||||
: HookSpec[Name]["output"][Field]
|
||||
}
|
||||
}
|
||||
|
||||
export type HookFunctions = {
|
||||
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
|
||||
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (input: {
|
||||
id: string
|
||||
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
|
||||
}) => Effect.Effect<void, never, never>
|
||||
readonly remove: (id: ID) => Effect.Effect<void>
|
||||
readonly hook: <Name extends keyof Hooks>(
|
||||
name: Name,
|
||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly triggerFor: <Name extends keyof Hooks>(
|
||||
id: ID,
|
||||
name: Name,
|
||||
input: HookInput<Name>,
|
||||
output: HookOutput<Name>,
|
||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
||||
readonly trigger: <Name extends keyof Hooks>(
|
||||
name: Name,
|
||||
input: HookInput<Name>,
|
||||
output: HookOutput<Name>,
|
||||
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
|
||||
readonly transform: State.Transform<PluginDraft>
|
||||
readonly reload: State.Reload
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
|
||||
@@ -91,127 +36,85 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
let hooks: {
|
||||
id: ID
|
||||
hooks: HookFunctions
|
||||
scope: Scope.Closeable
|
||||
}[] = []
|
||||
let registrations: {
|
||||
[Name in keyof Hooks]: {
|
||||
name: Name
|
||||
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
|
||||
}
|
||||
}[keyof Hooks][] = []
|
||||
const events = yield* EventV2.Service
|
||||
const locks = KeyedMutex.makeUnsafe<ID>()
|
||||
const scope = yield* Scope.make()
|
||||
const active = new Map<ID, Scope.Closeable>()
|
||||
let host: Parameters<Plugin["effect"]>[0]
|
||||
|
||||
const attach = Effect.fn("Plugin.attach")(function* (plugin: Plugin, host: Parameters<Plugin["effect"]>[0]) {
|
||||
const id = ID.make(plugin.id)
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const existing = active.get(id)
|
||||
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
|
||||
|
||||
const child = yield* Scope.fork(scope)
|
||||
yield* plugin.effect(host).pipe(
|
||||
Scope.provide(child),
|
||||
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
|
||||
)
|
||||
active.set(id, child)
|
||||
yield* events.publish(Event.Added, { id })
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const detach = Effect.fn("Plugin.detach")(function* (id: ID) {
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const current = active.get(id)
|
||||
active.delete(id)
|
||||
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const state = State.create<Map<ID, Plugin>, PluginDraft>({
|
||||
initial: () => new Map(),
|
||||
draft: (draft) => ({
|
||||
list: () => Array.from(draft.values()),
|
||||
add: (plugin) => draft.set(ID.make(plugin.id), plugin),
|
||||
remove: (id) => draft.delete(ID.make(id)),
|
||||
}),
|
||||
finalize: (draft) =>
|
||||
State.batch(
|
||||
Effect.gen(function* () {
|
||||
const desired = new Set<ID>()
|
||||
for (const plugin of draft.list()) desired.add(ID.make(plugin.id))
|
||||
|
||||
for (const id of active.keys()) {
|
||||
if (!desired.has(id)) yield* detach(id)
|
||||
}
|
||||
|
||||
for (const plugin of draft.list()) yield* attach(plugin, host)
|
||||
}).pipe(Effect.withSpan("Plugin.reconcile")),
|
||||
),
|
||||
})
|
||||
|
||||
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
|
||||
yield* Effect.addFinalizer((exit) =>
|
||||
Effect.gen(function* () {
|
||||
hooks = []
|
||||
active.clear()
|
||||
yield* State.batch(Scope.close(scope, exit))
|
||||
}),
|
||||
)
|
||||
|
||||
const svc = Service.of({
|
||||
add: Effect.fn("Plugin.add")(function* (input) {
|
||||
const id = ID.make(input.id)
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const existing = hooks.find((item) => item.id === id)
|
||||
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
|
||||
const childScope = yield* Scope.fork(scope)
|
||||
const result = yield* input.effect.pipe(
|
||||
Scope.provide(childScope),
|
||||
Effect.withSpan("Plugin.load", {
|
||||
attributes: {
|
||||
"plugin.id": id,
|
||||
},
|
||||
}),
|
||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
|
||||
)
|
||||
const next = {
|
||||
id,
|
||||
hooks: result ?? {},
|
||||
scope: childScope,
|
||||
}
|
||||
hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
|
||||
yield* events.publish(Event.Added, { id })
|
||||
}),
|
||||
)
|
||||
}),
|
||||
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
|
||||
return yield* svc.triggerFor(ID.make("*"), name, input, output)
|
||||
}),
|
||||
triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) {
|
||||
const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
|
||||
const event = {
|
||||
...input,
|
||||
...output,
|
||||
} as Record<string, unknown>
|
||||
|
||||
for (const [field, value] of Object.entries(output)) {
|
||||
if (value && typeof value === "object") {
|
||||
draftEntries.set(field, createDraft(value))
|
||||
event[field] = draftEntries.get(field)
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of hooks) {
|
||||
if (id !== ID.make("*") && item.id !== id) continue
|
||||
const match = item.hooks[name]
|
||||
if (!match) continue
|
||||
yield* match(event as any).pipe(
|
||||
Effect.withSpan(`Plugin.hook.${name}`, {
|
||||
attributes: {
|
||||
plugin: item.id,
|
||||
hook: name,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
for (const item of registrations) {
|
||||
if (item.name !== name) continue
|
||||
const result = item.callback(event as never)
|
||||
if (Effect.isEffect(result)) yield* result
|
||||
}
|
||||
|
||||
for (const [field, draft] of draftEntries) {
|
||||
event[field] = finishDraft(draft)
|
||||
}
|
||||
|
||||
return event as any
|
||||
}),
|
||||
remove: Effect.fn("Plugin.remove")(function* (id) {
|
||||
yield* locks.withLock(id)(
|
||||
Effect.gen(function* () {
|
||||
const existing = hooks.find((item) => item.id === id)
|
||||
hooks = hooks.filter((item) => item.id !== id)
|
||||
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
|
||||
const scope = yield* Scope.Scope
|
||||
const registration = { name, callback } as (typeof registrations)[number]
|
||||
let active = true
|
||||
registrations = [...registrations, registration]
|
||||
const dispose = Effect.sync(() => {
|
||||
if (!active) return
|
||||
active = false
|
||||
registrations = registrations.filter((item) => item !== registration)
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
return { dispose }
|
||||
}),
|
||||
const service = Service.of({
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
})
|
||||
return svc
|
||||
host = yield* PluginHost.make(service)
|
||||
return service
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
// opencode
|
||||
// sdcok
|
||||
export const locationLayer = layer.pipe(
|
||||
Layer.provideMerge(AgentV2.locationLayer),
|
||||
Layer.provideMerge(AISDK.locationLayer),
|
||||
Layer.provideMerge(Catalog.locationLayer),
|
||||
Layer.provideMerge(CommandV2.locationLayer),
|
||||
Layer.provideMerge(Integration.locationLayer),
|
||||
Layer.provideMerge(Reference.locationLayer),
|
||||
Layer.provideMerge(SkillV2.locationLayer),
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export * as AgentPlugin from "./agent"
|
||||
|
||||
import path from "path"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Global } from "../global"
|
||||
import { Location } from "../location"
|
||||
import { PermissionV2 } from "../permission"
|
||||
|
||||
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
|
||||
@@ -99,7 +100,8 @@ Rules:
|
||||
export const Plugin = define({
|
||||
id: "agent",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const worktree = ctx.location.directory
|
||||
const location = yield* Location.Service
|
||||
const worktree = location.directory
|
||||
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
|
||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export * as PluginBoot from "./boot"
|
||||
|
||||
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Integration } from "../integration"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
import { Config } from "../config"
|
||||
@@ -11,6 +11,7 @@ import { ConfigAgentPlugin } from "../config/plugin/agent"
|
||||
import { ConfigCommandPlugin } from "../config/plugin/command"
|
||||
import { ConfigSkillPlugin } from "../config/plugin/skill"
|
||||
import { ConfigReferencePlugin } from "../config/plugin/reference"
|
||||
import { ConfigExternalPlugin } from "../config/plugin/external"
|
||||
import { EventV2 } from "../event"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { FileSystem } from "../filesystem"
|
||||
@@ -29,18 +30,9 @@ import { SkillV2 } from "../skill"
|
||||
import { Reference } from "../reference"
|
||||
import { State } from "../state"
|
||||
import { PluginHost } from "./host"
|
||||
import { PluginInternal } from "./internal"
|
||||
|
||||
type InternalPlugin = PublicPlugin<any>
|
||||
|
||||
export interface Interface {
|
||||
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
|
||||
readonly wait: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
export const locationLayer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* CommandV2.Service
|
||||
@@ -57,75 +49,47 @@ export const layer = Layer.effect(
|
||||
const global = yield* Global.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const host = yield* PluginHost.make()
|
||||
const done = yield* Deferred.make<void>()
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
|
||||
yield* plugin.add({
|
||||
id: input.id,
|
||||
effect: input
|
||||
.effect(host)
|
||||
.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(CommandV2.Service, commands),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(FileSystem.Service, filesystem),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(SkillV2.Service, skill),
|
||||
Effect.provideService(Reference.Service, reference),
|
||||
),
|
||||
})
|
||||
})
|
||||
const add = <R>(input: PluginInternal.Plugin<R>) =>
|
||||
input
|
||||
.effect({ ...host, options: {} })
|
||||
.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(CommandV2.Service, commands),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(FileSystem.Service, filesystem),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(SkillV2.Service, skill),
|
||||
Effect.provideService(Reference.Service, reference),
|
||||
)
|
||||
|
||||
const boot = Effect.gen(function* () {
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
yield* add(ConfigCommandPlugin.Plugin)
|
||||
yield* add(ConfigSkillPlugin.Plugin)
|
||||
yield* add(ConfigReferencePlugin.Plugin)
|
||||
for (const item of ProviderPlugins) {
|
||||
yield* add(item)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||
|
||||
yield* boot.pipe(
|
||||
Effect.exit,
|
||||
Effect.flatMap((exit) => Deferred.done(done, exit)),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
add: (input) =>
|
||||
Deferred.await(done).pipe(
|
||||
Effect.andThen(
|
||||
plugin.add({
|
||||
id: input.id,
|
||||
effect: input.effect(host),
|
||||
}),
|
||||
),
|
||||
),
|
||||
wait: () => Deferred.await(done),
|
||||
})
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
yield* add(AgentPlugin.Plugin)
|
||||
yield* add(CommandPlugin.Plugin)
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigProviderPlugin.Plugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
yield* add(ConfigCommandPlugin.Plugin)
|
||||
yield* add(ConfigSkillPlugin.Plugin)
|
||||
yield* add(ConfigReferencePlugin.Plugin)
|
||||
for (const item of ProviderPlugins) yield* add(item)
|
||||
yield* add(ConfigExternalPlugin.Plugin)
|
||||
}),
|
||||
).pipe(Effect.withSpan("PluginBoot.boot"))
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer.pipe(
|
||||
).pipe(
|
||||
Layer.provideMerge(PluginV2.locationLayer),
|
||||
Layer.provideMerge(AISDK.locationLayer),
|
||||
Layer.provideMerge(Integration.locationLayer),
|
||||
Layer.provideMerge(Catalog.locationLayer),
|
||||
Layer.provideMerge(CommandV2.locationLayer),
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
export * as CommandPlugin from "./command"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { Location } from "../location"
|
||||
import PROMPT_INITIALIZE from "./command/initialize.txt"
|
||||
import PROMPT_REVIEW from "./command/review.txt"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "command",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const location = yield* Location.Service
|
||||
yield* ctx.command.transform((draft) => {
|
||||
draft.update("init", (command) => {
|
||||
command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
|
||||
command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
|
||||
command.description = "guided AGENTS.md setup"
|
||||
})
|
||||
draft.update("review", (command) => {
|
||||
command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
|
||||
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
|
||||
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
||||
command.subtask = true
|
||||
})
|
||||
|
||||
@@ -1,58 +1,31 @@
|
||||
export * as PluginHost from "./host"
|
||||
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { AISDK } from "../aisdk"
|
||||
import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
import { EventV2 } from "../event"
|
||||
import { FileSystem } from "../filesystem"
|
||||
import { Global } from "../global"
|
||||
import { Integration } from "../integration"
|
||||
import { Location } from "../location"
|
||||
import { ModelV2 } from "../model"
|
||||
import { Npm } from "../npm"
|
||||
import { PluginV2 } from "../plugin"
|
||||
import type { PluginV2 } from "../plugin"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { Reference } from "../reference"
|
||||
import { SkillV2 } from "../skill"
|
||||
|
||||
type EventMap = { [Item in SDKEvent as Item["type"]]: Item }
|
||||
type SDKHook = (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}) => Effect.Effect<void> | void
|
||||
type LanguageHook = (event: {
|
||||
readonly model: ModelV2Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}) => Effect.Effect<void> | void
|
||||
|
||||
export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
|
||||
const agents = yield* AgentV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* CommandV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const filesystem = yield* FileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const integration = yield* Integration.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
|
||||
return {
|
||||
options: {},
|
||||
agent: {
|
||||
get: (id) => agents.get(AgentV2.ID.make(id)),
|
||||
default: agents.default,
|
||||
list: agents.all,
|
||||
rebuild: agents.rebuild,
|
||||
reload: agents.reload,
|
||||
transform: (callback) =>
|
||||
agents.transform((draft) =>
|
||||
callback({
|
||||
@@ -65,51 +38,35 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
),
|
||||
},
|
||||
aisdk: {
|
||||
hook: (name, callback) => {
|
||||
if (name === "sdk") {
|
||||
const run = callback as SDKHook
|
||||
return plugin.hook("aisdk.sdk", (event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
const result = run(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
})
|
||||
}
|
||||
const run = callback as LanguageHook
|
||||
return plugin.hook("aisdk.language", (event) => {
|
||||
sdk: (callback) =>
|
||||
aisdk.hook.sdk((event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
package: event.package,
|
||||
options: event.options,
|
||||
sdk: event.sdk,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
}),
|
||||
language: (callback) =>
|
||||
aisdk.hook.language((event) => {
|
||||
const output = {
|
||||
model: event.model,
|
||||
sdk: event.sdk,
|
||||
options: event.options,
|
||||
language: event.language,
|
||||
}
|
||||
const result = run(output)
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||
)
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
get: (id) => catalog.provider.get(ProviderV2.ID.make(id)),
|
||||
list: catalog.provider.all,
|
||||
available: catalog.provider.available,
|
||||
},
|
||||
model: {
|
||||
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
|
||||
list: catalog.model.all,
|
||||
available: catalog.model.available,
|
||||
default: catalog.model.default,
|
||||
small: (providerID) => catalog.model.small(ProviderV2.ID.make(providerID)),
|
||||
},
|
||||
rebuild: catalog.rebuild,
|
||||
reload: catalog.reload,
|
||||
transform: (callback) =>
|
||||
catalog.transform((draft) =>
|
||||
callback({
|
||||
@@ -135,41 +92,11 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
),
|
||||
},
|
||||
command: {
|
||||
get: commands.get,
|
||||
list: commands.list,
|
||||
rebuild: commands.rebuild,
|
||||
reload: commands.reload,
|
||||
transform: commands.transform,
|
||||
},
|
||||
event: {
|
||||
subscribe: <Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
|
||||
Stream.unwrap(
|
||||
Effect.sync(() => {
|
||||
const definition = EventV2.registry.get(type)
|
||||
if (!definition) throw new Error(`Unknown event type: ${type}`)
|
||||
const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
|
||||
return events.subscribe(definition).pipe(
|
||||
Stream.map(
|
||||
(event) =>
|
||||
({
|
||||
id: event.id,
|
||||
type: event.type,
|
||||
properties: encode(event.data),
|
||||
}) as unknown as EventMap[Type],
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
},
|
||||
filesystem: {
|
||||
read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
|
||||
list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
|
||||
find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
|
||||
glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
|
||||
},
|
||||
integration: {
|
||||
get: (id) => integration.get(Integration.ID.make(id)),
|
||||
list: integration.list,
|
||||
rebuild: integration.rebuild,
|
||||
reload: integration.reload,
|
||||
transform: (callback) =>
|
||||
integration.transform((draft) =>
|
||||
callback({
|
||||
@@ -198,19 +125,12 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
}),
|
||||
),
|
||||
},
|
||||
location,
|
||||
npm,
|
||||
path: {
|
||||
home: global.home,
|
||||
data: global.data,
|
||||
cache: global.cache,
|
||||
config: global.config,
|
||||
state: global.state,
|
||||
temp: global.tmp,
|
||||
plugin: {
|
||||
reload: plugin.reload,
|
||||
transform: plugin.transform,
|
||||
},
|
||||
reference: {
|
||||
list: reference.list,
|
||||
rebuild: reference.rebuild,
|
||||
reload: reference.reload,
|
||||
transform: (callback) =>
|
||||
reference.transform((draft) =>
|
||||
callback({
|
||||
@@ -221,9 +141,7 @@ export const make = Effect.fn("PluginHost.make")(function* () {
|
||||
),
|
||||
},
|
||||
skill: {
|
||||
sources: skill.sources,
|
||||
list: skill.list,
|
||||
rebuild: skill.rebuild,
|
||||
reload: skill.reload,
|
||||
transform: (callback) =>
|
||||
skill.transform((draft) =>
|
||||
callback({
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export * as PluginInternal from "./internal"
|
||||
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { AgentV2 } from "../agent"
|
||||
import type { Catalog } from "../catalog"
|
||||
import type { CommandV2 } from "../command"
|
||||
import type { Config } from "../config"
|
||||
import type { EventV2 } from "../event"
|
||||
import type { FileSystem } from "../filesystem"
|
||||
import type { FSUtil } from "../fs-util"
|
||||
import type { Global } from "../global"
|
||||
import type { Integration } from "../integration"
|
||||
import type { Location } from "../location"
|
||||
import type { ModelsDev } from "../models-dev"
|
||||
import type { Npm } from "../npm"
|
||||
import type { Reference } from "../reference"
|
||||
import type { SkillV2 } from "../skill"
|
||||
|
||||
export type Requirements =
|
||||
| AgentV2.Service
|
||||
| Catalog.Service
|
||||
| CommandV2.Service
|
||||
| Config.Service
|
||||
| EventV2.Service
|
||||
| FileSystem.Service
|
||||
| FSUtil.Service
|
||||
| Global.Service
|
||||
| Integration.Service
|
||||
| Location.Service
|
||||
| ModelsDev.Service
|
||||
| Npm.Service
|
||||
| Reference.Service
|
||||
| SkillV2.Service
|
||||
|
||||
export interface Plugin<R = never> {
|
||||
readonly id: string
|
||||
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
|
||||
}
|
||||
|
||||
export function define<R>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ModelRequest } from "../model-request"
|
||||
import { ModelsDev } from "../models-dev"
|
||||
@@ -52,6 +53,7 @@ export const ModelsDevPlugin = define({
|
||||
id: "models-dev",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const events = yield* EventV2.Service
|
||||
yield* ctx.integration.transform(
|
||||
Effect.fn(function* (integrations) {
|
||||
const data = yield* modelsDev.get()
|
||||
@@ -128,8 +130,8 @@ export const ModelsDevPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.event.subscribe("models-dev.refreshed").pipe(
|
||||
Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
|
||||
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
export * as PluginPromise from "./promise"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
|
||||
import { Effect, Scope } from "effect"
|
||||
|
||||
// The Effect host hands back this registration shape; mirror it structurally so
|
||||
// we do not have to alias the Effect package's `Registration` against the Promise one.
|
||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||
|
||||
/**
|
||||
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
|
||||
* loader (`PluginV2` / `PluginBoot`) can run it unchanged.
|
||||
*
|
||||
* Hook registrations created during the async `setup` attach to the plugin's
|
||||
* scope, so unloading the plugin disposes them. The captured fiber context
|
||||
* preserves boot-time batching, so Promise-plugin transforms still coalesce
|
||||
* into one reload per domain.
|
||||
*/
|
||||
export function fromPromise(plugin: Plugin) {
|
||||
return define({
|
||||
id: plugin.id,
|
||||
effect: (host) =>
|
||||
Effect.gen(function* () {
|
||||
const scope = yield* Scope.Scope
|
||||
const context = yield* Effect.context<Scope.Scope>()
|
||||
|
||||
// Run a hook registration on the plugin scope and resolve once it is registered.
|
||||
const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
|
||||
Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({
|
||||
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
|
||||
}))
|
||||
|
||||
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
|
||||
|
||||
const transform =
|
||||
<Draft>(domain: {
|
||||
transform: (
|
||||
callback: (draft: Draft) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<HostRegistration, never, Scope.Scope>
|
||||
}) =>
|
||||
(callback: (draft: Draft) => Promise<void> | void) =>
|
||||
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
|
||||
|
||||
const context2: PluginContext = {
|
||||
options: host.options,
|
||||
agent: {
|
||||
transform: transform(host.agent),
|
||||
reload: () => run(host.agent.reload()),
|
||||
},
|
||||
aisdk: {
|
||||
sdk: (callback) =>
|
||||
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
language: (callback) =>
|
||||
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||
},
|
||||
catalog: {
|
||||
transform: transform(host.catalog),
|
||||
reload: () => run(host.catalog.reload()),
|
||||
},
|
||||
command: {
|
||||
transform: transform(host.command),
|
||||
reload: () => run(host.command.reload()),
|
||||
},
|
||||
integration: {
|
||||
transform: transform(host.integration),
|
||||
reload: () => run(host.integration.reload()),
|
||||
},
|
||||
plugin: {
|
||||
transform: transform(host.plugin),
|
||||
reload: () => run(host.plugin.reload()),
|
||||
},
|
||||
reference: {
|
||||
transform: transform(host.reference),
|
||||
reload: () => run(host.reference.reload()),
|
||||
},
|
||||
skill: {
|
||||
transform: transform(host.skill),
|
||||
reload: () => run(host.skill.reload()),
|
||||
},
|
||||
}
|
||||
|
||||
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -30,8 +30,10 @@ import { VercelPlugin } from "./provider/vercel"
|
||||
import { VenicePlugin } from "./provider/venice"
|
||||
import { XAIPlugin } from "./provider/xai"
|
||||
import { ZenmuxPlugin } from "./provider/zenmux"
|
||||
import type { PluginInternal } from "./internal"
|
||||
import type { Scope } from "effect"
|
||||
|
||||
export const ProviderPlugins = [
|
||||
export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
|
||||
AlibabaPlugin,
|
||||
AmazonBedrockPlugin,
|
||||
AnthropicPlugin,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const AlibabaPlugin = define({
|
||||
id: "alibaba",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/alibaba") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
type MantleSDK = {
|
||||
@@ -78,8 +78,7 @@ export const AmazonBedrockPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
|
||||
const options = { ...evt.options }
|
||||
@@ -112,8 +111,7 @@ export const AmazonBedrockPlugin = define({
|
||||
evt.sdk = mod.createAmazonBedrock(options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
|
||||
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const AnthropicPlugin = define({
|
||||
id: "anthropic",
|
||||
@@ -16,8 +16,7 @@ export const AnthropicPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
|
||||
@@ -28,8 +28,7 @@ export const AzurePlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/azure") return
|
||||
if (evt.model.providerID === ProviderV2.ID.azure) {
|
||||
@@ -47,8 +46,7 @@ export const AzurePlugin = define({
|
||||
evt.sdk = mod.createAzure(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.azure) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
@@ -74,8 +72,7 @@ export const AzureCognitiveServicesPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
|
||||
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CerebrasPlugin = define({
|
||||
id: "cerebras",
|
||||
@@ -15,8 +15,7 @@ export const CerebrasPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cerebras") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CloudflareAIGatewayPlugin = define({
|
||||
id: "cloudflare-ai-gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "ai-gateway-provider") return
|
||||
if (evt.options.baseURL) return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
|
||||
@@ -21,8 +21,7 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
@@ -38,8 +37,7 @@ export const CloudflareWorkersAIPlugin = define({
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== providerID) return
|
||||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const CoherePlugin = define({
|
||||
id: "cohere",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/cohere") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const DeepInfraPlugin = define({
|
||||
id: "deepinfra",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/deepinfra") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Npm } from "../../npm"
|
||||
|
||||
export const DynamicProviderPlugin = define({
|
||||
id: "dynamic-provider",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GatewayPlugin = define({
|
||||
id: "gateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/gateway") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
@@ -25,16 +25,14 @@ export const GithubCopilotPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/github-copilot") return
|
||||
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
|
||||
evt.sdk = mod.createOpenaiCompatible(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
|
||||
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import os from "os"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const GitLabPlugin = define({
|
||||
id: "gitlab",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "gitlab-ai-provider") return
|
||||
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
|
||||
@@ -32,8 +31,7 @@ export const GitLabPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
|
||||
const featureFlags =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function resolveProject(options: Record<string, any>) {
|
||||
@@ -84,8 +84,7 @@ export const GoogleVertexPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
|
||||
evt.options.fetch = authFetch(evt.options.fetch)
|
||||
@@ -104,8 +103,7 @@ export const GoogleVertexPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
@@ -139,8 +137,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
|
||||
@@ -166,8 +163,7 @@ export const GoogleVertexAnthropicPlugin = define({
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
|
||||
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GooglePlugin = define({
|
||||
id: "google",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/google") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const GroqPlugin = define({
|
||||
id: "groq",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/groq") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const KiloPlugin = define({
|
||||
id: "kilo",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Integration } from "../../integration"
|
||||
|
||||
export const LLMGatewayPlugin = define({
|
||||
id: "llmgateway",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const integrations = yield* Integration.Service
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
for (const item of evt.provider.list()) {
|
||||
@@ -11,7 +13,7 @@ export const LLMGatewayPlugin = define({
|
||||
if (item.provider.api.type !== "aisdk") continue
|
||||
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
|
||||
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
|
||||
if (!(yield* ctx.integration.get(item.provider.id))) continue
|
||||
if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
|
||||
evt.provider.update(item.provider.id, (provider) => {
|
||||
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
|
||||
provider.request.headers["X-Title"] = "opencode"
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const MistralPlugin = define({
|
||||
id: "mistral",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/mistral") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const NvidiaPlugin = define({
|
||||
id: "nvidia",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const OpenAICompatiblePlugin = define({
|
||||
id: "openai-compatible",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.sdk) return
|
||||
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { Integration } from "../../integration"
|
||||
import { browser, headless } from "./openai-auth"
|
||||
@@ -27,16 +27,14 @@ export const OpenAIPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/openai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
|
||||
evt.sdk = mod.createOpenAI(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.openai) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { Integration } from "../../integration"
|
||||
|
||||
export const OpencodePlugin = define({
|
||||
id: "opencode",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const integrations = yield* Integration.Service
|
||||
let hasKey = false
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (evt) {
|
||||
const item = evt.provider.get(ProviderV2.ID.opencode)
|
||||
if (!item) return
|
||||
const integration = yield* ctx.integration.get(item.provider.id)
|
||||
const integration = yield* integrations.get(Integration.ID.make(item.provider.id))
|
||||
hasKey = Boolean(
|
||||
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const OpenRouterPlugin = define({
|
||||
id: "openrouter",
|
||||
@@ -25,8 +25,7 @@ export const OpenRouterPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@openrouter/ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const PerplexityPlugin = define({
|
||||
id: "perplexity",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/perplexity") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { Npm } from "../../npm"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const SapAICorePlugin = define({
|
||||
id: "sap-ai-core",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
const npm = yield* Npm.Service
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
const serviceKey =
|
||||
@@ -17,7 +18,7 @@ export const SapAICorePlugin = define({
|
||||
|
||||
const installedPath = evt.package.startsWith("file://")
|
||||
? evt.package
|
||||
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||
|
||||
const mod = yield* Effect.promise(async () => {
|
||||
@@ -35,8 +36,7 @@ export const SapAICorePlugin = define({
|
||||
)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
|
||||
evt.language = evt.sdk(evt.model.api.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
|
||||
@@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
|
||||
export const SnowflakeCortexPlugin = define({
|
||||
id: "snowflake-cortex",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
|
||||
const token =
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const TogetherAIPlugin = define({
|
||||
id: "togetherai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/togetherai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const VenicePlugin = define({
|
||||
id: "venice",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "venice-ai-sdk-provider") return
|
||||
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const VercelPlugin = define({
|
||||
id: "vercel",
|
||||
@@ -16,8 +16,7 @@ export const VercelPlugin = define({
|
||||
}
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/vercel") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const XAIPlugin = define({
|
||||
id: "xai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.aisdk.hook(
|
||||
"sdk",
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
evt.sdk = mod.createXai(evt.options)
|
||||
}),
|
||||
)
|
||||
yield* ctx.aisdk.hook(
|
||||
"language",
|
||||
yield* ctx.aisdk.language(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "../internal"
|
||||
|
||||
export const ZenmuxPlugin = define({
|
||||
id: "zenmux",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export * as SkillPlugin from "./skill"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { define } from "./internal"
|
||||
import { Effect } from "effect"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { SkillV2 } from "../skill"
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Slug } from "../util/slug"
|
||||
import { EventV2 } from "../event"
|
||||
import { Database } from "../database/database"
|
||||
import { Location } from "../location"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
|
||||
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
||||
export type StrategyID = typeof StrategyID.Type
|
||||
@@ -125,10 +124,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
|
||||
|
||||
export const refreshAfterBoot = Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
const copies = yield* Service
|
||||
yield* Effect.gen(function* () {
|
||||
yield* boot.wait()
|
||||
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
|
||||
const result = yield* copies.refresh({ projectID: location.project.id })
|
||||
yield* Effect.logInfo("project copy refresh done", {
|
||||
|
||||
@@ -126,7 +126,7 @@ export const layer = Layer.effect(
|
||||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
list: Effect.fn("Reference.list")(function* () {
|
||||
return Array.from(materialized.values())
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as ReferenceGuidance from "./guidance"
|
||||
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { Reference } from "../reference"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
@@ -34,12 +33,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const references = yield* Reference.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("ReferenceGuidance.load")(function* () {
|
||||
yield* boot.wait()
|
||||
const available = (yield* references.list())
|
||||
.filter((reference) => reference.description !== undefined)
|
||||
.map((reference) => ({
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Integration } from "../../integration"
|
||||
import { IntegrationConnection } from "../../integration/connection"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ModelRequest } from "../../model-request"
|
||||
import { PluginBoot } from "../../plugin/boot"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
@@ -178,11 +177,9 @@ export const locationLayer = Layer.effect(
|
||||
const catalog = yield* Catalog.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const integrations = yield* Integration.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
return Service.of({
|
||||
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
|
||||
// Location plugins populate and filter the catalog asynchronously during layer startup.
|
||||
yield* boot.wait()
|
||||
const defaultModel = session.model ? undefined : yield* catalog.model.default()
|
||||
const selected = session.model
|
||||
? (yield* catalog.model.available()).find(
|
||||
|
||||
@@ -148,7 +148,7 @@ export const layer = Layer.effect(
|
||||
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
rebuild: state.rebuild,
|
||||
reload: state.reload,
|
||||
sources: Effect.fn("SkillV2.sources")(function* () {
|
||||
return state.get().sources
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,6 @@ export * as SkillGuidance from "./guidance"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
@@ -40,12 +39,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const skills = yield* SkillV2.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("SkillGuidance.load")(function* (selection) {
|
||||
yield* boot.wait()
|
||||
const agent = selection.info
|
||||
if (!agent) return SystemContext.empty
|
||||
const permitted = SkillV2.available(yield* skills.list(), agent)
|
||||
|
||||
+15
-15
@@ -3,7 +3,7 @@ export * as State from "./state"
|
||||
import { Context, Effect, Scope, Semaphore } from "effect"
|
||||
|
||||
/**
|
||||
* A replayable transform applied to a draft during rebuild.
|
||||
* A replayable transform applied to a draft during reload.
|
||||
*
|
||||
* Domain drafts expose readable and writable state while preserving concise
|
||||
* plugin/config code. Transforms may perform Effects before returning.
|
||||
@@ -19,14 +19,14 @@ export type Transform<DraftApi> = (
|
||||
transform: TransformCallback<DraftApi>,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
export type Rebuild = () => Effect.Effect<void>
|
||||
export type Reload = () => Effect.Effect<void>
|
||||
|
||||
export interface Transformable<DraftApi> {
|
||||
readonly transform: Transform<DraftApi>
|
||||
readonly rebuild: Rebuild
|
||||
readonly reload: Reload
|
||||
}
|
||||
|
||||
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
|
||||
const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
|
||||
defaultValue: () => undefined,
|
||||
})
|
||||
|
||||
@@ -34,15 +34,15 @@ export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const current = yield* CurrentBatch
|
||||
if (current) return yield* effect
|
||||
const rebuilds = new Set<Rebuild>()
|
||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
|
||||
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
|
||||
const reloads = new Set<Reload>()
|
||||
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
|
||||
yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export interface Options<State, DraftApi> {
|
||||
/** Creates the base value for initial state and every scoped-transform rebuild. */
|
||||
/** Creates the base value for initial state and every scoped-transform reload. */
|
||||
readonly initial: () => State
|
||||
/** Wraps mutable state in a domain-specific draft API. */
|
||||
readonly draft: MakeDraft<State, DraftApi>
|
||||
@@ -54,7 +54,7 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
|
||||
readonly get: () => State
|
||||
/**
|
||||
* Registers and applies a scoped transform. Closing the owning Scope removes
|
||||
* the transform and rebuilds the materialized state.
|
||||
* the transform and reloads the materialized state.
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
||||
const materialize = Effect.fnUntraced(function* () {
|
||||
const next = options.initial()
|
||||
const api = options.draft(next)
|
||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
|
||||
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update"))
|
||||
yield* commit(next)
|
||||
})
|
||||
|
||||
const rebuild = () => semaphore.withPermit(materialize())
|
||||
const reload = () => semaphore.withPermit(materialize())
|
||||
|
||||
const result: Interface<State, DraftApi> = {
|
||||
get: () => state,
|
||||
@@ -101,7 +101,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
||||
return Effect.gen(function* () {
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) {
|
||||
batch.add(rebuild)
|
||||
batch.add(reload)
|
||||
return
|
||||
}
|
||||
yield* materialize()
|
||||
@@ -116,13 +116,13 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
|
||||
)
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
const batch = yield* CurrentBatch
|
||||
if (batch) batch.add(rebuild)
|
||||
else yield* rebuild()
|
||||
if (batch) batch.add(reload)
|
||||
else yield* reload()
|
||||
return { dispose }
|
||||
}),
|
||||
)
|
||||
}),
|
||||
rebuild,
|
||||
reload,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { pathToFileURL } from "url"
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { PluginBoot } from "../plugin/boot"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Tool } from "./tool"
|
||||
@@ -58,10 +57,8 @@ export const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const tools = yield* Tools.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const boot = yield* PluginBoot.Service
|
||||
const skills = yield* SkillV2.Service
|
||||
const permission = yield* PermissionV2.Service
|
||||
yield* boot.wait()
|
||||
yield* tools
|
||||
.register({
|
||||
[name]: Tool.make({
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("AgentV2", () => {
|
||||
)
|
||||
description = "New description"
|
||||
hidden = false
|
||||
yield* agent.rebuild()
|
||||
yield* agent.reload()
|
||||
|
||||
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
|
||||
}),
|
||||
@@ -104,8 +104,12 @@ describe("AgentV2", () => {
|
||||
yield* AgentPlugin.Plugin.effect(
|
||||
host({
|
||||
agent: agentHost(agent),
|
||||
location: location({ directory: AbsolutePath.make("/project") }),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||
),
|
||||
)
|
||||
|
||||
const agents = yield* agent.all()
|
||||
|
||||
@@ -259,7 +259,7 @@ describe("CatalogV2", () => {
|
||||
expect((yield* catalog.model.default())?.id).toBe(old)
|
||||
|
||||
configured = false
|
||||
yield* catalog.rebuild()
|
||||
yield* catalog.reload()
|
||||
expect((yield* catalog.model.default())?.id).toBe(newest)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ Review files`,
|
||||
})
|
||||
|
||||
const command = yield* CommandV2.Service
|
||||
yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
|
||||
yield* ConfigCommandPlugin.Plugin.effect(host({ command: { ...command, reload: command.reload } })).pipe(
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
|
||||
export default define({
|
||||
id: "directory-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
agents.update("directory", (agent) => {
|
||||
agent.description = "Loaded from plugin directory"
|
||||
agent.mode = "subagent"
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,248 @@
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "../plugin/fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
describe("ConfigExternalPlugin", () => {
|
||||
it.live("resolves and loads a configured Promise plugin with options", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
const document = path.join(import.meta.dir, "config.json")
|
||||
|
||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(PluginV2.Service, plugins),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
path: document,
|
||||
info: decode({
|
||||
plugins: [
|
||||
{
|
||||
package: "../plugin/fixtures/config-promise-plugin.ts",
|
||||
options: { description: "Loaded from config" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||
description: "Loaded from config",
|
||||
mode: "subagent",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads a configured Effect plugin with options", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
|
||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(PluginV2.Service, plugins),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
path: path.join(import.meta.dir, "config.json"),
|
||||
info: decode({
|
||||
plugins: [
|
||||
{
|
||||
package: "../plugin/fixtures/config-effect-plugin.ts",
|
||||
options: { description: "Effect plugin from config" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* waitForAgent(agents, "effect-configured")).toMatchObject({
|
||||
description: "Effect plugin from config",
|
||||
mode: "subagent",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("ignores invalid plugins and continues loading", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
|
||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(PluginV2.Service, plugins),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
path: path.join(import.meta.dir, "config.json"),
|
||||
info: decode({
|
||||
plugins: [
|
||||
"../plugin/fixtures/missing-plugin.ts",
|
||||
"../plugin/fixtures/invalid-plugin.ts",
|
||||
{
|
||||
package: "../plugin/fixtures/config-promise-plugin.ts",
|
||||
options: { description: "Loaded after invalid plugins" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||
description: "Loaded after invalid plugins",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("installs and resolves npm plugin packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
let installed: string | undefined
|
||||
const npm = Npm.Service.of({
|
||||
add: (spec) =>
|
||||
Effect.sync(() => {
|
||||
installed = spec
|
||||
return {
|
||||
directory: import.meta.dir,
|
||||
entrypoint: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
|
||||
}
|
||||
}),
|
||||
install: () => Effect.void,
|
||||
which: () => Effect.succeed(undefined),
|
||||
})
|
||||
|
||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(PluginV2.Service, plugins),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode({
|
||||
plugins: [
|
||||
{
|
||||
package: "example-plugin@1.0.0",
|
||||
options: { description: "Installed from npm" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
|
||||
description: "Installed from npm",
|
||||
})
|
||||
expect(installed).toBe("example-plugin@1.0.0")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("loads plugin files from config directories", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const npm = yield* Npm.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
|
||||
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
|
||||
Effect.provideService(PluginV2.Service, plugins),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Directory({
|
||||
type: "directory",
|
||||
path: AbsolutePath.make(path.join(import.meta.dir, "fixtures")),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* waitForAgent(agents, "directory")).toMatchObject({
|
||||
description: "Loaded from plugin directory",
|
||||
mode: "subagent",
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) {
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const agent = yield* agents.get(AgentV2.ID.make(id))
|
||||
if (agent) return agent
|
||||
yield* Effect.sleep("10 millis")
|
||||
}
|
||||
return yield* Effect.die(`Timed out waiting for agent ${id}`)
|
||||
})
|
||||
@@ -15,11 +15,8 @@ const it = testEffect(PluginTestLayer)
|
||||
|
||||
const addPlugin = Effect.fn(function* (config: Config.Interface) {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make()
|
||||
yield* plugin.add({
|
||||
...ConfigProviderPlugin.Plugin,
|
||||
effect: ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)),
|
||||
})
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config))
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
|
||||
@@ -36,16 +36,11 @@ describe("ConfigSkillPlugin.Plugin", () => {
|
||||
|
||||
yield* ConfigSkillPlugin.Plugin.effect(
|
||||
host({
|
||||
location: location({ directory }),
|
||||
path: { ...host().path, home: "/home/test" },
|
||||
skill: SkillV2.Service.of({
|
||||
transform,
|
||||
rebuild: () => Effect.void,
|
||||
sources: () => Effect.succeed(sources),
|
||||
list: () => Effect.succeed([]),
|
||||
}),
|
||||
skill: { transform, reload: () => Effect.void },
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })),
|
||||
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
|
||||
Effect.provideService(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { DateTime, Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
|
||||
import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
|
||||
import { Tool } from "@opencode-ai/core/public"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
@@ -88,7 +88,6 @@ describe("LocationServiceMap", () => {
|
||||
|
||||
const update = (directory: string) =>
|
||||
Effect.gen(function* () {
|
||||
yield* PluginBoot.Service.use((boot) => boot.wait())
|
||||
yield* Reference.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
|
||||
@@ -197,36 +196,24 @@ describe("LocationServiceMap", () => {
|
||||
).pipe(
|
||||
Effect.flatMap((dir) =>
|
||||
Effect.gen(function* () {
|
||||
const boot = yield* PluginBoot.Service
|
||||
const catalogUpdated = yield* Deferred.make<void>()
|
||||
const seen: string[] = []
|
||||
yield* boot.add(
|
||||
define({
|
||||
id: "reviewer",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.event.subscribe("catalog.updated").pipe(
|
||||
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* ctx.agent.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
const plugins = yield* PluginV2.Service
|
||||
yield* plugins.transform((draft) =>
|
||||
draft.add(
|
||||
define({
|
||||
id: "reviewer",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
})
|
||||
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
catalog.provider.update("public", (provider) => {
|
||||
provider.name = "Public provider"
|
||||
})
|
||||
})
|
||||
}),
|
||||
}),
|
||||
.pipe(Effect.asVoid),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
yield* Deferred.await(catalogUpdated)
|
||||
expect(seen).toEqual(["Reviews code"])
|
||||
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||
description: "Reviews code",
|
||||
mode: "subagent",
|
||||
|
||||
@@ -1,127 +1,44 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Effect } from "effect"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { State } from "@opencode-ai/core/state"
|
||||
import { it } from "./lib/effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { PluginTestLayer } from "./plugin/fixture"
|
||||
|
||||
const events = Layer.mock(EventV2.Service)({
|
||||
publish: (definition, data) =>
|
||||
Effect.succeed({
|
||||
id: EventV2.ID.make("evt_plugin_test"),
|
||||
type: definition.type,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
||||
|
||||
function state() {
|
||||
return State.create({
|
||||
initial: () => ({ values: [] as string[] }),
|
||||
draft: (draft) => ({
|
||||
add: (value: string) => draft.values.push(value),
|
||||
}),
|
||||
})
|
||||
}
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
describe("PluginV2", () => {
|
||||
it.effect("closes plugin-owned scopes when the registry layer finalizes", () =>
|
||||
it.effect("reconciles transformed plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const values = state()
|
||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
let description = "first"
|
||||
|
||||
yield* plugin.add({
|
||||
id: PluginV2.ID.make("scoped"),
|
||||
effect: Effect.gen(function* () {
|
||||
yield* values.transform((editor) => {
|
||||
editor.add("scoped")
|
||||
})
|
||||
}),
|
||||
})
|
||||
expect(values.get().values).toEqual(["scoped"])
|
||||
|
||||
yield* Scope.close(layerScope, Exit.void)
|
||||
expect(values.get().values).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
|
||||
Effect.gen(function* () {
|
||||
let finalized = 0
|
||||
const values = State.create({
|
||||
initial: () => ({ values: [] as string[] }),
|
||||
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
|
||||
finalize: () => Effect.sync(() => finalized++),
|
||||
})
|
||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
||||
|
||||
yield* State.batch(
|
||||
Effect.forEach(
|
||||
["first", "second"],
|
||||
(id) =>
|
||||
plugin.add({
|
||||
id: PluginV2.ID.make(id),
|
||||
effect: values
|
||||
.transform((editor) => {
|
||||
editor.add(id)
|
||||
})
|
||||
const registration = yield* plugins.transform((draft) => {
|
||||
draft.add(
|
||||
define({
|
||||
id: "managed",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agents) =>
|
||||
agents.update("configured", (agent) => {
|
||||
agent.description = description
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid),
|
||||
}),
|
||||
{ discard: true },
|
||||
),
|
||||
)
|
||||
finalized = 0
|
||||
|
||||
yield* Scope.close(layerScope, Exit.void)
|
||||
expect(values.get().values).toEqual([])
|
||||
expect(finalized).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("serializes same-ID additions and leaves one removable attachment", () =>
|
||||
Effect.gen(function* () {
|
||||
const values = state()
|
||||
const layerScope = yield* Scope.fork(yield* Scope.Scope)
|
||||
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
|
||||
const id = PluginV2.ID.make("shared")
|
||||
const firstStarted = yield* Deferred.make<void>()
|
||||
const releaseFirst = yield* Deferred.make<void>()
|
||||
|
||||
const first = yield* plugin
|
||||
.add({
|
||||
id,
|
||||
effect: Effect.gen(function* () {
|
||||
yield* values.transform((editor) => {
|
||||
editor.add("first")
|
||||
})
|
||||
yield* Deferred.succeed(firstStarted, undefined)
|
||||
yield* Deferred.await(releaseFirst)
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.forkChild)
|
||||
yield* Deferred.await(firstStarted)
|
||||
)
|
||||
})
|
||||
|
||||
const second = yield* plugin
|
||||
.add({
|
||||
id,
|
||||
effect: Effect.gen(function* () {
|
||||
yield* values.transform((editor) => {
|
||||
editor.add("second")
|
||||
})
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.forkChild({ startImmediately: true }))
|
||||
expect(values.get().values).toEqual(["first"])
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||
|
||||
yield* Deferred.succeed(releaseFirst, undefined)
|
||||
yield* Fiber.join(first)
|
||||
yield* Fiber.join(second)
|
||||
expect(values.get().values).toEqual(["second"])
|
||||
description = "second"
|
||||
yield* plugins.reload()
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||
|
||||
yield* plugin.remove(id)
|
||||
expect(values.get().values).toEqual([])
|
||||
yield* registration.dispose
|
||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -24,9 +24,13 @@ describe("CommandPlugin.Plugin", () => {
|
||||
const command = yield* CommandV2.Service
|
||||
yield* CommandPlugin.Plugin.effect(
|
||||
host({
|
||||
command,
|
||||
location: location({ directory }, { projectDirectory: project }),
|
||||
command: { transform: command.transform, reload: command.reload },
|
||||
}),
|
||||
).pipe(
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory }, { projectDirectory: project })),
|
||||
),
|
||||
)
|
||||
|
||||
expect(yield* command.get("init")).toMatchObject({
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
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"
|
||||
@@ -8,23 +5,13 @@ 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(
|
||||
export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe(
|
||||
Layer.provideMerge(
|
||||
Layer.mergeAll(
|
||||
Credential.defaultLayer,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default define({
|
||||
id: "config-effect-plugin",
|
||||
effect: (ctx) =>
|
||||
ctx.agent
|
||||
.transform((agents) => {
|
||||
agents.update("effect-configured", (agent) => {
|
||||
agent.description = ctx.options.description
|
||||
agent.mode = "subagent"
|
||||
})
|
||||
})
|
||||
.pipe(Effect.asVoid),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
|
||||
export default define({
|
||||
id: "config-promise-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
agents.update("configured", (agent) => {
|
||||
agent.description = ctx.options.description
|
||||
agent.mode = "subagent"
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
export default {}
|
||||
@@ -1,120 +1,55 @@
|
||||
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
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 { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
|
||||
type Overrides = Partial<Omit<PluginContext, "options">>
|
||||
|
||||
export function host(overrides: Overrides = {}): PluginContext {
|
||||
return {
|
||||
aisdk: {
|
||||
hook: () => Effect.die("unused aisdk.hook"),
|
||||
},
|
||||
agent: {
|
||||
get: () => Effect.die("unused agent.get"),
|
||||
default: () => Effect.die("unused agent.default"),
|
||||
list: () => Effect.die("unused agent.list"),
|
||||
rebuild: () => Effect.die("unused agent.rebuild"),
|
||||
options: {},
|
||||
agent: overrides.agent ?? {
|
||||
transform: () => Effect.die("unused agent.transform"),
|
||||
reload: () => Effect.die("unused agent.reload"),
|
||||
},
|
||||
catalog: {
|
||||
provider: {
|
||||
get: () => Effect.die("unused catalog.provider.get"),
|
||||
list: () => Effect.die("unused catalog.provider.list"),
|
||||
available: () => Effect.die("unused catalog.provider.available"),
|
||||
},
|
||||
model: {
|
||||
get: () => Effect.die("unused catalog.model.get"),
|
||||
list: () => Effect.die("unused catalog.model.list"),
|
||||
available: () => Effect.die("unused catalog.model.available"),
|
||||
default: () => Effect.die("unused catalog.model.default"),
|
||||
small: () => Effect.die("unused catalog.model.small"),
|
||||
},
|
||||
rebuild: () => Effect.die("unused catalog.rebuild"),
|
||||
aisdk: overrides.aisdk ?? {
|
||||
sdk: () => Effect.die("unused aisdk.sdk"),
|
||||
language: () => Effect.die("unused aisdk.language"),
|
||||
},
|
||||
catalog: overrides.catalog ?? {
|
||||
transform: () => Effect.die("unused catalog.transform"),
|
||||
reload: () => Effect.die("unused catalog.reload"),
|
||||
},
|
||||
command: {
|
||||
get: () => Effect.die("unused command.get"),
|
||||
list: () => Effect.die("unused command.list"),
|
||||
rebuild: () => Effect.die("unused command.rebuild"),
|
||||
command: overrides.command ?? {
|
||||
transform: () => Effect.die("unused command.transform"),
|
||||
reload: () => Effect.die("unused command.reload"),
|
||||
},
|
||||
event: {
|
||||
subscribe: () => Stream.die("unused event.subscribe"),
|
||||
},
|
||||
filesystem: {
|
||||
read: () => Effect.die("unused filesystem.read"),
|
||||
list: () => Effect.die("unused filesystem.list"),
|
||||
find: () => Effect.die("unused filesystem.find"),
|
||||
glob: () => Effect.die("unused filesystem.glob"),
|
||||
},
|
||||
integration: {
|
||||
get: () => Effect.die("unused integration.get"),
|
||||
list: () => Effect.die("unused integration.list"),
|
||||
rebuild: () => Effect.die("unused integration.rebuild"),
|
||||
integration: overrides.integration ?? {
|
||||
transform: () => Effect.die("unused integration.transform"),
|
||||
reload: () => Effect.die("unused integration.reload"),
|
||||
},
|
||||
location: {
|
||||
directory: "/unused/location",
|
||||
project: { directory: "/unused/project" },
|
||||
plugin: overrides.plugin ?? {
|
||||
transform: () => Effect.die("unused plugin.transform"),
|
||||
reload: () => Effect.die("unused plugin.reload"),
|
||||
},
|
||||
npm: {
|
||||
add: () => Effect.die("unused npm.add"),
|
||||
},
|
||||
path: {
|
||||
home: "/unused/home",
|
||||
data: "/unused/data",
|
||||
cache: "/unused/cache",
|
||||
config: "/unused/config",
|
||||
state: "/unused/state",
|
||||
temp: "/unused/temp",
|
||||
},
|
||||
reference: {
|
||||
list: () => Effect.die("unused reference.list"),
|
||||
rebuild: () => Effect.die("unused reference.rebuild"),
|
||||
reference: overrides.reference ?? {
|
||||
transform: () => Effect.die("unused reference.transform"),
|
||||
reload: () => Effect.die("unused reference.reload"),
|
||||
},
|
||||
skill: {
|
||||
sources: () => Effect.die("unused skill.sources"),
|
||||
list: () => Effect.die("unused skill.list"),
|
||||
rebuild: () => Effect.die("unused skill.rebuild"),
|
||||
skill: overrides.skill ?? {
|
||||
transform: () => Effect.die("unused skill.transform"),
|
||||
},
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
|
||||
return {
|
||||
hook: (name, callback) => {
|
||||
if (name === "sdk") {
|
||||
const run = callback as AISDKHooks["sdk"]
|
||||
return plugin.hook("aisdk.sdk", (event) => {
|
||||
const output = { ...event }
|
||||
const result = run(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
|
||||
)
|
||||
})
|
||||
}
|
||||
const run = callback as AISDKHooks["language"]
|
||||
return plugin.hook("aisdk.language", (event) => {
|
||||
const output = { ...event }
|
||||
const result = run(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
|
||||
)
|
||||
})
|
||||
reload: () => Effect.die("unused skill.reload"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
|
||||
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
|
||||
return {
|
||||
...host().agent,
|
||||
reload: agent.reload,
|
||||
transform: (callback) =>
|
||||
agent.transform((draft) =>
|
||||
callback({
|
||||
@@ -136,10 +71,9 @@ export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
|
||||
}
|
||||
}
|
||||
|
||||
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
|
||||
export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] {
|
||||
return {
|
||||
...host().catalog,
|
||||
rebuild: catalog.rebuild,
|
||||
reload: catalog.reload,
|
||||
transform: (callback) =>
|
||||
catalog.transform((draft) =>
|
||||
callback({
|
||||
@@ -201,17 +135,9 @@ export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
|
||||
}
|
||||
}
|
||||
|
||||
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
|
||||
const info = (value: Integration.Info) => ({
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
methods: value.methods.map(method),
|
||||
connections: value.connections.map((item) => ({ ...item })),
|
||||
})
|
||||
export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
|
||||
return {
|
||||
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
|
||||
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
|
||||
rebuild: integration.rebuild,
|
||||
reload: integration.reload,
|
||||
transform: (callback) =>
|
||||
integration.transform((draft) =>
|
||||
callback({
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import path from "path"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
|
||||
import { Policy } from "@opencode-ai/core/policy"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
@@ -22,21 +20,13 @@ const locationLayer = Layer.succeed(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
|
||||
)
|
||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
||||
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
||||
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)),
|
||||
)
|
||||
const layer = Layer.mergeAll(
|
||||
catalog.pipe(Layer.provide(connections)),
|
||||
integrations,
|
||||
connections,
|
||||
events,
|
||||
locationLayer,
|
||||
plugins,
|
||||
Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)),
|
||||
)
|
||||
const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer)
|
||||
const it = testEffect(layer)
|
||||
|
||||
describe("ModelsDevPlugin", () => {
|
||||
@@ -58,7 +48,6 @@ describe("ModelsDevPlugin", () => {
|
||||
yield* ModelsDevPlugin.effect(
|
||||
host({
|
||||
catalog: catalogHost(catalog),
|
||||
event: { subscribe: () => Stream.never },
|
||||
integration: integrationHost(integrations),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
||||
import { define } from "@opencode-ai/plugin/v2/promise"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
describe("fromPromise", () => {
|
||||
it.effect("loads a promise plugin and registers a transform hook", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const promisePlugin = define({
|
||||
id: "promise-example",
|
||||
setup: async (ctx) => {
|
||||
expect(ctx.options.mode).toBe("strict")
|
||||
await ctx.agent.transform((draft) => {
|
||||
draft.update("reviewer", (item) => {
|
||||
item.description = "Reviews code"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const adapted = PluginPromise.fromPromise(promisePlugin)
|
||||
yield* adapted.effect({ ...host, options: { mode: "strict" } })
|
||||
|
||||
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
|
||||
description: "Reviews code",
|
||||
mode: "subagent",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disposes a hook registration on request", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
|
||||
const promisePlugin = define({
|
||||
id: "promise-dispose",
|
||||
setup: async (ctx) => {
|
||||
const registration = await ctx.agent.transform((draft) => {
|
||||
draft.update("temp", (item) => {
|
||||
item.description = "temporary"
|
||||
})
|
||||
})
|
||||
await registration.dispose()
|
||||
},
|
||||
})
|
||||
|
||||
const adapted = PluginPromise.fromPromise(promisePlugin)
|
||||
yield* adapted.effect(host)
|
||||
|
||||
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { createAlibaba } from "@ai-sdk/alibaba"
|
||||
import { Effect } from "effect"
|
||||
@@ -13,27 +14,25 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* AlibabaPlugin.effect(host)
|
||||
})
|
||||
|
||||
describe("AlibabaPlugin", () => {
|
||||
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -41,19 +40,16 @@ describe("AlibabaPlugin", () => {
|
||||
it.effect("ignores non-Alibaba SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -61,19 +57,16 @@ describe("AlibabaPlugin", () => {
|
||||
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
|
||||
const actual = result.sdk?.languageModel("qwen")
|
||||
expect(actual?.provider).toBe(expected.provider)
|
||||
@@ -84,12 +77,13 @@ describe("AlibabaPlugin", () => {
|
||||
it.effect("uses the old default languageModel(api.id) behavior", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
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 result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
|
||||
const language = result.sdk?.languageModel(item.api.id)
|
||||
expect(language?.modelId).toBe("qwen-plus")
|
||||
expect(language?.provider).toBe("alibaba.chat")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* AmazonBedrockPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -109,25 +111,22 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://base.example",
|
||||
endpoint: "https://endpoint.example",
|
||||
region: "us-east-1",
|
||||
},
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://base.example",
|
||||
endpoint: "https://endpoint.example",
|
||||
region: "us-east-1",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
|
||||
}),
|
||||
),
|
||||
@@ -137,24 +136,21 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://base.example",
|
||||
region: "us-east-1",
|
||||
},
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://base.example",
|
||||
region: "us-east-1",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
|
||||
}),
|
||||
),
|
||||
@@ -174,23 +170,20 @@ describe("AmazonBedrockPlugin", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeDefined()
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||
}),
|
||||
@@ -201,19 +194,16 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
||||
}),
|
||||
),
|
||||
@@ -223,19 +213,16 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
|
||||
}),
|
||||
),
|
||||
@@ -245,19 +232,16 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
|
||||
}),
|
||||
),
|
||||
@@ -267,27 +251,24 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
bearerToken: "option-token",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
bearerToken: "option-token",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
||||
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
|
||||
expect(headers).toEqual(["Bearer option-token"])
|
||||
@@ -299,27 +280,24 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
bearerToken: "option-token",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
bearerToken: "option-token",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
|
||||
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
|
||||
expect(headers).toEqual(["Bearer env-token"])
|
||||
@@ -331,28 +309,25 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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: {
|
||||
name: "amazon-bedrock",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
|
||||
region: "us-east-2",
|
||||
const result = yield* aisdk.runSDK({
|
||||
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: {
|
||||
name: "amazon-bedrock",
|
||||
bearerToken: "token",
|
||||
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
|
||||
region: "us-east-2",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
const language = result.sdk.responses("openai.gpt-5.5")
|
||||
expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
|
||||
"https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
|
||||
@@ -364,40 +339,33 @@ describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
|
||||
}),
|
||||
)
|
||||
@@ -405,23 +373,20 @@ describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("ignores other Bedrock provider subpaths", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
@@ -438,30 +403,27 @@ describe("AmazonBedrockPlugin", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const headers: Array<string | null> = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
headers.push(new Headers(init?.headers).get("Authorization"))
|
||||
return new Response("{}")
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
yield* Effect.promise(() =>
|
||||
bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
|
||||
body: "{}",
|
||||
@@ -476,72 +438,53 @@ describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("applies legacy cross-region inference prefixes", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
expect(calls).toEqual([
|
||||
"languageModel:us.anthropic.claude-sonnet-4-5",
|
||||
"languageModel:eu.anthropic.claude-sonnet-4-5",
|
||||
@@ -556,20 +499,17 @@ describe("AmazonBedrockPlugin", () => {
|
||||
withEnv({ AWS_REGION: "eu-west-1" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
|
||||
}),
|
||||
),
|
||||
@@ -578,6 +518,7 @@ describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("applies the full legacy cross-region prefix matrix", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
const cases = [
|
||||
{ region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
|
||||
@@ -647,18 +588,14 @@ describe("AmazonBedrockPlugin", () => {
|
||||
]
|
||||
yield* addPlugin()
|
||||
for (const item of cases) {
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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 },
|
||||
})
|
||||
}
|
||||
expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
|
||||
}),
|
||||
@@ -667,20 +604,17 @@ describe("AmazonBedrockPlugin", () => {
|
||||
it.effect("ignores non-Bedrock providers for language selection", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
expect(calls).toEqual([])
|
||||
expect(result.language).toBeUndefined()
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -13,8 +14,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* AnthropicPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -59,19 +61,16 @@ 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 aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
|
||||
}),
|
||||
)
|
||||
@@ -79,19 +78,16 @@ describe("AnthropicPlugin", () => {
|
||||
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 aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* AzureCognitiveServicesPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -114,20 +116,17 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||
it.effect("selects chat only for completion URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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 },
|
||||
})
|
||||
expect(calls).toEqual(["chat:deployment"])
|
||||
}),
|
||||
)
|
||||
@@ -135,32 +134,25 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: 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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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"])
|
||||
expect(ignored.language).toBeUndefined()
|
||||
}),
|
||||
@@ -169,51 +161,34 @@ describe("AzureCognitiveServicesPlugin", () => {
|
||||
it.effect("falls back from responses to messages, chat, then languageModel", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
const sdk = fakeSelectorSdk(calls)
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual([
|
||||
"messages:messages-deployment",
|
||||
"chat:chat-deployment",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { Effect } from "effect"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* AzurePlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -142,19 +144,16 @@ describe("AzurePlugin", () => {
|
||||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeDefined()
|
||||
}),
|
||||
),
|
||||
@@ -163,21 +162,17 @@ describe("AzurePlugin", () => {
|
||||
it.effect("rejects missing resourceName when baseURL is not configured", () =>
|
||||
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const exit = yield* plugin
|
||||
.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const exit = yield* aisdk
|
||||
.runSDK({
|
||||
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)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
}),
|
||||
@@ -187,20 +182,17 @@ describe("AzurePlugin", () => {
|
||||
it.effect("selects chat only for completion URLs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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"])
|
||||
}),
|
||||
)
|
||||
@@ -208,20 +200,17 @@ describe("AzurePlugin", () => {
|
||||
it.effect("selects chat from per-call useCompletionUrls", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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"])
|
||||
}),
|
||||
)
|
||||
@@ -229,21 +218,18 @@ describe("AzurePlugin", () => {
|
||||
it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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),
|
||||
options: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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),
|
||||
options: {},
|
||||
})
|
||||
expect(calls).toEqual(["responses:deployment"])
|
||||
}),
|
||||
)
|
||||
@@ -251,32 +237,25 @@ describe("AzurePlugin", () => {
|
||||
it.effect("uses the legacy Azure selector order and provider guard", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: 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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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"])
|
||||
expect(ignored.language).toBeUndefined()
|
||||
}),
|
||||
@@ -285,36 +264,29 @@ describe("AzurePlugin", () => {
|
||||
it.effect("falls back through the legacy Azure selector order", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
const make = (method: string) => (id: string) => {
|
||||
calls.push(`${method}:${id}`)
|
||||
return { modelId: id, provider: method, specificationVersion: "v3" }
|
||||
}
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CerebrasPlugin.effect(host)
|
||||
})
|
||||
|
||||
void mock.module("@ai-sdk/cerebras", () => ({
|
||||
@@ -59,26 +61,23 @@ describe("CerebrasPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
|
||||
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
|
||||
}),
|
||||
@@ -88,26 +87,23 @@ describe("CerebrasPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
|
||||
}),
|
||||
)
|
||||
@@ -116,26 +112,23 @@ describe("CerebrasPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
cerebrasOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(cerebrasOptions).toEqual([])
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -12,8 +13,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CloudflareAIGatewayPlugin.effect(host)
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
|
||||
@@ -111,19 +113,16 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("openai/gpt-5")).toBeDefined()
|
||||
}),
|
||||
),
|
||||
@@ -134,27 +133,24 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
metadata: { invoked_by: "test", project: "opencode" },
|
||||
cacheTtl: 300,
|
||||
cacheKey: "cache-key",
|
||||
skipCache: true,
|
||||
collectLog: false,
|
||||
},
|
||||
yield* aisdk.runSDK({
|
||||
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",
|
||||
metadata: { invoked_by: "test", project: "opencode" },
|
||||
cacheTtl: 300,
|
||||
cacheKey: "cache-key",
|
||||
skipCache: true,
|
||||
collectLog: false,
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
expect(aiGatewayCalls).toHaveLength(1)
|
||||
expect(aiGatewayCalls[0]).toEqual({
|
||||
@@ -181,25 +177,22 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
headers: {
|
||||
"cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }),
|
||||
},
|
||||
yield* aisdk.runSDK({
|
||||
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",
|
||||
headers: {
|
||||
"cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }),
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
expect(aiGatewayCalls[0]?.options).toMatchObject({
|
||||
metadata: { invoked_by: "header", project: "opencode" },
|
||||
@@ -213,25 +206,22 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
accountId: "auth-account",
|
||||
gateway: "auth-gateway",
|
||||
apiKey: "auth-token",
|
||||
},
|
||||
yield* aisdk.runSDK({
|
||||
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",
|
||||
accountId: "auth-account",
|
||||
gateway: "auth-gateway",
|
||||
apiKey: "auth-token",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
expect(aiGatewayCalls[0]).toMatchObject({
|
||||
accountId: "env-account",
|
||||
@@ -253,25 +243,22 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
accountId: "auth-account",
|
||||
gatewayId: "auth-gateway",
|
||||
apiKey: "auth-token",
|
||||
},
|
||||
yield* aisdk.runSDK({
|
||||
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",
|
||||
accountId: "auth-account",
|
||||
gatewayId: "auth-gateway",
|
||||
apiKey: "auth-token",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
|
||||
expect(aiGatewayCalls[0]).toMatchObject({
|
||||
accountId: "auth-account",
|
||||
@@ -287,20 +274,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(aiGatewayCalls[0]).toMatchObject({ apiKey: "cf-aig-token" })
|
||||
}),
|
||||
@@ -312,20 +296,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(result.sdk).toBeUndefined()
|
||||
expect(aiGatewayCalls).toHaveLength(0)
|
||||
@@ -338,20 +319,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(result.sdk).toBeUndefined()
|
||||
expect(aiGatewayCalls).toHaveLength(0)
|
||||
@@ -370,20 +348,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(result.sdk).toBeUndefined()
|
||||
expect(aiGatewayCalls).toHaveLength(0)
|
||||
@@ -396,27 +371,24 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(result.sdk.languageModel("anthropic/claude-sonnet-4-5")).toEqual({
|
||||
modelId: { unifiedModelID: "anthropic/claude-sonnet-4-5" },
|
||||
@@ -434,20 +406,17 @@ describe("CloudflareAIGatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetCalls()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(result.sdk).toBeUndefined()
|
||||
expect(aiGatewayCalls).toHaveLength(0)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CloudflareWorkersAIPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -81,6 +83,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
|
||||
@@ -89,18 +92,14 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
|
||||
const sdk = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" } },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const sdk = yield* aisdk.runSDK({
|
||||
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" } },
|
||||
})
|
||||
expect(provider.api).toEqual({
|
||||
type: "aisdk",
|
||||
package: "test-provider",
|
||||
@@ -134,24 +133,21 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(cloudflareURL(result.sdk)).toBe("https://proxy.example/v1/chat/completions")
|
||||
}),
|
||||
),
|
||||
@@ -181,29 +177,26 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
apiKey: "auth-key",
|
||||
baseURL: "https://proxy.example/v1",
|
||||
headers: { custom: "header" },
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
apiKey: "auth-key",
|
||||
baseURL: "https://proxy.example/v1",
|
||||
headers: { custom: "header" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
const headers = yield* Effect.promise(() => Promise.resolve(cloudflareHeaders(result.sdk)))
|
||||
expect(headers.authorization).toBe("Bearer env-key")
|
||||
expect(headers.custom).toBe("header")
|
||||
@@ -216,27 +209,24 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
name: "cloudflare-workers-ai",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
const result = yield* aisdk.runSDK({
|
||||
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",
|
||||
},
|
||||
}),
|
||||
package: "@ai-sdk/openai-compatible",
|
||||
options: {
|
||||
name: "cloudflare-workers-ai",
|
||||
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
expect(cloudflareURL(result.sdk)).toBe(
|
||||
"https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions",
|
||||
)
|
||||
@@ -247,20 +237,17 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
it.effect("selects languageModel with the API model ID", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(result.language).toBeDefined()
|
||||
expect(calls).toEqual(["languageModel:@cf/api-model"])
|
||||
}),
|
||||
@@ -270,24 +257,21 @@ describe("CloudflareWorkersAIPlugin", () => {
|
||||
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* CoherePlugin.effect(host)
|
||||
})
|
||||
|
||||
function fakeSelectorSdk(calls: string[]) {
|
||||
@@ -48,34 +50,27 @@ describe("CoherePlugin", () => {
|
||||
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* aisdk.runSDK({
|
||||
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: 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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -83,19 +78,16 @@ describe("CoherePlugin", () => {
|
||||
it.effect("uses the model provider ID as the bundled SDK name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(cohereOptions.at(-1)).toEqual({
|
||||
name: "custom-cohere",
|
||||
@@ -109,21 +101,18 @@ describe("CoherePlugin", () => {
|
||||
it.effect("leaves language selection to the default languageModel fallback", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
const sdk = fakeSelectorSdk(calls)
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
|
||||
expect(result.language).toBeUndefined()
|
||||
expect(calls).toEqual([])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* DeepInfraPlugin.effect(host)
|
||||
})
|
||||
|
||||
void mock.module("@ai-sdk/deepinfra", () => ({
|
||||
@@ -41,19 +43,16 @@ describe("DeepInfraPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -62,19 +61,16 @@ describe("DeepInfraPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat")
|
||||
expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }])
|
||||
}),
|
||||
@@ -84,19 +80,16 @@ describe("DeepInfraPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat")
|
||||
expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }])
|
||||
}),
|
||||
@@ -106,6 +99,7 @@ describe("DeepInfraPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const packages = [
|
||||
"unmatched-package",
|
||||
@@ -114,33 +108,25 @@ describe("DeepInfraPlugin", () => {
|
||||
]
|
||||
yield* Effect.forEach(packages, (item) =>
|
||||
Effect.gen(function* () {
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
expect(deepinfraOptions).toEqual([{ name: "deepinfra" }])
|
||||
}),
|
||||
@@ -150,31 +136,21 @@ describe("DeepInfraPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
resetDeepInfraMock()
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
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 sdkEvent = yield* aisdk.runSDK({
|
||||
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* aisdk.runLanguage({ 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"])
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PluginTestLayer } from "./fixture"
|
||||
const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
|
||||
const fixtureProviderPath = fileURLToPath(fixtureProvider)
|
||||
const it = testEffect(PluginTestLayer)
|
||||
const itWithAISDK = testEffect(AISDK.layer.pipe(Layer.provideMerge(PluginTestLayer)))
|
||||
const itWithAISDK = testEffect(AISDK.locationLayer.pipe(Layer.provideMerge(PluginTestLayer)))
|
||||
|
||||
function npmEntrypoint(entrypoint?: string) {
|
||||
return Npm.Service.of({
|
||||
@@ -29,11 +29,8 @@ function npmEntrypoint(entrypoint?: string) {
|
||||
|
||||
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: DynamicProviderPlugin.effect(npm ? { ...host, npm } : host),
|
||||
})
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* DynamicProviderPlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm ?? (yield* Npm.Service)))
|
||||
})
|
||||
|
||||
function tempEntrypoint(source: string) {
|
||||
@@ -51,20 +48,16 @@ function tempEntrypoint(source: string) {
|
||||
describe("DynamicProviderPlugin", () => {
|
||||
it.effect("creates an SDK from a provider factory export", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom" })
|
||||
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "dynamic", name: "custom" } })
|
||||
}),
|
||||
@@ -72,68 +65,56 @@ describe("DynamicProviderPlugin", () => {
|
||||
|
||||
it.effect("does not override an SDK already supplied by an earlier plugin", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const sdk = { marker: "existing" }
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{ sdk },
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
sdk,
|
||||
})
|
||||
expect(result.sdk).toBe(sdk)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("injects the provider ID as the SDK factory name", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom-provider" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("loads npm packages through their resolved import entrypoint", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(npmEntrypoint(fixtureProviderPath))
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "npm", name: "npm-provider" } })
|
||||
}),
|
||||
)
|
||||
|
||||
itWithAISDK.effect("wraps missing npm entrypoint failures as AISDK init errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(npmEntrypoint())
|
||||
const exit = yield* aisdk
|
||||
@@ -151,7 +132,6 @@ describe("DynamicProviderPlugin", () => {
|
||||
|
||||
itWithAISDK.effect("wraps dynamic import failures as AISDK init errors", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const exit = yield* aisdk
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GatewayPlugin.effect(host)
|
||||
})
|
||||
|
||||
mock.module("@ai-sdk/gateway", () => ({
|
||||
@@ -38,19 +40,16 @@ describe("GatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
expect(gatewayCalls).toHaveLength(1)
|
||||
}),
|
||||
@@ -60,24 +59,21 @@ describe("GatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }])
|
||||
expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel")
|
||||
@@ -88,35 +84,28 @@ describe("GatewayPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gatewayCalls.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
|
||||
for (const modelID of vercelGatewayModels) {
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* aisdk.runSDK({
|
||||
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: 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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GithubCopilotPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -40,31 +42,24 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const ignored = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const ignored = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(ignored.sdk).toBeUndefined()
|
||||
expect(result.sdk).toBeDefined()
|
||||
}),
|
||||
@@ -73,20 +68,17 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("selects languageModel when responses and chat are absent", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["languageModel:claude-sonnet-4"])
|
||||
}),
|
||||
)
|
||||
@@ -94,20 +86,17 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("selects languageModel with the API model ID when responses and chat are absent", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["languageModel:claude-sonnet-4"])
|
||||
}),
|
||||
)
|
||||
@@ -115,68 +104,49 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("uses responses for gpt-5 models except gpt-5-mini", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: 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: 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: 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: 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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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* aisdk.runLanguage({
|
||||
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([
|
||||
"responses:gpt-5",
|
||||
"responses:gpt-5.1-codex",
|
||||
@@ -190,44 +160,33 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("uses the API model ID when selecting responses or chat", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["responses:gpt-5", "chat:gpt-5-mini", "chat:claude-sonnet-4"])
|
||||
}),
|
||||
)
|
||||
@@ -265,20 +224,17 @@ describe("GithubCopilotPlugin", () => {
|
||||
it.effect("ignores non-Copilot providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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([])
|
||||
expect(result.language).toBeUndefined()
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GitLabPlugin.effect(host)
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
@@ -63,19 +65,16 @@ describe("GitLabPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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)
|
||||
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://gitlab.com")
|
||||
expect(gitlabSDKOptions[0].apiKey).toBe("env-token")
|
||||
@@ -103,19 +102,16 @@ describe("GitLabPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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")
|
||||
}),
|
||||
),
|
||||
@@ -131,31 +127,28 @@ describe("GitLabPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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",
|
||||
instanceUrl: "https://configured.gitlab.example",
|
||||
apiKey: "configured-token",
|
||||
aiGatewayHeaders: {
|
||||
"anthropic-beta": "configured-beta",
|
||||
"x-gitlab-test": "1",
|
||||
},
|
||||
featureFlags: {
|
||||
duo_agent_platform: false,
|
||||
custom_flag: true,
|
||||
},
|
||||
yield* aisdk.runSDK({
|
||||
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",
|
||||
instanceUrl: "https://configured.gitlab.example",
|
||||
apiKey: "configured-token",
|
||||
aiGatewayHeaders: {
|
||||
"anthropic-beta": "configured-beta",
|
||||
"x-gitlab-test": "1",
|
||||
},
|
||||
featureFlags: {
|
||||
duo_agent_platform: false,
|
||||
custom_flag: true,
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
})
|
||||
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://configured.gitlab.example")
|
||||
expect(gitlabSDKOptions[0].apiKey).toBe("configured-token")
|
||||
expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({
|
||||
@@ -175,19 +168,16 @@ describe("GitLabPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
gitlabSDKOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
expect(gitlabSDKOptions).toHaveLength(0)
|
||||
}),
|
||||
@@ -196,30 +186,27 @@ describe("GitLabPlugin", () => {
|
||||
it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
}),
|
||||
sdk: {
|
||||
workflowChat: (id: string, options: unknown) => {
|
||||
calls.push([id, options])
|
||||
return { id, options }
|
||||
},
|
||||
agenticChat: () => undefined,
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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" },
|
||||
},
|
||||
options: { featureFlags: { configured: true } },
|
||||
}),
|
||||
sdk: {
|
||||
workflowChat: (id: string, options: unknown) => {
|
||||
calls.push([id, options])
|
||||
return { id, options }
|
||||
},
|
||||
agenticChat: () => undefined,
|
||||
},
|
||||
{},
|
||||
)
|
||||
options: { featureFlags: { configured: true } },
|
||||
})
|
||||
expect(calls).toEqual([
|
||||
["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: "definition" }],
|
||||
])
|
||||
@@ -234,26 +221,23 @@ describe("GitLabPlugin", () => {
|
||||
it.effect("uses exact static workflow model ids when the provider recognizes them", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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])
|
||||
return { id, options }
|
||||
},
|
||||
agenticChat: () => undefined,
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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])
|
||||
return { id, options }
|
||||
},
|
||||
options: { featureFlags: { configured: true } },
|
||||
agenticChat: () => undefined,
|
||||
},
|
||||
{},
|
||||
)
|
||||
options: { featureFlags: { configured: true } },
|
||||
})
|
||||
expect(calls).toEqual([
|
||||
["duo-workflow-exact", { featureFlags: { configured: true }, workflowDefinition: undefined }],
|
||||
])
|
||||
@@ -264,30 +248,27 @@ describe("GitLabPlugin", () => {
|
||||
it.effect("uses provider feature flags instead of request feature flags", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 } },
|
||||
},
|
||||
}),
|
||||
sdk: {
|
||||
workflowChat: (id: string, options: unknown) => {
|
||||
calls.push([id, options])
|
||||
return { id, options }
|
||||
},
|
||||
agenticChat: () => undefined,
|
||||
yield* aisdk.runLanguage({
|
||||
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 } },
|
||||
},
|
||||
options: { featureFlags: { configured: true } },
|
||||
}),
|
||||
sdk: {
|
||||
workflowChat: (id: string, options: unknown) => {
|
||||
calls.push([id, options])
|
||||
return { id, options }
|
||||
},
|
||||
agenticChat: () => undefined,
|
||||
},
|
||||
{},
|
||||
)
|
||||
options: { featureFlags: { configured: true } },
|
||||
})
|
||||
expect(calls).toEqual([["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: undefined }]])
|
||||
}),
|
||||
)
|
||||
@@ -295,33 +276,30 @@ describe("GitLabPlugin", () => {
|
||||
it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: [string, unknown][] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {
|
||||
workflowChat: () => undefined,
|
||||
agenticChat: (id: string, options: unknown) => {
|
||||
const selected = options as {
|
||||
aiGatewayHeaders?: Record<string, string>
|
||||
featureFlags?: Record<string, boolean>
|
||||
}
|
||||
calls.push([
|
||||
id,
|
||||
{ aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } },
|
||||
])
|
||||
},
|
||||
yield* aisdk.runLanguage({
|
||||
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: {
|
||||
workflowChat: () => undefined,
|
||||
agenticChat: (id: string, options: unknown) => {
|
||||
const selected = options as {
|
||||
aiGatewayHeaders?: Record<string, string>
|
||||
featureFlags?: Record<string, boolean>
|
||||
}
|
||||
calls.push([
|
||||
id,
|
||||
{ aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } },
|
||||
])
|
||||
},
|
||||
options: { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } },
|
||||
},
|
||||
{},
|
||||
)
|
||||
options: { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } },
|
||||
})
|
||||
expect(calls).toEqual([
|
||||
["claude", { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } }],
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
@@ -14,8 +15,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* definition.effect(host)
|
||||
})
|
||||
|
||||
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
|
||||
@@ -111,22 +113,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
|
||||
"https://aiplatform.googleapis.com/v1/projects/gcp-project/locations/global/publishers/anthropic/models",
|
||||
)
|
||||
@@ -140,22 +139,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
|
||||
"https://cloud-location-aiplatform.googleapis.com/v1/projects/project/locations/cloud-location/publishers/anthropic/models",
|
||||
)
|
||||
@@ -166,19 +162,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
|
||||
"https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models",
|
||||
)
|
||||
@@ -188,19 +181,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1")
|
||||
}),
|
||||
)
|
||||
@@ -208,32 +198,25 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin(GoogleVertexPlugin)
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const sdkResult = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const languageResult = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const sdkResult = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
const languageResult = yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string }
|
||||
expect(language.config.baseURL).toBe(
|
||||
"https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models",
|
||||
@@ -245,23 +228,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("trims model IDs before selecting language models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["languageModel:claude-sonnet-4-5"])
|
||||
}),
|
||||
)
|
||||
@@ -269,20 +246,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
|
||||
it.effect("ignores non Vertex Anthropic providers for language selection", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin(GoogleVertexAnthropicPlugin)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual([])
|
||||
expect(result.language).toBeUndefined()
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect, mock } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
@@ -16,8 +17,9 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GoogleVertexPlugin.effect(host)
|
||||
})
|
||||
|
||||
function required<T>(value: T | undefined): T {
|
||||
@@ -154,6 +156,7 @@ describe("GoogleVertexPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
vertexOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const catalog = yield* Catalog.Service
|
||||
yield* catalog.transform((catalog) =>
|
||||
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
|
||||
@@ -166,22 +169,18 @@ describe("GoogleVertexPlugin", () => {
|
||||
)
|
||||
yield* addPlugin()
|
||||
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
|
||||
expect(provider.request.body.project).toBe("vertex-project")
|
||||
expect(provider.api).toEqual({
|
||||
@@ -293,23 +292,20 @@ describe("GoogleVertexPlugin", () => {
|
||||
Effect.gen(function* () {
|
||||
vertexOptions.length = 0
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(vertexOptions).toHaveLength(1)
|
||||
expect(vertexOptions[0].project).toBe("env-project")
|
||||
expect(vertexOptions[0].location).toBe("env-location")
|
||||
@@ -323,8 +319,9 @@ describe("GoogleVertexPlugin", () => {
|
||||
googleAuthOptions.length = 0
|
||||
const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (evt) =>
|
||||
yield* aisdk.hook.sdk((evt) =>
|
||||
Effect.promise(async () => {
|
||||
if (evt.model.providerID !== "google-vertex") return
|
||||
if (evt.package !== "@ai-sdk/openai-compatible") return
|
||||
@@ -345,22 +342,18 @@ describe("GoogleVertexPlugin", () => {
|
||||
yield* Effect.acquireUseRelease(
|
||||
Effect.void,
|
||||
() =>
|
||||
plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
),
|
||||
aisdk.runSDK({
|
||||
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" },
|
||||
}),
|
||||
() =>
|
||||
Effect.sync(() => {
|
||||
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
|
||||
@@ -377,20 +370,17 @@ describe("GoogleVertexPlugin", () => {
|
||||
it.effect("trims model IDs before selecting language models", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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: {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runLanguage({
|
||||
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: {},
|
||||
})
|
||||
expect(calls).toEqual(["languageModel:gemini-2.5-pro"])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
@@ -12,27 +13,25 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* 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
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeDefined()
|
||||
expect(result.sdk?.languageModel("gemini").provider).toBe("custom-google")
|
||||
}),
|
||||
@@ -41,19 +40,16 @@ describe("GooglePlugin", () => {
|
||||
it.effect("ignores non-Google SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -61,28 +57,21 @@ describe("GooglePlugin", () => {
|
||||
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()
|
||||
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 sdkEvent = yield* aisdk.runSDK({
|
||||
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* aisdk.runLanguage({
|
||||
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,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { createGroq } from "@ai-sdk/groq"
|
||||
import { Effect } from "effect"
|
||||
@@ -13,27 +14,25 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* GroqPlugin.effect(host)
|
||||
})
|
||||
|
||||
describe("GroqPlugin", () => {
|
||||
it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -41,19 +40,16 @@ describe("GroqPlugin", () => {
|
||||
it.effect("ignores non-Groq SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -61,19 +57,16 @@ describe("GroqPlugin", () => {
|
||||
it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -81,19 +74,16 @@ describe("GroqPlugin", () => {
|
||||
it.effect("matches the old bundled Groq SDK provider naming", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
|
||||
name: string
|
||||
}).languageModel("llama")
|
||||
@@ -106,26 +96,23 @@ describe("GroqPlugin", () => {
|
||||
it.effect("uses the default languageModel(api.id) behavior", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runLanguage({
|
||||
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" },
|
||||
})
|
||||
const language = result.language ?? sdk.languageModel(result.model.api.id)
|
||||
expect(language.modelId).toBe("llama-api")
|
||||
expect(language.provider).toBe("groq.chat")
|
||||
|
||||
@@ -13,8 +13,8 @@ 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) })
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* KiloPlugin.effect(host)
|
||||
})
|
||||
|
||||
describe("KiloPlugin", () => {
|
||||
|
||||
@@ -14,8 +14,9 @@ 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) })
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
const integration = yield* Integration.Service
|
||||
yield* LLMGatewayPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration))
|
||||
})
|
||||
|
||||
describe("LLMGatewayPlugin", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AISDK } from "@opencode-ai/core/aisdk"
|
||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
@@ -13,27 +14,25 @@ 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) })
|
||||
const aisdk = yield* AISDK.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* MistralPlugin.effect(host)
|
||||
})
|
||||
|
||||
describe("MistralPlugin", () => {
|
||||
it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
}),
|
||||
)
|
||||
@@ -41,19 +40,16 @@ describe("MistralPlugin", () => {
|
||||
it.effect("ignores non-Mistral SDK packages", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(result.sdk).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
@@ -61,25 +57,22 @@ describe("MistralPlugin", () => {
|
||||
it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (event) =>
|
||||
yield* aisdk.hook.sdk((event) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(event.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
)
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
const result = yield* aisdk.runSDK({
|
||||
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()
|
||||
expect(providers).toEqual(["mistral.chat"])
|
||||
}),
|
||||
@@ -88,25 +81,22 @@ describe("MistralPlugin", () => {
|
||||
it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const providers: string[] = []
|
||||
yield* addPlugin()
|
||||
yield* plugin.hook("aisdk.sdk", (event) =>
|
||||
yield* aisdk.hook.sdk((event) =>
|
||||
Effect.sync(() => {
|
||||
providers.push(event.sdk.languageModel("mistral-large").provider)
|
||||
}),
|
||||
)
|
||||
yield* plugin.trigger(
|
||||
"aisdk.sdk",
|
||||
{
|
||||
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" },
|
||||
},
|
||||
{},
|
||||
)
|
||||
yield* aisdk.runSDK({
|
||||
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" },
|
||||
})
|
||||
expect(providers).toEqual(["mistral.chat"])
|
||||
}),
|
||||
)
|
||||
@@ -114,6 +104,7 @@ describe("MistralPlugin", () => {
|
||||
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const aisdk = yield* AISDK.Service
|
||||
const calls: string[] = []
|
||||
const sdk = {
|
||||
languageModel: (id: string) => {
|
||||
@@ -122,18 +113,14 @@ describe("MistralPlugin", () => {
|
||||
},
|
||||
}
|
||||
yield* addPlugin()
|
||||
const result = yield* plugin.trigger(
|
||||
"aisdk.language",
|
||||
{
|
||||
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 result = yield* aisdk.runLanguage({
|
||||
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)
|
||||
expect(calls).toEqual(["languageModel:mistral-large"])
|
||||
expect(language).toBeDefined()
|
||||
|
||||
@@ -13,8 +13,8 @@ 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) })
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* NvidiaPlugin.effect(host)
|
||||
})
|
||||
|
||||
describe("NvidiaPlugin", () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user