Skip to content

Commit 6432474

Browse files
committed
feat(api): anthropic ollama native support params
1 parent 5c3c26d commit 6432474

4 files changed

Lines changed: 179 additions & 243 deletions

File tree

‎backend/lib/anthropic.py‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import aiohttp
2+
from fastapi import HTTPException
3+
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
4+
5+
6+
def extract_text_from_anthropic_messages(
7+
messages: List[Dict[str, Any]],
8+
system: Optional[Union[str, List[Dict[str, Any]]]] = None,
9+
) -> str:
10+
"""Extract plain text from Anthropic-format messages for token estimation."""
11+
parts = []
12+
13+
if isinstance(system, str):
14+
parts.append(system)
15+
elif isinstance(system, list):
16+
for block in system:
17+
if block.get("type") == "text":
18+
parts.append(block.get("text", ""))
19+
20+
for msg in messages:
21+
content = msg.get("content", "")
22+
if isinstance(content, str):
23+
parts.append(content)
24+
elif isinstance(content, list):
25+
for block in content:
26+
if block.get("type") == "text":
27+
parts.append(block.get("text", ""))
28+
29+
return " ".join(parts)
30+
31+
32+
async def fetch_anthropic_messages(
33+
model_config: Dict[str, Any],
34+
request_data: Dict[str, Any],
35+
anthropic_headers: Dict[str, str],
36+
) -> Dict[str, Any]:
37+
"""Proxy a non-streaming request to a native Anthropic Messages API endpoint."""
38+
api_base = model_config["params"]["api_base"]
39+
# Strip trailing /v1 if present so we can append /v1/messages
40+
url = api_base.rstrip("/").removesuffix("/v1") + "/v1/messages"
41+
42+
headers: Dict[str, str] = {"Content-Type": "application/json"}
43+
44+
api_key = model_config["params"].get("api_key")
45+
if api_key and api_key != "no_token":
46+
headers["x-api-key"] = api_key
47+
48+
# Forward Anthropic-specific headers from the client
49+
for h in ("anthropic-beta", "anthropic-version"):
50+
if anthropic_headers.get(h):
51+
headers[h] = anthropic_headers[h]
52+
53+
# Ensure a default anthropic-version if the client didn't send one
54+
if "anthropic-version" not in headers:
55+
headers["anthropic-version"] = "2023-06-01"
56+
57+
print(f"[anthropic-native] POST {url}")
58+
59+
async with aiohttp.ClientSession() as session:
60+
async with session.post(url, headers=headers, json=request_data) as resp:
61+
if resp.status != 200:
62+
text = await resp.text()
63+
print(f"[anthropic-native] Error {resp.status}: {text}")
64+
raise HTTPException(status_code=resp.status, detail=f"Model API error: {text}")
65+
return await resp.json()
66+
67+
68+
async def fetch_anthropic_messages_stream(
69+
model_config: Dict[str, Any],
70+
request_data: Dict[str, Any],
71+
anthropic_headers: Dict[str, str],
72+
) -> AsyncGenerator[str, None]:
73+
"""Proxy a streaming request to a native Anthropic Messages API endpoint.
74+
75+
Yields raw SSE chunks exactly as received from the backend.
76+
"""
77+
api_base = model_config["params"]["api_base"]
78+
url = api_base.rstrip("/").removesuffix("/v1") + "/v1/messages"
79+
80+
headers: Dict[str, str] = {"Content-Type": "application/json"}
81+
82+
api_key = model_config["params"].get("api_key")
83+
if api_key and api_key != "no_token":
84+
headers["x-api-key"] = api_key
85+
86+
for h in ("anthropic-beta", "anthropic-version"):
87+
if anthropic_headers.get(h):
88+
headers[h] = anthropic_headers[h]
89+
90+
if "anthropic-version" not in headers:
91+
headers["anthropic-version"] = "2023-06-01"
92+
93+
print(f"[anthropic-native-stream] POST {url}")
94+
95+
async with aiohttp.ClientSession() as session:
96+
async with session.post(url, headers=headers, json=request_data) as resp:
97+
if resp.status != 200:
98+
text = await resp.text()
99+
print(f"[anthropic-native-stream] Error {resp.status}: {text}")
100+
raise HTTPException(status_code=resp.status, detail=f"Model API error: {text}")
101+
102+
async for line in resp.content:
103+
line_str = line.decode("utf-8")
104+
if line_str.strip():
105+
yield line_str

‎backend/lib/anthropic_compat.py‎

Lines changed: 0 additions & 197 deletions
This file was deleted.

‎backend/lib/auth.py‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,26 @@
1717
METRICS_PASSWORD = METRICS_AUTH.get('password', 'change-me')
1818

1919
# Security
20-
security = HTTPBearer()
20+
security = HTTPBearer(auto_error=False)
2121

2222

2323

2424

2525

2626
# verify user token and model access
27-
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
28-
token = credentials.credentials
29-
for key in CONFIG['keys']:
30-
if key['token'] == token:
31-
return key
27+
def verify_token(request: Request, credentials: HTTPAuthorizationCredentials = Security(security)):
28+
# Try Bearer token first, then fall back to x-api-key header (Anthropic convention)
29+
token = None
30+
if credentials:
31+
token = credentials.credentials
32+
else:
33+
token = request.headers.get("x-api-key")
34+
35+
if token:
36+
for key in CONFIG['keys']:
37+
if key['token'] == token:
38+
return key
39+
3240
log_error("anonymous", 401)
3341
raise HTTPException(
3442
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -112,9 +120,9 @@ def check_rate_limit(user_key, rpm_limit):
112120

113121

114122

115-
def verify_auth(credentials: HTTPAuthorizationCredentials = Security(security)):
116-
user_key = verify_token(credentials)
117-
token = credentials.credentials
123+
def verify_auth(request: Request, credentials: HTTPAuthorizationCredentials = Security(security)):
124+
user_key = verify_token(request, credentials)
125+
token = credentials.credentials if credentials else request.headers.get("x-api-key")
118126
user = get_user_from_token(token)
119127
check_rate_limit(user["name"],user.get("rpm_limit", 60))
120128
return user_key

0 commit comments

Comments
 (0)