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:
parent
43894b0ac9
commit
f42e03937e
11 changed files with 802 additions and 24 deletions
47
.github/workflows/ci.yml
vendored
Normal file
47
.github/workflows/ci.yml
vendored
Normal 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"
|
||||||
|
|
@ -34,6 +34,9 @@ bun install
|
||||||
# Build the project (creates dist/main.js with shebang)
|
# Build the project (creates dist/main.js with shebang)
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
|
# Run the built binary
|
||||||
|
bun run ./dist/main.js
|
||||||
|
|
||||||
# The build command:
|
# The build command:
|
||||||
# 1. Compiles TypeScript to CommonJS for Node.js compatibility
|
# 1. Compiles TypeScript to CommonJS for Node.js compatibility
|
||||||
# 2. Adds Node shebang for CLI execution
|
# 2. Adds Node shebang for CLI execution
|
||||||
|
|
@ -64,3 +67,6 @@ Required for each provider:
|
||||||
Special modes:
|
Special modes:
|
||||||
|
|
||||||
- `PROXY_ONLY=true`: Run proxy server without spawning Claude Code
|
- `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
|
||||||
|
|
|
||||||
16
README.md
16
README.md
|
|
@ -23,6 +23,22 @@ $ anyclaude --model openai/gpt-5-mini
|
||||||
|
|
||||||
Switch models in the Claude UI with `/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
|
## FAQ
|
||||||
|
|
||||||
### What providers are supported?
|
### What providers are supported?
|
||||||
|
|
|
||||||
8
bun.lock
8
bun.lock
|
|
@ -3,6 +3,10 @@
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "openclaude",
|
"name": "openclaude",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/yargs-parser": "^21.0.3",
|
||||||
|
"yargs-parser": "^22.0.0",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.1",
|
"@ai-sdk/anthropic": "^2.0.1",
|
||||||
"@ai-sdk/azure": "^2.0.6",
|
"@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/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=="],
|
"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=="],
|
"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=="],
|
"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": ["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.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@
|
||||||
"description": "Run Claude Code with OpenAI, Google, xAI, and others.",
|
"description": "Run Claude Code with OpenAI, Google, xAI, and others.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,15 @@ import {
|
||||||
import { convertToAnthropicStream } from "./convert-to-anthropic-stream";
|
import { convertToAnthropicStream } from "./convert-to-anthropic-stream";
|
||||||
import { convertToLanguageModelMessage } from "./convert-to-language-model-prompt";
|
import { convertToLanguageModelMessage } from "./convert-to-language-model-prompt";
|
||||||
import { providerizeSchema } from "./json-schema";
|
import { providerizeSchema } from "./json-schema";
|
||||||
|
import {
|
||||||
|
writeDebugToTempFile,
|
||||||
|
logDebugError,
|
||||||
|
displayDebugStartup,
|
||||||
|
isDebugEnabled,
|
||||||
|
isVerboseDebugEnabled,
|
||||||
|
queueErrorMessage,
|
||||||
|
debug
|
||||||
|
} from "./debug";
|
||||||
|
|
||||||
export type CreateAnthropicProxyOptions = {
|
export type CreateAnthropicProxyOptions = {
|
||||||
providers: Record<string, ProviderV2>;
|
providers: Record<string, ProviderV2>;
|
||||||
|
|
@ -25,6 +34,9 @@ export const createAnthropicProxy = ({
|
||||||
port,
|
port,
|
||||||
providers,
|
providers,
|
||||||
}: CreateAnthropicProxyOptions): string => {
|
}: CreateAnthropicProxyOptions): string => {
|
||||||
|
// Log debug status on startup
|
||||||
|
displayDebugStartup();
|
||||||
|
|
||||||
const proxy = http
|
const proxy = http
|
||||||
.createServer((req, res) => {
|
.createServer((req, res) => {
|
||||||
if (!req.url) {
|
if (!req.url) {
|
||||||
|
|
@ -42,6 +54,10 @@ export const createAnthropicProxy = ({
|
||||||
const proxyToAnthropic = (body?: AnthropicMessagesRequest) => {
|
const proxyToAnthropic = (body?: AnthropicMessagesRequest) => {
|
||||||
delete req.headers["host"];
|
delete req.headers["host"];
|
||||||
|
|
||||||
|
const requestBody = body ? JSON.stringify(body) : null;
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
const responseChunks: Buffer[] = [];
|
||||||
|
|
||||||
const proxy = https.request(
|
const proxy = https.request(
|
||||||
{
|
{
|
||||||
host: "api.anthropic.com",
|
host: "api.anthropic.com",
|
||||||
|
|
@ -50,17 +66,66 @@ export const createAnthropicProxy = ({
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
},
|
},
|
||||||
(proxiedRes) => {
|
(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, {
|
proxiedRes.pipe(res, {
|
||||||
end: true,
|
end: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (body) {
|
|
||||||
proxy.end(JSON.stringify(body));
|
if (requestBody) {
|
||||||
|
proxy.end(requestBody);
|
||||||
} else {
|
} else {
|
||||||
req.pipe(proxy, {
|
req.on('data', (chunk) => {
|
||||||
end: true,
|
chunks.push(chunk);
|
||||||
|
proxy.write(chunk);
|
||||||
|
});
|
||||||
|
req.on('end', () => {
|
||||||
|
proxy.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -176,14 +241,75 @@ export const createAnthropicProxy = ({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onError: ({ error }) => {
|
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
|
res
|
||||||
.writeHead(400, {
|
.writeHead(statusCode, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
})
|
})
|
||||||
.end(
|
.end(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "error",
|
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.
|
// 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(
|
await convertToAnthropicStream(stream.fullStream).pipeTo(
|
||||||
new WritableStream({
|
new WritableStream({
|
||||||
write(chunk) {
|
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(
|
res.write(
|
||||||
`event: ${chunk.type}\ndata: ${JSON.stringify(chunk)}\n\n`
|
`event: ${chunk.type}\ndata: ${JSON.stringify(chunk)}\n\n`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
|
if (streamChunks.length > 0) {
|
||||||
|
debug(2, `Stream completed for ${providerName}/${model}: ${streamChunks.length} chunks in ${Date.now() - startTime}ms`);
|
||||||
|
}
|
||||||
res.end();
|
res.end();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
175
src/convert-anthropic-messages.test.ts
Normal file
175
src/convert-anthropic-messages.test.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -255,13 +255,21 @@ export function convertToAnthropicMessagesPrompt({
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
anthropicContent.push({
|
// Check if we already have a tool call with this ID
|
||||||
type: "tool_use",
|
const existingToolCall = anthropicContent.find(
|
||||||
id: part.toolCallId,
|
(c) => c.type === "tool_use" && c.id === part.toolCallId
|
||||||
name: part.toolName,
|
);
|
||||||
input: part.input,
|
|
||||||
cache_control: cacheControl,
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
186
src/debug.ts
Normal file
186
src/debug.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,8 +67,18 @@ export function providerizeSchema(
|
||||||
|
|
||||||
// Only add required properties for OpenAI
|
// Only add required properties for OpenAI
|
||||||
if (provider === "openai") {
|
if (provider === "openai") {
|
||||||
result.required = Object.keys(schema.properties);
|
// Preserve existing required fields if they exist, otherwise don't mark any as required
|
||||||
result.additionalProperties = false;
|
// 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;
|
return result;
|
||||||
|
|
|
||||||
119
src/main.ts
119
src/main.ts
|
|
@ -10,6 +10,93 @@ import {
|
||||||
createAnthropicProxy,
|
createAnthropicProxy,
|
||||||
type CreateAnthropicProxyOptions,
|
type CreateAnthropicProxyOptions,
|
||||||
} from "./anthropic-proxy";
|
} 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.
|
// providers are supported providers to proxy requests by name.
|
||||||
// Model names are split when requested by `/`. The provider
|
// Model names are split when requested by `/`. The provider
|
||||||
|
|
@ -23,7 +110,10 @@ const providers: CreateAnthropicProxyOptions["providers"] = {
|
||||||
const body = JSON.parse(init.body);
|
const body = JSON.parse(init.body);
|
||||||
const maxTokens = body.max_tokens;
|
const maxTokens = body.max_tokens;
|
||||||
delete 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);
|
init.body = JSON.stringify(body);
|
||||||
}
|
}
|
||||||
return globalThis.fetch(url, init);
|
return globalThis.fetch(url, init);
|
||||||
|
|
@ -56,10 +146,22 @@ const proxyURL = createAnthropicProxy({
|
||||||
providers,
|
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") {
|
if (process.env.PROXY_ONLY === "true") {
|
||||||
console.log("Proxy only mode: "+proxyURL);
|
console.log("Proxy only mode: " + proxyURL);
|
||||||
} else {
|
} else {
|
||||||
const claudeArgs = process.argv.slice(2);
|
const claudeArgs = filteredArgs;
|
||||||
const proc = spawn("claude", claudeArgs, {
|
const proc = spawn("claude", claudeArgs, {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|
@ -68,12 +170,15 @@ if (process.env.PROXY_ONLY === "true") {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
});
|
});
|
||||||
proc.on("exit", (code) => {
|
proc.on("exit", (code) => {
|
||||||
if (claudeArgs[0] === "-h" || claudeArgs[0] === "--help") {
|
if (helpRequested) {
|
||||||
console.log("\nCustom Models:")
|
console.log("\nanyclaude flags:");
|
||||||
console.log(" --model <provider>/<model> e.g. openai/o3");
|
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);
|
process.exit(code);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue