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>
This commit is contained in:
Wes
2026-09-23 08:44:13 -07:00
committed by GitHub
parent b6a26556ec
commit 0cc63fe3e6
10 changed files with 444 additions and 19 deletions

View File

@@ -10,6 +10,7 @@ use crate::auth::{PkceOAuthConfig, PkceOAuthTokenSource, StaticTokenSource, Toke
use crate::config::{
is_openai_host, normalize_effort_for_anthropic_route, normalize_effort_for_databricks_v2,
normalize_effort_for_provider, Config, OpenAiApi, Provider, ThinkingEffort,
MAX_TOOL_CALLS_PER_TURN,
};
use crate::types::{
AgentError, HistoryItem, LlmResponse, ProviderStop, ToolCall, ToolDef, ToolResultContent,
@@ -588,9 +589,21 @@ fn anthropic_body(
HistoryItem::Assistant {
text,
tool_calls,
reasoning_details: _,
reasoning_details,
} => {
flush(&mut messages, &mut pending);
// Native Anthropic blocks carry signed, potentially interleaved
// thinking. Replay the original order, not flattened text/calls.
if let Some(blocks) = reasoning_details
.as_ref()
.and_then(|v| v.get("anthropic_content"))
.and_then(Value::as_array)
{
if !blocks.is_empty() {
messages.push(json!({ "role": "assistant", "content": blocks }));
}
continue;
}
let mut content: Vec<Value> = Vec::new();
if !text.is_empty() {
content.push(json!({ "type": "text", "text": text }));
@@ -688,6 +701,9 @@ fn stamp_rolling_cache_breakpoint(messages: &mut [Value]) {
.get_mut("content")
.and_then(Value::as_array_mut)
.and_then(|c| c.last_mut())
// Thinking blocks must remain byte-for-byte unchanged, and cannot
// carry an explicit cache breakpoint.
.filter(|b| !matches!(b["type"].as_str(), Some("thinking" | "redacted_thinking")))
.and_then(Value::as_object_mut)
{
block.insert("cache_control".into(), json!({ "type": "ephemeral" }));
@@ -746,7 +762,9 @@ fn openai_body(
let mut msg = serde_json::Map::new();
msg.insert("role".into(), json!("assistant"));
msg.insert("content".into(), json!(text.as_str()));
if let Some(details) = reasoning_details {
// OpenRouter owns the array shape; native Anthropic state must
// not leak into Chat requests after a session model switch.
if let Some(details) = reasoning_details.as_ref().filter(|v| v.is_array()) {
msg.insert("reasoning_details".into(), details.clone());
}
if !tool_calls.is_empty() {
@@ -1386,6 +1404,14 @@ fn parse_anthropic(v: Value) -> Result<LlmResponse, AgentError> {
}
}
}
// The run loop caps other providers' calls by truncating them. Native
// content must be replayed intact, so reject oversized turns before any
// tools execute rather than silently altering signed/interleaved state.
if tool_calls.len() > MAX_TOOL_CALLS_PER_TURN {
return Err(AgentError::Llm(format!(
"Anthropic response exceeds {MAX_TOOL_CALLS_PER_TURN} tool calls; cannot truncate native content"
)));
}
// anthropic_input_tokens() returns Option<SumUsageResult> because it sums
// three fields that can collectively overflow u64. Propagate the overflow
// signal via `input_tokens_overflowed` so the run loop can poison the
@@ -1415,7 +1441,13 @@ fn parse_anthropic(v: Value) -> Result<LlmResponse, AgentError> {
// total from them. Always None for this provider.
total_tokens: None,
reasoning,
reasoning_details: None,
// Truncated content is not a valid signed assistant turn; the run loop
// keeps only its text and never executes its tool calls.
reasoning_details: v
.get("content")
.filter(|v| v.is_array())
.filter(|_| stop != ProviderStop::MaxTokens)
.map(|blocks| json!({ "anthropic_content": blocks })),
// Stamped by the dispatch layer (complete) after parse.
request_model: None,
})

View File

@@ -54,3 +54,112 @@ async fn gpt_fqn_completion_and_summary_use_responses() {
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"));
}
}

View File

@@ -293,7 +293,8 @@ fn prefix_matches(token: &str, s: &str) -> bool {
/// Databricks Unity Catalog model-service names are catalog data, not model
/// family hints. Both capability interpreters use this shape check before
/// family matching so services cannot inherit endpoint capabilities accidentally.
/// GPT-5+ services have a route-only Responses exception.
/// Verified exact records may supply capabilities; other GPT-5+ services have
/// a route-only Responses exception.
pub(crate) fn is_databricks_model_service_fqn(model: &str) -> bool {
let mut components = model.split('.');
let (Some(catalog), Some(schema), Some(service)) =
@@ -334,14 +335,14 @@ pub fn resolve(provider: &str, raw_model_id: &str) -> CapabilityResult {
let canon = canonical_provider(provider);
let blank = raw_model_id.trim().is_empty();
// FQNs keep neutral effort capabilities, but GPT-5+ service names need
// Responses for tools with reasoning. Only inspect the service component:
// catalog/schema names must never choose a model protocol.
// Uncurated FQNs keep neutral effort capabilities. Exact records are verified
// service contracts, not family-name inference. The GPT-5+ route-only fallback
// inspects just the service component, never catalog/schema names.
let model_service_fqn =
canon == "databricks_v2" && is_databricks_model_service_fqn(raw_model_id);
// 1. Provider-qualified exact-record lookup (case-insensitive on the id).
if !blank && !model_service_fqn {
if !blank {
for rec in &m.exact_records {
if rec.provider == canon && rec.raw_model_id.eq_ignore_ascii_case(raw_model_id) {
return CapabilityResult {
@@ -747,6 +748,13 @@ mod tests {
Q::Vector { id: "boundary-claude-3-digit-run-anthropic-probe", provider: "anthropic", raw_model_id: "claude-35", note: Some("Probes whether the claude-3 prefix binds a longer digit run ('35').") },
Q::Vector { id: "boundary-claude-opus-4-70-anthropic-probe", provider: "anthropic", raw_model_id: "claude-opus-4-70", note: Some("Probes whether the claude-opus-4-7 prefix binds a longer digit run ('70').") },
Q::Vector { id: "boundary-gpt-5-1234-openai-probe", provider: "openai", raw_model_id: "gpt-5-1234", note: Some("Probes a 4-digit run after the gpt-5 stem.") },
Q::Section { group: "Verified Databricks Opus UC service and exact-record boundaries", note: None },
Q::Vector { id: "dbv2-opus-uc-exact", provider: "databricks_v2", raw_model_id: "data_workflow_tools.goose.goose-claude-opus-5-5", note: None },
Q::Vector { id: "dbv2-opus-uc-case", provider: "databricks_v2", raw_model_id: "DATA_WORKFLOW_TOOLS.GOOSE.GOOSE-CLAUDE-OPUS-5-5", note: None },
Q::Vector { id: "dbv2-opus-uc-namespace", provider: "databricks_v2", raw_model_id: "other.goose.goose-claude-opus-5-5", note: None },
Q::Vector { id: "dbv2-opus-uc-service", provider: "databricks_v2", raw_model_id: "data_workflow_tools.goose.goose-claude-opus-5-5-preview", note: None },
Q::Vector { id: "dbv2-opus-uc-system", provider: "databricks_v2", raw_model_id: "system.ai.claude-opus-5-5", note: None },
Q::Vector { id: "dbv2-opus-uc-provider", provider: "openai", raw_model_id: "data_workflow_tools.goose.goose-claude-opus-5-5", note: None },
Q::Section { group: "Databricks FQN GPT-5+ Responses routing", note: Some("Only the service component selects Responses; effort capabilities remain neutral.") },
Q::Vector { id: "dbv2-fqn-responses-0", provider: "databricks_v2", raw_model_id: "catalog.schema.goose-gpt-6-astra", note: None },
Q::Vector { id: "dbv2-fqn-responses-1", provider: "databricks_v2", raw_model_id: "catalog.schema.goose-gpt-5", note: None },
@@ -886,7 +894,7 @@ mod tests {
}
#[test]
fn corpus_has_exactly_158_executable_vectors() {
fn corpus_has_exactly_164_executable_vectors() {
// Locks the vector count so a silent INPUTS edit can't quietly drop
// coverage; must equal the gate in the TS harness
// (modelCapabilitiesCorpus.test.mjs).
@@ -895,7 +903,7 @@ mod tests {
.filter(|q| matches!(q, Q::Vector { .. }))
.count();
assert_eq!(
vectors, 158,
vectors, 164,
"corpus executable-vector count changed; update this gate deliberately"
);
}

View File

@@ -215,9 +215,10 @@ pub struct LlmResponse {
///
/// Empty string when the provider returned no reasoning content.
pub reasoning: String,
/// Raw `reasoning_details` array from an OpenRouter response, if present.
/// Replayed on subsequent turns so the model can continue its chain-of-thought.
/// `None` for all non-OpenRouter providers.
/// Provider-owned replay state: OpenRouter's raw `reasoning_details` array,
/// or an `anthropic_content` object holding ordered native content blocks.
/// Kept opaque so signed thinking survives tool-result continuation. Each
/// serializer consumes only its own shape, including after model switches.
pub reasoning_details: Option<Value>,
/// The model id that was actually sent in the request body — the
/// actually-requested model after any auto/mesh resolution. Populated by

View File

@@ -0,0 +1,136 @@
//! Native replay must remain consistent with the real ACP loop's recovery limits.
mod common;
use common::{spawn_capturing_llm, Harness};
use serde_json::json;
async fn session(h: &mut Harness) -> String {
let id = h
.send(
"initialize",
json!({"protocolVersion":1,"clientCapabilities":{}}),
)
.await;
h.recv_until(|v| v["id"] == id).await;
let id = h
.send(
"session/new",
json!({"cwd":"/tmp","mcpServers":[{
"name":"probe", "command":env!("CARGO_BIN_EXE_fake-mcp"), "args":[], "env":[]
}]}),
)
.await;
h.recv_until(|v| v["id"] == id).await["result"]["sessionId"]
.as_str()
.unwrap()
.to_owned()
}
async fn native_harness(url: &str) -> Harness {
// Native Anthropic avoids Databricks discovery consuming response fixtures;
// the exact UC dispatcher and effort contract are tested in llm_fqn_tests.
Harness::spawn_with_env(
url,
&[
("BUZZ_AGENT_PROVIDER", "anthropic"),
("ANTHROPIC_API_KEY", "test"),
("ANTHROPIC_BASE_URL", url),
("BUZZ_AGENT_MODEL", "claude-opus-5-5"),
("BUZZ_AGENT_THINKING_EFFORT", "high"),
],
)
.await
}
#[tokio::test]
async fn oversized_native_turn_fails_before_tools_execute() {
let mut content = vec![json!({"type":"thinking", "thinking":"", "signature":"signed"})];
content.extend((0..65).map(|n| {
json!({"type":"tool_use", "id":format!("call-{n}"),
"name":"probe__tool_0", "input":{}})
}));
let llm = spawn_capturing_llm(vec![json!({"stop_reason":"tool_use", "content":content})]).await;
let mut h = native_harness(&llm.url).await;
let sid = session(&mut h).await;
let id = h
.send(
"session/prompt",
json!({"sessionId":sid,"prompt":[{"type":"text","text":"go"}]}),
)
.await;
let mut tool_started = false;
let reply = h
.recv_until_approving(|v| {
tool_started |= v["params"]["update"]["sessionUpdate"] == "tool_call";
v["id"] == id
})
.await;
assert!(
reply["error"]
.to_string()
.contains("cannot truncate native content"),
"{reply}"
);
assert!(
!tool_started,
"oversized native turn must execute no partial subset"
);
assert_eq!(llm.captured.lock().await.len(), 1);
h.shutdown().await;
}
#[tokio::test]
async fn native_max_tokens_recovers_without_signed_state_or_partial_tools() {
let llm = spawn_capturing_llm(vec![
json!({"stop_reason":"max_tokens", "content":[
{"type":"thinking", "thinking":"", "signature":"incomplete-signature"},
{"type":"text", "text":"partial-text"},
{"type":"tool_use", "id":"partial-call", "name":"probe__tool_0", "input":"{broken"},
{"type":"redacted_thinking", "data":"incomplete-redaction"}
]}),
json!({"stop_reason":"end_turn", "content":[{"type":"text", "text":"done"}]}),
])
.await;
let mut h = native_harness(&llm.url).await;
let sid = session(&mut h).await;
let id = h
.send(
"session/prompt",
json!({"sessionId":sid,"prompt":[{"type":"text","text":"go"}]}),
)
.await;
let mut tool_started = false;
let reply = h
.recv_until_approving(|v| {
tool_started |= v["params"]["update"]["sessionUpdate"] == "tool_call";
v["id"] == id
})
.await;
assert_eq!(reply["result"]["stopReason"], "end_turn", "{reply}");
assert!(!tool_started);
let requests = llm.captured.lock().await;
assert_eq!(requests.len(), 2);
let retry = &requests[1]["messages"];
let assistant = retry
.as_array()
.unwrap()
.iter()
.find(|m| m["role"] == "assistant")
.unwrap();
let blocks = assistant["content"].as_array().unwrap();
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], "partial-text");
let serialized = retry.to_string();
for forbidden in [
"incomplete-signature",
"incomplete-redaction",
"partial-call",
"tool_result",
] {
assert!(!serialized.contains(forbidden), "{retry}");
}
assert!(serialized.contains("output token limit"));
drop(requests);
h.shutdown().await;
}

View File

@@ -306,7 +306,7 @@ with a TypeScript lookup table or an id comparison in a component.
refresh only local persona/team/managed-agent caches; they must never
invalidate the remote relay directory.
17. **Databricks model discovery has one shared catalog authority.** Desktop and ACP call the shared `buzz-agent` discovery library; Desktop passes the effective merged `DATABRICKS_MODEL_FILTER` explicitly, and the library applies it to raw workspace endpoint IDs and Unity Catalog model-service FQNs after the additive union. A successful filtered-empty catalog is authoritative: it stays empty, disables switching, and never falls through to configured or known-model fallback. UC FQNs retain neutral effort capabilities. A boundary-matched GPT-5-or-newer family in the service-name component selects OpenAI Responses so tools can coexist with reasoning; other FQNs use MLflow Chat Completions. Catalog/schema components never influence routing. Keep this route-only rule identical in the Rust and TypeScript capability interpreters. Global Defaults preserves the discovered model ID as the selected value while its closed trigger renders the provider-scoped display label; do not force the raw persisted ID over that label.
17. **Databricks model discovery has one shared catalog authority.** Desktop and ACP call the shared `buzz-agent` discovery library; Desktop passes the effective merged `DATABRICKS_MODEL_FILTER` explicitly, and the library applies it to raw workspace endpoint IDs and Unity Catalog model-service FQNs after the additive union. A successful filtered-empty catalog is authoritative: it stays empty, disables switching, and never falls through to configured or known-model fallback. Verified provider-qualified exact records in `scripts/model-capabilities.json` take precedence for UC FQNs: `data_workflow_tools.goose.goose-claude-opus-5-5` uses Anthropic Messages, adaptive thinking, and `output_config.effort` (default medium). This does not confer capabilities on other namespaces or similarly named services. Uncurated UC FQNs retain neutral effort capabilities. A boundary-matched GPT-5-or-newer family in their service-name component selects OpenAI Responses so tools can coexist with reasoning; other uncurated FQNs use MLflow Chat Completions. Catalog/schema components never infer routing. Keep exact-record precedence and fallback rules identical in the Rust and TypeScript capability interpreters and shared corpus. Effort inheritance, persistence, and clearing semantics are unchanged. Global Defaults preserves the discovered model ID as the selected value while its closed trigger renders the provider-scoped display label; do not force the raw persisted ID over that label.
## Channel-only runtime controls

View File

@@ -328,13 +328,13 @@ export function resolveModelCapabilities(
): CapabilityResult {
const canon = canonicalizeProvider(provider);
const blank = rawModelId.trim().length === 0;
// FQNs keep neutral effort capabilities; only GPT-5+ service names
// select Responses. Catalog/schema names never choose the protocol.
// Uncurated FQNs keep neutral effort capabilities; verified exact records
// take precedence. Catalog/schema names never infer a protocol.
const modelServiceFqn =
canon === "databricks_v2" && isDatabricksModelServiceFqn(rawModelId);
// 1. Provider-qualified exact-record lookup (case-insensitive on the id).
if (!blank && !modelServiceFqn) {
if (!blank) {
const idLower = rawModelId.toLowerCase();
for (const rec of MANIFEST.exact_records) {
if (

View File

@@ -25,10 +25,10 @@ const corpus = JSON.parse(readFileSync(fileURLToPath(corpusUrl), "utf8"));
// (`_group`) are skipped. Mirrors the Rust corpus filter.
const executable = corpus.filter((entry) => entry.expect != null);
test("corpus has exactly 158 executable vectors", () => {
test("corpus has exactly 164 executable vectors", () => {
// Locks the vector count so a silent corpus edit can't quietly drop coverage;
// must equal the gate in the Rust suite (model_capabilities.rs).
assert.equal(executable.length, 158);
assert.equal(executable.length, 164);
});
test("registry label aliases refuse an unprefixed query", () => {

View File

@@ -431,6 +431,24 @@
"databricks-claude-opus-4-7"
],
"exact_records": [
{
"provider": "databricks_v2",
"raw_model_id": "data_workflow_tools.goose.goose-claude-opus-5-5",
"registry_label": "Claude Opus 5.5",
"thinking_mode": "adaptive",
"supported_efforts": [
"low",
"medium",
"high",
"xhigh",
"max"
],
"default_effort": "medium",
"databricks_v2_wire_route": "anthropic-messages",
"normalization_policy": "none",
"_provenance": "Exact UC service: native Messages with adaptive thinking and output_config.effort=high verified by synthetic tool-result round trip on 2026-09-23. Not inferred for other UC namespaces.",
"source": "https://docs.databricks.com/aws/en/ai-gateway/query-model-services; https://platform.claude.com/docs/en/build-with-claude/effort"
},
{
"provider": "databricks_v2",
"raw_model_id": "databricks-gpt-5-4-mini",

View File

@@ -2347,6 +2347,127 @@
"registry_label": null
}
},
{
"_group": "Verified Databricks Opus UC service and exact-record boundaries"
},
{
"id": "dbv2-opus-uc-exact",
"provider": "databricks_v2",
"raw_model_id": "data_workflow_tools.goose.goose-claude-opus-5-5",
"expect": {
"thinking_mode": "adaptive",
"supported_efforts": [
"low",
"medium",
"high",
"xhigh",
"max"
],
"default_effort": "medium",
"databricks_v2_wire_route": "anthropic-messages",
"normalization_policy": "none",
"registry_label": "Claude Opus 5.5"
}
},
{
"id": "dbv2-opus-uc-case",
"provider": "databricks_v2",
"raw_model_id": "DATA_WORKFLOW_TOOLS.GOOSE.GOOSE-CLAUDE-OPUS-5-5",
"expect": {
"thinking_mode": "adaptive",
"supported_efforts": [
"low",
"medium",
"high",
"xhigh",
"max"
],
"default_effort": "medium",
"databricks_v2_wire_route": "anthropic-messages",
"normalization_policy": "none",
"registry_label": "Claude Opus 5.5"
}
},
{
"id": "dbv2-opus-uc-namespace",
"provider": "databricks_v2",
"raw_model_id": "other.goose.goose-claude-opus-5-5",
"expect": {
"thinking_mode": "none",
"supported_efforts": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"default_effort": "medium",
"databricks_v2_wire_route": "mlflow-chat",
"normalization_policy": "openai-clamp-max-to-xhigh",
"registry_label": null
}
},
{
"id": "dbv2-opus-uc-service",
"provider": "databricks_v2",
"raw_model_id": "data_workflow_tools.goose.goose-claude-opus-5-5-preview",
"expect": {
"thinking_mode": "none",
"supported_efforts": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"default_effort": "medium",
"databricks_v2_wire_route": "mlflow-chat",
"normalization_policy": "openai-clamp-max-to-xhigh",
"registry_label": null
}
},
{
"id": "dbv2-opus-uc-system",
"provider": "databricks_v2",
"raw_model_id": "system.ai.claude-opus-5-5",
"expect": {
"thinking_mode": "none",
"supported_efforts": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"default_effort": "medium",
"databricks_v2_wire_route": "mlflow-chat",
"normalization_policy": "openai-clamp-max-to-xhigh",
"registry_label": null
}
},
{
"id": "dbv2-opus-uc-provider",
"provider": "openai",
"raw_model_id": "data_workflow_tools.goose.goose-claude-opus-5-5",
"expect": {
"thinking_mode": "none",
"supported_efforts": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"default_effort": "medium",
"databricks_v2_wire_route": "not-applicable",
"normalization_policy": "openai-clamp-max-to-xhigh",
"registry_label": null
}
},
{
"_group": "Databricks FQN GPT-5+ Responses routing",
"_note": "Only the service component selects Responses; effort capabilities remain neutral."