Files
sub2api/backend/internal/service/openai_model_alias.go
T
Long LiandCursor 983a3db3af fix(openai): persist GPT-6 Astra Codex capabilities from upstream sync
Official OpenAI hosts and models.dev entries without an api field left
Astra falling back to reasoning effort none and empty Fast tiers. Persist
complete per-model metadata, advertise Astra reasoning/Fast locally, and
surface partial sync warnings when image models remain incomplete.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 14:45:06 +09:00

180 lines
5.2 KiB
Go

package service
import "strings"
func lastOpenAIModelSegment(model string) string {
model = strings.TrimSpace(model)
if model == "" {
return ""
}
if strings.Contains(model, "/") {
parts := strings.Split(model, "/")
model = parts[len(parts)-1]
}
return strings.TrimSpace(model)
}
func canonicalizeOpenAIModelAliasSpelling(model string) string {
model = strings.ToLower(lastOpenAIModelSegment(model))
if model == "" {
return ""
}
normalized := strings.ReplaceAll(model, "_", "-")
normalized = strings.Join(strings.Fields(normalized), "-")
for strings.Contains(normalized, "--") {
normalized = strings.ReplaceAll(normalized, "--", "-")
}
if strings.HasPrefix(normalized, "gpt5") {
normalized = "gpt-5" + strings.TrimPrefix(normalized, "gpt5")
}
if !strings.HasPrefix(normalized, "gpt-") && !strings.Contains(normalized, "codex") {
return ""
}
replacements := []struct {
from string
to string
}{
{"gpt-5.4mini", "gpt-5.4-mini"},
{"gpt-5.4nano", "gpt-5.4-nano"},
{"gpt-5.3-codexspark", "gpt-5.3-codex-spark"},
{"gpt-5.3codexspark", "gpt-5.3-codex-spark"},
{"gpt-5.3codex", "gpt-5.3-codex"},
}
for _, replacement := range replacements {
normalized = strings.ReplaceAll(normalized, replacement.from, replacement.to)
}
return normalized
}
func normalizeKnownOpenAICodexModel(model string) string {
normalized := canonicalizeOpenAIModelAliasSpelling(model)
if normalized == "" {
return ""
}
if mapped := getNormalizedCodexModel(normalized); mapped != "" {
return mapped
}
if strings.HasSuffix(normalized, "-openai-compact") {
if mapped := getNormalizedCodexModel(strings.TrimSuffix(normalized, "-openai-compact")); mapped != "" {
return mapped
}
}
switch {
case strings.Contains(normalized, "gpt-5.6-sol"):
return "gpt-5.6-sol"
case strings.Contains(normalized, "gpt-5.6-terra"):
return "gpt-5.6-terra"
case strings.Contains(normalized, "gpt-5.6-luna"):
return "gpt-5.6-luna"
case normalized == "gpt-5.6":
return "gpt-5.6-sol"
case strings.HasPrefix(normalized, "gpt-5.6-"):
suffix := strings.TrimPrefix(normalized, "gpt-5.6-")
if suffix == "max" || isKnownCodexModelSuffix(suffix) {
return "gpt-5.6-sol"
}
return ""
case strings.Contains(normalized, "gpt-5.5-pro"):
return "gpt-5.5-pro"
case strings.Contains(normalized, "gpt-5.5"):
return "gpt-5.5"
case strings.Contains(normalized, "gpt-5.4-mini"):
return "gpt-5.4-mini"
case strings.Contains(normalized, "gpt-5.4-nano"):
return "gpt-5.4-nano"
case strings.Contains(normalized, "gpt-5.4"):
return "gpt-5.4"
case strings.Contains(normalized, "gpt-5.2"):
return "gpt-5.2"
case strings.Contains(normalized, "gpt-5.3-codex-spark"):
return "gpt-5.3-codex-spark"
case strings.Contains(normalized, "gpt-5.3-codex"):
return "gpt-5.3-codex"
case strings.Contains(normalized, "gpt-5.3"):
return "gpt-5.3-codex"
case strings.Contains(normalized, "codex"):
return "gpt-5.3-codex"
case strings.Contains(normalized, "gpt-5"):
return "gpt-5.4"
default:
return ""
}
}
// isOpenAIGPT56Model 判断是否 GPT-5.6 系列模型;入参可为原始模型名
// (含大小写/路径/后缀变体)或已归一化的基名,两者均能正确识别。
func isOpenAIGPT56Model(model string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(model)
if normalized == "gpt-5.6" {
return true
}
if suffix, ok := strings.CutPrefix(normalized, "gpt-5.6-"); ok && (suffix == "max" || isKnownCodexModelSuffix(suffix)) {
return true
}
for _, prefix := range []string{"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"} {
if normalized == prefix || strings.HasPrefix(normalized, prefix+"-") {
return true
}
}
return false
}
// isOpenAIGPT6AstraModel reports GPT-6 Astra and dated/provider-prefixed variants.
// Bare "gpt-6" and other future gpt-6-* models are intentionally excluded until
// their capabilities are confirmed (see #6622).
func isOpenAIGPT6AstraModel(model string) bool {
normalized := canonicalizeOpenAIModelAliasSpelling(model)
return normalized == "gpt-6-astra" || strings.HasPrefix(normalized, "gpt-6-astra-")
}
func appendUsageBillingModelCandidate(candidates []string, seen map[string]struct{}, model string) []string {
trimmed := strings.TrimSpace(model)
if trimmed == "" {
return candidates
}
add := func(candidate string) {
candidate = strings.TrimSpace(candidate)
if candidate == "" {
return
}
key := strings.ToLower(candidate)
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
candidates = append(candidates, candidate)
}
add(trimmed)
if canonical := canonicalizeOpenAIModelAliasSpelling(trimmed); canonical != "" {
add(canonical)
}
if normalized := normalizeKnownOpenAICodexModel(trimmed); normalized != "" {
add(normalized)
}
return candidates
}
func usageBillingModelCandidates(primary string, alternates ...string) []string {
seen := make(map[string]struct{}, 1+len(alternates))
candidates := appendUsageBillingModelCandidate(nil, seen, primary)
for _, alternate := range alternates {
candidates = appendUsageBillingModelCandidate(candidates, seen, alternate)
}
return candidates
}
func firstUsageBillingModel(candidates []string) string {
for _, candidate := range candidates {
if trimmed := strings.TrimSpace(candidate); trimmed != "" {
return trimmed
}
}
return ""
}