From 8adc5052f092158381a07c0bbfe088018c5a2eec Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Thu, 26 Feb 2026 14:11:21 -0800 Subject: [PATCH 1/2] feat(opencode): prioritize text/markdown in webfetch --- packages/opencode/src/tool/webfetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index a66e66c097b8..0d2c8ea68343 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -53,7 +53,7 @@ export const WebFetchTool = Tool.define("webfetch", { break default: acceptHeader = - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" + "text/markdown,text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8" } const headers = { "User-Agent": From f70cf96a062984db050d5af2282a446d97ce897c Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Thu, 26 Feb 2026 19:04:39 -0800 Subject: [PATCH 2/2] test: add accept header tests for webfetch tool --- packages/opencode/test/tool/webfetch.test.ts | 124 +++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index 0214700fedca..1bc2c3f47746 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -29,6 +29,130 @@ async function withFetch( } } +describe("tool.webfetch accept headers", () => { + test("markdown format sends text/markdown as highest priority", async () => { + let captured: RequestInit | undefined + await withFetch( + async (_url, init) => { + captured = init + return new Response("# Hello", { status: 200, headers: { "content-type": "text/markdown" } }) + }, + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + await webfetch.execute({ url: "https://example.com", format: "markdown" }, ctx) + const accept = (captured?.headers as Record)?.Accept ?? "" + expect(accept).toStartWith("text/markdown") + }, + }) + }, + ) + }) + + test("text format sends text/plain as highest priority", async () => { + let captured: RequestInit | undefined + await withFetch( + async (_url, init) => { + captured = init + return new Response("hello", { status: 200, headers: { "content-type": "text/plain" } }) + }, + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + await webfetch.execute({ url: "https://example.com", format: "text" }, ctx) + const accept = (captured?.headers as Record)?.Accept ?? "" + expect(accept).toStartWith("text/plain") + }, + }) + }, + ) + }) + + test("html format sends text/html as highest priority", async () => { + let captured: RequestInit | undefined + await withFetch( + async (_url, init) => { + captured = init + return new Response("

hi

", { status: 200, headers: { "content-type": "text/html" } }) + }, + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + await webfetch.execute({ url: "https://example.com", format: "html" }, ctx) + const accept = (captured?.headers as Record)?.Accept ?? "" + expect(accept).toStartWith("text/html") + }, + }) + }, + ) + }) + + test("default accept header prioritizes text/markdown", async () => { + let captured: RequestInit | undefined + await withFetch( + async (_url, init) => { + captured = init + return new Response("# Hello", { status: 200, headers: { "content-type": "text/markdown" } }) + }, + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + // format defaults to "markdown" via zod, so we test the explicit markdown path + await webfetch.execute({ url: "https://example.com", format: "markdown" }, ctx) + const accept = (captured?.headers as Record)?.Accept ?? "" + expect(accept).toContain("text/markdown") + expect(accept.indexOf("text/markdown")).toBeLessThan(accept.indexOf("text/html")) + }, + }) + }, + ) + }) + + test("markdown format returns server markdown without conversion", async () => { + const md = "# Title\n\nSome **bold** text" + await withFetch( + async () => new Response(md, { status: 200, headers: { "content-type": "text/markdown; charset=utf-8" } }), + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + const result = await webfetch.execute({ url: "https://example.com/page", format: "markdown" }, ctx) + expect(result.output).toBe(md) + }, + }) + }, + ) + }) + + test("markdown format converts html response to markdown", async () => { + const html = "

Title

Hello world

" + await withFetch( + async () => new Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }), + async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const webfetch = await WebFetchTool.init() + const result = await webfetch.execute({ url: "https://example.com/page", format: "markdown" }, ctx) + expect(result.output).toContain("Title") + expect(result.output).toContain("Hello world") + expect(result.output).not.toContain("

") + }, + }) + }, + ) + }) +}) + describe("tool.webfetch", () => { test("returns image responses as file attachments", async () => { const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])