diff --git a/CHANGELOG.md b/CHANGELOG.md index 1665b76..1aac5f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,15 @@ migrate them to the sibling `patch.responses` key / the model patch's `responses (env override: `COYOTE_PATCH__RESPONSES`). - Sessions saved after this change may contain the new `reasoning` thinking blocks (encrypted reasoning items from the Responses API); older coyote binaries cannot load those sessions. +- OpenAI native web search (the bundled `web_search_coyote` tool) uses the chat-only +`gpt-4o-*search-preview` mechanism, which the responses wire does not serve: until the tool is +updated, pin `wire_api: chat` on the openai client when relying on it. ### Feat - support `wire_api: responses | chat` on the openai and openai-compatible clients, with wire-aware request patching on both patch layers - report token usage (including cached input tokens) on the openai responses path -- streaming responses that end with `response.incomplete` now surface the truncation reason (e.g. `max_output_tokens`) when no output was delivered — matching the non-streaming behavior — and record token usage for truncated turns instead of ending silently +- streaming responses that end with `response.incomplete` now surface the truncation reason (e.g. `max_output_tokens`) when no visible output (text or tool calls) was delivered — matching the non-streaming behavior — and record token usage for truncated turns instead of ending silently - the responses wire always sends `include: ["reasoning.encrypted_content"]` and `"strict": false` tool definitions; strict third-party `/v1/responses` servers may reject these as unknown params when opted in via `wire_api: responses` (workaround: a `responses`-keyed client patch, e.g. `include: null` for Groq) ## v0.10.3 (2026-09-15) diff --git a/src/client/openai.rs b/src/client/openai.rs index c3853b2..ffc0933 100644 --- a/src/client/openai.rs +++ b/src/client/openai.rs @@ -954,6 +954,10 @@ fn openai_responses_handle_event(data: &Value, handler: &mut SseHandler) -> Resu // partial text or tool calls are returned rather than discarded // by a hard error; only an empty truncated response bails. if handler.has_received_visible_output() { + debug!( + "response truncated ({}); keeping partial output", + data["response"]["incomplete_details"]["reason"] + ); return Ok(true); } match data["response"]["incomplete_details"]["reason"].as_str() { diff --git a/src/client/stream.rs b/src/client/stream.rs index 960eedf..9f2acb9 100644 --- a/src/client/stream.rs +++ b/src/client/stream.rs @@ -552,6 +552,23 @@ mod tests { assert!(handler.has_received_content()); } + /// Pins the deliberate exclusion: thinking blocks count as received + /// content but not as visible output, so a thinking-only stream is + /// still an empty response to the caller. + #[test] + fn thinking_blocks_do_not_count_as_visible_output() { + let (mut handler, _rx) = new_handler(); + handler.thinking_block(ThinkingBlock::Thinking { + thinking: "hmm".to_string(), + signature: "sig".to_string(), + }); + assert!(handler.has_received_content()); + assert!(!handler.has_received_visible_output()); + + handler.text("hello").unwrap(); + assert!(handler.has_received_visible_output()); + } + /// Claude splits usage across events: `message_start` carries the input /// and cache counts with a provisional output count, `message_delta` /// carries only the final output count. Later non-None fields must win.