Wilker commited on
Commit
025f2a2
Β·
1 Parent(s): dbb524c

feat(app): /v1/messages agora suporta tools e formato Anthropic

Browse files
Files changed (1) hide show
  1. app.py +136 -20
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
- reply = result["choices"][0]["message"]["content"]
1625
  usage = result.get("usage", {})
 
 
1626
  except Exception as e:
1627
- reply = f"Erro: {str(e)}"
1628
  usage = {}
1629
-
1630
- chunk_size = 20
1631
- for i in range(0, len(reply), chunk_size):
1632
- chunk = reply[i:i+chunk_size]
1633
- yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
1634
- await asyncio.sleep(0.01)
 
 
 
 
 
 
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':'end_turn','stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
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
- reply = result["choices"][0]["message"]["content"]
1644
  usage = result.get("usage", {})
 
 
1645
  return {
1646
  "id": f"msg_thc_{uuid.uuid4().hex[:8]}",
1647
  "type": "message",
1648
  "role": "assistant",
1649
- "content": [{"type": "text", "text": reply}],
1650
  "model": req.model,
1651
- "stop_reason": "end_turn",
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
- reply = result["choices"][0]["message"]["content"]
1682
  usage = result.get("usage", {})
 
 
1683
  except Exception as e:
1684
- reply = f"Erro: {str(e)}"
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
- chunk_size = 20
1695
- for i in range(0, len(reply), chunk_size):
1696
- chunk = reply[i:i+chunk_size]
1697
- yield f"event: content_block_delta\ndata: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':chunk}})}\n\n"
1698
- await asyncio.sleep(0.01)
 
 
 
 
 
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':'end_turn','stop_sequence':None},'usage':{'output_tokens':usage.get('completion_tokens',0)}})}\n\n"
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")