Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- **Silent permission reply failures** ([#35](https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk/pull/35)) - `replyToPendingApprovals` never inspected the resolved value of `permission.reply`. Managed clients use `responseStyle: "fields"` without `throwOnError`, so API-level failures resolve as `{ error }` instead of throwing — a failed reply was silently recorded as replied and never retried, leaving OpenCode waiting on the permission request. The result is now checked via `extractSdkResult` (matching the question-reply handling): on error, a warning is logged and surfaced in the response `warnings`, and the approval id is not recorded as replied so the next turn retries it.

## [3.0.6] - 2026-06-11

### Fixed
Expand Down
49 changes: 49 additions & 0 deletions src/opencode-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,55 @@ describe("opencode-language-model", () => {
expect(mockClient.permission.reply).toHaveBeenCalledTimes(1);
});

it("should warn and not record the approval as replied when permission.reply returns an API error", async () => {
mockClient.permission.reply.mockResolvedValueOnce({
error: { message: "unknown permission request" },
});
const logger = { warn: vi.fn(), error: vi.fn() };
model = new OpencodeLanguageModel({
modelId: "anthropic/claude-3-5-sonnet-20241022",
settings: { logger },
clientManager: mockClientManager as unknown as OpencodeClientManager,
});
const promptWithApproval: LanguageModelV3Prompt = [
{
role: "user",
content: [{ type: "text", text: "Continue after approval." }],
},
{
role: "tool",
content: [
{
type: "tool-approval-response",
approvalId: "approval-1",
approved: true,
},
],
},
];

const result = await model.doGenerate({
prompt: promptWithApproval,
});

const expectedWarning = expect.stringContaining(
"Failed to apply tool approval response for approval-1",
);
expect(logger.warn).toHaveBeenCalledWith(expectedWarning);
expect(result.warnings).toContainEqual({
type: "other",
message: expectedWarning,
});

// The failed reply must not be recorded as replied, so the next turn
// retries it.
await model.doGenerate({
prompt: promptWithApproval,
});

expect(mockClient.permission.reply).toHaveBeenCalledTimes(2);
});

it("should not emit duplicate document sources for local files with source metadata", async () => {
mockClient.session.prompt.mockResolvedValueOnce({
data: {
Expand Down
16 changes: 15 additions & 1 deletion src/opencode-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,11 @@
};

this.logger.debug?.(
`[doGenerate] raw parts from OpenCode: ${JSON.stringify(responseData.parts?.map((p) => ({ type: p.type, ...(p.type === "text" ? { text: (p as any).text?.slice(0, 200) } : {}), ...(p.type === "tool" ? { tool: (p as any).tool, callID: (p as any).callID, status: (p as any).state?.status, input: (p as any).state?.input } : {}) })))}`,

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 343 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type
);
const content = this.extractContentFromParts(responseData.parts ?? []);
this.logger.debug?.(
`[doGenerate] extracted content: ${JSON.stringify(content.map((c) => ({ type: c.type, ...(c.type === "text" ? { text: (c as any).text?.slice(0, 200) } : {}), ...(c.type === "tool-call" ? { toolName: (c as any).toolName } : {}) })))}`,

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 20

Unexpected any. Specify a different type

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 22

Unexpected any. Specify a different type

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type

Check warning on line 347 in src/opencode-language-model.ts

View workflow job for this annotation

GitHub Actions / Node 18

Unexpected any. Specify a different type
);
const usage = this.extractUsageFromParts(responseData.parts ?? []);
const finishReason = resolveStructuredOutputFinishReason(
Expand Down Expand Up @@ -750,12 +750,26 @@

for (const response of pendingResponses) {
try {
await permissionApi.reply({
const result = await permissionApi.reply({
requestID: response.approvalId,
reply: response.approved ? "once" : "reject",
...(response.reason ? { message: response.reason } : {}),
...(directory ? { directory } : {}),
});

// Fields-style clients report API failures via the result rather
// than throwing, so a missing check would record the approval as
// replied while OpenCode keeps waiting on the permission request.
const { error: resultError } = extractSdkResult(result);
if (resultError) {
const warning =
`Failed to apply tool approval response for ${response.approvalId}: ` +
`${extractErrorMessage(resultError)}`;
this.logger.warn(warning);
warnings.push(warning);
continue;
}

repliedApprovalIds.add(response.approvalId);
} catch (error) {
const warning =
Expand Down
Loading