Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:40:05.512Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

responses.rs

1265 lines · 43.0 KB · rust
1use anyhow::{Context as _, Result, anyhow};
2use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
3use http_client::{
4    AsyncBody, CustomHeaders, HttpClient, Method, Request as HttpRequest, RequestBuilderExt,
5};
6use serde::{Deserialize, Serialize, ser::SerializeSeq as _};
7use serde_json::Value;
8use std::sync::Arc;
9
10use crate::{ReasoningEffort, RequestError, Role, ServiceTier, ToolChoice};
11use language_model_core::{
12    CompactedContext, LanguageModelProviderId, ProviderCompactionState, SharedString,
13};
14
15pub const COMPACTION_STATE_FORMAT: &str = "openai.responses.input-items.v1";
16
17#[derive(Serialize, Debug)]
18pub struct Request {
19    pub model: String,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub instructions: Option<String>,
22    #[serde(skip_serializing_if = "ResponseInput::is_empty")]
23    pub input: ResponseInput,
24    #[serde(skip_serializing_if = "Vec::is_empty")]
25    pub include: Vec<ResponseIncludable>,
26    #[serde(default)]
27    pub stream: bool,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub temperature: Option<f32>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub top_p: Option<f32>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub max_output_tokens: Option<u64>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub parallel_tool_calls: Option<bool>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub tool_choice: Option<ToolChoice>,
38    #[serde(skip_serializing_if = "Vec::is_empty")]
39    pub tools: Vec<ToolDefinition>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub prompt_cache_key: Option<String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub reasoning: Option<ReasoningConfig>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub store: Option<bool>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub service_tier: Option<ServiceTier>,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub context_management: Option<Vec<ContextManagement>>,
50}
51
52impl Request {
53    pub fn into_compact_request(self) -> CompactRequest {
54        CompactRequest {
55            model: self.model,
56            instructions: self.instructions,
57            input: self.input,
58            prompt_cache_key: self.prompt_cache_key,
59            service_tier: self.service_tier,
60        }
61    }
62}
63
64#[derive(Serialize, Debug)]
65pub struct CompactRequest {
66    pub model: String,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub instructions: Option<String>,
69    pub input: ResponseInput,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub prompt_cache_key: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub service_tier: Option<ServiceTier>,
74}
75
76#[derive(Deserialize, Debug)]
77pub struct CompactedResponse {
78    pub id: String,
79    pub created_at: u64,
80    pub object: String,
81    pub output: Vec<Value>,
82    pub usage: ResponseUsage,
83}
84
85impl CompactedResponse {
86    pub fn into_compacted_context(
87        self,
88        owner: LanguageModelProviderId,
89    ) -> Result<CompactedContext> {
90        Ok(CompactedContext::ProviderState(
91            provider_compaction_state_from_items(owner, self.output)?,
92        ))
93    }
94}
95
96/// Packages a canonical replacement window into opaque provider state owned by
97/// `owner`.
98///
99/// Several backends speak the OpenAI Responses protocol (OpenAI itself, Zed
100/// Cloud's OpenAI models, OpenAI-compatible endpoints, and others), but their
101/// encrypted compaction items are not interchangeable: only the backend that
102/// produced an item can decrypt it. The owner recorded here is what
103/// [`provider_compaction_items`] later compares against, so it must identify
104/// the backend whose infrastructure produced the items, not merely the wire
105/// protocol.
106pub fn provider_compaction_state_from_items(
107    owner: LanguageModelProviderId,
108    items: Vec<Value>,
109) -> Result<ProviderCompactionState> {
110    validate_compaction_items(&items)?;
111    Ok(ProviderCompactionState::new(
112        owner,
113        SharedString::new_static(COMPACTION_STATE_FORMAT),
114        serde_json::to_string(&items)?,
115    ))
116}
117
118/// Recovers the canonical replacement window from `state` if it is owned by
119/// `owner`, or `None` when the state belongs to a different backend and the
120/// caller should fall back to replaying the full transcript.
121pub fn provider_compaction_items(
122    state: &ProviderCompactionState,
123    owner: &LanguageModelProviderId,
124) -> Result<Option<Vec<Value>>> {
125    if state.provider_id() != owner {
126        return Ok(None);
127    }
128    if state.format() != COMPACTION_STATE_FORMAT {
129        return Err(anyhow!(
130            "unsupported OpenAI compaction state format: {}",
131            state.format()
132        ));
133    }
134
135    let items = serde_json::from_str::<Vec<Value>>(state.payload())
136        .context("malformed OpenAI compaction state payload")?;
137    validate_compaction_items(&items)?;
138    Ok(Some(items))
139}
140
141fn validate_compaction_items(items: &[Value]) -> Result<()> {
142    if items.is_empty() {
143        return Err(anyhow!("OpenAI returned an empty compaction output"));
144    }
145    if !items.iter().any(|item| {
146        item.get("type")
147            .and_then(Value::as_str)
148            .is_some_and(|item_type| item_type == "compaction")
149    }) {
150        return Err(anyhow!(
151            "OpenAI compaction output did not contain a compaction item"
152        ));
153    }
154    Ok(())
155}
156
157#[derive(Debug, Default)]
158pub struct ResponseInput {
159    provider_items: Vec<Value>,
160    generated_items: Vec<ResponseInputItem>,
161}
162
163impl ResponseInput {
164    pub fn new(provider_items: Vec<Value>, generated_items: Vec<ResponseInputItem>) -> Self {
165        Self {
166            provider_items,
167            generated_items,
168        }
169    }
170
171    pub fn is_empty(&self) -> bool {
172        self.provider_items.is_empty() && self.generated_items.is_empty()
173    }
174
175    /// Filters only the items this crate generated from the request.
176    ///
177    /// Provider items are a canonical replacement window that must be replayed
178    /// verbatim, so they are exempt from filtering. Callers that rewrite the
179    /// input to satisfy backend-specific requirements (and therefore can't
180    /// tolerate arbitrary items inside a replayed window) should not accept
181    /// provider-native compaction state in the first place.
182    pub fn retain(&mut self, predicate: impl FnMut(&ResponseInputItem) -> bool) {
183        self.generated_items.retain(predicate);
184    }
185}
186
187impl Serialize for ResponseInput {
188    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
189    where
190        S: serde::Serializer,
191    {
192        let mut sequence = serializer
193            .serialize_seq(Some(self.provider_items.len() + self.generated_items.len()))?;
194        for item in &self.provider_items {
195            sequence.serialize_element(item)?;
196        }
197        for item in &self.generated_items {
198            sequence.serialize_element(item)?;
199        }
200        sequence.end()
201    }
202}
203
204/// Server-side context management configuration.
205///
206/// <https://developers.openai.com/api/docs/guides/compaction>
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(tag = "type", rename_all = "snake_case")]
209pub enum ContextManagement {
210    Compaction { compact_threshold: u64 },
211}
212
213#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
214#[serde(rename_all = "snake_case")]
215pub enum ResponseIncludable {
216    #[serde(rename = "reasoning.encrypted_content")]
217    ReasoningEncryptedContent,
218}
219
220#[derive(Debug, Serialize, Deserialize)]
221#[serde(tag = "type", rename_all = "snake_case")]
222pub enum ResponseInputItem {
223    Message(ResponseMessageItem),
224    FunctionCall(ResponseFunctionCallItem),
225    FunctionCallOutput(ResponseFunctionCallOutputItem),
226    CustomToolCall(ResponseCustomToolCallItem),
227    CustomToolCallOutput(ResponseCustomToolCallOutputItem),
228    Reasoning(ResponseReasoningInputItem),
229    Compaction(ResponseCompactionItem),
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
233pub struct ResponseCompactionItem {
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub id: Option<Arc<str>>,
236    pub encrypted_content: Arc<str>,
237}
238
239#[derive(Debug, Serialize, Deserialize)]
240pub struct ResponseMessageItem {
241    pub role: Role,
242    pub content: Vec<ResponseInputContent>,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub phase: Option<String>,
245}
246
247#[derive(Debug, Serialize, Deserialize)]
248pub struct ResponseFunctionCallItem {
249    pub call_id: String,
250    pub name: String,
251    pub arguments: String,
252}
253
254#[derive(Debug, Serialize, Deserialize)]
255pub struct ResponseFunctionCallOutputItem {
256    pub call_id: String,
257    pub output: ResponseFunctionCallOutputContent,
258}
259
260#[derive(Debug, Serialize, Deserialize)]
261pub struct ResponseCustomToolCallItem {
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub id: Option<String>,
264    pub call_id: String,
265    pub name: String,
266    pub input: String,
267}
268
269#[derive(Debug, Serialize, Deserialize)]
270pub struct ResponseCustomToolCallOutputItem {
271    pub call_id: String,
272    pub output: ResponseFunctionCallOutputContent,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
276pub struct ResponseReasoningInputItem {
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub id: Option<String>,
279    #[serde(default)]
280    pub summary: Vec<ResponseReasoningSummaryPart>,
281    #[serde(default, skip_serializing_if = "Vec::is_empty")]
282    pub content: Vec<Value>,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub encrypted_content: Option<String>,
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub status: Option<String>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
290#[serde(tag = "type", rename_all = "snake_case")]
291pub enum ResponseReasoningSummaryPart {
292    SummaryText { text: String },
293}
294
295#[derive(Debug, Serialize, Deserialize)]
296#[serde(untagged)]
297pub enum ResponseFunctionCallOutputContent {
298    List(Vec<ResponseInputContent>),
299    Text(String),
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303#[serde(tag = "type")]
304pub enum ResponseInputContent {
305    #[serde(rename = "input_text")]
306    Text { text: String },
307    #[serde(rename = "input_image")]
308    Image { image_url: String },
309    #[serde(rename = "output_text")]
310    OutputText {
311        text: String,
312        #[serde(default)]
313        annotations: Vec<serde_json::Value>,
314    },
315    #[serde(rename = "refusal")]
316    Refusal { refusal: String },
317}
318
319#[derive(Serialize, Debug)]
320pub struct ReasoningConfig {
321    pub effort: ReasoningEffort,
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub summary: Option<ReasoningSummaryMode>,
324}
325
326#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
327#[serde(rename_all = "lowercase")]
328pub enum ReasoningSummaryMode {
329    Auto,
330    Concise,
331    Detailed,
332}
333
334#[derive(Serialize, Deserialize, Debug)]
335#[serde(tag = "type", rename_all = "snake_case")]
336pub enum ToolDefinition {
337    Function {
338        name: String,
339        #[serde(skip_serializing_if = "Option::is_none")]
340        description: Option<String>,
341        #[serde(skip_serializing_if = "Option::is_none")]
342        parameters: Option<Value>,
343        #[serde(skip_serializing_if = "Option::is_none")]
344        strict: Option<bool>,
345    },
346    Custom {
347        name: String,
348        #[serde(skip_serializing_if = "Option::is_none")]
349        description: Option<String>,
350        #[serde(skip_serializing_if = "Option::is_none")]
351        format: Option<CustomToolFormat>,
352    },
353}
354
355#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
356#[serde(tag = "type", rename_all = "snake_case")]
357pub enum CustomToolFormat {
358    Text,
359    Grammar {
360        syntax: CustomToolGrammarSyntax,
361        definition: String,
362    },
363}
364
365#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
366#[serde(rename_all = "lowercase")]
367pub enum CustomToolGrammarSyntax {
368    Lark,
369    Regex,
370}
371
372#[derive(Serialize, Deserialize, Debug, Clone)]
373pub struct ResponseError {
374    #[serde(default)]
375    pub code: Option<String>,
376    pub message: String,
377    #[serde(default)]
378    pub param: Option<Value>,
379}
380
381/// Payload of the top-level `error` SSE event from the Responses API.
382///
383/// OpenAI's spec documents the error fields as being at the top level of the
384/// event, but in practice the API often nests them under an `error` object.
385#[derive(Deserialize, Debug, Clone, Default)]
386pub struct GenericStreamErrorPayload {
387    #[serde(flatten)]
388    top_level: PartialResponseError,
389    #[serde(default)]
390    error: Option<PartialResponseError>,
391}
392
393#[derive(Deserialize, Debug, Clone, Default)]
394struct PartialResponseError {
395    #[serde(default)]
396    code: Option<String>,
397    #[serde(default)]
398    message: Option<String>,
399    #[serde(default)]
400    param: Option<Value>,
401}
402
403impl GenericStreamErrorPayload {
404    pub fn into_response_error(self) -> ResponseError {
405        let nested = self.error.unwrap_or_default();
406        ResponseError {
407            code: self.top_level.code.or(nested.code),
408            message: self
409                .top_level
410                .message
411                .or(nested.message)
412                .unwrap_or_default(),
413            param: self.top_level.param.or(nested.param),
414        }
415    }
416}
417
418#[derive(Deserialize, Debug)]
419#[serde(tag = "type")]
420pub enum StreamEvent {
421    #[serde(rename = "response.created")]
422    Created { response: ResponseSummary },
423    #[serde(rename = "response.in_progress")]
424    InProgress { response: ResponseSummary },
425    #[serde(rename = "response.output_item.added")]
426    OutputItemAdded {
427        output_index: usize,
428        #[serde(default)]
429        sequence_number: Option<u64>,
430        item: ResponseOutputItem,
431    },
432    #[serde(rename = "response.output_item.done")]
433    OutputItemDone {
434        output_index: usize,
435        #[serde(default)]
436        sequence_number: Option<u64>,
437        item: ResponseOutputItem,
438    },
439    #[serde(rename = "response.content_part.added")]
440    ContentPartAdded {
441        item_id: String,
442        output_index: usize,
443        content_index: usize,
444        part: Value,
445    },
446    #[serde(rename = "response.content_part.done")]
447    ContentPartDone {
448        item_id: String,
449        output_index: usize,
450        content_index: usize,
451        part: Value,
452    },
453    #[serde(rename = "response.output_text.delta")]
454    OutputTextDelta {
455        item_id: String,
456        output_index: usize,
457        #[serde(default)]
458        content_index: Option<usize>,
459        delta: String,
460    },
461    #[serde(rename = "response.output_text.done")]
462    OutputTextDone {
463        item_id: String,
464        output_index: usize,
465        #[serde(default)]
466        content_index: Option<usize>,
467        text: String,
468    },
469    #[serde(rename = "response.refusal.delta")]
470    RefusalDelta {
471        item_id: String,
472        output_index: usize,
473        content_index: usize,
474        delta: String,
475        #[serde(default)]
476        sequence_number: Option<u64>,
477    },
478    #[serde(rename = "response.refusal.done")]
479    RefusalDone {
480        item_id: String,
481        output_index: usize,
482        content_index: usize,
483        refusal: String,
484        #[serde(default)]
485        sequence_number: Option<u64>,
486    },
487    #[serde(rename = "response.reasoning_summary_part.added")]
488    ReasoningSummaryPartAdded {
489        item_id: String,
490        output_index: usize,
491        summary_index: usize,
492    },
493    #[serde(rename = "response.reasoning_summary_text.delta")]
494    ReasoningSummaryTextDelta {
495        item_id: String,
496        output_index: usize,
497        delta: String,
498    },
499    #[serde(rename = "response.reasoning_summary_text.done")]
500    ReasoningSummaryTextDone {
501        item_id: String,
502        output_index: usize,
503        text: String,
504    },
505    #[serde(rename = "response.reasoning_summary_part.done")]
506    ReasoningSummaryPartDone {
507        item_id: String,
508        output_index: usize,
509        summary_index: usize,
510    },
511    #[serde(rename = "response.reasoning.delta")]
512    ReasoningDelta {
513        #[serde(default)]
514        item_id: Option<String>,
515        #[serde(default)]
516        output_index: Option<usize>,
517        delta: String,
518    },
519    #[serde(rename = "response.reasoning.done")]
520    ReasoningDone {
521        #[serde(default)]
522        item_id: Option<String>,
523        #[serde(default)]
524        output_index: Option<usize>,
525        #[serde(default)]
526        text: Option<String>,
527    },
528    #[serde(rename = "response.function_call_arguments.delta")]
529    FunctionCallArgumentsDelta {
530        item_id: String,
531        output_index: usize,
532        delta: String,
533        #[serde(default)]
534        sequence_number: Option<u64>,
535    },
536    #[serde(rename = "response.function_call_arguments.done")]
537    FunctionCallArgumentsDone {
538        item_id: String,
539        output_index: usize,
540        arguments: String,
541        #[serde(default)]
542        sequence_number: Option<u64>,
543    },
544    #[serde(rename = "response.custom_tool_call_input.delta")]
545    CustomToolCallInputDelta {
546        item_id: String,
547        output_index: usize,
548        delta: String,
549        #[serde(default)]
550        sequence_number: Option<u64>,
551    },
552    #[serde(rename = "response.custom_tool_call_input.done")]
553    CustomToolCallInputDone {
554        item_id: String,
555        output_index: usize,
556        input: String,
557        #[serde(default)]
558        sequence_number: Option<u64>,
559    },
560    #[serde(rename = "response.completed")]
561    Completed { response: ResponseSummary },
562    #[serde(rename = "response.incomplete")]
563    Incomplete { response: ResponseSummary },
564    #[serde(rename = "response.failed")]
565    Failed { response: ResponseSummary },
566    #[serde(rename = "response.error")]
567    Error { error: ResponseError },
568    #[serde(rename = "error")]
569    GenericError {
570        #[serde(flatten)]
571        error: GenericStreamErrorPayload,
572    },
573    #[serde(other)]
574    Unknown,
575}
576
577#[derive(Serialize, Deserialize, Debug, Default, Clone)]
578pub struct ResponseSummary {
579    #[serde(default)]
580    pub id: Option<String>,
581    #[serde(default)]
582    pub status: Option<String>,
583    #[serde(default)]
584    pub incomplete_details: Option<ResponseIncompleteDetails>,
585    #[serde(default)]
586    pub error: Option<ResponseError>,
587    #[serde(default)]
588    pub usage: Option<ResponseUsage>,
589    #[serde(default)]
590    pub output: Vec<ResponseOutputItem>,
591    #[serde(default)]
592    pub service_tier: Option<crate::ServiceTier>,
593}
594
595#[derive(Serialize, Deserialize, Debug, Default, Clone)]
596pub struct ResponseIncompleteDetails {
597    #[serde(default)]
598    pub reason: Option<String>,
599}
600
601#[derive(Serialize, Deserialize, Debug, Default, Clone)]
602pub struct ResponseUsage {
603    #[serde(default)]
604    pub input_tokens: Option<u64>,
605    #[serde(default)]
606    pub input_tokens_details: ResponseInputTokensDetails,
607    #[serde(default)]
608    pub output_tokens: Option<u64>,
609    #[serde(default)]
610    pub output_tokens_details: ResponseOutputTokensDetails,
611    #[serde(default)]
612    pub total_tokens: Option<u64>,
613}
614
615#[derive(Serialize, Deserialize, Debug, Default, Clone)]
616pub struct ResponseInputTokensDetails {
617    #[serde(default)]
618    pub cached_tokens: u64,
619}
620
621#[derive(Serialize, Deserialize, Debug, Default, Clone)]
622pub struct ResponseOutputTokensDetails {
623    #[serde(default)]
624    pub reasoning_tokens: u64,
625}
626
627#[derive(Serialize, Deserialize, Debug, Clone)]
628#[serde(tag = "type", rename_all = "snake_case")]
629pub enum ResponseOutputItem {
630    Message(ResponseOutputMessage),
631    FunctionCall(ResponseFunctionToolCall),
632    CustomToolCall(ResponseCustomToolCall),
633    Reasoning(ResponseReasoningItem),
634    Compaction(ResponseCompactionItem),
635    /// Deserialization-only catch-all; must never be serialized back to the API.
636    #[serde(other)]
637    Unknown,
638}
639
640#[derive(Serialize, Deserialize, Debug, Clone)]
641pub struct ResponseReasoningItem {
642    #[serde(default)]
643    pub id: Option<String>,
644    #[serde(default)]
645    pub summary: Vec<ReasoningSummaryPart>,
646    #[serde(default)]
647    pub content: Vec<Value>,
648    #[serde(default)]
649    pub encrypted_content: Option<String>,
650    #[serde(default)]
651    pub status: Option<String>,
652}
653
654#[derive(Serialize, Deserialize, Debug, Clone)]
655#[serde(tag = "type", rename_all = "snake_case")]
656pub enum ReasoningSummaryPart {
657    SummaryText {
658        text: String,
659    },
660    #[serde(other)]
661    Unknown,
662}
663
664#[derive(Serialize, Deserialize, Debug, Clone)]
665pub struct ResponseOutputMessage {
666    #[serde(default)]
667    pub id: Option<String>,
668    #[serde(default)]
669    pub content: Vec<Value>,
670    #[serde(default)]
671    pub role: Option<String>,
672    #[serde(default)]
673    pub status: Option<String>,
674    #[serde(default)]
675    pub phase: Option<String>,
676}
677
678#[derive(Serialize, Deserialize, Debug, Clone)]
679pub struct ResponseFunctionToolCall {
680    #[serde(default)]
681    pub id: Option<String>,
682    #[serde(default)]
683    pub arguments: String,
684    #[serde(default)]
685    pub call_id: Option<String>,
686    #[serde(default)]
687    pub name: Option<String>,
688    #[serde(default)]
689    pub status: Option<String>,
690}
691
692#[derive(Serialize, Deserialize, Debug, Clone)]
693pub struct ResponseCustomToolCall {
694    #[serde(default)]
695    pub id: Option<String>,
696    #[serde(default)]
697    pub status: Option<String>,
698    #[serde(default)]
699    pub call_id: Option<String>,
700    #[serde(default)]
701    pub name: Option<String>,
702    #[serde(default)]
703    pub input: String,
704}
705
706pub async fn compact_response(
707    client: &dyn HttpClient,
708    provider_name: &str,
709    api_url: &str,
710    api_key: &str,
711    request: CompactRequest,
712    extra_headers: &CustomHeaders,
713) -> Result<CompactedResponse, RequestError> {
714    let request = HttpRequest::builder()
715        .method(Method::POST)
716        .uri(format!("{api_url}/responses/compact"))
717        .header("Content-Type", "application/json")
718        .header("Authorization", format!("Bearer {}", api_key.trim()))
719        .extra_headers(extra_headers)
720        .body(AsyncBody::from(
721            serde_json::to_string(&request).map_err(|error| RequestError::Other(error.into()))?,
722        ))
723        .map_err(|error| RequestError::Other(error.into()))?;
724
725    let mut response = client.send(request).await?;
726    let mut body = String::new();
727    response
728        .body_mut()
729        .read_to_string(&mut body)
730        .await
731        .map_err(|error| RequestError::Other(error.into()))?;
732
733    if response.status().is_success() {
734        serde_json::from_str(&body).map_err(|error| RequestError::Other(error.into()))
735    } else {
736        Err(RequestError::HttpResponseError {
737            provider: provider_name.to_owned(),
738            status_code: response.status(),
739            body,
740            headers: response.headers().clone(),
741        })
742    }
743}
744
745pub async fn stream_response(
746    client: &dyn HttpClient,
747    provider_name: &str,
748    api_url: &str,
749    api_key: &str,
750    request: Request,
751    extra_headers: &CustomHeaders,
752) -> Result<BoxStream<'static, Result<StreamEvent>>, RequestError> {
753    let uri = format!("{api_url}/responses");
754    let is_streaming = request.stream;
755    let request = HttpRequest::builder()
756        .method(Method::POST)
757        .uri(uri)
758        .header("Content-Type", "application/json")
759        .header("Authorization", format!("Bearer {}", api_key.trim()))
760        .extra_headers(extra_headers)
761        .body(AsyncBody::from(
762            serde_json::to_string(&request).map_err(|e| RequestError::Other(e.into()))?,
763        ))
764        .map_err(|e| RequestError::Other(e.into()))?;
765
766    let mut response = client.send(request).await?;
767    if response.status().is_success() {
768        if is_streaming {
769            let reader = BufReader::new(response.into_body());
770            Ok(reader
771                .lines()
772                .filter_map(|line| async move {
773                    match line {
774                        Ok(line) => {
775                            let line = line
776                                .strip_prefix("data: ")
777                                .or_else(|| line.strip_prefix("data:"))?;
778                            if line == "[DONE]" || line.is_empty() {
779                                None
780                            } else {
781                                match serde_json::from_str::<StreamEvent>(line) {
782                                    Ok(event) => Some(Ok(event)),
783                                    Err(error) => {
784                                        log::error!(
785                                            "Failed to parse OpenAI responses stream event: `{}`\nResponse: `{}`",
786                                            error,
787                                            line,
788                                        );
789                                        Some(Err(anyhow!(error)))
790                                    }
791                                }
792                            }
793                        }
794                        Err(error) => Some(Err(anyhow!(error))),
795                    }
796                })
797                .boxed())
798        } else {
799            let mut body = String::new();
800            response
801                .body_mut()
802                .read_to_string(&mut body)
803                .await
804                .map_err(|e| RequestError::Other(e.into()))?;
805
806            match serde_json::from_str::<ResponseSummary>(&body) {
807                Ok(response_summary) => {
808                    let events = vec![
809                        StreamEvent::Created {
810                            response: response_summary.clone(),
811                        },
812                        StreamEvent::InProgress {
813                            response: response_summary.clone(),
814                        },
815                    ];
816
817                    let mut all_events = events;
818                    for (output_index, item) in response_summary.output.iter().enumerate() {
819                        all_events.push(StreamEvent::OutputItemAdded {
820                            output_index,
821                            sequence_number: None,
822                            item: item.clone(),
823                        });
824
825                        match item {
826                            ResponseOutputItem::Message(message) => {
827                                for content_item in &message.content {
828                                    if let Some(text) = content_item.get("text") {
829                                        if let Some(text_str) = text.as_str() {
830                                            if let Some(ref item_id) = message.id {
831                                                all_events.push(StreamEvent::OutputTextDelta {
832                                                    item_id: item_id.clone(),
833                                                    output_index,
834                                                    content_index: None,
835                                                    delta: text_str.to_string(),
836                                                });
837                                            }
838                                        }
839                                    }
840                                }
841                            }
842                            ResponseOutputItem::FunctionCall(function_call) => {
843                                if let Some(ref item_id) = function_call.id {
844                                    all_events.push(StreamEvent::FunctionCallArgumentsDone {
845                                        item_id: item_id.clone(),
846                                        output_index,
847                                        arguments: function_call.arguments.clone(),
848                                        sequence_number: None,
849                                    });
850                                }
851                            }
852                            ResponseOutputItem::CustomToolCall(custom_tool_call) => {
853                                if let Some(ref item_id) = custom_tool_call.id {
854                                    all_events.push(StreamEvent::CustomToolCallInputDone {
855                                        item_id: item_id.clone(),
856                                        output_index,
857                                        input: custom_tool_call.input.clone(),
858                                        sequence_number: None,
859                                    });
860                                }
861                            }
862                            ResponseOutputItem::Reasoning(reasoning) => {
863                                if let Some(ref item_id) = reasoning.id {
864                                    for part in &reasoning.summary {
865                                        if let ReasoningSummaryPart::SummaryText { text } = part {
866                                            all_events.push(
867                                                StreamEvent::ReasoningSummaryTextDelta {
868                                                    item_id: item_id.clone(),
869                                                    output_index,
870                                                    delta: text.clone(),
871                                                },
872                                            );
873                                        }
874                                    }
875                                }
876                            }
877                            // No synthesized deltas; the `OutputItemDone`
878                            // event pushed below carries the full item.
879                            ResponseOutputItem::Compaction(_) => {}
880                            ResponseOutputItem::Unknown => {}
881                        }
882
883                        all_events.push(StreamEvent::OutputItemDone {
884                            output_index,
885                            sequence_number: None,
886                            item: item.clone(),
887                        });
888                    }
889
890                    let status = response_summary.status.clone();
891                    all_events.push(match status.as_deref() {
892                        Some("incomplete") => StreamEvent::Incomplete {
893                            response: response_summary,
894                        },
895                        Some("failed") => StreamEvent::Failed {
896                            response: response_summary,
897                        },
898                        _ => StreamEvent::Completed {
899                            response: response_summary,
900                        },
901                    });
902
903                    Ok(futures::stream::iter(all_events.into_iter().map(Ok)).boxed())
904                }
905                Err(error) => {
906                    log::error!(
907                        "Failed to parse OpenAI non-streaming response: `{}`\nResponse: `{}`",
908                        error,
909                        body,
910                    );
911                    Err(RequestError::Other(anyhow!(error)))
912                }
913            }
914        }
915    } else {
916        let mut body = String::new();
917        response
918            .body_mut()
919            .read_to_string(&mut body)
920            .await
921            .map_err(|e| RequestError::Other(e.into()))?;
922
923        Err(RequestError::HttpResponseError {
924            provider: provider_name.to_owned(),
925            status_code: response.status(),
926            body,
927            headers: response.headers().clone(),
928        })
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935    use futures::executor::block_on;
936    use http_client::FakeHttpClient;
937    use language_model_core::OPEN_AI_PROVIDER_ID;
938    use serde_json::json;
939    use std::sync::{Arc, Mutex};
940
941    #[test]
942    fn compact_response_posts_supported_request_fields() {
943        let captured_request = Arc::new(Mutex::new(None));
944        let captured_request_for_handler = captured_request.clone();
945        let http_client = FakeHttpClient::create(move |request| {
946            let captured_request = captured_request_for_handler.clone();
947            async move {
948                let method = request.method().clone();
949                let uri = request.uri().to_string();
950                let authorization = request
951                    .headers()
952                    .get("Authorization")
953                    .and_then(|value| value.to_str().ok())
954                    .map(str::to_string);
955                let mut body = request.into_body();
956                let mut body_text = String::new();
957                body.read_to_string(&mut body_text).await?;
958                *captured_request.lock().unwrap() = Some((method, uri, authorization, body_text));
959
960                Ok(http_client::Response::builder()
961                    .status(200)
962                    .body(AsyncBody::from(
963                        json!({
964                            "id": "resp_compact",
965                            "created_at": 1_700_000_000,
966                            "object": "response.compaction",
967                            "output": [{
968                                "type": "compaction",
969                                "id": "cmp_manual",
970                                "encrypted_content": "opaque-state"
971                            }],
972                            "usage": {
973                                "input_tokens": 100,
974                                "input_tokens_details": {"cached_tokens": 20},
975                                "output_tokens": 10,
976                                "output_tokens_details": {"reasoning_tokens": 5},
977                                "total_tokens": 110
978                            }
979                        })
980                        .to_string(),
981                    ))?)
982            }
983        });
984        let response = block_on(compact_response(
985            http_client.as_ref(),
986            "OpenAI",
987            "https://api.openai.com/v1",
988            "secret",
989            compact_test_request(),
990            &CustomHeaders::default(),
991        ))
992        .unwrap();
993
994        assert_eq!(
995            provider_compaction_items(
996                &match response
997                    .into_compacted_context(OPEN_AI_PROVIDER_ID)
998                    .unwrap()
999                {
1000                    CompactedContext::ProviderState(state) => state,
1001                    CompactedContext::Summary { .. } => panic!("expected provider state"),
1002                },
1003                &OPEN_AI_PROVIDER_ID
1004            )
1005            .unwrap(),
1006            Some(vec![json!({
1007                "type": "compaction",
1008                "id": "cmp_manual",
1009                "encrypted_content": "opaque-state"
1010            })])
1011        );
1012        let (method, uri, authorization, body) = captured_request.lock().unwrap().take().unwrap();
1013        assert_eq!(method, Method::POST);
1014        assert_eq!(uri, "https://api.openai.com/v1/responses/compact");
1015        assert_eq!(authorization.as_deref(), Some("Bearer secret"));
1016        assert_eq!(
1017            serde_json::from_str::<Value>(&body).unwrap(),
1018            json!({
1019                "model": "gpt-5.4",
1020                "input": [{
1021                    "type": "message",
1022                    "role": "user",
1023                    "content": [{
1024                        "type": "input_text",
1025                        "text": "Retain this context."
1026                    }]
1027                }],
1028                "prompt_cache_key": "thread-123",
1029                "service_tier": "priority"
1030            })
1031        );
1032    }
1033
1034    #[test]
1035    fn compact_response_reports_http_and_deserialization_errors() {
1036        let http_client = FakeHttpClient::create(|_| async move {
1037            Ok(http_client::Response::builder()
1038                .status(429)
1039                .header("retry-after", "5")
1040                .body(AsyncBody::from("rate limited"))?)
1041        });
1042
1043        let error = block_on(compact_response(
1044            http_client.as_ref(),
1045            "OpenAI",
1046            "https://api.openai.com/v1",
1047            "secret",
1048            compact_test_request(),
1049            &CustomHeaders::default(),
1050        ))
1051        .unwrap_err();
1052
1053        match error {
1054            RequestError::HttpResponseError {
1055                provider,
1056                status_code,
1057                body,
1058                headers,
1059            } => {
1060                assert_eq!(provider, "OpenAI");
1061                assert_eq!(status_code, 429);
1062                assert_eq!(body, "rate limited");
1063                assert_eq!(headers["retry-after"], "5");
1064            }
1065            error => panic!("expected an HTTP response error, got {error:?}"),
1066        }
1067
1068        let http_client = FakeHttpClient::create(|_| async move {
1069            Ok(http_client::Response::builder()
1070                .status(200)
1071                .body(AsyncBody::from("not valid JSON"))?)
1072        });
1073
1074        let error = block_on(compact_response(
1075            http_client.as_ref(),
1076            "OpenAI",
1077            "https://api.openai.com/v1",
1078            "secret",
1079            compact_test_request(),
1080            &CustomHeaders::default(),
1081        ))
1082        .unwrap_err();
1083
1084        assert!(
1085            matches!(error, RequestError::Other(_)),
1086            "expected malformed JSON to produce a request error, got {error:?}"
1087        );
1088    }
1089
1090    #[test]
1091    fn compacted_response_preserves_canonical_output_items() {
1092        let output = vec![
1093            json!({
1094                "type": "message",
1095                "role": "user",
1096                "content": "Retained user context.",
1097                "provider_extension": {"preserve": true}
1098            }),
1099            json!({
1100                "type": "compaction",
1101                "id": "cmp_manual",
1102                "encrypted_content": "opaque-state"
1103            }),
1104        ];
1105        let response: CompactedResponse = serde_json::from_value(json!({
1106            "id": "resp_compact",
1107            "created_at": 1_700_000_000,
1108            "object": "response.compaction",
1109            "output": &output,
1110            "usage": {
1111                "input_tokens": 100,
1112                "input_tokens_details": {"cached_tokens": 20},
1113                "output_tokens": 10,
1114                "output_tokens_details": {"reasoning_tokens": 5},
1115                "total_tokens": 110
1116            }
1117        }))
1118        .unwrap();
1119
1120        let CompactedContext::ProviderState(state) = response
1121            .into_compacted_context(OPEN_AI_PROVIDER_ID)
1122            .unwrap()
1123        else {
1124            panic!("expected provider state");
1125        };
1126        assert_eq!(
1127            provider_compaction_items(&state, &OPEN_AI_PROVIDER_ID).unwrap(),
1128            Some(output)
1129        );
1130    }
1131
1132    #[test]
1133    fn compacted_response_rejects_output_without_compaction_item() {
1134        let response: CompactedResponse = serde_json::from_value(json!({
1135            "id": "resp_compact",
1136            "created_at": 1_700_000_000,
1137            "object": "response.compaction",
1138            "output": [{
1139                "type": "message",
1140                "role": "user",
1141                "content": "Retained user context."
1142            }],
1143            "usage": {
1144                "input_tokens": 100,
1145                "input_tokens_details": {"cached_tokens": 20},
1146                "output_tokens": 10,
1147                "output_tokens_details": {"reasoning_tokens": 5},
1148                "total_tokens": 110
1149            }
1150        }))
1151        .unwrap();
1152
1153        assert!(
1154            response
1155                .into_compacted_context(OPEN_AI_PROVIDER_ID)
1156                .unwrap_err()
1157                .to_string()
1158                .contains("compaction item")
1159        );
1160    }
1161
1162    #[test]
1163    fn compacted_response_rejects_empty_output() {
1164        let response: CompactedResponse = serde_json::from_value(json!({
1165            "id": "resp_compact",
1166            "created_at": 1_700_000_000,
1167            "object": "response.compaction",
1168            "output": [],
1169            "usage": {
1170                "input_tokens": 100,
1171                "input_tokens_details": {"cached_tokens": 20},
1172                "output_tokens": 10,
1173                "output_tokens_details": {"reasoning_tokens": 5},
1174                "total_tokens": 110
1175            }
1176        }))
1177        .unwrap();
1178
1179        assert!(
1180            response
1181                .into_compacted_context(OPEN_AI_PROVIDER_ID)
1182                .unwrap_err()
1183                .to_string()
1184                .contains("empty")
1185        );
1186    }
1187
1188    #[test]
1189    fn provider_compaction_items_ignores_state_owned_by_another_provider() {
1190        let items = vec![json!({
1191            "type": "compaction",
1192            "id": "cmp_manual",
1193            "encrypted_content": "opaque-state"
1194        })];
1195        let state =
1196            provider_compaction_state_from_items(OPEN_AI_PROVIDER_ID, items.clone()).unwrap();
1197
1198        assert_eq!(
1199            provider_compaction_items(&state, &LanguageModelProviderId::new("anthropic")).unwrap(),
1200            None
1201        );
1202
1203        // The same window stamped for a different OpenAI-protocol backend is
1204        // opaque to OpenAI proper: encrypted compaction items are only
1205        // decryptable by the infrastructure that produced them.
1206        let compatible_backend = LanguageModelProviderId::new("my-compatible-endpoint");
1207        let state =
1208            provider_compaction_state_from_items(compatible_backend.clone(), items).unwrap();
1209        assert_eq!(
1210            provider_compaction_items(&state, &OPEN_AI_PROVIDER_ID).unwrap(),
1211            None
1212        );
1213        assert!(
1214            provider_compaction_items(&state, &compatible_backend)
1215                .unwrap()
1216                .is_some()
1217        );
1218    }
1219
1220    #[test]
1221    fn provider_compaction_items_rejects_unknown_open_ai_format() {
1222        let state = ProviderCompactionState::new(
1223            OPEN_AI_PROVIDER_ID,
1224            "openai.responses.input-items.v2",
1225            "[]",
1226        );
1227        assert!(
1228            provider_compaction_items(&state, &OPEN_AI_PROVIDER_ID)
1229                .unwrap_err()
1230                .to_string()
1231                .contains("unsupported OpenAI compaction state format")
1232        );
1233    }
1234
1235    #[test]
1236    fn provider_compaction_items_rejects_malformed_open_ai_state() {
1237        let state = ProviderCompactionState::new(
1238            OPEN_AI_PROVIDER_ID,
1239            COMPACTION_STATE_FORMAT,
1240            "not valid JSON",
1241        );
1242
1243        assert!(provider_compaction_items(&state, &OPEN_AI_PROVIDER_ID).is_err());
1244    }
1245
1246    fn compact_test_request() -> CompactRequest {
1247        CompactRequest {
1248            model: "gpt-5.4".to_string(),
1249            instructions: None,
1250            input: ResponseInput::new(
1251                Vec::new(),
1252                vec![ResponseInputItem::Message(ResponseMessageItem {
1253                    role: Role::User,
1254                    content: vec![ResponseInputContent::Text {
1255                        text: "Retain this context.".to_string(),
1256                    }],
1257                    phase: None,
1258                })],
1259            ),
1260            prompt_cache_key: Some("thread-123".to_string()),
1261            service_tier: Some(ServiceTier::Priority),
1262        }
1263    }
1264}
1265
Served at tenant.openagents/omega Member data and write actions are omitted.