Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ Issues = "https://github.com/holegots/claude-code-proxy/issues"
[project.scripts]
claude-code-proxy = "src.main:main"

[tool.uv]
dev-dependencies = [
[dependency-groups]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"black>=23.0.0",
Expand Down
83 changes: 66 additions & 17 deletions src/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from src.core.config import config
from src.core.logging import logger
from src.core.client import OpenAIClient
from src.models.claude import ClaudeMessagesRequest, ClaudeTokenCountRequest
from src.models.claude import (
ClaudeMessagesRequest,
ClaudeTokenCountRequest,
EventLoggingBatchResponse,
)
from src.conversion.request_converter import convert_claude_to_openai
from src.conversion.response_converter import (
convert_openai_to_claude_response,
Expand All @@ -28,34 +32,37 @@
custom_headers=custom_headers,
)

async def validate_api_key(x_api_key: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):

async def validate_api_key(
x_api_key: Optional[str] = Header(None), authorization: Optional[str] = Header(None)
):
"""Validate the client's API key from either x-api-key header or Authorization header."""
client_api_key = None

# Extract API key from headers
if x_api_key:
client_api_key = x_api_key
elif authorization and authorization.startswith("Bearer "):
client_api_key = authorization.replace("Bearer ", "")

# Skip validation if ANTHROPIC_API_KEY is not set in the environment
if not config.anthropic_api_key:
return

# Validate the client API key
if not client_api_key or not config.validate_client_api_key(client_api_key):
logger.warning(f"Invalid API key provided by client")
raise HTTPException(
status_code=401,
detail="Invalid API key. Please provide a valid Anthropic API key."
status_code=401, detail="Invalid API key. Please provide a valid Anthropic API key."
)


@router.post("/v1/messages")
async def create_message(request: ClaudeMessagesRequest, http_request: Request, _: None = Depends(validate_api_key)):
async def create_message(
request: ClaudeMessagesRequest, http_request: Request, _: None = Depends(validate_api_key)
):
try:
logger.debug(
f"Processing Claude request: model={request.model}, stream={request.stream}"
)
logger.debug(f"Processing Claude request: model={request.model}, stream={request.stream}")

# Generate unique request ID for cancellation tracking
request_id = str(uuid.uuid4())
Expand Down Expand Up @@ -104,12 +111,8 @@ async def create_message(request: ClaudeMessagesRequest, http_request: Request,
return JSONResponse(status_code=e.status_code, content=error_response)
else:
# Non-streaming response
openai_response = await openai_client.create_chat_completion(
openai_request, request_id
)
claude_response = convert_openai_to_claude_response(
openai_response, request
)
openai_response = await openai_client.create_chat_completion(openai_request, request_id)
claude_response = convert_openai_to_claude_response(openai_response, request)
return claude_response
except HTTPException:
raise
Expand Down Expand Up @@ -160,6 +163,51 @@ async def count_tokens(request: ClaudeTokenCountRequest, _: None = Depends(valid
raise HTTPException(status_code=500, detail=str(e))


@router.post("/api/event_logging/batch")
async def event_logging_batch(request: Request) -> EventLoggingBatchResponse:
try:
try:
body = await request.json()
except ValueError:
body = {}

batch_id = body.get("batch_id") or f"batch_{uuid.uuid4().hex[:16]}"
events = body.get("events", [])
event_items = events if isinstance(events, list) else [events] if events else []
processed_count = len(event_items)

if event_items:
logger.debug(
f"Event logging batch received: batch_id={batch_id}, "
f"events_count={processed_count}"
)
for index, event in enumerate(event_items[:5], start=1):
event_type = (
event.get("event_type", "unknown") if isinstance(event, dict) else "unknown"
)
logger.debug(f"Event {index}: type={event_type}")

remaining_count = processed_count - 5
if remaining_count > 0:
logger.debug(f"Event logging batch has {remaining_count} additional events")

return EventLoggingBatchResponse(
success=True,
batch_id=batch_id,
processed_count=processed_count,
message="Events logged successfully",
)

except Exception as e:
logger.warning(f"Event logging batch error: {e}")
return EventLoggingBatchResponse(
success=True,
batch_id=f"batch_{uuid.uuid4().hex[:16]}",
processed_count=0,
message="Events received",
)


@router.get("/health")
async def health_check():
"""Health check endpoint"""
Expand Down Expand Up @@ -228,6 +276,7 @@ async def root():
"endpoints": {
"messages": "/v1/messages",
"count_tokens": "/v1/messages/count_tokens",
"event_logging_batch": "/api/event_logging/batch",
"health": "/health",
"test_connection": "/test-connection",
},
Expand Down
20 changes: 7 additions & 13 deletions src/conversion/request_converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
from typing import Dict, Any, List
from venv import logger
from src.core.constants import Constants
from src.models.claude import ClaudeMessagesRequest, ClaudeMessage
from src.core.config import config
Expand Down Expand Up @@ -30,17 +29,12 @@ def convert_claude_to_openai(
for block in claude_request.system:
if hasattr(block, "type") and block.type == Constants.CONTENT_TEXT:
text_parts.append(block.text)
elif (
isinstance(block, dict)
and block.get("type") == Constants.CONTENT_TEXT
):
elif isinstance(block, dict) and block.get("type") == Constants.CONTENT_TEXT:
text_parts.append(block.get("text", ""))
system_text = "\n\n".join(text_parts)

if system_text.strip():
openai_messages.append(
{"role": Constants.ROLE_SYSTEM, "content": system_text.strip()}
)
openai_messages.append({"role": Constants.ROLE_SYSTEM, "content": system_text.strip()})

# Process Claude messages
i = 0
Expand Down Expand Up @@ -133,7 +127,7 @@ def convert_claude_user_message(msg: ClaudeMessage) -> Dict[str, Any]:
"""Convert Claude user message to OpenAI format."""
if msg.content is None:
return {"role": Constants.ROLE_USER, "content": ""}

if isinstance(msg.content, str):
return {"role": Constants.ROLE_USER, "content": msg.content}

Expand Down Expand Up @@ -172,7 +166,7 @@ def convert_claude_assistant_message(msg: ClaudeMessage) -> Dict[str, Any]:

if msg.content is None:
return {"role": Constants.ROLE_ASSISTANT, "content": None}

if isinstance(msg.content, str):
return {"role": Constants.ROLE_ASSISTANT, "content": msg.content}

Expand Down Expand Up @@ -246,7 +240,7 @@ def parse_tool_result_content(content):
else:
try:
result_parts.append(json.dumps(item, ensure_ascii=False))
except:
except (TypeError, ValueError):
result_parts.append(str(item))
return "\n".join(result_parts).strip()

Expand All @@ -255,10 +249,10 @@ def parse_tool_result_content(content):
return content.get("text", "")
try:
return json.dumps(content, ensure_ascii=False)
except:
except (TypeError, ValueError):
return str(content)

try:
return str(content)
except:
except Exception:
return "Unparseable content"
Loading