use super::{AppState, HeaderMap, ModelRouteError, UpstreamProvider, Value, json};
pub(super) fn append_stored_provider_models(state: &AppState, catalog: &mut Value) {
let Ok(providers) = state.provider_store.list() else {
return;
};
let Some(data) = catalog.get_mut("data").and_then(Value::as_array_mut) else {
return;
};
for provider in providers.iter().filter(|record| {
record.enabled && record.kind != crate::providers::ProviderKind::ZaiCodingPlan
}) {
for model in &provider.models {
if data
.iter()
.any(|entry| entry.get("id").and_then(Value::as_str) == Some(model.as_str()))
{
data.push(json!({
"id": format!("{}/{}", provider.name, model),
"object": "model",
"owned_by": provider.name,
}));
continue;
}
data.push(json!({
"id": model,
"object": "model",
"owned_by": provider.name,
}));
}
}
}
pub(super) async fn append_zai_models(
state: &AppState,
claims: &crate::token::TokenClaims,
headers: &HeaderMap,
path: &str,
catalog: &mut Value,
) {
let Ok(Some(provider)) = crate::zai_coding_plan::resolve(state) else {
return;
};
let Ok((_, registry, _)) =
crate::zai_coding_plan::authorize_catalog(&provider, claims, headers, path)
else {
return;
};
if let Err(reason) = crate::zai_coding_plan::credential_healthy(&state.client, &provider).await
{
if let Some(object) = catalog.as_object_mut() {
let degraded = object
.entry("degraded_providers")
.or_insert_with(|| Value::Array(Vec::new()));
if let Some(entries) = degraded.as_array_mut()
&& !entries.iter().any(|entry| entry == "z.ai")
{
entries.push(Value::String("z.ai".into()));
}
let reasons = object
.entry("degraded_reasons")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Some(reasons) = reasons.as_object_mut() {
reasons.insert("z.ai".into(), Value::String(reason));
}
}
return;
}
let Some(data) = catalog.get_mut("data").and_then(Value::as_array_mut) else {
return;
};
for entry in registry {
if data
.iter()
.any(|model| model.get("id").and_then(Value::as_str) == Some(&entry.exposed_id))
{
continue;
}
data.push(json!({
"id": entry.exposed_id,
"object": "model",
"owned_by": entry.owner,
"display_name": entry.display_name,
}));
}
}
pub(super) fn stored_provider_for_model(
state: &AppState,
model: &str,
) -> Result<Option<crate::providers::ResolvedProvider>, ModelRouteError> {
if let Some((name, bare)) = model.split_once('/')
&& state
.provider_store
.get(name)
.is_ok_and(|provider| provider.is_some())
{
return match state.provider_store.resolve(name) {
Ok(Some(provider)) if provider.declares(bare) => Ok(Some(provider)),
Ok(Some(_)) => Err(ModelRouteError::NotFound(format!(
"provider '{name}' does not advertise model '{bare}'"
))),
_ => Ok(None),
};
}
let Ok(providers) = state.provider_store.list() else {
return Ok(None);
};
let mut declaring = providers
.into_iter()
.filter(|record| {
record.enabled
&& if record.kind == crate::providers::ProviderKind::ZaiCodingPlan {
crate::zai_coding_plan::canonical_for_any_client(&record.models, model)
.is_some()
} else {
record.models.iter().any(|id| id == model)
}
})
.map(|record| record.name);
let Some(first) = declaring.next() else {
return Ok(None);
};
if let Some(second) = declaring.next() {
return Err(ModelRouteError::Ambiguous(format!(
"model '{model}' is declared by multiple stored providers ({first}, {second}); name \
one as '<provider>/{model}' to disambiguate"
)));
}
Ok(state.provider_store.resolve(&first).ok().flatten())
}
pub(super) fn route_stored_provider(
state: &AppState,
provider: &crate::providers::ResolvedProvider,
model: &str,
) -> AppState {
let mut routed = state.clone();
routed.upstream_provider = if provider.kind == crate::providers::ProviderKind::ZaiCodingPlan {
UpstreamProvider::ZaiCodingPlan
} else {
UpstreamProvider::OpenAICompatible
};
routed
.openai_compatible
.provider_name
.clone_from(&provider.name);
routed.bridge_model = Some(
if provider.kind == crate::providers::ProviderKind::ZaiCodingPlan {
crate::zai_coding_plan::canonical_for_any_client(&provider.models, model)
.unwrap_or_else(|| model.to_string())
} else {
bare_model_id(model).to_string()
},
);
routed
}
#[must_use]
pub fn bare_model_id(model: &str) -> &str {
model.split_once('/').map_or(model, |(_, bare)| bare)
}