Spaces:
Running
Running
File size: 9,699 Bytes
457b8fd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# individual_endpoint_dialog.py
"""
Individual Endpoint Configuration Dialog for Glossarion
- Uses the application's WindowManager for consistent UI
- Allows enabling/disabling per-key custom endpoint (e.g., Azure, Ollama/local OpenAI-compatible)
- Persists changes to the in-memory key object and refreshes the parent list
"""
import tkinter as tk
from tkinter import ttk, messagebox
import ttkbootstrap as tb
from typing import Callable
try:
# For type hints only; not required at runtime
from multi_api_key_manager import APIKeyEntry # noqa: F401
except Exception:
pass
class IndividualEndpointDialog:
def __init__(self, parent, translator_gui, key, refresh_callback: Callable[[], None], status_callback: Callable[[str], None]):
self.parent = parent
self.translator_gui = translator_gui
self.key = key
self.refresh_callback = refresh_callback
self.status_callback = status_callback
self.dialog = None
self.canvas = None
self._build()
def _build(self):
title = f"Configure Individual Endpoint — {getattr(self.key, 'model', '')}"
if hasattr(self.translator_gui, 'wm'):
# Use WindowManager scrollable dialog for consistency
self.dialog, scrollable_frame, self.canvas = self.translator_gui.wm.setup_scrollable(
self.parent,
title,
width=700,
height=420,
max_width_ratio=0.85,
max_height_ratio=0.45
)
else:
self.dialog = tk.Toplevel(self.parent)
self.dialog.title(title)
self.dialog.geometry("700x420")
scrollable_frame = self.dialog
main = tk.Frame(scrollable_frame, padx=20, pady=16)
main.pack(fill=tk.BOTH, expand=True)
# Header
header = tk.Frame(main)
header.pack(fill=tk.X, pady=(0, 10))
tk.Label(header, text="Per-Key Custom Endpoint", font=("TkDefaultFont", 14, "bold")).pack(side=tk.LEFT)
# Enable toggle
self.enable_var = tk.BooleanVar(value=bool(getattr(self.key, 'use_individual_endpoint', False)))
tb.Checkbutton(header, text="Enable", variable=self.enable_var, bootstyle="round-toggle",
command=self._toggle_fields).pack(side=tk.RIGHT)
# Description
desc = (
"Use a custom endpoint for this API key only. Works with OpenAI-compatible servers\n"
"like Azure OpenAI or local providers (e.g., Ollama at http://localhost:11434/v1)."
)
tk.Label(main, text=desc, fg='gray', justify=tk.LEFT).pack(anchor=tk.W)
# Form
form = tk.LabelFrame(main, text="Endpoint Settings", padx=14, pady=12)
form.pack(fill=tk.BOTH, expand=False, pady=(10, 0))
# Endpoint URL
tk.Label(form, text="Endpoint Base URL:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10), pady=6)
self.endpoint_var = tk.StringVar(value=getattr(self.key, 'azure_endpoint', '') or '')
self.endpoint_entry = tb.Entry(form, textvariable=self.endpoint_var)
self.endpoint_entry.grid(row=0, column=1, sticky=tk.EW, pady=6)
# Azure API version (optional; required if using Azure)
tk.Label(form, text="Azure API Version:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10), pady=6)
self.api_version_var = tk.StringVar(value=getattr(self.key, 'azure_api_version', '2025-01-01-preview') or '2025-01-01-preview')
self.api_version_combo = ttk.Combobox(
form,
textvariable=self.api_version_var,
values=[
'2025-01-01-preview',
'2024-12-01-preview',
'2024-10-01-preview',
'2024-08-01-preview',
'2024-06-01',
'2024-02-01',
'2023-12-01-preview'
],
width=24,
state='readonly'
)
self.api_version_combo.grid(row=1, column=1, sticky=tk.W, pady=6)
# Helper text
hint = (
"Hints:\n"
"- Ollama: http://localhost:11434/v1\n"
"- Azure OpenAI: https://<resource>.openai.azure.com/ (version required)\n"
"- Other OpenAI-compatible: Provide the base URL ending with /v1 if applicable"
)
tk.Label(form, text=hint, fg='gray', justify=tk.LEFT, font=('TkDefaultFont', 9)).grid(
row=2, column=0, columnspan=2, sticky=tk.W, pady=(4, 0)
)
# Grid weights
form.columnconfigure(1, weight=1)
# Buttons
btns = tk.Frame(main)
btns.pack(fill=tk.X, pady=(14, 0))
tb.Button(btns, text="Save", bootstyle="success", command=self._on_save).pack(side=tk.RIGHT)
tb.Button(btns, text="Cancel", bootstyle="secondary", command=self._on_close).pack(side=tk.RIGHT, padx=(0, 8))
tb.Button(btns, text="Disable", bootstyle="danger-outline", command=self._on_disable).pack(side=tk.LEFT)
# Initial toggle state
self._toggle_fields()
# Window close protocol
self.dialog.protocol("WM_DELETE_WINDOW", self._on_close)
# Auto-size with WM if available
if hasattr(self.translator_gui, 'wm') and self.canvas is not None:
self.translator_gui.wm.auto_resize_dialog(self.dialog, self.canvas, max_width_ratio=0.9, max_height_ratio=0.45)
def _toggle_fields(self):
enabled = self.enable_var.get()
state = tk.NORMAL if enabled else tk.DISABLED
self.endpoint_entry.config(state=state)
# API version is only relevant for Azure but we leave it enabled while toggle is on
self.api_version_combo.config(state='readonly' if enabled else 'disabled')
def _is_azure_endpoint(self, url: str) -> bool:
if not url:
return False
url_l = url.lower()
return (".openai.azure.com" in url_l) or ("azure.com/openai" in url_l) or ("/openai/deployments/" in url_l)
def _validate(self) -> bool:
if not self.enable_var.get():
return True
url = (self.endpoint_var.get() or '').strip()
if not url:
messagebox.showerror("Validation Error", "Endpoint Base URL is required when Enable is ON.")
return False
if not (url.startswith("http://") or url.startswith("https://")):
messagebox.showerror("Validation Error", "Endpoint URL must start with http:// or https://")
return False
if self._is_azure_endpoint(url):
ver = (self.api_version_var.get() or '').strip()
if not ver:
messagebox.showerror("Validation Error", "Azure API Version is required for Azure endpoints.")
return False
return True
def _persist_to_config_if_possible(self):
"""Best-effort persistence: update translator_gui.config['multi_api_keys'] for this key entry.
We match by api_key and model to find the entry. If not found, skip silently.
"""
try:
cfg = getattr(self.translator_gui, 'config', None)
if not isinstance(cfg, dict):
return
key_list = cfg.get('multi_api_keys', [])
# Find by api_key AND model (best-effort)
api_key = getattr(self.key, 'api_key', None)
model = getattr(self.key, 'model', None)
for entry in key_list:
if entry.get('api_key') == api_key and entry.get('model') == model:
entry['use_individual_endpoint'] = bool(getattr(self.key, 'use_individual_endpoint', False))
entry['azure_endpoint'] = getattr(self.key, 'azure_endpoint', None)
entry['azure_api_version'] = getattr(self.key, 'azure_api_version', None)
break
# Save without message
if hasattr(self.translator_gui, 'save_config'):
self.translator_gui.save_config(show_message=False)
except Exception:
# Non-fatal
pass
def _on_save(self):
if not self._validate():
return
enabled = self.enable_var.get()
url = (self.endpoint_var.get() or '').strip()
ver = (self.api_version_var.get() or '').strip()
# Apply to key object
self.key.use_individual_endpoint = enabled
self.key.azure_endpoint = url if enabled else None
# Keep API version even if disabled, but it's only used when enabled
self.key.azure_api_version = ver or getattr(self.key, 'azure_api_version', '2025-01-01-preview')
# Notify parent UI
if callable(self.refresh_callback):
try:
self.refresh_callback()
except Exception:
pass
if callable(self.status_callback):
try:
if enabled and url:
self.status_callback(f"Individual endpoint set: {url}")
else:
self.status_callback("Individual endpoint disabled")
except Exception:
pass
# Best-effort persistence to config
self._persist_to_config_if_possible()
self.dialog.destroy()
def _on_disable(self):
# Disable quickly
self.enable_var.set(False)
self._toggle_fields()
# Apply immediately and close
self._on_save()
def _on_close(self):
self.dialog.destroy()
|