From 61a33fd688d572c4fef21a2b01ecc042b33a9d91 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 06:45:41 -0600 Subject: [PATCH 1/3] fix: surface actionable error when OpenCode prompt returns no response data On opencode CLI 1.17.x an invalid or unavailable provider/model ID (e.g. github-copilot/gpt-5 from #21) no longer produces an empty HTTP body; the prompt call succeeds but result.data is falsy, bypassing the actionable empty-JSON error from #24 and hitting the generic "No response data from OpenCode" error with no guidance. - doGenerate now throws an APICallError naming the model ID, suggesting `opencode models`, including the server error payload when available, and carrying errorType "EmptyResponseData" - doStream surfaces the same error as an error stream part and terminates the stream instead of hanging on the event subscription - wrapError returns already-wrapped AI SDK errors unchanged instead of re-wrapping them and losing their metadata - export createEmptyResponseDataError, with tests mirroring the #24 coverage in errors.test.ts and opencode-language-model.test.ts Verified against dist/ with opencode CLI 1.17.3: both generateText and streamText now produce the actionable message for github-copilot/gpt-5. --- CHANGELOG.md | 6 ++ src/errors.test.ts | 54 +++++++++++++++++ src/errors.ts | 35 +++++++++++ src/index.ts | 1 + src/opencode-language-model.test.ts | 94 +++++++++++++++++++++++++++++ src/opencode-language-model.ts | 35 +++++++++-- 6 files changed, 220 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24aaf34..d505b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.0.5] - 2026-06-11 +### Added + +- **Exported `createEmptyResponseDataError`** - New error factory alongside the existing `createAPICallError` / `createTimeoutError` exports. + ### Fixed +- **Empty response data errors** - Prompt calls that succeed but return no response data (the behavior of opencode CLI 1.17.x for invalid or unavailable `provider/model` IDs, e.g. the `github-copilot/gpt-5` repro from [#21](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk/issues/21)) now throw an actionable `APICallError` that names the requested model ID, suggests checking `opencode models`, includes the server error payload when available, and carries `errorType: "EmptyResponseData"` — replacing the generic `No response data from OpenCode`. The streaming path surfaces the same error as an `error` stream part and terminates the stream instead of hanging on the event subscription. +- **`wrapError` double-wrapping** - `wrapError` now returns already-wrapped AI SDK errors (`APICallError`, `LoadAPIKeyError`) unchanged instead of re-wrapping them and losing their metadata. - **Singleton client manager recovery after dispose** - `OpencodeClientManager.dispose()` now releases the singleton slot when the disposed manager is the singleton, so a later `createOpencode()` builds a fresh client manager. Previously the singleton kept pointing at the disposed instance, and every subsequent provider in the same process failed with `Client manager has been disposed`. - **client-options example** - Step 2 of `examples/client-options.ts` now genuinely demonstrates the preconfigured-client pattern by passing an isolated manager via `clientManager: OpencodeClientManager.createInstance({ client })`. Previously the preconfigured client was handed to the singleton that step 1 had already initialized, so it was ignored and step 2's requests flowed through step 1's client while the demo reported success. The example now also echoes each outgoing request's `x-demo-source` header so the output proves which client served it, drops a spurious `await` on the synchronous v2 `createOpencodeClient()`, and allows overriding the example model via `OPENCODE_MODEL`. diff --git a/src/errors.test.ts b/src/errors.test.ts index 295ef14..f26da83 100644 --- a/src/errors.test.ts +++ b/src/errors.test.ts @@ -6,6 +6,7 @@ import { isOutputLengthError, createAuthenticationError, createAPICallError, + createEmptyResponseDataError, createTimeoutError, extractErrorMessage, wrapError, @@ -240,6 +241,45 @@ describe("errors", () => { }); }); + describe("createEmptyResponseDataError", () => { + it("should create actionable error with model guidance", () => { + const result = createEmptyResponseDataError(undefined, { + sessionId: "session-123", + modelId: "github-copilot/gpt-5", + }); + + expect(result).toBeInstanceOf(APICallError); + expect(result.message).toContain("OpenCode returned no response data"); + expect(result.message).toContain('for model "github-copilot/gpt-5"'); + expect(result.message).toContain("provider/model"); + expect(result.message).toContain("opencode models"); + expect(result.isRetryable).toBe(false); + expect(result.data).toMatchObject({ + errorType: "EmptyResponseData", + sessionId: "session-123", + modelId: "github-copilot/gpt-5", + }); + }); + + it("should omit model hint when modelId is missing", () => { + const result = createEmptyResponseDataError(undefined); + + expect(result.message).toContain("OpenCode returned no response data"); + expect(result.message).not.toContain("for model"); + expect(result.message).not.toContain("Original error"); + }); + + it("should include original error details when available", () => { + const result = createEmptyResponseDataError( + { message: "model not found", statusCode: 400 }, + { modelId: "github-copilot/gpt-5" }, + ); + + expect(result.message).toContain("Original error: model not found"); + expect(result.statusCode).toBe(400); + }); + }); + describe("createTimeoutError", () => { it("should create timeout error with duration", () => { const result = createTimeoutError(5000); @@ -370,5 +410,19 @@ describe("errors", () => { sessionId: "session-123", }); }); + + it("should return already-wrapped AI SDK errors unchanged", () => { + const original = createEmptyResponseDataError(undefined, { + sessionId: "session-123", + modelId: "github-copilot/gpt-5", + }); + const result = wrapError(original, { sessionId: "other-session" }); + + expect(result).toBe(original); + expect((result as APICallError).data).toMatchObject({ + errorType: "EmptyResponseData", + sessionId: "session-123", + }); + }); }); }); diff --git a/src/errors.ts b/src/errors.ts index 13ebfb7..3c98bfd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -209,6 +209,36 @@ function createEmptyResponseError( }); } +/** + * Create an actionable error for OpenCode responses that complete without + * any response data. + */ +export function createEmptyResponseDataError( + error: unknown, + metadata?: Partial, +): APICallError { + const modelHint = metadata?.modelId ? ` for model "${metadata.modelId}"` : ""; + const detail = error ? ` Original error: ${extractErrorMessage(error)}` : ""; + + return new APICallError({ + message: + `OpenCode returned no response data${modelHint}. ` + + "This usually means the configured provider/model ID is invalid or unavailable. " + + "Run `opencode models` to list available model IDs and check the OpenCode provider configuration." + + detail, + url: "opencode://session", + requestBodyValues: {}, + statusCode: metadata?.statusCode ?? extractStatusCode(error), + isRetryable: false, + data: { + errorType: "EmptyResponseData", + sessionId: metadata?.sessionId, + messageId: metadata?.messageId, + modelId: metadata?.modelId, + }, + }); +} + /** * Create a timeout error. */ @@ -375,6 +405,11 @@ export function wrapError( error: unknown, metadata?: Partial, ): Error { + // Errors created by this module are already actionable AI SDK errors. + if (APICallError.isInstance(error) || LoadAPIKeyError.isInstance(error)) { + return error; + } + if (isAuthenticationError(error)) { return createAuthenticationError(error); } diff --git a/src/index.ts b/src/index.ts index f741b70..6d2e6af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,6 +54,7 @@ export { isOutputLengthError, createAuthenticationError, createAPICallError, + createEmptyResponseDataError, createTimeoutError, extractErrorMessage, wrapError, diff --git a/src/opencode-language-model.test.ts b/src/opencode-language-model.test.ts index 9d5e5da..2bec82a 100644 --- a/src/opencode-language-model.test.ts +++ b/src/opencode-language-model.test.ts @@ -351,6 +351,39 @@ describe("opencode-language-model", () => { ); }); + it("should wrap missing OpenCode response data with model context", async () => { + mockClient.session.prompt.mockResolvedValueOnce({ data: undefined }); + + await expect( + model.doGenerate({ + prompt: basicPrompt, + }), + ).rejects.toMatchObject({ + message: expect.stringContaining( + 'OpenCode returned no response data for model "anthropic/claude-3-5-sonnet-20241022"', + ), + isRetryable: false, + data: expect.objectContaining({ + errorType: "EmptyResponseData", + sessionId: "session-123", + modelId: "anthropic/claude-3-5-sonnet-20241022", + }), + }); + }); + + it("should include response error details when response data is missing", async () => { + mockClient.session.prompt.mockResolvedValueOnce({ + data: undefined, + error: { message: "model not found" }, + }); + + await expect( + model.doGenerate({ + prompt: basicPrompt, + }), + ).rejects.toThrow("Original error: model not found"); + }); + it("should handle JSON mode", async () => { await model.doGenerate({ prompt: basicPrompt, @@ -876,6 +909,67 @@ describe("opencode-language-model", () => { expect(iteratorReturn).toHaveBeenCalled(); }); + it("should emit actionable error when prompt resolves without data", async () => { + const iteratorReturn = vi + .fn<() => Promise>, []>() + .mockResolvedValue({ done: true, value: undefined }); + + const hangingStream: AsyncIterable = { + [Symbol.asyncIterator]() { + return { + next() { + return new Promise>(() => { + // Intentionally never resolves to emulate server silence. + }); + }, + return: iteratorReturn, + }; + }, + }; + + mockClient.event.subscribe.mockResolvedValueOnce({ + stream: hangingStream, + }); + mockClient.session.prompt.mockResolvedValueOnce({ data: undefined }); + + const result = await model.doStream({ + prompt: basicPrompt, + }); + + const reader = result.stream.getReader(); + const parts = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for stream to close")); + }, 500); + + (async () => { + const streamParts: unknown[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + streamParts.push(value); + } + clearTimeout(timeout); + resolve(streamParts); + })().catch((error: unknown) => { + clearTimeout(timeout); + reject(error); + }); + }); + + const errorPart = parts.find((part: any) => part.type === "error") as + | { error?: { message?: string; data?: { errorType?: string } } } + | undefined; + expect(errorPart).toBeDefined(); + expect(errorPart?.error?.message).toContain( + 'OpenCode returned no response data for model "anthropic/claude-3-5-sonnet-20241022"', + ); + expect(errorPart?.error?.data).toMatchObject({ + errorType: "EmptyResponseData", + }); + expect(iteratorReturn).toHaveBeenCalled(); + }); + it("should terminate stream when abort signal fires without incoming events", async () => { const iteratorReturn = vi .fn<() => Promise>, []>() diff --git a/src/opencode-language-model.ts b/src/opencode-language-model.ts index 733c5cd..6d32d74 100644 --- a/src/opencode-language-model.ts +++ b/src/opencode-language-model.ts @@ -32,7 +32,12 @@ import { import { mapOpencodeFinishReason } from "./map-opencode-finish-reason.js"; import { getLogger, logUnsupportedCallOptions } from "./logger.js"; import { validateModelId, validateSettings } from "./validation.js"; -import { wrapError, extractErrorMessage, isAbortError } from "./errors.js"; +import { + wrapError, + extractErrorMessage, + isAbortError, + createEmptyResponseDataError, +} from "./errors.js"; import { safeStringifyToolInput, planFilePartConversion, @@ -265,7 +270,10 @@ export class OpencodeLanguageModel implements LanguageModelV3 { const data = result.data; if (!data) { - throw new Error("No response data from OpenCode"); + throw createEmptyResponseDataError(result.error, { + sessionId, + modelId: this.modelId, + }); } const responseData = data as { @@ -418,15 +426,32 @@ export class OpencodeLanguageModel implements LanguageModelV3 { } } - client.session.prompt(requestBody).catch((error: unknown) => { + const handlePromptFailure = (error: unknown) => { const wrappedError = wrapError(error, { sessionId, modelId: this.modelId, }); logger.error(`Prompt error: ${extractErrorMessage(wrappedError)}`); - controller.enqueue({ type: "error", error: wrappedError }); + try { + controller.enqueue({ type: "error", error: wrappedError }); + } catch (enqueueError) { + logger.debug?.( + `Failed to enqueue prompt error after stream closed: ${extractErrorMessage(enqueueError)}`, + ); + } resolvePromptFailed?.(); - }); + }; + + client.session.prompt(requestBody).then((result) => { + if (!result.data) { + handlePromptFailure( + createEmptyResponseDataError(result.error, { + sessionId, + modelId: this.modelId, + }), + ); + } + }, handlePromptFailure); const state = createStreamState(); let lastMessageInfo: Message | undefined; From 9442c9e0913c0608e6be0b808a5c41aa4b13d703 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 06:58:11 -0600 Subject: [PATCH 2/3] fix: tolerate responseStyle "data" clients in prompt result handling Codex review flagged that the new no-data check in doStream assumed fields-style SDK results. A client configured with responseStyle: "data" (allowed by OpencodeClientOptions) resolves session.prompt to the payload itself, or undefined on failure, so the check would flag every successful prompt as missing data (terminating a healthy stream) or throw a TypeError on undefined. - Managed clients are now always created with responseStyle: "fields"; clientOptions.responseStyle "data" is ignored with a warning, mirroring the existing baseUrl/directory normalization - doGenerate/doStream normalize the prompt result shape, treating fields-style results ({ data, error }) and data-style payloads uniformly, so caller-supplied data-style clients get correct content on success and the actionable EmptyResponseData error on failure --- CHANGELOG.md | 1 + src/opencode-client-manager.test.ts | 27 +++++++++ src/opencode-client-manager.ts | 6 ++ src/opencode-language-model.test.ts | 93 +++++++++++++++++++++++++++++ src/opencode-language-model.ts | 30 ++++++++-- 5 files changed, 153 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d505b9e..ccda2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Clearer stale-options warnings** - The client manager's "already initialized" warnings now point at the escape hatches that actually work (`OpencodeClientManager.createInstance()` with the `clientManager` provider setting, or `OpencodeClientManager.resetInstance()`) instead of the previous generic advice. +- **`clientOptions.responseStyle` normalization** - Managed clients are now always created with fields-style SDK results; `clientOptions.responseStyle: "data"` is ignored with a warning since the provider's response handling requires `{ data, error }` results. Prompt-result handling also tolerates data-style results from caller-supplied (preconfigured) clients. ## [3.0.4] - 2026-06-11 diff --git a/src/opencode-client-manager.test.ts b/src/opencode-client-manager.test.ts index 7bfdcd7..4ec2f11 100644 --- a/src/opencode-client-manager.test.ts +++ b/src/opencode-client-manager.test.ts @@ -341,6 +341,33 @@ describe("opencode-client-manager", () => { ); }); + it('should force fields responseStyle and warn when clientOptions requests "data"', async () => { + const warn = vi.fn(); + + const instance = OpencodeClientManager.getInstance({ + baseUrl: "http://custom-server:8080", + clientOptions: { + responseStyle: "data", + }, + logger: { + warn, + error: vi.fn(), + }, + }); + + await instance.getClient(); + + const { createOpencodeClient } = await import("@opencode-ai/sdk/v2"); + expect(createOpencodeClient).toHaveBeenCalledWith( + expect.objectContaining({ + responseStyle: "fields", + }), + ); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("Ignoring clientOptions.responseStyle"), + ); + }); + it("should not warn for undefined reserved keys in clientOptions", async () => { const warn = vi.fn(); diff --git a/src/opencode-client-manager.ts b/src/opencode-client-manager.ts index 7c05455..140fa67 100644 --- a/src/opencode-client-manager.ts +++ b/src/opencode-client-manager.ts @@ -287,11 +287,17 @@ export class OpencodeClientManager { "Ignoring clientOptions.directory because directory is managed by model settings/defaultSettings.", ); } + if (optionsRecord.responseStyle === "data") { + this.logger.warn( + 'Ignoring clientOptions.responseStyle "data" because the provider requires fields-style SDK results.', + ); + } return { ...options, baseUrl, directory: this.options.cwd, + responseStyle: "fields", }; } diff --git a/src/opencode-language-model.test.ts b/src/opencode-language-model.test.ts index 2bec82a..df4fee3 100644 --- a/src/opencode-language-model.test.ts +++ b/src/opencode-language-model.test.ts @@ -384,6 +384,47 @@ describe("opencode-language-model", () => { ).rejects.toThrow("Original error: model not found"); }); + it("should handle data-style prompt results from custom clients", async () => { + // A caller-supplied client configured with responseStyle: "data" + // resolves session.prompt to the payload itself. + mockClient.session.prompt.mockResolvedValueOnce({ + info: { + id: "msg-1", + sessionID: "session-123", + role: "assistant", + finish: "end_turn", + }, + parts: [{ id: "part-1", type: "text", text: "Hello, world!" }], + }); + + const result = await model.doGenerate({ + prompt: basicPrompt, + }); + + expect(result.content[0]).toMatchObject({ + type: "text", + text: "Hello, world!", + }); + }); + + it("should wrap undefined data-style prompt results with model context", async () => { + // A responseStyle: "data" client resolves to undefined on failure. + mockClient.session.prompt.mockResolvedValueOnce(undefined); + + await expect( + model.doGenerate({ + prompt: basicPrompt, + }), + ).rejects.toMatchObject({ + message: expect.stringContaining( + 'OpenCode returned no response data for model "anthropic/claude-3-5-sonnet-20241022"', + ), + data: expect.objectContaining({ + errorType: "EmptyResponseData", + }), + }); + }); + it("should handle JSON mode", async () => { await model.doGenerate({ prompt: basicPrompt, @@ -970,6 +1011,58 @@ describe("opencode-language-model", () => { expect(iteratorReturn).toHaveBeenCalled(); }); + it("should not flag data-style prompt results as missing data", async () => { + mockClient.event.subscribe.mockResolvedValueOnce({ + stream: (async function* () { + yield { + type: "message.part.updated", + properties: { + part: { + id: "part-1", + sessionID: "session-123", + messageID: "msg-1", + type: "text", + text: "Hello", + }, + delta: "Hello", + }, + }; + yield { + type: "session.idle", + properties: { + sessionID: "session-123", + }, + }; + })(), + }); + // A caller-supplied client configured with responseStyle: "data" + // resolves session.prompt to the payload itself. + mockClient.session.prompt.mockResolvedValueOnce({ + info: { + id: "msg-1", + sessionID: "session-123", + role: "assistant", + finish: "end_turn", + }, + parts: [{ id: "part-1", type: "text", text: "Hello" }], + }); + + const result = await model.doStream({ + prompt: basicPrompt, + }); + + const parts: unknown[] = []; + const reader = result.stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + } + + expect(parts.some((part: any) => part.type === "error")).toBe(false); + expect(parts.some((part: any) => part.type === "finish")).toBe(true); + }); + it("should terminate stream when abort signal fires without incoming events", async () => { const iteratorReturn = vi .fn<() => Promise>, []>() diff --git a/src/opencode-language-model.ts b/src/opencode-language-model.ts index 6d32d74..34d43b7 100644 --- a/src/opencode-language-model.ts +++ b/src/opencode-language-model.ts @@ -90,6 +90,27 @@ function convertUsage(usage: StreamingUsage): LanguageModelV3Usage { }; } +/** + * Normalize a session.prompt result. Managed clients resolve to a + * fields-style result ({ data, error }), but a caller-supplied client may be + * configured with responseStyle: "data", which resolves to the payload + * itself (or undefined on failure). + */ +function extractPromptResult(result: unknown): { + data?: unknown; + error?: unknown; +} { + if ( + result && + typeof result === "object" && + ("data" in result || "error" in result) + ) { + return result as { data?: unknown; error?: unknown }; + } + + return { data: result }; +} + function getMessageIDFromProviderOptions( options: LanguageModelV3CallOptions, ): string | undefined { @@ -268,9 +289,9 @@ export class OpencodeLanguageModel implements LanguageModelV3 { const result = await client.session.prompt(requestBody); - const data = result.data; + const { data, error: responseError } = extractPromptResult(result); if (!data) { - throw createEmptyResponseDataError(result.error, { + throw createEmptyResponseDataError(responseError, { sessionId, modelId: this.modelId, }); @@ -443,9 +464,10 @@ export class OpencodeLanguageModel implements LanguageModelV3 { }; client.session.prompt(requestBody).then((result) => { - if (!result.data) { + const { data, error: responseError } = extractPromptResult(result); + if (!data) { handlePromptFailure( - createEmptyResponseDataError(result.error, { + createEmptyResponseDataError(responseError, { sessionId, modelId: this.modelId, }), From 66ad8d0a6947dd6d5316cd235ef543a5eca1afd8 Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Thu, 11 Jun 2026 07:12:01 -0600 Subject: [PATCH 3/3] fix: normalize data-style session.create results too Codex follow-up: a preconfigured responseStyle "data" client without settings.sessionId never reached the prompt-result normalization because getOrCreateSession() still read result.data from session.create, treating a successful data-style create payload as a failure. Generalize extractPromptResult to extractSdkResult and apply it to session.create as well, so data-style clients work end-to-end. --- CHANGELOG.md | 2 +- src/opencode-language-model.test.ts | 21 +++++++++++++++++++++ src/opencode-language-model.ts | 13 +++++++------ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccda2da..13e5004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Clearer stale-options warnings** - The client manager's "already initialized" warnings now point at the escape hatches that actually work (`OpencodeClientManager.createInstance()` with the `clientManager` provider setting, or `OpencodeClientManager.resetInstance()`) instead of the previous generic advice. -- **`clientOptions.responseStyle` normalization** - Managed clients are now always created with fields-style SDK results; `clientOptions.responseStyle: "data"` is ignored with a warning since the provider's response handling requires `{ data, error }` results. Prompt-result handling also tolerates data-style results from caller-supplied (preconfigured) clients. +- **`clientOptions.responseStyle` normalization** - Managed clients are now always created with fields-style SDK results; `clientOptions.responseStyle: "data"` is ignored with a warning since the provider's response handling requires `{ data, error }` results. Session-creation and prompt result handling also tolerate data-style results from caller-supplied (preconfigured) clients. ## [3.0.4] - 2026-06-11 diff --git a/src/opencode-language-model.test.ts b/src/opencode-language-model.test.ts index df4fee3..dca954e 100644 --- a/src/opencode-language-model.test.ts +++ b/src/opencode-language-model.test.ts @@ -384,6 +384,27 @@ describe("opencode-language-model", () => { ).rejects.toThrow("Original error: model not found"); }); + it("should handle data-style session creation results from custom clients", async () => { + // A caller-supplied client configured with responseStyle: "data" + // resolves session.create to the session payload itself. + mockClient.session.create.mockResolvedValueOnce({ + id: "session-data-style", + }); + + const result = await model.doGenerate({ + prompt: basicPrompt, + }); + + expect(result.providerMetadata?.opencode?.sessionId).toBe( + "session-data-style", + ); + expect(mockClient.session.prompt).toHaveBeenCalledWith( + expect.objectContaining({ + sessionID: "session-data-style", + }), + ); + }); + it("should handle data-style prompt results from custom clients", async () => { // A caller-supplied client configured with responseStyle: "data" // resolves session.prompt to the payload itself. diff --git a/src/opencode-language-model.ts b/src/opencode-language-model.ts index 34d43b7..53e6bb0 100644 --- a/src/opencode-language-model.ts +++ b/src/opencode-language-model.ts @@ -91,12 +91,12 @@ function convertUsage(usage: StreamingUsage): LanguageModelV3Usage { } /** - * Normalize a session.prompt result. Managed clients resolve to a + * Normalize an SDK request result. Managed clients resolve to a * fields-style result ({ data, error }), but a caller-supplied client may be * configured with responseStyle: "data", which resolves to the payload * itself (or undefined on failure). */ -function extractPromptResult(result: unknown): { +function extractSdkResult(result: unknown): { data?: unknown; error?: unknown; } { @@ -289,7 +289,7 @@ export class OpencodeLanguageModel implements LanguageModelV3 { const result = await client.session.prompt(requestBody); - const { data, error: responseError } = extractPromptResult(result); + const { data, error: responseError } = extractSdkResult(result); if (!data) { throw createEmptyResponseDataError(responseError, { sessionId, @@ -464,7 +464,7 @@ export class OpencodeLanguageModel implements LanguageModelV3 { }; client.session.prompt(requestBody).then((result) => { - const { data, error: responseError } = extractPromptResult(result); + const { data, error: responseError } = extractSdkResult(result); if (!data) { handlePromptFailure( createEmptyResponseDataError(responseError, { @@ -754,10 +754,11 @@ export class OpencodeLanguageModel implements LanguageModelV3 { : {}), }); - const data = result.data as { id: string } | undefined; + const { data: createData, error: createError } = extractSdkResult(result); + const data = createData as { id: string } | undefined; if (!data?.id) { throw new Error( - `Failed to create session: ${JSON.stringify(result.error ?? result.data)}`, + `Failed to create session: ${JSON.stringify(createError ?? createData)}`, ); }