fix(opencode): match the upstream free-tier request contract (#14013)

Match the upstream OpenCode free-tier request contract (issue #13935): canonical
ses_/msg_ identity ids, versioned User-Agent, and the measured body requirements
(stream:true + non-empty tools) with a learn-and-reuse tool-name cache, so
no-auth oc/* requests stop being refused with 403 FreeTierError.

Supersedes #13937 (session regex and minimum-version rule kept, credited below).
Complements #14011 (refusal classification) and #13819 (stream_options strip),
both already merged.

Closes #13935

Co-authored-by: AStupidBear <16422976+AStupidBear@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Dizzle
2026-09-18 17:57:10 +02:00
committed by GitHub
parent 2ebafaedce
commit 0349627c86
12 changed files with 1609 additions and 142 deletions

View File

@@ -236,7 +236,10 @@ describe("OpencodeExecutor", () => {
});
it("omits accept header when stream is false", async () => {
const result = await zenExecutor.execute(createInput("big-pickle", false));
// A paid model carries the client's non-streaming expectation through. A free-tier
// model does not: the gated tier only answers streamed requests, so the executor
// announces the event stream and the JSON body is rebuilt from it.
const result = await zenExecutor.execute(createInput("gpt-5.6-luna", false));
assert.deepEqual(result.headers, {
Authorization: "Bearer test-key",
@@ -245,6 +248,12 @@ describe("OpencodeExecutor", () => {
assert.deepEqual(fetchCalls[0].options.headers, result.headers);
});
it("announces the event stream for a free-tier model even when the client wants JSON", async () => {
const result = await zenExecutor.execute(createInput("big-pickle", false));
assert.equal(result.headers["Accept"], "text/event-stream");
});
it("omits authorization when credentials are missing", async () => {
const result = await zenExecutor.execute(createInput("minimax-m2.5-free", true, null));