feat: add codex retry gateway tui client
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
dist/
|
||||
build/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Shujakuin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,73 @@
|
||||
# codex-retry-gateway-tui
|
||||
|
||||
Terminal UI for `codex-retry-gateway`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run once from a public git repo with `uvx`:
|
||||
|
||||
```bash
|
||||
uvx --from git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git codex-retry-gateway-tui
|
||||
```
|
||||
|
||||
Install as a user command:
|
||||
|
||||
```bash
|
||||
uv tool install git+https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui.git
|
||||
codex-retry-gateway-tui --api-url http://127.0.0.1:4610/__codex_retry_gateway
|
||||
```
|
||||
|
||||
Default gateway URL:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:4610/__codex_retry_gateway
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
- `--api-url` / `CODEX_RETRY_GATEWAY_TUI_API_URL`
|
||||
- `--status-url` / `CODEX_RETRY_GATEWAY_TUI_STATUS_URL`
|
||||
- `--save-config`
|
||||
- `--install`
|
||||
- `--refresh-seconds` / `CODEX_RETRY_GATEWAY_TUI_REFRESH_SECONDS`
|
||||
- `--timeout` / `CODEX_RETRY_GATEWAY_TUI_TIMEOUT`
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `CODEX_RETRY_GATEWAY_TUI_API_URL`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_API_URL_FILE`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_STATUS_URL`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_STATUS_URL_FILE`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_VERSION_CHECK_URL`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_VERSION_CHECK_TIMEOUT`
|
||||
- `CODEX_RETRY_GATEWAY_TUI_NO_VERSION_CHECK`
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
cd apps/codex-retry-gateway-tui
|
||||
uv run codex-retry-gateway-tui
|
||||
```
|
||||
|
||||
Views:
|
||||
|
||||
- overview: current gateway status and summary counts
|
||||
- requests: recent requests, request ID, timing, usage, attempts, and `usage_last_updated_at`
|
||||
- logs: recent gateway logs
|
||||
- profiles: saved profiles, active profile default selection, profile actions
|
||||
|
||||
Controls:
|
||||
|
||||
- `1` overview
|
||||
- `2` requests
|
||||
- `3` logs
|
||||
- `4` profiles
|
||||
- `/` filter
|
||||
- `r` refresh
|
||||
- `p` probe selected profile
|
||||
- `s` switch to selected profile
|
||||
- `w` save selected profile snapshot
|
||||
- `d` delete selected inactive profile
|
||||
- `o` open selected profile upstream URL
|
||||
|
||||
This client uses only the public gateway admin API and does not need SSH or secrets.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
[project]
|
||||
name = "codex-retry-gateway-tui"
|
||||
version = "0.1.0"
|
||||
description = "Terminal UI for codex-retry-gateway monitoring and control"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
license = "MIT"
|
||||
license-files = ["LICENSE"]
|
||||
authors = [
|
||||
{ name = "Shujakuin" },
|
||||
]
|
||||
keywords = ["codex", "gateway", "tui", "textual", "client"]
|
||||
dependencies = [
|
||||
"textual>=0.89.1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project.scripts]
|
||||
codex-retry-gateway-tui = "codex_retry_gateway_tui:main"
|
||||
codex-gateway-tui = "codex_retry_gateway_tui:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui"
|
||||
Repository = "https://gitea.shujk.top/shujakuin/codex-retry-gateway-tui"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["codex_retry_gateway_tui"]
|
||||
include-package-data = true
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
def load_module():
|
||||
module_path = Path(__file__).resolve().parents[1] / "codex_retry_gateway_tui.py"
|
||||
spec = importlib.util.spec_from_file_location("codex_retry_gateway_tui", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class CodexRetryGatewayTUITests(unittest.TestCase):
|
||||
def test_summary_line_uses_api_snapshot(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
"listen": "127.0.0.1:4610",
|
||||
"config": {"profile_name": "pc", "upstream_base_url": "https://example.com/v1"},
|
||||
"metrics": {"total_proxy_request_count": 11, "inspected_response_count": 7, "matched_response_count": 2, "reasoning_516_count": 1},
|
||||
}
|
||||
self.assertIn("profile pc", mod.summary_line(payload))
|
||||
self.assertIn("req 11", mod.summary_line(payload))
|
||||
|
||||
def test_request_rows_sort_desc_and_keep_request_id(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
"entries": [
|
||||
{"seq": 1, "request_id": "req_a", "path": "/responses"},
|
||||
{"seq": 2, "request_id": "req_b", "path": "/v1/responses"},
|
||||
]
|
||||
}
|
||||
rows = mod.normalize_request_rows(payload)
|
||||
self.assertEqual([row["request_id"] for row in rows], ["req_b", "req_a"])
|
||||
|
||||
def test_profile_rows_show_active_first(self) -> None:
|
||||
mod = load_module()
|
||||
payload = {
|
||||
"profiles": [
|
||||
{"name": "beta", "active": False, "summary": {"listen_host": "127.0.0.1", "listen_port": 4611, "upstream_base_url": "u1", "auth_mode": "passthrough", "auth_source": "passthrough", "request_history_limit": 10}},
|
||||
{"name": "alpha", "active": True, "summary": {"listen_host": "127.0.0.1", "listen_port": 4610, "upstream_base_url": "u0", "auth_mode": "manual_bearer", "auth_source": "manual_file", "request_history_limit": 20}},
|
||||
]
|
||||
}
|
||||
rows = mod.normalize_profile_rows(payload)
|
||||
self.assertEqual([row["name"] for row in rows], ["alpha", "beta"])
|
||||
|
||||
def test_version_update_message_only_for_newer_versions(self) -> None:
|
||||
mod = load_module()
|
||||
self.assertIn("0.1.0 -> 0.1.1", mod.version_update_message("0.1.1", "0.1.0"))
|
||||
self.assertEqual(mod.version_update_message("0.1.0", "0.1.0"), "")
|
||||
|
||||
def test_gateway_url_helpers_and_request_age(self) -> None:
|
||||
mod = load_module()
|
||||
self.assertEqual(
|
||||
mod.normalize_gateway_url("http://127.0.0.1:4610/__codex_retry_gateway/api/status"),
|
||||
"http://127.0.0.1:4610/__codex_retry_gateway",
|
||||
)
|
||||
self.assertEqual(
|
||||
mod.gateway_status_url("http://127.0.0.1:4610/__codex_retry_gateway"),
|
||||
"http://127.0.0.1:4610/__codex_retry_gateway/api/status",
|
||||
)
|
||||
self.assertIn("updated", mod.render_request_detail({"seq": 1, "request_id": "r", "method": "POST", "path": "/responses", "status_code": 200, "upstream_status_code": 200, "upstream_attempt_count": 1, "first_response_delay_ms": 10, "duration_ms": 20, "request_body_bytes": 3, "response_bytes_received": 4, "stream_chunk_count": 1, "started_at": "2026-06-30T00:00:00Z", "finished_at": "2026-06-30T00:00:01Z", "usage_last_updated_at": "2026-06-30T00:00:01Z"}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,130 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "codex-retry-gateway-tui"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "textual" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "textual", specifier = ">=0.89.1" }]
|
||||
|
||||
[[package]]
|
||||
name = "linkify-it-py"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "uc-micro-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
linkify = [
|
||||
{ name = "linkify-it-py" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "8.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||
{ name = "mdit-py-plugins" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uc-micro-py"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user