@tduyng/pi-opencode-z 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,42 +1,54 @@
1
1
  # pi-opencode-z
2
2
 
3
- Use opencode's Zen models from Pi, including the free ones.
3
+ opencode's Zen models in pi, free ones included. No API key, no proxy, no key of your own: the bridge drives the opencode server already running on your machine.
4
4
 
5
- Since September 2026 the free tier answers any direct API call with `403 FreeTierError: "OpenCode's free tier can only be used from within OpenCode"`. No API key or proxy gets around it, because the check is on where the request originates. So this extension does not call the Zen API. It drives the opencode background server already running on your machine and lets opencode make the upstream call, which is the one thing the free tier accepts.
5
+ ## What it costs
6
+
7
+ No tools, no prompt block, no schema. It is a model provider, so the context and the cold start are pi's own.
8
+
9
+ The 7k input floor is opencode's, not this extension's. A plain `opencode run` on a free model spends 9,176 input tokens on a one-line prompt, while this bridge spends 6,941 with 2,048 of those served from cache. That floor is opencode's system prompt and built-in tool schemas, and every approach pays it, because stripping those tools is exactly what trips the 403.
10
+
11
+ What the bridge does save is per turn. opencode holds the conversation, so each turn sends only the new message instead of re-serializing the transcript, and the tool schemas sit in the cached prefix rather than being pasted back as prose on every request.
12
+
13
+ ## Why it works this way
14
+
15
+ Since September 2026 the free tier answers any direct API call with `403 FreeTierError: "OpenCode's free tier can only be used from within OpenCode"`. No key and no proxy gets around it, because the check is on where the request originates.
16
+
17
+ So the extension does not call the Zen API. It drives the opencode background server and lets opencode make the upstream call, which is the one thing the free tier accepts.
6
18
 
7
19
  ## How it works
8
20
 
9
- The bridge reads the server's url and password from `~/.local/state/opencode/service.json` and talks to it over HTTP basic auth as user `opencode`. If the server is not running, `opencode service start` brings it back.
21
+ The bridge reads the server url and password from `~/.local/state/opencode/service.json` and talks to it over HTTP basic auth as user `opencode`. If the server is not running, `opencode service start` brings it back.
10
22
 
11
- Each Pi session gets one opencode session, rooted in a throwaway temp directory that holds its `opencode.jsonc`. Nothing is written to your repo or to `~/.config/opencode`.
23
+ Each pi session gets one opencode session, rooted in a throwaway temp directory holding its `opencode.jsonc`. Nothing is written to your repo or to `~/.config/opencode`.
12
24
 
13
- Pi's own tools reach the model through a local MCP server, exposed as `pi_read`, `pi_bash`, `pi_edit` and so on. When the model calls one, the bridge holds that MCP request open, hands the call to Pi, and Pi runs the real tool against your real project. The answer goes back over the same held request and opencode carries on in the same session, none the wiser. The model never touches your files directly. Pi does, under Pi's own permissions.
25
+ Pi's tools reach the model through a local MCP server, exposed as `pi_read`, `pi_bash`, `pi_edit`, and so on. When the model calls one, the bridge holds that MCP request open, hands the call to pi, and pi runs the real tool against your real project. The answer goes back over the same held request and opencode carries on in the same session. The model never touches your files directly, and pi does, under pi's own permissions.
14
26
 
15
27
  Pi hands providers a transcript rather than a prompt and a tool list, so the bridge replays the transcript's system messages to recover the current instructions and tool set before each turn.
16
28
 
17
- Cold start costs nothing extra. A plain `opencode run` on a free model spends 9176 input tokens on a one-line prompt. The bridge spends 6941, with 2048 of those served from cache. That 7k floor is opencode's own system prompt and built-in tool schemas. Every approach to this problem pays it, because stripping those tools is exactly what trips the 403.
29
+ A mode switch that adds or removes tools keeps the session too: the bridge pushes `notifications/tools/list_changed` over MCP, opencode re-lists, and the cached prefix survives.
18
30
 
19
- The savings are per turn. opencode holds the conversation, so each turn sends only the new message instead of re-serializing the whole transcript. Tool schemas sit in the cached prefix rather than being pasted back as prose every time. Extensions that drive the opencode CLI instead have to resend everything on every turn and teach the model to hand-write tool calls inside text markers. A mode switch that adds or removes tools keeps the session too: the bridge pushes `notifications/tools/list_changed` over MCP, opencode re-lists, and the cached prefix survives.
31
+ A full tool-calling turn, measured end to end with a real file read, takes about 10 seconds.
20
32
 
21
- A full tool-calling turn, measured end to end including a real file read, takes about 10 seconds.
22
-
23
- ## Using it
33
+ ## Use
24
34
 
25
35
  Run `/model` and pick anything under the `opencode-z` provider, for example `opencode-z/mimo-v2.6-flash-free`.
26
36
 
27
- From the command line:
28
-
29
- ```
37
+ ```text
30
38
  pi --provider opencode-z --model mimo-v2.6-flash-free
31
39
  ```
32
40
 
33
- ## Commands
34
-
35
- `/opencode-z status` shows the server url, the config file, the active models filter, the MCP endpoint, live sessions and the model list. `/opencode-z models` lists the models alone, free ones first. `/opencode-z refresh` re-reads the config file and the catalogue from opencode. `/opencode-z config` shows the resolved settings and where they come from. `/opencode-z help` prints usage.
41
+ | Command | Effect |
42
+ | --- | --- |
43
+ | `/opencode-z status` | Server url, config file, models filter, MCP endpoint, live sessions, model list |
44
+ | `/opencode-z models` | The models alone, free ones first |
45
+ | `/opencode-z refresh` | Re-read the config and the catalogue without restarting pi |
46
+ | `/opencode-z config` | Resolved settings and where they come from |
47
+ | `/opencode-z help` | Usage |
36
48
 
37
- ## Settings
49
+ ## Configure
38
50
 
39
- Settings live in `opencode-z.json` inside Pi's agent directory (usually `~/.pi/agent/opencode-z.json`). A project's `.pi/opencode-z.json` overrides it field by field. Environment variables override both, so either mechanism works and one-off runs never need to touch a file. `/opencode-z config` shows the resolved values, and `/opencode-z refresh` re-reads both files without restarting Pi.
51
+ Settings live in `~/.pi/agent/opencode-z.json`, and a project's `.pi/opencode-z.json` overrides it field by field. Environment variables override both, so a one-off run never needs a file.
40
52
 
41
53
  Free models only:
42
54
 
@@ -55,16 +67,26 @@ A pinned list with a custom binary:
55
67
  }
56
68
  ```
57
69
 
58
- `bin` overrides the `opencode` executable. `models` narrows what gets registered: `free` keeps zero-cost models, a comma-separated string or an array of ids pins the list. Unset or empty, the bridge registers every Zen model your account can reach, free and paid. Only Zen models appear here (the `opencode` provider in opencode's catalogue), never other providers. `OPENCODE_Z_BIN` and `OPENCODE_Z_MODELS` do the same per process and win over the file.
70
+ `models` narrows what gets registered: `free` keeps zero-cost models, and a comma-separated string or an array pins the list. Unset or empty, every Zen model your account can reach is registered. Only Zen models appear, never other providers. `OPENCODE_Z_BIN` and `OPENCODE_Z_MODELS` do the same per process and win over the file.
59
71
 
60
72
  ## Notes
61
73
 
62
- The bridge never denies opencode's built-in tools. A `deny` rule anywhere in the agent's permissions trips the free tier 403 even for tools that were allowed, so every action stays on `ask` and the bridge rejects the unwanted ones at runtime. A weaker model sometimes reaches for its built-in `read` first, gets refused, and retries with `pi_read`, which costs one round trip.
74
+ The bridge never denies opencode's built-in tools. A `deny` rule anywhere in the agent's permissions trips the free-tier 403 even for tools that were allowed, so every action stays on `ask` and the bridge rejects the unwanted ones at runtime. A weaker model sometimes reaches for its built-in `read` first, gets refused, and retries with `pi_read`, which costs one round trip.
75
+
76
+ opencode adds and retires Zen models often, so the catalogue is read live at startup and `/opencode-z refresh` re-reads it.
77
+
78
+ The first prompt lists the exact MCP names available in that turn. Some model hosts display these tools under a `functions.` namespace, and the bridge states that the label does not rename a tool, which keeps a model from mistaking a disabled `read` for `pi_read`.
63
79
 
64
- opencode adds and retires Zen models often. The catalogue is read live at startup, and `/opencode-z refresh` re-reads it without restarting Pi.
80
+ A step that produces reasoning but no text and no tool call is re-prompted through the same bounded recovery path, and so is a step whose only text is a `pi_` call written out as chat template markup.
65
81
 
66
- The first prompt lists the exact MCP names available in that turn. Some model hosts display these tools under a `functions.` namespace. The bridge states that the label does not rename a tool, which keeps a model from mistaking disabled `read` for `pi_read`. A successful model step with reasoning but no text or tool call is re-prompted through the same bounded recovery path, and so is a step whose only text is a pi_ call written out as chat template markup instead of dispatched.
82
+ opencode stops a whole step when the model calls two of its built-in tools at once: refusing one declines the rest. The bridge counts its own refusals and, when that stop was its doing, re-prompts the same session with a nudge back to the `pi_` tools, at most twice per turn, instead of failing the turn.
67
83
 
68
- opencode stops a whole step when the model calls two of its built-in tools at once: refusing one declines the rest. The bridge counts its own refusals and, when that stop was its doing, re-prompts the same session with a nudge back to the pi_ tools (at most twice per turn) instead of failing the turn.
84
+ When a turn fails, the bridge interrupts opencode and answers waiting tool calls with an error. It keeps the opencode session when pi's transcript still ends at the last completed user message, so a retry reuses the prompt cache, and drops it to rebuild from the transcript when a partial assistant or tool result is present. A timeout usually means the model was still working, since large contexts on high thinking levels are slow, or that opencode got stuck waiting.
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ pi install npm:@tduyng/pi-opencode-z
90
+ ```
69
91
 
70
- When a turn fails, the bridge interrupts opencode and answers waiting tool calls with an error. It keeps the opencode session when Pi's transcript still ends at the last completed user message, so a retry can reuse the prompt cache. If Pi's transcript contains a partial assistant or tool result, the bridge drops the session and rebuilds it from Pi's transcript. A timeout usually means the model was still working (large contexts on high thinking levels are slow) or opencode got stuck waiting.
92
+ Local checkout: `pi install /path/to/dotfiles/pi/agent/extensions/pi-opencode-z`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tduyng/pi-opencode-z",
3
- "version": "0.0.1",
4
- "description": "Use opencode's Zen models from Pi, including the free ones, by driving the local opencode server and bridging Pi tools over MCP.",
3
+ "version": "0.0.3",
4
+ "description": "opencode's Zen models in Pi, free ones included, by driving the local opencode server. No API key of your own.",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "pi-package",
package/src/client.ts CHANGED
@@ -128,9 +128,11 @@ export async function api<T = unknown>(
128
128
  try {
129
129
  return await request<T>(info, method, path, body, signal)
130
130
  } catch (error) {
131
- // A cached url or password goes stale when the server restarts. Both failures happen
132
- // before the app sees the request, so re-resolving cannot duplicate it.
133
- const stale = error instanceof TypeError || (error instanceof Error && /failed \(401\)/.test(error.message))
131
+ // A cached url or password goes stale when the server restarts. Retry a read, or a write
132
+ // the server rejected outright with 401; a POST that drops mid-flight may already have run.
133
+ const stale =
134
+ (error instanceof Error && error.message.includes('(401)')) ||
135
+ ((method === 'GET' || method === 'DELETE') && error instanceof TypeError)
134
136
  if (signal?.aborted || !stale) throw error
135
137
  service = undefined
136
138
  return request<T>(await resolveService(), method, path, body, signal)
package/src/config.ts CHANGED
@@ -88,7 +88,7 @@ export function resolveBin(envRaw: string | undefined, fileBin: string | undefin
88
88
  }
89
89
 
90
90
  export function bridgeBin(): string {
91
- return resolveBin(process.env['OPENCODE_Z_BIN'], loadBridgeFile().bin)
91
+ return resolveBin(process.env['OPENCODE_Z_BIN'], readBridgeFile(configPath()).bin)
92
92
  }
93
93
 
94
94
  export function bridgeModelsRaw(): string | undefined {
package/src/index.ts CHANGED
@@ -51,10 +51,12 @@ export default async function opencodeBridge(pi: ExtensionAPI): Promise<void> {
51
51
 
52
52
  async function start(): Promise<void> {
53
53
  await resolveService()
54
- mcpServer = await startMcpServer()
55
- attachMcpServer(runtime, mcpServer)
54
+ const models = await loadModels()
55
+ const server = await startMcpServer()
56
+ mcpServer = server
57
+ attachMcpServer(runtime, server)
56
58
  events = startEventStream((event) => routeEvent(runtime, event))
57
- register(await loadModels())
59
+ register(models)
58
60
  startupError = undefined
59
61
  }
60
62