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
5 changes: 5 additions & 0 deletions .changeset/fix-deepseek-tool-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-bridge-copilot-chat": patch
---

Serialize tool-only Chat Completions messages with provider-compatible empty content instead of `null`.
15 changes: 15 additions & 0 deletions src/provider/content.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import test from "node:test";
import { chatContent } from "./content";

test("uses an empty string for textless Chat Completions content", () => {
assert.equal(chatContent("", []), "");
});

test("preserves text and image content parts", () => {
const image = { type: "image_url" as const, image_url: { url: "data:image/png;base64,AA==" } };
assert.deepEqual(chatContent("Describe this image", [image]), [
{ type: "text", text: "Describe this image" },
image,
]);
});
13 changes: 13 additions & 0 deletions src/provider/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface ContentPart {
type: "text" | "image_url";
text?: string;
image_url?: { url: string };
}

export type ChatContent = string | ContentPart[];

export function chatContent(value: string, images: readonly ContentPart[]): ChatContent {
return images.length
? [...(value ? [{ type: "text" as const, text: value }] : []), ...images]
: value;
}
15 changes: 5 additions & 10 deletions src/provider/messages.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import * as vscode from "vscode";
import { chatContent, type ChatContent, type ContentPart } from "./content";

const MAX_INLINE_IMAGE_BYTES = 3_750_000;
export type { ChatContent, ContentPart } from "./content";

export interface ContentPart {
type: "text" | "image_url";
text?: string;
image_url?: { url: string };
}
const MAX_INLINE_IMAGE_BYTES = 3_750_000;

export interface ChatMessage {
role: "user" | "assistant" | "tool";
content: string | ContentPart[] | null;
content: ChatContent;
tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }>;
tool_call_id?: string;
reasoning_content?: string;
Expand Down Expand Up @@ -47,9 +44,7 @@ export function convertChatMessages(messages: readonly vscode.LanguageModelChatR
}
}
const value = text.join("\n");
const content: string | ContentPart[] | null = images.length
? [...(value ? [{ type: "text" as const, text: value }] : []), ...images]
: value || null;
const content = chatContent(value, images);
const current: ChatMessage = {
role,
content,
Expand Down
25 changes: 25 additions & 0 deletions src/provider/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,31 @@ test("builds a Chat Completions request with usage and optional tools", () => {
});
});

test("keeps tool-only assistant content provider-compatible", () => {
const chatModel = { ...model, endpoint: "chat-completions" as const };
const body = buildRequestBody(
chatModel,
[{
role: "assistant",
content: "",
tool_calls: [{ id: "call-1", type: "function", function: { name: "lookup", arguments: "{}" } }],
}],
[],
[],
[],
undefined,
1_024,
"auto",
);

assert.deepEqual(body.messages, [{
role: "assistant",
content: "",
tool_calls: [{ id: "call-1", type: "function", function: { name: "lookup", arguments: "{}" } }],
}]);
assert.equal(JSON.stringify(body).includes('"content":null'), false);
});

test("extension request fields override catalog request options", () => {
const body = buildRequestBody(model, [], [{ type: "message", role: "user", content: "Hello" }], [], [], undefined, 512, "auto");
const merged = mergeRequestBody({
Expand Down