@bojackduy/opencode-system-override 0.1.0 → 0.1.1
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 +35 -0
- package/package.json +1 -1
- package/src/server.test.ts +59 -1
- package/src/server.ts +35 -0
package/README.md
CHANGED
|
@@ -77,6 +77,31 @@ RAW_SYSTEM_OVERRIDE_END_9F3A
|
|
|
77
77
|
Select the `Raw` agent and chat — the model sees only your text as its
|
|
78
78
|
system prompt. Leave the space empty for a truly empty system (pure LLM).
|
|
79
79
|
|
|
80
|
+
## Custom agents (any name works)
|
|
81
|
+
|
|
82
|
+
The hook never checks the agent's name — it only looks for the marker pair
|
|
83
|
+
in the assembled system text. So you can give any agent its own override by
|
|
84
|
+
putting the markers in its file:
|
|
85
|
+
|
|
86
|
+
```md
|
|
87
|
+
---
|
|
88
|
+
description: Code reviewer with no default baggage
|
|
89
|
+
mode: primary
|
|
90
|
+
permission:
|
|
91
|
+
"*": deny
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
RAW_SYSTEM_OVERRIDE_START_9F3A
|
|
95
|
+
|
|
96
|
+
You are a senior reviewer. Be terse. Flag only real bugs.
|
|
97
|
+
|
|
98
|
+
RAW_SYSTEM_OVERRIDE_END_9F3A
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Each agent carries its own text between the markers; agents without markers
|
|
102
|
+
are completely untouched. The shipped `agents/Raw.md` is just a blank
|
|
103
|
+
starter — copy it to `Pirate.md`, `Reviewer.md`, whatever you like.
|
|
104
|
+
|
|
80
105
|
## How it works
|
|
81
106
|
|
|
82
107
|
OpenCode assembles one system array per request. The
|
|
@@ -90,6 +115,16 @@ and mutates `output.system` **in place**:
|
|
|
90
115
|
| Text between markers | system = that text |
|
|
91
116
|
| Nothing/whitespace between markers | system = `[]` (pure LLM) |
|
|
92
117
|
|
|
118
|
+
A second hook, `chat.headers`, keeps OpenCode Zen free-tier models working.
|
|
119
|
+
Zen only serves anonymous free models to requests carrying the opencode
|
|
120
|
+
client `User-Agent`, and now also requires a stable per-conversation
|
|
121
|
+
`x-opencode-session` header — without them you get `must using opencode
|
|
122
|
+
for opencode free endpoint` / `429 FreeUsageLimitError`. For `opencode`
|
|
123
|
+
provider requests only, the plugin injects `User-Agent: opencode/<version>`
|
|
124
|
+
(when absent or not already `opencode/*`) and `x-opencode-session:
|
|
125
|
+
<sessionID>`, merging in place so other plugins' headers are preserved.
|
|
126
|
+
Override the UA with `OPENCODE_USER_AGENT=opencode/x.y.z` if needed.
|
|
127
|
+
|
|
93
128
|
## Dev
|
|
94
129
|
|
|
95
130
|
```sh
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-system-override",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Overwrite the OpenCode system prompt per agent — a raw-system hook that replaces the assembled system with the prompt between markers in your agent file.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "AGPL-3.0-or-later",
|
package/src/server.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ const fakeClient = {
|
|
|
15
15
|
|
|
16
16
|
async function hooks() {
|
|
17
17
|
logged.length = 0
|
|
18
|
-
const server = (pluginModule as unknown as { server: (input: unknown) => Promise<Record<string, (input: unknown, output:
|
|
18
|
+
const server = (pluginModule as unknown as { server: (input: unknown) => Promise<Record<string, (input: unknown, output: never) => Promise<void>>> }).server
|
|
19
19
|
return server({ client: fakeClient } as never)
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -29,6 +29,20 @@ async function transform(system: string[]) {
|
|
|
29
29
|
return output.system
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
async function runHeaders(
|
|
33
|
+
input: { sessionID?: string; model?: { providerID: string }; provider?: { info: { id: string } } },
|
|
34
|
+
initial: Record<string, string> = {},
|
|
35
|
+
) {
|
|
36
|
+
const h = await hooks()
|
|
37
|
+
const output = { headers: { ...initial } }
|
|
38
|
+
const ref = output.headers
|
|
39
|
+
await (
|
|
40
|
+
h["chat.headers"] as (input: unknown, output: { headers: Record<string, string> }) => Promise<void>
|
|
41
|
+
)(input, output as never)
|
|
42
|
+
expect(output.headers).toBe(ref)
|
|
43
|
+
return output.headers
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
describe("system-override transform", () => {
|
|
33
47
|
test("leaves non-raw agents untouched (no markers)", async () => {
|
|
34
48
|
const system = ["default agent prompt", "environment info"]
|
|
@@ -58,3 +72,47 @@ describe("system-override transform", () => {
|
|
|
58
72
|
expect(logged.join("\n")).toContain("END marker missing")
|
|
59
73
|
})
|
|
60
74
|
})
|
|
75
|
+
|
|
76
|
+
describe("chat.headers (Zen free-tier gating)", () => {
|
|
77
|
+
test("injects User-Agent + x-opencode-session for opencode provider", async () => {
|
|
78
|
+
const out = await runHeaders(
|
|
79
|
+
{ sessionID: "ses_abc123", model: { providerID: "opencode" } },
|
|
80
|
+
{},
|
|
81
|
+
)
|
|
82
|
+
expect(out["User-Agent"]).toMatch(/^opencode\//)
|
|
83
|
+
expect(out["x-opencode-session"]).toBe("ses_abc123")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("ignores non-opencode providers (no UA spoof, no session leak)", async () => {
|
|
87
|
+
const out = await runHeaders(
|
|
88
|
+
{ sessionID: "ses_abc123", model: { providerID: "anthropic" } },
|
|
89
|
+
{},
|
|
90
|
+
)
|
|
91
|
+
expect(out).toEqual({})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("never clobbers an existing opencode/* User-Agent", async () => {
|
|
95
|
+
const out = await runHeaders(
|
|
96
|
+
{ sessionID: "ses_xyz", model: { providerID: "opencode" } },
|
|
97
|
+
{ "User-Agent": "opencode/9.9.9" },
|
|
98
|
+
)
|
|
99
|
+
expect(out["User-Agent"]).toBe("opencode/9.9.9")
|
|
100
|
+
expect(out["x-opencode-session"]).toBe("ses_xyz")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("repairs a non-opencode User-Agent", async () => {
|
|
104
|
+
const out = await runHeaders(
|
|
105
|
+
{ sessionID: "ses_1", model: { providerID: "opencode" } },
|
|
106
|
+
{ "User-Agent": "curl/8.0" },
|
|
107
|
+
)
|
|
108
|
+
expect(out["User-Agent"]).toMatch(/^opencode\//)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("preserves an existing session header", async () => {
|
|
112
|
+
const out = await runHeaders(
|
|
113
|
+
{ sessionID: "ses_new", model: { providerID: "opencode" } },
|
|
114
|
+
{ "x-opencode-session": "ses_old" },
|
|
115
|
+
)
|
|
116
|
+
expect(out["x-opencode-session"]).toBe("ses_old")
|
|
117
|
+
})
|
|
118
|
+
})
|
package/src/server.ts
CHANGED
|
@@ -24,6 +24,23 @@ const PLUGIN_ID = "opencode-system-override"
|
|
|
24
24
|
const START = "RAW_SYSTEM_OVERRIDE_START_9F3A"
|
|
25
25
|
const END = "RAW_SYSTEM_OVERRIDE_END_9F3A"
|
|
26
26
|
|
|
27
|
+
// Zen free-tier gating: anonymous free models at opencode.ai/zen only serve
|
|
28
|
+
// requests carrying the opencode client User-Agent, and the vendor now also
|
|
29
|
+
// requires a stable per-conversation `x-opencode-session` header (missing it
|
|
30
|
+
// surfaces as "must using opencode for opencode free endpoint" / 429
|
|
31
|
+
// FreeUsageLimitError). Inject both via the `chat.headers` hook so free
|
|
32
|
+
// models keep working regardless of which agent (Raw or not) is active.
|
|
33
|
+
//
|
|
34
|
+
// - User-Agent: only set when absent or not already `opencode/*`, so we never
|
|
35
|
+
// clobber the real client UA or another plugin's contribution.
|
|
36
|
+
// - x-opencode-session: stable per conversation → use the opencode sessionID
|
|
37
|
+
// directly (that is exactly the vendor's stated intent).
|
|
38
|
+
// - Scoped to the `opencode` provider only, so we never leak session IDs or
|
|
39
|
+
// spoof UA to third-party providers.
|
|
40
|
+
const OPENCODE_PROVIDER_ID = "opencode"
|
|
41
|
+
const FALLBACK_USER_AGENT = "opencode/1.18.34"
|
|
42
|
+
const SESSION_HEADER = "x-opencode-session"
|
|
43
|
+
|
|
27
44
|
const server: Plugin = async ({ client }) => {
|
|
28
45
|
const log = (message: string) => {
|
|
29
46
|
try {
|
|
@@ -34,6 +51,24 @@ const server: Plugin = async ({ client }) => {
|
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
return {
|
|
54
|
+
"chat.headers": async (input, output) => {
|
|
55
|
+
// Only touch opencode-provider requests (Zen / Zen-Go free + paid).
|
|
56
|
+
const providerID =
|
|
57
|
+
input.model?.providerID ?? input.provider?.info?.id ?? input.provider?.info?.name
|
|
58
|
+
if (providerID !== OPENCODE_PROVIDER_ID) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
output.headers ??= {}
|
|
62
|
+
const existingUA =
|
|
63
|
+
output.headers["User-Agent"] ?? output.headers["user-agent"]
|
|
64
|
+
if (!existingUA || !existingUA.startsWith("opencode/")) {
|
|
65
|
+
output.headers["User-Agent"] = process.env.OPENCODE_USER_AGENT ?? FALLBACK_USER_AGENT
|
|
66
|
+
}
|
|
67
|
+
if (input.sessionID && !output.headers[SESSION_HEADER]) {
|
|
68
|
+
output.headers[SESSION_HEADER] = input.sessionID
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
37
72
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
38
73
|
// Join defensively in case multiple system blocks exist.
|
|
39
74
|
const assembled = output.system.join("\n")
|