feat: separate image profiles in tui
This commit is contained in:
+285
-107
@@ -23,7 +23,7 @@ from typing import Any
|
||||
|
||||
|
||||
APP_NAME = "codex-retry-gateway-tui"
|
||||
FALLBACK_VERSION = "0.1.5"
|
||||
FALLBACK_VERSION = "0.1.6"
|
||||
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
|
||||
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
|
||||
DEFAULT_API_URL = DEFAULT_GATEWAY_URL
|
||||
@@ -537,6 +537,7 @@ def normalize_gateway_url(api_url: str) -> str:
|
||||
"/api/logs",
|
||||
"/api/requests",
|
||||
"/api/profiles",
|
||||
"/api/image-profiles",
|
||||
"/api/config",
|
||||
"/api/restore",
|
||||
):
|
||||
@@ -1261,11 +1262,12 @@ def summary_line(payload: dict[str, Any]) -> str:
|
||||
listen = payload.get("listen") or "-"
|
||||
upstream = config.get("upstream_base_url") or "-"
|
||||
active = config.get("profile_name") or state.get("profile_name") or "-"
|
||||
image_active = config.get("image_profile_name") or state.get("image_profile_name") or "-"
|
||||
request_total = format_count(metrics.get("total_proxy_request_count"))
|
||||
inspected = format_count(metrics.get("inspected_response_count"))
|
||||
matched = format_count(metrics.get("matched_response_count"))
|
||||
reasoning_516 = format_count(metrics.get("reasoning_516_count"))
|
||||
return f"{listen} | profile {active} | upstream {upstream} | req {request_total} | inspected {inspected} | matched {matched} | 516 {reasoning_516}"
|
||||
return f"{listen} | text {active} | image {image_active} | upstream {upstream} | req {request_total} | inspected {inspected} | matched {matched} | 516 {reasoning_516}"
|
||||
|
||||
|
||||
def state_summary(payload: dict[str, Any]) -> str:
|
||||
@@ -1276,6 +1278,7 @@ def state_summary(payload: dict[str, Any]) -> str:
|
||||
f"config {paths.get('config_path') or '-'}",
|
||||
f"requests {paths.get('requests_path') or '-'}",
|
||||
f"profiles {paths.get('profiles_dir') or '-'}",
|
||||
f"image profiles {paths.get('image_profiles_dir') or '-'}",
|
||||
]
|
||||
if state.get("gateway_base_url"):
|
||||
bits.append(f"gateway {state.get('gateway_base_url')}")
|
||||
@@ -1292,7 +1295,7 @@ def status_summary(status_payload: dict[str, Any], status_error: str = "") -> st
|
||||
metrics = status_payload.get("metrics") if isinstance(status_payload.get("metrics"), dict) else {}
|
||||
listen = status_payload.get("listen") or "-"
|
||||
return (
|
||||
f"live {listen} | active {config.get('profile_name') or state.get('profile_name') or '-'} | "
|
||||
f"live {listen} | text {config.get('profile_name') or state.get('profile_name') or '-'} | image {config.get('image_profile_name') or state.get('image_profile_name') or '-'} | "
|
||||
f"516 {metrics.get('reasoning_516_count') or 0} | latest seq {metrics.get('total_proxy_request_count') or 0}"
|
||||
)
|
||||
|
||||
@@ -1493,14 +1496,6 @@ def normalize_profile_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
||||
"auth_file": str(summary.get("auth_file") or ""),
|
||||
"auth_json_path": str(summary.get("auth_json_path") or ""),
|
||||
"auth_json_key": str(summary.get("auth_json_key") or "-"),
|
||||
"image_base_url": str(summary.get("image_base_url") or "-"),
|
||||
"image_auth_mode": str(summary.get("image_auth_mode") or "-"),
|
||||
"image_auth_env": str(summary.get("image_auth_env") or "-"),
|
||||
"image_auth_file": str(summary.get("image_auth_file") or ""),
|
||||
"image_manual_secret_file": str(summary.get("image_manual_secret_file") or ""),
|
||||
"image_auth_json_path": str(summary.get("image_auth_json_path") or ""),
|
||||
"image_auth_json_key": str(summary.get("image_auth_json_key") or "-"),
|
||||
"image_auth_source": str(summary.get("image_auth_source") or "disabled"),
|
||||
"request_history_limit": summary.get("request_history_limit"),
|
||||
"model_remap": str(summary.get("model_remap") or ""),
|
||||
"auth_source": str(summary.get("auth_source") or "-"),
|
||||
@@ -1512,6 +1507,39 @@ def normalize_profile_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
||||
return rows
|
||||
|
||||
|
||||
def normalize_image_profile_rows(payload: dict[str, Any], filter_text: str = "") -> list[dict[str, Any]]:
|
||||
needle = filter_text.strip().lower()
|
||||
rows = []
|
||||
for profile in payload.get("image_profiles") or []:
|
||||
if not isinstance(profile, dict):
|
||||
continue
|
||||
summary = profile.get("summary") if isinstance(profile.get("summary"), dict) else {}
|
||||
haystack = " ".join(
|
||||
str(profile.get(key) or "")
|
||||
for key in ("name", "file_path", "active")
|
||||
).lower() + " " + " ".join(str(summary.get(key) or "") for key in summary.keys()).lower()
|
||||
if needle and needle not in haystack:
|
||||
continue
|
||||
rows.append(
|
||||
{
|
||||
"name": str(profile.get("name") or ""),
|
||||
"active": bool(profile.get("active")),
|
||||
"file_path": str(profile.get("file_path") or ""),
|
||||
"base_url": str(summary.get("base_url") or "-"),
|
||||
"auth_mode": str(summary.get("auth_mode") or "-"),
|
||||
"auth_env": str(summary.get("auth_env") or "-"),
|
||||
"auth_file": str(summary.get("auth_file") or ""),
|
||||
"manual_secret_file": str(summary.get("manual_secret_file") or ""),
|
||||
"auth_json_path": str(summary.get("auth_json_path") or ""),
|
||||
"auth_json_key": str(summary.get("auth_json_key") or "-"),
|
||||
"auth_source": str(summary.get("auth_source") or "disabled"),
|
||||
"raw": profile,
|
||||
}
|
||||
)
|
||||
rows.sort(key=lambda item: (not item["active"], item["name"].lower()))
|
||||
return rows
|
||||
|
||||
|
||||
def render_profile_detail(row: dict[str, Any]) -> str:
|
||||
bits = [
|
||||
f"{row['name']}",
|
||||
@@ -1520,11 +1548,6 @@ def render_profile_detail(row: dict[str, Any]) -> str:
|
||||
f"text upstream {short_text(row['upstream_base_url'], 64)}",
|
||||
f"text auth {row['auth_mode']}/{row['auth_source']}",
|
||||
]
|
||||
if row.get("image_base_url") and row["image_base_url"] != "-":
|
||||
bits.append(f"image upstream {short_text(row['image_base_url'], 64)}")
|
||||
bits.append(f"image auth {row['image_auth_mode']}/{row['image_auth_source']}")
|
||||
else:
|
||||
bits.append("image upstream disabled")
|
||||
if row.get("request_history_limit") is not None:
|
||||
bits.append(f"history {row['request_history_limit']}")
|
||||
if row.get("reasoning_equals"):
|
||||
@@ -1538,6 +1561,20 @@ def render_profile_detail(row: dict[str, Any]) -> str:
|
||||
return " | ".join(bits)
|
||||
|
||||
|
||||
def render_image_profile_detail(row: dict[str, Any]) -> str:
|
||||
bits = [
|
||||
f"image {row['name']}",
|
||||
"active" if row["active"] else "inactive",
|
||||
f"upstream {short_text(row['base_url'], 64)}",
|
||||
f"auth {row['auth_mode']}/{row['auth_source']}",
|
||||
]
|
||||
if row.get("auth_file"):
|
||||
bits.append("auth file configured")
|
||||
if row.get("auth_json_path"):
|
||||
bits.append("auth json configured")
|
||||
return " | ".join(bits)
|
||||
|
||||
|
||||
def profile_form_state(row: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = row.get("raw") if isinstance(row.get("raw"), dict) else {}
|
||||
form = raw.get("form") if isinstance(raw.get("form"), dict) else {}
|
||||
@@ -1554,15 +1591,6 @@ def profile_form_state(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False),
|
||||
"auth_json_path": form.get("auth_json_path") or "",
|
||||
"auth_json_key": form.get("auth_json_key") or row.get("auth_json_key") or "",
|
||||
"image_base_url": form.get("image_base_url") or "",
|
||||
"image_auth_mode": form.get("image_auth_mode") or "fixed_bearer",
|
||||
"image_auth_env": form.get("image_auth_env") or "CODEX_RETRY_GATEWAY_IMAGE_API_KEY",
|
||||
"image_auth_file": form.get("image_auth_file") or "",
|
||||
"image_manual_secret": "",
|
||||
"image_manual_secret_file": form.get("image_manual_secret_file") or "",
|
||||
"image_manual_secret_configured": parse_bool_value(form.get("image_manual_secret_configured"), False),
|
||||
"image_auth_json_path": form.get("image_auth_json_path") or "",
|
||||
"image_auth_json_key": form.get("image_auth_json_key") or "OPENAI_API_KEY",
|
||||
"request_history_limit": parse_int_value(form.get("request_history_limit") or row.get("request_history_limit"))
|
||||
if parse_int_value(form.get("request_history_limit") or row.get("request_history_limit")) is not None
|
||||
else DEFAULT_PROFILE_REQUEST_HISTORY_LIMIT,
|
||||
@@ -1598,15 +1626,6 @@ def profile_payload_from_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"manual_secret_configured": bool(state.get("manual_secret_configured")),
|
||||
"auth_json_path": str(state.get("auth_json_path") or "").strip(),
|
||||
"auth_json_key": str(state.get("auth_json_key") or "").strip(),
|
||||
"image_base_url": str(state.get("image_base_url") or "").strip(),
|
||||
"image_auth_mode": str(state.get("image_auth_mode") or "fixed_bearer").strip(),
|
||||
"image_auth_env": str(state.get("image_auth_env") or "").strip(),
|
||||
"image_auth_file": str(state.get("image_auth_file") or "").strip(),
|
||||
"image_manual_secret": "",
|
||||
"image_manual_secret_file": str(state.get("image_manual_secret_file") or "").strip(),
|
||||
"image_manual_secret_configured": bool(state.get("image_manual_secret_configured")),
|
||||
"image_auth_json_path": str(state.get("image_auth_json_path") or "").strip(),
|
||||
"image_auth_json_key": str(state.get("image_auth_json_key") or "").strip(),
|
||||
"request_history_limit": state.get("request_history_limit"),
|
||||
"model_remap": str(state.get("model_remap") or "").strip(),
|
||||
"reasoning_equals": state.get("reasoning_equals") or [],
|
||||
@@ -1629,8 +1648,6 @@ def profile_editor_document(row: dict[str, Any]) -> str:
|
||||
[
|
||||
"# Edit the selected codex-retry-gateway profile and save.",
|
||||
"# Leave manual_secret empty to keep the current secret file.",
|
||||
"# image_base_url applies to both /images/* and /v1/images/*.",
|
||||
"# Leave image_manual_secret empty to keep the current image secret file.",
|
||||
"# Changing name creates a new profile file; it does not delete the old one.",
|
||||
"",
|
||||
f"name = {toml_string(state.get('name'))}",
|
||||
@@ -1646,17 +1663,6 @@ def profile_editor_document(row: dict[str, Any]) -> str:
|
||||
f"auth_json_path = {toml_string(state.get('auth_json_path'))}",
|
||||
f"auth_json_key = {toml_string(state.get('auth_json_key'))}",
|
||||
"",
|
||||
"# Optional image-specific upstream and authentication.",
|
||||
f"image_base_url = {toml_string(state.get('image_base_url'))}",
|
||||
f"image_auth_mode = {toml_string(state.get('image_auth_mode'))}",
|
||||
f"image_auth_env = {toml_string(state.get('image_auth_env'))}",
|
||||
f"image_auth_file = {toml_string(state.get('image_auth_file'))}",
|
||||
'image_manual_secret = ""',
|
||||
f"image_manual_secret_file = {toml_string(state.get('image_manual_secret_file'))}",
|
||||
f"image_manual_secret_configured = {toml_bool(state.get('image_manual_secret_configured'))}",
|
||||
f"image_auth_json_path = {toml_string(state.get('image_auth_json_path'))}",
|
||||
f"image_auth_json_key = {toml_string(state.get('image_auth_json_key'))}",
|
||||
"",
|
||||
f"request_history_limit = {state.get('request_history_limit') if state.get('request_history_limit') is not None else 0}",
|
||||
f"model_remap = {model_remap_value}",
|
||||
f"reasoning_equals = {toml_int_list(state.get('reasoning_equals'))}",
|
||||
@@ -1687,15 +1693,6 @@ def profile_payload_from_editor_text(text: str) -> dict[str, Any]:
|
||||
"manual_secret_configured": parse_bool_value(data.get("manual_secret_configured"), False),
|
||||
"auth_json_path": str(data.get("auth_json_path") or "").strip(),
|
||||
"auth_json_key": str(data.get("auth_json_key") or "").strip(),
|
||||
"image_base_url": str(data.get("image_base_url") or "").strip(),
|
||||
"image_auth_mode": str(data.get("image_auth_mode") or "fixed_bearer").strip(),
|
||||
"image_auth_env": str(data.get("image_auth_env") or "").strip(),
|
||||
"image_auth_file": str(data.get("image_auth_file") or "").strip(),
|
||||
"image_manual_secret": str(data.get("image_manual_secret") or "").strip(),
|
||||
"image_manual_secret_file": str(data.get("image_manual_secret_file") or "").strip(),
|
||||
"image_manual_secret_configured": parse_bool_value(data.get("image_manual_secret_configured"), False),
|
||||
"image_auth_json_path": str(data.get("image_auth_json_path") or "").strip(),
|
||||
"image_auth_json_key": str(data.get("image_auth_json_key") or "").strip(),
|
||||
"request_history_limit": parse_int_value(data.get("request_history_limit")),
|
||||
"model_remap": str(data.get("model_remap") or "").strip(),
|
||||
"reasoning_equals": normalize_editor_integer_list(data.get("reasoning_equals")),
|
||||
@@ -1744,6 +1741,108 @@ def edit_profile_payload_with_editor(row: dict[str, Any]) -> dict[str, Any] | No
|
||||
pass
|
||||
|
||||
|
||||
def image_profile_form_state(row: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = row.get("raw") if isinstance(row.get("raw"), dict) else {}
|
||||
form = raw.get("form") if isinstance(raw.get("form"), dict) else {}
|
||||
return {
|
||||
"name": row.get("name") or "",
|
||||
"base_url": form.get("base_url") or row.get("base_url") or "",
|
||||
"auth_mode": form.get("auth_mode") or row.get("auth_mode") or "fixed_bearer",
|
||||
"auth_env": form.get("auth_env") or "CODEX_RETRY_GATEWAY_IMAGE_API_KEY",
|
||||
"auth_file": form.get("auth_file") or "",
|
||||
"manual_secret": "",
|
||||
"manual_secret_file": form.get("manual_secret_file") or "",
|
||||
"manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False),
|
||||
"auth_json_path": form.get("auth_json_path") or "",
|
||||
"auth_json_key": form.get("auth_json_key") or "OPENAI_API_KEY",
|
||||
}
|
||||
|
||||
|
||||
def image_profile_payload_from_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
state = image_profile_form_state(row)
|
||||
return {
|
||||
"name": str(state.get("name") or "").strip(),
|
||||
"base_url": str(state.get("base_url") or "").strip(),
|
||||
"auth_mode": str(state.get("auth_mode") or "fixed_bearer").strip(),
|
||||
"auth_env": str(state.get("auth_env") or "").strip(),
|
||||
"auth_file": str(state.get("auth_file") or "").strip(),
|
||||
"manual_secret": "",
|
||||
"manual_secret_file": str(state.get("manual_secret_file") or "").strip(),
|
||||
"manual_secret_configured": bool(state.get("manual_secret_configured")),
|
||||
"auth_json_path": str(state.get("auth_json_path") or "").strip(),
|
||||
"auth_json_key": str(state.get("auth_json_key") or "").strip(),
|
||||
}
|
||||
|
||||
|
||||
def image_profile_editor_document(row: dict[str, Any]) -> str:
|
||||
state = image_profile_form_state(row)
|
||||
return "\n".join(
|
||||
[
|
||||
"# Edit the selected codex-retry-gateway image profile and save.",
|
||||
"# base_url applies to both /images/* and /v1/images/*.",
|
||||
"# Leave manual_secret empty to keep the current image secret file.",
|
||||
"# Changing name creates a new image profile file; it does not delete the old one.",
|
||||
"",
|
||||
f"name = {toml_string(state.get('name'))}",
|
||||
f"base_url = {toml_string(state.get('base_url'))}",
|
||||
f"auth_mode = {toml_string(state.get('auth_mode'))}",
|
||||
f"auth_env = {toml_string(state.get('auth_env'))}",
|
||||
f"auth_file = {toml_string(state.get('auth_file'))}",
|
||||
'manual_secret = ""',
|
||||
f"manual_secret_file = {toml_string(state.get('manual_secret_file'))}",
|
||||
f"manual_secret_configured = {toml_bool(state.get('manual_secret_configured'))}",
|
||||
f"auth_json_path = {toml_string(state.get('auth_json_path'))}",
|
||||
f"auth_json_key = {toml_string(state.get('auth_json_key'))}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def image_profile_payload_from_editor_text(text: str) -> dict[str, Any]:
|
||||
data = tomllib.loads(text)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("editor payload must be a TOML object")
|
||||
return {
|
||||
"name": str(data.get("name") or "").strip(),
|
||||
"base_url": str(data.get("base_url") or "").strip(),
|
||||
"auth_mode": str(data.get("auth_mode") or "fixed_bearer").strip(),
|
||||
"auth_env": str(data.get("auth_env") or "").strip(),
|
||||
"auth_file": str(data.get("auth_file") or "").strip(),
|
||||
"manual_secret": str(data.get("manual_secret") or "").strip(),
|
||||
"manual_secret_file": str(data.get("manual_secret_file") or "").strip(),
|
||||
"manual_secret_configured": parse_bool_value(data.get("manual_secret_configured"), False),
|
||||
"auth_json_path": str(data.get("auth_json_path") or "").strip(),
|
||||
"auth_json_key": str(data.get("auth_json_key") or "").strip(),
|
||||
}
|
||||
|
||||
|
||||
def edit_image_profile_payload_with_editor(row: dict[str, Any]) -> dict[str, Any] | None:
|
||||
original_text = image_profile_editor_document(row)
|
||||
editor_command = resolve_editor_command()
|
||||
fd, raw_path = tempfile.mkstemp(prefix=f"codex-retry-image-profile-{row.get('name') or 'profile'}-", suffix=".toml")
|
||||
os.close(fd)
|
||||
temp_path = Path(raw_path)
|
||||
temp_path.write_text(original_text, encoding="utf-8")
|
||||
try:
|
||||
result = subprocess.run(editor_command + [str(temp_path)], check=False)
|
||||
edited_text = temp_path.read_text(encoding="utf-8")
|
||||
if edited_text == original_text:
|
||||
return None
|
||||
payload = image_profile_payload_from_editor_text(edited_text)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"editor exited with status {result.returncode}")
|
||||
return payload
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"{exc}; kept draft at {temp_path}") from exc
|
||||
finally:
|
||||
if temp_path.exists():
|
||||
try:
|
||||
if temp_path.read_text(encoding="utf-8") == original_text:
|
||||
temp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def edit_request_table_preferences_with_editor(preferences: dict[str, Any]) -> dict[str, Any] | None:
|
||||
original_text = request_table_preferences_document(preferences)
|
||||
editor_command = resolve_editor_command()
|
||||
@@ -1824,12 +1923,13 @@ def fetch_dashboard_snapshot(
|
||||
"logs": build_api_url(gateway_root, "/api/logs", {"limit": 200}),
|
||||
"requests": build_api_url(gateway_root, "/api/requests", {"limit": 200}),
|
||||
"profiles": build_api_url(gateway_root, "/api/profiles"),
|
||||
"image_profiles": build_api_url(gateway_root, "/api/image-profiles"),
|
||||
}
|
||||
|
||||
def fetch_optional(url: str) -> tuple[dict[str, Any], str]:
|
||||
return fetch_optional_payload(url, timeout, access_key)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
with ThreadPoolExecutor(max_workers=5) as pool:
|
||||
futures = {
|
||||
name: pool.submit(fetch_optional, url)
|
||||
for name, url in endpoints.items()
|
||||
@@ -1838,12 +1938,14 @@ def fetch_dashboard_snapshot(
|
||||
logs_payload, logs_error = futures["logs"].result()
|
||||
requests_payload, requests_error = futures["requests"].result()
|
||||
profiles_payload, profiles_error = futures["profiles"].result()
|
||||
image_profiles_payload, image_profiles_error = futures["image_profiles"].result()
|
||||
|
||||
payload = status_payload if isinstance(status_payload, dict) else {}
|
||||
requests_source = requests_payload if isinstance(requests_payload, dict) else {}
|
||||
logs_source = logs_payload if isinstance(logs_payload, dict) else {}
|
||||
profiles_source = profiles_payload if isinstance(profiles_payload, dict) else {}
|
||||
errors = [message for message in (status_error, logs_error, requests_error, profiles_error) if message]
|
||||
image_profiles_source = image_profiles_payload if isinstance(image_profiles_payload, dict) else {}
|
||||
errors = [message for message in (status_error, logs_error, requests_error, profiles_error, image_profiles_error) if message]
|
||||
return {
|
||||
"payload": payload,
|
||||
"status_payload": payload,
|
||||
@@ -1854,6 +1956,10 @@ def fetch_dashboard_snapshot(
|
||||
profiles_source,
|
||||
filter_text if current_view == "profiles" else "",
|
||||
),
|
||||
"image_profiles": normalize_image_profile_rows(
|
||||
image_profiles_source,
|
||||
filter_text if current_view == "profiles" else "",
|
||||
),
|
||||
"active_profile_name": (
|
||||
str(
|
||||
payload.get("config", {}).get("profile_name")
|
||||
@@ -1862,6 +1968,14 @@ def fetch_dashboard_snapshot(
|
||||
or ""
|
||||
).strip()
|
||||
),
|
||||
"active_image_profile_name": (
|
||||
str(
|
||||
payload.get("config", {}).get("image_profile_name")
|
||||
or payload.get("state", {}).get("image_profile_name")
|
||||
or image_profiles_source.get("active_image_profile")
|
||||
or ""
|
||||
).strip()
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1903,6 +2017,7 @@ def run_textual(
|
||||
("2", "show_requests", "Requests"),
|
||||
("3", "show_logs", "Logs"),
|
||||
("4", "show_profiles", "Profiles"),
|
||||
("i", "toggle_profile_kind", "Text/Image"),
|
||||
("m", "toggle_request_table_density", "Wide/Compact"),
|
||||
("c", "edit_request_columns", "Columns"),
|
||||
("shift+left", "scroll_table_left", "Scroll Left"),
|
||||
@@ -1924,8 +2039,10 @@ def run_textual(
|
||||
self.requests: list[dict[str, Any]] = []
|
||||
self.logs: list[dict[str, Any]] = []
|
||||
self.profiles: list[dict[str, Any]] = []
|
||||
self.image_profiles: list[dict[str, Any]] = []
|
||||
self.request_by_key: dict[str, dict[str, Any]] = {}
|
||||
self.profile_by_key: dict[str, dict[str, Any]] = {}
|
||||
self.image_profile_by_key: dict[str, dict[str, Any]] = {}
|
||||
self.log_by_key: dict[str, dict[str, Any]] = {}
|
||||
self.current_view = "overview"
|
||||
self.filter_mode = "requests"
|
||||
@@ -1934,6 +2051,8 @@ def run_textual(
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
self.refresh_generation = 0
|
||||
self.active_profile_name = ""
|
||||
self.active_image_profile_name = ""
|
||||
self.profile_kind = "text"
|
||||
self.visible_tables = {
|
||||
"overview": "requests",
|
||||
"requests": "requests",
|
||||
@@ -1975,18 +2094,7 @@ def run_textual(
|
||||
profiles = self.query_one("#profiles_table", DataTable)
|
||||
profiles.cursor_type = "row"
|
||||
profiles.zebra_stripes = True
|
||||
profiles.add_columns(
|
||||
"Name",
|
||||
"Active",
|
||||
"Listen",
|
||||
"Text Upstream",
|
||||
"Text Auth",
|
||||
"Image Upstream",
|
||||
"Image Auth",
|
||||
"History",
|
||||
"Reasoning",
|
||||
"Source",
|
||||
)
|
||||
self._rebuild_profile_table_columns()
|
||||
|
||||
self._set_view("overview")
|
||||
self.refresh_data(refresh=True)
|
||||
@@ -2002,7 +2110,7 @@ def run_textual(
|
||||
self.query_one("#logs_table").display = active_table == "logs"
|
||||
self.query_one("#profiles_table").display = active_table == "profiles"
|
||||
self.query_one("#controls", Static).update(
|
||||
"views: 1 overview | 2 requests | 3 logs | 4 profiles | / filter | r refresh | m compact/wide | c columns | Shift-Left/Right scroll | e edit | p probe | s switch | w save | d delete | u open upstream"
|
||||
"views: 1 overview | 2 requests | 3 logs | 4 profiles | i text/image | / filter | r refresh | m compact/wide | c columns | Shift-Left/Right scroll | e edit | p probe | s switch | w save | d delete | u open upstream"
|
||||
)
|
||||
self._focus_table()
|
||||
self._update_table_headers()
|
||||
@@ -2019,7 +2127,7 @@ def run_textual(
|
||||
def _update_table_headers(self) -> None:
|
||||
visible_columns = request_table_visible_columns(self.request_table_preferences)
|
||||
counts = (
|
||||
f"requests {len(self.requests)} | logs {len(self.logs)} | profiles {len(self.profiles)} | "
|
||||
f"requests {len(self.requests)} | logs {len(self.logs)} | text profiles {len(self.profiles)} | image profiles {len(self.image_profiles)} | active {self.profile_kind} | "
|
||||
f"req cols {len(visible_columns)}/{len(REQUEST_TABLE_COLUMNS)} | c edit columns"
|
||||
)
|
||||
self.query_one("#tables", Static).update(counts)
|
||||
@@ -2094,6 +2202,35 @@ def run_textual(
|
||||
else:
|
||||
self.request_by_key = {}
|
||||
|
||||
def _profile_rows(self) -> list[dict[str, Any]]:
|
||||
return self.image_profiles if self.profile_kind == "image" else self.profiles
|
||||
|
||||
def _active_profile_name(self) -> str:
|
||||
return self.active_image_profile_name if self.profile_kind == "image" else self.active_profile_name
|
||||
|
||||
def _profile_api_base(self) -> str:
|
||||
return "/api/image-profiles" if self.profile_kind == "image" else "/api/profiles"
|
||||
|
||||
def _profile_kind_label(self) -> str:
|
||||
return "image profile" if self.profile_kind == "image" else "text profile"
|
||||
|
||||
def _rebuild_profile_table_columns(self) -> None:
|
||||
table = self.query_one("#profiles_table", DataTable)
|
||||
table.clear(columns=True)
|
||||
if self.profile_kind == "image":
|
||||
table.add_columns("Name", "Active", "Image Upstream", "Image Auth", "Source")
|
||||
else:
|
||||
table.add_columns(
|
||||
"Name",
|
||||
"Active",
|
||||
"Listen",
|
||||
"Text Upstream",
|
||||
"Text Auth",
|
||||
"History",
|
||||
"Reasoning",
|
||||
"Source",
|
||||
)
|
||||
|
||||
def _current_table_widget(self) -> DataTable | None:
|
||||
table_id = self.visible_tables.get(self.current_view, "requests")
|
||||
if table_id == "logs":
|
||||
@@ -2110,10 +2247,11 @@ def run_textual(
|
||||
|
||||
def _select_profile_row(self, profile_name: str) -> bool:
|
||||
table = self.query_one("#profiles_table", DataTable)
|
||||
if not self.profiles:
|
||||
rows = self._profile_rows()
|
||||
if not rows:
|
||||
return False
|
||||
if profile_name:
|
||||
for index, row in enumerate(self.profiles):
|
||||
for index, row in enumerate(rows):
|
||||
if row["name"] == profile_name:
|
||||
return self._select_table_row(table, index)
|
||||
return self._select_table_row(table, 0)
|
||||
@@ -2130,9 +2268,10 @@ def run_textual(
|
||||
table = self.query_one("#profiles_table", DataTable)
|
||||
if table.cursor_row is None or table.cursor_row < 0:
|
||||
return None
|
||||
if table.cursor_row >= len(self.profiles):
|
||||
rows = self._profile_rows()
|
||||
if table.cursor_row >= len(rows):
|
||||
return None
|
||||
return self.profiles[table.cursor_row]
|
||||
return rows[table.cursor_row]
|
||||
|
||||
def _selected_log_row(self) -> dict[str, Any] | None:
|
||||
table = self.query_one("#logs_table", DataTable)
|
||||
@@ -2180,12 +2319,14 @@ def run_textual(
|
||||
self.requests = result["requests"]
|
||||
self.logs = result["logs"]
|
||||
self.profiles = result["profiles"]
|
||||
self.image_profiles = result["image_profiles"]
|
||||
self.active_profile_name = result.get("active_profile_name") or ""
|
||||
self.active_image_profile_name = result.get("active_image_profile_name") or ""
|
||||
self.last_request_key = selected_request_key
|
||||
self.loading = False
|
||||
self.render_all()
|
||||
if self.current_view == "profiles":
|
||||
if not self._select_profile_row(self.active_profile_name):
|
||||
if not self._select_profile_row(self._active_profile_name()):
|
||||
self._select_profile_row("")
|
||||
version_bits = [version_message] if version_message else []
|
||||
version_bits.append(status_summary(self.payload, self.status_error))
|
||||
@@ -2217,7 +2358,7 @@ def run_textual(
|
||||
elif self.current_view == "profiles":
|
||||
row = self._selected_profile_row()
|
||||
if row:
|
||||
self.render_detail(row, kind="profile")
|
||||
self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile")
|
||||
else:
|
||||
self.query_one("#detail", Static).update("no profiles")
|
||||
|
||||
@@ -2251,31 +2392,46 @@ def run_textual(
|
||||
def render_profiles(self) -> None:
|
||||
table = self.query_one("#profiles_table", DataTable)
|
||||
table.clear()
|
||||
self.profile_by_key = {}
|
||||
for row in self.profiles:
|
||||
rows = self._profile_rows()
|
||||
if self.profile_kind == "image":
|
||||
self.image_profile_by_key = {}
|
||||
else:
|
||||
self.profile_by_key = {}
|
||||
for row in rows:
|
||||
key = row["name"]
|
||||
self.profile_by_key[key] = row
|
||||
table.add_row(
|
||||
row["name"],
|
||||
"yes" if row["active"] else "",
|
||||
f"{row['listen_host']}:{row['listen_port']}",
|
||||
short_text(row["upstream_base_url"], 24),
|
||||
f"{row['auth_mode']}/{row['auth_source']}",
|
||||
short_text(row["image_base_url"], 24),
|
||||
f"{row['image_auth_mode']}/{row['image_auth_source']}",
|
||||
str(row["request_history_limit"] if row["request_history_limit"] is not None else "-"),
|
||||
short_text(row["reasoning_equals"], 12),
|
||||
short_text(row["file_path"], 28),
|
||||
key=key,
|
||||
)
|
||||
if self.current_view == "profiles" and self.active_profile_name:
|
||||
self._select_profile_row(self.active_profile_name)
|
||||
if self.profile_kind == "image":
|
||||
self.image_profile_by_key[key] = row
|
||||
table.add_row(
|
||||
row["name"],
|
||||
"yes" if row["active"] else "",
|
||||
short_text(row["base_url"], 36),
|
||||
f"{row['auth_mode']}/{row['auth_source']}",
|
||||
short_text(row["file_path"], 36),
|
||||
key=key,
|
||||
)
|
||||
else:
|
||||
self.profile_by_key[key] = row
|
||||
table.add_row(
|
||||
row["name"],
|
||||
"yes" if row["active"] else "",
|
||||
f"{row['listen_host']}:{row['listen_port']}",
|
||||
short_text(row["upstream_base_url"], 28),
|
||||
f"{row['auth_mode']}/{row['auth_source']}",
|
||||
str(row["request_history_limit"] if row["request_history_limit"] is not None else "-"),
|
||||
short_text(row["reasoning_equals"], 12),
|
||||
short_text(row["file_path"], 32),
|
||||
key=key,
|
||||
)
|
||||
if self.current_view == "profiles" and self._active_profile_name():
|
||||
self._select_profile_row(self._active_profile_name())
|
||||
|
||||
def render_detail(self, row: dict[str, Any], *, kind: str) -> None:
|
||||
if kind == "request":
|
||||
self.query_one("#detail", Static).update(render_request_detail(row))
|
||||
elif kind == "profile":
|
||||
self.query_one("#detail", Static).update(render_profile_detail(row))
|
||||
elif kind == "image_profile":
|
||||
self.query_one("#detail", Static).update(render_image_profile_detail(row))
|
||||
else:
|
||||
message = str(row.get("message") or "").strip()
|
||||
self.query_one("#detail", Static).update(f"seq {row['seq']} | {short_time(row['at'])} | {message}")
|
||||
@@ -2297,9 +2453,22 @@ def run_textual(
|
||||
|
||||
def action_show_profiles(self) -> None:
|
||||
self._set_view("profiles")
|
||||
if not self._select_profile_row(self.active_profile_name):
|
||||
if not self._select_profile_row(self._active_profile_name()):
|
||||
self._select_profile_row("")
|
||||
|
||||
def action_toggle_profile_kind(self) -> None:
|
||||
self.profile_kind = "image" if self.profile_kind == "text" else "text"
|
||||
self._rebuild_profile_table_columns()
|
||||
self.render_profiles()
|
||||
if self.current_view == "profiles":
|
||||
if not self._select_profile_row(self._active_profile_name()):
|
||||
self._select_profile_row("")
|
||||
row = self._selected_profile_row()
|
||||
if row:
|
||||
self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile")
|
||||
self._update_table_headers()
|
||||
self._set_status(f"profile view {self.profile_kind}")
|
||||
|
||||
def action_newest_request(self) -> None:
|
||||
if self.requests:
|
||||
self.render_detail(self.requests[0], kind="request")
|
||||
@@ -2393,7 +2562,8 @@ def run_textual(
|
||||
row = self._selected_profile_row()
|
||||
if not row:
|
||||
return
|
||||
upstream = str(row.get("upstream_base_url") or "").strip()
|
||||
upstream_value = row.get("base_url") if self.profile_kind == "image" else row.get("upstream_base_url")
|
||||
upstream = str(upstream_value or "").strip()
|
||||
if not upstream or upstream == "-":
|
||||
return
|
||||
open_url(upstream)
|
||||
@@ -2408,11 +2578,15 @@ def run_textual(
|
||||
return
|
||||
try:
|
||||
with self.suspend():
|
||||
payload = edit_profile_payload_with_editor(row)
|
||||
payload = (
|
||||
edit_image_profile_payload_with_editor(row)
|
||||
if self.profile_kind == "image"
|
||||
else edit_profile_payload_with_editor(row)
|
||||
)
|
||||
if payload is None:
|
||||
self._set_status(f"edit {row['name']} cancelled (no changes)")
|
||||
return
|
||||
status, result = post_json(build_api_url(gateway_base_url, "/api/profiles"), timeout, payload, access_key)
|
||||
status, result = post_json(build_api_url(gateway_base_url, self._profile_api_base()), timeout, payload, access_key)
|
||||
self._set_status(f"edit {payload.get('name') or row['name']} -> {status} | {short_text(result, 96)}")
|
||||
self.refresh_data(refresh=True)
|
||||
except Exception as exc:
|
||||
@@ -2424,7 +2598,7 @@ def run_textual(
|
||||
return
|
||||
try:
|
||||
status, payload = post_json(
|
||||
build_api_url(gateway_base_url, "/api/profiles/probe"),
|
||||
build_api_url(gateway_base_url, f"{self._profile_api_base()}/probe"),
|
||||
timeout,
|
||||
{"profile": row["name"]},
|
||||
access_key,
|
||||
@@ -2439,7 +2613,7 @@ def run_textual(
|
||||
return
|
||||
try:
|
||||
status, payload = post_json(
|
||||
build_api_url(gateway_base_url, "/api/profiles/switch"),
|
||||
build_api_url(gateway_base_url, f"{self._profile_api_base()}/switch"),
|
||||
timeout,
|
||||
{"profile": row["name"]},
|
||||
access_key,
|
||||
@@ -2453,9 +2627,9 @@ def run_textual(
|
||||
row = self._selected_profile_row()
|
||||
if not row:
|
||||
return
|
||||
payload = profile_payload_from_row(row)
|
||||
payload = image_profile_payload_from_row(row) if self.profile_kind == "image" else profile_payload_from_row(row)
|
||||
try:
|
||||
status, result = post_json(build_api_url(gateway_base_url, "/api/profiles"), timeout, payload, access_key)
|
||||
status, result = post_json(build_api_url(gateway_base_url, self._profile_api_base()), timeout, payload, access_key)
|
||||
self._set_status(f"save profile -> {status} | {short_text(result, 96)}")
|
||||
self.refresh_data(refresh=True)
|
||||
except Exception as exc:
|
||||
@@ -2466,7 +2640,11 @@ def run_textual(
|
||||
if not row or row["active"]:
|
||||
return
|
||||
try:
|
||||
status, payload = delete_json(build_api_url(gateway_base_url, f"/api/profiles/{urllib.parse.quote(row['name'])}"), timeout, access_key)
|
||||
status, payload = delete_json(
|
||||
build_api_url(gateway_base_url, f"{self._profile_api_base()}/{urllib.parse.quote(row['name'])}"),
|
||||
timeout,
|
||||
access_key,
|
||||
)
|
||||
self._set_status(f"delete {row['name']} -> {status} | {short_text(payload, 96)}")
|
||||
self.refresh_data(refresh=True)
|
||||
except Exception as exc:
|
||||
@@ -2490,9 +2668,9 @@ def run_textual(
|
||||
self.render_detail(row, kind="log")
|
||||
elif event.data_table.id == "profiles_table":
|
||||
key = str(event.row_key.value)
|
||||
row = self.profile_by_key.get(key)
|
||||
row = self.image_profile_by_key.get(key) if self.profile_kind == "image" else self.profile_by_key.get(key)
|
||||
if row:
|
||||
self.render_detail(row, kind="profile")
|
||||
self.render_detail(row, kind="image_profile" if self.profile_kind == "image" else "profile")
|
||||
|
||||
CodexRetryGatewayTui().run()
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user