Skip to content

Commit b29c24b

Browse files
fix: forward Responses reasoning configuration (vllm-project#225)
1 parent b1dbce5 commit b29c24b

23 files changed

Lines changed: 3557 additions & 81 deletions

crates/agentic-server-core/benches/executor_throughput.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ fn make_request(input: &str, stream: bool, prev_id: Option<String>) -> RequestPa
140140
stream,
141141
store: true,
142142
include: None,
143+
reasoning: None,
143144
temperature: None,
144145
top_p: None,
145146
max_output_tokens: None,

crates/agentic-server-core/src/executor/compaction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ fn request_payload(model: String, input: ResponsesInput, instructions: Option<St
146146
stream: false,
147147
store: false,
148148
include: None,
149+
reasoning: None,
149150
temperature: None,
150151
top_p: None,
151152
max_output_tokens: None,

crates/agentic-server-core/src/executor/modes/conversation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ mod tests {
160160
stream: false,
161161
store: true,
162162
include: None,
163+
reasoning: None,
163164
temperature: None,
164165
top_p: None,
165166
max_output_tokens: None,

crates/agentic-server-core/src/executor/modes/response.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ mod tests {
116116
stream: false,
117117
store: true,
118118
include: None,
119+
reasoning: None,
119120
temperature: None,
120121
top_p: None,
121122
max_output_tokens: None,

crates/agentic-server-core/src/executor/rehydrate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ mod tests {
336336
stream: false,
337337
store: true,
338338
include: None,
339+
reasoning: None,
339340
temperature: None,
340341
top_p: None,
341342
max_output_tokens: None,

crates/agentic-server-core/src/executor/upstream.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ mod tests {
314314
stream: true,
315315
store: false,
316316
include: None,
317+
reasoning: None,
317318
temperature: None,
318319
top_p: None,
319320
max_output_tokens: None,

crates/agentic-server-core/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ pub use types::{
2525
FunctionToolResultMessage, GatewayCallStatus, IncompleteDetails, InputContent, InputFileContent,
2626
InputFunctionToolCall, InputImageContent, InputItem, InputMessage, InputMessageContent, InputTextContent,
2727
InputTokenDetails, McpCall, McpCallStatus, McpToolParam, NonEmptyToolName, OutputItem, OutputMessage,
28-
OutputTextContent, OutputTokenDetails, ReasoningOutput, ReasoningTextContent, RequestPayload, ResponsePayload,
29-
ResponseUsage, ResponsesInput, ResponsesTool, ToolCallOutput, ToolChoice, ToolOutputContent, UpstreamRequest,
30-
UpstreamTool, WebSearchAction, WebSearchActionFindInPage, WebSearchActionOpenPage, WebSearchActionSearch,
31-
WebSearchCall, WebSearchCallStatus, WebSearchContextSize, WebSearchFilters, WebSearchSource, WebSearchToolParam,
32-
WebSearchUserLocation,
28+
OutputTextContent, OutputTokenDetails, ReasoningConfig, ReasoningOutput, ReasoningTextContent, RequestPayload,
29+
ResponsePayload, ResponseUsage, ResponsesInput, ResponsesTool, ToolCallOutput, ToolChoice, ToolOutputContent,
30+
UpstreamRequest, UpstreamTool, WebSearchAction, WebSearchActionFindInPage, WebSearchActionOpenPage,
31+
WebSearchActionSearch, WebSearchCall, WebSearchCallStatus, WebSearchContextSize, WebSearchFilters, WebSearchSource,
32+
WebSearchToolParam, WebSearchUserLocation,
3333
};
3434
pub use utils::{utcnow_str, uuid7_str};

crates/agentic-server-core/src/types/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ pub use io::{
1515
WebSearchSource,
1616
};
1717
pub use request_response::{
18-
CompactRequest, CompactedResponse, ContextManagement, IncompleteDetails, RequestPayload, ResponsePayload,
19-
UpstreamRequest, UpstreamTool,
18+
CompactRequest, CompactedResponse, ContextManagement, IncompleteDetails, ReasoningConfig, RequestPayload,
19+
ResponsePayload, UpstreamRequest, UpstreamTool,
2020
};
2121
pub use tools::{
2222
CodeInterpreterToolParam, CodexNamespaceMember, CodexNamespaceToolParam, CustomToolParam, EmptyToolNameError,

crates/agentic-server-core/src/types/request_response.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ use super::tools::ResponsesTool;
1111
use crate::tool::{CodexNamespaceHandler, CustomHandler, ToolError};
1212
use crate::utils::common::serialize_to_string;
1313

14+
/// Standard Responses API reasoning generation settings.
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct ReasoningConfig {
17+
#[serde(default, skip_serializing_if = "Option::is_none")]
18+
pub context: Option<String>,
19+
#[serde(default, skip_serializing_if = "Option::is_none")]
20+
pub effort: Option<String>,
21+
#[serde(default, skip_serializing_if = "Option::is_none")]
22+
pub generate_summary: Option<String>,
23+
#[serde(default, skip_serializing_if = "Option::is_none")]
24+
pub mode: Option<String>,
25+
#[serde(default, skip_serializing_if = "Option::is_none")]
26+
pub summary: Option<String>,
27+
}
28+
1429
#[derive(Debug, Clone, Serialize, Deserialize)]
1530
pub struct RequestPayload {
1631
pub model: String,
@@ -26,6 +41,8 @@ pub struct RequestPayload {
2641
#[serde(default = "default_true")]
2742
pub store: bool,
2843
pub include: Option<Vec<String>>,
44+
#[serde(default, skip_serializing_if = "Option::is_none")]
45+
pub reasoning: Option<Box<ReasoningConfig>>,
2946
pub temperature: Option<f64>,
3047
pub top_p: Option<f64>,
3148
pub max_output_tokens: Option<u32>,
@@ -62,6 +79,8 @@ pub struct UpstreamRequest<'a> {
6279
#[serde(skip_serializing_if = "Option::is_none")]
6380
pub include: Option<&'a Vec<String>>,
6481
#[serde(skip_serializing_if = "Option::is_none")]
82+
pub reasoning: Option<&'a ReasoningConfig>,
83+
#[serde(skip_serializing_if = "Option::is_none")]
6584
pub temperature: Option<f64>,
6685
#[serde(skip_serializing_if = "Option::is_none")]
6786
pub top_p: Option<f64>,
@@ -73,6 +92,7 @@ pub struct UpstreamRequest<'a> {
7392
pub metadata: Option<&'a Value>,
7493
#[serde(skip_serializing_if = "Option::is_none")]
7594
pub parallel_tool_calls: Option<bool>,
95+
#[serde(skip_serializing_if = "Option::is_none")]
7696
pub cache_salt: Option<&'a str>,
7797
}
7898

@@ -155,6 +175,7 @@ impl RequestPayload {
155175
tools,
156176
tool_choice: Some(tool_choice),
157177
include: self.include.as_ref(),
178+
reasoning: self.reasoning.as_deref(),
158179
temperature: self.temperature,
159180
top_p: self.top_p,
160181
max_output_tokens: self.max_output_tokens,
@@ -329,7 +350,18 @@ mod tests {
329350
}
330351

331352
#[test]
332-
fn request_payload_forwards_cache_salt_upstream() {
353+
fn request_payload_omits_absent_and_forwards_present_cache_salt_upstream() {
354+
let payload: RequestPayload = serde_json::from_value(serde_json::json!({
355+
"model": "test-model",
356+
"input": "hello"
357+
}))
358+
.expect("request should deserialize");
359+
360+
let upstream = serde_json::to_value(payload.to_upstream_request(false).expect("request should normalize"))
361+
.expect("upstream request should serialize");
362+
363+
assert!(upstream.get("cache_salt").is_none());
364+
333365
let payload: RequestPayload = serde_json::from_value(serde_json::json!({
334366
"model": "test-model",
335367
"input": "hello",
@@ -343,6 +375,72 @@ mod tests {
343375
assert_eq!(upstream["cache_salt"], "tenant-a");
344376
}
345377

378+
#[test]
379+
fn request_payload_forwards_reasoning_configuration_upstream() {
380+
let reasoning = serde_json::json!({
381+
"context": "all_turns",
382+
"effort": "high",
383+
"generate_summary": "concise",
384+
"mode": "pro",
385+
"summary": "detailed"
386+
});
387+
let payload: RequestPayload = serde_json::from_value(serde_json::json!({
388+
"model": "test-model",
389+
"input": "hello",
390+
"reasoning": reasoning
391+
}))
392+
.expect("request should deserialize");
393+
394+
for stream in [false, true] {
395+
let upstream = serde_json::to_value(payload.to_upstream_request(stream).expect("request should normalize"))
396+
.expect("upstream request should serialize");
397+
398+
assert_eq!(upstream["reasoning"], reasoning);
399+
assert_eq!(upstream["stream"], stream);
400+
}
401+
}
402+
403+
#[test]
404+
fn request_payload_handles_reasoning_boundaries() {
405+
for reasoning in [serde_json::json!({}), serde_json::json!({"effort": "minimal"})] {
406+
let payload: RequestPayload = serde_json::from_value(serde_json::json!({
407+
"model": "test-model",
408+
"input": "hello",
409+
"reasoning": reasoning
410+
}))
411+
.expect("valid reasoning object should deserialize");
412+
let upstream = serde_json::to_value(payload.to_upstream_request(false).expect("request should normalize"))
413+
.expect("upstream request should serialize");
414+
415+
assert_eq!(upstream["reasoning"], reasoning);
416+
}
417+
418+
for reasoning in [
419+
serde_json::Value::Null,
420+
serde_json::json!("high"),
421+
serde_json::json!({"effort": 3}),
422+
] {
423+
let parsed = serde_json::from_value::<RequestPayload>(serde_json::json!({
424+
"model": "test-model",
425+
"input": "hello",
426+
"reasoning": reasoning
427+
}));
428+
429+
if reasoning.is_null() {
430+
let upstream = serde_json::to_value(
431+
parsed
432+
.expect("null should be treated as absent")
433+
.to_upstream_request(false)
434+
.expect("request should normalize"),
435+
)
436+
.expect("upstream request should serialize");
437+
assert!(upstream.get("reasoning").is_none());
438+
} else {
439+
assert!(parsed.is_err(), "non-object reasoning configuration should be rejected");
440+
}
441+
}
442+
}
443+
346444
#[test]
347445
fn request_payload_uses_option_tool_choice_for_missing_vs_explicit() {
348446
let absent: RequestPayload = serde_json::from_value(serde_json::json!({

crates/agentic-server-core/tests/cassettes/README.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ model requested by Codex 0.149.1.
8383
--openai URL OpenAI upstream (default https://api.openai.com)
8484
--tools FILE JSON file containing a tools array (responses mode only)
8585
--tool-choice VALUE "auto", "none", "required", or JSON e.g. '{"type":"function","name":"foo"}'
86+
--reasoning JSON JSON object containing Responses reasoning settings
8687
--input-file FILE JSON string or item array for one HTTP Responses turn
8788
--max-output-tokens N max_output_tokens for Responses requests (default 1024; use 0 to omit)
8889
--proxy-port PORT Local proxy port (default 7070)
@@ -192,7 +193,7 @@ turns:
192193
| Script | Cassettes | Backend |
193194
|--------|-----------|---------|
194195
| `record_text_only_cassettes.sh` | 10 text-only cassettes (responses + conv modes, streaming + non-streaming) | OpenAI (`OPENAI_API_KEY`) |
195-
| `record_reasoning_cassettes.sh` | 2 reasoning cassettes (single turn, streaming + non-streaming) | vLLM |
196+
| `record_reasoning_cassettes.sh` | Matching explicit-reasoning cassettes (streaming + non-streaming) | gateway and OpenAI reference; optional direct vLLM |
196197
| `record_tool_call_cassettes.sh` | 8 tool-call cassettes (4 tool_choice modes x streaming + non-streaming) | vLLM |
197198
| `record_codex_cli_tool_call_cassettes.sh` | Codex function/namespace/custom-tool matrix | gateway, vLLM, and OpenAI |
198199
| `record_custom_tool_cassettes.sh` | Matching two-turn custom-tool flows (streaming + non-streaming) | gateway and OpenAI reference |
@@ -207,12 +208,44 @@ OPENAI_API_KEY=sk-... bash tests/cassettes/record_text_only_cassettes.sh
207208
MODEL=gpt-4o-mini OPENAI_API_KEY=sk-... bash tests/cassettes/record_text_only_cassettes.sh
208209
```
209210

210-
### Reasoning (vLLM)
211+
### Reasoning (gateway and OpenAI)
212+
213+
The default records the same explicit `reasoning` object against OpenAI and the
214+
gateway for both response modes. The gateway fixture uses the same OpenAI model
215+
as its reference so the comparison isolates gateway request and response
216+
handling from model differences. Use `REASONING_RECORD_SET=gateway`,
217+
`REASONING_RECORD_SET=openai`, or `REASONING_RECORD_SET=vllm` to record one
218+
provider. The gateway recording requires a running gateway and reasoning-capable
219+
upstream; the optional direct-vLLM set retains the legacy accumulator workflow.
220+
Every selected recording is staged and validated before any final fixture is
221+
replaced, so a failed provider or response cannot leave a partially refreshed
222+
comparison set.
211223

212224
```bash
213-
vllm serve Qwen/Qwen3-30B-A3B-FP8 --reasoning-parser deepseek_r1 --port 5050 > server.log 2>&1
225+
# Start the gateway against the same OpenAI ground-truth model in one terminal.
226+
OPENAI_API_KEY=sk-... \
227+
cargo run -p agentic-server -- \
228+
--llm-api-base https://api.openai.com \
229+
--skip-llm-ready-check
214230
215-
VLLM_URL=http://0.0.0.0:5050 MODEL=Qwen/Qwen3-30B-A3B-FP8 bash tests/cassettes/record_reasoning_cassettes.sh
231+
# Record the OpenAI-reference and gateway pairs from another terminal.
232+
OPENAI_API_KEY=sk-... \
233+
GATEWAY_URL=http://localhost:9000 \
234+
MODEL=gpt-5.6 \
235+
bash crates/agentic-server-core/tests/cassettes/record_reasoning_cassettes.sh
236+
237+
# To refresh only the gateway-facing pair instead:
238+
REASONING_RECORD_SET=gateway \
239+
GATEWAY_URL=http://localhost:9000 \
240+
MODEL=gpt-5.6 \
241+
bash crates/agentic-server-core/tests/cassettes/record_reasoning_cassettes.sh
242+
243+
vllm serve Qwen/Qwen3-30B-A3B-FP8 --reasoning-parser qwen3 --port 5050 > server.log 2>&1
244+
245+
REASONING_RECORD_SET=vllm \
246+
VLLM_URL=http://0.0.0.0:5050 \
247+
MODEL=Qwen/Qwen3-30B-A3B-FP8 \
248+
bash crates/agentic-server-core/tests/cassettes/record_reasoning_cassettes.sh
216249
```
217250

218251
### Tool calls (vLLM)

0 commit comments

Comments
 (0)