| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582 |
- package service
- import (
- "bytes"
- "context"
- "crypto/sha256"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "sort"
- "strings"
- "sync"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
- "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
- "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
- "golang.org/x/net/http2"
- "golang.org/x/sync/singleflight"
- )
- // chatgptCodexModelsURL is the ChatGPT Codex models manifest endpoint.
- // Package-level variable so tests can point it at a stub server.
- var chatgptCodexModelsURL = "https://chatgpt.com/backend-api/codex/models"
- const (
- openAIModelsCacheBodyLimit = 1 << 20
- // openAIModelsCacheMaxEntries 上限按「账号数 × 客户端版本数 × 代理形态」
- // 估算:缓存同时覆盖 OAuth 与 API Key 账号,且缓存键含 Authorization 与
- // Version 头,不同客户端版本各自占一条;64 条在大规模部署下会被淘汰导致
- // 额外上游请求。单条清单通常几十 KB,512 条最坏内存占用在几十 MB 量级。
- openAIModelsCacheMaxEntries = 512
- // 三段时效:≤TTL 为新鲜(直接返回缓存,零上游请求);TTL 到 StaleTTL 之间
- // 乐观返回旧值并后台单飞刷新(携带上游 ETag,304 续期);超过 StaleTTL 丢弃
- // 缓存同步等待刷新。TTL 取 1 分钟:manifest 变化低频,1 分钟内同账号重复
- // 请求完全吸收;StaleTTL 取 5 分钟,控制旧内容最长可见时间在分钟级。
- openAIModelsCacheTTL = 60 * time.Second
- openAIModelsCacheStaleTTL = 5 * time.Minute
- codexModelsManifestRequestTimeout = 15 * time.Second
- codexAutoModelPrefix = "codex-auto-"
- )
- // FilterCodexModelIDsForGroup removes dedicated media-generation models,
- // wildcard mapping keys, and Codex automatic modes from a client catalog.
- // Automatic modes are retained only when the group's enabled model allowlist
- // explicitly selects the exact slug; account model mappings describe routing
- // and are not feature opt-ins. Wildcard keys such as "foo-*" are routing
- // patterns, not concrete Codex models. When the allowlist is enabled the
- // catalog is additionally restricted by FilterForListing (wildcard entries
- // expand against the catalog).
- func FilterCodexModelIDsForGroup(modelIDs []string, group *Group) []string {
- explicitlyEnabled := make(map[string]struct{})
- if group != nil && group.ModelAllowlistEnabled() {
- for _, modelID := range group.ModelAllowlist.Models {
- modelID = strings.TrimSpace(modelID)
- if strings.HasPrefix(modelID, codexAutoModelPrefix) {
- explicitlyEnabled[modelID] = struct{}{}
- }
- }
- }
- filtered := make([]string, 0, len(modelIDs))
- for _, modelID := range modelIDs {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- continue
- }
- if isCodexDedicatedMediaModel(modelID) {
- continue
- }
- if strings.Contains(modelID, "*") {
- continue
- }
- if strings.HasPrefix(modelID, codexAutoModelPrefix) {
- if _, ok := explicitlyEnabled[modelID]; !ok {
- continue
- }
- }
- filtered = append(filtered, modelID)
- }
- if group != nil && group.ModelAllowlistEnabled() {
- filtered = group.ModelAllowlist.FilterForListing(filtered)
- }
- return filtered
- }
- func isCodexDedicatedMediaModel(modelID string) bool {
- canonical := codexProviderQualifiedModelID(modelID)
- return IsGPTImageGenerationModel(canonical) ||
- isImageGenerationModel(canonical) ||
- xai.IsGrokImagineModel(modelID)
- }
- func codexProviderQualifiedModelID(modelID string) string {
- modelID = strings.TrimSpace(modelID)
- if slash := strings.LastIndexByte(modelID, '/'); slash >= 0 {
- modelID = strings.TrimSpace(modelID[slash+1:])
- }
- return strings.TrimPrefix(modelID, "models/")
- }
- // OpenAIModelsResponse carries either an OpenAI model list or a Codex manifest
- // together with upstream and client caching metadata.
- type OpenAIModelsResponse struct {
- Body []byte
- ETag string
- upstreamETag string
- upstreamSourceBody []byte
- convertedFromOpenAIModelList bool
- NotModified bool
- }
- // BuildGroupConfiguredCodexModelsManifest builds a Codex catalog from configured
- // public model names, supplemented by defaults for unmapped OpenAI accounts. The
- // boolean result distinguishes "no explicit configuration" from a configured
- // catalog that becomes empty after group-level filtering.
- func (s *OpenAIGatewayService) BuildGroupConfiguredCodexModelsManifest(
- ctx context.Context,
- group *Group,
- ifNoneMatch string,
- ) (*OpenAIModelsResponse, bool, error) {
- if s == nil || s.accountRepo == nil || group == nil || group.Platform != PlatformOpenAI {
- return nil, false, nil
- }
- visible, catalog, err := loadCodexGroupCatalogAccounts(ctx, s.accountRepo, group.ID)
- if err != nil {
- return nil, false, fmt.Errorf("load group configured Codex models: %w", err)
- }
- configuredModels := openAIConfiguredCodexModelIDsForGroup(visible, group)
- if len(configuredModels) == 0 {
- return nil, false, nil
- }
- body, err := buildCodexModelsManifestForAccounts(
- PlatformOpenAI,
- configuredModels,
- catalog,
- group,
- nil,
- true,
- )
- if err != nil {
- return nil, false, fmt.Errorf("initialize group configured Codex models: %w", err)
- }
- body, _, err = mergeConfiguredCodexModelsManifest(
- body,
- nil,
- group.ModelAllowlist.Models,
- group.ModelAllowlistEnabled(),
- )
- if err != nil {
- return nil, false, fmt.Errorf("build group configured Codex models: %w", err)
- }
- manifest := &OpenAIModelsResponse{
- Body: body,
- ETag: codexModelsManifestBodyETag(body),
- }
- if codexModelsManifestETagMatches(ifNoneMatch, manifest.ETag) {
- manifest.Body = nil
- manifest.NotModified = true
- }
- return manifest, true, nil
- }
- // MergeGroupConfiguredCodexModels adds account model aliases that are visible
- // to the authenticated OpenAI group without discarding metadata from upstream
- // Codex model entries. A group's custom models list also filters the picker,
- // matching the standard /v1/models display policy.
- func (s *OpenAIGatewayService) MergeGroupConfiguredCodexModels(
- ctx context.Context,
- group *Group,
- manifest *OpenAIModelsResponse,
- ifNoneMatch string,
- ) error {
- if s == nil || s.accountRepo == nil || group == nil || manifest == nil || manifest.NotModified {
- return nil
- }
- if group.Platform != PlatformOpenAI || len(manifest.Body) == 0 {
- return nil
- }
- var configuredModels []string
- if !group.CodexModelsManifestConfig.Enabled {
- var err error
- configuredModels, err = s.groupConfiguredCodexModelIDs(ctx, group)
- if err != nil {
- return fmt.Errorf("load group configured Codex models: %w", err)
- }
- }
- body, changed, err := mergeConfiguredCodexModelsManifest(
- manifest.Body,
- configuredModels,
- group.ModelAllowlist.Models,
- group.ModelAllowlistEnabled(),
- )
- if err != nil {
- return fmt.Errorf("merge group configured Codex models: %w", err)
- }
- if group.CodexModelsManifestConfig.Enabled && group.ModelAllowlistEnabled() {
- body, err = orderPinnedCodexModelsBySelection(body, group.ModelAllowlist)
- if err != nil {
- return fmt.Errorf("order pinned Codex models: %w", err)
- }
- changed = true
- }
- if changed {
- manifest.Body = body
- manifest.ETag = codexModelsManifestBodyETag(body)
- }
- if codexModelsManifestETagMatches(ifNoneMatch, manifest.ETag) {
- manifest.Body = nil
- manifest.NotModified = true
- }
- return nil
- }
- func (s *OpenAIGatewayService) groupConfiguredCodexModelIDs(ctx context.Context, group *Group) ([]string, error) {
- if group == nil {
- return nil, nil
- }
- accounts, err := s.accountRepo.ListSchedulableByGroupID(ctx, group.ID)
- if err != nil {
- return nil, err
- }
- return openAIConfiguredCodexModelIDsForGroup(accounts, group), nil
- }
- // loadCodexGroupCatalogAccounts separates picker membership from capability
- // intersection. visible accounts are currently schedulable and decide which
- // public aliases appear. catalog accounts are persistently enabled group
- // members; the availability query ignores transient rate-limit, overload, and
- // temporary-unschedulable state so those conditions cannot widen advertised
- // capabilities. Persistently disabled accounts are excluded because routing
- // cannot select them. If the availability query fails, the catalog falls back
- // to the schedulable set so a listing error does not fail the client request.
- func loadCodexGroupCatalogAccounts(ctx context.Context, repo AccountRepository, groupID int64) (visible []Account, catalog []Account, err error) {
- if repo == nil {
- return nil, nil, nil
- }
- visible, err = repo.ListSchedulableByGroupID(ctx, groupID)
- if err != nil {
- return nil, nil, err
- }
- catalog = visible
- groupAccounts, listErr := repo.ListModelAvailabilityCandidates(
- ctx,
- &groupID,
- []string{
- PlatformAnthropic,
- PlatformOpenAI,
- PlatformGemini,
- PlatformAntigravity,
- PlatformGrok,
- PlatformKimi,
- PlatformZhipu,
- PlatformDeepseek,
- PlatformMiniMax,
- },
- false,
- )
- if listErr != nil {
- return visible, catalog, nil
- }
- return visible, groupAccounts, nil
- }
- func openAIConfiguredCodexModelIDs(accounts []Account) []string {
- seen := make(map[string]struct{})
- models := make([]string, 0)
- for i := range accounts {
- account := &accounts[i]
- if account.Platform != PlatformOpenAI {
- continue
- }
- for modelID := range account.GetModelMapping() {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" || strings.Contains(modelID, "*") {
- continue
- }
- if _, exists := seen[modelID]; exists {
- continue
- }
- seen[modelID] = struct{}{}
- models = append(models, modelID)
- }
- }
- sort.Strings(models)
- return models
- }
- func openAIConfiguredCodexModelIDsForGroup(accounts []Account, group *Group) []string {
- models := supplementUnmappedOpenAIModels(accounts, openAIConfiguredCodexModelIDs(accounts))
- if group == nil || !group.ModelAllowlistEnabled() {
- return models
- }
- seen := make(map[string]struct{}, len(models)+len(group.ModelAllowlist.Models))
- for _, modelID := range models {
- seen[modelID] = struct{}{}
- }
- for _, selectedModel := range group.ModelAllowlist.Models {
- selectedModel = strings.TrimSpace(selectedModel)
- if selectedModel == "" || strings.Contains(selectedModel, "*") {
- continue
- }
- for i := range accounts {
- account := &accounts[i]
- if account.Platform != PlatformOpenAI {
- continue
- }
- mappedModel, matched := account.ResolveMappedModel(selectedModel)
- if !matched || strings.TrimSpace(mappedModel) == "" {
- continue
- }
- if _, exists := seen[selectedModel]; !exists {
- seen[selectedModel] = struct{}{}
- models = append(models, selectedModel)
- }
- break
- }
- }
- sort.Strings(models)
- return models
- }
- const (
- configuredCodexModelPriority = 50
- configuredCodexCustomDescription = "Custom model routed through Sub2API."
- configuredCodexFallbackContext = 272_000
- configuredCodexDeepSeekV4Context = 1_000_000
- configuredCodexGrokContext = 500_000
- configuredCodexGrokBuildContext = 256_000
- configuredCodexGPT56MaxContext = 872_000
- configuredCodexGPT6AstraContext = 1_050_000
- configuredCodexToolOutputMaxTokens = 10_000
- )
- type configuredCodexReasoningLevel struct {
- Effort string `json:"effort"`
- Description string `json:"description"`
- }
- type configuredCodexTruncationPolicy struct {
- Mode string `json:"mode"`
- Limit int64 `json:"limit"`
- }
- type configuredCodexServiceTier struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- }
- type configuredCodexModelMessages struct {
- InstructionsTemplate string `json:"instructions_template"`
- InstructionsVariables any `json:"instructions_variables"`
- Approvals any `json:"approvals"`
- CollaborationModes any `json:"collaboration_modes"`
- AutoReview any `json:"auto_review"`
- Permissions any `json:"permissions"`
- MultiAgent any `json:"multi_agent"`
- TokenBudget any `json:"token_budget"`
- GuardianV2 any `json:"guardian_v2"`
- }
- // configuredCodexModelDescriptor is the minimum complete ModelInfo contract
- // understood by current Codex clients. Several nullable fields are intentionally
- // emitted: unlike ordinary OpenAI /v1/models entries, the Codex manifest parser
- // requires them to be present.
- type configuredCodexModelDescriptor struct {
- Slug string `json:"slug"`
- DisplayName string `json:"display_name"`
- Description string `json:"description"`
- DefaultReasoningLevel *string `json:"default_reasoning_level,omitempty"`
- SupportedReasoningLevels []configuredCodexReasoningLevel `json:"supported_reasoning_levels"`
- MultiAgentReasoningEffort *string `json:"multi_agent_reasoning_effort,omitempty"`
- ShellType string `json:"shell_type"`
- Visibility string `json:"visibility"`
- SupportedInAPI bool `json:"supported_in_api"`
- Priority int `json:"priority"`
- AdditionalSpeedTiers []string `json:"additional_speed_tiers"`
- ServiceTiers []configuredCodexServiceTier `json:"service_tiers"`
- DefaultServiceTier any `json:"default_service_tier"`
- AvailabilityNUX any `json:"availability_nux"`
- Upgrade any `json:"upgrade"`
- ModelMessages configuredCodexModelMessages `json:"model_messages"`
- IncludeSkillsUsageInstructions bool `json:"include_skills_usage_instructions"`
- IncludePluginUsageInstructions bool `json:"include_plugin_usage_instructions"`
- IncludeAppsUsageInstructions bool `json:"include_apps_usage_instructions"`
- SupportsReasoningSummaryParameter bool `json:"supports_reasoning_summary_parameter"`
- DefaultReasoningSummary string `json:"default_reasoning_summary"`
- SupportVerbosity bool `json:"support_verbosity"`
- DefaultVerbosity *string `json:"default_verbosity"`
- ApplyPatchToolType *string `json:"apply_patch_tool_type"`
- WebSearchToolType string `json:"web_search_tool_type"`
- TruncationPolicy configuredCodexTruncationPolicy `json:"truncation_policy"`
- SupportsImageDetailOriginal bool `json:"supports_image_detail_original"`
- SupportsParallelToolCalls bool `json:"supports_parallel_tool_calls"`
- ContextWindow int64 `json:"context_window"`
- MaxContextWindow int64 `json:"max_context_window"`
- AutoCompactTokenLimit any `json:"auto_compact_token_limit"`
- CompHash any `json:"comp_hash"`
- EffectiveContextWindowPercent int64 `json:"effective_context_window_percent"`
- ExperimentalSupportedTools []string `json:"experimental_supported_tools"`
- InputModalities []string `json:"input_modalities"`
- SupportsSearchTool bool `json:"supports_search_tool"`
- UseResponsesLite bool `json:"use_responses_lite"`
- NodeREPLAutoReviewRequired bool `json:"node_repl_auto_review_required"`
- NodeREPLDisabled bool `json:"node_repl_disabled"`
- AutoReviewModelOverride any `json:"auto_review_model_override"`
- ModelSpecialty any `json:"model_specialty"`
- ToolMode any `json:"tool_mode"`
- MultiAgentVersion any `json:"multi_agent_version"`
- }
- type codexModelMetadataOverride struct {
- UpstreamModelMetadata
- reasoningConflict bool
- inputModalitiesConflict bool
- }
- func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescriptor {
- modelID = strings.TrimSpace(modelID)
- noReasoningLevel := "none"
- descriptor := configuredCodexModelDescriptor{
- Slug: modelID,
- DisplayName: modelID,
- Description: configuredCodexCustomDescription,
- DefaultReasoningLevel: &noReasoningLevel,
- SupportedReasoningLevels: []configuredCodexReasoningLevel{
- {Effort: "none", Description: configuredCodexReasoningLevelDescription("none")},
- },
- ShellType: "unified_exec",
- Visibility: "list",
- SupportedInAPI: true,
- Priority: configuredCodexModelPriority,
- AdditionalSpeedTiers: []string{},
- ServiceTiers: []configuredCodexServiceTier{},
- ModelMessages: configuredCodexModelMessages{InstructionsTemplate: openai.CodexBaseInstructionsForModel(modelID)},
- SupportsReasoningSummaryParameter: true,
- DefaultReasoningSummary: "auto",
- WebSearchToolType: "text",
- TruncationPolicy: configuredCodexTruncationPolicy{Mode: "bytes", Limit: configuredCodexToolOutputMaxTokens},
- ContextWindow: configuredCodexFallbackContext,
- MaxContextWindow: configuredCodexFallbackContext,
- EffectiveContextWindowPercent: 95,
- ExperimentalSupportedTools: []string{},
- InputModalities: []string{"text"},
- }
- if isDeepSeekCodexModel(modelID) {
- defaultReasoningLevel := "high"
- descriptor.DisplayName = deepSeekCodexDisplayName(modelID)
- descriptor.Description = "DeepSeek coding and reasoning model routed through Sub2API."
- descriptor.DefaultReasoningLevel = &defaultReasoningLevel
- descriptor.SupportedReasoningLevels = []configuredCodexReasoningLevel{
- {Effort: "low", Description: "Fast responses with lighter reasoning"},
- {Effort: "high", Description: "Greater reasoning depth for coding and agent tasks"},
- {Effort: "max", Description: "Maximum reasoning depth for complex tasks"},
- }
- descriptor.SupportsParallelToolCalls = true
- descriptor.ContextWindow = configuredCodexDeepSeekV4Context
- descriptor.MaxContextWindow = configuredCodexDeepSeekV4Context
- }
- if isGrokCodexModel(modelID) {
- descriptor.DisplayName = grokCodexDisplayName(modelID)
- descriptor.Description = "Grok coding and reasoning model routed through Sub2API."
- descriptor.SupportsParallelToolCalls = true
- descriptor.ContextWindow = grokCodexContextWindow(modelID)
- descriptor.MaxContextWindow = descriptor.ContextWindow
- if grokCodexSupportsReasoningEffort(modelID) {
- defaultReasoningLevel := "high"
- descriptor.DefaultReasoningLevel = &defaultReasoningLevel
- descriptor.SupportedReasoningLevels = configuredCodexGrokReasoningLevels(modelID)
- }
- }
- if isClaudeCodexModel(modelID) {
- descriptor.DisplayName = claudeCodexDisplayName(modelID)
- descriptor.Description = "Claude coding and reasoning model routed through Sub2API."
- descriptor.SupportsParallelToolCalls = true
- if levels := configuredCodexClaudeReasoningLevels(modelID); len(levels) > 0 {
- defaultReasoningLevel := claudeCodexDefaultReasoningLevel(levels)
- descriptor.DefaultReasoningLevel = &defaultReasoningLevel
- descriptor.SupportedReasoningLevels = levels
- }
- }
- if isOpenAICodexGPTModel(modelID) {
- descriptor.DisplayName = openaiCodexDisplayName(modelID)
- descriptor.Description = "OpenAI GPT coding model routed through Sub2API."
- descriptor.SupportsParallelToolCalls = true
- descriptor.ServiceTiers = configuredCodexServiceTiersForModel(modelID)
- if isOpenAICodexReasoningGPTModel(modelID) {
- defaultReasoningLevel := "medium"
- if getNormalizedCodexModel(modelID) == "gpt-5.6-sol" {
- defaultReasoningLevel = "low"
- }
- descriptor.DefaultReasoningLevel = &defaultReasoningLevel
- descriptor.SupportedReasoningLevels = configuredCodexGPTReasoningLevels(modelID)
- descriptor.DefaultReasoningSummary = "none"
- descriptor.TruncationPolicy = configuredCodexTruncationPolicy{Mode: "tokens", Limit: configuredCodexToolOutputMaxTokens}
- if isOpenAIGPT56Model(modelID) {
- descriptor.MaxContextWindow = configuredCodexGPT56MaxContext
- }
- if isOpenAIGPT6AstraModel(modelID) {
- // Codex resolves the Ultra workflow to this effort before inference.
- // openai/codex a9896da3: codex-rs/models-manager/models.json.
- multiAgentEffort := "xhigh"
- descriptor.MultiAgentReasoningEffort = &multiAgentEffort
- descriptor.MultiAgentVersion = "v2"
- descriptor.ContextWindow = configuredCodexGPT6AstraContext
- descriptor.MaxContextWindow = configuredCodexGPT6AstraContext
- }
- }
- if SupportsVerbosity(modelID) {
- defaultVerbosity := "low"
- descriptor.SupportVerbosity = true
- descriptor.DefaultVerbosity = &defaultVerbosity
- }
- }
- return descriptor
- }
- func configuredCodexServiceTiersForModel(modelID string) []configuredCodexServiceTier {
- tiers := make([]configuredCodexServiceTier, 0, 2)
- if configuredCodexSupportsPriorityServiceTier(modelID) {
- tiers = append(tiers, configuredCodexServiceTier{
- ID: OpenAIFastTierPriority,
- Name: "Fast",
- Description: "Priority processing for lower latency.",
- })
- }
- if configuredCodexSupportsUltrafastServiceTier(modelID) {
- tiers = append(tiers, configuredCodexServiceTier{
- ID: OpenAIFastTierUltrafast,
- Name: "Ultrafast",
- Description: "Ultra-low latency processing.",
- })
- }
- return tiers
- }
- func configuredCodexSupportsPriorityServiceTier(modelID string) bool {
- normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
- for _, family := range []string{"gpt-5.4", "gpt-5.5", "gpt-5.6"} {
- if normalized == family || strings.HasPrefix(normalized, family+"-") {
- return true
- }
- }
- // GPT-6 Astra advertises Fast via service_tier=priority in public model metadata.
- return isOpenAIGPT6AstraModel(modelID)
- }
- func configuredCodexSupportsUltrafastServiceTier(modelID string) bool {
- return normalizeKnownOpenAICodexModel(modelID) == "gpt-5.6-sol"
- }
- func configuredCodexGrokReasoningLevels(modelID string) []configuredCodexReasoningLevel {
- levels := []configuredCodexReasoningLevel{
- {Effort: "low", Description: "Fast responses with lighter reasoning"},
- {Effort: "medium", Description: "Balanced reasoning for most coding tasks"},
- {Effort: "high", Description: "Greater reasoning depth for coding and agent tasks"},
- }
- if GrokSupportsXHighReasoningEffort(modelID) {
- levels = append(levels, configuredCodexReasoningLevel{
- Effort: "xhigh",
- Description: "Extra-high reasoning depth for difficult tasks",
- })
- }
- return levels
- }
- func configuredCodexClaudeReasoningLevels(modelID string) []configuredCodexReasoningLevel {
- descriptions := map[string]string{
- "low": "Fast responses with lighter reasoning",
- "medium": "Balanced reasoning for most coding tasks",
- "high": "Greater reasoning depth for coding and agent tasks",
- "xhigh": "Extra-high reasoning depth for difficult tasks",
- "max": "Maximum reasoning depth for complex tasks",
- }
- levels := claude.EffortLevelsForModel(modelID)
- out := make([]configuredCodexReasoningLevel, 0, len(levels))
- for _, effort := range levels {
- out = append(out, configuredCodexReasoningLevel{
- Effort: effort,
- Description: descriptions[effort],
- })
- }
- return out
- }
- func claudeCodexDefaultReasoningLevel(levels []configuredCodexReasoningLevel) string {
- for _, preferred := range []string{"medium", "high", "low"} {
- for _, level := range levels {
- if level.Effort == preferred {
- return preferred
- }
- }
- }
- if len(levels) == 0 {
- return ""
- }
- return levels[0].Effort
- }
- func configuredCodexGPTReasoningLevels(modelID string) []configuredCodexReasoningLevel {
- levels := []configuredCodexReasoningLevel{
- {Effort: "low", Description: "Fast responses with lighter reasoning"},
- {Effort: "medium", Description: "Balanced reasoning for most coding tasks"},
- {Effort: "high", Description: "Greater reasoning depth for coding and agent tasks"},
- {Effort: "xhigh", Description: "Extra-high reasoning depth for difficult tasks"},
- }
- normalized := getNormalizedCodexModel(modelID)
- if isOpenAIGPT56Model(modelID) || isOpenAIGPT6AstraModel(modelID) {
- levels = append(levels, configuredCodexReasoningLevel{
- Effort: "max",
- Description: "Maximum reasoning depth for complex tasks",
- })
- }
- if isOpenAIGPT6AstraModel(modelID) || normalized == "gpt-5.6-sol" || normalized == "gpt-5.6-terra" {
- levels = append(levels, configuredCodexReasoningLevel{
- Effort: "ultra",
- Description: "Maximum reasoning with automatic task delegation",
- })
- }
- return levels
- }
- func isOpenAICodexGPTModel(modelID string) bool {
- normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
- if normalized == "" || strings.HasPrefix(normalized, "gpt-image") {
- return false
- }
- return strings.HasPrefix(normalized, "gpt-")
- }
- func isOpenAICodexReasoningGPTModel(modelID string) bool {
- normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
- return isOpenAIGPT6AstraModel(normalized) || strings.HasPrefix(normalized, "gpt-5")
- }
- func isOpenAICodexImageInputModel(modelID string) bool {
- normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
- return isOpenAIGPT6AstraModel(normalized) ||
- strings.HasPrefix(normalized, "gpt-5") ||
- strings.HasPrefix(normalized, "gpt-4o") ||
- strings.HasPrefix(normalized, "gpt-4.1") ||
- strings.HasPrefix(normalized, "gpt-4.5") ||
- strings.HasPrefix(normalized, "gpt-4-turbo") ||
- strings.HasPrefix(normalized, "gpt-4-vision")
- }
- func isOfficialOpenAICodexCatalogModel(modelID string) bool {
- normalized := strings.ToLower(codexProviderQualifiedModelID(modelID))
- if normalized == "" || isCodexDedicatedMediaModel(normalized) {
- return false
- }
- if strings.HasPrefix(normalized, "codex-") {
- return true
- }
- if strings.HasPrefix(normalized, "o1") || strings.HasPrefix(normalized, "o3") || strings.HasPrefix(normalized, "o4") {
- return true
- }
- if !strings.HasPrefix(normalized, "gpt-") {
- return false
- }
- for _, incompatibleFamily := range []string{"audio", "realtime", "transcribe", "tts"} {
- if strings.Contains(normalized, incompatibleFamily) {
- return false
- }
- }
- return true
- }
- func openaiCodexDisplayName(modelID string) string {
- normalized := canonicalizeOpenAIModelAliasSpelling(modelID)
- if normalized == "" {
- return modelID
- }
- for _, model := range openai.DefaultModels {
- if strings.EqualFold(model.ID, normalized) && strings.TrimSpace(model.DisplayName) != "" {
- return model.DisplayName
- }
- }
- return modelID
- }
- func deepSeekCodexDisplayName(modelID string) string {
- switch strings.ToLower(strings.TrimSpace(modelID)) {
- case "deepseek-v4-pro", "deepseek-4-pro":
- return "DeepSeek V4 Pro"
- case "deepseek-v4-flash", "deepseek-4-flash":
- return "DeepSeek V4 Flash"
- default:
- return modelID
- }
- }
- func isDeepSeekCodexModel(modelID string) bool {
- return strings.HasPrefix(strings.ToLower(strings.TrimSpace(modelID)), "deepseek-")
- }
- func isGrokCodexModel(modelID string) bool {
- return xai.IsGrokModelID(modelID)
- }
- func grokCodexSupportsReasoningEffort(modelID string) bool {
- if grokSupportsReasoningEffort(modelID) {
- return true
- }
- canonical := xai.ResolveGrokTextResponsesModelID(modelID)
- if canonical == "" || strings.EqualFold(canonical, modelID) {
- return false
- }
- return grokSupportsReasoningEffort(canonical)
- }
- func grokCodexDisplayName(modelID string) string {
- normalized := strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(modelID)))
- if normalized == "" {
- return modelID
- }
- if name := grokDefaultDisplayName(normalized); name != "" {
- return name
- }
- canonical := strings.ToLower(xai.ResolveGrokTextResponsesModelID(normalized))
- if canonical != "" && canonical != normalized {
- if name := grokDefaultDisplayName(canonical); name != "" {
- return name
- }
- }
- return modelID
- }
- func grokDefaultDisplayName(modelID string) string {
- for _, model := range xai.DefaultModels() {
- if model.ID == modelID {
- return strings.TrimSpace(model.DisplayName)
- }
- }
- return ""
- }
- func grokCodexContextWindow(modelID string) int64 {
- normalized := strings.ToLower(xai.StripGrokProviderPrefix(strings.TrimSpace(modelID)))
- if strings.HasPrefix(normalized, "grok-build") {
- return configuredCodexGrokBuildContext
- }
- return configuredCodexGrokContext
- }
- func isClaudeCodexModel(modelID string) bool {
- platform, detected := DetectModelPlatform(modelID)
- return detected && platform == PlatformAnthropic
- }
- func claudeCodexDisplayName(modelID string) string {
- normalized := strings.ToLower(codexProviderQualifiedModelID(modelID))
- normalized = strings.TrimPrefix(normalized, "anthropic.")
- if normalized == "" {
- return modelID
- }
- for _, model := range claude.DefaultModels {
- if strings.EqualFold(model.ID, normalized) && strings.TrimSpace(model.DisplayName) != "" {
- return model.DisplayName
- }
- }
- if canonical, ok := claude.ModelIDOverrides[normalized]; ok {
- for _, model := range claude.DefaultModels {
- if model.ID == canonical && strings.TrimSpace(model.DisplayName) != "" {
- return model.DisplayName
- }
- }
- }
- return modelID
- }
- // BuildCodexModelsManifest builds a standalone Codex model catalog for models
- // routed through a custom provider. The response is also suitable for saving
- // as model_catalog_json in clients that do not refresh custom-provider catalogs.
- func BuildCodexModelsManifest(modelIDs []string) ([]byte, error) {
- return buildCodexModelsManifest(modelIDs, nil, nil, nil, nil)
- }
- // BuildCodexModelsManifestForGroup derives input capabilities from the
- // concrete Responses route and group accounts behind a group. Unknown or mixed
- // capabilities fail closed to the text-only descriptor used by the standalone
- // builder. Caller-supplied model IDs still decide which slugs appear; advertised
- // capabilities intersect all active group members that map the alias, including
- // accounts that are not currently schedulable.
- func (s *GatewayService) BuildCodexModelsManifestForGroup(
- ctx context.Context,
- group *Group,
- platformOverride string,
- modelIDs []string,
- ) ([]byte, error) {
- if s == nil || s.accountRepo == nil || group == nil {
- return BuildCodexModelsManifest(modelIDs)
- }
- effectivePlatform := strings.TrimSpace(platformOverride)
- if effectivePlatform == "" {
- effectivePlatform = group.Platform
- }
- if effectivePlatform != PlatformComposite && !isConcreteRequestPlatform(effectivePlatform) {
- return BuildCodexModelsManifest(modelIDs)
- }
- _, catalog, err := loadCodexGroupCatalogAccounts(ctx, s.accountRepo, group.ID)
- if err != nil {
- return BuildCodexModelsManifest(modelIDs)
- }
- var compositeRoutes []CompositeModelRoute
- compositeRoutesAvailable := true
- if effectivePlatform == PlatformComposite && s.compositeResolver != nil && s.compositeResolver.repo != nil {
- compositeRoutes, err = s.compositeResolver.repo.ListByGroup(ctx, group.ID, false)
- if err != nil {
- compositeRoutesAvailable = false
- }
- }
- return buildCodexModelsManifestForAccounts(
- effectivePlatform,
- modelIDs,
- catalog,
- group,
- compositeRoutes,
- compositeRoutesAvailable,
- )
- }
- func buildCodexModelsManifestForAccounts(
- effectivePlatform string,
- modelIDs []string,
- accounts []Account,
- group *Group,
- compositeRoutes []CompositeModelRoute,
- compositeRoutesAvailable bool,
- ) ([]byte, error) {
- imageInputModels := make(map[string]bool, len(modelIDs))
- searchToolModels := make(map[string]bool, len(modelIDs))
- metadataModels := codexCatalogMetadataModels(
- effectivePlatform,
- modelIDs,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- )
- modelMetadata := make(map[string]codexModelMetadataOverride, len(modelIDs))
- for _, modelID := range modelIDs {
- modelID = strings.TrimSpace(modelID)
- if groupCodexModelSupportsImageInput(
- effectivePlatform,
- modelID,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- ) {
- imageInputModels[modelID] = true
- }
- if groupCodexModelSupportsSearchTool(
- effectivePlatform,
- modelID,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- ) {
- searchToolModels[modelID] = true
- }
- if metadata, ok := groupCodexModelMetadata(
- effectivePlatform,
- modelID,
- accounts,
- group,
- compositeRoutes,
- compositeRoutesAvailable,
- ); ok {
- modelMetadata[modelID] = metadata
- }
- }
- return buildCodexModelsManifest(modelIDs, imageInputModels, searchToolModels, metadataModels, modelMetadata)
- }
- func buildCodexModelsManifest(
- modelIDs []string,
- imageInputModels map[string]bool,
- searchToolModels map[string]bool,
- metadataModels map[string]string,
- modelMetadata map[string]codexModelMetadataOverride,
- ) ([]byte, error) {
- seen := make(map[string]struct{}, len(modelIDs))
- models := make([]json.RawMessage, 0, len(modelIDs))
- for _, modelID := range modelIDs {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- continue
- }
- if _, exists := seen[modelID]; exists {
- continue
- }
- metadataModelID := strings.TrimSpace(metadataModels[modelID])
- if metadataModelID == "" {
- metadataModelID = modelID
- }
- if isCodexDedicatedMediaModel(modelID) || isCodexDedicatedMediaModel(metadataModelID) {
- continue
- }
- seen[modelID] = struct{}{}
- descriptor := newConfiguredCodexModelDescriptor(metadataModelID)
- descriptor.Slug = modelID
- descriptor.SupportsSearchTool = searchToolModels[modelID]
- if metadata, ok := modelMetadata[modelID]; ok {
- applyUpstreamModelMetadataToCodexDescriptor(&descriptor, metadata)
- }
- if imageInputModels[modelID] {
- // Apply the capability-derived modality after upstream metadata so
- // a stale official Astra snapshot cannot downgrade the catalog.
- descriptor.InputModalities = []string{"text", "image"}
- }
- if metadataModelID != modelID {
- descriptor.DisplayName = modelID
- descriptor.Description = configuredCodexCustomDescription
- }
- encoded, err := json.Marshal(descriptor)
- if err != nil {
- return nil, err
- }
- if capabilities := modelMetadata[modelID].CodexToolCapabilities; len(capabilities) > 0 {
- var fields map[string]json.RawMessage
- if err := json.Unmarshal(encoded, &fields); err != nil {
- return nil, err
- }
- applyCodexToolCapabilities(fields, capabilities, true)
- encoded, err = json.Marshal(fields)
- if err != nil {
- return nil, err
- }
- }
- models = append(models, encoded)
- }
- return json.Marshal(struct {
- Models []json.RawMessage `json:"models"`
- }{Models: models})
- }
- func codexCatalogMetadataModels(
- platform string,
- modelIDs []string,
- accounts []Account,
- compositeRoutes []CompositeModelRoute,
- compositeRoutesAvailable bool,
- ) map[string]string {
- metadataModels := make(map[string]string, len(modelIDs))
- for _, modelID := range modelIDs {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- continue
- }
- metadataModelID := resolveCodexCatalogMetadataModel(
- platform,
- modelID,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- )
- if metadataModelID != "" && metadataModelID != modelID {
- metadataModels[modelID] = metadataModelID
- }
- }
- return metadataModels
- }
- func resolveCodexCatalogMetadataModel(
- platform string,
- modelID string,
- accounts []Account,
- compositeRoutes []CompositeModelRoute,
- compositeRoutesAvailable bool,
- ) string {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- return ""
- }
- if platform == PlatformComposite {
- if !compositeRoutesAvailable {
- return modelID
- }
- if route, matched := matchCompositeRoute(compositeRoutes, modelID, CompositeRouteEndpointResponses); matched {
- if upstreamModel := strings.TrimSpace(route.UpstreamModel); upstreamModel != "" {
- return upstreamModel
- }
- return modelID
- }
- if codexCompositeRouteMatchesModel(compositeRoutes, modelID) {
- return modelID
- }
- claimedPlatforms := make(map[string]struct{})
- for _, account := range accounts {
- accountPlatform := strings.TrimSpace(account.Platform)
- if !isConcreteRequestPlatform(accountPlatform) || !codexExplicitModelMappingClaims(account, modelID) {
- continue
- }
- claimedPlatforms[accountPlatform] = struct{}{}
- }
- if len(claimedPlatforms) > 1 {
- return modelID
- }
- for accountPlatform := range claimedPlatforms {
- return uniqueCodexMappedModel(accounts, accountPlatform, modelID)
- }
- detectedPlatform, detected := DetectModelPlatform(modelID)
- if !detected {
- return modelID
- }
- platform = detectedPlatform
- }
- return uniqueCodexMappedModel(accounts, platform, modelID)
- }
- func uniqueCodexMappedModel(accounts []Account, platform string, modelID string) string {
- targets := make(map[string]struct{})
- for i := range accounts {
- account := &accounts[i]
- if account.Platform != platform {
- continue
- }
- mappedModel, matched := account.ResolveMappedModel(modelID)
- mappedModel = strings.TrimSpace(mappedModel)
- if !matched || mappedModel == "" {
- continue
- }
- targets[mappedModel] = struct{}{}
- }
- if len(targets) != 1 {
- return modelID
- }
- for target := range targets {
- return target
- }
- return modelID
- }
- func groupCodexModelSupportsImageInput(
- platform string,
- modelID string,
- accounts []Account,
- compositeRoutes []CompositeModelRoute,
- compositeRoutesAvailable bool,
- ) bool {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- return false
- }
- upstreamModel := modelID
- if platform == PlatformComposite {
- var resolved bool
- platform, upstreamModel, resolved = resolveCodexCompositeModelTarget(
- modelID,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- )
- if !resolved {
- return false
- }
- }
- if platform != PlatformOpenAI && platform != PlatformGrok {
- return false
- }
- candidates := 0
- for i := range accounts {
- account := &accounts[i]
- if account.Platform != platform || !account.IsModelSupported(upstreamModel) {
- continue
- }
- candidates++
- if !accountCodexModelSupportsImageInput(account, account.GetMappedModel(upstreamModel)) {
- return false
- }
- }
- return candidates > 0
- }
- // groupCodexModelSupportsSearchTool advertises client-side tool discovery only
- // when every account that may serve the model uses the gateway's Responses to
- // Chat Completions bridge. Native Responses routes must declare the capability
- // in their upstream Codex manifest instead of having it inferred here.
- func groupCodexModelSupportsSearchTool(
- platform string,
- modelID string,
- accounts []Account,
- compositeRoutes []CompositeModelRoute,
- compositeRoutesAvailable bool,
- ) bool {
- modelID = strings.TrimSpace(modelID)
- if modelID == "" {
- return false
- }
- upstreamModel := modelID
- if platform == PlatformComposite {
- var resolved bool
- platform, upstreamModel, resolved = resolveCodexCompositeModelTarget(
- modelID,
- accounts,
- compositeRoutes,
- compositeRoutesAvailable,
- )
- if !resolved {
- return false
- }
- }
- if platform != PlatformOpenAI {
- return false
- }
- candidates := 0
- for i := range accounts {
- account := &accounts[i]
- if account.Platform != platform || !account.IsModelSupported(upstreamModel) {
- continue
- }
- candidates++
- if !shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
- return false
- }
- }
- return candidates > 0
- }
- func resolveCodexCompositeModelTarget(
- modelID string,
- accounts []Account,
- routes []CompositeModelRoute,
- routesAvailable bool,
- ) (string, string, bool) {
- if !routesAvailable {
- return "", "", false
- }
- if route, matched := matchCompositeRoute(routes, modelID, CompositeRouteEndpointResponses); matched {
- upstreamModel := strings.TrimSpace(route.UpstreamModel)
- if upstreamModel == "" {
- upstreamModel = modelID
- }
- return route.TargetPlatform, upstreamModel, true
- }
- if codexCompositeRouteMatchesModel(routes, modelID) {
- return "", "", false
- }
- claimedPlatforms := make(map[string]struct{})
- for _, account := range accounts {
- platform := strings.TrimSpace(account.Platform)
- if !isConcreteRequestPlatform(platform) || !codexExplicitModelMappingClaims(account, modelID) {
- continue
- }
- claimedPlatforms[platform] = struct{}{}
- }
- if len(claimedPlatforms) > 1 {
- return "", "", false
- }
- for platform := range claimedPlatforms {
- return platform, modelID, true
- }
- platform, detected := DetectModelPlatform(modelID)
- if !detected {
- return "", "", false
- }
- return platform, modelID, true
- }
- func codexCompositeRouteMatchesModel(routes []CompositeModelRoute, modelID string) bool {
- for _, route := range routes {
- publicModel := strings.TrimSpace(route.PublicModel)
- if publicModel == "" {
- continue
- }
- switch normalizeCompositeRouteMatchType(route.MatchType) {
- case CompositeRouteMatchPrefix:
- if strings.HasPrefix(modelID, publicModel) {
- return true
- }
- default:
- if modelID == publicModel {
- return true
- }
- }
- }
- return false
- }
- func codexExplicitModelMappingClaims(account Account, modelID string) bool {
- if account.Credentials == nil || strings.TrimSpace(modelID) == "" {
- return false
- }
- mapped := strings.TrimSpace(account.GetModelMapping()[modelID])
- return mapped != ""
- }
- func accountCodexModelSupportsImageInput(account *Account, upstreamModel string) bool {
- if account == nil {
- return false
- }
- switch account.Platform {
- case PlatformOpenAI:
- if metadata, ok := account.GetUpstreamModelMetadata(upstreamModel); ok {
- if modalities := normalizeCodexInputModalities(metadata.InputModalities); len(modalities) > 0 {
- // Official GPT-6 Astra metadata briefly shipped with a stale
- // text-only modality list. Keep explicit provider metadata
- // authoritative for compatible hosts, but repair that stale
- // official snapshot at the capability boundary.
- if isOpenAIGPT6AstraModel(upstreamModel) && isOfficialOpenAICodexAccount(account) {
- return true
- }
- return stringSliceContains(modalities, "image")
- }
- }
- if !isOpenAICodexImageInputModel(upstreamModel) {
- return false
- }
- if account.IsOpenAIOAuth() {
- return true
- }
- if !account.IsOpenAIApiKey() {
- return false
- }
- // Compatible model lists often omit modalities. Preserve the known GPT
- // fallback unless a synced snapshot above explicitly narrows it.
- return true
- case PlatformGrok:
- if !isOfficialGrokCodexBaseURL(account.GetGrokBaseURL()) {
- return false
- }
- canonical := xai.ResolveGrokTextResponsesModelID(upstreamModel)
- return isGrokCodexImageInputModel(canonical)
- default:
- return false
- }
- }
- func isOfficialOpenAICodexAccount(account *Account) bool {
- if account == nil || account.Platform != PlatformOpenAI {
- return false
- }
- if account.IsOpenAIOAuth() {
- return true
- }
- return account.IsOpenAIApiKey() && isOfficialOpenAIModelsBaseURL(account.GetOpenAIBaseURL())
- }
- func isGrokCodexImageInputModel(model string) bool {
- switch strings.ToLower(strings.TrimSpace(model)) {
- case "grok-4.3",
- "grok-4.5",
- "grok-4.6",
- "grok-build-0.1",
- "grok-4.20-0309-reasoning",
- "grok-4.20-0309-non-reasoning",
- "grok-4.20-multi-agent-0309":
- return true
- default:
- return false
- }
- }
- func isOfficialGrokCodexBaseURL(raw string) bool {
- parsed, err := url.Parse(strings.TrimSpace(raw))
- if err != nil || parsed.Host == "" {
- return false
- }
- return xai.IsOfficialBaseURLHost(strings.TrimSuffix(parsed.Hostname(), "."))
- }
- // BuildDeepSeekCodexModelsManifest preserves the historical entry point for
- // callers that still use the provider-specific function name.
- func BuildDeepSeekCodexModelsManifest(modelIDs []string) ([]byte, error) {
- return BuildCodexModelsManifest(modelIDs)
- }
- func mergeConfiguredCodexModelsManifest(
- body []byte,
- configuredModels []string,
- selectedModels []string,
- filterBySelection bool,
- ) ([]byte, bool, error) {
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil {
- return nil, false, err
- }
- var upstreamModels []json.RawMessage
- if err := json.Unmarshal(envelope["models"], &upstreamModels); err != nil {
- return nil, false, err
- }
- selected := make(map[string]struct{}, len(selectedModels))
- for _, modelID := range selectedModels {
- modelID = strings.TrimSpace(modelID)
- if modelID != "" {
- selected[modelID] = struct{}{}
- }
- }
- // 白名单条目匹配统一走 GroupModelAllowlist.Allows(通配条目按前缀展开)。
- allowlist := GroupModelAllowlist{Enabled: filterBySelection, Models: selectedModels}
- seen := make(map[string]struct{}, len(upstreamModels)+len(configuredModels))
- merged := make([]json.RawMessage, 0, len(upstreamModels)+len(configuredModels))
- changed := false
- for _, rawModel := range upstreamModels {
- var descriptor struct {
- Slug string `json:"slug"`
- }
- if err := json.Unmarshal(rawModel, &descriptor); err != nil || strings.TrimSpace(descriptor.Slug) == "" {
- if filterBySelection {
- changed = true
- continue
- }
- merged = append(merged, rawModel)
- continue
- }
- descriptor.Slug = strings.TrimSpace(descriptor.Slug)
- if isCodexDedicatedMediaModel(descriptor.Slug) {
- changed = true
- continue
- }
- if filterBySelection && !allowlist.Allows(descriptor.Slug) {
- changed = true
- continue
- }
- if strings.HasPrefix(descriptor.Slug, codexAutoModelPrefix) {
- _, explicitlyEnabled := selected[descriptor.Slug]
- explicitlyEnabled = filterBySelection && explicitlyEnabled
- if !explicitlyEnabled {
- changed = true
- continue
- }
- visibleModel, visibilityChanged, err := codexModelWithVisibility(rawModel, "list")
- if err != nil {
- return nil, false, err
- }
- rawModel = visibleModel
- changed = changed || visibilityChanged
- }
- seen[descriptor.Slug] = struct{}{}
- merged = append(merged, rawModel)
- }
- for _, modelID := range configuredModels {
- if isCodexDedicatedMediaModel(modelID) {
- continue
- }
- if filterBySelection && !allowlist.Allows(modelID) {
- continue
- }
- if strings.HasPrefix(modelID, codexAutoModelPrefix) {
- if _, explicitlyEnabled := selected[modelID]; !filterBySelection || !explicitlyEnabled {
- continue
- }
- }
- if _, exists := seen[modelID]; exists {
- continue
- }
- rawModel, err := json.Marshal(newConfiguredCodexModelDescriptor(modelID))
- if err != nil {
- return nil, false, err
- }
- merged = append(merged, rawModel)
- seen[modelID] = struct{}{}
- changed = true
- }
- if !changed {
- return body, false, nil
- }
- rawModels, err := json.Marshal(merged)
- if err != nil {
- return nil, false, err
- }
- envelope["models"] = rawModels
- mergedBody, err := json.Marshal(envelope)
- if err != nil {
- return nil, false, err
- }
- return mergedBody, true, nil
- }
- func codexModelWithVisibility(rawModel json.RawMessage, visibility string) (json.RawMessage, bool, error) {
- var fields map[string]json.RawMessage
- if err := json.Unmarshal(rawModel, &fields); err != nil {
- return nil, false, err
- }
- var current string
- if rawVisibility, ok := fields["visibility"]; ok {
- if err := json.Unmarshal(rawVisibility, ¤t); err == nil && current == visibility {
- return rawModel, false, nil
- }
- }
- rawVisibility, err := json.Marshal(visibility)
- if err != nil {
- return nil, false, err
- }
- fields["visibility"] = rawVisibility
- updated, err := json.Marshal(fields)
- if err != nil {
- return nil, false, err
- }
- return updated, true, nil
- }
- type codexModelsManifestUpstreamError struct {
- err error
- retryable bool
- statusCode int
- headers http.Header
- body []byte
- }
- func (e *codexModelsManifestUpstreamError) Error() string { return e.err.Error() }
- func (e *codexModelsManifestUpstreamError) Unwrap() error { return e.err }
- // IsRetryableCodexModelsManifestError reports whether another selected account
- // may succeed without changing the request. API key upstream 404/405 responses
- // mean that the selected account does not expose a model-discovery endpoint, so
- // another account may still serve the manifest. Other upstream 4xx responses,
- // except 429 and ChatGPT-backend 401, are intentionally not retried. A manifest
- // 401 from the ChatGPT Codex backend reflects the selected OAuth account's
- // upstream token rather than the client request (the client's own API key was
- // already validated locally). Custom API key upstreams keep the no-failover 401
- // behavior because their /models auth semantics are not authoritative for the
- // account.
- func IsRetryableCodexModelsManifestError(err error) bool {
- var upstreamErr *codexModelsManifestUpstreamError
- return errors.As(err, &upstreamErr) && upstreamErr.retryable
- }
- func isRetryableCodexModelsManifestStatus(statusCode int, useAPIKeyUpstream bool) bool {
- return (useAPIKeyUpstream &&
- (statusCode == http.StatusNotFound || statusCode == http.StatusMethodNotAllowed)) ||
- (statusCode == http.StatusUnauthorized && !useAPIKeyUpstream) ||
- statusCode == http.StatusTooManyRequests ||
- (statusCode >= http.StatusInternalServerError && statusCode < 600)
- }
- func isRetryableCodexModelsManifestTransportError(err error) bool {
- if err == nil || errors.Is(err, context.Canceled) {
- return false
- }
- if errors.Is(err, context.DeadlineExceeded) ||
- errors.Is(err, io.EOF) ||
- errors.Is(err, io.ErrUnexpectedEOF) ||
- errors.Is(err, net.ErrClosed) {
- return true
- }
- var opErr *net.OpError
- if errors.As(err, &opErr) {
- return true
- }
- var dnsErr *net.DNSError
- if errors.As(err, &dnsErr) {
- return true
- }
- var goAwayErr http2.GoAwayError
- if errors.As(err, &goAwayErr) {
- return true
- }
- var streamErr http2.StreamError
- if errors.As(err, &streamErr) {
- return true
- }
- var connectionErr http2.ConnectionError
- if errors.As(err, &connectionErr) {
- return true
- }
- var netErr net.Error
- if errors.As(err, &netErr) && netErr.Timeout() {
- return true
- }
- // net/http uses unexported HTTP/2 error types, so typed matching is not
- // possible for errors produced by the standard library transport.
- message := strings.ToLower(err.Error())
- if strings.Contains(message, "http2:") &&
- (strings.Contains(message, "goaway") ||
- strings.Contains(message, "refused_stream") ||
- strings.Contains(message, "frame too large")) {
- return true
- }
- if strings.Contains(message, "stream error: stream id ") {
- return true
- }
- for _, code := range []http2.ErrCode{
- http2.ErrCodeNo,
- http2.ErrCodeProtocol,
- http2.ErrCodeInternal,
- http2.ErrCodeFlowControl,
- http2.ErrCodeSettingsTimeout,
- http2.ErrCodeStreamClosed,
- http2.ErrCodeFrameSize,
- http2.ErrCodeRefusedStream,
- http2.ErrCodeCancel,
- http2.ErrCodeCompression,
- http2.ErrCodeConnect,
- http2.ErrCodeEnhanceYourCalm,
- http2.ErrCodeInadequateSecurity,
- http2.ErrCodeHTTP11Required,
- } {
- if strings.Contains(message, "connection error: "+strings.ToLower(code.String())) {
- return true
- }
- }
- return false
- }
- type openAIModelsRequest struct {
- url string
- headers http.Header
- proxyURL string
- accountID int64
- credentialAccountID int64
- credentialAccount *Account
- accountConcurrency int
- useAPIKeyUpstream bool
- // Cached bodies have already been converted to their requested format.
- standardModelsList bool
- }
- type openAIModelsCacheEntry struct {
- manifest *OpenAIModelsResponse
- order uint64
- expiresAt time.Time
- staleUntil time.Time
- }
- type openAIModelsCacheState uint8
- const (
- openAIModelsCacheMiss openAIModelsCacheState = iota
- openAIModelsCacheFresh
- openAIModelsCacheStale
- )
- type openAIModelsCache struct {
- mu sync.Mutex
- entries map[string]openAIModelsCacheEntry
- nextOrder uint64
- refresh singleflight.Group
- }
- func (c *openAIModelsCache) get(key string, now time.Time) (*OpenAIModelsResponse, openAIModelsCacheState) {
- c.mu.Lock()
- defer c.mu.Unlock()
- entry, ok := c.entries[key]
- if !ok {
- return nil, openAIModelsCacheMiss
- }
- if !now.Before(entry.staleUntil) {
- delete(c.entries, key)
- return nil, openAIModelsCacheMiss
- }
- if now.Before(entry.expiresAt) {
- return entry.manifest, openAIModelsCacheFresh
- }
- return entry.manifest, openAIModelsCacheStale
- }
- func (c *openAIModelsCache) set(key string, manifest *OpenAIModelsResponse, now time.Time) {
- if manifest == nil || len(manifest.Body) > openAIModelsCacheBodyLimit {
- return
- }
- remainingBodyBudget := openAIModelsCacheBodyLimit - len(manifest.Body)
- if len(manifest.upstreamSourceBody) > remainingBodyBudget {
- return
- }
- c.mu.Lock()
- defer c.mu.Unlock()
- if c.entries == nil {
- c.entries = make(map[string]openAIModelsCacheEntry)
- }
- if _, exists := c.entries[key]; !exists && len(c.entries) >= openAIModelsCacheMaxEntries {
- oldestKey := ""
- var oldestOrder uint64
- for candidateKey, entry := range c.entries {
- if !now.Before(entry.staleUntil) {
- delete(c.entries, candidateKey)
- continue
- }
- if oldestKey == "" || entry.order < oldestOrder {
- oldestKey = candidateKey
- oldestOrder = entry.order
- }
- }
- if len(c.entries) >= openAIModelsCacheMaxEntries && oldestKey != "" {
- delete(c.entries, oldestKey)
- }
- }
- c.nextOrder++
- c.entries[key] = openAIModelsCacheEntry{
- manifest: manifest,
- order: c.nextOrder,
- expiresAt: now.Add(openAIModelsCacheTTL),
- staleUntil: now.Add(openAIModelsCacheStaleTTL),
- }
- }
- // FetchCodexModelsManifest fetches the live Codex models manifest from either
- // the ChatGPT backend for OAuth accounts or a custom upstream for API key accounts.
- //
- // After validating the stable top-level envelope, OAuth response bodies are
- // passed through verbatim. Custom API key manifests receive only the narrowly
- // scoped compatibility adjustments required by custom-provider Codex clients.
- func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, account *Account, clientVersion, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- if account == nil {
- return nil, infraerrors.New(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_ACCOUNT_REQUIRED", "account is required")
- }
- credAccount, err := resolveCredentialAccount(ctx, s.accountRepo, account)
- if err != nil {
- return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_CREDENTIALS_FAILED", "resolve credential account: %v", err)
- }
- clientVersion = strings.TrimSpace(clientVersion)
- if clientVersion == "" {
- clientVersion = CodexCanonicalClientVersion()
- }
- requestEndpoint := chatgptCodexModelsURL
- authToken := ""
- useAPIKeyUpstream := false
- appendModelsPath := false
- switch {
- case credAccount.IsOpenAIOAuth():
- authToken = strings.TrimSpace(credAccount.GetOpenAIAccessToken())
- if authToken == "" && !credAccount.IsOpenAIAgentIdentity() {
- return nil, infraerrors.New(http.StatusBadGateway, "OPENAI_CODEX_MODELS_TOKEN_MISSING", "account has no Codex backend access token")
- }
- case credAccount.IsOpenAIApiKey():
- baseURL := strings.TrimSpace(credAccount.GetOpenAIBaseURL())
- authToken = strings.TrimSpace(credAccount.GetOpenAIApiKey())
- if authToken == "" {
- return nil, infraerrors.New(http.StatusBadGateway, "OPENAI_CODEX_MODELS_API_KEY_MISSING", "account has no API key for the Codex models upstream")
- }
- normalizedBaseURL, validateErr := s.validateUpstreamBaseURL(baseURL)
- if validateErr != nil {
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_API_KEY_UPSTREAM_INVALID", "invalid Codex models upstream base URL: %v", validateErr)
- }
- requestEndpoint = normalizedBaseURL
- useAPIKeyUpstream = true
- appendModelsPath = true
- default:
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_ACCOUNT_TYPE_UNSUPPORTED", "account type %q cannot fetch the Codex models manifest", credAccount.Type)
- }
- requestURL, err := buildCodexModelsManifestURL(requestEndpoint, appendModelsPath, clientVersion)
- if err != nil {
- if useAPIKeyUpstream {
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_API_KEY_UPSTREAM_INVALID", "invalid Codex models upstream base URL: %v", err)
- }
- return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_REQUEST_FAILED", "parse codex models request URL: %v", err)
- }
- headers := make(http.Header)
- if useAPIKeyUpstream {
- headers.Set("Authorization", "Bearer "+authToken)
- credAccount.ApplyHeaderOverrides(headers)
- } else {
- authHeaders, authErr := s.buildOpenAIAuthenticationHeaders(ctx, credAccount, authToken)
- if authErr != nil {
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_AUTH_FAILED", "build Codex models authentication: %v", authErr)
- }
- for key, values := range authHeaders {
- for _, value := range values {
- headers.Add(key, value)
- }
- }
- setOpenAIChatGPTAccountHeaders(headers, credAccount)
- }
- headers.Set("Accept", "application/json")
- overrideUA := ""
- if !useAPIKeyUpstream {
- overrideUA = credAccount.GetOpenAIUserAgent()
- }
- identity := resolveCodexOutboundIdentity(overrideUA)
- headers.Set("Originator", identity.originator)
- headers.Set("User-Agent", identity.userAgent)
- // Version 头优先与 client_version 查询参数同源:客户端自报版本合法且不低于上游
- // 门槛时原样使用;否则回退规范版本,避免陈旧 version 触发上游 404(issue #3901)。
- // client_version 查询参数本身始终按客户端原值透传(内容协商语义,契约见
- // TestFetchCodexModelsManifestPassthrough)。
- headerVersion := NormalizeCodexClientVersion(clientVersion)
- if headerVersion == "" || CompareVersions(headerVersion, codexUpstreamMinVersion) < 0 {
- headerVersion = identity.version
- }
- headers.Set("Version", headerVersion)
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- request := openAIModelsRequest{
- url: requestURL.String(),
- headers: headers,
- proxyURL: proxyURL,
- accountID: account.ID,
- credentialAccountID: credAccount.ID,
- credentialAccount: credAccount,
- accountConcurrency: account.Concurrency,
- useAPIKeyUpstream: useAPIKeyUpstream,
- }
- if useAPIKeyUpstream {
- return s.fetchCachedOpenAIModels(ctx, request, s.fetchCodexModelsManifestUpstreamForRequest(request), ifNoneMatch)
- }
- // OAuth 账号同样经过账号级缓存;闭包保留 agent identity 任务恢复逻辑,
- // 错误时仍交给 handleCodexModelsManifestAccountAuthError 处理账号状态。
- oauthFetch := func(fetchCtx context.Context, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- manifest, fetchErr := s.fetchCodexModelsManifestUpstream(fetchCtx, request, ifNoneMatch)
- if !credAccount.IsOpenAIAgentIdentity() || !isAgentIdentityTaskInvalidCodexModelsError(fetchErr) {
- s.handleCodexModelsManifestAccountAuthError(fetchCtx, account, credAccount, fetchErr)
- return manifest, fetchErr
- }
- expectedTaskID := strings.TrimSpace(credAccount.GetCredential("task_id"))
- if recoverErr := s.recoverAgentIdentityTask(fetchCtx, credAccount, expectedTaskID); recoverErr != nil {
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_AUTH_FAILED", "agent identity task recovery failed: %v", recoverErr)
- }
- authHeaders, authErr := s.buildOpenAIAuthenticationHeaders(fetchCtx, credAccount, "")
- if authErr != nil {
- return nil, infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_AUTH_FAILED", "build Codex models authentication after task recovery: %v", authErr)
- }
- request.headers.Del("Authorization")
- request.headers.Del("ChatGPT-Account-ID")
- for key, values := range authHeaders {
- for _, value := range values {
- request.headers.Add(key, value)
- }
- }
- setOpenAIChatGPTAccountHeaders(request.headers, credAccount)
- return s.fetchCodexModelsManifestUpstream(fetchCtx, request, ifNoneMatch)
- }
- return s.fetchCachedOpenAIModels(ctx, request, oauthFetch, ifNoneMatch)
- }
- func isAgentIdentityTaskInvalidCodexModelsError(err error) bool {
- var upstreamErr *codexModelsManifestUpstreamError
- return errors.As(err, &upstreamErr) &&
- isAgentIdentityTaskInvalidHTTPResponse(upstreamErr.statusCode, upstreamErr.body)
- }
- // handleCodexModelsManifestAccountAuthError feeds manifest 401s from the
- // ChatGPT Codex backend into the shared upstream-error state machinery
- // (token cache invalidation, temp-unschedulable cooldown, or permanent
- // disable for token_revoked/token_invalidated). Without this, an account
- // whose OAuth token was revoked upstream stays active and schedulable and
- // keeps being selected for every subsequent /models request (#4544).
- //
- // Scope is deliberately limited to plain OAuth accounts: the manifest
- // endpoint authenticates with the same token as /responses forwarding, so a
- // 401 is authoritative for the account. Agent Identity accounts are excluded
- // because their 401s can be task-scoped and have a dedicated recovery flow,
- // and API key manifests come from custom upstreams whose /models auth may
- // diverge from their chat endpoints.
- func (s *OpenAIGatewayService) handleCodexModelsManifestAccountAuthError(ctx context.Context, account, credAccount *Account, err error) {
- if s == nil || account == nil || err == nil {
- return
- }
- if credAccount == nil || !credAccount.IsOpenAIOAuth() || credAccount.IsOpenAIAgentIdentity() {
- return
- }
- var upstreamErr *codexModelsManifestUpstreamError
- if !errors.As(err, &upstreamErr) || upstreamErr.statusCode != http.StatusUnauthorized {
- return
- }
- headers := upstreamErr.headers
- if headers == nil {
- headers = http.Header{}
- }
- s.handleOpenAIAccountUpstreamError(ctx, account, upstreamErr.statusCode, headers, upstreamErr.body)
- }
- func (s *OpenAIGatewayService) fetchCachedOpenAIModels(ctx context.Context, request openAIModelsRequest, fetch func(ctx context.Context, ifNoneMatch string) (*OpenAIModelsResponse, error), ifNoneMatch string) (*OpenAIModelsResponse, error) {
- if err := ctx.Err(); err != nil {
- return nil, err
- }
- cacheKey := buildOpenAIModelsCacheKey(request)
- manifest, state := s.openAIModelsCache.get(cacheKey, time.Now())
- if state == openAIModelsCacheFresh {
- return openAIModelsResponseForClient(manifest, ifNoneMatch), nil
- }
- resultCh := s.refreshCachedOpenAIModels(cacheKey, request, fetch)
- if state == openAIModelsCacheStale {
- return openAIModelsResponseForClient(manifest, ifNoneMatch), nil
- }
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- case result := <-resultCh:
- if result.Err != nil {
- return nil, result.Err
- }
- manifest, ok := result.Val.(*OpenAIModelsResponse)
- if !ok || manifest == nil {
- return nil, infraerrors.New(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_REQUEST_FAILED", "invalid shared Codex models manifest result")
- }
- return openAIModelsResponseForClient(manifest, ifNoneMatch), nil
- }
- }
- func (s *OpenAIGatewayService) refreshCachedOpenAIModels(cacheKey string, request openAIModelsRequest, fetch func(ctx context.Context, ifNoneMatch string) (*OpenAIModelsResponse, error)) <-chan singleflight.Result {
- return s.openAIModelsCache.refresh.DoChan(cacheKey, func() (any, error) {
- ctx, cancel := context.WithTimeout(context.Background(), codexModelsManifestRequestTimeout)
- defer cancel()
- cached, _ := s.openAIModelsCache.get(cacheKey, time.Now())
- ifNoneMatch := ""
- if cached != nil {
- ifNoneMatch = cached.upstreamETag
- }
- manifest, err := fetch(ctx, ifNoneMatch)
- if err != nil {
- return nil, err
- }
- if manifest.NotModified && cached != nil {
- s.openAIModelsCache.set(cacheKey, cached, time.Now())
- return cached, nil
- }
- if !manifest.NotModified {
- s.openAIModelsCache.set(cacheKey, manifest, time.Now())
- }
- return manifest, nil
- })
- }
- func (s *OpenAIGatewayService) fetchCodexModelsManifestUpstreamForRequest(request openAIModelsRequest) func(ctx context.Context, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- return func(ctx context.Context, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- return s.fetchCodexModelsManifestUpstream(ctx, request, ifNoneMatch)
- }
- }
- func (s *OpenAIGatewayService) fetchOpenAIModelsUpstream(ctx context.Context, request openAIModelsRequest, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- reqCtx, cancel := context.WithTimeout(ctx, codexModelsManifestRequestTimeout)
- defer cancel()
- req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, request.url, nil)
- if err != nil {
- return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_REQUEST_FAILED", "create codex models request: %v", err)
- }
- req.Header = request.headers.Clone()
- if ifNoneMatch = strings.TrimSpace(ifNoneMatch); ifNoneMatch != "" {
- req.Header.Set("If-None-Match", ifNoneMatch)
- }
- var resp *http.Response
- if request.useAPIKeyUpstream {
- if s.httpUpstream == nil {
- return nil, infraerrors.New(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_UPSTREAM_NOT_CONFIGURED", "Codex models upstream HTTP client is not configured")
- }
- req = req.WithContext(WithHTTPUpstreamProfile(req.Context(), HTTPUpstreamProfileOpenAI))
- resp, err = s.httpUpstream.Do(req, request.proxyURL, request.accountID, request.accountConcurrency)
- } else {
- handled := false
- if s.pluginManager != nil {
- resp, handled, err = s.pluginManager.RoundTripOpenAIOAuth(reqCtx, req, request.proxyURL, request.credentialAccount)
- }
- if !handled {
- client, clientErr := httpclient.GetClient(httpclient.Options{
- ProxyURL: request.proxyURL,
- Timeout: codexModelsManifestRequestTimeout,
- ResponseHeaderTimeout: 10 * time.Second,
- })
- if clientErr != nil {
- return nil, infraerrors.Newf(http.StatusInternalServerError, "OPENAI_CODEX_MODELS_PROXY_INVALID", "invalid proxy configuration: %v", clientErr)
- }
- resp, err = client.Do(req)
- }
- }
- if err != nil {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "codex models manifest request failed: %v", err),
- retryable: isRetryableCodexModelsManifestTransportError(err),
- }
- }
- defer func() { _ = resp.Body.Close() }()
- if resp.StatusCode == http.StatusNotModified {
- return &OpenAIModelsResponse{ETag: resp.Header.Get("ETag"), NotModified: true}, nil
- }
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
- body = s.redactAgentIdentitySensitiveBody(reqCtx, request.credentialAccount, body)
- message := strings.TrimSpace(string(body))
- if message == "" {
- message = resp.Status
- }
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "codex models manifest upstream error %d: %s", resp.StatusCode, message),
- statusCode: resp.StatusCode,
- headers: resp.Header.Clone(),
- body: body,
- retryable: isRetryableCodexModelsManifestStatus(resp.StatusCode, request.useAPIKeyUpstream),
- }
- }
- bodyLimit := resolveModelsListReadLimit(s.cfg)
- body, err := io.ReadAll(io.LimitReader(resp.Body, bodyLimit+1))
- if err != nil {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "read codex models manifest response: %v", err),
- retryable: isRetryableCodexModelsManifestTransportError(err),
- }
- }
- if int64(len(body)) > bodyLimit {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(http.StatusBadGateway, "OPENAI_CODEX_MODELS_UPSTREAM_FAILED", "codex models manifest response exceeds %d bytes", bodyLimit),
- retryable: true,
- }
- }
- etag := resp.Header.Get("ETag")
- return &OpenAIModelsResponse{Body: body, ETag: etag, upstreamETag: etag}, nil
- }
- func (s *OpenAIGatewayService) fetchCodexModelsManifestUpstream(ctx context.Context, request openAIModelsRequest, ifNoneMatch string) (*OpenAIModelsResponse, error) {
- response, err := s.fetchOpenAIModelsUpstream(ctx, request, ifNoneMatch)
- if err != nil || response.NotModified {
- return response, err
- }
- body := response.Body
- upstreamBody := body
- convertedFromOpenAIModelList := false
- if request.useAPIKeyUpstream {
- convertedBody := convertOpenAIModelListToCodexManifestForAccount(body, request.credentialAccount)
- convertedFromOpenAIModelList = !bytes.Equal(convertedBody, body)
- body = convertedBody
- }
- if err := validateCodexModelsManifestEnvelope(body); err != nil {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(
- http.StatusBadGateway,
- "OPENAI_CODEX_MODELS_UPSTREAM_INVALID_MANIFEST",
- "codex models manifest upstream returned an invalid envelope: %v",
- err,
- ),
- retryable: true,
- }
- }
- if request.useAPIKeyUpstream {
- body, err = completeAPIKeyCodexModelsManifestMetadata(
- body,
- false,
- request.credentialAccount,
- )
- if err != nil {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(
- http.StatusBadGateway,
- "OPENAI_CODEX_MODELS_UPSTREAM_INVALID_MANIFEST",
- "codex models manifest upstream metadata could not be completed: %v",
- err,
- ),
- retryable: true,
- }
- }
- body, err = adjustAPIKeyCodexModelsManifest(body, request.credentialAccount)
- if err != nil {
- return nil, &codexModelsManifestUpstreamError{
- err: infraerrors.Newf(
- http.StatusBadGateway,
- "OPENAI_CODEX_MODELS_UPSTREAM_INVALID_MANIFEST",
- "codex models manifest upstream could not be adjusted: %v",
- err,
- ),
- retryable: true,
- }
- }
- }
- etag := response.upstreamETag
- manifest := &OpenAIModelsResponse{
- Body: body,
- ETag: etag,
- upstreamETag: etag,
- upstreamSourceBody: append([]byte(nil), upstreamBody...),
- convertedFromOpenAIModelList: convertedFromOpenAIModelList,
- }
- if request.useAPIKeyUpstream && !bytes.Equal(body, upstreamBody) {
- manifest.ETag = codexModelsManifestBodyETag(body)
- }
- return manifest, nil
- }
- func codexModelsManifestBodyETag(body []byte) string {
- sum := sha256.Sum256(body)
- return fmt.Sprintf(`"%x"`, sum)
- }
- // CodexModelsManifestETag returns the strong ETag for a generated client
- // catalog. It is based on the final JSON body after local filtering.
- func CodexModelsManifestETag(body []byte) string {
- return codexModelsManifestBodyETag(body)
- }
- var apiKeyCodexModelsWithoutResponsesLite = map[string]struct{}{
- "gpt-6-astra": {},
- "gpt-5.6-sol": {},
- "gpt-5.6-terra": {},
- "gpt-5.6-luna": {},
- }
- // adjustAPIKeyCodexModelsManifest prevents Codex from selecting Responses
- // Lite for custom API key providers. Those clients do not install web.run in
- // Lite mode, so the affected model manifests must advertise the full Responses
- // path. Return the original body when no targeted true value is present.
- func adjustAPIKeyCodexModelsManifest(body []byte, account *Account) ([]byte, error) {
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil {
- return nil, fmt.Errorf("decode JSON object: %w", err)
- }
- var models []json.RawMessage
- if err := json.Unmarshal(envelope["models"], &models); err != nil {
- return nil, fmt.Errorf("decode top-level models array: %w", err)
- }
- changed := false
- for i, rawModel := range models {
- var model map[string]json.RawMessage
- if err := json.Unmarshal(rawModel, &model); err != nil || model == nil {
- continue
- }
- var slug string
- if err := json.Unmarshal(model["slug"], &slug); err != nil {
- continue
- }
- target := slug
- if account != nil {
- target = account.GetMappedModel(slug)
- }
- if isOpenAIGPT6AstraModel(target) {
- target = "gpt-6-astra"
- }
- if _, targeted := apiKeyCodexModelsWithoutResponsesLite[target]; !targeted {
- continue
- }
- var useResponsesLite bool
- if err := json.Unmarshal(model["use_responses_lite"], &useResponsesLite); err != nil || !useResponsesLite {
- continue
- }
- model["use_responses_lite"] = json.RawMessage("false")
- adjusted, err := json.Marshal(model)
- if err != nil {
- return nil, fmt.Errorf("encode model %q: %w", slug, err)
- }
- models[i] = adjusted
- changed = true
- }
- if !changed {
- return body, nil
- }
- adjustedModels, err := json.Marshal(models)
- if err != nil {
- return nil, fmt.Errorf("encode top-level models array: %w", err)
- }
- envelope["models"] = adjustedModels
- adjusted, err := json.Marshal(envelope)
- if err != nil {
- return nil, fmt.Errorf("encode JSON object: %w", err)
- }
- return adjusted, nil
- }
- // convertOpenAIModelListToCodexManifest rewrites a standard OpenAI
- // GET /v1/models response ({"object":"list","data":[{"id":...},...]}) into the
- // same complete Codex manifest used by locally generated custom-provider
- // catalogs. Bodies that already carry a top-level models field, are not the
- // standard list shape, or yield no usable model IDs are returned unchanged so
- // envelope validation reports the original payload.
- func convertOpenAIModelListToCodexManifest(body []byte) []byte {
- return convertOpenAIModelListToCodexManifestForAccount(body, nil)
- }
- func convertOpenAIModelListToCodexManifestForAccount(body []byte, account *Account) []byte {
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil || envelope == nil {
- return body
- }
- if _, ok := envelope["models"]; ok {
- return body
- }
- data, ok := envelope["data"]
- if !ok {
- return body
- }
- var entries []map[string]json.RawMessage
- if err := json.Unmarshal(data, &entries); err != nil {
- return body
- }
- modelIDs := make([]string, 0, len(entries))
- modelMetadata := make(map[string]codexModelMetadataOverride, len(entries))
- metadataModels := make(map[string]string, len(entries))
- for _, entry := range entries {
- var id string
- if err := json.Unmarshal(entry["id"], &id); err != nil {
- continue
- }
- id = strings.TrimSpace(id)
- if id == "" {
- continue
- }
- modelIDs = append(modelIDs, id)
- capabilityModel := id
- if account != nil {
- capabilityModel = account.GetMappedModel(id)
- }
- metadataModels[id] = capabilityModel
- capabilities := accountCodexToolCapabilities(account, capabilityModel)
- applyCodexToolCapabilities(capabilities, entry, true)
- modelMetadata[id] = codexModelMetadataOverride{UpstreamModelMetadata: UpstreamModelMetadata{
- CodexToolCapabilities: capabilities,
- }}
- }
- if len(modelIDs) == 0 {
- return body
- }
- imageInputModels := make(map[string]bool, len(modelIDs))
- for _, modelID := range modelIDs {
- if accountCodexModelSupportsImageInput(account, modelID) {
- imageInputModels[modelID] = true
- }
- }
- searchToolModels := make(map[string]bool, len(modelIDs))
- if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
- for _, modelID := range modelIDs {
- searchToolModels[modelID] = true
- }
- }
- converted, err := buildCodexModelsManifest(modelIDs, imageInputModels, searchToolModels, metadataModels, modelMetadata)
- if err != nil {
- return body
- }
- return converted
- }
- // completeAPIKeyCodexModelsManifestMetadata fills fields omitted by standard
- // OpenAI-compatible /models endpoints. Existing provider metadata always wins;
- // only absent or null values are synthesized.
- // CompleteAPIKeyCodexModelsManifestForClient fills the complete ModelInfo
- // contract immediately before a group-specific API key manifest is returned.
- // The shared upstream cache remains independent from local group policy.
- func (s *OpenAIGatewayService) CompleteAPIKeyCodexModelsManifestForClient(manifest *OpenAIModelsResponse, account *Account) error {
- if manifest == nil || account == nil || !account.IsOpenAIApiKey() || manifest.NotModified || len(manifest.Body) == 0 {
- return nil
- }
- body := manifest.Body
- if len(manifest.upstreamSourceBody) > 0 {
- body = append([]byte(nil), manifest.upstreamSourceBody...)
- if manifest.convertedFromOpenAIModelList {
- body = convertOpenAIModelListToCodexManifestForAccount(body, account)
- }
- }
- var err error
- body, err = applySyncedAPIKeyCodexModelMetadata(body, account, manifest.convertedFromOpenAIModelList)
- if err != nil {
- return err
- }
- body, err = completeAPIKeyCodexModelsManifestMetadata(
- body,
- true,
- account,
- )
- if err != nil {
- return err
- }
- body, err = adjustAPIKeyCodexModelsManifest(body, account)
- if err != nil {
- return err
- }
- manifest.Body = body
- manifest.ETag = codexModelsManifestBodyETag(manifest.Body)
- return nil
- }
- func applySyncedAPIKeyCodexModelMetadata(body []byte, account *Account, overwriteLocalDefaults bool) ([]byte, error) {
- snapshot := account.GetUpstreamModelMetadataSnapshot()
- if snapshot == nil || len(snapshot.Models) == 0 {
- return body, nil
- }
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil {
- return nil, fmt.Errorf("decode JSON object: %w", err)
- }
- var models []json.RawMessage
- if err := json.Unmarshal(envelope["models"], &models); err != nil {
- return nil, fmt.Errorf("decode top-level models array: %w", err)
- }
- changed := false
- for i, rawModel := range models {
- var model map[string]json.RawMessage
- if err := json.Unmarshal(rawModel, &model); err != nil || model == nil {
- continue
- }
- var slug string
- if err := json.Unmarshal(model["slug"], &slug); err != nil {
- continue
- }
- slug = strings.TrimSpace(slug)
- lookupModel := account.GetMappedModel(slug)
- metadata, ok := snapshot.Models[lookupModel]
- if !ok {
- continue
- }
- if lookupModel != slug {
- metadata.DisplayName = ""
- metadata.Description = ""
- }
- descriptor := newConfiguredCodexModelDescriptor(slug)
- applyUpstreamModelMetadataToCodexDescriptor(
- &descriptor,
- codexModelMetadataOverride{UpstreamModelMetadata: metadata},
- )
- descriptorBody, err := json.Marshal(descriptor)
- if err != nil {
- return nil, fmt.Errorf("encode synced model %q: %w", slug, err)
- }
- var syncedFields map[string]json.RawMessage
- if err := json.Unmarshal(descriptorBody, &syncedFields); err != nil {
- return nil, fmt.Errorf("decode synced model %q: %w", slug, err)
- }
- fields := make([]string, 0, 7)
- if strings.TrimSpace(metadata.DisplayName) != "" {
- fields = append(fields, "display_name")
- }
- if strings.TrimSpace(metadata.Description) != "" {
- fields = append(fields, "description")
- }
- if metadata.Reasoning != nil {
- fields = append(fields, "default_reasoning_level", "supported_reasoning_levels")
- }
- if len(normalizeCodexInputModalities(metadata.InputModalities)) > 0 {
- fields = append(fields, "input_modalities")
- }
- if metadata.ContextWindow > 0 {
- fields = append(fields, "context_window", "max_context_window")
- }
- // List conversion has already applied live fields over account capabilities.
- modelChanged := applyCodexToolCapabilities(model, metadata.CodexToolCapabilities, false)
- for _, field := range fields {
- value, exists := syncedFields[field]
- if !exists {
- continue
- }
- current, currentExists := model[field]
- current = bytes.TrimSpace(current)
- if !overwriteLocalDefaults && currentExists && len(current) > 0 && !bytes.Equal(current, []byte("null")) {
- continue
- }
- if bytes.Equal(current, bytes.TrimSpace(value)) {
- continue
- }
- model[field] = value
- modelChanged = true
- }
- if !modelChanged {
- continue
- }
- encoded, err := json.Marshal(model)
- if err != nil {
- return nil, fmt.Errorf("encode model %q with synced metadata: %w", slug, err)
- }
- models[i] = encoded
- changed = true
- }
- if !changed {
- return body, nil
- }
- encodedModels, err := json.Marshal(models)
- if err != nil {
- return nil, fmt.Errorf("encode models with synced metadata: %w", err)
- }
- envelope["models"] = encodedModels
- updated, err := json.Marshal(envelope)
- if err != nil {
- return nil, fmt.Errorf("encode manifest with synced metadata: %w", err)
- }
- return updated, nil
- }
- func completeAPIKeyCodexModelsManifestMetadata(body []byte, completeAll bool, account *Account) ([]byte, error) {
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil {
- return nil, fmt.Errorf("decode JSON object: %w", err)
- }
- var models []json.RawMessage
- if err := json.Unmarshal(envelope["models"], &models); err != nil {
- return nil, fmt.Errorf("decode top-level models array: %w", err)
- }
- officialOpenAI := account != nil && isOfficialOpenAIModelsBaseURL(account.GetOpenAIBaseURL())
- changed := false
- if officialOpenAI {
- filtered := make([]json.RawMessage, 0, len(models))
- for _, rawModel := range models {
- var model struct {
- Slug string `json:"slug"`
- }
- if err := json.Unmarshal(rawModel, &model); err != nil || strings.TrimSpace(model.Slug) == "" {
- filtered = append(filtered, rawModel)
- continue
- }
- if !isOfficialOpenAICodexCatalogModel(model.Slug) {
- changed = true
- continue
- }
- filtered = append(filtered, rawModel)
- }
- models = filtered
- }
- for i, rawModel := range models {
- var model map[string]json.RawMessage
- if err := json.Unmarshal(rawModel, &model); err != nil || model == nil {
- continue
- }
- var slug string
- if err := json.Unmarshal(model["slug"], &slug); err != nil {
- continue
- }
- slug = strings.TrimSpace(slug)
- if slug == "" {
- continue
- }
- completeDescriptor := completeAll || isDeepSeekCodexModel(slug)
- forceOfficialImage := officialOpenAI && isOpenAICodexImageInputModel(slug)
- if !completeDescriptor && !forceOfficialImage {
- continue
- }
- descriptor := newConfiguredCodexModelDescriptor(slug)
- descriptor.SupportsSearchTool = shouldForwardOpenAIResponsesViaRawChatCompletions(account)
- if accountCodexModelSupportsImageInput(account, slug) {
- descriptor.InputModalities = []string{"text", "image"}
- }
- if forceOfficialImage {
- descriptor.InputModalities = []string{"text", "image"}
- descriptor.SupportsImageDetailOriginal = true
- }
- defaultBody, err := json.Marshal(descriptor)
- if err != nil {
- return nil, fmt.Errorf("encode default model %q: %w", slug, err)
- }
- var defaults map[string]json.RawMessage
- if err := json.Unmarshal(defaultBody, &defaults); err != nil {
- return nil, fmt.Errorf("decode default model %q: %w", slug, err)
- }
- capabilityModel := slug
- if account != nil {
- capabilityModel = account.GetMappedModel(slug)
- }
- capabilities := accountCodexToolCapabilities(account, capabilityModel)
- modelChanged := applyCodexToolCapabilities(model, capabilities, false)
- if completeDescriptor {
- merged, err := mergeMissingCodexModelFields(model, defaults)
- if err != nil {
- return nil, fmt.Errorf("complete model %q: %w", slug, err)
- }
- modelChanged = merged || modelChanged
- }
- if forceOfficialImage {
- modalities, err := json.Marshal([]string{"text", "image"})
- if err != nil {
- return nil, fmt.Errorf("encode input modalities for model %q: %w", slug, err)
- }
- if !bytes.Equal(bytes.TrimSpace(model["input_modalities"]), modalities) {
- model["input_modalities"] = modalities
- modelChanged = true
- }
- imageDetailOriginal := json.RawMessage("true")
- if !bytes.Equal(bytes.TrimSpace(model["supports_image_detail_original"]), imageDetailOriginal) {
- model["supports_image_detail_original"] = imageDetailOriginal
- modelChanged = true
- }
- }
- if !modelChanged {
- continue
- }
- encoded, err := json.Marshal(model)
- if err != nil {
- return nil, fmt.Errorf("encode completed model %q: %w", slug, err)
- }
- models[i] = encoded
- changed = true
- }
- if !changed {
- return body, nil
- }
- encodedModels, err := json.Marshal(models)
- if err != nil {
- return nil, fmt.Errorf("encode top-level models array: %w", err)
- }
- envelope["models"] = encodedModels
- completed, err := json.Marshal(envelope)
- if err != nil {
- return nil, fmt.Errorf("encode JSON object: %w", err)
- }
- return completed, nil
- }
- func mergeMissingCodexModelFields(current, defaults map[string]json.RawMessage) (bool, error) {
- changed := false
- for key, defaultValue := range defaults {
- currentValue, exists := current[key]
- if exists && stringSliceContains(codexToolCapabilityFields, key) {
- continue
- }
- if !exists || (bytes.Equal(bytes.TrimSpace(currentValue), []byte("null")) &&
- !bytes.Equal(bytes.TrimSpace(defaultValue), []byte("null"))) {
- current[key] = defaultValue
- changed = true
- continue
- }
- var currentObject map[string]json.RawMessage
- var defaultObject map[string]json.RawMessage
- if err := json.Unmarshal(currentValue, ¤tObject); err != nil || currentObject == nil {
- continue
- }
- if err := json.Unmarshal(defaultValue, &defaultObject); err != nil || defaultObject == nil {
- continue
- }
- nestedChanged, err := mergeMissingCodexModelFields(currentObject, defaultObject)
- if err != nil {
- return false, err
- }
- if !nestedChanged {
- continue
- }
- mergedValue, err := json.Marshal(currentObject)
- if err != nil {
- return false, fmt.Errorf("encode field %q: %w", key, err)
- }
- current[key] = mergedValue
- changed = true
- }
- return changed, nil
- }
- func validateCodexModelsManifestEnvelope(body []byte) error {
- var envelope map[string]json.RawMessage
- if err := json.Unmarshal(body, &envelope); err != nil {
- return fmt.Errorf("decode JSON object: %w", err)
- }
- if envelope == nil {
- return errors.New("expected a JSON object")
- }
- models, ok := envelope["models"]
- if !ok {
- return errors.New("missing top-level models array")
- }
- models = bytes.TrimSpace(models)
- var entries []json.RawMessage
- if len(models) == 0 || models[0] != '[' {
- return errors.New("top-level models field is not an array")
- }
- if err := json.Unmarshal(models, &entries); err != nil {
- return fmt.Errorf("decode top-level models array: %w", err)
- }
- return nil
- }
- func buildOpenAIModelsCacheKey(request openAIModelsRequest) string {
- hasher := sha256.New()
- _, _ = fmt.Fprintf(hasher, "%d\n%d\n%t\n%s\n%s\n", request.accountID, request.credentialAccountID, request.standardModelsList, request.proxyURL, request.url)
- headerNames := make([]string, 0, len(request.headers))
- for name := range request.headers {
- headerNames = append(headerNames, name)
- }
- sort.Strings(headerNames)
- for _, name := range headerNames {
- _, _ = fmt.Fprintf(hasher, "%s\n", strings.ToLower(name))
- for _, value := range request.headers[name] {
- _, _ = fmt.Fprintf(hasher, "%s\n", value)
- }
- }
- return fmt.Sprintf("%x", hasher.Sum(nil))
- }
- func cloneOpenAIModelsResponse(manifest *OpenAIModelsResponse) *OpenAIModelsResponse {
- if manifest == nil {
- return nil
- }
- cloned := *manifest
- if manifest.Body != nil {
- cloned.Body = append([]byte(nil), manifest.Body...)
- }
- if manifest.upstreamSourceBody != nil {
- cloned.upstreamSourceBody = append([]byte(nil), manifest.upstreamSourceBody...)
- }
- return &cloned
- }
- func openAIModelsResponseForClient(manifest *OpenAIModelsResponse, ifNoneMatch string) *OpenAIModelsResponse {
- if manifest == nil {
- return nil
- }
- if codexModelsManifestETagMatches(ifNoneMatch, manifest.ETag) {
- return &OpenAIModelsResponse{ETag: manifest.ETag, NotModified: true}
- }
- return cloneOpenAIModelsResponse(manifest)
- }
- func codexModelsManifestETagMatches(ifNoneMatch, etag string) bool {
- etag = strings.TrimSpace(etag)
- if etag == "" {
- return false
- }
- normalize := func(value string) string {
- value = strings.TrimSpace(value)
- if len(value) >= 2 && strings.EqualFold(value[:2], "W/") {
- value = strings.TrimSpace(value[2:])
- }
- return value
- }
- want := normalize(etag)
- for _, candidate := range strings.Split(ifNoneMatch, ",") {
- candidate = strings.TrimSpace(candidate)
- if candidate == "*" || normalize(candidate) == want {
- return true
- }
- }
- return false
- }
- // CodexModelsManifestETagMatches applies If-None-Match semantics to a Codex
- // catalog ETag, including weak and comma-separated validators.
- func CodexModelsManifestETagMatches(ifNoneMatch, etag string) bool {
- return codexModelsManifestETagMatches(ifNoneMatch, etag)
- }
- func isOfficialOpenAIModelsBaseURL(raw string) bool {
- parsed, err := url.Parse(strings.TrimSpace(raw))
- if err != nil {
- return false
- }
- hostname := strings.TrimSuffix(parsed.Hostname(), ".")
- return strings.EqualFold(hostname, "api.openai.com")
- }
- func buildCodexModelsManifestURL(endpoint string, appendModelsPath bool, clientVersion string) (*url.URL, error) {
- requestURL, err := url.Parse(endpoint)
- if err != nil {
- return nil, err
- }
- if requestURL.Fragment != "" {
- return nil, fmt.Errorf("URL fragments are not supported")
- }
- query := requestURL.Query()
- requestURL.RawQuery = ""
- requestURL.ForceQuery = false
- if appendModelsPath {
- requestURL, err = url.Parse(buildOpenAIModelsURL(requestURL.String()))
- if err != nil {
- return nil, err
- }
- }
- query.Set("client_version", clientVersion)
- requestURL.RawQuery = query.Encode()
- return requestURL, nil
- }
|