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
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: Lowers an OpenAI Responses-API request into a `ChatCompletionRequest`.
//!
//! Owner: server (OpenAI API layer).
//! Invariants: none beyond the types.
use super::*;
#[derive(Debug)]
pub enum LowerResponsesError {
/// 2026-09-26: The request cannot be lowered: `input` is neither a string nor an array,
/// a tool definition or `tool_choice` does not parse, or a tool type is a built-in
/// hosted tool or unknown.
BadRequest(String),
/// 2026-09-26: `previous_response_id` was set but the resolver returned nothing. The
/// Responses handler answers 400 with `param=previous_response_id` and
/// `code=response_not_found`.
PriorNotFound(String),
}
impl LowerResponsesError {
pub fn message(&self) -> &str {
match self {
Self::BadRequest(m) | Self::PriorNotFound(m) => m.as_str(),
}
}
}
/// 2026-09-26: Lower an OpenAI Responses-API request into a `ChatCompletionRequest` for
/// the chat-completions pipeline.
///
/// When `previous_response_id` is set, `resolve_prior` is called with it (the Responses
/// handler passes a closure over [`crate::response_store::ResponseStore`]) and the
/// returned transcript goes before the current input. `None` from the resolver yields
/// [`LowerResponsesError::PriorNotFound`].
pub fn lower_responses_to_chat(
r: ResponsesRequest,
resolve_prior: impl FnOnce(&str) -> Option<Vec<IncomingMessage>>,
) -> Result<ChatCompletionRequest, LowerResponsesError> {
let mut messages: Vec<IncomingMessage> = Vec::new();
if let Some(prior_id) = r.previous_response_id.as_deref() {
match resolve_prior(prior_id) {
Some(prior) => messages.extend(prior),
None => {
return Err(LowerResponsesError::PriorNotFound(format!(
"previous_response_id '{prior_id}' not found or expired"
)));
}
}
}
if let Some(instr) = r.instructions.clone() {
// 2026-09-26: `instructions` becomes a system message at index 0, ahead of the
// resumed transcript; system messages inside the transcript are kept.
messages.insert(0, IncomingMessage::synthetic_system(instr));
}
match &r.input {
serde_json::Value::String(s) => {
messages.push(IncomingMessage::synthetic_user_text(s.clone()));
}
serde_json::Value::Array(items) => {
for it in items {
if let Some(m) = IncomingMessage::from_responses_input_item(it) {
messages.push(m);
}
}
}
_ => {
return Err(LowerResponsesError::BadRequest(
"`input` must be a string or array of input items".into(),
));
}
}
// 2026-09-26: Function tools (type `function`, `namespace` or absent) are parsed into
// `ToolDefinition`; a built-in hosted tool type or any other type fails the request.
// A list that parses to no tools becomes `None`.
let tools: Option<Vec<crate::tool_parser::ToolDefinition>> = match r.tools {
None => None,
Some(list) => {
// 2026-09-26: Not pre-sized from `list.len()`: that count comes from the request
// body, and an allocation sized by it is an uncontrolled-allocation sink (CWE-789).
let mut parsed = Vec::new();
for raw in list {
let ty = raw.get("type").and_then(|v| v.as_str()).unwrap_or("");
match ty {
"function" | "" | "namespace" => {
// 2026-09-26: A tool without a nested `function` object is in the flat
// Responses form; its `name`, `description`, `parameters` and `strict`
// are moved into one so the chat-format `ToolDefinition` accepts it.
let normalized = if raw.get("function").is_some() {
raw
} else if let Some(obj) = raw.as_object() {
let mut function = serde_json::Map::new();
for key in ["name", "description", "parameters", "strict"] {
if let Some(v) = obj.get(key) {
function.insert(key.to_string(), v.clone());
}
}
serde_json::json!({
"type": "function",
"function": serde_json::Value::Object(function),
})
} else {
raw
};
match serde_json::from_value::<crate::tool_parser::ToolDefinition>(
normalized,
) {
Ok(td) => parsed.push(td),
Err(e) => {
return Err(LowerResponsesError::BadRequest(format!(
"invalid tool definition: {e}"
)));
}
}
}
builtin @ ("web_search"
| "web_search_preview"
| "file_search"
| "computer_use_preview"
| "code_interpreter"
| "image_generation"
| "mcp"
| "local_shell"
| "custom_tool") => {
return Err(LowerResponsesError::BadRequest(format!(
"built-in tool '{builtin}' is not supported by this server. Metrale Engine serves inference only and does not ship hosted tools (web search, file search, code interpreter, computer use, image generation, MCP). Provide your own `function`-type tools instead."
)));
}
other => {
return Err(LowerResponsesError::BadRequest(format!(
"unknown tool type '{other}'. Supported types: 'function'."
)));
}
}
}
if parsed.is_empty() {
None
} else {
Some(parsed)
}
}
};
Ok(ChatCompletionRequest {
model: r.model,
adapter: None,
src_lang: None,
tgt_lang: None,
num_beams: None,
length_penalty: None,
early_stopping: None,
messages,
max_tokens: r.max_output_tokens.unwrap_or_else(default_max_tokens),
temperature: r.temperature,
top_k: None,
top_p: r.top_p,
top_n_sigma: None,
min_p: None,
repetition_penalty: None,
presence_penalty: None,
frequency_penalty: None,
logit_bias: None,
stream: r.stream,
return_token_ids: false,
enable_thinking: None,
thinking: None,
thinking_token_budget: None,
repetition_detection: None,
reasoning: r.reasoning,
chat_template_kwargs: None,
tools,
tool_choice: match r.tool_choice {
None => None,
Some(raw) => {
let normalized = match raw.as_str() {
Some(_) => raw,
None => match raw.as_object() {
Some(obj) if obj.get("function").is_some() => raw,
Some(obj)
if obj.get("type").and_then(|v| v.as_str()) == Some("function")
&& obj.get("name").is_some() =>
{
let mut function = serde_json::Map::new();
function.insert(
"name".to_string(),
obj.get("name").cloned().unwrap_or(serde_json::Value::Null),
);
serde_json::json!({
"type": "function",
"function": serde_json::Value::Object(function),
})
}
_ => raw,
},
};
match serde_json::from_value::<crate::tool_parser::ToolChoice>(normalized) {
Ok(tc) => Some(tc),
Err(e) => {
return Err(LowerResponsesError::BadRequest(format!(
"invalid tool_choice: {e}"
)));
}
}
}
},
stop: Vec::new(),
response_format: None,
min_tokens: 0,
seed: None,
logprobs: None,
top_logprobs: None,
timeout: None,
n: 1,
stream_options: None,
parallel_tool_calls: None,
verbosity: None,
service_tier: r.service_tier,
store: r.store,
metadata: r.metadata,
safety_identifier: None,
prompt_cache_key: None,
user: None,
modalities: None,
audio: None,
prediction: None,
web_search_options: None,
reasoning_effort: None,
})
}