Wilker commited on
Commit Β·
025f2a2
1
Parent(s): dbb524c
feat(app): /v1/messages agora suporta tools e formato Anthropic
Browse files
app.py
CHANGED
|
@@ -645,6 +645,59 @@ def convert_to_gemini_format(chat_messages, system_content=None):
|
|
| 645 |
return payload
|
| 646 |
|
| 647 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
# ββ Helper: Sanitiza mensagens para GGUF ββββββββββββββββββββββββββββββββββββββ
|
| 649 |
def sanitize_chat_for_gguf(chat_messages):
|
| 650 |
messages = [{"role": m["role"], "content": m["content"]} for m in chat_messages]
|
|
@@ -1223,6 +1276,20 @@ class AnthropicMessage(BaseModel):
|
|
| 1223 |
role: str
|
| 1224 |
content: str
|
| 1225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
class AnthropicRequest(BaseModel):
|
| 1227 |
model: Optional[str] = Field(default=DEFAULT_MODEL_KEY)
|
| 1228 |
messages: List[AnthropicMessage]
|
|
@@ -1230,6 +1297,15 @@ class AnthropicRequest(BaseModel):
|
|
| 1230 |
temperature: Optional[float] = Field(default=0.7)
|
| 1231 |
system: Optional[str] = None
|
| 1232 |
stream: Optional[bool] = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
|
| 1234 |
|
| 1235 |
async def chat_completions_async(req: ChatRequest):
|
|
@@ -1591,6 +1667,22 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1591 |
raise ChatError(str(e) + "\n" + err)
|
| 1592 |
|
| 1593 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1594 |
@app.post("/v1/messages")
|
| 1595 |
async def anthropic_messages(req: AnthropicRequest, request: Request):
|
| 1596 |
"""Rota compatΓvel com SDK Anthropic β suporta stream=true (SSE) e stream=false (JSON)."""
|
|
@@ -1601,6 +1693,7 @@ async def anthropic_messages(req: AnthropicRequest, request: Request):
|
|
| 1601 |
for m in req.messages:
|
| 1602 |
msgs.append(Message(role=m.role, content=m.content))
|
| 1603 |
|
|
|
|
| 1604 |
chat_req = ChatRequest(
|
| 1605 |
model=req.model,
|
| 1606 |
messages=msgs,
|
|
@@ -1609,6 +1702,8 @@ async def anthropic_messages(req: AnthropicRequest, request: Request):
|
|
| 1609 |
mode="medium",
|
| 1610 |
web=False,
|
| 1611 |
free_mode=False,
|
|
|
|
|
|
|
| 1612 |
)
|
| 1613 |
|
| 1614 |
if req.stream:
|
|
@@ -1621,34 +1716,44 @@ async def anthropic_messages(req: AnthropicRequest, request: Request):
|
|
| 1621 |
|
| 1622 |
try:
|
| 1623 |
result = await chat_completions_async(chat_req)
|
| 1624 |
-
|
| 1625 |
usage = result.get("usage", {})
|
|
|
|
|
|
|
| 1626 |
except Exception as e:
|
| 1627 |
-
|
| 1628 |
usage = {}
|
| 1629 |
-
|
| 1630 |
-
|
| 1631 |
-
for
|
| 1632 |
-
|
| 1633 |
-
|
| 1634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1635 |
|
| 1636 |
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1637 |
-
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':
|
| 1638 |
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1639 |
|
| 1640 |
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
| 1641 |
|
| 1642 |
result = await chat_completions_async(chat_req)
|
| 1643 |
-
|
| 1644 |
usage = result.get("usage", {})
|
|
|
|
|
|
|
| 1645 |
return {
|
| 1646 |
"id": f"msg_thc_{uuid.uuid4().hex[:8]}",
|
| 1647 |
"type": "message",
|
| 1648 |
"role": "assistant",
|
| 1649 |
-
"content":
|
| 1650 |
"model": req.model,
|
| 1651 |
-
"stop_reason":
|
| 1652 |
"stop_sequence": None,
|
| 1653 |
"usage": {
|
| 1654 |
"input_tokens": usage.get("prompt_tokens", 0),
|
|
@@ -1666,6 +1771,7 @@ async def anthropic_messages_stream(req: AnthropicRequest, request: Request):
|
|
| 1666 |
for m in req.messages:
|
| 1667 |
msgs.append(Message(role=m.role, content=m.content))
|
| 1668 |
|
|
|
|
| 1669 |
chat_req = ChatRequest(
|
| 1670 |
model=req.model,
|
| 1671 |
messages=msgs,
|
|
@@ -1674,15 +1780,20 @@ async def anthropic_messages_stream(req: AnthropicRequest, request: Request):
|
|
| 1674 |
mode="medium",
|
| 1675 |
web=False,
|
| 1676 |
free_mode=False,
|
|
|
|
|
|
|
| 1677 |
)
|
| 1678 |
|
| 1679 |
try:
|
| 1680 |
result = await chat_completions_async(chat_req)
|
| 1681 |
-
|
| 1682 |
usage = result.get("usage", {})
|
|
|
|
|
|
|
| 1683 |
except Exception as e:
|
| 1684 |
-
|
| 1685 |
usage = {}
|
|
|
|
| 1686 |
|
| 1687 |
async def event_stream():
|
| 1688 |
msg_id = f"msg_thc_{uuid.uuid4().hex[:8]}"
|
|
@@ -1691,14 +1802,19 @@ async def anthropic_messages_stream(req: AnthropicRequest, request: Request):
|
|
| 1691 |
yield f"event: content_block_start\ndata: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
|
| 1692 |
yield f"event: ping\ndata: {json.dumps({'type':'ping'})}\n\n"
|
| 1693 |
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1699 |
|
| 1700 |
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1701 |
-
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':
|
| 1702 |
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1703 |
|
| 1704 |
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
|
|
|
| 645 |
return payload
|
| 646 |
|
| 647 |
|
| 648 |
+
def convert_tools_to_gemini_format(tools):
|
| 649 |
+
if not tools:
|
| 650 |
+
return None
|
| 651 |
+
declarations = []
|
| 652 |
+
for t in tools:
|
| 653 |
+
fn = t.function if hasattr(t, "function") else t["function"]
|
| 654 |
+
name = fn.name if hasattr(fn, "name") else fn["name"]
|
| 655 |
+
description = (fn.description if hasattr(fn, "description") else fn.get("description")) or ""
|
| 656 |
+
parameters = (
|
| 657 |
+
fn.parameters.model_dump(exclude_none=True)
|
| 658 |
+
if hasattr(fn, "parameters") and fn.parameters
|
| 659 |
+
else {"type": "object", "properties": {}}
|
| 660 |
+
)
|
| 661 |
+
declarations.append({
|
| 662 |
+
"name": name,
|
| 663 |
+
"description": description,
|
| 664 |
+
"parameters": parameters,
|
| 665 |
+
})
|
| 666 |
+
return [{"functionDeclarations": declarations}]
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def convert_anthropic_tools_to_openai(tools: List[AnthropicTool]) -> List[dict]:
|
| 670 |
+
converted = []
|
| 671 |
+
for t in tools:
|
| 672 |
+
schema = t.input_schema or {"type": "object", "properties": {}}
|
| 673 |
+
converted.append({
|
| 674 |
+
"type": "function",
|
| 675 |
+
"function": {
|
| 676 |
+
"name": t.name,
|
| 677 |
+
"description": t.description or "",
|
| 678 |
+
"parameters": schema,
|
| 679 |
+
}
|
| 680 |
+
})
|
| 681 |
+
return converted
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def convert_openai_tool_calls_to_anthropic(tool_calls: List[dict]) -> List[dict]:
|
| 685 |
+
blocks = []
|
| 686 |
+
for tc in tool_calls:
|
| 687 |
+
fn = tc.get("function", {})
|
| 688 |
+
try:
|
| 689 |
+
arguments = json.loads(fn.get("arguments", "{}"))
|
| 690 |
+
except (json.JSONDecodeError, TypeError):
|
| 691 |
+
arguments = {}
|
| 692 |
+
blocks.append({
|
| 693 |
+
"type": "tool_use",
|
| 694 |
+
"id": tc.get("id", f"toolu_{uuid.uuid4().hex[:8]}"),
|
| 695 |
+
"name": fn.get("name", ""),
|
| 696 |
+
"input": arguments,
|
| 697 |
+
})
|
| 698 |
+
return blocks
|
| 699 |
+
|
| 700 |
+
|
| 701 |
# ββ Helper: Sanitiza mensagens para GGUF ββββββββββββββββββββββββββββββββββββββ
|
| 702 |
def sanitize_chat_for_gguf(chat_messages):
|
| 703 |
messages = [{"role": m["role"], "content": m["content"]} for m in chat_messages]
|
|
|
|
| 1276 |
role: str
|
| 1277 |
content: str
|
| 1278 |
|
| 1279 |
+
|
| 1280 |
+
class AnthropicTool(BaseModel):
|
| 1281 |
+
name: str
|
| 1282 |
+
description: Optional[str] = None
|
| 1283 |
+
input_schema: Optional[dict] = Field(default=None, alias="input_schema")
|
| 1284 |
+
|
| 1285 |
+
@field_validator("input_schema", mode="before")
|
| 1286 |
+
@classmethod
|
| 1287 |
+
def coerce_input_schema(cls, v):
|
| 1288 |
+
if v is None:
|
| 1289 |
+
return {"type": "object", "properties": {}}
|
| 1290 |
+
return v
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
class AnthropicRequest(BaseModel):
|
| 1294 |
model: Optional[str] = Field(default=DEFAULT_MODEL_KEY)
|
| 1295 |
messages: List[AnthropicMessage]
|
|
|
|
| 1297 |
temperature: Optional[float] = Field(default=0.7)
|
| 1298 |
system: Optional[str] = None
|
| 1299 |
stream: Optional[bool] = False
|
| 1300 |
+
tools: Optional[List[AnthropicTool]] = Field(default=None, description="Ferramentas no formato Anthropic")
|
| 1301 |
+
tool_choice: Optional[str] = Field(default="auto", description="'auto', 'any', ou nome de uma tool")
|
| 1302 |
+
|
| 1303 |
+
@field_validator("messages")
|
| 1304 |
+
@classmethod
|
| 1305 |
+
def messages_not_empty(cls, v):
|
| 1306 |
+
if not v:
|
| 1307 |
+
raise ValueError("messages nΓ£o pode estar vazio")
|
| 1308 |
+
return v
|
| 1309 |
|
| 1310 |
|
| 1311 |
async def chat_completions_async(req: ChatRequest):
|
|
|
|
| 1667 |
raise ChatError(str(e) + "\n" + err)
|
| 1668 |
|
| 1669 |
|
| 1670 |
+
def build_anthropic_content(message: dict) -> List[dict]:
|
| 1671 |
+
tool_calls = message.get("tool_calls") or []
|
| 1672 |
+
if tool_calls:
|
| 1673 |
+
return [
|
| 1674 |
+
{
|
| 1675 |
+
"type": "tool_use",
|
| 1676 |
+
"id": tc.get("id", f"toolu_{uuid.uuid4().hex[:8]}"),
|
| 1677 |
+
"name": tc.get("function", {}).get("name", ""),
|
| 1678 |
+
"input": json.loads(tc.get("function", {}).get("arguments", "{}")),
|
| 1679 |
+
}
|
| 1680 |
+
for tc in tool_calls
|
| 1681 |
+
]
|
| 1682 |
+
text = message.get("content", "") or ""
|
| 1683 |
+
return [{"type": "text", "text": text}]
|
| 1684 |
+
|
| 1685 |
+
|
| 1686 |
@app.post("/v1/messages")
|
| 1687 |
async def anthropic_messages(req: AnthropicRequest, request: Request):
|
| 1688 |
"""Rota compatΓvel com SDK Anthropic β suporta stream=true (SSE) e stream=false (JSON)."""
|
|
|
|
| 1693 |
for m in req.messages:
|
| 1694 |
msgs.append(Message(role=m.role, content=m.content))
|
| 1695 |
|
| 1696 |
+
openai_tools = convert_anthropic_tools_to_openai(req.tools) if req.tools else None
|
| 1697 |
chat_req = ChatRequest(
|
| 1698 |
model=req.model,
|
| 1699 |
messages=msgs,
|
|
|
|
| 1702 |
mode="medium",
|
| 1703 |
web=False,
|
| 1704 |
free_mode=False,
|
| 1705 |
+
tools=openai_tools,
|
| 1706 |
+
tool_choice=req.tool_choice or "auto",
|
| 1707 |
)
|
| 1708 |
|
| 1709 |
if req.stream:
|
|
|
|
| 1716 |
|
| 1717 |
try:
|
| 1718 |
result = await chat_completions_async(chat_req)
|
| 1719 |
+
message = result["choices"][0]["message"]
|
| 1720 |
usage = result.get("usage", {})
|
| 1721 |
+
content_blocks = build_anthropic_content(message)
|
| 1722 |
+
stop_reason = "tool_use" if any(b.get("type") == "tool_use" for b in content_blocks) else "end_turn"
|
| 1723 |
except Exception as e:
|
| 1724 |
+
content_blocks = [{"type": "text", "text": f"Erro: {str(e)}"}]
|
| 1725 |
usage = {}
|
| 1726 |
+
stop_reason = "end_turn"
|
| 1727 |
+
|
| 1728 |
+
for block in content_blocks:
|
| 1729 |
+
block_type = block.get("type", "text")
|
| 1730 |
+
if block_type == "text":
|
| 1731 |
+
chunk_size = 20
|
| 1732 |
+
for i in range(0, len(block.get("text", "")), chunk_size):
|
| 1733 |
+
chunk = block["text"][i:i+chunk_size]
|
| 1734 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
|
| 1735 |
+
await asyncio.sleep(0.01)
|
| 1736 |
+
elif block_type == "tool_use":
|
| 1737 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'tool_use_delta','input':block.get('input', {})}})}\n\n"
|
| 1738 |
|
| 1739 |
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1740 |
+
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':stop_reason,'stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
|
| 1741 |
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1742 |
|
| 1743 |
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
| 1744 |
|
| 1745 |
result = await chat_completions_async(chat_req)
|
| 1746 |
+
message = result["choices"][0]["message"]
|
| 1747 |
usage = result.get("usage", {})
|
| 1748 |
+
content_blocks = build_anthropic_content(message)
|
| 1749 |
+
stop_reason = "tool_use" if any(b.get("type") == "tool_use" for b in content_blocks) else "end_turn"
|
| 1750 |
return {
|
| 1751 |
"id": f"msg_thc_{uuid.uuid4().hex[:8]}",
|
| 1752 |
"type": "message",
|
| 1753 |
"role": "assistant",
|
| 1754 |
+
"content": content_blocks,
|
| 1755 |
"model": req.model,
|
| 1756 |
+
"stop_reason": stop_reason,
|
| 1757 |
"stop_sequence": None,
|
| 1758 |
"usage": {
|
| 1759 |
"input_tokens": usage.get("prompt_tokens", 0),
|
|
|
|
| 1771 |
for m in req.messages:
|
| 1772 |
msgs.append(Message(role=m.role, content=m.content))
|
| 1773 |
|
| 1774 |
+
openai_tools = convert_anthropic_tools_to_openai(req.tools) if req.tools else None
|
| 1775 |
chat_req = ChatRequest(
|
| 1776 |
model=req.model,
|
| 1777 |
messages=msgs,
|
|
|
|
| 1780 |
mode="medium",
|
| 1781 |
web=False,
|
| 1782 |
free_mode=False,
|
| 1783 |
+
tools=openai_tools,
|
| 1784 |
+
tool_choice=req.tool_choice or "auto",
|
| 1785 |
)
|
| 1786 |
|
| 1787 |
try:
|
| 1788 |
result = await chat_completions_async(chat_req)
|
| 1789 |
+
message = result["choices"][0]["message"]
|
| 1790 |
usage = result.get("usage", {})
|
| 1791 |
+
content_blocks = build_anthropic_content(message)
|
| 1792 |
+
stop_reason = "tool_use" if any(b.get("type") == "tool_use" for b in content_blocks) else "end_turn"
|
| 1793 |
except Exception as e:
|
| 1794 |
+
content_blocks = [{"type": "text", "text": f"Erro: {str(e)}"}]
|
| 1795 |
usage = {}
|
| 1796 |
+
stop_reason = "end_turn"
|
| 1797 |
|
| 1798 |
async def event_stream():
|
| 1799 |
msg_id = f"msg_thc_{uuid.uuid4().hex[:8]}"
|
|
|
|
| 1802 |
yield f"event: content_block_start\ndata: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
|
| 1803 |
yield f"event: ping\ndata: {json.dumps({'type':'ping'})}\n\n"
|
| 1804 |
|
| 1805 |
+
for block in content_blocks:
|
| 1806 |
+
block_type = block.get("type", "text")
|
| 1807 |
+
if block_type == "text":
|
| 1808 |
+
chunk_size = 20
|
| 1809 |
+
for i in range(0, len(block.get("text", "")), chunk_size):
|
| 1810 |
+
chunk = block["text"][i:i+chunk_size]
|
| 1811 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
|
| 1812 |
+
await asyncio.sleep(0.01)
|
| 1813 |
+
elif block_type == "tool_use":
|
| 1814 |
+
yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'tool_use_delta','input':block.get('input', {})}})}\n\n"
|
| 1815 |
|
| 1816 |
yield f"event: content_block_stop\ndata: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
|
| 1817 |
+
yield f"event: message_delta\ndata: {json.dumps({'type':'message_delta','delta':{'stop_reason':stop_reason,'stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
|
| 1818 |
yield f"event: message_stop\ndata: {json.dumps({'type':'message_stop'})}\n\n"
|
| 1819 |
|
| 1820 |
return StreamingResponse(event_stream(), media_type="text/event-stream")
|