A FastAPI service that translates Claude Messages API requests to OpenAI-compatible chat completions endpoints
Project description
FastAPI Claude-to-OpenAI Forwarder
A FastAPI service that translates Claude Messages API requests to OpenAI-compatible chat completions endpoints.
Features and Functionality
The FastAPI service accepts Claude Messages API-style requests and translates them to OpenAI-compatible chat completions endpoints. Key features include:
- Claude-to-OpenAI request translation
- OpenAI-to-Claude response translation
- Streaming SSE translation
- Claude Code-style tool calling
- Support for two backend modes:
httpxandlitellm
Project Structure
The project is organized into the following components:
src/claude_openai_forwarder/app.py: The FastAPI application entry point, responsible for handling incoming requests and routing them to the appropriate backend.src/claude_openai_forwarder/translators/: A module containing the logic for translating Claude requests to OpenAI format and vice versa.src/claude_openai_forwarder/backends/: A module providing the backend implementations for interacting with OpenAI-compatible APIs, includinghttpxandlitellm.tests/test_backends.py: A test suite for verifying the correctness of the backend implementations and translation logic.requirements.txt: A file listing the Python dependencies required by the project.
src/claude_openai_forwarder/app.py: FastAPI entrypointsrc/claude_openai_forwarder/translators/: request, response, and streaming translationsrc/claude_openai_forwarder/backends/: backend abstraction and implementationstests/test_backends.py: backend and translator testsrequirements.txt: Python dependencies
Backend Modes
The project supports two backend modes:
httpx: Use when the upstream exposes a compatible OpenAI-style/chat/completionsAPI.- Characteristics: direct HTTP calls, minimal provider-specific behavior, ignores
MODEL_PROVIDER - Example:
BACKEND_TYPE=httpx OPENAI_API_KEY=... OPENAI_BASE_URL=https://api.openai.com/v1 DEFAULT_OPENAI_MODEL=gpt-4o-mini FORCE_TOOL_IN_PROMPT=false
- Characteristics: direct HTTP calls, minimal provider-specific behavior, ignores
litellm: Use when provider-specific routing or compatibility behavior matters.- Characteristics: provider-aware routing, model/provider normalization, uses
MODEL_PROVIDER - Example (for NVIDIA NIM):
BACKEND_TYPE=litellm OPENAI_API_KEY=... OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1 MODEL_PROVIDER=nvidia_nim DEFAULT_OPENAI_MODEL=meta/llama-4-maverick-17b-128e-instruct FORCE_TOOL_IN_PROMPT=true
- Characteristics: provider-aware routing, model/provider normalization, uses
MODEL_PROVIDER
MODEL_PROVIDER is a LiteLLM-only setting.
Rules:
BACKEND_TYPE=httpx: do not rely onMODEL_PROVIDERBACKEND_TYPE=litellmwith OpenAI:MODEL_PROVIDER=openaior unsetBACKEND_TYPE=litellmwith NVIDIA NIM:MODEL_PROVIDER=nvidia_nim- any other LiteLLM provider: set
MODEL_PROVIDERto the provider name LiteLLM expects
In this repo, explicit MODEL_PROVIDER is preferred over relying on model-name prefix guessing.
FORCE_TOOL_IN_PROMPT
FORCE_TOOL_IN_PROMPT controls whether tool definitions and tool history are translated into prompt text instead of native OpenAI tools, tool_calls, and tool messages.
Set it per provider/tool support:
- OpenAI via
httpx- usually
FORCE_TOOL_IN_PROMPT=false
- usually
- OpenAI via
litellm- usually
FORCE_TOOL_IN_PROMPT=false
- usually
- NVIDIA NIM via
litellm- use
FORCE_TOOL_IN_PROMPT=true
- use
- NVIDIA NIM via
httpx- use
trueif native tool calling is unreliable for the selected model - use
falseif the upstream behaves correctly with native OpenAI tool calls
- use
- any provider without reliable native tool-calling support
- use
FORCE_TOOL_IN_PROMPT=true
- use
Practical rule:
- if the provider supports native OpenAI
toolsandtool_callsend to end, usefalse - if the provider emits tool use as text, or fails native multi-turn tool-calling flows, use
true
OpenAI API vs NVIDIA NIM
In this project, the practical difference is:
- OpenAI API is the native request/response target for the proxy
- NVIDIA NIM exposes an OpenAI-compatible HTTP surface, so raw
httpxcalls can still work - LiteLLM still needs to know the real provider for NVIDIA NIM, which is why
MODEL_PROVIDER=nvidia_nimmatters - some NVIDIA model flows are more reliable with prompt-embedded tool instructions than native tool calling, which is why this repo currently uses
FORCE_TOOL_IN_PROMPT=truefor the NVIDIA + LiteLLM example
Token Counting Endpoint
POST /v1/messages/count_tokens
Counts tokens for a given message payload without generating a response. Returns a Claude‑compatible response containing only the usage field (input tokens and output_tokens = 0).
Example
curl http://127.0.0.1:8000/v1/messages/count_tokens \
-H "Content-Type: application/json" \
-H "x-api-key: $CLAUDE_API_KEY" \
-d '{
"model": "claude-3-5-sonnet-20241022",
"messages": [
{"role":"user","content":"Hello"},
{"role":"assistant","content":"Hi!"}
]
}'
Response:
{
"id": "...",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": null,
"usage": {"input_tokens": 12, "output_tokens": 0}
}
The proxy is designed to preserve Claude-compatible tool calling semantics.
Important requirements:
- Claude tool calls must be returned as content blocks with
type="tool_use" - Claude tool-calling responses must end with
stop_reason="tool_use"
Current verified behavior:
- non-streaming tool calls work on both backends
- streaming tool calls work on both backends
- mixed streaming output that contains prose followed by embedded JSON
tool_useis translated into proper Claude tool-use events - tool handling is generic and not limited to specific tool names such as
BashorGlob
Verified Findings
During backend verification, these problems were found and resolved:
- LiteLLM provider routing was too dependent on model-prefix guessing
- LiteLLM responses could contain object-shaped tool calls and stream deltas that needed normalization
- some models emitted explanatory text followed by embedded JSON
tool_use, which initially surfaced as plain text instead of Claude tool calls
Current verified state:
httpxbackend workslitellmbackend works- backend tests pass with the current changes
Installation
Requirements:
- Python 3.11+
uvrecommended
Install from source:
source ~/.bashrc
UV_CACHE_DIR=/tmp/uv-cache uv pip install -r requirements.txt
cp .env.example .env
Build and install from wheel:
uv pip install .
uv build --wheel
uv pip install dist/claude_openai_forwarder-0.1.0-py3-none-any.whl
Running The API
From Source
uvicorn src.claude_to_openai_forwarder.app:app --port 8005
From installed wheel:
claude-to-openai-forwarder
Or specify a different port:
claude_to_openai_forwarder --port 8002
Default endpoint:
http://127.0.0.1:8000/v1/messages
Configuration
The project is configured using the following environment variables:
| Variable | Description | Default Value |
|---|---|---|
BACKEND_TYPE |
The backend to use (either httpx or litellm) |
httpx |
OPENAI_API_KEY |
The OpenAI API key | None |
OPENAI_BASE_URL |
The base URL for the OpenAI API | https://api.openai.com/v1 |
CLAUDE_API_KEY |
Optional inbound API key required from clients via x-api-key |
None |
MODEL_PROVIDER |
The model provider (used with litellm backend) |
None |
DEFAULT_OPENAI_MODEL |
The default OpenAI model to use | gpt-4o-mini |
FORCE_TOOL_IN_PROMPT |
Whether to embed tool definitions in the prompt | false |
HOST |
The host to bind the API to | 0.0.0.0 |
PORT |
The port to bind the API to | 8000 |
LOG_LEVEL |
The logging level | INFO |
The backend reads configuration from .env.
Key variables:
BACKEND_TYPEOPENAI_API_KEYOPENAI_BASE_URLCLAUDE_API_KEYMODEL_PROVIDERDEFAULT_OPENAI_MODELFORCE_TOOL_IN_PROMPTHOSTPORTLOG_LEVEL
See .env.example for a working example.
Inbound Auth
Clients call this proxy with the x-api-key header.
Behavior:
- if
CLAUDE_API_KEYis set, the header must exactly match that value - if
CLAUDE_API_KEYis unset, the proxy falls back to permissive local/test auth behavior
Example:
curl -X POST http://127.0.0.1:8000/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: sk-ant-test123" \
-d '{"model":"claude-3-5-sonnet-20241022","max_tokens":64,"messages":[{"role":"user","content":"hello"}]}'
Claude Client Settings
If you want a Claude client that reads ~/.claude/settings.json to use this proxy, configure the env.ANTHROPIC_* values to point at the local service.
Example ~/.claude/settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8000",
"ANTHROPIC_API_KEY": "sk-ant-test123",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-3-5-sonnet-20241022",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-3-5-sonnet-20241022",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-3-5-sonnet-20241022"
}
}
Notes:
ANTHROPIC_BASE_URLshould point to the proxy root URL, not/v1/messagesANTHROPIC_API_KEYshould match the proxy'sCLAUDE_API_KEY- the Claude model names in the client config should stay Claude-style names
- the proxy translates those Claude model names using
CLAUDE_MODEL_MAPorDEFAULT_OPENAI_MODEL
Verification
Recommended verification commands in this workspace:
source ~/.bashrc
UV_CACHE_DIR=/tmp/uv-cache uv run python -m compileall app
UV_CACHE_DIR=/tmp/uv-cache uv run python -m unittest tests.test_backends
Optional manual check:
./test_with_tools.sh
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_to_openai_forwarder-0.1.0-py3-none-any.whl.
File metadata
- Download URL: claude_to_openai_forwarder-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99817ebdc94f7293169cf24ddbcfa65f34a8a819534c47f6c172fd849d814b06
|
|
| MD5 |
06335626155fffe4ed2dffa08a9ff7b5
|
|
| BLAKE2b-256 |
4a5055871b189206124ea5575a70a853d6c8b90d89f0494834b40e868efefa33
|