use indexmap::IndexMap;
use oino_ai::{AssistantMessage, InputModality, JsonValue, Message, Model, ModelCost};
use oino_ai::{StopReason, ThinkingLevel, ThinkingLevelMap};
use serde::{Deserialize, Deserializer, Serialize};
pub const VIRTUAL_MODEL_API: &str = "oino-virtual";
pub const VIRTUAL_MODEL_STATE_ENTRY: &str = "oino.virtual-model-state";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VirtualModelDefinition {
pub provider: String,
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_levels: Option<Vec<ThinkingLevel>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_window: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<Vec<InputModality>>,
}
#[must_use]
pub fn is_virtual_api(api: &str) -> bool {
api == VIRTUAL_MODEL_API
}
#[must_use]
pub fn create_virtual_model(definition: &VirtualModelDefinition) -> Model {
let levels = definition
.thinking_levels
.clone()
.unwrap_or_else(|| vec![ThinkingLevel::Off]);
let slot =
|level: ThinkingLevel| Some(levels.contains(&level).then(|| level.as_str().to_owned()));
Model {
id: definition.id.clone(),
name: definition.name.clone(),
api: VIRTUAL_MODEL_API.to_owned(),
provider: definition.provider.clone(),
base_url: String::new(),
input: definition
.input
.clone()
.unwrap_or_else(|| vec![InputModality::Text, InputModality::Image]),
input_limits: None,
cost: ModelCost::default(),
headers: None,
kind: None,
reasoning: levels.iter().any(|level| *level != ThinkingLevel::Off),
thinking_level_map: Some(ThinkingLevelMap {
off: slot(ThinkingLevel::Off),
minimal: slot(ThinkingLevel::Minimal),
low: slot(ThinkingLevel::Low),
medium: slot(ThinkingLevel::Medium),
high: slot(ThinkingLevel::High),
xhigh: slot(ThinkingLevel::Xhigh),
max: slot(ThinkingLevel::Max),
}),
prompt_cache: None,
context_window: definition.context_window.unwrap_or(0),
max_tokens: definition.max_tokens.unwrap_or(0),
sampling_params: None,
sampling_params_by_thinking_level: None,
compat: None,
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RegisteredVirtualModel {
pub definition: VirtualModelDefinition,
pub extension_id: String,
pub model: Model,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VirtualModelError {
#[error("Virtual model provider and id must not be empty.")]
EmptyId,
#[error("Virtual model {provider}/{id} conflicts with a physical model.")]
ConflictsWithPhysical {
provider: String,
id: String,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct VirtualModelRegistry {
by_provider: IndexMap<String, IndexMap<String, RegisteredVirtualModel>>,
}
impl VirtualModelRegistry {
pub fn register(
&mut self,
extension_id: &str,
definition: VirtualModelDefinition,
is_physical: bool,
) -> Result<(), VirtualModelError> {
if definition.provider.trim().is_empty() || definition.id.trim().is_empty() {
return Err(VirtualModelError::EmptyId);
}
if is_physical {
return Err(VirtualModelError::ConflictsWithPhysical {
provider: definition.provider,
id: definition.id,
});
}
let model = create_virtual_model(&definition);
self.by_provider
.entry(definition.provider.clone())
.or_default()
.insert(
definition.id.clone(),
RegisteredVirtualModel {
definition,
extension_id: extension_id.to_owned(),
model,
},
);
Ok(())
}
pub fn unregister(&mut self, provider: &str, id: &str) -> bool {
let Some(models) = self.by_provider.get_mut(provider) else {
return false;
};
let removed = models.shift_remove(id).is_some();
if models.is_empty() {
self.by_provider.shift_remove(provider);
}
removed
}
pub fn remove_extension(&mut self, extension_id: &str) {
for models in self.by_provider.values_mut() {
models.retain(|_, entry| entry.extension_id != extension_id);
}
self.by_provider.retain(|_, models| !models.is_empty());
}
#[must_use]
pub fn get(&self, provider: &str, id: &str) -> Option<&RegisteredVirtualModel> {
self.by_provider.get(provider)?.get(id)
}
pub fn models_for(&self, provider: &str) -> impl Iterator<Item = &Model> {
self.by_provider
.get(provider)
.into_iter()
.flat_map(|models| models.values().map(|entry| &entry.model))
}
pub fn provider_ids(&self) -> impl Iterator<Item = &str> {
self.by_provider.keys().map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = &RegisteredVirtualModel> {
self.by_provider.values().flat_map(IndexMap::values)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RouteReason {
User,
Continuation,
Retry,
Direct,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoutePrevious {
pub model: Model,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<ThinkingLevel>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteFailed {
pub model: Model,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<ThinkingLevel>,
pub message: AssistantMessage,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteRequest {
pub model: Model,
pub thinking_level: ThinkingLevel,
pub reason: RouteReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub previous: Option<RoutePrevious>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failed: Option<RouteFailed>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<JsonValue>,
pub messages: Vec<Message>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RouteTarget {
pub provider: String,
pub id: String,
}
impl<'de> Deserialize<'de> for RouteTarget {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Key(String),
Object { provider: String, id: String },
}
match Raw::deserialize(deserializer)? {
Raw::Object { provider, id } => Ok(Self { provider, id }),
Raw::Key(key) => key
.split_once('/')
.map(|(provider, id)| Self {
provider: provider.to_owned(),
id: id.to_owned(),
})
.ok_or_else(|| {
serde::de::Error::custom(format!("route model \"{key}\" is not provider/id"))
}),
}
}
}
fn present<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<JsonValue>, D::Error> {
JsonValue::deserialize(deserializer).map(Some)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RouteResult {
pub model: RouteTarget,
pub thinking_level: ThinkingLevel,
#[serde(
default,
deserialize_with = "present",
skip_serializing_if = "Option::is_none"
)]
pub state: Option<JsonValue>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ModelRoute {
pub model: Model,
pub thinking_level: ThinkingLevel,
pub state: Option<JsonValue>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RouteError {
#[error("Virtual model {provider}/{id} is not registered.")]
NotRegistered {
provider: String,
id: String,
},
#[error("Virtual model {provider}/{id} routed to {target}, which is not a physical model.")]
NotPhysical {
provider: String,
id: String,
target: String,
},
#[error("Virtual model {provider}/{id} routed to {target}, which has no credentials.")]
NoCredentials {
provider: String,
id: String,
target: String,
},
#[error("{message}")]
Router {
message: String,
},
}
#[must_use]
pub fn find_latest_response(messages: &[Message]) -> Option<&AssistantMessage> {
messages.iter().rev().find_map(|message| match message {
Message::Assistant(assistant)
if !matches!(
assistant.stop_reason,
StopReason::Error | StopReason::Aborted
) =>
{
Some(assistant)
}
_ => None,
})
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VirtualModelStateData {
pub provider: String,
pub model_id: String,
pub state: JsonValue,
}
#[must_use]
pub fn should_store_route_state(
reason: RouteReason,
current: Option<&JsonValue>,
returned: Option<&JsonValue>,
) -> bool {
match (reason, returned) {
(RouteReason::Direct, _) | (_, None) => false,
(_, Some(returned)) => current != Some(returned),
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BranchEntryRef<'a> {
ModelChange {
provider: &'a str,
model_id: &'a str,
},
Assistant {
provider: &'a str,
model: &'a str,
api: &'a str,
},
Custom {
custom_type: &'a str,
data: Option<&'a JsonValue>,
},
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BranchSelection {
pub provider: String,
pub model_id: String,
}
#[must_use]
pub fn branch_selection(
branch: &[BranchEntryRef<'_>],
is_registered_virtual: impl Fn(&str, &str) -> bool,
) -> Option<BranchSelection> {
for (index, entry) in branch.iter().enumerate().rev() {
match *entry {
BranchEntryRef::ModelChange { provider, model_id } => {
return Some(BranchSelection {
provider: provider.to_owned(),
model_id: model_id.to_owned(),
});
}
BranchEntryRef::Assistant {
provider,
model,
api,
} if !is_virtual_api(api) => {
let change = branch[..index].iter().rev().find_map(|e| match *e {
BranchEntryRef::ModelChange { provider, model_id } => {
Some((provider, model_id))
}
_ => None,
});
return Some(match change {
Some((p, m)) if is_registered_virtual(p, m) => BranchSelection {
provider: p.to_owned(),
model_id: m.to_owned(),
},
_ => BranchSelection {
provider: provider.to_owned(),
model_id: model.to_owned(),
},
});
}
_ => {}
}
}
None
}
#[must_use]
pub fn virtual_model_state<'a>(
branch: &[BranchEntryRef<'a>],
provider: &str,
model_id: &str,
) -> Option<&'a JsonValue> {
branch.iter().rev().find_map(|entry| match *entry {
BranchEntryRef::Custom {
custom_type,
data: Some(data),
} if custom_type == VIRTUAL_MODEL_STATE_ENTRY
&& data.get("provider").and_then(JsonValue::as_str) == Some(provider)
&& data.get("modelId").and_then(JsonValue::as_str) == Some(model_id) =>
{
data.get("state")
}
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn definition() -> VirtualModelDefinition {
serde_json::from_value(json!({"provider": "router", "id": "auto", "name": "Auto", "thinkingLevels": ["low", "high"]}))
.unwrap()
}
#[test]
fn catalog_entry_matches_pi() {
let model = create_virtual_model(&definition());
assert_eq!(
serde_json::to_value(&model).unwrap(),
json!({
"id": "auto", "name": "Auto", "api": "oino-virtual", "provider": "router", "baseUrl": "",
"input": ["text", "image"], "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0},
"reasoning": true,
"thinkingLevelMap": {"off": null, "minimal": null, "low": "low", "medium": null, "high": "high", "xhigh": null, "max": null},
"contextWindow": 0, "maxTokens": 0
})
);
assert_eq!(
model.available_thinking_levels(),
vec![ThinkingLevel::Low, ThinkingLevel::High]
);
}
#[test]
fn registry_conflicts_and_removal() {
let mut registry = VirtualModelRegistry::default();
assert_eq!(
registry
.register("ext", definition(), true)
.unwrap_err()
.to_string(),
"Virtual model router/auto conflicts with a physical model."
);
registry.register("ext", definition(), false).unwrap();
assert_eq!(registry.models_for("router").count(), 1);
registry.remove_extension("ext");
assert!(registry.get("router", "auto").is_none());
}
#[test]
fn route_result_accepts_key_or_object() {
let a: RouteResult =
serde_json::from_value(json!({"model": "anthropic/claude", "thinkingLevel": "high"}))
.unwrap();
let b: RouteResult = serde_json::from_value(json!({"model": {"provider": "anthropic", "id": "claude"}, "thinkingLevel": "high", "state": null})).unwrap();
assert_eq!(a.model, b.model);
assert_eq!(a.state, None);
assert_eq!(b.state, Some(JsonValue::Null));
}
#[test]
fn branch_selection_rules() {
let branch = [
BranchEntryRef::ModelChange {
provider: "router",
model_id: "auto",
},
BranchEntryRef::Assistant {
provider: "anthropic",
model: "claude",
api: "anthropic-messages",
},
];
let selection = branch_selection(&branch, |p, m| p == "router" && m == "auto").unwrap();
assert_eq!(selection.model_id, "auto");
let selection = branch_selection(&branch, |_, _| false).unwrap();
assert_eq!(selection.model_id, "claude");
let state = json!({"provider": "router", "modelId": "auto", "state": {"phase": "plan"}});
let branch = [BranchEntryRef::Custom {
custom_type: VIRTUAL_MODEL_STATE_ENTRY,
data: Some(&state),
}];
assert_eq!(
virtual_model_state(&branch, "router", "auto"),
Some(&json!({"phase": "plan"}))
);
}
#[test]
fn state_storage_rules() {
let current = json!({"a": 1});
assert!(!should_store_route_state(
RouteReason::User,
Some(¤t),
None
));
assert!(!should_store_route_state(
RouteReason::User,
Some(¤t),
Some(&json!({"a": 1}))
));
assert!(should_store_route_state(
RouteReason::User,
Some(¤t),
Some(&json!({"a": 2}))
));
assert!(!should_store_route_state(
RouteReason::Direct,
None,
Some(&json!(1))
));
}
}