Refactor app.py for logging and configuration updates

Refactor app.py to improve logging, update configuration paths, and enhance session management.
This commit is contained in:
Marcos Lohmann
2025-12-05 09:03:49 -03:00
committed by GitHub
parent 9f4ed30a33
commit f0f75e0e19

View File

@@ -6,7 +6,8 @@
# export API_KEY="minha-chave" # export API_KEY="minha-chave"
# export GENAI_BUCKET="lohmann-ai-br" # export GENAI_BUCKET="lohmann-ai-br"
# export GENAI_UPLOAD_PREFIX="genai-uploads/" # export GENAI_UPLOAD_PREFIX="genai-uploads/"
# export LLM_CONFIG_PATH="/home/app/llm_models.json" # export OCI_CONFIG_FILE="./credentials.conf" # opcional, padrão: ./credentials.conf
# export LLM_CONFIG_PATH="./llm_models.json" # opcional, padrão: ./llm_models.json
# export DEBUG_AUTH=true # opcional # export DEBUG_AUTH=true # opcional
# python app.py # porta 8000 # python app.py # porta 8000
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -22,8 +23,17 @@ import base64
import time import time
import mimetypes import mimetypes
import hmac import hmac
import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Generator from typing import Any, Dict, List, Optional, Generator
from functools import lru_cache, wraps
# Configurar logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__) app = Flask(__name__)
@@ -42,7 +52,7 @@ try:
methods=["GET", "POST", "OPTIONS"] methods=["GET", "POST", "OPTIONS"]
) )
except Exception as _e: except Exception as _e:
print("AVISO: flask-cors não instalado; CORS mínimo será aplicado via after_request.") logger.warning("flask-cors não instalado; CORS mínimo será aplicado via after_request.")
@app.after_request @app.after_request
def add_cors_headers(resp): def add_cors_headers(resp):
@@ -51,11 +61,35 @@ def add_cors_headers(resp):
resp.headers.setdefault("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Channel, X-Cuid") resp.headers.setdefault("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Channel, X-Cuid")
return resp return resp
# ==========================
# Constantes de Parâmetros de Modelo
# ==========================
# Parâmetros de modelo com mapeamento 1:1 (sem transformação)
SIMPLE_MODEL_PARAMS = [
"temperature",
"top_p",
"top_k",
"frequency_penalty",
"presence_penalty",
"reasoning_effort",
"verbosity"
]
# ========================== # ==========================
# Configuração e Autenticação OCI # Configuração e Autenticação OCI
# ========================== # ==========================
def load_config(config_file="/home/app/credentials.conf"): def load_config(config_file=None):
"""Carrega configuração OCI de arquivo.
Args:
config_file: Caminho do arquivo de configuração. Se None, usa variável de ambiente OCI_CONFIG_FILE
ou padrão './credentials.conf'
"""
if config_file is None:
config_file = os.environ.get("OCI_CONFIG_FILE", "./credentials.conf")
config = {} config = {}
try: try:
with open(config_file, 'r') as f: with open(config_file, 'r') as f:
@@ -85,8 +119,8 @@ if not TEST_MODE:
private_key_content=config.get("key_content"), private_key_content=config.get("key_content"),
) )
except Exception as e: except Exception as e:
print(f"Erro ao inicializar signer OCI: {e}") logger.error(f"Erro ao inicializar signer OCI: {e}")
print("Executando em modo de teste...") logger.info("Executando em modo de teste...")
TEST_MODE = True TEST_MODE = True
# ========================== # ==========================
@@ -111,7 +145,7 @@ def _parse_bearer_token(auth_header: str) -> str:
def check_api_key(): def check_api_key():
expected_key = os.environ.get("API_KEY") expected_key = os.environ.get("API_KEY")
if not expected_key: if not expected_key:
print("AVISO: API_KEY não configurada nas variáveis de ambiente.") logger.warning("API_KEY não configurada nas variáveis de ambiente.")
return return
provided_key = request.headers.get("X-API-Key") provided_key = request.headers.get("X-API-Key")
@@ -119,7 +153,7 @@ def check_api_key():
bearer_token = _parse_bearer_token(auth_header) bearer_token = _parse_bearer_token(auth_header)
if DEBUG_AUTH: if DEBUG_AUTH:
print(f"[auth] method={request.method} path={request.path} " logger.debug(f"[auth] method={request.method} path={request.path} "
f"X-API-Key={'<set>' if provided_key else '<none>'} " f"X-API-Key={'<set>' if provided_key else '<none>'} "
f"Authorization={'<set>' if auth_header else '<none>'}") f"Authorization={'<set>' if auth_header else '<none>'}")
@@ -152,11 +186,26 @@ if not TEST_MODE:
object_client = oci.object_storage.ObjectStorageClient(config) object_client = oci.object_storage.ObjectStorageClient(config)
namespace = object_client.get_namespace().data namespace = object_client.get_namespace().data
except Exception as e: except Exception as e:
print(f"Erro ao inicializar ObjectStorageClient: {e}") logger.error(f"Erro ao inicializar ObjectStorageClient: {e}")
TEST_MODE = True TEST_MODE = True
FILE_INDEX: Dict[str, str] = {} FILE_INDEX: Dict[str, str] = {}
# ==========================
# Cache de Clientes OCI
# ==========================
@lru_cache(maxsize=10)
def get_oci_inference_client(region: str) -> 'oci.generative_ai_inference.GenerativeAiInferenceClient':
"""Retorna cliente OCI GenAI Inference com cache"""
endpoint = f"https://inference.generativeai.{region}.oci.oraclecloud.com"
return oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config,
service_endpoint=endpoint,
retry_strategy=oci.retry.NoneRetryStrategy(),
timeout=(10, 240)
)
# ========================== # ==========================
# Helpers: Signed URL (PAR) + Upload # Helpers: Signed URL (PAR) + Upload
# ========================== # ==========================
@@ -214,7 +263,7 @@ def upload_file_to_bucket(file_storage, filename: str) -> Dict[str, Any]:
# Modelos — JSON externo (hot-reload) # Modelos — JSON externo (hot-reload)
# ========================== # ==========================
LLM_CONFIG_PATH = os.environ.get("LLM_CONFIG_PATH", "/home/app/llm_models.json") LLM_CONFIG_PATH = os.environ.get("LLM_CONFIG_PATH", "./llm_models.json")
SUPPORTED_MODELS_DEFAULTS: Dict[str, Dict[str, Any]] = { SUPPORTED_MODELS_DEFAULTS: Dict[str, Dict[str, Any]] = {
"gpt5": { "gpt5": {
@@ -244,7 +293,7 @@ def get_supported_models() -> Dict[str, Dict[str, Any]]:
raise ValueError("Arquivo de modelos não contém 'models' válidos.") raise ValueError("Arquivo de modelos não contém 'models' válidos.")
return valid return valid
except Exception as e: except Exception as e:
print(f"[warn] Usando SUPPORTED_MODELS_DEFAULTS (motivo: {e})") logger.warning(f"Usando SUPPORTED_MODELS_DEFAULTS (motivo: {e})")
return SUPPORTED_MODELS_DEFAULTS return SUPPORTED_MODELS_DEFAULTS
def get_model_config(model_name: str) -> Dict[str, Any]: def get_model_config(model_name: str) -> Dict[str, Any]:
@@ -278,7 +327,7 @@ def session_controller(region, agent_endpoint_id, channel, cuid):
SESSION_STORE[session_key] = { SESSION_STORE[session_key] = {
"sessionId": new_session_id, "createdAt": now, "lastUsedAt": now, "sessionKey": session_key "sessionId": new_session_id, "createdAt": now, "lastUsedAt": now, "sessionKey": session_key
} }
print(f"[agent] nova sessão criada (TEST): key={session_key} id={new_session_id}") logger.info(f"[agent] nova sessão criada (TEST): key={session_key} id={new_session_id}")
return {"id": new_session_id, "sessionKey": session_key, "reused": False} return {"id": new_session_id, "sessionKey": session_key, "reused": False}
try: try:
@@ -300,7 +349,7 @@ def session_controller(region, agent_endpoint_id, channel, cuid):
SESSION_STORE[session_key] = { SESSION_STORE[session_key] = {
"sessionId": data.get("id"), "createdAt": now, "lastUsedAt": now, "sessionKey": session_key "sessionId": data.get("id"), "createdAt": now, "lastUsedAt": now, "sessionKey": session_key
} }
print(f"[agent] nova sessão criada: key={session_key} id={data.get('id')}") logger.info(f"[agent] nova sessão criada: key={session_key} id={data.get('id')}")
data["sessionKey"] = session_key data["sessionKey"] = session_key
data["reused"] = False data["reused"] = False
return data return data
@@ -311,7 +360,7 @@ def _invalidate_session(session_key: str):
try: try:
if session_key in SESSION_STORE: if session_key in SESSION_STORE:
del SESSION_STORE[session_key] del SESSION_STORE[session_key]
print(f"[agent] sessão invalidada: key={session_key}") logger.info(f"[agent] sessão invalidada: key={session_key}")
except Exception: except Exception:
pass pass
@@ -365,7 +414,7 @@ def ensure_data_url(image_url: str) -> str:
b64 = base64.b64encode(content).decode("utf-8") b64 = base64.b64encode(content).decode("utf-8")
return f"data:{mime};base64,{b64}" return f"data:{mime};base64,{b64}"
except Exception as e: except Exception as e:
print(f"[warn] Falha ao baixar imagem '{image_url}': {e}") logger.warning(f"Falha ao baixar imagem '{image_url}': {e}")
return image_url return image_url
def to_oci_messages(openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def to_oci_messages(openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@@ -402,24 +451,18 @@ def to_oci_messages(openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any
def build_oci_chat_payload(messages: List[Dict[str, Any]], params: Dict[str, Any]) -> Dict[str, Any]: def build_oci_chat_payload(messages: List[Dict[str, Any]], params: Dict[str, Any]) -> Dict[str, Any]:
"""Constrói payload para OCI Chat API""" """Constrói payload para OCI Chat API"""
payload = {"messages": messages} payload = {"messages": messages}
if "temperature" in params:
payload["temperature"] = params["temperature"] # Parâmetros simples (mapeamento 1:1)
if "top_p" in params: for param in SIMPLE_MODEL_PARAMS:
payload["top_p"] = params["top_p"] if param in params:
if "top_k" in params: payload[param] = params[param]
payload["top_k"] = params["top_k"]
if "frequency_penalty" in params: # Tratamento especial: max_tokens → max_completion_tokens (compatibilidade OpenAI)
payload["frequency_penalty"] = params["frequency_penalty"]
if "presence_penalty" in params:
payload["presence_penalty"] = params["presence_penalty"]
if "max_completion_tokens" in params: if "max_completion_tokens" in params:
payload["max_completion_tokens"] = params["max_completion_tokens"] payload["max_completion_tokens"] = params["max_completion_tokens"]
elif "max_tokens" in params: elif "max_tokens" in params:
payload["max_completion_tokens"] = params["max_tokens"] payload["max_completion_tokens"] = params["max_tokens"]
if "reasoning_effort" in params:
payload["reasoning_effort"] = params["reasoning_effort"]
if "verbosity" in params:
payload["verbosity"] = params["verbosity"]
return payload return payload
def extract_token_usage(oci_response: Any) -> Dict[str, Optional[int]]: def extract_token_usage(oci_response: Any) -> Dict[str, Optional[int]]:
@@ -449,14 +492,91 @@ def extract_token_usage(oci_response: Any) -> Dict[str, Optional[int]]:
if usage["total_tokens"] is None and usage["prompt_tokens"] and usage["completion_tokens"]: if usage["total_tokens"] is None and usage["prompt_tokens"] and usage["completion_tokens"]:
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"] usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
except Exception as e: except Exception as e:
print(f"[warn] Erro ao extrair token usage: {e}") logger.warning(f"Erro ao extrair token usage: {e}")
return usage
def extract_agent_token_usage(agent_response):
"""
Extrai informações de token usage de uma resposta de agente OCI.
Suporta múltiplas etapas de tool calling.
Estrutura esperada:
{
"traces": [
{
"traceType": "GENERATION_TRACE",
"usage": [
{
"usageDetails": {
"inputTokenCount": int,
"outputTokenCount": int
}
}
]
}
]
}
Args:
agent_response: Resposta do agente (dict)
Returns:
dict: {"prompt_tokens": int, "completion_tokens": int, "total_tokens": int}
"""
usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
if not agent_response or not isinstance(agent_response, dict):
return usage
try:
# Obter traces
traces = agent_response.get('traces', [])
total_input_tokens = 0
total_output_tokens = 0
# Iterar por todos os traces
for trace in traces:
# Verificar se é um GENERATION_TRACE (pode vir como traceType ou trace_type)
trace_type = trace.get('traceType') or trace.get('trace_type', '')
if trace_type == 'GENERATION_TRACE':
# Obter lista de usage
usage_list = trace.get('usage', [])
# Iterar por cada entrada de usage
for usage_entry in usage_list:
# Obter usageDetails (pode vir como usageDetails ou usage_details)
usage_details = usage_entry.get('usageDetails') or usage_entry.get('usage_details', {})
# Extrair contagens (pode vir em camelCase ou snake_case)
input_tokens = (
usage_details.get('inputTokenCount') or
usage_details.get('input_token_count', 0)
)
output_tokens = (
usage_details.get('outputTokenCount') or
usage_details.get('output_token_count', 0)
)
total_input_tokens += input_tokens
total_output_tokens += output_tokens
# Atualizar usage com os totais
usage["prompt_tokens"] = total_input_tokens
usage["completion_tokens"] = total_output_tokens
usage["total_tokens"] = total_input_tokens + total_output_tokens
except Exception as e:
logger.warning(f"Erro ao extrair token usage de agente: {e}")
return usage return usage
def oci_chat_invoke(model_region: str, compartment_id: str, model_ocid: str, oci_payload: Dict[str, Any]) -> Dict[str, Any]: def oci_chat_invoke(model_region: str, compartment_id: str, model_ocid: str, oci_payload: Dict[str, Any]) -> Dict[str, Any]:
"""Invoca modelo OCI GenAI e retorna resposta com token usage""" """Invoca modelo OCI GenAI e retorna resposta com token usage"""
print(">>> OCI CHAT REQUEST (payload que será enviado):") logger.debug(">>> OCI CHAT REQUEST (payload que será enviado):")
print(json.dumps(oci_payload, ensure_ascii=False, indent=2)) logger.debug(json.dumps(oci_payload, ensure_ascii=False, indent=2))
if TEST_MODE: if TEST_MODE:
return { return {
@@ -468,10 +588,7 @@ def oci_chat_invoke(model_region: str, compartment_id: str, model_ocid: str, oci
} }
try: try:
endpoint = f"https://inference.generativeai.{model_region}.oci.oraclecloud.com" client = get_oci_inference_client(model_region)
client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config, service_endpoint=endpoint, retry_strategy=oci.retry.NoneRetryStrategy(), timeout=(10, 240)
)
chat_detail = oci.generative_ai_inference.models.ChatDetails() chat_detail = oci.generative_ai_inference.models.ChatDetails()
generic = oci.generative_ai_inference.models.GenericChatRequest() generic = oci.generative_ai_inference.models.GenericChatRequest()
@@ -499,18 +616,14 @@ def oci_chat_invoke(model_region: str, compartment_id: str, model_ocid: str, oci
generic.messages = sdk_messages generic.messages = sdk_messages
if "temperature" in oci_payload: # Parâmetros simples (mapeamento 1:1)
generic.temperature = oci_payload["temperature"] for param in SIMPLE_MODEL_PARAMS:
if "top_p" in oci_payload: if param in oci_payload:
generic.top_p = oci_payload["top_p"] setattr(generic, param, oci_payload[param])
if "top_k" in oci_payload:
generic.top_k = oci_payload["top_k"] # Tratamento especial: max_completion_tokens
if "frequency_penalty" in oci_payload:
generic.frequency_penalty = oci_payload["frequency_penalty"]
if "presence_penalty" in oci_payload:
generic.presence_penalty = oci_payload["presence_penalty"]
if "max_completion_tokens" in oci_payload: if "max_completion_tokens" in oci_payload:
generic.max_tokens = oci_payload["max_completion_tokens"] generic.max_completion_tokens = oci_payload["max_completion_tokens"]
chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=model_ocid) chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=model_ocid)
chat_detail.chat_request = generic chat_detail.chat_request = generic
@@ -705,6 +818,191 @@ def _extract_agent_text(agent_payload: Any) -> str:
def test(): def test():
return jsonify({"test": "ok", "version": "2.0-refactored"}) return jsonify({"test": "ok", "version": "2.0-refactored"})
# ==========================
# Endpoints Globais OpenAI v1 (compatibilidade total com SDK OpenAI)
# ==========================
@app.route("/v1/models", methods=["GET"])
def list_all_models():
"""
Lista todos os modelos disponíveis.
Compatível com: OpenAI SDK client.models.list()
"""
supported = get_supported_models()
now = int(time.time())
models_list = []
for name, cfg in supported.items():
models_list.append({
"id": name,
"object": "model",
"created": now,
"owned_by": "oci.genai",
"permission": [],
"root": name,
"parent": None,
"type": cfg.get("type", "model"),
"region": cfg.get("region"),
"ocid": cfg.get("id"),
"compartmentId": cfg.get("compartmentId"),
"params": cfg.get("params", {})
})
return jsonify({"object": "list", "data": models_list})
@app.route("/v1/models/<model_id>", methods=["GET"])
def get_model_info(model_id):
"""
Retorna informações de um modelo específico.
Compatível com: OpenAI SDK client.models.retrieve(model_id)
"""
try:
model_config = get_model_config(model_id)
return jsonify({
"id": model_id,
"object": "model",
"created": int(time.time()),
"owned_by": "oci.genai",
"permission": [],
"root": model_id,
"parent": None,
"ocid": model_config.get("id"),
"compartmentId": model_config.get("compartmentId"),
"region": model_config.get("region"),
"type": model_config.get("type", "model"),
"params": model_config.get("params", {})
})
except ValueError as e:
return jsonify({"error": {"message": str(e), "type": "invalid_request_error", "code": "model_not_found"}}), 404
@app.route("/v1/chat/completions", methods=["POST"])
def global_chat_completions():
"""
Chat completion global.
Compatível com: OpenAI SDK client.chat.completions.create()
"""
try:
body = request.get_json(force=True, silent=False) or {}
except Exception as e:
return jsonify({"error": {"message": f"JSON inválido: {e}", "type": "invalid_request_error"}}), 400
model_name = body.get("model")
if not model_name:
return jsonify({"error": {"message": "Campo 'model' é obrigatório", "type": "invalid_request_error", "param": "model"}}), 400
try:
model_config = get_model_config(model_name)
except ValueError as e:
return jsonify({"error": {"message": str(e), "type": "invalid_request_error", "code": "model_not_found"}}), 404
# Redireciona para a lógica existente baseado no tipo
if model_config.get("type") == "agent":
return _handle_agent_chat(model_name, model_config, body)
# Lógica para modelos
msgs = body.get("messages") or []
if not isinstance(msgs, list) or not msgs:
return jsonify({"error": {"message": "Campo 'messages' é obrigatório e deve ser uma lista", "type": "invalid_request_error", "param": "messages"}}), 400
params = model_config.get("params", {}).copy()
for k in ["temperature", "top_p", "top_k", "max_tokens", "frequency_penalty",
"presence_penalty", "reasoning_effort", "verbosity", "max_completion_tokens"]:
if k in body and body[k] is not None:
params[k] = body[k]
oci_msgs = to_oci_messages(msgs)
oci_payload = build_oci_chat_payload(oci_msgs, params)
model_region = model_config.get("region")
compartment_id = model_config.get("compartmentId")
model_ocid = model_config.get("id")
oci_result = oci_chat_invoke(model_region, compartment_id, model_ocid, oci_payload)
if isinstance(oci_result, dict):
output_text = oci_result.get("output_text")
usage = oci_result.get("usage", {})
else:
output_text = None
usage = {}
if output_text is None:
output_text = json.dumps(oci_result, ensure_ascii=False)
if body.get("stream") is True:
return Response(
stream_with_context(sse_chat_stream(model_name, output_text)),
mimetype="text/event-stream"
)
return jsonify(to_openai_chat_response(model_name, output_text, usage))
@app.route("/v1/completions", methods=["POST"])
def global_text_completions():
"""
Text completion global.
Compatível com: OpenAI SDK client.completions.create()
"""
try:
body = request.get_json(force=True, silent=False) or {}
except Exception as e:
return jsonify({"error": {"message": f"JSON inválido: {e}", "type": "invalid_request_error"}}), 400
model_name = body.get("model")
if not model_name:
return jsonify({"error": {"message": "Campo 'model' é obrigatório", "type": "invalid_request_error", "param": "model"}}), 400
try:
model_config = get_model_config(model_name)
except ValueError as e:
return jsonify({"error": {"message": str(e), "type": "invalid_request_error", "code": "model_not_found"}}), 404
# Redireciona para a lógica existente baseado no tipo
if model_config.get("type") == "agent":
return _handle_agent_completion(model_name, model_config, body)
# Lógica para modelos
prompt = body.get("prompt")
if not prompt:
return jsonify({"error": {"message": "Campo 'prompt' é obrigatório", "type": "invalid_request_error", "param": "prompt"}}), 400
params = model_config.get("params", {}).copy()
for k in ["temperature", "top_p", "top_k", "max_tokens", "frequency_penalty", "presence_penalty"]:
if k in body and body[k] is not None:
params[k] = body[k]
# Converte prompt para formato de mensagem
msgs = [{"role": "USER", "content": [{"type": "TEXT", "text": str(prompt)}]}]
oci_payload = build_oci_chat_payload(msgs, params)
model_region = model_config.get("region")
compartment_id = model_config.get("compartmentId")
model_ocid = model_config.get("id")
oci_result = oci_chat_invoke(model_region, compartment_id, model_ocid, oci_payload)
if isinstance(oci_result, dict):
output_text = oci_result.get("output_text")
usage = oci_result.get("usage", {})
else:
output_text = None
usage = {}
if output_text is None:
output_text = json.dumps(oci_result, ensure_ascii=False)
if body.get("stream") is True:
return Response(
stream_with_context(sse_chat_stream(model_name, output_text)),
mimetype="text/event-stream"
)
return jsonify(to_openai_text_response(model_name, output_text, usage))
# ==========================
# Endpoints OpenAI v1 — ESTRUTURA /genai/{modelname}/v1/...
# ==========================
@app.route("/genai/<model_name>/v1/models", methods=["GET"]) @app.route("/genai/<model_name>/v1/models", methods=["GET"])
def v1_models(model_name): def v1_models(model_name):
""" """
@@ -741,8 +1039,8 @@ def v1_chat_completions(model_name):
except Exception as e: except Exception as e:
return jsonify({"error": f"JSON inválido: {e}"}), 400 return jsonify({"error": f"JSON inválido: {e}"}), 400
print(f">>> /genai/{model_name}/v1/chat/completions body recebido:") logger.debug(f">>> /genai/{model_name}/v1/chat/completions body recebido:")
print(json.dumps(body, ensure_ascii=False, indent=2)) logger.debug(json.dumps(body, ensure_ascii=False, indent=2))
try: try:
model_config = get_model_config(model_name) model_config = get_model_config(model_name)
@@ -853,8 +1151,8 @@ def _handle_agent_chat(model_name: str, model_config: Dict[str, Any], body: Dict
mimetype="text/event-stream" mimetype="text/event-stream"
) )
# Resposta normal (agents não retornam token usage real, então usamos None) # Extrair token usage da resposta do agente
usage = {"prompt_tokens": None, "completion_tokens": None, "total_tokens": None} usage = extract_agent_token_usage(agent_resp)
resp = to_openai_chat_response(model_name, response_text, usage) resp = to_openai_chat_response(model_name, response_text, usage)
resp["_agent"] = {"session_id": session_id, "reused": sess.get("reused", False)} resp["_agent"] = {"session_id": session_id, "reused": sess.get("reused", False)}
return jsonify(resp) return jsonify(resp)
@@ -888,7 +1186,7 @@ def _handle_agent_completion(model_name: str, model_config: Dict[str, Any], body
if "error" in sess: if "error" in sess:
# Se falhar ao criar sessão, tenta continuar sem sessão (alguns agents não precisam) # Se falhar ao criar sessão, tenta continuar sem sessão (alguns agents não precisam)
print(f"[warn] Falha ao criar sessão para agent: {sess['error']}") logger.warning(f"Falha ao criar sessão para agent: {sess['error']}")
session_error = True session_error = True
else: else:
session_id = sess["id"] session_id = sess["id"]
@@ -930,8 +1228,8 @@ def _handle_agent_completion(model_name: str, model_config: Dict[str, Any], body
mimetype="text/event-stream" mimetype="text/event-stream"
) )
# Resposta normal (agents não retornam token usage real) # Extrair token usage da resposta do agente
usage = {"prompt_tokens": None, "completion_tokens": None, "total_tokens": None} usage = extract_agent_token_usage(agent_resp)
resp = to_openai_text_response(model_name, response_text, usage) resp = to_openai_text_response(model_name, response_text, usage)
if session_id: if session_id:
resp["_agent"] = {"session_id": session_id, "reused": sess.get("reused", False)} resp["_agent"] = {"session_id": session_id, "reused": sess.get("reused", False)}
@@ -945,8 +1243,8 @@ def v1_text_completions(model_name):
except Exception as e: except Exception as e:
return jsonify({"error": f"JSON inválido: {e}"}), 400 return jsonify({"error": f"JSON inválido: {e}"}), 400
print(f">>> /genai/{model_name}/v1/completions body recebido:") logger.debug(f">>> /genai/{model_name}/v1/completions body recebido:")
print(json.dumps(body, ensure_ascii=False, indent=2)) logger.debug(json.dumps(body, ensure_ascii=False, indent=2))
try: try:
model_config = get_model_config(model_name) model_config = get_model_config(model_name)
@@ -1137,8 +1435,8 @@ def oci_session(model_name):
return jsonify({"error": f"Modelo '{model_name}' não é um agent. Use type='agent' no JSON."}), 400 return jsonify({"error": f"Modelo '{model_name}' não é um agent. Use type='agent' no JSON."}), 400
data = request.get_json() or {} data = request.get_json() or {}
print(f">>> /genai/{model_name}/session payload recebido:") logger.debug(f">>> /genai/{model_name}/session payload recebido:")
print(json.dumps(data, ensure_ascii=False, indent=2)) logger.debug(json.dumps(data, ensure_ascii=False, indent=2))
channel = data.get("channel") channel = data.get("channel")
cuid = data.get("cuid") cuid = data.get("cuid")
@@ -1152,6 +1450,53 @@ def oci_session(model_name):
response_data = session_controller(model_region, agent_endpoint_id, channel, cuid) response_data = session_controller(model_region, agent_endpoint_id, channel, cuid)
return jsonify(response_data) return jsonify(response_data)
def _chat_with_retry_on_session_expired(model_region, agent_endpoint_id, session_id, session_key, user_message):
"""
Envia mensagem ao agente com retry automático em caso de sessão expirada (409).
Returns:
tuple: (response_data, session_id, error_response)
- Se sucesso: (response_data, session_id, None)
- Se erro: (None, None, error_response)
"""
# Primeira tentativa
response_data = ask_agent(model_region, agent_endpoint_id, session_id, user_message)
# Se retornou erro 409 (sessão inválida), tenta recuperar
if isinstance(response_data, dict) and response_data.get("_http_status") == 409:
logger.info(f"[chat] Sessão expirou (409), invalidando e recriando...")
# Invalida sessão local
_invalidate_session(session_key)
# Extrai channel e cuid do session_key
channel, cuid = session_key.split(":", 1)
# Cria nova sessão
sess = session_controller(model_region, agent_endpoint_id, channel, cuid)
if "error" in sess:
return None, None, (jsonify({
"error": f"Falha ao recriar sessão após erro 409: {sess.get('error')}",
"details": sess
}), 500)
new_session_id = sess.get("id")
# Retry com nova sessão
response_data = ask_agent(model_region, agent_endpoint_id, new_session_id, user_message)
# Se ainda falhou, retorna erro
if isinstance(response_data, dict) and response_data.get("_http_status") == 409:
return None, None, (jsonify({
"error": "Falha persistente de sessão após retry",
"details": response_data
}), 500)
return response_data, new_session_id, None
return response_data, session_id, None
@app.route("/genai/<model_name>/chat", methods=["POST"]) @app.route("/genai/<model_name>/chat", methods=["POST"])
def oci_chat(model_name): def oci_chat(model_name):
""" """
@@ -1174,8 +1519,8 @@ def oci_chat(model_name):
return jsonify({"error": f"Modelo '{model_name}' não é um agent. Use type='agent' no JSON."}), 400 return jsonify({"error": f"Modelo '{model_name}' não é um agent. Use type='agent' no JSON."}), 400
data = request.get_json() or {} data = request.get_json() or {}
print(f">>> /genai/{model_name}/chat payload recebido:") logger.debug(f">>> /genai/{model_name}/chat payload recebido:")
print(json.dumps(data, ensure_ascii=False, indent=2)) logger.debug(json.dumps(data, ensure_ascii=False, indent=2))
user_message = data.get("userMessage") user_message = data.get("userMessage")
if not user_message: if not user_message:
@@ -1201,13 +1546,12 @@ def oci_chat(model_name):
} }
}), 400 }), 400
# Modo automático: gerencia sessão internamente # Modo automático: gerencia sessão internamente com retry
if channel and cuid: if channel and cuid:
session_key = f"{channel}:{cuid}" session_key = f"{channel}:{cuid}"
# Tenta obter/criar sessão # Obter/criar sessão
sess = session_controller(model_region, agent_endpoint_id, channel, cuid) sess = session_controller(model_region, agent_endpoint_id, channel, cuid)
if "error" in sess: if "error" in sess:
return jsonify({ return jsonify({
"error": f"Falha ao criar sessão: {sess.get('error')}", "error": f"Falha ao criar sessão: {sess.get('error')}",
@@ -1216,45 +1560,24 @@ def oci_chat(model_name):
session_id = sess.get("id") session_id = sess.get("id")
# Primeira tentativa # Enviar mensagem com retry automático
response_data = ask_agent(model_region, agent_endpoint_id, session_id, user_message) response_data, session_id, error = _chat_with_retry_on_session_expired(
model_region, agent_endpoint_id, session_id, session_key, user_message
)
# Se retornou erro 409 (sessão inválida), tenta recuperar if error:
if isinstance(response_data, dict) and response_data.get("_http_status") == 409: return error
print(f"[chat] Sessão expirou (409), invalidando e recriando...")
# Invalida sessão local # Extrair token usage e retornar
_invalidate_session(session_key) usage = extract_agent_token_usage(response_data)
# Cria nova sessão
sess = session_controller(model_region, agent_endpoint_id, channel, cuid)
if "error" in sess:
return jsonify({
"error": f"Falha ao recriar sessão após erro 409: {sess.get('error')}",
"details": sess
}), 500
session_id = sess.get("id")
# Retry com nova sessão
response_data = ask_agent(model_region, agent_endpoint_id, session_id, user_message)
# Se ainda falhou, retorna erro
if isinstance(response_data, dict) and response_data.get("_http_status") == 409:
return jsonify({
"error": "Falha persistente de sessão após retry",
"details": response_data
}), 500
# Retorna resposta com informações de sessão
return jsonify({ return jsonify({
"agentResponse": response_data, "agentResponse": response_data,
"sessionInfo": { "sessionInfo": {
"sessionId": session_id, "sessionId": session_id,
"sessionKey": session_key, "sessionKey": session_key,
"reused": sess.get("reused", False) "reused": sess.get("reused", False)
} },
"usage": usage
}) })
# Modo manual: usa sessionId fornecido # Modo manual: usa sessionId fornecido
@@ -1269,7 +1592,13 @@ def oci_chat(model_name):
"details": response_data "details": response_data
}), 409 }), 409
return jsonify({"agentResponse": response_data}) # Extrair token usage da resposta do agente
usage = extract_agent_token_usage(response_data)
return jsonify({
"agentResponse": response_data,
"usage": usage
})
@app.route("/genai/<model_name>/inference", methods=["POST"]) @app.route("/genai/<model_name>/inference", methods=["POST"])
def oci_inference(model_name): def oci_inference(model_name):
@@ -1287,8 +1616,8 @@ def oci_inference(model_name):
return jsonify({"error": f"'{model_name}' é um agent. Use /genai/{model_name}/chat ao invés de /inference."}), 400 return jsonify({"error": f"'{model_name}' é um agent. Use /genai/{model_name}/chat ao invés de /inference."}), 400
data = request.get_json() or {} data = request.get_json() or {}
print(f">>> /genai/{model_name}/inference payload recebido:") logger.debug(f">>> /genai/{model_name}/inference payload recebido:")
print(json.dumps(data, ensure_ascii=False, indent=2)) logger.debug(json.dumps(data, ensure_ascii=False, indent=2))
prompt = data.get("prompt") prompt = data.get("prompt")
if not prompt: if not prompt:
@@ -1313,13 +1642,7 @@ def oci_inference(model_name):
}) })
try: try:
endpoint = f"https://inference.generativeai.{model_region}.oci.oraclecloud.com" client = get_oci_inference_client(model_region)
client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config,
service_endpoint=endpoint,
retry_strategy=oci.retry.NoneRetryStrategy(),
timeout=(10, 240)
)
# Cria mensagem # Cria mensagem
content = oci.generative_ai_inference.models.TextContent() content = oci.generative_ai_inference.models.TextContent()
@@ -1347,12 +1670,18 @@ def oci_inference(model_name):
chat_response = client.chat(chat_detail) chat_response = client.chat(chat_detail)
chat_choices = chat_response.data.chat_response.choices chat_choices = chat_response.data.chat_response.choices
# Extrair token usage da resposta do modelo
usage = extract_token_usage(chat_response)
chat_data = { chat_data = {
"text": chat_choices[0].message.content[0].text, "text": chat_choices[0].message.content[0].text,
"finish_reason": chat_choices[0].finish_reason "finish_reason": chat_choices[0].finish_reason
} }
return jsonify({"response": chat_data}) return jsonify({
"response": chat_data,
"usage": usage
})
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@@ -1361,6 +1690,6 @@ def oci_inference(model_name):
# ========================== # ==========================
if __name__ == '__main__': if __name__ == '__main__':
print("=" * 60) logger.info("=" * 60)
print("OCI GenAI Proxy v2.0.3") logger.info("OCI GenAI Proxy v2.0.3")
app.run(host='0.0.0.0', port=8000, debug=False) app.run(host='0.0.0.0', port=8000, debug=False)