fix(opencode): report a provider 401 from the hello probe as missing authentication

OpenRouter answers an empty or unknown key with HTTP 401 and messages such
as "Missing Authentication header" or "User not found.", which the probe's
wording regex missed, so readiness showed a generic probe error instead of
the missing-account state. Classify the structured APIError status 401 as
opencode_hello_probe_auth_required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-11 11:48:38 -06:00
1 parent 76da2fec4f
commit 44b2fd3e1c
3 files changed
+41 -2

No files matched your search

@@ -106,6 +106,18 @@ export function createOpenCodeJsonlParser() {
};
}
/** HTTP statuses that OpenCode reports on provider API error events. */
export function openCodeErrorStatusCodes(stdout: string): number[] {
const statuses: number[] = [];
for (const rawLine of stdout.split(/\r?\n/)) {
const event = parseJson(rawLine.trim());
if (!event || asString(event.type, "") !== "error") continue;
const status = parseObject(parseObject(event.error).data).statusCode;
if (typeof status === "number" && Number.isInteger(status)) statuses.push(status);
}
return statuses;
}
export function isOpenCodeUnknownSessionError(stdout: string, stderr: string): boolean {
const haystack = `${stdout}\n${stderr}`
.split(/\r?\n/)
@@ -119,6 +119,30 @@ describe("opencode remote environment diagnostics", () => {
},
);
// Recorded from OpenCode 1.18.34 against OpenRouter with an empty and an unknown key.
it.each(["Missing Authentication header", "User not found."])(
"reports a provider 401 as missing authentication, not a generic failure (%s)", async message => {
const successfulProbe = await runAdapterExecutionTargetProcess.getMockImplementation()!();
runAdapterExecutionTargetProcess.mockResolvedValueOnce({ ...successfulProbe, exitCode: 1, stderr: "",
stdout: JSON.stringify({ type: "error", sessionID: "session-1", error: { name: "APIError",
data: { message, statusCode: 401, isRetryable: false, responseHeaders: { "access-control-expose-headers": "WWW-Authenticate" } } } }) });
const result = await testEnvironment({ companyId: "company-1", adapterType: "paperclip_runner",
config: { model: "openrouter/deepseek/deepseek-v4-flash-0731", env: { XDG_CONFIG_HOME: configHome } } });
expect(result.status).toBe("warn");
expect(result.checks.map(check => [check.code, check.level])).toContainEqual(["opencode_hello_probe_auth_required", "warn"]);
expect(result.checks.some(check => check.level === "error")).toBe(false);
},
);
it.each([500, 429])("keeps other provider statuses a generic probe failure (%s)", async statusCode => {
const successfulProbe = await runAdapterExecutionTargetProcess.getMockImplementation()!();
runAdapterExecutionTargetProcess.mockResolvedValueOnce({ ...successfulProbe, exitCode: 1, stderr: "",
stdout: JSON.stringify({ type: "error", error: { name: "APIError", data: { message: "Upstream error", statusCode } } }) });
const result = await testEnvironment({ companyId: "company-1", adapterType: "paperclip_runner",
config: { model: "openrouter/deepseek/deepseek-v4-flash-0731", env: { XDG_CONFIG_HOME: configHome } } });
expect(result.checks).toContainEqual(expect.objectContaining({ code: "opencode_hello_probe_failed", level: "error" }));
});
it("gives each concurrent native probe its own OpenCode data directory", async () => {
vi.stubEnv("HOME", "/host/user");
vi.stubEnv("XDG_DATA_HOME", "/host/user/.local/share");
@@ -27,7 +27,7 @@ import {
overrideAdapterExecutionTargetRemoteCwd,
} from "@paperclipai/adapter-utils/execution-target";
import { discoverOpenCodeModels, ensureOpenCodeModelConfiguredAndAvailable, requireOpenCodeModelId } from "./models.js";
import { parseOpenCodeJsonl } from "./parse.js";
import { openCodeErrorStatusCodes, parseOpenCodeJsonl } from "./parse.js";
import { SANDBOX_INSTALL_COMMAND } from "../index.js";
import { prepareOpenCodeRuntimeConfig, prepareManagedOpenCodeRemoteHomes } from "./runtime-config.js";
@@ -418,6 +418,9 @@ export async function testEnvironment(
const parsed = parseOpenCodeJsonl(probe.stdout);
const detail = summarizeProbeDetail(probe.stdout, probe.stderr, parsed.errorMessage);
const authEvidence = `${parsed.errorMessage ?? ""}\n${probe.stdout}\n${probe.stderr}`.trim();
// A provider 401 is an authentication rejection whatever its wording
// (OpenRouter says "Missing Authentication header" or "User not found.").
const providerRejectedAuth = openCodeErrorStatusCodes(probe.stdout).includes(401);
if (probe.timedOut) {
checks.push({
@@ -450,7 +453,7 @@ export async function testEnvironment(
...(detail ? { detail } : {}),
hint: "Run `opencode models` and choose an available provider/model ID.",
});
} else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) {
} else if (providerRejectedAuth || OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) {
checks.push({
code: "opencode_hello_probe_auth_required",
level: "warn",