Enable GPT-5 support (again)

This commit is contained in:
Ammar Bandukwala 2025-08-11 12:45:36 -05:00 • committed by GitHub
commit 8962b98d51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 840 additions and 42 deletions

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

@ -0,0 +1,58 @@
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 -f dist/main.cjs
test -x dist/main.js
head -n 1 dist/main.js | grep -q "#!/usr/bin/env node"
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Test with Node.js
run: |
# Test that the build works with --help
node dist/main.js --help
# Verify it shows Claude Code help output
node dist/main.js --help | grep -q "Usage: claude"

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

@ -1,6 +1,6 @@
# anyclaude
![NPM Version](https://img.shields.io/npm/v/anyclaude)
[![NPM Version](https://img.shields.io/npm/v/anyclaude)](https://www.npmjs.com/package/anyclaude)
Use Claude Code with OpenAI, Google, xAI, and other providers.
@ -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

@ -2,7 +2,11 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "openclaude",
"name": "anyclaude",
"dependencies": {
"json-schema": "^0.4.0",
"yargs-parser": "^22.0.0",
},
"devDependencies": {
"@ai-sdk/anthropic": "^2.0.1",
"@ai-sdk/azure": "^2.0.6",
@ -11,8 +15,8 @@
"@ai-sdk/xai": "^2.0.3",
"@types/bun": "latest",
"@types/json-schema": "^7.0.15",
"@types/yargs-parser": "^21.0.3",
"ai": "^5.0.8",
"json-schema": "^0.4.0",
"zod": "3.25.76",
},
"peerDependencies": {
@ -23,46 +27,54 @@
"packages": {
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-HtNbpNV9qXQosHu00+CBMEcdTerwZY+kpVMNak0xP/P5TF6XkPf7IyizhLuc7y5zcXMjZCMA7jDGkcEdZCEdkw=="],
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.6", "", { "dependencies": { "@ai-sdk/openai": "2.0.6", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-c8JbroE7B9Pyx3D8jxEwXop++vQqeR58adVrzPY7m29b/jZY7bsn6SuMheUF324sHbNJGd1DplqibIUzOs2qbA=="],
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.9", "", { "dependencies": { "@ai-sdk/openai": "2.0.9", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-cUOTwxJ58MwQhU4qtmv/nEozGzhOJKUIRwcFx3NvdHw27sH10Dvt7MsrmSnO2CD6WRH7rxwFqxd4SlfRDIJybA=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1roLdgMbFU3Nr4MC97/te7w6OqxsWBkDUkpbCcvxF3jz/ku91WVaJldn/PKU8feMKNyI5W9wnqhbjb1BqbExOQ=="],
"@ai-sdk/google": ["@ai-sdk/google@2.0.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-rQkY4nQGZnQw6pfftqsGsXXAqkGnl1gYjSbvD8+zyrVg2KAYeJU0NxIaacLWCORNcW+yQ/cqTpK/Uku/SIdEqA=="],
"@ai-sdk/google": ["@ai-sdk/google@2.0.4", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-d8CF3uzabVqxPwcuXLVv5OIq55bM5oKKNNMQacYQMEv3I9W6EYYYeaM9Buo+/yi1IdKsRIPsa9LQO/H9S9x8yQ=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.6", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-YmnhiyqllxnGo0Jo23jfi7NCOK+8BhUPPV/Cm/8MGFgI9oX/s6xGIp6KqSK2GFlAz2vxyy+aAlpt0cv/a0SxYQ=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.9", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-KLWQ4LWPgKH7V3KtbZ4n8pKkTyuujj+qLwphU5fg6d2WP86OQRJGMTi11UqALZ5LTABUVp3qH62CGGXzjnJV3w=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-SE49TeOGppPtP4GTOXAl7WOhXXx42/VKpei3pNozjrD8NTkLX29Mib5t06VB5leaMjHNmyJMLECiWbWQV5W1ug=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-4eXN6m1x6gs+PJEqjCi2G5RKAgydCkH1mMqDngc3Cz11lojehGdthQfr696jCT/qQY1UZDOu2PVFWcYPgTFV2A=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="],
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.3", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.3", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-9Hsim5y4KJa+6DQBvoKcCp6ooWvfdbn1quOCEgSAnni6OJBL6QuImOcDmRr/YE6hrYo2RgLF56MtI2OyQqbh5w=="],
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.5", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.5", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-YqhReQdh5dbCrNpg4xKVVaZe3LlUZ+c0n/RhgjC2X7AfKfJlGR+yfHsT9LJsbu3PhSzOQyGWeAvGqrCJSMetqQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
"@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"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=="],
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
"bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"ai": ["ai@5.0.9", "", { "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-l7YdzsCTId7CDNNQ+roC0m4uDMmVIO9NpDC3bI2RxNDss3XpGIvsbHq7bT+IZzMYIqGbHK17HI0eiOOvtwzN6Q=="],
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"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=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
}
}

View file

@ -13,16 +13,20 @@
"bin": {
"anyclaude": "./dist/main.js"
},
"files": [
"dist/main.js",
"dist/main.cjs"
],
"devDependencies": {
"@types/bun": "latest",
"@types/json-schema": "^7.0.15",
"@types/yargs-parser": "^21.0.3",
"@ai-sdk/anthropic": "^2.0.1",
"@ai-sdk/azure": "^2.0.6",
"@ai-sdk/google": "^2.0.3",
"@ai-sdk/openai": "^2.0.6",
"@ai-sdk/xai": "^2.0.3",
"@types/bun": "latest",
"@types/json-schema": "^7.0.15",
"ai": "^5.0.8",
"json-schema": "^0.4.0",
"zod": "3.25.76"
},
"peerDependencies": {
@ -31,6 +35,13 @@
"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 --format cjs --outfile dist/main.cjs ./src/main.ts && node -e \"const fs=require('fs');const f='dist/main.cjs';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace(/import\\.meta\\.url/g,'__filename'))\" && printf '#!/usr/bin/env node\\nrequire(\"./main.cjs\");\\n' > dist/main.js && chmod +x dist/main.js",
"test": "bun test",
"typecheck": "tsc --noEmit",
"install:global": "bun run build && npm pack --silent && npm install -g anyclaude-*.tgz"
},
"dependencies": {
"yargs-parser": "^22.0.0",
"json-schema": "^0.4.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);
});
}