[
  {
    "path": ".dockerignore",
    "content": "# Git\n.git\n.gitignore\n.gitattributes\n\n\n# CI\n.codeclimate.yml\n.travis.yml\n.taskcluster.yml\n\n# Docker\ndocker-compose.yml\nDockerfile\n.docker\n.dockerignore\n\n# Byte-compiled / optimized / DLL files\n**/__pycache__/\n**/*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Virtual environment\n.env\n.venv/\nvenv/\n\n# PyCharm\n.idea\n\n# Python mode for VIM\n.ropeproject\n**/.ropeproject\n\n# Vim swap files\n**/*.swp\n\n# VS Code\n.vscode/\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: publish\non:\n  push:\n    branches:\n      - main\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n      contents: read\n\n    steps:\n      - uses: actions/checkout@main\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@master\n        with:\n          platforms: all\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@master\n\n      - name: Log in to Github's container registry\n        run: echo \"${{ secrets.GITHUB_TOKEN }}\" | docker login ghcr.io -u $ --password-stdin\n\n      # Docker images have to have lowercase names, and Github Actions doesn't\n      # have template functions\n      - name: Collect image metadata\n        id: meta\n        uses: docker/metadata-action@master\n        with:\n          images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}\n\n      - name: Build\n        uses: docker/build-push-action@master\n        with:\n          context: .\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Environment variables\n.env\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# Virtual environments\nvenv/\nenv/\nENV/\n\n# Logs\n*.log\n\n# IDE specific files\n.idea/\n.vscode/\n*.swp\n*.swo"
  },
  {
    "path": ".python-version",
    "content": "3.10\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:latest\n\nWORKDIR /claude-code-proxy\n\n# Copy package specifications\nCOPY pyproject.toml uv.lock ./\n\n# Install uv and project dependencies\nRUN pip install --upgrade uv && uv sync --locked\n\n# Copy project code to current directory\nCOPY . .\n\n# Start the proxy\nEXPOSE 8082\nCMD uv run uvicorn server:app --host 0.0.0.0 --port 8082 --reload\n"
  },
  {
    "path": "README.md",
    "content": "# Anthropic API Proxy for Gemini & OpenAI Models 🔄\n\n**Use Anthropic clients (like Claude Code) with Gemini, OpenAI, or direct Anthropic backends.** 🤝\n\nA proxy server that lets you use Anthropic clients with Gemini, OpenAI, or Anthropic models themselves (a transparent proxy of sorts), all via LiteLLM. 🌉\n\n\n![Anthropic API Proxy](pic.png)\n\n## Quick Start ⚡\n\n### Prerequisites\n\n- OpenAI API key 🔑\n- Google AI Studio (Gemini) API key (if using Google provider) 🔑\n- Google Cloud Project with Vertex AI API enabled (if using Application Default Credentials for Gemini) ☁️\n- [uv](https://github.com/astral-sh/uv) installed.\n\n### Setup 🛠️\n\n#### From source\n\n1. **Clone this repository**:\n   ```bash\n   git clone https://github.com/1rgs/claude-code-proxy.git\n   cd claude-code-proxy\n   ```\n\n2. **Install uv** (if you haven't already):\n   ```bash\n   curl -LsSf https://astral.sh/uv/install.sh | sh\n   ```\n   *(`uv` will handle dependencies based on `pyproject.toml` when you run the server)*\n\n3. **Configure Environment Variables**:\n   Copy the example environment file:\n   ```bash\n   cp .env.example .env\n   ```\n   Edit `.env` and fill in your API keys and model configurations:\n\n   *   `ANTHROPIC_API_KEY`: (Optional) Needed only if proxying *to* Anthropic models.\n   *   `OPENAI_API_KEY`: Your OpenAI API key (Required if using the default OpenAI preference or as fallback).\n   *   `GEMINI_API_KEY`: Your Google AI Studio (Gemini) API key (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`).\n   *   `USE_VERTEX_AUTH` (Optional): Set to `true` to use Application Default Credentials (ADC) will be used (no static API key required). Note: when USE_VERTEX_AUTH=true, you must configure `VERTEX_PROJECT` and `VERTEX_LOCATION`.\n   *   `VERTEX_PROJECT` (Optional): Your Google Cloud Project ID (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`).\n   *   `VERTEX_LOCATION` (Optional): The Google Cloud region for Vertex AI (e.g., `us-central1`) (Required if `PREFERRED_PROVIDER=google` and `USE_VERTEX_AUTH=true`).\n   *   `PREFERRED_PROVIDER` (Optional): Set to `openai` (default), `google`, or `anthropic`. This determines the primary backend for mapping `haiku`/`sonnet`.\n   *   `BIG_MODEL` (Optional): The model to map `sonnet` requests to. Defaults to `gpt-4.1` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.5-pro-preview-03-25`. Ignored when `PREFERRED_PROVIDER=anthropic`.\n   *   `SMALL_MODEL` (Optional): The model to map `haiku` requests to. Defaults to `gpt-4.1-mini` (if `PREFERRED_PROVIDER=openai`) or `gemini-2.0-flash`. Ignored when `PREFERRED_PROVIDER=anthropic`.\n\n   **Mapping Logic:**\n   - If `PREFERRED_PROVIDER=openai` (default), `haiku`/`sonnet` map to `SMALL_MODEL`/`BIG_MODEL` prefixed with `openai/`.\n   - If `PREFERRED_PROVIDER=google`, `haiku`/`sonnet` map to `SMALL_MODEL`/`BIG_MODEL` prefixed with `gemini/` *if* those models are in the server's known `GEMINI_MODELS` list (otherwise falls back to OpenAI mapping).\n   - If `PREFERRED_PROVIDER=anthropic`, `haiku`/`sonnet` requests are passed directly to Anthropic with the `anthropic/` prefix without remapping to different models.\n\n4. **Run the server**:\n   ```bash\n   uv run uvicorn server:app --host 0.0.0.0 --port 8082 --reload\n   ```\n   *(`--reload` is optional, for development)*\n\n#### Docker\n\nIf using docker, download the example environment file to `.env` and edit it as described above.\n```bash\ncurl -O .env https://raw.githubusercontent.com/1rgs/claude-code-proxy/refs/heads/main/.env.example\n```\n\nThen, you can either start the container with [docker compose](https://docs.docker.com/compose/) (preferred):\n\n```yml\nservices:\n  proxy:\n    image: ghcr.io/1rgs/claude-code-proxy:latest\n    restart: unless-stopped\n    env_file: .env\n    ports:\n      - 8082:8082\n```\n\nOr with a command:\n\n```bash\ndocker run -d --env-file .env -p 8082:8082 ghcr.io/1rgs/claude-code-proxy:latest\n```\n\n### Using with Claude Code 🎮\n\n1. **Install Claude Code** (if you haven't already):\n   ```bash\n   npm install -g @anthropic-ai/claude-code\n   ```\n\n2. **Connect to your proxy**:\n   ```bash\n   ANTHROPIC_BASE_URL=http://localhost:8082 claude\n   ```\n\n3. **That's it!** Your Claude Code client will now use the configured backend models (defaulting to Gemini) through the proxy. 🎯\n\n## Model Mapping 🗺️\n\nThe proxy automatically maps Claude models to either OpenAI or Gemini models based on the configured model:\n\n| Claude Model | Default Mapping | When BIG_MODEL/SMALL_MODEL is a Gemini model |\n|--------------|--------------|---------------------------|\n| haiku | openai/gpt-4o-mini | gemini/[model-name] |\n| sonnet | openai/gpt-4o | gemini/[model-name] |\n\n### Supported Models\n\n#### OpenAI Models\nThe following OpenAI models are supported with automatic `openai/` prefix handling:\n- o3-mini\n- o1\n- o1-mini\n- o1-pro\n- gpt-4.5-preview\n- gpt-4o\n- gpt-4o-audio-preview\n- chatgpt-4o-latest\n- gpt-4o-mini\n- gpt-4o-mini-audio-preview\n- gpt-4.1\n- gpt-4.1-mini\n\n#### Gemini Models\nThe following Gemini models are supported with automatic `gemini/` prefix handling:\n- gemini-2.5-pro\n- gemini-2.5-flash\n\n### Model Prefix Handling\nThe proxy automatically adds the appropriate prefix to model names:\n- OpenAI models get the `openai/` prefix\n- Gemini models get the `gemini/` prefix\n- The BIG_MODEL and SMALL_MODEL will get the appropriate prefix based on whether they're in the OpenAI or Gemini model lists\n\nFor example:\n- `gpt-4o` becomes `openai/gpt-4o`\n- `gemini-2.5-pro-preview-03-25` becomes `gemini/gemini-2.5-pro-preview-03-25`\n- When BIG_MODEL is set to a Gemini model, Claude Sonnet will map to `gemini/[model-name]`\n\n### Customizing Model Mapping\n\nControl the mapping using environment variables in your `.env` file or directly:\n\n**Example 1: Default (Use OpenAI)**\nNo changes needed in `.env` beyond API keys, or ensure:\n```dotenv\nOPENAI_API_KEY=\"your-openai-key\"\nGEMINI_API_KEY=\"your-google-key\" # Needed if PREFERRED_PROVIDER=google\n# PREFERRED_PROVIDER=\"openai\" # Optional, it's the default\n# BIG_MODEL=\"gpt-4.1\" # Optional, it's the default\n# SMALL_MODEL=\"gpt-4.1-mini\" # Optional, it's the default\n```\n\n**Example 2a: Prefer Google (using GEMINI_API_KEY)**\n```dotenv\nGEMINI_API_KEY=\"your-google-key\"\nOPENAI_API_KEY=\"your-openai-key\" # Needed for fallback\nPREFERRED_PROVIDER=\"google\"\n# BIG_MODEL=\"gemini-2.5-pro\" # Optional, it's the default for Google pref\n# SMALL_MODEL=\"gemini-2.5-flash\" # Optional, it's the default for Google pref\n```\n\n**Example 2b: Prefer Google (using Vertex AI with Application Default Credentials)**\n```dotenv\nOPENAI_API_KEY=\"your-openai-key\" # Needed for fallback\nPREFERRED_PROVIDER=\"google\"\nVERTEX_PROJECT=\"your-gcp-project-id\"\nVERTEX_LOCATION=\"us-central1\"\nUSE_VERTEX_AUTH=true\n# BIG_MODEL=\"gemini-2.5-pro\" # Optional, it's the default for Google pref\n# SMALL_MODEL=\"gemini-2.5-flash\" # Optional, it's the default for Google pref\n```\n\n**Example 3: Use Direct Anthropic (\"Just an Anthropic Proxy\" Mode)**\n```dotenv\nANTHROPIC_API_KEY=\"sk-ant-...\"\nPREFERRED_PROVIDER=\"anthropic\"\n# BIG_MODEL and SMALL_MODEL are ignored in this mode\n# haiku/sonnet requests are passed directly to Anthropic models\n```\n\n*Use case: This mode enables you to use the proxy infrastructure (for logging, middleware, request/response processing, etc.) while still using actual Anthropic models rather than being forced to remap to OpenAI or Gemini.*\n\n**Example 4: Use Specific OpenAI Models**\n```dotenv\nOPENAI_API_KEY=\"your-openai-key\"\nGEMINI_API_KEY=\"your-google-key\"\nPREFERRED_PROVIDER=\"openai\"\nBIG_MODEL=\"gpt-4o\" # Example specific model\nSMALL_MODEL=\"gpt-4o-mini\" # Example specific model\n```\n\n## How It Works 🧩\n\nThis proxy works by:\n\n1. **Receiving requests** in Anthropic's API format 📥\n2. **Translating** the requests to OpenAI format via LiteLLM 🔄\n3. **Sending** the translated request to OpenAI 📤\n4. **Converting** the response back to Anthropic format 🔄\n5. **Returning** the formatted response to the client ✅\n\nThe proxy handles both streaming and non-streaming responses, maintaining compatibility with all Claude clients. 🌊\n\n## Contributing 🤝\n\nContributions are welcome! Please feel free to submit a Pull Request. 🎁\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"anthropic-proxy\"\nversion = \"0.1.0\"\ndescription = \"Proxy that translates between Anthropic API and LiteLLM\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"fastapi[standard]>=0.115.11\",\n    \"uvicorn>=0.34.0\",\n    \"httpx>=0.25.0\",\n    \"pydantic>=2.0.0\",\n    \"litellm>=1.77.7\",\n    \"python-dotenv>=1.0.0\",\n    \"google-auth>=2.41.1\",\n    \"google-cloud-aiplatform>=1.120.0\",\n]\n\n"
  },
  {
    "path": "server.py",
    "content": "from fastapi import FastAPI, Request, HTTPException\nimport uvicorn\nimport logging\nimport json\nfrom pydantic import BaseModel, Field, field_validator\nfrom typing import List, Dict, Any, Optional, Union, Literal\nimport httpx\nimport os\nfrom fastapi.responses import JSONResponse, StreamingResponse\nimport litellm\nimport uuid\nimport time\nfrom dotenv import load_dotenv\nimport re\nfrom datetime import datetime\nimport sys\n\n# Load environment variables from .env file\nload_dotenv()\n\n# Configure logging\nlogging.basicConfig(\n    level=logging.WARN,  # Change to INFO level to show more details\n    format='%(asctime)s - %(levelname)s - %(message)s',\n)\nlogger = logging.getLogger(__name__)\n\n# Configure uvicorn to be quieter\nimport uvicorn\n# Tell uvicorn's loggers to be quiet\nlogging.getLogger(\"uvicorn\").setLevel(logging.WARNING)\nlogging.getLogger(\"uvicorn.access\").setLevel(logging.WARNING)\nlogging.getLogger(\"uvicorn.error\").setLevel(logging.WARNING)\n\n# Create a filter to block any log messages containing specific strings\nclass MessageFilter(logging.Filter):\n    def filter(self, record):\n        # Block messages containing these strings\n        blocked_phrases = [\n            \"LiteLLM completion()\",\n            \"HTTP Request:\", \n            \"selected model name for cost calculation\",\n            \"utils.py\",\n            \"cost_calculator\"\n        ]\n        \n        if hasattr(record, 'msg') and isinstance(record.msg, str):\n            for phrase in blocked_phrases:\n                if phrase in record.msg:\n                    return False\n        return True\n\n# Apply the filter to the root logger to catch all messages\nroot_logger = logging.getLogger()\nroot_logger.addFilter(MessageFilter())\n\n# Custom formatter for model mapping logs\nclass ColorizedFormatter(logging.Formatter):\n    \"\"\"Custom formatter to highlight model mappings\"\"\"\n    BLUE = \"\\033[94m\"\n    GREEN = \"\\033[92m\"\n    YELLOW = \"\\033[93m\"\n    RED = \"\\033[91m\"\n    RESET = \"\\033[0m\"\n    BOLD = \"\\033[1m\"\n    \n    def format(self, record):\n        if record.levelno == logging.debug and \"MODEL MAPPING\" in record.msg:\n            # Apply colors and formatting to model mapping logs\n            return f\"{self.BOLD}{self.GREEN}{record.msg}{self.RESET}\"\n        return super().format(record)\n\n# Apply custom formatter to console handler\nfor handler in logger.handlers:\n    if isinstance(handler, logging.StreamHandler):\n        handler.setFormatter(ColorizedFormatter('%(asctime)s - %(levelname)s - %(message)s'))\n\napp = FastAPI()\n\n# Get API keys from environment\nANTHROPIC_API_KEY = os.environ.get(\"ANTHROPIC_API_KEY\")\nOPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")\nGEMINI_API_KEY = os.environ.get(\"GEMINI_API_KEY\")\n\n# Get Vertex AI project and location from environment (if set)\nVERTEX_PROJECT = os.environ.get(\"VERTEX_PROJECT\", \"unset\")\nVERTEX_LOCATION = os.environ.get(\"VERTEX_LOCATION\", \"unset\")\n\n# Option to use Gemini API key instead of ADC for Vertex AI\nUSE_VERTEX_AUTH = os.environ.get(\"USE_VERTEX_AUTH\", \"False\").lower() == \"true\"\n\n# Get OpenAI base URL from environment (if set)\nOPENAI_BASE_URL = os.environ.get(\"OPENAI_BASE_URL\")\n\n# Get preferred provider (default to openai)\nPREFERRED_PROVIDER = os.environ.get(\"PREFERRED_PROVIDER\", \"openai\").lower()\n\n# Get model mapping configuration from environment\n# Default to latest OpenAI models if not set\nBIG_MODEL = os.environ.get(\"BIG_MODEL\", \"gpt-4.1\")\nSMALL_MODEL = os.environ.get(\"SMALL_MODEL\", \"gpt-4.1-mini\")\n\n# List of OpenAI models\nOPENAI_MODELS = [\n    \"o3-mini\",\n    \"o1\",\n    \"o1-mini\",\n    \"o1-pro\",\n    \"gpt-4.5-preview\",\n    \"gpt-4o\",\n    \"gpt-4o-audio-preview\",\n    \"chatgpt-4o-latest\",\n    \"gpt-4o-mini\",\n    \"gpt-4o-mini-audio-preview\",\n    \"gpt-4.1\",  # Added default big model\n    \"gpt-4.1-mini\" # Added default small model\n]\n\n# List of Gemini models\nGEMINI_MODELS = [\n    \"gemini-2.5-flash\",\n    \"gemini-2.5-pro\"\n]\n\n# Helper function to clean schema for Gemini\ndef clean_gemini_schema(schema: Any) -> Any:\n    \"\"\"Recursively removes unsupported fields from a JSON schema for Gemini.\"\"\"\n    if isinstance(schema, dict):\n        # Remove specific keys unsupported by Gemini tool parameters\n        schema.pop(\"additionalProperties\", None)\n        schema.pop(\"default\", None)\n\n        # Check for unsupported 'format' in string types\n        if schema.get(\"type\") == \"string\" and \"format\" in schema:\n            allowed_formats = {\"enum\", \"date-time\"}\n            if schema[\"format\"] not in allowed_formats:\n                logger.debug(f\"Removing unsupported format '{schema['format']}' for string type in Gemini schema.\")\n                schema.pop(\"format\")\n\n        # Recursively clean nested schemas (properties, items, etc.)\n        for key, value in list(schema.items()): # Use list() to allow modification during iteration\n            schema[key] = clean_gemini_schema(value)\n    elif isinstance(schema, list):\n        # Recursively clean items in a list\n        return [clean_gemini_schema(item) for item in schema]\n    return schema\n\n# Models for Anthropic API requests\nclass ContentBlockText(BaseModel):\n    type: Literal[\"text\"]\n    text: str\n\nclass ContentBlockImage(BaseModel):\n    type: Literal[\"image\"]\n    source: Dict[str, Any]\n\nclass ContentBlockToolUse(BaseModel):\n    type: Literal[\"tool_use\"]\n    id: str\n    name: str\n    input: Dict[str, Any]\n\nclass ContentBlockToolResult(BaseModel):\n    type: Literal[\"tool_result\"]\n    tool_use_id: str\n    content: Union[str, List[Dict[str, Any]], Dict[str, Any], List[Any], Any]\n\nclass SystemContent(BaseModel):\n    type: Literal[\"text\"]\n    text: str\n\nclass Message(BaseModel):\n    role: Literal[\"user\", \"assistant\"] \n    content: Union[str, List[Union[ContentBlockText, ContentBlockImage, ContentBlockToolUse, ContentBlockToolResult]]]\n\nclass Tool(BaseModel):\n    name: str\n    description: Optional[str] = None\n    input_schema: Dict[str, Any]\n\nclass ThinkingConfig(BaseModel):\n    enabled: bool = True\n\nclass MessagesRequest(BaseModel):\n    model: str\n    max_tokens: int\n    messages: List[Message]\n    system: Optional[Union[str, List[SystemContent]]] = None\n    stop_sequences: Optional[List[str]] = None\n    stream: Optional[bool] = False\n    temperature: Optional[float] = 1.0\n    top_p: Optional[float] = None\n    top_k: Optional[int] = None\n    metadata: Optional[Dict[str, Any]] = None\n    tools: Optional[List[Tool]] = None\n    tool_choice: Optional[Dict[str, Any]] = None\n    thinking: Optional[ThinkingConfig] = None\n    original_model: Optional[str] = None  # Will store the original model name\n    \n    @field_validator('model')\n    def validate_model_field(cls, v, info): # Renamed to avoid conflict\n        original_model = v\n        new_model = v # Default to original value\n\n        logger.debug(f\"📋 MODEL VALIDATION: Original='{original_model}', Preferred='{PREFERRED_PROVIDER}', BIG='{BIG_MODEL}', SMALL='{SMALL_MODEL}'\")\n\n        # Remove provider prefixes for easier matching\n        clean_v = v\n        if clean_v.startswith('anthropic/'):\n            clean_v = clean_v[10:]\n        elif clean_v.startswith('openai/'):\n            clean_v = clean_v[7:]\n        elif clean_v.startswith('gemini/'):\n            clean_v = clean_v[7:]\n\n        # --- Mapping Logic --- START ---\n        mapped = False\n        if PREFERRED_PROVIDER == \"anthropic\":\n            # Don't remap to big/small models, just add the prefix\n            new_model = f\"anthropic/{clean_v}\"\n            mapped = True\n\n        # Map Haiku to SMALL_MODEL based on provider preference\n        elif 'haiku' in clean_v.lower():\n            if PREFERRED_PROVIDER == \"google\" and SMALL_MODEL in GEMINI_MODELS:\n                new_model = f\"gemini/{SMALL_MODEL}\"\n                mapped = True\n            else:\n                new_model = f\"openai/{SMALL_MODEL}\"\n                mapped = True\n\n        # Map Sonnet to BIG_MODEL based on provider preference\n        elif 'sonnet' in clean_v.lower():\n            if PREFERRED_PROVIDER == \"google\" and BIG_MODEL in GEMINI_MODELS:\n                new_model = f\"gemini/{BIG_MODEL}\"\n                mapped = True\n            else:\n                new_model = f\"openai/{BIG_MODEL}\"\n                mapped = True\n\n        # Add prefixes to non-mapped models if they match known lists\n        elif not mapped:\n            if clean_v in GEMINI_MODELS and not v.startswith('gemini/'):\n                new_model = f\"gemini/{clean_v}\"\n                mapped = True # Technically mapped to add prefix\n            elif clean_v in OPENAI_MODELS and not v.startswith('openai/'):\n                new_model = f\"openai/{clean_v}\"\n                mapped = True # Technically mapped to add prefix\n        # --- Mapping Logic --- END ---\n\n        if mapped:\n            logger.debug(f\"📌 MODEL MAPPING: '{original_model}' ➡️ '{new_model}'\")\n        else:\n             # If no mapping occurred and no prefix exists, log warning or decide default\n             if not v.startswith(('openai/', 'gemini/', 'anthropic/')):\n                 logger.warning(f\"⚠️ No prefix or mapping rule for model: '{original_model}'. Using as is.\")\n             new_model = v # Ensure we return the original if no rule applied\n\n        # Store the original model in the values dictionary\n        values = info.data\n        if isinstance(values, dict):\n            values['original_model'] = original_model\n\n        return new_model\n\nclass TokenCountRequest(BaseModel):\n    model: str\n    messages: List[Message]\n    system: Optional[Union[str, List[SystemContent]]] = None\n    tools: Optional[List[Tool]] = None\n    thinking: Optional[ThinkingConfig] = None\n    tool_choice: Optional[Dict[str, Any]] = None\n    original_model: Optional[str] = None  # Will store the original model name\n    \n    @field_validator('model')\n    def validate_model_token_count(cls, v, info): # Renamed to avoid conflict\n        # Use the same logic as MessagesRequest validator\n        # NOTE: Pydantic validators might not share state easily if not class methods\n        # Re-implementing the logic here for clarity, could be refactored\n        original_model = v\n        new_model = v # Default to original value\n\n        logger.debug(f\"📋 TOKEN COUNT VALIDATION: Original='{original_model}', Preferred='{PREFERRED_PROVIDER}', BIG='{BIG_MODEL}', SMALL='{SMALL_MODEL}'\")\n\n        # Remove provider prefixes for easier matching\n        clean_v = v\n        if clean_v.startswith('anthropic/'):\n            clean_v = clean_v[10:]\n        elif clean_v.startswith('openai/'):\n            clean_v = clean_v[7:]\n        elif clean_v.startswith('gemini/'):\n            clean_v = clean_v[7:]\n\n        # --- Mapping Logic --- START ---\n        mapped = False\n        # Map Haiku to SMALL_MODEL based on provider preference\n        if 'haiku' in clean_v.lower():\n            if PREFERRED_PROVIDER == \"google\" and SMALL_MODEL in GEMINI_MODELS:\n                new_model = f\"gemini/{SMALL_MODEL}\"\n                mapped = True\n            else:\n                new_model = f\"openai/{SMALL_MODEL}\"\n                mapped = True\n\n        # Map Sonnet to BIG_MODEL based on provider preference\n        elif 'sonnet' in clean_v.lower():\n            if PREFERRED_PROVIDER == \"google\" and BIG_MODEL in GEMINI_MODELS:\n                new_model = f\"gemini/{BIG_MODEL}\"\n                mapped = True\n            else:\n                new_model = f\"openai/{BIG_MODEL}\"\n                mapped = True\n\n        # Add prefixes to non-mapped models if they match known lists\n        elif not mapped:\n            if clean_v in GEMINI_MODELS and not v.startswith('gemini/'):\n                new_model = f\"gemini/{clean_v}\"\n                mapped = True # Technically mapped to add prefix\n            elif clean_v in OPENAI_MODELS and not v.startswith('openai/'):\n                new_model = f\"openai/{clean_v}\"\n                mapped = True # Technically mapped to add prefix\n        # --- Mapping Logic --- END ---\n\n        if mapped:\n            logger.debug(f\"📌 TOKEN COUNT MAPPING: '{original_model}' ➡️ '{new_model}'\")\n        else:\n             if not v.startswith(('openai/', 'gemini/', 'anthropic/')):\n                 logger.warning(f\"⚠️ No prefix or mapping rule for token count model: '{original_model}'. Using as is.\")\n             new_model = v # Ensure we return the original if no rule applied\n\n        # Store the original model in the values dictionary\n        values = info.data\n        if isinstance(values, dict):\n            values['original_model'] = original_model\n\n        return new_model\n\nclass TokenCountResponse(BaseModel):\n    input_tokens: int\n\nclass Usage(BaseModel):\n    input_tokens: int\n    output_tokens: int\n    cache_creation_input_tokens: int = 0\n    cache_read_input_tokens: int = 0\n\nclass MessagesResponse(BaseModel):\n    id: str\n    model: str\n    role: Literal[\"assistant\"] = \"assistant\"\n    content: List[Union[ContentBlockText, ContentBlockToolUse]]\n    type: Literal[\"message\"] = \"message\"\n    stop_reason: Optional[Literal[\"end_turn\", \"max_tokens\", \"stop_sequence\", \"tool_use\"]] = None\n    stop_sequence: Optional[str] = None\n    usage: Usage\n\n@app.middleware(\"http\")\nasync def log_requests(request: Request, call_next):\n    # Get request details\n    method = request.method\n    path = request.url.path\n    \n    # Log only basic request details at debug level\n    logger.debug(f\"Request: {method} {path}\")\n    \n    # Process the request and get the response\n    response = await call_next(request)\n    \n    return response\n\n# Not using validation function as we're using the environment API key\n\ndef parse_tool_result_content(content):\n    \"\"\"Helper function to properly parse and normalize tool result content.\"\"\"\n    if content is None:\n        return \"No content provided\"\n        \n    if isinstance(content, str):\n        return content\n        \n    if isinstance(content, list):\n        result = \"\"\n        for item in content:\n            if isinstance(item, dict) and item.get(\"type\") == \"text\":\n                result += item.get(\"text\", \"\") + \"\\n\"\n            elif isinstance(item, str):\n                result += item + \"\\n\"\n            elif isinstance(item, dict):\n                if \"text\" in item:\n                    result += item.get(\"text\", \"\") + \"\\n\"\n                else:\n                    try:\n                        result += json.dumps(item) + \"\\n\"\n                    except:\n                        result += str(item) + \"\\n\"\n            else:\n                try:\n                    result += str(item) + \"\\n\"\n                except:\n                    result += \"Unparseable content\\n\"\n        return result.strip()\n        \n    if isinstance(content, dict):\n        if content.get(\"type\") == \"text\":\n            return content.get(\"text\", \"\")\n        try:\n            return json.dumps(content)\n        except:\n            return str(content)\n            \n    # Fallback for any other type\n    try:\n        return str(content)\n    except:\n        return \"Unparseable content\"\n\ndef convert_anthropic_to_litellm(anthropic_request: MessagesRequest) -> Dict[str, Any]:\n    \"\"\"Convert Anthropic API request format to LiteLLM format (which follows OpenAI).\"\"\"\n    # LiteLLM already handles Anthropic models when using the format model=\"anthropic/claude-3-opus-20240229\"\n    # So we just need to convert our Pydantic model to a dict in the expected format\n    \n    messages = []\n    \n    # Add system message if present\n    if anthropic_request.system:\n        # Handle different formats of system messages\n        if isinstance(anthropic_request.system, str):\n            # Simple string format\n            messages.append({\"role\": \"system\", \"content\": anthropic_request.system})\n        elif isinstance(anthropic_request.system, list):\n            # List of content blocks\n            system_text = \"\"\n            for block in anthropic_request.system:\n                if hasattr(block, 'type') and block.type == \"text\":\n                    system_text += block.text + \"\\n\\n\"\n                elif isinstance(block, dict) and block.get(\"type\") == \"text\":\n                    system_text += block.get(\"text\", \"\") + \"\\n\\n\"\n            \n            if system_text:\n                messages.append({\"role\": \"system\", \"content\": system_text.strip()})\n    \n    # Add conversation messages\n    for idx, msg in enumerate(anthropic_request.messages):\n        content = msg.content\n        if isinstance(content, str):\n            messages.append({\"role\": msg.role, \"content\": content})\n        else:\n            # Special handling for tool_result in user messages\n            # OpenAI/LiteLLM format expects the assistant to call the tool, \n            # and the user's next message to include the result as plain text\n            if msg.role == \"user\" and any(block.type == \"tool_result\" for block in content if hasattr(block, \"type\")):\n                # For user messages with tool_result, split into separate messages\n                text_content = \"\"\n                \n                # Extract all text parts and concatenate them\n                for block in content:\n                    if hasattr(block, \"type\"):\n                        if block.type == \"text\":\n                            text_content += block.text + \"\\n\"\n                        elif block.type == \"tool_result\":\n                            # Add tool result as a message by itself - simulate the normal flow\n                            tool_id = block.tool_use_id if hasattr(block, \"tool_use_id\") else \"\"\n                            \n                            # Handle different formats of tool result content\n                            result_content = \"\"\n                            if hasattr(block, \"content\"):\n                                if isinstance(block.content, str):\n                                    result_content = block.content\n                                elif isinstance(block.content, list):\n                                    # If content is a list of blocks, extract text from each\n                                    for content_block in block.content:\n                                        if hasattr(content_block, \"type\") and content_block.type == \"text\":\n                                            result_content += content_block.text + \"\\n\"\n                                        elif isinstance(content_block, dict) and content_block.get(\"type\") == \"text\":\n                                            result_content += content_block.get(\"text\", \"\") + \"\\n\"\n                                        elif isinstance(content_block, dict):\n                                            # Handle any dict by trying to extract text or convert to JSON\n                                            if \"text\" in content_block:\n                                                result_content += content_block.get(\"text\", \"\") + \"\\n\"\n                                            else:\n                                                try:\n                                                    result_content += json.dumps(content_block) + \"\\n\"\n                                                except:\n                                                    result_content += str(content_block) + \"\\n\"\n                                elif isinstance(block.content, dict):\n                                    # Handle dictionary content\n                                    if block.content.get(\"type\") == \"text\":\n                                        result_content = block.content.get(\"text\", \"\")\n                                    else:\n                                        try:\n                                            result_content = json.dumps(block.content)\n                                        except:\n                                            result_content = str(block.content)\n                                else:\n                                    # Handle any other type by converting to string\n                                    try:\n                                        result_content = str(block.content)\n                                    except:\n                                        result_content = \"Unparseable content\"\n                            \n                            # In OpenAI format, tool results come from the user (rather than being content blocks)\n                            text_content += f\"Tool result for {tool_id}:\\n{result_content}\\n\"\n                \n                # Add as a single user message with all the content\n                messages.append({\"role\": \"user\", \"content\": text_content.strip()})\n            else:\n                # Regular handling for other message types\n                processed_content = []\n                for block in content:\n                    if hasattr(block, \"type\"):\n                        if block.type == \"text\":\n                            processed_content.append({\"type\": \"text\", \"text\": block.text})\n                        elif block.type == \"image\":\n                            processed_content.append({\"type\": \"image\", \"source\": block.source})\n                        elif block.type == \"tool_use\":\n                            # Handle tool use blocks if needed\n                            processed_content.append({\n                                \"type\": \"tool_use\",\n                                \"id\": block.id,\n                                \"name\": block.name,\n                                \"input\": block.input\n                            })\n                        elif block.type == \"tool_result\":\n                            # Handle different formats of tool result content\n                            processed_content_block = {\n                                \"type\": \"tool_result\",\n                                \"tool_use_id\": block.tool_use_id if hasattr(block, \"tool_use_id\") else \"\"\n                            }\n                            \n                            # Process the content field properly\n                            if hasattr(block, \"content\"):\n                                if isinstance(block.content, str):\n                                    # If it's a simple string, create a text block for it\n                                    processed_content_block[\"content\"] = [{\"type\": \"text\", \"text\": block.content}]\n                                elif isinstance(block.content, list):\n                                    # If it's already a list of blocks, keep it\n                                    processed_content_block[\"content\"] = block.content\n                                else:\n                                    # Default fallback\n                                    processed_content_block[\"content\"] = [{\"type\": \"text\", \"text\": str(block.content)}]\n                            else:\n                                # Default empty content\n                                processed_content_block[\"content\"] = [{\"type\": \"text\", \"text\": \"\"}]\n                                \n                            processed_content.append(processed_content_block)\n                \n                messages.append({\"role\": msg.role, \"content\": processed_content})\n    \n    # Cap max_tokens for OpenAI models to their limit of 16384\n    max_tokens = anthropic_request.max_tokens\n    if anthropic_request.model.startswith(\"openai/\") or anthropic_request.model.startswith(\"gemini/\"):\n        max_tokens = min(max_tokens, 16384)\n        logger.debug(f\"Capping max_tokens to 16384 for OpenAI/Gemini model (original value: {anthropic_request.max_tokens})\")\n    \n    # Create LiteLLM request dict\n    litellm_request = {\n        \"model\": anthropic_request.model,  # it understands \"anthropic/claude-x\" format\n        \"messages\": messages,\n        \"max_completion_tokens\": max_tokens,\n        \"temperature\": anthropic_request.temperature,\n        \"stream\": anthropic_request.stream,\n    }\n\n    # Only include thinking field for Anthropic models\n    if anthropic_request.thinking and anthropic_request.model.startswith(\"anthropic/\"):\n        litellm_request[\"thinking\"] = anthropic_request.thinking\n\n    # Add optional parameters if present\n    if anthropic_request.stop_sequences:\n        litellm_request[\"stop\"] = anthropic_request.stop_sequences\n    \n    if anthropic_request.top_p:\n        litellm_request[\"top_p\"] = anthropic_request.top_p\n    \n    if anthropic_request.top_k:\n        litellm_request[\"top_k\"] = anthropic_request.top_k\n    \n    # Convert tools to OpenAI format\n    if anthropic_request.tools:\n        openai_tools = []\n        is_gemini_model = anthropic_request.model.startswith(\"gemini/\")\n\n        for tool in anthropic_request.tools:\n            # Convert to dict if it's a pydantic model\n            if hasattr(tool, 'dict'):\n                tool_dict = tool.dict()\n            else:\n                # Ensure tool_dict is a dictionary, handle potential errors if 'tool' isn't dict-like\n                try:\n                    tool_dict = dict(tool) if not isinstance(tool, dict) else tool\n                except (TypeError, ValueError):\n                     logger.error(f\"Could not convert tool to dict: {tool}\")\n                     continue # Skip this tool if conversion fails\n\n            # Clean the schema if targeting a Gemini model\n            input_schema = tool_dict.get(\"input_schema\", {})\n            if is_gemini_model:\n                 logger.debug(f\"Cleaning schema for Gemini tool: {tool_dict.get('name')}\")\n                 input_schema = clean_gemini_schema(input_schema)\n\n            # Create OpenAI-compatible function tool\n            openai_tool = {\n                \"type\": \"function\",\n                \"function\": {\n                    \"name\": tool_dict[\"name\"],\n                    \"description\": tool_dict.get(\"description\", \"\"),\n                    \"parameters\": input_schema # Use potentially cleaned schema\n                }\n            }\n            openai_tools.append(openai_tool)\n\n        litellm_request[\"tools\"] = openai_tools\n    \n    # Convert tool_choice to OpenAI format if present\n    if anthropic_request.tool_choice:\n        if hasattr(anthropic_request.tool_choice, 'dict'):\n            tool_choice_dict = anthropic_request.tool_choice.dict()\n        else:\n            tool_choice_dict = anthropic_request.tool_choice\n            \n        # Handle Anthropic's tool_choice format\n        choice_type = tool_choice_dict.get(\"type\")\n        if choice_type == \"auto\":\n            litellm_request[\"tool_choice\"] = \"auto\"\n        elif choice_type == \"any\":\n            litellm_request[\"tool_choice\"] = \"any\"\n        elif choice_type == \"tool\" and \"name\" in tool_choice_dict:\n            litellm_request[\"tool_choice\"] = {\n                \"type\": \"function\",\n                \"function\": {\"name\": tool_choice_dict[\"name\"]}\n            }\n        else:\n            # Default to auto if we can't determine\n            litellm_request[\"tool_choice\"] = \"auto\"\n    \n    return litellm_request\n\ndef convert_litellm_to_anthropic(litellm_response: Union[Dict[str, Any], Any], \n                                 original_request: MessagesRequest) -> MessagesResponse:\n    \"\"\"Convert LiteLLM (OpenAI format) response to Anthropic API response format.\"\"\"\n    \n    # Enhanced response extraction with better error handling\n    try:\n        # Get the clean model name to check capabilities\n        clean_model = original_request.model\n        if clean_model.startswith(\"anthropic/\"):\n            clean_model = clean_model[len(\"anthropic/\"):]\n        elif clean_model.startswith(\"openai/\"):\n            clean_model = clean_model[len(\"openai/\"):]\n        \n        # Check if this is a Claude model (which supports content blocks)\n        is_claude_model = clean_model.startswith(\"claude-\")\n        \n        # Handle ModelResponse object from LiteLLM\n        if hasattr(litellm_response, 'choices') and hasattr(litellm_response, 'usage'):\n            # Extract data from ModelResponse object directly\n            choices = litellm_response.choices\n            message = choices[0].message if choices and len(choices) > 0 else None\n            content_text = message.content if message and hasattr(message, 'content') else \"\"\n            tool_calls = message.tool_calls if message and hasattr(message, 'tool_calls') else None\n            finish_reason = choices[0].finish_reason if choices and len(choices) > 0 else \"stop\"\n            usage_info = litellm_response.usage\n            response_id = getattr(litellm_response, 'id', f\"msg_{uuid.uuid4()}\")\n        else:\n            # For backward compatibility - handle dict responses\n            # If response is a dict, use it, otherwise try to convert to dict\n            try:\n                response_dict = litellm_response if isinstance(litellm_response, dict) else litellm_response.dict()\n            except AttributeError:\n                # If .dict() fails, try to use model_dump or __dict__ \n                try:\n                    response_dict = litellm_response.model_dump() if hasattr(litellm_response, 'model_dump') else litellm_response.__dict__\n                except AttributeError:\n                    # Fallback - manually extract attributes\n                    response_dict = {\n                        \"id\": getattr(litellm_response, 'id', f\"msg_{uuid.uuid4()}\"),\n                        \"choices\": getattr(litellm_response, 'choices', [{}]),\n                        \"usage\": getattr(litellm_response, 'usage', {})\n                    }\n                    \n            # Extract the content from the response dict\n            choices = response_dict.get(\"choices\", [{}])\n            message = choices[0].get(\"message\", {}) if choices and len(choices) > 0 else {}\n            content_text = message.get(\"content\", \"\")\n            tool_calls = message.get(\"tool_calls\", None)\n            finish_reason = choices[0].get(\"finish_reason\", \"stop\") if choices and len(choices) > 0 else \"stop\"\n            usage_info = response_dict.get(\"usage\", {})\n            response_id = response_dict.get(\"id\", f\"msg_{uuid.uuid4()}\")\n        \n        # Create content list for Anthropic format\n        content = []\n        \n        # Add text content block if present (text might be None or empty for pure tool call responses)\n        if content_text is not None and content_text != \"\":\n            content.append({\"type\": \"text\", \"text\": content_text})\n        \n        # Add tool calls if present (tool_use in Anthropic format) - only for Claude models\n        if tool_calls and is_claude_model:\n            logger.debug(f\"Processing tool calls: {tool_calls}\")\n            \n            # Convert to list if it's not already\n            if not isinstance(tool_calls, list):\n                tool_calls = [tool_calls]\n                \n            for idx, tool_call in enumerate(tool_calls):\n                logger.debug(f\"Processing tool call {idx}: {tool_call}\")\n                \n                # Extract function data based on whether it's a dict or object\n                if isinstance(tool_call, dict):\n                    function = tool_call.get(\"function\", {})\n                    tool_id = tool_call.get(\"id\", f\"tool_{uuid.uuid4()}\")\n                    name = function.get(\"name\", \"\")\n                    arguments = function.get(\"arguments\", \"{}\")\n                else:\n                    function = getattr(tool_call, \"function\", None)\n                    tool_id = getattr(tool_call, \"id\", f\"tool_{uuid.uuid4()}\")\n                    name = getattr(function, \"name\", \"\") if function else \"\"\n                    arguments = getattr(function, \"arguments\", \"{}\") if function else \"{}\"\n                \n                # Convert string arguments to dict if needed\n                if isinstance(arguments, str):\n                    try:\n                        arguments = json.loads(arguments)\n                    except json.JSONDecodeError:\n                        logger.warning(f\"Failed to parse tool arguments as JSON: {arguments}\")\n                        arguments = {\"raw\": arguments}\n                \n                logger.debug(f\"Adding tool_use block: id={tool_id}, name={name}, input={arguments}\")\n                \n                content.append({\n                    \"type\": \"tool_use\",\n                    \"id\": tool_id,\n                    \"name\": name,\n                    \"input\": arguments\n                })\n        elif tool_calls and not is_claude_model:\n            # For non-Claude models, convert tool calls to text format\n            logger.debug(f\"Converting tool calls to text for non-Claude model: {clean_model}\")\n            \n            # We'll append tool info to the text content\n            tool_text = \"\\n\\nTool usage:\\n\"\n            \n            # Convert to list if it's not already\n            if not isinstance(tool_calls, list):\n                tool_calls = [tool_calls]\n                \n            for idx, tool_call in enumerate(tool_calls):\n                # Extract function data based on whether it's a dict or object\n                if isinstance(tool_call, dict):\n                    function = tool_call.get(\"function\", {})\n                    tool_id = tool_call.get(\"id\", f\"tool_{uuid.uuid4()}\")\n                    name = function.get(\"name\", \"\")\n                    arguments = function.get(\"arguments\", \"{}\")\n                else:\n                    function = getattr(tool_call, \"function\", None)\n                    tool_id = getattr(tool_call, \"id\", f\"tool_{uuid.uuid4()}\")\n                    name = getattr(function, \"name\", \"\") if function else \"\"\n                    arguments = getattr(function, \"arguments\", \"{}\") if function else \"{}\"\n                \n                # Convert string arguments to dict if needed\n                if isinstance(arguments, str):\n                    try:\n                        args_dict = json.loads(arguments)\n                        arguments_str = json.dumps(args_dict, indent=2)\n                    except json.JSONDecodeError:\n                        arguments_str = arguments\n                else:\n                    arguments_str = json.dumps(arguments, indent=2)\n                \n                tool_text += f\"Tool: {name}\\nArguments: {arguments_str}\\n\\n\"\n            \n            # Add or append tool text to content\n            if content and content[0][\"type\"] == \"text\":\n                content[0][\"text\"] += tool_text\n            else:\n                content.append({\"type\": \"text\", \"text\": tool_text})\n        \n        # Get usage information - extract values safely from object or dict\n        if isinstance(usage_info, dict):\n            prompt_tokens = usage_info.get(\"prompt_tokens\", 0)\n            completion_tokens = usage_info.get(\"completion_tokens\", 0)\n        else:\n            prompt_tokens = getattr(usage_info, \"prompt_tokens\", 0)\n            completion_tokens = getattr(usage_info, \"completion_tokens\", 0)\n        \n        # Map OpenAI finish_reason to Anthropic stop_reason\n        stop_reason = None\n        if finish_reason == \"stop\":\n            stop_reason = \"end_turn\"\n        elif finish_reason == \"length\":\n            stop_reason = \"max_tokens\"\n        elif finish_reason == \"tool_calls\":\n            stop_reason = \"tool_use\"\n        else:\n            stop_reason = \"end_turn\"  # Default\n        \n        # Make sure content is never empty\n        if not content:\n            content.append({\"type\": \"text\", \"text\": \"\"})\n        \n        # Create Anthropic-style response\n        anthropic_response = MessagesResponse(\n            id=response_id,\n            model=original_request.model,\n            role=\"assistant\",\n            content=content,\n            stop_reason=stop_reason,\n            stop_sequence=None,\n            usage=Usage(\n                input_tokens=prompt_tokens,\n                output_tokens=completion_tokens\n            )\n        )\n        \n        return anthropic_response\n        \n    except Exception as e:\n        import traceback\n        error_traceback = traceback.format_exc()\n        error_message = f\"Error converting response: {str(e)}\\n\\nFull traceback:\\n{error_traceback}\"\n        logger.error(error_message)\n        \n        # In case of any error, create a fallback response\n        return MessagesResponse(\n            id=f\"msg_{uuid.uuid4()}\",\n            model=original_request.model,\n            role=\"assistant\",\n            content=[{\"type\": \"text\", \"text\": f\"Error converting response: {str(e)}. Please check server logs.\"}],\n            stop_reason=\"end_turn\",\n            usage=Usage(input_tokens=0, output_tokens=0)\n        )\n\nasync def handle_streaming(response_generator, original_request: MessagesRequest):\n    \"\"\"Handle streaming responses from LiteLLM and convert to Anthropic format.\"\"\"\n    try:\n        # Send message_start event\n        message_id = f\"msg_{uuid.uuid4().hex[:24]}\"  # Format similar to Anthropic's IDs\n        \n        message_data = {\n            'type': 'message_start',\n            'message': {\n                'id': message_id,\n                'type': 'message',\n                'role': 'assistant',\n                'model': original_request.model,\n                'content': [],\n                'stop_reason': None,\n                'stop_sequence': None,\n                'usage': {\n                    'input_tokens': 0,\n                    'cache_creation_input_tokens': 0,\n                    'cache_read_input_tokens': 0,\n                    'output_tokens': 0\n                }\n            }\n        }\n        yield f\"event: message_start\\ndata: {json.dumps(message_data)}\\n\\n\"\n        \n        # Content block index for the first text block\n        yield f\"event: content_block_start\\ndata: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\\n\\n\"\n        \n        # Send a ping to keep the connection alive (Anthropic does this)\n        yield f\"event: ping\\ndata: {json.dumps({'type': 'ping'})}\\n\\n\"\n        \n        tool_index = None\n        current_tool_call = None\n        tool_content = \"\"\n        accumulated_text = \"\"  # Track accumulated text content\n        text_sent = False  # Track if we've sent any text content\n        text_block_closed = False  # Track if text block is closed\n        input_tokens = 0\n        output_tokens = 0\n        has_sent_stop_reason = False\n        last_tool_index = 0\n        \n        # Process each chunk\n        async for chunk in response_generator:\n            try:\n\n                \n                # Check if this is the end of the response with usage data\n                if hasattr(chunk, 'usage') and chunk.usage is not None:\n                    if hasattr(chunk.usage, 'prompt_tokens'):\n                        input_tokens = chunk.usage.prompt_tokens\n                    if hasattr(chunk.usage, 'completion_tokens'):\n                        output_tokens = chunk.usage.completion_tokens\n                \n                # Handle text content\n                if hasattr(chunk, 'choices') and len(chunk.choices) > 0:\n                    choice = chunk.choices[0]\n                    \n                    # Get the delta from the choice\n                    if hasattr(choice, 'delta'):\n                        delta = choice.delta\n                    else:\n                        # If no delta, try to get message\n                        delta = getattr(choice, 'message', {})\n                    \n                    # Check for finish_reason to know when we're done\n                    finish_reason = getattr(choice, 'finish_reason', None)\n                    \n                    # Process text content\n                    delta_content = None\n                    \n                    # Handle different formats of delta content\n                    if hasattr(delta, 'content'):\n                        delta_content = delta.content\n                    elif isinstance(delta, dict) and 'content' in delta:\n                        delta_content = delta['content']\n                    \n                    # Accumulate text content\n                    if delta_content is not None and delta_content != \"\":\n                        accumulated_text += delta_content\n                        \n                        # Always emit text deltas if no tool calls started\n                        if tool_index is None and not text_block_closed:\n                            text_sent = True\n                            yield f\"event: content_block_delta\\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': delta_content}})}\\n\\n\"\n                    \n                    # Process tool calls\n                    delta_tool_calls = None\n                    \n                    # Handle different formats of tool calls\n                    if hasattr(delta, 'tool_calls'):\n                        delta_tool_calls = delta.tool_calls\n                    elif isinstance(delta, dict) and 'tool_calls' in delta:\n                        delta_tool_calls = delta['tool_calls']\n                    \n                    # Process tool calls if any\n                    if delta_tool_calls:\n                        # First tool call we've seen - need to handle text properly\n                        if tool_index is None:\n                            # If we've been streaming text, close that text block\n                            if text_sent and not text_block_closed:\n                                text_block_closed = True\n                                yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\\n\\n\"\n                            # If we've accumulated text but not sent it, we need to emit it now\n                            # This handles the case where the first delta has both text and a tool call\n                            elif accumulated_text and not text_sent and not text_block_closed:\n                                # Send the accumulated text\n                                text_sent = True\n                                yield f\"event: content_block_delta\\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': accumulated_text}})}\\n\\n\"\n                                # Close the text block\n                                text_block_closed = True\n                                yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\\n\\n\"\n                            # Close text block even if we haven't sent anything - models sometimes emit empty text blocks\n                            elif not text_block_closed:\n                                text_block_closed = True\n                                yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\\n\\n\"\n                                \n                        # Convert to list if it's not already\n                        if not isinstance(delta_tool_calls, list):\n                            delta_tool_calls = [delta_tool_calls]\n                        \n                        for tool_call in delta_tool_calls:\n                            # Get the index of this tool call (for multiple tools)\n                            current_index = None\n                            if isinstance(tool_call, dict) and 'index' in tool_call:\n                                current_index = tool_call['index']\n                            elif hasattr(tool_call, 'index'):\n                                current_index = tool_call.index\n                            else:\n                                current_index = 0\n                            \n                            # Check if this is a new tool or a continuation\n                            if tool_index is None or current_index != tool_index:\n                                # New tool call - create a new tool_use block\n                                tool_index = current_index\n                                last_tool_index += 1\n                                anthropic_tool_index = last_tool_index\n                                \n                                # Extract function info\n                                if isinstance(tool_call, dict):\n                                    function = tool_call.get('function', {})\n                                    name = function.get('name', '') if isinstance(function, dict) else \"\"\n                                    tool_id = tool_call.get('id', f\"toolu_{uuid.uuid4().hex[:24]}\")\n                                else:\n                                    function = getattr(tool_call, 'function', None)\n                                    name = getattr(function, 'name', '') if function else ''\n                                    tool_id = getattr(tool_call, 'id', f\"toolu_{uuid.uuid4().hex[:24]}\")\n                                \n                                # Start a new tool_use block\n                                yield f\"event: content_block_start\\ndata: {json.dumps({'type': 'content_block_start', 'index': anthropic_tool_index, 'content_block': {'type': 'tool_use', 'id': tool_id, 'name': name, 'input': {}}})}\\n\\n\"\n                                current_tool_call = tool_call\n                                tool_content = \"\"\n                            \n                            # Extract function arguments\n                            arguments = None\n                            if isinstance(tool_call, dict) and 'function' in tool_call:\n                                function = tool_call.get('function', {})\n                                arguments = function.get('arguments', '') if isinstance(function, dict) else ''\n                            elif hasattr(tool_call, 'function'):\n                                function = getattr(tool_call, 'function', None)\n                                arguments = getattr(function, 'arguments', '') if function else ''\n                            \n                            # If we have arguments, send them as a delta\n                            if arguments:\n                                # Try to detect if arguments are valid JSON or just a fragment\n                                try:\n                                    # If it's already a dict, use it\n                                    if isinstance(arguments, dict):\n                                        args_json = json.dumps(arguments)\n                                    else:\n                                        # Otherwise, try to parse it\n                                        json.loads(arguments)\n                                        args_json = arguments\n                                except (json.JSONDecodeError, TypeError):\n                                    # If it's a fragment, treat it as a string\n                                    args_json = arguments\n                                \n                                # Add to accumulated tool content\n                                tool_content += args_json if isinstance(args_json, str) else \"\"\n                                \n                                # Send the update\n                                yield f\"event: content_block_delta\\ndata: {json.dumps({'type': 'content_block_delta', 'index': anthropic_tool_index, 'delta': {'type': 'input_json_delta', 'partial_json': args_json}})}\\n\\n\"\n                    \n                    # Process finish_reason - end the streaming response\n                    if finish_reason and not has_sent_stop_reason:\n                        has_sent_stop_reason = True\n                        \n                        # Close any open tool call blocks\n                        if tool_index is not None:\n                            for i in range(1, last_tool_index + 1):\n                                yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\\n\\n\"\n                        \n                        # If we accumulated text but never sent or closed text block, do it now\n                        if not text_block_closed:\n                            if accumulated_text and not text_sent:\n                                # Send the accumulated text\n                                yield f\"event: content_block_delta\\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': accumulated_text}})}\\n\\n\"\n                            # Close the text block\n                            yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\\n\\n\"\n                        \n                        # Map OpenAI finish_reason to Anthropic stop_reason\n                        stop_reason = \"end_turn\"\n                        if finish_reason == \"length\":\n                            stop_reason = \"max_tokens\"\n                        elif finish_reason == \"tool_calls\":\n                            stop_reason = \"tool_use\"\n                        elif finish_reason == \"stop\":\n                            stop_reason = \"end_turn\"\n                        \n                        # Send message_delta with stop reason and usage\n                        usage = {\"output_tokens\": output_tokens}\n                        \n                        yield f\"event: message_delta\\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': usage})}\\n\\n\"\n                        \n                        # Send message_stop event\n                        yield f\"event: message_stop\\ndata: {json.dumps({'type': 'message_stop'})}\\n\\n\"\n                        \n                        # Send final [DONE] marker to match Anthropic's behavior\n                        yield \"data: [DONE]\\n\\n\"\n                        return\n            except Exception as e:\n                # Log error but continue processing other chunks\n                logger.error(f\"Error processing chunk: {str(e)}\")\n                continue\n        \n        # If we didn't get a finish reason, close any open blocks\n        if not has_sent_stop_reason:\n            # Close any open tool call blocks\n            if tool_index is not None:\n                for i in range(1, last_tool_index + 1):\n                    yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': i})}\\n\\n\"\n            \n            # Close the text content block\n            yield f\"event: content_block_stop\\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\\n\\n\"\n            \n            # Send final message_delta with usage\n            usage = {\"output_tokens\": output_tokens}\n            \n            yield f\"event: message_delta\\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': usage})}\\n\\n\"\n            \n            # Send message_stop event\n            yield f\"event: message_stop\\ndata: {json.dumps({'type': 'message_stop'})}\\n\\n\"\n            \n            # Send final [DONE] marker to match Anthropic's behavior\n            yield \"data: [DONE]\\n\\n\"\n    \n    except Exception as e:\n        import traceback\n        error_traceback = traceback.format_exc()\n        error_message = f\"Error in streaming: {str(e)}\\n\\nFull traceback:\\n{error_traceback}\"\n        logger.error(error_message)\n        \n        # Send error message_delta\n        yield f\"event: message_delta\\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'error', 'stop_sequence': None}, 'usage': {'output_tokens': 0}})}\\n\\n\"\n        \n        # Send message_stop event\n        yield f\"event: message_stop\\ndata: {json.dumps({'type': 'message_stop'})}\\n\\n\"\n        \n        # Send final [DONE] marker\n        yield \"data: [DONE]\\n\\n\"\n\n@app.post(\"/v1/messages\")\nasync def create_message(\n    request: MessagesRequest,\n    raw_request: Request\n):\n    try:\n        # print the body here\n        body = await raw_request.body()\n    \n        # Parse the raw body as JSON since it's bytes\n        body_json = json.loads(body.decode('utf-8'))\n        original_model = body_json.get(\"model\", \"unknown\")\n        \n        # Get the display name for logging, just the model name without provider prefix\n        display_model = original_model\n        if \"/\" in display_model:\n            display_model = display_model.split(\"/\")[-1]\n        \n        # Clean model name for capability check\n        clean_model = request.model\n        if clean_model.startswith(\"anthropic/\"):\n            clean_model = clean_model[len(\"anthropic/\"):]\n        elif clean_model.startswith(\"openai/\"):\n            clean_model = clean_model[len(\"openai/\"):]\n        \n        logger.debug(f\"📊 PROCESSING REQUEST: Model={request.model}, Stream={request.stream}\")\n        \n        # Convert Anthropic request to LiteLLM format\n        litellm_request = convert_anthropic_to_litellm(request)\n        \n        # Determine which API key to use based on the model\n        if request.model.startswith(\"openai/\"):\n            litellm_request[\"api_key\"] = OPENAI_API_KEY\n            # Use custom OpenAI base URL if configured\n            if OPENAI_BASE_URL:\n                litellm_request[\"api_base\"] = OPENAI_BASE_URL\n                logger.debug(f\"Using OpenAI API key and custom base URL {OPENAI_BASE_URL} for model: {request.model}\")\n            else:\n                logger.debug(f\"Using OpenAI API key for model: {request.model}\")\n        elif request.model.startswith(\"gemini/\"):\n            if USE_VERTEX_AUTH:\n                litellm_request[\"vertex_project\"] = VERTEX_PROJECT\n                litellm_request[\"vertex_location\"] = VERTEX_LOCATION\n                litellm_request[\"custom_llm_provider\"] = \"vertex_ai\"\n                logger.debug(f\"Using Gemini ADC with project={VERTEX_PROJECT}, location={VERTEX_LOCATION} and model: {request.model}\")\n            else:\n                litellm_request[\"api_key\"] = GEMINI_API_KEY\n                logger.debug(f\"Using Gemini API key for model: {request.model}\")\n        else:\n            litellm_request[\"api_key\"] = ANTHROPIC_API_KEY\n            logger.debug(f\"Using Anthropic API key for model: {request.model}\")\n        \n        # For OpenAI models - modify request format to work with limitations\n        if \"openai\" in litellm_request[\"model\"] and \"messages\" in litellm_request:\n            logger.debug(f\"Processing OpenAI model request: {litellm_request['model']}\")\n            \n            # For OpenAI models, we need to convert content blocks to simple strings\n            # and handle other requirements\n            for i, msg in enumerate(litellm_request[\"messages\"]):\n                # Special case - handle message content directly when it's a list of tool_result\n                # This is a specific case we're seeing in the error\n                if \"content\" in msg and isinstance(msg[\"content\"], list):\n                    is_only_tool_result = True\n                    for block in msg[\"content\"]:\n                        if not isinstance(block, dict) or block.get(\"type\") != \"tool_result\":\n                            is_only_tool_result = False\n                            break\n                    \n                    if is_only_tool_result and len(msg[\"content\"]) > 0:\n                        logger.warning(f\"Found message with only tool_result content - special handling required\")\n                        # Extract the content from all tool_result blocks\n                        all_text = \"\"\n                        for block in msg[\"content\"]:\n                            all_text += \"Tool Result:\\n\"\n                            result_content = block.get(\"content\", [])\n                            \n                            # Handle different formats of content\n                            if isinstance(result_content, list):\n                                for item in result_content:\n                                    if isinstance(item, dict) and item.get(\"type\") == \"text\":\n                                        all_text += item.get(\"text\", \"\") + \"\\n\"\n                                    elif isinstance(item, dict):\n                                        # Fall back to string representation of any dict\n                                        try:\n                                            item_text = item.get(\"text\", json.dumps(item))\n                                            all_text += item_text + \"\\n\"\n                                        except:\n                                            all_text += str(item) + \"\\n\"\n                            elif isinstance(result_content, str):\n                                all_text += result_content + \"\\n\"\n                            else:\n                                try:\n                                    all_text += json.dumps(result_content) + \"\\n\"\n                                except:\n                                    all_text += str(result_content) + \"\\n\"\n                        \n                        # Replace the list with extracted text\n                        litellm_request[\"messages\"][i][\"content\"] = all_text.strip() or \"...\"\n                        logger.warning(f\"Converted tool_result to plain text: {all_text.strip()[:200]}...\")\n                        continue  # Skip normal processing for this message\n                \n                # 1. Handle content field - normal case\n                if \"content\" in msg:\n                    # Check if content is a list (content blocks)\n                    if isinstance(msg[\"content\"], list):\n                        # Convert complex content blocks to simple string\n                        text_content = \"\"\n                        for block in msg[\"content\"]:\n                            if isinstance(block, dict):\n                                # Handle different content block types\n                                if block.get(\"type\") == \"text\":\n                                    text_content += block.get(\"text\", \"\") + \"\\n\"\n                                \n                                # Handle tool_result content blocks - extract nested text\n                                elif block.get(\"type\") == \"tool_result\":\n                                    tool_id = block.get(\"tool_use_id\", \"unknown\")\n                                    text_content += f\"[Tool Result ID: {tool_id}]\\n\"\n                                    \n                                    # Extract text from the tool_result content\n                                    result_content = block.get(\"content\", [])\n                                    if isinstance(result_content, list):\n                                        for item in result_content:\n                                            if isinstance(item, dict) and item.get(\"type\") == \"text\":\n                                                text_content += item.get(\"text\", \"\") + \"\\n\"\n                                            elif isinstance(item, dict):\n                                                # Handle any dict by trying to extract text or convert to JSON\n                                                if \"text\" in item:\n                                                    text_content += item.get(\"text\", \"\") + \"\\n\"\n                                                else:\n                                                    try:\n                                                        text_content += json.dumps(item) + \"\\n\"\n                                                    except:\n                                                        text_content += str(item) + \"\\n\"\n                                    elif isinstance(result_content, dict):\n                                        # Handle dictionary content\n                                        if result_content.get(\"type\") == \"text\":\n                                            text_content += result_content.get(\"text\", \"\") + \"\\n\"\n                                        else:\n                                            try:\n                                                text_content += json.dumps(result_content) + \"\\n\"\n                                            except:\n                                                text_content += str(result_content) + \"\\n\"\n                                    elif isinstance(result_content, str):\n                                        text_content += result_content + \"\\n\"\n                                    else:\n                                        try:\n                                            text_content += json.dumps(result_content) + \"\\n\"\n                                        except:\n                                            text_content += str(result_content) + \"\\n\"\n                                \n                                # Handle tool_use content blocks\n                                elif block.get(\"type\") == \"tool_use\":\n                                    tool_name = block.get(\"name\", \"unknown\")\n                                    tool_id = block.get(\"id\", \"unknown\")\n                                    tool_input = json.dumps(block.get(\"input\", {}))\n                                    text_content += f\"[Tool: {tool_name} (ID: {tool_id})]\\nInput: {tool_input}\\n\\n\"\n                                \n                                # Handle image content blocks\n                                elif block.get(\"type\") == \"image\":\n                                    text_content += \"[Image content - not displayed in text format]\\n\"\n                        \n                        # Make sure content is never empty for OpenAI models\n                        if not text_content.strip():\n                            text_content = \"...\"\n                        \n                        litellm_request[\"messages\"][i][\"content\"] = text_content.strip()\n                    # Also check for None or empty string content\n                    elif msg[\"content\"] is None:\n                        litellm_request[\"messages\"][i][\"content\"] = \"...\" # Empty content not allowed\n                \n                # 2. Remove any fields OpenAI doesn't support in messages\n                for key in list(msg.keys()):\n                    if key not in [\"role\", \"content\", \"name\", \"tool_call_id\", \"tool_calls\"]:\n                        logger.warning(f\"Removing unsupported field from message: {key}\")\n                        del msg[key]\n            \n            # 3. Final validation - check for any remaining invalid values and dump full message details\n            for i, msg in enumerate(litellm_request[\"messages\"]):\n                # Log the message format for debugging\n                logger.debug(f\"Message {i} format check - role: {msg.get('role')}, content type: {type(msg.get('content'))}\")\n                \n                # If content is still a list or None, replace with placeholder\n                if isinstance(msg.get(\"content\"), list):\n                    logger.warning(f\"CRITICAL: Message {i} still has list content after processing: {json.dumps(msg.get('content'))}\")\n                    # Last resort - stringify the entire content as JSON\n                    litellm_request[\"messages\"][i][\"content\"] = f\"Content as JSON: {json.dumps(msg.get('content'))}\"\n                elif msg.get(\"content\") is None:\n                    logger.warning(f\"Message {i} has None content - replacing with placeholder\")\n                    litellm_request[\"messages\"][i][\"content\"] = \"...\" # Fallback placeholder\n        \n        # Only log basic info about the request, not the full details\n        logger.debug(f\"Request for model: {litellm_request.get('model')}, stream: {litellm_request.get('stream', False)}\")\n        \n        # Handle streaming mode\n        if request.stream:\n            # Use LiteLLM for streaming\n            num_tools = len(request.tools) if request.tools else 0\n            \n            log_request_beautifully(\n                \"POST\", \n                raw_request.url.path, \n                display_model, \n                litellm_request.get('model'),\n                len(litellm_request['messages']),\n                num_tools,\n                200  # Assuming success at this point\n            )\n            # Ensure we use the async version for streaming\n            response_generator = await litellm.acompletion(**litellm_request)\n            \n            return StreamingResponse(\n                handle_streaming(response_generator, request),\n                media_type=\"text/event-stream\"\n            )\n        else:\n            # Use LiteLLM for regular completion\n            num_tools = len(request.tools) if request.tools else 0\n            \n            log_request_beautifully(\n                \"POST\", \n                raw_request.url.path, \n                display_model, \n                litellm_request.get('model'),\n                len(litellm_request['messages']),\n                num_tools,\n                200  # Assuming success at this point\n            )\n            start_time = time.time()\n            litellm_response = litellm.completion(**litellm_request)\n            logger.debug(f\"✅ RESPONSE RECEIVED: Model={litellm_request.get('model')}, Time={time.time() - start_time:.2f}s\")\n            \n            # Convert LiteLLM response to Anthropic format\n            anthropic_response = convert_litellm_to_anthropic(litellm_response, request)\n            \n            return anthropic_response\n                \n    except Exception as e:\n        import traceback\n        error_traceback = traceback.format_exc()\n        \n        # Capture as much info as possible about the error\n        error_details = {\n            \"error\": str(e),\n            \"type\": type(e).__name__,\n            \"traceback\": error_traceback\n        }\n        \n        # Check for LiteLLM-specific attributes\n        for attr in ['message', 'status_code', 'response', 'llm_provider', 'model']:\n            if hasattr(e, attr):\n                error_details[attr] = getattr(e, attr)\n        \n        # Check for additional exception details in dictionaries\n        if hasattr(e, '__dict__'):\n            for key, value in e.__dict__.items():\n                if key not in error_details and key not in ['args', '__traceback__']:\n                    error_details[key] = str(value)\n        \n        # Helper function to safely serialize objects for JSON\n        def sanitize_for_json(obj):\n            \"\"\"递归地清理对象使其可以JSON序列化\"\"\"\n            if isinstance(obj, dict):\n                return {k: sanitize_for_json(v) for k, v in obj.items()}\n            elif isinstance(obj, list):\n                return [sanitize_for_json(item) for item in obj]\n            elif hasattr(obj, '__dict__'):\n                return sanitize_for_json(obj.__dict__)\n            elif hasattr(obj, 'text'):\n                return str(obj.text)\n            else:\n                try:\n                    json.dumps(obj)\n                    return obj\n                except (TypeError, ValueError):\n                    return str(obj)\n        \n        # Log all error details with safe serialization\n        sanitized_details = sanitize_for_json(error_details)\n        logger.error(f\"Error processing request: {json.dumps(sanitized_details, indent=2)}\")\n        \n        # Format error for response\n        error_message = f\"Error: {str(e)}\"\n        if 'message' in error_details and error_details['message']:\n            error_message += f\"\\nMessage: {error_details['message']}\"\n        if 'response' in error_details and error_details['response']:\n            error_message += f\"\\nResponse: {error_details['response']}\"\n        \n        # Return detailed error\n        status_code = error_details.get('status_code', 500)\n        raise HTTPException(status_code=status_code, detail=error_message)\n\n@app.post(\"/v1/messages/count_tokens\")\nasync def count_tokens(\n    request: TokenCountRequest,\n    raw_request: Request\n):\n    try:\n        # Log the incoming token count request\n        original_model = request.original_model or request.model\n        \n        # Get the display name for logging, just the model name without provider prefix\n        display_model = original_model\n        if \"/\" in display_model:\n            display_model = display_model.split(\"/\")[-1]\n        \n        # Clean model name for capability check\n        clean_model = request.model\n        if clean_model.startswith(\"anthropic/\"):\n            clean_model = clean_model[len(\"anthropic/\"):]\n        elif clean_model.startswith(\"openai/\"):\n            clean_model = clean_model[len(\"openai/\"):]\n        \n        # Convert the messages to a format LiteLLM can understand\n        converted_request = convert_anthropic_to_litellm(\n            MessagesRequest(\n                model=request.model,\n                max_tokens=100,  # Arbitrary value not used for token counting\n                messages=request.messages,\n                system=request.system,\n                tools=request.tools,\n                tool_choice=request.tool_choice,\n                thinking=request.thinking\n            )\n        )\n        \n        # Use LiteLLM's token_counter function\n        try:\n            # Import token_counter function\n            from litellm import token_counter\n            \n            # Log the request beautifully\n            num_tools = len(request.tools) if request.tools else 0\n            \n            log_request_beautifully(\n                \"POST\",\n                raw_request.url.path,\n                display_model,\n                converted_request.get('model'),\n                len(converted_request['messages']),\n                num_tools,\n                200  # Assuming success at this point\n            )\n            \n            # Prepare token counter arguments\n            token_counter_args = {\n                \"model\": converted_request[\"model\"],\n                \"messages\": converted_request[\"messages\"],\n            }\n            \n            # Add custom base URL for OpenAI models if configured\n            if request.model.startswith(\"openai/\") and OPENAI_BASE_URL:\n                token_counter_args[\"api_base\"] = OPENAI_BASE_URL\n            \n            # Count tokens\n            token_count = token_counter(**token_counter_args)\n            \n            # Return Anthropic-style response\n            return TokenCountResponse(input_tokens=token_count)\n            \n        except ImportError:\n            logger.error(\"Could not import token_counter from litellm\")\n            # Fallback to a simple approximation\n            return TokenCountResponse(input_tokens=1000)  # Default fallback\n            \n    except Exception as e:\n        import traceback\n        error_traceback = traceback.format_exc()\n        logger.error(f\"Error counting tokens: {str(e)}\\n{error_traceback}\")\n        raise HTTPException(status_code=500, detail=f\"Error counting tokens: {str(e)}\")\n\n@app.get(\"/\")\nasync def root():\n    return {\"message\": \"Anthropic Proxy for LiteLLM\"}\n\n# Define ANSI color codes for terminal output\nclass Colors:\n    CYAN = \"\\033[96m\"\n    BLUE = \"\\033[94m\"\n    GREEN = \"\\033[92m\"\n    YELLOW = \"\\033[93m\"\n    RED = \"\\033[91m\"\n    MAGENTA = \"\\033[95m\"\n    RESET = \"\\033[0m\"\n    BOLD = \"\\033[1m\"\n    UNDERLINE = \"\\033[4m\"\n    DIM = \"\\033[2m\"\ndef log_request_beautifully(method, path, claude_model, openai_model, num_messages, num_tools, status_code):\n    \"\"\"Log requests in a beautiful, twitter-friendly format showing Claude to OpenAI mapping.\"\"\"\n    # Format the Claude model name nicely\n    claude_display = f\"{Colors.CYAN}{claude_model}{Colors.RESET}\"\n    \n    # Extract endpoint name\n    endpoint = path\n    if \"?\" in endpoint:\n        endpoint = endpoint.split(\"?\")[0]\n    \n    # Extract just the OpenAI model name without provider prefix\n    openai_display = openai_model\n    if \"/\" in openai_display:\n        openai_display = openai_display.split(\"/\")[-1]\n    openai_display = f\"{Colors.GREEN}{openai_display}{Colors.RESET}\"\n    \n    # Format tools and messages\n    tools_str = f\"{Colors.MAGENTA}{num_tools} tools{Colors.RESET}\"\n    messages_str = f\"{Colors.BLUE}{num_messages} messages{Colors.RESET}\"\n    \n    # Format status code\n    status_str = f\"{Colors.GREEN}✓ {status_code} OK{Colors.RESET}\" if status_code == 200 else f\"{Colors.RED}✗ {status_code}{Colors.RESET}\"\n    \n\n    # Put it all together in a clear, beautiful format\n    log_line = f\"{Colors.BOLD}{method} {endpoint}{Colors.RESET} {status_str}\"\n    model_line = f\"{claude_display} → {openai_display} {tools_str} {messages_str}\"\n    \n    # Print to console\n    print(log_line)\n    print(model_line)\n    sys.stdout.flush()\n\nif __name__ == \"__main__\":\n    import sys\n    if len(sys.argv) > 1 and sys.argv[1] == \"--help\":\n        print(\"Run with: uvicorn server:app --reload --host 0.0.0.0 --port 8082\")\n        sys.exit(0)\n    \n    # Configure uvicorn to run with minimal logs\n    uvicorn.run(app, host=\"0.0.0.0\", port=8082, log_level=\"error\")\n"
  },
  {
    "path": "tests.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nComprehensive test suite for Claude-on-OpenAI Proxy.\n\nThis script provides tests for both streaming and non-streaming requests,\nwith various scenarios including tool use, multi-turn conversations,\nand content blocks.\n\nUsage:\n  python tests.py                    # Run all tests\n  python tests.py --no-streaming     # Skip streaming tests\n  python tests.py --simple           # Run only simple tests\n  python tests.py --tools            # Run tool-related tests only\n\"\"\"\n\nimport os\nimport json\nimport time\nimport httpx\nimport argparse\nimport asyncio\nimport sys\nfrom datetime import datetime\nfrom typing import Dict, Any, List, Optional, Set\nfrom dotenv import load_dotenv\n\n# Load environment variables\nload_dotenv()\n\n# Configuration\nANTHROPIC_API_KEY = os.environ.get(\"ANTHROPIC_API_KEY\")\nPROXY_API_KEY = os.environ.get(\"ANTHROPIC_API_KEY\")  # Using same key for proxy\nANTHROPIC_API_URL = \"https://api.anthropic.com/v1/messages\"\nPROXY_API_URL = \"http://localhost:8082/v1/messages\"\nANTHROPIC_VERSION = \"2023-06-01\"\nMODEL = \"claude-3-sonnet-20240229\"  # Change to your preferred model\n\n# Headers\nanthropic_headers = {\n    \"x-api-key\": ANTHROPIC_API_KEY,\n    \"anthropic-version\": ANTHROPIC_VERSION,\n    \"content-type\": \"application/json\",\n}\n\nproxy_headers = {\n    \"x-api-key\": PROXY_API_KEY,\n    \"anthropic-version\": ANTHROPIC_VERSION,\n    \"content-type\": \"application/json\",\n}\n\n# Tool definitions\ncalculator_tool = {\n    \"name\": \"calculator\",\n    \"description\": \"Evaluate mathematical expressions\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"expression\": {\n                \"type\": \"string\",\n                \"description\": \"The mathematical expression to evaluate\"\n            }\n        },\n        \"required\": [\"expression\"]\n    }\n}\n\nweather_tool = {\n    \"name\": \"weather\",\n    \"description\": \"Get weather information for a location\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"location\": {\n                \"type\": \"string\",\n                \"description\": \"The city or location to get weather for\"\n            },\n            \"units\": {\n                \"type\": \"string\",\n                \"enum\": [\"celsius\", \"fahrenheit\"],\n                \"description\": \"Temperature units\"\n            }\n        },\n        \"required\": [\"location\"]\n    }\n}\n\nsearch_tool = {\n    \"name\": \"search\",\n    \"description\": \"Search for information on the web\",\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"query\": {\n                \"type\": \"string\", \n                \"description\": \"The search query\"\n            }\n        },\n        \"required\": [\"query\"]\n    }\n}\n\n# Test scenarios\nTEST_SCENARIOS = {\n    # Simple text response\n    \"simple\": {\n        \"model\": MODEL,\n        \"max_tokens\": 300,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"Hello, world! Can you tell me about Paris in 2-3 sentences?\"}\n        ]\n    },\n    \n    # Basic tool use\n    \"calculator\": {\n        \"model\": MODEL,\n        \"max_tokens\": 300,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"What is 135 + 7.5 divided by 2.5?\"}\n        ],\n        \"tools\": [calculator_tool],\n        \"tool_choice\": {\"type\": \"auto\"}\n    },\n    \n    # Multiple tools\n    \"multi_tool\": {\n        \"model\": MODEL,\n        \"max_tokens\": 500,\n        \"temperature\": 0.7,\n        \"top_p\": 0.95,\n        \"system\": \"You are a helpful assistant that uses tools when appropriate. Be concise and precise.\",\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"I'm planning a trip to New York next week. What's the weather like and what are some interesting places to visit?\"}\n        ],\n        \"tools\": [weather_tool, search_tool],\n        \"tool_choice\": {\"type\": \"auto\"}\n    },\n    \n    # Multi-turn conversation\n    \"multi_turn\": {\n        \"model\": MODEL,\n        \"max_tokens\": 500,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"Let's do some math. What is 240 divided by 8?\"},\n            {\"role\": \"assistant\", \"content\": \"To calculate 240 divided by 8, I'll perform the division:\\n\\n240 ÷ 8 = 30\\n\\nSo the result is 30.\"},\n            {\"role\": \"user\", \"content\": \"Now multiply that by 4 and tell me the result.\"}\n        ],\n        \"tools\": [calculator_tool],\n        \"tool_choice\": {\"type\": \"auto\"}\n    },\n    \n    # Content blocks\n    \"content_blocks\": {\n        \"model\": MODEL,\n        \"max_tokens\": 500,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": [\n                {\"type\": \"text\", \"text\": \"I need to know the weather in Los Angeles and calculate 75.5 / 5. Can you help with both?\"}\n            ]}\n        ],\n        \"tools\": [calculator_tool, weather_tool],\n        \"tool_choice\": {\"type\": \"auto\"}\n    },\n    \n    # Simple streaming test\n    \"simple_stream\": {\n        \"model\": MODEL,\n        \"max_tokens\": 100,\n        \"stream\": True,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"Count from 1 to 5, with one number per line.\"}\n        ]\n    },\n    \n    # Tool use with streaming\n    \"calculator_stream\": {\n        \"model\": MODEL,\n        \"max_tokens\": 300,\n        \"stream\": True,\n        \"messages\": [\n            {\"role\": \"user\", \"content\": \"What is 135 + 17.5 divided by 2.5?\"}\n        ],\n        \"tools\": [calculator_tool],\n        \"tool_choice\": {\"type\": \"auto\"}\n    }\n}\n\n# Required event types for Anthropic streaming responses\nREQUIRED_EVENT_TYPES = {\n    \"message_start\", \n    \"content_block_start\", \n    \"content_block_delta\", \n    \"content_block_stop\", \n    \"message_delta\", \n    \"message_stop\"\n}\n\n# ================= NON-STREAMING TESTS =================\n\ndef get_response(url, headers, data):\n    \"\"\"Send a request and get the response.\"\"\"\n    start_time = time.time()\n    response = httpx.post(url, headers=headers, json=data, timeout=30)\n    elapsed = time.time() - start_time\n    \n    print(f\"Response time: {elapsed:.2f} seconds\")\n    return response\n\ndef compare_responses(anthropic_response, proxy_response, check_tools=False):\n    \"\"\"Compare the two responses to see if they're similar enough.\"\"\"\n    anthropic_json = anthropic_response.json()\n    proxy_json = proxy_response.json()\n    \n    print(\"\\n--- Anthropic Response Structure ---\")\n    print(json.dumps({k: v for k, v in anthropic_json.items() if k != \"content\"}, indent=2))\n    \n    print(\"\\n--- Proxy Response Structure ---\")\n    print(json.dumps({k: v for k, v in proxy_json.items() if k != \"content\"}, indent=2))\n    \n    # Basic structure verification with more flexibility\n    # The proxy might map values differently, so we're more lenient in our checks\n    assert proxy_json.get(\"role\") == \"assistant\", \"Proxy role is not 'assistant'\"\n    assert proxy_json.get(\"type\") == \"message\", \"Proxy type is not 'message'\"\n    \n    # Check if stop_reason is reasonable (might be different between Anthropic and our proxy)\n    valid_stop_reasons = [\"end_turn\", \"max_tokens\", \"stop_sequence\", \"tool_use\", None]\n    assert proxy_json.get(\"stop_reason\") in valid_stop_reasons, \"Invalid stop reason\"\n    \n    # Check content exists and has valid structure\n    assert \"content\" in anthropic_json, \"No content in Anthropic response\"\n    assert \"content\" in proxy_json, \"No content in Proxy response\"\n    \n    anthropic_content = anthropic_json[\"content\"]\n    proxy_content = proxy_json[\"content\"]\n    \n    # Make sure content is a list and has at least one item\n    assert isinstance(anthropic_content, list), \"Anthropic content is not a list\"\n    assert isinstance(proxy_content, list), \"Proxy content is not a list\" \n    assert len(proxy_content) > 0, \"Proxy content is empty\"\n    \n    # If we're checking for tool uses\n    if check_tools:\n        # Check if content has tool use\n        anthropic_tool = None\n        proxy_tool = None\n        \n        # Find tool use in Anthropic response\n        for item in anthropic_content:\n            if item.get(\"type\") == \"tool_use\":\n                anthropic_tool = item\n                break\n                \n        # Find tool use in Proxy response\n        for item in proxy_content:\n            if item.get(\"type\") == \"tool_use\":\n                proxy_tool = item\n                break\n        \n        # At least one of them should have a tool use\n        if anthropic_tool is not None:\n            print(\"\\n---------- ANTHROPIC TOOL USE ----------\")\n            print(json.dumps(anthropic_tool, indent=2))\n            \n            if proxy_tool is not None:\n                print(\"\\n---------- PROXY TOOL USE ----------\")\n                print(json.dumps(proxy_tool, indent=2))\n                \n                # Check tool structure\n                assert proxy_tool.get(\"name\") is not None, \"Proxy tool has no name\"\n                assert proxy_tool.get(\"input\") is not None, \"Proxy tool has no input\"\n                \n                print(\"\\n✅ Both responses contain tool use\")\n            else:\n                print(\"\\n⚠️ Proxy response does not contain tool use, but Anthropic does\")\n        elif proxy_tool is not None:\n            print(\"\\n---------- PROXY TOOL USE ----------\")\n            print(json.dumps(proxy_tool, indent=2))\n            print(\"\\n⚠️ Proxy response contains tool use, but Anthropic does not\")\n        else:\n            print(\"\\n⚠️ Neither response contains tool use\")\n    \n    # Check if content has text\n    anthropic_text = None\n    proxy_text = None\n    \n    for item in anthropic_content:\n        if item.get(\"type\") == \"text\":\n            anthropic_text = item.get(\"text\")\n            break\n            \n    for item in proxy_content:\n        if item.get(\"type\") == \"text\":\n            proxy_text = item.get(\"text\")\n            break\n    \n    # For tool use responses, there might not be text content\n    if check_tools and (anthropic_text is None or proxy_text is None):\n        print(\"\\n⚠️ One or both responses don't have text content (expected for tool-only responses)\")\n        return True\n    \n    assert anthropic_text is not None, \"No text found in Anthropic response\"\n    assert proxy_text is not None, \"No text found in Proxy response\"\n    \n    # Print the first few lines of each text response\n    max_preview_lines = 5\n    anthropic_preview = \"\\n\".join(anthropic_text.strip().split(\"\\n\")[:max_preview_lines])\n    proxy_preview = \"\\n\".join(proxy_text.strip().split(\"\\n\")[:max_preview_lines])\n    \n    print(\"\\n---------- ANTHROPIC TEXT PREVIEW ----------\")\n    print(anthropic_preview)\n    \n    print(\"\\n---------- PROXY TEXT PREVIEW ----------\")\n    print(proxy_preview)\n    \n    # Check for some minimum text overlap - proxy might have different exact wording\n    # but should have roughly similar content\n    return True  # We're not enforcing similarity, just basic structure\n\ndef test_request(test_name, request_data, check_tools=False):\n    \"\"\"Run a test with the given request data.\"\"\"\n    print(f\"\\n{'='*20} RUNNING TEST: {test_name} {'='*20}\")\n    \n    # Log the request data\n    print(f\"\\nRequest data:\\n{json.dumps({k: v for k, v in request_data.items() if k != 'messages'}, indent=2)}\")\n    \n    # Make copies of the request data to avoid modifying the original\n    anthropic_data = request_data.copy()\n    proxy_data = request_data.copy()\n    \n    try:\n        # Send requests to both APIs\n        print(\"\\nSending to Anthropic API...\")\n        anthropic_response = get_response(ANTHROPIC_API_URL, anthropic_headers, anthropic_data)\n        \n        print(\"\\nSending to Proxy...\")\n        proxy_response = get_response(PROXY_API_URL, proxy_headers, proxy_data)\n        \n        # Check response codes\n        print(f\"\\nAnthropic status code: {anthropic_response.status_code}\")\n        print(f\"Proxy status code: {proxy_response.status_code}\")\n        \n        if anthropic_response.status_code != 200 or proxy_response.status_code != 200:\n            print(\"\\n⚠️ One or both requests failed\")\n            if anthropic_response.status_code != 200:\n                print(f\"Anthropic error: {anthropic_response.text}\")\n            if proxy_response.status_code != 200:\n                print(f\"Proxy error: {proxy_response.text}\")\n            return False\n        \n        # Compare the responses\n        result = compare_responses(anthropic_response, proxy_response, check_tools=check_tools)\n        if result:\n            print(f\"\\n✅ Test {test_name} passed!\")\n            return True\n        else:\n            print(f\"\\n❌ Test {test_name} failed!\")\n            return False\n    \n    except Exception as e:\n        print(f\"\\n❌ Error in test {test_name}: {str(e)}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\n# ================= STREAMING TESTS =================\n\nclass StreamStats:\n    \"\"\"Track statistics about a streaming response.\"\"\"\n    \n    def __init__(self):\n        self.event_types = set()\n        self.event_counts = {}\n        self.first_event_time = None\n        self.last_event_time = None\n        self.total_chunks = 0\n        self.events = []\n        self.text_content = \"\"\n        self.content_blocks = {}\n        self.has_tool_use = False\n        self.has_error = False\n        self.error_message = \"\"\n        self.text_content_by_block = {}\n        \n    def add_event(self, event_data):\n        \"\"\"Track information about each received event.\"\"\"\n        now = datetime.now()\n        if self.first_event_time is None:\n            self.first_event_time = now\n        self.last_event_time = now\n        \n        self.total_chunks += 1\n        \n        # Record event type and increment count\n        if \"type\" in event_data:\n            event_type = event_data[\"type\"]\n            self.event_types.add(event_type)\n            self.event_counts[event_type] = self.event_counts.get(event_type, 0) + 1\n            \n            # Track specific event data\n            if event_type == \"content_block_start\":\n                block_idx = event_data.get(\"index\")\n                content_block = event_data.get(\"content_block\", {})\n                if content_block.get(\"type\") == \"tool_use\":\n                    self.has_tool_use = True\n                self.content_blocks[block_idx] = content_block\n                self.text_content_by_block[block_idx] = \"\"\n                \n            elif event_type == \"content_block_delta\":\n                block_idx = event_data.get(\"index\")\n                delta = event_data.get(\"delta\", {})\n                if delta.get(\"type\") == \"text_delta\":\n                    text = delta.get(\"text\", \"\")\n                    self.text_content += text\n                    # Also track text by block ID\n                    if block_idx in self.text_content_by_block:\n                        self.text_content_by_block[block_idx] += text\n                        \n        # Keep track of all events for debugging\n        self.events.append(event_data)\n                \n    def get_duration(self):\n        \"\"\"Calculate the total duration of the stream in seconds.\"\"\"\n        if self.first_event_time is None or self.last_event_time is None:\n            return 0\n        return (self.last_event_time - self.first_event_time).total_seconds()\n        \n    def summarize(self):\n        \"\"\"Print a summary of the stream statistics.\"\"\"\n        print(f\"Total chunks: {self.total_chunks}\")\n        print(f\"Unique event types: {sorted(list(self.event_types))}\")\n        print(f\"Event counts: {json.dumps(self.event_counts, indent=2)}\")\n        print(f\"Duration: {self.get_duration():.2f} seconds\")\n        print(f\"Has tool use: {self.has_tool_use}\")\n        \n        # Print the first few lines of content\n        if self.text_content:\n            max_preview_lines = 5\n            text_preview = \"\\n\".join(self.text_content.strip().split(\"\\n\")[:max_preview_lines])\n            print(f\"Text preview:\\n{text_preview}\")\n        else:\n            print(\"No text content extracted\")\n            \n        if self.has_error:\n            print(f\"Error: {self.error_message}\")\n\nasync def stream_response(url, headers, data, stream_name):\n    \"\"\"Send a streaming request and process the response.\"\"\"\n    print(f\"\\nStarting {stream_name} stream...\")\n    stats = StreamStats()\n    error = None\n    \n    try:\n        async with httpx.AsyncClient() as client:\n            # Add stream flag to ensure it's streamed\n            request_data = data.copy()\n            request_data[\"stream\"] = True\n            \n            start_time = time.time()\n            async with client.stream(\"POST\", url, json=request_data, headers=headers, timeout=30) as response:\n                if response.status_code != 200:\n                    error_text = await response.aread()\n                    stats.has_error = True\n                    stats.error_message = f\"HTTP {response.status_code}: {error_text.decode('utf-8')}\"\n                    error = stats.error_message\n                    print(f\"Error: {stats.error_message}\")\n                    return stats, error\n                \n                print(f\"{stream_name} connected, receiving events...\")\n                \n                # Process each chunk\n                buffer = \"\"\n                async for chunk in response.aiter_text():\n                    if not chunk.strip():\n                        continue\n                    \n                    # Handle multiple events in one chunk\n                    buffer += chunk\n                    events = buffer.split(\"\\n\\n\")\n                    \n                    # Process all complete events\n                    for event_text in events[:-1]:  # All but the last (possibly incomplete) event\n                        if not event_text.strip():\n                            continue\n                        \n                        # Parse server-sent event format\n                        if \"data: \" in event_text:\n                            # Extract the data part\n                            data_parts = []\n                            for line in event_text.split(\"\\n\"):\n                                if line.startswith(\"data: \"):\n                                    data_part = line[len(\"data: \"):]\n                                    # Skip the \"[DONE]\" marker\n                                    if data_part == \"[DONE]\":\n                                        break\n                                    data_parts.append(data_part)\n                            \n                            if data_parts:\n                                try:\n                                    event_data = json.loads(\"\".join(data_parts))\n                                    stats.add_event(event_data)\n                                except json.JSONDecodeError as e:\n                                    print(f\"Error parsing event: {e}\\nRaw data: {''.join(data_parts)}\")\n                    \n                    # Keep the last (potentially incomplete) event for the next iteration\n                    buffer = events[-1] if events else \"\"\n                    \n                # Process any remaining complete events in the buffer\n                if buffer.strip():\n                    lines = buffer.strip().split(\"\\n\")\n                    data_lines = [line[len(\"data: \"):] for line in lines if line.startswith(\"data: \")]\n                    if data_lines and data_lines[0] != \"[DONE]\":\n                        try:\n                            event_data = json.loads(\"\".join(data_lines))\n                            stats.add_event(event_data)\n                        except:\n                            pass\n                \n            elapsed = time.time() - start_time\n            print(f\"{stream_name} stream completed in {elapsed:.2f} seconds\")\n    except Exception as e:\n        stats.has_error = True\n        stats.error_message = str(e)\n        error = str(e)\n        print(f\"Error in {stream_name} stream: {e}\")\n    \n    return stats, error\n\ndef compare_stream_stats(anthropic_stats, proxy_stats):\n    \"\"\"Compare the statistics from the two streams to see if they're similar enough.\"\"\"\n    \n    print(\"\\n--- Stream Comparison ---\")\n    \n    # Required events\n    anthropic_missing = REQUIRED_EVENT_TYPES - anthropic_stats.event_types\n    proxy_missing = REQUIRED_EVENT_TYPES - proxy_stats.event_types\n    \n    print(f\"Anthropic missing event types: {anthropic_missing}\")\n    print(f\"Proxy missing event types: {proxy_missing}\")\n    \n    # Check if proxy has the required events\n    if proxy_missing:\n        print(f\"⚠️ Proxy is missing required event types: {proxy_missing}\")\n    else:\n        print(\"✅ Proxy has all required event types\")\n    \n    # Compare content\n    if anthropic_stats.text_content and proxy_stats.text_content:\n        anthropic_preview = \"\\n\".join(anthropic_stats.text_content.strip().split(\"\\n\")[:5])\n        proxy_preview = \"\\n\".join(proxy_stats.text_content.strip().split(\"\\n\")[:5])\n        \n        print(\"\\n--- Anthropic Content Preview ---\")\n        print(anthropic_preview)\n        \n        print(\"\\n--- Proxy Content Preview ---\")\n        print(proxy_preview)\n    \n    # Compare tool use\n    if anthropic_stats.has_tool_use and proxy_stats.has_tool_use:\n        print(\"✅ Both have tool use\")\n    elif anthropic_stats.has_tool_use and not proxy_stats.has_tool_use:\n        print(\"⚠️ Anthropic has tool use but proxy does not\")\n    elif not anthropic_stats.has_tool_use and proxy_stats.has_tool_use:\n        print(\"⚠️ Proxy has tool use but Anthropic does not\")\n    \n    # Success as long as proxy has some content and no errors\n    return (not proxy_stats.has_error and \n            len(proxy_stats.text_content) > 0 or proxy_stats.has_tool_use)\n\nasync def test_streaming(test_name, request_data):\n    \"\"\"Run a streaming test with the given request data.\"\"\"\n    print(f\"\\n{'='*20} RUNNING STREAMING TEST: {test_name} {'='*20}\")\n    \n    # Log the request data\n    print(f\"\\nRequest data:\\n{json.dumps({k: v for k, v in request_data.items() if k != 'messages'}, indent=2)}\")\n    \n    # Make copies of the request data to avoid modifying the original\n    anthropic_data = request_data.copy()\n    proxy_data = request_data.copy()\n    \n    if not anthropic_data.get(\"stream\"):\n        anthropic_data[\"stream\"] = True\n    if not proxy_data.get(\"stream\"):\n        proxy_data[\"stream\"] = True\n    \n    check_tools = \"tools\" in request_data\n    \n    try:\n        # Send streaming requests\n        anthropic_stats, anthropic_error = await stream_response(\n            ANTHROPIC_API_URL, anthropic_headers, anthropic_data, \"Anthropic\"\n        )\n        \n        proxy_stats, proxy_error = await stream_response(\n            PROXY_API_URL, proxy_headers, proxy_data, \"Proxy\"\n        )\n        \n        # Print statistics\n        print(\"\\n--- Anthropic Stream Statistics ---\")\n        anthropic_stats.summarize()\n        \n        print(\"\\n--- Proxy Stream Statistics ---\")\n        proxy_stats.summarize()\n        \n        # Compare the responses\n        if anthropic_error:\n            print(f\"\\n⚠️ Anthropic stream had an error: {anthropic_error}\")\n            # If Anthropic errors, the test passes if proxy does anything useful\n            if not proxy_error and proxy_stats.total_chunks > 0:\n                print(f\"\\n✅ Test {test_name} passed! (Proxy worked even though Anthropic failed)\")\n                return True\n            else:\n                print(f\"\\n❌ Test {test_name} failed! Both streams had errors.\")\n                return False\n        \n        if proxy_error:\n            print(f\"\\n❌ Test {test_name} failed! Proxy had an error: {proxy_error}\")\n            return False\n        \n        result = compare_stream_stats(anthropic_stats, proxy_stats)\n        if result:\n            print(f\"\\n✅ Test {test_name} passed!\")\n            return True\n        else:\n            print(f\"\\n❌ Test {test_name} failed!\")\n            return False\n    \n    except Exception as e:\n        print(f\"\\n❌ Error in test {test_name}: {str(e)}\")\n        import traceback\n        traceback.print_exc()\n        return False\n\n# ================= MAIN =================\n\nasync def run_tests(args):\n    \"\"\"Run all tests based on command-line arguments.\"\"\"\n    # Track test results\n    results = {}\n    \n    # First run non-streaming tests\n    if not args.streaming_only:\n        print(\"\\n\\n=========== RUNNING NON-STREAMING TESTS ===========\\n\")\n        for test_name, test_data in TEST_SCENARIOS.items():\n            # Skip streaming tests\n            if test_data.get(\"stream\"):\n                continue\n                \n            # Skip tool tests if requested\n            if args.simple and \"tools\" in test_data:\n                continue\n                \n            # Skip non-tool tests if tools_only\n            if args.tools_only and \"tools\" not in test_data:\n                continue\n                \n            # Run the test\n            check_tools = \"tools\" in test_data\n            result = test_request(test_name, test_data, check_tools=check_tools)\n            results[test_name] = result\n    \n    # Now run streaming tests\n    if not args.no_streaming:\n        print(\"\\n\\n=========== RUNNING STREAMING TESTS ===========\\n\")\n        for test_name, test_data in TEST_SCENARIOS.items():\n            # Only select streaming tests, or force streaming\n            if not test_data.get(\"stream\") and not test_name.endswith(\"_stream\"):\n                continue\n                \n            # Skip tool tests if requested\n            if args.simple and \"tools\" in test_data:\n                continue\n                \n            # Skip non-tool tests if tools_only\n            if args.tools_only and \"tools\" not in test_data:\n                continue\n                \n            # Run the streaming test\n            result = await test_streaming(test_name, test_data)\n            results[f\"{test_name}_streaming\"] = result\n    \n    # Print summary\n    print(\"\\n\\n=========== TEST SUMMARY ===========\\n\")\n    total = len(results)\n    passed = sum(1 for v in results.values() if v)\n    \n    for test, result in results.items():\n        print(f\"{test}: {'✅ PASS' if result else '❌ FAIL'}\")\n    \n    print(f\"\\nTotal: {passed}/{total} tests passed\")\n    \n    if passed == total:\n        print(\"\\n🎉 All tests passed!\")\n        return True\n    else:\n        print(f\"\\n⚠️ {total - passed} tests failed\")\n        return False\n\nasync def main():\n    # Check that API key is set\n    if not ANTHROPIC_API_KEY:\n        print(\"Error: ANTHROPIC_API_KEY not set in .env file\")\n        return\n    \n    # Parse command-line arguments\n    parser = argparse.ArgumentParser(description=\"Test the Claude-on-OpenAI proxy\")\n    parser.add_argument(\"--no-streaming\", action=\"store_true\", help=\"Skip streaming tests\")\n    parser.add_argument(\"--streaming-only\", action=\"store_true\", help=\"Only run streaming tests\")\n    parser.add_argument(\"--simple\", action=\"store_true\", help=\"Only run simple tests (no tools)\")\n    parser.add_argument(\"--tools-only\", action=\"store_true\", help=\"Only run tool tests\")\n    args = parser.parse_args()\n    \n    # Run tests\n    success = await run_tests(args)\n    sys.exit(0 if success else 1)\n\nif __name__ == \"__main__\":\n    asyncio.run(main()) "
  }
]