fix(databricks): route tool reasoning requests to responses api

Use the native Responses API payload and stream parser for Databricks Responses-family requests that include function tools and an explicit reasoning effort. Keep the existing Chat Completions-shaped compatibility payload for other Responses-family requests and leave non-Responses models on the existing invocation path.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-05-15 10:52:45 +10:00
parent e7cc82abf5
commit 5f59cc405f
+119 -6
View File
@@ -18,7 +18,7 @@ use super::formats::openai_responses::create_responses_request;
use super::oauth;
use super::openai_compatible::{
handle_response_openai_compat, handle_status, map_http_error_to_provider_error,
stream_openai_compat,
stream_openai_compat, stream_responses_compat,
};
use super::retry::ProviderRetry;
use super::utils::{ImageFormat, RequestLog};
@@ -382,8 +382,8 @@ impl Provider for DatabricksProvider {
let client_request_id = self.build_client_request_id(session_id);
if Self::is_responses_model(&model_config.model_name) {
let mut payload = create_responses_request(model_config, system, messages, tools)?;
adapt_responses_payload_for_databricks(&mut payload);
let (mut payload, protocol) =
create_responses_payload_for_databricks(model_config, system, messages, tools)?;
payload["stream"] = Value::Bool(true);
if let Some(ref client_request_id) = client_request_id {
payload["client_request_id"] = Value::String(client_request_id.clone());
@@ -405,7 +405,10 @@ impl Provider for DatabricksProvider {
let _ = log.error(e);
})?;
stream_openai_compat(response, log)
match protocol {
DatabricksResponsesProtocol::ChatCompletions => stream_openai_compat(response, log),
DatabricksResponsesProtocol::ResponsesApi => stream_responses_compat(response, log),
}
} else {
let mut payload =
create_request(model_config, system, messages, tools, &self.image_format)?;
@@ -586,9 +589,43 @@ impl EmbeddingCapable for DatabricksProvider {
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum DatabricksResponsesProtocol {
ChatCompletions,
ResponsesApi,
}
fn responses_protocol_for_databricks(
model_name: &str,
tools: &[Tool],
) -> DatabricksResponsesProtocol {
let (_model_name, reasoning_effort) = super::utils::extract_reasoning_effort(model_name);
if !tools.is_empty() && reasoning_effort.is_some() {
DatabricksResponsesProtocol::ResponsesApi
} else {
DatabricksResponsesProtocol::ChatCompletions
}
}
fn create_responses_payload_for_databricks(
model_config: &ModelConfig,
system: &str,
messages: &[Message],
tools: &[Tool],
) -> Result<(Value, DatabricksResponsesProtocol)> {
let protocol = responses_protocol_for_databricks(&model_config.model_name, tools);
let mut payload = create_responses_request(model_config, system, messages, tools)?;
if protocol == DatabricksResponsesProtocol::ChatCompletions {
adapt_responses_payload_for_databricks(&mut payload);
}
Ok((payload, protocol))
}
/// Adapt a payload produced by `create_responses_request` (OpenAI Responses API format)
/// into the Chat Completions format that Databricks' `serving-endpoints/responses`
/// endpoint actually expects.
/// into the Chat Completions format accepted by Databricks' `serving-endpoints/responses`
/// route for Responses-family models without the `tools + reasoning_effort` combination.
fn adapt_responses_payload_for_databricks(payload: &mut Value) {
let obj = match payload.as_object_mut() {
Some(o) => o,
@@ -733,6 +770,7 @@ fn convert_responses_input_to_messages(items: &[Value]) -> Vec<Value> {
#[cfg(test)]
mod tests {
use super::*;
use rmcp::object;
fn test_provider() -> DatabricksProvider {
DatabricksProvider {
@@ -876,4 +914,79 @@ mod tests {
assert!(tools[0].get("name").is_none());
assert!(tools[0].get("strict").is_none());
}
#[test]
fn responses_protocol_uses_native_responses_for_tools_with_reasoning_effort() {
let tool = Tool::new(
"my_tool",
"A tool",
object!({
"type": "object",
"properties": {}
}),
);
assert_eq!(
responses_protocol_for_databricks(
"databricks-gpt-5.5-high",
std::slice::from_ref(&tool)
),
DatabricksResponsesProtocol::ResponsesApi
);
assert_eq!(
responses_protocol_for_databricks("databricks-gpt-5.5", std::slice::from_ref(&tool)),
DatabricksResponsesProtocol::ChatCompletions
);
assert_eq!(
responses_protocol_for_databricks("databricks-gpt-5.5-high", &[]),
DatabricksResponsesProtocol::ChatCompletions
);
}
#[test]
fn responses_payload_keeps_native_format_for_tools_with_reasoning_effort() {
let model_config = ModelConfig {
model_name: "databricks-gpt-5.5-high".to_string(),
context_limit: None,
temperature: None,
max_tokens: Some(4096),
toolshim: false,
toolshim_model: None,
fast_model_config: None,
request_params: None,
reasoning: None,
};
let tool = Tool::new(
"my_tool",
"A tool",
object!({
"type": "object",
"properties": {}
}),
);
let (payload, protocol) = create_responses_payload_for_databricks(
&model_config,
"You are helpful.",
&[],
&[tool],
)
.unwrap();
assert_eq!(protocol, DatabricksResponsesProtocol::ResponsesApi);
assert_eq!(payload["model"], "databricks-gpt-5.5");
assert!(payload.get("input").is_some());
assert!(payload.get("messages").is_none());
assert_eq!(payload["reasoning"]["effort"], "high");
assert_eq!(payload["reasoning"]["summary"], "auto");
assert!(payload.get("reasoning_effort").is_none());
assert_eq!(payload["max_output_tokens"], 4096);
assert!(payload.get("max_completion_tokens").is_none());
let tools = payload["tools"].as_array().unwrap();
assert_eq!(tools[0]["type"], "function");
assert_eq!(tools[0]["name"], "my_tool");
assert_eq!(tools[0]["strict"], false);
assert!(tools[0].get("function").is_none());
}
}