Compare commits

..
Author SHA1 Message Date
Shoubhit Dash b0dac0d5b3 refactor(core): read the Plan directory from plugin options
Core config no longer knows about Plan mode. This removes the plan key, its schema, and the regenerated OpenAPI and client output. The opencode.plan plugin reads directory from its plugin options instead. It logs and falls back to ~/.opencode/plan when the options are invalid, so a typo never leaves the Plan agent without its edit restrictions.
2026-10-06 19:44:50 +05:30
Shoubhit Dash 36e25abab9 feat(core): pass plugin options to built-in plugins
Package plugins already receive the options from their plugins entry. Built-in, SDK, and instance plugins were only enabled, and their options were dropped. An exact add entry now passes its options to that plugin and folds them into its revision, so an edit restarts the plugin with the new values. Wildcards never set options, and a later plain entry for the same ID resets them.
2026-10-06 19:44:34 +05:30
Shoubhit Dash 50867e1b2d feat(core): add plan.directory config
The Plan agent wrote plans only to a hard-coded ~/.opencode/plan. The new plan.directory key sets that directory. Relative paths resolve against the current checkout root and ~/ against the home directory; the default stays ~/.opencode/plan.

The Plan plugin reads the key through ConfigEntryObserver, so config edits apply without a restart. It names its edit allow rule with FileAccess.resource, so a directory inside the project matches the Location-relative resources that file tools check.
2026-10-06 18:52:00 +05:30
Shoubhit Dash d8da92780f refactor(core): export FileAccess.resource for permission naming
FileAccess.resolve names in-project targets relative to the Location directory and everything else by absolute path. Move that rule into an exported pure function so permission rule builders can name a directory the same way resolve names the files inside it.
2026-10-06 18:50:18 +05:30
26 changed files with 402 additions and 976 deletions

No files matched your search

+14 -11
View File
@@ -76,6 +76,18 @@ export const resolvePath = (directory: string, input: string, home = Global.Path
)
}
/** Name a path the way permission rules see it: Location-relative inside the project, absolute outside it. */
export const resource = (location: Location.Info, absolute: string) =>
internal(location, absolute) ? slash(path.relative(location.directory, absolute) || ".") : slash(absolute)
const internal = (location: Location.Info, absolute: string) => {
const worktree = path.resolve(location.project.directory)
return (
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
)
}
const slash = (value: string) => value.replaceAll("\\", "/")
const invocation = (context: Invocation) => ({
sessionID: context.sessionID,
@@ -92,16 +104,7 @@ const layer = Layer.effect(
const resolve = Effect.fn("FileAccess.resolve")(function* (input: ResolveInput) {
const absolute = AbsolutePath.make(resolvePath(location.directory, input.path))
const worktree = path.resolve(location.project.directory)
const internal =
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
if (internal) {
return {
absolute,
resource: slash(path.relative(location.directory, absolute) || "."),
} satisfies Target
}
if (internal(location, absolute)) return { absolute, resource: resource(location, absolute) } satisfies Target
const type =
input.kind === "directory"
? "Directory"
@@ -112,7 +115,7 @@ const layer = Layer.effect(
const directory = AbsolutePath.make(type === "Directory" ? absolute : path.dirname(absolute))
return {
absolute,
resource: slash(absolute),
resource: resource(location, absolute),
externalDirectory: {
action: "external_directory",
directory,
+1 -1
View File
@@ -1 +1 @@
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MiMo-V2.6-Flash","description":"MiMo Flash model for multimodal coding agents and long-context automation","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028},"canonical_model_id":"xiaomi/mimo-v2.6-flash"},"XiaomiMiMo/MiMo-V2.5":{"id":"XiaomiMiMo/MiMo-V2.5","name":"MiMo-V2.5","description":"Open MiMo model for multimodal coding agents and long-coLine truncated
{"deepinfra":{"id":"deepinfra","env":["DEEPINFRA_API_KEY"],"npm":"@ai-sdk/deepinfra","name":"Deep Infra","doc":"https://deepinfra.com/models","models":{"tencent/Hy3":{"id":"tencent/Hy3","name":"Hy3","description":"Tencent Hy reasoning model for coding, instruction following, and agent tasks","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-07-06","last_updated":"2026-07-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":262144,"input":192000,"output":128000},"cost":{"input":0.13,"output":0.53,"cache_read":0.033},"canonical_model_id":"tencent/hy3"},"tencent/Hy4-preview":{"id":"tencent/Hy4-preview","name":"Hy4 preview","description":"A next-generation productivity model with significantly enhanced Agent and complex task execution capabilities.","family":"Hy","attachment":false,"reasoning":true,"reasoning_options":[{"type":"effort","values":["none","high"]}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-08-28","last_updated":"2026-08-28","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":64000},"cost":{"input":0.834,"output":2.501,"cache_read":0.042},"canonical_model_id":"tencent/hy4-preview"},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"id":"meta-llama/Llama-3.3-70B-Instruct-Turbo","name":"Llama 3.3 70B Turbo","description":"Compact Llama instruction model for fast chat and local deployment","family":"llama","attachment":false,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2024-12-06","last_updated":"2024-12-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"limit":{"context":131072,"output":16384},"cost":{"input":0.1,"output":0.32}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"id":"meta-llama/Llama-4-Scout-17B-16E-Instruct","name":"Llama 4 Scout 17B","description":"Open multimodal Llama model for long-context analysis and efficient agents","family":"llama","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":327680,"output":16384},"cost":{"input":0.1,"output":0.3}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"id":"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8","name":"Llama 4 Maverick 17B FP8","description":"Open multimodal Llama model for strong reasoning and fast responses","family":"llama","attachment":true,"reasoning":false,"tool_call":false,"structured_output":true,"release_date":"2025-04-05","last_updated":"2025-04-05","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":0.2,"output":0.8}},"XiaomiMiMo/MiMo-V2.6-Pro":{"id":"XiaomiMiMo/MiMo-V2.6-Pro","name":"MiMo-V2.6-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.43,"output":0.87,"cache_read":0.0036},"canonical_model_id":"xiaomi/mimo-v2.6-pro"},"XiaomiMiMo/MiMo-V2.5-Pro":{"id":"XiaomiMiMo/MiMo-V2.5-Pro","name":"MiMo-V2.5-Pro","description":"Stronger MiMo Pro tier for multimodal reasoning and coding-agent execution","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"interleaved":{"field":"reasoning_content"},"structured_output":true,"temperature":true,"knowledge":"2024-12","release_date":"2026-04-22","last_updated":"2026-04-22","modalities":{"input":["text","audio"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":16384},"status":"deprecated","cost":{"input":1,"output":3,"cache_read":0.2},"canonical_model_id":"xiaomi/mimo-v2.5-pro"},"XiaomiMiMo/MiMo-V2.6-Flash":{"id":"XiaomiMiMo/MiMo-V2.6-Flash","name":"MiMo-V2.6-Flash","description":"MiMo Flash model for multimodal coding agents and long-context automation","family":"mimo","attachment":true,"reasoning":true,"reasoning_options":[{"type":"toggle"}],"tool_call":true,"structured_output":true,"temperature":true,"release_date":"2026-09-22","last_updated":"2026-09-22","modalities":{"input":["text","image","audio","video"],"output":["text"]},"open_weights":true,"limit":{"context":1048576,"output":131072},"cost":{"input":0.14,"output":0.28,"cache_read":0.0028},"canonical_model_id":"xiaomi/mimo-v2.6-flash"},"XiaomiMiMo/MiMo-V2.5":{"id":"XiaomiMiMo/MiMo-V2.5","name":"MiMo-V2.5","description":"Open MiMo model for multimodal coding agents and long-coLine truncated
+19 -3
View File
@@ -5,12 +5,17 @@ import { define } from "@opencode/plugin/effect/plugin"
import { Agent } from "@opencode/schema/agent"
import type { SessionEvent } from "@opencode/schema/session-event"
import { Global } from "@opencode/util/global"
import { Effect, Stream } from "effect"
import { Effect, Option, Schema, Stream } from "effect"
import path from "path"
import { FileAccess } from "../file-access.js"
import { Permission } from "../permission.js"
const plan = Agent.ID.make("plan")
const Options = Schema.Struct({
directory: Schema.optional(Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()))),
})
const enter = (directory: string) => `<system-reminder>
You are in Plan mode. Discuss the plan with the user directly in the conversation. Do not create or update plan files unless the user explicitly asks you to; when they do, write them only in:
${directory}
@@ -28,7 +33,14 @@ export const Plugin = define({
id: "opencode.plan",
effect: Effect.fn(function* (ctx) {
const global = yield* Global.Service
const directory = path.join(global.home, ".opencode", "plan")
const options = Schema.decodeUnknownOption(Options)(ctx.options)
if (Option.isNone(options))
yield* Effect.logWarning("ignoring invalid Plan plugin options", { options: ctx.options })
const directory = FileAccess.resolvePath(
ctx.location.project.directory,
Option.getOrUndefined(options)?.directory ?? "~/.opencode/plan",
global.home,
)
const enterReminder = enter(directory)
yield* ctx.agent.transform((editor) => {
editor.update(plan, (item) => {
@@ -37,7 +49,11 @@ export const Plugin = define({
item.mode = "primary"
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
item.permissions.push({ action: "edit", resource: "*", effect: "deny" })
item.permissions.push({ action: "edit", resource: path.join(directory, "*"), effect: "allow" })
item.permissions.push({
action: "edit",
resource: path.join(FileAccess.resource(ctx.location, directory), "*"),
effect: "allow",
})
item.permissions.push({ action: "external_directory", resource: path.join(directory, "*"), effect: "allow" })
})
})
+13 -2
View File
@@ -28,6 +28,7 @@ const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
const definitions = [...pre, ...post]
const enabled = new Set(definitions.map((plugin) => plugin.id))
const packages = new Map<string, Plugin.Generation>()
const options = new Map<string, Record<string, unknown>>()
const pending = new Set<string>()
const failures = new Map<
string,
@@ -52,6 +53,7 @@ const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
operation.target.startsWith("opencode.")
if (selectsPlugins) {
matched.forEach((plugin) => enabled.add(plugin.id))
if (definitions.some((plugin) => plugin.id === operation.target)) options.set(operation.target, operation.options)
continue
}
@@ -88,10 +90,19 @@ const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
enabled.add(plugin.id)
}
const withOptions = (plugin: Plugin.Generation): Plugin.Generation => {
const selected = options.get(plugin.id)
if (!selected || Object.keys(selected).length === 0) return plugin
return {
...plugin,
revision: JSON.stringify([plugin.revision, selected]),
effect: (host) => plugin.effect({ ...host, options: selected }),
}
}
const ordered = [
...pre.filter((plugin) => enabled.has(plugin.id)),
...pre.filter((plugin) => enabled.has(plugin.id)).map(withOptions),
...[...packages.values()].filter((plugin) => enabled.has(plugin.id)),
...post.filter((plugin) => enabled.has(plugin.id)),
...post.filter((plugin) => enabled.has(plugin.id)).map(withOptions),
]
// Registry activation dies on a duplicate ID, which would drop the whole generation including builtins.
// Keep the first occurrence in boot order and report later ones like any other plugin setup failure.
+94 -4
View File
@@ -5,11 +5,14 @@ import type { SessionContext } from "@opencode/plugin/effect/session"
import type { ToolHooks } from "@opencode/plugin/effect/tool"
import { Agent } from "@opencode/core/agent"
import { Environment } from "@opencode/core/environment/index"
import { Location } from "@opencode/core/location"
import { Event } from "@opencode/schema/event"
import { Model } from "@opencode/core/model"
import { PlanPlugin } from "@opencode/core/plugin/plan"
import { Permission } from "@opencode/core/permission"
import { Project } from "@opencode/core/project"
import { Provider } from "@opencode/core/provider"
import { AbsolutePath } from "@opencode/core/schema"
import { Session } from "@opencode/core/session"
import { SessionEvent } from "@opencode/core/session/event"
import { SessionInbox } from "@opencode/core/session/inbox"
@@ -35,7 +38,10 @@ const agentSelected = (agent: Agent.ID, previous: Agent.ID): SessionEvent.AgentS
})
/** Runs the plan plugin against stubbed domains, capturing persisted reminders and the context hook. */
const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.AgentSelected> = []) {
const run = Effect.fnUntraced(function* (
events: ReadonlyArray<SessionEvent.AgentSelected> = [],
input: { options?: Record<string, unknown>; location?: Location.Info } = {},
) {
const persisted = new Array<string>()
let contextHook: ((input: SessionContext) => Effect.Effect<void>) | undefined
let toolHook: ((input: ToolHooks["execute.after"]) => Effect.Effect<void>) | undefined
@@ -51,8 +57,9 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.Agen
],
} satisfies Types.DeepMutable<Agent.Info>
const driver = Environment.makeMemoryDriver()
yield* PlanPlugin.Plugin.effect(
host({
yield* PlanPlugin.Plugin.effect({
...host({
location: input.location,
agent: {
get: () => Effect.die("unused agent.get"),
list: () => Effect.die("unused agent.list"),
@@ -106,7 +113,8 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.Agen
},
},
}),
).pipe(
options: input.options ?? {},
}).pipe(
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home })),
Effect.provideService(
Environment.Service,
@@ -309,3 +317,85 @@ describe("plan plugin mutations", () => {
}),
)
})
describe("plan plugin directory", () => {
it.effect("uses an absolute directory from plugin options", () =>
Effect.gen(function* () {
const { planAgent, contextHook, toolHook } = yield* run([], { options: { directory: "/plans" } })
const messages = [Message.user("where do plans go?")]
yield* contextHook(request(plan, messages))
const reminder = messages[0]?.content[0]
expect(reminder?.type === "text" && reminder.text).toContain("/plans")
expect(Permission.evaluate("edit", "/plans/work.md", planAgent.permissions).effect).toBe("allow")
expect(Permission.evaluate("edit", "/home/plan-test/.opencode/plan/work.md", planAgent.permissions).effect).toBe(
"deny",
)
const event = toolError(
"edit",
new ToolFailure({
message: "Unable to modify file",
error: new Permission.BlockedError({ rules: [], permission: "edit", resources: ["source.ts"] }),
}),
)
yield* toolHook(event)
expect(event.error.message).toBe("Cannot use edit to modify files outside the Plan directory: /plans")
}),
)
it.effect("expands a home-relative directory from plugin options", () =>
Effect.gen(function* () {
const { planAgent, contextHook } = yield* run([], { options: { directory: "~/plans" } })
const messages = [Message.user("where do plans go?")]
yield* contextHook(request(plan, messages))
const reminder = messages[0]?.content[0]
expect(reminder?.type === "text" && reminder.text).toContain("/home/plan-test/plans")
expect(Permission.evaluate("edit", "/home/plan-test/plans/work.md", planAgent.permissions).effect).toBe("allow")
}),
)
it.effect("resolves a relative directory against the project root", () =>
Effect.gen(function* () {
const { planAgent, contextHook } = yield* run([], {
options: { directory: ".opencode/plans" },
location: new Location.Info({
directory: AbsolutePath.make("/workspace/packages/app"),
project: {
id: Project.ID.global,
directory: AbsolutePath.make("/workspace"),
canonical: AbsolutePath.make("/workspace/canonical"),
},
}),
})
const messages = [Message.user("where do plans go?")]
yield* contextHook(request(plan, messages))
const reminder = messages[0]?.content[0]
expect(reminder?.type === "text" && reminder.text).toContain("/workspace/.opencode/plans")
expect(Permission.evaluate("edit", "../../.opencode/plans/work.md", planAgent.permissions).effect).toBe("allow")
expect(Permission.evaluate("edit", "src/index.ts", planAgent.permissions).effect).toBe("deny")
}),
)
it.effect("allows the default directory when the location is the home directory", () =>
Effect.gen(function* () {
const { planAgent } = yield* run([], {
location: new Location.Info({
directory: AbsolutePath.make(home),
project: { id: Project.ID.global, directory: AbsolutePath.make(home), canonical: AbsolutePath.make(home) },
}),
})
expect(Permission.evaluate("edit", ".opencode/plan/work.md", planAgent.permissions).effect).toBe("allow")
expect(Permission.evaluate("edit", "notes.md", planAgent.permissions).effect).toBe("deny")
}),
)
it.effect("falls back to the default directory when options are invalid", () =>
Effect.gen(function* () {
for (const options of [{ directory: 42 }, { directory: " " }]) {
const { planAgent } = yield* run([], { options })
expect(Permission.evaluate("edit", "/home/plan-test/.opencode/plan/work.md", planAgent.permissions).effect).toBe(
"allow",
)
}
}),
)
})
@@ -258,4 +258,71 @@ describe("PluginSupervisor", () => {
expect(source.activations).toBe(2)
}),
)
it.effect("passes exact add options to a non-package plugin and restarts it when they change", () =>
Effect.gen(function* () {
const seen = new Array<Record<string, unknown>>()
const sdk = yield* SdkPlugins.Service
yield* sdk.register(
define({
id: "options-probe",
effect: (ctx) => Effect.sync(() => seen.push(ctx.options)),
}),
)
source.activations = 0
source.operations = [{ type: "add", target: "options-probe", options: { directory: "/plans" } }]
const directory = yield* tmpdirScoped()
const locations = yield* LocationServiceMap.Service
yield* Effect.gen(function* () {
const plugins = yield* Plugin.Service
yield* plugins.awaitActivation
expect(seen).toEqual([{ directory: "/plans" }])
source.operations = [{ type: "add", target: "options-probe", options: { directory: "/srv/plans" } }]
yield* sdk.register(define({ id: "options-reload", effect: () => Effect.void }))
yield* advance(() => source.activations === 2)
yield* plugins.awaitActivation
expect(seen).toEqual([{ directory: "/plans" }, { directory: "/srv/plans" }])
}).pipe(
Effect.scoped,
Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(directory.path) }))),
)
}).pipe(
Effect.ensuring(
Effect.sync(() => {
source.operations = []
}),
),
),
)
it.effect("ignores options from a wildcard selector", () =>
Effect.gen(function* () {
const seen = new Array<Record<string, unknown>>()
const sdk = yield* SdkPlugins.Service
yield* sdk.register(
define({
id: "options-probe",
effect: (ctx) => Effect.sync(() => seen.push(ctx.options)),
}),
)
source.operations = [{ type: "add", target: "*", options: { directory: "/plans" } }]
const directory = yield* tmpdirScoped()
const locations = yield* LocationServiceMap.Service
yield* Effect.gen(function* () {
const plugins = yield* Plugin.Service
yield* plugins.awaitActivation
expect(seen).toEqual([{}])
}).pipe(
Effect.scoped,
Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(directory.path) }))),
)
}).pipe(
Effect.ensuring(
Effect.sync(() => {
source.operations = []
}),
),
),
)
})
+17 -1
View File
@@ -135,7 +135,7 @@ OpenCode includes these visible agents:
| Agent | Mode | Purpose |
| --- | --- | --- |
| **Build** (`build`) | `primary` | Default coding agent. Tools are allowed by default; sensitive environment-file reads and access outside the workspace ask for approval. |
| **Plan** (`plan`) | `primary` | Explores and plans without editing normal project files. It may write OpenCode plan files when asked, and shell commands remain permission-controlled. |
| **Plan** (`plan`) | `primary` | Explores and plans without editing normal project files. It may write plan files to the [plan directory](/agents#plan-directory) when asked, and shell commands remain permission-controlled. |
| **General** (`general`) | `subagent` | Handles research and multi-step work with broad tool access, but cannot launch more subagents. |
| **Explore** (`explore`) | `subagent` | Searches and reads code or web sources without editing files. |
@@ -155,6 +155,22 @@ Override a built-in by using the same ID:
Hidden `compaction`, `title`, and `summary` agents perform maintenance and cannot be selected directly. V2 has no built-in `scout` agent.
### Plan directory
The Plan agent writes plan files to `~/.opencode/plan` and cannot edit other files. Set the `directory` option of the built-in `opencode.plan` plugin to use another directory.
```jsonc
{
"plugins": [{ "package": "opencode.plan", "options": { "directory": ".opencode/plans" } }],
}
```
- Relative paths resolve against the root of the current checkout, in global and project configuration alike. Each linked worktree gets its own plan directory.
- Outside version control, relative paths resolve against the opened directory.
- Absolute paths are used as-is, and `~/` resolves against your home directory.
Changing the directory does not move existing plan files.
## Merging
Agent definitions merge in configuration order. Later scalar values replace earlier values, request maps merge by key, and permission rules append:
@@ -1,98 +0,0 @@
---
title: "Budgets API"
description: "Read effective monthly budgets for every workspace member and manage each member's custom budget."
---
Read effective monthly budgets for every workspace member and manage each member's custom budget from an integration.
## Quick start
Create a [service account](/console/api#service-accounts) API key with **All** permissions in the [Console](https://opencode.ai/console).
Inference-only keys cannot access budget data or change limits.
```bash
export CONSOLE_URL="https://opencode.ai/console"
export SERVICE_API_KEY="oc_sk_..."
```
List member budgets:
```bash
curl --fail-with-body \
"${CONSOLE_URL}/api/v1/budgets/members" \
--header "Authorization: Bearer ${SERVICE_API_KEY}" \
--header "Accept: application/json"
```
## List member budgets
```text
GET /api/v1/budgets/members
```
The response includes every current member, ordered by email. A limit with source `custom` is a member override;
source `default` is inherited from the workspace default. A null limit and source mean the member is unlimited.
```json
[
{
"user_id": "user_...",
"email": "alice@example.com",
"limit_micro_cents": "7525000000",
"spent_micro_cents": "1250000000",
"exceeded": false,
"resets_at": "2026-10-01T00:00:00.000Z",
"source": "custom",
"updated_at": "2026-09-03T12:00:00.000Z"
}
]
```
Monetary response fields are decimal strings in microcents so integrations do not lose precision. There are
100,000,000 microcents per US dollar. Spend and reset timestamps cover the current UTC calendar month. `updated_at` is
null when there is no custom member override.
## Set a custom budget
```text
PUT /api/v1/budgets/members/:user_id
```
Set a $75.25 monthly budget for one member:
```bash
curl --fail-with-body --request PUT \
"${CONSOLE_URL}/api/v1/budgets/members/user_..." \
--header "Authorization: Bearer ${SERVICE_API_KEY}" \
--header "Content-Type: application/json" \
--data '{"budget_dollars":75.25}'
```
`budget_dollars` accepts a non-negative US-dollar amount with at most two decimal places. The PUT is idempotent and
returns HTTP 204. Use the member ID returned by the list operation.
## Return to the workspace default
```text
DELETE /api/v1/budgets/members/:user_id
```
Remove the custom override:
```bash
curl --fail-with-body --request DELETE \
"${CONSOLE_URL}/api/v1/budgets/members/user_..." \
--header "Authorization: Bearer ${SERVICE_API_KEY}"
```
Deleting an override does not delete the member. The member inherits the workspace default budget afterward, or
becomes unlimited when the workspace has no default. Repeating the request is safe and returns HTTP 204.
## Errors
| Status | Meaning |
| ------ | -------------------------------------------------------- |
| `400` | Invalid member ID or budget amount. |
| `401` | Missing, invalid, expired, or revoked service API key. |
| `403` | The API key does not have permission to manage budgets. |
| `404` | The member is not in the workspace bound to the API key. |
@@ -1,166 +0,0 @@
---
title: "API Overview"
description: "Authenticate with the OpenCode Console API using service account keys or user tokens."
---
The Console API lets integrations read workspace configuration and manage budgets. Paths are relative
to the Console base URL.
```bash
export CONSOLE_URL="https://opencode.ai/console"
export SERVICE_API_KEY="oc_sk_..."
curl --fail-with-body "${CONSOLE_URL}/api/v2/config" \
--header "Authorization: Bearer ${SERVICE_API_KEY}"
```
| API | Endpoints |
| ----------------------------------- | ------------------------------------------ |
| [Inference](/console/api/inference) | `https://opencode.ai/inference/...` |
| [Providers](/console/api/providers) | `https://opencode.ai/inference/custom/...` |
| [Budgets](/console/api/budgets) | `/api/v1/budgets/members` |
| [Config](#workspace-config) | `GET /api/v2/config` |
## Service accounts
Integrations authenticate with a service account: a non-human member of the workspace with its own API keys, usage,
and [budget](/console/budgets#service-account-budgets). Owners and admins manage them from **Keys**.
1. Open **Keys** and click **Add Service Account**, then name it, for example `CI Pipeline`.
2. Open the service account and click **Add API Key**.
3. Choose a **Key name**, **Permissions**, and an optional **Expiry date**, then click **Create key**.
4. Copy the key. It is shown only once.
```text
oc_sk_1a2b3c4d5e6f_...
```
Send the key as a bearer token. It is bound to one workspace, so no workspace header is needed.
```text
Authorization: Bearer oc_sk_...
```
### Permissions
| Permission | Allows |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| Inference only | [Inference](/console/api/inference) and [Providers](/console/api/providers) requests, and `GET /api/v2/config`. |
| All | Everything above, plus the Console API with admin access, except managing models. |
Some actions always require a person signed in to the Console, even with an **All** key: inviting members, changing
roles, removing members, and creating or revoking keys.
### Revoke keys
Open the service account and revoke a key from **API Keys**. Removing the service account revokes all its keys.
Automations using a revoked or expired key get HTTP `401` immediately.
## User tokens
User tokens act as a signed-in person with their workspace role. Send the token with an `x-org-id` header naming the
workspace.
```bash
curl --fail-with-body "${CONSOLE_URL}/api/v2/config" \
--header "Authorization: Bearer ${USER_TOKEN}" \
--header "x-org-id: org_..."
```
Tokens from the [device flow](#device-flow) are bound to the workspace chosen during sign-in and do not need the
header. A different `x-org-id` returns `403`.
## Device flow
OpenCode signs in with the OAuth device authorization grant (RFC 8628). Other CLIs can use the same flow.
1. Request a device code. Send `supports_org_scope=true` to bind the token to a workspace.
```bash
curl --fail-with-body "${CONSOLE_URL}/auth/device/code" \
--data "client_id=my-cli" \
--data "supports_org_scope=true"
```
The response includes `device_code`, `user_code`, `verification_uri_complete`, `expires_in` (600 seconds), and
`interval` (5 seconds).
2. Open `verification_uri_complete` in a browser. It is a path such as `/console/device?user_code=...`, relative to
`https://opencode.ai`. The person signs in, picks a workspace, and approves.
3. Poll for the token every `interval` seconds with the same `client_id`.
```bash
curl --fail-with-body "${CONSOLE_URL}/auth/device/token" \
--data "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
--data "device_code=..." \
--data "client_id=my-cli"
```
Until approval the response is `400` with `authorization_pending`. On success it returns `access_token`,
`refresh_token`, `expires_in`, and `org_id`.
4. Refresh before the access token expires. The workspace binding is preserved.
```bash
curl --fail-with-body "${CONSOLE_URL}/auth/device/token" \
--data "grant_type=refresh_token" \
--data "refresh_token=..." \
--data "client_id=my-cli"
```
Each refresh token can be used once. Reusing an old refresh token revokes the whole session.
## Workspace config
`GET /api/v2/config` returns the providers and models available to the workspace in the OpenCode V2
[config](/config) format. OpenCode loads it after `/connect`.
```bash
curl --fail-with-body "${CONSOLE_URL}/api/v2/config" \
--header "Authorization: Bearer ${SERVICE_API_KEY}"
```
```json
{
"providers": {
"opencode": {
"name": "OpenCode",
"env": ["OPENCODE_CONSOLE_TOKEN"],
"package": "aisdk:@ai-sdk/openai-compatible",
"settings": {
"baseURL": "https://opencode.ai/inference/openai/v1",
"apiKey": "{env:OPENCODE_CONSOLE_TOKEN}"
},
"headers": { "x-opencode-org-id": "org_..." },
"models": {
"kimi-k2.6": { "name": "Kimi K2.6" }
}
}
}
}
```
| Field | Description |
| ----------------------- | -------------------------------------------------------------------------------- |
| `providers` | Console, Go, and connected providers, each with its gateway settings and models. |
| `websearch` | Hosted [web search](/console/websearch), for members when enabled. |
| `mcp` | The Console MCP server, for members. |
| `experimental.policies` | Workspace [policy](/policies#console) statements. |
Every key permission level and member role can read the config.
## Errors
Errors return JSON with a `_tag` naming the error.
```json
{ "_tag": "Forbidden" }
```
| Status | Meaning |
| ------ | ---------------------------------------------------------------------------------------- |
| `400` | `OrgRequired`: a user token was sent without `x-org-id`. |
| `401` | Missing, invalid, expired, or revoked credential. |
| `403` | Wrong workspace, missing permission, or an **Inference only** key on a Console endpoint. |
| `404` | The workspace does not exist or was deleted. |
@@ -1,84 +0,0 @@
---
title: "Billing"
description: "Add credits, set up auto-recharge, manage payment methods, and download invoices in OpenCode Console."
---
Console is free for any team size, with no seat fees. You pay only for usage, from prepaid credits that cover the
whole workspace. Owners and admins manage billing in **Settings › Billing**.
Usage through [your own providers](/console/providers) is billed by the provider, not by Console.
## Add a payment method
1. Open **Settings › Billing**.
2. Under **Payment methods**, click **Add**.
3. Choose a method and finish the Stripe setup.
Supported payment methods:
- Card
- Link
- Alipay
- UPI
- WeChat Pay
Use **⋯ › Make default** to choose the method for automatic charges. Removing a method can stop auto-recharge and Go
renewals that use it.
## Add credits
1. Under **Credits**, click **Add credits**.
2. Pick $10, $50, or $100, or choose **Other** and enter an amount.
3. Choose a payment method and click **Confirm and pay**.
A processing fee of 4.6% + $0.31 is added on top, so a $50 purchase costs $52.61 and the full $50 is added to the
balance.
## Auto-recharge
Auto-recharge buys credits when the balance runs low.
1. Under **Auto-recharge**, click **Add rule**.
2. Set the balance that triggers a charge and the amount to add.
3. Click **Save**.
Auto-recharge charges the default payment method, including the processing fee. If a charge fails, auto-recharge
turns off until you set it up again.
## Check your balance
The **Credits** card on **Overview** shows the balance for owners and admins.
## When credits run out
Paid requests fail with HTTP `402` once the balance reaches zero. The balance never goes negative.
```text
Upstream request failed: Insufficient account funds
```
## Invoices
Every payment appears under **Invoices** with its status and a link to **Download invoice** or **View receipt**.
| Entry | Created by |
| ---------------- | ------------------ |
| Balance top-up | Adding credits |
| Automatic top-up | Auto-recharge |
| Go subscription | Subscribing to Go |
| Go renewal | Monthly Go renewal |
| Monthly Usage | Invoiced billing |
## Invoiced billing
Workspaces with a credit agreement can be invoiced monthly instead of buying credits. Contact support to set it up.
- Usage draws on a credit limit instead of a prepaid balance.
- The invoice for each month is issued on the 1st (UTC) and is due 14 days later.
- Open invoices have a **Pay now** button.
- Requests fail with HTTP `402` when the credit limit is reached or an invoice is overdue.
## Go
[OpenCode Go](/console/go) subscriptions use the same payment methods and appear in the same invoice list. Turn on
**Extra usage** on the **Overview** credits card to pay from credits once a Go limit is reached.
@@ -1,86 +1,98 @@
---
title: "Budgets"
description: "Cap monthly spend for a workspace, its members, and its service accounts."
title: "Budgets API"
description: "Read effective monthly budgets for every workspace member and manage each member's custom budget."
---
Budgets cap how much a workspace spends each month. Owners and admins set them from the **Budget limits** panel on
the **Members** and **Keys** pages.
Read effective monthly budgets for every workspace member and manage each member's custom budget from an integration.
## Budget levels
## Quick start
| Budget | Applies to | Where to set it |
| ----------------------- | ------------------------------------------- | -------------------------------------------------- |
| Workspace | Combined spend of everyone in the workspace | **Members** or **Keys › Monthly workspace budget** |
| Default user | Each member without a custom limit | **Members › Monthly default user budget** |
| Custom user | One member | **Members** table or member page |
| Default service account | Each service account without a custom limit | **Keys › Monthly default service account budget** |
| Custom service account | One service account | Service account page |
Create a service account API key with **All** permissions in the [Console](https://opencode.ai/console).
Inference-only keys cannot access budget data or change limits.
A request must fit under both the workspace budget and the person's own budget. Without any budget, spend is
unlimited.
## Workspace budget
The workspace budget blocks requests for everyone once the workspace reaches it.
1. Open **Members** or **Keys**.
2. In **Budget limits**, click **Monthly workspace budget**.
3. Enter a **Workspace limit** in USD and click **Save**.
## Member budgets
The default user budget applies to every member who has no custom limit. Set it under **Monthly default user budget**.
Give one member a custom limit from either place:
- Click the **Limit** cell in their **Members** row and enter an amount.
- Open the member and click **Set User Budget** under **Monthly Budget**.
Custom limits are highlighted in the Members table. Clear a custom limit to return the member to the default; the
member page then shows **Using Default**.
## Service account budgets
Service accounts work the same way. Set **Monthly default service account budget** on the **Keys** page, or open a
service account and click **Set Service Account Budget**.
## Amounts and resets
- Amounts are in US dollars with up to two decimal places.
- Budgets reset on the 1st of each month at 00:00 UTC.
- A `$0` budget blocks all spend that counts toward budgets.
## What counts
| Spend | Counts toward budgets |
| ------------------------------------------------------------------- | ---------------------------- |
| Paid Console models | Yes |
| Models on [your own providers](/console/providers) with a price set | Yes, at the configured price |
| [Web search](/console/websearch) | Yes |
| Free models | No |
| [Go](/console/go) usage within the plan | No |
| Go extra usage paid from credits | Yes |
## When a budget is reached
New requests fail with HTTP `429` until the budget resets or an admin raises it. The response does not say which
budget was reached.
```text
Upstream request failed: Account budget exceeded
```bash
export CONSOLE_URL="https://opencode.ai/console"
export SERVICE_API_KEY="oc_sk_..."
```
Budgets are checked when a request starts. Requests already in progress finish, so spend can end slightly above the
limit.
List member budgets:
## Track spend
```bash
curl --fail-with-body \
"${CONSOLE_URL}/api/v1/budgets/members" \
--header "Authorization: Bearer ${SERVICE_API_KEY}" \
--header "Accept: application/json"
```
Each member sees their own budget on **Overview**: the amount left, the amount spent, and the reset date.
## List member budgets
Owners and admins of workspaces with more than one member also see **Usage**. It breaks down cost by day, model, and
person, and the **Export usage** button downloads daily totals for the last 7, 30, or 90 days as CSV.
```text
GET /api/v1/budgets/members
```
## Automate budgets
The response includes every current member, ordered by email. A limit with source `custom` is a member override;
source `default` is inherited from the workspace default. A null limit and source mean the member is unlimited.
Use the [Budgets API](/console/api/budgets) to read every member's budget and set custom limits from your own
tools.
```json
[
{
"user_id": "user_...",
"email": "alice@example.com",
"limit_micro_cents": "7525000000",
"spent_micro_cents": "1250000000",
"exceeded": false,
"resets_at": "2026-10-01T00:00:00.000Z",
"source": "custom",
"updated_at": "2026-09-03T12:00:00.000Z"
}
]
```
Monetary response fields are decimal strings in microcents so integrations do not lose precision. There are
100,000,000 microcents per US dollar. Spend and reset timestamps cover the current UTC calendar month. `updated_at` is
null when there is no custom member override.
## Set a custom budget
```text
PUT /api/v1/budgets/members/:user_id
```
Set a $75.25 monthly budget for one member:
```bash
curl --fail-with-body --request PUT \
"${CONSOLE_URL}/api/v1/budgets/members/user_..." \
--header "Authorization: Bearer ${SERVICE_API_KEY}" \
--header "Content-Type: application/json" \
--data '{"budget_dollars":75.25}'
```
`budget_dollars` accepts a non-negative US-dollar amount with at most two decimal places. The PUT is idempotent and
returns HTTP 204. Use the member ID returned by the list operation.
## Return to the workspace default
```text
DELETE /api/v1/budgets/members/:user_id
```
Remove the custom override:
```bash
curl --fail-with-body --request DELETE \
"${CONSOLE_URL}/api/v1/budgets/members/user_..." \
--header "Authorization: Bearer ${SERVICE_API_KEY}"
```
Deleting an override does not delete the member. The member inherits the workspace default budget afterward, or
becomes unlimited when the workspace has no default. Repeating the request is safe and returns HTTP 204.
## Errors
| Status | Meaning |
| ------ | -------------------------------------------------------- |
| `400` | Invalid member ID or budget amount. |
| `401` | Missing, invalid, expired, or revoked service API key. |
| `403` | The API key does not have permission to manage budgets. |
| `404` | The member is not in the workspace bound to the API key. |
@@ -1,11 +1,11 @@
---
title: "Providers API"
description: "Call the providers connected to your workspace through the Console gateway with a Console key."
title: "BYOK"
description: "Call your own provider connections through the Console gateway with a Console key."
---
Every [provider](/console/providers) connected to your workspace is served through the Console gateway at its own
URL. OpenCode uses it automatically after `/connect`; call it directly to use the same providers from your own tools.
Console adds the provider credential before forwarding, so the provider secret never leaves the workspace.
Bring your own key (BYOK) connects a provider account you already pay for to your workspace. Console stores the
provider credential and gives each connection one gateway URL. Members and integrations call that URL with a Console
key and never handle the provider secret.
```bash
curl -X POST "https://opencode.ai/inference/custom/conn_.../chat/completions" \
@@ -17,18 +17,29 @@ curl -X POST "https://opencode.ai/inference/custom/conn_.../chat/completions" \
}'
```
## Provider URL
## Connect a provider
Copy the **Console API URL** from the provider's page under **Providers**.
1. In the [Console](https://opencode.ai/console), open **Providers** and click **Connect Provider**.
2. Pick a provider from the catalog and paste its API key, or click **Add Custom Provider** for any other endpoint.
3. Copy the **Console API URL** from the provider page.
```text
https://opencode.ai/inference/custom/<connection_id>
```
Catalog providers come with their models preconfigured. Custom providers need a few more fields:
| Field | Description |
| ---------- | --------------------------------------------------------------------------------- |
| Base URL | Where Console forwards requests, for example `https://api.deepseek.com/v1`. |
| API schema | Chat Completions, Responses API, Anthropic Compatible, or Google Generative AI. |
| Auth mode | Bearer, API key header, or Custom header, plus the credential to send. |
| Models | The model IDs members can request, with optional pricing used for usage tracking. |
## Authentication
Replace `<token>` with a [service account key](/console/api#service-accounts) created in Console. Console swaps it for
the connection's credential before forwarding.
Replace `<token>` with a service account key created in Console. Console swaps it for the connection's credential
before forwarding, so the provider secret stays in the workspace.
```text
Authorization: Bearer <token>
@@ -42,7 +53,7 @@ Console API URL.
Append the provider's API path to the Console API URL. Console forwards the request to the connection's base URL and
returns the provider response unchanged.
| Format | Path |
| API schema | Path |
| -------------------- | --------------------------------- |
| Chat Completions | `/chat/completions` |
| Responses API | `/responses` |
@@ -105,7 +116,16 @@ curl -X POST "https://opencode.ai/inference/custom/conn_.../models/gemini-3.1-pr
}'
```
## Usage
## Use in OpenCode
Requests count toward [budgets](/console/budgets) using the pricing set on each model. The provider
bills you for the tokens.
Members do not need the gateway URL. Connections appear as providers in OpenCode for everyone who connects to the
workspace with `/connect`, and `/models` lists their enabled models.
```text
/connect
```
## Usage and limits
The provider bills you for the tokens. Console records every request and, using the pricing configured on the model,
counts it toward workspace and member monthly limits.
+6 -7
View File
@@ -5,9 +5,9 @@ description: "Reliable access to open coding models with two usage tiers."
OpenCode Go gives you reliable access to popular open coding models, with two monthly plans:
| Plan | Price | Included usage |
| ----------- | ------------- | ------------------------------------------- |
| **Go** | **$10/month** | Lower-cost access to the models below |
| Plan | Price | Included usage |
| ---- | ----- | -------------- |
| **Go** | **$10/month** | Lower-cost access to the models below |
| **Go Plus** | **$40/month** | Higher usage limits across the models below |
Go works like any other provider in OpenCode. You subscribe to OpenCode Go and get your API key. It's **completely optional** and you don't need it to use OpenCode.
@@ -29,10 +29,7 @@ The service is designed primarily for international users and provides stable gl
/models
```
<Callout>
Go and Go Plus are for workspaces with a single member, such as the **Personal** workspace created when you sign up. A
workspace with a Go subscription cannot invite members.
</Callout>
<Callout>Only one member per workspace can subscribe to OpenCode Go or Go Plus.</Callout>
The current list of models includes:
@@ -69,6 +66,7 @@ The current list of models includes:
The list of models may change as we test and add new ones.
## Where can I use it?
OpenCode Go is designed for [OpenCode](https://opencode.ai) and other coding agents
@@ -345,6 +343,7 @@ If you also have credits in your Console balance, you can enable the **Use balan
option in the console. When enabled, Go will fall back to your [pay-as-you-go balance](/console/models#pricing)
after you've reached your usage limits instead of blocking requests.
### Why some models have lower usage
With Go, the included monthly usage varies by model.
@@ -3,29 +3,14 @@ title: "Intro"
---
The [OpenCode Console](https://opencode.ai/console) is an optional service that provides additional benefits to
using OpenCode, on your own or as a team.
using OpenCode particularly as a team
- [Workspaces](/console/workspaces) for sharing access with your [team](/console/members)
- [Inference](/console/models) for both proprietary and open source models
- [Providers](/console/providers) for connecting your own provider accounts through the Console gateway
- Usage tracking and [budget](/console/budgets) controls
- Inference for both proprietary and open source models
- LLM Gateway for connecting your own providers
- Usage tracking and budget controls
- Deploy team wide policies to control OpenCode behavior
- [Web search](/console/websearch)
- [OpenCode Go and Go Plus](/console/go), $10 and $40 monthly subscriptions for open source model access
## Get started
You don't need a team to use the Console. Every account starts with a **Personal** workspace where you are the only
member.
1. Sign in to the [Console](https://opencode.ai/console).
2. Subscribe to [Go](/console/go), add [credits](/console/billing#add-credits), or connect a [provider](/console/providers).
3. Run `/connect` in OpenCode and choose the workspace.
4. Optionally, [invite](/console/members#invite-members) your team.
```text
/connect
```
- Web search
- OpenCode Go, $10 subscription for open source model access
## Policies
@@ -18,7 +18,7 @@ curl -X POST "https://opencode.ai/inference/openai/v1/chat/completions" \
## Authentication
Replace `<token>` with a [service account key](/console/api#service-accounts) created in the [Console](https://opencode.ai/console). Paid models
Replace `<token>` with a service account key created in the [Console](https://opencode.ai/console). Paid models
require the header; free chat models can be called without it.
```text
@@ -1,67 +0,0 @@
---
title: "Members"
description: "Invite people to an OpenCode Console workspace, manage their roles, and remove them."
---
Owners and admins manage people from the **Members** page. Each row shows the member's role, join date, monthly
usage, and budget limit.
## Invite members
1. Open **Members** and click **Invite Members**.
2. Add one or more email addresses. Separate them with spaces, commas, or semicolons, or paste a list.
3. Click **Send invitations**.
Each person receives an email titled **Join Acme Inc. on OpenCode** with a **Join workspace** button.
Invitations expire after 7 days. Everyone joins as **Member**; [change their role](#change-a-role) after they join.
<Callout>
Workspaces with an [OpenCode Go](/console/go) subscription cannot invite members. Create a separate workspace for your
team.
</Callout>
Members can also join automatically:
- [SSO](/console/sso#automatic-membership) adds people on your verified email domains when they first sign in.
- [Directory sync](/console/scim) adds and removes people as you assign them in your identity provider.
## Accept an invitation
The invitee must sign in with the email address the invitation was sent to. There are two ways to accept:
- Click **Join workspace** in the invitation email, then sign in or create an account.
- Sign in to the Console, open the workspace menu, and click **Join** under **Pending invitations**.
If they are signed in with a different account, Console asks them to **Sign out and continue** with the invited email.
## Manage pending invitations
Pending invitations appear in the Members table as **Pending · Expires** followed by the date, or **Expired**.
| Action | How |
| ------ | ----------------------------------------------------------------------------------------- |
| Resend | **⋯ › Resend invitation**. Sends a new link valid for 7 days; the old link stops working. |
| Revoke | **⋯ › Remove...**. The link stops working immediately. |
## Change a role
Open the role menu in a member's row and pick **Member**, **Admin**, or **Owner**.
You can only grant roles up to your own, and the workspace always keeps at least one owner. See
[Roles](/console/workspaces#roles).
## Remove a member
1. Click **⋯ › Remove...** in the member's row.
2. Confirm with **Remove member**.
Access is revoked immediately, and you can invite them again later. Their past usage stays in the workspace's reports.
Service accounts and their API keys belong to the workspace, so removing the member who created them does not revoke
them. Revoke keys from **Keys** if needed. See [Service accounts](/console/api#service-accounts).
## Member details
Click a member to see their monthly budget, models and clients they use, usage charts, and request logs. From here
you can set their [budget](/console/budgets#member-budgets) and export their usage as CSV.
@@ -381,9 +381,33 @@ For subscription models, see [Go privacy](/console/go#privacy).
## For teams
Share Console models with your team from a [workspace](/console/workspaces). Owners and admins can
[invite members](/console/members), choose which models are enabled from the **Models** tab, set
[budgets](/console/budgets), and connect [their own providers](/console/providers).
You can invite teammates, assign roles, and curate the models your team uses.
<Callout>
Managing workspaces is currently free for teams as part of the beta. More pricing details will be shared later.
</Callout>
### Roles
Invite teammates to your workspace and assign roles:
- **Admin:** Manage models, members, API keys, and billing.
- **Member:** Manage only their own API keys.
Admins can also set monthly spending limits for each member to keep costs under control.
### Model access
Admins can enable or disable specific models for the workspace. Requests made to a disabled model return an error.
For example, you can disable a model that collects data so members of your workspace cannot use it.
### Bring your own key
You can use your own OpenAI or Anthropic API keys while still accessing other models through Console. Tokens used
with your own keys are billed directly by the provider.
For example, if your organization already has an OpenAI API key, you can use it instead of the one Console provides.
## Background
@@ -1,124 +0,0 @@
---
title: "Providers"
description: "Connect your own model provider accounts to an OpenCode Console workspace and choose which models members can use."
---
Connect provider accounts you already pay for, such as Anthropic, OpenAI, or AWS Bedrock, and share their models
with everyone in the workspace. Console stores the credential and serves each provider through its gateway, so members
use the models without ever seeing the provider key. The same gateway is available to your own tools through the
[Providers API](/console/api/providers).
Owners and admins manage providers from the **Providers** tab. Members get the enabled models in OpenCode after
running `/connect`.
## Connect a provider
1. Open **Providers** and click **Connect Provider**.
2. Pick a provider from the catalog and enter its API key.
3. Optionally click **Test connection**, then click **Save**.
The catalog includes Anthropic, OpenAI, Google, xAI, DeepSeek, Mistral, Groq, OpenRouter, and many more. Catalog
providers come with their models preconfigured.
### Cloud providers
Cloud providers need a few extra details.
| Provider | Details |
| --------------------- | ------------------------------------- |
| Azure | Resource name and API key |
| AWS Bedrock | Region and API key |
| Google Vertex AI | Region and service account JSON |
| Cloudflare AI Gateway | Account ID, Gateway ID, and API token |
| Databricks | Workspace URL and token |
### Custom providers
Click **Add Custom Provider** to connect any other endpoint, then add its models with **Add Model**.
| Field | Description |
| ---------- | ------------------------------------------------------------------------------- |
| Base URL | Where Console forwards requests, for example `https://api.deepseek.com/v1`. |
| Format | Chat Completions, Responses API, Anthropic Compatible, or Google Generative AI. |
| Auth mode | Bearer, API key header, or Custom header. |
| Credential | The key Console sends to the provider. |
## Choose models
Open a provider to see its models. Only enabled models are available to members.
- Use the **Enabled** toggle to turn a model on or off.
- Turn on **New models** to enable models the provider adds in the future.
## Edit models
Click a model to edit it. Changes are saved when you close the panel.
| Section | What you can change |
| ------------- | --------------------------------------------------------------- |
| General | Display name, API ID, tools support, input and output types |
| Variants | Variants with their own headers or body |
| Configuration | Default variant, request headers and body, token limits |
| Cost Tiers | Input, output, cache read, and cache write prices per 1M tokens |
### Custom pricing
Catalog models start with the provider's list price. If you have negotiated pricing, for example a discount on your
OpenAI contract, override the prices under **Cost Tiers** so usage reports and [budgets](/console/budgets) match what
you actually pay.
| Field | List price | Your price |
| ---------------- | ---------- | ---------- |
| Input price | $2.50 | $2.00 |
| Output price | $15.00 | $12.00 |
| Cache read price | $0.25 | $0.20 |
Prices you change are highlighted on the provider page. Use **Restore default value** on a field, or **⋯ › Restore...**
on the model, to return to the list price.
Add a **Context** tier when a model charges more above a context size, for example for prompts over 200K tokens.
### Custom models
Click **Add Model** to add a model the catalog doesn't include, with its **Display name** and **Model ID**. Use
**⋯ › Duplicate...** on an existing model to start from its settings.
## OpenCode models
The **Models** tab manages the [models](/console/models) Console provides, which are paid from workspace credits. It
works like a provider page:
- Turn individual models on or off.
- Turn **Free models** off to block free models for the workspace.
- Turn **New models** on to enable new Console models automatically.
Disable the whole provider from **⋯ › Disable** to use only your own providers.
## Use in OpenCode
Members run `/connect` and choose the workspace. Each enabled provider appears as the workspace name followed by the
provider name, and `/models` lists its enabled models.
```text
Acme Inc. / Anthropic
```
Turn on **Managed providers only** in **Settings › General** to restrict OpenCode to the workspace's providers for
everyone.
## Disable or remove
Open the provider's **⋯** menu:
- **Disable** hides the provider and its models from members but keeps its settings.
- **Remove...** deletes the provider and its models. The dialog shows who used it recently, and offers
**Disable instead**.
To rotate a key, click the credential on the provider page and enter the new one.
## Billing
The provider bills you for tokens; Console does not charge credits for them. Console still records each request and
its cost using the model's pricing, so usage reports and [budgets](/console/budgets) include it.
To call a provider from your own tools instead of OpenCode, use the [Providers API](/console/api/providers).
@@ -1,91 +0,0 @@
---
title: "Directory Sync"
description: "Add and remove workspace members automatically from Okta, Microsoft Entra ID, or any SCIM 2.0 identity provider."
---
Directory sync (SCIM 2.0) adds people to the workspace when you assign them in your identity provider, and removes
their access when you unassign them. Roles stay managed in the Console.
```text
https://opencode.ai/console/scim/v2
```
## Before you start
- Set up [SSO](/console/sso) for the workspace.
- [Verify](/console/sso#verify-email-domains) every email domain your people sign in with. Directory sync only adds
people on a verified domain; invite anyone else from [Members](/console/members).
## Create a token
1. Open **Settings › Security** and click **Set up** under **Directory sync**.
2. Copy the **SCIM base URL** and the token. The token is shown only once, so store it in a secrets manager.
Your identity provider sends the token on every request.
```text
Authorization: Bearer <token>
```
## Connect your identity provider
### Okta
1. Add the **SCIM 2.0 Test App (Header Auth)** app.
2. Set the base URL, and enter `Bearer ` followed by the token in **API Token**.
3. Enable **Create Users**, **Update User Attributes**, and **Deactivate Users**. Leave **Import Groups** and password
sync off.
### Microsoft Entra ID
1. In your enterprise app, open **Provisioning** and set the mode to **Automatic**.
2. Enter the base URL as **Tenant URL** and the token as **Secret Token**.
3. Assign users and groups, then start provisioning.
### Other providers
Use a SCIM 2.0 app with header token authentication, pointed at the base URL. Console supports the `/Users` and
`/Groups` endpoints.
Assign one test person first. **Directory sync** shows **Connected to your identity provider** after the first
request.
## What syncs
| In your identity provider | In the Console |
| ------------------------- | --------------------------------------------------------------------------------- |
| Assign a person | Added as **Member**. Existing members keep their role. |
| Update a name | Profile updated. |
| Unassign or deactivate | Access suspended and shown as **Deactivated**. API keys they created are revoked. |
| Reassign | Access restored. Revoked keys stay revoked. |
| Delete | Removed from the workspace. Their account and other workspaces are unaffected. |
- Roles and email addresses are not changed by directory sync.
- The last owner cannot be deactivated or deleted.
- Workspaces with a [Go](/console/go) subscription cannot add members through directory sync.
## Groups
Push groups to tag their members, for example with Okta **Push Groups** or Entra group assignment. Assign the people
before pushing their groups.
```text
Platform Admins → platform-admins
```
Each group tags its members with a tag named after the group. Under **Settings › Security › Group tags** you can
rename a tag, point several groups at one tag, or stop tagging a group. Leaving a group removes its tag.
## Rotate the token
1. Click **New token** and install it in your identity provider.
2. Wait until the new token shows a last use.
3. Revoke the old token.
Revoking a token stops future updates but does not change existing members. Deleting the SSO connection revokes all
tokens.
## SSO and directory sync
Once a workspace has issued a token, SSO no longer [adds members automatically](/console/sso#automatic-membership).
The directory decides who belongs, even if the token is later revoked.
@@ -1,91 +0,0 @@
---
title: "SSO"
description: "Sign in to an OpenCode Console workspace through your identity provider with OIDC or SAML."
---
Single sign-on (SSO) sends workspace members through your company's identity provider when they sign in. Owners and
admins set it up in **Settings › Security**, and it's included with every workspace.
| Protocol | Providers |
| -------------- | -------------------------------------------------------------------------------------- |
| OpenID Connect | Google Workspace, Microsoft Entra ID, Okta, Auth0, Ping Identity, or any OIDC provider |
| SAML 2.0 | Google Workspace, Microsoft Entra ID, Okta, or any SAML provider |
Each workspace has one SSO connection.
## Set up OIDC
1. Open **Settings › Security**, click **Configure SSO**, and choose **OpenID Connect** and your provider.
2. Create an app in your provider by following the steps shown, with the `openid`, `email`, and `profile` scopes.
3. Enter the **Issuer URL**, or your tenant or domain, plus the **Client ID**, **Client Secret**, and a
**Display Name**, then click **Save**.
4. Copy the **Callback URL** from the connection into your provider's allowed redirect URIs.
```text
https://opencode.ai/console/auth/sso/ssoconn_.../callback
```
Console checks that the issuer is reachable when you save. SSO is active as soon as the connection is saved.
## Set up SAML
1. Open **Settings › Security**, click **Configure SSO**, and choose **SAML 2.0** and your provider.
2. Copy the **ACS URL** and **SP Entity ID** into your provider. Custom providers can use the **SP Metadata URL** or
download the XML instead.
3. Add your provider's metadata by URL, XML upload, or paste. You can also enter the **IdP Entity ID**, **IdP SSO
URL**, and **IdP Certificate** by hand.
4. Review the details and click **Activate SSO**.
```text
ACS URL: https://opencode.ai/console/auth/sso/ssoconn_.../callback
SP Entity ID: https://opencode.ai/console/auth/sso/ssoconn_.../metadata
```
Your provider must sign the response or the assertion, and send the user's email as an `email` attribute or as the
NameID.
## Verify email domains
People who sign in to the [Console](https://opencode.ai/console) with an email on a verified domain are sent to your
identity provider. Once SSO is active, click **Add domain** under **Email domains**, enter your domain, and publish
the TXT record shown.
```text
Name: _opencode-console-challenge.example.com
Type: TXT
Value: oc-verify=...
```
Click **Verify Domain** once the record is published. DNS changes can take a while to propagate. Each domain can
belong to only one workspace.
## SSO enforcement
Once a workspace has SSO, admins and members must sign in through it to open the workspace, and Console redirects
them automatically. Connecting OpenCode with `/connect` follows the same rule.
Owners can also sign in without SSO, so a misconfigured provider cannot lock the workspace out of its settings.
## Automatic membership
People on a verified domain join the workspace as **Member** the first time they sign in through SSO. They are not
added automatically when:
- They have a pending invitation. The invitation is used instead.
- The workspace uses [directory sync](/console/scim), which decides membership.
- The workspace has a [Go](/console/go) subscription.
Invitations to an SSO workspace send the invitee to your identity provider when they open the link.
## Existing accounts
If someone already has a Console account with the same email, Console asks them to sign in to it once to link SSO.
Invited people and people added by directory sync are linked automatically.
## Edit or delete
Click **Edit** on the connection to change its details. Leave the secret or certificate blank to keep the current
one. To switch between OIDC and SAML, delete the connection and create a new one.
Delete the connection from **⋯ › Delete...**. Members go back to their other sign-in methods, and directory sync stops
with its tokens revoked. Email domains are kept.
@@ -1,9 +1,9 @@
---
title: "Web Search"
description: "Hosted web search for OpenCode through Console."
title: "Websearch"
description: "Hosted websearch for OpenCode through Console."
---
Console provides hosted web search to connected OpenCode v2 users. A workspace owner or admin enables it once for the workspace, without giving members a separate search provider key.
Console provides hosted Websearch to connected OpenCode v2 users. A workspace owner or admin enables it once for the workspace, without giving members a separate search provider key.
Each successful search costs **$0.01** and counts toward the workspace balance and member spending limits.
@@ -37,8 +37,8 @@ See the [Websearch guide](/websearch) to configure permissions or disable the to
Console charges **$0.01** after a search returns a valid result set. Failed and rate-limited searches are not charged.
Web search appears separately from model usage in **Usage**, **My Activity**, member activity, invoices, and CSV exports. Workspace and member monthly spending limits include web search charges.
Websearch appears separately from model usage in **Usage**, **My Activity**, member activity, invoices, and CSV exports. Workspace and member monthly spending limits include Websearch charges.
## Privacy
Hosted web search has zero data retention. Console and its hosted search provider process each query and its results without storing their contents.
Hosted Websearch has zero data retention. Console and its hosted search provider process each query and its results without storing their contents.
@@ -1,87 +0,0 @@
---
title: "Basics"
description: "Create, switch, rename, and delete OpenCode Console workspaces, and understand member roles."
---
A workspace holds everything in the [Console](https://opencode.ai/console): members, credits, budgets, providers,
policies, and service accounts. Usage and billing are tracked per workspace.
A workspace can have a single member. You can use credits, [your own providers](/console/providers), budgets, and [Go](/console/go) on your own,
and [invite members](/console/members) only when you need to.
## Your first workspace
Console creates a workspace named **Personal** when you sign up, with you as its owner and only member. If you sign
up from an invitation, you join the inviting workspace instead and no Personal workspace is created.
## Create a workspace
1. Open the workspace menu in the top bar.
2. Click **Create Workspace**.
3. Enter a **Workspace name** and click **Create**.
You become the owner of the new workspace.
## Switch workspaces
Pick another workspace from the workspace menu. Console keeps you on the same page, so switching from
`/acme/members` opens the other workspace's Members page.
Pending invitations appear at the top of the same menu. See [Members](/console/members#accept-an-invitation).
## Connect OpenCode
Run `/connect` in OpenCode and choose the workspace in the browser. OpenCode then uses that workspace's models,
policies, and budgets.
```text
/connect
```
Run `/connect` again to switch OpenCode to another workspace. Workspace [policies](/policies#console) are replaced
with the new workspace's, and cleared when you disconnect.
## Roles
Every member has one role. New members always join as **Member**.
| Role | Can do |
| ------ | -------------------------------------------------------------------------------------------------------------------- |
| Member | Use OpenCode, see their own usage, logs, and budget. |
| Admin | Everything a member can, plus manage members, billing, budgets, providers, policies, service accounts, and settings. |
| Owner | Everything an admin can, plus grant the Owner role and delete the workspace. |
Members only see **Overview**, **Logs**, **Leaderboard** when it is visible to members, and **Settings › Account**.
### Role rules
- A workspace can have several owners, and always keeps at least one.
- You cannot assign a role higher than your own, so only owners can make someone an owner.
- You cannot change your own role.
### Transfer ownership
1. As an owner, open **Members** and set the new owner's role to **Owner**.
2. Ask the new owner to change your role to **Admin** or **Member**.
## Delete a workspace
Only owners can delete a workspace, and only when they belong to at least one other workspace.
1. Open **Settings › General** and click **Delete workspace**.
2. Tick every acknowledgment.
3. Click **Delete workspace** to confirm.
Deletion is immediate and cannot be undone:
- Members lose access right away.
- All service account API keys and pending invitations are revoked.
- Auto-recharge stops.
- An active Go subscription ends.
- Any remaining credit balance is forfeited.
<Callout type="warning">Deletion is blocked while a payment is processing. Try again in a few minutes.</Callout>
## Leave a workspace
Members cannot remove themselves. Ask an owner or admin to [remove you](/console/members#remove-a-member).
@@ -213,7 +213,7 @@ Shipped agents append these policies:
| Agent | Additional policy |
| ------------ | ------------------------------------------------------------------------------ |
| `build` | Allows questions |
| `plan` | Allows questions; denies edits except files under `~/.opencode/plan` |
| `plan` | Allows questions; denies edits except files in the [plan directory](/agents#plan-directory) (default `~/.opencode/plan`) |
| `general` | Denies questions and launching subagents |
| `explore` | Denies everything except reads, globs, grep, web fetches, and web searches; asks for external directories and `.env` reads |
| `title` | Denies all actions |
@@ -81,6 +81,15 @@ Two built-in plugins ignore removals so that a repository cannot switch off
`opencode.provider.opencode`, the Console connection that delivers organization
policy.
To pass options to a built-in plugin, use the object form with the plugin ID as `package`. A later entry for the same
ID replaces its options. Wildcards enable plugins but never set options.
```jsonc title="opencode.jsonc"
{
"plugins": [{ "package": "opencode.plan", "options": { "directory": ".opencode/plans" } }]
}
```
## Manage
Install, list, check, update, or remove global package plugins with the CLI.
+4 -17
View File
@@ -137,29 +137,16 @@ export const docsSections: DocsSection[] = [
items: [
{ title: "Intro", slug: "console" },
{ title: "Models", slug: "console/models" },
{ title: "Providers", slug: "console/providers" },
{ title: "Web Search", slug: "console/websearch" },
{ title: "Websearch", slug: "console/websearch" },
{ title: "Go", slug: "console/go" },
],
},
{
title: "Workspace",
items: [
{ title: "Basics", slug: "console/workspaces" },
{ title: "Members", slug: "console/members" },
{ title: "SSO", slug: "console/sso" },
{ title: "SCIM", slug: "console/scim" },
{ title: "Budgets", slug: "console/budgets" },
{ title: "Billing", slug: "console/billing" },
],
},
{
title: "API",
items: [
{ title: "Overview", slug: "console/api" },
{ title: "Inference", slug: "console/api/inference" },
{ title: "Providers", slug: "console/api/providers" },
{ title: "Budgets", slug: "console/api/budgets" },
{ title: "Inference", slug: "console/inference" },
{ title: "BYOK", slug: "console/byok" },
{ title: "Budgets", slug: "console/budgets" },
],
},
],
@@ -1,5 +0,0 @@
---
export const prerender = false
return Astro.redirect(`${import.meta.env.BASE_URL}docs/console/api/inference/`, 301)
---