diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6e8ba15..79a2d9f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,57 +2,57 @@ name: CI
on:
push:
- branches: [ main, master ]
+ branches: [main, master]
pull_request:
- branches: [ main, master ]
+ 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"
\ No newline at end of file
+ - 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"
diff --git a/AGENTS.md b/AGENTS.md
index 57a9438..12d0c3f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,7 @@
# Repository Guidelines
## 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`.
- `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`.
@@ -8,6 +9,7 @@
- Assets/config: `README.md`, `CLAUDE.md`, `tsconfig.json`, `demo.png`.
## Build, Test, and Development Commands
+
- Install: `bun install`.
- Build: `bun run build` (outputs `dist/main.js` with Node shebang).
- 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`.
## Coding Style & Naming Conventions
+
- Language: TypeScript (ESNext, strict mode enabled).
- Indentation: 2 spaces; keep lines reasonable; use explicit imports (`verbatimModuleSyntax`).
- 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.
## Testing Guidelines
+
- No test runner is configured yet. If adding 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.
- Add a `test` script in `package.json` and document how to run it in `README.md`.
## 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.
- 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.
## Security & Configuration Tips
+
- 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.
diff --git a/README.md b/README.md
index 0e83cbb..1e6bbc2 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ Use Claude Code with OpenAI, Google, xAI, and other providers.
- Extremely simple setup - just a basic command wrapper
- Uses the AI SDK for simple support of new providers
- Works with Claude Code GitHub Actions
+- Optimized for OpenAI's gpt-5 series
@@ -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
+`ANTHROPIC_MODEL` and `ANTHROPIC_SMALL_MODEL` are supported with the `/` syntax.
+
### How does this work?
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.)
-
-## 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 `/` syntax.
diff --git a/bun.lock b/bun.lock
index 4502346..aa817c9 100644
--- a/bun.lock
+++ b/bun.lock
@@ -17,6 +17,7 @@
"@types/json-schema": "^7.0.15",
"@types/yargs-parser": "^21.0.3",
"ai": "^5.0.8",
+ "prettier": "^3.6.2",
"zod": "3.25.76",
},
"peerDependencies": {
@@ -67,6 +68,8 @@
"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=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
diff --git a/package.json b/package.json
index ef96301..494dab0 100644
--- a/package.json
+++ b/package.json
@@ -18,15 +18,16 @@
"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",
+ "@types/yargs-parser": "^21.0.3",
"ai": "^5.0.8",
+ "prettier": "^3.6.2",
"zod": "3.25.76"
},
"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",
"test": "bun test",
"typecheck": "tsc --noEmit",
+ "fmt": "prettier --write .",
"install:global": "bun run build && npm pack --silent && npm install -g anyclaude-*.tgz"
},
"dependencies": {
"yargs-parser": "^22.0.0",
"json-schema": "^0.4.0"
+ },
+ "prettier": {
+ "printWidth": 80,
+ "singleQuote": false,
+ "semi": true,
+ "trailingComma": "es5",
+ "arrowParens": "always",
+ "bracketSpacing": true,
+ "endOfLine": "lf"
}
}
diff --git a/src/anthropic-api-types.ts b/src/anthropic-api-types.ts
index af56cc1..061b616 100644
--- a/src/anthropic-api-types.ts
+++ b/src/anthropic-api-types.ts
@@ -52,14 +52,14 @@ export interface AnthropicRedactedThinkingContent {
type AnthropicContentSource =
| {
- type: "base64";
- media_type: string;
- data: string;
- }
+ type: "base64";
+ media_type: string;
+ data: string;
+ }
| {
- type: "url";
- url: string;
- };
+ type: "url";
+ url: string;
+ };
export interface AnthropicImageContent {
type: "image";
@@ -91,25 +91,25 @@ export interface AnthropicToolResultContent {
export type AnthropicTool =
| {
- name: string;
- description: string | undefined;
- input_schema: JSONSchema7;
- }
+ name: string;
+ description: string | undefined;
+ input_schema: JSONSchema7;
+ }
| {
- name: string;
- type: "computer_20250124" | "computer_20241022";
- display_width_px: number;
- display_height_px: number;
- display_number: number;
- }
+ name: string;
+ type: "computer_20250124" | "computer_20241022";
+ display_width_px: number;
+ display_height_px: number;
+ display_number: number;
+ }
| {
- name: string;
- type: "text_editor_20250124" | "text_editor_20241022";
- }
+ name: string;
+ type: "text_editor_20250124" | "text_editor_20241022";
+ }
| {
- name: string;
- type: "bash_20250124" | "bash_20241022";
- };
+ name: string;
+ type: "bash_20250124" | "bash_20241022";
+ };
export type AnthropicToolChoice =
| { type: "auto" | "any" }
@@ -122,65 +122,70 @@ export type AnthropicStreamUsage = {
export type AnthropicStreamChunk =
| {
- type: "message_start";
- message: AnthropicAssistantMessage & {
- id: string;
- model: string;
- stop_reason: string | null;
- stop_sequence: string | null;
+ type: "message_start";
+ message: AnthropicAssistantMessage & {
+ id: string;
+ model: string;
+ stop_reason: string | null;
+ stop_sequence: string | null;
+ usage: AnthropicStreamUsage;
+ };
+ }
+ | {
+ type: "content_block_start";
+ index: number;
+ content_block:
+ | {
+ type: "text";
+ text: string;
+ }
+ | {
+ type: "thinking";
+ thinking: string;
+ signature?: string;
+ }
+ | {
+ type: "tool_use";
+ id: string;
+ name: string;
+ input: any;
+ };
+ }
+ | {
+ type: "content_block_delta";
+ index: number;
+ delta:
+ | {
+ type: "text_delta";
+ text: string;
+ }
+ | {
+ type: "input_json_delta";
+ partial_json: string;
+ };
+ }
+ | {
+ type: "content_block_stop";
+ index: number;
+ }
+ | {
+ type: "message_delta";
+ delta: {
+ stop_reason: string;
+ stop_sequence: string | null;
+ };
usage: AnthropicStreamUsage;
- };
- }
- | {
- type: "content_block_start";
- index: number;
- content_block:
- | {
- type: "text";
- text: string;
}
- | {
- type: "tool_use";
- id: string;
- name: string;
- input: any;
- };
- }
| {
- type: "content_block_delta";
- index: number;
- delta:
- | {
- type: "text_delta";
- text: string;
+ type: "message_stop";
}
- | {
- type: "input_json_delta";
- partial_json: string;
+ | {
+ type: "error";
+ error: {
+ type: "api_error";
+ message: string;
+ };
};
- }
- | {
- type: "content_block_stop";
- index: number;
- }
- | {
- type: "message_delta";
- delta: {
- stop_reason: string;
- stop_sequence: string | null;
- };
- usage: AnthropicStreamUsage;
- }
- | {
- type: "message_stop";
- }
- | {
- type: "error";
- error: {
- type: "api_error";
- message: string;
- };
- };
export type AnthropicMessagesRequest = {
model: string;
diff --git a/src/anthropic-proxy.ts b/src/anthropic-proxy.ts
index 36b312a..2860120 100644
--- a/src/anthropic-proxy.ts
+++ b/src/anthropic-proxy.ts
@@ -11,14 +11,14 @@ import {
import { convertToAnthropicStream } from "./convert-to-anthropic-stream";
import { convertToLanguageModelMessage } from "./convert-to-language-model-prompt";
import { providerizeSchema } from "./json-schema";
-import {
- writeDebugToTempFile,
- logDebugError,
+import {
+ writeDebugToTempFile,
+ logDebugError,
displayDebugStartup,
isDebugEnabled,
isVerboseDebugEnabled,
queueErrorMessage,
- debug
+ debug,
} from "./debug";
export type CreateAnthropicProxyOptions = {
@@ -26,6 +26,90 @@ export type CreateAnthropicProxyOptions = {
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
// Anthropic Message API requests and proxies them through
// the appropriate provider - converting the results back
@@ -36,7 +120,7 @@ export const createAnthropicProxy = ({
}: CreateAnthropicProxyOptions): string => {
// Log debug status on startup
displayDebugStartup();
-
+
const proxy = http
.createServer((req, res) => {
if (!req.url) {
@@ -67,18 +151,18 @@ export const createAnthropicProxy = ({
},
(proxiedRes) => {
const statusCode = proxiedRes.statusCode ?? 500;
-
+
// Collect response data for debugging
- proxiedRes.on('data', (chunk) => {
+ proxiedRes.on("data", (chunk) => {
responseChunks.push(chunk);
});
- proxiedRes.on('end', () => {
+ proxiedRes.on("end", () => {
// Write debug info to temp file for 4xx errors (except 429)
if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
- const requestBodyToLog = requestBody
+ const requestBodyToLog = requestBody
? JSON.parse(requestBody)
- : chunks.length > 0
+ : chunks.length > 0
? (() => {
try {
return JSON.parse(Buffer.concat(chunks).toString());
@@ -120,11 +204,11 @@ export const createAnthropicProxy = ({
if (requestBody) {
proxy.end(requestBody);
} else {
- req.on('data', (chunk) => {
+ req.on("data", (chunk) => {
chunks.push(chunk);
proxy.write(chunk);
});
- req.on('end', () => {
+ req.on("end", () => {
proxy.end();
});
}
@@ -183,140 +267,227 @@ export const createAnthropicProxy = ({
system = body.system.map((s) => s.text).join("\n");
}
- const tools = body.tools?.reduce((acc, tool) => {
- acc[tool.name] = {
- description: tool.name,
- inputSchema: jsonSchema(
- providerizeSchema(providerName, tool.input_schema)
- ),
- };
- return acc;
- }, {} as Record);
+ const tools = body.tools?.reduce(
+ (acc, tool) => {
+ acc[tool.name] = {
+ description: tool.description || tool.name,
+ inputSchema: jsonSchema(
+ providerizeSchema(providerName, tool.input_schema)
+ ),
+ };
+ return acc;
+ },
+ {} as Record
+ );
- const stream = streamText({
- model: provider.languageModel(model),
- system,
- tools,
- messages: coreMessages,
- maxOutputTokens: body.max_tokens,
- temperature: body.temperature,
+ let stream;
+ try {
+ stream = streamText({
+ model: provider.languageModel(model),
+ system,
+ tools,
+ messages: coreMessages,
+ maxOutputTokens: body.max_tokens,
+ temperature: body.temperature,
- onFinish: ({ response, usage, finishReason }) => {
- // If the body is already being streamed,
- // we don't need to do any conversion here.
- if (body.stream) {
- return;
- }
+ onFinish: ({ response, usage, finishReason }) => {
+ // If the body is already being streamed,
+ // we don't need to do any conversion here.
+ if (body.stream) {
+ return;
+ }
- // There should only be one message.
- const message = response.messages[0];
- if (!message) {
- throw new Error("No message found");
- }
+ // There should only be one message.
+ const message = response.messages[0];
+ if (!message) {
+ throw new Error("No message found");
+ }
- const prompt = convertToAnthropicMessagesPrompt({
- prompt: [convertToLanguageModelMessage(message, {})],
- sendReasoning: true,
- warnings: [],
- });
- const promptMessage = prompt.prompt.messages[0];
- if (!promptMessage) {
- throw new Error("No prompt message found");
- }
+ const prompt = convertToAnthropicMessagesPrompt({
+ prompt: [convertToLanguageModelMessage(message, {})],
+ sendReasoning: true,
+ warnings: [],
+ });
+ const promptMessage = prompt.prompt.messages[0];
+ if (!promptMessage) {
+ throw new Error("No prompt message found");
+ }
- res.writeHead(200, { "Content-Type": "application/json" }).end(
+ res.writeHead(200, { "Content-Type": "application/json" }).end(
+ JSON.stringify({
+ id: "msg_" + Date.now(),
+ type: "message",
+ role: promptMessage.role,
+ content: promptMessage.content,
+ model: body.model,
+ stop_reason: mapAnthropicStopReason(finishReason),
+ stop_sequence: null,
+ usage: {
+ input_tokens: usage.inputTokens,
+ output_tokens: usage.outputTokens,
+ },
+ })
+ );
+ },
+ 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.",
+ },
+ };
+ } 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.",
+ },
+ };
+ }
+
+ // Write comprehensive debug info to temp file
+ const debugFile = writeDebugToTempFile(
+ statusCode,
+ {
+ method: "POST",
+ url: req.url,
+ headers: req.headers,
+ body: body,
+ },
+ {
+ statusCode,
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ provider: providerName,
+ model: model,
+ originalError:
+ error instanceof Error
+ ? {
+ message: error.message,
+ stack: error.stack,
+ name: error.name,
+ }
+ : error,
+ error:
+ transformedError instanceof Error
+ ? {
+ message: transformedError.message,
+ stack: transformedError.stack,
+ name: transformedError.name,
+ }
+ : transformedError,
+ wasTransformed: isOpenAIServerError,
+ _debugInfo: {
+ requestSize: JSON.stringify(body).length,
+ toolCount: body.tools?.length || 0,
+ systemPromptLength:
+ body.system?.reduce(
+ (acc, s) => acc + s.text.length,
+ 0
+ ) || 0,
+ messageCount: body.messages.length,
+ },
+ }),
+ }
+ );
+
+ if (debugFile) {
+ logDebugError("Provider", statusCode, debugFile, {
+ provider: providerName,
+ model,
+ });
+ }
+
+ res
+ .writeHead(statusCode, {
+ "Content-Type": "application/json",
+ })
+ .end(
+ JSON.stringify({
+ type: "error",
+ 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) {
+ try {
+ 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({
- id: "msg_" + Date.now(),
- type: "message",
- role: promptMessage.role,
- content: promptMessage.content,
- model: body.model,
- stop_reason: mapAnthropicStopReason(finishReason),
- stop_sequence: null,
- usage: {
- input_tokens: usage.inputTokens,
- output_tokens: usage.outputTokens,
+ type: "error",
+ error: {
+ type: "overloaded_error",
+ message: `Failed to process response from ${providerName}. The service may be temporarily unavailable.`,
},
})
);
- },
- onError: ({ error }) => {
- let statusCode = 400; // Provider errors are returned as 400
- let transformedError = error;
-
- // Check if this is an OpenAI server error that we should transform
- const isOpenAIServerError = providerName === 'openai' &&
- error && typeof error === 'object' &&
- 'error' in error && (error as any).error?.code === 'server_error';
-
- if (isOpenAIServerError) {
- debug(1, `OpenAI server error detected in onError for ${model}. Transforming to 429 to trigger retry...`);
- // Transform to rate limit error to trigger retry
- statusCode = 429;
- transformedError = {
- type: "error",
- error: {
- type: "rate_limit_error",
- message: "OpenAI server temporarily unavailable. Please retry your request."
- }
- };
- }
-
- // Write comprehensive debug info to temp file
- const debugFile = writeDebugToTempFile(
- statusCode,
- {
- method: "POST",
- url: req.url,
- headers: req.headers,
- body: body,
- },
- {
- statusCode,
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- provider: providerName,
- model: model,
- originalError: error instanceof Error ? {
- message: error.message,
- stack: error.stack,
- name: error.name,
- } : error,
- error: transformedError instanceof Error ? {
- message: transformedError.message,
- stack: transformedError.stack,
- name: transformedError.name,
- } : transformedError,
- wasTransformed: isOpenAIServerError,
- _debugInfo: {
- requestSize: JSON.stringify(body).length,
- toolCount: body.tools?.length || 0,
- systemPromptLength: body.system?.reduce((acc, s) => acc + s.text.length, 0) || 0,
- messageCount: body.messages.length
- }
- }),
- }
- );
-
- if (debugFile) {
- logDebugError("Provider", statusCode, debugFile, { provider: providerName, model });
- }
-
- res
- .writeHead(statusCode, {
- "Content-Type": "application/json",
- })
- .end(
- JSON.stringify({
- type: "error",
- error: transformedError instanceof Error ? transformedError.message : transformedError,
- })
- );
- },
- });
-
- if (!body.stream) {
- await stream.consumeStream();
+ }
return;
}
@@ -329,99 +500,133 @@ export const createAnthropicProxy = ({
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);
+ try {
+ 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,
+ });
}
-
- // 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`);
+ // Check for streaming errors and convert them to Anthropic format
+ if (chunk.type === "error") {
+ // Store original error for debugging
+ const originalError = { ...chunk };
+
+ // Convert provider-specific errors to Anthropic format
+ const errorConversion = convertProviderErrorToAnthropic(
+ chunk,
+ providerName,
+ model
+ );
+ chunk = errorConversion.converted;
+
+ // 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: errorConversion.wasConverted
+ ? chunk
+ : null,
+ wasTransformed: errorConversion.wasConverted,
+ errorType: errorConversion.errorType,
+ 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();
- },
- })
- );
+
+ // 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();
+ },
+ })
+ );
+ } 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) => {
res.writeHead(500, {
"Content-Type": "application/json",
diff --git a/src/convert-anthropic-messages.test.ts b/src/convert-anthropic-messages.test.ts
index 47b98c1..ee2ff19 100644
--- a/src/convert-anthropic-messages.test.ts
+++ b/src/convert-anthropic-messages.test.ts
@@ -34,7 +34,7 @@ describe("convertToAnthropicMessagesPrompt", () => {
// 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"
@@ -75,7 +75,7 @@ describe("convertToAnthropicMessagesPrompt", () => {
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"
@@ -125,7 +125,7 @@ describe("convertToAnthropicMessagesPrompt", () => {
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(
@@ -134,7 +134,7 @@ describe("convertToAnthropicMessagesPrompt", () => {
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");
@@ -163,7 +163,7 @@ describe("convertToAnthropicMessagesPrompt", () => {
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"
@@ -172,4 +172,4 @@ describe("convertToAnthropicMessagesPrompt", () => {
}
});
});
-});
\ No newline at end of file
+});
diff --git a/src/convert-anthropic-messages.ts b/src/convert-anthropic-messages.ts
index f0b69c8..7ef0461 100644
--- a/src/convert-anthropic-messages.ts
+++ b/src/convert-anthropic-messages.ts
@@ -15,7 +15,7 @@ import type {
AnthropicToolResultContent,
} from "./anthropic-api-types";
import type { ModelMessage, FilePart, TextPart, ToolCallPart } from "ai";
-import type { ReasoningUIPart } from 'ai';
+import type { ReasoningUIPart } from "ai";
export function convertToAnthropicMessagesPrompt({
prompt,
@@ -85,7 +85,9 @@ export function convertToAnthropicMessagesPrompt({
const isLastPart = j === content.length - 1;
const cacheControl =
getCacheControl(part.providerOptions) ??
- (isLastPart ? getCacheControl(message.providerOptions) : undefined);
+ (isLastPart
+ ? getCacheControl(message.providerOptions)
+ : undefined);
if (part.type === "text") {
anthropicContent.push({
@@ -103,12 +105,13 @@ export function convertToAnthropicMessagesPrompt({
part.data instanceof URL
? { type: "url", url: part.data.toString() }
: {
- type: "base64",
- media_type: "application/pdf",
- data: typeof part.data === "string"
- ? part.data
- : convertUint8ArrayToBase64(part.data),
- },
+ type: "base64",
+ media_type: "application/pdf",
+ data:
+ typeof part.data === "string"
+ ? part.data
+ : convertUint8ArrayToBase64(part.data),
+ },
cache_control: cacheControl,
});
} else if (mediaType?.startsWith("image/")) {
@@ -118,12 +121,13 @@ export function convertToAnthropicMessagesPrompt({
part.data instanceof URL
? { type: "url", url: part.data.toString() }
: {
- type: "base64",
- media_type: mediaType ?? "image/jpeg",
- data: typeof part.data === "string"
- ? part.data
- : convertUint8ArrayToBase64(part.data),
- },
+ type: "base64",
+ media_type: mediaType ?? "image/jpeg",
+ data:
+ typeof part.data === "string"
+ ? part.data
+ : convertUint8ArrayToBase64(part.data),
+ },
cache_control: cacheControl,
});
} else {
@@ -141,7 +145,9 @@ export function convertToAnthropicMessagesPrompt({
const isLastPart = i === content.length - 1;
const cacheControl =
getCacheControl(part.providerOptions) ??
- (isLastPart ? getCacheControl(message.providerOptions) : undefined);
+ (isLastPart
+ ? getCacheControl(message.providerOptions)
+ : undefined);
// Map LanguageModelV2ToolResultPart.output to Anthropic tool_result content
let toolResultContent: AnthropicToolResultContent["content"];
@@ -167,14 +173,26 @@ export function convertToAnthropicMessagesPrompt({
case "content":
toolResultContent = part.output.value.map((c) =>
c.type === "text"
- ? { type: "text" as const, text: c.text, cache_control: undefined }
- : c.mediaType === "application/pdf"
- ? { type: "text" as const, text: "[document content omitted]", cache_control: undefined }
- : {
- type: "image" as const,
- source: { type: "base64" as const, media_type: c.mediaType, data: c.data },
+ ? {
+ type: "text" as const,
+ text: c.text,
cache_control: undefined,
}
+ : c.mediaType === "application/pdf"
+ ? {
+ type: "text" as const,
+ text: "[document content omitted]",
+ cache_control: undefined,
+ }
+ : {
+ type: "image" as const,
+ source: {
+ type: "base64" as const,
+ media_type: c.mediaType,
+ data: c.data,
+ },
+ cache_control: undefined,
+ }
);
isError = false;
break;
@@ -259,7 +277,7 @@ export function convertToAnthropicMessagesPrompt({
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({
diff --git a/src/convert-to-anthropic-stream.ts b/src/convert-to-anthropic-stream.ts
index 02a4c65..73e15cc 100644
--- a/src/convert-to-anthropic-stream.ts
+++ b/src/convert-to-anthropic-stream.ts
@@ -9,6 +9,7 @@ export function convertToAnthropicStream(
stream: ReadableStream>>
): ReadableStream {
let index = 0; // content block index within the current message
+ let reasoningBuffer = ""; // Buffer for accumulating reasoning text
const transform = new TransformStream<
TextStreamPart>,
@@ -126,14 +127,38 @@ export function convertToAnthropicStream(
});
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 "abort":
case "raw":
case "source":
case "file":
- case "reasoning-start":
- case "reasoning-delta":
- case "reasoning-end":
// ignore for Anthropic stream mapping
break;
default: {
diff --git a/src/convert-to-language-model-prompt.ts b/src/convert-to-language-model-prompt.ts
index eaaa8eb..5b5a208 100644
--- a/src/convert-to-language-model-prompt.ts
+++ b/src/convert-to-language-model-prompt.ts
@@ -169,9 +169,7 @@ function convertPartToLanguageModelPart(
string,
{ mimeType: string | undefined; data: Uint8Array }
>
-):
- | LanguageModelV2TextPart
- | LanguageModelV2FilePart {
+): LanguageModelV2TextPart | LanguageModelV2FilePart {
if (part.type === "text") {
return {
type: "text",
diff --git a/src/debug.ts b/src/debug.ts
index 1cce71f..02017dd 100644
--- a/src/debug.ts
+++ b/src/debug.ts
@@ -31,8 +31,13 @@ export function writeDebugToTempFile(
): 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) {
+
+ if (
+ !debugEnabled ||
+ statusCode === 429 ||
+ statusCode < 400 ||
+ statusCode >= 500
+ ) {
return null;
}
@@ -54,13 +59,13 @@ export function writeDebugToTempFile(
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
- 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`;
- fs.appendFileSync(errorLogPath, errorMessage, 'utf8');
-
+ fs.appendFileSync(errorLogPath, errorMessage, "utf8");
+
return filepath;
} catch (error) {
console.error("[ANYCLAUDE DEBUG] Failed to write debug file:", error);
@@ -83,13 +88,13 @@ export function queueErrorMessage(message: string): void {
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═══════════════════════════════════════\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');
+ process.stderr.write("═══════════════════════════════════════\n\n");
pendingErrorMessages = [];
}
}
@@ -104,7 +109,7 @@ export function logDebugError(
context?: { provider?: string; model?: string }
): void {
if (!debugFile) return;
-
+
let message = `${type} error`;
if (context?.provider && context?.model) {
message += ` (${context.provider}/${context.model})`;
@@ -112,7 +117,7 @@ export function logDebugError(
message += ` ${statusCode}`;
}
message += ` - Debug info written to: ${debugFile}`;
-
+
queueErrorMessage(message);
}
@@ -123,15 +128,15 @@ 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');
+ 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("Verbose: Duplicate filtering details enabled\n");
}
- process.stderr.write('═══════════════════════════════════════\n\n');
+ process.stderr.write("═══════════════════════════════════════\n\n");
}
}
@@ -146,10 +151,10 @@ export function displayDebugStartup(): void {
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
}
@@ -172,15 +177,16 @@ export function isVerboseDebugEnabled(): boolean {
*/
export function debug(level: 1 | 2, message: string, data?: any): void {
if (getDebugLevel() >= level) {
- const prefix = '[ANYCLAUDE DEBUG]';
+ 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);
+ const dataStr =
+ typeof data === "object"
+ ? JSON.stringify(data).substring(0, 200)
+ : String(data);
console.error(`${prefix} ${message}`, dataStr);
} else {
console.error(`${prefix} ${message}`);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/invalid-data-content-error.ts b/src/invalid-data-content-error.ts
index 5cc3f2e..1fa2c8b 100644
--- a/src/invalid-data-content-error.ts
+++ b/src/invalid-data-content-error.ts
@@ -1,4 +1,4 @@
-import { AISDKError } from 'ai';
+import { AISDKError } from "ai";
const name = "AI_InvalidDataContentError";
const marker = `vercel.ai.error.${name}`;
diff --git a/src/json-schema.ts b/src/json-schema.ts
index 0187a4c..b8a90ef 100644
--- a/src/json-schema.ts
+++ b/src/json-schema.ts
@@ -22,7 +22,10 @@ export function providerizeSchema(
let processedProperty = property as JSONSchema7;
// 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 };
delete processedProperty.format;
}
diff --git a/src/main.ts b/src/main.ts
index 8082947..13d5b74 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -112,8 +112,23 @@ const providers: CreateAnthropicProxyOptions["providers"] = {
delete body["max_tokens"];
if (typeof maxTokens !== "undefined")
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;
+
init.body = JSON.stringify(body);
}
return globalThis.fetch(url, init);