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
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 {