CrossRelay stage 2: Codex /v1/responses relay, config.toml manager, Codex page

This commit is contained in:
kate522 committed 2026-10-18 18:35:01 +08:00
1 parent 404704171d
commit 5112f7ca06
30 files changed
+3539 -72

No files matched your search

+162 -17
View File
@@ -6,6 +6,7 @@ import (
"time"
"gitea.rutua.cn/kate522/CrossRelay/internal/claudecfg"
"gitea.rutua.cn/kate522/CrossRelay/internal/codexcfg"
"gitea.rutua.cn/kate522/CrossRelay/internal/config"
)
@@ -29,12 +30,22 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
"models": p.Models,
}
}
var activeCodex any
if p := s.store.ActiveCodex(); p != nil {
activeCodex = map[string]any{
"id": p.ID,
"name": p.Name,
"base_url": p.BaseURL,
"models": p.Models,
}
}
writeJSON(w, http.StatusOK, map[string]any{
"running": true,
"version": Version,
"port": s.port,
"uptime_sec": int(time.Since(s.started).Seconds()),
"active_claude_provider": active,
"active_codex_provider": activeCodex,
"stats": s.snapshotStats(),
"language": cfg.Language,
"port_mode": cfg.Gateway.PortMode,
@@ -84,10 +95,33 @@ func (s *Server) handleDeleteProvider(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"deleted": id})
}
// handleApply activates a provider and writes it into Claude Code's
// settings.json.
// applyInput selects which client configuration receives the provider.
type applyInput struct {
Target string `json:"target"`
}
// handleApply activates a provider and writes it into the target
// client's configuration. The body is optional; an absent or empty
// target means Claude Code, keeping older clients working.
func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var in applyInput
if r.Body != nil {
_ = decodeJSON(r, &in, 1<<20) // an empty body leaves the zero value
}
switch in.Target {
case "", "claude":
s.applyClaude(w, id)
case "codex":
s.applyCodex(w, id)
default:
writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`)
}
}
// applyClaude activates a provider and writes it into Claude Code's
// settings.json.
func (s *Server) applyClaude(w http.ResponseWriter, id string) {
p, err := s.store.ActivateClaude(id)
if err != nil {
writeAPIError(w, http.StatusNotFound, err.Error())
@@ -109,18 +143,84 @@ func (s *Server) handleApply(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]any{
"provider": p,
"target": "claude",
"env": settings.Env,
})
}
// handleRestore puts back the most recent backup of settings.json.
// applyCodex activates a provider and writes it into Codex's
// config.toml.
func (s *Server) applyCodex(w http.ResponseWriter, id string) {
p, err := s.store.ActivateCodex(id)
if err != nil {
writeAPIError(w, http.StatusNotFound, err.Error())
return
}
mgr, err := s.codexManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
injection, err := mgr.Apply(codexcfg.ApplyOptions{
BaseURL: fmt.Sprintf("http://127.0.0.1:%d", s.port),
Token: s.store.Config().RelayToken,
})
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
// Remember the provider the user had before the first apply so
// restore can put it back instead of dropping the key.
if injection.PriorProvider != "" && s.store.Config().CodexPriorProvider == "" {
if err := s.store.SetCodexPriorProvider(injection.PriorProvider); err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
}
writeJSON(w, http.StatusOK, map[string]any{
"provider": p,
"target": "codex",
"injection": injection,
})
}
// restoreInput selects the client and, optionally, one specific backup
// file to roll back to.
type restoreInput struct {
Target string `json:"target"`
Backup string `json:"backup,omitempty"`
}
// handleRestore puts a client's configuration back. Claude restores
// the newest backup (or the named one); Codex removes the managed
// keys, or rolls back to the named backup.
func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
var in restoreInput
if r.Body != nil {
_ = decodeJSON(r, &in, 1<<20)
}
switch in.Target {
case "", "claude":
s.restoreClaude(w, in.Backup)
case "codex":
s.restoreCodex(w, in.Backup)
default:
writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`)
}
}
func (s *Server) restoreClaude(w http.ResponseWriter, backup string) {
mgr, err := s.claudeManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
name, err := mgr.Restore()
var name string
if backup != "" {
name, err = mgr.RestoreFrom(backup)
} else {
name, err = mgr.Restore()
}
if err != nil {
writeAPIError(w, http.StatusNotFound, err.Error())
return
@@ -129,21 +229,66 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"restored_from": name})
}
func (s *Server) restoreCodex(w http.ResponseWriter, backup string) {
mgr, err := s.codexManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
if backup != "" {
name, err := mgr.RestoreBackup(backup)
if err != nil {
writeAPIError(w, http.StatusNotFound, err.Error())
return
}
_ = s.store.ClearActiveCodex()
writeJSON(w, http.StatusOK, map[string]any{"restored_from": name})
return
}
if err := mgr.RemoveManaged(s.store.Config().CodexPriorProvider); err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
_ = s.store.ClearActiveCodex()
_ = s.store.SetCodexPriorProvider("")
writeJSON(w, http.StatusOK, map[string]any{"restored_from": ""})
}
func (s *Server) handleBackups(w http.ResponseWriter, r *http.Request) {
mgr, err := s.claudeManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
switch target := r.URL.Query().Get("target"); target {
case "", "claude":
mgr, err := s.claudeManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
items, err := mgr.Backups()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
if items == nil {
items = []claudecfg.BackupInfo{}
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
case "codex":
mgr, err := s.codexManager()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
items, err := mgr.Backups()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
if items == nil {
items = []codexcfg.BackupInfo{}
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
default:
writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`)
}
items, err := mgr.Backups()
if err != nil {
writeAPIError(w, http.StatusInternalServerError, err.Error())
return
}
if items == nil {
items = []claudecfg.BackupInfo{}
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) {