Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions sdk/cliproxy/antigravity_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,17 +458,26 @@ func (s *Service) buildAntigravityReverseAliasMap(auth *coreauth.Auth) map[strin
s.cfgMu.RUnlock()
}
channel := coreauth.OAuthModelAliasChannel(auth.Provider, auth.AuthKind())
aliases := oauthModelAliasesForAuth(cfg, channel, auth.Attributes)
// The probe maps ids that are already registered back to their upstream, so it has no
// catalog or exclusion list of its own. A nil catalog keeps every global fork, which is
// what an id registered only by a fork needs to resolve.
aliases := oauthModelAliasesForAuth(cfg, channel, auth.Attributes, nil, nil)
if len(aliases) == 0 {
return nil
}
aliasMap := make(map[string]string, len(aliases))
for _, entry := range aliases {
aliasName := strings.ToLower(strings.TrimSpace(entry.Alias))
upstreamName := strings.ToLower(strings.TrimSpace(entry.Name))
if aliasName != "" && upstreamName != "" {
aliasMap[aliasName] = upstreamName
if aliasName == "" || upstreamName == "" {
continue
}
// One alias can carry a per-auth rename and a global fork. Per-auth entries come
// first and decide what this credential sends upstream, so the first entry wins.
if _, exists := aliasMap[aliasName]; exists {
continue
}
aliasMap[aliasName] = upstreamName
}
return aliasMap
}
Expand Down
53 changes: 53 additions & 0 deletions sdk/cliproxy/antigravity_models_alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package cliproxy

import (
"testing"

coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
)

func antigravityAuth(aliases string) *coreauth.Auth {
return &coreauth.Auth{
ID: "antigravity-alias-test",
Provider: "antigravity",
Attributes: map[string]string{
"auth_kind": coreauth.AuthKindOAuth,
"model_aliases": aliases,
},
}
}

// The capability probe maps a registered id back to the upstream the credential is actually
// sent. The (name, alias) merge can return a per-auth rename and a global fork under one
// alias; request-time resolution consults the per-auth entry, so the reverse map must too.
func TestBuildAntigravityReverseAliasMapPrefersPerAuthEntry(t *testing.T) {
cfg := &config.Config{OAuthModelAlias: map[string][]config.OAuthModelAlias{
"antigravity": {{Name: "gemini-3-pro", Alias: "gemini-3-pro-preview", Fork: true}},
}}
auth := antigravityAuth(`[{"name":"gemini-3-pro-internal","alias":"gemini-3-pro-preview"}]`)
got := (&Service{cfg: cfg}).buildAntigravityReverseAliasMap(auth)
if got["gemini-3-pro-preview"] != "gemini-3-pro-internal" {
t.Fatalf("alias resolved to %q, want the per-auth upstream gemini-3-pro-internal", got["gemini-3-pro-preview"])
}
}

// A global fork with no per-auth entry for its alias must stay in the map: the id exists only
// because the fork registered it.
func TestBuildAntigravityReverseAliasMapKeepsUnshadowedGlobalFork(t *testing.T) {
cfg := &config.Config{OAuthModelAlias: map[string][]config.OAuthModelAlias{
"antigravity": {{Name: "gemini-3-pro", Alias: "gemini-3-pro-preview", Fork: true}},
}}
auth := antigravityAuth(`[{"name":"gemini-3-flash-internal","alias":"gemini-3-flash"}]`)
got := (&Service{cfg: cfg}).buildAntigravityReverseAliasMap(auth)
if got["gemini-3-pro-preview"] != "gemini-3-pro" {
t.Fatalf("forked alias resolved to %q, want gemini-3-pro", got["gemini-3-pro-preview"])
}
if got["gemini-3-flash"] != "gemini-3-flash-internal" {
t.Fatalf("per-auth alias resolved to %q, want gemini-3-flash-internal", got["gemini-3-flash"])
}
// resolveAntigravityUpstreamModelID strips the credential prefix before the lookup.
if id := resolveAntigravityUpstreamModelID("ag/gemini-3-pro-preview", "ag", got); id != "gemini-3-pro" {
t.Fatalf("prefixed id resolved to %q, want gemini-3-pro", id)
}
}
2 changes: 1 addition & 1 deletion sdk/cliproxy/service_executors.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ func (s *Service) tryRegisterPluginModelsForAuth(ctx context.Context, a *coreaut
return true
}
models := applyExcludedModels(result.Models, activeExcluded)
models = applyOAuthModelAliasForAuth(s.cfg, providerKey, activeAuthKind, activeAuth.Attributes, models)
models = applyOAuthModelAliasForAuth(s.cfg, providerKey, activeAuthKind, activeAuth.Attributes, activeExcluded, models)
if len(models) > 0 {
s.registerResolvedModelsForAuth(activeAuth, providerKey, applyModelPrefixes(models, activeAuth.Prefix, s.cfg != nil && s.cfg.ForceModelPrefix))
return true
Expand Down
128 changes: 105 additions & 23 deletions sdk/cliproxy/service_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,16 @@ func (s *Service) registerModelsForAuthWithCache(ctx context.Context, a *coreaut
if ctx.Err() != nil {
return
}
models = applyOAuthModelAliasForAuth(s.cfg, provider, authKind, a.Attributes, models)
if ctx.Err() != nil {
return
}
key := provider
if key == "" {
key = strings.ToLower(strings.TrimSpace(a.Provider))
}
// Plugin models are appended after the alias pass, so a per-auth source that a static
// plugin supplies is not in `models` yet; the alias merge still needs to know it exists.
models = applyOAuthModelAliasForAuthWithCatalog(s.cfg, provider, authKind, a.Attributes, excluded, models, catalogModelIDs(s.appendPluginModels(key, models)))
if ctx.Err() != nil {
return
}
models = s.appendPluginModels(key, models)
if len(models) > 0 {
s.registerResolvedModelsForAuth(a, key, applyModelPrefixes(models, a.Prefix, s.cfg != nil && s.cfg.ForceModelPrefix))
Expand Down Expand Up @@ -962,25 +964,65 @@ func rewriteModelInfoName(name, oldID, newID string) string {
}

func applyOAuthModelAlias(cfg *config.Config, provider, authKind string, models []*ModelInfo) []*ModelInfo {
return applyOAuthModelAliasForAuth(cfg, provider, authKind, nil, models)
return applyOAuthModelAliasForAuth(cfg, provider, authKind, nil, nil, models)
}

// applyOAuthModelAliasForAuth applies the merged alias table to a catalog that has already
// had `excluded` removed. excluded is the same effective list the caller filtered with, so a
// source that exclusions removed is not mistaken for a hidden upstream model.
func applyOAuthModelAliasForAuth(cfg *config.Config, provider, authKind string, attributes map[string]string, excluded []string, models []*ModelInfo) []*ModelInfo {
return applyOAuthModelAliasForAuthWithCatalog(cfg, provider, authKind, attributes, excluded, models, catalogModelIDs(models))
}

func applyOAuthModelAliasForAuth(cfg *config.Config, provider, authKind string, attributes map[string]string, models []*ModelInfo) []*ModelInfo {
// applyOAuthModelAliasForAuthWithCatalog is applyOAuthModelAliasForAuth with an explicit set
// of upstream ids the credential will end up serving, for callers whose final catalog is
// larger than `models` (plugin models are appended after the alias pass).
func applyOAuthModelAliasForAuthWithCatalog(cfg *config.Config, provider, authKind string, attributes map[string]string, excluded []string, models []*ModelInfo, catalog map[string]struct{}) []*ModelInfo {
if len(models) == 0 {
return models
}
channel := coreauth.OAuthModelAliasChannel(provider, authKind)
if channel == "" {
return models
}
aliases := oauthModelAliasesForAuth(cfg, channel, attributes)
aliases := oauthModelAliasesForAuth(cfg, channel, attributes, catalog, excluded)
if len(aliases) == 0 {
return models
}
return applyOAuthModelAliasEntries(aliases, models)
}

func oauthModelAliasesForAuth(cfg *config.Config, channel string, attributes map[string]string) []config.OAuthModelAlias {
// catalogModelIDs is the set of upstream ids present in the catalog, lowercased, so the
// alias merge can tell whether a per-auth source model can supply a catalog entry itself.
func catalogModelIDs(models []*ModelInfo) map[string]struct{} {
ids := make(map[string]struct{}, len(models))
for _, model := range models {
if model == nil {
continue
}
if id := strings.ToLower(strings.TrimSpace(model.ID)); id != "" {
ids[id] = struct{}{}
}
}
return ids
}

func modelExcluded(id string, excluded []string) bool {
for _, pattern := range excluded {
if trimmed := strings.ToLower(strings.TrimSpace(pattern)); trimmed != "" && matchWildcard(trimmed, id) {
return true
}
}
return false
}

// oauthModelAliasesForAuth merges a credential's aliases with the channel's global table.
// catalog is the set of upstream ids the catalog has after exclusions; nil means unknown,
// which keeps every global fork. excluded is the effective exclusion list the catalog was
// filtered with (global oauth-excluded-models or the synthesizer's merged attribute): a
// per-auth source that exclusions removed must not be treated as a hidden model, or the
// global fork would register an id that request-time resolution routes to the excluded model.
func oauthModelAliasesForAuth(cfg *config.Config, channel string, attributes map[string]string, catalog map[string]struct{}, excluded []string) []config.OAuthModelAlias {
perAuthAliases := coreauth.OAuthModelAliasesFromAttributes(attributes)
if cfg == nil || len(cfg.OAuthModelAlias) == 0 {
return perAuthAliases
Expand All @@ -992,24 +1034,64 @@ func oauthModelAliasesForAuth(cfg *config.Config, channel string, attributes map
if len(globalAliases) == 0 {
return perAuthAliases
}
// Two tables, two jobs. The global table may fork a catalog entry (clone an upstream
// id under a new client-visible id); a per-auth entry decides what one credential sends
// upstream for that id, and request-time resolution consults per-auth first. A hidden
// upstream model (Codex "gpt-reserve") needs both: a global fork so the id is routable,
// and a per-auth rename so the body says gpt-reserve. Keyed on alias alone, the per-auth
// entry evicted the fork and the id was never registered.
//
// So the key is the (name, alias) pair, with one exception. A global entry that shares
// its alias with a per-auth entry is a fallback for the catalog, kept only when the
// per-auth source model is not in the catalog itself (the hidden-model case) and only
// when it is a fork. Keeping it otherwise would let applyOAuthModelAliasEntries build
// the catalog entry from the global source, in catalog order, while the credential
// routes the id to its own upstream: the registered entry would carry the wrong
// upstream's metadata and the per-auth source would stay exposed under its own id.
type aliasPair struct{ name, alias string }
pairOf := func(entry config.OAuthModelAlias) aliasPair {
return aliasPair{
name: strings.ToLower(strings.TrimSpace(entry.Name)),
alias: strings.ToLower(strings.TrimSpace(entry.Alias)),
}
}
out := make([]config.OAuthModelAlias, 0, len(perAuthAliases)+len(globalAliases))
seenAlias := make(map[string]struct{}, len(perAuthAliases)+len(globalAliases))
add := func(aliases []config.OAuthModelAlias) {
for _, entry := range aliases {
alias := strings.TrimSpace(entry.Alias)
if alias == "" {
continue
}
key := strings.ToLower(alias)
if _, exists := seenAlias[key]; exists {
continue
}
seenAlias[key] = struct{}{}
out = append(out, entry)
seenPair := make(map[aliasPair]struct{}, len(perAuthAliases)+len(globalAliases))
// alias -> whether some per-auth source for that alias can supply the catalog entry.
perAuthAlias := make(map[string]bool, len(perAuthAliases))
for _, entry := range perAuthAliases {
pair := pairOf(entry)
if pair.alias == "" {
continue
}
if _, exists := seenPair[pair]; exists {
continue
}
seenPair[pair] = struct{}{}
// "Can supply the entry" is the fork-fallback question. An excluded source, or
// an excluded alias, answers yes on purpose: the fork is dropped and the id stays
// unroutable through this credential, which is what the exclusion asked for.
canSupply := modelExcluded(pair.name, excluded) || modelExcluded(pair.alias, excluded)
if !canSupply && catalog != nil {
_, canSupply = catalog[pair.name]
Comment thread
SamGu-NRX marked this conversation as resolved.
}
perAuthAlias[pair.alias] = perAuthAlias[pair.alias] || canSupply
out = append(out, entry)
}
for _, entry := range globalAliases {
pair := pairOf(entry)
if pair.alias == "" {
continue
}
if _, exists := seenPair[pair]; exists {
continue
}
if sourceInCatalog, shadowed := perAuthAlias[pair.alias]; shadowed && (!entry.Fork || sourceInCatalog) {
continue
Comment thread
SamGu-NRX marked this conversation as resolved.
}
seenPair[pair] = struct{}{}
out = append(out, entry)
}
add(perAuthAliases)
add(globalAliases)
return out
}

Expand Down
Loading
Loading