From d8f2e17dd7d7532b1987d025003e87fbf513ea5f Mon Sep 17 00:00:00 2001 From: yetone Date: Thu, 1 Oct 2026 02:53:12 +0800 Subject: [PATCH] =?UTF-8?q?plugins:=20a=20plugin's=20fetch=20gets=20its=20?= =?UTF-8?q?body=20as=20a=20string,=20as=20OpenCode=20hands=20it=20?= =?UTF-8?q?=E2=80=94=20the=20host=20passed=20a=20Buffer,=20and=20the=20plu?= =?UTF-8?q?gins=20that=20reshape=20a=20request=20(Grok,=20WorkBuddy,=20MiM?= =?UTF-8?q?o)=20touch=20only=20a=20string=20body,=20so=20a=20moved=20provi?= =?UTF-8?q?der's=20requests=20went=20out=20as=20the=20gateway=20built=20th?= =?UTF-8?q?em.=20A=20UTF-8=20body=20now=20goes=20as=20a=20string;=20one=20?= =?UTF-8?q?that=20isn't=20stays=20bytes.=20TestFakePlugin=20fails=20withou?= =?UTF-8?q?t=20the=20change.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/plugin/host.js | 15 ++++++++++++++- internal/plugin/plugin_test.go | 4 ++++ internal/plugin/testdata/fake/index.js | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/plugin/host.js b/internal/plugin/host.js index 16c764f7..3d211cf4 100644 --- a/internal/plugin/host.js +++ b/internal/plugin/host.js @@ -709,6 +709,19 @@ function sdkHeaders(npm, key) { return { authorization: `Bearer ${key}` } } +// bodyOf is the body as OpenCode hands it to a plugin's fetch: the string +// the AI SDK built, as the plugins reshape only a string body. One that +// isn't UTF-8 text stays bytes. +function bodyOf(b64) { + if (!b64) return undefined + const b = Buffer.from(b64, "base64") + try { + return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(b) + } catch { + return b + } +} + async function doFetch(id, params) { const key = accountKey(params.provider, params.account) return inScope(params.provider, key, () => fetchAs(id, key, params)) @@ -750,7 +763,7 @@ async function fetchAs(id, key, { provider, model, npm, url, method, headers, bo const res = await f(url, { method: method ?? "POST", headers: h, - body: body ? Buffer.from(body, "base64") : undefined, + body: bodyOf(body), signal: ctl.signal, }) const rh = {} diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 9c1ac5d2..cc2e1f19 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -145,6 +145,10 @@ func TestFakePlugin(t *testing.T) { if h.Get("Authorization") != "Bearer fresh-r-blue" || h.Get("X-Plugin-Model") != "fake-1" || h.Get("X-Models") != "fake-1,fake-claude,fake-gemini" { t.Fatalf("headers = %v", h) } + // the plugin's fetch got the body as a string, as OpenCode hands it + if h.Get("X-Body-Type") != "string" { + t.Fatalf("the plugin got a %s body", h.Get("X-Body-Type")) + } b, _ = os.ReadFile(AuthPath()) json.Unmarshal(b, &saved) if saved["fakeco"]["access"] != "fresh-r-blue" { diff --git a/internal/plugin/testdata/fake/index.js b/internal/plugin/testdata/fake/index.js index 59bbc7e3..8640ec12 100644 --- a/internal/plugin/testdata/fake/index.js +++ b/internal/plugin/testdata/fake/index.js @@ -50,6 +50,7 @@ export const FakePlugin = async ({ client }) => ({ const h = new Headers(init.headers) h.set("authorization", "Bearer " + (a.type === "oauth" ? a.access : a.key)) h.set("x-models", Object.keys(provider.models).sort().join(",")) + h.set("x-body-type", typeof init.body) return fetch(url, { ...init, headers: h }) }, }),