synaps_cli/runtime/openai/catalog/
openrouter.rs1use serde::Deserialize;
4
5use super::{CatalogModel, CatalogProviderKind, CatalogSource, Modality, PricingSummary, ReasoningSupport};
6
7#[derive(Debug, Deserialize)]
8struct OpenRouterModelsResponse {
9 data: Vec<OpenRouterModelItem>,
10}
11
12#[derive(Debug, Deserialize)]
13struct OpenRouterModelItem {
14 id: String,
15 #[serde(default)]
16 name: Option<String>,
17 #[serde(default)]
18 context_length: Option<u64>,
19 #[serde(default)]
20 architecture: Option<OpenRouterArchitecture>,
21 #[serde(default)]
22 pricing: Option<OpenRouterPricing>,
23 #[serde(default)]
24 top_provider: Option<OpenRouterTopProvider>,
25 #[serde(default)]
26 supported_parameters: Vec<String>,
27}
28
29#[derive(Debug, Deserialize)]
30struct OpenRouterArchitecture {
31 #[serde(default)]
32 input_modalities: Vec<String>,
33}
34
35#[derive(Debug, Deserialize)]
36struct OpenRouterPricing {
37 #[serde(default)]
38 prompt: Option<String>,
39 #[serde(default)]
40 completion: Option<String>,
41 #[serde(default)]
42 internal_reasoning: Option<String>,
43}
44
45#[derive(Debug, Deserialize)]
46struct OpenRouterTopProvider {
47 #[serde(default)]
48 max_completion_tokens: Option<u64>,
49}
50
51pub fn parse_openrouter_catalog_models(body: &str) -> Result<Vec<CatalogModel>, serde_json::Error> {
54 let resp: OpenRouterModelsResponse = serde_json::from_str(body)?;
55 Ok(resp
56 .data
57 .into_iter()
58 .filter_map(|item| {
59 let mut m = CatalogModel::new("openrouter", "OpenRouter", item.id)?;
60 m.provider_kind = CatalogProviderKind::OpenRouter;
61 m.label = item.name.filter(|n| !n.trim().is_empty());
62 m.context_tokens = item.context_length;
63 m.max_output_tokens = item.top_provider.as_ref().and_then(|p| p.max_completion_tokens);
64
65 let mods: Vec<Modality> = item
66 .architecture
67 .map(|a| a.input_modalities.iter().map(|s| Modality::from_str(s)).collect())
68 .unwrap_or_else(|| vec![Modality::Text]);
69 m.input_modalities = mods;
70
71 let pricing = item.pricing.map(|p| PricingSummary {
72 prompt: p.prompt.filter(|v| !v.trim().is_empty()),
73 completion: p.completion.filter(|v| !v.trim().is_empty()),
74 internal_reasoning: p.internal_reasoning.filter(|v| !v.trim().is_empty()),
75 }).unwrap_or_default();
76
77 let has = |param: &str| item.supported_parameters.iter().any(|p| p == param);
78 let internal_priced = pricing.has_internal_reasoning_cost();
79
80 m.reasoning = if has("verbosity") {
81 ReasoningSupport::AnthropicAdaptive { adaptive: true }
82 } else if has("reasoning") || has("include_reasoning") || has("reasoning_effort") || internal_priced {
83 ReasoningSupport::OpenRouter {
84 include_reasoning: has("include_reasoning"),
85 effort: has("reasoning_effort"),
86 verbosity: false,
87 internal_reasoning_priced: internal_priced,
88 }
89 } else {
90 ReasoningSupport::None
91 };
92
93 m.pricing = pricing;
94 m.source = CatalogSource::Live;
95 Some(m)
96 })
97 .collect())
98}