Summarize the task tool output above and continue with your task.
task_id: ses_3cbe7a666ffeUFS4vpGVcAY1sB (for resuming to continue this task if needed)
<task_result>
I've added a `hidden_providers` configuration option that hides providers from the model select menu while keeping them functional.
**Usage** - Add to your `opencode.json`:
```json
{
"hidden_providers": ["openrouter", "vercel"]
}
```
**What this hides:**
- Model select menu (TUI)
- Provider list dialog
- `opencode models` CLI output
**What still works:**
- Direct model configuration in `opencode.json`
- `--model` CLI flag
- API calls
**Difference from `disabled_providers`:**
- `disabled_providers`: Provider cannot be used at all
- `hidden_providers`: Provider only hidden from UI menus, still functional when configured directly
The implementation filters providers in:
1. `src/config/config.ts:1043-1048` - Config schema
2. `src/server/routes/provider.ts:41,45` - Provider list API
3. `src/cli/cmd/models.ts:39,67` - CLI models command
</task_result>Summarize the task tool output above and continue with your task.
Added hidden_providers config option to hide providers from UI menus while keeping them functional.
Key points:
opencode.json: {"hidden_providers": ["openrouter", "vercel"]}opencode models output--model flag, API callsdisabled_providers (which completely disables provider)Modified files:
src/config/config.ts:1043-1048 - Schemasrc/server/routes/provider.ts:41,45 - API filteringsrc/cli/cmd/models.ts:39,67 - CLI filteringHow can I use one of the models like morph/morph-v3-fast from openrouter, as a replacement editing tool? What about embeddings models, can they be used as tools or within plugins in opencode?
No files foundFound 3 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","name":"Privatemode AI","doc":"https://docs.privatemode.ai/api/overview","models":{"whisper-large-v3":{"id":"whisper-large-v3","name":"Whisper large-v3","family":"whisper","attachment":true,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2023-09","release_date":"2023-09-01","last_updated":"2023-09-01","modalities":{"input":["audio"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":0,"output":4096}},"qwen3-embedding-4b":{"id":"qwen3-embedding-4b","name":"Qwen3-Embedding 4B","family":"qwen","attachment":false,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2025-06","release_date":"2025-06-06","last_updated":"2025-06-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":32000,"output":2560}},"gpt-oss-120b":{"id":"gpt-oss-120b","name":"gpt-oss-120b","family":"gpt-oss","attachment":false,"reasoning":true,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2025-08","release_date":"2025-08-04","last_updated":"2025-08-14","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":128000}},"gemma-3-27b":{"id":"gemma-3-27b","name":"Gemma 3 27B","family":"gemma","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2024-08","release_date":"2025-03-12","last_updated":"2025-03-12","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":8192}},"qwen3-coder-30b-a3...Found 22 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 205: // Backwards compatibility: legacy top-level `tools` config
Line 732: // Convert legacy tools config to permissions
/home/jm/data/code/opencode/packages/opencode/src/session/prompt.ts:
Line 1275: Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
/home/jm/data/code/opencode/packages/opencode/test/config/config.test.ts:
Line 976: test("migrates legacy tools config to permissions - allow", async () => {
Line 1007: test("migrates legacy tools config to permissions - deny", async () => {
Line 1234: test("migrates mixed legacy tools config", async () => {
Line 1269: test("merges legacy tools with existing permission config", async () => {
/home/jm/data/code/opencode/packages/opencode/test/provider/transform.test.ts:
Line 520: test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
/home/jm/data/code/opencode/packages/opencode/src/tool/batch.ts:
Line 40: const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" })
/home/jm/data/code/opencode/packages/opencode/test/agent/agent.test.ts:
Line 407: test("legacy tools config converts to permissions", async () => {
Line 430: test("legacy tools config maps write/edit/patch/multiedit to edit permission", async () => {
/home/jm/data/code/opencode/packages/opencode/src/agent/agent.ts:
Line 79: description: "The default agent. Executes tools based on configured permissions.",
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMOD...<file>
00201| if (Flag.OPENCODE_PERMISSION) {
00202| result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
00203| }
00204|
00205| // Backwards compatibility: legacy top-level `tools` config
00206| if (result.tools) {
00207| const perms: Record<string, Config.PermissionAction> = {}
00208| for (const [tool, enabled] of Object.entries(result.tools)) {
00209| const action: Config.PermissionAction = enabled ? "allow" : "deny"
00210| if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
00211| perms.edit = action
00212| continue
00213| }
00214| perms[tool] = action
00215| }
00216| result.permission = mergeDeep(perms, result.permission ?? {})
00217| }
00218|
00219| if (!result.username) result.username = os.userInfo().username
00220|
00221| // Handle migration from autoshare to share field
00222| if (result.autoshare === true && !result.share) {
00223| result.share = "auto"
00224| }
00225|
00226| if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
00227|
00228| // Apply flag overrides for compaction settings
00229| if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
00230| result.compaction = { ...result.compaction, auto: false }
00231| }
00232| if (Flag.OPENCODE_DISABLE_PRUNE) {
00233| result.compaction = { ...result.compaction, prune: false }
00234| }
00235|
00236| result.plugin = deduplicatePlugins(result.plugin ?? [])
00237|
00238| return {
00239| config: result,
00240| directories,
00241| deps,
00242| }
00243| })
00244|
00245| export async function waitForDependencies() {
00246| const deps = await state().then((x) => x.deps)
00247| await Promise.all(deps)
00248| }
00249|
00250| export async function installDependencies(dir: string) {
00251| const pkg = path.join(dir, "package...Found 100 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/transform.ts:
Line 75: if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
Line 78: toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"),
Line 99: if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) {
Line 101: const normalizedId = part.toolCallId
Line 108: toolCallId: normalizedId,
/home/jm/data/code/opencode/packages/opencode/test/provider/provider.test.ts:
Line 225: tool_call: true,
Line 428: tool_call: true,
Line 467: tool_call: true,
Line 655: tool_call: true,
Line 694: tool_call: true,
Line 850: tool_call: true,
Line 887: tool_call: true,
Line 1032: tool_call: true,
Line 1107: tool_call: true,
Line 1149: tool_call: true,
Line 1229: tool_call: true,
Line 1297: test("model with tool_call false", async () => {
Line 1312: tool_call: false,
Line 1332: test("model defaults tool_call to true when not specified", async () => {
Line 1347: // tool_call not specified
Line 1382: tool_call: true,
Line 1425: tool_call: true,
Line 1491: tool_call: true,
Line 1674: tool_call: true,
Line 1745: tool_call: true,
Line 2089: tool_call: true,
/home/jm/data/code/opencode/packages/opencode/src/session/prompt.ts:
Line 663: callID: options.toolCallId,
Line 668: const match = input.processor.partFromToolCall(options.toolCallId)
Line 688: tool: { messageID: input.processor.message.id, callID: options.toolCallId },
Line 746: callID: ...Found 42 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/transform.ts:
Line 38: case "@openrouter/ai-sdk-provider":
Line 39: return "openrouter"
Line 201: openrouter: {
Line 367: if (model.api.npm === "@openrouter/ai-sdk-provider") {
Line 381: case "@openrouter/ai-sdk-provider":
Line 620: if (input.model.api.npm === "@openrouter/ai-sdk-provider") {
Line 726: if (model.providerID === "openrouter") {
/home/jm/data/code/opencode/packages/opencode/test/provider/provider.test.ts:
Line 359: const result = Provider.parseModel("openrouter/anthropic/claude-3-opus")
Line 360: expect(result.providerID).toBe("openrouter")
Line 1777: openrouter: {
Line 1797: expect(providers["openrouter"]).toBeDefined()
Line 1800: const intellect = providers["openrouter"].models["prime-intellect/intellect-3"]
Line 1802: expect(intellect.api.url).toBe("https://openrouter.ai/api/v1")
Line 1805: const deepseek = providers["openrouter"].models["deepseek/deepseek-r1-0528"]
Line 1807: expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1")
/home/jm/data/code/opencode/packages/opencode/test/provider/transform.test.ts:
Line 1428: describe("@openrouter/ai-sdk-provider", () => {
Line 1431: id: "openrouter/test-model",
Line 1432: providerID: "openrouter",
Line 1435: url: "https://openrouter.ai",
Line 1436: npm: "@openrouter/ai-sdk-provider",
Line 1445: id: "openrouter/gpt-4",
Line 1446: providerID: "openrouter",
Line 1449: url: "https://openrouter.ai",
Line 1450: npm: "@openrouter/ai-sdk-provider",
Line 1461: id: "openrouter/gemini-3-5-pro",
Line 1462: providerID: "openrouter",
Line 1465: url: "https://openrouter.ai",
Line 1466: npm: "@openrouter/ai-sdk-provider",
Line 1475: id: "openrouter/grok-4",
Line 1476: pr...Found 1 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","name":"Privatemode AI","doc":"https://docs.privatemode.ai/api/overview","models":{"whisper-large-v3":{"id":"whisper-large-v3","name":"Whisper large-v3","family":"whisper","attachment":true,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2023-09","release_date":"2023-09-01","last_updated":"2023-09-01","modalities":{"input":["audio"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":0,"output":4096}},"qwen3-embedding-4b":{"id":"qwen3-embedding-4b","name":"Qwen3-Embedding 4B","family":"qwen","attachment":false,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2025-06","release_date":"2025-06-06","last_updated":"2025-06-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":32000,"output":2560}},"gpt-oss-120b":{"id":"gpt-oss-120b","name":"gpt-oss-120b","family":"gpt-oss","attachment":false,"reasoning":true,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2025-08","release_date":"2025-08-04","last_updated":"2025-08-14","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":128000}},"gemma-3-27b":{"id":"gemma-3-27b","name":"Gemma 3 27B","family":"gemma","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2024-08","release_date":"2025-03-12","last_updated":"2025-03-12","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":8192}},"qwen3-coder-30b-a3...<file>
00001| import z from "zod"
00002| import { Tool } from "./tool"
00003| import DESCRIPTION from "./batch.txt"
00004|
00005| const DISALLOWED = new Set(["batch"])
00006| const FILTERED_FROM_SUGGESTIONS = new Set(["invalid", "patch", ...DISALLOWED])
00007|
00008| export const BatchTool = Tool.define("batch", async () => {
00009| return {
00010| description: DESCRIPTION,
00011| parameters: z.object({
00012| tool_calls: z
00013| .array(
00014| z.object({
00015| tool: z.string().describe("The name of the tool to execute"),
00016| parameters: z.object({}).loose().describe("Parameters for the tool"),
00017| }),
00018| )
00019| .min(1, "Provide at least one tool call")
00020| .describe("Array of tool calls to execute in parallel"),
00021| }),
00022| formatValidationError(error) {
00023| const formattedErrors = error.issues
00024| .map((issue) => {
00025| const path = issue.path.length > 0 ? issue.path.join(".") : "root"
00026| return ` - ${path}: ${issue.message}`
00027| })
00028| .join("\n")
00029|
00030| return `Invalid parameters for tool 'batch':\n${formattedErrors}\n\nExpected payload format:\n [{"tool": "tool_name", "parameters": {...}}, {...}]`
00031| },
00032| async execute(params, ctx) {
00033| const { Session } = await import("../session")
00034| const { Identifier } = await import("../id/id")
00035|
00036| const toolCalls = params.tool_calls.slice(0, 25)
00037| const discardedCalls = params.tool_calls.slice(25)
00038|
00039| const { ToolRegistry } = await import("./registry")
00040| const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" })
00041| const toolMap = new Map(availableTools.map((t) => [t.id, t]))
00042|
00043| const executeCall = async (call: (typeof toolCalls)[0]) => {
00044| const callStartTime = D...Found 23 matches
/home/jm/data/code/opencode/packages/opencode/src/tool/astgrep.ts:
Line 7: export const AstGrepTool = Tool.define("astgrep", {
/home/jm/data/code/opencode/packages/opencode/src/tool/websearch.ts:
Line 40: export const WebSearchTool = Tool.define("websearch", async () => {
/home/jm/data/code/opencode/packages/opencode/src/tool/webfetch.ts:
Line 11: export const WebFetchTool = Tool.define("webfetch", {
/home/jm/data/code/opencode/packages/opencode/src/tool/task.ts:
Line 28: export const TaskTool = Tool.define("task", async (ctx) => {
/home/jm/data/code/opencode/packages/opencode/src/tool/skill.ts:
Line 10: export const SkillTool = Tool.define("skill", async (ctx) => {
/home/jm/data/code/opencode/packages/opencode/src/tool/read.ts:
Line 17: export const ReadTool = Tool.define("read", {
/home/jm/data/code/opencode/packages/opencode/src/tool/codesearch.ts:
Line 36: export const CodeSearchTool = Tool.define("codesearch", {
/home/jm/data/code/opencode/packages/opencode/src/tool/batch.ts:
Line 8: export const BatchTool = Tool.define("batch", async () => {
/home/jm/data/code/opencode/packages/opencode/src/tool/bash.ts:
Line 55: export const BashTool = Tool.define("bash", async () => {
/home/jm/data/code/opencode/packages/opencode/src/tool/write.ts:
Line 19: export const WriteTool = Tool.define("write", {
/home/jm/data/code/opencode/packages/opencode/src/tool/ls.ts:
Line 38: export const ListTool = Tool.define("list", {
/home/jm/data/code/opencode/packages/opencode/src/tool/grep.ts:
Line 12: export const GrepTool = Tool.define("grep", {
/home/jm/data/code/opencode/packages/opencode/src/tool/glob.ts:
Line 9: export const GlobTool = Tool.define("glob", {
/home/jm/data/code/opencode/packages/opencode/src/tool/edit.ts:
Line 27: export const EditTool = Tool.define("edit", {
/home/jm/data/code/opencode/packages/opencode/src/tool/apply_patch.ts:
Line 21: export const ApplyPatchTool = Tool.define("apply_patch", {
/home/jm/...Found 24 matches
/home/jm/data/code/opencode/packages/opencode/test/provider/provider.test.ts:
Line 746: expect(model.capabilities.toolcall).toBe(true)
Line 1297: test("model with tool_call false", async () => {
Line 1327: expect(providers["no-tools"].models["basic-model"].capabilities.toolcall).toBe(false)
Line 1332: test("model defaults tool_call to true when not specified", async () => {
Line 1362: expect(providers["default-tools"].models["model"].capabilities.toolcall).toBe(true)
/home/jm/data/code/opencode/packages/opencode/src/tool/batch.ts:
Line 40: const availableTools = await ToolRegistry.tools({ modelID: "", providerID: "" })
/home/jm/data/code/opencode/packages/opencode/src/provider/provider.ts:
Line 660: toolcall: model.tool_call,
Line 788: toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","name":"Privatemode AI","doc":"https://docs.privatemode.ai/api/overview","models":{"whisper-large-v3":{"id":"whisper-large-v3","name":"Whisper large-v3","family":"whisper","attachment":true,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2023-09","release_date":"2023-09-01","last_updated":"2023-09-01","modalities":{"input":["audio"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":0,"output":4096}},"qwen3-embedding-4b":{"id":"qwen3-embedding-4b","name":"Qwen3-Embedding 4B","family":"qwen","attachment":false,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2025-06","release_date":"2025-06-06","last_updated":"2025-06-06","modalities":{"input":["text"],"output":["text"]},"open_weights":tru...Found 1 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","name":"Privatemode AI","doc":"https://docs.privatemode.ai/api/overview","models":{"whisper-large-v3":{"id":"whisper-large-v3","name":"Whisper large-v3","family":"whisper","attachment":true,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2023-09","release_date":"2023-09-01","last_updated":"2023-09-01","modalities":{"input":["audio"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":0,"output":4096}},"qwen3-embedding-4b":{"id":"qwen3-embedding-4b","name":"Qwen3-Embedding 4B","family":"qwen","attachment":false,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2025-06","release_date":"2025-06-06","last_updated":"2025-06-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":32000,"output":2560}},"gpt-oss-120b":{"id":"gpt-oss-120b","name":"gpt-oss-120b","family":"gpt-oss","attachment":false,"reasoning":true,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2025-08","release_date":"2025-08-04","last_updated":"2025-08-14","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":128000}},"gemma-3-27b":{"id":"gemma-3-27b","name":"Gemma 3 27B","family":"gemma","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2024-08","release_date":"2025-03-12","last_updated":"2025-03-12","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":8192}},"qwen3-coder-30b-a3...<file>
00901| .describe("Delete word backward in input"),
00902| history_previous: z.string().optional().default("up").describe("Previous history item"),
00903| history_next: z.string().optional().default("down").describe("Next history item"),
00904| session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
00905| session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
00906| session_parent: z.string().optional().default("<leader>up").describe("Go to parent session"),
00907| terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
00908| terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
00909| tips_toggle: z.string().optional().default("<leader>h").describe("Toggle tips on home screen"),
00910| display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"),
00911| })
00912| .strict()
00913| .meta({
00914| ref: "KeybindsConfig",
00915| })
00916|
00917| export const TUI = z.object({
00918| scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"),
00919| scroll_acceleration: z
00920| .object({
00921| enabled: z.boolean().describe("Enable scroll acceleration"),
00922| })
00923| .optional()
00924| .describe("Scroll acceleration settings"),
00925| diff_style: z
00926| .enum(["auto", "stacked"])
00927| .optional()
00928| .describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
00929| })
00930|
00931| export const Server = z
00932| .object({
00933| port: z.number().int().positive().optional().describe("Port to listen on"),
00934| hostname: z.string().optional().describe("Hostname to listen on"),
00935| mdns: z.boolean...Found 100 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 70: // 2) Global config (~/.config/opencode/opencode.json{,c})
Line 72: // 4) Project config (opencode.json{,c})
Line 73: // 5) .opencode directories (.opencode/agents/, .opencode/commands/, .opencode/plugins/, .opencode/opencode.json{,c})
Line 108: for (const file of ["opencode.jsonc", "opencode.json"]) {
Line 152: for (const file of ["opencode.jsonc", "opencode.json"]) {
Line 186: for (const file of ["opencode.jsonc", "opencode.json"]) {
Line 494: * 2. Local opencode.json
Line 496: * 4. Global opencode.json
Line 676: .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
Line 1047: "Hide providers from the model select menu and provider list. Providers remain functional when configured directly.",
Line 1091: .describe("Custom provider configurations and model overrides"),
Line 1204: mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
Line 1205: mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
Line 1375: const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
/home/jm/data/code/opencode/packages/opencode/test/provider/provider.test.ts:
Line 13: path.join(dir, "opencode.json"),
Line 40: path.join(dir, "opencode.json"),
Line 67: path.join(dir, "opencode.json"),
Line 91: path.join(dir, "opencode.json"),
Line 117: path.join(dir, "opencode.json"),
Line 148: path.join(dir, "opencode.json"),
Line 174: test("custom model alias via config", async () => {
Line 178: path.join(dir, "opencode.json"),
Line 213: path.join(dir, "opencode.json"),
Line 256: path.join(dir, "opencode.json"),
Line 288: path.join(dir, "opencode.json"),
Line 315: path.jo.../home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/context/theme/opencode.jsonNo files found<file>
00002| import path from "path"
00003|
00004| import { tmpdir } from "../fixture/fixture"
00005| import { Instance } from "../../src/project/instance"
00006| import { Provider } from "../../src/provider/provider"
00007| import { Env } from "../../src/env"
00008|
00009| test("provider loaded from env variable", async () => {
00010| await using tmp = await tmpdir({
00011| init: async (dir) => {
00012| await Bun.write(
00013| path.join(dir, "opencode.json"),
00014| JSON.stringify({
00015| $schema: "https://opencode.ai/config.json",
00016| }),
00017| )
00018| },
00019| })
00020| await Instance.provide({
00021| directory: tmp.path,
00022| init: async () => {
00023| Env.set("ANTHROPIC_API_KEY", "test-api-key")
00024| },
00025| fn: async () => {
00026| const providers = await Provider.list()
00027| expect(providers["anthropic"]).toBeDefined()
00028| // Provider should retain its connection source even if custom loaders
00029| // merge additional options.
00030| expect(providers["anthropic"].source).toBe("env")
00031| expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined()
00032| },
00033| })
00034| })
00035|
00036| test("provider loaded from config with apiKey option", async () => {
00037| await using tmp = await tmpdir({
00038| init: async (dir) => {
00039| await Bun.write(
00040| path.join(dir, "opencode.json"),
00041| JSON.stringify({
00042| $schema: "https://opencode.ai/config.json",
00043| provider: {
00044| anthropic: {
00045| options: {
00046| apiKey: "config-api-key",
00047| },
00048| },
00049| },
00050| }),
00051| )
00052| },
00053| })
00054| await Instance.provide({
00055| directory: tmp.path,
00056| fn: async () => {
00057| const providers = await Pr...Found 100 matches
/home/jm/data/code/opencode/packages/opencode/package.json:
Line 68: "@ai-sdk/provider": "2.0.1",
Line 69: "@ai-sdk/provider-utils": "3.0.20",
Line 74: "@gitlab/gitlab-ai-provider": "3.4.0",
Line 78: "@modelcontextprotocol/sdk": "1.25.2",
Line 86: "@openrouter/ai-sdk-provider": "1.5.4",
Line 97: "ai-gateway-provider": "2.3.1",
/home/jm/data/code/opencode/packages/opencode/test/tool/fixtures/models-api.json:
Line 9: "models": {
Line 104: "models": {
Line 214: "models": {
Line 579: "models": {
Line 619: "models": {
Line 661: "models": {
Line 771: "models": {
Line 1749: "models": {
Line 1888: "models": {
Line 2278: "models": {
Line 2304: "doc": "https://www.alibabacloud.com/help/en/model-studio/models",
Line 2305: "models": {
Line 2937: "doc": "https://docs.x.ai/docs/models",
Line 2938: "models": {
Line 3300: "models": {
Line 3390: "models": {
Line 4493: "doc": "https://docs.cohere.com/docs/models",
Line 4494: "models": {
Line 4616: "models": {
Line 4672: "doc": "https://console.groq.com/docs/models",
Line 4673: "models": {
Line 4964: "models": {
Line 5006: "models": {
Line 5320: "doc": "https://docs.mistral.ai/getting-started/models/",
Line 5321: "models": {
Line 5746: "models": {
Line 6604: "models": {
Line 9550: "models": {
Line 9800: "models": {
Line 9842: "doc": "https://www.alibabacloud.com/help/en/model-studio/models",
Line 9843: "models": {
Line 10815: "doc": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude",
Line 10816: "models": {
Line 10970: "models": {
Line 12191: "npm": "venice-ai-sdk-provider",
Line 12194: "models": {
Line 12643: "doc": "https://cloud.siliconflow.com/models",
Line 12644: "models": {
Line 13805: "doc": "https://docs.vivgrid.com/...<file>
00175| await using tmp = await tmpdir({
00176| init: async (dir) => {
00177| await Bun.write(
00178| path.join(dir, "opencode.json"),
00179| JSON.stringify({
00180| $schema: "https://opencode.ai/config.json",
00181| provider: {
00182| anthropic: {
00183| models: {
00184| "my-alias": {
00185| id: "claude-sonnet-4-20250514",
00186| name: "My Custom Alias",
00187| },
00188| },
00189| },
00190| },
00191| }),
00192| )
00193| },
00194| })
00195| await Instance.provide({
00196| directory: tmp.path,
00197| init: async () => {
00198| Env.set("ANTHROPIC_API_KEY", "test-api-key")
00199| },
00200| fn: async () => {
00201| const providers = await Provider.list()
00202| expect(providers["anthropic"]).toBeDefined()
00203| expect(providers["anthropic"].models["my-alias"]).toBeDefined()
00204| expect(providers["anthropic"].models["my-alias"].name).toBe("My Custom Alias")
00205| },
00206| })
00207| })
00208|
00209| test("custom provider with npm package", async () => {
00210| await using tmp = await tmpdir({
00211| init: async (dir) => {
00212| await Bun.write(
00213| path.join(dir, "opencode.json"),
00214| JSON.stringify({
00215| $schema: "https://opencode.ai/config.json",
00216| provider: {
00217| "custom-provider": {
00218| name: "Custom Provider",
00219| npm: "@ai-sdk/openai-compatible",
00220| api: "https://api.custom.com/v1",
00221| env: ["CUSTOM_API_KEY"],
00222| models: {
00223| "custom-model": {
00224| name: "Custom Model",
00225| tool_call: true,
00226| limit: {
00227| context: 128000,
00228| ...<file>
00671| .object({
00672| model: z.string().optional(),
00673| variant: z
00674| .string()
00675| .optional()
00676| .describe("Default model variant for this agent (applies only when using the agent's configured model)."),
00677| temperature: z.number().optional(),
00678| top_p: z.number().optional(),
00679| prompt: z.string().optional(),
00680| tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"),
00681| disable: z.boolean().optional(),
00682| description: z.string().optional().describe("Description of when to use the agent"),
00683| mode: z.enum(["subagent", "primary", "all"]).optional(),
00684| hidden: z
00685| .boolean()
00686| .optional()
00687| .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"),
00688| options: z.record(z.string(), z.any()).optional(),
00689| color: z
00690| .union([
00691| z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"),
00692| z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
00693| ])
00694| .optional()
00695| .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"),
00696| steps: z
00697| .number()
00698| .int()
00699| .positive()
00700| .optional()
00701| .describe("Maximum number of agentic iterations before forcing text-only response"),
00702| maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."),
00703| permission: Permission.optional(),
00704| })
00705| .catchall(z.any())
00706| .transform((agent, ctx) => {
00707| const knownKeys = new Set([
00708| "name",
00709| "model",
00710| "variant",
00711| "prompt",
00712| "desc...<file>
00001| // the approaches in this edit tool are sourced from
00002| // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
00003| // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
00004| // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
00005|
00006| import z from "zod"
00007| import * as path from "path"
00008| import { Tool } from "./tool"
00009| import { LSP } from "../lsp"
00010| import { createTwoFilesPatch, diffLines } from "diff"
00011| import DESCRIPTION from "./edit.txt"
00012| import { File } from "../file"
00013| import { FileWatcher } from "../file/watcher"
00014| import { Bus } from "../bus"
00015| import { FileTime } from "../file/time"
00016| import { Filesystem } from "../util/filesystem"
00017| import { Instance } from "../project/instance"
00018| import { Snapshot } from "@/snapshot"
00019| import { assertExternalDirectory } from "./external-directory"
00020|
00021| const MAX_DIAGNOSTICS_PER_FILE = 20
00022|
00023| function normalizeLineEndings(text: string): string {
00024| return text.replaceAll("\r\n", "\n")
00025| }
00026|
00027| export const EditTool = Tool.define("edit", {
00028| description: DESCRIPTION,
00029| parameters: z.object({
00030| filePath: z.string().describe("The absolute path to the file to modify"),
00031| oldString: z.string().describe("The text to replace"),
00032| newString: z.string().describe("The text to replace it with (must be different from oldString)"),
00033| replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
00034| }),
00035| async execute(params, ctx) {
00036| if (!params.filePath) {
00037| throw new Error("filePath is required")
00038| }
00039|
00040| if (params.oldString === params.newString) {
00041| throw new Error("oldString and newString must be different")
00042| }
00043|
000...<file>
00001| import z from "zod"
00002| import { Tool } from "./tool"
00003| import { EditTool } from "./edit"
00004| import DESCRIPTION from "./multiedit.txt"
00005| import path from "path"
00006| import { Instance } from "../project/instance"
00007|
00008| export const MultiEditTool = Tool.define("multiedit", {
00009| description: DESCRIPTION,
00010| parameters: z.object({
00011| filePath: z.string().describe("The absolute path to the file to modify"),
00012| edits: z
00013| .array(
00014| z.object({
00015| filePath: z.string().describe("The absolute path to the file to modify"),
00016| oldString: z.string().describe("The text to replace"),
00017| newString: z.string().describe("The text to replace it with (must be different from oldString)"),
00018| replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
00019| }),
00020| )
00021| .describe("Array of edit operations to perform sequentially on the file"),
00022| }),
00023| async execute(params, ctx) {
00024| const tool = await EditTool.init()
00025| const results = []
00026| for (const [, edit] of params.edits.entries()) {
00027| const result = await tool.execute(
00028| {
00029| filePath: params.filePath,
00030| oldString: edit.oldString,
00031| newString: edit.newString,
00032| replaceAll: edit.replaceAll,
00033| },
00034| ctx,
00035| )
00036| results.push(result)
00037| }
00038| return {
00039| title: path.relative(Instance.worktree, params.filePath),
00040| metadata: {
00041| results: results.map((r) => r.metadata),
00042| },
00043| output: results.at(-1)!.output,
00044| }
00045| },
00046| })
00047|
(End of file - total 47 lines)
</file>Found 5 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 1050: small_model: z
/home/jm/data/code/opencode/packages/opencode/test/provider/provider.test.ts:
Line 941: test("getSmallModel respects config small_model override", async () => {
Line 948: small_model: "anthropic/claude-sonnet-4-20250514",
/home/jm/data/code/opencode/packages/opencode/src/provider/provider.ts:
Line 1150: if (cfg.small_model) {
Line 1151: const parsed = parseModel(cfg.small_model)<file>
01046| .describe(
01047| "Hide providers from the model select menu and provider list. Providers remain functional when configured directly.",
01048| ),
01049| model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
01050| small_model: z
01051| .string()
01052| .describe("Small model to use for tasks like title generation in the format of provider/model")
01053| .optional(),
01054| default_agent: z
01055| .string()
01056| .optional()
01057| .describe(
01058| "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
01059| ),
01060| username: z
01061| .string()
01062| .optional()
01063| .describe("Custom username to display in conversations instead of system username"),
01064| mode: z
01065| .object({
01066| build: Agent.optional(),
01067| plan: Agent.optional(),
01068| })
01069| .catchall(Agent)
01070| .optional()
01071| .describe("@deprecated Use `agent` field instead."),
01072| agent: z
01073| .object({
01074| // primary
01075| plan: Agent.optional(),
01076| build: Agent.optional(),
01077| // subagent
01078| general: Agent.optional(),
01079| explore: Agent.optional(),
01080| // specialized
01081| title: Agent.optional(),
01082| summary: Agent.optional(),
01083| compaction: Agent.optional(),
01084| })
01085| .catchall(Agent)
01086| .optional()
01087| .describe("Agent configuration, see https://opencode.ai/docs/agents"),
01088| provider: z
01089| .record(z.string(), Provider)
01090| .optional()
01091| .describe("Custom provider configurations and ...<file>
00942| await using tmp = await tmpdir({
00943| init: async (dir) => {
00944| await Bun.write(
00945| path.join(dir, "opencode.json"),
00946| JSON.stringify({
00947| $schema: "https://opencode.ai/config.json",
00948| small_model: "anthropic/claude-sonnet-4-20250514",
00949| }),
00950| )
00951| },
00952| })
00953| await Instance.provide({
00954| directory: tmp.path,
00955| init: async () => {
00956| Env.set("ANTHROPIC_API_KEY", "test-api-key")
00957| },
00958| fn: async () => {
00959| const model = await Provider.getSmallModel("anthropic")
00960| expect(model).toBeDefined()
00961| expect(model?.providerID).toBe("anthropic")
00962| expect(model?.id).toBe("claude-sonnet-4-20250514")
00963| },
00964| })
00965| })
00966|
00967| test("provider.sort prioritizes preferred models", () => {
00968| const models = [
00969| { id: "random-model", name: "Random" },
00970| { id: "claude-sonnet-4-latest", name: "Claude Sonnet 4" },
00971| { id: "gpt-5-turbo", name: "GPT-5 Turbo" },
00972| { id: "other-model", name: "Other" },
00973| ] as any[]
00974|
00975| const sorted = Provider.sort(models)
00976| expect(sorted[0].id).toContain("sonnet-4")
00977| expect(sorted[0].id).toContain("latest")
00978| expect(sorted[sorted.length - 1].id).not.toContain("gpt-5")
00979| expect(sorted[sorted.length - 1].id).not.toContain("sonnet-4")
00980| })
00981|
00982| test("multiple providers can be configured simultaneously", async () => {
00983| await using tmp = await tmpdir({
00984| init: async (dir) => {
00985| await Bun.write(
00986| path.join(dir, "opencode.json"),
00987| JSON.stringify({
00988| $schema: "https://opencode.ai/config.json",
00989| provider: {
00990| anthropic: {
00991| options: { timeout: 30000 },
(File has more lines. Use 'offset' parameter to read ...<file>
01146|
01147| export async function getSmallModel(providerID: string) {
01148| const cfg = await Config.get()
01149|
01150| if (cfg.small_model) {
01151| const parsed = parseModel(cfg.small_model)
01152| return getModel(parsed.providerID, parsed.modelID)
01153| }
01154|
01155| const provider = await state().then((state) => state.providers[providerID])
01156| if (provider) {
01157| let priority = [
01158| "claude-haiku-4-5",
01159| "claude-haiku-4.5",
01160| "3-5-haiku",
01161| "3.5-haiku",
01162| "gemini-3-flash",
01163| "gemini-2.5-flash",
01164| "gpt-5-nano",
01165| ]
01166| if (providerID.startsWith("opencode")) {
01167| priority = ["gpt-5-nano"]
01168| }
01169| if (providerID.startsWith("github-copilot")) {
01170| // prioritize free models for github copilot
01171| priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
01172| }
01173| for (const item of priority) {
01174| for (const model of Object.keys(provider.models)) {
01175| if (model.includes(item)) return getModel(providerID, model)
01176| }
01177| }
01178| }
01179|
01180| // Check if opencode provider is available before using it
01181| const opencodeProvider = await state().then((state) => state.providers["opencode"])
01182| if (opencodeProvider && opencodeProvider.models["gpt-5-nano"]) {
01183| return getModel("opencode", "gpt-5-nano")
01184| }
01185|
01186| return undefined
01187| }
01188|
01189| const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
01190| export function sort(models: Model[]) {
01191| return sortBy(
01192| models,
01193| [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"],
01194| [(model) => (model.id.includes("latest") ? 0 : 1), "asc"],
01195| [(model) => model.id, "desc...Found 100 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 56: if (target.plugin && source.plugin) {
Line 57: merged.plugin = Array.from(new Set([...target.plugin, ...source.plugin]))
Line 73: // 5) .opencode directories (.opencode/agents/, .opencode/commands/, .opencode/plugins/, .opencode/opencode.json{,c})
Line 118: result.plugin = result.plugin || []
Line 158: result.plugin ??= []
Line 172: result.plugin.push(...(await loadPlugin(dir)))
Line 182: // Kept separate from directories array to avoid write operations when installing plugins
Line 184: // This way it only loads config file and not skills/plugins/commands
Line 236: result.plugin = deduplicatePlugins(result.plugin ?? [])
Line 259: "@opencode-ai/plugin": targetVersion,
Line 269: // This allows local plugins and custom tools to use external packages
Line 308: const depVersion = dependencies["@opencode-ai/plugin"]
Line 313: const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
Line 316: pkg: "@opencode-ai/plugin",
Line 454: const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
Line 455: async function loadPlugin(dir: string) {
Line 456: const plugins: string[] = []
Line 464: plugins.push(pathToFileURL(item).href)
Line 466: return plugins
Line 470: * Extracts a canonical plugin name from a plugin specifier.
Line 475: * getPluginName("file:///path/to/plugin/foo.js") // "foo"
Line 476: * getPluginName("oh-my-opencode@2.4.3") // "oh-my-opencode"
Line 477: * getPluginName("@scope/pkg@1.0.0") // "@scope/pkg"
Line 479: export function getPluginName(plugin: string): string {
Line 480: if (plugin.startsWith("file://")) {
Line 481: return path.parse(new URL(plugin).pathname).name
Line 483: const lastAt = plugin.lastIndexOf("@")
Line 485: return plugin.substring(...<file>
00811| model_list: z.string().optional().default("<leader>m").describe("List available models"),
00812| model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
00813| model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
00814| model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"),
00815| model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"),
00816| command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
00817| agent_list: z.string().optional().default("<leader>a").describe("List agents"),
00818| agent_cycle: z.string().optional().default("tab").describe("Next agent"),
00819| agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
00820| variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
00821| input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
00822| input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
00823| input_submit: z.string().optional().default("return").describe("Submit input"),
00824| input_newline: z
00825| .string()
00826| .optional()
00827| .default("shift+return,ctrl+return,alt+return,ctrl+j")
00828| .describe("Insert newline in input"),
00829| input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
00830| input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
00831| input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
00832| input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
00833| ...Found 11 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 259: "@opencode-ai/plugin": targetVersion,
Line 308: const depVersion = dependencies["@opencode-ai/plugin"]
Line 313: const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
Line 316: pkg: "@opencode-ai/plugin",
/home/jm/data/code/opencode/packages/opencode/src/plugin/index.ts:
Line 1: import type { Hooks, PluginInput, Plugin as PluginInstance } from "@opencode-ai/plugin"
/home/jm/data/code/opencode/packages/opencode/src/plugin/copilot.ts:
Line 1: import type { Hooks, PluginInput } from "@opencode-ai/plugin"
/home/jm/data/code/opencode/packages/opencode/src/plugin/codex.ts:
Line 1: import type { Hooks, PluginInput } from "@opencode-ai/plugin"
/home/jm/data/code/opencode/packages/opencode/src/tool/registry.ts:
Line 20: import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
/home/jm/data/code/opencode/packages/opencode/test/tool/registry.test.ts:
Line 91: "@opencode-ai/plugin": "^0.0.0",
/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/auth.ts:
Line 13: import type { Hooks } from "@opencode-ai/plugin"
/home/jm/data/code/opencode/packages/opencode/src/provider/auth.ts:
Line 6: import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"/home/jm/data/code/opencode/packages/opencode/src/plugin/index.ts
/home/jm/data/code/opencode/packages/opencode/src/plugin/copilot.ts
/home/jm/data/code/opencode/packages/opencode/src/plugin/codex.ts
/home/jm/data/code/opencode/packages/opencode/test/plugin/auth-override.test.ts
/home/jm/data/code/opencode/packages/opencode/test/plugin/codex.test.tsFound 5 matches
/home/jm/data/code/opencode/packages/opencode/src/config/config.ts:
Line 479: export function getPluginName(plugin: string): string {
Line 501: export function deduplicatePlugins(plugins: string[]): string[] {
/home/jm/data/code/opencode/packages/opencode/src/plugin/index.ts:
Line 16: export namespace Plugin {
/home/jm/data/code/opencode/packages/opencode/src/plugin/copilot.ts:
Line 20: export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
/home/jm/data/code/opencode/packages/opencode/src/plugin/codex.ts:
Line 351: export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {<file>
00001| import type { Hooks, PluginInput, Plugin as PluginInstance } from "@opencode-ai/plugin"
00002| import { Config } from "../config/config"
00003| import { Bus } from "../bus"
00004| import { Log } from "../util/log"
00005| import { createOpencodeClient } from "@opencode-ai/sdk"
00006| import { Server } from "../server/server"
00007| import { BunProc } from "../bun"
00008| import { Instance } from "../project/instance"
00009| import { Flag } from "../flag/flag"
00010| import { CodexAuthPlugin } from "./codex"
00011| import { Session } from "../session"
00012| import { NamedError } from "@opencode-ai/util/error"
00013| import { CopilotAuthPlugin } from "./copilot"
00014| import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth"
00015|
00016| export namespace Plugin {
00017| const log = Log.create({ service: "plugin" })
00018|
00019| const BUILTIN = ["opencode-anthropic-auth@0.0.13"]
00020|
00021| // Built-in plugins that are directly imported (not installed from npm)
00022| const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin]
00023|
00024| const state = Instance.state(async () => {
00025| const client = createOpencodeClient({
00026| baseUrl: "http://localhost:4096",
00027| // @ts-ignore - fetch type incompatibility
00028| fetch: async (...args) => Server.App().fetch(...args),
00029| })
00030| const config = await Config.get()
00031| const hooks: Hooks[] = []
00032| const input: PluginInput = {
00033| client,
00034| project: Instance.project,
00035| worktree: Instance.worktree,
00036| directory: Instance.directory,
00037| serverUrl: Server.url(),
00038| $: Bun.$,
00039| }
00040|
00041| for (const plugin of INTERNAL_PLUGINS) {
00042| log.info("loading internal plugin", { name: plugin.name })
00043| const init = await plugin(input)
00044| hooks.push(init)
00045| }
00046|
...Found 3 matches
/home/jm/data/code/opencode/packages/opencode/src/provider/models-snapshot.ts:
Line 2: export const snapshot = {"privatemode-ai":{"id":"privatemode-ai","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","name":"Privatemode AI","doc":"https://docs.privatemode.ai/api/overview","models":{"whisper-large-v3":{"id":"whisper-large-v3","name":"Whisper large-v3","family":"whisper","attachment":true,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2023-09","release_date":"2023-09-01","last_updated":"2023-09-01","modalities":{"input":["audio"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":0,"output":4096}},"qwen3-embedding-4b":{"id":"qwen3-embedding-4b","name":"Qwen3-Embedding 4B","family":"qwen","attachment":false,"reasoning":false,"tool_call":false,"structured_output":false,"temperature":true,"knowledge":"2025-06","release_date":"2025-06-06","last_updated":"2025-06-06","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":32000,"output":2560}},"gpt-oss-120b":{"id":"gpt-oss-120b","name":"gpt-oss-120b","family":"gpt-oss","attachment":false,"reasoning":true,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2025-08","release_date":"2025-08-04","last_updated":"2025-08-14","modalities":{"input":["text"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":128000}},"gemma-3-27b":{"id":"gemma-3-27b","name":"Gemma 3 27B","family":"gemma","attachment":true,"reasoning":false,"tool_call":true,"structured_output":true,"temperature":true,"knowledge":"2024-08","release_date":"2025-03-12","last_updated":"2025-03-12","modalities":{"input":["text","image"],"output":["text"]},"open_weights":true,"cost":{"input":0,"output":0},"limit":{"context":128000,"output":8192}},"qwen3-coder-30b-a3...Based on my exploration:
Using morph/morph-v3-fast from openrouter:
edit and multiedit tools are pure text manipulation - they don't use any LLM modelopencode.json:{
"agent": {
"build": { "model": "openrouter/morph/morph-v3-fast" }
}
}
Or globally: {"model": "openrouter/morph/morph-v3-fast"}@openrouter/ai-sdk-providerEmbeddings models:
qwen3-embedding-4b)small_model config is only for lightweight tasks like title generation, not embeddingsCould I create a custom tool, called "speed-edit" and give that to certain agents by disabling the original edit and using this new one?
Yes! Create a custom tool and configure agent permissions.
Steps:
.opencode/plugins/speed-edit.ts:import z from "zod"
import { Tool } from "@opencode-ai/plugin"
import { EditTool } from "../tool/edit" // or implement directly
export default async function () {
return {
name: "speed-edit-plugin",
tools: [{
define: Tool.define("speed-edit", {
description: "Fast editing tool",
parameters: z.object({
filePath: z.string(),
oldString: z.string(),
newString: z.string()
}),
async execute(params, ctx) {
// Your custom logic here
}
})
}],
hooks: {}
}
}
opencode.json:{
"agent": {
"fast-editor": {
"model": "openrouter/morph/morph-v3-fast",
"permission": {
"edit": "deny",
"speed-edit": "allow"
}
},
"default": {
"permission": {
"edit": "allow"
}
}
}
}
{
"plugin": ["file:///path/to/.opencode/plugins/speed-edit.ts"]
}
Result: The fast-editor agent can only use speed-edit, not the original edit, while default agent retains access to edit.
1. Plugins are the extension mechanism
Everything in OpenCode is extended through plugins. A plugin is a TypeScript function that returns configuration for agents, tools, hooks, and other features. Plugins can be distributed via npm.
2. Agents define AI behaviour
Agents are configured AI assistants with specific prompts, models, and tool access. OpenCode has two modes: primary (main agent) and subagent (delegated tasks). Agent prompts are full TypeScript strings, giving complete control.
3. Hooks intercept lifecycle events
Hooks let plugins react to events like tool execution, session creation, context limits, and more. They enable features like auto-compaction, TDD enforcement, and context monitoring.
4. Tools extend agent capabilities
Custom tools give agents new abilities. Tools are defined with Zod schemas for parameters and can access the plugin context for session management, file operations, etc.
5. Skills work differently in OpenCode
OpenCode can load Claude Code skills, but also has its own skill system. Skills in OpenCode are simpler — markdown files that agents can invoke for domain knowledge.
task() calls for explore/librarian agents (always use background_task)tool.execute.before hook when modifying tool argumentsWait for response before proceeding.
After identifying the intent, read the relevant reference file and follow its guidance.
import type { Plugin } from "@opencode-ai/plugin"
const MyPlugin: Plugin = async (ctx) => {
return {
tool: {
/* custom tools */
},
config: {
agents: {
/* agent definitions */
},
},
event: async (input) => {
/* lifecycle events */
},
"tool.execute.before": async (input, output) => {
/* pre-tool hook */
},
"tool.execute.after": async (input, output) => {
/* post-tool hook */
},
}
}
export default MyPlugin
Agent Definition:
import type { AgentConfig } from "@opencode-ai/sdk"
const myAgent: AgentConfig = {
description: "What this agent does (shown in delegation UI)",
mode: "subagent", // or "primary"
model: "anthropic/claude-sonnet-4",
temperature: 0.1,
tools: { write: true, edit: true, bash: true },
prompt: `Full agent prompt here...`,
}
Custom Tool:
import { z } from "zod"
const myTool = {
description: "What this tool does",
parameters: z.object({
input: z.string().describe("Parameter description"),
}),
async execute(params, ctx) {
// Tool logic
return { result: "output" }
},
}
Key Hooks:
event — Session lifecycle (created, deleted, error)tool.execute.before — Modify tool args before executiontool.execute.after — Process tool resultsexperimental.session.compacting — Inject context into summarieschat.message — Intercept user messagesctx)The plugin receives a context object with:
ctx.client — OpenCode client for session operationsctx.directory — Current working directoryctx.client.session.summarize() — Trigger context compaction| Mode | Purpose | Use Case |
|---|---|---|
primary |
Main conversation agent | Custom main agent replacing default |
subagent |
Delegated task executor | Specialized agents for specific work |
Agents can restrict tool access:
tools: {
write: true, // File writing
edit: true, // File editing
bash: true, // Shell commands
background_task: false, // Prevent sub-subagent spawning
}
chat.message — User input receivedtool.execute.before — Before each tool calltool.execute.after — After each tool callevent — Session events (async, not blocking)Plugins are distributed via npm:
# Install
bunx my-opencode-plugin install
# This registers in ~/.config/opencode/opencode.json
references/plugin-architecture.md — Plugin structure, entry points, exportsreferences/agent-configuration.md — Agent config, modes, prompt designreferences/lifecycle-hooks.md — All available hooks and patternsreferences/custom-tools.md — Tool definition, Zod schemas, executionDoes this give you any ideas of ways we should improve?
Also we can probably revert the changes we made to hide providers, I see in the config there is a "disabled_providers" list that can be set
Let me check the changes we made and revert them:
bun.lock --- 1/105 --- Text
19 "@tsconfig/bun": "1.0.10", 19 "@tsconfig/bun": "1.0.10",
20 "husky": "9.1.7", 20 "husky": "9.1.7",
21 "prettier": "3.8.0", 21 "prettier": "3.8.0",
22 "semver": "^7.6.0", 22 "semver": "7.7.4",
23 "turbo": "2.5.6", 23 "turbo": "2.5.6",
24 }, 24 },
25 }, 25 },
bun.lock --- 2/105 --- Text
481 }, 481 },
482 }, 482 },
483 "packages/web": { 483 "packages/web": {
484 "name": "@opencode-ai/web", 484 "name": "opencode-web",
485 "version": "1.1.53", 485 "version": "1.1.53",
486 "dependencies": { 486 "dependencies": {
487 "@cloudflare/vite-plugin": 487 "@cloudflare/vite-plugin":
... "1.21.2", ... "1.21.2",
488 "hono": "4.11.5", 488 "hono": "4.11.5",
... 489 "solid-js": "1.9.10",
489 "vite": "7.3.1", 490 "vite": "7.3.1",
... 491 "vite-plugin-solid": "2.11
... ... .10",
490 "wrangler": "4.60.0", 492 "wrangler": "4.60.0",
491 }, 493 },
492 "devDependencies": { ...
493 "solid-js": "1.9.10", ...
494 "vite-plugin-solid": "2.11 ...
... .10", ...
495 }, ...
496 }, 494 },
497 "packages/worker-test": { 495 "packages/worker-test": {
498 "name": "@opencode-ai/worker 496 "name": "@opencode-ai/worker
... -test", ...<file>
01041| .optional()
01042| .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
01043| hidden_providers: z
01044| .array(z.string())
01045| .optional()
01046| .describe(
01047| "Hide providers from the model select menu and provider list. Providers remain functional when configured directly.",
01048| ),
01049| model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
01050| small_model: z
01051| .string()
01052| .describe("Small model to use for tasks like title generation in the format of provider/model")
01053| .optional(),
01054| default_agent: z
01055| .string()
01056| .optional()
01057| .describe(
01058| "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
01059| ),
01060| username: z
(File has more lines. Use 'offset' parameter to read beyond line 1060)
</file><file>
00031| ),
00032| },
00033| },
00034| },
00035| },
00036| }),
00037| async (c) => {
00038| const config = await Config.get()
00039| const disabled = new Set(config.disabled_providers ?? [])
00040| const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
00041| const hidden = new Set(config.hidden_providers ?? [])
00042|
00043| const allProviders = await ModelsDev.get()
00044| const filteredProviders: Record<string, (typeof allProviders)[string]> = {}
00045| for (const [key, value] of Object.entries(allProviders)) {
00046| if ((enabled ? enabled.has(key) : true) && !disabled.has(key) && !hidden.has(key)) {
00047| filteredProviders[key] = value
00048| }
00049| }
00050|
00051| const connected = await Provider.list()
00052| const providers = Object.assign(
00053| mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
00054| connected,
00055| )
00056| return c.json({
00057| all: Object.values(providers),
00058| default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
00059| connected: Object.keys(connected),
00060| })
00061| },
00062| )
00063| .get(
00064| "/auth",
00065| describeRoute({
00066| summary: "Get provider auth methods",
00067| description: "Retrieve available authentication methods for all AI providers.",
00068| operationId: "provider.auth",
00069| responses: {
00070| 200: {
00071| description: "Provider auth methods",
00072| content: {
00073| "application/json": {
00074| schema: resolver(z.record(z.string(), z.array(ProviderAuth.Method))),
00075| },
00076| },
00077| ...<file>
00026| })
00027| },
00028| handler: async (args) => {
00029| if (args.refresh) {
00030| await ModelsDev.refresh()
00031| UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
00032| }
00033|
00034| await Instance.provide({
00035| directory: process.cwd(),
00036| async fn() {
00037| const { Config } = await import("@/config/config")
00038| const config = await Config.get()
00039| const hidden = new Set(config.hidden_providers ?? [])
00040| const providers = await Provider.list()
00041|
00042| function printModels(providerID: string, verbose?: boolean) {
00043| const provider = providers[providerID]
00044| const sortedModels = Object.entries(provider.models).sort(([a], [b]) => a.localeCompare(b))
00045| for (const [modelID, model] of sortedModels) {
00046| process.stdout.write(`${providerID}/${modelID}`)
00047| process.stdout.write(EOL)
00048| if (verbose) {
00049| process.stdout.write(JSON.stringify(model, null, 2))
00050| process.stdout.write(EOL)
00051| }
00052| }
00053| }
00054|
00055| if (args.provider) {
00056| const provider = providers[args.provider]
00057| if (!provider) {
00058| UI.error(`Provider not found: ${args.provider}`)
00059| return
00060| }
00061|
00062| printModels(args.provider, args.verbose)
00063| return
00064| }
00065|
00066| const providerIDs = Object.keys(providers)
00067| .filter((id) => !hidden.has(id))
00068| .sort((a, b) => {
00069| const aIsOpencode = a.startsWith("opencode")
00070| const bIsOpencode = b.startsWith("opencode")
00071| if (aIsOpencode && !bIsOpencode) return -1
00072| if (!aIsOpencode && bIsOpencode) return ...Now I'll revert the hidden_providers changes:
Edit applied successfully.Edit applied successfully.Edit applied successfully.<file>
01041| .optional()
01042| .describe("When set, ONLY these providers will be enabled. All other providers will be ignored"),
01043| hidden_providers: z
01044| .array(z.string())
01045| .optional()
01046| .describe(
01047| "Hide providers from the model select menu and provider list. Providers remain functional when configured directly.",
01048| ),
01049| model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
01050| small_model: z
01051| .string()
01052| .describe("Small model to use for tasks like title generation in the format of provider/model")
01053| .optional(),
01054| default_agent: z
01055| .string()
(File has more lines. Use 'offset' parameter to read beyond line 1055)
</file>Edit applied successfully.hidden_providers changes reverted - you can use disabled_providers in opencode.json instead:
{
"disabled_providers": ["openrouter", "vercel"]
}
Improvement ideas from plugin docs:
Tool validation - Tools have Zod schemas but error messages could be clearer (like edit tool's "oldString not found" could suggest which replacer strategies failed)
Plugin loading safety - Plugin system imports ALL exports from plugin modules - could add warnings for non-plugin exports to prevent accidental exports
Hook documentation - Hooks like tool.execute.before exist but no examples of common patterns (e.g., modifying tool args, logging)
Agent mode clarity - primary vs subagent modes have different behaviors but documentation could be more explicit about when to use each
Tool registry - ToolRegistry.tools() shows tools exist but no easy way to discover what tools are available to a specific agent
Plugin distribution - CLI installer mentioned (bunx my-opencode-plugin install) but no scaffolding/template for new plugins