From 70e86757bf1e82a50b0b3efbda117f6067b69cec Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 12 Dec 2025 03:00:53 -0500 Subject: [PATCH] chore(cliproxy): improve config and token management --- logs/main.log | 175 +++++++++++++++++ src/cliproxy/account-manager.ts | 43 ++++ src/web-server/routes.ts | 87 +++++++-- ui/src/components/add-account-dialog.tsx | 112 ++++++----- ui/src/components/quick-setup-wizard.tsx | 239 +++++++++++++++++++---- ui/src/hooks/use-cliproxy.ts | 20 +- ui/src/lib/api-client.ts | 11 +- ui/src/lib/model-catalogs.ts | 118 +++++++++++ ui/src/pages/cliproxy.tsx | 116 +---------- 9 files changed, 702 insertions(+), 219 deletions(-) create mode 100644 logs/main.log create mode 100644 ui/src/lib/model-catalogs.ts diff --git a/logs/main.log b/logs/main.log new file mode 100644 index 00000000..c340cadf --- /dev/null +++ b/logs/main.log @@ -0,0 +1,175 @@ +[2025-12-12 02:50:06] [info] [main.go:413] CLIProxyAPI Version: 6.6.5, Commit: fc054db, BuiltAt: 2025-12-12T00:54:34Z +[2025-12-12 02:50:06] [info] [server.go:466] management routes registered after secret key configuration +[2025-12-12 02:50:06] [info] [watcher.go:1000] full client load complete - 5 clients (5 auth files + 0 Gemini API keys + 0 Vertex API keys + 0 Claude API keys + 0 Codex keys + 0 OpenAI-compat) +[2025-12-12 02:50:06] [info] [service.go:543] file watcher started for config and auth directory changes +[2025-12-12 02:50:06] [info] [service.go:549] core auth auto-refresh started (interval=15m0s) +[2025-12-12 02:50:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:18 | 200 | 0s | 127.0.0.1 | GET "/" +[2025-12-12 02:50:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:18 | 200 | 0s | 127.0.0.1 | GET "/v1/models" +[2025-12-12 02:50:19] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:19 | 200 | 0s | ::1 | GET "/" +[2025-12-12 02:50:19] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:19 | 200 | 0s | ::1 | GET "/" +[2025-12-12 02:50:19] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:19 | 200 | 49ms | ::1 | GET "/v0/management/debug" +[2025-12-12 02:50:19] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:19 | 200 | 50ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 49ms | ::1 | GET "/v0/management/usage" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 0s | ::1 | GET "/v1/models" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 49ms | ::1 | GET "/v0/management/config.yaml" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 49ms | ::1 | GET "/v0/management/auth-files" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:50:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:20 | 200 | 49ms | ::1 | GET "/v0/management/oauth-excluded-models" +[2025-12-12 02:50:32] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:32 | 200 | 0s | 127.0.0.1 | GET "/" +[2025-12-12 02:50:32] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:32 | 200 | 0s | 127.0.0.1 | GET "/v1/models" +[2025-12-12 02:50:42] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:42 | 200 | 2.115s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:43] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:43 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:43] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:43 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:43] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:43 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:43] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:43 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:44 | 200 | 773ms | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:45] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:45 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:45] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:45 | 200 | 1.568s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:45] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:45 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:45] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:50:45 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:50:47] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:47 | 200 | 1.16s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:47] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:47 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:48] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:48 | 200 | 6.774s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:49] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:49 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:50] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:50 | 200 | 7.951s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:50] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:50 | 200 | 0s | ::1 | GET "/" +[2025-12-12 02:50:50] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:50 | 200 | 0s | ::1 | GET "/" +[2025-12-12 02:50:50] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:50 | 200 | 50ms | ::1 | GET "/v0/management/debug" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 49ms | ::1 | GET "/v0/management/usage" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 0s | ::1 | GET "/v1/models" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 53ms | ::1 | GET "/v0/management/auth-files" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 53ms | ::1 | GET "/v0/management/config.yaml" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 49ms | ::1 | GET "/v0/management/oauth-excluded-models" +[2025-12-12 02:50:51] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:51 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:52] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:52 | 200 | 2.247s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:55] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:55 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:50:55] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:55 | 200 | 13.83s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:55] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:55 | 200 | 3.023s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:50:55] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:50:55 | 200 | 10.921s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:00] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:00 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:00] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:00 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:00] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:00 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:00] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:00 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:02] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:02 | 200 | 1.16s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:02] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:02 | 200 | 1.254s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:02] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:02 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:02] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:02 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:02] [error] [gin_logger.go:60] [GIN] 2025/12/12 - 02:51:02 | 501 | 0s | 127.0.0.1 | POST "/api/provider/agy/v1/messages/count_tokens?beta=true" +[2025-12-12 02:51:03] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:03 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:05] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:05 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:06] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:06 | 200 | 3.412s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:06] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:06 | 200 | 8.706s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:07] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:07 | 200 | 6.799s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:08] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:08 | 200 | 10.614s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:09] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:09 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:10] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:10 | 200 | 1.661s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:11] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:11 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:13 | 200 | 3.155s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:16] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:16 | 200 | 8.675s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:17] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:17 | 200 | 1.132s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:17] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:17 | 200 | 1.16s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:17] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:17 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:18 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:18 | 200 | 2.018s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:18 | 200 | 2.137s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:18 | 200 | 2.219s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:19] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:19 | 200 | 2.993s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:20 | 200 | 955ms | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:20] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:20 | 200 | 1.468s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:21] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:21 | 200 | 23.465s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:21] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:21 | 200 | 2.506s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:22] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:22 | 200 | 3.102s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 50ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 50ms | ::1 | GET "/v0/management/usage" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 0s | ::1 | GET "/v1/models" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 50ms | ::1 | GET "/v0/management/config.yaml" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 50ms | ::1 | GET "/v0/management/auth-files" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:51:23] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:23 | 200 | 49ms | ::1 | GET "/v0/management/oauth-excluded-models" +[2025-12-12 02:51:29] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:29 | 200 | 6.533s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:30] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:30 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:30] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:30 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:31] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:31 | 200 | 1.337s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:34 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:35] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:35 | 200 | 5.291s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:36] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:36 | 200 | 1.482s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:38] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:38 | 200 | 2.867s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:40] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:40 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:40] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:40 | 200 | 5.468s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:43 | 200 | 7.802s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:45] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:45 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:47] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:47 | 200 | 4.344s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:48] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:48 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:48] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:48 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:52] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:52 | 200 | 4.022s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:52] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:52 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:51:54] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:54 | 200 | 5.541s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:57] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:57 | 200 | 3.579s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:58] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:58 | 200 | 4.434s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:51:59] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:51:59 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:01] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:01 | 200 | 6.851s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:03] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:03 | 200 | 2.477s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:04] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:04 | 200 | 2.796s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:06] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:06 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:08] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:08 | 200 | 3.953s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:09] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:09 | 200 | 741ms | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:11] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:11 | 200 | 1.983s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:12] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:12 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:12] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:12 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:13 | 200 | 3.488s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:52:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:18 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/usage" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 0s | ::1 | GET "/v1/models" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/config.yaml" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/auth-files" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:52:34] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:34 | 200 | 49ms | ::1 | GET "/v0/management/oauth-excluded-models" +[2025-12-12 02:52:42] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:42 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:43 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:52:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:52:43 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:18] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:18 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:43 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:43 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:53:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:53:43 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:13] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:13 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:43 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:43] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:43 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:54:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:54:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:14 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:14 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:14 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:44 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:55:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:55:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:14 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:14 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:14 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:44] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:44 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:56:57] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:57 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:56:57] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:57 | 200 | 49ms | ::1 | GET "/v0/management/usage" +[2025-12-12 02:56:57] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:57 | 200 | 0s | ::1 | GET "/v1/models" +[2025-12-12 02:56:58] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:58 | 200 | 49ms | ::1 | GET "/v0/management/config.yaml" +[2025-12-12 02:56:58] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:58 | 200 | 49ms | ::1 | GET "/v0/management/auth-files" +[2025-12-12 02:56:58] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:58 | 200 | 49ms | ::1 | GET "/v0/management/config" +[2025-12-12 02:56:58] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:56:58 | 200 | 49ms | ::1 | GET "/v0/management/oauth-excluded-models" +[2025-12-12 02:57:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:14 | 200 | 1ms | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:57:14] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:14 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:57:15] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:15 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:57:16] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:16 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:57:22] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:22 | 200 | 0s | 127.0.0.1 | POST "/api/provider/agy/api/event_logging/batch" +[2025-12-12 02:57:22] [info] [gin_logger.go:64] [GIN] 2025/12/12 - 02:57:22 | 200 | 5.549s | 127.0.0.1 | POST "/api/provider/agy/v1/messages?beta=true" +[2025-12-12 02:57:26] [error] [service.go:613] error stopping API server: failed to shutdown HTTP server: context deadline exceeded +[2025-12-12 02:57:26] [error] [service.go:425] service shutdown returned error: failed to shutdown HTTP server: context deadline exceeded diff --git a/src/cliproxy/account-manager.ts b/src/cliproxy/account-manager.ts index f1b53d71..b1fb5dc7 100644 --- a/src/cliproxy/account-manager.ts +++ b/src/cliproxy/account-manager.ts @@ -130,11 +130,54 @@ export function saveAccountsRegistry(registry: AccountsRegistry): void { }); } +/** + * Sync registry with actual token files + * Removes stale entries where token file no longer exists + * Called automatically when loading accounts + */ +function syncRegistryWithTokenFiles(registry: AccountsRegistry): boolean { + const authDir = getAuthDir(); + let modified = false; + + for (const [_providerName, providerAccounts] of Object.entries(registry.providers)) { + if (!providerAccounts) continue; + + const staleIds: string[] = []; + + for (const [accountId, meta] of Object.entries(providerAccounts.accounts)) { + const tokenPath = path.join(authDir, meta.tokenFile); + if (!fs.existsSync(tokenPath)) { + staleIds.push(accountId); + } + } + + // Remove stale accounts + for (const id of staleIds) { + delete providerAccounts.accounts[id]; + modified = true; + + // Update default if deleted + if (providerAccounts.default === id) { + const remainingIds = Object.keys(providerAccounts.accounts); + providerAccounts.default = remainingIds[0] || 'default'; + } + } + } + + return modified; +} + /** * Get all accounts for a provider */ export function getProviderAccounts(provider: CLIProxyProvider): AccountInfo[] { const registry = loadAccountsRegistry(); + + // Sync with actual token files (removes stale entries) + if (syncRegistryWithTokenFiles(registry)) { + saveAccountsRegistry(registry); + } + const providerAccounts = registry.providers[provider]; if (!providerAccounts) { diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 87f9170f..8108563e 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -11,7 +11,12 @@ import { getCcsDir, getConfigPath, loadConfig, loadSettings } from '../utils/con import { Config, Settings } from '../types/config'; import { expandPath } from '../utils/helpers'; import { runHealthChecks, fixHealthIssue } from './health-service'; -import { getAllAuthStatus, getOAuthConfig, initializeAccounts } from '../cliproxy/auth-handler'; +import { + getAllAuthStatus, + getOAuthConfig, + initializeAccounts, + triggerOAuth, +} from '../cliproxy/auth-handler'; import { fetchCliproxyStats, fetchCliproxyModels, @@ -33,6 +38,7 @@ import { removeAccount as removeAccountFn, } from '../cliproxy/account-manager'; import type { CLIProxyProvider } from '../cliproxy/types'; +import { getClaudeEnvVars } from '../cliproxy/config-generator'; // Unified config imports import { hasUnifiedConfig, @@ -200,12 +206,25 @@ function updateSettingsFile( /** * Helper: Create cliproxy variant settings + * Includes base URL and auth token for proper Claude CLI integration */ -function createCliproxySettings(name: string, model?: string): string { +function createCliproxySettings(name: string, provider: CLIProxyProvider, model?: string): string { const settingsPath = path.join(getCcsDir(), `${name}.settings.json`); + // Get base env vars from provider config (includes BASE_URL, AUTH_TOKEN) + const baseEnv = getClaudeEnvVars(provider); + const settings: Settings = { - env: model ? { ANTHROPIC_MODEL: model } : {}, + env: { + ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '', + ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '', + ANTHROPIC_MODEL: model || (baseEnv.ANTHROPIC_MODEL as string) || '', + ANTHROPIC_DEFAULT_OPUS_MODEL: model || (baseEnv.ANTHROPIC_DEFAULT_OPUS_MODEL as string) || '', + ANTHROPIC_DEFAULT_SONNET_MODEL: + model || (baseEnv.ANTHROPIC_DEFAULT_SONNET_MODEL as string) || '', + ANTHROPIC_DEFAULT_HAIKU_MODEL: + (baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL as string) || model || '', + }, }; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); @@ -356,7 +375,7 @@ apiRoutes.post('/cliproxy', (req: Request, res: Response): void => { } // Create settings file for variant - const settingsPath = createCliproxySettings(name, model); + const settingsPath = createCliproxySettings(name, provider as CLIProxyProvider, model); // Include account if specified (defaults to 'default' if not provided) config.cliproxy[name] = { @@ -534,10 +553,34 @@ apiRoutes.post('/cliproxy/accounts/:provider/default', (req: Request, res: Respo /** * DELETE /api/cliproxy/accounts/:provider/:accountId - Remove an account */ -apiRoutes.delete( - '/api/cliproxy/accounts/:provider/:accountId', - (req: Request, res: Response): void => { - const { provider, accountId } = req.params; +apiRoutes.delete('/cliproxy/accounts/:provider/:accountId', (req: Request, res: Response): void => { + const { provider, accountId } = req.params; + + // Validate provider + const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; + if (!validProviders.includes(provider as CLIProxyProvider)) { + res.status(400).json({ error: `Invalid provider: ${provider}` }); + return; + } + + const success = removeAccountFn(provider as CLIProxyProvider, accountId); + + if (success) { + res.json({ provider, accountId, deleted: true }); + } else { + res.status(404).json({ error: 'Account not found' }); + } +}); + +/** + * POST /api/cliproxy/auth/:provider/start - Start OAuth flow for a provider + * Opens browser for authentication and returns account info when complete + */ +apiRoutes.post( + '/cliproxy/auth/:provider/start', + async (req: Request, res: Response): Promise => { + const { provider } = req.params; + const { nickname } = req.body; // Validate provider const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; @@ -546,12 +589,30 @@ apiRoutes.delete( return; } - const success = removeAccountFn(provider as CLIProxyProvider, accountId); + try { + // Trigger OAuth flow - this opens browser and waits for completion + const account = await triggerOAuth(provider as CLIProxyProvider, { + add: true, // Always add mode from UI + headless: false, // Force interactive mode + nickname: nickname || undefined, + }); - if (success) { - res.json({ provider, accountId, deleted: true }); - } else { - res.status(404).json({ error: 'Account not found' }); + if (account) { + res.json({ + success: true, + account: { + id: account.id, + email: account.email, + nickname: account.nickname, + provider: account.provider, + isDefault: account.isDefault, + }, + }); + } else { + res.status(400).json({ error: 'Authentication failed or was cancelled' }); + } + } catch (error) { + res.status(500).json({ error: (error as Error).message }); } } ); diff --git a/ui/src/components/add-account-dialog.tsx b/ui/src/components/add-account-dialog.tsx index 0fac8fdc..f9a80af7 100644 --- a/ui/src/components/add-account-dialog.tsx +++ b/ui/src/components/add-account-dialog.tsx @@ -1,8 +1,6 @@ /** * Add Account Dialog Component - * Simple dialog to add another OAuth account to a provider - * - * Shows auth command + refresh button (no variant creation) + * Triggers OAuth flow server-side to add another account to a provider */ import { useState } from 'react'; @@ -14,9 +12,10 @@ import { DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; -import { Copy, Check, RefreshCw, Terminal } from 'lucide-react'; -import { useCliproxyAuth } from '@/hooks/use-cliproxy'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Loader2, ExternalLink, User } from 'lucide-react'; +import { useStartAuth } from '@/hooks/use-cliproxy'; interface AddAccountDialogProps { open: boolean; @@ -26,69 +25,82 @@ interface AddAccountDialogProps { } export function AddAccountDialog({ open, onClose, provider, displayName }: AddAccountDialogProps) { - const [copied, setCopied] = useState(false); - const [isRefreshing, setIsRefreshing] = useState(false); - const { refetch } = useCliproxyAuth(); + const [nickname, setNickname] = useState(''); + const startAuthMutation = useStartAuth(); - const authCommand = `ccs ${provider} --auth --add`; - - const copyCommand = async () => { - await navigator.clipboard.writeText(authCommand); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + const handleStartAuth = () => { + startAuthMutation.mutate( + { provider, nickname: nickname.trim() || undefined }, + { + onSuccess: () => { + setNickname(''); + onClose(); + }, + } + ); }; - const handleRefresh = async () => { - setIsRefreshing(true); - await refetch(); - setIsRefreshing(false); - onClose(); + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen && !startAuthMutation.isPending) { + setNickname(''); + onClose(); + } }; return ( - + Add {displayName} Account - Run the command below in your terminal to authenticate a new account + Click the button below to authenticate a new account. A browser window will open for + OAuth.
- - -
- - Run this command: -
-
- - {authCommand} - - -
-
- This will open your browser to authenticate with {displayName} -
-
-
+
+ +
+ + setNickname(e.target.value)} + placeholder="e.g., work, personal" + disabled={startAuthMutation.isPending} + className="flex-1" + /> +
+

+ A friendly name to identify this account. Auto-generated from email if left empty. +

+
-
- -
+ + {startAuthMutation.isPending && ( +

+ Complete the OAuth flow in your browser... +

+ )}
diff --git a/ui/src/components/quick-setup-wizard.tsx b/ui/src/components/quick-setup-wizard.tsx index 573c09d6..ab4e58a5 100644 --- a/ui/src/components/quick-setup-wizard.tsx +++ b/ui/src/components/quick-setup-wizard.tsx @@ -17,6 +17,13 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Copy, Check, @@ -26,9 +33,12 @@ import { Terminal, User, Sparkles, + ExternalLink, + Loader2, } from 'lucide-react'; -import { useCliproxyAuth, useCreateVariant } from '@/hooks/use-cliproxy'; +import { useCliproxyAuth, useCreateVariant, useStartAuth } from '@/hooks/use-cliproxy'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; +import { MODEL_CATALOGS } from '@/lib/model-catalogs'; interface QuickSetupWizardProps { open: boolean; @@ -53,9 +63,11 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { const [modelName, setModelName] = useState(''); const [copied, setCopied] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); + const [isAddingNewAccount, setIsAddingNewAccount] = useState(false); // Track explicit "add account" action const { data: authData, refetch } = useCliproxyAuth(); const createMutation = useCreateVariant(); + const startAuthMutation = useStartAuth(); // Get auth status for selected provider const providerAuth = authData?.authStatus.find( @@ -73,26 +85,22 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { setVariantName(''); setModelName(''); setCopied(false); + setIsAddingNewAccount(false); }, 0); return () => clearTimeout(timer); } }, [open]); // Auto-advance from auth step when account detected - // Use timeout to avoid synchronous setState in effect (React lint rule) + // BUT only if user didn't explicitly click "Add new account" useEffect(() => { - if (step === 'auth' && accounts.length > 0) { + if (step === 'auth' && accounts.length > 0 && !isAddingNewAccount) { const timer = setTimeout(() => { - if (accounts.length === 1) { - setSelectedAccount(accounts[0]); - setStep('variant'); - } else { - setStep('account'); - } + setStep('account'); }, 0); return () => clearTimeout(timer); } - }, [step, accounts]); + }, [step, accounts, isAddingNewAccount]); const copyCommand = async (cmd: string) => { await navigator.clipboard.writeText(cmd); @@ -106,17 +114,32 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { setIsRefreshing(false); }; + const handleStartAuth = () => { + startAuthMutation.mutate( + { provider: selectedProvider }, + { + onSuccess: (data) => { + // Account created, select it and advance to variant step + if (data.account) { + setSelectedAccount(data.account as OAuthAccount); + setStep('variant'); + } + refetch(); // Refresh auth status + }, + } + ); + }; + const handleProviderSelect = (providerId: string) => { setSelectedProvider(providerId); const auth = authData?.authStatus.find((s: AuthStatus) => s.provider === providerId); const provAccounts = auth?.accounts || []; if (provAccounts.length === 0) { + // No accounts - go to auth step to add first account setStep('auth'); - } else if (provAccounts.length === 1) { - setSelectedAccount(provAccounts[0]); - setStep('variant'); } else { + // Has accounts - always show account selection (includes "Add new account" option) setStep('account'); } }; @@ -152,9 +175,36 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { }; const currentProgress = getStepProgress(step); + // Prevent accidental close when user has made progress + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + // Allow closing from success step or provider step (no progress yet) + if (step === 'success' || step === 'provider') { + onClose(); + return; + } + // For other steps, require explicit close via Cancel/Back + // The X button still works, but clicking outside doesn't close + } + }; + return ( - - + + { + // Prevent closing on outside click when user has made progress + if (step !== 'success' && step !== 'provider') { + e.preventDefault(); + } + }} + onEscapeKeyDown={(e) => { + // Prevent ESC close during auth or variant creation + if (startAuthMutation.isPending || createMutation.isPending) { + e.preventDefault(); + } + }} + > @@ -192,6 +242,48 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { {/* Step: Authentication */} {step === 'auth' && (
+ {/* Primary: OAuth Button */} +
+

+ Authenticate with {providers.find((p) => p.id === selectedProvider)?.name} to add + an account +

+ + {startAuthMutation.isPending && ( +

+ Complete the OAuth flow in your browser... +

+ )} +
+ + {/* Divider */} +
+
+ +
+
+ Or use terminal +
+
+ + {/* Secondary: CLI Command */}
@@ -210,21 +302,25 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { )}
-
- This will open your browser to authenticate with{' '} - {providers.find((p) => p.id === selectedProvider)?.name} -
- -
@@ -233,15 +329,23 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) { {/* Step: Account Selection */} {step === 'account' && (
+ {/* Existing accounts header */} +
+ Select an account ({accounts.length}) +
+
{accounts.map((acc: OAuthAccount) => ( ))}
-
- + +
+
@@ -286,30 +421,54 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
- - setModelName(e.target.value)} - placeholder="e.g., gemini-2.5-pro" - /> + + +
+ Default: {MODEL_CATALOGS[selectedProvider]?.defaultModel || 'provider default'} +
- +
+ + +
+

+ Skip if you just wanted to add an account without creating a variant +

)} diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index 8a98b235..5b92eb8f 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -18,7 +18,7 @@ export function useCliproxy() { export function useCliproxyAuth() { return useQuery({ queryKey: ['cliproxy-auth'], - queryFn: () => api.cliproxy.auth(), + queryFn: () => api.cliproxy.getAuthStatus(), }); } @@ -118,6 +118,24 @@ export function useRemoveAccount() { }); } +// OAuth flow hook +export function useStartAuth() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ provider, nickname }: { provider: string; nickname?: string }) => + api.cliproxy.auth.start(provider, nickname), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] }); + queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] }); + toast.success(`Account added for ${variables.provider}`); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} + // Stats and models hooks for Overview tab export function useCliproxyStats() { return useQuery({ diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 0259281b..3419796e 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -172,7 +172,7 @@ export const api = { }, cliproxy: { list: () => request<{ variants: Variant[] }>('/cliproxy'), - auth: () => request<{ authStatus: AuthStatus[] }>('/cliproxy/auth'), + getAuthStatus: () => request<{ authStatus: AuthStatus[] }>('/cliproxy/auth'), create: (data: CreateVariant) => request('/cliproxy', { method: 'POST', @@ -235,6 +235,15 @@ export const api = { remove: (provider: string, accountId: string) => request(`/cliproxy/accounts/${provider}/${accountId}`, { method: 'DELETE' }), }, + // OAuth flow + auth: { + /** Start OAuth flow - opens browser for authentication */ + start: (provider: string, nickname?: string) => + request<{ success: boolean; account: OAuthAccount }>(`/cliproxy/auth/${provider}/start`, { + method: 'POST', + body: JSON.stringify({ nickname }), + }), + }, }, accounts: { list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'), diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts new file mode 100644 index 00000000..7cce9894 --- /dev/null +++ b/ui/src/lib/model-catalogs.ts @@ -0,0 +1,118 @@ +/** + * Model Catalogs for CLIProxy providers + * Shared data for Quick Setup Wizard and Provider Editor + */ + +import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector'; + +/** Model catalog data - mirrors src/cliproxy/model-catalog.ts */ +export const MODEL_CATALOGS: Record = { + agy: { + provider: 'agy', + displayName: 'Antigravity', + defaultModel: 'gemini-claude-opus-4-5-thinking', + models: [ + { + id: 'gemini-claude-opus-4-5-thinking', + name: 'Claude Opus 4.5 Thinking', + description: 'Most capable, extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5-thinking', + name: 'Claude Sonnet 4.5 Thinking', + description: 'Balanced with extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5', + name: 'Claude Sonnet 4.5', + description: 'Fast and capable', + }, + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + description: 'Google latest model via Antigravity', + }, + ], + }, + gemini: { + provider: 'gemini', + displayName: 'Gemini', + defaultModel: 'gemini-2.5-pro', + models: [ + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + tier: 'paid', + description: 'Latest model, requires paid Google account', + }, + { + id: 'gemini-2.5-pro', + name: 'Gemini 2.5 Pro', + description: 'Stable, works with free Google account', + }, + ], + }, + codex: { + provider: 'codex', + displayName: 'Codex', + defaultModel: 'gpt-5.1-codex-max', + models: [ + { + id: 'gpt-5.1-codex-max', + name: 'Codex Max (5.1)', + description: 'Most capable Codex model', + presetMapping: { + default: 'gpt-5.1-codex-max', + opus: 'gpt-5.1-codex-max-high', + sonnet: 'gpt-5.1-codex-max', + haiku: 'gpt-5.1-codex-mini-high', + }, + }, + { + id: 'gpt-5.2', + name: 'GPT 5.2', + description: 'Latest GPT model', + presetMapping: { + default: 'gpt-5.2', + opus: 'gpt-5.2', + sonnet: 'gpt-5.2', + haiku: 'gpt-5.2', + }, + }, + { + id: 'gpt-5.1-codex-mini', + name: 'Codex Mini', + description: 'Fast and efficient Codex model', + }, + ], + }, + qwen: { + provider: 'qwen', + displayName: 'Qwen', + defaultModel: 'qwen-coder-plus', + models: [ + { + id: 'qwen-coder-plus', + name: 'Qwen Coder Plus', + description: 'Alibaba code-focused model', + }, + { + id: 'qwen-max', + name: 'Qwen Max', + description: 'Most capable Qwen model', + }, + ], + }, + iflow: { + provider: 'iflow', + displayName: 'iFlow', + defaultModel: 'iflow-default', + models: [ + { + id: 'iflow-default', + name: 'iFlow Default', + description: 'Default iFlow model', + }, + ], + }, +}; diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index f716fcca..2e0dfa17 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -15,7 +15,6 @@ import { QuickSetupWizard } from '@/components/quick-setup-wizard'; import { AddAccountDialog } from '@/components/add-account-dialog'; import { ProviderEditor } from '@/components/cliproxy/provider-editor'; import { ProviderLogo } from '@/components/cliproxy/provider-logo'; -import type { ProviderCatalog } from '@/components/cliproxy/provider-model-selector'; import { useCliproxy, useCliproxyAuth, @@ -24,120 +23,9 @@ import { useDeleteVariant, } from '@/hooks/use-cliproxy'; import type { AuthStatus, Variant } from '@/lib/api-client'; +import { MODEL_CATALOGS } from '@/lib/model-catalogs'; import { cn } from '@/lib/utils'; -// Model catalog data - mirrors src/cliproxy/model-catalog.ts -const MODEL_CATALOGS: Record = { - agy: { - provider: 'agy', - displayName: 'Antigravity', - defaultModel: 'gemini-claude-opus-4-5-thinking', - models: [ - { - id: 'gemini-claude-opus-4-5-thinking', - name: 'Claude Opus 4.5 Thinking', - description: 'Most capable, extended thinking', - }, - { - id: 'gemini-claude-sonnet-4-5-thinking', - name: 'Claude Sonnet 4.5 Thinking', - description: 'Balanced with extended thinking', - }, - { - id: 'gemini-claude-sonnet-4-5', - name: 'Claude Sonnet 4.5', - description: 'Fast and capable', - }, - { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', - description: 'Google latest model via Antigravity', - }, - ], - }, - gemini: { - provider: 'gemini', - displayName: 'Gemini', - defaultModel: 'gemini-2.5-pro', - models: [ - { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro', - tier: 'paid', - description: 'Latest model, requires paid Google account', - }, - { - id: 'gemini-2.5-pro', - name: 'Gemini 2.5 Pro', - description: 'Stable, works with free Google account', - }, - ], - }, - codex: { - provider: 'codex', - displayName: 'Codex', - defaultModel: 'gpt-5.1-codex-max', - models: [ - { - id: 'gpt-5.1-codex-max', - name: 'Codex Max (5.1)', - description: 'Most capable Codex model', - presetMapping: { - default: 'gpt-5.1-codex-max', - opus: 'gpt-5.1-codex-max-high', - sonnet: 'gpt-5.1-codex-max', - haiku: 'gpt-5.1-codex-mini-high', - }, - }, - { - id: 'gpt-5.2', - name: 'GPT 5.2', - description: 'Latest GPT model', - presetMapping: { - default: 'gpt-5.2', - opus: 'gpt-5.2', - sonnet: 'gpt-5.2', - haiku: 'gpt-5.2', - }, - }, - { - id: 'gpt-5.1-codex-mini', - name: 'Codex Mini', - description: 'Fast and efficient Codex model', - }, - ], - }, - qwen: { - provider: 'qwen', - displayName: 'Qwen', - defaultModel: 'qwen-coder-plus', - models: [ - { - id: 'qwen-coder-plus', - name: 'Qwen Coder Plus', - description: 'Alibaba code-focused model', - }, - { - id: 'qwen-max', - name: 'Qwen Max', - description: 'Most capable Qwen model', - }, - ], - }, - iflow: { - provider: 'iflow', - displayName: 'iFlow', - defaultModel: 'iflow-default', - models: [ - { - id: 'iflow-default', - name: 'iFlow Default', - description: 'Default iFlow model', - }, - ], - }, -}; - // Sidebar provider item function ProviderSidebarItem({ status, @@ -207,7 +95,7 @@ function VariantSidebarItem({ return (