Skip to content

Commit e42187a

Browse files
committed
fix: stabilize codex config management
1 parent 75bb455 commit e42187a

4 files changed

Lines changed: 142 additions & 16 deletions

File tree

internal/api/handlers/management/config_lists.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,7 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
960960
type codexKeyPatch struct {
961961
APIKey *string `json:"api-key"`
962962
APIKeyEntries *[]config.CodexAPIKeyEntry `json:"api-key-entries"`
963+
Name *string `json:"name"`
963964
Prefix *string `json:"prefix"`
964965
BaseURL *string `json:"base-url"`
965966
ProxyURL *string `json:"proxy-url"`
@@ -1004,6 +1005,9 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
10041005
if body.Value.APIKeyEntries != nil {
10051006
entry.APIKeyEntries = append([]config.CodexAPIKeyEntry(nil), (*body.Value.APIKeyEntries)...)
10061007
}
1008+
if body.Value.Name != nil {
1009+
entry.Name = strings.TrimSpace(*body.Value.Name)
1010+
}
10071011
if body.Value.Prefix != nil {
10081012
entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
10091013
}
@@ -1038,17 +1042,38 @@ func (h *Handler) PatchCodexKey(c *gin.Context) {
10381042
func (h *Handler) DeleteCodexKey(c *gin.Context) {
10391043
h.mu.Lock()
10401044
defer h.mu.Unlock()
1045+
if idxStr := c.Query("index"); idxStr != "" {
1046+
var idx int
1047+
_, err := fmt.Sscanf(idxStr, "%d", &idx)
1048+
if err == nil && idx >= 0 && idx < len(h.cfg.CodexKey) {
1049+
h.cfg.CodexKey = append(h.cfg.CodexKey[:idx], h.cfg.CodexKey[idx+1:]...)
1050+
h.cfg.SanitizeCodexKeys()
1051+
h.persistLocked(c)
1052+
return
1053+
}
1054+
c.JSON(400, gin.H{"error": "invalid index"})
1055+
return
1056+
}
10411057
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
10421058
if baseRaw, okBase := c.GetQuery("base-url"); okBase {
10431059
base := strings.TrimSpace(baseRaw)
1044-
out := make([]config.CodexKey, 0, len(h.cfg.CodexKey))
1045-
for _, v := range h.cfg.CodexKey {
1046-
if codexKeyContainsAPIKey(v, val) && strings.TrimSpace(v.BaseURL) == base {
1047-
continue
1060+
matchIndex := -1
1061+
matchCount := 0
1062+
for i := range h.cfg.CodexKey {
1063+
if codexKeyContainsAPIKey(h.cfg.CodexKey[i], val) && strings.TrimSpace(h.cfg.CodexKey[i].BaseURL) == base {
1064+
matchCount++
1065+
if matchIndex == -1 {
1066+
matchIndex = i
1067+
}
10481068
}
1049-
out = append(out, v)
10501069
}
1051-
h.cfg.CodexKey = out
1070+
if matchCount > 1 {
1071+
c.JSON(400, gin.H{"error": "multiple items match api-key and base-url; index is required"})
1072+
return
1073+
}
1074+
if matchIndex != -1 {
1075+
h.cfg.CodexKey = append(h.cfg.CodexKey[:matchIndex], h.cfg.CodexKey[matchIndex+1:]...)
1076+
}
10521077
h.cfg.SanitizeCodexKeys()
10531078
h.persistLocked(c)
10541079
return
@@ -1075,16 +1100,6 @@ func (h *Handler) DeleteCodexKey(c *gin.Context) {
10751100
h.persistLocked(c)
10761101
return
10771102
}
1078-
if idxStr := c.Query("index"); idxStr != "" {
1079-
var idx int
1080-
_, err := fmt.Sscanf(idxStr, "%d", &idx)
1081-
if err == nil && idx >= 0 && idx < len(h.cfg.CodexKey) {
1082-
h.cfg.CodexKey = append(h.cfg.CodexKey[:idx], h.cfg.CodexKey[idx+1:]...)
1083-
h.cfg.SanitizeCodexKeys()
1084-
h.persistLocked(c)
1085-
return
1086-
}
1087-
}
10881103
c.JSON(400, gin.H{"error": "missing api-key or index"})
10891104
}
10901105

@@ -1164,6 +1179,7 @@ func normalizeCodexKey(entry *config.CodexKey) {
11641179
return
11651180
}
11661181
entry.APIKey = strings.TrimSpace(entry.APIKey)
1182+
entry.Name = strings.TrimSpace(entry.Name)
11671183
entry.Prefix = strings.TrimSpace(entry.Prefix)
11681184
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
11691185
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)

internal/api/handlers/management/config_lists_delete_keys_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,82 @@ func TestDeleteCodexKey_RequiresBaseURLWhenAPIKeyDuplicated(t *testing.T) {
382382
t.Fatalf("codex keys len = %d, want 2", got)
383383
}
384384
}
385+
386+
func TestDeleteCodexKey_RequiresIndexWhenAPIKeyAndBaseURLDuplicated(t *testing.T) {
387+
t.Parallel()
388+
gin.SetMode(gin.TestMode)
389+
390+
h := &Handler{
391+
cfg: &config.Config{
392+
CodexKey: []config.CodexKey{
393+
{
394+
BaseURL: "https://codex.example.com",
395+
APIKeyEntries: []config.CodexAPIKeyEntry{
396+
{APIKey: "shared-key", ProxyURL: "http://proxy-a.example.com:8080"},
397+
},
398+
},
399+
{
400+
BaseURL: "https://codex.example.com",
401+
APIKeyEntries: []config.CodexAPIKeyEntry{
402+
{APIKey: "shared-key", ProxyURL: "http://proxy-b.example.com:8080"},
403+
},
404+
},
405+
},
406+
},
407+
configFilePath: writeTestConfigFile(t),
408+
}
409+
410+
rec := httptest.NewRecorder()
411+
c, _ := gin.CreateTestContext(rec)
412+
c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/codex-api-key?api-key=shared-key&base-url=https://codex.example.com", nil)
413+
414+
h.DeleteCodexKey(c)
415+
416+
if rec.Code != http.StatusBadRequest {
417+
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusBadRequest, rec.Body.String())
418+
}
419+
if got := len(h.cfg.CodexKey); got != 2 {
420+
t.Fatalf("codex keys len = %d, want 2", got)
421+
}
422+
}
423+
424+
func TestDeleteCodexKey_DeletesDuplicateByIndex(t *testing.T) {
425+
t.Parallel()
426+
gin.SetMode(gin.TestMode)
427+
428+
h := &Handler{
429+
cfg: &config.Config{
430+
CodexKey: []config.CodexKey{
431+
{
432+
BaseURL: "https://codex.example.com",
433+
APIKeyEntries: []config.CodexAPIKeyEntry{
434+
{APIKey: "shared-key", ProxyURL: "http://proxy-a.example.com:8080"},
435+
},
436+
},
437+
{
438+
BaseURL: "https://codex.example.com",
439+
APIKeyEntries: []config.CodexAPIKeyEntry{
440+
{APIKey: "shared-key", ProxyURL: "http://proxy-b.example.com:8080"},
441+
},
442+
},
443+
},
444+
},
445+
configFilePath: writeTestConfigFile(t),
446+
}
447+
448+
rec := httptest.NewRecorder()
449+
c, _ := gin.CreateTestContext(rec)
450+
c.Request = httptest.NewRequest(http.MethodDelete, "/v0/management/codex-api-key?index=1", nil)
451+
452+
h.DeleteCodexKey(c)
453+
454+
if rec.Code != http.StatusOK {
455+
t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
456+
}
457+
if got := len(h.cfg.CodexKey); got != 1 {
458+
t.Fatalf("codex keys len = %d, want 1", got)
459+
}
460+
if got := h.cfg.CodexKey[0].APIKeyEntries[0].ProxyURL; got != "http://proxy-a.example.com:8080" {
461+
t.Fatalf("remaining proxy-url = %q, want %q", got, "http://proxy-a.example.com:8080")
462+
}
463+
}

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ type CodexKey struct {
421421
// continue to load without migration.
422422
APIKey string `yaml:"api-key" json:"api-key"`
423423

424+
// Name is an optional display name for distinguishing Codex config blocks.
425+
// 它只用于配置管理和展示,不参与上游鉴权、模型路由或 Auth ID 生成。
426+
Name string `yaml:"name,omitempty" json:"name,omitempty"`
427+
424428
// Priority controls selection preference when multiple credentials match.
425429
// Higher values are preferred; defaults to 0.
426430
Priority int `yaml:"priority,omitempty" json:"priority,omitempty"`
@@ -1016,6 +1020,7 @@ func (cfg *Config) SanitizeCodexKeys() {
10161020
for i := range cfg.CodexKey {
10171021
e := cfg.CodexKey[i]
10181022
e.APIKey = strings.TrimSpace(e.APIKey)
1023+
e.Name = strings.TrimSpace(e.Name)
10191024
e.Prefix = normalizeModelPrefix(e.Prefix)
10201025
e.BaseURL = strings.TrimSpace(e.BaseURL)
10211026
e.ProxyURL = strings.TrimSpace(e.ProxyURL)

internal/config/config_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,32 @@ func TestLoadConfigOptionalRoutingSourcePreference(t *testing.T) {
130130
}
131131
}
132132

133+
func TestLoadConfigOptionalCodexKeyName(t *testing.T) {
134+
t.Parallel()
135+
136+
configPath := filepath.Join(t.TempDir(), "config.yaml")
137+
content := `codex-api-key:
138+
- name: " Primary Codex "
139+
base-url: "https://codex.example.com"
140+
api-key-entries:
141+
- api-key: "codex-key"
142+
`
143+
if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil {
144+
t.Fatalf("write config: %v", err)
145+
}
146+
147+
cfg, err := LoadConfigOptional(configPath, false)
148+
if err != nil {
149+
t.Fatalf("LoadConfigOptional() error = %v", err)
150+
}
151+
if got := len(cfg.CodexKey); got != 1 {
152+
t.Fatalf("CodexKey len = %d, want 1", got)
153+
}
154+
if got := cfg.CodexKey[0].Name; got != "Primary Codex" {
155+
t.Fatalf("CodexKey[0].Name = %q, want %q", got, "Primary Codex")
156+
}
157+
}
158+
133159
func TestIsKnownDefaultValueRecognizesQuotaCacheRefreshInterval(t *testing.T) {
134160
t.Parallel()
135161

0 commit comments

Comments
 (0)