From a7ac3debebb0ce7eb80862b9f70b798c7251fd27 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:47:22 +1100 Subject: [PATCH 1/2] fix(opencode): failed OpenCode text generation no longer shows Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 28 +++++++++++++++++++ .../src/server/textGeneration.ts | 16 ++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/provider-opencode/src/server/textGeneration.test.ts b/packages/provider-opencode/src/server/textGeneration.test.ts index 10694ef45ef55..89717c1e6e8c3 100644 --- a/packages/provider-opencode/src/server/textGeneration.test.ts +++ b/packages/provider-opencode/src/server/textGeneration.test.ts @@ -456,6 +456,34 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { ), ); + it.effect("shows a readable message when OpenCode stores the provider error as ", () => + withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => + Effect.gen(function* () { + runtimeMock.state.promptResult = { + data: { + info: { + error: { + name: "APIError", + data: { + message: "", + statusCode: 503, + responseBody: "Service unavailable", + }, + }, + }, + parts: [], + }, + }; + + const error = yield* textGeneration + .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) + .pipe(Effect.flip); + + expect(error.detail).toBe("Service unavailable (HTTP 503)."); + }), + ), + ); + it.effect("deletes the session when the prompt request rejects", () => withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => Effect.gen(function* () { diff --git a/packages/provider-opencode/src/server/textGeneration.ts b/packages/provider-opencode/src/server/textGeneration.ts index ab8d41f1568b2..7fe29a2157cf3 100644 --- a/packages/provider-opencode/src/server/textGeneration.ts +++ b/packages/provider-opencode/src/server/textGeneration.ts @@ -9,6 +9,7 @@ import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations"; import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts"; +import { readableProviderErrorMessage } from "./providerErrorMessage.ts"; const GENERATION_TIMEOUT = "3 minutes"; @@ -111,14 +112,15 @@ function getOpenCodePromptFailure(error: unknown): OpenCodePromptFailure | null "name" in error && typeof error.name === "string" && error.name.trim().length > 0 ? error.name.trim() : undefined; + const data = "data" in error && error.data && typeof error.data === "object" ? error.data : {}; const message = - "data" in error && - error.data && - typeof error.data === "object" && - "message" in error.data && - typeof error.data.message === "string" - ? error.data.message.trim() - : ""; + readableProviderErrorMessage( + "message" in data && typeof data.message === "string" ? data.message : undefined, + "statusCode" in data && typeof data.statusCode === "number" ? data.statusCode : undefined, + "responseBody" in data && typeof data.responseBody === "string" + ? data.responseBody + : undefined, + ) ?? ""; if (message.length > 0) { return { ...(name ? { name } : {}), From 2fe5615338bc37c78efeb9a707008fa02087c8e7 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:05:25 +1100 Subject: [PATCH 2/2] fix(opencode): text generation errors use fixed details for known provider failures Map OpenCode's typed prompt error (name and HTTP status) to fixed detail text and keep the raw provider error only in the cause, following effect-services.md "Errors". Co-Authored-By: Claude Opus 5.5 --- .../src/server/textGeneration.test.ts | 79 +++++++++++++------ .../src/server/textGeneration.ts | 72 +++++++++++------ 2 files changed, 103 insertions(+), 48 deletions(-) diff --git a/packages/provider-opencode/src/server/textGeneration.test.ts b/packages/provider-opencode/src/server/textGeneration.test.ts index 89717c1e6e8c3..a5a7440257875 100644 --- a/packages/provider-opencode/src/server/textGeneration.test.ts +++ b/packages/provider-opencode/src/server/textGeneration.test.ts @@ -456,32 +456,65 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => { ), ); - it.effect("shows a readable message when OpenCode stores the provider error as ", () => - withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => - Effect.gen(function* () { - runtimeMock.state.promptResult = { - data: { - info: { - error: { - name: "APIError", - data: { - message: "", - statusCode: 503, - responseBody: "Service unavailable", - }, + it.effect( + "maps OpenCode prompt errors to fixed details and keeps provider text in the cause", + () => + Effect.forEach( + [ + { + error: { + name: "APIError", + data: { + message: "Org quota secret-org exceeded", + statusCode: 429, + isRetryable: true, }, }, - parts: [], + rawText: "secret-org", + detail: "The model provider's usage or rate limit was reached. Try again later.", }, - }; - - const error = yield* textGeneration - .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) - .pipe(Effect.flip); - - expect(error.detail).toBe("Service unavailable (HTTP 503)."); - }), - ), + { + error: { + name: "APIError", + data: { + message: "", + statusCode: 503, + isRetryable: true, + responseBody: "Upstream gateway secret-host", + }, + }, + rawText: "secret-host", + detail: "The model provider failed the request (HTTP 503).", + }, + { + error: { name: "UnknownError", data: { message: "Provider stack trace secret-token" } }, + rawText: "secret-token", + detail: "OpenCode could not generate the text.", + }, + ], + (testCase) => + withOpenCodeTextGeneration(DEFAULT_OPENCODE_SETTINGS, (textGeneration) => + Effect.gen(function* () { + runtimeMock.state.promptResult = { + data: { info: { error: testCase.error }, parts: [] }, + }; + + const error = yield* textGeneration + .generateCommitMessage(DEFAULT_COMMIT_MESSAGE_INPUT) + .pipe(Effect.flip); + + expect(error.detail).toBe(testCase.detail); + expect(error.message).not.toContain(testCase.rawText); + expect(error.cause).toMatchObject({ + _tag: "OpenCodeTextGenerationPromptResponseError", + providerErrorName: testCase.error.name, + cause: testCase.error, + }); + expect((error.cause as Error).message).not.toContain(testCase.rawText); + }), + ), + { discard: true }, + ), ); it.effect("deletes the session when the prompt request rejects", () => diff --git a/packages/provider-opencode/src/server/textGeneration.ts b/packages/provider-opencode/src/server/textGeneration.ts index 7fe29a2157cf3..570d2ebf6e2f1 100644 --- a/packages/provider-opencode/src/server/textGeneration.ts +++ b/packages/provider-opencode/src/server/textGeneration.ts @@ -9,7 +9,6 @@ import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import * as TextGenerationOperations from "@t3tools/provider-core/server/textGenerationOperations"; import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts"; -import { readableProviderErrorMessage } from "./providerErrorMessage.ts"; const GENERATION_TIMEOUT = "3 minutes"; @@ -71,15 +70,40 @@ export class OpenCodeTextGenerationPromptResponseError extends Schema.TaggedErro { ...openCodePromptErrorContext, providerErrorName: Schema.optional(Schema.String), - providerMessage: Schema.String, + statusCode: Schema.optional(Schema.Int), + cause: Schema.Defect(), }, ) { override get message(): string { const providerError = this.providerErrorName ? ` ${this.providerErrorName}` : ""; - return `OpenCode prompt${providerError} failed for ${this.operation} in ${this.cwd} using ${this.providerId}/${this.modelId} (session ${this.sessionId}): ${this.providerMessage}`; + const status = this.statusCode === undefined ? "" : `, HTTP ${this.statusCode}`; + return `OpenCode prompt${providerError} failed for ${this.operation} in ${this.cwd} using ${this.providerId}/${this.modelId} (session ${this.sessionId}${status}).`; } } +function openCodePromptResponseErrorDetail(error: OpenCodeTextGenerationPromptResponseError) { + if ( + error.providerErrorName === "ProviderAuthError" || + error.statusCode === 401 || + error.statusCode === 403 + ) { + return "The model provider rejected the credentials. Check the provider's sign-in or API key in OpenCode."; + } + if (error.statusCode === 429) { + return "The model provider's usage or rate limit was reached. Try again later."; + } + if (error.providerErrorName === "ContextOverflowError") { + return "The prompt is too long for the selected model's context window."; + } + if (error.providerErrorName === "MessageAbortedError") { + return "OpenCode stopped the generation."; + } + if (error.statusCode !== undefined) { + return `The model provider failed the request (HTTP ${error.statusCode}).`; + } + return "OpenCode could not generate the text."; +} + export class OpenCodeTextGenerationEmptyOutputError extends Schema.TaggedError()( "OpenCodeTextGenerationEmptyOutputError", { @@ -95,7 +119,7 @@ export class OpenCodeTextGenerationEmptyOutputError extends Schema.TaggedError 0) { - return { - ...(name ? { name } : {}), - message, - }; - } - - if (name) { - return { name, message: name }; + const statusCode = + "statusCode" in data && typeof data.statusCode === "number" && Number.isInteger(data.statusCode) + ? data.statusCode + : undefined; + const hasMessage = + "message" in data && typeof data.message === "string" && data.message.trim().length > 0; + if (!name && statusCode === undefined && !hasMessage) { + return null; } - return null; + return { + ...(name ? { name } : {}), + ...(statusCode !== undefined ? { statusCode } : {}), + }; } function isOpenCodeTextPart(part: unknown): part is OpenCodeTextPart { @@ -265,12 +283,16 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" ), ), ); - const promptFailure = getOpenCodePromptFailure(result.data?.info?.error); + const promptError = result.data?.info?.error; + const promptFailure = getOpenCodePromptFailure(promptError); if (promptFailure) { return yield* new OpenCodeTextGenerationPromptResponseError({ ...promptContext, ...(promptFailure.name ? { providerErrorName: promptFailure.name } : {}), - providerMessage: promptFailure.message, + ...(promptFailure.statusCode !== undefined + ? { statusCode: promptFailure.statusCode } + : {}), + cause: promptError, }); } const responseParts = result.data?.parts ?? []; @@ -314,7 +336,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" Effect.fail( new TextGenerationError({ operation: cause.operation, - detail: cause.providerMessage, + detail: openCodePromptResponseErrorDetail(cause), cause, }), ),