refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
commit 8db7487c89
466 changed files with 9405 additions and 11071 deletions

View file

@ -1,12 +1,12 @@
export * as AgentPlugin from "./agent"
import path from "path"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { Global } from "@opencode-ai/util/global"
import { Location } from "../location"
import { PermissionV2 } from "../permission"
import { Permission } from "../permission"
// Combined output files written by the Shell service, e.g. `<data>/shell/<projectID>/<shellID>.out`.
// Whitelisted so agents can read a command's full captured output without an external-directory prompt.
@ -105,13 +105,13 @@ export const Plugin = define({
const location = yield* Location.Service
const worktree = location.directory
const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")]
const readonlyExternalDirectory: PermissionV2.Ruleset = [
const readonlyExternalDirectory: Permission.Ruleset = [
{ action: "external_directory", resource: "*", effect: "ask" },
...whitelistedDirs.map(
(resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }),
(resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }),
),
]
const defaults: PermissionV2.Ruleset = [
const defaults: Permission.Ruleset = [
{ action: "*", resource: "*", effect: "allow" },
...readonlyExternalDirectory,
{ action: "question", resource: "*", effect: "deny" },
@ -124,24 +124,24 @@ export const Plugin = define({
]
yield* ctx.agent.transform((draft) => {
draft.update(AgentV2.defaultID, (item) => {
item.name = AgentV2.Name.make("Build")
draft.update(Agent.defaultID, (item) => {
item.name = Agent.Name.make("Build")
item.description = "The default agent. Executes tools based on configured permissions."
item.mode = "primary"
item.permissions.push(
...PermissionV2.merge(defaults, [
...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" },
{ action: "plan_enter", resource: "*", effect: "allow" },
]),
)
})
draft.update(AgentV2.ID.make("plan"), (item) => {
item.name = AgentV2.Name.make("Plan")
draft.update(Agent.ID.make("plan"), (item) => {
item.name = Agent.Name.make("Plan")
item.description = "Plan mode. Disallows all edit tools."
item.mode = "primary"
item.permissions.push(
...PermissionV2.merge(defaults, [
...Permission.merge(defaults, [
{ action: "question", resource: "*", effect: "allow" },
{ action: "plan_exit", resource: "*", effect: "allow" },
{ action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" },
@ -156,22 +156,22 @@ export const Plugin = define({
)
})
draft.update(AgentV2.ID.make("general"), (item) => {
item.name = AgentV2.Name.make("General")
draft.update(Agent.ID.make("general"), (item) => {
item.name = Agent.Name.make("General")
item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
item.mode = "subagent"
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("explore"), (item) => {
item.name = AgentV2.Name.make("Explore")
draft.update(Agent.ID.make("explore"), (item) => {
item.name = Agent.Name.make("Explore")
item.description =
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
item.system = PROMPT_EXPLORE
item.mode = "subagent"
item.permissions.push(
...PermissionV2.merge(
...Permission.merge(
defaults,
[
{ action: "*", resource: "*", effect: "deny" },
@ -187,28 +187,28 @@ export const Plugin = define({
)
})
draft.update(AgentV2.ID.make("compaction"), (item) => {
item.name = AgentV2.Name.make("Compaction")
draft.update(Agent.ID.make("compaction"), (item) => {
item.name = Agent.Name.make("Compaction")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_COMPACTION
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("title"), (item) => {
item.name = AgentV2.Name.make("Title")
draft.update(Agent.ID.make("title"), (item) => {
item.name = Agent.Name.make("Title")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_TITLE
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
draft.update(AgentV2.ID.make("summary"), (item) => {
item.name = AgentV2.Name.make("Summary")
draft.update(Agent.ID.make("summary"), (item) => {
item.name = Agent.Name.make("Summary")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_SUMMARY
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
})
})
}),

View file

@ -1,6 +1,6 @@
export * as CommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import { Location } from "../location"
import PROMPT_INITIALIZE from "./command/initialize.txt"

View file

@ -1,8 +1,8 @@
export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/effect/tool"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { State } from "../state"
@ -28,7 +28,7 @@ export interface Interface {
) => Effect.Effect<Domains[Domain][Name]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/PluginHooks") {}
const layer = Layer.effect(
Service,

View file

@ -1,48 +1,44 @@
export * as PluginHost from "./host"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { Plugin } from "@opencode-ai/plugin/effect"
import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { App } from "../app"
import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { AISDK } from "../aisdk"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Command } from "../command"
import { Credential } from "../credential"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { Integration } from "../integration"
import { Location } from "../location"
import { ModelV2 } from "../model"
import type { PluginV2 } from "../plugin"
import { Model } from "../model"
import { PluginRuntime } from "./runtime"
import { ProviderV2 } from "../provider"
import { Provider } from "../provider"
import { Reference } from "../reference"
import { AbsolutePath, type DeepMutable } from "../schema"
import { SkillV2 } from "../skill"
import { Tool } from "../tool/tool"
import { Tools } from "../tool/tools"
import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
import { Skill } from "../skill"
import { Tool } from "../tool"
import { Workspace } from "../workspace"
import { WebSearch } from "../websearch"
import { PluginHooks } from "./hooks"
const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../plugin").Interface) {
const app = yield* App.Metadata
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service
const events = yield* EventV2.Service
const commands = yield* Command.Service
const bus = yield* Bus.Service
const integration = yield* Integration.Service
const location = yield* Location.Service
const reference = yield* Reference.Service
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const skill = yield* Skill.Service
const tools = yield* Tool.Service
const websearch = yield* WebSearch.Service
const toolHooks = yield* ToolHooks.Service
const hooks = yield* PluginHooks.Service
const runtime = yield* PluginRuntime.Service
const locationInfo = () =>
@ -51,7 +47,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID,
project: location.project,
})
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
const locationRef = (input?: {
readonly location?: { readonly directory?: string; readonly workspace?: string }
}) =>
input?.location === undefined
? undefined
: Location.Ref.make({
@ -59,7 +57,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID:
input.location.workspace === undefined
? location.workspaceID
: WorkspaceV2.ID.make(input.location.workspace),
: Workspace.ID.make(input.location.workspace),
})
const isCurrentLocation = (ref: Location.Ref) =>
ref.directory === location.directory && ref.workspaceID === location.workspaceID
@ -70,7 +68,22 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
app,
options: {},
agent: {
get: (id) => agents.get(AgentV2.ID.make(id)),
get: (input) => {
const ref = locationRef(input)
const output =
ref && !isCurrentLocation(ref)
? runtime.location.agent
.list(ref)
.pipe(Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID) })))
: response(agents.get(input.agentID))
return output.pipe(
Effect.flatMap((result) =>
result.data
? Effect.succeed({ ...result, data: result.data })
: Effect.fail(new Error(`Agent not found: ${input.agentID}`)),
),
)
},
list: (input) => {
const ref = locationRef(input)
if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref)
@ -81,10 +94,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
agents.transform((draft) => {
callback({
list: () => mutable(draft.list()),
get: (id) => mutable(draft.get(AgentV2.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) => draft.update(AgentV2.ID.make(id), update),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
get: (id) => mutable(draft.get(Agent.ID.make(id))),
default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)),
update: (id, update) => draft.update(Agent.ID.make(id), update),
remove: (id) => draft.remove(Agent.ID.make(id)),
})
}),
},
@ -121,7 +134,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
list: () => response(catalog.provider.available()),
get: (input) =>
catalog.provider
.get(ProviderV2.ID.make(input.providerID))
.get(Provider.ID.make(input.providerID))
.pipe(
Effect.flatMap((provider) =>
provider === undefined
@ -131,7 +144,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
),
},
model: {
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: () => response(catalog.model.available()),
default: () => response(catalog.model.default()),
},
@ -141,21 +153,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
callback({
provider: {
list: () => mutable(draft.provider.list()),
get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))),
update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
get: (id) => mutable(draft.provider.get(Provider.ID.make(id))),
update: (id, update) => draft.provider.update(Provider.ID.make(id), update),
remove: (id) => draft.provider.remove(Provider.ID.make(id)),
},
model: {
get: (providerID, modelID) =>
mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))),
mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))),
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)),
default: {
get: draft.model.default.get,
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)),
},
},
})
@ -170,7 +182,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
event: {
subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)),
subscribe: () => bus.subscribe().pipe(Stream.filter(EventManifest.isServer)),
},
integration: {
list: () => response(integration.list()),
@ -279,88 +291,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
transform: (callback) =>
skill.transform((draft) => {
callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
source: (source) => draft.source(Schema.decodeUnknownSync(Skill.Source)(source)),
list: draft.list,
})
}),
},
tool: {
transform: (callback) =>
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.Any
readonly options?: Tool.RegisterOptions
}> = []
yield* Effect.sync(() =>
tools
.transform((draft) =>
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
add: (tool) => draft.add(tool),
}),
)
yield* tools
.registerBatch(
registrations.map((registration) => ({
tools: { [registration.name]: registration.tool },
...(registration.options === undefined ? {} : { options: registration.options }),
})),
)
.pipe(Effect.orDie)
return { dispose: Effect.void }
}),
hook: (name, callback) => {
if (name === "execute.before") {
return toolHooks.hook.before((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
)
})
}
return toolHooks.hook.after((event) => {
// Decode first so plugin mutations cannot alias the canonical outcome.
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
messageID: event.messageID,
callID: event.callID,
input: event.input,
...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event),
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => {
const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output)
if (decoded._tag === "None")
return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool })
if (decoded.value.status !== event.status)
return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool })
return Effect.sync(() => {
if (event.status === "completed" && decoded.value.status === "completed") {
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
return
}
if (event.status === "error" && decoded.value.status === "error") {
event.error = decoded.value.error
event.content = decoded.value.content
event.metadata = decoded.value.metadata
event.outputPaths = decoded.value.outputPaths
}
})
}),
)
})
},
.pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
hook: (name, callback) => hooks.register("tool", name, callback),
},
websearch: {
providers: () => response(websearch.providers()),

View file

@ -1,11 +1,11 @@
export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Command } from "../command"
import { Config } from "../config"
import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command"
@ -14,7 +14,7 @@ import { ConfigPolicyPlugin } from "../config/plugin/policy"
import { ConfigReferencePlugin } from "../config/plugin/reference"
import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigWebSearchPlugin } from "../config/plugin/websearch"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { FileMutation } from "../file-mutation"
import { Form } from "../form"
import { FileSystem } from "../filesystem"
@ -27,28 +27,28 @@ import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { ModelsDev } from "../models-dev"
import { Npm } from "@opencode-ai/util/npm"
import { PermissionV2 } from "../permission"
import { Permission } from "../permission"
import { Reference } from "../reference"
import { WebSearch } from "../websearch"
import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { PatchTool } from "../tool/patch"
import { EditTool } from "../tool/edit"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
import { QuestionTool } from "../tool/question"
import { Skill } from "../skill"
import { PatchTool } from "../tool/plugin/patch"
import { EditTool } from "../tool/plugin/edit"
import { GlobTool } from "../tool/plugin/glob"
import { GrepTool } from "../tool/plugin/grep"
import { QuestionTool } from "../tool/plugin/question"
import { ReadToolFileSystem } from "../tool/read-filesystem"
import { ReadTool } from "../tool/read"
import { ShellTool } from "../tool/shell"
import { SkillTool } from "../tool/skill"
import { SubagentTool } from "../tool/subagent"
import { Tools } from "../tool/tools"
import { WebFetchTool } from "../tool/webfetch"
import { WebSearchTool } from "../tool/websearch"
import { ReadTool } from "../tool/plugin/read"
import { ShellTool } from "../tool/plugin/shell"
import { SkillTool } from "../tool/plugin/skill"
import { SubagentTool } from "../tool/plugin/subagent"
import { Tool } from "../tool"
import { WebFetchTool } from "../tool/plugin/webfetch"
import { WebSearchTool } from "../tool/plugin/websearch"
import { WellKnown } from "../wellknown"
import { WriteTool } from "../tool/write"
import { WriteTool } from "../tool/plugin/write"
import { AgentPlugin } from "./agent"
import { CommandPlugin } from "./command"
import { ModelsDevPlugin } from "./models-dev"
@ -62,11 +62,11 @@ import { WarmingPlugin } from "./warming"
import { WellKnownPlugin } from "../wellknown/plugin"
const services = Effect.fn("PluginInternal.services")(function* () {
const agent = yield* AgentV2.Service
const agent = yield* Agent.Service
const catalog = yield* Catalog.Service
const command = yield* CommandV2.Service
const command = yield* Command.Service
const config = yield* Config.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const mutation = yield* FileMutation.Service
const filesystem = yield* FileSystem.Service
const fs = yield* FSUtil.Service
@ -79,7 +79,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const locationMutation = yield* LocationMutation.Service
const models = yield* ModelsDev.Service
const npm = yield* Npm.Service
const permission = yield* PermissionV2.Service
const permission = yield* Permission.Service
const runtime = yield* PluginRuntime.Service
const form = yield* Form.Service
const read = yield* ReadToolFileSystem.Service
@ -88,15 +88,15 @@ const services = Effect.fn("PluginInternal.services")(function* () {
const ripgrep = yield* Ripgrep.Service
const instructions = yield* SessionInstructions.Service
const shell = yield* Shell.Service
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const skill = yield* Skill.Service
const tools = yield* Tool.Service
const wellknown = yield* WellKnown.Service
return Context.mergeAll(
Context.make(AgentV2.Service, agent),
Context.make(Agent.Service, agent),
Context.make(Catalog.Service, catalog),
Context.make(CommandV2.Service, command),
Context.make(Command.Service, command),
Context.make(Config.Service, config),
Context.make(EventV2.Service, events),
Context.make(Bus.Service, bus),
Context.make(FileMutation.Service, mutation),
Context.make(FileSystem.Service, filesystem),
Context.make(FSUtil.Service, fs),
@ -109,7 +109,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(LocationMutation.Service, locationMutation),
Context.make(ModelsDev.Service, models),
Context.make(Npm.Service, npm),
Context.make(PermissionV2.Service, permission),
Context.make(Permission.Service, permission),
Context.make(PluginRuntime.Service, runtime),
Context.make(Form.Service, form),
Context.make(ReadToolFileSystem.Service, read),
@ -118,8 +118,8 @@ const services = Effect.fn("PluginInternal.services")(function* () {
Context.make(Ripgrep.Service, ripgrep),
Context.make(SessionInstructions.Service, instructions),
Context.make(Shell.Service, shell),
Context.make(SkillV2.Service, skill),
Context.make(Tools.Service, tools),
Context.make(Skill.Service, skill),
Context.make(Tool.Service, tools),
Context.make(WellKnown.Service, wellknown),
)
})

View file

@ -1,13 +1,14 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Integration } from "@opencode-ai/schema/integration"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { ModelsDev } from "../models-dev"
export const ModelsDevPlugin = define({
id: "opencode.models-dev",
effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loaded = { data: structuredClone(yield* modelsDev.get()) }
yield* ctx.integration.transform((integrations) => {
for (const provider of loaded.data) {
@ -28,14 +29,14 @@ export const ModelsDevPlugin = define({
for (const provider of loaded.data) {
catalog.provider.update(provider.info.id, (draft) => {
Object.assign(draft, provider.info)
draft.integrationID = provider.info.id
draft.integrationID = Integration.ID.make(provider.info.id)
})
for (const model of provider.models) {
catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model))
}
}
})
yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() =>
modelsDev.get().pipe(
Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))),

View file

@ -1,8 +1,8 @@
export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin"
import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool"
import { define } from "@opencode-ai/plugin/effect/plugin"
import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin"
import type { Info } from "@opencode-ai/plugin/promise/tool"
import { Agent } from "@opencode-ai/schema/agent"
import { Integration } from "@opencode-ai/schema/integration"
import { Location } from "@opencode-ai/schema/location"
@ -14,7 +14,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { WebSearch } from "@opencode-ai/schema/websearch"
import { DateTime, Effect, Scope, Stream } from "effect"
import { Tool } from "../tool/tool"
import { Tool } from "../tool"
type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
@ -23,7 +23,7 @@ type JsonValue = null | boolean | number | string | Array<JsonValue> | { [key: s
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
* loader (`PluginV2` / `PluginSupervisor`) can run it unchanged.
* loader (`Plugin` / `PluginSupervisor`) 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
@ -61,7 +61,7 @@ export function fromPromise(plugin: Plugin) {
app: host.app,
options: host.options,
agent: {
get: (id) => run(host.agent.get(id)),
get: (input) => run(host.agent.get({ ...input, agentID: Agent.ID.make(input.agentID) })),
list: (input) => run(host.agent.list(input)),
transform: transform(host.agent),
reload: () => run(host.agent.reload()),
@ -77,7 +77,6 @@ export function fromPromise(plugin: Plugin) {
run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })),
},
model: {
get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)),
list: (input) => run(host.catalog.model.list(input)),
default: (input) =>
run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })),
@ -165,7 +164,44 @@ export function fromPromise(plugin: Plugin) {
}),
),
},
transform: transform(host.integration),
transform: (callback) =>
register(
host.integration.transform((draft) =>
callback({
list: draft.list,
get: draft.get,
update: draft.update,
remove: draft.remove,
method: {
list: draft.method.list,
update: (input) => {
if (!("authorize" in input)) return draft.method.update(input)
const refresh = input.refresh
draft.method.update({
...input,
authorize: (inputs) =>
Effect.promise(() => input.authorize(inputs)).pipe(
Effect.map((authorization) =>
authorization.mode === "auto"
? {
...authorization,
callback: Effect.promise(() => authorization.callback),
}
: {
...authorization,
callback: (code) => Effect.promise(() => authorization.callback(code)),
},
),
),
refresh:
refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)),
})
},
remove: draft.method.remove,
},
}),
),
),
reload: () => run(host.integration.reload()),
connection: {
active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)),
@ -190,8 +226,11 @@ export function fromPromise(plugin: Plugin) {
register(
host.tool.transform((draft) =>
callback({
add: (name: string, tool: Any, options?: RegisterOptions) =>
draft.add(name, fromPromiseTool(tool), options),
add: (tool: Info) =>
draft.add({
...tool,
execute: (input, context) => executePromiseTool(tool, input, context),
}),
}),
),
),
@ -333,15 +372,10 @@ function wireEvent(value: unknown): unknown {
return wire(value)
}
function fromPromiseTool(tool: Any): Tool.Any {
return {
...tool,
execute: (input, context) =>
Effect.promise(() =>
tool.execute(input, {
...context,
progress: (update) => Effect.runPromise(context.progress(update)),
}),
),
}
}
const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
Effect.promise(() =>
tool.execute(input, {
...context,
progress: (update) => Effect.runPromise(context.progress(update)),
}),
)

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const AlibabaPlugin = define({
id: "opencode.provider.alibaba",

View file

@ -1,7 +1,7 @@
import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
type MantleSDK = {
languageModel: (modelID: string) => LanguageModelV3
@ -64,8 +64,8 @@ export const AmazonBedrockPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
evt.provider.update(item.provider.id, (provider) => {
if (typeof provider.settings?.endpoint !== "string") return
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
@ -112,10 +112,10 @@ export const AmazonBedrockPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.providerID !== Provider.ID.amazonBedrock) return
if (
ProviderV2.isAISDK(evt.model.package) &&
ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
Provider.isAISDK(evt.model.package) &&
Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
) {
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
return

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const AnthropicPlugin = define({
id: "opencode.provider.anthropic",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {
...provider.headers,

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
if (useChat && sdk.chat) return sdk.chat(modelID)
@ -15,8 +15,8 @@ export const AzurePlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/azure") continue
const configured = item.provider.settings?.resourceName
const resourceName =
typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME
@ -30,11 +30,11 @@ export const AzurePlugin = define({
"sdk",
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) {
if (evt.model.providerID === Provider.ID.azure) {
if (
!evt.options.resourceName &&
!evt.options.baseURL &&
(!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
(!Provider.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string")
) {
throw new Error(
"AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it",
@ -48,7 +48,7 @@ export const AzurePlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return
if (evt.model.providerID !== Provider.ID.azure) return
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,
@ -66,8 +66,8 @@ export const AzureCognitiveServicesPlugin = define({
const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
if (!resourceName) return
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!item.provider.id.includes("azure-cognitive-services")) continue
evt.provider.update(item.provider.id, (provider) => {
provider.settings = {
@ -80,7 +80,7 @@ export const AzureCognitiveServicesPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
if (evt.model.providerID !== Provider.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(
evt.sdk,
evt.model.modelID ?? evt.model.id,

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const CerebrasPlugin = define({
id: "opencode.provider.cerebras",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" }
})

View file

@ -1,7 +1,7 @@
import os from "os"
import { App } from "../../app"
import { Effect, Option, Schema } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const CloudflareAIGatewayPlugin = define({
id: "opencode.provider.cloudflare-ai-gateway",

View file

@ -1,10 +1,10 @@
import os from "os"
import { App } from "../../app"
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
const providerID = Provider.ID.make("cloudflare-workers-ai")
export const CloudflareWorkersAIPlugin = define({
id: "opencode.provider.cloudflare-workers-ai",
@ -13,7 +13,7 @@ export const CloudflareWorkersAIPlugin = define({
const item = evt.provider.get(providerID)
if (!item) return
evt.provider.update(item.provider.id, (provider) => {
if (!ProviderV2.isAISDK(provider.package)) return
if (!Provider.isAISDK(provider.package)) return
if (typeof provider.settings?.baseURL === "string") return
const accountId = resolveAccountId(provider.settings ?? {})
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
@ -61,7 +61,7 @@ function hasWorkersEndpoint(model: {
readonly package?: string
readonly settings?: Readonly<Record<string, unknown>>
}) {
return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
return Provider.isAISDK(model.package) && typeof model.settings?.baseURL === "string"
}
function sdkOptions(options: Record<string, any>, app: App.Info) {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const CoherePlugin = define({
id: "opencode.provider.cohere",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const DeepInfraPlugin = define({
id: "opencode.provider.deepinfra",

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Npm } from "@opencode-ai/util/npm"
import { importModule } from "@opencode-ai/util/runtime-import"

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GatewayPlugin = define({
id: "opencode.provider.gateway",

View file

@ -1,14 +1,14 @@
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { Effect, Option, Schema, Semaphore, Stream } from "effect"
import { Catalog } from "../../catalog"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { CopilotModels } from "../../github-copilot/models"
import { App } from "../../app"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { Model } from "../../model"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
import type { PluginInternal } from "../internal"
const clientID = "Ov23li8tweQw6odWQebz"
@ -152,11 +152,11 @@ export const GithubCopilotPlugin = define({
id: "opencode.provider.github-copilot",
effect: Effect.fn(function* (ctx) {
const catalog = yield* Catalog.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
const loaded: {
baseURL?: string
models?: Map<ModelV2.ID, ModelV2.Info>
models?: Map<Model.ID, Model.Info>
} = {}
const load = Effect.fn("GithubCopilotPlugin.load")(function* () {
@ -171,8 +171,8 @@ export const GithubCopilotPlugin = define({
}
loaded.baseURL = copilotBaseURL(credential.metadata)
const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot)
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot)
const provider = yield* catalog.provider.get(Provider.ID.githubCopilot)
const existing = (yield* catalog.model.all()).filter((model) => model.providerID === Provider.ID.githubCopilot)
loaded.models = yield* Effect.tryPromise({
try: () =>
CopilotModels.get(
@ -197,11 +197,11 @@ export const GithubCopilotPlugin = define({
draft.method.update(oauth(ctx.app))
})
yield* ctx.catalog.transform((evt) => {
const item = evt.provider.get(ProviderV2.ID.githubCopilot)
const item = evt.provider.get(Provider.ID.githubCopilot)
if (!item) return
if (loaded.models) {
for (const id of item.models.keys()) {
if (!loaded.models.has(ModelV2.ID.make(id))) evt.model.remove(item.provider.id, id)
if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id)
}
for (const [id, model] of loaded.models) {
evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model)))
@ -209,12 +209,12 @@ export const GithubCopilotPlugin = define({
} else if (loaded.baseURL) {
for (const id of item.models.keys()) {
evt.model.update(item.provider.id, id, (model) => {
model.settings = ProviderV2.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
})
}
}
if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) {
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
if (item.models.has(Model.ID.make("gpt-5-chat-latest"))) {
evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => {
// This chat-only alias conflicts with the Copilot GPT-5 Responses route,
// so hide it only for Copilot rather than for every provider catalog.
model.enabled = false
@ -222,7 +222,7 @@ export const GithubCopilotPlugin = define({
}
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("github-copilot")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
@ -231,7 +231,7 @@ export const GithubCopilotPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.model.providerID !== Provider.ID.githubCopilot) return
if (evt.package !== "@ai-sdk/github-copilot" && evt.package !== "@ai-sdk/anthropic") return
evt.options.fetch = copilotFetch(
typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined,
@ -255,7 +255,7 @@ export const GithubCopilotPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.model.providerID !== Provider.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
return

View file

@ -1,8 +1,8 @@
import os from "os"
import { App } from "../../app"
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const GitLabPlugin = define({
id: "opencode.provider.gitlab",
@ -35,7 +35,7 @@ export const GitLabPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
if (evt.model.providerID !== Provider.ID.gitlab) return
const featureFlags =
typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
const id = evt.model.modelID ?? evt.model.id

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
function resolveProject(options: Record<string, any>) {
// models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs
@ -59,12 +59,12 @@ export const GoogleVertexPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (!Provider.isAISDK(item.provider.package)) continue
if (
ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" &&
!(
item.provider.id === ProviderV2.ID.googleVertex &&
ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
item.provider.id === Provider.ID.googleVertex &&
Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible")
)
)
continue
@ -78,7 +78,7 @@ export const GoogleVertexPlugin = define({
...(typeof provider.settings?.baseURL === "string"
? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) }
: {}),
...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
...(Provider.packageName(provider.package)?.includes("@ai-sdk/openai-compatible")
? { fetch: authFetch(provider.settings?.fetch) }
: {}),
}
@ -88,7 +88,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
if (evt.model.providerID === Provider.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch)
return
}
@ -108,7 +108,7 @@ export const GoogleVertexPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
if (evt.model.providerID !== Provider.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)
@ -120,8 +120,8 @@ export const GoogleVertexAnthropicPlugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue
const project =
item.provider.settings?.project ??
process.env.GOOGLE_CLOUD_PROJECT ??
@ -167,7 +167,7 @@ export const GoogleVertexAnthropicPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
if (evt.model.providerID !== Provider.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
}),
)

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GooglePlugin = define({
id: "opencode.provider.google",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const GroqPlugin = define({
id: "opencode.provider.groq",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const KiloPlugin = define({
id: "opencode.provider.kilo",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }

View file

@ -1,7 +1,7 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Integration } from "../../integration"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
export const LLMGatewayPlugin = define({
id: "opencode.provider.llmgateway",
@ -11,8 +11,8 @@ export const LLMGatewayPlugin = define({
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (item.provider.disabled) continue
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue
if (!configured.has(Integration.ID.make(item.provider.id))) continue
evt.provider.update(item.provider.id, (provider) => {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const MistralPlugin = define({
id: "opencode.provider.mistral",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const NvidiaPlugin = define({
id: "opencode.provider.nvidia",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const OpenAICompatiblePlugin = define({
id: "opencode.provider.openai-compatible",

View file

@ -1,14 +1,14 @@
import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
import { App } from "../../app"
import { Credential } from "../../credential"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { Model } from "../../model"
import { OauthCallbackPage } from "../../oauth/page"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
import type { PluginInternal } from "../internal"
import { OpenAICodex } from "./openai-codex"
@ -162,7 +162,7 @@ const headless = (app: App.Info) => ({
export const OpenAIPlugin = define({
id: "opencode.provider.openai",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const loading = Semaphore.makeUnsafe(1)
let chatgpt = false
@ -181,17 +181,17 @@ export const OpenAIPlugin = define({
yield* load()
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue
if (!item.models.has(Model.ID.make("gpt-5-chat-latest"))) continue
evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => {
// OpenAIPlugin sends OpenAI models through Responses; this alias is a
// chat-completions-only model, so hide it only from OpenAI's catalog.
model.enabled = false
})
}
if (!chatgpt) return
const item = evt.provider.get(ProviderV2.ID.openai)
const item = evt.provider.get(Provider.ID.openai)
if (!item) return
for (const model of item.models.values()) {
// ChatGPT-plan tokens only authorize codex-eligible models, and the
@ -211,7 +211,7 @@ export const OpenAIPlugin = define({
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),
@ -227,7 +227,7 @@ export const OpenAIPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
if (evt.model.providerID !== Provider.ID.openai) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,14 +1,14 @@
import { Duration, Effect, Schema, Semaphore, Stream } from "effect"
import type { Scope } from "effect"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import type { CredentialValue } from "@opencode-ai/sdk/v2/types"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { EventV2 } from "../../event"
import { Bus } from "../../bus"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { Model } from "../../model"
import { Provider } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider"
import { Money } from "@opencode-ai/schema/money"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
@ -82,10 +82,10 @@ function oauth(http: HttpClient.HttpClient) {
} satisfies IntegrationOAuthMethodRegistration
}
export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | Scope.Scope>({
export const OpencodePlugin = define<HttpClient.HttpClient | Bus.Service | Scope.Scope>({
id: "opencode.provider.opencode",
effect: Effect.fn(function* (ctx) {
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const http = yield* HttpClient.HttpClient
const loading = Semaphore.makeUnsafe(1)
let connected = false
@ -120,7 +120,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
catalog.provider.update(providerID, (provider) => {
provider.integrationID = Integration.ID.make("opencode")
if (item.name !== undefined) provider.name = item.name
provider.package = item.npm ? ProviderV2.aisdk(item.npm) : ""
provider.package = item.npm ? Provider.aisdk(item.npm) : ""
provider.settings = {
...provider.settings,
...withoutCredentials(item.options),
@ -131,12 +131,12 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
for (const [modelID, config] of Object.entries(item.models ?? {})) {
catalog.model.update(providerID, modelID, (model) => {
if (config.family !== undefined) model.family = config.family
if (config.family !== undefined) model.family = Model.Family.make(config.family)
if (config.name !== undefined) model.name = config.name
if (config.id !== undefined) model.modelID = config.id
model.compatibility = ModelV2.compatibility(config.interleaved) ?? model.compatibility
if (config.id !== undefined) model.modelID = Model.ID.make(config.id)
model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility
if (config.provider !== undefined) {
model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined
model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined
if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api }
}
if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call
@ -147,7 +147,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
if (config.variants !== undefined) {
model.variants ??= []
for (const [id, options] of Object.entries(config.variants)) {
const variantID = ModelV2.VariantID.make(id)
const variantID = Model.VariantID.make(id)
let existing = model.variants.find((item) => item.id === variantID)
if (!existing) {
existing = { id: variantID }
@ -174,7 +174,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
}
}
const item = catalog.provider.get(ProviderV2.ID.opencode)
const item = catalog.provider.get(Provider.ID.opencode)
if (!item) return
const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.settings?.apiKey)
catalog.provider.update(item.provider.id, (provider) => {
@ -190,7 +190,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
})
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")),
Stream.runForEach(refresh),
Effect.forkScoped({ startImmediately: true }),

View file

@ -1,19 +1,19 @@
import { Effect } from "effect"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Model } from "../../model"
import { Provider } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const OpenRouterPlugin = define({
id: "opencode.provider.openrouter",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }
})
for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) {
for (const modelID of [Model.ID.make("gpt-5-chat-latest"), Model.ID.make("openai/gpt-5-chat")]) {
if (!item.models.has(modelID)) continue
evt.model.update(item.provider.id, modelID, (model) => {
// These are OpenRouter-specific OpenAI chat aliases that do not work

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const PerplexityPlugin = define({
id: "opencode.provider.perplexity",

View file

@ -1,8 +1,8 @@
import { Effect } from "effect"
import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Npm } from "@opencode-ai/util/npm"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
import { importModule } from "@opencode-ai/util/runtime-import"
export const SapAICorePlugin = define({
@ -12,7 +12,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return
const serviceKey =
process.env.AICORE_SERVICE_KEY ??
(typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined)
@ -42,7 +42,7 @@ export const SapAICorePlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@ -70,7 +70,7 @@ export const SnowflakeCortexPlugin = define({
yield* ctx.aisdk.hook(
"sdk",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
if (evt.model.providerID !== Provider.ID.make("snowflake-cortex")) return
const token =
process.env.SNOWFLAKE_CORTEX_TOKEN ??
process.env.SNOWFLAKE_CORTEX_PAT ??

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const TogetherAIPlugin = define({
id: "opencode.provider.togetherai",

View file

@ -1,5 +1,5 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
export const VenicePlugin = define({
id: "opencode.provider.venice",

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const VercelPlugin = define({
id: "opencode.provider.vercel",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/vercel") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" }
})

View file

@ -1,12 +1,12 @@
import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Clock, Deferred, Effect, Option, Schema } from "effect"
import { App } from "../../app"
import { Credential } from "../../credential"
import { Integration } from "../../integration"
import { OauthCallbackPage } from "../../oauth/page"
import { ProviderV2 } from "../../provider"
import { Provider } from "../../provider"
const clientID = "b1a00492-073a-47ea-816f-4c329264a828"
const issuer = "https://auth.x.ai/oauth2"
@ -173,7 +173,7 @@ export const XAIPlugin = define({
yield* ctx.aisdk.hook(
"language",
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
if (evt.model.providerID !== Provider.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
}),
)

View file

@ -1,14 +1,14 @@
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ProviderV2 } from "../../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Provider } from "../../provider"
export const ZenmuxPlugin = define({
id: "opencode.provider.zenmux",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform((evt) => {
for (const item of evt.provider.list()) {
if (!ProviderV2.isAISDK(item.provider.package)) continue
if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (!Provider.isAISDK(item.provider.package)) continue
if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue
if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.headers = {

View file

@ -1,16 +1,16 @@
export * as PluginRuntime from "./runtime"
import { Context, Effect, Layer } from "effect"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Job } from "../job"
import { Location } from "../location"
import { LocationServiceMap } from "../location-service-map"
import { SessionV2 } from "../session"
import { Session } from "../session"
export interface Interface {
readonly session: Pick<
SessionV2.Interface,
Session.Interface,
"get" | "create" | "messages" | "prompt" | "generate" | "command" | "resume" | "interrupt" | "synthetic"
>
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
@ -18,7 +18,7 @@ export interface Interface {
readonly agent: {
readonly list: (
ref: Location.Ref,
) => Effect.Effect<{ readonly location: Location.Info; readonly data: AgentV2.Info[] }>
) => Effect.Effect<{ readonly location: Location.Info; readonly data: Agent.Info[] }>
}
}
}
@ -74,7 +74,7 @@ export const layerWithCell = (cell: Cell) =>
export const providerLayerWithCell = (cell: Cell) =>
Layer.effectDiscard(
Effect.gen(function* () {
const sessions = yield* SessionV2.Service
const sessions = yield* Session.Service
const jobs = yield* Job.Service
const locations = yield* LocationServiceMap.Service
const runtime: Interface = {
@ -85,7 +85,7 @@ export const providerLayerWithCell = (cell: Cell) =>
list: (ref) =>
Effect.gen(function* () {
const location = yield* Location.Service
const agents = yield* AgentV2.Service
const agents = yield* Agent.Service
return {
location: new Location.Info({
directory: location.directory,
@ -118,7 +118,7 @@ export const providerNodeWithCell = (cell: Cell) =>
makeGlobalNode({
name: "plugin-runtime-provider",
layer: providerLayerWithCell(cell),
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node],
deps: [node, Session.node, Job.node, LocationServiceMap.node],
})
export const providerNode = providerNodeWithCell(defaultCell)

View file

@ -1,12 +1,12 @@
export * as SdkPlugins from "./sdk"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/effect/plugin"
import { Context, Effect, Layer } from "effect"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "../event"
import type { PluginV2 } from "../plugin"
import { Bus } from "../bus"
import type { Versioned } from "../plugin"
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
export const Updated = Bus.ephemeral({ type: "sdk.plugin.updated", schema: {} })
/**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
@ -21,7 +21,7 @@ export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {
*/
export interface Interface {
readonly register: (plugin: Plugin) => Effect.Effect<void>
readonly all: () => readonly PluginV2.Versioned[]
readonly all: () => readonly Versioned[]
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
@ -29,17 +29,17 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Sd
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const plugins = new Map<string, PluginV2.Versioned>()
const bus = yield* Bus.Service
const plugins = new Map<string, Versioned>()
let revision = 0
return Service.of({
register: (plugin) =>
Effect.sync(() => {
plugins.set(plugin.id, { ...plugin, version: String(++revision) })
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
}).pipe(Effect.andThen(bus.publish(Updated, {})), Effect.asVoid),
all: () => [...plugins.values()],
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
export const node = makeGlobalNode({ service: Service, layer, deps: [Bus.node] })

View file

@ -2,10 +2,10 @@
export * as SkillPlugin from "./skill"
import { define, type Context } from "@opencode-ai/plugin/v2/effect/plugin"
import { define, type Context } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import { AbsolutePath } from "../schema"
import { SkillV2 } from "../skill"
import { Skill } from "../skill"
import { Config } from "../config"
import { Location } from "../location"
import { FSUtil } from "@opencode-ai/util/fs-util"
@ -29,11 +29,11 @@ export const Plugin = define({
const reportContent = yield* reportContentWithDiagnostics(ctx.app)
yield* ctx.skill.transform((draft) => {
draft.source(
SkillV2.EmbeddedSource.make({
Skill.EmbeddedSource.make({
type: "embedded",
skill: SkillV2.Info.make({
id: SkillV2.ID.make("opencode"),
name: SkillV2.Name.make("OpenCode"),
skill: Skill.Info.make({
id: Skill.ID.make("opencode"),
name: Skill.Name.make("OpenCode"),
description: OpencodeDescription,
location: AbsolutePath.make("/builtin/opencode.md"),
content: OpencodeContent,
@ -41,11 +41,11 @@ export const Plugin = define({
}),
)
draft.source(
SkillV2.EmbeddedSource.make({
Skill.EmbeddedSource.make({
type: "embedded",
skill: SkillV2.Info.make({
id: SkillV2.ID.make("report"),
name: SkillV2.Name.make("Report"),
skill: Skill.Info.make({
id: Skill.ID.make("report"),
name: Skill.Name.make("Report"),
description: REPORT_DESCRIPTION,
slash: true,
location: AbsolutePath.make("/builtin/report.md"),

View file

@ -4,11 +4,11 @@ Use this guide as the starting point for work involving OpenCode itself. It
covers the core concepts needed to configure and customize OpenCode, extend it
with plugins, and build integrations with the OpenCode SDK, clients, and API.
Full documentation is available at <https://v2.opencode.ai/docs>. This overview is
Full documentation is available at <https://opencode.ai/v2/docs/>. This overview is
only an index of core concepts. Before answering a question about a topic below,
fetch the URL named in that section and use the full page as the source of
truth. Follow links from that page when the question needs more detail. Fetch
<https://v2.opencode.ai/llms.txt> first when you need to discover the relevant
<https://opencode.ai/v2/docs/> first when you need to discover the relevant
documentation page.
## Version policy
@ -16,10 +16,10 @@ documentation page.
Always answer for OpenCode V2 unless the user explicitly asks about V1,
legacy OpenCode, or migrating from V1.
Use only <https://v2.opencode.ai/docs> documentation as the source of truth for V2.
Use only <https://opencode.ai/v2/docs/> documentation as the source of truth for V2.
Do not use <https://opencode.ai/docs/>, which documents V1, and do not use
general web search to resolve a V2 documentation question when the V2 docs or
their `llms.txt` index cover it. The schema served from
linked pages cover it. The schema served from
<https://opencode.ai/config.json> may describe V1 even though V2 configuration
files include that URL for editor integration. Never use it to infer V2 field
names or shapes. If V2 documentation is missing or contradictory, state the
@ -29,7 +29,7 @@ V1 documentation and syntax may be consulted only when the user explicitly
asks about V1 or when needed as migration input. Outputs and recommendations
must still use V2 unless the user specifically requests a V1 result.
## [Configuration](https://v2.opencode.ai/docs/config)
## [Configuration](https://opencode.ai/v2/docs/config)
OpenCode configuration uses JSON or JSONC. Include the published schema so the
user's editor can validate fields and provide autocomplete:
@ -60,14 +60,14 @@ linked topic guide as the source of truth, and preserve unrelated settings when
editing an existing file. Keep the published `$schema` URL in configuration
examples, but do not fetch it to determine the V2 configuration shape.
See the [full configuration guide](https://v2.opencode.ai/docs/config) for
See the [full configuration guide](https://opencode.ai/v2/docs/config) for
every field, examples, config locations, and links to dedicated feature guides.
## [V1 to V2 migration](https://v2.opencode.ai/docs/migrate-v1)
## [V1 to V2 migration](https://opencode.ai/v2/docs/migrate-v1)
For any request to migrate OpenCode configuration, agents, commands, skills,
plugins, integrations, or other behavior from V1 to V2, read the full
[migration guide](https://v2.opencode.ai/docs/migrate-v1) before acting. In
[migration guide](https://opencode.ai/v2/docs/migrate-v1) before acting. In
the repository, its source is `packages/www/content/docs/(Get started)/migrate-v1.mdx`.
V1 config files and `.opencode/` definitions are intended to remain compatible.
@ -76,18 +76,18 @@ V2 config uses more ergonomic shapes, but conversion is optional. When the user
requests conversion, inspect the complete configuration, preserve behavior and
unrelated settings, and apply only the relevant migrations from the guide. For
plugin migrations, fetch and follow both the migration guide and the full
[plugins guide](https://v2.opencode.ai/docs/build/plugins). If non-API V1
[plugins guide](https://opencode.ai/v2/docs/build/plugins). If non-API V1
functionality fails in V2, use the `report` skill to file it as a compatibility
bug.
## [Plugins](https://v2.opencode.ai/docs/build/plugins)
## [Plugins](https://opencode.ai/v2/docs/build/plugins)
For questions about creating, configuring, loading, publishing, or migrating
plugins, fetch the full [plugins guide](https://v2.opencode.ai/docs/build/plugins)
plugins, fetch the full [plugins guide](https://opencode.ai/v2/docs/build/plugins)
before answering. This includes questions about the Effect plugin API, hooks,
transforms, tools, plugin context capabilities, and package entrypoints.
## [Service](https://v2.opencode.ai/docs/troubleshooting#check-the-background-service)
## [Service](https://opencode.ai/v2/docs/troubleshooting#check-the-background-service)
OpenCode uses a client-server architecture. Interfaces such as the TUI connect
to a background OpenCode service, which owns sessions, configuration, plugins,
@ -106,7 +106,7 @@ Check its status after restarting:
opencode2 service status
```
## [API](https://v2.opencode.ai/docs/api)
## [API](https://opencode.ai/v2/docs/api)
OpenCode exposes an HTTP API from its server. The API is described by an
OpenAPI document available from the running server at `/openapi.json`.
@ -135,15 +135,15 @@ connected to an explicit server instead of its managed background service, use
the same configured server and authentication context rather than constructing
an unauthenticated request separately.
See the [full API reference](https://v2.opencode.ai/docs/api) for available
See the [full API reference](https://opencode.ai/v2/docs/api) for available
endpoints, parameters, request bodies, and response schemas. The
raw [OpenAPI specification](https://v2.opencode.ai/openapi.json) is also
raw [OpenAPI specification](https://opencode.ai/v2/openapi.json) is also
available for code generation and other tooling.
## [Client](https://v2.opencode.ai/docs/build/client)
## [Client](https://opencode.ai/v2/docs/build/client)
For questions about connecting an application to OpenCode over the network,
fetch the full [client guide](https://v2.opencode.ai/docs/build/client) before
fetch the full [client guide](https://opencode.ai/v2/docs/build/client) before
answering.
`@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP
@ -154,7 +154,7 @@ exposes typed Effects, Streams, and decoded OpenCode schema values. Its
`Service` API can discover, start, stop, and authenticate with the local
background service from a Node application.
## [Troubleshooting](https://v2.opencode.ai/docs/troubleshooting)
## [Troubleshooting](https://opencode.ai/v2/docs/troubleshooting)
OpenCode runs a client and a background server. Start by determining whether a
problem belongs to the client, the shared server, or one project.
@ -174,6 +174,6 @@ problem belongs to the client, the shared server, or one project.
- Redact API keys, authorization headers, prompts, file contents, and other
sensitive data before sharing diagnostics.
See the [full troubleshooting guide](https://v2.opencode.ai/docs/troubleshooting)
See the [full troubleshooting guide](https://opencode.ai/v2/docs/troubleshooting)
for service lifecycle commands, API inspection, log locations, explicit server
connections, issue-reporting details, and local development paths.

View file

@ -1,18 +1,17 @@
export * as PluginSupervisor from "./supervisor"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import { Event } from "@opencode-ai/schema/config"
import { Context, Deferred, Effect, Layer, Option, Schema, Semaphore, Stream } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { AgentV2 } from "../agent"
import { Agent } from "../agent"
import { Catalog } from "../catalog"
import { CommandV2 } from "../command"
import { Command } from "../command"
import { Config } from "../config"
import { ConfigPlugin } from "../config/plugin"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { FileMutation } from "../file-mutation"
import { FileSystem } from "../filesystem"
import { Form } from "../form"
@ -25,16 +24,16 @@ import { Location } from "../location"
import { LocationMutation } from "../location-mutation"
import { ModelsDev } from "../models-dev"
import { Npm } from "@opencode-ai/util/npm"
import { PermissionV2 } from "../permission"
import { PluginV2 } from "../plugin"
import { Permission } from "../permission"
import { Plugin } from "../plugin"
import { PluginPromise } from "../plugin/promise"
import { Reference } from "../reference"
import { Ripgrep } from "../ripgrep"
import { SessionInstructions } from "../session/instructions"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { Skill } from "../skill"
import { ReadToolFileSystem } from "../tool/read-filesystem"
import { ToolRegistry } from "../tool/registry"
import { Tool } from "../tool"
import { WebSearch } from "../websearch"
import { WellKnown } from "../wellknown"
import { PluginInternal } from "./internal"
@ -46,7 +45,9 @@ const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({
id: Schema.String,
effect: Schema.declare<Plugin["effect"]>((input): input is Plugin["effect"] => typeof input === "function"),
effect: Schema.declare<import("@opencode-ai/plugin/effect/plugin").Plugin["effect"]>(
(input): input is import("@opencode-ai/plugin/effect/plugin").Plugin["effect"] => typeof input === "function",
),
}),
Schema.Struct({
id: Schema.String,
@ -113,15 +114,15 @@ const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Con
})
const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
pre: readonly PluginV2.Versioned[],
post: readonly PluginV2.Versioned[],
pre: readonly Plugin.Versioned[],
post: readonly Plugin.Versioned[],
operations: readonly Operation[],
) {
const matches = (selector: string, target: string) =>
selector === "*" || (selector.endsWith(".*") ? target.startsWith(selector.slice(0, -1)) : selector === target)
const definitions = [...pre, ...post]
const enabled = new Set(definitions.map((plugin) => plugin.id))
const packages = new Map<string, PluginV2.Versioned>()
const packages = new Map<string, Plugin.Versioned>()
const plugins = () => [...definitions, ...packages.values()]
for (const operation of operations) {
@ -183,7 +184,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
id: plugin.id,
version: JSON.stringify(operation),
effect: (host) => plugin.effect({ ...host, options: operation.options }),
} satisfies PluginV2.Versioned
} satisfies Plugin.Versioned
})
function discoverDirectory(fs: FSUtil.Interface, directory: string) {
@ -211,10 +212,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pl
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const registry = yield* PluginV2.Service
const registry = yield* Plugin.Service
const sdk = yield* SdkPlugins.Service
const config = yield* Config.Service
const events = yield* EventV2.Service
const bus = yield* Bus.Service
const lock = Semaphore.makeUnsafe(1)
const ready = yield* Deferred.make<void>()
let observed = 0
@ -238,7 +239,7 @@ const layer = Layer.effect(
}),
)
})
const updates = yield* events
const updates = yield* bus
.subscribe([Event.Updated, SdkPlugins.Updated])
.pipe(Stream.toQueue({ capacity: 1, strategy: "sliding" }))
const signals = yield* Stream.concat(
@ -276,13 +277,13 @@ export const node = makeLocationNode({
service: Service,
layer: nodeLayer,
deps: [
PluginV2.node,
Plugin.node,
SdkPlugins.node,
AgentV2.node,
Agent.node,
Catalog.node,
CommandV2.node,
Command.node,
Config.node,
EventV2.node,
Bus.node,
FileMutation.node,
FileSystem.node,
FSUtil.node,
@ -295,7 +296,7 @@ export const node = makeLocationNode({
LocationMutation.node,
ModelsDev.node,
Npm.node,
PermissionV2.node,
Permission.node,
PluginRuntime.node,
Form.node,
ReadToolFileSystem.node,
@ -303,8 +304,8 @@ export const node = makeLocationNode({
Ripgrep.node,
SessionInstructions.node,
Shell.node,
SkillV2.node,
ToolRegistry.toolsNode,
Skill.node,
Tool.node,
WebSearch.node,
WellKnown.node,
],

View file

@ -1,6 +1,6 @@
export * as SystemPromptPlugin from "./system-prompt"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect } from "effect"
import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt"
@ -33,14 +33,16 @@ function make(id: string, select: (modelID: string) => string | undefined) {
effect: Effect.fn(`SystemPromptPlugin.${id}`)(function* (ctx) {
yield* ctx.session.hook("context", (event) =>
Effect.gen(function* () {
if ((yield* ctx.agent.get(event.agent))?.system) return
if ((yield* ctx.agent.get({ agentID: event.agent })).data.system) return
const system = event.system[0]
if (!system) return
const model = yield* ctx.catalog.model.get(event.model.providerID, event.model.id)
const model = (yield* ctx.catalog.model.list()).data.find(
(model) => model.providerID === event.model.providerID && model.id === event.model.id,
)
const prompt = select(`${model?.modelID ?? event.model.id} ${model?.family ?? ""}`.toLowerCase())
if (!prompt) return
event.system[0] = { ...system, text: prompt }
}),
}).pipe(Effect.catch(() => Effect.void)),
)
}),
})

View file

@ -1,9 +1,9 @@
export * as VariantPlugin from "./variant"
import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { ModelV2 } from "../model"
import { ProviderV2 } from "../provider"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Model } from "../model"
import { Provider } from "../provider"
export const Plugin = define({
id: "opencode.variant",
@ -32,13 +32,13 @@ export const Plugin = define({
export function generate(
model: { readonly id: string; readonly modelID?: string; readonly package?: string },
provider?: { readonly package: string },
): NonNullable<ModelV2.Info["variants"]> {
): NonNullable<Model.Info["variants"]> {
const packageName = model.package ?? provider?.package
if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return []
const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase()
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
return ["high", "max"].map((id) => ({
id: ModelV2.VariantID.make(id),
id: Model.VariantID.make(id),
settings: { reasoningEffort: id },
}))
}

View file

@ -1,6 +1,6 @@
export * as WarmingPlugin from "./warming"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Clock, Duration, Effect, Scope } from "effect"
import { Config } from "../config"
import { SessionSchema } from "../session/schema"

View file

@ -1,6 +1,6 @@
export * as WebSearchExa from "./exa"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Schema, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { WebSearchMcp } from "./mcp"

View file

@ -1,6 +1,6 @@
export * as WebSearchParallel from "./parallel"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Effect, Schema, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { App } from "../../app"