models.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. package xai
  2. import (
  3. "strings"
  4. "sync/atomic"
  5. )
  6. // runtimeMappingOpts holds operator-configured defaults applied when Grok
  7. // accounts leave credentials.model_mapping empty. Updated from settings.
  8. var runtimeMappingOpts atomic.Value // ModelMappingOptions
  9. var runtimeMappingVersion atomic.Uint64
  10. func init() {
  11. runtimeMappingOpts.Store(ModelMappingOptions{})
  12. runtimeMappingVersion.Store(1)
  13. }
  14. // SetRuntimeModelMappingOptions updates process-wide defaults used by
  15. // DefaultModelMapping (e.g. after settings load). Safe for concurrent use.
  16. func SetRuntimeModelMappingOptions(opts ModelMappingOptions) {
  17. runtimeMappingOpts.Store(opts)
  18. runtimeMappingVersion.Add(1)
  19. }
  20. // RuntimeModelMappingVersion changes whenever runtime mapping options change.
  21. // Account-level caches include it so settings updates take effect without a restart.
  22. func RuntimeModelMappingVersion() uint64 {
  23. return runtimeMappingVersion.Load()
  24. }
  25. // RuntimeModelMappingOptions returns the last options set via SetRuntimeModelMappingOptions.
  26. func RuntimeModelMappingOptions() ModelMappingOptions {
  27. if v := runtimeMappingOpts.Load(); v != nil {
  28. if opts, ok := v.(ModelMappingOptions); ok {
  29. return opts
  30. }
  31. }
  32. return ModelMappingOptions{}
  33. }
  34. // Model describes an xAI model in OpenAI-compatible /models shape.
  35. type Model struct {
  36. ID string `json:"id"`
  37. Object string `json:"object"`
  38. Type string `json:"type,omitempty"`
  39. Created int64 `json:"created,omitempty"`
  40. OwnedBy string `json:"owned_by"`
  41. DisplayName string `json:"display_name,omitempty"`
  42. }
  43. // DefaultTextModel is the built-in fallback for empty model fields and Grok
  44. // text aliases (e.g. "grok", "grok-latest"). Operators may override the runtime
  45. // default via settings key grok_default_text_model.
  46. const DefaultTextModel = "grok-4.6"
  47. // Official Imagine model IDs (https://docs.x.ai/docs/models).
  48. const (
  49. DefaultImagineImageQualityModel = "grok-imagine-image-quality"
  50. DefaultImagineImageFastModel = "grok-imagine-image"
  51. DefaultImagineImage20Model = "grok-imagine-image-2.0"
  52. DefaultImagineVideoModel = "grok-imagine-video"
  53. DefaultImagineVideo15Model = "grok-imagine-video-1.5"
  54. DefaultImagineVideo15LegacyModel = "grok-imagine-video-1.5-preview"
  55. )
  56. // ModelMappingOptions controls optional expansions of the default mapping.
  57. // Cross-client wildcards (gpt-*/claude-*) default ON via settings
  58. // grok_cross_client_model_map_enabled so Codex/Claude clients keep working
  59. // against Grok groups (map to DefaultText / grok-4.6). Operators may disable.
  60. type ModelMappingOptions struct {
  61. // DefaultText is the target for empty models and optional cross-client maps.
  62. // Empty → DefaultTextModel (grok-4.6).
  63. DefaultText string
  64. // EnableCrossClientMap merges gpt-*/codex-*/o*/claude-* → DefaultText.
  65. EnableCrossClientMap bool
  66. }
  67. func (o ModelMappingOptions) defaultText() string {
  68. if t := strings.TrimSpace(o.DefaultText); t != "" {
  69. return t
  70. }
  71. return DefaultTextModel
  72. }
  73. var defaultModels = []Model{
  74. // Text
  75. {ID: "grok-4.6", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.6"},
  76. {ID: "grok-4.5", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.5"},
  77. {ID: "grok-4.3", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.3"},
  78. {ID: "grok-build-0.1", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Build 0.1"},
  79. {ID: "grok-composer-2.5-fast", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Composer 2.5 Fast"},
  80. {ID: "grok-4.20-0309-reasoning", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.20 Reasoning"},
  81. {ID: "grok-4.20-0309-non-reasoning", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.20 Non Reasoning"},
  82. {ID: "grok-4.20-multi-agent-0309", Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok 4.20 Multi Agent"},
  83. // Imagine
  84. {ID: DefaultImagineImageQualityModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image Quality"},
  85. {ID: DefaultImagineImageFastModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image"},
  86. {ID: DefaultImagineImage20Model, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Image 2.0"},
  87. {ID: DefaultImagineVideoModel, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video"},
  88. {ID: DefaultImagineVideo15Model, Object: "model", Type: "model", OwnedBy: "xai", DisplayName: "Grok Imagine Video 1.5"},
  89. }
  90. // grokTextResponsesModelAliases is the source of truth for Grok text models
  91. // accepted by the Responses path: client-facing / undated aliases → canonical
  92. // upstream ID. Used by DefaultModelMapping and IsGrokTextResponsesModelID.
  93. var grokTextResponsesModelAliases = map[string]string{
  94. "grok": DefaultTextModel,
  95. "grok-latest": DefaultTextModel,
  96. "grok-4.6": "grok-4.6",
  97. "grok-4.6-latest": "grok-4.6",
  98. "grok-4.5": "grok-4.5",
  99. "grok-4.5-latest": "grok-4.5",
  100. "grok-4.3": "grok-4.3",
  101. "grok-4.3-latest": "grok-4.3",
  102. "grok-3-mini": "grok-3-mini",
  103. "grok-3-mini-fast": "grok-3-mini-fast",
  104. "grok-build": "grok-build-0.1",
  105. "grok-build-latest": "grok-build-0.1",
  106. "grok-build-0.1": "grok-build-0.1",
  107. "grok-composer-2.5-fast": "grok-composer-2.5-fast",
  108. "grok-composer": "grok-composer-2.5-fast",
  109. "composer-2.5": "grok-composer-2.5-fast",
  110. "grok-4.20-reasoning": "grok-4.20-0309-reasoning",
  111. "grok-4.20-0309-reasoning": "grok-4.20-0309-reasoning",
  112. "grok-4.20-non-reasoning": "grok-4.20-0309-non-reasoning",
  113. "grok-4.20-0309-non-reasoning": "grok-4.20-0309-non-reasoning",
  114. "grok-4.20-multi-agent": "grok-4.20-multi-agent-0309",
  115. "grok-4.20-multi-agent-latest": "grok-4.20-multi-agent-0309",
  116. "grok-4.20-multi-agent-0309": "grok-4.20-multi-agent-0309",
  117. }
  118. func DefaultModels() []Model {
  119. out := make([]Model, len(defaultModels))
  120. copy(out, defaultModels)
  121. return out
  122. }
  123. func DefaultModelIDs() []string {
  124. models := DefaultModels()
  125. ids := make([]string, 0, len(models))
  126. for _, model := range models {
  127. ids = append(ids, model.ID)
  128. }
  129. return ids
  130. }
  131. // DefaultModelMapping returns native Grok/Imagine identity + aliases, using
  132. // runtime options (default text model / optional cross-client wildcards).
  133. // Does NOT enable gpt-*/claude-* unless SetRuntimeModelMappingOptions enables them.
  134. func DefaultModelMapping() map[string]string {
  135. return ModelMappingWithOptions(RuntimeModelMappingOptions())
  136. }
  137. // ModelMappingWithOptions builds the default Grok mapping with optional
  138. // cross-client wildcards and a configurable default text model.
  139. func ModelMappingWithOptions(opts ModelMappingOptions) map[string]string {
  140. defaultText := opts.defaultText()
  141. mapping := make(map[string]string, len(defaultModels)+len(grokTextResponsesModelAliases)+48)
  142. for _, model := range defaultModels {
  143. mapping[model.ID] = model.ID
  144. }
  145. for alias, canonical := range grokTextResponsesModelAliases {
  146. // Remap aliases that pointed at DefaultTextModel constant to runtime default.
  147. if (alias == "grok" || alias == "grok-latest") && canonical == DefaultTextModel {
  148. mapping[alias] = defaultText
  149. } else {
  150. mapping[alias] = canonical
  151. }
  152. }
  153. // Imagine aliases / legacy IDs → official catalog.
  154. mapping["grok-imagine"] = DefaultImagineImageQualityModel
  155. mapping["grok-imagine-1"] = DefaultImagineImageQualityModel
  156. // Backward-compatible client alias; xAI exposes image editing through the
  157. // image-quality model rather than a separate grok-imagine-edit model.
  158. mapping["grok-imagine-edit"] = DefaultImagineImageQualityModel
  159. mapping["grok-imagine-image"] = DefaultImagineImageFastModel
  160. mapping["grok-imagine-image-quality"] = DefaultImagineImageQualityModel
  161. // Keep official IDs as identity so client-requested model strings are not
  162. // rewritten on the wire (pricing still canonicalizes 1.5* via CanonicalImagineVideoModel).
  163. mapping["grok-imagine-video"] = DefaultImagineVideoModel
  164. mapping["grok-imagine-video-1.5"] = DefaultImagineVideo15Model
  165. mapping["grok-imagine-video-1.5-preview"] = DefaultImagineVideo15Model
  166. // Informal alias only:
  167. mapping["grok-video-1.5"] = DefaultImagineVideo15Model
  168. if opts.EnableCrossClientMap {
  169. // Codex / OpenAI Responses client defaults (wildcard patterns).
  170. mapping["gpt-*"] = defaultText
  171. mapping["codex-*"] = defaultText
  172. mapping["o1*"] = defaultText
  173. mapping["o3*"] = defaultText
  174. mapping["o4*"] = defaultText
  175. // Claude Code defaults when operators intentionally enable bridging.
  176. mapping["claude-*"] = defaultText
  177. }
  178. addGrokProviderPrefixedMappings(mapping)
  179. return mapping
  180. }
  181. func addGrokProviderPrefixedMappings(mapping map[string]string) {
  182. snapshot := make(map[string]string, len(mapping))
  183. for key, value := range mapping {
  184. snapshot[key] = value
  185. }
  186. for key, value := range snapshot {
  187. if !isGrokNativeOrAlias(key) {
  188. continue
  189. }
  190. for _, prefix := range []string{"xai/", "x-ai/", "grok/"} {
  191. mapping[prefix+key] = value
  192. }
  193. }
  194. }
  195. func isGrokNativeOrAlias(model string) bool {
  196. model = strings.ToLower(strings.TrimSpace(model))
  197. if model == "" || strings.Contains(model, "*") {
  198. return false
  199. }
  200. return strings.HasPrefix(model, "grok") ||
  201. strings.HasPrefix(model, "imagine") ||
  202. strings.HasPrefix(model, "composer")
  203. }
  204. // StripGrokProviderPrefix removes common provider prefixes accepted for
  205. // xAI/Grok models, returning the native model ID.
  206. func StripGrokProviderPrefix(model string) string {
  207. trimmed := strings.TrimSpace(model)
  208. lower := strings.ToLower(trimmed)
  209. for _, prefix := range []string{"xai/", "x-ai/", "grok/"} {
  210. if strings.HasPrefix(lower, prefix) {
  211. return strings.TrimSpace(trimmed[len(prefix):])
  212. }
  213. }
  214. return trimmed
  215. }
  216. // IsGrokModelID reports whether model looks like a native Grok/xAI model id
  217. // (including aliases). Claude/OpenAI model names return false.
  218. func IsGrokModelID(model string) bool {
  219. normalized := strings.ToLower(StripGrokProviderPrefix(model))
  220. if normalized == "" {
  221. return false
  222. }
  223. if strings.HasPrefix(normalized, "grok") {
  224. return true
  225. }
  226. if strings.HasPrefix(normalized, "imagine") {
  227. return true
  228. }
  229. return false
  230. }
  231. // IsGrokImagineModel reports whether model is a Grok Imagine image or video
  232. // model. These media models cannot act as the primary Codex agent model.
  233. func IsGrokImagineModel(model string) bool {
  234. normalized := strings.ToLower(StripGrokProviderPrefix(model))
  235. if normalized == "" {
  236. return false
  237. }
  238. if strings.HasPrefix(normalized, "imagine") {
  239. return true
  240. }
  241. switch {
  242. case normalized == "grok-imagine",
  243. normalized == "grok-imagine-1",
  244. normalized == "grok-imagine-edit",
  245. normalized == "grok-video-1.5":
  246. return true
  247. case strings.HasPrefix(normalized, "grok-imagine-image"),
  248. strings.HasPrefix(normalized, "grok-imagine-video"):
  249. return true
  250. default:
  251. return false
  252. }
  253. }
  254. // IsGrokTextResponsesModelID reports whether model is a known Grok text model
  255. // for the Responses API. Imagine image/video and unknown custom ids return false.
  256. func IsGrokTextResponsesModelID(model string) bool {
  257. normalized := strings.ToLower(StripGrokProviderPrefix(model))
  258. _, ok := grokTextResponsesModelAliases[normalized]
  259. return ok
  260. }
  261. // ResolveGrokTextResponsesModelID canonicalizes a Grok text alias before upstream.
  262. // empty or bare aliases that resolve via DefaultTextModel use defaultText when set.
  263. func ResolveGrokTextResponsesModelID(model string, defaultText ...string) string {
  264. fallback := DefaultTextModel
  265. if len(defaultText) > 0 && strings.TrimSpace(defaultText[0]) != "" {
  266. fallback = strings.TrimSpace(defaultText[0])
  267. }
  268. trimmed := strings.TrimSpace(model)
  269. if trimmed == "" {
  270. return fallback
  271. }
  272. normalized := strings.ToLower(StripGrokProviderPrefix(trimmed))
  273. if canonical, ok := grokTextResponsesModelAliases[normalized]; ok {
  274. if (normalized == "grok" || normalized == "grok-latest") && canonical == DefaultTextModel {
  275. return fallback
  276. }
  277. return canonical
  278. }
  279. return StripGrokProviderPrefix(trimmed)
  280. }
  281. // ResolveDefaultTextModel returns defaultText (or DefaultTextModel) when model is empty.
  282. func ResolveDefaultTextModel(model string, defaultText ...string) string {
  283. if trimmed := strings.TrimSpace(model); trimmed != "" {
  284. return trimmed
  285. }
  286. if len(defaultText) > 0 && strings.TrimSpace(defaultText[0]) != "" {
  287. return strings.TrimSpace(defaultText[0])
  288. }
  289. return DefaultTextModel
  290. }
  291. // CanonicalImagineVideoModel normalizes video model ids for pricing tables.
  292. // Legacy "grok-imagine-video-1.5" shares the 1.5 price family with preview.
  293. func CanonicalImagineVideoModel(model string) string {
  294. m := strings.ToLower(StripGrokProviderPrefix(model))
  295. switch {
  296. case m == "" || m == DefaultImagineVideoModel || m == "grok-imagine-video-preview":
  297. return DefaultImagineVideoModel
  298. case strings.HasPrefix(m, "grok-imagine-video-1.5") || m == "grok-video-1.5":
  299. return DefaultImagineVideo15Model
  300. default:
  301. return m
  302. }
  303. }