Skip to content

HostedSpec与模型工具

追踪 hosted web search 与本地 runtime 的边界,解释 provider、模式、Responses Lite、Guardian 和 standalone extension 如何决定模型原生工具。

基于rust-v0.150.0
CodexRustToolsResponses API

HostedSpec与模型工具 ​

Codex 的工具表里有一类工具没有本地 Registry runtime,却仍然会出现在 Responses 请求中。当前最清晰的例子是 hosted web_search:Core 只生成 ToolSpec::WebSearch,provider 负责执行,模型响应中的 WebSearchCall 再由 Responses 流处理路径消费。它和 extension 的 web/run 不是同一条实现,只在特定条件下互相替代。

本文面向已经读过SpecPlan生成算法和ToolSpec与函数规格 的读者。本文只研究 hosted web search 的规格生成、请求消费者和响应边界,不展开搜索 provider 的服务器实现, 也不把 MCP/extension 的搜索 handler 当作 hosted tool。读完后,读者应能根据 provider capability、Responses Lite、 Guardian、web search mode、模型输入模态和 web/run 来源,判断当前 Step 是否生成 hosted spec。

1. 两种工具面 ​

1.1 Runtime工具 ​

普通 function、custom、MCP 和 extension 工具进入 ToolRegistry,拥有 ToolExecutor runtime,调用时会创建 ToolInvocation 并在本地执行。它们的 ToolSpec 与 runtime 由同一个注册条目关联。

1.2 Hosted工具 ​

hosted spec 只进入 hosted_specs,随后追加到 build_model_visible_specs 的规格序列;它不进入 Registry,不参与 tool_runtime() 查找,也不走 ToolPayload::Function/Custom 的本地 handler 分派。模型返回 hosted call 后,Responses 流层按 ResponseItem::WebSearchCall 等协议项处理。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs
  • codex-rs/core/src/tools/spec_plan.rs :: build_model_visible_specs
  • codex-rs/core/src/tools/router.rs :: ToolRouter::build_tool_call
rust
let hosted_specs = hosted_model_tool_specs(
    turn_context,
    standalone_web_search_tool.as_slice(),
);

// build_model_visible_specs 收集 direct runtime specs 后追加 hosted specs。
specs.extend(hosted_specs);
merge_into_namespaces(specs)

ToolRouter::build_tool_call 只归一化 FunctionCall、client ToolSearchCall 和 CustomToolCall;WebSearchCall 不会 被转成普通 ToolCall。这不是遗漏,而是 hosted tool 没有本地 handler 的协议边界。

2. 模式映射 ​

2.1 WebSearchMode ​

create_web_search_tool 把配置层的 WebSearchMode 映射到 Responses wire 字段:Cached 关闭 external access, Indexed 同时开启 external access 和 indexed access,Live 只开启 external access;Disabled 或 None 直接返回 None。

源码位置:codex-rs/core/src/tools/hosted_spec.rs :: create_web_search_tool

rust
let (external_web_access, indexed_web_access) = match options.web_search_mode {
    Some(WebSearchMode::Cached) => (false, None),
    Some(WebSearchMode::Indexed) => (true, Some(true)),
    Some(WebSearchMode::Live) => (true, None),
    Some(WebSearchMode::Disabled) | None => return None,
};

Some(ToolSpec::WebSearch {
    external_web_access: Some(external_web_access),
    indexed_web_access,
    filters: options
        .web_search_config
        .and_then(|config| config.filters.clone().map(Into::into)),
    user_location: options
        .web_search_config
        .and_then(|config| config.user_location.clone().map(Into::into)),
    search_context_size: options
        .web_search_config
        .and_then(|config| config.search_context_size),
    search_content_types,
})

WebSearchMode 决定是否生成 spec;WebSearchConfig 只补 filters、location 和 context size。两者不能混为一个 开关:配置存在不代表 mode 已启用。

2.2 内容模态 ​

WebSearchToolType::TextAndImage 会把 search_content_types 设为 text、image;Text 模式省略该字段。 这个判断读取模型能力对应的 model_info.web_search_tool_type,不是读取当前输入里是否已经出现图片。

源码位置:codex-rs/core/src/tools/hosted_spec.rs :: create_web_search_tool

rust
let search_content_types = match options.web_search_tool_type {
    WebSearchToolType::Text => None,
    WebSearchToolType::TextAndImage => Some(
        ["text", "image"]
            .into_iter()
            .map(str::to_string)
            .collect(),
    ),
};

3. 生成门槛 ​

3.1 Responses Lite ​

hosted_model_tool_specs 首先检查 model_info.use_responses_lite。Lite 请求接受 client-executed tool schemas, 但当前代码不把 hosted Responses tools 放进 Lite 的 tools 数组,因此直接返回空列表。普通 Function、Custom 和 namespace 仍可由 create_tools_json_for_responses_lite 合并后发送,它们与 hosted WebSearch 是不同的 wire surface。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs
  • codex-rs/tools/src/tool_spec.rs :: create_tools_json_for_responses_lite
rust
if turn_context.model_info.use_responses_lite
    || crate::guardian::is_guardian_reviewer_source(&turn_context.session_source)
{
    return Vec::new();
}

3.2 Guardian ​

Guardian reviewer 在 build_tool_router 的外部来源阶段已经获得空 hosted specs;hosted_model_tool_specs 也有 独立的 Guardian guard。这是双层保护:即使测试或其他调用方直接调用 hosted spec helper,review session 仍不会得到 hosted web search。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: build_tool_router
  • codex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs
rust
let hosted_specs = if crate::guardian::is_guardian_reviewer_source(
    &turn_context.session_source,
) {
    Vec::new()
} else {
    // 普通 session 才继续 append MCP/extension/dynamic 并生成 hosted specs。
    hosted_model_tool_specs(turn_context, standalone_web_search_tool.as_slice())
};

3.3 Capability ​

普通 session 还必须满足 provider capability web_search。如果 provider 不支持 hosted search,web_search_mode 会被构造成 None,最终不会生成 ToolSpec::WebSearch。provider 支持能力和用户 mode 是两个独立条件。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs

rust
let web_search_mode = (!standalone_web_search_available
    && turn_context.provider.capabilities().web_search)
    .then_some(turn_context.config.web_search_mode.value());

4. Runtime竞争 ​

4.1 Extension ​

standalone web search 的开关要求 namespace tools、provider web_search capability 和 Feature::StandaloneWebSearch;extension executor 还必须真实提供 ToolName::namespaced("web", "run"),并且 当前 web_search_mode 不能是 Disabled。满足这些条件时,extension runtime 成为本地工具,hosted spec 被抑制。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: standalone_web_search_enabled
  • codex-rs/core/src/tools/spec_plan.rs :: append_extension_tool_executors
rust
let is_standalone_web_search = tool_name == ToolName::namespaced("web", "run");
if is_standalone_web_search && (!standalone_web_search_enabled || !web_search_mode_on) {
    continue;
}

if registry.register_external(runtime) && is_standalone_web_search {
    standalone_web_search_tool = Some(tool_name);
}

4.2 MCP与dynamic同名 ​

当前实现只把 extension executor 返回的 web/run 名称传给 hosted_model_tool_specs。因此 dynamic 或 MCP 即使 注册了同名 namespace/tool,也不会设置 registered_extension_tool_names 中的 standalone winner;hosted spec 仍可能 生成。这个行为由当前 spec_plan_tests 明确覆盖,不能根据“名称相同就应该互斥”的直觉改写。

相关源码:

  • codex-rs/core/src/tools/spec_plan.rs :: append_extension_tool_executors
  • codex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs

5. Wire与响应 ​

5.1 请求规格 ​

ToolSpec::WebSearch 通过 serde 直接成为 Responses tools 数组元素;它没有 parameters JSON Schema,也没有 本地 ToolExecutor metadata。filters、location、context size 和 content types 是 provider-facing fields。

源码位置:codex-rs/tools/src/tool_spec.rs :: ToolSpec::WebSearch

rust
#[serde(rename = "web_search")]
WebSearch {
    #[serde(skip_serializing_if = "Option::is_none")]
    external_web_access: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    indexed_web_access: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    filters: Option<ResponsesApiWebSearchFilters>,
    #[serde(skip_serializing_if = "Option::is_none")]
    user_location: Option<ResponsesApiWebSearchUserLocation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    search_context_size: Option<WebSearchContextSize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    search_content_types: Option<Vec<String>>,
}

5.2 响应消费 ​

handle_output_item_done 调用 ToolRouter::build_tool_call;对于 hosted WebSearchCall,该函数返回 Ok(None), 随后 handle_non_tool_response_item 将它作为普通 stream turn item 处理,而不是排队本地 tool future。Hosted search 的开始/结束事件由 Responses 流解析和 session event 路径消费。

相关源码:

  • codex-rs/core/src/stream_events_utils.rs :: handle_output_item_done
  • codex-rs/core/src/stream_events_utils.rs :: handle_non_tool_response_item
rust
match ToolRouter::build_tool_call(item.clone()) {
    Ok(Some(call)) => {
        // 只有本地可分派的 ToolCall 才进入 tool future。
        output.tool_future = Some(Box::pin(
            ctx.tool_runtime
                .clone()
                .handle_tool_call(call, ctx.cancellation_token.child_token()),
        ));
        output.needs_follow_up = true;
    }
    Ok(None) => {
        let finalized_turn_item = finalize_non_tool_response_item(
            ctx.sess.as_ref(),
            TurnItemContributorPolicy::Run(ctx.turn_store.as_ref()),
            &item,
            plan_mode,
        )
        .await;
        // WebSearchCall 在这里作为流事件/turn item,而不是本地 handler。
    }
    Err(error) => return Err(error.into()),
}

“模型执行了 web search”与“Core 调用了一个本地工具”是两种不同事实:前者由 provider/Responses stream 产生 WebSearchCall,后者才会进入 ToolCallRuntime。

6. 测试路径 ​

6.1 Spec字段 ​

hosted_spec_tests::web_search_tool_preserves_configured_options 输入 Live、域名过滤、近似位置、低 context size 和 TextAndImage,断言所有字段都投影到 ToolSpec::WebSearch。web_search_tool_is_absent_when_disabled 则断言 Disabled 返回 None,不会生成一个“disabled hosted tool”。

源码位置:

  • codex-rs/core/src/tools/hosted_spec_tests.rs :: web_search_tool_preserves_configured_options
  • codex-rs/core/src/tools/hosted_spec_tests.rs :: web_search_tool_is_absent_when_disabled

6.2 Fallback门槛 ​

spec_plan_tests::hosted_web_search_fallback_follows_winning_browser_runtime 构造 extension 与 MCP 同名 browser runtime,断言 namespace winner 与 hosted spec 同时存在;这证明当前互斥条件只看 extension winner。

hosted_web_search_and_standalone_image_generation_follow_runtime_gates 覆盖 TextAndImage、Responses Lite、dynamic/MCP 同名 web/run、standalone extension web/run 和 Bedrock cached search 的组合,断言 hosted 与 runtime surface 的差异。

源码位置:

  • codex-rs/core/src/tools/spec_plan_tests.rs :: hosted_web_search_fallback_follows_winning_browser_runtime
  • codex-rs/core/src/tools/spec_plan_tests.rs :: hosted_web_search_and_standalone_image_generation_follow_runtime_gates

这些测试证明规格生成和门槛组合,不证明 provider 真实搜索质量、服务端查询结果或网络可用性。

7. 阅读练习 ​

在 Codex 源码 workspace 中运行:

bash
cargo test -p codex-core web_search_tool_preserves_configured_options
cargo test -p codex-core web_search_tool_is_absent_when_disabled
cargo test -p codex-core hosted_web_search_fallback_follows_winning_browser_runtime
cargo test -p codex-core hosted_web_search_and_standalone_image_generation_follow_runtime_gates

然后尝试回答:

  1. Live、Indexed、Cached 三种 mode 的 external_web_access 和 indexed_web_access 分别是什么?
  2. 为什么 Responses Lite 直接没有 hosted spec,但仍然可以有 client-executed function tools?
  3. extension、dynamic、MCP 都提供 web/run 时,哪一种会抑制 hosted web search?依据哪个数组判断?
  4. WebSearchCall 为什么不会进入 ToolRouter::build_tool_call 的本地 future?它由哪个 stream 分支消费?
  5. TextAndImage 改变的是哪个模型能力字段?它是否由当前输入内容动态推断?

8. 边界 ​

Hosted spec 只描述 provider 原生工具的请求契约,不负责:

  • provider 服务端是否真的执行搜索或返回高质量结果;
  • 本地 Registry、approval、sandbox、hook 和 ToolOutput 生命周期;
  • MCP/dynamic/extension 搜索工具的协议和业务实现;
  • WebSearchCall 的所有事件细节和持久化策略。

排查 hosted web search 消失时,应按“Responses Lite/Guardian → provider capability → standalone extension winner → mode → WebSearchToolType → 最终 Prompt.tools”顺序检查。尤其不要把 dynamic/MCP 的同名 runtime 误认为会自动取代 hosted spec;当前源码和测试明确表明,两者可以同时出现在可见工具面。