fix: preserve responses reasoning history

This commit is contained in:
Keisuke Hirata 2026-05-29 16:54:11 +09:00
parent b870a77a55
commit 8ed5939ebb
No known key found for this signature in database
2 changed files with 8 additions and 32 deletions

View File

@ -62,8 +62,8 @@ pub(crate) struct ResponsesRequest {
pub(crate) struct ReasoningConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
/// Reasoning encrypted_content は同一 user turn 内だけ再利用する。
/// 古い turn の reasoning item は request input から除外する
/// API 側の reasoning retention policy。Insomnia はこの値を送るが、
/// persisted reasoning item の client-side filtering はしない
pub context: &'static str,
/// summary の出力制御。`"auto"` 固定で summary_text を受け取る。
pub summary: &'static str,
@ -240,9 +240,8 @@ impl OpenAIResponsesScheme {
/// `Item` 列を `input[]` に変換する。
fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
let current_turn_start = current_turn_start_index(items);
let mut out = Vec::with_capacity(items.len());
for (idx, item) in items.iter().enumerate() {
for item in items {
match item {
Item::Message { role, content, .. } => {
let (role_str, text_variant): (&'static str, fn(String) -> InputContent) =
@ -299,9 +298,6 @@ fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
encrypted_content,
..
} => {
if idx < current_turn_start {
continue;
}
let summary_parts = summary
.iter()
.filter(|s| !s.is_empty())
@ -324,26 +320,6 @@ fn convert_items_to_input(items: &[Item]) -> Vec<InputItem> {
out
}
/// Responses の `reasoning.context = "current_turn"` に合わせ、直近の
/// user message 以降だけを current turn とみなす。ToolResult は Responses
/// wire 上では user 側 item だが、新しい人間/外部入力ではなく function-call
/// chain の継続なので turn reset には使わない。System/developer notes も
/// 同一 turn 内の補助入力になり得るため reset しない。
fn current_turn_start_index(items: &[Item]) -> usize {
items
.iter()
.rposition(|item| {
matches!(
item,
Item::Message {
role: Role::User,
..
}
)
})
.unwrap_or(0)
}
fn convert_tool(tool: &ToolDefinition) -> ResponseTool {
ResponseTool {
r#type: "function",
@ -506,7 +482,7 @@ mod tests {
}
#[test]
fn old_turn_reasoning_items_are_omitted_for_current_turn_context() {
fn persisted_reasoning_items_are_preserved_across_user_turns() {
let scheme = OpenAIResponsesScheme::new();
let old_reasoning = Item::reasoning("old").with_encrypted_content("OLD_ENC");
let current_reasoning = Item::reasoning("current").with_encrypted_content("CURRENT_ENC");
@ -527,7 +503,7 @@ mod tests {
_ => None,
})
.collect();
assert_eq!(encrypted, vec!["CURRENT_ENC"]);
assert_eq!(encrypted, vec!["OLD_ENC", "CURRENT_ENC"]);
}
#[test]

View File

@ -85,9 +85,9 @@ reasoning トークンは各ターンの後に破棄される。次ターンに
1. `previous_response_id` パラメータで過去のレスポンスを参照
2. `response.output` の全アイテムを次の `input` に手動で渡す
ステートレス利用(`store=false`、ZDR組織の場合は `include=["reasoning.encrypted_content"]` を指定すれば暗号化された推論コンテンツを受け取り、次リクエストに渡すことで推論を引き継げる。ただし Insomnia は Responses リクエストに `reasoning.context="current_turn"` を明示し、直近の user message 以降の同一ターン内 reasoning item だけを `input` に残す。過去ターンの persisted `encrypted_content` は、履歴に残っていても次ターンへ盲目的には再送しない。
ステートレス利用(`store=false`、ZDR組織の場合は `include=["reasoning.encrypted_content"]` を指定すれば暗号化された推論コンテンツを受け取り、次リクエストに渡すことで推論を引き継げる。Insomnia は Responses リクエストに `reasoning.context="current_turn"` を明示するが、このパラメータの正確な履歴境界 semantics は provider 側の責務として扱い、履歴から復元した reasoning item を client 側でターン境界に基づいて削除しない。
同一ターン内の function-call loop で、`reasoning item → function_call → function_call_output → 次の Responses request` の連続性を保つため、直近 user message 以降の reasoning item は保持する。ToolResult は wire 上で user 側 item に見えるが、新しい user turn ではなく function-call chain の継続なので reasoning reset の境界にはしない。
同一ターン内の function-call loop で、`reasoning item → function_call → function_call_output → 次の Responses request` の連続性を保つため、履歴上の reasoning item は通常の API message として保持する。ToolResult は wire 上で user 側 item に見えるが、reasoning item の削除境界としては扱わない。
#### モデル世代差
@ -187,7 +187,7 @@ Ollamaはローカル実行プラットフォームで、モデルごとに思
**ChatGPT を使うとき**
- 新規実装は **Responses API** を選ぶChat Completions は推論引き継ぎが弱い)
- ZDR組織でも `reasoning.encrypted_content` で推論を引き継げるが、Insomnia では `reasoning.context="current_turn"` に合わせて同一 user turn / function-call loop 内だけ再送する
- ZDR組織でも `reasoning.encrypted_content` で推論を引き継げる。Insomnia は `reasoning.context="current_turn"` を送るが、履歴上の reasoning item は通常の API message として扱い、独自の turn-boundary filtering はしない
- raw reasoning の抽出を試みない(規約違反の可能性)
**Ollama を使うとき**