Merge pull request #7057 from Sjeary/codex/issue-7042-max-context-window

fix(codex): 保留本地模型目录的最大上下文窗口
This commit is contained in:
Wesley Liddick
2026-09-12 20:11:40 +08:00
committed by GitHub
4 changed files with 216 additions and 0 deletions
@@ -0,0 +1,187 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestCodexContextWindowSyncRoundTrip(t *testing.T) {
for _, tc := range []struct {
name string
fields string
wantWindow int64
wantMax int64
}{
{"distinct windows", `"context_window":272000,"max_context_window":872000`, 272000, 872000},
{"equal windows", `"context_window":272000,"max_context_window":272000`, 272000, 272000},
{"default only", `"context_window":272000`, 272000, 272000},
{"maximum only", `"max_context_window":872000`, 872000, 872000},
{"registry style limit", `"limit":{"context":128000}`, 128000, 128000},
{"zero maximum", `"context_window":272000,"max_context_window":0`, 272000, 272000},
{"negative maximum", `"context_window":272000,"max_context_window":-1`, 272000, 272000},
{"maximum below default", `"context_window":272000,"max_context_window":128000`, 128000, 128000},
} {
t.Run(tc.name, func(t *testing.T) {
account := codexContextWindowAccount(t, 1, `"context_window":64000`)
upstream := &httpUpstreamRecorder{resp: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"models":[{
"slug":"gpt-6-astra","reasoning":true,
"supported_reasoning_levels":[{"effort":"high"}],
"input_modalities":["text","image"],%s
}]}`, tc.fields))),
}}
repo := &upstreamModelMetadataRepoStub{}
syncer := &AccountTestService{accountRepo: repo, httpUpstream: upstream, cfg: upstreamModelSyncTestConfig()}
catalog, err := syncer.SyncUpstreamModelCatalog(context.Background(), &account)
require.NoError(t, err)
require.Empty(t, catalog.Warnings)
require.Len(t, upstream.requests, 1)
require.Equal(t, account.ID, repo.accountID)
// Reload the JSON representation persisted in account.extra, replacing
// the legacy snapshot so the test exercises resync as well as parsing.
persisted, err := json.Marshal(repo.updates)
require.NoError(t, err)
account.Extra = nil
require.NoError(t, json.Unmarshal(persisted, &account.Extra))
model := codexContextWindowManifest(t, []Account{account})
require.EqualValues(t, tc.wantWindow, model["context_window"])
require.EqualValues(t, tc.wantMax, model["max_context_window"])
})
}
}
func TestCodexContextWindowAccountIntersection(t *testing.T) {
for _, tc := range []struct {
name string
accounts []string
wantWindow int64
wantMax int64
}{
{
"independent minima",
[]string{`"context_window":272000,"max_context_window":872000`, `"context_window":300000,"max_context_window":512000`},
272000, 512000,
},
{
"legacy snapshot caps maximum",
[]string{`"context_window":272000,"max_context_window":872000`, `"context_window":300000`},
272000, 300000,
},
{
"legacy snapshot alone",
[]string{`"context_window":272000`},
272000, 272000,
},
{
"invalid maximum uses known default",
[]string{`"context_window":272000,"max_context_window":872000`, `"context_window":300000,"max_context_window":-1`},
272000, 300000,
},
} {
t.Run(tc.name, func(t *testing.T) {
accounts := make([]Account, len(tc.accounts))
for i, fields := range tc.accounts {
accounts[i] = codexContextWindowAccount(t, int64(i+1), fields)
}
for range 2 {
model := codexContextWindowManifest(t, accounts)
require.EqualValues(t, tc.wantWindow, model["context_window"])
require.EqualValues(t, tc.wantMax, model["max_context_window"])
for i, j := 0, len(accounts)-1; i < j; i, j = i+1, j-1 {
accounts[i], accounts[j] = accounts[j], accounts[i]
}
}
})
}
}
func TestCodexContextWindowRegistryEnrichment(t *testing.T) {
for _, tc := range []struct {
name string
fields string
wantWindow int64
wantMax int64
}{
{"preserve upstream limits", `"context_window":272000,"max_context_window":872000`, 272000, 872000},
{"upstream default remains ceiling", `"context_window":272000`, 272000, 272000},
{"registry supplies missing limits", `"description":"Model without context metadata"`, 1050000, 1050000},
} {
t.Run(tc.name, func(t *testing.T) {
account := codexContextWindowAccount(t, 1, `"context_window":64000`)
account.Type = AccountTypeAPIKey
account.Credentials["api_key"] = "test-key"
account.Credentials["base_url"] = "https://provider.example/v1"
upstream := &httpUpstreamRecorder{responses: []*http.Response{
{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(fmt.Sprintf(
`{"data":[{"id":"gpt-6-astra",%s}]}`, tc.fields))),
},
{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"provider":{
"id":"provider","api":"https://provider.example/v1","models":{
"gpt-6-astra":{"id":"gpt-6-astra","reasoning":true,
"reasoning_options":[{"type":"effort","values":["high"]}],
"modalities":{"input":["text","image"]},"limit":{"context":1050000}}
}
}}`)),
},
}}
syncer := &AccountTestService{
accountRepo: &upstreamModelMetadataRepoStub{}, httpUpstream: upstream, cfg: upstreamModelSyncTestConfig(),
}
catalog, err := syncer.SyncUpstreamModelCatalog(context.Background(), &account)
require.NoError(t, err)
require.Empty(t, catalog.Warnings)
require.Len(t, upstream.requests, 2)
model := codexContextWindowManifest(t, []Account{account})
require.EqualValues(t, tc.wantWindow, model["context_window"])
require.EqualValues(t, tc.wantMax, model["max_context_window"])
})
}
}
func codexContextWindowAccount(t *testing.T, id int64, fields string) Account {
t.Helper()
account := Account{
ID: id, Platform: PlatformOpenAI, Type: AccountTypeOAuth,
Credentials: map[string]any{
"access_token": "test-token",
"model_mapping": map[string]any{"gpt-6-astra": "gpt-6-astra"},
},
}
err := json.Unmarshal([]byte(fmt.Sprintf(`{"upstream_model_metadata":{
"source":"upstream","models":{"gpt-6-astra":{
"id":"gpt-6-astra","reasoning":true,
"supported_reasoning_levels":["high"],"input_modalities":["text","image"],%s
}}
}}`, fields)), &account.Extra)
require.NoError(t, err)
return account
}
func codexContextWindowManifest(t *testing.T, accounts []Account) map[string]any {
t.Helper()
const groupID int64 = 7042
svc := &GatewayService{accountRepo: codexModelsVisibilityAccountRepo{byGroup: map[int64][]Account{
groupID: accounts,
}}}
body, err := svc.BuildCodexModelsManifestForGroup(
context.Background(), &Group{ID: groupID, Platform: PlatformOpenAI}, "", []string{"gpt-6-astra"},
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
return models[0]
}
@@ -350,6 +350,21 @@ func intersectUpstreamModelMetadata(modelID string, candidates []UpstreamModelMe
if !contextKnown {
result.ContextWindow = 0
}
for i, candidate := range candidates {
maxContextWindow := candidate.MaxContextWindow
if maxContextWindow <= 0 {
// Older snapshots only stored the default window. Keep that bound
// until a sync supplies the upstream's explicit maximum.
maxContextWindow = candidate.ContextWindow
}
if maxContextWindow <= 0 {
result.MaxContextWindow = 0
break
}
if i == 0 || maxContextWindow < result.MaxContextWindow {
result.MaxContextWindow = maxContextWindow
}
}
return result
}
@@ -405,6 +420,10 @@ func applyUpstreamModelMetadataToCodexDescriptor(
descriptor.ContextWindow = metadata.ContextWindow
descriptor.MaxContextWindow = metadata.ContextWindow
}
if metadata.MaxContextWindow > 0 {
descriptor.MaxContextWindow = metadata.MaxContextWindow
descriptor.ContextWindow = min(descriptor.ContextWindow, metadata.MaxContextWindow)
}
}
func configuredCodexReasoningLevelDescription(level string) string {
@@ -36,6 +36,7 @@ type UpstreamModelMetadata struct {
SupportedReasoningLevels []string `json:"supported_reasoning_levels,omitempty"`
InputModalities []string `json:"input_modalities,omitempty"`
ContextWindow int64 `json:"context_window,omitempty"`
MaxContextWindow int64 `json:"max_context_window,omitempty"`
MaxOutputTokens int64 `json:"max_output_tokens,omitempty"`
CodexToolCapabilities map[string]json.RawMessage `json:"codex_tool_capabilities,omitempty"`
}
@@ -389,6 +390,7 @@ func upstreamModelMetadataIsUseful(metadata UpstreamModelMetadata) bool {
len(metadata.InputModalities) > 0 ||
len(metadata.CodexToolCapabilities) > 0 ||
metadata.ContextWindow > 0 ||
metadata.MaxContextWindow > 0 ||
metadata.MaxOutputTokens > 0
}
@@ -472,6 +474,11 @@ func mergeUpstreamModelMetadata(primary, fallback UpstreamModelMetadata) (Upstre
}
if merged.ContextWindow <= 0 && fallback.ContextWindow > 0 {
merged.ContextWindow = fallback.ContextWindow
// Keep the registry's context limits together. A direct upstream default
// without an explicit maximum remains the conservative ceiling.
if merged.MaxContextWindow <= 0 {
merged.MaxContextWindow = fallback.MaxContextWindow
}
changed = true
}
if merged.MaxOutputTokens <= 0 && fallback.MaxOutputTokens > 0 {
@@ -582,6 +589,7 @@ func upstreamMetadataFromModelsDevModel(modelID string, model modelsDevModel) Up
SupportedReasoningLevels: levels,
InputModalities: normalizeCodexInputModalities(model.Modalities.Input),
ContextWindow: model.Limit.Context,
MaxContextWindow: model.Limit.Context,
MaxOutputTokens: model.Limit.Output,
}
if len(levels) > 0 {
@@ -1360,6 +1368,7 @@ func upstreamMetadataFromCapabilityEntry(modelID string, entry upstreamModelCapa
SupportedReasoningLevels: levels,
InputModalities: normalizeCodexInputModalities(modalities),
ContextWindow: contextWindow,
MaxContextWindow: entry.MaxContextWindow,
MaxOutputTokens: maxOutputTokens,
}
}
+1
View File
@@ -620,6 +620,7 @@ export interface UpstreamModelMetadata {
supported_reasoning_levels?: string[]
input_modalities?: string[]
context_window?: number
max_context_window?: number
max_output_tokens?: number
}