feat(claw): serve the vision flag on model listings
The models route's cards serialize as rhema-provider's ModelCard spells them, so supports_vision rides exactly when the adapter knows it and is absent when it does not — unknown stays absent, never false. The route's doc now pins that contract, and a listing builder on Backend lets the surface tests mount one offline.
This commit is contained in:
1 parent
23b13f41b8
commit
cfb349508f
3 files changed
+79
-2
No files matched your search
@@ -115,6 +115,18 @@ impl Backend {
|
||||
list_models: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches a live listing. [`construct`] builds the real ones off
|
||||
/// the provider; this shape exists so the surface's model routes
|
||||
/// can be exercised offline — a test mounts a listing over a
|
||||
/// scripted card set.
|
||||
pub fn with_listing(
|
||||
mut self,
|
||||
list_models: impl Fn() -> ModelListing + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.list_models = Some(Arc::new(list_models));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently active backend instance: id plus the model turns run on.
|
||||
|
||||
@@ -1864,8 +1864,12 @@ async fn providers_active(
|
||||
}
|
||||
|
||||
/// `GET /v1/providers/{id}/models`: the instance's live listing,
|
||||
/// straight from the provider. An id without a listing (unknown, or the
|
||||
/// mock) is a 404.
|
||||
/// straight from the provider, the cards serialized as rhema-provider's
|
||||
/// `ModelCard` spells them. `supports_vision` rides when the adapter
|
||||
/// knows — `true` or `false` — and is absent when it does not: unknown
|
||||
/// stays absent, never `false`, and a client reads the key's absence as
|
||||
/// no verdict at all. An id without a listing (unknown, or the mock) is
|
||||
/// a 404.
|
||||
async fn providers_models(
|
||||
State(state): State<ClawState>,
|
||||
Path(id): Path<String>,
|
||||
|
||||
@@ -1630,6 +1630,67 @@ async fn providers_are_listed_switched_and_persisted() {
|
||||
assert_eq!(daemon.backends().active().1, "mock-large");
|
||||
}
|
||||
|
||||
/// The models route: the cards serialize as rhema-provider's
|
||||
/// `ModelCard`, so a model the adapter knows is vision-capable rides
|
||||
/// with `supports_vision: true` and an unknown one omits the key
|
||||
/// entirely — unknown stays absent, never `false`.
|
||||
#[tokio::test]
|
||||
async fn model_listings_carry_the_vision_flag_only_when_known() {
|
||||
let card = |id: &str, vision: Option<bool>| rhema_provider::ModelCard {
|
||||
id: id.to_owned(),
|
||||
display_name: id.to_owned(),
|
||||
description: String::new(),
|
||||
input_token_limit: None,
|
||||
output_token_limit: None,
|
||||
supports_vision: vision,
|
||||
generation_methods: vec!["generateContent".to_owned()],
|
||||
pricing: None,
|
||||
};
|
||||
let cards = vec![
|
||||
card("claude-sonnet-4-5", Some(true)),
|
||||
card("some-foreign-model", None),
|
||||
];
|
||||
let backend =
|
||||
rhema_claw::Backend::headless("anthropic", Arc::new(MockProvider::new([reply("on it")])))
|
||||
.with_listing(move || {
|
||||
let cards = cards.clone();
|
||||
Box::pin(async move { Ok(cards) })
|
||||
});
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let daemon = Daemon::build(
|
||||
&config(&dir, None),
|
||||
Arc::new(
|
||||
rhema_claw::BackendRegistry::new(vec![backend], None)
|
||||
.expect("a single backend always constructs"),
|
||||
),
|
||||
rhema_rsd::DispatcherConfig::default(),
|
||||
)
|
||||
.expect("the daemon assembles");
|
||||
let base = serve(&daemon).await;
|
||||
|
||||
let listed: Value = reqwest::Client::new()
|
||||
.get(format!("{base}/v1/providers/anthropic/models"))
|
||||
.send()
|
||||
.await
|
||||
.expect("the listing reads")
|
||||
.json()
|
||||
.await
|
||||
.expect("the listing parses");
|
||||
let entries = listed["models"].as_array().expect("the models array");
|
||||
assert_eq!(entries.len(), 2, "{listed}");
|
||||
assert_eq!(entries[0]["id"], "claude-sonnet-4-5");
|
||||
assert_eq!(
|
||||
entries[0]["supports_vision"], true,
|
||||
"a known-vision model says so: {listed}"
|
||||
);
|
||||
assert_eq!(entries[1]["id"], "some-foreign-model");
|
||||
assert!(
|
||||
entries[1].get("supports_vision").is_none(),
|
||||
"unknown stays absent, not false: {:?}",
|
||||
entries[1]
|
||||
);
|
||||
}
|
||||
|
||||
/// The credentials surface: a runtime login adds a backend, the list
|
||||
/// names where its credential came from, a re-add replaces it in place,
|
||||
/// and a removal drops it — refusing to remove the last backend, and
|
||||
|
||||
Reference in new issue
Block a user