OpenAI GPT-5 improvements (#9)

A series of fixes for making GPT-5 reliable and effective with anyclaude.

## UX changes

- Converts opaque OpenAI service errors into 429s so CC natively retries instead of failing. These are very common.
- Rich debugging support via `ANYCLAUDE_DEBUG`
- Supports specifying reasoning effort and service tier 

## Codebase

- Added CI

## Remaining issues

- GPT-5 often fails to use the native tool calls. These failures seems intermittent.
This commit is contained in:
Ammar Bandukwala 2025-08-10 15:50:08 -05:00 committed by GitHub
commit f42e03937e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 802 additions and 24 deletions

47
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,47 @@
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"

View file

@ -34,6 +34,9 @@ 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
@ -64,3 +67,6 @@ 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

View file

@ -23,6 +23,22 @@ $ 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?

View file

@ -3,6 +3,10 @@
"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",
@ -49,6 +53,8 @@
"@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=="],
@ -61,6 +67,8 @@
"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=="],

View file

@ -31,6 +31,12 @@
"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"
"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"
}
}

View file

@ -11,6 +11,15 @@ 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<string, ProviderV2>;
@ -25,6 +34,9 @@ export const createAnthropicProxy = ({
port,
providers,
}: CreateAnthropicProxyOptions): string => {
// Log debug status on startup
displayDebugStartup();
const proxy = http
.createServer((req, res) => {
if (!req.url) {
@ -42,6 +54,10 @@ 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",
@ -50,17 +66,66 @@ export const createAnthropicProxy = ({
headers: req.headers,
},
(proxiedRes) => {
res.writeHead(proxiedRes.statusCode ?? 500, proxiedRes.headers);
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);
proxiedRes.pipe(res, {
end: true,
});
}
);
if (body) {
proxy.end(JSON.stringify(body));
if (requestBody) {
proxy.end(requestBody);
} else {
req.pipe(proxy, {
end: true,
req.on('data', (chunk) => {
chunks.push(chunk);
proxy.write(chunk);
});
req.on('end', () => {
proxy.end();
});
}
};
@ -176,14 +241,75 @@ 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(400, {
.writeHead(statusCode, {
"Content-Type": "application/json",
})
.end(
JSON.stringify({
type: "error",
error: error instanceof Error ? error.message : error,
error: transformedError instanceof Error ? transformedError.message : transformedError,
})
);
},
@ -199,14 +325,99 @@ 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();
},
})

View file

@ -0,0 +1,175 @@
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);
}
});
});
});

View file

@ -255,13 +255,21 @@ export function convertToAnthropicMessagesPrompt({
}
case "tool-call": {
anthropicContent.push({
type: "tool_use",
id: part.toolCallId,
name: part.toolName,
input: part.input,
cache_control: cacheControl,
});
// 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,
});
}
break;
}
}

186
src/debug.ts Normal file
View file

@ -0,0 +1,186 @@
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}`);
}
}
}

View file

@ -67,8 +67,18 @@ export function providerizeSchema(
// Only add required properties for OpenAI
if (provider === "openai") {
result.required = Object.keys(schema.properties);
result.additionalProperties = false;
// 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;
}
}
return result;

View file

@ -10,6 +10,93 @@ 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
@ -23,7 +110,10 @@ const providers: CreateAnthropicProxyOptions["providers"] = {
const body = JSON.parse(init.body);
const maxTokens = body.max_tokens;
delete body["max_tokens"];
body.max_completion_tokens = maxTokens;
if (typeof maxTokens !== "undefined")
body.max_completion_tokens = maxTokens;
if (reasoningEffort) body.reasoning = { effort: reasoningEffort };
if (serviceTier) body.service_tier = serviceTier;
init.body = JSON.stringify(body);
}
return globalThis.fetch(url, init);
@ -56,10 +146,22 @@ 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 = process.argv.slice(2);
const claudeArgs = filteredArgs;
const proc = spawn("claude", claudeArgs, {
env: {
...process.env,
@ -68,12 +170,15 @@ if (process.env.PROXY_ONLY === "true") {
stdio: "inherit",
});
proc.on("exit", (code) => {
if (claudeArgs[0] === "-h" || claudeArgs[0] === "--help") {
console.log("\nCustom Models:")
console.log(" --model <provider>/<model> e.g. openai/o3");
if (helpRequested) {
console.log("\nanyclaude flags:");
console.log(" --model <provider>/<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}>`);
}
}
process.exit(code);
});
}