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_specscodex-rs/core/src/tools/spec_plan.rs :: build_model_visible_specscodex-rs/core/src/tools/router.rs :: ToolRouter::build_tool_call
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
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
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_specscodex-rs/tools/src/tool_spec.rs :: create_tools_json_for_responses_lite
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_routercodex-rs/core/src/tools/spec_plan.rs :: hosted_model_tool_specs
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
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_enabledcodex-rs/core/src/tools/spec_plan.rs :: append_extension_tool_executors
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_executorscodex-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
#[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_donecodex-rs/core/src/stream_events_utils.rs :: handle_non_tool_response_item
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_optionscodex-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_runtimecodex-rs/core/src/tools/spec_plan_tests.rs :: hosted_web_search_and_standalone_image_generation_follow_runtime_gates
这些测试证明规格生成和门槛组合,不证明 provider 真实搜索质量、服务端查询结果或网络可用性。
7. 阅读练习
在 Codex 源码 workspace 中运行:
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然后尝试回答:
- Live、Indexed、Cached 三种 mode 的
external_web_access和indexed_web_access分别是什么? - 为什么 Responses Lite 直接没有 hosted spec,但仍然可以有 client-executed function tools?
- extension、dynamic、MCP 都提供
web/run时,哪一种会抑制 hosted web search?依据哪个数组判断? WebSearchCall为什么不会进入ToolRouter::build_tool_call的本地 future?它由哪个 stream 分支消费?- 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;当前源码和测试明确表明,两者可以同时出现在可见工具面。
