opencode-fetch-writer 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tonydeng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # opencode-fetch-writer
2
+
3
+ [![CI](https://github.com/tonydeng/opencode-fetch-writer/actions/workflows/ci.yml/badge.svg)](https://github.com/tonydeng/opencode-fetch-writer/actions/workflows/ci.yml)
4
+
5
+ OpenCode plugin that rewrites the **User-Agent** and adds/removes **HTTP headers** on outgoing provider requests — for any provider, configured purely through plugin options.
6
+
7
+ **中文文档:[README.zh-CN.md](./README.zh-CN.md)**
8
+
9
+ ## Why
10
+
11
+ Two common failure modes when pointing OpenCode at a custom or enterprise model gateway:
12
+
13
+ 1. **Your configured `User-Agent` is silently ignored.** Some SDKs merge their own default UA *after* your `provider.options.headers`, so the gateway never sees the UA you configured.
14
+ 2. **The gateway rejects the request** because of the default client fingerprint (UA or headers added by intermediate layers).
15
+
16
+ This plugin solves both by wrapping the provider's `options.fetch` through the official `config` hook. `options.fetch` is the final outbound choke point — headers set there cannot be overridden by SDK defaults.
17
+
18
+ ## Install
19
+
20
+ ### OpenCode v1 (1.x, tuple syntax)
21
+
22
+ ```jsonc
23
+ // opencode.jsonc
24
+ {
25
+ "plugin": [
26
+ ["opencode-fetch-writer@0.1.0", {
27
+ "providerId": "my-provider",
28
+ "uaTarget": "my-app/1.0.0",
29
+ "headersToStrip": ["x-unwanted-header"],
30
+ "headersToInject": {
31
+ "X-Client-Name": "my-app"
32
+ }
33
+ }]
34
+ ]
35
+ }
36
+ ```
37
+
38
+ ### OpenCode v2 (object syntax)
39
+
40
+ ```jsonc
41
+ {
42
+ "plugins": [
43
+ {
44
+ "package": "opencode-fetch-writer@0.1.0",
45
+ "options": {
46
+ "providerId": "my-provider",
47
+ "uaTarget": "my-app/1.0.0",
48
+ "headersToStrip": ["x-unwanted-header"],
49
+ "headersToInject": {
50
+ "X-Client-Name": "my-app"
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ ### Local development
59
+
60
+ ```jsonc
61
+ {
62
+ "plugin": ["file://./src/index.ts"]
63
+ }
64
+ ```
65
+
66
+ ## Options
67
+
68
+ | Option | Type | Default | Description |
69
+ |---|---|---|---|
70
+ | `providerId` | `string` | — | Provider ID (key in your `provider` map). **Required** — the plugin stays inactive without it. |
71
+ | `uaTarget` | `string` | — | Target User-Agent. Omit to leave the UA unchanged. |
72
+ | `headersToStrip` | `string[]` | `[]` | Header names to delete from outgoing requests. |
73
+ | `headersToInject` | `Record<string, string>` | `{}` | Headers to inject when missing. **Never overwrites** existing values. |
74
+ | `debug` | `boolean` | `false` | Enable debug logging to stderr. |
75
+
76
+ ## Environment variables
77
+
78
+ | Variable | Description | Default |
79
+ |---|---|---|
80
+ | `FETCH_WRITER_UA` | Overrides the `uaTarget` option | — |
81
+ | `FETCH_WRITER_DEBUG` | Set to `1` to enable debug logging | `0` |
82
+
83
+ ## Behavior notes
84
+
85
+ - **Marker guard**: the plugin marks the wrapped fetch and refuses to wrap twice. If the plugin gets loaded through both auto-discovery and an explicit config entry, your requests are still wrapped exactly once.
86
+ - **Activation log**: on startup, the plugin always prints `[fetch-writer] patched options.fetch for provider "…"` to stderr so you can confirm it took effect.
87
+ - **Both fetch call shapes** supported: `fetch(url, init)` and `fetch(new Request(...))`.
88
+
89
+ ## Troubleshooting
90
+
91
+ - **Plugin not taking effect** — check that `providerId` matches the key in your `provider` map exactly (case-sensitive).
92
+ - **UA still overridden** — make sure no other plugin or provider option also sets a custom `fetch` after this one.
93
+ - **Double patching** — the marker guard handles it; if you see the activation line twice, you have two *different* fetch wrappers active, not this plugin twice.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ bun install
99
+ bun run typecheck
100
+ bun run build
101
+ bun test
102
+ ```
103
+
104
+ ## License
105
+
106
+ [MIT](./LICENSE)
@@ -0,0 +1,106 @@
1
+ # opencode-fetch-writer
2
+
3
+ [![CI](https://github.com/tonydeng/opencode-fetch-writer/actions/workflows/ci.yml/badge.svg)](https://github.com/tonydeng/opencode-fetch-writer/actions/workflows/ci.yml)
4
+
5
+ OpenCode 插件:改写 provider 出站请求的 **User-Agent** 并增删 **HTTP headers**——适用于任意 provider,完全通过插件 options 配置。
6
+
7
+ **English: [README.md](./README.md)**
8
+
9
+ ## 为什么需要
10
+
11
+ 把 OpenCode 指向自定义或企业级模型网关时,常见两类故障:
12
+
13
+ 1. **你配置的 `User-Agent` 被静默忽略。** 部分 SDK 在合并 `provider.options.headers` *之后*再叠加自己的默认 UA,网关根本看不到你配置的 UA。
14
+ 2. **网关直接拒绝请求**,因为默认客户端指纹不合规(UA 或中间层附加的 headers)。
15
+
16
+ 本插件通过官方 `config` hook 包装 provider 的 `options.fetch` 解决以上两个问题。`options.fetch` 是请求的最终出口——在这里设置的 headers 不会被 SDK 默认值覆盖。
17
+
18
+ ## 安装
19
+
20
+ ### OpenCode v1(1.x,元组语法)
21
+
22
+ ```jsonc
23
+ // opencode.jsonc
24
+ {
25
+ "plugin": [
26
+ ["opencode-fetch-writer@0.1.0", {
27
+ "providerId": "my-provider",
28
+ "uaTarget": "my-app/1.0.0",
29
+ "headersToStrip": ["x-unwanted-header"],
30
+ "headersToInject": {
31
+ "X-Client-Name": "my-app"
32
+ }
33
+ }]
34
+ ]
35
+ }
36
+ ```
37
+
38
+ ### OpenCode v2(对象语法)
39
+
40
+ ```jsonc
41
+ {
42
+ "plugins": [
43
+ {
44
+ "package": "opencode-fetch-writer@0.1.0",
45
+ "options": {
46
+ "providerId": "my-provider",
47
+ "uaTarget": "my-app/1.0.0",
48
+ "headersToStrip": ["x-unwanted-header"],
49
+ "headersToInject": {
50
+ "X-Client-Name": "my-app"
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ ### 本地开发
59
+
60
+ ```jsonc
61
+ {
62
+ "plugin": ["file://./src/index.ts"]
63
+ }
64
+ ```
65
+
66
+ ## Options
67
+
68
+ | 选项 | 类型 | 默认值 | 说明 |
69
+ |---|---|---|---|
70
+ | `providerId` | `string` | — | Provider ID(`provider` 映射中的键)。**必填**——缺省时插件不激活。 |
71
+ | `uaTarget` | `string` | — | 目标 User-Agent。缺省则不改写 UA。 |
72
+ | `headersToStrip` | `string[]` | `[]` | 需要从出站请求中删除的 header 名。 |
73
+ | `headersToInject` | `Record<string, string>` | `{}` | 缺失时注入的 headers。**从不覆盖**已有值。 |
74
+ | `debug` | `boolean` | `false` | 向 stderr 输出调试日志。 |
75
+
76
+ ## 环境变量
77
+
78
+ | 变量 | 说明 | 默认值 |
79
+ |---|---|---|
80
+ | `FETCH_WRITER_UA` | 覆盖 `uaTarget` 选项 | — |
81
+ | `FETCH_WRITER_DEBUG` | 设为 `1` 启用调试日志 | `0` |
82
+
83
+ ## 行为说明
84
+
85
+ - **MARKER 守卫**:插件在包装后的 fetch 上打标记,拒绝二次包装。即使同时被 plugins/ 目录自动加载和配置文件显式引用,请求也只会被包装一次。
86
+ - **激活日志**:启动时插件总会向 stderr 输出一行 `[fetch-writer] patched options.fetch for provider "…"`,便于确认生效。
87
+ - **兼容两种 fetch 调用形态**:`fetch(url, init)` 与 `fetch(new Request(...))`。
88
+
89
+ ## 问题排查
90
+
91
+ - **插件不生效**——检查 `providerId` 是否与 `provider` 映射中的键完全一致(区分大小写)。
92
+ - **UA 仍被覆盖**——确认没有其他插件或 provider option 在本插件之后再次设置自定义 `fetch`。
93
+ - **双重注入**——MARKER 守卫已处理;若激活日志出现两次,说明有两个*不同*的 fetch 包装器在生效,而非本插件重复加载。
94
+
95
+ ## 开发
96
+
97
+ ```bash
98
+ bun install
99
+ bun run typecheck
100
+ bun run build
101
+ bun test
102
+ ```
103
+
104
+ ## 许可证
105
+
106
+ [MIT](./LICENSE)
@@ -0,0 +1,40 @@
1
+ /**
2
+ * opencode-fetch-writer
3
+ *
4
+ * Generic OpenCode plugin that rewrites outgoing HTTP headers (including
5
+ * User-Agent) for a configured provider, by wrapping its `options.fetch`
6
+ * through the official `config` hook.
7
+ *
8
+ * Why `options.fetch`? It is the final outbound choke point: headers set
9
+ * here cannot be overridden by SDK defaults. This is not true for
10
+ * `provider.options.headers` — some SDKs merge their own User-Agent after
11
+ * yours, silently discarding the configured value.
12
+ *
13
+ * Features:
14
+ * - Rewrite the User-Agent to any target string
15
+ * - Strip unwanted headers (e.g. ones injected by intermediate layers)
16
+ * - Idempotently inject fallback headers (never overwrites existing values)
17
+ * - Marker guard against double-wrapping when the plugin gets loaded twice
18
+ * (auto-discovery in a plugins/ dir + an explicit entry in the config)
19
+ *
20
+ * Environment variables (all optional):
21
+ * FETCH_WRITER_UA - overrides the `uaTarget` option
22
+ * FETCH_WRITER_DEBUG - set to "1" to enable debug logging (stderr)
23
+ */
24
+ import type { Plugin } from "@opencode-ai/plugin";
25
+ export interface FetchWriterOptions {
26
+ /** Provider ID (key in the `provider` map) whose fetch will be wrapped. Required to activate the plugin. */
27
+ providerId?: string;
28
+ /** Target User-Agent string. Omit to leave the UA unchanged. */
29
+ uaTarget?: string;
30
+ /** Header names to delete from outgoing requests. Default: [] */
31
+ headersToStrip?: string[];
32
+ /** Headers to inject when missing. Never overwrites existing values. Default: {} */
33
+ headersToInject?: Record<string, string>;
34
+ /** Enable debug logging to stderr. Default: false */
35
+ debug?: boolean;
36
+ }
37
+ /** Marker guard: prevents double-wrapping when the plugin is loaded twice */
38
+ export declare const MARKER = "__fetchWriterPatched";
39
+ export declare const fetchWriter: Plugin;
40
+ export default fetchWriter;
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * opencode-fetch-writer
3
+ *
4
+ * Generic OpenCode plugin that rewrites outgoing HTTP headers (including
5
+ * User-Agent) for a configured provider, by wrapping its `options.fetch`
6
+ * through the official `config` hook.
7
+ *
8
+ * Why `options.fetch`? It is the final outbound choke point: headers set
9
+ * here cannot be overridden by SDK defaults. This is not true for
10
+ * `provider.options.headers` — some SDKs merge their own User-Agent after
11
+ * yours, silently discarding the configured value.
12
+ *
13
+ * Features:
14
+ * - Rewrite the User-Agent to any target string
15
+ * - Strip unwanted headers (e.g. ones injected by intermediate layers)
16
+ * - Idempotently inject fallback headers (never overwrites existing values)
17
+ * - Marker guard against double-wrapping when the plugin gets loaded twice
18
+ * (auto-discovery in a plugins/ dir + an explicit entry in the config)
19
+ *
20
+ * Environment variables (all optional):
21
+ * FETCH_WRITER_UA - overrides the `uaTarget` option
22
+ * FETCH_WRITER_DEBUG - set to "1" to enable debug logging (stderr)
23
+ */
24
+ /** Marker guard: prevents double-wrapping when the plugin is loaded twice */
25
+ export const MARKER = "__fetchWriterPatched";
26
+ export const fetchWriter = async (_input, options) => {
27
+ const providerId = options?.providerId;
28
+ const uaTarget = options?.uaTarget ?? process.env.FETCH_WRITER_UA;
29
+ const headersToStrip = options?.headersToStrip ?? [];
30
+ const headersToInject = options?.headersToInject ?? {};
31
+ const debug = options?.debug === true || process.env.FETCH_WRITER_DEBUG === "1";
32
+ const log = (msg) => {
33
+ if (debug)
34
+ console.error(`[fetch-writer] ${msg}`);
35
+ };
36
+ return {
37
+ config: async (config) => {
38
+ if (!providerId) {
39
+ console.error("[fetch-writer] no providerId configured, plugin inactive");
40
+ return;
41
+ }
42
+ const provider = config.provider?.[providerId];
43
+ if (!provider) {
44
+ log(`provider "${providerId}" not found in config, skipping`);
45
+ return;
46
+ }
47
+ const opts = (provider.options ??= {});
48
+ const existingFetch = opts.fetch;
49
+ if (typeof existingFetch?.[MARKER] === "boolean") {
50
+ log("options.fetch already patched, skipping (marker guard)");
51
+ return;
52
+ }
53
+ const realFetch = existingFetch ?? globalThis.fetch.bind(globalThis);
54
+ const wrapped = (async (req, init) => {
55
+ try {
56
+ // Support both fetch(url, init) and fetch(Request) call shapes
57
+ const headers = new Headers(init?.headers ?? undefined);
58
+ if (init?.headers === undefined && typeof Request !== "undefined" && req instanceof Request) {
59
+ req.headers.forEach((v, k) => {
60
+ if (!headers.has(k))
61
+ headers.set(k, v);
62
+ });
63
+ }
64
+ if (uaTarget) {
65
+ log(`user-agent: ${headers.get("user-agent")} -> ${uaTarget}`);
66
+ headers.set("user-agent", uaTarget);
67
+ }
68
+ for (const name of headersToStrip)
69
+ headers.delete(name);
70
+ for (const [name, value] of Object.entries(headersToInject)) {
71
+ if (!headers.has(name))
72
+ headers.set(name, value);
73
+ }
74
+ return realFetch(req, init ? { ...init, headers } : { headers });
75
+ }
76
+ catch (err) {
77
+ log(`wrap failed, falling back to raw fetch: ${err}`);
78
+ return realFetch(req, init);
79
+ }
80
+ });
81
+ wrapped[MARKER] = true;
82
+ opts.fetch = wrapped;
83
+ // Always emit one activation line so users can confirm the patch at startup
84
+ console.error(`[fetch-writer] patched options.fetch for provider "${providerId}"`);
85
+ },
86
+ };
87
+ };
88
+ export default fetchWriter;
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "opencode-fetch-writer",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin: rewrite User-Agent and add/remove headers on provider requests",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "bun test"
18
+ },
19
+ "keywords": [
20
+ "opencode",
21
+ "opencode-plugin",
22
+ "plugin",
23
+ "fetch",
24
+ "user-agent",
25
+ "headers",
26
+ "rewrite",
27
+ "http"
28
+ ],
29
+ "author": "tonydeng",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/tonydeng/opencode-fetch-writer.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/tonydeng/opencode-fetch-writer/issues"
37
+ },
38
+ "homepage": "https://github.com/tonydeng/opencode-fetch-writer#readme",
39
+ "peerDependencies": {
40
+ "@opencode-ai/plugin": ">=1.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@opencode-ai/plugin": "^1.17.0",
44
+ "@types/bun": "latest",
45
+ "typescript": "^5.7.0"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "README.zh-CN.md",
51
+ "LICENSE"
52
+ ]
53
+ }