Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -12,6 +12,7 @@ from dotenv import load_dotenv
|
|
| 12 |
from starlette.responses import StreamingResponse
|
| 13 |
from openai import OpenAI
|
| 14 |
from typing import List, Optional, Dict, Any
|
|
|
|
| 15 |
|
| 16 |
load_dotenv()
|
| 17 |
|
|
@@ -47,6 +48,12 @@ app.add_middleware(
|
|
| 47 |
allow_headers=["*"],
|
| 48 |
)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# Validazione API
|
| 51 |
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
| 52 |
def verify_api_key(api_key: str = Depends(api_key_header)):
|
|
@@ -57,17 +64,119 @@ def verify_api_key(api_key: str = Depends(api_key_header)):
|
|
| 57 |
raise HTTPException(status_code=403, detail="API key non valida")
|
| 58 |
return api_key
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
def
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
|
|
|
| 66 |
# Chiama API (senza Streaming)
|
| 67 |
def call_api_sync(params: ChatCompletionRequest):
|
| 68 |
''' Chiamata API senza streaming. Se da errore 429 lo rifa'''
|
| 69 |
try:
|
| 70 |
client = get_openai_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
response_format = getattr(params, 'response_format', None)
|
| 72 |
if response_format and getattr(response_format, 'type', None) == 'json_schema':
|
| 73 |
response = client.beta.chat.completions.parse(**params.model_dump())
|
|
@@ -87,6 +196,9 @@ async def _resp_async_generator(params: ChatCompletionRequest):
|
|
| 87 |
client = get_openai_client()
|
| 88 |
try:
|
| 89 |
response = client.chat.completions.create(**params.model_dump())
|
|
|
|
|
|
|
|
|
|
| 90 |
for chunk in response:
|
| 91 |
chunk_data = chunk.to_dict() if hasattr(chunk, "to_dict") else chunk
|
| 92 |
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
@@ -112,7 +224,6 @@ async def health_check():
|
|
| 112 |
|
| 113 |
@app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)])
|
| 114 |
async def chat_completions(req: ChatCompletionRequest):
|
| 115 |
-
print(req)
|
| 116 |
try:
|
| 117 |
if not req.messages:
|
| 118 |
raise HTTPException(status_code=400, detail="Nessun messaggio fornito")
|
|
|
|
| 12 |
from starlette.responses import StreamingResponse
|
| 13 |
from openai import OpenAI
|
| 14 |
from typing import List, Optional, Dict, Any
|
| 15 |
+
import copy
|
| 16 |
|
| 17 |
load_dotenv()
|
| 18 |
|
|
|
|
| 48 |
allow_headers=["*"],
|
| 49 |
)
|
| 50 |
|
| 51 |
+
# Client OpenAI
|
| 52 |
+
def get_openai_client():
|
| 53 |
+
''' Client OpenAI passando in modo RANDOM le Chiavi API. In questo modo posso aggirare i limiti "Quota Exceeded" '''
|
| 54 |
+
api_key = random.choice(API_KEYS)
|
| 55 |
+
return OpenAI(api_key=api_key, base_url=BASE_URL)
|
| 56 |
+
|
| 57 |
# Validazione API
|
| 58 |
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
| 59 |
def verify_api_key(api_key: str = Depends(api_key_header)):
|
|
|
|
| 64 |
raise HTTPException(status_code=403, detail="API key non valida")
|
| 65 |
return api_key
|
| 66 |
|
| 67 |
+
# Correzione payload con content=None
|
| 68 |
+
def sanitize_messages(messages):
|
| 69 |
+
"""Convert None content to empty string to avoid Gemini API errors"""
|
| 70 |
+
if not messages:
|
| 71 |
+
return messages
|
| 72 |
+
for message in messages:
|
| 73 |
+
if message.get('content') is None:
|
| 74 |
+
message['content'] = " "
|
| 75 |
+
return messages
|
| 76 |
+
|
| 77 |
+
# Funzione per conversione Payload OpenAI to GEMINI (anomalia per ACTION) AnyOf, e property: {}
|
| 78 |
+
def convert_openai_schema_for_gemini(tools_schema):
|
| 79 |
+
if isinstance(tools_schema, str):
|
| 80 |
+
try:
|
| 81 |
+
tools_schema = json.loads(tools_schema)
|
| 82 |
+
except json.JSONDecodeError:
|
| 83 |
+
raise ValueError("Stringa JSON non valida fornita")
|
| 84 |
+
converted_schema = []
|
| 85 |
+
for tool in tools_schema:
|
| 86 |
+
if tool.get("type") != "function":
|
| 87 |
+
converted_schema.append(tool)
|
| 88 |
+
continue
|
| 89 |
+
converted_tool = {"type": "function", "function": {}}
|
| 90 |
+
func_def = tool.get("function", {})
|
| 91 |
+
if not func_def:
|
| 92 |
+
continue
|
| 93 |
+
converted_tool["function"]["name"] = func_def.get("name", "")
|
| 94 |
+
converted_tool["function"]["description"] = func_def.get("description", "")
|
| 95 |
+
if "parameters" in func_def:
|
| 96 |
+
params = func_def["parameters"]
|
| 97 |
+
converted_params = {"type": "object"}
|
| 98 |
+
if "properties" in params:
|
| 99 |
+
converted_properties = {}
|
| 100 |
+
for prop_name, prop_value in params["properties"].items():
|
| 101 |
+
cleaned = clean_schema_property(prop_value)
|
| 102 |
+
if cleaned: # Aggiungi solo se il dizionario non è vuoto
|
| 103 |
+
converted_properties[prop_name] = cleaned
|
| 104 |
+
if converted_properties:
|
| 105 |
+
converted_params["properties"] = converted_properties
|
| 106 |
+
if "required" in params:
|
| 107 |
+
converted_params["required"] = params["required"]
|
| 108 |
+
converted_tool["function"]["parameters"] = converted_params
|
| 109 |
+
converted_schema.append(converted_tool)
|
| 110 |
+
return converted_schema
|
| 111 |
+
|
| 112 |
+
def clean_schema_property(prop):
|
| 113 |
+
if not isinstance(prop, dict):
|
| 114 |
+
return prop
|
| 115 |
+
result = {}
|
| 116 |
+
for key, value in prop.items():
|
| 117 |
+
if key in ("title", "default"):
|
| 118 |
+
continue
|
| 119 |
+
elif key == "anyOf":
|
| 120 |
+
if isinstance(value, list):
|
| 121 |
+
for item in value:
|
| 122 |
+
if isinstance(item, dict) and item.get("type") != "null":
|
| 123 |
+
cleaned_item = clean_schema_property(item)
|
| 124 |
+
for k, v in cleaned_item.items():
|
| 125 |
+
if k not in result:
|
| 126 |
+
result[k] = v
|
| 127 |
+
break
|
| 128 |
+
elif key == "oneOf":
|
| 129 |
+
if isinstance(value, list) and len(value) > 0:
|
| 130 |
+
cleaned_item = clean_schema_property(value[0])
|
| 131 |
+
for k, v in cleaned_item.items():
|
| 132 |
+
if k not in result:
|
| 133 |
+
result[k] = v
|
| 134 |
+
elif isinstance(value, dict):
|
| 135 |
+
cleaned_item = clean_schema_property(value)
|
| 136 |
+
for k, v in cleaned_item.items():
|
| 137 |
+
if k not in result:
|
| 138 |
+
result[k] = v
|
| 139 |
+
elif key == "properties" and isinstance(value, dict):
|
| 140 |
+
new_props = {}
|
| 141 |
+
for prop_name, prop_value in value.items():
|
| 142 |
+
cleaned_prop = clean_schema_property(prop_value)
|
| 143 |
+
if cleaned_prop:
|
| 144 |
+
new_props[prop_name] = cleaned_prop
|
| 145 |
+
if not new_props: # Se vuoto, sostituisci con dummy
|
| 146 |
+
new_props = {"dummy": {"type": "string"}}
|
| 147 |
+
result[key] = new_props
|
| 148 |
+
elif key == "items" and isinstance(value, dict):
|
| 149 |
+
result[key] = clean_schema_property(value)
|
| 150 |
+
elif isinstance(value, list):
|
| 151 |
+
result[key] = [clean_schema_property(item) if isinstance(item, dict) else item for item in value]
|
| 152 |
+
else:
|
| 153 |
+
result[key] = value
|
| 154 |
+
if "type" not in result and "properties" in result and result["properties"]:
|
| 155 |
+
result["type"] = "object"
|
| 156 |
+
return result
|
| 157 |
+
|
| 158 |
+
def convert_payload_for_gemini(payload: ChatCompletionRequest):
|
| 159 |
+
if hasattr(payload, "model_dump"):
|
| 160 |
+
payload_converted = json.loads(payload.model_dump_json())
|
| 161 |
+
elif isinstance(payload, dict):
|
| 162 |
+
payload_converted = payload.copy()
|
| 163 |
+
else:
|
| 164 |
+
raise ValueError("Formato payload non supportato")
|
| 165 |
+
if "tools" in payload_converted:
|
| 166 |
+
payload_converted["tools"] = convert_openai_schema_for_gemini(payload_converted["tools"])
|
| 167 |
+
new_payload = ChatCompletionRequest.model_validate(payload_converted)
|
| 168 |
+
return new_payload
|
| 169 |
|
| 170 |
+
# ---------------------------------- Funzioni per Chat Completion ---------------------------------------
|
| 171 |
# Chiama API (senza Streaming)
|
| 172 |
def call_api_sync(params: ChatCompletionRequest):
|
| 173 |
''' Chiamata API senza streaming. Se da errore 429 lo rifa'''
|
| 174 |
try:
|
| 175 |
client = get_openai_client()
|
| 176 |
+
if params.messages:
|
| 177 |
+
params.messages = sanitize_messages(params.messages)
|
| 178 |
+
params = convert_payload_for_gemini(params)
|
| 179 |
+
print(params)
|
| 180 |
response_format = getattr(params, 'response_format', None)
|
| 181 |
if response_format and getattr(response_format, 'type', None) == 'json_schema':
|
| 182 |
response = client.beta.chat.completions.parse(**params.model_dump())
|
|
|
|
| 196 |
client = get_openai_client()
|
| 197 |
try:
|
| 198 |
response = client.chat.completions.create(**params.model_dump())
|
| 199 |
+
if params.messages:
|
| 200 |
+
params.messages = sanitize_messages(params.messages)
|
| 201 |
+
params = convert_payload_for_gemini(params)
|
| 202 |
for chunk in response:
|
| 203 |
chunk_data = chunk.to_dict() if hasattr(chunk, "to_dict") else chunk
|
| 204 |
yield f"data: {json.dumps(chunk_data)}\n\n"
|
|
|
|
| 224 |
|
| 225 |
@app.post("/v1/chat/completions", dependencies=[Depends(verify_api_key)])
|
| 226 |
async def chat_completions(req: ChatCompletionRequest):
|
|
|
|
| 227 |
try:
|
| 228 |
if not req.messages:
|
| 229 |
raise HTTPException(status_code=400, detail="Nessun messaggio fornito")
|