diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index e2658581e7..cee3336c76 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "net/url" + "regexp" "sort" "strings" "sync" @@ -443,7 +444,7 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr Priority: configuredCodexModelPriority, AdditionalSpeedTiers: []string{}, ServiceTiers: []configuredCodexServiceTier{}, - ModelMessages: configuredCodexModelMessages{InstructionsTemplate: openai.CodexBaseInstructionsForModel(modelID)}, + ModelMessages: configuredCodexModelMessages{InstructionsTemplate: codexInstructionsTemplateForModel(modelID)}, SupportsReasoningSummaryParameter: true, DefaultReasoningSummary: "auto", WebSearchToolType: "text", @@ -636,6 +637,47 @@ func configuredCodexGPTReasoningLevels(modelID string) []configuredCodexReasonin return levels } +// codexGPTIdentityPatterns match the GPT self-identification that the bundled +// Codex prompts open with: +// - "You are Codex, a coding agent based on GPT-5." / "…an agent based on GPT-6." / "You are Codex, based on GPT-5." +// - "You are GPT-5.1 running in the Codex CLI, …" +var codexGPTIdentityPatterns = []struct { + re *regexp.Regexp + repl string +}{ + {regexp.MustCompile(`,?\s*based on GPT-\d+(?:\.\d+)?(?:-[A-Za-z0-9]+)*`), ""}, + {regexp.MustCompile(`\bYou are GPT-\d+(?:\.\d+)?(?:-[A-Za-z0-9]+)*`), "You are Codex"}, +} + +// codexInstructionsTemplateForModel returns the Codex base prompt advertised for +// modelID in the models manifest. +// +// OpenAI GPT models keep the bundled prompt verbatim. Every other family the +// manifest can list (Gemini, Claude, DeepSeek, Grok, gpt-oss, custom aliases) +// gets the same prompt without the GPT self-identification: the claim is false +// for those models, and Google's Antigravity backend answers requests whose +// system prompt is the Codex prompt carrying a "GPT-5" identity with +// 429 RESOURCE_EXHAUSTED — which also puts the account into rate-limit +// cooldown. Codex sends this template as `instructions` for the selected model. +func codexInstructionsTemplateForModel(modelID string) string { + base := openai.CodexBaseInstructionsForModel(modelID) + if codexModelKeepsGPTIdentity(modelID) { + return base + } + for _, p := range codexGPTIdentityPatterns { + base = p.re.ReplaceAllString(base, p.repl) + } + return base +} + +// codexModelKeepsGPTIdentity reports whether modelID is an OpenAI GPT model +// whose Codex prompt may claim a GPT identity. gpt-oss models are open-weight +// and typically served by non-OpenAI providers, so they do not. +func codexModelKeepsGPTIdentity(modelID string) bool { + return isOpenAICodexGPTModel(modelID) && + !strings.HasPrefix(canonicalizeOpenAIModelAliasSpelling(modelID), "gpt-oss") +} + func isOpenAICodexGPTModel(modelID string) bool { normalized := canonicalizeOpenAIModelAliasSpelling(modelID) if normalized == "" || strings.HasPrefix(normalized, "gpt-image") { diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index 888a32f55c..5ab3b87f6c 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -3703,3 +3703,59 @@ func TestFetchCodexModelsManifestOAuthSharedAcrossGroupsWithIndependentFiltering require.Equal(t, []string{"model-b"}, got[92]) require.EqualValues(t, 1, calls.Load(), "同一账号两个分组同时请求时只发一次上游请求") } + +// Scenario: 非 OpenAI GPT 模型的 Codex 提示词不声称自己是 GPT。 +// Antigravity(Google) 对「Codex 提示词 + GPT-5 身份」直接回 429 RESOURCE_EXHAUSTED。 +func TestBuildCodexModelsManifestStripsGPTIdentityForNonGPTModels(t *testing.T) { + t.Parallel() + + nonGPT := []string{ + "gemini-3-flash", "claude-sonnet-4-6", "deepseek-v4-flash", "grok-4.3", + "gpt-oss-120b-medium", "company-coding-model", "company-codex-alias", + } + body, err := BuildCodexModelsManifest(nonGPT) + require.NoError(t, err) + + var manifest struct { + Models []struct { + Slug string `json:"slug"` + ModelMessages struct { + InstructionsTemplate string `json:"instructions_template"` + } `json:"model_messages"` + } `json:"models"` + } + require.NoError(t, json.Unmarshal(body, &manifest)) + require.Len(t, manifest.Models, len(nonGPT)) + for _, m := range manifest.Models { + tmpl := m.ModelMessages.InstructionsTemplate + require.NotContains(t, tmpl, "GPT-", m.Slug) + require.True(t, strings.HasPrefix(tmpl, "You are Codex"), "%s: %.60q", m.Slug, tmpl) + // Only the identity clause is removed; the rest of the bundled prompt is kept. + base := openai.CodexBaseInstructionsForModel(m.Slug) + require.Less(t, len(base)-len(tmpl), 32, m.Slug) + } +} + +// Scenario: OpenAI GPT 模型的 Codex 提示词保持原样。 +func TestBuildCodexModelsManifestKeepsGPTIdentityForGPTModels(t *testing.T) { + t.Parallel() + + for _, model := range []string{"gpt-5.5", "gpt-6-astra", "gpt-5.2", "gpt-5.3-codex-spark"} { + require.Equal(t, openai.CodexBaseInstructionsForModel(model), codexInstructionsTemplateForModel(model), model) + } +} + +// Scenario: 每份内置 Codex 提示词的身份声明都能被识别并去掉。 +func TestCodexGPTIdentityPatternsCoverBundledPrompts(t *testing.T) { + t.Parallel() + + for _, model := range []string{"gpt-5.1", "gpt-5.2", "gpt-5.5", "gpt-6-astra", "gpt-5-codex"} { + tmpl := openai.CodexBaseInstructionsForModel(model) + require.Contains(t, tmpl, "GPT-", "bundled prompt for %s changed; update this test", model) + for _, p := range codexGPTIdentityPatterns { + tmpl = p.re.ReplaceAllString(tmpl, p.repl) + } + require.NotContains(t, tmpl, "GPT-", model) + require.True(t, strings.HasPrefix(tmpl, "You are Codex"), "%s: %.60q", model, tmpl) + } +}