Feature Gate 常被理解为“界面里藏一个实验按钮”。在 Codex 中,它更接近 Runtime 组装输入:同一 份源码可以根据有效 Feature 集合选择不同 Code Mode Provider、工具规格、网络服务、多 Agent 版本 和模型请求参数。
因此不能只问“某 Feature 是 true 还是 false”。还要问:默认值来自哪里、Profile 是否覆盖、旧键 怎样迁移、依赖怎样补齐、受管要求是否允许,以及哪个组件最终消费它。
一个 Feature 有三层身份
| 层 | 数据 | 解决的问题 |
|---|---|---|
| Registry | Feature ID、canonical key、Stage、default | 这个开关叫什么,处于什么生命周期 |
| Resolution | defaults、base、profile、override、dependency | 当前 Session 中最终是否启用 |
| Consumption | ThreadManager、Tool Plan、Session、Turn | 启用后究竟改变哪条 Runtime 分支 |
Stage 不是 enabled。Stable Feature 仍可以被关闭;Experimental Feature 可以被显式开启;Removed Feature 可能为了旧配置可解析而保留枚举项,却没有任何生产行为。
Registry 把开关元数据集中起来
每个 Feature 的规范条目包含 ID、配置 key、生命周期阶段和默认值。集中 Registry 带来三个约束:
- CLI
--enable name、TOML key 和诊断共享 canonical name; - UnderDevelopment 默认必须关闭;
- 默认开启项必须是 Stable 或兼容性的 Removed 项。
这些约束由 Registry 测试保护,而不是依赖开发者记住约定。
@dataclass(frozen=True)
class FeatureSpec:
feature: Feature
key: str
stage: Stage
default_enabled: bool
def validate_registry(registry: list[FeatureSpec]) -> None:
assert unique(spec.feature for spec in registry)
assert unique(spec.key for spec in registry)
for spec in registry:
if spec.stage is UNDER_DEVELOPMENT:
assert not spec.default_enabled
if spec.default_enabled:
assert spec.stage in {STABLE, REMOVED}
Stage 主要服务发布与诊断。真正的运行分支只应读取规范化后的有效集合。
有效集合按五步形成
当前解析顺序是:
Registry defaults
→ base config
→ selected profile
→ explicit overrides
→ dependency normalization
后一步覆盖前一步。Base 和 Profile 都会先应用旧顶层兼容开关,再应用 [features] 表;显式
override 用于更靠近 Runtime 的特殊选择。最后才补依赖。
def resolve_features(
registry: FeatureRegistry,
base: FeatureConfigSource,
profile: FeatureConfigSource,
overrides: FeatureOverrides,
requirements: FeatureRequirements,
) -> EffectiveFeatures:
enabled = registry.default_enabled_set()
for source in (base, profile):
apply_legacy_toggles(enabled, source)
apply_feature_table(enabled, source.features)
apply_explicit_overrides(enabled, overrides)
normalize_dependencies(enabled)
return requirements.constrain(enabled)
不要在伪代码里加入一套通用依赖图。当前源码明确规范化的依赖只有:开启 CodeModeOnly 时确保 CodeMode 同时开启。其他组合通常在消费点求交集,而不是自动打开更多能力。
bool 和结构化 Feature 共用一个开关语义
简单 Feature 可以写成布尔值:
[features]
plugins = true
需要参数的 Feature 可以写成表:
[features.code_mode_host]
enabled = true
disable_in_process_fallback = true
结构化表不能只解析参数而忘记 enabled,也不能在把 resolved state 写回配置时覆盖其他参数。内部
的 FeatureToml 因而支持 Enabled(bool) 与 Config(T) 两种形状,统一提取开关,同时保留配置体。
def read_feature_entry(entry: bool | FeatureConfig) -> tuple[bool | None, FeatureConfig | None]:
if isinstance(entry, bool):
return entry, None
return entry.enabled, entry
def materialize_resolved_state(entry: FeatureEntry | None, enabled: bool) -> FeatureEntry:
if entry is None:
return FeatureEntry(enabled=enabled)
entry.enabled = enabled
return entry # 其他结构化参数保持不变
兼容键不等于兼容行为
旧配置可能还包含已删除的 undo、js_repl、旧 Tool Search 或 TUI App Server 开关。解析层可以
识别并忽略这些键,避免旧文件让新版本直接崩溃;但它不会重新启用已经删除的实现。
另一些 legacy alias 会映射到 canonical Feature,并记录迁移提示。因而必须区分:
- canonical key:直接控制当前 Feature;
- legacy alias:映射当前 Feature,同时产生诊断;
- removed compatibility key:接受输入但不改变有效集合;
- unknown key:记录 warning,不发明功能。
如果文章看到枚举里有 RemovedFeature 就说“Codex 支持该功能”,会把解析兼容误写成产品能力。
Enabled 仍然只是必要条件
Apps 是最直接的例子。只有 Feature 开启且认证模式满足条件时,Apps 才真正可用。Tool Plan 还可能 要求 Plugins 同时开启。平台、模型能力、执行环境能力和受管 Requirements 也可以继续收窄。
可以把最终能力写成谓词:
apps_available = (
features.enabled(APPS)
and features.enabled(PLUGINS)
and auth.has_chatgpt_account
and requirements.allow_apps
)
这个谓词不意味着某个单一函数恰好这样书写,而是解释为什么“Feature=true”与“模型看得到工具” 之间还有条件。
Feature 在不同时间尺度被消费
某些分支在 ThreadManager 创建时冻结。例如 CodeModeHost 决定使用进程外 Provider、禁用 Provider 还是进程内 Provider。某些分支在 Session 或 Step 构建工具计划时读取,例如 CodeModeOnly、 UnifiedExec、Plugins 与 Apps。FastMode 还要进入 TurnContext,与当轮 service tier 共同决定请求。
这带来一个重要后果:运行期间修改配置,不保证现有 Thread 的所有服务立即重组。若 Provider 在 ThreadManager 创建时选择,简单刷新下一 Step 的 Tool Plan 无法替换它。后续章节会分别说明配置 刷新与 Thread/Step 快照。
几个真实 Runtime 分支
Code Mode Provider
def choose_code_mode_provider(config: Config) -> CodeModeProvider:
if config.features.enabled(CODE_MODE_HOST):
provider = ProcessOwnedProvider()
return provider.with_fallback(
enabled=not config.code_mode.disable_in_process_fallback
)
if config.code_mode.disable_in_process_fallback:
return DisabledProvider()
return InProcessProvider()
这里 Feature 与参数共同决定三分支。只看 CodeModeHost bool 会漏掉“host 关闭但 fallback 也被禁用” 的 Disabled 状态。
多 Agent 版本
MultiAgentV2 开启时强制 V2;若总 Agent 能力关闭则 Disabled;否则再结合模型声明与 Collab Feature
选择 V1 或 Disabled。它不是一个简单 if collab。
Deferred Executor
开启后,Session 可以在部分执行器仍启动时允许 Turn 进入;关闭时,启动顺序更严格。该 Feature 改变并发时序和失败位置,不只是模型是否看到一个工具名称。
Network Proxy
开启后 Config/Session 可能建立受管代理,并让沙箱网络路径使用它。关闭不代表网络一定不可用, 只表示不走这条受管代理组合。
UnderDevelopment 警告只针对实际显式启用项
系统会检查有效 [features] 表,收集启用且 Stage 为 UnderDevelopment 的 canonical key,形成
Warning Event。若开关随后被 Requirements 禁用,不应继续把它列为“正在运行”。用户也可以显式
抑制警告,但这不会改变 Feature 风险或能力。
失败矩阵
| 输入或条件 | 解析/消费结果 |
|---|---|
| 未知 Feature key | warning,忽略,不创建动态 Feature |
| legacy alias | 映射 canonical Feature并记录迁移提示 |
| removed compatibility key | 可反序列化但不改变 Runtime |
| CodeModeOnly=true、CodeMode=false | 依赖规范化后 CodeMode=true |
| Apps=true、认证不满足 | Apps 不可用 |
| 结构化 Feature 缺少 enabled | 采用该配置类型的默认/未指定语义 |
| UnderDevelopment 显式开启 | 运行并产生不稳定警告,除非被约束或抑制 |
| Requirements 禁止 Feature | 领域 Config 阶段拒绝或关闭,普通层不能绕过 |
| Feature 在 Manager 创建后改变 | 已冻结的 Provider 不自动替换 |
测试重点不是数量,而是组合
def test_code_mode_only_normalizes_dependency() -> None:
resolved = resolve(base={"code_mode_only": True, "code_mode": False})
assert resolved.enabled("code_mode_only")
assert resolved.enabled("code_mode")
def test_removed_key_is_not_runtime_capability() -> None:
resolved = resolve(base={"undo": True})
assert not resolved.has_runtime_capability("undo")
def test_apps_requires_auth_after_feature_resolution() -> None:
resolved = resolve(base={"apps": True})
assert not apps_available(resolved, auth=ApiKeyAuth())
assert apps_available(resolved, auth=ChatGptAuth())
生产测试还覆盖默认开启项阶段、结构化 Feature 参数、来源覆盖顺序、Warning Event,以及 Core Config 和 Tool Plan 的具体消费点。新增 Feature 若只给 Registry 加一行而没有消费测试,很容易成为永远不 生效或无法关闭的“幽灵开关”。
小结
Feature Gate 是 Runtime 组合协议。Registry 定义身份与默认值,Resolver 处理来源、兼容键和依赖, Requirements 收窄权限,具体组件再在自己的生命周期读取有效集合。
当配置与 Feature 都确定后,App Server 才能创建共享的 ThreadManager。下一节将追踪一个 Thread 怎样从新建、恢复或派生请求进入 Session,为什么必须等到第一个 SessionConfigured 事件后才能 注册为可见 Thread。
评论
登录后即可评论