feat: preserve image profile configuration
This commit is contained in:
@@ -97,15 +97,12 @@ Controls:
|
|||||||
- `2` requests
|
- `2` requests
|
||||||
- `3` logs
|
- `3` logs
|
||||||
- `4` profiles
|
- `4` profiles
|
||||||
- `,` / `.` select request-table column for visibility/sort actions
|
- `c` edit request-table column visibility, order, and widths
|
||||||
- `v` toggle the selected requests column visible/hidden
|
|
||||||
- `c` sort requests by the selected column
|
|
||||||
- `C` toggle requests sort ascending/descending
|
|
||||||
- `e` edit selected profile in `$VISUAL` / `$EDITOR`
|
- `e` edit selected profile in `$VISUAL` / `$EDITOR`
|
||||||
- `/` filter
|
- `/` filter
|
||||||
- `r` refresh
|
- `r` refresh
|
||||||
- `m` switch request table compact/wide width profile
|
- `m` switch request table compact/wide width profile
|
||||||
- `[` / `]` narrow or widen the requests usage column
|
- `Shift-Left` / `Shift-Right` scroll the active table
|
||||||
- `p` probe selected profile
|
- `p` probe selected profile
|
||||||
- `s` switch to selected profile
|
- `s` switch to selected profile
|
||||||
- `w` save selected profile snapshot
|
- `w` save selected profile snapshot
|
||||||
@@ -114,4 +111,4 @@ Controls:
|
|||||||
|
|
||||||
This client uses only the public gateway admin API and does not need SSH or secrets.
|
This client uses only the public gateway admin API and does not need SSH or secrets.
|
||||||
|
|
||||||
Profile editing uses a temp TOML draft opened in `$VISUAL` or `$EDITOR` and then saves it back through the gateway profiles API.
|
Profile editing uses a temp TOML draft opened in `$VISUAL` or `$EDITOR` and then saves it back through the gateway profiles API. Both the text upstream and optional image upstream are preserved; image routing applies to `/images/*` and `/v1/images/*`. Manual secrets are always blank in the draft, so a blank value keeps the existing secret file.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
APP_NAME = "codex-retry-gateway-tui"
|
APP_NAME = "codex-retry-gateway-tui"
|
||||||
FALLBACK_VERSION = "0.1.4"
|
FALLBACK_VERSION = "0.1.5"
|
||||||
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
|
DEFAULT_GATEWAY_ADMIN_PATH = "/__codex_retry_gateway"
|
||||||
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
|
DEFAULT_GATEWAY_URL = "http://127.0.0.1:4610/__codex_retry_gateway"
|
||||||
DEFAULT_API_URL = DEFAULT_GATEWAY_URL
|
DEFAULT_API_URL = DEFAULT_GATEWAY_URL
|
||||||
@@ -1403,6 +1403,7 @@ def normalize_request_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
|||||||
"error": str(entry.get("error") or ""),
|
"error": str(entry.get("error") or ""),
|
||||||
"upstream_origin": str(entry.get("upstream")["origin"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_origin") or ""),
|
"upstream_origin": str(entry.get("upstream")["origin"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_origin") or ""),
|
||||||
"upstream_path": str(entry.get("upstream")["path"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_path") or ""),
|
"upstream_path": str(entry.get("upstream")["path"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_path") or ""),
|
||||||
|
"upstream_route": str(entry.get("upstream")["route"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_route") or ""),
|
||||||
"upstream_auth_mode": str(entry.get("upstream")["auth_mode"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_mode") or ""),
|
"upstream_auth_mode": str(entry.get("upstream")["auth_mode"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_mode") or ""),
|
||||||
"upstream_auth_source": str(entry.get("upstream")["auth_source"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_source") or ""),
|
"upstream_auth_source": str(entry.get("upstream")["auth_source"] if isinstance(entry.get("upstream"), dict) else entry.get("upstream_auth_source") or ""),
|
||||||
"raw": entry,
|
"raw": entry,
|
||||||
@@ -1456,6 +1457,8 @@ def render_request_detail(row: dict[str, Any]) -> str:
|
|||||||
bits.append(f"origin {row['upstream_origin']}")
|
bits.append(f"origin {row['upstream_origin']}")
|
||||||
if row.get("upstream_path"):
|
if row.get("upstream_path"):
|
||||||
bits.append(f"upstream path {row['upstream_path']}")
|
bits.append(f"upstream path {row['upstream_path']}")
|
||||||
|
if row.get("upstream_route"):
|
||||||
|
bits.append(f"route {row['upstream_route']}")
|
||||||
if row.get("upstream_auth_mode"):
|
if row.get("upstream_auth_mode"):
|
||||||
bits.append(f"auth {row['upstream_auth_mode']}/{row.get('upstream_auth_source') or '-'}")
|
bits.append(f"auth {row['upstream_auth_mode']}/{row.get('upstream_auth_source') or '-'}")
|
||||||
retry_wave = request_retry_wave_summary(row)
|
retry_wave = request_retry_wave_summary(row)
|
||||||
@@ -1490,6 +1493,14 @@ def normalize_profile_rows(payload: dict[str, Any], filter_text: str = "") -> li
|
|||||||
"auth_file": str(summary.get("auth_file") or ""),
|
"auth_file": str(summary.get("auth_file") or ""),
|
||||||
"auth_json_path": str(summary.get("auth_json_path") or ""),
|
"auth_json_path": str(summary.get("auth_json_path") or ""),
|
||||||
"auth_json_key": str(summary.get("auth_json_key") 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"),
|
"request_history_limit": summary.get("request_history_limit"),
|
||||||
"model_remap": str(summary.get("model_remap") or ""),
|
"model_remap": str(summary.get("model_remap") or ""),
|
||||||
"auth_source": str(summary.get("auth_source") or "-"),
|
"auth_source": str(summary.get("auth_source") or "-"),
|
||||||
@@ -1506,9 +1517,14 @@ def render_profile_detail(row: dict[str, Any]) -> str:
|
|||||||
f"{row['name']}",
|
f"{row['name']}",
|
||||||
"active" if row["active"] else "inactive",
|
"active" if row["active"] else "inactive",
|
||||||
f"listen {row['listen_host']}:{row['listen_port']}",
|
f"listen {row['listen_host']}:{row['listen_port']}",
|
||||||
f"upstream {short_text(row['upstream_base_url'], 64)}",
|
f"text upstream {short_text(row['upstream_base_url'], 64)}",
|
||||||
f"auth {row['auth_mode']}/{row['auth_source']}",
|
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:
|
if row.get("request_history_limit") is not None:
|
||||||
bits.append(f"history {row['request_history_limit']}")
|
bits.append(f"history {row['request_history_limit']}")
|
||||||
if row.get("reasoning_equals"):
|
if row.get("reasoning_equals"):
|
||||||
@@ -1538,6 +1554,15 @@ def profile_form_state(row: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False),
|
"manual_secret_configured": parse_bool_value(form.get("manual_secret_configured"), False),
|
||||||
"auth_json_path": form.get("auth_json_path") or "",
|
"auth_json_path": form.get("auth_json_path") or "",
|
||||||
"auth_json_key": form.get("auth_json_key") or row.get("auth_json_key") 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"))
|
"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
|
if parse_int_value(form.get("request_history_limit") or row.get("request_history_limit")) is not None
|
||||||
else DEFAULT_PROFILE_REQUEST_HISTORY_LIMIT,
|
else DEFAULT_PROFILE_REQUEST_HISTORY_LIMIT,
|
||||||
@@ -1573,6 +1598,15 @@ def profile_payload_from_row(row: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"manual_secret_configured": bool(state.get("manual_secret_configured")),
|
"manual_secret_configured": bool(state.get("manual_secret_configured")),
|
||||||
"auth_json_path": str(state.get("auth_json_path") or "").strip(),
|
"auth_json_path": str(state.get("auth_json_path") or "").strip(),
|
||||||
"auth_json_key": str(state.get("auth_json_key") 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"),
|
"request_history_limit": state.get("request_history_limit"),
|
||||||
"model_remap": str(state.get("model_remap") or "").strip(),
|
"model_remap": str(state.get("model_remap") or "").strip(),
|
||||||
"reasoning_equals": state.get("reasoning_equals") or [],
|
"reasoning_equals": state.get("reasoning_equals") or [],
|
||||||
@@ -1595,6 +1629,8 @@ def profile_editor_document(row: dict[str, Any]) -> str:
|
|||||||
[
|
[
|
||||||
"# Edit the selected codex-retry-gateway profile and save.",
|
"# Edit the selected codex-retry-gateway profile and save.",
|
||||||
"# Leave manual_secret empty to keep the current secret file.",
|
"# 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.",
|
"# Changing name creates a new profile file; it does not delete the old one.",
|
||||||
"",
|
"",
|
||||||
f"name = {toml_string(state.get('name'))}",
|
f"name = {toml_string(state.get('name'))}",
|
||||||
@@ -1609,6 +1645,18 @@ def profile_editor_document(row: dict[str, Any]) -> str:
|
|||||||
f"manual_secret_configured = {toml_bool(state.get('manual_secret_configured'))}",
|
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_path = {toml_string(state.get('auth_json_path'))}",
|
||||||
f"auth_json_key = {toml_string(state.get('auth_json_key'))}",
|
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"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"model_remap = {model_remap_value}",
|
||||||
f"reasoning_equals = {toml_int_list(state.get('reasoning_equals'))}",
|
f"reasoning_equals = {toml_int_list(state.get('reasoning_equals'))}",
|
||||||
@@ -1639,6 +1687,15 @@ def profile_payload_from_editor_text(text: str) -> dict[str, Any]:
|
|||||||
"manual_secret_configured": parse_bool_value(data.get("manual_secret_configured"), False),
|
"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_path": str(data.get("auth_json_path") or "").strip(),
|
||||||
"auth_json_key": str(data.get("auth_json_key") 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")),
|
"request_history_limit": parse_int_value(data.get("request_history_limit")),
|
||||||
"model_remap": str(data.get("model_remap") or "").strip(),
|
"model_remap": str(data.get("model_remap") or "").strip(),
|
||||||
"reasoning_equals": normalize_editor_integer_list(data.get("reasoning_equals")),
|
"reasoning_equals": normalize_editor_integer_list(data.get("reasoning_equals")),
|
||||||
@@ -1918,7 +1975,18 @@ def run_textual(
|
|||||||
profiles = self.query_one("#profiles_table", DataTable)
|
profiles = self.query_one("#profiles_table", DataTable)
|
||||||
profiles.cursor_type = "row"
|
profiles.cursor_type = "row"
|
||||||
profiles.zebra_stripes = True
|
profiles.zebra_stripes = True
|
||||||
profiles.add_columns("Name", "Active", "Listen", "Upstream", "Auth", "History", "Reasoning", "Source")
|
profiles.add_columns(
|
||||||
|
"Name",
|
||||||
|
"Active",
|
||||||
|
"Listen",
|
||||||
|
"Text Upstream",
|
||||||
|
"Text Auth",
|
||||||
|
"Image Upstream",
|
||||||
|
"Image Auth",
|
||||||
|
"History",
|
||||||
|
"Reasoning",
|
||||||
|
"Source",
|
||||||
|
)
|
||||||
|
|
||||||
self._set_view("overview")
|
self._set_view("overview")
|
||||||
self.refresh_data(refresh=True)
|
self.refresh_data(refresh=True)
|
||||||
@@ -2193,6 +2261,8 @@ def run_textual(
|
|||||||
f"{row['listen_host']}:{row['listen_port']}",
|
f"{row['listen_host']}:{row['listen_port']}",
|
||||||
short_text(row["upstream_base_url"], 24),
|
short_text(row["upstream_base_url"], 24),
|
||||||
f"{row['auth_mode']}/{row['auth_source']}",
|
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 "-"),
|
str(row["request_history_limit"] if row["request_history_limit"] is not None else "-"),
|
||||||
short_text(row["reasoning_equals"], 12),
|
short_text(row["reasoning_equals"], 12),
|
||||||
short_text(row["file_path"], 28),
|
short_text(row["file_path"], 28),
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "codex-retry-gateway-tui"
|
name = "codex-retry-gateway-tui"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
description = "Terminal UI for codex-retry-gateway monitoring and control"
|
description = "Terminal UI for codex-retry-gateway monitoring and control"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -409,6 +409,11 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
"reasoning_equals": [516, 1034],
|
"reasoning_equals": [516, 1034],
|
||||||
"raw": {
|
"raw": {
|
||||||
"form": {
|
"form": {
|
||||||
|
"image_base_url": "https://images.example/v1",
|
||||||
|
"image_auth_mode": "manual_bearer",
|
||||||
|
"image_manual_secret_file": "/run/secrets/images-token",
|
||||||
|
"image_manual_secret_configured": True,
|
||||||
|
"image_auth_json_key": "IMAGE_API_KEY",
|
||||||
"retryable_error_messages": [
|
"retryable_error_messages": [
|
||||||
"Selected model is at capacity. Please try a different model.",
|
"Selected model is at capacity. Please try a different model.",
|
||||||
"stream disconnected before completion: Concurrency limit exceeded for account, please retry later",
|
"stream disconnected before completion: Concurrency limit exceeded for account, please retry later",
|
||||||
@@ -419,6 +424,17 @@ class CodexRetryGatewayTUITests(unittest.TestCase):
|
|||||||
text = mod.profile_editor_document(row)
|
text = mod.profile_editor_document(row)
|
||||||
payload = mod.profile_payload_from_editor_text(text)
|
payload = mod.profile_payload_from_editor_text(text)
|
||||||
self.assertEqual(payload["name"], "pc")
|
self.assertEqual(payload["name"], "pc")
|
||||||
|
self.assertEqual(payload["image_base_url"], "https://images.example/v1")
|
||||||
|
self.assertEqual(payload["image_auth_mode"], "manual_bearer")
|
||||||
|
self.assertEqual(payload["image_manual_secret_file"], "/run/secrets/images-token")
|
||||||
|
self.assertTrue(payload["image_manual_secret_configured"])
|
||||||
|
self.assertEqual(payload["image_manual_secret"], "")
|
||||||
|
self.assertNotIn("test-image-profile-secret", text)
|
||||||
|
saved_payload = mod.profile_payload_from_row(row)
|
||||||
|
self.assertEqual(saved_payload["image_base_url"], "https://images.example/v1")
|
||||||
|
self.assertEqual(saved_payload["image_auth_mode"], "manual_bearer")
|
||||||
|
self.assertEqual(saved_payload["image_manual_secret_file"], "/run/secrets/images-token")
|
||||||
|
self.assertEqual(saved_payload["image_manual_secret"], "")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"stream disconnected before completion: Concurrency limit exceeded for account, please retry later",
|
"stream disconnected before completion: Concurrency limit exceeded for account, please retry later",
|
||||||
payload["retryable_error_messages"],
|
payload["retryable_error_messages"],
|
||||||
|
|||||||
Reference in New Issue
Block a user