From 06a5a1a7aa31acfefeedf0d25422a7646c9994a6 Mon Sep 17 00:00:00 2001 From: Grass Zhang Date: Sun, 1 Feb 2026 11:30:44 +0000 Subject: [PATCH 01/10] fix: handle array content format in OpenAI messages The OpenAI Chat Completions API allows message content to be either a plain string or an array of content parts (e.g. [{type: "text", text: "..."}]). When an upstream client sends array content, the previous code passed it directly to template literals, resulting in "[object Object]" being sent to Claude CLI. Add extractContent() helper that normalises both string and array formats into a single string, and update the OpenAIChatMessage type to reflect the actual API spec. --- src/adapter/openai-to-cli.ts | 39 ++++++++++++++++++++++++++++++++---- src/types/openai.ts | 12 ++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/adapter/openai-to-cli.ts b/src/adapter/openai-to-cli.ts index c8ecaa1..41af8a3 100644 --- a/src/adapter/openai-to-cli.ts +++ b/src/adapter/openai-to-cli.ts @@ -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"; @@ -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 ?? ""; + return ""; + }) + .filter(Boolean) + .join("\n"); + } + + return String(content ?? ""); +} + /** * Convert OpenAI messages array to a single prompt string for Claude CLI * @@ -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(`\n${msg.content}\n\n`); + parts.push(`\n${text}\n\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(`\n${msg.content}\n\n`); + parts.push(`\n${text}\n\n`); break; } } diff --git a/src/types/openai.ts b/src/types/openai.ts index c116658..706ef62 100644 --- a/src/types/openai.ts +++ b/src/types/openai.ts @@ -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 }; +} + export interface OpenAIChatMessage { role: "system" | "user" | "assistant"; - content: string; + content: string | OpenAIContentPart[]; } export interface OpenAIChatRequest { From c2df17fa134384cbe4cc6accdb28fa949f76cce4 Mon Sep 17 00:00:00 2001 From: Cora AI Date: Wed, 4 Feb 2026 12:21:17 -0500 Subject: [PATCH 02/10] feat: add system prompt support for OpenClaw integration Adds support for passing system messages to Claude Code CLI via the --append-system-prompt flag. This enables OpenClaw agents to properly send their identity, backstory, and instructions through the proxy. Changes: - Support both "system" and "developer" message roles (OpenAI spec) - Extract system messages separately from conversation messages - Pass system prompt via --append-system-prompt CLI flag - Add optional tools parameter for future tool restriction support This fixes the issue where OpenClaw system messages (containing agent identity and configuration) were being ignored, causing Claude to respond with its default identity instead of the configured persona. Co-Authored-By: Claude Sonnet 4.5 --- src/adapter/openai-to-cli.ts | 54 ++++++++++++++++++++++++++++-------- src/server/routes.ts | 4 +++ src/subprocess/manager.ts | 16 ++++++++++- src/types/openai.ts | 2 +- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/adapter/openai-to-cli.ts b/src/adapter/openai-to-cli.ts index 41af8a3..b205c46 100644 --- a/src/adapter/openai-to-cli.ts +++ b/src/adapter/openai-to-cli.ts @@ -13,6 +13,8 @@ export interface CliInput { prompt: string; model: ClaudeModel; sessionId?: string; + systemPrompt?: string; + tools?: string[]; } const MODEL_MAP: Record = { @@ -76,45 +78,75 @@ export function extractContent( } /** - * Convert OpenAI messages array to a single prompt string for Claude CLI + * Extract system messages and conversation from OpenAI messages array * - * Claude Code CLI in --print mode expects a single prompt, not a conversation. - * We format the messages into a readable format that preserves context. + * System messages should be passed via --append-system-prompt flag, + * not embedded in the user prompt (more reliable for OpenClaw integration). */ -export function messagesToPrompt(messages: OpenAIChatRequest["messages"]): string { - const parts: string[] = []; +export function extractMessagesContent(messages: OpenAIChatRequest["messages"]): { + systemPrompt: string | undefined; + conversationPrompt: string; +} { + const systemParts: string[] = []; + const conversationParts: string[] = []; for (const msg of messages) { const text = extractContent(msg.content); switch (msg.role) { case "system": - // System messages become context instructions - parts.push(`\n${text}\n\n`); + case "developer": + // System/developer messages go to --append-system-prompt flag + // "developer" is OpenAI's newer role for system-level instructions + systemParts.push(text); break; case "user": // User messages are the main prompt - parts.push(text); + conversationParts.push(text); break; case "assistant": // Previous assistant responses for context - parts.push(`\n${text}\n\n`); + conversationParts.push(`\n${text}\n\n`); break; } } - return parts.join("\n").trim(); + return { + systemPrompt: systemParts.length > 0 ? systemParts.join("\n\n").trim() : undefined, + conversationPrompt: conversationParts.join("\n").trim(), + }; +} + +/** + * Convert OpenAI messages array to a single prompt string for Claude CLI + * + * @deprecated Use extractMessagesContent instead for better system prompt handling + */ +export function messagesToPrompt(messages: OpenAIChatRequest["messages"]): string { + const { systemPrompt, conversationPrompt } = extractMessagesContent(messages); + + if (systemPrompt) { + return `\n${systemPrompt}\n\n\n${conversationPrompt}`; + } + + return conversationPrompt; } /** * Convert OpenAI chat request to CLI input format */ export function openaiToCli(request: OpenAIChatRequest): CliInput { + const { systemPrompt, conversationPrompt } = extractMessagesContent(request.messages); + return { - prompt: messagesToPrompt(request.messages), + prompt: conversationPrompt, model: extractModel(request.model), sessionId: request.user, // Use OpenAI's user field for session mapping + systemPrompt, + // TODO: Extract tool names from request.tools and map to Claude Code tool names + // For now, let Claude Code use all its builtin tools + tools: undefined, }; } diff --git a/src/server/routes.ts b/src/server/routes.ts index ffe2e5b..3fa27a8 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -179,6 +179,8 @@ async function handleStreamingResponse( subprocess.start(cliInput.prompt, { model: cliInput.model, sessionId: cliInput.sessionId, + systemPrompt: cliInput.systemPrompt, + tools: cliInput.tools, }).catch((err) => { console.error("[Streaming] Subprocess start error:", err); reject(err); @@ -234,6 +236,8 @@ async function handleNonStreamingResponse( .start(cliInput.prompt, { model: cliInput.model, sessionId: cliInput.sessionId, + systemPrompt: cliInput.systemPrompt, + tools: cliInput.tools, }) .catch((error) => { res.status(500).json({ diff --git a/src/subprocess/manager.ts b/src/subprocess/manager.ts index 6551a81..ac06971 100644 --- a/src/subprocess/manager.ts +++ b/src/subprocess/manager.ts @@ -21,6 +21,8 @@ import type { ClaudeModel } from "../adapter/openai-to-cli.js"; export interface SubprocessOptions { model: ClaudeModel; sessionId?: string; + systemPrompt?: string; + tools?: string[]; cwd?: string; timeout?: number; } @@ -137,13 +139,25 @@ export class ClaudeSubprocess extends EventEmitter { "--model", options.model, // Model alias (opus/sonnet/haiku) "--no-session-persistence", // Don't save sessions - prompt, // Pass prompt as argument (more reliable than stdin) ]; + // Add system prompt if provided (backstory/memories from OpenClaw) + if (options.systemPrompt) { + args.push("--append-system-prompt", options.systemPrompt); + } + + // Add tool restrictions if provided + if (options.tools && options.tools.length > 0) { + args.push("--tools", options.tools.join(",")); + } + if (options.sessionId) { args.push("--session-id", options.sessionId); } + // Prompt goes last + args.push(prompt); + return args; } diff --git a/src/types/openai.ts b/src/types/openai.ts index 706ef62..5d435f4 100644 --- a/src/types/openai.ts +++ b/src/types/openai.ts @@ -14,7 +14,7 @@ export interface OpenAIContentPart { } export interface OpenAIChatMessage { - role: "system" | "user" | "assistant"; + role: "system" | "developer" | "user" | "assistant"; content: string | OpenAIContentPart[]; } From db7ff87776eb0bb4af05bbb5ebf054294bd8b6b9 Mon Sep 17 00:00:00 2001 From: Cora AI Date: Wed, 4 Feb 2026 21:12:07 -0500 Subject: [PATCH 03/10] feat: add debugging logs and skip permissions for service mode - Add console.error logging for system prompt tracking - Add --dangerously-skip-permissions flag for service operations - Log assistant messages and results for debugging Co-Authored-By: Claude Sonnet 4.5 --- package-lock.json | 7 +++++-- src/subprocess/manager.ts | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfdfc81..f6561bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { - "name": "claude-code-cli-provider", + "name": "claude-max-api-proxy", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "claude-code-cli-provider", + "name": "claude-max-api-proxy", "version": "1.0.0", "license": "MIT", "dependencies": { "express": "^4.21.2", "uuid": "^11.0.5" }, + "bin": { + "claude-max-api": "dist/server/standalone.js" + }, "devDependencies": { "@types/express": "^5.0.0", "@types/node": "^22.10.7", diff --git a/src/subprocess/manager.ts b/src/subprocess/manager.ts index ac06971..4e7e80e 100644 --- a/src/subprocess/manager.ts +++ b/src/subprocess/manager.ts @@ -139,11 +139,16 @@ export class ClaudeSubprocess extends EventEmitter { "--model", options.model, // Model alias (opus/sonnet/haiku) "--no-session-persistence", // Don't save sessions + "--dangerously-skip-permissions", // Allow file operations (running as service) ]; // Add system prompt if provided (backstory/memories from OpenClaw) if (options.systemPrompt) { + console.error(`[Subprocess] System prompt: ${options.systemPrompt.length} chars`); + console.error(`[Subprocess] System prompt content:\n${options.systemPrompt}`); args.push("--append-system-prompt", options.systemPrompt); + } else { + console.error("[Subprocess] NO system prompt provided"); } // Add tool restrictions if provided @@ -180,8 +185,10 @@ export class ClaudeSubprocess extends EventEmitter { // Emit content delta for streaming this.emit("content_delta", message as ClaudeCliStreamEvent); } else if (isAssistantMessage(message)) { + console.error(`[Response] Assistant message:`, JSON.stringify(message.message.content)); this.emit("assistant", message); } else if (isResultMessage(message)) { + console.error(`[Response] Result:`, message.result); this.emit("result", message); } } catch { From fae2aba80bb95dab6601d4cba8ee8755fb693ceb Mon Sep 17 00:00:00 2001 From: Cora AI Date: Wed, 4 Feb 2026 21:14:52 -0500 Subject: [PATCH 04/10] feat: add DEBUG_SUBPROCESS environment variable for optional logging - Replace all console.error calls with conditional debug() method - Debug logging disabled by default, enabled via DEBUG_SUBPROCESS=true - Add documentation in README troubleshooting section - Keeps production logs clean while allowing detailed debugging when needed Co-Authored-By: Claude Sonnet 4.5 --- README.md | 14 ++++++++++++++ src/subprocess/manager.ts | 30 +++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f976dfa..e53704d 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,20 @@ Check that the Claude CLI is in your PATH: which claude ``` +### Enable debug logging + +To troubleshoot subprocess issues, enable detailed debug logging: +```bash +DEBUG_SUBPROCESS=true node dist/server/standalone.js +``` + +This will log: +- Subprocess spawn events and PIDs +- Stdout/stderr data flow +- System prompt content +- Assistant messages and results +- Process exit codes + ## Contributing Contributions welcome! Please submit PRs with tests. diff --git a/src/subprocess/manager.ts b/src/subprocess/manager.ts index 4e7e80e..670b5eb 100644 --- a/src/subprocess/manager.ts +++ b/src/subprocess/manager.ts @@ -38,12 +38,24 @@ export interface SubprocessEvents { const DEFAULT_TIMEOUT = 300000; // 5 minutes +// Debug logging controlled by environment variable +const DEBUG = process.env.DEBUG_SUBPROCESS === "true"; + export class ClaudeSubprocess extends EventEmitter { private process: ChildProcess | null = null; private buffer: string = ""; private timeoutId: NodeJS.Timeout | null = null; private isKilled: boolean = false; + /** + * Conditional debug logging + */ + private debug(...args: any[]): void { + if (DEBUG) { + console.error(...args); + } + } + /** * Start the Claude CLI subprocess with the given prompt */ @@ -86,12 +98,12 @@ export class ClaudeSubprocess extends EventEmitter { // Close stdin since we pass prompt as argument this.process.stdin?.end(); - console.error(`[Subprocess] Process spawned with PID: ${this.process.pid}`); + this.debug(`[Subprocess] Process spawned with PID: ${this.process.pid}`); // Parse JSON stream from stdout this.process.stdout?.on("data", (chunk: Buffer) => { const data = chunk.toString(); - console.error(`[Subprocess] Received ${data.length} bytes of stdout`); + this.debug(`[Subprocess] Received ${data.length} bytes of stdout`); this.buffer += data; this.processBuffer(); }); @@ -102,13 +114,13 @@ export class ClaudeSubprocess extends EventEmitter { if (errorText) { // Don't emit as error unless it's actually an error // Claude CLI may write debug info to stderr - console.error("[Subprocess stderr]:", errorText.slice(0, 200)); + this.debug("[Subprocess stderr]:", errorText.slice(0, 200)); } }); // Handle process close this.process.on("close", (code) => { - console.error(`[Subprocess] Process closed with code: ${code}`); + this.debug(`[Subprocess] Process closed with code: ${code}`); this.clearTimeout(); // Process any remaining buffer if (this.buffer.trim()) { @@ -144,11 +156,11 @@ export class ClaudeSubprocess extends EventEmitter { // Add system prompt if provided (backstory/memories from OpenClaw) if (options.systemPrompt) { - console.error(`[Subprocess] System prompt: ${options.systemPrompt.length} chars`); - console.error(`[Subprocess] System prompt content:\n${options.systemPrompt}`); + this.debug(`[Subprocess] System prompt: ${options.systemPrompt.length} chars`); + this.debug(`[Subprocess] System prompt content:\n${options.systemPrompt}`); args.push("--append-system-prompt", options.systemPrompt); } else { - console.error("[Subprocess] NO system prompt provided"); + this.debug("[Subprocess] NO system prompt provided"); } // Add tool restrictions if provided @@ -185,10 +197,10 @@ export class ClaudeSubprocess extends EventEmitter { // Emit content delta for streaming this.emit("content_delta", message as ClaudeCliStreamEvent); } else if (isAssistantMessage(message)) { - console.error(`[Response] Assistant message:`, JSON.stringify(message.message.content)); + this.debug(`[Response] Assistant message:`, JSON.stringify(message.message.content)); this.emit("assistant", message); } else if (isResultMessage(message)) { - console.error(`[Response] Result:`, message.result); + this.debug(`[Response] Result:`, message.result); this.emit("result", message); } } catch { From 809653246548a0b30787cec7da546b6a84d49f50 Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:04:57 +0800 Subject: [PATCH 05/10] fix: include usage data in streaming SSE response Without usage data in the final streaming chunk, downstream consumers (e.g. OpenClaw compaction) cannot track token consumption, causing sessions to grow unbounded and eventually hit context overflow. Co-Authored-By: Claude Opus 4.6 --- src/server/routes.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/server/routes.ts b/src/server/routes.ts index 3fa27a8..cdd186d 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -135,11 +135,19 @@ async function handleStreamingResponse( lastModel = message.message.model; }); - subprocess.on("result", (_result: ClaudeCliResult) => { + subprocess.on("result", (result: ClaudeCliResult) => { isComplete = true; if (!res.writableEnded) { - // Send final done chunk with finish_reason - const doneChunk = createDoneChunk(requestId, lastModel); + // Send final chunk with finish_reason and usage data + const doneChunk = { + ...createDoneChunk(requestId, lastModel), + usage: { + prompt_tokens: result.usage?.input_tokens || 0, + completion_tokens: result.usage?.output_tokens || 0, + total_tokens: + (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0), + }, + }; res.write(`data: ${JSON.stringify(doneChunk)}\n\n`); res.write("data: [DONE]\n\n"); res.end(); From bc21e513463f5d8e1ce7ceb4cd61deb2fd5c6bf9 Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:15:03 +0800 Subject: [PATCH 06/10] fix: handle undefined model in normalizeModelName to prevent crash When Claude CLI returns a rate limit response, modelUsage can be an empty object causing Object.keys({})[0] to return undefined, which crashes normalizeModelName. Credit: PR #7 by @wende Co-Authored-By: Claude Opus 4.6 --- src/adapter/cli-to-openai.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/adapter/cli-to-openai.ts b/src/adapter/cli-to-openai.ts index 1e43eab..bb5f638 100644 --- a/src/adapter/cli-to-openai.ts +++ b/src/adapter/cli-to-openai.ts @@ -102,7 +102,8 @@ export function cliResultToOpenai( * Normalize Claude model names to a consistent format * e.g., "claude-sonnet-4-5-20250929" -> "claude-sonnet-4" */ -function normalizeModelName(model: string): string { +function normalizeModelName(model: string | undefined): string { + if (!model) return "claude-sonnet-4"; if (model.includes("opus")) return "claude-opus-4"; if (model.includes("sonnet")) return "claude-sonnet-4"; if (model.includes("haiku")) return "claude-haiku-4"; From 4215a018e8faf87fa939c0b9c44a995404229c3f Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:15:50 +0800 Subject: [PATCH 07/10] fix: pass prompt via stdin to avoid E2BIG error Large prompts (conversation history + system prompt) can exceed the OS argument size limit, causing spawn() to fail with E2BIG. Passing via stdin has no size limitation. Credit: PR #12 by @kevinfealey Co-Authored-By: Claude Opus 4.6 --- src/subprocess/manager.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/subprocess/manager.ts b/src/subprocess/manager.ts index 670b5eb..6494d27 100644 --- a/src/subprocess/manager.ts +++ b/src/subprocess/manager.ts @@ -60,7 +60,7 @@ export class ClaudeSubprocess extends EventEmitter { * Start the Claude CLI subprocess with the given prompt */ async start(prompt: string, options: SubprocessOptions): Promise { - const args = this.buildArgs(prompt, options); + const args = this.buildArgs(options); const timeout = options.timeout || DEFAULT_TIMEOUT; return new Promise((resolve, reject) => { @@ -95,8 +95,12 @@ export class ClaudeSubprocess extends EventEmitter { } }); - // Close stdin since we pass prompt as argument - this.process.stdin?.end(); + // Write prompt to stdin instead of passing as CLI argument + // This avoids E2BIG errors when prompts exceed the OS argument size limit + if (this.process.stdin) { + this.process.stdin.write(prompt); + this.process.stdin.end(); + } this.debug(`[Subprocess] Process spawned with PID: ${this.process.pid}`); @@ -141,7 +145,7 @@ export class ClaudeSubprocess extends EventEmitter { /** * Build CLI arguments array */ - private buildArgs(prompt: string, options: SubprocessOptions): string[] { + private buildArgs(options: SubprocessOptions): string[] { const args = [ "--print", // Non-interactive mode "--output-format", @@ -172,8 +176,7 @@ export class ClaudeSubprocess extends EventEmitter { args.push("--session-id", options.sessionId); } - // Prompt goes last - args.push(prompt); + // Prompt is passed via stdin to avoid E2BIG errors with large prompts return args; } From c9bb9d27c7e8435b1189b287765c5cc8c9eb3143 Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:16:29 +0800 Subject: [PATCH 08/10] feat: add Claude 4.5/4.6 models and claude-max prefix support Add model mappings for claude-opus-4-6, claude-sonnet-4-5, and claude-max/* provider prefix used by OpenClaw. Also add opus-max and sonnet-max aliases. Credit: PR #10 by @jamshehan (model mapping portion) Co-Authored-By: Claude Opus 4.6 --- src/adapter/openai-to-cli.ts | 12 ++++++++++++ src/server/routes.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/adapter/openai-to-cli.ts b/src/adapter/openai-to-cli.ts index b205c46..34c5a88 100644 --- a/src/adapter/openai-to-cli.ts +++ b/src/adapter/openai-to-cli.ts @@ -20,15 +20,27 @@ export interface CliInput { const MODEL_MAP: Record = { // Direct model names "claude-opus-4": "opus", + "claude-opus-4-6": "opus", "claude-sonnet-4": "sonnet", + "claude-sonnet-4-5": "sonnet", "claude-haiku-4": "haiku", // With provider prefix "claude-code-cli/claude-opus-4": "opus", + "claude-code-cli/claude-opus-4-6": "opus", "claude-code-cli/claude-sonnet-4": "sonnet", + "claude-code-cli/claude-sonnet-4-5": "sonnet", "claude-code-cli/claude-haiku-4": "haiku", + // Claude-max prefix (from OpenClaw config) + "claude-max/claude-opus-4": "opus", + "claude-max/claude-opus-4-6": "opus", + "claude-max/claude-sonnet-4": "sonnet", + "claude-max/claude-sonnet-4-5": "sonnet", + "claude-max/claude-haiku-4": "haiku", // Aliases "opus": "opus", + "opus-max": "opus", "sonnet": "sonnet", + "sonnet-max": "sonnet", "haiku": "haiku", }; diff --git a/src/server/routes.ts b/src/server/routes.ts index cdd186d..abfcb93 100644 --- a/src/server/routes.ts +++ b/src/server/routes.ts @@ -269,12 +269,24 @@ export function handleModels(_req: Request, res: Response): void { res.json({ object: "list", data: [ + { + id: "claude-opus-4-6", + object: "model", + owned_by: "anthropic", + created: Math.floor(Date.now() / 1000), + }, { id: "claude-opus-4", object: "model", owned_by: "anthropic", created: Math.floor(Date.now() / 1000), }, + { + id: "claude-sonnet-4-5", + object: "model", + owned_by: "anthropic", + created: Math.floor(Date.now() / 1000), + }, { id: "claude-sonnet-4", object: "model", From 286d2f9bffbe013ed8cd2f2de4930a76fdc8d846 Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:24:38 +0800 Subject: [PATCH 09/10] docs: update README for fork with all enhancements - Rebrand as actively maintained fork of atalovesyou/claude-max-api-proxy - Document new models (Opus 4.6, Sonnet 4.5) and model aliases - Add "What's Different" comparison table - Add OpenClaw, Continue.dev, and Python client configuration examples - Add LaunchAgent setup instructions - Update architecture notes for stdin-based prompt delivery - Credit community PR contributors Co-Authored-By: Claude Opus 4.6 --- README.md | 174 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index e53704d..a6d8d25 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# Claude Code CLI Provider +# Claude Max API Proxy **Use your Claude Max subscription ($200/month) with any OpenAI-compatible client — no separate API costs!** -This provider wraps the Claude Code CLI as a subprocess and exposes an OpenAI-compatible HTTP API, allowing tools like Clawdbot, Continue.dev, or any OpenAI-compatible client to use your Claude Max subscription instead of paying per-API-call. +This proxy wraps the Claude Code CLI as a subprocess and exposes an OpenAI-compatible HTTP API, allowing tools like OpenClaw, Continue.dev, or any OpenAI-compatible client to use your Claude Max subscription instead of paying per-API-call. + +> **Fork note:** This is an actively maintained fork of [atalovesyou/claude-max-api-proxy](https://github.com/atalovesyou/claude-max-api-proxy) with additional features and bug fixes. See [What's Different](#whats-different-in-this-fork) below. ## Why This Exists @@ -10,20 +12,20 @@ This provider wraps the Claude Code CLI as a subprocess and exposes an OpenAI-co |----------|------|------------| | Claude API | ~$15/M input, ~$75/M output tokens | Pay per use | | Claude Max | $200/month flat | OAuth blocked for third-party API use | -| **This Provider** | $0 extra (uses Max subscription) | Routes through CLI | +| **This Proxy** | $0 extra (uses Max subscription) | Routes through CLI | -Anthropic blocks OAuth tokens from being used directly with third-party API clients. However, the Claude Code CLI *can* use OAuth tokens. This provider bridges that gap by wrapping the CLI and exposing a standard API. +Anthropic blocks OAuth tokens from being used directly with third-party API clients. However, the Claude Code CLI *can* use OAuth tokens. This proxy bridges that gap by wrapping the CLI and exposing a standard API. ## How It Works ``` -Your App (Clawdbot, etc.) +Your App (OpenClaw, Continue.dev, etc.) ↓ HTTP Request (OpenAI format) ↓ - Claude Code CLI Provider (this project) + Claude Max API Proxy (this project) ↓ - Claude Code CLI (subprocess) + Claude Code CLI (subprocess, prompt via stdin) ↓ OAuth Token (from Max subscription) ↓ @@ -35,12 +37,14 @@ Your App (Clawdbot, etc.) ## Features - **OpenAI-compatible API** — Works with any client that supports OpenAI's API format -- **Streaming support** — Real-time token streaming via Server-Sent Events -- **Multiple models** — Claude Opus, Sonnet, and Haiku +- **Streaming support** — Real-time token streaming via Server-Sent Events (with usage data) +- **Multiple models** — Claude Opus 4.6, Sonnet 4.5, Opus 4, Sonnet 4, and Haiku 4 +- **System prompt support** — Passes system/developer messages via `--append-system-prompt` - **Session management** — Maintains conversation context - **Auto-start service** — Optional LaunchAgent for macOS - **Zero configuration** — Uses existing Claude CLI authentication -- **Secure by design** — Uses spawn() to prevent shell injection +- **Secure by design** — Uses spawn() + stdin to prevent shell injection and E2BIG errors +- **Debug logging** — Optional `DEBUG_SUBPROCESS=true` for troubleshooting ## Prerequisites @@ -55,14 +59,17 @@ Your App (Clawdbot, etc.) ```bash # Clone the repository -git clone https://github.com/anthropics/claude-code-cli-provider.git -cd claude-code-cli-provider +git clone https://github.com/smartchainark/claude-max-api-proxy.git +cd claude-max-api-proxy # Install dependencies npm install # Build npm run build + +# (Optional) Install as global command +npm link ``` ## Usage @@ -71,6 +78,8 @@ npm run build ```bash node dist/server/standalone.js +# or if installed globally: +claude-max-api ``` The server runs at `http://localhost:3456` by default. @@ -88,7 +97,7 @@ curl http://localhost:3456/v1/models curl -X POST http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ - "model": "claude-opus-4", + "model": "claude-opus-4-6", "messages": [{"role": "user", "content": "Hello!"}] }' @@ -96,10 +105,22 @@ curl -X POST http://localhost:3456/v1/chat/completions \ curl -N -X POST http://localhost:3456/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ - "model": "claude-opus-4", + "model": "claude-opus-4-6", "messages": [{"role": "user", "content": "Hello!"}], "stream": true }' + +# With system prompt +curl -N -X POST http://localhost:3456/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "claude-sonnet-4-5", + "messages": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ], + "stream": true + }' ``` ## API Endpoints @@ -112,34 +133,53 @@ curl -N -X POST http://localhost:3456/v1/chat/completions \ ## Available Models -| Model ID | Maps To | -|----------|---------| -| `claude-opus-4` | Claude Opus 4.5 | -| `claude-sonnet-4` | Claude Sonnet 4 | -| `claude-haiku-4` | Claude Haiku 4 | +| Model ID | Maps To | Notes | +|----------|---------|-------| +| `claude-opus-4-6` | Claude Opus 4.6 | Latest, most capable | +| `claude-opus-4` | Claude Opus 4 | | +| `claude-sonnet-4-5` | Claude Sonnet 4.5 | Fast and capable | +| `claude-sonnet-4` | Claude Sonnet 4 | | +| `claude-haiku-4` | Claude Haiku 4 | Fastest | -## Configuration with Popular Tools +### Model aliases -### Clawdbot +The proxy also accepts prefixed model names for compatibility with different clients: -Clawdbot has **built-in support** for Claude CLI OAuth! Check your config: +| Prefix | Example | Maps To | +|--------|---------|---------| +| `claude-max/` | `claude-max/claude-opus-4-6` | `opus` | +| `claude-code-cli/` | `claude-code-cli/claude-sonnet-4-5` | `sonnet` | +| (none) | `opus-max`, `sonnet-max` | `opus`, `sonnet` | -```bash -clawdbot models status -``` +## Configuration with Popular Tools -If you see `anthropic:claude-cli=OAuth`, you're already using your Max subscription. +### OpenClaw -### Continue.dev +```json +{ + "providers": { + "claude-max": { + "baseUrl": "http://127.0.0.1:3456/v1", + "apiKey": "not-needed", + "api": "openai-completions", + "models": [ + { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (Max)" }, + { "id": "claude-sonnet-4-5", "name": "Claude Sonnet 4.5 (Max)" }, + { "id": "claude-haiku-4", "name": "Claude Haiku 4 (Max)" } + ] + } + } +} +``` -Add to your Continue config: +### Continue.dev ```json { "models": [{ - "title": "Claude (Max)", + "title": "Claude Opus 4.6 (Max)", "provider": "openai", - "model": "claude-opus-4", + "model": "claude-opus-4-6", "apiBase": "http://localhost:3456/v1", "apiKey": "not-needed" }] @@ -157,14 +197,53 @@ client = OpenAI( ) response = client.chat.completions.create( - model="claude-opus-4", + model="claude-opus-4-6", messages=[{"role": "user", "content": "Hello!"}] ) ``` ## Auto-Start on macOS -Create a LaunchAgent to start the provider automatically on login. See `docs/macos-setup.md` for detailed instructions. +Create a LaunchAgent to start the proxy automatically on login: + +```xml + + + + + + Label + com.claude-max-api-proxy + ProgramArguments + + /path/to/node + /path/to/claude-max-api-proxy/dist/server/standalone.js + + RunAtLoad + + KeepAlive + + + +``` + +## What's Different in This Fork + +Compared to the [original repo](https://github.com/atalovesyou/claude-max-api-proxy): + +| Feature | Original | This Fork | +|---------|----------|-----------| +| Streaming usage data | Missing | Included in final SSE chunk | +| System prompt | Embedded in user prompt | Via `--append-system-prompt` flag | +| Prompt delivery | CLI argument (E2BIG risk) | stdin (no size limit) | +| Model support | Opus 4, Sonnet 4, Haiku 4 | + Opus 4.6, Sonnet 4.5 | +| Model prefixes | `claude-code-cli/` only | + `claude-max/`, aliases | +| Undefined model crash | Crashes on rate limit | Graceful fallback | +| Debug logging | Always on (noisy) | Opt-in via `DEBUG_SUBPROCESS` | +| Permissions | Requires confirmation | `--dangerously-skip-permissions` for service mode | +| Array content | Not supported | Handles string and array content parts | + +Community PRs incorporated: [#7](https://github.com/atalovesyou/claude-max-api-proxy/pull/7), [#10](https://github.com/atalovesyou/claude-max-api-proxy/pull/10), [#12](https://github.com/atalovesyou/claude-max-api-proxy/pull/12). ## Architecture @@ -172,12 +251,12 @@ Create a LaunchAgent to start the provider automatically on login. See `docs/mac src/ ├── types/ │ ├── claude-cli.ts # Claude CLI JSON output types -│ └── openai.ts # OpenAI API types +│ └── openai.ts # OpenAI API types (with multimodal content parts) ├── adapter/ │ ├── openai-to-cli.ts # Convert OpenAI requests → CLI format │ └── cli-to-openai.ts # Convert CLI responses → OpenAI format ├── subprocess/ -│ └── manager.ts # Claude CLI subprocess management +│ └── manager.ts # Claude CLI subprocess management (stdin-based) ├── session/ │ └── manager.ts # Session ID mapping ├── server/ @@ -187,23 +266,6 @@ src/ └── index.ts # Package exports ``` -## Security - -- Uses Node.js `spawn()` instead of shell execution to prevent injection attacks -- No API keys stored or transmitted by this provider -- All authentication handled by Claude CLI's secure keychain storage -- Prompts passed as CLI arguments, not through shell interpretation - -## Cost Savings Example - -| Usage | API Cost | With This Provider | -|-------|----------|-------------------| -| 1M input tokens/month | ~$15 | $0 (included in Max) | -| 500K output tokens/month | ~$37.50 | $0 (included in Max) | -| **Monthly Total** | **~$52.50** | **$0 extra** | - -If you're already paying for Claude Max, this provider lets you use that subscription for API-style access at no additional cost. - ## Troubleshooting ### "Claude CLI not found" @@ -242,6 +304,13 @@ This will log: - Assistant messages and results - Process exit codes +## Security + +- Uses Node.js `spawn()` instead of shell execution to prevent injection attacks +- Prompts passed via stdin, not through shell interpretation or CLI arguments +- No API keys stored or transmitted by this proxy +- All authentication handled by Claude CLI's secure keychain storage + ## Contributing Contributions welcome! Please submit PRs with tests. @@ -252,5 +321,6 @@ MIT ## Acknowledgments -- Built for use with [Clawdbot](https://clawd.bot) +- Originally created by [atalovesyou](https://github.com/atalovesyou/claude-max-api-proxy) +- Community contributors: [@wende](https://github.com/wende), [@kevinfealey](https://github.com/kevinfealey), [@jamshehan](https://github.com/jamshehan) - Powered by [Claude Code CLI](https://github.com/anthropics/claude-code) From b65553865831f424a08225aeafbd2d40736eac77 Mon Sep 17 00:00:00 2001 From: botdev Date: Tue, 10 Feb 2026 14:27:16 +0800 Subject: [PATCH 10/10] docs: restore original sections (Cost Savings, Clawdbot) Keep original README content intact, only add fork-specific changes. Co-Authored-By: Claude Opus 4.6 --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index a6d8d25..bc2665d 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,16 @@ The proxy also accepts prefixed model names for compatibility with different cli ## Configuration with Popular Tools +### Clawdbot + +Clawdbot has **built-in support** for Claude CLI OAuth! Check your config: + +```bash +clawdbot models status +``` + +If you see `anthropic:claude-cli=OAuth`, you're already using your Max subscription. + ### OpenClaw ```json @@ -304,6 +314,16 @@ This will log: - Assistant messages and results - Process exit codes +## Cost Savings Example + +| Usage | API Cost | With This Provider | +|-------|----------|-------------------| +| 1M input tokens/month | ~$15 | $0 (included in Max) | +| 500K output tokens/month | ~$37.50 | $0 (included in Max) | +| **Monthly Total** | **~$52.50** | **$0 extra** | + +If you're already paying for Claude Max, this provider lets you use that subscription for API-style access at no additional cost. + ## Security - Uses Node.js `spawn()` instead of shell execution to prevent injection attacks @@ -322,5 +342,6 @@ MIT ## Acknowledgments - Originally created by [atalovesyou](https://github.com/atalovesyou/claude-max-api-proxy) +- Built for use with [Clawdbot](https://clawd.bot) - Community contributors: [@wende](https://github.com/wende), [@kevinfealey](https://github.com/kevinfealey), [@jamshehan](https://github.com/jamshehan) - Powered by [Claude Code CLI](https://github.com/anthropics/claude-code)