From 17898e7d9b302ab62be1111dc285b008f4b23241 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Thu, 6 Aug 2026 23:26:25 -0500 Subject: [PATCH] Cover ChatGPT Responses tool call IDs --- crates/galaxy_agent_rig/src/chatgpt.rs | 70 ++++++++++++++++++++++++++ crates/galaxy_agent_rig/src/stream.rs | 16 ++++++ plans/galaxy-local-first-rig.md | 4 ++ 3 files changed, 90 insertions(+) diff --git a/crates/galaxy_agent_rig/src/chatgpt.rs b/crates/galaxy_agent_rig/src/chatgpt.rs index 50cae334..76c2062d 100644 --- a/crates/galaxy_agent_rig/src/chatgpt.rs +++ b/crates/galaxy_agent_rig/src/chatgpt.rs @@ -169,8 +169,18 @@ impl AgentRuntime for ChatGPTSubscriptionRuntime { #[cfg(test)] mod tests { + use rig_core::client::CompletionClient; + use rig_core::completion::{AssistantContent, CompletionModel, Message}; + use rig_core::message::{ToolResultContent, UserContent}; + use rig_core::providers::chatgpt::ChatGPTAuth; + use rig_core::test_utils::RecordingHttpClient; + use super::*; + const COMPLETED_RESPONSE: &str = r#"data: {"type":"response.output_text.delta","delta":"ok"} +data: {"type":"response.completed","response":{"id":"resp_test","object":"response","created_at":1,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-5.3-codex","usage":{"input_tokens":1,"input_tokens_details":{"cached_tokens":0},"output_tokens":1,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":2},"output":[{"type":"message","id":"msg_test","status":"completed","role":"assistant","content":[{"type":"output_text","annotations":[],"text":"ok"}]}],"tools":[]}} +data: [DONE]"#; + #[test] fn runtime_descriptor_identifies_chatgpt_subscription() { let runtime = ChatGPTSubscriptionRuntime::new(ChatGPTSubscriptionRuntimeConfig { @@ -216,4 +226,64 @@ mod tests { })) ); } + + #[tokio::test] + async fn chatgpt_follow_up_request_preserves_responses_call_ids() { + let http_client = RecordingHttpClient::new(COMPLETED_RESPONSE); + let client = chatgpt::Client::builder() + .api_key(ChatGPTAuth::AccessToken { + access_token: "test-token".to_string(), + account_id: None, + }) + .http_client(http_client.clone()) + .build() + .expect("client should build"); + let model = client.completion_model("gpt-5.3-codex"); + let assistant_tool_call = AssistantContent::tool_call_with_call_id( + "fc_native_1", + "call_native_1".to_string(), + "read_files", + serde_json::json!({"files": ["Cargo.toml"]}), + ); + let tool_result = UserContent::tool_result_with_call_id( + "fc_native_1", + "call_native_1".to_string(), + rig_core::OneOrMany::one(ToolResultContent::text("contents")), + ); + let chat_history = rig_core::OneOrMany::many(vec![ + Message::Assistant { + id: None, + content: rig_core::OneOrMany::one(assistant_tool_call), + }, + Message::User { + content: rig_core::OneOrMany::one(tool_result), + }, + Message::user("Continue."), + ]) + .expect("history should contain messages"); + + model + .completion(rig_core::completion::CompletionRequest { + model: Some("gpt-5.3-codex".to_string()), + preamble: None, + chat_history, + documents: Vec::new(), + tools: Vec::new(), + temperature: None, + max_tokens: None, + tool_choice: None, + additional_params: None, + output_schema: None, + record_telemetry_content: false, + }) + .await + .expect("request should reach the mocked provider"); + + let requests = http_client.requests(); + assert_eq!(requests.len(), 1); + let body: serde_json::Value = serde_json::from_slice(&requests[0].body).unwrap(); + let input = body["input"].as_array().expect("input should be an array"); + assert_eq!(input[0]["call_id"], "call_native_1"); + assert_eq!(input[1]["call_id"], "call_native_1"); + } } diff --git a/crates/galaxy_agent_rig/src/stream.rs b/crates/galaxy_agent_rig/src/stream.rs index 53ccacc8..8b599fb8 100644 --- a/crates/galaxy_agent_rig/src/stream.rs +++ b/crates/galaxy_agent_rig/src/stream.rs @@ -310,4 +310,20 @@ mod tests { assert_eq!(call.id, "call_123"); assert_eq!(call.name, "read_files"); } + + #[test] + fn domain_tool_call_falls_back_to_wire_id_without_responses_call_id() { + let tool_call = rig_core::message::ToolCall::new( + "fc_item_123".to_string(), + rig_core::message::ToolFunction { + name: "read_files".to_string(), + arguments: serde_json::json!({"files": ["Cargo.toml"]}), + }, + ); + + let call = domain_tool_call(tool_call); + + assert_eq!(call.id, "fc_item_123"); + assert_eq!(call.name, "read_files"); + } } diff --git a/plans/galaxy-local-first-rig.md b/plans/galaxy-local-first-rig.md index 7fa29bdf..8193fa11 100644 --- a/plans/galaxy-local-first-rig.md +++ b/plans/galaxy-local-first-rig.md @@ -432,6 +432,10 @@ assigned to the phase that owns the affected flow before the related work is con monitor. - [x] Monitor teardown now clears orphaned in-memory state when completion metadata is missing. - [x] Refresh requests ignore completed or no-longer-long-running blocks. +- [ ] ChatGPT subscription follow-up: a reported OAuth-backed tool turn failed because the + Responses request lacked `call_id`. Rig's stream fallback and the serialized assistant/tool + follow-up are now covered by hermetic tests; complete a fresh authenticated end-to-end check and + investigate any remaining loss in the app-owned history handoff. - [x] Open-source project presentation: structure the About page around Galaxy’s local-first identity, audit the repository’s license and third-party notices, and make the root metadata, contribution guidance, and license files agree on an explicit license split (the repository