Update app.py

This commit is contained in:
Marcos Lohmann
2025-10-12 20:46:01 -03:00
committed by GitHub
parent 232ddb84c1
commit e0aee4b2a7

View File

@@ -2,12 +2,16 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Requisitos: # Requisitos:
# pip install flask oci requests pillow # pip install flask oci requests pillow
# # (recomendado p/ CORS em produção)
# pip install flask-cors
# Execução: # Execução:
# 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/"
# # opcional: onde está o JSON dos modelos # # opcional: onde está o JSON dos modelos
# export LLM_CONFIG_PATH="/home/app/llm_models.json" # export LLM_CONFIG_PATH="/home/app/llm_models.json"
# # opcional: debug da autenticação
# export DEBUG_AUTH=true
# python api.py # porta 8000 # python api.py # porta 8000
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -27,6 +31,30 @@ from typing import Any, Dict, List, Optional, Generator
app = Flask(__name__) app = Flask(__name__)
# ==========================
# CORS (habilita para OpenWebUI e browsers)
# ==========================
try:
from flask_cors import CORS
CORS(
app,
resources={r"/*": {"origins": "*"}},
supports_credentials=False,
allow_headers=["Content-Type", "Authorization", "X-API-Key", "X-Channel", "X-Cuid"],
expose_headers=["Content-Type", "Authorization", "X-API-Key"],
methods=["GET", "POST", "OPTIONS"]
)
except Exception as _e:
print("AVISO: flask-cors não instalado; CORS mínimo será aplicado via after_request.")
@app.after_request
def add_cors_headers(resp):
resp.headers.setdefault("Access-Control-Allow-Origin", "*")
resp.headers.setdefault("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
resp.headers.setdefault("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Channel, X-Cuid")
return resp
# ========================== # ==========================
# Configuração e Autenticação OCI # Configuração e Autenticação OCI
# ========================== # ==========================
@@ -69,42 +97,47 @@ if not TEST_MODE:
# Segurança API # Segurança API
# ========================== # ==========================
DEBUG_AUTH = os.environ.get("DEBUG_AUTH", "false").lower() == "true"
def _safe_equals(a: str, b: str) -> bool: def _safe_equals(a: str, b: str) -> bool:
if a is None or b is None: if a is None or b is None:
return False return False
return hmac.compare_digest(a, b) return hmac.compare_digest(a, b)
def _parse_bearer_token(auth_header: str) -> str: def _parse_bearer_token(auth_header: str) -> str:
# Suporta "Bearer <token>" (case-insensitive no prefixo). # Suporta "Bearer <token>" (case-insensitive no prefixo). Opcionalmente aceita "Token <token>".
if not auth_header: if not auth_header:
return "" return ""
parts = auth_header.strip().split() parts = auth_header.strip().split()
if len(parts) == 2 and parts[0].lower() == "bearer": if len(parts) == 2 and parts[0].lower() in ("bearer", "token"):
return parts[1] return parts[1]
return "" return ""
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:
# Sem chave configurada, não bloquear (mantém comportamento permissivo atual)
print("AVISO: API_KEY não configurada nas variáveis de ambiente.") print("AVISO: API_KEY não configurada nas variáveis de ambiente.")
return return
# 1) Suporte existente: X-API-Key
provided_key = request.headers.get("X-API-Key") provided_key = request.headers.get("X-API-Key")
# 2) Novo: Authorization: Bearer <API_KEY>
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
bearer_token = _parse_bearer_token(auth_header) bearer_token = _parse_bearer_token(auth_header)
# Válido se QUALQUER um bater if DEBUG_AUTH:
print(f"[auth] method={request.method} path={request.path} "
f"X-API-Key={'<set>' if provided_key else '<none>'} "
f"Authorization={'<set>' if auth_header else '<none>'}")
if _safe_equals(provided_key, expected_key) or _safe_equals(bearer_token, expected_key): if _safe_equals(provided_key, expected_key) or _safe_equals(bearer_token, expected_key):
return return
abort(401, description="Credenciais inválidas ou ausentes. Use X-API-Key ou Authorization: Bearer.") abort(401, description="Credenciais inválidas ou ausentes. Use X-API-Key ou Authorization: Bearer.")
@app.before_request @app.before_request
def before_all_requests(): def before_all_requests():
# Permitir preflight CORS (OPTIONS) sem autenticação
if request.method == "OPTIONS":
return "", 204
check_api_key() check_api_key()
# ========================== # ==========================
@@ -205,17 +238,16 @@ def get_signed_url_from_file_id(file_id: str, hours_valid: int = 24) -> Optional
# Modelos — defaults e JSON externo (hot-reload) # Modelos — defaults e JSON externo (hot-reload)
# ========================== # ==========================
# Defaults embutidos (fallback)
SUPPORTED_MODELS_DEFAULTS: Dict[str, Dict[str, Any]] = { SUPPORTED_MODELS_DEFAULTS: Dict[str, Dict[str, Any]] = {
"gpt5": { # OpenAI GPT-5 "gpt5": { # OpenAI GPT-5
"id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyasebknceb4ekbiaiisjtu3fj5i7s4io3ignvg4ip2uyma", "id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyasebknceb4ekbiaiisjtu3fj5i7s4io3ignvg4ip2uyma",
"params": {"max_completion_tokens": 2048, "reasoning_effort": "MEDIUM", "verbosity": "MEDIUM"} "params": {"max_completion_tokens": 2048, "reasoning_effort": "MEDIUM", "verbosity": "MEDIUM"}
}, },
"grok3mini": { # xAI Grok-3 Mini "grok3mini": { # xAI Grok-3 Mini
"id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyavwbgai5nlntsd5hngaileroifuoec5qxttmydhq7mykq", "id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyavwbgai5nlntsd5hngaileroifuoec5qxttmydhq7mykq",
"params": {"temperature": 1, "top_p": 1, "max_tokens": 600} "params": {"temperature": 1, "top_p": 1, "max_tokens": 600}
}, },
"llama4maverick": { # Meta Llama-4 Maverick "llama4maverick": { # Meta Llama-4 Maverick
"id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyayjawvuonfkw2ua4bob4rlnnlhs522pafbglivtwlfzta", "id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceyayjawvuonfkw2ua4bob4rlnnlhs522pafbglivtwlfzta",
"params": {"temperature": 1, "top_p": 0.75, "max_tokens": 600, "frequency_penalty": 0, "presence_penalty": 0} "params": {"temperature": 1, "top_p": 0.75, "max_tokens": 600, "frequency_penalty": 0, "presence_penalty": 0}
}, },
@@ -231,7 +263,7 @@ SUPPORTED_MODELS_DEFAULTS: Dict[str, Dict[str, Any]] = {
"id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceya3eub3uksacl5q35mrigancv6rbppihlg7ihhjofyc22q", "id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceya3eub3uksacl5q35mrigancv6rbppihlg7ihhjofyc22q",
"params": {"temperature": 1, "top_p": 1, "top_k": 0, "max_tokens": 2048, "frequency_penalty": 0, "presence_penalty": 0} "params": {"temperature": 1, "top_p": 1, "top_k": 0, "max_tokens": 2048, "frequency_penalty": 0, "presence_penalty": 0}
}, },
"grok4": { # xAI Grok-4 "grok4": { # xAI Grok-4
"id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceya3bsfz4ogiuv3yc7gcnlry7gi3zzx6tnikg6jltqszm2q", "id": "ocid1.generativeaimodel.oc1.us-chicago-1.amaaaaaask7dceya3bsfz4ogiuv3yc7gcnlry7gi3zzx6tnikg6jltqszm2q",
"params": {"temperature": 1, "top_p": 1, "top_k": 0, "max_tokens": 20000} "params": {"temperature": 1, "top_p": 1, "top_k": 0, "max_tokens": 20000}
} }
@@ -254,13 +286,11 @@ def get_supported_models() -> Dict[str, Dict[str, Any]]:
with open(LLM_CONFIG_PATH, "r", encoding="utf-8") as f: with open(LLM_CONFIG_PATH, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
models = data.get("models", {}) models = data.get("models", {})
# Validação simples: precisa ter 'id' em cada modelo
valid = {k: v for k, v in models.items() if isinstance(v, dict) and v.get("id")} valid = {k: v for k, v in models.items() if isinstance(v, dict) and v.get("id")}
if not valid: if not valid:
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:
# fallback nos defaults embutidos
print(f"[warn] Usando SUPPORTED_MODELS_DEFAULTS (motivo: {e})") print(f"[warn] Usando SUPPORTED_MODELS_DEFAULTS (motivo: {e})")
return SUPPORTED_MODELS_DEFAULTS return SUPPORTED_MODELS_DEFAULTS
@@ -286,12 +316,12 @@ def session_controller(region, agent_endpoint_id, channel, cuid):
existing["lastUsedAt"] = now existing["lastUsedAt"] = now
return {"id": existing["sessionId"], "sessionKey": session_key, "reused": True} return {"id": existing["sessionId"], "sessionKey": session_key, "reused": True}
# Sessão expirada ou inexistente → cria nova
if TEST_MODE: if TEST_MODE:
new_session_id = f"test_session_{agent_endpoint_id[:8]}_{int(now.timestamp())}" new_session_id = f"test_session_{agent_endpoint_id[:8]}_{int(now.timestamp())}"
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}")
return {"id": new_session_id, "sessionKey": session_key, "reused": False} return {"id": new_session_id, "sessionKey": session_key, "reused": False}
try: try:
@@ -313,12 +343,21 @@ 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')}")
data["sessionKey"] = session_key data["sessionKey"] = session_key
data["reused"] = False data["reused"] = False
return data return data
except Exception as e: except Exception as e:
return {"error": str(e), "sessionKey": session_key} return {"error": str(e), "sessionKey": session_key}
def _invalidate_session(session_key: str):
try:
if session_key in SESSION_STORE:
del SESSION_STORE[session_key]
print(f"[agent] sessão invalidada: key={session_key}")
except Exception:
pass
# ========================== # ==========================
# Funções de interação (Agente + Inference) # Funções de interação (Agente + Inference)
# ========================== # ==========================
@@ -336,14 +375,33 @@ def ask_agent(region, agent_endpoint_id, session_id, user_message):
base_url = f"https://agent-runtime.generativeai.{region}.oci.oraclecloud.com/20240531" base_url = f"https://agent-runtime.generativeai.{region}.oci.oraclecloud.com/20240531"
chat_url = f"{base_url}/agentEndpoints/{agent_endpoint_id}/actions/chat" chat_url = f"{base_url}/agentEndpoints/{agent_endpoint_id}/actions/chat"
payload = {"userMessage": user_message, "shouldStream": False, "sessionId": session_id} payload = {"userMessage": user_message, "shouldStream": False, "sessionId": session_id}
response = session.post(chat_url, json=payload)
response.raise_for_status() try:
return response.json() response = session.post(chat_url, json=payload)
status = response.status_code
text_body = None
try:
json_body = response.json()
except Exception:
json_body = None
text_body = response.text
if 200 <= status < 300:
return json_body if json_body is not None else {"message": text_body or ""}
else:
return {
"_http_status": status,
"_raw_text": text_body,
"_raw_json": json_body
}
except Exception as e:
return {
"_http_status": 0,
"error": f"Falha de rede ao chamar Agent: {e}"
}
def call_inference_model(region, compartment_id, model_id, prompt): def call_inference_model(region, compartment_id, model_id, prompt):
print(">>> /inference payload recebido:") print(">>> /inference payload recebido:")
data = {"prompt": prompt, "region": region, "compartment_id": compartment_id, "model_id": model_id}
if TEST_MODE: if TEST_MODE:
return {"response": f"Resposta simulada para o prompt: {prompt}"} return {"response": f"Resposta simulada para o prompt: {prompt}"}
@@ -408,7 +466,7 @@ def resolve_model_and_params(body: Dict[str, Any], path_model_id: str) -> Dict[s
3) path_model_id se for chave suportada ou OCID. 3) path_model_id se for chave suportada ou OCID.
Mescla defaults + overrides do corpo (OpenAI-like). Mescla defaults + overrides do corpo (OpenAI-like).
""" """
supported = get_supported_models() # HOT-RELOAD ⟵ lê JSON a cada chamada supported = get_supported_models() # HOT-RELOAD
user_model = body.get("model") user_model = body.get("model")
model_key = None model_key = None
model_ocid = None model_ocid = None
@@ -437,7 +495,7 @@ def resolve_model_and_params(body: Dict[str, Any], path_model_id: str) -> Dict[s
"temperature", "top_p", "top_k", "max_tokens", "frequency_penalty", "presence_penalty", "temperature", "top_p", "top_k", "max_tokens", "frequency_penalty", "presence_penalty",
"reasoning_effort", "verbosity", "max_completion_tokens" "reasoning_effort", "verbosity", "max_completion_tokens"
]: ]:
if k in body and body[k] is not None: if k in body and k in body and body[k] is not None:
overrides[k] = body[k] overrides[k] = body[k]
merged = {**defaults, **overrides} merged = {**defaults, **overrides}
@@ -508,7 +566,6 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
generic = oci.generative_ai_inference.models.GenericChatRequest() generic = oci.generative_ai_inference.models.GenericChatRequest()
generic.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC generic.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
# Converte nosso payload em objetos do SDK
sdk_messages = [] sdk_messages = []
for m in oci_payload["messages"]: for m in oci_payload["messages"]:
sdk_msg = oci.generative_ai_inference.models.Message() sdk_msg = oci.generative_ai_inference.models.Message()
@@ -527,7 +584,6 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
generic.messages = sdk_messages generic.messages = sdk_messages
# Parâmetros
if "temperature" in oci_payload: generic.temperature = oci_payload["temperature"] if "temperature" in oci_payload: generic.temperature = oci_payload["temperature"]
if "top_p" in oci_payload: generic.top_p = oci_payload["top_p"] if "top_p" in oci_payload: generic.top_p = oci_payload["top_p"]
if "top_k" in oci_payload: generic.top_k = oci_payload["top_k"] if "top_k" in oci_payload: generic.top_k = oci_payload["top_k"]
@@ -584,6 +640,120 @@ def sse_chat_stream(model_label: str, full_text: str) -> Generator[str, None, No
yield f"data: {json.dumps(endchunk)}\n\n" yield f"data: {json.dumps(endchunk)}\n\n"
yield "data: [DONE]\n\n" yield "data: [DONE]\n\n"
# ===== Helpers OpenAI-v1 p/ Agents (sem session_id em URL) =====
def _messages_to_text(messages: List[Dict[str, Any]]) -> str:
"""
Concatena conteúdo textual de mensagens OpenAI-like para enviar a um GenAI Agent.
Ignora imagens/partes não-textuais. Mantém a ordem.
"""
if not isinstance(messages, list):
return ""
chunks = []
for m in messages:
content = m.get("content", "")
if isinstance(content, str):
chunks.append(content)
elif isinstance(content, list):
for p in content:
if isinstance(p, dict) and p.get("type") == "text":
t = p.get("text", "")
if t:
chunks.append(t)
elif isinstance(p, str):
chunks.append(p)
elif content:
try:
chunks.append(json.dumps(content, ensure_ascii=False))
except Exception:
pass
return "\n".join(chunks).strip()
def _coerce_to_text(val: Any) -> str:
if val is None:
return ""
if isinstance(val, str):
return val
try:
if isinstance(val, dict):
if isinstance(val.get("text"), str):
return val["text"]
if isinstance(val.get("content"), str):
return val["content"]
if isinstance(val.get("content"), dict) and isinstance(val["content"].get("text"), str):
return val["content"]["text"]
if isinstance(val.get("content"), list):
for c in val["content"]:
if isinstance(c, dict) and isinstance(c.get("text"), str):
return c["text"]
data = val.get("data")
if isinstance(data, dict):
for key in ("message", "output", "text"):
if isinstance(data.get(key), str):
return data[key]
if isinstance(data.get("content"), dict) and isinstance(data["content"].get("text"), str):
return data["content"]["text"]
if isinstance(data.get("content"), list):
for c in data["content"]:
if isinstance(c, dict) and isinstance(c.get("text"), str):
return c["text"]
return json.dumps(val, ensure_ascii=False)
except Exception:
return str(val)
def _extract_agent_text(agent_payload: Any) -> str:
"""
Extrai o texto principal de respostas de GenAI Agent em diferentes formatos,
incluindo {"role":"AGENT","content":{"text":"..."}}.
"""
if agent_payload is None:
return ""
if isinstance(agent_payload, str):
try:
maybe_json = json.loads(agent_payload)
return _extract_agent_text(maybe_json)
except Exception:
return agent_payload
if isinstance(agent_payload, dict):
candidates = [
agent_payload.get("message"),
agent_payload.get("output"),
agent_payload.get("text"),
agent_payload.get("content"),
agent_payload.get("data"),
agent_payload.get("result"),
]
for c in candidates:
if c is not None:
txt = _coerce_to_text(c)
if txt:
return txt
return _coerce_to_text(agent_payload)
return _coerce_to_text(agent_payload)
def _resolve_agent_session(region: str, agent_endpoint_id: str) -> Dict[str, Any]:
"""
SEM session_id na URL. Cria/renova sessão automaticamente com base em:
- X-Channel (opcional, default "openai-v1")
- X-Cuid (opcional). Se ausente, deriva a partir do header de auth ou gera determinístico.
"""
channel = request.headers.get("X-Channel") or "openai-v1"
cuid = request.headers.get("X-Cuid")
if not cuid:
seed = request.headers.get("Authorization") or request.headers.get("X-API-Key") or uuid.uuid4().hex
cuid = uuid.uuid5(uuid.NAMESPACE_OID, seed).hex
return session_controller(region, agent_endpoint_id, channel, cuid)
def _agent_openai_like_answer(model_label: str, agent_payload: Dict[str, Any]) -> Dict[str, Any]:
"""
Mapeia a resposta do Agent para o formato OpenAI chat.completion.
Garante que 'choices[0].message.content' seja SEMPRE string.
"""
text = _extract_agent_text(agent_payload)
return to_openai_chat_response(model_label, text)
# ========================== # ==========================
# Endpoints nativos # Endpoints nativos
# ========================== # ==========================
@@ -627,7 +797,7 @@ def inference(region, compartment_id, model_id):
return jsonify(response_data) return jsonify(response_data)
# ========================== # ==========================
# Endpoints OpenAI v1 compat — CHAT # Endpoints OpenAI v1 compat — CHAT (LLMs)
# ========================== # ==========================
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/chat/completions", methods=["POST"]) @app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/chat/completions", methods=["POST"])
@@ -659,7 +829,7 @@ def v1_chat_completions(region, compartment_id, path_model_id):
oci_result.get("output_text") oci_result.get("output_text")
or oci_result.get("generated_text") or oci_result.get("generated_text")
or oci_result.get("inference_response", {}).get("output_text") or oci_result.get("inference_response", {}).get("output_text")
or oci_result.get("payload", {}).get("output_text") # dry-run or oci_result.get("payload", {}).get("output_text")
) )
else: else:
output_text = None output_text = None
@@ -718,6 +888,109 @@ def v1_text_completions(region, compartment_id, path_model_id):
return jsonify(to_openai_text_response(model_label, output_text)) return jsonify(to_openai_text_response(model_label, output_text))
# ==========================
# OpenAI v1 compatibility — GenAI Agents (auto-sessão com retry 409)
# ==========================
@app.route("/genai-agent/<region>/<agent_endpoint_id>/v1/chat/completions", methods=["POST"])
def agent_v1_chat_completions_auto(region, agent_endpoint_id):
"""
OpenAI v1 compat (SEM session_id na URL): cria/renova uma sessão automaticamente.
Faz retry 1x em caso de 409 (sessão inválida/expirada).
"""
try:
body = request.get_json(force=True, silent=False) or {}
except Exception as e:
return jsonify({"error": f"JSON inválido: {e}"}), 400
messages = body.get("messages")
if not messages or not isinstance(messages, list):
return jsonify({"error": "Campo 'messages' é obrigatório e deve ser uma lista."}), 400
# cria/renova sessão automaticamente (primeira tentativa)
sess = _resolve_agent_session(region, agent_endpoint_id)
if "error" in sess:
return jsonify({"error": f"Falha ao criar sessão: {sess['error']}"}), 500
session_id = sess.get("id")
session_key = sess.get("sessionKey") or ""
# extrai texto das mensagens OpenAI-like
user_message = _messages_to_text(messages)
if not user_message:
return jsonify({"error": "Não foi possível extrair texto de 'messages'."}), 400
# 1ª chamada ao Agent
agent_resp = ask_agent(region, agent_endpoint_id, session_id, user_message)
# Se veio erro 409, invalida e tenta 1x recriar sessão
if isinstance(agent_resp, dict) and agent_resp.get("_http_status") == 409:
print(f"[agent] 409 recebido. Invalidando sessão {session_key} e recriando...")
_invalidate_session(session_key)
sess2 = _resolve_agent_session(region, agent_endpoint_id)
if "error" in sess2:
return jsonify({
"error": {
"message": f"Agent chat 409 e falha ao recriar sessão: {sess2['error']}",
"type": "agent_session_error",
"code": 409
}
}), 502
session_id = sess2.get("id")
agent_resp = ask_agent(region, agent_endpoint_id, session_id, user_message)
# Se ainda for erro HTTP (qualquer status), retornar erro amigável
if isinstance(agent_resp, dict) and agent_resp.get("_http_status"):
status = agent_resp.get("_http_status")
raw_json = agent_resp.get("_raw_json")
raw_text = agent_resp.get("_raw_text")
msg = None
if isinstance(raw_json, dict):
msg = raw_json.get("message") or raw_json.get("error") or raw_json.get("title")
if not msg:
msg = json.dumps(raw_json, ensure_ascii=False)
if not msg:
msg = raw_text or f"HTTP {status} do Agent."
return jsonify({
"error": {
"message": msg,
"type": "agent_http_error",
"code": status
}
}), 502
model_label = body.get("model") or f"agent:{agent_endpoint_id}"
# streaming
if body.get("stream") is True:
stream_text = _extract_agent_text(agent_resp)
return Response(stream_with_context(sse_chat_stream(model_label, stream_text)),
mimetype="text/event-stream")
# normal
resp = _agent_openai_like_answer(model_label, agent_resp)
resp["_agent"] = {"session_id": session_id, "reused": sess.get("reused", False)}
return jsonify(resp)
@app.route("/genai-agent/<region>/<agent_endpoint_id>/v1/models", methods=["GET"])
def agent_v1_models(region, agent_endpoint_id):
"""
Retorna a lista de modelos suportados para compatibilidade OpenAI/v1 (agents).
Estrutura idêntica à rota /genai/.../v1/models.
"""
supported = get_supported_models() # HOT-RELOAD do JSON externo
data = []
for k, v in supported.items():
data.append({
"id": k,
"object": "model",
"owned_by": "oci.genai.agent",
"ocid": v.get("id"),
"params": v.get("params", {}),
"region": region,
"agent_endpoint": agent_endpoint_id
})
return jsonify({"object": "list", "data": data})
# ========================== # ==========================
# Endpoints OpenAI v1 — FILES # Endpoints OpenAI v1 — FILES
# ========================== # ==========================
@@ -814,7 +1087,7 @@ def v1_images_variations(region=None, compartment_id=None, path_model_id=None):
return jsonify({"created": int(time.time()), "data": [{"url": url, "note": "mock variation"}]}) return jsonify({"created": int(time.time()), "data": [{"url": url, "note": "mock variation"}]})
# ========================== # ==========================
# Endpoint OpenAI v1 /models # Endpoint OpenAI v1 /models (LLMs)
# ========================== # ==========================
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/models", methods=["GET"]) @app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/models", methods=["GET"])