This commit is contained in:
Timothy Jaeryang Baek
2026-03-17 17:58:01 -05:00
parent fcf7208352
commit de3317e26b
220 changed files with 17200 additions and 22836 deletions
+204 -212
View File
@@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
def is_anthropic_url(url: str) -> bool:
"""Check if the URL is an Anthropic API endpoint."""
return "api.anthropic.com" in url
return 'api.anthropic.com' in url
async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> dict:
@@ -31,56 +31,56 @@ async def get_anthropic_models(url: str, key: str, user: UserModel = None) -> di
try:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
headers = {
"x-api-key": key,
"anthropic-version": "2023-06-01",
'x-api-key': key,
'anthropic-version': '2023-06-01',
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
headers = include_user_info_headers(headers, user)
while True:
params = {"limit": 1000}
params = {'limit': 1000}
if after_id:
params["after_id"] = after_id
params['after_id'] = after_id
async with session.get(
f"{url}/models",
f'{url}/models',
headers=headers,
params=params,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
if response.status != 200:
error_detail = f"HTTP Error: {response.status}"
error_detail = f'HTTP Error: {response.status}'
try:
res = await response.json()
if "error" in res:
error_detail = f"External Error: {res['error']}"
if 'error' in res:
error_detail = f'External Error: {res["error"]}'
except Exception:
pass
return {"object": "list", "data": [], "error": error_detail}
return {'object': 'list', 'data': [], 'error': error_detail}
data = await response.json()
for model in data.get("data", []):
for model in data.get('data', []):
all_models.append(
{
"id": model.get("id"),
"object": "model",
"created": 0,
"owned_by": "anthropic",
"name": model.get("display_name", model.get("id")),
'id': model.get('id'),
'object': 'model',
'created': 0,
'owned_by': 'anthropic',
'name': model.get('display_name', model.get('id')),
}
)
if not data.get("has_more", False):
if not data.get('has_more', False):
break
after_id = data.get("last_id")
after_id = data.get('last_id')
except Exception as e:
log.error(f"Anthropic connection error: {e}")
log.error(f'Anthropic connection error: {e}')
return None
return {"object": "list", "data": all_models}
return {'object': 'list', 'data': all_models}
##############################
@@ -102,245 +102,241 @@ def convert_anthropic_to_openai_payload(anthropic_payload: dict) -> dict:
openai_payload = {}
# Model
openai_payload["model"] = anthropic_payload.get("model", "")
openai_payload['model'] = anthropic_payload.get('model', '')
# Build messages list
messages = []
# System prompt (Anthropic has it as top-level, OpenAI as a system message)
system = anthropic_payload.get("system")
system = anthropic_payload.get('system')
if system:
if isinstance(system, str):
messages.append({"role": "system", "content": system})
messages.append({'role': 'system', 'content': system})
elif isinstance(system, list):
# Anthropic supports system as list of content blocks
text_parts = []
for block in system:
if isinstance(block, dict) and block.get("type") == "text":
text_parts.append(block.get("text", ""))
if isinstance(block, dict) and block.get('type') == 'text':
text_parts.append(block.get('text', ''))
elif isinstance(block, str):
text_parts.append(block)
messages.append({"role": "system", "content": "\n".join(text_parts)})
messages.append({'role': 'system', 'content': '\n'.join(text_parts)})
# Convert messages
for msg in anthropic_payload.get("messages", []):
role = msg.get("role", "user")
content = msg.get("content")
for msg in anthropic_payload.get('messages', []):
role = msg.get('role', 'user')
content = msg.get('content')
if isinstance(content, str):
messages.append({"role": role, "content": content})
messages.append({'role': role, 'content': content})
elif isinstance(content, list):
# Convert Anthropic content blocks to OpenAI format
openai_content = []
tool_calls = []
for block in content:
block_type = block.get("type", "text")
block_type = block.get('type', 'text')
if block_type == "text":
if block_type == 'text':
openai_content.append(
{
"type": "text",
"text": block.get("text", ""),
'type': 'text',
'text': block.get('text', ''),
}
)
elif block_type == "image":
source = block.get("source", {})
if source.get("type") == "base64":
media_type = source.get("media_type", "image/png")
data = source.get("data", "")
elif block_type == 'image':
source = block.get('source', {})
if source.get('type') == 'base64':
media_type = source.get('media_type', 'image/png')
data = source.get('data', '')
openai_content.append(
{
"type": "image_url",
"image_url": {
"url": f"data:{media_type};base64,{data}",
'type': 'image_url',
'image_url': {
'url': f'data:{media_type};base64,{data}',
},
}
)
elif source.get("type") == "url":
elif source.get('type') == 'url':
openai_content.append(
{
"type": "image_url",
"image_url": {"url": source.get("url", "")},
'type': 'image_url',
'image_url': {'url': source.get('url', '')},
}
)
elif block_type == "tool_use":
elif block_type == 'tool_use':
tool_calls.append(
{
"id": block.get("id", ""),
"type": "function",
"function": {
"name": block.get("name", ""),
"arguments": (
json.dumps(block.get("input", {}))
if isinstance(block.get("input"), dict)
else str(block.get("input", "{}"))
'id': block.get('id', ''),
'type': 'function',
'function': {
'name': block.get('name', ''),
'arguments': (
json.dumps(block.get('input', {}))
if isinstance(block.get('input'), dict)
else str(block.get('input', '{}'))
),
},
}
)
elif block_type == "tool_result":
elif block_type == 'tool_result':
# Tool results become separate tool messages in OpenAI format
tool_content = block.get("content", "")
tool_content = block.get('content', '')
if isinstance(tool_content, list):
tool_text_parts = []
for tc in tool_content:
if isinstance(tc, dict) and tc.get("type") == "text":
tool_text_parts.append(tc.get("text", ""))
tool_content = "\n".join(tool_text_parts)
if isinstance(tc, dict) and tc.get('type') == 'text':
tool_text_parts.append(tc.get('text', ''))
tool_content = '\n'.join(tool_text_parts)
# Propagate error status if present
if block.get("is_error"):
tool_content = f"Error: {tool_content}"
if block.get('is_error'):
tool_content = f'Error: {tool_content}'
messages.append(
{
"role": "tool",
"tool_call_id": block.get("tool_use_id", ""),
"content": tool_content,
'role': 'tool',
'tool_call_id': block.get('tool_use_id', ''),
'content': tool_content,
}
)
# Build the message
if tool_calls:
# Assistant message with tool calls
msg_dict = {"role": role}
msg_dict = {'role': role}
if openai_content:
# If there's only text, flatten it
if len(openai_content) == 1 and openai_content[0]["type"] == "text":
msg_dict["content"] = openai_content[0]["text"]
if len(openai_content) == 1 and openai_content[0]['type'] == 'text':
msg_dict['content'] = openai_content[0]['text']
else:
msg_dict["content"] = openai_content
msg_dict['content'] = openai_content
else:
msg_dict["content"] = ""
msg_dict["tool_calls"] = tool_calls
msg_dict['content'] = ''
msg_dict['tool_calls'] = tool_calls
messages.append(msg_dict)
elif openai_content:
# If there's only a single text block, flatten it to a string
if len(openai_content) == 1 and openai_content[0]["type"] == "text":
messages.append(
{"role": role, "content": openai_content[0]["text"]}
)
if len(openai_content) == 1 and openai_content[0]['type'] == 'text':
messages.append({'role': role, 'content': openai_content[0]['text']})
else:
messages.append({"role": role, "content": openai_content})
messages.append({'role': role, 'content': openai_content})
else:
messages.append({"role": role, "content": str(content) if content else ""})
messages.append({'role': role, 'content': str(content) if content else ''})
openai_payload["messages"] = messages
openai_payload['messages'] = messages
# max_tokens
if "max_tokens" in anthropic_payload:
openai_payload["max_tokens"] = anthropic_payload["max_tokens"]
if 'max_tokens' in anthropic_payload:
openai_payload['max_tokens'] = anthropic_payload['max_tokens']
# Common parameters
for param in ("temperature", "top_p", "stop_sequences", "stream"):
for param in ('temperature', 'top_p', 'stop_sequences', 'stream'):
if param in anthropic_payload:
if param == "stop_sequences":
openai_payload["stop"] = anthropic_payload[param]
if param == 'stop_sequences':
openai_payload['stop'] = anthropic_payload[param]
else:
openai_payload[param] = anthropic_payload[param]
# Tools conversion: Anthropic → OpenAI
if "tools" in anthropic_payload:
if 'tools' in anthropic_payload:
openai_tools = []
for tool in anthropic_payload["tools"]:
for tool in anthropic_payload['tools']:
openai_tools.append(
{
"type": "function",
"function": {
"name": tool.get("name", ""),
"description": tool.get("description", ""),
"parameters": tool.get("input_schema", {}),
'type': 'function',
'function': {
'name': tool.get('name', ''),
'description': tool.get('description', ''),
'parameters': tool.get('input_schema', {}),
},
}
)
openai_payload["tools"] = openai_tools
openai_payload['tools'] = openai_tools
# tool_choice
if "tool_choice" in anthropic_payload:
tc = anthropic_payload["tool_choice"]
if 'tool_choice' in anthropic_payload:
tc = anthropic_payload['tool_choice']
if isinstance(tc, dict):
tc_type = tc.get("type", "auto")
if tc_type == "auto":
openai_payload["tool_choice"] = "auto"
elif tc_type == "any":
openai_payload["tool_choice"] = "required"
elif tc_type == "tool":
openai_payload["tool_choice"] = {
"type": "function",
"function": {"name": tc.get("name", "")},
tc_type = tc.get('type', 'auto')
if tc_type == 'auto':
openai_payload['tool_choice'] = 'auto'
elif tc_type == 'any':
openai_payload['tool_choice'] = 'required'
elif tc_type == 'tool':
openai_payload['tool_choice'] = {
'type': 'function',
'function': {'name': tc.get('name', '')},
}
return openai_payload
def convert_openai_to_anthropic_response(
openai_response: dict, model: str = ""
) -> dict:
def convert_openai_to_anthropic_response(openai_response: dict, model: str = '') -> dict:
"""
Convert a non-streaming OpenAI Chat Completions response to Anthropic Messages format.
"""
import uuid as _uuid
choice = {}
if openai_response.get("choices"):
choice = openai_response["choices"][0]
if openai_response.get('choices'):
choice = openai_response['choices'][0]
message = choice.get("message", {})
finish_reason = choice.get("finish_reason", "stop")
message = choice.get('message', {})
finish_reason = choice.get('finish_reason', 'stop')
# Map finish_reason to stop_reason
stop_reason_map = {
"stop": "end_turn",
"length": "max_tokens",
"tool_calls": "tool_use",
"content_filter": "end_turn",
'stop': 'end_turn',
'length': 'max_tokens',
'tool_calls': 'tool_use',
'content_filter': 'end_turn',
}
stop_reason = stop_reason_map.get(finish_reason, "end_turn")
stop_reason = stop_reason_map.get(finish_reason, 'end_turn')
# Build content blocks
content = []
msg_content = message.get("content")
msg_content = message.get('content')
if msg_content:
content.append({"type": "text", "text": msg_content})
content.append({'type': 'text', 'text': msg_content})
# Tool calls → tool_use blocks
tool_calls = message.get("tool_calls", [])
tool_calls = message.get('tool_calls', [])
for tc in tool_calls:
func = tc.get("function", {})
func = tc.get('function', {})
try:
tool_input = json.loads(func.get("arguments", "{}"))
tool_input = json.loads(func.get('arguments', '{}'))
except (json.JSONDecodeError, TypeError):
tool_input = {}
content.append(
{
"type": "tool_use",
"id": tc.get("id", f"toolu_{_uuid.uuid4().hex[:24]}"),
"name": func.get("name", ""),
"input": tool_input,
'type': 'tool_use',
'id': tc.get('id', f'toolu_{_uuid.uuid4().hex[:24]}'),
'name': func.get('name', ''),
'input': tool_input,
}
)
# Usage
openai_usage = openai_response.get("usage", {})
openai_usage = openai_response.get('usage', {})
usage = {
"input_tokens": openai_usage.get("prompt_tokens", 0),
"output_tokens": openai_usage.get("completion_tokens", 0),
'input_tokens': openai_usage.get('prompt_tokens', 0),
'output_tokens': openai_usage.get('completion_tokens', 0),
}
return {
"id": openai_response.get("id", f"msg_{_uuid.uuid4().hex[:24]}"),
"type": "message",
"role": "assistant",
"content": content,
"model": model or openai_response.get("model", ""),
"stop_reason": stop_reason,
"stop_sequence": None,
"usage": usage,
'id': openai_response.get('id', f'msg_{_uuid.uuid4().hex[:24]}'),
'type': 'message',
'role': 'assistant',
'content': content,
'model': model or openai_response.get('model', ''),
'stop_reason': stop_reason,
'stop_sequence': None,
'usage': usage,
}
async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str = ""):
async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str = ''):
"""
Convert an OpenAI SSE streaming response to Anthropic Messages SSE format.
@@ -352,10 +348,10 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
"""
import uuid as _uuid
msg_id = f"msg_{_uuid.uuid4().hex[:24]}"
msg_id = f'msg_{_uuid.uuid4().hex[:24]}'
input_tokens = 0
output_tokens = 0
stop_reason = "end_turn"
stop_reason = 'end_turn'
# Track content blocks with a running index.
# Each text block or tool_use block gets its own index.
@@ -369,35 +365,35 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
# Emit message_start
message_start = {
"type": "message_start",
"message": {
"id": msg_id,
"type": "message",
"role": "assistant",
"content": [],
"model": model,
"stop_reason": None,
"stop_sequence": None,
"usage": {"input_tokens": 0, "output_tokens": 0},
'type': 'message_start',
'message': {
'id': msg_id,
'type': 'message',
'role': 'assistant',
'content': [],
'model': model,
'stop_reason': None,
'stop_sequence': None,
'usage': {'input_tokens': 0, 'output_tokens': 0},
},
}
yield f"event: message_start\ndata: {json.dumps(message_start)}\n\n".encode()
yield f'event: message_start\ndata: {json.dumps(message_start)}\n\n'.encode()
try:
async for chunk in openai_stream_generator:
if isinstance(chunk, bytes):
chunk = chunk.decode("utf-8", errors="ignore")
chunk = chunk.decode('utf-8', errors='ignore')
for line in chunk.strip().split("\n"):
for line in chunk.strip().split('\n'):
line = line.strip()
if not line or not line.startswith("data:"):
if not line or not line.startswith('data:'):
continue
data_str = line[5:].strip()
if data_str == "[DONE]":
if data_str == '[DONE]':
continue
if data_str == "{}":
if data_str == '{}':
continue
try:
@@ -405,62 +401,58 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
except (json.JSONDecodeError, TypeError):
continue
choices = data.get("choices", [])
choices = data.get('choices', [])
if not choices:
# Check for usage in the final chunk
if data.get("usage"):
input_tokens = data["usage"].get("prompt_tokens", input_tokens)
output_tokens = data["usage"].get(
"completion_tokens", output_tokens
)
if data.get('usage'):
input_tokens = data['usage'].get('prompt_tokens', input_tokens)
output_tokens = data['usage'].get('completion_tokens', output_tokens)
continue
delta = choices[0].get("delta", {})
finish_reason = choices[0].get("finish_reason")
delta = choices[0].get('delta', {})
finish_reason = choices[0].get('finish_reason')
# Update usage if present
if data.get("usage"):
input_tokens = data["usage"].get("prompt_tokens", input_tokens)
output_tokens = data["usage"].get(
"completion_tokens", output_tokens
)
if data.get('usage'):
input_tokens = data['usage'].get('prompt_tokens', input_tokens)
output_tokens = data['usage'].get('completion_tokens', output_tokens)
# --- Handle text content ---
content = delta.get("content")
content = delta.get('content')
if content is not None:
if not text_block_open:
# Start a new text content block
block_start = {
"type": "content_block_start",
"index": current_block_index,
"content_block": {"type": "text", "text": ""},
'type': 'content_block_start',
'index': current_block_index,
'content_block': {'type': 'text', 'text': ''},
}
yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n".encode()
yield f'event: content_block_start\ndata: {json.dumps(block_start)}\n\n'.encode()
text_block_open = True
# Send text delta
block_delta = {
"type": "content_block_delta",
"index": current_block_index,
"delta": {"type": "text_delta", "text": content},
'type': 'content_block_delta',
'index': current_block_index,
'delta': {'type': 'text_delta', 'text': content},
}
yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n".encode()
yield f'event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n'.encode()
# --- Handle tool calls ---
tool_calls = delta.get("tool_calls")
tool_calls = delta.get('tool_calls')
if tool_calls:
# Close text block if one is open (text comes before tools)
if text_block_open:
block_stop = {
"type": "content_block_stop",
"index": current_block_index,
'type': 'content_block_stop',
'index': current_block_index,
}
yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode()
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
text_block_open = False
current_block_index += 1
for tc in tool_calls:
tc_index = tc.get("index", 0)
tc_index = tc.get('index', 0)
if tc_index not in tool_call_started:
# First time seeing this tool call — emit content_block_start
@@ -468,67 +460,67 @@ async def openai_stream_to_anthropic_stream(openai_stream_generator, model: str
tool_call_started[tc_index] = True
# Extract tool call ID and name from the first chunk
tc_id = tc.get("id", f"toolu_{_uuid.uuid4().hex[:24]}")
tc_name = tc.get("function", {}).get("name", "")
tc_id = tc.get('id', f'toolu_{_uuid.uuid4().hex[:24]}')
tc_name = tc.get('function', {}).get('name', '')
block_start = {
"type": "content_block_start",
"index": current_block_index,
"content_block": {
"type": "tool_use",
"id": tc_id,
"name": tc_name,
"input": {},
'type': 'content_block_start',
'index': current_block_index,
'content_block': {
'type': 'tool_use',
'id': tc_id,
'name': tc_name,
'input': {},
},
}
yield f"event: content_block_start\ndata: {json.dumps(block_start)}\n\n".encode()
yield f'event: content_block_start\ndata: {json.dumps(block_start)}\n\n'.encode()
current_block_index += 1
# Emit argument chunks as input_json_delta
args_chunk = tc.get("function", {}).get("arguments", "")
args_chunk = tc.get('function', {}).get('arguments', '')
if args_chunk:
block_delta = {
"type": "content_block_delta",
"index": tool_call_blocks[tc_index],
"delta": {
"type": "input_json_delta",
"partial_json": args_chunk,
'type': 'content_block_delta',
'index': tool_call_blocks[tc_index],
'delta': {
'type': 'input_json_delta',
'partial_json': args_chunk,
},
}
yield f"event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n".encode()
yield f'event: content_block_delta\ndata: {json.dumps(block_delta)}\n\n'.encode()
# --- Handle finish reason ---
if finish_reason is not None:
stop_reason_map = {
"stop": "end_turn",
"length": "max_tokens",
"tool_calls": "tool_use",
'stop': 'end_turn',
'length': 'max_tokens',
'tool_calls': 'tool_use',
}
stop_reason = stop_reason_map.get(finish_reason, "end_turn")
stop_reason = stop_reason_map.get(finish_reason, 'end_turn')
except Exception as e:
log.error(f"Error in Anthropic stream conversion: {e}")
log.error(f'Error in Anthropic stream conversion: {e}')
# Close any open text block
if text_block_open:
block_stop = {"type": "content_block_stop", "index": current_block_index}
yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode()
block_stop = {'type': 'content_block_stop', 'index': current_block_index}
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
# Close any open tool call blocks
for tc_index, block_index in tool_call_blocks.items():
block_stop = {"type": "content_block_stop", "index": block_index}
yield f"event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n".encode()
block_stop = {'type': 'content_block_stop', 'index': block_index}
yield f'event: content_block_stop\ndata: {json.dumps(block_stop)}\n\n'.encode()
# Emit message_delta with stop reason
message_delta = {
"type": "message_delta",
"delta": {
"stop_reason": stop_reason,
"stop_sequence": None,
'type': 'message_delta',
'delta': {
'stop_reason': stop_reason,
'stop_sequence': None,
},
"usage": {"output_tokens": output_tokens},
'usage': {'output_tokens': output_tokens},
}
yield f"event: message_delta\ndata: {json.dumps(message_delta)}\n\n".encode()
yield f'event: message_delta\ndata: {json.dumps(message_delta)}\n\n'.encode()
# Emit message_stop
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n".encode()
yield f'event: message_stop\ndata: {json.dumps({"type": "message_stop"})}\n\n'.encode()