From af5cfff352c2e4c933495fa733be95a0c9114b12 Mon Sep 17 00:00:00 2001 From: Kevin Fealey Date: Sat, 7 Feb 2026 22:32:20 -0500 Subject: [PATCH 1/2] Fix [object Object] serialization issue with multi-modal content - Handle OpenAI multi-modal content format where content can be an array of content parts (e.g., [{"type": "text", "text": "Hello"}]) in addition to plain strings - Add defensive string coercion in response handlers to prevent [object Object] appearing when unexpected types are encountered - Update OpenAI types to properly reflect that content can be string or array format per OpenAI API spec - Add debug logging for request diagnostics Fixes issue #2 in upstream repo where Clawdbot integration showed [object Object] instead of actual message content. --- src/adapter/cli-to-openai.ts | 32 ++++++++++++++++++++++++- src/adapter/openai-to-cli.ts | 46 +++++++++++++++++++++++++++++++++--- src/server/routes.ts | 25 ++++++++++++++++++-- src/types/openai.ts | 11 ++++++++- 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/src/adapter/cli-to-openai.ts b/src/adapter/cli-to-openai.ts index 1e43eab..2a65778 100644 --- a/src/adapter/cli-to-openai.ts +++ b/src/adapter/cli-to-openai.ts @@ -62,6 +62,33 @@ export function createDoneChunk(requestId: string, model: string): OpenAIChatChu }; } +/** + * Ensure content is always a string (defensive against unexpected types) + */ +function ensureString(value: unknown): string { + if (typeof value === "string") { + return value; + } + if (value === null || value === undefined) { + return ""; + } + // If it's an object, try to extract text content or stringify it + if (typeof value === "object") { + // Handle potential content array format + if (Array.isArray(value)) { + return value + .filter((item): item is { type: string; text: string } => + item && typeof item === "object" && item.type === "text" && typeof item.text === "string" + ) + .map((item) => item.text) + .join(""); + } + // Last resort: stringify the object + return JSON.stringify(value); + } + return String(value); +} + /** * Convert Claude CLI result to OpenAI non-streaming response */ @@ -74,6 +101,9 @@ export function cliResultToOpenai( ? Object.keys(result.modelUsage)[0] : "claude-sonnet-4"; + // Ensure content is always a string to prevent [object Object] issues + const content = ensureString(result.result); + return { id: `chatcmpl-${requestId}`, object: "chat.completion", @@ -84,7 +114,7 @@ export function cliResultToOpenai( index: 0, message: { role: "assistant", - content: result.result, + content, }, finish_reason: "stop", }, diff --git a/src/adapter/openai-to-cli.ts b/src/adapter/openai-to-cli.ts index c8ecaa1..557d6dc 100644 --- a/src/adapter/openai-to-cli.ts +++ b/src/adapter/openai-to-cli.ts @@ -46,6 +46,44 @@ export function extractModel(model: string): ClaudeModel { return "opus"; } +/** + * Extract text from message content (handles both string and array formats) + * + * OpenAI API allows content to be: + * - A string: "Hello" + * - An array of content parts: [{"type": "text", "text": "Hello"}] + */ +function extractContentText(content: unknown): string { + // Simple string content + if (typeof content === "string") { + return content; + } + + // Array of content parts (multi-modal format) + if (Array.isArray(content)) { + return content + .filter((part): part is { type: string; text: string } => + part && typeof part === "object" && part.type === "text" && typeof part.text === "string" + ) + .map((part) => part.text) + .join(""); + } + + // Null/undefined + if (content === null || content === undefined) { + return ""; + } + + // Unknown object - try to stringify as last resort + if (typeof content === "object") { + console.error("[extractContentText] Unexpected object content:", JSON.stringify(content).slice(0, 200)); + return JSON.stringify(content); + } + + // Other types - convert to string + return String(content); +} + /** * Convert OpenAI messages array to a single prompt string for Claude CLI * @@ -56,20 +94,22 @@ export function messagesToPrompt(messages: OpenAIChatRequest["messages"]): strin const parts: string[] = []; for (const msg of messages) { + const textContent = extractContentText(msg.content); + switch (msg.role) { case "system": // System messages become context instructions - parts.push(`\n${msg.content}\n\n`); + parts.push(`\n${textContent}\n\n`); break; case "user": // User messages are the main prompt - parts.push(msg.content); + parts.push(textContent); break; case "assistant": // Previous assistant responses for context - parts.push(`\n${msg.content}\n\n`); + parts.push(`\n${textContent}\n\n`); break; } } diff --git a/src/server/routes.ts b/src/server/routes.ts index ffe2e5b..2df9198 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -28,6 +28,21 @@ export async function handleChatCompletions( const body = req.body as OpenAIChatRequest; const stream = body.stream === true; + // Debug: Log incoming request for diagnostics + console.error(`[Request ${requestId}] Incoming request:`, JSON.stringify({ + model: body.model, + stream: body.stream, + messageCount: body.messages?.length, + messages: body.messages?.map((m, i) => ({ + index: i, + role: m.role, + contentType: typeof m.content, + contentPreview: typeof m.content === 'string' + ? m.content.slice(0, 100) + : JSON.stringify(m.content).slice(0, 100) + })) + })); + try { // Validate request if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) { @@ -109,7 +124,9 @@ async function handleStreamingResponse( // Handle streaming content deltas subprocess.on("content_delta", (event: ClaudeCliStreamEvent) => { - const text = event.event.delta?.text || ""; + // Defensive: ensure text is always a string + const rawText = event.event.delta?.text; + const text = typeof rawText === "string" ? rawText : (rawText ? String(rawText) : ""); if (text && !res.writableEnded) { const chunk = { id: `chatcmpl-${requestId}`, @@ -135,8 +152,12 @@ async function handleStreamingResponse( lastModel = message.message.model; }); - subprocess.on("result", (_result: ClaudeCliResult) => { + subprocess.on("result", (result: ClaudeCliResult) => { isComplete = true; + // Debug: log result type for diagnostics + if (typeof result.result !== "string") { + console.error(`[Streaming] WARNING: result.result is not a string, type: ${typeof result.result}`); + } if (!res.writableEnded) { // Send final done chunk with finish_reason const doneChunk = createDoneChunk(requestId, lastModel); diff --git a/src/types/openai.ts b/src/types/openai.ts index c116658..d9ccf3f 100644 --- a/src/types/openai.ts +++ b/src/types/openai.ts @@ -3,9 +3,18 @@ * Used for Clawdbot integration */ +// Content can be a string or an array of content parts (multi-modal) +export interface OpenAIContentPart { + type: "text" | "image_url"; + text?: string; + image_url?: { url: string }; +} + +export type OpenAIMessageContent = string | OpenAIContentPart[]; + export interface OpenAIChatMessage { role: "system" | "user" | "assistant"; - content: string; + content: OpenAIMessageContent; } export interface OpenAIChatRequest { From 3259928929f04e74d0e0af9167bb2b2f4faa332d Mon Sep 17 00:00:00 2001 From: Kevin Fealey Date: Sat, 7 Feb 2026 22:52:58 -0500 Subject: [PATCH 2/2] Add OpenClaw/Clawdbot configuration example to README Provides tested configuration for integrating with OpenClaw including provider setup, model definitions, and agent defaults. Addresses user confusion reported in upstream issue #2. --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f976dfa..9d19153 100644 --- a/README.md +++ b/README.md @@ -120,15 +120,59 @@ curl -N -X POST http://localhost:3456/v1/chat/completions \ ## Configuration with Popular Tools -### Clawdbot +### OpenClaw / Clawdbot -Clawdbot has **built-in support** for Claude CLI OAuth! Check your config: +Add the following to your `~/.openclaw/openclaw.json` (or equivalent config file): -```bash -clawdbot models status +```json +{ + "models": { + "providers": { + "claude-max-proxy": { + "baseUrl": "http://localhost:3456/v1", + "apiKey": "not-needed", + "api": "openai-completions", + "models": [ + { + "id": "claude-opus-4", + "name": "Claude Opus 4 (via Max Proxy)", + "reasoning": true, + "input": ["text"], + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, + "contextWindow": 200000, + "maxTokens": 32000 + }, + { + "id": "claude-sonnet-4", + "name": "Claude Sonnet 4 (via Max Proxy)", + "reasoning": true, + "input": ["text"], + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, + "contextWindow": 200000, + "maxTokens": 32000 + } + ] + } + } + }, + "agents": { + "defaults": { + "model": { + "primary": "claude-max-proxy/claude-opus-4" + }, + "models": { + "claude-max-proxy/claude-opus-4": { "alias": "opus4" }, + "claude-max-proxy/claude-sonnet-4": { "alias": "sonnet4" } + } + } + } +} ``` -If you see `anthropic:claude-cli=OAuth`, you're already using your Max subscription. +**Important:** Make sure the proxy server is running before starting OpenClaw: +```bash +claude-max-api # or: node dist/server/standalone.js +``` ### Continue.dev