Files
buzz/crates/buzz-agent/src/llm_fqn_tests.rs
Wes 0cc63fe3e6 fix(agent): preserve Databricks Opus UC reasoning and tool continuation (#7840)
## Summary
Fix `reasoning_effort: Extra inputs are not permitted` for Databricks v2
model `data_workflow_tools.goose.goose-claude-opus-5-5` without dropping
explicit High.

- Add one provider-qualified exact capability record: Anthropic
Messages, adaptive thinking, `output_config.effort`, medium upstream
default. Mirror exact-record precedence in Rust/TS; keep uncurated UC
services neutral and existing GPT routing unchanged.
- Preserve ordered native content, including signed/empty thinking and
redacted blocks, across tool results. Do not cache-stamp thinking blocks
or leak native state into Chat reasoning details. Discard truncated
native state; reject more than 64 native calls before any tool executes
instead of altering signed content.
- Add production-dispatch/ACP regressions and six shared capability
vectors. No relay/ACP protocol, schema, settings, inheritance, retries,
or other harness changes.

## Validation
At final head `82f43f8d07bfcc3f4af492afc0a58a02f8e549c4`, clean tree:
- Rebuilt real `buzz-agent`, drove ACP with a harmless MCP fixture
against the actual service. Two POSTs to
`/ai-gateway/anthropic/v1/messages`, both HTTP 200, exact FQN, High via
`output_config`, adaptive/summarized thinking, no `reasoning_effort`.
First response contained signed thinking + tool use; second request
preserved signature/content order and paired result; final ACP stop
`end_turn`. No credentials, signature contents, or conversation data
included here.
- Mandatory pre-commit/pre-push checks passed: formatting/lint,
file-size gate, Rust test lanes, desktop typechecking and full JS tests,
Tauri clippy/tests.
- Full `cargo test -p buzz-agent` passed on the pre-commit tree (only
hook formatting followed), including the new native truncation/65-call
ACP regressions, ordered interleaved signed/redacted two-tool capture,
nearby-ID negatives, and existing provider regressions.
- Independent API/replay, regression-boundary, and Rust/TS capability
reviews cleared.

Full `just ci`, packaging and manual desktop UI testing were not run
locally; repository CI remains the merge gate. Cross-harness internals
and other UC service contracts are outside scope.

## References
- [Databricks model
services](https://docs.databricks.com/aws/en/ai-gateway/query-model-services)
- [Anthropic thinking
preservation](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model)
- [Anthropic
effort](https://platform.claude.com/docs/en/build-with-claude/effort)
- Originating Buzz channel: `fccdea96-0f31-4343-ba6a-324214dc8d73`
- Originating thread:
buzz://message?channel=fccdea96-0f31-4343-ba6a-324214dc8d73&id=33f242923a317e269c1ff7b960cacd6ec1f3371eff2f97592857804c50fb100c

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
2026-09-23 09:44:13 -06:00

166 lines
7.8 KiB
Rust

// Included in llm::tests to reuse the production-path HTTP capture fixture.
#[tokio::test]
async fn gpt_fqn_completion_and_summary_use_responses() {
let response = json!({"status":"completed", "output":[{
"type":"message", "content":[{"type":"output_text", "text":"ok"}]
}]});
let (base_url, captured) = spawn_sequence_stub(vec![
StubHttpResponse::ok(response.clone()),
StubHttpResponse::ok(response),
])
.await;
let mut config = cfg(Provider::DatabricksV2);
config.base_url = base_url;
config.thinking_effort = Some(ThinkingEffort::High);
let model = "catalog.schema.goose-gpt-6-astra";
let llm = Llm::new(&config).unwrap();
let tools = vec![ToolDef {
name: "test_tool".into(),
description: "Test".into(),
input_schema: json!({"type":"object", "properties":{}}),
}];
let result = llm
.complete(
&config,
"system",
&[HistoryItem::User("hello".into())],
&tools,
model,
)
.await
.unwrap();
assert_eq!(result.text, "ok");
assert_eq!(
llm.summarize(&config, "system", "history", 128, model)
.await
.unwrap(),
"ok"
);
let requests = captured.lock().await;
let posts: Vec<_> = requests.iter().filter(|r| r.method == "POST").collect();
assert_eq!(posts.len(), 2);
for request in &posts {
assert_eq!(request.path, "/v1/ai-gateway/openai/v1/responses");
let body = request.body.as_ref().unwrap();
assert_eq!(body["model"], model);
assert!(body.get("input").is_some());
assert!(body.get("messages").is_none());
assert!(body.get("reasoning_effort").is_none());
}
let completion = posts[0].body.as_ref().unwrap();
assert!(completion["input"].is_array());
assert_eq!(completion["reasoning"]["effort"], "high");
assert_eq!(completion["tools"][0]["type"], "function");
assert_eq!(completion["tools"][0]["name"], "test_tool");
assert_eq!(posts[1].body.as_ref().unwrap()["max_output_tokens"], 128);
}
const OPUS_UC: &str = "data_workflow_tools.goose.goose-claude-opus-5-5";
#[tokio::test]
async fn opus_uc_replays_ordered_signed_content_with_high_effort() {
// Empty thinking can still carry a signature; redacted data is opaque.
let blocks = json!([
{"type":"thinking", "thinking":"", "signature":"signed-empty"},
{"type":"tool_use", "id":"a", "name":"test_tool", "input":{"n":1}},
{"type":"redacted_thinking", "data":"opaque-redacted"},
{"type":"tool_use", "id":"b", "name":"test_tool", "input":{"n":2}},
{"type":"thinking", "thinking":"checking", "signature":"signed-check"},
{"type":"text", "text":"running both"}
]);
let done = json!({"stop_reason":"end_turn", "content":[{"type":"text","text":"done"}]});
let (base_url, captured) = spawn_sequence_stub(vec![
StubHttpResponse::ok(json!({"stop_reason":"tool_use", "content":blocks})),
StubHttpResponse::ok(done.clone()),
StubHttpResponse::ok(done),
]).await;
let mut config = cfg(Provider::DatabricksV2);
config.base_url = base_url;
config.thinking_effort = Some(ThinkingEffort::High);
config.prompt_caching = true;
let llm = Llm::new(&config).unwrap();
let tools = vec![ToolDef { name:"test_tool".into(), description:"Test".into(),
input_schema:json!({"type":"object", "properties":{"n":{"type":"integer"}}}) }];
let mut history = vec![HistoryItem::User("run both".into())];
let first = llm.complete(&config, "system", &history, &tools, OPUS_UC).await.unwrap();
assert_eq!(first.stop, ProviderStop::ToolUse);
assert_eq!(first.reasoning, "checking");
assert_eq!(first.tool_calls.len(), 2);
assert_eq!(first.tool_calls[1].arguments, json!({"n":2}));
history.push(HistoryItem::Assistant { text:first.text, tool_calls:first.tool_calls,
reasoning_details:first.reasoning_details });
for id in ["a", "b"] {
history.push(HistoryItem::ToolResult(crate::types::ToolResult {
provider_id:id.into(), content:vec![ToolResultContent::Text(format!("result-{id}"))], is_error:false,
}));
}
let second = llm.complete(&config, "system", &history, &tools, OPUS_UC).await.unwrap();
assert_eq!(second.stop, ProviderStop::EndTurn);
assert_eq!(second.text, "done");
assert_eq!(llm.summarize(&config, "system", "history", 128, OPUS_UC).await.unwrap(), "done");
let requests = captured.lock().await;
let posts: Vec<_> = requests.iter().filter(|r| r.method == "POST").collect();
assert_eq!(posts.len(), 3);
for post in &posts {
assert_eq!(post.path, "/v1/ai-gateway/anthropic/v1/messages");
assert_eq!(post.body.as_ref().unwrap()["model"], OPUS_UC);
assert!(post.body.as_ref().unwrap().get("reasoning_effort").is_none());
}
for post in &posts[..2] {
let body = post.body.as_ref().unwrap();
assert_eq!(body["output_config"], json!({"effort":"high"}));
assert_eq!(body["thinking"], json!({"type":"adaptive", "display":"summarized"}));
assert_eq!(body["tools"][0]["input_schema"], tools[0].input_schema);
}
let body = posts[1].body.as_ref().unwrap();
let mut replay = body["messages"][1]["content"].clone();
assert_eq!(replay[5]["cache_control"], json!({"type":"ephemeral"}));
replay[5].as_object_mut().unwrap().remove("cache_control");
assert_eq!(replay, blocks);
let results = body["messages"][2]["content"].as_array().unwrap();
assert_eq!(results.len(), 2);
for (result, id) in results.iter().zip(["a", "b"]) {
assert_eq!(result["type"], "tool_result");
assert_eq!(result["tool_use_id"], id);
assert_eq!(result["content"][0]["text"], format!("result-{id}"));
}
}
#[tokio::test]
async fn neighboring_opus_uc_services_stay_on_mlflow_chat() {
for model in ["system.ai.claude-opus-5-5", "other.goose.goose-claude-opus-5-5",
"data_workflow_tools.goose.goose-claude-opus-5-5-preview"] {
let (base_url, captured) = spawn_sequence_stub(vec![StubHttpResponse::ok(json!({
"choices":[{"finish_reason":"stop", "message":{"content":"ok"}}]
}))]).await;
let mut config = cfg(Provider::DatabricksV2);
config.base_url = base_url;
config.thinking_effort = Some(ThinkingEffort::High);
Llm::new(&config).unwrap().complete(&config, "system", &[HistoryItem::User("hi".into())], &[], model).await.unwrap();
let requests = captured.lock().await;
let post = requests.iter().find(|r| r.method == "POST").unwrap();
assert_eq!(post.path, "/v1/ai-gateway/mlflow/v1/chat/completions");
let body = post.body.as_ref().unwrap();
assert_eq!(body["model"], model);
assert_eq!(body["reasoning_effort"], "high");
assert!(body.get("thinking").is_none());
}
}
#[test]
fn native_thinking_tail_is_not_cache_stamped_or_leaked_to_chat() {
for block in [json!({"type":"thinking", "thinking":"", "signature":"signed"}),
json!({"type":"redacted_thinking", "data":"opaque"})] {
let response = parse_anthropic(json!({"stop_reason":"end_turn", "content":[block]})).unwrap();
let history = vec![HistoryItem::Assistant { text:response.text, tool_calls:response.tool_calls,
reasoning_details:response.reasoning_details }];
let mut config = cfg(Provider::DatabricksV2);
config.prompt_caching = true;
let native = anthropic_body(&config, "system", &history, &[], OPUS_UC, Some(ThinkingEffort::High), "databricks_v2");
assert_eq!(native["messages"][0]["content"], json!([block]));
let chat = openai_body(&config, "system", &history, &[], "other", None);
assert!(chat["messages"][1].get("reasoning_details").is_none());
assert!(!responses_body(&config, "system", &history, &[], "other", None).to_string().contains("anthropic_content"));
}
}