This document describes the current runtime architecture on main, with the default settings:
CURSOR_ACP_TOOL_LOOP_MODE=opencodeCURSOR_ACP_PROVIDER_BOUNDARY=v1CURSOR_ACP_PROVIDER_BOUNDARY_AUTOFALLBACK=true
- OpenCode sends chat requests to the
cursor-acpprovider (/v1/chat/completions). - The plugin proxy spawns
cursor-agentper request and streams NDJSON (stream-jsonformat). - Assistant text/thinking streams back to OpenCode as SSE.
tool_callevents are intercepted at the provider boundary.- Intercepted calls are normalized and guarded, then returned as OpenAI
tool_calls. - OpenCode executes tools locally and sends results back as
role: "tool"messages in the next turn. - Prompt builder renders tool results into
TOOL_RESULT (call_id: ...)blocks for the nextcursor-agentinvocation.
- OpenCode owns execution of the active tool list.
- In
chat.params, existing OpenCode tool definitions are preserved and passed through. cursor-acpdoes not execute SDK/MCP tools in this mode; it translates tool-call protocol boundaries.
cursor-acpcan inject tool definitions and execute via internal router:- Local tools
- SDK-discovered tools
- MCP-discovered tools
- Used as compatibility path and fallback strategy.
- Tool-loop behavior is disabled.
The boundary abstraction is implemented in src/provider/boundary.ts and runtime handling in src/provider/runtime-interception.ts.
v1(default): shared extraction/interception path for Bun + Node proxy handlers.legacy: previous extraction/runtime behavior.
v1 adds:
- Tool name alias resolution (
shell,runCommand,delegateTask,skillMcp, etc.) - Schema compatibility normalization (
src/provider/tool-schema-compat.ts) - Optional edit-to-write reroute for malformed full-file replacement edits
- Tool-loop guard with fingerprinting (
src/provider/tool-loop-guard.ts) - Guarded fallback to
legacywhen enabled and specific termination/error conditions are met
OpenCode sends custom model option fields in the provider request body. When a model variant defines cursorModel, OpenCode merges the selected variant value into body.cursorModel before the request reaches cursor-acp.
The provider boundary resolves the runtime Cursor model in this order:
- Use
body.cursorModelwhen it is a non-empty string. - Otherwise normalize
body.modelby stripping thecursor-acp/provider prefix. - Fall back to
autowhen no model is available.
This lets a compact OpenCode model such as cursor-acp/gpt-5.3-codex call a concrete Cursor model like gpt-5.3-codex-high when the selected variant provides that mapping.
open-cursor sync-models --variants generates those compact entries from cursor-agent models. The default sync-models command still writes direct raw Cursor model IDs. Add --compact with --variants to remove raw model entries that were folded into generated variant groups while preserving custom and unknown entries.
Use --dry-run to preview the model sync without writing the config. Sync output includes added, updated, removed, priced, and skipped counts so repeated syncs are easy to audit before applying them.
Use open-cursor sync-models --json when automation needs the same sync summary as structured output. Use open-cursor models --explain to inspect the generated model families, default Cursor targets, variant mappings, and direct models without modifying the config.
Use open-cursor doctor --deep after install or sync to check Cursor model discovery, provider model config, compact variant presence, and whether configured cursorModel targets still exist in the current cursor-agent models output.
v1 loop safety is driven by two mechanisms:
- Schema validation guard: tracks repeated schema-invalid calls by tool + schema signature.
- Tool-loop guard: tracks repeated calls using fingerprints with error class awareness.
The guard classifies outcomes (validation, not_found, permission, timeout, tool_error, success, unknown) and terminates repetitive loops before they spin indefinitely.
The OpenCode plugin tool hook (buildToolHookEntries in src/plugin.ts) registers local tools (bash, read, write, edit, grep, ls, glob, mkdir, rm, stat) and compatibility aliases such as shell -> bash.
When tool-hook execution is used, path/cwd defaults are normalized against tool context (worktree / directory) to keep file and shell behavior workspace-aware.
cursor-agent --output-format stream-json emits a final result event with token usage when Cursor reports it. The proxy maps that payload to OpenAI-compatible usage so OpenCode can emit step-finish token data for tools such as OpenCode TokenSpeed Monitor.
Cursor fields are mapped as follows:
inputTokens + cacheReadTokens + cacheWriteTokens->usage.prompt_tokensoutputTokens->usage.completion_tokensreasoningTokens->usage.completion_tokens_details.reasoning_tokenscacheReadTokens->usage.prompt_tokens_details.cached_tokenscacheWriteTokens->usage.prompt_tokens_details.cache_write_tokens
prompt_tokens includes cache tokens because OpenAI-compatible parsers treat cached_tokens as a subset of total prompt tokens.
Non-stream responses include usage on the chat completion response. Stream responses emit the normal final stop chunk, then a usage-only chunk with choices: [], then [DONE].
If Cursor omits the final result.usage payload, the proxy omits usage instead of inventing zero-token metrics. This lets OpenCode and TokenSpeed fall back to their normal behavior without receiving misleading token counts.
Cost is passed through when a provider reports it as cost, totalCost, or total_cost. Cursor currently reports tokens but not request cost, so open-cursor install and open-cursor sync-models also write OpenCode cost config for known Cursor models using the official prices from Cursor Models & Pricing. Prices are stored per million tokens as input, output, cache_read, cache_write, and context_over_200k when Cursor documents a long-context rate.
Run bun run check:pricing to compare the current cursor-agent models output with the local official pricing map and warn when the Cursor pricing page markers change. Run bun run check:pricing:fixture for an offline fixture-based coverage check that does not require Cursor auth or network access.
- Proxy reuse is enabled by default (
CURSOR_ACP_REUSE_EXISTING_PROXY); this can reuse an already-running process on port32124. - During debugging or rollouts, disable reuse (
CURSOR_ACP_REUSE_EXISTING_PROXY=false) or restart OpenCode to ensure the active proxy process matches the latest build. - If
~/.config/opencodeis reset/wiped, reinstall or re-run model/config sync to restore provider config and plugin symlink.