1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
//! The one owner of the OpenAI **Responses API** wire contract (`POST
//! /v1/responses`), in **both** directions, shared by the newt-inference
//! transport (the [`InferenceBackend`](../../newt_inference/backend/trait.InferenceBackend.html)
//! `ChatReply` seam) and the newt-core agentic loop:
//!
//! - **encode** — [`build_responses_input`] splits chat-style messages into the
//! `(instructions, input)` the request body carries.
//! - **decode** — [`decode_response`] parses the reply payload into a typed
//! [`DecodedResponse`].
//!
//! Before this module there were TWO hand-rolled copies of *each* direction —
//! one pair in `newt-inference/src/responses.rs`, one in
//! `newt-core/src/agentic/mod.rs` — that had drifted (the inference decoder
//! gated on `part.type == "output_text"`; the agentic one pulled `part.text`
//! from any part and also extracted `function_call` / `reasoning`; the two
//! request builders joined `instructions` with different separators). Two
//! implementations of one wire shape is the sprawl this workspace treats as a
//! bug class, so this is the single owner. (Backend-neutral *policy* — tools,
//! reasoning effort, budgeting — stays in the agentic loop; only the wire
//! shaping lives here.)
//!
//! ## Invariant: HTTP `2xx` is NOT a completed response
//!
//! A `200 OK` transport status says only "the request was accepted". Whether the
//! *turn* finished lives in the body. [`decode_response`] is **fail-closed**: it
//! returns `Ok(DecodedResponse)` ONLY when the body carries affirmative success
//! output, and a typed [`ResponseDecodeError`] for every other case — a
//! top-level error (which always wins), a `failed` / `incomplete` / non-terminal
//! status, an explicit `refusal`, or a malformed/empty body. No consumer can
//! mistake a truncated, failed, refused, or empty body for an empty-but-fine
//! reply.
use serde_json::Value;
/// Whether a Responses request opts into OpenAI **server-side storage**
/// (`store: true`) of the response payload.
///
/// Newt's policy is `false` — **stateless** (#1526, invariant #5; the
/// sovereign-data doctrine). The harness replays the full turn history on every
/// request and never uses `previous_response_id`, so server-side retention buys
/// nothing — it would only leave an implicit, unaudited copy of the operator's
/// prompts, source, and reasoning on the provider. The Responses API defaults
/// `store` to **`true`**, so this MUST be set on every request; relying on the
/// default silently retains data. Both Responses request builders (the agentic
/// loop and the inference transport) send this one value, so the policy is
/// explicit and identical on every surface.
pub const STORE_RESPONSE_SERVER_SIDE: bool = false;
/// Minimal Responses request used by capability probes. Keeping it here
/// preserves the same explicit no-storage policy as production requests.
pub fn generation_probe_body(model: &str) -> Value {
serde_json::json!({
"model": model,
"input": "Reply with OK.",
"max_output_tokens": 16,
"store": STORE_RESPONSE_SERVER_SIDE,
})
}
// Model: GPT-5 | Harness: Codex | Operator: Shawn Hartsock | Time: 13:18 EDT | Date: 2026-08-12
/// Split chat-style messages into the Responses API's `(instructions, input)`:
/// `system`/`developer` messages concatenate into top-level `instructions`;
/// `user`/`assistant` become `input` message items with plain string content.
/// Any item already shaped as a Responses item (carrying a `type` field, e.g.
/// `function_call` / `function_call_output` / `reasoning`) passes through
/// untouched, preserving output order — the reasoning-echo contract the agentic
/// loop relies on.
///
/// This is the single request-shaper both the agentic loop and the inference
/// transport call, so the two can never drift on the instructions/input split.
#[must_use]
pub fn build_responses_input(messages: &[Value]) -> (Option<String>, Vec<Value>) {
agent_harness::render::responses_wire_messages(messages)
}
/// A successfully decoded, **completed** Responses turn. Produced ONLY when the
/// body carries affirmative evidence of success (assistant text and/or tool
/// calls) and no terminal error. The `echo` items stay as raw [`Value`]s because
/// the agentic loop replays them **verbatim** in the next request's `input` (the
/// Responses API requires the exact `function_call` + preceding `reasoning`).
#[derive(Debug, Clone)]
pub struct DecodedResponse {
/// Concatenated assistant text: every `message` item's `output_text` part,
/// with a flat top-level `output_text` fallback.
pub text: String,
/// Raw `function_call` output items, in output order — the tool calls the
/// model requested.
pub tool_calls: Vec<Value>,
/// Raw items to ECHO back with the tool calls, in output order: every
/// `function_call` AND the `reasoning` items that precede them.
pub echo: Vec<Value>,
/// The model id the server reports (`model`), if present.
pub model: Option<String>,
/// Token usage (`input_tokens`/`output_tokens`), if present.
pub usage: Option<crate::TokenUsage>,
}
/// Why a Responses body is NOT a usable completed turn. Kept distinct so a caller
/// cannot collapse a failure, a refusal, or a malformed body into an empty
/// "success". A `200 OK` transport status only means the request was accepted;
/// [`decode_response`] returns `Ok` ONLY with affirmative success output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponseDecodeError {
/// The model explicitly declined (a `refusal` content part) with no tool
/// calls. Represented separately from a provider/turn failure — the caller
/// returns it as the turn's answer. `message` carries the refusal text (plus
/// any accompanying assistant text).
Refused {
message: String,
usage: Option<crate::TokenUsage>,
},
/// The model BOTH refused AND requested tool calls — an undefined mixed
/// state. Newt has no partial-refusal policy, so it rejects the WHOLE turn
/// rather than silently drop the decline and execute the calls. Never leads
/// to a side effect.
MixedRefusalAndToolCalls { refusal: String },
/// A top-level `error` object/string is present. This ALWAYS wins, regardless
/// of `status` — a status-less error body is still a failure, not a success.
ProviderError(String),
/// `status == "failed"` (with no top-level error object).
Failed(String),
/// `status == "incomplete"` — the turn was truncated (e.g. `max_output_tokens`).
Incomplete { reason: Option<String> },
/// `status` present but not a recognized terminal value (`in_progress`,
/// `cancelled`, or anything unknown).
NonTerminal(String),
/// No terminal error and a completed/absent status, but NO affirmative
/// success output (no text, no tool calls, no recognized output item). A
/// `2xx` body with nothing usable is malformed, not an empty success.
Malformed(String),
}
impl std::fmt::Display for ResponseDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Refused { message, .. } => write!(f, "the model refused the request: {message}"),
Self::MixedRefusalAndToolCalls { refusal } => write!(
f,
"the model both refused and requested tool calls — refusing the whole turn: {refusal}"
),
Self::ProviderError(m) => write!(f, "the Responses provider reported an error: {m}"),
Self::Failed(m) => write!(f, "Responses turn failed: {m}"),
Self::Incomplete { reason } => write!(
f,
"Responses turn did not complete ({})",
reason.as_deref().unwrap_or("incomplete")
),
Self::NonTerminal(s) => {
write!(f, "Responses turn ended with non-terminal status {s:?}")
}
Self::Malformed(m) => write!(f, "malformed Responses body: {m}"),
}
}
}
impl std::error::Error for ResponseDecodeError {}
/// Decode a Responses-API JSON body into a **completed** [`DecodedResponse`], or a
/// typed [`ResponseDecodeError`] for every body that is not a trustworthy success.
///
/// Fail-CLOSED. In order: a top-level `error` always wins; an explicit `failed`
/// / non-terminal status is an error; a truncated (`incomplete`) turn is an
/// error; a `refusal`-only body is [`Refused`](ResponseDecodeError::Refused); a
/// completed-or-absent status is `Ok` ONLY if it carries affirmative output
/// (text or tool calls) — an empty or shape-unrecognized body is
/// [`Malformed`](ResponseDecodeError::Malformed), never an empty success. A
/// missing `status` (lenient/older servers) is accepted only with such output.
pub fn decode_response(json: &Value) -> Result<DecodedResponse, ResponseDecodeError> {
let usage = decode_usage(&json["usage"]);
let model = json["model"].as_str().map(str::to_string);
// 1. A top-level error ALWAYS wins, whatever the status says.
if let Some(message) = top_level_error(json) {
return Err(ResponseDecodeError::ProviderError(message));
}
// 2. Explicit terminal / non-terminal statuses.
match json["status"].as_str() {
Some("failed") => {
return Err(ResponseDecodeError::Failed(
"the provider reported status \"failed\" with no error detail".to_string(),
));
}
Some("incomplete") => {
return Err(ResponseDecodeError::Incomplete {
reason: json["incomplete_details"]["reason"]
.as_str()
.map(str::to_string),
});
}
// Only `completed` / absent may proceed to the success check below.
Some(other) if other != "completed" => {
return Err(ResponseDecodeError::NonTerminal(other.to_string()));
}
_ => {}
}
// 3. Parse the output; a refusal is classified explicitly BEFORE any output
// is accepted, so a decline is never silently dropped.
let (text, tool_calls, echo, refusal) = decode_output(json);
if let Some(refusal) = refusal {
// A refusal alongside tool calls is an undefined mixed state — reject the
// WHOLE turn rather than drop the decline and let the loop execute the
// calls (there is no partial-refusal policy).
if !tool_calls.is_empty() {
return Err(ResponseDecodeError::MixedRefusalAndToolCalls { refusal });
}
// Refusal-only, or refusal + accompanying assistant text (no calls): the
// decline IS the turn's answer, with the text appended for context.
let message = if text.is_empty() {
refusal
} else {
format!("{refusal}\n\n{text}")
};
return Err(ResponseDecodeError::Refused { message, usage });
}
// 4. A completed/absent status REQUIRES affirmative success output.
let has_output = !text.is_empty() || !tool_calls.is_empty();
if !has_output {
return Err(ResponseDecodeError::Malformed(
"no assistant text, tool calls, or recognized output".to_string(),
));
}
Ok(DecodedResponse {
text,
tool_calls,
echo,
model,
usage,
})
}
/// A top-level `error` (object with `message`, or a bare string). Returns `None`
/// only when `error` is absent/null.
fn top_level_error(json: &Value) -> Option<String> {
match &json["error"] {
Value::Null => None,
Value::String(s) => Some(s.clone()),
Value::Object(o) => Some(
o.get("message")
.and_then(Value::as_str)
.unwrap_or("the provider returned an error object with no message")
.to_string(),
),
other => Some(other.to_string()),
}
}
/// Extract `(text, tool_calls, echo, refusal)`. `refusal` is `Some` when a
/// `message` item carries a `refusal` content part (the model declining).
fn decode_output(json: &Value) -> (String, Vec<Value>, Vec<Value>, Option<String>) {
let mut text = String::new();
let mut tool_calls = Vec::new();
let mut echo = Vec::new();
let mut refusal = String::new();
let mut saw_refusal = false;
if let Some(items) = json["output"].as_array() {
for item in items {
match item["type"].as_str() {
Some("message") => {
if let Some(parts) = item["content"].as_array() {
for p in parts {
// A `refusal` part carries a `refusal` field; an
// `output_text` part carries `text`. Capture both.
if p["type"] == "refusal" {
// A refusal-typed part DECLINES the turn by its
// presence — record that independently of the text,
// so a degenerate refusal with an empty/absent
// `refusal` field cannot fail open (#1526 review).
saw_refusal = true;
if let Some(r) = p["refusal"].as_str() {
refusal.push_str(r);
}
} else if let Some(t) = p["text"].as_str() {
text.push_str(t);
}
}
}
}
Some("function_call") => {
tool_calls.push(item.clone());
echo.push(item.clone());
}
// Reasoning items (`rs_…`) carry the chain that produced the
// following function_call; the Responses API requires them echoed
// back alongside the call, so preserve them in output order.
Some("reasoning") => echo.push(item.clone()),
_ => {}
}
}
}
if text.is_empty() {
if let Some(t) = json["output_text"].as_str() {
text.push_str(t);
}
}
// Key the refusal on the PRESENCE of a refusal-typed part, not on non-empty
// text: a refusal with an empty/absent message still declines the turn and
// must not fail open into executing tool calls (#1526 review). Supply a
// default message when the model refused without text.
let refusal = saw_refusal.then(|| {
if refusal.is_empty() {
"(the model refused without a message)".to_string()
} else {
refusal
}
});
(text, tool_calls, echo, refusal)
}
/// Responses API usage (`input_tokens`/`output_tokens`), accepting the Chat
/// Completions names (`prompt_tokens`/`completion_tokens`) from lenient servers.
/// Pass the `usage` sub-object, not the whole body.
pub(crate) fn decode_usage(usage: &Value) -> Option<crate::TokenUsage> {
let input = usage["input_tokens"]
.as_u64()
.or_else(|| usage["prompt_tokens"].as_u64())
.map(|n| n as u32);
let output = usage["output_tokens"]
.as_u64()
.or_else(|| usage["completion_tokens"].as_u64())
.map(|n| n as u32);
input.zip(output).map(|(i, o)| crate::TokenUsage {
input_tokens: i,
output_tokens: o,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn build_input_splits_system_to_instructions_and_passes_typed_items() {
let msgs = vec![
json!({"role": "system", "content": "be terse"}),
json!({"role": "user", "content": "hi"}),
json!({"role": "assistant", "content": "hello"}),
// an already-typed Responses item passes through untouched, in order
json!({"type": "function_call_output", "call_id": "c1", "output": "ok"}),
];
let (instructions, input) = build_responses_input(&msgs);
assert_eq!(instructions.as_deref(), Some("be terse"));
assert_eq!(input.len(), 3);
assert_eq!(input[0]["role"], "user");
assert_eq!(input[0]["content"], "hi");
assert_eq!(input[2]["type"], "function_call_output");
}
#[test]
fn build_input_joins_multiple_system_messages_and_treats_developer_as_system() {
let msgs = vec![
json!({"role": "system", "content": "one"}),
json!({"role": "developer", "content": "two"}),
json!({"role": "user", "content": "go"}),
];
let (instructions, input) = build_responses_input(&msgs);
// system + developer concatenate into instructions, joined with a blank line.
assert_eq!(instructions.as_deref(), Some("one\n\ntwo"));
assert_eq!(input.len(), 1);
assert_eq!(input[0]["role"], "user");
}
#[test]
fn build_input_has_no_instructions_when_no_system_message() {
let (instructions, input) =
build_responses_input(&[json!({"role": "user", "content": "x"})]);
assert_eq!(instructions, None);
assert_eq!(input.len(), 1);
}
#[test]
fn completed_message_extracts_text_and_usage() {
let body = json!({
"status": "completed",
"model": "gpt-5.6-sol",
"output": [{
"type": "message",
"content": [{"type": "output_text", "text": "the answer"}],
}],
"usage": {"input_tokens": 11, "output_tokens": 7},
});
let d = decode_response(&body).expect("completed success");
assert_eq!(d.text, "the answer");
assert_eq!(d.model.as_deref(), Some("gpt-5.6-sol"));
assert_eq!(d.usage.unwrap().input_tokens, 11);
assert!(d.tool_calls.is_empty());
}
#[test]
fn missing_status_with_output_text_succeeds() {
// Legacy/lenient compatibility: absent status + affirmative output is Ok.
let d = decode_response(&json!({"output_text": "ok"})).expect("legacy success");
assert_eq!(d.text, "ok");
}
#[test]
fn missing_status_with_structured_message_succeeds() {
let d = decode_response(&json!({
"output": [{"type": "message", "content": [{"type": "output_text", "text": "hi"}]}]
}))
.expect("structured success");
assert_eq!(d.text, "hi");
}
#[test]
fn missing_status_with_top_level_error_fails() {
let err = decode_response(&json!({"error": {"message": "provider failure"}}))
.expect_err("a status-less error body is a failure, not empty success");
assert!(matches!(err, ResponseDecodeError::ProviderError(m) if m == "provider failure"));
}
#[test]
fn top_level_error_wins_over_a_completed_status() {
// Even a `completed` status with output must fail if `error` is present.
let err = decode_response(&json!({
"status": "completed",
"error": {"message": "late failure"},
"output": [{"type": "message", "content": [{"type": "output_text", "text": "x"}]}]
}))
.expect_err("top-level error always wins");
assert!(matches!(err, ResponseDecodeError::ProviderError(_)));
}
#[test]
fn empty_object_body_is_malformed() {
assert!(matches!(
decode_response(&json!({})),
Err(ResponseDecodeError::Malformed(_))
));
}
#[test]
fn completed_but_empty_body_is_malformed_not_empty_success() {
assert!(matches!(
decode_response(&json!({"status": "completed", "output": []})),
Err(ResponseDecodeError::Malformed(_))
));
}
#[test]
fn refusal_only_response_returns_refused() {
let body = json!({
"output": [{
"type": "message",
"content": [{"type": "refusal", "refusal": "I cannot comply"}]
}],
"usage": {"input_tokens": 4, "output_tokens": 6}
});
match decode_response(&body) {
Err(ResponseDecodeError::Refused { message, usage }) => {
assert_eq!(message, "I cannot comply");
assert_eq!(usage.unwrap().output_tokens, 6);
}
other => panic!("expected Refused, got {other:?}"),
}
}
#[test]
fn refusal_with_text_returns_refused_including_the_text() {
let body = json!({
"output": [{
"type": "message",
"content": [
{"type": "refusal", "refusal": "I won't run that"},
{"type": "output_text", "text": "but here is why"}
]
}]
});
match decode_response(&body) {
Err(ResponseDecodeError::Refused { message, .. }) => {
assert!(message.contains("I won't run that"), "got: {message}");
assert!(
message.contains("but here is why"),
"text preserved: {message}"
);
}
other => panic!("expected Refused, got {other:?}"),
}
}
#[test]
fn refusal_with_a_tool_call_is_mixed_and_rejected_never_executed() {
// The dangerous case: a decline AND a function call. The refusal must not
// be dropped and the call must not execute — the whole turn is rejected.
let body = json!({
"output": [
{"type": "message",
"content": [{"type": "refusal", "refusal": "I cannot perform that operation"}]},
{"type": "function_call", "call_id": "call_1", "name": "run_command",
"arguments": "{\"command\":\"rm -rf /\"}"}
]
});
match decode_response(&body) {
Err(ResponseDecodeError::MixedRefusalAndToolCalls { refusal }) => {
assert_eq!(refusal, "I cannot perform that operation");
}
other => panic!("expected MixedRefusalAndToolCalls, got {other:?}"),
}
}
#[test]
fn refusal_with_multiple_tool_calls_is_mixed_and_rejected() {
let body = json!({
"output": [
{"type": "message",
"content": [{"type": "refusal", "refusal": "no"}]},
{"type": "function_call", "call_id": "c1", "name": "a", "arguments": "{}"},
{"type": "function_call", "call_id": "c2", "name": "b", "arguments": "{}"}
]
});
assert!(matches!(
decode_response(&body),
Err(ResponseDecodeError::MixedRefusalAndToolCalls { .. })
));
}
#[test]
fn status_less_mixed_refusal_and_tool_call_is_still_rejected() {
// No `status` (lenient server) must NOT weaken the mixed-state rejection.
let body = json!({
"output": [
{"type": "message",
"content": [{"type": "refusal", "refusal": "declined"}]},
{"type": "function_call", "call_id": "c1", "name": "a", "arguments": "{}"}
]
});
assert!(matches!(
decode_response(&body),
Err(ResponseDecodeError::MixedRefusalAndToolCalls { .. })
));
}
#[test]
fn empty_text_refusal_part_with_a_tool_call_is_still_mixed_and_rejected() {
// #1526 review: a refusal-typed part with an EMPTY/absent `refusal` field
// still declines the turn — it must NOT fail open and execute the call.
let body = json!({
"output": [
{"type": "message", "content": [{"type": "refusal", "refusal": ""}]},
{"type": "function_call", "call_id": "c1", "name": "a", "arguments": "{}"}
]
});
assert!(matches!(
decode_response(&body),
Err(ResponseDecodeError::MixedRefusalAndToolCalls { .. })
));
}
#[test]
fn refusal_part_with_no_refusal_field_still_declines() {
// A refusal part missing the `refusal` field entirely is still a decline
// (Refused, not empty-success), with a default message.
let body = json!({
"output": [{"type": "message", "content": [{"type": "refusal"}]}]
});
assert!(matches!(
decode_response(&body),
Err(ResponseDecodeError::Refused { .. })
));
}
#[test]
fn incomplete_status_is_an_error_carrying_the_reason() {
let body = json!({
"status": "incomplete",
"incomplete_details": {"reason": "max_output_tokens"},
"output": [{"type": "message",
"content": [{"type": "output_text", "text": "partial…"}]}],
});
assert!(matches!(
decode_response(&body),
Err(ResponseDecodeError::Incomplete { reason: Some(r) }) if r == "max_output_tokens"
));
}
#[test]
fn failed_status_is_an_error() {
let err = decode_response(&json!({"status": "failed"}))
.expect_err("failed status must be an error");
assert!(matches!(err, ResponseDecodeError::Failed(_)));
}
#[test]
fn unknown_status_is_non_terminal_error() {
assert!(matches!(
decode_response(&json!({"status": "in_progress"})),
Err(ResponseDecodeError::NonTerminal(s)) if s == "in_progress"
));
}
#[test]
fn unknown_output_item_with_no_usable_content_is_malformed() {
// A shape we don't recognize, no text, no calls → malformed, not success.
assert!(matches!(
decode_response(&json!({
"status": "completed",
"output": [{"type": "web_search_call", "id": "ws_1"}]
})),
Err(ResponseDecodeError::Malformed(_))
));
}
#[test]
fn function_calls_and_reasoning_are_extracted_in_order() {
let call = json!({
"type": "function_call",
"name": "write_file",
"arguments": "{\"path\":\"a\"}",
"call_id": "call_1",
});
let reasoning = json!({"type": "reasoning", "id": "rs_1", "summary": []});
let body = json!({
"status": "completed",
"output": [reasoning.clone(), call.clone()],
});
let d = decode_response(&body).expect("tool-call turn is a success");
// tool_calls is just the function_call; echo preserves reasoning+call
// in output order so the loop can replay them verbatim.
assert_eq!(d.tool_calls, vec![call.clone()]);
assert_eq!(d.echo, vec![reasoning, call]);
assert_eq!(d.text, "");
}
#[test]
fn chat_completions_usage_names_are_accepted() {
let d = decode_response(&json!({
"status": "completed",
"output_text": "hi",
"usage": {"prompt_tokens": 3, "completion_tokens": 5},
}))
.expect("success");
let u = d.usage.expect("usage");
assert_eq!((u.input_tokens, u.output_tokens), (3, 5));
}
}