fix(runner): report the upstream cause of OpenCode provider failures (#19166)

OpenCode reports some OpenRouter failures as an UnknownError whose message is the provider's JSON error body. The provider failure now reads its HTTP status, error type and message from that body, so the run shows the upstream message instead of raw JSON.

The OpenCode nativeTurnFailure diagnostic now carries the provider failure's category, retryability and HTTP status (closed values only; no provider text or code), and recognizes the driver's opencode_empty_response code. Run status, error codes and retries do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Devin FoleyandClaude Opus 5.5 authored and GitHub committed 2026-11-04 18:57:26 -08:00
1 parent 737db73275
commit 7f9ef38273
4 files changed
+112 -2

No files matched your search

@@ -197,6 +197,45 @@ describe("native provider failure observation", () => {
expect(readNativeTurnFailureDiagnostic({ ...expected, signal: "SIGTERM" })).toEqual({ ...expected, signal: "SIGTERM" });
});
it("records an OpenCode upstream provider failure's closed fields without its message or code", () => {
const observed = createNativeProviderFailureObservation({ kind: "opencode", model });
const providerFailure = { schema: "paperclip.provider-failure.v1", provider: "openrouter", phase: "turn",
category: "unknown", message: "Private upstream explanation", providerCode: "private_provider_type",
httpStatus: 502, retryability: "unknown" };
observed.observe({ ...event, payload: { status: "failed", error: {
name: "UnknownError", data: { message: "Private upstream body" }, providerFailure,
} } });
expect(observed.forTerminal("turn", terminal)).toBeNull();
expect(observed.providerFailureForTerminal("turn", terminal)).toEqual(providerFailure);
const diagnostic = observed.unclassifiedTurnFailure("turn", terminal);
expect(diagnostic).toEqual({
provider: "opencode", category: "turn_failed",
providerFailureCategory: "unknown", providerFailureRetryability: "unknown", httpStatus: 502,
});
expect(JSON.stringify(diagnostic)).not.toMatch(/private/i);
expect(readNativeTurnFailureDiagnostic(diagnostic)).toEqual(diagnostic);
// A malformed provider failure adds nothing, and a persisted record keeps only known values.
const malformed = createNativeProviderFailureObservation({ kind: "opencode", model });
malformed.observe({ ...event, payload: { status: "failed", error: { providerFailure: { ...providerFailure, category: "private" } } } });
expect(malformed.unclassifiedTurnFailure("turn", terminal)).toEqual({ provider: "opencode", category: "turn_failed" });
expect(readNativeTurnFailureDiagnostic({ provider: "opencode", category: "turn_failed",
providerFailureCategory: "private", providerFailureRetryability: "unknown", httpStatus: 502 }))
.toEqual({ provider: "opencode", category: "turn_failed" });
expect(readNativeTurnFailureDiagnostic({ provider: "opencode", category: "turn_failed",
providerFailureCategory: "overloaded", providerFailureRetryability: "private", httpStatus: 42 }))
.toEqual({ provider: "opencode", category: "turn_failed", providerFailureCategory: "overloaded" });
});
it("names the OpenCode empty-response failure the driver writes", () => {
const observed = createNativeProviderFailureObservation({ kind: "opencode", model });
observed.observe({ ...event, payload: { status: "failed", error: {
code: "opencode_empty_response", message: "OpenCode ended the turn without a final response.", retryable: false,
} } });
const diagnostic = observed.unclassifiedTurnFailure("turn", terminal);
expect(diagnostic).toEqual({ provider: "opencode", category: "turn_failed", runnerFailureCode: "opencode_empty_response" });
expect(readNativeTurnFailureDiagnostic(diagnostic)).toEqual(diagnostic);
});
it.each([{ kind: "claude", model }, { kind: "codex" }, { kind: "codex", model: "other" }])(
"does not apply a rejection to another selected provider/model (%j)", (provider) => {
const observed = createNativeProviderFailureObservation(provider);
@@ -79,6 +79,14 @@ export interface NativeCodexTurnFailureDiagnostic {
*/
const OPENCODE_TURN_FAILURE_RUNNER_CODES: ReadonlySet<string> = new Set([
"opencode_server_exited", "opencode_event_stream_lost", "provider_terminal_error_missing",
"opencode_empty_response",
]);
const PROVIDER_FAILURE_CATEGORIES: ReadonlySet<string> = new Set<ProviderFailure["category"]>([
"rate_limit", "quota_exhausted", "authentication", "permission", "model_unavailable", "invalid_request",
"overloaded", "timeout", "unknown",
]);
const PROVIDER_FAILURE_RETRYABILITY: ReadonlySet<string> = new Set<ProviderFailure["retryability"]>([
"transient", "requires_action", "unknown",
]);
const OPENCODE_STREAM_FAILURE_KINDS: ReadonlySet<string> = new Set([
"server_exited", "connection_failed", "http_error", "stream_closed", "stream_error",
@@ -107,7 +115,11 @@ export interface NativeOpenCodeTurnFailureDiagnostic {
exitCode?: number;
signal?: string;
connectionError?: string;
/** The stream failure's HTTP status, or else the provider error's status. */
httpStatus?: number;
/** The provider error's category and retryability, when the turn carried one. */
providerFailureCategory?: ProviderFailure["category"];
providerFailureRetryability?: ProviderFailure["retryability"];
}
export type NativeTurnFailureDiagnostic = NativeCodexTurnFailureDiagnostic | NativeOpenCodeTurnFailureDiagnostic;
@@ -136,11 +148,27 @@ function openCodeStreamFailureFields(value: unknown): Omit<NativeOpenCodeTurnFai
};
}
/** Only the provider error's closed fields; its message and code stay out of diagnostics. */
function openCodeProviderFailureFields(
category: unknown, retryability: unknown, httpStatus: unknown,
): Pick<NativeOpenCodeTurnFailureDiagnostic, "providerFailureCategory" | "providerFailureRetryability" | "httpStatus"> {
if (typeof category !== "string" || !PROVIDER_FAILURE_CATEGORIES.has(category)) return {};
return {
providerFailureCategory: category as ProviderFailure["category"],
...(typeof retryability === "string" && PROVIDER_FAILURE_RETRYABILITY.has(retryability)
? { providerFailureRetryability: retryability as ProviderFailure["retryability"] } : {}),
...httpStatusField(httpStatus),
};
}
function openCodeTurnFailure(failure: Record<string, unknown>): NativeOpenCodeTurnFailureDiagnostic {
const providerFailure = readProviderFailure(failure.providerFailure);
return {
provider: "opencode",
category: "turn_failed",
...runnerFailureCodeField(failure.code, OPENCODE_TURN_FAILURE_RUNNER_CODES),
...(providerFailure ? openCodeProviderFailureFields(
providerFailure.category, providerFailure.retryability, providerFailure.httpStatus) : {}),
...openCodeStreamFailureFields(failure.opencodeFailure),
};
}
@@ -164,6 +192,7 @@ export function readNativeTurnFailureDiagnostic(value: unknown): NativeTurnFailu
provider: "opencode",
category: "turn_failed",
...runnerFailureCodeField(entry.runnerFailureCode, OPENCODE_TURN_FAILURE_RUNNER_CODES),
...openCodeProviderFailureFields(entry.providerFailureCategory, entry.providerFailureRetryability, entry.httpStatus),
...openCodeStreamFailureFields(entry),
};
}