Skip to content
Open
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
39 changes: 35 additions & 4 deletions src/adapter/openai-to-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* Converts OpenAI chat request format to Claude CLI input
*/

import type { OpenAIChatRequest } from "../types/openai.js";
import type {
OpenAIChatRequest,
OpenAIContentPart,
} from "../types/openai.js";

export type ClaudeModel = "opus" | "sonnet" | "haiku";

Expand Down Expand Up @@ -46,6 +49,32 @@ export function extractModel(model: string): ClaudeModel {
return "opus";
}

/**
* Extract text from message content.
*
* OpenAI API allows content to be either a plain string or an array of
* content parts (e.g. [{type: "text", text: "..."}]). This function
* normalises both forms into a single string.
*/
export function extractContent(
content: string | OpenAIContentPart[],
): string {
if (typeof content === "string") return content;

if (Array.isArray(content)) {
return content
.map((part) => {
if (typeof part === "string") return part;
if (part && typeof part === "object") return part.text ?? "";
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function silently ignores image_url content parts by only extracting the text field. When a message contains image content (type: "image_url"), this data is lost without any indication to the user. Consider either logging a warning when image content is encountered, or documenting this limitation in the function's docstring, since Claude CLI may not support image inputs.

Copilot uses AI. Check for mistakes.
return "";
})
Comment on lines +66 to +70
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for typeof part === "string" is inconsistent with the OpenAIContentPart type definition, which only allows objects with specific structure. According to the OpenAI API specification, content array elements should always be objects with a type field. Consider removing this check or updating the type definition if the API actually allows plain strings in the array.

Suggested change
.map((part) => {
if (typeof part === "string") return part;
if (part && typeof part === "object") return part.text ?? "";
return "";
})
.map((part) => part.text ?? "")

Copilot uses AI. Check for mistakes.
.filter(Boolean)
.join("\n");
}

return String(content ?? "");
}

/**
* Convert OpenAI messages array to a single prompt string for Claude CLI
*
Expand All @@ -56,20 +85,22 @@ export function messagesToPrompt(messages: OpenAIChatRequest["messages"]): strin
const parts: string[] = [];

for (const msg of messages) {
const text = extractContent(msg.content);

switch (msg.role) {
case "system":
// System messages become context instructions
parts.push(`<system>\n${msg.content}\n</system>\n`);
parts.push(`<system>\n${text}\n</system>\n`);
break;

case "user":
// User messages are the main prompt
parts.push(msg.content);
parts.push(text);
break;

case "assistant":
// Previous assistant responses for context
parts.push(`<previous_response>\n${msg.content}\n</previous_response>\n`);
parts.push(`<previous_response>\n${text}\n</previous_response>\n`);
break;
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/types/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@
* Used for Clawdbot integration
*/

/**
* A single content part in a multi-part message.
* See: https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages
*/
export interface OpenAIContentPart {
type: "text" | "image_url";
text?: string;
image_url?: { url: string; detail?: string };
}
Comment on lines +10 to +14
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAIContentPart interface should use discriminated unions to properly represent the OpenAI API spec. Currently, both text and image_url fields are optional, which allows invalid states like having both or neither field. The correct type should be: {type: "text"; text: string} | {type: "image_url"; image_url: {url: string; detail?: string}}. This ensures type safety and matches the actual API behavior where the field name corresponds to the type.

Suggested change
export interface OpenAIContentPart {
type: "text" | "image_url";
text?: string;
image_url?: { url: string; detail?: string };
}
export type OpenAIContentPart =
| {
type: "text";
text: string;
}
| {
type: "image_url";
image_url: { url: string; detail?: string };
};

Copilot uses AI. Check for mistakes.

export interface OpenAIChatMessage {
role: "system" | "user" | "assistant";
content: string;
content: string | OpenAIContentPart[];
}

export interface OpenAIChatRequest {
Expand Down
Loading