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:
parent
3fec97c7bf
commit
d48232522b
15 changed files with 716 additions and 424 deletions
96
.github/workflows/ci.yml
vendored
96
.github/workflows/ci.yml
vendored
|
|
@ -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"
|
||||
- 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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<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
|
||||
|
||||
`ANTHROPIC_MODEL` and `ANTHROPIC_SMALL_MODEL` are supported with the `<provider>/` 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 `<provider>/` syntax.
|
||||
|
|
|
|||
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
17
package.json
17
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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, Tool>);
|
||||
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<string, Tool>
|
||||
);
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export function convertToAnthropicStream(
|
|||
stream: ReadableStream<TextStreamPart<Record<string, Tool>>>
|
||||
): ReadableStream<AnthropicStreamChunk> {
|
||||
let index = 0; // content block index within the current message
|
||||
let reasoningBuffer = ""; // Buffer for accumulating reasoning text
|
||||
|
||||
const transform = new TransformStream<
|
||||
TextStreamPart<Record<string, Tool>>,
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -169,9 +169,7 @@ function convertPartToLanguageModelPart(
|
|||
string,
|
||||
{ mimeType: string | undefined; data: Uint8Array }
|
||||
>
|
||||
):
|
||||
| LanguageModelV2TextPart
|
||||
| LanguageModelV2FilePart {
|
||||
): LanguageModelV2TextPart | LanguageModelV2FilePart {
|
||||
if (part.type === "text") {
|
||||
return {
|
||||
type: "text",
|
||||
|
|
|
|||
58
src/debug.ts
58
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AISDKError } from 'ai';
|
||||
import { AISDKError } from "ai";
|
||||
|
||||
const name = "AI_InvalidDataContentError";
|
||||
const marker = `vercel.ai.error.${name}`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
17
src/main.ts
17
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue