mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-26 21:25:17 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f36db6c114 | |||
| fb5726baa8 | |||
| 71751ac339 | |||
| c6364f4b2e |
@@ -197,7 +197,7 @@ const layer = Layer.effect(
|
||||
start: location.directory,
|
||||
stop: location.project.directory,
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
// We load certain files from a few other folders in the ecosystem
|
||||
const claude = [
|
||||
@@ -235,7 +235,7 @@ const layer = Layer.effect(
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
return {
|
||||
entries: [...claude, ...agents, ...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()],
|
||||
directories,
|
||||
directories: [...directories, ...claude.map((entry) => entry.path), ...agents.map((entry) => entry.path)],
|
||||
files: directPaths,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export const OpencodeContent = opencodeContent
|
||||
export const ReportContent = reportContent
|
||||
|
||||
export const OpencodeDescription =
|
||||
"Use this skill for any question about OpenCode itself, including how OpenCode works, using or configuring it, migrating from V1 to V2, troubleshooting it, developing plugins or integrations, using the OpenCode SDK, clients, server, or API, and contributing to the OpenCode codebase. Also use it for OpenCode agents, commands, skills, tools, permissions, MCP servers, providers, models, themes, keybinds, formatters, the CLI, TUI, desktop app, and web app."
|
||||
"Use this skill for any question about OpenCode itself, including how OpenCode works, using or configuring it, troubleshooting it, developing plugins or integrations, using the OpenCode SDK, clients, server, or API, and contributing to the OpenCode codebase. Also use it for OpenCode agents, commands, skills, tools, permissions, MCP servers, providers, models, themes, keybinds, formatters, the CLI, TUI, desktop app, and web app."
|
||||
const REPORT_DESCRIPTION =
|
||||
"Use when the user wants to report an opencode issue or bug. Collect standard diagnostics, add user-specific reproduction context, and publish the issue with GitHub CLI."
|
||||
|
||||
|
||||
@@ -37,23 +37,6 @@ settings when editing an existing file.
|
||||
See the [full configuration guide](https://opencode.mintlify.site/config) for
|
||||
every field, examples, config locations, and links to dedicated feature guides.
|
||||
|
||||
## V1 to V2 migration
|
||||
|
||||
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://opencode.mintlify.site/migrate-v1) before acting. In
|
||||
the repository, its source is `packages/docs/migrate-v1.mdx`.
|
||||
|
||||
V1 config files and `.opencode/` definitions are intended to remain compatible.
|
||||
The only intentional breaking changes are the server API and plugin API. Native
|
||||
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. If
|
||||
the request includes a V1 plugin, explain that its API is not finalized and do
|
||||
not attempt migration yet. Once the V2 plugin API is finalized, OpenCode should
|
||||
be able to migrate most plugins. If non-API V1 functionality fails in V2, use
|
||||
the `report` skill to file it as a compatibility bug.
|
||||
|
||||
## Service
|
||||
|
||||
OpenCode uses a client-server architecture. Interfaces such as the TUI connect
|
||||
|
||||
@@ -62,6 +62,7 @@ type Dependencies = {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
}
|
||||
readonly config: Settings
|
||||
readonly models: SessionRunnerModel.Interface
|
||||
}
|
||||
|
||||
export type AutoInput = {
|
||||
@@ -77,6 +78,17 @@ type CompactInput = {
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}
|
||||
|
||||
type Selection = {
|
||||
readonly head: string
|
||||
readonly recent: string
|
||||
}
|
||||
|
||||
type FailureTarget = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}
|
||||
|
||||
export type ManualInput = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
@@ -103,7 +115,7 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
const serialize = (message: SessionMessage.Info) => {
|
||||
const serializeMessage = (message: SessionMessage.Info) => {
|
||||
if (message.type === "user") {
|
||||
const files =
|
||||
message.files?.map(
|
||||
@@ -136,7 +148,7 @@ const serialize = (message: SessionMessage.Info) => {
|
||||
return ""
|
||||
}
|
||||
|
||||
const settings = (documents: readonly Config.Entry[]) => {
|
||||
const resolveSettings = (documents: readonly Config.Entry[]) => {
|
||||
const configured = documents
|
||||
.filter((entry): entry is Config.Document => entry.type === "document")
|
||||
.flatMap((entry) => (entry.info.compaction ? [entry.info.compaction] : []))
|
||||
@@ -150,13 +162,13 @@ const settings = (documents: readonly Config.Entry[]) => {
|
||||
)
|
||||
}
|
||||
|
||||
const select = (
|
||||
const selectCompactionContext = (
|
||||
messages: readonly SessionMessage.Info[],
|
||||
tokens: number,
|
||||
): { readonly head: string; readonly recent: string } | undefined => {
|
||||
): Selection | undefined => {
|
||||
const conversation = messages
|
||||
.filter((message) => message.type !== "compaction")
|
||||
.map(serialize)
|
||||
.map(serializeMessage)
|
||||
.filter(Boolean)
|
||||
if (conversation.length === 0) return undefined
|
||||
let total = 0
|
||||
@@ -183,6 +195,15 @@ const select = (
|
||||
}
|
||||
}
|
||||
|
||||
const findCompletedSummary = (messages: readonly SessionMessage.Info[]) =>
|
||||
messages.find(
|
||||
(message): message is SessionMessage.CompactionCompleted =>
|
||||
message.type === "compaction" && message.status === "completed",
|
||||
)
|
||||
|
||||
const resolveOutputLimit = (request: LLMRequest) =>
|
||||
request.generation?.maxTokens ?? request.model.route.defaults.limits?.output ?? 0
|
||||
|
||||
export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) =>
|
||||
[
|
||||
input.previousSummary
|
||||
@@ -194,6 +215,13 @@ export const buildPrompt = (input: { readonly previousSummary?: string; readonly
|
||||
|
||||
const make = (dependencies: Dependencies) => {
|
||||
const config = dependencies.config
|
||||
const publishFailure = (target: FailureTarget, error: SessionError.Error) =>
|
||||
dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: target.sessionID,
|
||||
reason: target.reason,
|
||||
error,
|
||||
inputID: target.inputID,
|
||||
})
|
||||
const compact = Effect.fn("SessionCompaction.compact")(function* (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly model: Model
|
||||
@@ -216,7 +244,7 @@ const make = (dependencies: Dependencies) => {
|
||||
|
||||
const chunks: string[] = []
|
||||
let failure: SessionError.Error | undefined
|
||||
const summarized = yield* dependencies.llm
|
||||
yield* dependencies.llm
|
||||
.stream(
|
||||
LLM.request({
|
||||
model: input.model,
|
||||
@@ -241,32 +269,26 @@ const make = (dependencies: Dependencies) => {
|
||||
}
|
||||
return Effect.void
|
||||
}),
|
||||
Effect.as(true),
|
||||
Effect.catchTag("LLM.Error", (error) =>
|
||||
Effect.sync(() => {
|
||||
failure = toSessionError(error)
|
||||
return false
|
||||
}),
|
||||
),
|
||||
Effect.onInterrupt(() =>
|
||||
input.reason === "auto"
|
||||
? dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
||||
inputID: input.inputID,
|
||||
? publishFailure(input, {
|
||||
type: "compaction.interrupted",
|
||||
message: "Compaction was interrupted",
|
||||
})
|
||||
: Effect.void,
|
||||
),
|
||||
)
|
||||
const summary = chunks.join("")
|
||||
if (!summarized || failure || !summary.trim()) {
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
error: failure ?? { type: "compaction.failed", message: "Compaction produced no summary" },
|
||||
inputID: input.inputID,
|
||||
})
|
||||
if (failure || !summary.trim()) {
|
||||
yield* publishFailure(
|
||||
input,
|
||||
failure ?? { type: "compaction.failed", message: "Compaction produced no summary" },
|
||||
)
|
||||
return false
|
||||
}
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||
@@ -277,24 +299,21 @@ const make = (dependencies: Dependencies) => {
|
||||
})
|
||||
return true
|
||||
})
|
||||
const compactAvailable = Effect.fn("SessionCompaction.compactAvailable")(function* (
|
||||
const compactSelected = Effect.fn("SessionCompaction.compactSelected")(function* (
|
||||
input: CompactInput & {
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly output?: number
|
||||
},
|
||||
selected: Selection,
|
||||
) {
|
||||
const selected = select(input.messages, config.tokens)
|
||||
if (!selected) return false
|
||||
const previousSummary = input.messages.find(
|
||||
(message) => message.type === "compaction" && message.status === "completed",
|
||||
)
|
||||
const previousSummary = findCompletedSummary(input.messages)
|
||||
const summarizeRecent = selected.head.length === 0
|
||||
const previousRecent = previousSummary?.type === "compaction" ? previousSummary.recent : ""
|
||||
const previousRecent = previousSummary?.recent ?? ""
|
||||
return yield* compact({
|
||||
sessionID: input.sessionID,
|
||||
model: input.model,
|
||||
reason: input.reason,
|
||||
previousSummary: previousSummary?.type === "compaction" ? previousSummary.summary : undefined,
|
||||
previousSummary: previousSummary?.summary,
|
||||
context: (summarizeRecent ? [previousRecent, selected.recent] : [previousRecent, selected.head]).filter(
|
||||
Boolean,
|
||||
),
|
||||
@@ -304,40 +323,64 @@ const make = (dependencies: Dependencies) => {
|
||||
})
|
||||
})
|
||||
const compactAfterOverflow = Effect.fn("SessionCompaction.compactAfterOverflow")(function* (input: AutoInput) {
|
||||
return yield* compactAvailable({
|
||||
sessionID: input.sessionID,
|
||||
messages: input.messages,
|
||||
model: input.request.model,
|
||||
reason: "auto",
|
||||
output: input.request.generation?.maxTokens ?? input.request.model.route.defaults.limits?.output ?? 0,
|
||||
})
|
||||
const selected = selectCompactionContext(input.messages, config.tokens)
|
||||
if (!selected) return false
|
||||
return yield* compactSelected(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messages: input.messages,
|
||||
model: input.request.model,
|
||||
reason: "auto",
|
||||
output: resolveOutputLimit(input.request),
|
||||
},
|
||||
selected,
|
||||
)
|
||||
})
|
||||
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) {
|
||||
return yield* compactAvailable({ ...input, reason: "manual" })
|
||||
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: ManualInput) {
|
||||
const target = {
|
||||
sessionID: input.session.id,
|
||||
reason: "manual",
|
||||
inputID: input.inputID,
|
||||
} satisfies FailureTarget
|
||||
const selected = selectCompactionContext(input.messages, config.tokens)
|
||||
if (!selected) {
|
||||
yield* publishFailure(target, { type: "compaction.unavailable", message: "Nothing to compact yet" })
|
||||
return false
|
||||
}
|
||||
const resolved = yield* dependencies.models.resolve(input.session).pipe(
|
||||
Effect.catch((error) => publishFailure(target, toSessionError(error)).pipe(Effect.as(undefined))),
|
||||
)
|
||||
if (!resolved) return false
|
||||
return yield* compactSelected(
|
||||
{
|
||||
...target,
|
||||
messages: input.messages,
|
||||
model: resolved.model,
|
||||
},
|
||||
selected,
|
||||
)
|
||||
})
|
||||
const compactIfNeeded = Effect.fn("SessionCompaction.compactIfNeeded")(function* (input: AutoInput) {
|
||||
if (!config.auto) return false
|
||||
const context = input.request.model.route.defaults.limits?.context
|
||||
if (context === undefined || context <= 0) return false
|
||||
const output = input.request.generation?.maxTokens ?? input.request.model.route.defaults.limits?.output ?? 0
|
||||
const output = resolveOutputLimit(input.request)
|
||||
if (
|
||||
estimate({ system: input.request.system, messages: input.request.messages, tools: input.request.tools }) <=
|
||||
context - Math.max(output, config.buffer)
|
||||
)
|
||||
return false
|
||||
const selected = select(input.messages, config.tokens)
|
||||
const selected = selectCompactionContext(input.messages, config.tokens)
|
||||
if (!selected) return false
|
||||
const previousSummary = input.messages.find(
|
||||
(message) => message.type === "compaction" && message.status === "completed",
|
||||
)
|
||||
if (!selected.head && previousSummary?.type !== "compaction") return false
|
||||
const previousRecent = previousSummary?.type === "compaction" ? previousSummary.recent : ""
|
||||
const previousSummary = findCompletedSummary(input.messages)
|
||||
if (!selected.head && !previousSummary) return false
|
||||
const previousRecent = previousSummary?.recent ?? ""
|
||||
const summaryContext = [previousRecent, selected.head].filter(Boolean)
|
||||
const summaryOutput = Math.min(output || SUMMARY_OUTPUT_TOKENS, SUMMARY_OUTPUT_TOKENS)
|
||||
if (
|
||||
Token.estimate(
|
||||
buildPrompt({
|
||||
previousSummary: previousSummary?.type === "compaction" ? previousSummary.summary : undefined,
|
||||
previousSummary: previousSummary?.summary,
|
||||
context: summaryContext,
|
||||
}),
|
||||
) >
|
||||
@@ -348,7 +391,7 @@ const make = (dependencies: Dependencies) => {
|
||||
sessionID: input.sessionID,
|
||||
model: input.request.model,
|
||||
reason: "auto",
|
||||
previousSummary: previousSummary?.type === "compaction" ? previousSummary.summary : undefined,
|
||||
previousSummary: previousSummary?.summary,
|
||||
context: summaryContext,
|
||||
recent: selected.recent,
|
||||
output,
|
||||
@@ -368,43 +411,7 @@ export const layer = Layer.effect(
|
||||
const llm = yield* LLMClient.Service
|
||||
const config = yield* Config.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const configured = settings(yield* config.entries())
|
||||
const compaction = make({ events, llm, config: configured })
|
||||
|
||||
return Service.of({
|
||||
compactIfNeeded: compaction.compactIfNeeded,
|
||||
compactAfterOverflow: compaction.compactAfterOverflow,
|
||||
compactManual: Effect.fn("SessionCompaction.compactManual")(function* (input) {
|
||||
if (!select(input.messages, configured.tokens)) {
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.session.id,
|
||||
reason: "manual",
|
||||
error: { type: "compaction.unavailable", message: "Nothing to compact yet" },
|
||||
inputID: input.inputID,
|
||||
})
|
||||
return false
|
||||
}
|
||||
const resolved = yield* models.resolve(input.session).pipe(
|
||||
Effect.catch((error) =>
|
||||
events
|
||||
.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.session.id,
|
||||
reason: "manual",
|
||||
error: toSessionError(error),
|
||||
inputID: input.inputID,
|
||||
})
|
||||
.pipe(Effect.as(undefined)),
|
||||
),
|
||||
)
|
||||
if (!resolved) return false
|
||||
return yield* compaction.compactManual({
|
||||
sessionID: input.session.id,
|
||||
messages: input.messages,
|
||||
model: resolved.model,
|
||||
inputID: input.inputID,
|
||||
})
|
||||
}),
|
||||
})
|
||||
return Service.of(make({ events, llm, models, config: resolveSettings(yield* config.entries()) }))
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import { SessionRunnerSystemPrompt } from "./system-prompt"
|
||||
import { Snapshot } from "../../snapshot"
|
||||
import { makeLocationNode } from "../../effect/app-node"
|
||||
import { llmClient } from "../../effect/app-node-platform"
|
||||
import { AgentNotFoundError, StepFailedError } from "../error"
|
||||
import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "../error"
|
||||
import { toSessionError } from "../to-session-error"
|
||||
import { SessionRunnerRetry } from "./retry"
|
||||
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
||||
@@ -256,8 +256,8 @@ const layer = Layer.effect(
|
||||
tools: toolMaterialization?.definitions ?? [],
|
||||
toolChoice: isLastStep ? "none" : undefined,
|
||||
})
|
||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = []
|
||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error | StepFailedError>()
|
||||
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error | StepFailedError>> = []
|
||||
let needsContinuation = false
|
||||
const availableTools = new Map(request.tools.map((tool) => [tool.name, tool]))
|
||||
const requestEvent: SessionHooks["request"] = {
|
||||
@@ -363,6 +363,13 @@ const layer = Layer.effect(
|
||||
output: settlement.output,
|
||||
}),
|
||||
settlement.error,
|
||||
).pipe(
|
||||
// Terminal cleanup must own failAssistant because the provider may still be streaming another tool input.
|
||||
Effect.andThen(
|
||||
settlement.error?.type === "permission.rejected"
|
||||
? Effect.fail(new StepFailedError({ error: settlement.error }))
|
||||
: Effect.void,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -456,18 +463,26 @@ const layer = Layer.effect(
|
||||
: settled.value.flatMap((exit) => (exit._tag === "Failure" ? [exit.cause] : []))
|
||||
const toolsInterrupted = settledCauses.some(Cause.hasInterrupts)
|
||||
const userDeclined = settledCauses.some(isUserDeclined)
|
||||
const permissionRejected = settledCauses
|
||||
.map((cause) => Option.getOrUndefined(Cause.findErrorOption(cause)))
|
||||
.find(
|
||||
(error): error is StepFailedError =>
|
||||
error instanceof StepFailedError && error.error.type === "permission.rejected",
|
||||
)
|
||||
|
||||
if (userDeclined || streamInterrupted || toolsInterrupted) {
|
||||
if (userDeclined || permissionRejected || streamInterrupted || toolsInterrupted) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" }))
|
||||
yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" }))
|
||||
yield* serialized(
|
||||
publisher.failAssistant(permissionRejected?.error ?? { type: "aborted", message: "Step interrupted" }),
|
||||
)
|
||||
}
|
||||
// A settled tool fiber failure is one of two things. A defect from a tool
|
||||
// implementation becomes a failed tool call the model can read, and the step still
|
||||
// settles so the model may recover. A typed infrastructure failure (tool output
|
||||
// could not be persisted) also fails the assistant and then fails the drain.
|
||||
const settledFailure = settledCauses.find(
|
||||
(cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause),
|
||||
(cause) => !Cause.hasInterrupts(cause) && !isUserDeclined(cause) && !permissionRejected,
|
||||
)
|
||||
const infraError =
|
||||
settledFailure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(settledFailure))
|
||||
@@ -519,6 +534,7 @@ const layer = Layer.effect(
|
||||
|
||||
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
||||
if (userDeclined) return yield* Effect.interrupt
|
||||
if (permissionRejected) return yield* new UserInterruptedError()
|
||||
if ((toolsInterrupted || infraError !== undefined) && settledFailure)
|
||||
return yield* Effect.failCause(settledFailure)
|
||||
if (toolsInterrupted && settled._tag === "Failure") return yield* Effect.failCause(settled.cause)
|
||||
|
||||
@@ -934,54 +934,4 @@ describe("Config", () => {
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("discovers ecosystem skill directories without watching them as config", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((tmp) =>
|
||||
Effect.gen(function* () {
|
||||
const global = path.join(tmp.path, "global")
|
||||
const project = path.join(tmp.path, "project")
|
||||
const globalAgents = path.join(global, "home", ".agents")
|
||||
const globalClaude = path.join(global, "home", ".claude")
|
||||
const projectAgents = path.join(project, ".agents")
|
||||
const projectClaude = path.join(project, ".claude")
|
||||
yield* Effect.promise(() =>
|
||||
Promise.all(
|
||||
[global, project, globalAgents, globalClaude, projectAgents, projectClaude].map((directory) =>
|
||||
fs.mkdir(directory, { recursive: true }),
|
||||
),
|
||||
),
|
||||
)
|
||||
const targets: Watcher.WatchInput[] = []
|
||||
const watcher = Layer.succeed(
|
||||
Watcher.Service,
|
||||
Watcher.Service.of({
|
||||
subscribe: (input) => {
|
||||
targets.push(input)
|
||||
return Stream.empty
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const entries = yield* config.entries()
|
||||
|
||||
expect(entries.filter((entry) => entry.type === "agents").map((entry) => entry.path)).toEqual([
|
||||
AbsolutePath.make(globalAgents),
|
||||
AbsolutePath.make(projectAgents),
|
||||
])
|
||||
expect(entries.filter((entry) => entry.type === "claude").map((entry) => entry.path)).toEqual([
|
||||
AbsolutePath.make(globalClaude),
|
||||
AbsolutePath.make(projectClaude),
|
||||
])
|
||||
expect(targets).toEqual([{ path: AbsolutePath.make(global), type: "directory" }])
|
||||
}).pipe(Effect.provide(testLayer(project, global, project, undefined, watcher)))
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -64,7 +64,7 @@ import { Location } from "@opencode-ai/core/location"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { asc, eq } from "drizzle-orm"
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const requests: LLMRequest[] = []
|
||||
@@ -523,6 +523,22 @@ const recordedEventTypes = (id: SessionV2.ID) =>
|
||||
)
|
||||
})
|
||||
|
||||
const recordedToolInputEnds = (id: SessionV2.ID, callID: string) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
return (yield* db
|
||||
.select({ data: EventTable.data })
|
||||
.from(EventTable)
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, id),
|
||||
eq(EventTable.type, EventV2.versionedType(SessionEvent.Tool.Input.Ended.type, 1)),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
.pipe(Effect.orDie)).filter((event) => event.data.callID === callID)
|
||||
})
|
||||
|
||||
const recordedStepSettlementEvents = (id: SessionV2.ID, assistantMessageID: SessionMessage.ID) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
@@ -2964,7 +2980,7 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns tool-wrapped policy blocks to the model and continues", () =>
|
||||
it.effect("returns policy-blocked tools to the model and continues", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
const registry = yield* ToolRegistry.Service
|
||||
@@ -3106,7 +3122,7 @@ describe("SessionRunnerLLM", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("returns configured permission denials to the model and continues", () =>
|
||||
it.effect("preserves permission rejection and stops before continuation", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
const registry = yield* ToolRegistry.Service
|
||||
@@ -3117,13 +3133,19 @@ describe("SessionRunnerLLM", () => {
|
||||
[LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: "stop" })],
|
||||
]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(exit._tag).toBe("Failure")
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(yield* session.context(sessionID)).toMatchObject([
|
||||
{ type: "user" },
|
||||
{
|
||||
type: "assistant",
|
||||
finish: "error",
|
||||
error: {
|
||||
type: "permission.rejected",
|
||||
message: "Permission denied: edit",
|
||||
},
|
||||
content: [
|
||||
{
|
||||
type: "tool",
|
||||
@@ -3138,12 +3160,93 @@ describe("SessionRunnerLLM", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: "assistant", finish: "stop" },
|
||||
])
|
||||
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.failed.1")
|
||||
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
|
||||
}),
|
||||
)
|
||||
|
||||
const rejectPermissionWhileToolInputStreams = (lateEvent: LLMEvent) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const releaseLateEvent = yield* Deferred.make<void>()
|
||||
yield* registry.register({ permissionfail: permissionFail })
|
||||
const events = yield* EventV2.Service
|
||||
const permissionFailed = yield* events.subscribe(SessionEvent.Tool.Failed).pipe(
|
||||
Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-permission"),
|
||||
Stream.runHead,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* admit(session, "Reject permission while another tool input streams")
|
||||
responseStream = Stream.concat(
|
||||
Stream.fromIterable([
|
||||
LLMEvent.stepStart({ index: 0 }),
|
||||
LLMEvent.toolInputStart({ id: "call-streaming", name: "echo" }),
|
||||
LLMEvent.toolCall({ id: "call-permission", name: "permissionfail", input: {} }),
|
||||
]),
|
||||
Stream.fromEffect(Deferred.await(releaseLateEvent)).pipe(Stream.flatMap(() => Stream.make(lateEvent))),
|
||||
)
|
||||
|
||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||
yield* Fiber.join(permissionFailed).pipe(Effect.timeout("1 second"))
|
||||
yield* Effect.yieldNow
|
||||
const inputEndsBeforeRelease = yield* recordedToolInputEnds(sessionID, "call-streaming")
|
||||
yield* Deferred.succeed(releaseLateEvent, undefined)
|
||||
const exit = yield* Fiber.await(run)
|
||||
return {
|
||||
exit,
|
||||
inputEndsBeforeRelease,
|
||||
context: yield* session.context(sessionID),
|
||||
inputEnds: yield* recordedToolInputEnds(sessionID, "call-streaming"),
|
||||
}
|
||||
})
|
||||
|
||||
for (const testCase of [
|
||||
{
|
||||
name: "does not end concurrent tool input when permission is rejected",
|
||||
event: LLMEvent.toolInputDelta({
|
||||
id: "call-streaming",
|
||||
name: "echo",
|
||||
text: '{"text":"still streaming"}',
|
||||
}),
|
||||
defect: "Tool input delta after end: call-streaming",
|
||||
},
|
||||
{
|
||||
name: "does not duplicate concurrent tool input end when permission is rejected",
|
||||
event: LLMEvent.toolInputEnd({ id: "call-streaming", name: "echo" }),
|
||||
defect: "Duplicate tool input end: call-streaming",
|
||||
},
|
||||
]) {
|
||||
it.effect(testCase.name, () =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* rejectPermissionWhileToolInputStreams(testCase.event)
|
||||
|
||||
expect(result.inputEndsBeforeRelease).toHaveLength(0)
|
||||
expect(Exit.isFailure(result.exit)).toBe(true)
|
||||
if (Exit.isFailure(result.exit)) {
|
||||
expect(Cause.pretty(result.exit.cause)).not.toContain(testCase.defect)
|
||||
expect(Cause.hasDies(result.exit.cause)).toBe(false)
|
||||
}
|
||||
expect(result.context).toMatchObject([
|
||||
{ type: "user" },
|
||||
{
|
||||
type: "assistant",
|
||||
error: { type: "permission.rejected", message: "Permission denied: edit" },
|
||||
content: [
|
||||
{
|
||||
type: "tool",
|
||||
id: "call-streaming",
|
||||
state: { status: "error", error: { type: "aborted", message: "Tool execution interrupted" } },
|
||||
},
|
||||
{ type: "tool", id: "call-permission", state: { status: "error" } },
|
||||
],
|
||||
},
|
||||
])
|
||||
expect(result.inputEnds).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("interrupts runner continuation when a question is cancelled", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
|
||||
+61
-57
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Agents"
|
||||
description: ""
|
||||
description: "Configure and use primary agents and subagents in OpenCode."
|
||||
---
|
||||
|
||||
Agents combine a system prompt, model preference, tool permissions, and display
|
||||
@@ -8,6 +8,32 @@ metadata into a reusable assistant profile. OpenCode includes agents for common
|
||||
workflows, and you can override them or add your own in configuration or
|
||||
Markdown files.
|
||||
|
||||
## Modes
|
||||
|
||||
An agent's `mode` controls where it can run:
|
||||
|
||||
| Mode | Behavior |
|
||||
| --- | --- |
|
||||
| `primary` | Can be selected as the main agent for a session. It cannot be launched as a subagent. |
|
||||
| `subagent` | Can run in a child session through the `subagent` tool, but cannot be selected as the main agent. |
|
||||
| `all` | Can be used either way. This is the default for a custom agent when `mode` is omitted. |
|
||||
|
||||
In the TUI, press <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to cycle
|
||||
through visible primary and `all` agents, or use `/agents` to choose one.
|
||||
|
||||
Subagents run in child sessions with fresh context. A primary agent can invoke
|
||||
one with the `subagent` tool, either in the foreground or in the background.
|
||||
You can also `@` mention a visible subagent to ask the current agent to delegate
|
||||
work to it:
|
||||
|
||||
```text
|
||||
@explore find where authentication errors are handled
|
||||
```
|
||||
|
||||
The parent agent's `subagent` permission controls which agents it may launch.
|
||||
The child currently uses its own configured permissions, not a restricted copy
|
||||
of the parent's permissions.
|
||||
|
||||
## Built-in agents
|
||||
|
||||
| Agent | Mode | Purpose |
|
||||
@@ -40,33 +66,42 @@ be hidden. If it is unavailable, OpenCode falls back to `build`, then to the
|
||||
first visible agent that can run as a primary agent. This selection does not
|
||||
rewrite the agent already stored on an existing session.
|
||||
|
||||
## Modes
|
||||
## Configure agents
|
||||
|
||||
An agent's `mode` controls where it can run:
|
||||
### JSON or JSONC
|
||||
|
||||
| Mode | Behavior |
|
||||
| --- | --- |
|
||||
| `primary` | Can be selected as the main agent for a session. It cannot be launched as a subagent. |
|
||||
| `subagent` | Can run in a child session through the `subagent` tool, but cannot be selected as the main agent. |
|
||||
| `all` | Can be used either way. This is the default for a custom agent when `mode` is omitted. |
|
||||
Use the plural `agents` field in any [OpenCode configuration file](/config):
|
||||
|
||||
In the TUI, press <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> to cycle
|
||||
through visible primary and `all` agents, or use `/agents` to choose one.
|
||||
|
||||
Subagents run in child sessions with fresh context. A primary agent can invoke
|
||||
one with the `subagent` tool, either in the foreground or in the background.
|
||||
You can also `@` mention a visible subagent to ask the current agent to delegate
|
||||
work to it:
|
||||
|
||||
```text
|
||||
@explore find where authentication errors are handled
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"default_agent": "reviewer",
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"description": "Reviews changes for correctness, security, and missing tests",
|
||||
"mode": "all",
|
||||
"model": "anthropic/claude-sonnet-4-5#high",
|
||||
"system": "Review the current changes. Report findings before any summary.",
|
||||
"color": "warning",
|
||||
"steps": 8,
|
||||
"permissions": [
|
||||
{ "action": "edit", "resource": "*", "effect": "deny" },
|
||||
{ "action": "shell", "resource": "*", "effect": "deny" }
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "git push *", "effect": "ask" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The parent agent's `subagent` permission controls which agents it may launch.
|
||||
The child currently uses its own configured permissions, not a restricted copy
|
||||
of the parent's permissions.
|
||||
|
||||
## Configure agents
|
||||
Agent definitions merge in configuration order. Later scalar fields replace
|
||||
earlier values, request maps merge by key, and permission rules are appended.
|
||||
Global `permissions` are applied to every agent before its agent-specific rules,
|
||||
so a later agent rule can refine a global rule.
|
||||
|
||||
### Markdown files
|
||||
|
||||
@@ -104,40 +139,9 @@ Review for correctness, security, regressions, and missing tests.
|
||||
List findings in severity order with file and line references.
|
||||
```
|
||||
|
||||
### JSON or JSONC
|
||||
|
||||
Use the `agents` field in any [OpenCode configuration file](/config):
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"default_agent": "reviewer",
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"description": "Reviews changes for correctness, security, and missing tests",
|
||||
"mode": "all",
|
||||
"model": "anthropic/claude-sonnet-4-5#high",
|
||||
"system": "Review the current changes. Report findings before any summary.",
|
||||
"color": "warning",
|
||||
"steps": 8,
|
||||
"permissions": [
|
||||
{ "action": "edit", "resource": "*", "effect": "deny" },
|
||||
{ "action": "shell", "resource": "*", "effect": "deny" }
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "git push *", "effect": "ask" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Agent definitions merge in configuration order. Later scalar fields replace
|
||||
earlier values, request maps merge by key, and permission rules are appended.
|
||||
Global `permissions` are applied to every agent before its agent-specific rules,
|
||||
so a later agent rule can refine a global rule.
|
||||
For compatibility with older layouts, V2 also discovers Markdown under
|
||||
`agent/`, and treats files under `mode/` or `modes/` as primary agents. Prefer
|
||||
`agents/` for new files.
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Attachments"
|
||||
description: ""
|
||||
description: "Attach supported files and images to V2 prompts and configure image processing."
|
||||
---
|
||||
|
||||
OpenCode can add local context to a prompt as text or image media. Current V2
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
title: "Client"
|
||||
description: "Connect an application to the OpenCode HTTP API."
|
||||
---
|
||||
|
||||
`@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP
|
||||
API. Use it when your application connects to an OpenCode server over the
|
||||
network. Its types and methods are generated from the same contract as the
|
||||
[API reference](/api).
|
||||
|
||||
<Warning>
|
||||
The V2 API and client are beta. Method names, inputs, and outputs may change
|
||||
before the stable release.
|
||||
</Warning>
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @opencode-ai/client
|
||||
```
|
||||
|
||||
The package has two entrypoints:
|
||||
|
||||
- `@opencode-ai/client/promise` uses `fetch` and returns Promises or async
|
||||
iterables. It has no Effect runtime dependency.
|
||||
- `@opencode-ai/client/effect` returns Effects and Streams, decodes values into
|
||||
the V2 schema types, and requires an `HttpClient` service from Effect.
|
||||
|
||||
## Promise client
|
||||
|
||||
Create a client with the server URL, then call methods grouped by API resource:
|
||||
|
||||
```ts
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:4096",
|
||||
})
|
||||
|
||||
const session = await client.session.create({
|
||||
location: { directory: "/workspace" },
|
||||
})
|
||||
|
||||
await client.session.prompt({
|
||||
sessionID: session.id,
|
||||
text: "Review the current changes",
|
||||
})
|
||||
```
|
||||
|
||||
Pass default authentication or application headers to `OpenCode.make` with
|
||||
`headers`. You can also supply a custom `fetch` implementation. Each operation
|
||||
accepts request options as its final argument for an `AbortSignal` or
|
||||
per-request headers.
|
||||
|
||||
Streaming endpoints return async iterables:
|
||||
|
||||
```ts
|
||||
for await (const event of client.event.subscribe()) {
|
||||
console.log(event.type)
|
||||
}
|
||||
```
|
||||
|
||||
## Effect client
|
||||
|
||||
Install the `effect` peer dependency when using the Effect entrypoint. The
|
||||
client uses canonical V2 values such as `Location.Ref` and `Session.ID`, and
|
||||
returns typed failures in the Effect error channel.
|
||||
|
||||
```ts
|
||||
import { AbsolutePath, Location, OpenCode } from "@opencode-ai/client/effect"
|
||||
import { Effect } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:4096" })
|
||||
const session = yield* client.session.create({
|
||||
location: Location.Ref.make({
|
||||
directory: AbsolutePath.make("/workspace"),
|
||||
}),
|
||||
})
|
||||
|
||||
return yield* client.session.get({ sessionID: session.id })
|
||||
})
|
||||
|
||||
const session = await Effect.runPromise(
|
||||
program.pipe(Effect.provide(FetchHttpClient.layer)),
|
||||
)
|
||||
```
|
||||
|
||||
Streaming operations, including `client.event.subscribe()` and
|
||||
`client.session.log(...)`, return Effect `Stream` values.
|
||||
+38
-37
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Commands"
|
||||
description: ""
|
||||
description: "Create reusable slash commands from configuration or Markdown files."
|
||||
---
|
||||
|
||||
Custom commands turn a named prompt template into a slash command. Type the
|
||||
@@ -10,44 +10,9 @@ command in the TUI, followed by any arguments:
|
||||
/review src/auth
|
||||
```
|
||||
|
||||
## Configure with Markdown
|
||||
|
||||
OpenCode discovers `.md` command files in `commands/` directories:
|
||||
|
||||
```text
|
||||
~/.config/opencode/commands/ # Global
|
||||
.opencode/commands/ # Project
|
||||
```
|
||||
|
||||
Files may be nested; for example, `.opencode/commands/team/review.md` defines
|
||||
`/team/review`. Files with other extensions, including `.mdx`, are not
|
||||
discovered.
|
||||
|
||||
```md title=".opencode/commands/review.md"
|
||||
---
|
||||
description: Review code for correctness and missing tests
|
||||
agent: plan
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
---
|
||||
|
||||
Review $ARGUMENTS. Report bugs first, then missing tests.
|
||||
```
|
||||
|
||||
The file body, with surrounding whitespace removed, is the command template.
|
||||
JSON and Markdown commands share one registry. Project definitions take
|
||||
precedence over global definitions, and a later definition can override a
|
||||
built-in or earlier command with the same name. Changes are reloaded
|
||||
automatically.
|
||||
|
||||
Run it with:
|
||||
|
||||
```text
|
||||
/review src/auth
|
||||
```
|
||||
|
||||
## Configure with JSON
|
||||
|
||||
Add commands under the `commands` key in any OpenCode JSON or JSONC
|
||||
Add commands under the plural `commands` key in any OpenCode JSON or JSONC
|
||||
[configuration file](/config). Each entry's key is the command name and
|
||||
`template` is required.
|
||||
|
||||
@@ -65,6 +30,42 @@ Add commands under the `commands` key in any OpenCode JSON or JSONC
|
||||
}
|
||||
```
|
||||
|
||||
Run it with:
|
||||
|
||||
```text
|
||||
/review src/auth
|
||||
```
|
||||
|
||||
## Configure with Markdown
|
||||
|
||||
OpenCode discovers `.md` command files in both the singular `command/` and
|
||||
plural `commands/` directories:
|
||||
|
||||
```text
|
||||
~/.config/opencode/commands/ # Global
|
||||
.opencode/commands/ # Project
|
||||
```
|
||||
|
||||
The equivalent `command/` paths also work. Files may be nested; for example,
|
||||
`.opencode/commands/team/review.md` defines `/team/review`. Files with other
|
||||
extensions, including `.mdx`, are not discovered.
|
||||
|
||||
```md title=".opencode/commands/review.md"
|
||||
---
|
||||
description: Review code for correctness and missing tests
|
||||
agent: plan
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
---
|
||||
|
||||
Review $ARGUMENTS. Report bugs first, then missing tests.
|
||||
```
|
||||
|
||||
The file body, with surrounding whitespace removed, is the command template.
|
||||
JSON and Markdown commands share one registry. Project definitions take
|
||||
precedence over global definitions, and a later definition can override a
|
||||
built-in or earlier command with the same name. Changes are reloaded
|
||||
automatically.
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | Required | Behavior |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Compaction"
|
||||
description: ""
|
||||
title: "Context compaction"
|
||||
description: "Configure and run context compaction in OpenCode V2."
|
||||
---
|
||||
|
||||
Compaction replaces the active model context from an older part of a session
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Config"
|
||||
description: ""
|
||||
description: "Configure OpenCode."
|
||||
---
|
||||
|
||||
<Tip>
|
||||
|
||||
+10
-10
@@ -20,13 +20,17 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get started",
|
||||
"pages": ["index", "migrate-v1", "config", "troubleshooting"]
|
||||
"pages": ["index", "config", "troubleshooting"]
|
||||
},
|
||||
{
|
||||
"group": "Migrate from V1",
|
||||
"pages": ["migrate-v1"]
|
||||
},
|
||||
{
|
||||
"group": "Configure",
|
||||
"pages": [
|
||||
"providers",
|
||||
"models",
|
||||
"providers",
|
||||
"agents",
|
||||
"permissions",
|
||||
"sharing",
|
||||
@@ -39,19 +43,15 @@
|
||||
"compaction",
|
||||
"formatters",
|
||||
"lsp",
|
||||
"references"
|
||||
"references",
|
||||
"plugins"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Build",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Build with OpenCode",
|
||||
"pages": ["plugins", "client", "sdk/index"]
|
||||
}
|
||||
]
|
||||
"tab": "SDK",
|
||||
"pages": ["sdk/index"]
|
||||
},
|
||||
{
|
||||
"tab": "API",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Formatters"
|
||||
description: ""
|
||||
description: "Configure formatter settings and understand formatter support in OpenCode V2."
|
||||
---
|
||||
|
||||
OpenCode V2 accepts formatter configuration, but it does not yet include a
|
||||
|
||||
@@ -156,7 +156,7 @@ Use `/undo` when a change isn't what you wanted.
|
||||
|
||||
OpenCode stages a conversation revert and restores your original message so you can revise it. In a Git repository, it
|
||||
also restores file changes when snapshots were captured successfully. Run `/undo` multiple times to move the conversation
|
||||
boundary back, or use `/redo` to restore the staged conversation and files. See [Undo](/snapshots) for
|
||||
boundary back, or use `/redo` to restore the staged conversation and files. See [Snapshots and undo](/snapshots) for
|
||||
limitations and safety details.
|
||||
|
||||
```text
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Instructions"
|
||||
description: ""
|
||||
description: "Give OpenCode global, project, and directory-specific guidance."
|
||||
---
|
||||
|
||||
Instructions are privileged context that guide an agent throughout a session.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "LSP"
|
||||
description: ""
|
||||
description: "Configure language servers and understand LSP support in OpenCode V2."
|
||||
---
|
||||
|
||||
Language Server Protocol (LSP) integrations can provide code diagnostics,
|
||||
|
||||
+3
-11
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "MCP servers"
|
||||
description: ""
|
||||
description: "Connect local and remote Model Context Protocol servers to OpenCode."
|
||||
---
|
||||
|
||||
OpenCode can connect to [Model Context Protocol](https://modelcontextprotocol.io/) servers and make their tools, prompts, and instructions available to agents. MCP tools consume model context, so enable only the servers you need.
|
||||
@@ -132,21 +132,13 @@ For a server that supports dynamic client registration, only the remote server i
|
||||
}
|
||||
```
|
||||
|
||||
When the server reports that it needs authentication, run `/connect` in the TUI:
|
||||
|
||||
```text
|
||||
/connect
|
||||
```
|
||||
|
||||
Select the MCP server under **Services**, then complete the browser authorization flow.
|
||||
|
||||
You can also authenticate from the command line:
|
||||
When the server reports `needs authentication`, start the browser authorization flow:
|
||||
|
||||
```bash
|
||||
opencode2 mcp auth sentry
|
||||
```
|
||||
|
||||
The CLI command prints the authorization URL and waits for the redirect to OpenCode's loopback callback server.
|
||||
The command prints the authorization URL and waits for the redirect to OpenCode's loopback callback server.
|
||||
|
||||
If the provider issued client credentials, configure them using V2's snake_case field names:
|
||||
|
||||
|
||||
+41
-471
@@ -1,25 +1,11 @@
|
||||
---
|
||||
title: "Migrate from V1"
|
||||
title: "Overview"
|
||||
description: "Move from OpenCode V1 to the OpenCode 2.0 beta."
|
||||
---
|
||||
|
||||
<Note>
|
||||
The only intentional breaking changes in V2 are the server API and the plugin API. All other functionality is intended
|
||||
to remain compatible with V1.
|
||||
</Note>
|
||||
|
||||
Existing config files, agent definitions, command definitions, skills, and other files in `.opencode/` should continue to
|
||||
work without changes. If one of these stops working in V2, treat it as a beta compatibility bug rather than an expected
|
||||
migration requirement.
|
||||
|
||||
<Tip>
|
||||
Run `/report` if existing V1 functionality does not work in V2. The report skill collects diagnostics and helps you file
|
||||
a compatibility issue.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
OpenCode 2.0 is in beta. Beta data may be wiped, features may break unintentionally, and the server and plugin APIs may
|
||||
continue to change.
|
||||
OpenCode 2.0 is in beta. Back up important configuration and data before migrating. Beta data may be wiped, features may
|
||||
break, and configuration and plugin APIs may change.
|
||||
</Warning>
|
||||
|
||||
During the beta, OpenCode V1 and V2 use different executable names. You can keep using `opencode` for V1 while trying V2
|
||||
@@ -39,13 +25,9 @@ Start it in your project with:
|
||||
opencode2
|
||||
```
|
||||
|
||||
## Configuration
|
||||
## Back up your configuration
|
||||
|
||||
This section covers both JSON/JSONC configuration and file-based definitions under `.opencode/`.
|
||||
|
||||
### Use your existing configuration
|
||||
|
||||
V2 reads existing global and project configuration from the same locations as V1:
|
||||
Before making changes, back up your global and project configuration files:
|
||||
|
||||
```text
|
||||
~/.config/opencode/opencode.json(c)
|
||||
@@ -53,467 +35,55 @@ V2 reads existing global and project configuration from the same locations as V1
|
||||
<project>/.opencode/opencode.json(c)
|
||||
```
|
||||
|
||||
V2 reads these same locations. It detects V1-shaped configuration and translates it in memory without rewriting the
|
||||
source file. Existing V1 configuration is intended to keep working, so you do not need to convert it to try or adopt V2.
|
||||
V2 reads these same locations. It detects V1-shaped configuration and translates supported fields in memory without
|
||||
rewriting the source file. Keep shared files in their V1 shape while evaluating both versions; V1 does not understand the
|
||||
native plural V2 fields and may silently ignore them.
|
||||
|
||||
### Ask OpenCode to migrate
|
||||
## Update configuration
|
||||
|
||||
The V1 config format remains supported. The native V2 format is optional and makes several settings more explicit and
|
||||
ergonomic.
|
||||
Automatic translation is a compatibility aid, not a guarantee that every V1 setting behaves identically. Convert your
|
||||
configuration to the native V2 shape only when you no longer need V1 to read those same files.
|
||||
|
||||
The recommended migration path is to ask OpenCode to update the configuration for you:
|
||||
<Note>
|
||||
Do not mix V1 and V2 field names in one file. Back up the V1 file, update the complete working file to the V2 shape, and
|
||||
validate it against the current schema. Restore the V1 copy before using V1 again.
|
||||
</Note>
|
||||
|
||||
```text
|
||||
Migrate my OpenCode configuration, including file-based definitions, from the V1 format to the native V2 format.
|
||||
Preserve its behavior and all unrelated settings.
|
||||
```
|
||||
The main field changes are:
|
||||
|
||||
OpenCode can inspect the complete file, apply the relevant changes below, and avoid rewriting settings that do not need to
|
||||
change. Do not mix V1 and V2 field names manually in one file.
|
||||
|
||||
### Sharing
|
||||
|
||||
The deprecated V1 `autoshare` boolean becomes the explicit `share` policy:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{ "autoshare": true }
|
||||
|
||||
// V2
|
||||
{ "share": "auto" }
|
||||
```
|
||||
|
||||
Use `"manual"`, `"auto"`, or `"disabled"`. If the V1 file already uses `share`, no change is needed.
|
||||
|
||||
### Permissions and tools
|
||||
|
||||
V1 groups permission effects by tool. V2 uses one ordered `permissions` array, making precedence and exceptions explicit:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"permission": {
|
||||
"bash": {
|
||||
"git push *": "ask"
|
||||
},
|
||||
"edit": "allow"
|
||||
},
|
||||
"tools": {
|
||||
"websearch": false
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"permissions": [
|
||||
{ "action": "shell", "resource": "git push *", "effect": "ask" },
|
||||
{ "action": "edit", "resource": "*", "effect": "allow" },
|
||||
{ "action": "websearch", "resource": "*", "effect": "deny" }
|
||||
]
|
||||
}
|
||||
```
|
||||
| V1 | V2 |
|
||||
| --- | --- |
|
||||
| `permission` | `permissions` |
|
||||
| `agent` and `mode` | `agents` |
|
||||
| `snapshot` | `snapshots` |
|
||||
| `attachment` | `attachments` |
|
||||
| `command` | `commands` |
|
||||
| `reference` | `references` |
|
||||
| `plugin` | `plugins` |
|
||||
| `provider` | `providers` |
|
||||
| Servers directly under `mcp` | Servers under `mcp.servers` |
|
||||
| `skills.paths` and `skills.urls` | A single `skills` array |
|
||||
|
||||
Permission actions also changed: `bash` is now `shell`, `task` is now `subagent`, and `write` and `patch` are now `edit`.
|
||||
See [Permissions](/permissions) for the ordered V2 rule format.
|
||||
|
||||
### Agents and modes
|
||||
V1-only fields including `logLevel`, `server`, `layout`, `disabled_providers`, `enabled_providers`, and `small_model` are
|
||||
not carried into the native V2 configuration. Remove them and use the current [Config](/config) and
|
||||
[Providers](/providers) guides to configure their replacements where applicable.
|
||||
|
||||
The singular `agent` and deprecated `mode` maps become `agents`. Agent fields become more consistent with the rest of the
|
||||
V2 config:
|
||||
## Review extensions
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"agent": {
|
||||
"reviewer": {
|
||||
"prompt": "Review for correctness and missing tests.",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"variant": "high",
|
||||
"disable": false,
|
||||
"permission": {
|
||||
"edit": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
V2 continues to discover file-based agents, commands, and skills from OpenCode configuration directories. Their schemas
|
||||
have changed, so review each extension against the current [Agents](/agents), [Commands](/commands), and
|
||||
[Skills](/skills) guides.
|
||||
|
||||
// V2
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"system": "Review for correctness and missing tests.",
|
||||
"model": "anthropic/claude-sonnet-4-5#high",
|
||||
"disabled": false,
|
||||
"permissions": [
|
||||
{ "action": "edit", "resource": "*", "effect": "deny" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`prompt` becomes `system`, `disable` becomes `disabled`, and a separate `variant` joins the model reference after `#`.
|
||||
`temperature`, `top_p`, and provider-specific `options` move under `request.body`. `maxSteps` becomes `steps`. Entries from
|
||||
the old `mode` map become primary agents.
|
||||
|
||||
### Snapshots
|
||||
|
||||
Rename the singular `snapshot` field to `snapshots`. Its boolean value does not change:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{ "snapshot": false }
|
||||
|
||||
// V2
|
||||
{ "snapshots": false }
|
||||
```
|
||||
|
||||
### Attachments
|
||||
|
||||
Rename the singular `attachment` object to `attachments`. Nested image settings keep the same names:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{ "attachment": { "image": { "auto_resize": true } } }
|
||||
|
||||
// V2
|
||||
{ "attachments": { "image": { "auto_resize": true } } }
|
||||
```
|
||||
|
||||
### MCP servers
|
||||
|
||||
V2 groups servers under `mcp.servers`, replaces `enabled` with the inverse `disabled`, and separates timeout purposes:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"mcp": {
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": ["npx", "@playwright/mcp"],
|
||||
"enabled": true,
|
||||
"timeout": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": ["npx", "@playwright/mcp"],
|
||||
"disabled": false,
|
||||
"timeout": {
|
||||
"catalog": 30000,
|
||||
"execution": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remote OAuth fields use snake case: `clientId` becomes `client_id`, `clientSecret` becomes `client_secret`,
|
||||
`callbackPort` becomes `callback_port`, and `redirectUri` becomes `redirect_uri`. The V1 `experimental.mcp_timeout` value
|
||||
also becomes the default `mcp.timeout.catalog` and `mcp.timeout.execution` values. See [MCP servers](/mcp).
|
||||
|
||||
### Compaction
|
||||
|
||||
V2 groups the retained-context token budget under `keep` and gives the reserve a clearer name:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"compaction": {
|
||||
"preserve_recent_tokens": 8000,
|
||||
"reserved": 20000
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"compaction": {
|
||||
"keep": {
|
||||
"tokens": 8000
|
||||
},
|
||||
"buffer": 20000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`auto` and `prune` keep their names. V2 has no native `tail_turns` field; recent context is retained by token budget instead.
|
||||
See [Compaction](/compaction).
|
||||
|
||||
### Skills
|
||||
|
||||
V1 separates extra skill paths and URLs. V2 combines both into one ordered array:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"skills": {
|
||||
"paths": ["./team-skills"],
|
||||
"urls": ["https://example.com/skills/"]
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"skills": ["./team-skills", "https://example.com/skills/"]
|
||||
}
|
||||
```
|
||||
|
||||
Existing skill files and automatic `.opencode/skills/` discovery do not change. See [Skills](/skills).
|
||||
|
||||
### Commands
|
||||
|
||||
Rename the singular `command` map to `commands`. Join a separate model `variant` to the model reference:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"command": {
|
||||
"review": {
|
||||
"template": "Review the current changes.",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"variant": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"commands": {
|
||||
"review": {
|
||||
"template": "Review the current changes.",
|
||||
"model": "anthropic/claude-sonnet-4-5#high"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`template`, `description`, `agent`, and `subtask` keep their names. Existing Markdown command definitions remain supported.
|
||||
See [Commands](/commands).
|
||||
|
||||
### References
|
||||
|
||||
Rename the deprecated singular `reference` map to `references`:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{ "reference": { "docs": "../docs" } }
|
||||
|
||||
// V2
|
||||
{ "references": { "docs": "../docs" } }
|
||||
```
|
||||
|
||||
V1 already accepts `references`, so no change is needed when the file uses it. Reference entries keep the
|
||||
same shapes. See [References](/references).
|
||||
|
||||
### Providers
|
||||
|
||||
Rename the singular `provider` map to `providers`. V2 separates the runtime package, endpoint, and request settings:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"provider": {
|
||||
"acme": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://llm.example.com/v1",
|
||||
"options": {
|
||||
"apiKey": "{env:ACME_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"providers": {
|
||||
"acme": {
|
||||
"package": "aisdk:@ai-sdk/openai-compatible",
|
||||
"settings": {
|
||||
"baseURL": "https://llm.example.com/v1",
|
||||
"apiKey": "{env:ACME_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
V1 `npm` becomes `package`, and AI SDK packages receive the `aisdk:` prefix. `api` becomes `settings.baseURL`. Provider
|
||||
`options` are separated into `settings`, `headers`, and `body` according to their request role. See [Providers](/providers).
|
||||
|
||||
### Models and variants
|
||||
|
||||
Models remain nested under their provider, but several model fields become more explicit:
|
||||
|
||||
- `id` becomes `modelID`.
|
||||
- `tool_call` and `modalities` become `capabilities.tools`, `capabilities.input`, and `capabilities.output`.
|
||||
- A `status` of `"deprecated"` becomes `disabled: true`.
|
||||
- Cache costs move from `cache_read` and `cache_write` to `cache.read` and `cache.write`.
|
||||
- Provider-specific `options` become `settings`.
|
||||
- A V1 variants object becomes a V2 array with an `id` on each entry.
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"variants": {
|
||||
"high": {
|
||||
"reasoningEffort": "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"variants": [
|
||||
{
|
||||
"id": "high",
|
||||
"settings": {
|
||||
"reasoningEffort": "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See [Models](/models) for the complete native model shape.
|
||||
|
||||
### Fields without native equivalents
|
||||
|
||||
Most fields that keep the same shape, including `shell`, `model`, `default_agent`, `autoupdate`, `watcher`, `formatter`,
|
||||
`lsp`, `instructions`, `enterprise`, and `tool_output`, require no migration.
|
||||
|
||||
These V1 fields do not have one-to-one native V2 config fields:
|
||||
|
||||
- `logLevel`: use `OPENCODE_LOG_LEVEL` when starting OpenCode.
|
||||
- `server`: use the V2 service and explicit server options; the server API is an intentional breaking change.
|
||||
- `layout`: remove it; V1 already treated it as deprecated and always used stretch layout.
|
||||
- `enabled_providers` and `disabled_providers`: there is no native provider allowlist or denylist field yet.
|
||||
- `small_model`: V2 selects models for internal maintenance agents without a separate top-level field.
|
||||
- `compaction.tail_turns`: V2 uses `compaction.keep.tokens` instead.
|
||||
|
||||
If your V1 configuration relies on a field without a native equivalent, keep using the supported V1 format rather than
|
||||
forcing a manual conversion. Run `/report` if V2 does not preserve the behavior you rely on.
|
||||
|
||||
### Agent files
|
||||
|
||||
V1 agent files may use `agent/`, `agents/`, `mode/`, or `modes/`. V2 still discovers all four directories. The preferred
|
||||
V2 location is:
|
||||
|
||||
```text
|
||||
.opencode/agents/<name>.md
|
||||
```
|
||||
|
||||
Files under a V1 `mode/` or `modes/` directory represent primary agents. When moving one into `agents/`, add
|
||||
`mode: primary` to its frontmatter. Files under `agent/` can move to `agents/` without changing their path-derived ID.
|
||||
|
||||
When converting the frontmatter to native V2 fields:
|
||||
|
||||
- Keep the Markdown body as the agent's system instructions.
|
||||
- Rename `prompt` to `system` when it appears in JSON configuration; file bodies do not need a `system` field.
|
||||
- Rename `disable` to `disabled` and `permission` to `permissions`.
|
||||
- Join `model` and `variant` as `provider/model#variant`.
|
||||
- Move `temperature`, `top_p`, and provider-specific options under `request.body`.
|
||||
|
||||
V2 translates legacy agent frontmatter automatically, so these edits are optional. See [Agents](/agents).
|
||||
|
||||
### Command files
|
||||
|
||||
V1 command files may use `command/` or `commands/`. V2 discovers both. The preferred location is:
|
||||
|
||||
```text
|
||||
.opencode/commands/<name>.md
|
||||
```
|
||||
|
||||
Move files from `command/` to the same relative path under `commands/` to preserve command names. The Markdown body remains
|
||||
the command template, and `description`, `agent`, and `subtask` frontmatter keep the same names. If frontmatter has separate
|
||||
`model` and `variant` fields, append the variant to the model and remove `variant`:
|
||||
|
||||
```yaml
|
||||
# V1
|
||||
model: anthropic/claude-sonnet-4-5
|
||||
variant: high
|
||||
|
||||
# V2
|
||||
model: anthropic/claude-sonnet-4-5#high
|
||||
```
|
||||
|
||||
See [Commands](/commands).
|
||||
|
||||
### Skill files
|
||||
|
||||
V2 discovers skills from both `.opencode/skill/` and `.opencode/skills/`. The preferred layout is:
|
||||
|
||||
```text
|
||||
.opencode/skills/<skill-id>/SKILL.md
|
||||
```
|
||||
|
||||
Move the complete skill directory, not only `SKILL.md`, so relative scripts, references, and other supporting files remain
|
||||
available. Keep the directory name stable to preserve the skill ID. Existing skill frontmatter and Markdown bodies do not
|
||||
require a V2 rewrite. See [Skills](/skills).
|
||||
|
||||
### Instruction files
|
||||
|
||||
Existing `AGENTS.md` files stay in place. V2 discovers the global `~/.config/opencode/AGENTS.md` and project `AGENTS.md`
|
||||
files from the current directory up to the project root.
|
||||
|
||||
If a V1 setup relied on a `CLAUDE.md` fallback, move that guidance into the applicable `AGENTS.md`. V2 currently only
|
||||
discovers `AGENTS.md`; because non-API V1 behavior is intended to remain compatible, also run `/report` with the affected
|
||||
project details. See [Instructions](/instructions).
|
||||
|
||||
## Plugins
|
||||
|
||||
Rename `plugin` to `plugins`. Replace a package-and-options tuple with an object:
|
||||
|
||||
```jsonc
|
||||
// V1
|
||||
{
|
||||
"plugin": [
|
||||
"opencode-example-plugin",
|
||||
["./plugin/local.ts", { "enabled": true }]
|
||||
]
|
||||
}
|
||||
|
||||
// V2
|
||||
{
|
||||
"plugins": [
|
||||
"opencode-example-plugin",
|
||||
{
|
||||
"package": "./plugin/local.ts",
|
||||
"options": { "enabled": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
V2 discovers local plugins from both `.opencode/plugin/` and `.opencode/plugins/`; use `.opencode/plugins/` for V2 files.
|
||||
Moving a file between these directories does not migrate its implementation.
|
||||
|
||||
<Warning>V1 plugins will not work in V2.</Warning>
|
||||
|
||||
The config entry can be translated automatically, but plugin implementation code must be ported to the new API. The V2
|
||||
plugin API is still being finalized during beta, and detailed plugin migration guidance will be published when it is
|
||||
ready.
|
||||
|
||||
Once the V2 plugin API is finalized, OpenCode should be able to migrate the majority of V1 plugins while keeping related
|
||||
local modules and dependencies together. See the current beta [Plugins guide](/plugins).
|
||||
|
||||
## Server API and clients
|
||||
|
||||
OpenCode 2 has a revised, more ergonomic server API and a new set of clients. Integrations that call the V1 server API
|
||||
must migrate to the V2 API.
|
||||
|
||||
Use the `@opencode-ai/client` package to access the new clients. The server API and clients are still being finalized
|
||||
during beta, so their contracts may continue to change. See the generated [API reference](/api) for the current endpoints,
|
||||
request types, and responses.
|
||||
<Warning>
|
||||
V1 plugins are not guaranteed to work with V2. The plugin API is changing during beta; review and port each plugin using
|
||||
the [V2 plugin guide](/plugins).
|
||||
</Warning>
|
||||
|
||||
## Verify your setup
|
||||
|
||||
Start `opencode2` in a project and verify your model, provider credentials, agents, permissions, MCP servers, and plugins
|
||||
before relying on the beta for regular work. Keep your V1 setup until you have confirmed the V2 behavior you need, and do
|
||||
not point V1 at configuration that you have converted to the native V2 shape.
|
||||
before relying on the beta for regular work. Keep your V1 setup and backups until you have confirmed the V2 behavior you
|
||||
need, and do not point V1 at configuration that you have converted to the native V2 shape.
|
||||
|
||||
+100
-90
@@ -1,43 +1,49 @@
|
||||
---
|
||||
title: "Models"
|
||||
description: ""
|
||||
description: "Select, configure, and customize models in OpenCode 2.0."
|
||||
---
|
||||
|
||||
OpenCode builds its model catalog from [Models.dev](https://models.dev), provider integrations, and your configuration.
|
||||
Only enabled models whose provider is available for the current project appear in the model picker.
|
||||
|
||||
Connect a provider with `/connect` in the TUI, or configure it in [Providers](/providers).
|
||||
Connect a provider with `/connect` in the TUI, or configure a provider and its credentials in `opencode.json`.
|
||||
|
||||
## Choose a model
|
||||
## Select a model
|
||||
|
||||
Open the model picker with `/models` or the default `<leader>m` keybind. The picker shows the models available from
|
||||
providers connected to the current project.
|
||||
Open the model picker with `/models` or the default `<leader>m` keybind. Use `/variants` to choose a variant for the
|
||||
current model, or press `ctrl+t` to cycle through its variants.
|
||||
|
||||
Select a model to use it in the current session. Switching models updates that session without changing your config. Use
|
||||
the catalog entries shown in the picker rather than guessing a provider or model name.
|
||||
A model reference has this form:
|
||||
|
||||
## Per-run model
|
||||
```text
|
||||
provider/model#variant
|
||||
```
|
||||
|
||||
Select a model for one non-interactive run with `--model` or `-m`:
|
||||
The variant is optional. OpenCode splits at the first `/`, so model IDs may contain additional slashes:
|
||||
|
||||
```text
|
||||
openai/gpt-5.2
|
||||
openai/gpt-5.2#high
|
||||
openrouter/anthropic/claude-sonnet-4.5#high
|
||||
```
|
||||
|
||||
Use the catalog IDs shown by `/models`, not a provider's display name. Omit `#variant` to use the model's base settings.
|
||||
|
||||
### Command line
|
||||
|
||||
Select a model for a non-interactive run with `--model` or `-m`:
|
||||
|
||||
```bash
|
||||
opencode2 run --model openai/gpt-5.2 "Explain this repository"
|
||||
opencode2 run -m openai/gpt-5.2#high "Review the current changes"
|
||||
```
|
||||
|
||||
Agents and commands can also select their own model. See [Agents](/agents) and [Commands](/commands).
|
||||
<Note>
|
||||
`opencode2 run` accepts `provider/model#variant`. The current `opencode2 mini --model` option accepts only
|
||||
`provider/model`; choose its variant from the interactive interface.
|
||||
</Note>
|
||||
|
||||
## Variants
|
||||
|
||||
Variants are named request overlays for one model, commonly used for reasoning effort or token budgets. Available names
|
||||
are model-specific and are derived from current catalog metadata. Do not assume that names such as `low`, `high`, or
|
||||
`max` exist for every model; `/variants` shows the valid choices.
|
||||
|
||||
Use `/variants` to choose one for the current model, or press `ctrl+t` to cycle through available variants.
|
||||
|
||||
## Configure
|
||||
|
||||
### Default model
|
||||
## Set the default
|
||||
|
||||
Set `model` in `opencode.json` or `opencode.jsonc`:
|
||||
|
||||
@@ -48,13 +54,81 @@ Set `model` in `opencode.json` or `opencode.jsonc`:
|
||||
}
|
||||
```
|
||||
|
||||
The explicit object form is equivalent:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"model": {
|
||||
"providerID": "openrouter",
|
||||
"model": "anthropic/claude-sonnet-4.5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Root, agent, and command `model` fields accept these same selection forms. See [Config](/config) for configuration
|
||||
locations and precedence.
|
||||
|
||||
The configured model becomes the catalog default when its provider is available and the model is enabled. Otherwise,
|
||||
session execution falls back to the newest available supported model. An explicit model already selected on a session
|
||||
takes precedence over the default; switching models changes that session and does not rewrite your config.
|
||||
|
||||
See [Config](/config) for configuration locations and precedence.
|
||||
## Variants
|
||||
|
||||
### Model settings
|
||||
Variants are named request overlays for one model, commonly used for reasoning effort or token budgets. Available names
|
||||
are model-specific and are derived from current catalog metadata. Do not assume that names such as `low`, `high`, or
|
||||
`max` exist for every model; `/variants` shows the valid choices.
|
||||
|
||||
Add a variant, or override a catalog variant with the same ID, under the model's `variants` array:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"settings": {
|
||||
"reasoningEffort": "medium"
|
||||
},
|
||||
"variants": [
|
||||
{
|
||||
"id": "fast",
|
||||
"settings": {
|
||||
"reasoningEffort": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deep",
|
||||
"settings": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"deep-review": {
|
||||
"description": "Review with high reasoning effort",
|
||||
"template": "Review the current changes for correctness and missing tests.",
|
||||
"model": "openai/gpt-5.2#deep"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Variant entries support `settings`, `headers`, and `body`. Selecting one deeply overlays its values on the effective
|
||||
provider and model configuration. An unknown variant fails model resolution instead of silently using the base model.
|
||||
|
||||
<Warning>
|
||||
V2 uses `providers` (plural) and an array of `{ "id": "..." }` variant entries. The V1 `provider` key and
|
||||
object-shaped `variants` configuration are not the V2 format.
|
||||
</Warning>
|
||||
|
||||
## Configure a model
|
||||
|
||||
Provider and model entries can supply three kinds of request configuration:
|
||||
|
||||
@@ -98,46 +172,7 @@ Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2
|
||||
model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and
|
||||
enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog.
|
||||
|
||||
### Custom variants
|
||||
|
||||
Add a variant, or override a catalog variant with the same ID, under the model's `variants` array:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"settings": {
|
||||
"reasoningEffort": "medium"
|
||||
},
|
||||
"variants": [
|
||||
{
|
||||
"id": "fast",
|
||||
"settings": {
|
||||
"reasoningEffort": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deep",
|
||||
"settings": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Variant entries support `settings`, `headers`, and `body`. Selecting one deeply overlays its values on the effective
|
||||
provider and model configuration. An unknown variant fails model resolution instead of silently using the base model.
|
||||
|
||||
### Local models
|
||||
## Local and compatible models
|
||||
|
||||
For an OpenAI-compatible server, define a provider package, endpoint, and at least one model:
|
||||
|
||||
@@ -175,34 +210,9 @@ Use the server's real model name, limits, modalities, and tool support. OpenCode
|
||||
manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as
|
||||
`"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets.
|
||||
|
||||
### Model references
|
||||
|
||||
Configuration and CLI options identify a model as `provider/model`, with an optional `#variant`:
|
||||
|
||||
```text
|
||||
openai/gpt-5.2
|
||||
openai/gpt-5.2#high
|
||||
openrouter/anthropic/claude-sonnet-4.5#high
|
||||
```
|
||||
|
||||
OpenCode splits the reference at the first `/`, so model IDs may contain additional slashes. Provider and model IDs are
|
||||
case-sensitive. Provider IDs cannot contain `/` or `#`, and model IDs cannot contain `#`.
|
||||
|
||||
The expanded config form is equivalent when generated or programmatic configuration is more convenient:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"model": {
|
||||
"providerID": "openrouter",
|
||||
"model": "anthropic/claude-sonnet-4.5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Root, agent, and command `model` fields accept both forms. Use the IDs shown by `/models`, not provider display names.
|
||||
|
||||
### Caveats
|
||||
## Caveats
|
||||
|
||||
- Provider and model IDs are case-sensitive. Provider IDs cannot contain `/` or `#`; model IDs cannot contain `#`.
|
||||
- The selector object uses `model`, while a provider catalog entry uses `modelID` for the upstream API identifier.
|
||||
- The root `model` currently sets the default provider and model only. Although its selection shape accepts a variant,
|
||||
the V2 catalog default does not retain it; select a variant in the TUI, with `opencode2 run`, or on an agent or command.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "Permissions"
|
||||
description: ""
|
||||
description: "Configure ordered V2 rules for tool access, approvals, and agent overrides."
|
||||
---
|
||||
|
||||
Permissions control whether an agent may perform an action on a resource. V2
|
||||
configuration uses the `permissions` field and an ordered array of rules.
|
||||
configuration uses the plural `permissions` field and an ordered array of rules.
|
||||
|
||||
<Warning>
|
||||
The V1 object syntax uses different field and action names. Do not use
|
||||
|
||||
@@ -22,7 +22,7 @@ function.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add ordered entries to the `plugins` field in `opencode.json(c)`:
|
||||
Add ordered entries to the plural `plugins` field in `opencode.json(c)`:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -57,19 +57,21 @@ precedence rather than replacing the entire array.
|
||||
|
||||
### Local discovery
|
||||
|
||||
OpenCode automatically scans this directory in every discovered OpenCode config
|
||||
directory:
|
||||
OpenCode automatically scans both of these directories in every discovered
|
||||
OpenCode config directory:
|
||||
|
||||
```text
|
||||
.opencode/plugin/
|
||||
.opencode/plugins/
|
||||
```
|
||||
|
||||
The equivalent global directory is `~/.config/opencode/plugins/`. Direct `.ts`
|
||||
and `.js` children are loaded. An immediate child directory is also loaded as a
|
||||
The equivalent global directories are
|
||||
`~/.config/opencode/plugin/` and `~/.config/opencode/plugins/`. Direct `.ts` and
|
||||
`.js` children are loaded. An immediate child directory is also loaded as a
|
||||
package when OpenCode can resolve a string `exports`, `module`, or `main`
|
||||
entrypoint, or an `index.ts` or `index.js` file.
|
||||
|
||||
A `plugins/` directory beside a project-root `opencode.json` is not discovered
|
||||
A `plugin/` directory beside a project-root `opencode.json` is not discovered
|
||||
automatically. Put it under `.opencode/`, or add its file explicitly with a
|
||||
relative config entry.
|
||||
|
||||
@@ -84,7 +86,7 @@ applied in order:
|
||||
"plugins": [
|
||||
"-opencode.provider.*",
|
||||
"opencode.provider.openai",
|
||||
"-./plugins/old.ts",
|
||||
"-./plugin/old.ts",
|
||||
"-*",
|
||||
"./plugins/only-this-one.ts"
|
||||
]
|
||||
|
||||
+78
-10
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Providers"
|
||||
description: ""
|
||||
description: "Connect LLM providers and configure endpoints, packages, models, and variants."
|
||||
---
|
||||
|
||||
OpenCode builds its provider and model catalog from [Models.dev](https://models.dev), then applies the `providers`
|
||||
@@ -57,7 +57,7 @@ When several credential sources exist, OpenCode uses the stored credential first
|
||||
|
||||
<Warning>Do not commit API keys or authorization headers to your repository.</Warning>
|
||||
|
||||
## Configure
|
||||
## Configure providers
|
||||
|
||||
The `providers` object is keyed by provider ID. Each provider accepts these fields:
|
||||
|
||||
@@ -75,7 +75,7 @@ Configuration files are applied from lowest to highest precedence. `settings` an
|
||||
merged case-insensitively. At request time, provider values are inherited by the model, model values override them, and
|
||||
the selected variant is applied last.
|
||||
|
||||
### Endpoint
|
||||
### Custom endpoint
|
||||
|
||||
Override `settings.baseURL` to send an existing provider through a proxy or compatible endpoint. Its existing package,
|
||||
models, and connection continue to apply:
|
||||
@@ -95,7 +95,7 @@ models, and connection continue to apply:
|
||||
|
||||
`settings` is package-specific. A field only has an effect when the selected package supports it.
|
||||
|
||||
### Headers and body
|
||||
### Custom headers and body
|
||||
|
||||
Headers and body fields can be set at provider, model, or variant scope:
|
||||
|
||||
@@ -127,7 +127,7 @@ Headers and body fields can be set at provider, model, or variant scope:
|
||||
These are request overlays, not a generic authentication scheme. Prefer `/connect`, `env`, or `settings.apiKey` for
|
||||
provider credentials unless the endpoint explicitly requires a custom header.
|
||||
|
||||
### Provider packages
|
||||
## Custom providers and packages
|
||||
|
||||
For an OpenAI-compatible service, use the V2 native compatible package. The model map is explicit because a custom
|
||||
provider has no Models.dev catalog entries:
|
||||
@@ -179,10 +179,10 @@ not validate package-specific keys.
|
||||
|
||||
`package` may also be set on one model to override the provider package for that model.
|
||||
|
||||
### Models
|
||||
## Models
|
||||
|
||||
Add a model under a provider's `models` map. The object key is the model ID used in OpenCode; `modelID` is the ID sent to
|
||||
the provider:
|
||||
`models` adds a model or overlays an existing catalog model. The object key is the model ID used in OpenCode. Set
|
||||
`modelID` when the upstream API expects a different ID:
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
@@ -193,7 +193,13 @@ the provider:
|
||||
"models": {
|
||||
"coding": {
|
||||
"modelID": "gpt-5.2",
|
||||
"name": "GPT-5.2 Coding"
|
||||
"name": "GPT-5.2 Coding",
|
||||
"family": "gpt-5",
|
||||
"limit": {
|
||||
"context": 200000,
|
||||
"input": 180000,
|
||||
"output": 32000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,4 +207,66 @@ the provider:
|
||||
}
|
||||
```
|
||||
|
||||
See [Models](/models) for model selection, defaults, capabilities, limits, costs, and variants.
|
||||
A model supports `modelID`, `family`, `name`, `package`, `settings`, `headers`, `body`, `capabilities`, `variants`,
|
||||
`cost`, `disabled`, and `limit`. If supplied, `capabilities` requires `tools`, `input`, and `output`. `limit` may set
|
||||
`context`, `input`, and `output`. Cost values are USD per million tokens:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"cost": {
|
||||
"input": 3,
|
||||
"output": 15,
|
||||
"cache": {
|
||||
"read": 0.3,
|
||||
"write": 3.75
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `disabled: true` on a model to remove it from the available model list. V2 does not define provider-level
|
||||
whitelist or blacklist fields.
|
||||
|
||||
## Variants
|
||||
|
||||
Variants are named request overlays for one model. They can override `settings`, `headers`, and `body`; the selected
|
||||
package determines which values are meaningful.
|
||||
|
||||
```jsonc title="opencode.jsonc"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"providers": {
|
||||
"openai": {
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"variants": [
|
||||
{
|
||||
"id": "fast",
|
||||
"settings": {
|
||||
"reasoningEffort": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deep",
|
||||
"settings": {
|
||||
"reasoningEffort": "high",
|
||||
"reasoningSummary": "auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Choose a variant with the TUI variant picker or `/variants`. Explicit CLI, agent, and command model references use
|
||||
`provider/model#variant`; for example:
|
||||
|
||||
```bash
|
||||
opencode2 run --model openai/gpt-5.2#deep "Review this project"
|
||||
```
|
||||
|
||||
The current V2 top-level `model` default does not retain a `#variant` selection, so choose the variant separately.
|
||||
Selecting an ID that is not defined for the model fails instead of silently using the default request settings.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "References"
|
||||
description: ""
|
||||
description: "Make local directories and Git repositories available as project context."
|
||||
---
|
||||
|
||||
References give OpenCode named access to directories outside the current
|
||||
|
||||
@@ -1,75 +1,4 @@
|
||||
---
|
||||
title: "SDK"
|
||||
description: "Embed an OpenCode host in an Effect application."
|
||||
description: "Build with OpenCode."
|
||||
---
|
||||
|
||||
`@opencode-ai/sdk-next` is the Effect-native SDK for applications that need to
|
||||
host OpenCode in-process. Unlike the [network client](/client), it assembles the
|
||||
OpenCode server and routes API calls through its HTTP router in memory. It opens
|
||||
no HTTP listener and adds no network hop between the client and server.
|
||||
|
||||
<Warning>
|
||||
The V2 SDK is beta and currently private to the OpenCode workspace. It is not
|
||||
published for external installation yet, and its package name and API may
|
||||
change before release.
|
||||
</Warning>
|
||||
|
||||
## Create a host
|
||||
|
||||
`OpenCode.create()` creates a scoped host. Closing its Effect Scope releases
|
||||
the router, location services, fibers, and scoped plugin registrations.
|
||||
|
||||
```ts
|
||||
import {
|
||||
AbsolutePath,
|
||||
Location,
|
||||
OpenCode,
|
||||
} from "@opencode-ai/sdk-next"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const program = Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.create()
|
||||
const session = yield* opencode.sessions.create({
|
||||
location: Location.Ref.make({
|
||||
directory: AbsolutePath.make("/workspace"),
|
||||
}),
|
||||
})
|
||||
|
||||
return yield* opencode.sessions.get({ sessionID: session.id })
|
||||
}),
|
||||
)
|
||||
|
||||
const session = await Effect.runPromise(program)
|
||||
```
|
||||
|
||||
The embedded host uses the same routes, middleware, codecs, errors, and schema
|
||||
values as `@opencode-ai/client/effect`. It exposes the full generated client and
|
||||
adds the convenience aliases `sessions` and `events` for the session and event
|
||||
groups.
|
||||
|
||||
## Use as a service
|
||||
|
||||
Use `OpenCode.layer` when the host should be provided through Effect dependency
|
||||
injection:
|
||||
|
||||
```ts
|
||||
import { OpenCode } from "@opencode-ai/sdk-next"
|
||||
import { Effect } from "effect"
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.Service
|
||||
return yield* opencode.sessions.active()
|
||||
})
|
||||
|
||||
const active = await Effect.runPromise(
|
||||
program.pipe(Effect.provide(OpenCode.layer)),
|
||||
)
|
||||
```
|
||||
|
||||
## Register plugins
|
||||
|
||||
Call `opencode.plugin(...)` to register an embedded V2 plugin. Embedded plugins
|
||||
use the same discovery and location-scoped activation path as configured
|
||||
plugins. The SDK also exports `Tool` for plugin-defined tools. See the
|
||||
[Plugins guide](/plugins) for the plugin shape and available hooks.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Session sharing"
|
||||
description: ""
|
||||
description: "Understand the current beta status of session sharing in OpenCode V2."
|
||||
---
|
||||
|
||||
Session sharing is not yet available in OpenCode V2. V2 does not currently
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Skills"
|
||||
description: ""
|
||||
description: "Add reusable, on-demand instructions to OpenCode."
|
||||
---
|
||||
|
||||
Skills are Markdown instructions that OpenCode can advertise to an agent and
|
||||
@@ -45,9 +45,9 @@ OpenCode automatically adds the following source directories:
|
||||
|
||||
| Scope | Sources |
|
||||
| --- | --- |
|
||||
| Global | `~/.config/opencode/skills` |
|
||||
| Global | `~/.config/opencode/skill`, `~/.config/opencode/skills` |
|
||||
| Global compatibility | `~/.claude/skills`, `~/.agents/skills` |
|
||||
| Project | `.opencode/skills` |
|
||||
| Project | `.opencode/skill`, `.opencode/skills` |
|
||||
| Project compatibility | `.claude/skills`, `.agents/skills` |
|
||||
|
||||
For project sources, OpenCode searches from the current directory upward to
|
||||
@@ -166,11 +166,12 @@ wins. Sources are registered in this order, from lower to higher precedence:
|
||||
1. Built-in skills
|
||||
2. `.claude/skills` sources, global first and then from the current directory upward
|
||||
3. `.agents/skills` sources, global first and then from the current directory upward
|
||||
4. `~/.config/opencode/skills`
|
||||
5. Project `.opencode/skills`, from the project root toward the current directory
|
||||
4. `~/.config/opencode/skill` and `~/.config/opencode/skills`
|
||||
5. Project `.opencode/skill` and `.opencode/skills`, from the project root toward the current directory
|
||||
6. Explicit `skills` config entries, in config priority and array order
|
||||
|
||||
Avoid duplicate IDs unless an override is intentional.
|
||||
Within one `.opencode` directory, `skills` has precedence over `skill`. Avoid
|
||||
duplicate IDs unless an override is intentional.
|
||||
|
||||
## Runtime loading
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Undo"
|
||||
description: ""
|
||||
title: "Snapshots and undo"
|
||||
description: "Understand filesystem snapshots, undo, redo, and message reverts in OpenCode V2."
|
||||
---
|
||||
|
||||
OpenCode snapshots let the default interactive TUI roll back conversation history and related file changes. They are a
|
||||
@@ -104,5 +104,6 @@ Review the staged file summary and your Git diff before continuing. Commit or ba
|
||||
using undo on a dirty worktree.
|
||||
|
||||
<Note>
|
||||
`/undo` and `/redo` are interactive TUI commands. The non-interactive `run` command does not provide them.
|
||||
`/undo` and `/redo` are interactive commands in the default V2 TUI. The non-interactive `run` command and the minimal
|
||||
interactive interface do not provide these slash commands.
|
||||
</Note>
|
||||
|
||||
Reference in New Issue
Block a user