Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- **Feishu: outbound bot-to-bot @mention resolution** via new `mention_map` config option. Maps agent-friendly names (e.g. `BOT-A`) to Feishu open_ids so that when an agent writes `@BOT-A` in its reply, cc-connect converts it to a native Feishu `<at>` tag that triggers a real notification. Layered on top of `resolve_mentions` (group-member matching) with higher priority, so explicit config always wins (#1322).

### Fixed
- **codex**: `/model` chooser now surfaces `gpt-5.x` (and any future frontier chat model) when the model list is populated by fetching `GET /v1/models` from the provider. The previous hard-coded 11-entry allowlist (`o1/o3/o4/gpt-4o/gpt-4.1/codex-mini-latest`) had not been updated since 2026-03 and silently filtered out every `gpt-5*` variant (including `gpt-5.6-sol/terra/luna`) returned by the API, so users on `codex-cli >= 0.143` could not pick GPT-5.6 in cc-connect even after upgrading the CLI. The allowlist is replaced with pattern rules: accept the `gpt-*` / `chatgpt-*` / `codex-*` / `o1-*` / `o3-*` / `o4-*` / `o5-*` families plus the bare `o1/o3/o4/o5` IDs, and reject non-chat modalities (`embedding`, `whisper`, `tts`, `dall-e`, `audio-preview`, `realtime`, `transcribe`, `moderation`, `image`, `search-preview`). New frontier chat models are picked up automatically without a code change. Note: this only affects the API-fetch fallback path; users with an explicit `model_catalog_json` in `~/.codex/config.toml` or a `models = [...]` list in cc-connect's provider config were unaffected.
- **Feishu recall fallback probes**: throttle repeated active-message recall checks so long-running turns do not continuously call platform message APIs.
- **Skill discovery depth-1 only**: skill scanning no longer recurses into subdirectories. Only `<skill_dir>/<name>/SKILL.md` is registered; nested SKILL.md files (e.g. inside `<name>/references/...`) are treated as skill assets and ignored, matching the Claude Code CLI convention. Previously, nested SKILL.md files leaked into platform command menus as phantom slash commands (101 leaked commands from `frontend-design` skill alone) (#1304).
- **Feishu: tighter `@` mention detection in `SendWithStatusFooter` / `buildReplyContent`** — a bare `@` inside an email address, URL, or escaped character no longer false-positives as a mention. Mention detection now checks for the resolved `<at user_id="...">` tag instead of a substring match, so card rendering (and the notation-style status footer) is preserved for content that merely contains `@`. Real `@mentions` still force `MsgTypeText` so Feishu fires the mention event (#1322).
Expand Down
54 changes: 48 additions & 6 deletions agent/codex/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,53 @@ func (a *Agent) AvailableModels(ctx context.Context) []core.ModelOption {
}
}

var openaiChatModels = map[string]bool{
"o4-mini": true, "o3": true, "o3-mini": true, "o1": true, "o1-mini": true,
"gpt-4.1": true, "gpt-4.1-mini": true, "gpt-4.1-nano": true,
"gpt-4o": true, "gpt-4o-mini": true,
"codex-mini-latest": true,
// nonChatSubstrings identifies non chat/completion modalities returned by
// GET /v1/models that must not appear in the codex /model chooser.
var nonChatSubstrings = []string{
"embedding", "whisper", "tts", "moderation", "dall-e",
"realtime", "transcribe", "search-preview", "image",
"audio-preview",
}

// isCodexChatModel reports whether an OpenAI-compatible model ID names a
// chat/completion model that Codex CLI can drive. Used to filter the
// /v1/models response into the /model command suggestion list.
//
// Rules (case-insensitive):
// - Reject any ID containing a non-chat modality substring (embedding,
// whisper, tts, dall-e, audio-preview, realtime, transcribe, moderation,
// image, search-preview).
// - Accept known chat family prefixes: gpt-*, chatgpt-*, codex-*, o1-*,
// o3-*, o4-*, o5-*.
// - Accept bare reasoning family IDs: o1 / o3 / o4 / o5.
//
// Uses pattern matching rather than a static allowlist so new frontier models
// (gpt-5.x, gpt-6, o5-*, codex-*, etc.) are picked up automatically.
func isCodexChatModel(id string) bool {
if id == "" {
return false
}
lower := strings.ToLower(id)
for _, s := range nonChatSubstrings {
if strings.Contains(lower, s) {
return false
}
}
switch {
case strings.HasPrefix(lower, "gpt-"),
strings.HasPrefix(lower, "chatgpt-"),
strings.HasPrefix(lower, "codex-"),
strings.HasPrefix(lower, "o1-"),
strings.HasPrefix(lower, "o3-"),
strings.HasPrefix(lower, "o4-"),
strings.HasPrefix(lower, "o5-"):
return true
}
switch lower {
case "o1", "o3", "o4", "o5":
return true
}
return false
}

func (a *Agent) fetchModelsFromAPI(ctx context.Context) []core.ModelOption {
Expand Down Expand Up @@ -290,7 +332,7 @@ func (a *Agent) fetchModelsFromAPI(ctx context.Context) []core.ModelOption {

var models []core.ModelOption
for _, m := range result.Data {
if openaiChatModels[m.ID] {
if isCodexChatModel(m.ID) {
models = append(models, core.ModelOption{Name: m.ID})
}
}
Expand Down
74 changes: 74 additions & 0 deletions agent/codex/codex_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,77 @@ func TestWorkspaceAgentOptions_PreservesStdIOAppServerURL(t *testing.T) {
t.Fatalf("WorkspaceAgentOptions()[app_server_url] = %#v, want stdio://", got)
}
}

func TestIsCodexChatModel(t *testing.T) {
tests := []struct {
id string
want bool
}{
{"", false},

// Legacy / current chat families (must keep working).
{"gpt-4o", true},
{"gpt-4o-mini", true},
{"gpt-4.1", true},
{"gpt-4.1-mini", true},
{"gpt-4.1-nano", true},
{"gpt-3.5-turbo", true},
{"chatgpt-4o-latest", true},
{"o1", true},
{"o1-mini", true},
{"o1-preview", true},
{"o3", true},
{"o3-mini", true},
{"o4", true},
{"o4-mini", true},
{"codex-mini-latest", true},

// GPT-5 series — the regression that motivated this change.
{"gpt-5", true},
{"gpt-5-mini", true},
{"gpt-5.3", true},
{"gpt-5.3-codex", true},
{"gpt-5.4", true},
{"gpt-5.5", true},
{"gpt-5.6", true},
{"gpt-5.6-sol", true},
{"gpt-5.6-terra", true},
{"gpt-5.6-luna", true},

// Case insensitivity (defensive; ids from /v1/models are usually lower).
{"GPT-5.6", true},
{"Codex-Mini-Latest", true},

// Non-chat modalities that /v1/models returns — must be rejected so
// they never show up in the /model chooser.
{"text-embedding-ada-002", false},
{"text-embedding-3-small", false},
{"text-embedding-3-large", false},
{"whisper-1", false},
{"tts-1", false},
{"tts-1-hd", false},
{"gpt-4o-realtime-preview", false},
{"gpt-4o-audio-preview", false},
{"gpt-4o-transcribe", false},
{"gpt-4o-search-preview", false},
{"dall-e-2", false},
{"dall-e-3", false},
{"gpt-image-1", false},
{"text-moderation-latest", false},
{"omni-moderation-latest", false},

// Unrelated model families that should not be surfaced.
{"babbage-002", false},
{"davinci-002", false},
{"claude-3-5-sonnet", false},
{"gemini-1.5-pro", false},
}

for _, tc := range tests {
t.Run(tc.id, func(t *testing.T) {
if got := isCodexChatModel(tc.id); got != tc.want {
t.Fatalf("isCodexChatModel(%q) = %v, want %v", tc.id, got, tc.want)
}
})
}
}
Loading