diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4deb993..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: CI - -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.bun/install/cache - node_modules - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} - restore-keys: | - ${{ runner.os }}-bun- - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Run tests - run: bun test - - - name: Type check - run: bun run typecheck - - - name: Build - run: bun run build - - - name: Verify build output - run: | - test -f dist/main.js - test -x dist/main.js - head -n 1 dist/main.js | grep -q "#!/usr/bin/env node" \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 5a35039..0665b16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,9 +34,6 @@ bun install # Build the project (creates dist/main.js with shebang) bun run build -# Run the built binary -bun run ./dist/main.js - # The build command: # 1. Compiles TypeScript to CommonJS for Node.js compatibility # 2. Adds Node shebang for CLI execution @@ -67,6 +64,3 @@ Required for each provider: Special modes: - `PROXY_ONLY=true`: Run proxy server without spawning Claude Code -- `ANYCLAUDE_DEBUG=1|2`: Enable debug logging (1=basic, 2=verbose) - -- OpenAI's gpt-5 was released in August 2025 diff --git a/README.md b/README.md index f3cecc1..89745b3 100644 --- a/README.md +++ b/README.md @@ -23,22 +23,6 @@ $ anyclaude --model openai/gpt-5-mini Switch models in the Claude UI with `/model openai/gpt-5-mini`. -### GPT-5 Support - -Use --reasoning-effort (alias: -e) to control OpenAI reasoning.effort. Allowed values: minimal, low, medium, high. - -```sh -anyclaude --model openai/gpt-5-mini -e high -``` - -Use --service-tier (alias: -t) to control OpenAI service tier. Allowed values: flex, priority. - -```sh -anyclaude --model openai/gpt-5-mini -t priority -``` - -Note these flags may be extended to other providers in the future. - ## FAQ ### What providers are supported? diff --git a/bun.lock b/bun.lock index e9df3c0..e733246 100644 --- a/bun.lock +++ b/bun.lock @@ -3,10 +3,6 @@ "workspaces": { "": { "name": "openclaude", - "dependencies": { - "@types/yargs-parser": "^21.0.3", - "yargs-parser": "^22.0.0", - }, "devDependencies": { "@ai-sdk/anthropic": "^2.0.1", "@ai-sdk/azure": "^2.0.6", @@ -53,8 +49,6 @@ "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], - "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], - "ai": ["ai@5.0.8", "", { "dependencies": { "@ai-sdk/gateway": "1.0.4", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-qbnhj046UvG30V1S5WhjBn+RBGEAmi8PSZWqMhRsE3EPxvO5BcePXTZFA23e9MYyWS9zr4Vm8Mv3wQXwLmtIBw=="], "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], @@ -67,8 +61,6 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], diff --git a/package.json b/package.json index 28d2b46..2f8417a 100644 --- a/package.json +++ b/package.json @@ -31,12 +31,6 @@ "description": "Run Claude Code with OpenAI, Google, xAI, and others.", "license": "MIT", "scripts": { - "build": "bun build --target node --outfile dist/main.js ./src/main.ts --format cjs --external ai --external @ai-sdk/* --external zod && node -e \"const fs=require('fs');const p='dist/main.js';fs.writeFileSync(p,'#!/usr/bin/env node\\n'+fs.readFileSync(p,'utf8'))\" && chmod +x dist/main.js", - "test": "bun test", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@types/yargs-parser": "^21.0.3", - "yargs-parser": "^22.0.0" + "build": "bun build --target node --outfile dist/main.js ./src/main.ts --format cjs --external ai --external @ai-sdk/* --external zod && node -e \"const fs=require('fs');const p='dist/main.js';fs.writeFileSync(p,'#!/usr/bin/env node\\n'+fs.readFileSync(p,'utf8'))\" && chmod +x dist/main.js" } } diff --git a/src/anthropic-proxy.ts b/src/anthropic-proxy.ts index 36b312a..147e7b0 100644 --- a/src/anthropic-proxy.ts +++ b/src/anthropic-proxy.ts @@ -11,15 +11,6 @@ import { import { convertToAnthropicStream } from "./convert-to-anthropic-stream"; import { convertToLanguageModelMessage } from "./convert-to-language-model-prompt"; import { providerizeSchema } from "./json-schema"; -import { - writeDebugToTempFile, - logDebugError, - displayDebugStartup, - isDebugEnabled, - isVerboseDebugEnabled, - queueErrorMessage, - debug -} from "./debug"; export type CreateAnthropicProxyOptions = { providers: Record; @@ -34,9 +25,6 @@ export const createAnthropicProxy = ({ port, providers, }: CreateAnthropicProxyOptions): string => { - // Log debug status on startup - displayDebugStartup(); - const proxy = http .createServer((req, res) => { if (!req.url) { @@ -54,10 +42,6 @@ export const createAnthropicProxy = ({ const proxyToAnthropic = (body?: AnthropicMessagesRequest) => { delete req.headers["host"]; - const requestBody = body ? JSON.stringify(body) : null; - const chunks: Buffer[] = []; - const responseChunks: Buffer[] = []; - const proxy = https.request( { host: "api.anthropic.com", @@ -66,66 +50,17 @@ export const createAnthropicProxy = ({ headers: req.headers, }, (proxiedRes) => { - const statusCode = proxiedRes.statusCode ?? 500; - - // Collect response data for debugging - proxiedRes.on('data', (chunk) => { - responseChunks.push(chunk); - }); - - proxiedRes.on('end', () => { - // Write debug info to temp file for 4xx errors (except 429) - if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) { - const requestBodyToLog = requestBody - ? JSON.parse(requestBody) - : chunks.length > 0 - ? (() => { - try { - return JSON.parse(Buffer.concat(chunks).toString()); - } catch { - return Buffer.concat(chunks).toString(); - } - })() - : null; - - const responseBody = Buffer.concat(responseChunks).toString(); - const debugFile = writeDebugToTempFile( - statusCode, - { - method: req.method, - url: req.url, - headers: req.headers, - body: requestBodyToLog, - }, - { - statusCode, - headers: proxiedRes.headers, - body: responseBody, - } - ); - - if (debugFile) { - logDebugError("HTTP", statusCode, debugFile); - } - } - }); - - res.writeHead(statusCode, proxiedRes.headers); + res.writeHead(proxiedRes.statusCode ?? 500, proxiedRes.headers); proxiedRes.pipe(res, { end: true, }); } ); - - if (requestBody) { - proxy.end(requestBody); + if (body) { + proxy.end(JSON.stringify(body)); } else { - req.on('data', (chunk) => { - chunks.push(chunk); - proxy.write(chunk); - }); - req.on('end', () => { - proxy.end(); + req.pipe(proxy, { + end: true, }); } }; @@ -241,75 +176,14 @@ export const createAnthropicProxy = ({ ); }, onError: ({ error }) => { - let statusCode = 400; // Provider errors are returned as 400 - let transformedError = error; - - // Check if this is an OpenAI server error that we should transform - const isOpenAIServerError = providerName === 'openai' && - error && typeof error === 'object' && - 'error' in error && (error as any).error?.code === 'server_error'; - - if (isOpenAIServerError) { - debug(1, `OpenAI server error detected in onError for ${model}. Transforming to 429 to trigger retry...`); - // Transform to rate limit error to trigger retry - statusCode = 429; - transformedError = { - type: "error", - error: { - type: "rate_limit_error", - message: "OpenAI server temporarily unavailable. Please retry your request." - } - }; - } - - // Write comprehensive debug info to temp file - const debugFile = writeDebugToTempFile( - statusCode, - { - method: "POST", - url: req.url, - headers: req.headers, - body: body, - }, - { - statusCode, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - provider: providerName, - model: model, - originalError: error instanceof Error ? { - message: error.message, - stack: error.stack, - name: error.name, - } : error, - error: transformedError instanceof Error ? { - message: transformedError.message, - stack: transformedError.stack, - name: transformedError.name, - } : transformedError, - wasTransformed: isOpenAIServerError, - _debugInfo: { - requestSize: JSON.stringify(body).length, - toolCount: body.tools?.length || 0, - systemPromptLength: body.system?.reduce((acc, s) => acc + s.text.length, 0) || 0, - messageCount: body.messages.length - } - }), - } - ); - - if (debugFile) { - logDebugError("Provider", statusCode, debugFile, { provider: providerName, model }); - } - res - .writeHead(statusCode, { + .writeHead(400, { "Content-Type": "application/json", }) .end( JSON.stringify({ type: "error", - error: transformedError instanceof Error ? transformedError.message : transformedError, + error: error instanceof Error ? error.message : error, }) ); }, @@ -325,99 +199,14 @@ export const createAnthropicProxy = ({ // We already send the error to the client. }); - // Collect all stream chunks for debugging if enabled - const streamChunks: any[] = []; - const startTime = Date.now(); - await convertToAnthropicStream(stream.fullStream).pipeTo( new WritableStream({ write(chunk) { - // Collect chunks for debug dump (only in verbose mode to save memory) - if (isVerboseDebugEnabled()) { - streamChunks.push({ - timestamp: Date.now() - startTime, - chunk: chunk - }); - } - - // Check for streaming errors and log them (but don't interrupt the stream) - if (chunk.type === "error") { - // Store original error for debugging - const originalError = { ...chunk }; - - // Check if this is an OpenAI server error (any sequence) - const isOpenAIServerError = providerName === 'openai' && - (chunk as any).error?.code === 'server_error'; - - if (isOpenAIServerError) { - debug(1, `OpenAI server error detected for ${model} at sequence ${(chunk as any).sequence_number}. This is a known transient issue with OpenAI.`); - debug(1, `Transforming to 429 rate limit error to trigger Claude Code's automatic retry...`); - - // Transform OpenAI server errors to 429 rate limit errors - // This should trigger Claude Code's built-in retry mechanism - chunk = { - type: "error", - sequence_number: (chunk as any).sequence_number, - error: { - type: "rate_limit_error" as any, - code: "rate_limit_error", - message: "OpenAI server temporarily unavailable. Please retry your request.", - param: null - } - } as any; - } else { - // Log other errors normally - debug(1, `Streaming error chunk detected for ${providerName}/${model} at ${Date.now() - startTime}ms:`, chunk); - } - - // Write comprehensive debug info including full stream dump - const debugFile = writeDebugToTempFile( - 400, // Streaming errors are sent as 400 - { - method: "POST", - url: req.url, - headers: req.headers, - body: body, - }, - { - statusCode: 400, - headers: { "Content-Type": "text/event-stream" }, - body: JSON.stringify({ - provider: providerName, - model: model, - streamingError: originalError, - transformedError: isOpenAIServerError ? chunk : null, - wasTransformed: isOpenAIServerError, - fullChunk: JSON.stringify(originalError), - streamDuration: Date.now() - startTime, - streamChunkCount: streamChunks.length, - allStreamChunks: streamChunks, - _debugInfo: { - requestSize: JSON.stringify(body).length, - toolCount: body.tools?.length || 0, - systemPromptLength: body.system?.reduce((acc, s) => acc + s.text.length, 0) || 0, - messageCount: body.messages.length - } - }), - } - ); - - if (debugFile) { - logDebugError("Streaming", 400, debugFile, { provider: providerName, model }); - } else if (isDebugEnabled()) { - queueErrorMessage(`Failed to write debug file for streaming error`); - } - } - - // Write all chunks (including errors) to the stream - matching original behavior res.write( `event: ${chunk.type}\ndata: ${JSON.stringify(chunk)}\n\n` ); }, close() { - if (streamChunks.length > 0) { - debug(2, `Stream completed for ${providerName}/${model}: ${streamChunks.length} chunks in ${Date.now() - startTime}ms`); - } res.end(); }, }) diff --git a/src/convert-anthropic-messages.test.ts b/src/convert-anthropic-messages.test.ts deleted file mode 100644 index 47b98c1..0000000 --- a/src/convert-anthropic-messages.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { convertToAnthropicMessagesPrompt } from "./convert-anthropic-messages"; -import type { LanguageModelV2Prompt } from "@ai-sdk/provider"; - -describe("convertToAnthropicMessagesPrompt", () => { - describe("duplicate tool call filtering", () => { - it("should filter out duplicate tool calls with the same ID", () => { - const prompt: LanguageModelV2Prompt = [ - { - role: "assistant", - content: [ - { - type: "tool-call", - toolCallId: "call_123", - toolName: "TodoWrite", - input: { todos: ["item1", "item2"] }, - }, - { - type: "tool-call", - toolCallId: "call_123", // Duplicate ID - toolName: "TodoWrite", - input: {}, // Empty input - }, - ], - }, - ]; - - const result = convertToAnthropicMessagesPrompt({ - prompt, - sendReasoning: false, - warnings: [], - }); - - // Should only have one tool_use in the output - const assistantMessage = result.prompt.messages[0]; - expect(assistantMessage?.role).toBe("assistant"); - - if (assistantMessage?.role === "assistant") { - const toolUses = assistantMessage.content.filter( - (c) => c.type === "tool_use" - ); - expect(toolUses).toHaveLength(1); - expect(toolUses[0]?.id).toBe("call_123"); - expect(toolUses[0]?.name).toBe("TodoWrite"); - expect(toolUses[0]?.input).toEqual({ todos: ["item1", "item2"] }); - } - }); - - it("should keep tool calls with different IDs", () => { - const prompt: LanguageModelV2Prompt = [ - { - role: "assistant", - content: [ - { - type: "tool-call", - toolCallId: "call_123", - toolName: "TodoWrite", - input: { todos: ["item1"] }, - }, - { - type: "tool-call", - toolCallId: "call_456", // Different ID - toolName: "Read", - input: { file: "test.txt" }, - }, - ], - }, - ]; - - const result = convertToAnthropicMessagesPrompt({ - prompt, - sendReasoning: false, - warnings: [], - }); - - const assistantMessage = result.prompt.messages[0]; - expect(assistantMessage?.role).toBe("assistant"); - - if (assistantMessage?.role === "assistant") { - const toolUses = assistantMessage.content.filter( - (c) => c.type === "tool_use" - ); - expect(toolUses).toHaveLength(2); - expect(toolUses[0]?.id).toBe("call_123"); - expect(toolUses[0]?.name).toBe("TodoWrite"); - expect(toolUses[1]?.id).toBe("call_456"); - expect(toolUses[1]?.name).toBe("Read"); - } - }); - - it("should handle mixed content with duplicate tool calls", () => { - const prompt: LanguageModelV2Prompt = [ - { - role: "assistant", - content: [ - { - type: "text", - text: "Let me help you with that.", - }, - { - type: "tool-call", - toolCallId: "call_abc", - toolName: "Search", - input: { query: "test" }, - }, - { - type: "text", - text: "Processing...", - }, - { - type: "tool-call", - toolCallId: "call_abc", // Duplicate ID - toolName: "Search", - input: { query: "different" }, - }, - ], - }, - ]; - - const result = convertToAnthropicMessagesPrompt({ - prompt, - sendReasoning: false, - warnings: [], - }); - - const assistantMessage = result.prompt.messages[0]; - expect(assistantMessage?.role).toBe("assistant"); - - if (assistantMessage?.role === "assistant") { - // Should have 2 text blocks and 1 tool_use (duplicate filtered) - const textBlocks = assistantMessage.content.filter( - (c) => c.type === "text" - ); - const toolUses = assistantMessage.content.filter( - (c) => c.type === "tool_use" - ); - - expect(textBlocks).toHaveLength(2); - expect(toolUses).toHaveLength(1); - expect(toolUses[0]?.id).toBe("call_abc"); - expect(toolUses[0]?.input).toEqual({ query: "test" }); // Should keep the first one - } - }); - - it("should handle empty tool call arrays correctly", () => { - const prompt: LanguageModelV2Prompt = [ - { - role: "assistant", - content: [ - { - type: "text", - text: "No tools needed.", - }, - ], - }, - ]; - - const result = convertToAnthropicMessagesPrompt({ - prompt, - sendReasoning: false, - warnings: [], - }); - - const assistantMessage = result.prompt.messages[0]; - expect(assistantMessage?.role).toBe("assistant"); - - if (assistantMessage?.role === "assistant") { - const toolUses = assistantMessage.content.filter( - (c) => c.type === "tool_use" - ); - expect(toolUses).toHaveLength(0); - } - }); - }); -}); \ No newline at end of file diff --git a/src/convert-anthropic-messages.ts b/src/convert-anthropic-messages.ts index f0b69c8..a5d3339 100644 --- a/src/convert-anthropic-messages.ts +++ b/src/convert-anthropic-messages.ts @@ -255,21 +255,13 @@ export function convertToAnthropicMessagesPrompt({ } case "tool-call": { - // Check if we already have a tool call with this ID - const existingToolCall = anthropicContent.find( - (c) => c.type === "tool_use" && c.id === part.toolCallId - ); - - // Skip duplicate tool calls (OpenAI doesn't allow duplicate IDs) - if (!existingToolCall) { - anthropicContent.push({ - type: "tool_use", - id: part.toolCallId, - name: part.toolName, - input: part.input, - cache_control: cacheControl, - }); - } + anthropicContent.push({ + type: "tool_use", + id: part.toolCallId, + name: part.toolName, + input: part.input, + cache_control: cacheControl, + }); break; } } diff --git a/src/debug.ts b/src/debug.ts deleted file mode 100644 index 1cce71f..0000000 --- a/src/debug.ts +++ /dev/null @@ -1,186 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; - -// Store error messages to display later -let pendingErrorMessages: string[] = []; - -export interface DebugInfo { - statusCode: number; - request: { - method?: string; - url?: string; - headers: any; - body: any; - }; - response?: { - statusCode: number; - headers: any; - body: string; - }; -} - -/** - * Write debug info to a temp file when ANYCLAUDE_DEBUG is set - * @returns The path to the debug file, or null if not written - */ -export function writeDebugToTempFile( - statusCode: number, - request: DebugInfo["request"], - response?: DebugInfo["response"] -): string | null { - // Log 4xx errors (except 429) when ANYCLAUDE_DEBUG is set - const debugEnabled = process.env.ANYCLAUDE_DEBUG; - - if (!debugEnabled || statusCode === 429 || statusCode < 400 || statusCode >= 500) { - return null; - } - - try { - const tmpDir = os.tmpdir(); - const timestamp = Date.now(); - const randomId = Math.random().toString(36).substring(2, 8); - const filename = `anyclaude-debug-${timestamp}-${randomId}.json`; - const filepath = path.join(tmpDir, filename); - - const debugData = { - timestamp: new Date().toISOString(), - request: { - method: request.method, - url: request.url, - headers: request.headers, - body: request.body, - }, - response: response || null, - }; - - fs.writeFileSync(filepath, JSON.stringify(debugData, null, 2), 'utf8'); - - // Also write a simpler error log file that's easier to tail - const errorLogPath = path.join(tmpDir, 'anyclaude-errors.log'); - const errorMessage = `[${new Date().toISOString()}] HTTP ${statusCode} - Debug: ${filepath}\n`; - fs.appendFileSync(errorLogPath, errorMessage, 'utf8'); - - return filepath; - } catch (error) { - console.error("[ANYCLAUDE DEBUG] Failed to write debug file:", error); - return null; - } -} - -/** - * Queue an error message to be displayed later - */ -export function queueErrorMessage(message: string): void { - pendingErrorMessages.push(message); - // Display after a short delay to avoid being overwritten - setTimeout(displayPendingErrors, 500); -} - -/** - * Display pending error messages to stderr with formatting - */ -function displayPendingErrors(): void { - if (pendingErrorMessages.length > 0) { - // Use stderr and add newlines to separate from Claude's output - process.stderr.write('\n\n═══════════════════════════════════════\n'); - process.stderr.write('ANYCLAUDE DEBUG - Errors detected:\n'); - process.stderr.write('═══════════════════════════════════════\n'); - pendingErrorMessages.forEach(msg => { - process.stderr.write(msg + '\n'); - }); - process.stderr.write('═══════════════════════════════════════\n\n'); - pendingErrorMessages = []; - } -} - -/** - * Log a debug error and queue it for display - */ -export function logDebugError( - type: "HTTP" | "Provider" | "Streaming", - statusCode: number, - debugFile: string | null, - context?: { provider?: string; model?: string } -): void { - if (!debugFile) return; - - let message = `${type} error`; - if (context?.provider && context?.model) { - message += ` (${context.provider}/${context.model})`; - } else if (statusCode) { - message += ` ${statusCode}`; - } - message += ` - Debug info written to: ${debugFile}`; - - queueErrorMessage(message); -} - -/** - * Display debug mode startup message - */ -export function displayDebugStartup(): void { - const level = getDebugLevel(); - if (level > 0) { - const tmpDir = os.tmpdir(); - const errorLogPath = path.join(tmpDir, 'anyclaude-errors.log'); - process.stderr.write('\n═══════════════════════════════════════\n'); - process.stderr.write(`ANYCLAUDE DEBUG MODE ENABLED (Level ${level})\n`); - process.stderr.write(`Error log: ${errorLogPath}\n`); - process.stderr.write(`Debug files: ${tmpDir}/anyclaude-debug-*.json\n`); - if (level >= 2) { - process.stderr.write('Verbose: Duplicate filtering details enabled\n'); - } - process.stderr.write('═══════════════════════════════════════\n\n'); - } -} - -/** - * Check if debug mode is enabled - */ -/** - * Get the debug level from ANYCLAUDE_DEBUG environment variable - * Returns 0 if not set, 1 for basic debug, 2 for verbose debug - * Defaults to 1 if unrecognized string is passed - */ -export function getDebugLevel(): number { - const debugValue = process.env.ANYCLAUDE_DEBUG; - if (!debugValue) return 0; - - const level = parseInt(debugValue, 10); - if (isNaN(level)) return 1; // Default to level 1 for any non-numeric value - - return Math.max(0, Math.min(2, level)); // Clamp to 0-2 range -} - -export function isDebugEnabled(): boolean { - return getDebugLevel() > 0; -} - -/** - * Check if verbose debug mode (level 2) is enabled - */ -export function isVerboseDebugEnabled(): boolean { - return getDebugLevel() >= 2; -} - -/** - * Log a debug message at the specified level - * @param level - Minimum debug level required to show this message (1 or 2) - * @param message - The message to log - * @param data - Optional data to append to the message - */ -export function debug(level: 1 | 2, message: string, data?: any): void { - if (getDebugLevel() >= level) { - const prefix = '[ANYCLAUDE DEBUG]'; - if (data !== undefined) { - // For objects/errors, stringify with a length limit - const dataStr = typeof data === 'object' ? - JSON.stringify(data).substring(0, 200) : - String(data); - console.error(`${prefix} ${message}`, dataStr); - } else { - console.error(`${prefix} ${message}`); - } - } -} \ No newline at end of file diff --git a/src/json-schema.ts b/src/json-schema.ts index 0187a4c..db65d8d 100644 --- a/src/json-schema.ts +++ b/src/json-schema.ts @@ -67,18 +67,8 @@ export function providerizeSchema( // Only add required properties for OpenAI if (provider === "openai") { - // Preserve existing required fields if they exist, otherwise don't mark any as required - // This prevents marking optional fields as required which causes OpenAI validation errors - if (schema.required && Array.isArray(schema.required)) { - result.required = schema.required; - } - // Only set additionalProperties to false if it's not already defined - // This preserves the original schema's intent - if (schema.additionalProperties === undefined) { - result.additionalProperties = false; - } else { - result.additionalProperties = schema.additionalProperties; - } + result.required = Object.keys(schema.properties); + result.additionalProperties = false; } return result; diff --git a/src/main.ts b/src/main.ts index 8082947..5bf2456 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,93 +10,6 @@ import { createAnthropicProxy, type CreateAnthropicProxyOptions, } from "./anthropic-proxy"; -import yargsParser from "yargs-parser"; - -const FLAGS = { - reasoningEffort: { - long: "reasoning-effort", - short: "e", - values: ["minimal", "low", "medium", "high"] as const, - }, - serviceTier: { - long: "service-tier", - short: "t", - values: ["flex", "priority"] as const, - }, -} as const; - -function parseAnyclaudeFlags(rawArgs: string[]) { - const parsed = yargsParser(rawArgs, { - configuration: { - "unknown-options-as-args": false, - "halt-at-non-option": false, - "camel-case-expansion": false, - "dot-notation": false, - }, - }); - const reasoningEffort = (parsed[FLAGS.reasoningEffort.long] ?? - parsed[FLAGS.reasoningEffort.short]) as string | undefined; - const serviceTier = (parsed[FLAGS.serviceTier.long] ?? - parsed[FLAGS.serviceTier.short]) as string | undefined; - const specs = Object.values(FLAGS); - const filteredArgs: string[] = []; - let helpRequested = false; - let i = 0; - let passthrough = false; - while (i < rawArgs.length) { - const arg = rawArgs[i]!; - if (passthrough) { - filteredArgs.push(arg); - i++; - continue; - } - if (arg === "--") { - passthrough = true; - filteredArgs.push(arg); - i++; - continue; - } - if (arg === "-h" || arg === "--help") helpRequested = true; - let matched = false; - for (const spec of specs) { - const long = `--${spec.long}`; - const short = `-${spec.short}`; - if (arg === long || arg === short) { - i += 2; - matched = true; - break; - } - if (arg.startsWith(long + "=") || arg.startsWith(short + "=")) { - i += 1; - matched = true; - break; - } - } - if (matched) continue; - filteredArgs.push(arg); - i++; - } - return { reasoningEffort, serviceTier, filteredArgs, helpRequested }; -} - -const rawArgs = process.argv.slice(2); -const { reasoningEffort, serviceTier, filteredArgs, helpRequested } = - parseAnyclaudeFlags(rawArgs); - -for (const [key, spec] of Object.entries(FLAGS) as Array< - [keyof typeof FLAGS, (typeof FLAGS)[keyof typeof FLAGS]] ->) { - const val = (key === "reasoningEffort" ? reasoningEffort : serviceTier) as - | string - | undefined; - if (val) { - const allowed = new Set(spec.values as readonly string[]); - if (!allowed.has(val as any)) { - console.error(`Invalid ${spec.long}. Use ${spec.values.join("|")}.`); - process.exit(1); - } - } -} // providers are supported providers to proxy requests by name. // Model names are split when requested by `/`. The provider @@ -110,10 +23,7 @@ const providers: CreateAnthropicProxyOptions["providers"] = { const body = JSON.parse(init.body); const maxTokens = body.max_tokens; delete body["max_tokens"]; - if (typeof maxTokens !== "undefined") - body.max_completion_tokens = maxTokens; - if (reasoningEffort) body.reasoning = { effort: reasoningEffort }; - if (serviceTier) body.service_tier = serviceTier; + body.max_completion_tokens = maxTokens; init.body = JSON.stringify(body); } return globalThis.fetch(url, init); @@ -146,22 +56,10 @@ const proxyURL = createAnthropicProxy({ providers, }); -const params = [ - `proxy=${proxyURL}`, - ...( - Object.entries({ reasoningEffort, serviceTier }) as Array< - [keyof typeof FLAGS, string | undefined] - > - ).map(([k, v]) => (v ? `${FLAGS[k].long}=${v}` : undefined)), -] - .filter(Boolean) - .join(" "); -console.log(`[anyclaude] ${params}`); - if (process.env.PROXY_ONLY === "true") { - console.log("Proxy only mode: " + proxyURL); + console.log("Proxy only mode: "+proxyURL); } else { - const claudeArgs = filteredArgs; + const claudeArgs = process.argv.slice(2); const proc = spawn("claude", claudeArgs, { env: { ...process.env, @@ -170,15 +68,12 @@ if (process.env.PROXY_ONLY === "true") { stdio: "inherit", }); proc.on("exit", (code) => { - if (helpRequested) { - console.log("\nanyclaude flags:"); - console.log(" --model / e.g. openai/gpt-5"); - for (const spec of Object.values(FLAGS)) { - const vals = spec.values.join("|"); - console.log(` --${spec.long}, -${spec.short} <${vals}>`); - } + if (claudeArgs[0] === "-h" || claudeArgs[0] === "--help") { + console.log("\nCustom Models:") + console.log(" --model / e.g. openai/o3"); } process.exit(code); }); } +