模型请求构造
本文承接 ModelClient结构 和 推理强度与服务层,只研究一次 Responses 请求是如何从 Prompt 和 turn 参数构造出来的。读者应能在代码中找到这条主线:Prompt 输入 -> get_formatted_input_for_request -> build_responses_request -> ResponsesApiRequest -> HTTP/WebSocket 发送。
本文不解释 Responses 事件如何解析,也不把工具实现本身归入本篇。范围是请求构造时的输入、能力门控和 wire 字段映射。
1. 构造主线
build_responses_request 不是单纯的 struct literal:它先改写输入快照,再选择 instructions/tools 形式,然后汇总 reasoning、verbosity、schema、service tier 和 metadata。各步骤的 owner 不同:Prompt 负责输入语义,ModelInfo 负责能力,ModelClient 负责 provider 和 session 状态。
2. Prompt输入
Prompt 的 input 是请求的对话快照,tools 和 base_instructions 是请求构造时才映射的输入。get_formatted_input_for_request 始终先 clone;只在 use_responses_lite 为 true 时移除图像 detail,不改写 Prompt 原始值。非 OpenAI provider 还会在 builder 内清理 internal chat metadata 和 encrypted function args。
源码位置:codex-rs/core/src/client_common.rs :: Prompt::get_formatted_input_for_request
pub(crate) fn get_formatted_input_for_request(
&self,
use_responses_lite: bool,
) -> Vec<ResponseItem> {
let mut input = self.input.clone();
if use_responses_lite {
strip_image_details(&mut input);
}
input
}
fn strip_image_details(items: &mut [ResponseItem]) {
for item in items {
match item {
ResponseItem::Message { content, .. } => {
for content_item in content {
if let ContentItem::InputImage { detail, .. } = content_item {
*detail = None;
}
}
}
ResponseItem::FunctionCallOutput { output, .. }
| ResponseItem::CustomToolCallOutput { output, .. } => {
if let Some(content) = output.content_items_mut() {
for content_item in content {
if let FunctionCallOutputContentItem::InputImage { detail, .. } =
content_item
{
*detail = None;
}
}
}
}
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::AgentMessage { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::ToolSearchOutput { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::Compaction { .. }
| ResponseItem::CompactionTrigger { .. }
| ResponseItem::ContextCompaction { .. }
| ResponseItem::Other => {}
}
}
}这个分支的实际含义是“为特定 wire 方式准备输入快照”,不是通用的图像清理器。因此测试必须同时证明 lite 和非 lite 两条路径。
3. Lite分支
build_responses_request 根据 model_info.use_responses_lite 把工具和基础指令放到不同的位置。Lite 路径把工具包成 AdditionalTools 前缀,并把 base instructions 作为 developer message 插入 input;instructions 字段则是空字符串。非 Lite 路径使用顶层 instructions 和 tools 字段。
源码位置:codex-rs/core/src/client.rs :: ModelClient::build_responses_request
let (instructions, tools) = if model_info.use_responses_lite {
let tools = if self.state.provider.capabilities().namespace_tools {
create_tools_json_for_responses_lite(&prompt.tools)?
} else {
create_tools_json_for_responses_api(&prompt.tools)?
};
let mut prefix = vec![ResponseItem::AdditionalTools {
id: None,
role: "developer".to_string(),
tools,
}];
if !prompt.base_instructions.text.is_empty() {
prefix.push(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: prompt.base_instructions.text.clone(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
});
}
input.splice(0..0, prefix);
(String::new(), None)
} else {
(
prompt.base_instructions.text.clone(),
Some(create_tools_raw_json_for_responses_api(&prompt.tools)?.into()),
)
};namespace_tools 是 Lite 路径内部的又一个能力开关:它决定用 namespace 工具 JSON 还是通用 Responses API 工具 JSON。这表明“使用 Lite”不等于所有 Lite provider 都有相同的工具表示。
4. 外部清理
在工具和指令决定前,构造器会根据 provider 是否为 OpenAI 清理输入项的内部 chat metadata,并移除函数调用中的 encrypted arguments。这是 provider 边界,不是 Prompt 的通用 clone 行为。
源码位置:codex-rs/core/src/client.rs :: ModelClient::build_responses_request
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
let is_openai = self.state.provider.info().is_openai();
if !is_openai {
for item in &mut input {
item.clear_internal_chat_message_metadata_passthrough();
if let ResponseItem::FunctionCall {
encrypted_function_args,
..
} = item
{
*encrypted_function_args = None;
}
}
}因此同一个 Prompt 在 OpenAI 和非 OpenAI provider 上可能生成不同的 input 快照;不能只比较 Prompt 的 Rust 值来推断最终 wire body。
5. 能力门控
reasoning、summary、verbosity、parallel tool calls 和 service tier 都不是无条件复制。build_reasoning 使用显式 effort 或模型默认值,summary 要经过模型能力判断;parallel_tool_calls 在 Responses Lite 下强制关闭;verbosity 不支持时被省略,并记录 warning;service tier 交给模型的过滤函数。
源码位置:codex-rs/core/src/client.rs :: ModelClient::build_reasoning
fn build_reasoning(
model_info: &ModelInfo,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
) -> Reasoning {
Reasoning {
effort: effort
.or_else(|| model_info.default_reasoning_level.clone())
.map(reasoning_effort_for_request),
summary: (model_info.supports_reasoning_summary_parameter
&& summary != ReasoningSummaryConfig::None)
.then_some(summary),
// When Responses Lite is disabled, omit context so Responses uses the default,
// which is currently `current_turn`.
context: model_info
.use_responses_lite
.then_some(ReasoningContext::AllTurns),
}
}6. 请求字段
下面是构造器的完整结尾:text 来自 verbosity 与 output schema,prompt_cache_key 来自 metadata 或 override,client_metadata 则来自 CodexResponsesMetadata 的快照。store 只由 Azure Responses endpoint 决定,并行工具调用在 Lite 下被关闭。include 固定请求 reasoning encrypted content。
源码位置:codex-rs/core/src/client.rs :: ModelClient::build_responses_request
let reasoning = Self::build_reasoning(model_info, effort, summary);
let stream_options = (self.state.concurrent_reasoning_summaries_enabled
&& is_openai
&& reasoning.summary.is_some())
.then_some(StreamOptions {
reasoning_summary_delivery: codex_api::ReasoningSummaryDelivery::SequentialCutoff,
});
let include = vec!["reasoning.encrypted_content".to_string()];
let verbosity = if model_info.support_verbosity {
self.state.model_verbosity.or(model_info.default_verbosity)
} else {
if self.state.model_verbosity.is_some() {
warn!(
"model_verbosity is set but ignored as the model does not support verbosity: {}",
model_info.slug
);
}
None
};
let text = create_text_param_for_request(
verbosity,
&prompt.output_schema,
prompt.output_schema_strict,
);
let prompt_cache_key = Some(self.prompt_cache_key(responses_metadata));
let service_tier = model_info.service_tier_for_request(service_tier);
let request = ResponsesApiRequest {
model: model_info.slug.clone(),
instructions,
input,
tools,
tool_choice: "auto".to_string(),
parallel_tool_calls: prompt.parallel_tool_calls && !model_info.use_responses_lite,
reasoning: Some(reasoning),
store: provider.is_azure_responses_endpoint(),
stream: true,
stream_options,
include,
service_tier,
prompt_cache_key,
text,
client_metadata: Some(responses_metadata.client_metadata()),
};
Ok(request)7. metadata输出
CodexResponsesMetadata 不是 request builder 临时拼出的 map;它是 caller-owned snapshot,通过 client_metadata() 生成 wire map。源码特别说明,完整 turn metadata 以 x-codex-turn-metadata 为 canonical 数据,其他 flat keys 是兼容投影。
源码位置:codex-rs/core/src/responses_metadata.rs :: CodexResponsesMetadata
/// Caller-owned snapshot of Codex metadata sent to ResponsesAPI.
///
/// The full Codex turn metadata blob is transported canonically as
/// `client_metadata["x-codex-turn-metadata"]`. Flat `client_metadata` keys and direct HTTP/ws
/// headers are generated compatibility projections of this snapshot, not separate sources of
/// truth.
#[derive(Clone, Debug)]
pub struct CodexResponsesMetadata {
pub(crate) installation_id: String,
pub(crate) session_id: String,
pub(crate) thread_id: String,
pub(crate) turn_id: Option<String>,
pub(crate) window_id: String,
pub(crate) request_kind: Option<CodexResponsesRequestKind>,
pub(crate) forked_from_thread_id: Option<ThreadId>,
pub(crate) parent_thread_id: Option<ThreadId>,
pub(crate) parent_turn_id: Option<String>,
pub(crate) subagent_header: Option<String>,
pub(crate) subagent_kind: Option<String>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) sandbox: Option<String>,
pub(crate) workspaces: BTreeMap<String, TurnMetadataWorkspace>,
pub(crate) code_mode_tool_names: Option<BTreeMap<String, ToolName>>,
pub(crate) turn_started_at_unix_ms: Option<i64>,
pub(crate) extra: BTreeMap<String, String>,
}8. 失败路径
请求构造阶段的失败主要是工具 JSON 序列化失败;create_tools_json_for_responses_lite 、create_tools_json_for_responses_api 和 create_tools_raw_json_for_responses_api 都返回 Result ,因此错误会在构造 ResponsesApiRequest 前传出。能力不支持时则是另一类可恢复路径:字段被省略、关闭或过滤,不会让 builder 自动扩展成服务端能力。
9. 请求测试
| 测试 | 输入与动作 | 断言 | 未覆盖边界 |
|---|---|---|---|
responses_lite_request_copies_strip_image_details | 含图像 detail 的 Prompt,分别以 lite 与非 lite 格式化 | lite copy 清空 detail,非 lite 保留 detail,原 Prompt 不变 | 不证明 provider 接受所有图像格式 |
responses_lite_* | 构造 lite request,检查 input、metadata 和 parallel tools | Lite 字段布局符合断言 | 不证明服务端实际执行工具 |
这些测试覆盖图像 detail 的副本改写、Lite 输入布局和 compact transport contract。namespace 工具断言依赖测试 fixture 与 provider capability 的组合,不能从单个 fixture 外推所有 provider 的 namespace tool 支持情况。
10. 复现路径
RUST_MIN_STACK=16777216 cargo test -p codex-core responses_lite_request_copies_strip_image_details
RUST_MIN_STACK=16777216 cargo test -p codex-core responses_lite_prepares_images
RUST_MIN_STACK=16777216 cargo test -p codex-core responses_lite_compact_request_uses_lite_transport_contract源码练习:在 build_responses_request 中分别设置 use_responses_lite 和 namespace_tools,对比 instructions 、tools 、input 和 parallel_tool_calls 的输出;再传入不支持 verbosity 的 ModelInfo,观察它是如何被省略的。
11. 源码导航
先读 ModelInfo能力模型理解能力字段,再读 Prompt::get_formatted_input_for_request 和 ModelClient::build_responses_request。对照 ModelClient结构 可以找到请求何时被 HTTP/WebSocket 消费;后续阅读 Responses 事件系列时,应保持“构造与解析是两个 owner”这个边界。
