Skip to content

模型请求构造

从 Prompt 输入归一化到 ResponsesApiRequest,追踪指令、工具、reasoning、schema 和 metadata 的构造路径。

基于rust-v0.150.0
CodexRustModelResponses

模型请求构造 ​

本文承接 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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
/// 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 toolsLite 字段布局符合断言不证明服务端实际执行工具

这些测试覆盖图像 detail 的副本改写、Lite 输入布局和 compact transport contract。namespace 工具断言依赖测试 fixture 与 provider capability 的组合,不能从单个 fixture 外推所有 provider 的 namespace tool 支持情况。

10. 复现路径 ​

bash
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”这个边界。