feat: add in-place config editing, OpenAI GPT-oss models, and Cohere Embed v4.0

- Add PUT endpoints for editing OCI, GenAI, ADB, and MCP configurations
- Add edit buttons in all config tables with inline form pre-fill
- Add OpenAI GPT-oss 120B and 20B models to GenAI catalog
- Add Cohere Embed v4.0 (Multimodal, 1536d) to embedding catalog
- Update OCI list endpoint to return user_ocid and fingerprint for editing
- Add S.editing state management with cancel support
- Bump version to 1.4
This commit is contained in:
nogueiraguh
2026-03-03 19:02:26 -03:00
parent d71ed4e08a
commit 09371cb675
3 changed files with 291 additions and 90 deletions

View File

@@ -58,6 +58,8 @@ GENAI_MODELS = {
"google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC"},
"xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC"},
"xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"},
"openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC"},
"openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC"},
}
EMBEDDING_MODELS = {
@@ -65,6 +67,7 @@ EMBEDDING_MODELS = {
"cohere.embed-multilingual-v3.0": {"name":"Cohere Embed Multilingual v3.0","dims":1024},
"cohere.embed-english-light-v3.0": {"name":"Cohere Embed English Light v3.0","dims":384},
"cohere.embed-multilingual-light-v3.0": {"name":"Cohere Embed Multilingual Light v3.0","dims":384},
"cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536},
}
GENAI_REGIONS = [
@@ -426,8 +429,9 @@ async def save_oci(
@app.get("/api/oci/configs")
async def list_oci(u=Depends(current_user)):
with db() as c:
if u["role"]=="admin": rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs").fetchall()
else: rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,created_at"
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
else: rows=c.execute(f"SELECT {cols} FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall()
return [dict(r) for r in rows]
@app.delete("/api/oci/configs/{cid}")
@@ -441,6 +445,43 @@ async def del_oci(cid: str, u=Depends(require("admin","user"))):
if d.exists(): shutil.rmtree(d)
return {"ok": True}
@app.put("/api/oci/configs/{cid}")
async def update_oci(
cid: str,
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
user_ocid: str = Form(...), fingerprint: str = Form(...),
region: str = Form(...), compartment_id: str = Form(""),
key_passphrase: str = Form(""),
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
u = Depends(require("admin","user"))
):
with db() as c:
existing = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
kp = cdir / "oci_api_key.pem"
if private_key and private_key.filename:
key_bytes = await private_key.read()
kp.write_bytes(key_bytes); kp.chmod(0o600)
key_text = key_bytes.decode("utf-8", errors="ignore")
if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
if public_key and public_key.filename:
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
cfg_file = cdir / "config"
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
if key_passphrase:
cfg_content += f"pass_phrase={key_passphrase}\n"
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
with db() as c:
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=? WHERE id=?",
(tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, compartment_id or None, cid))
_audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}")
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"])
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
@app.post("/api/oci/test/{cid}")
async def test_oci(cid: str, u=Depends(require("admin","user"))):
cp = OCI_DIR / cid / "config"
@@ -595,6 +636,28 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))):
with db() as c: c.execute("DELETE FROM genai_configs WHERE id=?", (gid,))
return {"ok": True}
@app.put("/api/genai/configs/{gid}")
async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin","user"))):
with db() as c:
existing = c.execute("SELECT * FROM genai_configs WHERE id=?", (gid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com"
with db() as c:
if req.is_default:
c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],))
c.execute(
"""UPDATE genai_configs SET name=?,oci_config_id=?,model_id=?,model_ocid=?,compartment_id=?,
genai_region=?,endpoint=?,serving_type=?,dedicated_endpoint_id=?,temperature=?,max_tokens=?,
top_p=?,top_k=?,frequency_penalty=?,presence_penalty=?,is_default=? WHERE id=?""",
(req.name, req.oci_config_id, req.model_id, req.model_ocid,
req.compartment_id, req.genai_region, ep, req.serving_type, req.dedicated_endpoint_id,
req.temperature, req.max_tokens, req.top_p, req.top_k,
req.frequency_penalty, req.presence_penalty, int(req.is_default), gid))
_audit(u["id"], u["username"], "update_genai_config", gid, req.model_id)
_config_log("genai", gid, req.name, "success", "save", f"Modelo atualizado: {req.model_id} ({req.genai_region})", u["id"], u["username"])
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
@app.post("/api/genai/test/{gid}")
async def test_genai(gid: str, u=Depends(require("admin","user"))):
with db() as c:
@@ -878,6 +941,23 @@ async def del_mcp(mid: str, u=Depends(require("admin","user"))):
with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,))
return {"ok": True}
@app.put("/api/mcp/servers/{mid}")
async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))):
with db() as c:
existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
with db() as c:
c.execute(
"UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=? WHERE id=?",
(req.name, req.description, req.server_type, req.command,
json.dumps(req.args) if req.args else None,
json.dumps(req.env_vars) if req.env_vars else None, req.url,
json.dumps(req.tools) if req.tools else None, req.linked_adb_id, mid))
_audit(u["id"], u["username"], "update_mcp", mid, req.name)
_config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"])
return {"id": mid, "name": req.name, "server_type": req.server_type}
@app.put("/api/mcp/servers/{mid}/toggle")
async def toggle_mcp(mid: str, u=Depends(require("admin","user"))):
with db() as c:
@@ -900,18 +980,57 @@ async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("ad
return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id}
# ── ADB Vector DB Config ─────────────────────────────────────────────────────
@app.post("/api/adb/parse-wallet")
async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
"""Extract DSN names from tnsnames.ora inside wallet ZIP (temporary parse, no save)."""
import zipfile, tempfile
try:
data = await wallet.read()
with tempfile.TemporaryDirectory() as tmp:
zp = Path(tmp) / "wallet.zip"
zp.write_bytes(data)
with zipfile.ZipFile(str(zp), 'r') as z:
z.extractall(tmp)
tns = Path(tmp) / "tnsnames.ora"
if not tns.exists():
raise HTTPException(400, "Wallet ZIP não contém tnsnames.ora")
tns_text = tns.read_text(errors="ignore")
dsn_names = re.findall(r'^(\w[\w\-.]*)\s*=', tns_text, re.MULTILINE)
if not dsn_names:
raise HTTPException(400, "Nenhum DSN encontrado no tnsnames.ora")
return {"dsn_names": dsn_names, "files": [n for n in os.listdir(tmp) if n != "wallet.zip"]}
except zipfile.BadZipFile:
raise HTTPException(400, "Arquivo não é um ZIP válido")
@app.post("/api/adb/config")
async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))):
async def save_adb(
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(...), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
):
vid = str(uuid.uuid4())
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
with db() as c:
c.execute(
"INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls,genai_config_id,embedding_model_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(vid, u["id"], req.config_name, req.dsn, req.username, _enc(req.password),
_enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls),
req.genai_config_id, req.embedding_model_id))
_audit(u["id"], u["username"], "save_adb_config", vid, req.config_name)
_config_log("adb", vid, req.config_name, "success", "save", f"Conexão salva: {req.config_name} ({req.dsn})", u["id"], u["username"])
return {"id": vid, "config_name": req.config_name}
(vid, u["id"], config_name, dsn, username, _enc(password),
_enc(wallet_password) if wallet_password else None, table_name, int(use_mtls_bool),
genai_config_id or None, embedding_model_id))
# Auto-save wallet if provided
if wallet and wallet.filename:
import zipfile
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
zp = wdir / "wallet.zip"
zp.write_bytes(await wallet.read())
with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir))
with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid))
_config_log("adb", vid, config_name, "success", "upload", f"Wallet enviada com a conexão", u["id"], u["username"])
_audit(u["id"], u["username"], "save_adb_config", vid, config_name)
_config_log("adb", vid, config_name, "success", "save", f"Conexão salva: {config_name} ({dsn})", u["id"], u["username"])
return {"id": vid, "config_name": config_name}
@app.post("/api/adb/{vid}/upload-wallet")
async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
@@ -967,6 +1086,44 @@ async def del_adb(vid: str, u=Depends(require("admin","user"))):
if d.exists(): shutil.rmtree(d)
return {"ok": True}
@app.put("/api/adb/configs/{vid}")
async def update_adb(
vid: str,
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(""), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-multilingual-v3.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
):
with db() as c:
existing = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
sets = "config_name=?,dsn=?,username=?,table_name=?,use_mtls=?,genai_config_id=?,embedding_model_id=?"
vals = [config_name, dsn, username, table_name, int(use_mtls_bool), genai_config_id or None, embedding_model_id]
if password:
sets += ",password_enc=?"
vals.append(_enc(password))
if wallet_password:
sets += ",wallet_password_enc=?"
vals.append(_enc(wallet_password))
vals.append(vid)
with db() as c:
c.execute(f"UPDATE adb_vector_configs SET {sets} WHERE id=?", vals)
if wallet and wallet.filename:
import zipfile
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
zp = wdir / "wallet.zip"
zp.write_bytes(await wallet.read())
with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir))
with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid))
_config_log("adb", vid, config_name, "success", "upload", "Wallet atualizado", u["id"], u["username"])
_audit(u["id"], u["username"], "update_adb_config", vid, config_name)
_config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"])
return {"id": vid, "config_name": config_name}
@app.post("/api/adb/{vid}/ensure-table")
async def ensure_table(vid: str, u=Depends(require("admin","user"))):
with db() as c: