Skip to content

远程Compact请求协议

从远程 compact 的调用入口、请求载荷和认证头,到响应历史的筛选与安装,解释协议字段如何连接 Codex 的上下文状态。

基于rust-v0.150.0
CodexRustContextCompact

远程Compact请求协议 ​

本文承接 本地Compact执行与写回,研究当前实现中远程 compact 的公共协议层。这里的“协议”不只是 URL:它包括调用方选择的模型与能力、由普通 Responses 请求复用的 body 字段、认证和 session headers、历史 item 的编码,以及服务端返回后哪些 item 能进入下一次模型请求。

本文不展开 V2 attempt 的重试分类和模型 fallback;那些行为属于后续文章。读完后,读者应该可以沿着代码回答:

  • 为什么 compact 请求使用 /v1/responses/compact,却仍然携带普通 Responses 的 tools、reasoning 和缓存键;
  • API key、ChatGPT 和 Agent Identity 三种认证为什么得到不同的 header;
  • 服务端返回的 developer、tool call 和普通 user message 为什么不会全部原样写回;
  • 请求失败、空输入和取消分别在哪一层结束,哪些状态尚未被替换。

1. 能力边界 ​

远程 compact 的入口已经由上层选择为 CompactionImplementation::ResponsesCompact。自动压缩和手动压缩共享 请求构造器,但触发元数据不同:自动路径带有窗口压力原因,手动路径使用用户请求原因。协议层只消费这些元数据, 不会自行决定是否应该压缩。

图中最重要的边界是 D 与 F:HTTP 成功只代表服务端返回了候选 transcript,不代表 live history 已经改变;只有 后续筛选、上下文注入和 replace_compacted_history 完成,结果才会被下一次 turn 消费。

2. 请求入口 ​

远程 attempt 先克隆会话历史,再为上下文窗口重写尾部 function-call output。这个步骤发生在网络请求之前,目的是让 Compact 服务收到一个仍然可解释、但不会因为最后几个巨大 tool output 而超过窗口的输入。

源码位置:codex-rs/core/src/compact_remote_request.rs :: run_remote_compact_attempt

rust
let turn_context = &step_context.turn;
let mut history = sess.clone_history().await;
let base_instructions = sess.get_base_instructions().await;
let (rewritten_outputs, estimated_deleted_tokens) =
    trim_function_call_history_to_fit_context_window(
        &mut history,
        turn_context.as_ref(),
        &base_instructions,
    );
if rewritten_outputs > 0 {
    info!(
        turn_id = %turn_context.sub_id,
        rewritten_outputs,
        "rewrote history outputs before remote compaction"
    );
}

注意这里修改的是克隆出来的 history,不是 session 的 live history。即使网络调用随后失败,当前会话仍保留原始历史; estimated_deleted_tokens 只用于分析数据修正,不会提前提交新的 token 状态。

请求输入随后转换为 Prompt。这里的 input_modalities 决定历史如何编码,model_visible_specs() 决定服务端看到的 工具集合;因此 compact 并不是只发送一段摘要提示词。

源码位置:codex-rs/core/src/compact_remote_request.rs :: run_remote_compact_attempt

rust
let trace_input_history = compaction_trace
    .is_enabled()
    .then(|| history.raw_items().to_vec());
let prompt_input = history.for_prompt(&turn_context.model_info.input_modalities);
let tool_router = &step_context.tool_router;
let prompt = Prompt {
    input: prompt_input,
    tools: tool_router.model_visible_specs(),
    parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
    base_instructions,
    output_schema: None,
    output_schema_strict: true,
};

output_schema 明确为空,因为 compact 的结果是新的 ResponseItem 列表,而不是普通 turn 的结构化最终答案。 但 tools 和 parallel_tool_calls 仍然保留,是为了让 provider 使用与当前模型请求一致的上下文能力。

3. 请求载荷 ​

ModelClient::compact_conversation_history 先调用已有的 build_responses_request,再从普通请求中解构出 compact endpoint 支持的字段。这种实现把模型选择、指令渲染、推理设置和缓存键的来源集中到一条路径,降低了普通 Responses 与 compact 请求发生字段漂移的风险。

源码位置:codex-rs/core/src/client.rs :: ModelClient::compact_conversation_history

rust
let request = self.build_responses_request(
    &client_setup.api_provider,
    prompt,
    model_info,
    settings.effort,
    settings.summary,
    settings.service_tier,
    responses_metadata,
)?;
let ResponsesApiRequest {
    model,
    instructions,
    mut input,
    tools,
    parallel_tool_calls,
    reasoning,
    service_tier,
    prompt_cache_key,
    text,
    ..
} = request;
self.prepare_response_items_for_request(&mut input);
let payload = ApiCompactionInput {
    model: &model,
    input: &input,
    instructions: &instructions,
    tools,
    parallel_tool_calls,
    reasoning,
    service_tier: service_tier.as_deref(),
    prompt_cache_key: prompt_cache_key.as_deref(),
    text,
};

可以把 body 分成三组来阅读:

组字段消费者
历史input、instructionsCompact 服务的 transcript 归纳器
能力tools、parallel_tool_calls、textprovider 的请求解释器
控制model、reasoning、service_tier、prompt_cache_key路由、推理和缓存层

prepare_response_items_for_request 在包装 payload 前运行,说明历史的 wire 编码是请求协议的一部分,而不是服务端 收到后才补救的内部细节。

空输入是一个明确的短路边界:

源码位置:codex-rs/core/src/client.rs :: ModelClient::compact_conversation_history

rust
if prompt.input.is_empty() {
    return Ok(Vec::new());
}

此时不会创建 transport、不会发 HTTP 请求,也不会得到 replacement history。上层若继续安装结果,安装的只能是空列表; 因此调用方必须保证空 compact 不会被误判为一次成功摘要。

4. 认证边界 ​

compact 请求的 endpoint 由 provider transport 拼接为 /v1/responses/compact。认证本身由 ApiCompactClient 使用 当前 api_auth 完成,Codex 额外添加安装、session、thread 和兼容性 headers。

下面的 headers 由同一个方法加入,因此调试抓包时应把它们视作一个协议集合,而不是普通 Responses 请求的可选装饰。

源码位置:codex-rs/core/src/client.rs :: ModelClient::compact_conversation_history

rust
let mut extra_headers = ApiHeaderMap::new();
if let Ok(header_value) = HeaderValue::from_str(&responses_metadata.installation_id) {
    extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
}
extra_headers.extend(build_responses_headers(
    self.state.beta_features_header.as_deref(),
    turn_state.as_ref(),
));
add_originator_header(&mut extra_headers, self.state.originator.as_str());
extra_headers.extend(self.build_responses_compatibility_headers(responses_metadata));
extra_headers.extend(build_session_headers(
    Some(responses_metadata.session_id.to_string()),
    Some(responses_metadata.thread_id.to_string()),
));

Agent Identity 是认证差异中最容易漏读的一项。测试没有断言“所有请求都使用 Bearer”,而是专门断言任务范围的 AgentAssertion,并同时校验 account id;这说明认证 scheme 与 compact endpoint 无关,取决于当前 session 的 CodexAuth。

源码位置:codex-rs/core/tests/suite/compact_remote.rs :: remote_compact_uses_agent_identity_assertion

rust
assert_eq!(compact_request.path(), "/v1/responses/compact");
assert!(
    compact_request
        .header("authorization")
        .is_some_and(|value| value.starts_with("AgentAssertion ")),
    "compact request should use task-scoped AgentAssertion auth"
);
assert_eq!(
    compact_request.header("chatgpt-account-id").as_deref(),
    Some("account-compact")
);

5. 服务差异 ​

CompactConversationRequestSettings 只把 service_tier 在非 API key 认证下传入。这个判断发生在远程 attempt,而 不是 provider 内部,因此请求构造阶段已经知道认证模式。

源码位置:codex-rs/core/src/compact_remote_request.rs :: run_remote_compact_attempt

rust
CompactConversationRequestSettings {
    effort: turn_context.reasoning_effort.clone(),
    summary: turn_context.reasoning_summary,
    service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
        None
    } else {
        turn_context.config.service_tier.clone()
    },
}

测试用同一个 parity helper 构造五轮不同输入,再比较普通 /responses 与 compact request。API key 的断言是 service_tier == None,ChatGPT auth 的断言则是 Some("priority");两者都要求复用 prompt_cache_key,并且 都不把 responses-only 字段带进 compact body。

源码位置:codex-rs/core/tests/suite/compact_remote.rs :: assert_remote_manual_compact_request_parity

rust
assert_remote_manual_compact_request_parity(
    CodexAuth::from_api_key("dummy"),
    Some(ServiceTier::Fast),
    None,
    "remote_manual_compact_api_auth_prompt_cache_key_request_diff",
    "After five varied API-key-auth turns, remote manual compaction omits service_tier, reuses prompt_cache_key, and still omits responses-only fields.",
)
.await?;

Bedrock 测试进一步说明“provider 能发 compact 请求”与“启用 V2”是两个维度:Amazon Bedrock 仍使用 v1 endpoint, 请求 model 使用 Bedrock provider model,响应中的 type: compaction item 会进入后续 Responses 请求。

6. 历史协议 ​

服务端返回的响应项在进入 Session 前会被包装为 ResponseItemEnvelope。Codex 不会把列表原样交给 Session。process_compacted_history 先按 history_item_groups 还原关联项,再调用 should_keep_compacted_history_item 做协议到内部语义的筛选。

源码位置:codex-rs/core/src/compact_remote.rs :: process_compacted_history

rust
// Remote output is filtered before replacement-history installation.
let (initial_context, world_state_baseline) =
    build_compaction_initial_context(sess, initial_context_injection).await;
let compacted_history = history_item_groups(compacted_history)
    .filter(|group| should_keep_compacted_history_item(&group.source))
    .flat_map(HistoryItemGroup::into_items)
    .collect();
(
    insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context),
    world_state_baseline,
)

筛选规则不是按 role 做简单白名单。真实 user message 需要经过 parse_turn_item,hook prompt 也会保留; developer 消息和普通工具调用则丢弃。这样做的原因是远程输出可能包含过时的指令包装,不能让它覆盖当前 session 重新构造的初始上下文。

源码位置:codex-rs/core/src/compact_remote.rs :: should_keep_compacted_history_item

rust
pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
    match item {
        ResponseItem::Message { role, .. } if role == "developer" => false,
        ResponseItem::Message { role, .. } if role == "user" => {
            matches!(
                crate::event_mapping::parse_turn_item(item),
                Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
            )
        }
        ResponseItem::Message { role, .. } if role == "assistant" => true,
        ResponseItem::AgentMessage { .. } => true,
        ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
        ResponseItem::CompactionTrigger { .. } => false,
        _ => false,
    }
}

图中“保留”仍然不是 live history:注入策略决定初始 context 放在最后一个真实 user 或 summary 之前还是留给下一次 普通 turn。DoNotInject 用于手动和 pre-turn;mid-turn 才需要立即注入,以便 continuation 仍能看到当前 turn 的 系统上下文。

远程任务还会为 ContextCompactionItem 建立一个 trace compaction ID,把 lifecycle event、endpoint attempt 和安装 checkpoint 关联起来。fallback attempt 复用该 compaction ID,但使用 fallback TurnContext 的模型与 provider;只有成功的 attempt 才进入窗口推进和 history 安装。

源码位置:codex-rs/core/src/compact_remote.rs :: run_remote_compact_task_inner_impl

rust
let compaction_id = context_compaction_item.id.clone();
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
    turn_context.sub_id.as_str(),
    compaction_id.as_str(),
    turn_context.model_info.slug.as_str(),
    turn_context.provider.info().name.as_str(),
);

7. 超时取消 ​

Compact endpoint 是 unary 调用,没有 SSE 增量事件,所以 timeout 不能沿用一次流式响应的等待语义。客户端把 provider 的 stream_idle_timeout 乘以 4,作为一次 compact 请求的 idle timeout。

源码位置:codex-rs/core/src/client.rs :: ModelClient::compact_conversation_history

rust
let compact_request_timeout = client_setup
    .api_provider
    .stream_idle_timeout
    .saturating_mul(COMPACT_REQUEST_TIMEOUT_IDLE_MULTIPLIER);
let client = ApiCompactClient::new(
    transport,
    client_setup.api_provider,
    client_setup.api_auth,
)
.with_telemetry(Some(request_telemetry));
let result = client
    .compact_input(
        &payload,
        extra_headers,
        compact_request_timeout,
        turn_state.as_deref(),
    )
    .await
    .map_err(|error| self.state.provider.map_api_error(error));

失败时 result 只返回错误,run_remote_compact_attempt 也只返回 Err;窗口推进和 history 替换都在上层成功分支 之后。因此 HTTP 错误、认证错误、超时或取消不会把半成品写进 live history。V2 的 fallback 和失败分类不在本文展开, 但这个返回边界是后续降级逻辑能够安全重试的前提。

这张图只说明客户端的写回边界,不表示 provider 一定会在取消后立即停止服务器端计算。

8. 写回边界 ​

远程响应变成可见历史的最后一段代码如下。先推进窗口,再过滤和注入,最后安装、重算 token、发送完成事件。

源码位置:codex-rs/core/src/compact_remote.rs :: run_remote_compact_task_inner_impl

rust
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let (new_history, world_state_baseline) =
    process_compacted_history(sess.as_ref(), new_history, &initial_context_injection).await;
sess.replace_compacted_history(
    new_history,
    reference_context_item,
    world_state_baseline,
    CompactedHistoryMetadata {
        message: String::new(),
        window_number: new_window_number,
        window_ids: new_window_ids,
    },
)
.await;
sess.recompute_token_usage(compaction_turn_context).await;

这段顺序给出了一个可执行的调试断点表:网络成功但没有 replace_compacted_history,问题在响应处理;history 已替换 但 token 未更新,问题在提交后的重算;下一次请求仍带旧 item,则应检查恢复或 request history 视图,而不是重新猜测 Compact API 的摘要内容。

这张图把三个层次分开:Prompt 是 Codex 内部输入,ResponsesApiRequest 是公共请求构造结果, ApiCompactionInput 才是 compact endpoint 的 body;返回值重新回到 ResponseItem,之后还要经过历史筛选。

9. 协议测试 ​

测试的输入、断言和覆盖边界必须分开阅读:

测试输入断言未覆盖范围
remote_compact_replaces_history_for_followups一次正常 turn 后手动 compactendpoint、headers、request kind、字段复用、follow-up 只带 replacement itemprovider 摘要质量
remote_manual_compact_api_auth_omits_service_tier_and_reuses_prompt_cache_keyAPI key、配置 fast tier、多轮混合输入不发送 tier,复用 cache keyChatGPT/Agent Identity 认证
remote_manual_compact_chatgpt_auth_reuses_service_tier_and_prompt_cache_keyChatGPT auth、fast tier发送 priority,复用 cache keyAPI key 的错误响应
remote_compact_uses_agent_identity_assertion任务范围 Agent IdentityAgentAssertion scheme 与 account idassertion 过期后的刷新
amazon_bedrock_uses_remote_compaction_endpointBedrock provider、V2 开启仍使用 v1 endpoint,compaction item 进入后续请求非 Bedrock provider 的路由
snapshot_request_shape_remote_mid_turn_continuation_compactionmid-turn continuation压缩后上下文注入与请求形状网络中断后的恢复

这些测试能证明协议边界和安装前后的请求形状,不能证明服务端如何生成摘要,也不能替代取消、网络断开和 provider 限流的端到端测试。

10. 源码练习 ​

  1. 在 compact_conversation_history 的 prepare_response_items_for_request 前后打印 input,比较内部 ResponseItem 与 wire item;解释哪些字段是编码归一化而不是历史筛选。
  2. 给 should_keep_compacted_history_item 增加一个 user 角色但无法解析为 TurnItem 的 fixture,验证它不会 进入 replacement history,并说明这如何防止旧 developer 包装泄漏。
  3. 将 API key parity 测试中的 expected_service_tier 临时改为 Some("priority"),观察断言失败位置;再反向修改 run_remote_compact_attempt,确认测试能捕获认证策略回归。
  4. 在 HTTP 返回错误时记录 Session::current_window_id 和 history_version,证明错误路径不会触发写回;不要只检查 UI 的 error event。

完成这些练习后,读者应能从一个 compact 抓包反查到 Prompt、ResponsesApiRequest、ApiCompactionInput 和 process_compacted_history,并判断问题发生在请求构造、provider 传输还是 replacement history 安装阶段。

11. 可执行验证 ​

在本版本源码对应的 codex-rs workspace 中运行协议测试。若测试因环境凭据或显式 skip 被跳过,应把它说明为 “未执行”,不能把 0 tests 当作通过。

bash
cd codex-rs
RUST_MIN_STACK=16777216 cargo test -p codex-core --test suite compact_remote_replaces_history_for_followups -- --exact --nocapture
RUST_MIN_STACK=16777216 cargo test -p codex-core --test suite remote_compact_uses_agent_identity_assertion -- --exact --nocapture

第一条命令验证 endpoint、公共字段复用、headers 和 follow-up replacement history;第二条验证任务范围认证方案。 它们不能证明摘要内容质量、provider 限流行为或取消后的服务器端资源回收。