GPT-5 Improvements (#12)

- Fixed major bug where tool descriptions were never sent to the LLM (!!)
- A large variety of making server errors recoverable
- Thinking traces now appear for OpenAI models
This commit is contained in:
Ammar Bandukwala 2025-08-11 21:04:41 -05:00 • committed by GitHub
commit d48232522b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 716 additions and 424 deletions

View file

@ -1,6 +1,7 @@
# Repository Guidelines # Repository Guidelines
## Project Structure & Module Organization ## Project Structure & Module Organization
- `src/`: TypeScript sources. Key files: `main.ts` (CLI entry), `anthropic-proxy.ts` (HTTP proxy), `convert-*.ts` (format converters), `detect-mimetype.ts`, `json-schema.ts`. - `src/`: TypeScript sources. Key files: `main.ts` (CLI entry), `anthropic-proxy.ts` (HTTP proxy), `convert-*.ts` (format converters), `detect-mimetype.ts`, `json-schema.ts`.
- `dist/`: Bundled CLI entry `main.js` (created by build). - `dist/`: Bundled CLI entry `main.js` (created by build).
- `package.json`, `bun.lock`: Bun-based build and deps; `bin.anyclaude` points to `dist/main.js`. - `package.json`, `bun.lock`: Bun-based build and deps; `bin.anyclaude` points to `dist/main.js`.
@ -8,6 +9,7 @@
- Assets/config: `README.md`, `CLAUDE.md`, `tsconfig.json`, `demo.png`. - Assets/config: `README.md`, `CLAUDE.md`, `tsconfig.json`, `demo.png`.
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Install: `bun install`. - Install: `bun install`.
- Build: `bun run build` (outputs `dist/main.js` with Node shebang). - Build: `bun run build` (outputs `dist/main.js` with Node shebang).
- Run CLI (after build): `./dist/main.js --model openai/gpt-5-mini`. - Run CLI (after build): `./dist/main.js --model openai/gpt-5-mini`.
@ -17,6 +19,7 @@
- Nix shell: `direnv allow` (or `nix develop`); format Nix/shell files with `nix fmt`. - Nix shell: `direnv allow` (or `nix develop`); format Nix/shell files with `nix fmt`.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
- Language: TypeScript (ESNext, strict mode enabled). - Language: TypeScript (ESNext, strict mode enabled).
- Indentation: 2 spaces; keep lines reasonable; use explicit imports (`verbatimModuleSyntax`). - Indentation: 2 spaces; keep lines reasonable; use explicit imports (`verbatimModuleSyntax`).
- Files: kebab-case `.ts` (e.g., `convert-to-anthropic-stream.ts`). - Files: kebab-case `.ts` (e.g., `convert-to-anthropic-stream.ts`).
@ -24,16 +27,19 @@
- Exports: prefer named exports; keep modules single‑purpose and small. - Exports: prefer named exports; keep modules single‑purpose and small.
## Testing Guidelines ## Testing Guidelines
- No test runner is configured yet. If adding tests: - No test runner is configured yet. If adding tests:
- Place under `src/**/*.test.ts` or `src/__tests__/`. - Place under `src/**/*.test.ts` or `src/__tests__/`.
- Prioritize pure units (converters, schema, MIME detection). Avoid live provider calls by default; gate with env vars. - Prioritize pure units (converters, schema, MIME detection). Avoid live provider calls by default; gate with env vars.
- Add a `test` script in `package.json` and document how to run it in `README.md`. - Add a `test` script in `package.json` and document how to run it in `README.md`.
## Commit & Pull Request Guidelines ## Commit & Pull Request Guidelines
- Commits: imperative, concise subjects (e.g., "Add Nix development environment and Claude guidance file"). Include rationale in the body when helpful. - Commits: imperative, concise subjects (e.g., "Add Nix development environment and Claude guidance file"). Include rationale in the body when helpful.
- PRs: clear description, linked issues, commands used to verify (with relevant env vars), and expected behavior. Avoid committing secrets; scrub logs. - PRs: clear description, linked issues, commands used to verify (with relevant env vars), and expected behavior. Avoid committing secrets; scrub logs.
- Keep scope small; update `README.md`/`CLAUDE.md` when behavior or env vars change. - Keep scope small; update `README.md`/`CLAUDE.md` when behavior or env vars change.
## Security & Configuration Tips ## Security & Configuration Tips
- Never commit API keys. Use `direnv` for local secrets and keep `.envrc` minimal. - Never commit API keys. Use `direnv` for local secrets and keep `.envrc` minimal.
- Provider envs: `OPENAI_*`, `GOOGLE_*`, `XAI_*`, `AZURE_*`, optional `ANTHROPIC_*`. Use `PROXY_ONLY=true` to inspect the proxy without launching Claude. - Provider envs: `OPENAI_*`, `GOOGLE_*`, `XAI_*`, `AZURE_*`, optional `ANTHROPIC_*`. Use `PROXY_ONLY=true` to inspect the proxy without launching Claude.

View file

@ -7,6 +7,7 @@ Use Claude Code with OpenAI, Google, xAI, and other providers.
- Extremely simple setup - just a basic command wrapper - Extremely simple setup - just a basic command wrapper
- Uses the AI SDK for simple support of new providers - Uses the AI SDK for simple support of new providers
- Works with Claude Code GitHub Actions - Works with Claude Code GitHub Actions
- Optimized for OpenAI's gpt-5 series
<img src="./demo.png" width="65%"> <img src="./demo.png" width="65%">
@ -51,14 +52,10 @@ See [the providers](./src/main.ts#L17) for the implementation.
Set a custom OpenAI endpoint with `OPENAI_API_URL` to use OpenRouter Set a custom OpenAI endpoint with `OPENAI_API_URL` to use OpenRouter
`ANTHROPIC_MODEL` and `ANTHROPIC_SMALL_MODEL` are supported with the `<provider>/` syntax.
### How does this work? ### How does this work?
Claude Code has added support for customizing the Anthropic endpoint with `ANTHROPIC_BASE_URL`. Claude Code has added support for customizing the Anthropic endpoint with `ANTHROPIC_BASE_URL`.
anyclaude spawns a simple HTTP server that translates between Anthropic's format and the [AI SDK](https://github.com/vercel/ai) format, enabling support for any [AI SDK](https://github.com/vercel/ai) provider (e.g., Google, OpenAI, etc.) anyclaude spawns a simple HTTP server that translates between Anthropic's format and the [AI SDK](https://github.com/vercel/ai) format, enabling support for any [AI SDK](https://github.com/vercel/ai) provider (e.g., Google, OpenAI, etc.)
## Do other models work better in Claude Code?
Not really, but it's fun to experiment with them.
`ANTHROPIC_MODEL` and `ANTHROPIC_SMALL_MODEL` are supported with the `<provider>/` syntax.

View file

@ -17,6 +17,7 @@
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.3",
"ai": "^5.0.8", "ai": "^5.0.8",
"prettier": "^3.6.2",
"zod": "3.25.76", "zod": "3.25.76",
}, },
"peerDependencies": { "peerDependencies": {
@ -67,6 +68,8 @@
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],

View file

@ -18,15 +18,16 @@
"dist/main.cjs" "dist/main.cjs"
], ],
"devDependencies": { "devDependencies": {
"@types/bun": "latest",
"@types/json-schema": "^7.0.15",
"@types/yargs-parser": "^21.0.3",
"@ai-sdk/anthropic": "^2.0.1", "@ai-sdk/anthropic": "^2.0.1",
"@ai-sdk/azure": "^2.0.6", "@ai-sdk/azure": "^2.0.6",
"@ai-sdk/google": "^2.0.3", "@ai-sdk/google": "^2.0.3",
"@ai-sdk/openai": "^2.0.6", "@ai-sdk/openai": "^2.0.6",
"@ai-sdk/xai": "^2.0.3", "@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", "ai": "^5.0.8",
"prettier": "^3.6.2",
"zod": "3.25.76" "zod": "3.25.76"
}, },
"peerDependencies": { "peerDependencies": {
@ -38,10 +39,20 @@
"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", "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", "test": "bun test",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"fmt": "prettier --write .",
"install:global": "bun run build && npm pack --silent && npm install -g anyclaude-*.tgz" "install:global": "bun run build && npm pack --silent && npm install -g anyclaude-*.tgz"
}, },
"dependencies": { "dependencies": {
"yargs-parser": "^22.0.0", "yargs-parser": "^22.0.0",
"json-schema": "^0.4.0" "json-schema": "^0.4.0"
},
"prettier": {
"printWidth": 80,
"singleQuote": false,
"semi": true,
"trailingComma": "es5",
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf"
} }
} }

View file

@ -139,6 +139,11 @@ export type AnthropicStreamChunk =
type: "text"; type: "text";
text: string; text: string;
} }
| {
type: "thinking";
thinking: string;
signature?: string;
}
| { | {
type: "tool_use"; type: "tool_use";
id: string; id: string;

View file

@ -18,7 +18,7 @@ import {
isDebugEnabled, isDebugEnabled,
isVerboseDebugEnabled, isVerboseDebugEnabled,
queueErrorMessage, queueErrorMessage,
debug debug,
} from "./debug"; } from "./debug";
export type CreateAnthropicProxyOptions = { export type CreateAnthropicProxyOptions = {
@ -26,6 +26,90 @@ export type CreateAnthropicProxyOptions = {
port?: number; port?: number;
}; };
/**
* Converts provider-specific errors to Anthropic-compatible error formats.
* This ensures Claude Code can properly handle and potentially retry errors.
*
* @see https://docs.anthropic.com/en/api/errors
* @see https://docs.anthropic.com/en/api/streaming#error-handling
*/
function convertProviderErrorToAnthropic(
chunk: any,
providerName: string,
model: string
): { converted: any; wasConverted: boolean; errorType: string } {
// Check if this is an OpenAI server error
const isOpenAIServerError =
providerName === "openai" && chunk.error?.code === "server_error";
// Check if this is an OpenAI rate limit error for context length
const isOpenAIRateLimitError =
providerName === "openai" &&
chunk.error?.message?.error?.code === "rate_limit_exceeded" &&
chunk.error?.message?.error?.type === "tokens";
if (isOpenAIServerError) {
debug(
1,
`OpenAI server error detected for ${model}. Transforming to 429 rate limit error to trigger Claude Code's automatic retry...`
);
// Transform OpenAI server errors to 429 rate limit errors
// This triggers Claude Code's built-in retry mechanism
return {
converted: {
type: "error",
sequence_number: chunk.sequence_number,
error: {
type: "rate_limit_error",
code: "rate_limit_error",
message:
"OpenAI server temporarily unavailable. Please retry your request.",
param: null,
},
},
wasConverted: true,
errorType: "server_error",
};
}
if (isOpenAIRateLimitError) {
debug(
1,
`OpenAI rate limit (context length) error detected for ${model}. Request too large.`
);
// Transform OpenAI context length errors to Anthropic's request_too_large format
// This properly signals to Claude Code that the request exceeds size limits and should NOT be retried
// According to Anthropic docs, request_too_large (413) is used when request exceeds maximum allowed bytes
return {
converted: {
type: "error",
error: {
type: "request_too_large",
message: `Request exceeds context length limit for ${model}: ${
chunk.error?.message?.error?.message || "Context length exceeded"
}`,
},
},
wasConverted: true,
errorType: "rate_limit_context",
};
}
// No conversion needed - return original
debug(
1,
`Streaming error chunk detected for ${providerName}/${model}:`,
chunk
);
return {
converted: chunk,
wasConverted: false,
errorType: "other",
};
}
// createAnthropicProxy creates a proxy server that accepts // createAnthropicProxy creates a proxy server that accepts
// Anthropic Message API requests and proxies them through // Anthropic Message API requests and proxies them through
// the appropriate provider - converting the results back // the appropriate provider - converting the results back
@ -69,11 +153,11 @@ export const createAnthropicProxy = ({
const statusCode = proxiedRes.statusCode ?? 500; const statusCode = proxiedRes.statusCode ?? 500;
// Collect response data for debugging // Collect response data for debugging
proxiedRes.on('data', (chunk) => { proxiedRes.on("data", (chunk) => {
responseChunks.push(chunk); responseChunks.push(chunk);
}); });
proxiedRes.on('end', () => { proxiedRes.on("end", () => {
// Write debug info to temp file for 4xx errors (except 429) // Write debug info to temp file for 4xx errors (except 429)
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) { if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
const requestBodyToLog = requestBody const requestBodyToLog = requestBody
@ -120,11 +204,11 @@ export const createAnthropicProxy = ({
if (requestBody) { if (requestBody) {
proxy.end(requestBody); proxy.end(requestBody);
} else { } else {
req.on('data', (chunk) => { req.on("data", (chunk) => {
chunks.push(chunk); chunks.push(chunk);
proxy.write(chunk); proxy.write(chunk);
}); });
req.on('end', () => { req.on("end", () => {
proxy.end(); proxy.end();
}); });
} }
@ -183,17 +267,22 @@ export const createAnthropicProxy = ({
system = body.system.map((s) => s.text).join("\n"); system = body.system.map((s) => s.text).join("\n");
} }
const tools = body.tools?.reduce((acc, tool) => { const tools = body.tools?.reduce(
(acc, tool) => {
acc[tool.name] = { acc[tool.name] = {
description: tool.name, description: tool.description || tool.name,
inputSchema: jsonSchema( inputSchema: jsonSchema(
providerizeSchema(providerName, tool.input_schema) providerizeSchema(providerName, tool.input_schema)
), ),
}; };
return acc; return acc;
}, {} as Record<string, Tool>); },
{} as Record<string, Tool>
);
const stream = streamText({ let stream;
try {
stream = streamText({
model: provider.languageModel(model), model: provider.languageModel(model),
system, system,
tools, tools,
@ -245,20 +334,50 @@ export const createAnthropicProxy = ({
let transformedError = error; let transformedError = error;
// Check if this is an OpenAI server error that we should transform // Check if this is an OpenAI server error that we should transform
const isOpenAIServerError = providerName === 'openai' && const isOpenAIServerError =
error && typeof error === 'object' && providerName === "openai" &&
'error' in error && (error as any).error?.code === 'server_error'; error &&
typeof error === "object" &&
"error" in error &&
(error as any).error?.code === "server_error";
if (isOpenAIServerError) { if (isOpenAIServerError) {
debug(1, `OpenAI server error detected in onError for ${model}. Transforming to 429 to trigger retry...`); debug(
1,
`OpenAI server error detected in onError for ${model}. Transforming to 429 to trigger retry...`
);
// Transform to rate limit error to trigger retry // Transform to rate limit error to trigger retry
statusCode = 429; statusCode = 429;
transformedError = { transformedError = {
type: "error", type: "error",
error: { error: {
type: "rate_limit_error", type: "rate_limit_error",
message: "OpenAI server temporarily unavailable. Please retry your request." message:
} "OpenAI server temporarily unavailable. Please retry your request.",
},
};
} else if (
// Check if this is an OpenAI rate limit error (non-streaming)
providerName === "openai" &&
error &&
typeof error === "object" &&
"error" in error &&
(error as any).error?.code === "rate_limit_exceeded"
) {
debug(
1,
`OpenAI rate limit 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:
(error as any).error?.message ||
"Rate limit exceeded. Please retry your request.",
},
}; };
} }
@ -277,29 +396,42 @@ export const createAnthropicProxy = ({
body: JSON.stringify({ body: JSON.stringify({
provider: providerName, provider: providerName,
model: model, model: model,
originalError: error instanceof Error ? { originalError:
error instanceof Error
? {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
name: error.name, name: error.name,
} : error, }
error: transformedError instanceof Error ? { : error,
error:
transformedError instanceof Error
? {
message: transformedError.message, message: transformedError.message,
stack: transformedError.stack, stack: transformedError.stack,
name: transformedError.name, name: transformedError.name,
} : transformedError, }
: transformedError,
wasTransformed: isOpenAIServerError, wasTransformed: isOpenAIServerError,
_debugInfo: { _debugInfo: {
requestSize: JSON.stringify(body).length, requestSize: JSON.stringify(body).length,
toolCount: body.tools?.length || 0, toolCount: body.tools?.length || 0,
systemPromptLength: body.system?.reduce((acc, s) => acc + s.text.length, 0) || 0, systemPromptLength:
messageCount: body.messages.length body.system?.reduce(
} (acc, s) => acc + s.text.length,
0
) || 0,
messageCount: body.messages.length,
},
}), }),
} }
); );
if (debugFile) { if (debugFile) {
logDebugError("Provider", statusCode, debugFile, { provider: providerName, model }); logDebugError("Provider", statusCode, debugFile, {
provider: providerName,
model,
});
} }
res res
@ -309,14 +441,53 @@ export const createAnthropicProxy = ({
.end( .end(
JSON.stringify({ JSON.stringify({
type: "error", type: "error",
error: transformedError instanceof Error ? transformedError.message : transformedError, error:
transformedError instanceof Error
? transformedError.message
: transformedError,
}) })
); );
}, },
}); });
} catch (error) {
// Handle connection errors and other synchronous errors from streamText
debug(1, `Connection error for ${providerName}/${model}:`, error);
// Return a 503 Service Unavailable to trigger Claude Code's retry
res.writeHead(503, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
type: "error",
error: {
type: "overloaded_error",
message: `Connection failed to ${providerName}. The service may be temporarily unavailable.`,
},
})
);
return;
}
if (!body.stream) { if (!body.stream) {
try {
await stream.consumeStream(); await stream.consumeStream();
} catch (error) {
debug(
1,
`Error consuming stream for ${providerName}/${model}:`,
error
);
// Return a 503 to trigger retry
res.writeHead(503, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
type: "error",
error: {
type: "overloaded_error",
message: `Failed to process response from ${providerName}. The service may be temporarily unavailable.`,
},
})
);
}
return; return;
} }
@ -329,6 +500,7 @@ export const createAnthropicProxy = ({
const streamChunks: any[] = []; const streamChunks: any[] = [];
const startTime = Date.now(); const startTime = Date.now();
try {
await convertToAnthropicStream(stream.fullStream).pipeTo( await convertToAnthropicStream(stream.fullStream).pipeTo(
new WritableStream({ new WritableStream({
write(chunk) { write(chunk) {
@ -336,39 +508,22 @@ export const createAnthropicProxy = ({
if (isVerboseDebugEnabled()) { if (isVerboseDebugEnabled()) {
streamChunks.push({ streamChunks.push({
timestamp: Date.now() - startTime, timestamp: Date.now() - startTime,
chunk: chunk chunk: chunk,
}); });
} }
// Check for streaming errors and log them (but don't interrupt the stream) // Check for streaming errors and convert them to Anthropic format
if (chunk.type === "error") { if (chunk.type === "error") {
// Store original error for debugging // Store original error for debugging
const originalError = { ...chunk }; const originalError = { ...chunk };
// Check if this is an OpenAI server error (any sequence) // Convert provider-specific errors to Anthropic format
const isOpenAIServerError = providerName === 'openai' && const errorConversion = convertProviderErrorToAnthropic(
(chunk as any).error?.code === 'server_error'; chunk,
providerName,
if (isOpenAIServerError) { model
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...`); chunk = errorConversion.converted;
// 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 // Write comprehensive debug info including full stream dump
const debugFile = writeDebugToTempFile( const debugFile = writeDebugToTempFile(
@ -386,8 +541,11 @@ export const createAnthropicProxy = ({
provider: providerName, provider: providerName,
model: model, model: model,
streamingError: originalError, streamingError: originalError,
transformedError: isOpenAIServerError ? chunk : null, transformedError: errorConversion.wasConverted
wasTransformed: isOpenAIServerError, ? chunk
: null,
wasTransformed: errorConversion.wasConverted,
errorType: errorConversion.errorType,
fullChunk: JSON.stringify(originalError), fullChunk: JSON.stringify(originalError),
streamDuration: Date.now() - startTime, streamDuration: Date.now() - startTime,
streamChunkCount: streamChunks.length, streamChunkCount: streamChunks.length,
@ -395,17 +553,26 @@ export const createAnthropicProxy = ({
_debugInfo: { _debugInfo: {
requestSize: JSON.stringify(body).length, requestSize: JSON.stringify(body).length,
toolCount: body.tools?.length || 0, toolCount: body.tools?.length || 0,
systemPromptLength: body.system?.reduce((acc, s) => acc + s.text.length, 0) || 0, systemPromptLength:
messageCount: body.messages.length body.system?.reduce(
} (acc, s) => acc + s.text.length,
0
) || 0,
messageCount: body.messages.length,
},
}), }),
} }
); );
if (debugFile) { if (debugFile) {
logDebugError("Streaming", 400, debugFile, { provider: providerName, model }); logDebugError("Streaming", 400, debugFile, {
provider: providerName,
model,
});
} else if (isDebugEnabled()) { } else if (isDebugEnabled()) {
queueErrorMessage(`Failed to write debug file for streaming error`); queueErrorMessage(
`Failed to write debug file for streaming error`
);
} }
} }
@ -416,12 +583,50 @@ export const createAnthropicProxy = ({
}, },
close() { close() {
if (streamChunks.length > 0) { if (streamChunks.length > 0) {
debug(2, `Stream completed for ${providerName}/${model}: ${streamChunks.length} chunks in ${Date.now() - startTime}ms`); debug(
2,
`Stream completed for ${providerName}/${model}: ${
streamChunks.length
} chunks in ${Date.now() - startTime}ms`
);
} }
res.end(); res.end();
}, },
}) })
); );
} catch (error) {
debug(
1,
`Error in stream processing for ${providerName}/${model}:`,
error
);
// If we haven't started writing the response yet, send a proper error
if (!res.headersSent) {
res.writeHead(503, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
type: "error",
error: {
type: "overloaded_error",
message: `Stream processing failed for ${providerName}. The service may be temporarily unavailable.`,
},
})
);
} else {
// If we've already started streaming, send an error event
res.write(
`event: error\ndata: ${JSON.stringify({
type: "error",
error: {
type: "overloaded_error",
message: `Stream interrupted. The service may be temporarily unavailable.`,
},
})}\n\n`
);
res.end();
}
}
})().catch((err) => { })().catch((err) => {
res.writeHead(500, { res.writeHead(500, {
"Content-Type": "application/json", "Content-Type": "application/json",

View file

@ -15,7 +15,7 @@ import type {
AnthropicToolResultContent, AnthropicToolResultContent,
} from "./anthropic-api-types"; } from "./anthropic-api-types";
import type { ModelMessage, FilePart, TextPart, ToolCallPart } from "ai"; import type { ModelMessage, FilePart, TextPart, ToolCallPart } from "ai";
import type { ReasoningUIPart } from 'ai'; import type { ReasoningUIPart } from "ai";
export function convertToAnthropicMessagesPrompt({ export function convertToAnthropicMessagesPrompt({
prompt, prompt,
@ -85,7 +85,9 @@ export function convertToAnthropicMessagesPrompt({
const isLastPart = j === content.length - 1; const isLastPart = j === content.length - 1;
const cacheControl = const cacheControl =
getCacheControl(part.providerOptions) ?? getCacheControl(part.providerOptions) ??
(isLastPart ? getCacheControl(message.providerOptions) : undefined); (isLastPart
? getCacheControl(message.providerOptions)
: undefined);
if (part.type === "text") { if (part.type === "text") {
anthropicContent.push({ anthropicContent.push({
@ -105,7 +107,8 @@ export function convertToAnthropicMessagesPrompt({
: { : {
type: "base64", type: "base64",
media_type: "application/pdf", media_type: "application/pdf",
data: typeof part.data === "string" data:
typeof part.data === "string"
? part.data ? part.data
: convertUint8ArrayToBase64(part.data), : convertUint8ArrayToBase64(part.data),
}, },
@ -120,7 +123,8 @@ export function convertToAnthropicMessagesPrompt({
: { : {
type: "base64", type: "base64",
media_type: mediaType ?? "image/jpeg", media_type: mediaType ?? "image/jpeg",
data: typeof part.data === "string" data:
typeof part.data === "string"
? part.data ? part.data
: convertUint8ArrayToBase64(part.data), : convertUint8ArrayToBase64(part.data),
}, },
@ -141,7 +145,9 @@ export function convertToAnthropicMessagesPrompt({
const isLastPart = i === content.length - 1; const isLastPart = i === content.length - 1;
const cacheControl = const cacheControl =
getCacheControl(part.providerOptions) ?? getCacheControl(part.providerOptions) ??
(isLastPart ? getCacheControl(message.providerOptions) : undefined); (isLastPart
? getCacheControl(message.providerOptions)
: undefined);
// Map LanguageModelV2ToolResultPart.output to Anthropic tool_result content // Map LanguageModelV2ToolResultPart.output to Anthropic tool_result content
let toolResultContent: AnthropicToolResultContent["content"]; let toolResultContent: AnthropicToolResultContent["content"];
@ -167,12 +173,24 @@ export function convertToAnthropicMessagesPrompt({
case "content": case "content":
toolResultContent = part.output.value.map((c) => toolResultContent = part.output.value.map((c) =>
c.type === "text" c.type === "text"
? { type: "text" as const, text: c.text, cache_control: undefined } ? {
type: "text" as const,
text: c.text,
cache_control: undefined,
}
: c.mediaType === "application/pdf" : c.mediaType === "application/pdf"
? { type: "text" as const, text: "[document content omitted]", cache_control: undefined } ? {
type: "text" as const,
text: "[document content omitted]",
cache_control: undefined,
}
: { : {
type: "image" as const, type: "image" as const,
source: { type: "base64" as const, media_type: c.mediaType, data: c.data }, source: {
type: "base64" as const,
media_type: c.mediaType,
data: c.data,
},
cache_control: undefined, cache_control: undefined,
} }
); );

View file

@ -9,6 +9,7 @@ export function convertToAnthropicStream(
stream: ReadableStream<TextStreamPart<Record<string, Tool>>> stream: ReadableStream<TextStreamPart<Record<string, Tool>>>
): ReadableStream<AnthropicStreamChunk> { ): ReadableStream<AnthropicStreamChunk> {
let index = 0; // content block index within the current message let index = 0; // content block index within the current message
let reasoningBuffer = ""; // Buffer for accumulating reasoning text
const transform = new TransformStream< const transform = new TransformStream<
TextStreamPart<Record<string, Tool>>, TextStreamPart<Record<string, Tool>>,
@ -126,14 +127,38 @@ export function convertToAnthropicStream(
}); });
break; break;
} }
case "reasoning-start": {
// Start a new thinking content block for OpenAI reasoning
controller.enqueue({
type: "content_block_start",
index,
content_block: { type: "thinking" as any, thinking: "" },
});
reasoningBuffer = ""; // Clear the buffer
break;
}
case "reasoning-delta": {
// Accumulate reasoning text and send as delta
reasoningBuffer += chunk.text;
controller.enqueue({
type: "content_block_delta",
index,
delta: { type: "text_delta", text: chunk.text },
});
break;
}
case "reasoning-end": {
// End the thinking content block
controller.enqueue({ type: "content_block_stop", index });
index += 1;
reasoningBuffer = ""; // Clear the buffer
break;
}
case "start": case "start":
case "abort": case "abort":
case "raw": case "raw":
case "source": case "source":
case "file": case "file":
case "reasoning-start":
case "reasoning-delta":
case "reasoning-end":
// ignore for Anthropic stream mapping // ignore for Anthropic stream mapping
break; break;
default: { default: {

View file

@ -169,9 +169,7 @@ function convertPartToLanguageModelPart(
string, string,
{ mimeType: string | undefined; data: Uint8Array } { mimeType: string | undefined; data: Uint8Array }
> >
): ): LanguageModelV2TextPart | LanguageModelV2FilePart {
| LanguageModelV2TextPart
| LanguageModelV2FilePart {
if (part.type === "text") { if (part.type === "text") {
return { return {
type: "text", type: "text",

View file

@ -32,7 +32,12 @@ export function writeDebugToTempFile(
// Log 4xx errors (except 429) when ANYCLAUDE_DEBUG is set // Log 4xx errors (except 429) when ANYCLAUDE_DEBUG is set
const debugEnabled = process.env.ANYCLAUDE_DEBUG; const debugEnabled = process.env.ANYCLAUDE_DEBUG;
if (!debugEnabled || statusCode === 429 || statusCode < 400 || statusCode >= 500) { if (
!debugEnabled ||
statusCode === 429 ||
statusCode < 400 ||
statusCode >= 500
) {
return null; return null;
} }
@ -54,12 +59,12 @@ export function writeDebugToTempFile(
response: response || null, response: response || null,
}; };
fs.writeFileSync(filepath, JSON.stringify(debugData, null, 2), 'utf8'); fs.writeFileSync(filepath, JSON.stringify(debugData, null, 2), "utf8");
// Also write a simpler error log file that's easier to tail // Also write a simpler error log file that's easier to tail
const errorLogPath = path.join(tmpDir, 'anyclaude-errors.log'); const errorLogPath = path.join(tmpDir, "anyclaude-errors.log");
const errorMessage = `[${new Date().toISOString()}] HTTP ${statusCode} - Debug: ${filepath}\n`; const errorMessage = `[${new Date().toISOString()}] HTTP ${statusCode} - Debug: ${filepath}\n`;
fs.appendFileSync(errorLogPath, errorMessage, 'utf8'); fs.appendFileSync(errorLogPath, errorMessage, "utf8");
return filepath; return filepath;
} catch (error) { } catch (error) {
@ -83,13 +88,13 @@ export function queueErrorMessage(message: string): void {
function displayPendingErrors(): void { function displayPendingErrors(): void {
if (pendingErrorMessages.length > 0) { if (pendingErrorMessages.length > 0) {
// Use stderr and add newlines to separate from Claude's output // Use stderr and add newlines to separate from Claude's output
process.stderr.write('\n\n═══════════════════════════════════════\n'); process.stderr.write("\n\n═══════════════════════════════════════\n");
process.stderr.write('ANYCLAUDE DEBUG - Errors detected:\n'); process.stderr.write("ANYCLAUDE DEBUG - Errors detected:\n");
process.stderr.write('═══════════════════════════════════════\n'); process.stderr.write("═══════════════════════════════════════\n");
pendingErrorMessages.forEach(msg => { pendingErrorMessages.forEach((msg) => {
process.stderr.write(msg + '\n'); process.stderr.write(msg + "\n");
}); });
process.stderr.write('═══════════════════════════════════════\n\n'); process.stderr.write("═══════════════════════════════════════\n\n");
pendingErrorMessages = []; pendingErrorMessages = [];
} }
} }
@ -123,15 +128,15 @@ export function displayDebugStartup(): void {
const level = getDebugLevel(); const level = getDebugLevel();
if (level > 0) { if (level > 0) {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const errorLogPath = path.join(tmpDir, 'anyclaude-errors.log'); const errorLogPath = path.join(tmpDir, "anyclaude-errors.log");
process.stderr.write('\n═══════════════════════════════════════\n'); process.stderr.write("\n═══════════════════════════════════════\n");
process.stderr.write(`ANYCLAUDE DEBUG MODE ENABLED (Level ${level})\n`); process.stderr.write(`ANYCLAUDE DEBUG MODE ENABLED (Level ${level})\n`);
process.stderr.write(`Error log: ${errorLogPath}\n`); process.stderr.write(`Error log: ${errorLogPath}\n`);
process.stderr.write(`Debug files: ${tmpDir}/anyclaude-debug-*.json\n`); process.stderr.write(`Debug files: ${tmpDir}/anyclaude-debug-*.json\n`);
if (level >= 2) { if (level >= 2) {
process.stderr.write('Verbose: Duplicate filtering details enabled\n'); process.stderr.write("Verbose: Duplicate filtering details enabled\n");
} }
process.stderr.write('═══════════════════════════════════════\n\n'); process.stderr.write("═══════════════════════════════════════\n\n");
} }
} }
@ -172,12 +177,13 @@ export function isVerboseDebugEnabled(): boolean {
*/ */
export function debug(level: 1 | 2, message: string, data?: any): void { export function debug(level: 1 | 2, message: string, data?: any): void {
if (getDebugLevel() >= level) { if (getDebugLevel() >= level) {
const prefix = '[ANYCLAUDE DEBUG]'; const prefix = "[ANYCLAUDE DEBUG]";
if (data !== undefined) { if (data !== undefined) {
// For objects/errors, stringify with a length limit // For objects/errors, stringify with a length limit
const dataStr = typeof data === 'object' ? const dataStr =
JSON.stringify(data).substring(0, 200) : typeof data === "object"
String(data); ? JSON.stringify(data).substring(0, 200)
: String(data);
console.error(`${prefix} ${message}`, dataStr); console.error(`${prefix} ${message}`, dataStr);
} else { } else {
console.error(`${prefix} ${message}`); console.error(`${prefix} ${message}`);

View file

@ -1,4 +1,4 @@
import { AISDKError } from 'ai'; import { AISDKError } from "ai";
const name = "AI_InvalidDataContentError"; const name = "AI_InvalidDataContentError";
const marker = `vercel.ai.error.${name}`; const marker = `vercel.ai.error.${name}`;

View file

@ -22,7 +22,10 @@ export function providerizeSchema(
let processedProperty = property as JSONSchema7; let processedProperty = property as JSONSchema7;
// Remove uri format for OpenAI and Google // Remove uri format for OpenAI and Google
if ((provider === "openai" || provider === "google") && processedProperty.format === "uri") { if (
(provider === "openai" || provider === "google") &&
processedProperty.format === "uri"
) {
processedProperty = { ...processedProperty }; processedProperty = { ...processedProperty };
delete processedProperty.format; delete processedProperty.format;
} }

View file

@ -112,8 +112,23 @@ const providers: CreateAnthropicProxyOptions["providers"] = {
delete body["max_tokens"]; delete body["max_tokens"];
if (typeof maxTokens !== "undefined") if (typeof maxTokens !== "undefined")
body.max_completion_tokens = maxTokens; body.max_completion_tokens = maxTokens;
if (reasoningEffort) body.reasoning = { effort: reasoningEffort };
// Set up reasoning parameters for OpenAI
if (reasoningEffort) {
body.reasoning = {
effort: reasoningEffort,
summary: "auto", // Request reasoning summaries from OpenAI
};
} else {
// Always request reasoning summaries for models that support it
body.reasoning = { summary: "auto" };
}
// Enable automatic truncation to prevent context length errors
body.parallel_tool_calls = true;
if (serviceTier) body.service_tier = serviceTier; 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);