Skip to content

Commit fada0e0

Browse files
committed
Merge branch 'feat/codex-extra-models': codex-extra-models config
2 parents a2b97ff + 3d2d749 commit fada0e0

7 files changed

Lines changed: 531 additions & 2 deletions

File tree

cmd/server/main.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ func setKiroIncognitoMode(cfg *config.Config, useIncognito, noIncognito bool) {
6464
}
6565
}
6666

67+
// applyCodexExtraModelsFromConfig translates the config Codex extra-model
68+
// declarations into registry specs and applies them to the model catalog.
69+
// Safe to call before StartModelsUpdater so the embedded catalog is augmented
70+
// immediately and subsequent remote refreshes preserve the clones.
71+
func applyCodexExtraModelsFromConfig(cfg *config.Config) {
72+
if cfg == nil || len(cfg.CodexExtraModels) == 0 {
73+
registry.SetCodexExtraModels(nil)
74+
return
75+
}
76+
specs := make([]registry.CodexExtraModelSpec, 0, len(cfg.CodexExtraModels))
77+
for _, m := range cfg.CodexExtraModels {
78+
specs = append(specs, registry.CodexExtraModelSpec{
79+
ID: m.ID,
80+
InheritFrom: m.InheritFrom,
81+
})
82+
}
83+
registry.SetCodexExtraModels(specs)
84+
}
85+
6786
// main is the entry point of the application.
6887
// It parses command-line flags, loads configuration, and starts the appropriate
6988
// service based on the provided flags (login, codex-login, or server mode).
@@ -594,6 +613,10 @@ func main() {
594613
if localModel && (!tuiMode || standalone) {
595614
log.Info("Local model mode: using embedded model catalog, remote model updates disabled")
596615
}
616+
// Apply Codex extra-model declarations from config. Runs before
617+
// StartModelsUpdater so the embedded catalog is augmented immediately,
618+
// and remote refreshes preserve the clones via applyCodexExtrasLocked.
619+
applyCodexExtraModelsFromConfig(cfg)
597620
if tuiMode {
598621
if standalone {
599622
// Standalone mode: start an embedded local server and connect TUI client to it.

config.example.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ nonstream-keepalive-interval: 0
231231
# user-agent: "codex_cli_rs/0.114.0 (Mac OS 14.2.0; x86_64) vscode/1.111.0"
232232
# beta-features: "multi_agent"
233233

234+
# Codex extra-model declarations (parameter-equivalent clones).
235+
# Each entry registers a synthetic Codex model whose parameters are cloned
236+
# from an existing Codex model in the catalog. The clone is injected into
237+
# every Codex tier (codex-free / codex-team / codex-plus / codex-pro) where
238+
# the inherit-from base model is present.
239+
#
240+
# Use this for temporarily exposing upstream model IDs that share parameters
241+
# with a known model (e.g., a faster variant your client expects). When the
242+
# upstream catalog later ships a model with the same ID, the upstream entry
243+
# wins automatically and the clone is silently superseded.
244+
#
245+
# The clone's Version field is set to the new ID, so requests are forwarded
246+
# to upstream as-is. The caller assumes the risk that the upstream API
247+
# actually accepts the new ID.
248+
# codex-extra-models:
249+
# - id: gpt-5.4-mini-fast
250+
# inherit-from: gpt-5.4-mini
251+
234252
# Kiro (AWS CodeWhisperer) configuration
235253
# Note: Kiro API currently only operates in us-east-1 region
236254
#kiro:

internal/config/config.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ type Config struct {
146146
// gemini-api-key, codex-api-key, claude-api-key, openai-compatibility, vertex-api-key, and ampcode.
147147
OAuthModelAlias map[string][]OAuthModelAlias `yaml:"oauth-model-alias,omitempty" json:"oauth-model-alias,omitempty"`
148148

149+
// CodexExtraModels declares synthetic Codex models that clone their parameters
150+
// from an existing Codex model already present in the catalog. The clone is
151+
// injected into every Codex tier (free/team/plus/pro) where the inherit-from
152+
// base model exists. When the upstream catalog later ships a model with the
153+
// same ID, the upstream entry wins and the clone is silently superseded.
154+
//
155+
// Use this to expose temporary same-parameter aliases (e.g., upstream-only
156+
// model IDs) without maintaining a fork of the remote models.json.
157+
CodexExtraModels []CodexExtraModel `yaml:"codex-extra-models,omitempty" json:"codex-extra-models,omitempty"`
158+
149159
// Payload defines default and override rules for provider payload parameters.
150160
Payload PayloadConfig `yaml:"payload" json:"payload"`
151161

@@ -262,6 +272,17 @@ type OAuthModelAlias struct {
262272
Fork bool `yaml:"fork,omitempty" json:"fork,omitempty"`
263273
}
264274

275+
// CodexExtraModel declares a synthetic Codex model whose parameters are cloned
276+
// from an existing entry in the catalog. The clone is applied to every Codex
277+
// tier where the base model is present.
278+
type CodexExtraModel struct {
279+
// ID is the new model identifier exposed to clients.
280+
ID string `yaml:"id" json:"id"`
281+
// InheritFrom is the existing Codex model ID whose parameters are cloned
282+
// (e.g., "gpt-5.4-mini").
283+
InheritFrom string `yaml:"inherit-from" json:"inherit-from"`
284+
}
285+
265286
// AmpModelMapping defines a model name mapping for Amp CLI requests.
266287
// When Amp requests a model that isn't available locally, this mapping
267288
// allows routing to an alternative model that IS available.
@@ -766,6 +787,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
766787
// Normalize global OAuth model name aliases.
767788
cfg.SanitizeOAuthModelAlias()
768789

790+
// Normalize Codex extra-model declarations.
791+
cfg.SanitizeCodexExtraModels()
792+
769793
// Validate raw payload rules and drop invalid entries.
770794
cfg.SanitizePayloadRules()
771795

@@ -937,6 +961,33 @@ func (cfg *Config) SanitizeOAuthModelAlias() {
937961
cfg.OAuthModelAlias = out
938962
}
939963

964+
// SanitizeCodexExtraModels validates and de-duplicates Codex extra-model entries.
965+
// Entries with empty fields, self-references, or duplicate IDs are dropped.
966+
func (cfg *Config) SanitizeCodexExtraModels() {
967+
if cfg == nil || len(cfg.CodexExtraModels) == 0 {
968+
return
969+
}
970+
seen := make(map[string]struct{}, len(cfg.CodexExtraModels))
971+
out := make([]CodexExtraModel, 0, len(cfg.CodexExtraModels))
972+
for _, m := range cfg.CodexExtraModels {
973+
id := strings.TrimSpace(m.ID)
974+
base := strings.TrimSpace(m.InheritFrom)
975+
if id == "" || base == "" {
976+
continue
977+
}
978+
if strings.EqualFold(id, base) {
979+
continue
980+
}
981+
key := strings.ToLower(id)
982+
if _, ok := seen[key]; ok {
983+
continue
984+
}
985+
seen[key] = struct{}{}
986+
out = append(out, CodexExtraModel{ID: id, InheritFrom: base})
987+
}
988+
cfg.CodexExtraModels = out
989+
}
990+
940991
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
941992
// not actionable, specifically those missing a BaseURL. It trims whitespace before
942993
// evaluation and preserves the relative order of remaining entries.

internal/registry/extra_models.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Package registry — extra_models.go injects synthetic Codex models declared
2+
// in user configuration. Each spec clones an existing Codex model's parameters,
3+
// only changing the ID and Version. Clones are applied to every Codex tier
4+
// (free/team/plus/pro) where the inherit-from base model is present.
5+
//
6+
// Injected entries carry the ModelInfo.Injected marker so the registry can
7+
// safely re-apply or remove them on configuration changes and remote refreshes
8+
// without affecting upstream-defined data. When the upstream catalog later
9+
// ships a model with the same ID, the upstream entry takes priority and the
10+
// clone is silently superseded.
11+
package registry
12+
13+
import (
14+
"strings"
15+
"sync"
16+
)
17+
18+
// CodexExtraModelSpec describes a synthetic Codex model that clones its
19+
// parameters from an existing Codex model.
20+
type CodexExtraModelSpec struct {
21+
ID string
22+
InheritFrom string
23+
}
24+
25+
var (
26+
codexExtrasMu sync.RWMutex
27+
codexExtras []CodexExtraModelSpec
28+
)
29+
30+
// SetCodexExtraModels replaces the active spec list and immediately re-applies
31+
// the resulting clones to the catalog store. Safe to call at startup and on
32+
// configuration reload.
33+
func SetCodexExtraModels(specs []CodexExtraModelSpec) {
34+
sanitized := sanitizeCodexExtraSpecs(specs)
35+
codexExtrasMu.Lock()
36+
codexExtras = sanitized
37+
codexExtrasMu.Unlock()
38+
39+
modelsCatalogStore.mu.Lock()
40+
applyCodexExtrasLocked()
41+
modelsCatalogStore.mu.Unlock()
42+
}
43+
44+
// sanitizeCodexExtraSpecs trims fields, drops invalid/self-referential entries,
45+
// and removes duplicate IDs (case-insensitive).
46+
func sanitizeCodexExtraSpecs(specs []CodexExtraModelSpec) []CodexExtraModelSpec {
47+
if len(specs) == 0 {
48+
return nil
49+
}
50+
seen := make(map[string]struct{}, len(specs))
51+
out := make([]CodexExtraModelSpec, 0, len(specs))
52+
for _, s := range specs {
53+
id := strings.TrimSpace(s.ID)
54+
base := strings.TrimSpace(s.InheritFrom)
55+
if id == "" || base == "" || strings.EqualFold(id, base) {
56+
continue
57+
}
58+
key := strings.ToLower(id)
59+
if _, ok := seen[key]; ok {
60+
continue
61+
}
62+
seen[key] = struct{}{}
63+
out = append(out, CodexExtraModelSpec{ID: id, InheritFrom: base})
64+
}
65+
return out
66+
}
67+
68+
// applyCodexExtrasLocked rebuilds the codex tiers in the catalog store: it
69+
// first removes any previously-injected clones (identified by the Injected
70+
// marker), then injects clones for the current spec list. The caller MUST hold
71+
// modelsCatalogStore.mu (write lock).
72+
func applyCodexExtrasLocked() {
73+
if modelsCatalogStore.data == nil {
74+
return
75+
}
76+
data := modelsCatalogStore.data
77+
data.CodexFree = stripInjectedFromTier(data.CodexFree)
78+
data.CodexTeam = stripInjectedFromTier(data.CodexTeam)
79+
data.CodexPlus = stripInjectedFromTier(data.CodexPlus)
80+
data.CodexPro = stripInjectedFromTier(data.CodexPro)
81+
82+
codexExtrasMu.RLock()
83+
specs := append([]CodexExtraModelSpec(nil), codexExtras...)
84+
codexExtrasMu.RUnlock()
85+
if len(specs) == 0 {
86+
return
87+
}
88+
data.CodexFree = applyExtrasToTier(data.CodexFree, specs)
89+
data.CodexTeam = applyExtrasToTier(data.CodexTeam, specs)
90+
data.CodexPlus = applyExtrasToTier(data.CodexPlus, specs)
91+
data.CodexPro = applyExtrasToTier(data.CodexPro, specs)
92+
}
93+
94+
// applyExtrasToTier appends a clone for every spec whose base is present and
95+
// whose target ID does NOT yet exist in the tier (the upstream entry always
96+
// wins). Each clone is marked with Injected = true so future rebuilds can
97+
// strip it cleanly.
98+
func applyExtrasToTier(tier []*ModelInfo, specs []CodexExtraModelSpec) []*ModelInfo {
99+
if len(specs) == 0 || len(tier) == 0 {
100+
return tier
101+
}
102+
byID := make(map[string]*ModelInfo, len(tier))
103+
for _, m := range tier {
104+
if m == nil {
105+
continue
106+
}
107+
id := strings.TrimSpace(m.ID)
108+
if id == "" {
109+
continue
110+
}
111+
byID[strings.ToLower(id)] = m
112+
}
113+
clones := make([]*ModelInfo, 0, len(specs))
114+
for _, spec := range specs {
115+
if _, exists := byID[strings.ToLower(spec.ID)]; exists {
116+
continue
117+
}
118+
base, ok := byID[strings.ToLower(spec.InheritFrom)]
119+
if !ok {
120+
continue
121+
}
122+
clone := cloneModelInfo(base)
123+
clone.ID = spec.ID
124+
clone.Version = spec.ID
125+
clone.Injected = true
126+
clones = append(clones, clone)
127+
}
128+
if len(clones) == 0 {
129+
return tier
130+
}
131+
out := make([]*ModelInfo, 0, len(tier)+len(clones))
132+
out = append(out, tier...)
133+
out = append(out, clones...)
134+
return out
135+
}
136+
137+
// stripInjectedFromTier returns a tier with all Injected entries removed.
138+
// When no entries are marked Injected the original slice is returned without
139+
// allocation.
140+
func stripInjectedFromTier(tier []*ModelInfo) []*ModelInfo {
141+
if len(tier) == 0 {
142+
return tier
143+
}
144+
hasInjected := false
145+
for _, m := range tier {
146+
if m != nil && m.Injected {
147+
hasInjected = true
148+
break
149+
}
150+
}
151+
if !hasInjected {
152+
return tier
153+
}
154+
out := make([]*ModelInfo, 0, len(tier))
155+
for _, m := range tier {
156+
if m == nil || m.Injected {
157+
continue
158+
}
159+
out = append(out, m)
160+
}
161+
return out
162+
}
163+
164+
// stripCodexExtras returns a shallow-cloned catalog with all Injected entries
165+
// removed from the four Codex tiers. Used by the remote refresh path so
166+
// change-detection compares apples-to-apples (untouched upstream payload vs
167+
// untouched cached payload). When no entries are marked Injected, the original
168+
// pointer is returned without any allocation.
169+
func stripCodexExtras(data *staticModelsJSON) *staticModelsJSON {
170+
if data == nil {
171+
return nil
172+
}
173+
free := stripInjectedFromTier(data.CodexFree)
174+
team := stripInjectedFromTier(data.CodexTeam)
175+
plus := stripInjectedFromTier(data.CodexPlus)
176+
pro := stripInjectedFromTier(data.CodexPro)
177+
if sameSlice(free, data.CodexFree) &&
178+
sameSlice(team, data.CodexTeam) &&
179+
sameSlice(plus, data.CodexPlus) &&
180+
sameSlice(pro, data.CodexPro) {
181+
return data
182+
}
183+
out := *data
184+
out.CodexFree = free
185+
out.CodexTeam = team
186+
out.CodexPlus = plus
187+
out.CodexPro = pro
188+
return &out
189+
}
190+
191+
// sameSlice reports whether two slices share the same underlying array head.
192+
// Used as a cheap check to avoid a header-copy when stripInjectedFromTier
193+
// returned the input slice unchanged.
194+
func sameSlice(a, b []*ModelInfo) bool {
195+
return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0])
196+
}

0 commit comments

Comments
 (0)