Wilker commited on
Commit ·
ce54b53
1
Parent(s): d3880a1
feat: adiciona suporte a tool calling (groq/mistral/openrouter/gemini)
Browse files
app.py
CHANGED
|
@@ -411,6 +411,23 @@ logger.info("Construindo índices de conhecimento (RAG) e skills...")
|
|
| 411 |
reload_indexes()
|
| 412 |
|
| 413 |
# ── Schemas ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
class Message(BaseModel):
|
| 415 |
role: Literal["system", "user", "assistant"]
|
| 416 |
content: constr(min_length=1, max_length=10000)
|
|
@@ -430,6 +447,8 @@ class ChatRequest(BaseModel):
|
|
| 430 |
messages: List[Message] = Field(min_length=1, description="Lista de mensagens")
|
| 431 |
max_tokens: Optional[int] = Field(default=8192, ge=1, le=1000000, description="Máximo de tokens")
|
| 432 |
temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0, description="Temperatura de geração")
|
|
|
|
|
|
|
| 433 |
|
| 434 |
@field_validator("model")
|
| 435 |
@classmethod
|
|
@@ -645,6 +664,27 @@ 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]
|
|
@@ -685,6 +725,8 @@ def chat_completions(req: ChatRequest):
|
|
| 685 |
t0 = time.time()
|
| 686 |
|
| 687 |
if backend == "transformers":
|
|
|
|
|
|
|
| 688 |
tokenizer = state["tokenizer"]
|
| 689 |
model = state["model"]
|
| 690 |
|
|
@@ -710,6 +752,8 @@ def chat_completions(req: ChatRequest):
|
|
| 710 |
completion_tokens = len(generated)
|
| 711 |
|
| 712 |
elif backend == "gguf":
|
|
|
|
|
|
|
| 713 |
llm = state["model"]
|
| 714 |
safe_chat = sanitize_chat_for_gguf(chat)
|
| 715 |
result = llm.create_chat_completion(
|
|
@@ -723,6 +767,8 @@ def chat_completions(req: ChatRequest):
|
|
| 723 |
completion_tokens = usage.get("completion_tokens", 0)
|
| 724 |
|
| 725 |
elif backend == "kilo":
|
|
|
|
|
|
|
| 726 |
model_id = state["model"]
|
| 727 |
kilo_api_key = os.environ.get("KILO_API_KEY")
|
| 728 |
if not kilo_api_key:
|
|
@@ -787,10 +833,11 @@ def chat_completions(req: ChatRequest):
|
|
| 787 |
"max_tokens": max_tokens,
|
| 788 |
"temperature": temperature if temperature else 0.0,
|
| 789 |
}
|
|
|
|
|
|
|
|
|
|
| 790 |
|
| 791 |
-
|
| 792 |
-
async def call_openrouter_api():
|
| 793 |
-
async with httpx.AsyncClient() as client:
|
| 794 |
resp = await client.post(
|
| 795 |
"https://openrouter.ai/api/v1/chat/completions",
|
| 796 |
json=payload,
|
|
@@ -815,7 +862,9 @@ def chat_completions(req: ChatRequest):
|
|
| 815 |
if "choices" not in data or not data["choices"]:
|
| 816 |
raise APIError(f"Resposta inválida da OpenRouter API: {data}")
|
| 817 |
|
| 818 |
-
|
|
|
|
|
|
|
| 819 |
usage = data.get("usage", {})
|
| 820 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 821 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -836,6 +885,9 @@ def chat_completions(req: ChatRequest):
|
|
| 836 |
"max_tokens": max_tokens,
|
| 837 |
"temperature": temperature if temperature else 0.0,
|
| 838 |
}
|
|
|
|
|
|
|
|
|
|
| 839 |
|
| 840 |
payload_bytes = len(json.dumps(payload))
|
| 841 |
logger.info(f"[GROQ DEBUG] Payload size: {payload_bytes} bytes, Messages: {len(chat)}")
|
|
@@ -867,7 +919,9 @@ def chat_completions(req: ChatRequest):
|
|
| 867 |
if "choices" not in data or not data["choices"]:
|
| 868 |
raise APIError(f"Resposta inválida da Groq API: {data}")
|
| 869 |
|
| 870 |
-
|
|
|
|
|
|
|
| 871 |
usage = data.get("usage", {})
|
| 872 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 873 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -888,6 +942,9 @@ def chat_completions(req: ChatRequest):
|
|
| 888 |
"max_tokens": max_tokens,
|
| 889 |
"temperature": temperature if temperature else 0.0,
|
| 890 |
}
|
|
|
|
|
|
|
|
|
|
| 891 |
|
| 892 |
@async_retry_with_backoff(max_retries=3, initial_delay=1, backoff_factor=2)
|
| 893 |
async def call_mistral_api():
|
|
@@ -916,7 +973,9 @@ def chat_completions(req: ChatRequest):
|
|
| 916 |
if "choices" not in data or not data["choices"]:
|
| 917 |
raise APIError(f"Resposta inválida da Mistral API: {data}")
|
| 918 |
|
| 919 |
-
|
|
|
|
|
|
|
| 920 |
usage = data.get("usage", {})
|
| 921 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 922 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -935,6 +994,9 @@ def chat_completions(req: ChatRequest):
|
|
| 935 |
"temperature": temperature if temperature else 0.0,
|
| 936 |
}
|
| 937 |
|
|
|
|
|
|
|
|
|
|
| 938 |
if model_id.startswith("gemini"):
|
| 939 |
gemini_payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
| 940 |
|
|
@@ -969,7 +1031,10 @@ def chat_completions(req: ChatRequest):
|
|
| 969 |
|
| 970 |
logger.info(f"[GEMINI DEBUG] finishReason={data['candidates'][0].get('finishReason')}, content={data['candidates'][0].get('content', {})}")
|
| 971 |
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
| 973 |
usage = data.get("usageMetadata", {})
|
| 974 |
prompt_tokens = usage.get("promptTokenCount", 0)
|
| 975 |
completion_tokens = usage.get("candidatesTokenCount", 0)
|
|
@@ -977,13 +1042,31 @@ def chat_completions(req: ChatRequest):
|
|
| 977 |
finish_reason = "stop"
|
| 978 |
if data["candidates"][0].get("finishReason") == "MAX_TOKENS":
|
| 979 |
finish_reason = "length"
|
| 980 |
-
|
| 981 |
else:
|
| 982 |
raise BackendError("Backend inválido")
|
| 983 |
|
| 984 |
elapsed = time.time() - t0
|
| 985 |
logger.info(f"Resposta gerada em {elapsed:.1f}s ({backend}, web={req.web})")
|
| 986 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
return {
|
| 988 |
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 989 |
"object": "chat.completion",
|
|
@@ -992,8 +1075,8 @@ def chat_completions(req: ChatRequest):
|
|
| 992 |
"mode": req.mode,
|
| 993 |
"choices": [{
|
| 994 |
"index": 0,
|
| 995 |
-
"message":
|
| 996 |
-
"finish_reason": "stop"
|
| 997 |
}],
|
| 998 |
"usage": {
|
| 999 |
"prompt_tokens": prompt_tokens,
|
|
@@ -1212,6 +1295,8 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1212 |
t0 = time.time()
|
| 1213 |
|
| 1214 |
if backend == "transformers":
|
|
|
|
|
|
|
| 1215 |
tokenizer = state["tokenizer"]
|
| 1216 |
model = state["model"]
|
| 1217 |
|
|
@@ -1237,6 +1322,8 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1237 |
completion_tokens = len(generated)
|
| 1238 |
|
| 1239 |
elif backend == "gguf":
|
|
|
|
|
|
|
| 1240 |
llm = state["model"]
|
| 1241 |
safe_chat = sanitize_chat_for_gguf(chat)
|
| 1242 |
result = llm.create_chat_completion(
|
|
@@ -1250,6 +1337,8 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1250 |
completion_tokens = usage.get("completion_tokens", 0)
|
| 1251 |
|
| 1252 |
elif backend == "kilo":
|
|
|
|
|
|
|
| 1253 |
model_id = state["model"]
|
| 1254 |
kilo_api_key = os.environ.get("KILO_API_KEY")
|
| 1255 |
if not kilo_api_key:
|
|
@@ -1310,6 +1399,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1310 |
"max_tokens": max_tokens,
|
| 1311 |
"temperature": temperature if temperature else 0.0,
|
| 1312 |
}
|
|
|
|
|
|
|
|
|
|
| 1313 |
|
| 1314 |
async with httpx.AsyncClient() as client:
|
| 1315 |
resp = await client.post(
|
|
@@ -1334,7 +1426,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1334 |
if "choices" not in data or not data["choices"]:
|
| 1335 |
raise APIError(f"Resposta inválida da OpenRouter API: {data}")
|
| 1336 |
|
| 1337 |
-
|
|
|
|
|
|
|
| 1338 |
usage = data.get("usage", {})
|
| 1339 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1340 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -1355,6 +1449,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1355 |
"max_tokens": max_tokens,
|
| 1356 |
"temperature": temperature if temperature else 0.0,
|
| 1357 |
}
|
|
|
|
|
|
|
|
|
|
| 1358 |
|
| 1359 |
payload_bytes = len(json.dumps(payload))
|
| 1360 |
logger.info(f"[GROQ DEBUG] Payload size: {payload_bytes} bytes, Messages: {len(chat)}")
|
|
@@ -1382,7 +1479,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1382 |
if "choices" not in data or not data["choices"]:
|
| 1383 |
raise APIError(f"Resposta inválida da Groq API: {data}")
|
| 1384 |
|
| 1385 |
-
|
|
|
|
|
|
|
| 1386 |
usage = data.get("usage", {})
|
| 1387 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1388 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -1403,6 +1502,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1403 |
"max_tokens": max_tokens,
|
| 1404 |
"temperature": temperature if temperature else 0.0,
|
| 1405 |
}
|
|
|
|
|
|
|
|
|
|
| 1406 |
|
| 1407 |
async with httpx.AsyncClient() as client:
|
| 1408 |
resp = await client.post(
|
|
@@ -1427,7 +1529,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1427 |
if "choices" not in data or not data["choices"]:
|
| 1428 |
raise APIError(f"Resposta inválida da Mistral API: {data}")
|
| 1429 |
|
| 1430 |
-
|
|
|
|
|
|
|
| 1431 |
usage = data.get("usage", {})
|
| 1432 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1433 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
@@ -1446,6 +1550,9 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1446 |
"temperature": temperature if temperature else 0.0,
|
| 1447 |
}
|
| 1448 |
|
|
|
|
|
|
|
|
|
|
| 1449 |
if model_id.startswith("gemini"):
|
| 1450 |
gemini_payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
| 1451 |
|
|
@@ -1476,7 +1583,10 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1476 |
|
| 1477 |
logger.info(f"[GEMINI DEBUG] finishReason={data['candidates'][0].get('finishReason')}, content={data['candidates'][0].get('content', {})}")
|
| 1478 |
|
| 1479 |
-
|
|
|
|
|
|
|
|
|
|
| 1480 |
usage = data.get("usageMetadata", {})
|
| 1481 |
prompt_tokens = usage.get("promptTokenCount", 0)
|
| 1482 |
completion_tokens = usage.get("candidatesTokenCount", 0)
|
|
@@ -1491,6 +1601,25 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1491 |
elapsed = time.time() - t0
|
| 1492 |
logger.info(f"Resposta gerada em {elapsed:.1f}s ({backend}, web={req.web})")
|
| 1493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1494 |
return {
|
| 1495 |
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 1496 |
"object": "chat.completion",
|
|
@@ -1499,8 +1628,8 @@ async def chat_completions_async(req: ChatRequest):
|
|
| 1499 |
"mode": req.mode,
|
| 1500 |
"choices": [{
|
| 1501 |
"index": 0,
|
| 1502 |
-
"message":
|
| 1503 |
-
"finish_reason": "stop"
|
| 1504 |
}],
|
| 1505 |
"usage": {
|
| 1506 |
"prompt_tokens": prompt_tokens,
|
|
|
|
| 411 |
reload_indexes()
|
| 412 |
|
| 413 |
# ── Schemas ────────────────────────────────────────────────────────────────────
|
| 414 |
+
class ToolFunctionParameters(BaseModel):
|
| 415 |
+
type: str = "object"
|
| 416 |
+
properties: dict = {}
|
| 417 |
+
required: Optional[List[str]] = None
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
class ToolFunction(BaseModel):
|
| 421 |
+
name: str
|
| 422 |
+
description: Optional[str] = None
|
| 423 |
+
parameters: Optional[ToolFunctionParameters] = None
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class Tool(BaseModel):
|
| 427 |
+
type: Literal["function"] = "function"
|
| 428 |
+
function: ToolFunction
|
| 429 |
+
|
| 430 |
+
|
| 431 |
class Message(BaseModel):
|
| 432 |
role: Literal["system", "user", "assistant"]
|
| 433 |
content: constr(min_length=1, max_length=10000)
|
|
|
|
| 447 |
messages: List[Message] = Field(min_length=1, description="Lista de mensagens")
|
| 448 |
max_tokens: Optional[int] = Field(default=8192, ge=1, le=1000000, description="Máximo de tokens")
|
| 449 |
temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0, description="Temperatura de geração")
|
| 450 |
+
tools: Optional[List[Tool]] = Field(default=None, description="Ferramentas disponíveis (formato OpenAI)")
|
| 451 |
+
tool_choice: Optional[str] = Field(default="auto", description="'auto', 'none', ou nome de uma tool")
|
| 452 |
|
| 453 |
@field_validator("model")
|
| 454 |
@classmethod
|
|
|
|
| 664 |
return payload
|
| 665 |
|
| 666 |
|
| 667 |
+
def convert_tools_to_gemini_format(tools):
|
| 668 |
+
if not tools:
|
| 669 |
+
return None
|
| 670 |
+
declarations = []
|
| 671 |
+
for t in tools:
|
| 672 |
+
fn = t.function if hasattr(t, "function") else t["function"]
|
| 673 |
+
name = fn.name if hasattr(fn, "name") else fn["name"]
|
| 674 |
+
description = (fn.description if hasattr(fn, "description") else fn.get("description")) or ""
|
| 675 |
+
parameters = (
|
| 676 |
+
fn.parameters.model_dump(exclude_none=True)
|
| 677 |
+
if hasattr(fn, "parameters") and fn.parameters
|
| 678 |
+
else {"type": "object", "properties": {}}
|
| 679 |
+
)
|
| 680 |
+
declarations.append({
|
| 681 |
+
"name": name,
|
| 682 |
+
"description": description,
|
| 683 |
+
"parameters": parameters,
|
| 684 |
+
})
|
| 685 |
+
return [{"functionDeclarations": declarations}]
|
| 686 |
+
|
| 687 |
+
|
| 688 |
# ── Helper: Sanitiza mensagens para GGUF ──────────────────────────────────────
|
| 689 |
def sanitize_chat_for_gguf(chat_messages):
|
| 690 |
messages = [{"role": m["role"], "content": m["content"]} for m in chat_messages]
|
|
|
|
| 725 |
t0 = time.time()
|
| 726 |
|
| 727 |
if backend == "transformers":
|
| 728 |
+
if req.tools:
|
| 729 |
+
logger.warning("Backend 'transformers' não suporta tool calling — tools será ignorado.")
|
| 730 |
tokenizer = state["tokenizer"]
|
| 731 |
model = state["model"]
|
| 732 |
|
|
|
|
| 752 |
completion_tokens = len(generated)
|
| 753 |
|
| 754 |
elif backend == "gguf":
|
| 755 |
+
if req.tools:
|
| 756 |
+
logger.warning("Backend 'gguf' não suporta tool calling — tools será ignorado.")
|
| 757 |
llm = state["model"]
|
| 758 |
safe_chat = sanitize_chat_for_gguf(chat)
|
| 759 |
result = llm.create_chat_completion(
|
|
|
|
| 767 |
completion_tokens = usage.get("completion_tokens", 0)
|
| 768 |
|
| 769 |
elif backend == "kilo":
|
| 770 |
+
if req.tools:
|
| 771 |
+
logger.warning("Backend 'kilo' não suporta tool calling — tools será ignorado.")
|
| 772 |
model_id = state["model"]
|
| 773 |
kilo_api_key = os.environ.get("KILO_API_KEY")
|
| 774 |
if not kilo_api_key:
|
|
|
|
| 833 |
"max_tokens": max_tokens,
|
| 834 |
"temperature": temperature if temperature else 0.0,
|
| 835 |
}
|
| 836 |
+
if req.tools:
|
| 837 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 838 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 839 |
|
| 840 |
+
async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
| 841 |
resp = await client.post(
|
| 842 |
"https://openrouter.ai/api/v1/chat/completions",
|
| 843 |
json=payload,
|
|
|
|
| 862 |
if "choices" not in data or not data["choices"]:
|
| 863 |
raise APIError(f"Resposta inválida da OpenRouter API: {data}")
|
| 864 |
|
| 865 |
+
message_obj = data["choices"][0]["message"]
|
| 866 |
+
text = message_obj.get("content", "") or ""
|
| 867 |
+
tool_calls = message_obj.get("tool_calls")
|
| 868 |
usage = data.get("usage", {})
|
| 869 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 870 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 885 |
"max_tokens": max_tokens,
|
| 886 |
"temperature": temperature if temperature else 0.0,
|
| 887 |
}
|
| 888 |
+
if req.tools:
|
| 889 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 890 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 891 |
|
| 892 |
payload_bytes = len(json.dumps(payload))
|
| 893 |
logger.info(f"[GROQ DEBUG] Payload size: {payload_bytes} bytes, Messages: {len(chat)}")
|
|
|
|
| 919 |
if "choices" not in data or not data["choices"]:
|
| 920 |
raise APIError(f"Resposta inválida da Groq API: {data}")
|
| 921 |
|
| 922 |
+
message_obj = data["choices"][0]["message"]
|
| 923 |
+
text = message_obj.get("content", "") or ""
|
| 924 |
+
tool_calls = message_obj.get("tool_calls")
|
| 925 |
usage = data.get("usage", {})
|
| 926 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 927 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 942 |
"max_tokens": max_tokens,
|
| 943 |
"temperature": temperature if temperature else 0.0,
|
| 944 |
}
|
| 945 |
+
if req.tools:
|
| 946 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 947 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 948 |
|
| 949 |
@async_retry_with_backoff(max_retries=3, initial_delay=1, backoff_factor=2)
|
| 950 |
async def call_mistral_api():
|
|
|
|
| 973 |
if "choices" not in data or not data["choices"]:
|
| 974 |
raise APIError(f"Resposta inválida da Mistral API: {data}")
|
| 975 |
|
| 976 |
+
message_obj = data["choices"][0]["message"]
|
| 977 |
+
text = message_obj.get("content", "") or ""
|
| 978 |
+
tool_calls = message_obj.get("tool_calls")
|
| 979 |
usage = data.get("usage", {})
|
| 980 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 981 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 994 |
"temperature": temperature if temperature else 0.0,
|
| 995 |
}
|
| 996 |
|
| 997 |
+
if req.tools:
|
| 998 |
+
gemini_payload["tools"] = convert_tools_to_gemini_format(req.tools)
|
| 999 |
+
|
| 1000 |
if model_id.startswith("gemini"):
|
| 1001 |
gemini_payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
| 1002 |
|
|
|
|
| 1031 |
|
| 1032 |
logger.info(f"[GEMINI DEBUG] finishReason={data['candidates'][0].get('finishReason')}, content={data['candidates'][0].get('content', {})}")
|
| 1033 |
|
| 1034 |
+
parts = data["candidates"][0]["content"]["parts"]
|
| 1035 |
+
text_parts = [p.get("text", "") for p in parts if "text" in p]
|
| 1036 |
+
text = "".join(text_parts)
|
| 1037 |
+
function_calls = [p["functionCall"] for p in parts if "functionCall" in p]
|
| 1038 |
usage = data.get("usageMetadata", {})
|
| 1039 |
prompt_tokens = usage.get("promptTokenCount", 0)
|
| 1040 |
completion_tokens = usage.get("candidatesTokenCount", 0)
|
|
|
|
| 1042 |
finish_reason = "stop"
|
| 1043 |
if data["candidates"][0].get("finishReason") == "MAX_TOKENS":
|
| 1044 |
finish_reason = "length"
|
|
|
|
| 1045 |
else:
|
| 1046 |
raise BackendError("Backend inválido")
|
| 1047 |
|
| 1048 |
elapsed = time.time() - t0
|
| 1049 |
logger.info(f"Resposta gerada em {elapsed:.1f}s ({backend}, web={req.web})")
|
| 1050 |
|
| 1051 |
+
message = {"role": "assistant", "content": text}
|
| 1052 |
+
openai_tool_calls = None
|
| 1053 |
+
if tool_calls:
|
| 1054 |
+
openai_tool_calls = tool_calls
|
| 1055 |
+
elif function_calls:
|
| 1056 |
+
openai_tool_calls = [
|
| 1057 |
+
{
|
| 1058 |
+
"id": f"call_{uuid.uuid4().hex[:8]}",
|
| 1059 |
+
"type": "function",
|
| 1060 |
+
"function": {
|
| 1061 |
+
"name": fc["name"],
|
| 1062 |
+
"arguments": json.dumps(fc.get("args", {})),
|
| 1063 |
+
},
|
| 1064 |
+
}
|
| 1065 |
+
for fc in function_calls
|
| 1066 |
+
]
|
| 1067 |
+
if openai_tool_calls:
|
| 1068 |
+
message["tool_calls"] = openai_tool_calls
|
| 1069 |
+
|
| 1070 |
return {
|
| 1071 |
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 1072 |
"object": "chat.completion",
|
|
|
|
| 1075 |
"mode": req.mode,
|
| 1076 |
"choices": [{
|
| 1077 |
"index": 0,
|
| 1078 |
+
"message": message,
|
| 1079 |
+
"finish_reason": "tool_calls" if openai_tool_calls else (finish_reason or "stop"),
|
| 1080 |
}],
|
| 1081 |
"usage": {
|
| 1082 |
"prompt_tokens": prompt_tokens,
|
|
|
|
| 1295 |
t0 = time.time()
|
| 1296 |
|
| 1297 |
if backend == "transformers":
|
| 1298 |
+
if req.tools:
|
| 1299 |
+
logger.warning("Backend 'transformers' não suporta tool calling — tools será ignorado.")
|
| 1300 |
tokenizer = state["tokenizer"]
|
| 1301 |
model = state["model"]
|
| 1302 |
|
|
|
|
| 1322 |
completion_tokens = len(generated)
|
| 1323 |
|
| 1324 |
elif backend == "gguf":
|
| 1325 |
+
if req.tools:
|
| 1326 |
+
logger.warning("Backend 'gguf' não suporta tool calling — tools será ignorado.")
|
| 1327 |
llm = state["model"]
|
| 1328 |
safe_chat = sanitize_chat_for_gguf(chat)
|
| 1329 |
result = llm.create_chat_completion(
|
|
|
|
| 1337 |
completion_tokens = usage.get("completion_tokens", 0)
|
| 1338 |
|
| 1339 |
elif backend == "kilo":
|
| 1340 |
+
if req.tools:
|
| 1341 |
+
logger.warning("Backend 'kilo' não suporta tool calling — tools será ignorado.")
|
| 1342 |
model_id = state["model"]
|
| 1343 |
kilo_api_key = os.environ.get("KILO_API_KEY")
|
| 1344 |
if not kilo_api_key:
|
|
|
|
| 1399 |
"max_tokens": max_tokens,
|
| 1400 |
"temperature": temperature if temperature else 0.0,
|
| 1401 |
}
|
| 1402 |
+
if req.tools:
|
| 1403 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 1404 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 1405 |
|
| 1406 |
async with httpx.AsyncClient() as client:
|
| 1407 |
resp = await client.post(
|
|
|
|
| 1426 |
if "choices" not in data or not data["choices"]:
|
| 1427 |
raise APIError(f"Resposta inválida da OpenRouter API: {data}")
|
| 1428 |
|
| 1429 |
+
message_obj = data["choices"][0]["message"]
|
| 1430 |
+
text = message_obj.get("content", "") or ""
|
| 1431 |
+
tool_calls = message_obj.get("tool_calls")
|
| 1432 |
usage = data.get("usage", {})
|
| 1433 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1434 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 1449 |
"max_tokens": max_tokens,
|
| 1450 |
"temperature": temperature if temperature else 0.0,
|
| 1451 |
}
|
| 1452 |
+
if req.tools:
|
| 1453 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 1454 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 1455 |
|
| 1456 |
payload_bytes = len(json.dumps(payload))
|
| 1457 |
logger.info(f"[GROQ DEBUG] Payload size: {payload_bytes} bytes, Messages: {len(chat)}")
|
|
|
|
| 1479 |
if "choices" not in data or not data["choices"]:
|
| 1480 |
raise APIError(f"Resposta inválida da Groq API: {data}")
|
| 1481 |
|
| 1482 |
+
message_obj = data["choices"][0]["message"]
|
| 1483 |
+
text = message_obj.get("content", "") or ""
|
| 1484 |
+
tool_calls = message_obj.get("tool_calls")
|
| 1485 |
usage = data.get("usage", {})
|
| 1486 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1487 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 1502 |
"max_tokens": max_tokens,
|
| 1503 |
"temperature": temperature if temperature else 0.0,
|
| 1504 |
}
|
| 1505 |
+
if req.tools:
|
| 1506 |
+
payload["tools"] = [t.model_dump(exclude_none=True) for t in req.tools]
|
| 1507 |
+
payload["tool_choice"] = req.tool_choice or "auto"
|
| 1508 |
|
| 1509 |
async with httpx.AsyncClient() as client:
|
| 1510 |
resp = await client.post(
|
|
|
|
| 1529 |
if "choices" not in data or not data["choices"]:
|
| 1530 |
raise APIError(f"Resposta inválida da Mistral API: {data}")
|
| 1531 |
|
| 1532 |
+
message_obj = data["choices"][0]["message"]
|
| 1533 |
+
text = message_obj.get("content", "") or ""
|
| 1534 |
+
tool_calls = message_obj.get("tool_calls")
|
| 1535 |
usage = data.get("usage", {})
|
| 1536 |
prompt_tokens = usage.get("prompt_tokens", 0)
|
| 1537 |
completion_tokens = usage.get("completion_tokens", 0)
|
|
|
|
| 1550 |
"temperature": temperature if temperature else 0.0,
|
| 1551 |
}
|
| 1552 |
|
| 1553 |
+
if req.tools:
|
| 1554 |
+
gemini_payload["tools"] = convert_tools_to_gemini_format(req.tools)
|
| 1555 |
+
|
| 1556 |
if model_id.startswith("gemini"):
|
| 1557 |
gemini_payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
| 1558 |
|
|
|
|
| 1583 |
|
| 1584 |
logger.info(f"[GEMINI DEBUG] finishReason={data['candidates'][0].get('finishReason')}, content={data['candidates'][0].get('content', {})}")
|
| 1585 |
|
| 1586 |
+
parts = data["candidates"][0]["content"]["parts"]
|
| 1587 |
+
text_parts = [p.get("text", "") for p in parts if "text" in p]
|
| 1588 |
+
text = "".join(text_parts)
|
| 1589 |
+
function_calls = [p["functionCall"] for p in parts if "functionCall" in p]
|
| 1590 |
usage = data.get("usageMetadata", {})
|
| 1591 |
prompt_tokens = usage.get("promptTokenCount", 0)
|
| 1592 |
completion_tokens = usage.get("candidatesTokenCount", 0)
|
|
|
|
| 1601 |
elapsed = time.time() - t0
|
| 1602 |
logger.info(f"Resposta gerada em {elapsed:.1f}s ({backend}, web={req.web})")
|
| 1603 |
|
| 1604 |
+
message = {"role": "assistant", "content": text}
|
| 1605 |
+
openai_tool_calls = None
|
| 1606 |
+
if tool_calls:
|
| 1607 |
+
openai_tool_calls = tool_calls
|
| 1608 |
+
elif function_calls:
|
| 1609 |
+
openai_tool_calls = [
|
| 1610 |
+
{
|
| 1611 |
+
"id": f"call_{uuid.uuid4().hex[:8]}",
|
| 1612 |
+
"type": "function",
|
| 1613 |
+
"function": {
|
| 1614 |
+
"name": fc["name"],
|
| 1615 |
+
"arguments": json.dumps(fc.get("args", {})),
|
| 1616 |
+
},
|
| 1617 |
+
}
|
| 1618 |
+
for fc in function_calls
|
| 1619 |
+
]
|
| 1620 |
+
if openai_tool_calls:
|
| 1621 |
+
message["tool_calls"] = openai_tool_calls
|
| 1622 |
+
|
| 1623 |
return {
|
| 1624 |
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
| 1625 |
"object": "chat.completion",
|
|
|
|
| 1628 |
"mode": req.mode,
|
| 1629 |
"choices": [{
|
| 1630 |
"index": 0,
|
| 1631 |
+
"message": message,
|
| 1632 |
+
"finish_reason": "tool_calls" if openai_tool_calls else (finish_reason or "stop"),
|
| 1633 |
}],
|
| 1634 |
"usage": {
|
| 1635 |
"prompt_tokens": prompt_tokens,
|