chore(cliproxy): improve config and token management

This commit is contained in:
kaitranntt
2025-12-12 03:00:53 -05:00
parent 92b7065e10
commit 70e86757bf
9 changed files with 702 additions and 219 deletions
+175
View File
@@ -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
+43
View File
@@ -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) {
+74 -13
View File
@@ -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<void> => {
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 });
}
}
);
+62 -50
View File
@@ -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 (
<Dialog open={open} onOpenChange={onClose}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add {displayName} Account</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<Card>
<CardContent className="p-4 space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Terminal className="w-4 h-4" />
Run this command:
</div>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm">
{authCommand}
</code>
<Button variant="outline" size="icon" onClick={copyCommand}>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
</div>
<div className="text-xs text-muted-foreground">
This will open your browser to authenticate with {displayName}
</div>
</CardContent>
</Card>
<div className="space-y-2">
<Label htmlFor="nickname">Nickname (optional)</Label>
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-muted-foreground" />
<Input
id="nickname"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
placeholder="e.g., work, personal"
disabled={startAuthMutation.isPending}
className="flex-1"
/>
</div>
<p className="text-xs text-muted-foreground">
A friendly name to identify this account. Auto-generated from email if left empty.
</p>
</div>
<div className="flex items-center justify-end gap-2">
<Button variant="ghost" onClick={onClose}>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" onClick={onClose} disabled={startAuthMutation.isPending}>
Cancel
</Button>
<Button onClick={handleRefresh} disabled={isRefreshing}>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Checking...' : 'I ran the command'}
<Button onClick={handleStartAuth} disabled={startAuthMutation.isPending}>
{startAuthMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Authenticating...
</>
) : (
<>
<ExternalLink className="w-4 h-4 mr-2" />
Authenticate
</>
)}
</Button>
</div>
{startAuthMutation.isPending && (
<p className="text-sm text-center text-muted-foreground">
Complete the OAuth flow in your browser...
</p>
)}
</div>
</DialogContent>
</Dialog>
+199 -40
View File
@@ -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 (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-lg">
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
className="sm:max-w-lg"
onPointerDownOutside={(e) => {
// 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();
}
}}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-primary" />
@@ -192,6 +242,48 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
{/* Step: Authentication */}
{step === 'auth' && (
<div className="space-y-4">
{/* Primary: OAuth Button */}
<div className="text-center space-y-3">
<p className="text-sm text-muted-foreground">
Authenticate with {providers.find((p) => p.id === selectedProvider)?.name} to add
an account
</p>
<Button
onClick={handleStartAuth}
disabled={startAuthMutation.isPending}
className="w-full gap-2"
size="lg"
>
{startAuthMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Authenticating...
</>
) : (
<>
<ExternalLink className="w-4 h-4" />
Authenticate in Browser
</>
)}
</Button>
{startAuthMutation.isPending && (
<p className="text-xs text-muted-foreground">
Complete the OAuth flow in your browser...
</p>
)}
</div>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or use terminal</span>
</div>
</div>
{/* Secondary: CLI Command */}
<Card>
<CardContent className="p-4 space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -210,21 +302,25 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
)}
</Button>
</div>
<div className="text-xs text-muted-foreground">
This will open your browser to authenticate with{' '}
{providers.find((p) => p.id === selectedProvider)?.name}
</div>
</CardContent>
</Card>
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setStep('provider')}>
<Button
variant="ghost"
onClick={() => setStep('provider')}
disabled={startAuthMutation.isPending}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
<Button onClick={handleRefresh} disabled={isRefreshing}>
<Button
variant="outline"
onClick={handleRefresh}
disabled={isRefreshing || startAuthMutation.isPending}
>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Checking...' : 'I ran the command'}
{isRefreshing ? 'Checking...' : 'Refresh Status'}
</Button>
</div>
</div>
@@ -233,15 +329,23 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
{/* Step: Account Selection */}
{step === 'account' && (
<div className="space-y-4">
{/* Existing accounts header */}
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Select an account ({accounts.length})
</div>
<div className="grid gap-2">
{accounts.map((acc: OAuthAccount) => (
<button
key={acc.id}
type="button"
onClick={() => handleAccountSelect(acc)}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors text-left"
>
<div className="flex items-center gap-3">
<User className="w-5 h-5 text-muted-foreground" />
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
<User className="w-4 h-4 text-muted-foreground" />
</div>
<div>
<div className="font-medium">{acc.email || acc.id}</div>
{acc.isDefault && (
@@ -253,10 +357,41 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
</button>
))}
</div>
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={() => setStep('auth')}>
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or</span>
</div>
</div>
{/* Add new account button - more prominent */}
<button
type="button"
className="w-full flex items-center gap-3 p-3 border-2 border-dashed border-primary/50 rounded-lg hover:border-primary hover:bg-primary/5 transition-colors text-left"
onClick={() => {
setIsAddingNewAccount(true);
setStep('auth');
}}
>
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0">
<ExternalLink className="w-4 h-4 text-primary" />
</div>
<div>
<div className="font-medium text-primary">Add new account</div>
<div className="text-xs text-muted-foreground">
Authenticate with a different account
</div>
</div>
</button>
<div className="flex items-center justify-between pt-2">
<Button variant="ghost" onClick={() => setStep('provider')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Add different account
Back
</Button>
</div>
</div>
@@ -286,30 +421,54 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
</div>
<div className="space-y-2">
<Label htmlFor="model-name">Model (optional)</Label>
<Input
id="model-name"
value={modelName}
onChange={(e) => setModelName(e.target.value)}
placeholder="e.g., gemini-2.5-pro"
/>
<Label>Model</Label>
<Select value={modelName} onValueChange={setModelName}>
<SelectTrigger>
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
{MODEL_CATALOGS[selectedProvider]?.models.map((m) => (
<SelectItem key={m.id} value={m.id}>
<div className="flex items-center gap-2">
<span>{m.name}</span>
{m.description && (
<span className="text-xs text-muted-foreground">
- {m.description}
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<div className="text-xs text-muted-foreground">
Default: {MODEL_CATALOGS[selectedProvider]?.defaultModel || 'provider default'}
</div>
</div>
<div className="flex items-center justify-between pt-2">
<Button
variant="ghost"
onClick={() => (accounts.length > 1 ? setStep('account') : setStep('auth'))}
onClick={() => (accounts.length > 0 ? setStep('account') : setStep('provider'))}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back
</Button>
<Button
onClick={handleCreateVariant}
disabled={!variantName || createMutation.isPending}
>
{createMutation.isPending ? 'Creating...' : 'Create Variant'}
</Button>
<div className="flex items-center gap-2">
<Button variant="ghost" onClick={onClose}>
Skip
</Button>
<Button
onClick={handleCreateVariant}
disabled={!variantName || createMutation.isPending}
>
{createMutation.isPending ? 'Creating...' : 'Create Variant'}
</Button>
</div>
</div>
<p className="text-xs text-center text-muted-foreground">
Skip if you just wanted to add an account without creating a variant
</p>
</div>
)}
+19 -1
View File
@@ -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({
+10 -1
View File
@@ -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'),
+118
View File
@@ -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<string, ProviderCatalog> = {
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',
},
],
},
};
+2 -114
View File
@@ -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<string, ProviderCatalog> = {
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 (
<button
className={cn(
'w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors cursor-pointer text-left pl-6',
'group w-full flex items-center gap-3 px-3 py-2 rounded-lg transition-colors cursor-pointer text-left pl-6',
isSelected
? 'bg-primary/10 border border-primary/20'
: 'hover:bg-muted border border-transparent'