-
Notifications
You must be signed in to change notification settings - Fork 316
Expand file tree
/
Copy pathendpoints.py
More file actions
289 lines (257 loc) · 11 KB
/
endpoints.py
File metadata and controls
289 lines (257 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
from fastapi import APIRouter, HTTPException, Request, Header, Depends
from fastapi.responses import JSONResponse, StreamingResponse
from datetime import datetime
import uuid
from typing import Optional
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.conversion.request_converter import convert_claude_to_openai
from src.conversion.response_converter import (
convert_openai_to_claude_response,
convert_openai_streaming_to_claude_with_cancellation,
)
from src.core.model_manager import model_manager
router = APIRouter()
# Create OpenAI client - use env key if available, otherwise create a dummy one
if config.openai_api_key:
openai_client = OpenAIClient(
config.openai_api_key,
config.openai_base_url,
config.request_timeout,
api_version=config.azure_api_version,
)
else:
# In passthrough mode, create a client with a dummy key for compatibility
# The actual client will be created per-request with user's API key
openai_client = OpenAIClient(
"sk-dummy", # This won't be used for actual requests
config.openai_base_url,
config.request_timeout,
api_version=config.azure_api_version,
)
def get_openai_client(user_api_key: Optional[str] = None) -> OpenAIClient:
"""Get OpenAI client - use env key if available, otherwise create with user-provided key."""
if config.openai_api_key:
# Proxy mode: use pre-configured client
return openai_client
elif user_api_key:
# Passthrough mode: create client with user-provided key
if not config.validate_api_key(user_api_key):
raise HTTPException(
status_code=401,
detail="Invalid OpenAI API key format. Please provide a valid API key starting with 'sk-'."
)
return OpenAIClient(
user_api_key,
config.openai_base_url,
config.request_timeout,
api_version=config.azure_api_version,
)
else:
raise HTTPException(
status_code=401,
detail="No OpenAI API key available. Please set OPENAI_API_KEY environment variable or provide your OpenAI API key in Authorization header."
)
async def validate_api_key_and_extract(x_api_key: Optional[str] = Header(None), authorization: Optional[str] = Header(None)):
"""Extract API key and handle validation based on configuration."""
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 ", "")
# If OPENAI_API_KEY is configured in environment, use proxy validation
if config.openai_api_key:
# Proxy mode: validate against ANTHROPIC_API_KEY if configured
if config.anthropic_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."
)
# Return None since we'll use env OPENAI_API_KEY
return None
else:
# Passthrough mode: no ANTHROPIC validation, use client key as OpenAI key
if not client_api_key:
raise HTTPException(
status_code=401,
detail="No API key provided. Please provide your OpenAI API key in Authorization header."
)
# Return the client API key to be used as OpenAI key
return client_api_key
@router.post("/v1/messages")
async def create_message(request: ClaudeMessagesRequest, http_request: Request, user_api_key: str = Depends(validate_api_key_and_extract)):
try:
logger.debug(
f"Processing Claude request: model={request.model}, stream={request.stream}"
)
# Generate unique request ID for cancellation tracking
request_id = str(uuid.uuid4())
# Get OpenAI client (either default or created with user's key)
openai_client = get_openai_client(user_api_key)
# Convert Claude request to OpenAI format
openai_request = convert_claude_to_openai(request, model_manager)
# Check if client disconnected before processing
if await http_request.is_disconnected():
raise HTTPException(status_code=499, detail="Client disconnected")
if request.stream:
# Streaming response - wrap in error handling
try:
openai_stream = openai_client.create_chat_completion_stream(
openai_request, request_id
)
return StreamingResponse(
convert_openai_streaming_to_claude_with_cancellation(
openai_stream,
request,
logger,
http_request,
openai_client,
request_id,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
},
)
except HTTPException as e:
# Convert to proper error response for streaming
logger.error(f"Streaming error: {e.detail}")
import traceback
logger.error(traceback.format_exc())
error_message = openai_client.classify_openai_error(e.detail)
error_response = {
"type": "error",
"error": {"type": "api_error", "message": error_message},
}
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
)
return claude_response
except HTTPException:
raise
except Exception as e:
import traceback
logger.error(f"Unexpected error processing request: {e}")
logger.error(traceback.format_exc())
error_message = openai_client.classify_openai_error(str(e))
raise HTTPException(status_code=500, detail=error_message)
@router.post("/v1/messages/count_tokens")
async def count_tokens(request: ClaudeTokenCountRequest, user_api_key: str = Depends(validate_api_key_and_extract)):
try:
# For token counting, we'll use a simple estimation
# In a real implementation, you might want to use tiktoken or similar
total_chars = 0
# Count system message characters
if request.system:
if isinstance(request.system, str):
total_chars += len(request.system)
elif isinstance(request.system, list):
for block in request.system:
if hasattr(block, "text"):
total_chars += len(block.text)
# Count message characters
for msg in request.messages:
if msg.content is None:
continue
elif isinstance(msg.content, str):
total_chars += len(msg.content)
elif isinstance(msg.content, list):
for block in msg.content:
if hasattr(block, "text") and block.text is not None:
total_chars += len(block.text)
# Rough estimation: 4 characters per token
estimated_tokens = max(1, total_chars // 4)
return {"input_tokens": estimated_tokens}
except Exception as e:
logger.error(f"Error counting tokens: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"openai_api_configured": bool(config.openai_api_key),
"api_key_valid": config.validate_api_key() if config.openai_api_key else "per_request",
"client_api_key_validation": bool(config.anthropic_api_key),
}
@router.get("/test-connection")
async def test_connection():
"""Test API connectivity to OpenAI"""
try:
# Use default client if available, otherwise require user API key
if not config.openai_api_key:
return JSONResponse(
status_code=400,
content={
"status": "failed",
"message": "No default OpenAI API key configured. Test connection requires OPENAI_API_KEY environment variable.",
"timestamp": datetime.now().isoformat(),
}
)
# Simple test request to verify API connectivity
test_response = await openai_client.create_chat_completion(
{
"model": config.small_model,
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 5,
}
)
return {
"status": "success",
"message": "Successfully connected to OpenAI API",
"model_used": config.small_model,
"timestamp": datetime.now().isoformat(),
"response_id": test_response.get("id", "unknown"),
}
except Exception as e:
logger.error(f"API connectivity test failed: {e}")
return JSONResponse(
status_code=503,
content={
"status": "failed",
"error_type": "API Error",
"message": str(e),
"timestamp": datetime.now().isoformat(),
"suggestions": [
"Check your OPENAI_API_KEY is valid",
"Verify your API key has the necessary permissions",
"Check if you have reached rate limits",
],
},
)
@router.get("/")
async def root():
"""Root endpoint"""
return {
"message": "Claude-to-OpenAI API Proxy v1.0.0",
"status": "running",
"config": {
"openai_base_url": config.openai_base_url,
"max_tokens_limit": config.max_tokens_limit,
"api_key_configured": bool(config.openai_api_key),
"client_api_key_validation": bool(config.anthropic_api_key),
"big_model": config.big_model,
"small_model": config.small_model,
},
"endpoints": {
"messages": "/v1/messages",
"count_tokens": "/v1/messages/count_tokens",
"health": "/health",
"test_connection": "/test-connection",
},
}