kunobi_decision/client/
models.rs1use reqwest::Method;
4use serde::Deserialize;
5
6use super::call::{Call, DecodeFailure};
7use super::{Client, Provider};
8use crate::types::ModelCard;
9use crate::{Questions, SystemOneRequest, SystemOneResult, noul};
10
11#[derive(Debug, Clone)]
13pub struct Models {
14 client: Client,
15}
16
17impl Models {
18 pub(crate) fn new(client: Client) -> Self {
19 Self { client }
20 }
21
22 pub fn list(&self) -> Call<Vec<ModelCard>> {
29 match self.client.provider() {
30 Provider::OpenRouter => Call::new(
31 self.client.clone(),
32 Method::GET,
33 "/v1/models?output_modalities=decisions",
34 Ok(None),
35 unwrap_openrouter_models,
36 ),
37 Provider::Cloudflare { account_id } => Call::new(
38 self.client.clone(),
39 Method::GET,
40 format!(
41 "/client/v4/accounts/{account_id}/ai/models/search?search=%40cf%2Fcloudflare%2Fclef&per_page=100"
42 ),
43 Ok(None),
44 unwrap_clef_models,
45 ),
46 Provider::Liquid => {
49 let mut questions = Questions::new();
50 questions.add("available", noul("Does the state say ready?"));
51 let body =
52 SystemOneRequest::new("ready", questions).to_body(self.client.default_model());
53 Call::new(
54 self.client.clone(),
55 Method::POST,
56 "/v1/systemone",
57 body.map(Some),
58 unwrap_liquid_model,
59 )
60 }
61 _ => Call::new(
62 self.client.clone(),
63 Method::GET,
64 "/v1/models",
65 Ok(None),
66 unwrap_models,
67 ),
68 }
69 }
70}
71
72fn unwrap_models(body: &[u8]) -> Result<Vec<ModelCard>, DecodeFailure> {
73 #[derive(Deserialize)]
74 struct Wire {
75 models: Vec<ModelCard>,
76 }
77
78 serde_json::from_slice::<Wire>(body)
79 .map(|wire| wire.models)
80 .map_err(|err| DecodeFailure {
81 message: "Unexpected response shape from GET /v1/models; expected { models: [...] }."
82 .into(),
83 source: Some(err),
84 })
85}
86
87fn unwrap_clef_models(body: &[u8]) -> Result<Vec<ModelCard>, DecodeFailure> {
88 let models: Vec<ModelCard> = super::provider::unwrap_cloudflare(body)?;
89 let models: Vec<_> = models
90 .into_iter()
91 .filter_map(|mut model| {
92 let name = model.name.strip_prefix("@cf/cloudflare/")?;
93 if !matches!(name, "clef" | "clef-flash") {
94 return None;
95 }
96 model.name = name.to_owned();
97 Some(model)
98 })
99 .collect();
100 if models.is_empty() {
101 return Err(DecodeFailure {
102 message: "Cloudflare did not list any Clef models for this account".into(),
103 source: None,
104 });
105 }
106 Ok(models)
107}
108
109fn unwrap_liquid_model(body: &[u8]) -> Result<Vec<ModelCard>, DecodeFailure> {
110 let result: SystemOneResult = super::call::parse_json(body)?;
111 Ok(vec![ModelCard {
112 name: result.model,
113 description: "Liquid decision model (inference probe)".into(),
114 release_date: String::new(),
115 }])
116}
117
118fn unwrap_openrouter_models(body: &[u8]) -> Result<Vec<ModelCard>, DecodeFailure> {
119 #[derive(Deserialize)]
120 struct Catalog {
121 data: Vec<Model>,
122 }
123 #[derive(Deserialize)]
124 struct Model {
125 id: String,
126 #[serde(default)]
127 description: String,
128 architecture: Architecture,
129 }
130 #[derive(Deserialize)]
131 struct Architecture {
132 output_modalities: Vec<String>,
133 }
134 let catalog: Catalog = super::call::parse_json(body)?;
135 Ok(catalog
136 .data
137 .into_iter()
138 .filter(|m| {
139 m.architecture
140 .output_modalities
141 .iter()
142 .any(|kind| kind == "decisions")
143 })
144 .map(|m| ModelCard {
145 name: m.id,
146 description: m.description,
147 release_date: String::new(),
148 })
149 .collect())
150}