# 调研结果：按模型上下文配置

本文件整合 cc-switch 实现机制、CodexPlusPlus 源码根因、#931 运行时验证结论，作为 feature 设计依据。

## 一、问题背景

issue #1171 / #931：CodexPlusPlus 使用第三方模型（deepseek-v4-pro 1M、Claude 200K）时，上下文窗口被锁在 258K，无法按模型配置。

## 二、#931 运行时根因与解法（已验证）

### 根因
- `codex` 客户端二进制硬编码 `context_window: 272000`
- codex App 用**编译时内置模型列表**作为最终权威；custom provider 的 slug 不在二进制列表里，触发回落到 272000
- 离线可复现（断网仍是 258.4K），排除服务端 catalog 拉取

### 解法（jsjingsong 验证可行）
`model_catalog_json` 指向外部 catalog 文件，让 codex 读外部 catalog 覆盖编译默认值。关键约束：

| 路径格式 | 结果 |
| --- | --- |
| 相对路径 `"cc-switch-model-catalog.json"` | ❌（Windows） |
| 绝对路径单引号 `'C:\Users\...\file.json'` | ❌ |
| 绝对路径双引号 + `\\` 转义 `"C:\\Users\\...\\file.json"` | ✅ |

> 注：以上是 **Windows** 结论。Mac 路径格式（相对/绝对、是否需转义）需原型验证。

### 验证结果
- context_window 扩到 950K（=1,000,000 × 0.95，受 `effective_context_window_percent` 影响）
- auto_compact 手写 850000 生效

### 副作用（#1064 确认）
配 catalog 后，**只有 `model =` 指定的模型出现在模型列表**，其余 catalog 模型不显示。这是 #1171 要解决的核心痛点之一——生成 catalog 时须把 profile 全部模型写进去，而非仅当前 model。

## 三、CodexPlusPlus 源码根因

### 已有能力
- per-profile 单值配置：`RelayProfile.context_window` / `auto_compact_limit`，写入 config.toml 顶层 `model_context_window` / `model_auto_compact_token_limit`（`relay_config.rs` 的 `apply_context_limits_to_config`）
- 读取 codex 的 `model_catalog_json`：`model_catalog.rs` 的 `models_from_config_model_catalog_json`

### 增量根因
1. **解析丢弃字段**：`parse_model_catalog_json_models`（`model_catalog.rs:678`）读 catalog 只取 `slug`，丢弃 `context_window`，前端无法按模型显示窗口
2. **无生成 catalog 能力**：apply 流程全程不生成 catalog 文件。测试 `apply_relay_profile_does_not_write_model_catalog_json_for_selected_models`（`relay_config.rs:938`）明确验证「不写」；`apply_relay_profile_preserves_user_model_catalog_json`（`:974`）仅保留用户手写指针
3. **`model_list` 是纯文本 slug 字符串**：`RelayProfile.model_list`（`settings.rs:43`），按换行/逗号分隔，无 per-model 元数据。但**不进 config.toml**（仅 `merge_model_into_model_list` 内部合并），所以加后缀不污染 codex 请求
4. 内置 `assets/codex-models.json` 第三方模型无元数据时回落默认值，加剧「都按同一长度显示」

### apply 流程（插入点）
```
complete_relay_profile_config → merge_common → preserve_unmanaged
→ apply_context_limits_to_config（写顶层单值）
→ 落盘
```
全程不触 catalog 生成。新步骤插在 `apply_context_limits_to_config` 之后、落盘之前。

## 四、cc-switch 实现机制（参考实现）

cc-switch 对 codex 用原生 `model_catalog_json`（`src-tauri/src/codex_config.rs`）：

1. 每个 provider preset 的 `modelCatalog` 是模型数组，每条带 `contextWindow`
2. 生成 catalog JSON：`codex_catalog_model_entry`（`:314`）每条写
   - `slug` / `display_name` / `description`
   - `context_window` / `max_context_window`（同值）
   - `priority` / `additional_speed_tiers` / `service_tiers` / `availability_nux` / `upgrade`
3. **template clone 思路**：从 `codex debug models --bundled` 取 codex 自带 entry 做 clone，再覆盖字段，保证字段齐全（避免 codex 因缺字段忽略整个条目）
4. config.toml 写 `model_catalog_json = "<filename>"` 指针
5. 顶层 `model_context_window` 作为未单独配置时的回退默认

### cc-switch catalog 不写的字段
- **不写 `auto_compact_token_limit`**：catalog 条目无此字段，压缩靠 codex 默认比例
- cc-switch 内部 spec 用 `n` 别名，但**最终 catalog 写的是 `context_window`**（`n` 不进产物）

## 五、关键字段对照

| 字段 | 来源 | 作用 |
| --- | --- | --- |
| `context_window` | catalog 条目 | 模型上下文窗口 |
| `max_context_window` | catalog 条目 | 最大窗口（与 context_window 同值） |
| `effective_context_window_percent` | catalog 条目（cc-switch 写 95） | effective window = context_window × percent，影响显示与实际可用 |
| `auto_compact_token_limit` | catalog 条目 / config.toml 顶层 | 压缩触发阈值；codex 内置模型为 null（按比例） |
| `model_context_window` | config.toml 顶层 | 未单独配置时的回退默认 |
| `model_auto_compact_token_limit` | config.toml 顶层 | 压缩回退默认 |
| `model_catalog_json` | config.toml 顶层 | 指向 catalog 文件的指针 |

## 六、auto_compact 行为结论

codex 内置 catalog（`assets/codex-models.json`）里 gpt-5.5 自身就是 `auto_compact_token_limit: null` + `context_window: 272000`。若 null 导致低值压缩，codex 自带模型早异常——证明 **null = codex 按 context_window 比例自动算**，非固定小值。

→ 第一阶段 catalog 条目 auto_compact 留 null 可行，不会出现「1M 到 220K 就压缩」。但须实跑验证；若第三方模型异常，fallback 写 `context_window × 0.85`。

## 七、现有 issue 汇总

| issue | 主题 |
| --- | --- |
| #931 | 运行时根因（硬编码 272000）+ model_catalog_json 解法（最深） |
| #1171 | 源码侧根因 + 自动化方案（本 fork 对应） |
| #1064 #287 #857 | 按模型 1M / 单模型列表副作用 |
| #940 #948 #540 #787 #813 #835 | 同类 258K cap 诉求 |
| #251 #130 #188 #905 | 压缩阈值可配 |
| #751 | 上下文显示错误 |

## 八、关键约束（影响实现）

1. **Mac 路径格式待验证**：#931 转义结论是 Windows 的；Mac 用相对路径（`model-catalogs/<id>.json`）是否可行须原型验证
2. **单模型副作用**：生成 catalog 必须包含 profile 全部模型，否则切换丢失
3. **effective_context_window_percent**：若想显示真实 1M 而非 950K，catalog 条目要写 `100` 或省略让 codex 默认
4. **template 字段齐全**：抄 cc-switch，clone codex 自带 entry 再覆盖，避免缺字段被忽略
5. **向后兼容**：无后缀 no-op，不破坏 per-profile 单值行为和现有「不写 catalog」测试

## 九、阶段一 A/B 实跑验证结论（Mac）

> 验证日期：2026-06-24  
> 验证环境：macOS，`/usr/local/bin/codex --version` = 0.128.0  
> 验证人：Kimi Code CLI（任务 6）

### A. 预检——手写最小 catalog

手写 catalog `/tmp/cpp-proto-catalog.json`，基于 `codex debug models --bundled` 的 `gpt-5.5` entry 做模板 clone，仅覆盖 `slug` / `display_name` / `description` / `context_window` / `max_context_window` / `effective_context_window_percent` / `auto_compact_token_limit` / `priority` 等字段后，执行：

```bash
codex debug models \
  -c model="deepseek-v4-pro" \
  -c model_catalog_json="/tmp/cpp-proto-catalog.json" \
  | python3 -c "..."
```

**输出关键摘录**：

```json
[
  {
    "slug": "deepseek-v4-pro",
    "context_window": 1000000,
    "effective_context_window_percent": 100,
    "auto_compact_token_limit": null
  }
]
```

**结论**：Mac 绝对路径 `/tmp/...` 可被 codex 0.128.0 正常读取；`context_window` 成功报到 `1000000`（而非 272000）。

### B. 对拍——原型产物实跑 codex

执行 example 生成 catalog：

```bash
cargo run -p codex-plus-core --example generate_model_catalog -- \
  "deepseek-v4-pro[1M]" "claude-sonnet-4[200K]" 2>/dev/null > /tmp/cpp-proto-out.json
```

`/tmp/cpp-proto-out.json` 内容检查：

- `slug` 已剥离后缀，为 `deepseek-v4-pro` 和 `claude-sonnet-4`；
- `context_window` / `max_context_window` 分别为 `1000000` 和 `200000`；
- `effective_context_window_percent` 为 `100`；
- `auto_compact_token_limit` 为 `null`；
- 输出中无 `[1M]` / `[200K]` 后缀；
- 条目已按 cc-switch 思路 clone `codex debug models --bundled` 的 `gpt-5.5` entry，字段齐全。

将原型产物喂给 codex：

```bash
codex debug models \
  -c model="deepseek-v4-pro" \
  -c model_catalog_json="/tmp/cpp-proto-out.json"
```

**输出关键摘录**：

```json
[
  {
    "slug": "deepseek-v4-pro",
    "context_window": 1000000,
    "effective_context_window_percent": 100
  },
  {
    "slug": "claude-sonnet-4",
    "context_window": 200000,
    "effective_context_window_percent": 100
  }
]
```

**结论**：原型产物通过 codex 0.128.0 schema 校验，`deepseek-v4-pro` 报 `1000000`、`claude-sonnet-4` 报 `200000`，且无单模型副作用，两个模型同时列出。

### 必验项逐项结论

| 项 | 结论 | 备注 |
| --- | --- | --- |
| 1. Mac 绝对路径 | ✅ 生效 | `-c model_catalog_json="/tmp/cpp-proto-catalog.json"` 正常读取 |
| 1. Mac 相对路径 | ✅ 在 config.toml 场景生效；⚠️ `-c` 内联场景不生效 | 通过临时写入 `~/.codex/config.toml` 的 `model_catalog_json = "model-catalogs/proto-two.json"` 验证通过；说明 apply 流程写入的相对路径会被 codex 以 `~/.codex/` 为基准解析。但直接用 `-c model_catalog_json="model-catalogs/..."` 时，codex 以**当前工作目录**解析，找不到文件。 |
| 2. 字段名 `context_window` + `max_context_window` | ✅ 生效 | A 预检中 codex 报告 `context_window: 1000000` |
| 3. `auto_compact_token_limit: null` | 未直接暴露，需后续使用验证 | `codex debug models` 输出中只保留 `context_window` / `max_context_window` / `effective_context_window_percent`，不返回 compact 触发阈值；null 时 codex 大概率按 context_window 比例自动计算（与内置模型一致），但第三方模型实际压缩行为需真实会话验证。 |
| 4. `effective_context_window_percent` | ✅ 显式写 100 生效 | 当 catalog 显式写 `"effective_context_window_percent": 100` 时，codex 输出 `effective_context_window_percent: 100`，effective window = 1M；省略会回落默认 95。 |
| 5. 单模型副作用 | ✅ catalog 含全部模型时均列出 | 用含 `deepseek-v4-pro` + `claude-sonnet-4` 的有效 catalog 验证，`codex debug models` 同时列出两个模型，#1064 可通过写入全部模型解决。 |

### 意外发现 / 非阻塞项

1. **codex 0.128.0 对 catalog 字段要求严格**：仅写最小字段会报 `missing field supported_reasoning_levels` / `base_instructions` 等。已通过内置 bundled entry 模板 clone 解决。
2. **`-c` 内联覆盖的相对路径解析基准与 config.toml 不同**：
   - config.toml 里的 `model_catalog_json = "model-catalogs/xxx.json"` 以 `~/.codex/` 为基准；
   - `-c model_catalog_json="model-catalogs/xxx.json"` 以运行命令时的当前工作目录为基准。
   这会影响手工验证时的路径写法，但不影响 apply 流程（apply 写入 config.toml）。
3. **effective_context_window_percent 默认 95**：codex 内置模型也写 95；本 feature 选择显式写入 `100`，让第三方模型按真实窗口显示。

### 阶段一结论

- A 预检通过：Mac 绝对路径生效，`context_window` 正确报到 1M。
- B 对拍通过：原型产物（template-clone + 后缀剥离 + effective=100）通过 codex 校验，多模型同时列出。
- 阶段一所有必验项均得到明确结论，可进入阶段二（解析回传）与阶段三（前端结构化 UI）。
