use super::*;
pub struct ModelRequestTargetInput<'a> {
pub provider_plugin_id: Option<&'a str>,
pub selected_model_id: Option<&'a str>,
pub provider_context: &'a bcode_model::ProviderRequestContext,
pub native_default: bool,
}
#[derive(Debug, Clone)]
pub struct ResolvedModelRequestTarget {
pub provider_plugin_id: Option<String>,
pub requested_model_id: Option<String>,
pub model_id: String,
pub provider_context: bcode_model::ProviderRequestContext,
pub cache: bcode_model::ModelCacheInfo,
pub max_output_tokens: Option<u32>,
pub pricing: Option<bcode_model::ModelPricingInfo>,
pub catalog_provider_id: Option<String>,
pub pricing_target: Option<Box<bcode_session_models::SessionPricingTarget>>,
pub catalog_identity: Option<bcode_model_catalog::ModelCatalogIdentity>,
}
impl ResolvedModelRequestTarget {
pub(super) fn effective_max_output_tokens(
&self,
provider_context: &bcode_model::ProviderRequestContext,
) -> Option<u32> {
model_metadata_override(provider_context, &self.model_id)
.max_output_tokens
.or(self.max_output_tokens)
}
}
#[derive(Debug, Error)]
pub enum ModelRequestTargetError {
#[error("failed to resolve provider models: {0}")]
ModelList(String),
#[error("model provider has no usable models")]
NoUsableModel,
#[error("managed default authentication unavailable")]
ManagedAuthentication,
}
pub async fn resolve_model_request_target(
state: &ServerState,
input: ModelRequestTargetInput<'_>,
) -> Result<ResolvedModelRequestTarget, ModelRequestTargetError> {
let mut provider_context = managed_default_routing::refresh(
state,
input.provider_plugin_id,
input.provider_context,
input.native_default,
)
.await?;
let models = resolved_provider_models(
state,
input.provider_plugin_id.map(ToOwned::to_owned),
bcode_model::ModelListRequest {
provider_context: provider_context.clone(),
selected_model_id: input.selected_model_id.map(ToOwned::to_owned),
},
)
.await
.map_err(ModelRequestTargetError::ModelList)?;
let model = models
.models
.iter()
.find(|model| model.is_default)
.or_else(|| models.models.first())
.ok_or(ModelRequestTargetError::NoUsableModel)?;
let model_id = model.model_id.clone();
let cache = model.cache.clone();
let catalog_provider_id = catalog_provider_id_for_policy(&models.catalog.policy);
let max_output_tokens = resolve_request_max_output_tokens(
&state.model_catalog,
&models.catalog.policy,
&model_id,
model.max_output_tokens,
)
.await;
let pricing = model.pricing.clone();
let api_surface = resolve_request_api_surface(
&state.model_catalog,
&models.catalog.policy,
&model_id,
model.api_surface,
input.provider_context.api_surface,
)
.await;
provider_context.api_surface = api_surface;
select_host_auth_pool_candidate(state, input.provider_plugin_id, &mut provider_context).await;
let catalog_identity = if let Some(provider_id) = catalog_provider_id.as_deref() {
state
.model_catalog
.model_identity(provider_id, &model_id)
.await
} else {
None
};
let support_hint = match &models.catalog.policy {
bcode_model::ModelCatalogPolicy::ExpandSupported { target, .. } => Some(target),
bcode_model::ModelCatalogPolicy::EnrichOnly { target, .. } => target.as_ref(),
_ => None,
};
let pricing_target = support_hint
.map(|hint| bcode_session_models::SessionPricingTarget {
provider: hint.provider.clone(),
auth_mode: hint.auth_mode.clone(),
api_surface: hint.api_surface.clone(),
integration: hint.integration.clone(),
})
.map(Box::new);
Ok(ResolvedModelRequestTarget {
pricing_target,
provider_plugin_id: input.provider_plugin_id.map(ToOwned::to_owned),
requested_model_id: input.selected_model_id.map(ToOwned::to_owned),
model_id,
provider_context,
cache,
max_output_tokens,
pricing,
catalog_provider_id,
catalog_identity,
})
}
async fn resolve_request_max_output_tokens(
catalog: &bcode_model_catalog::ModelCatalogResolver,
policy: &bcode_model::ModelCatalogPolicy,
model_id: &str,
resolved_limit: Option<u32>,
) -> Option<u32> {
if resolved_limit.is_some_and(|limit| limit > 0) {
return resolved_limit;
}
let provider_id = catalog_provider_id_for_policy(policy)?;
catalog
.model_max_output_tokens(&provider_id, model_id)
.await
}
async fn resolve_request_api_surface(
catalog: &bcode_model_catalog::ModelCatalogResolver,
policy: &bcode_model::ModelCatalogPolicy,
model_id: &str,
resolved_surface: Option<bcode_model::ModelApiSurface>,
existing_surface: Option<bcode_model::ModelApiSurface>,
) -> Option<bcode_model::ModelApiSurface> {
if resolved_surface.is_some() {
return resolved_surface;
}
let Some(catalog_provider_id) = catalog_provider_id_for_policy(policy) else {
return existing_surface;
};
catalog
.model_api_surface(&catalog_provider_id, model_id)
.await
.or(existing_surface)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn request_limit_falls_back_to_canonical_fable_catalog_alias() {
let catalog = bcode_model_catalog::ModelCatalogResolver::embedded();
let policy = bcode_model::ModelCatalogPolicy::ExpandSupported {
provider_id: "bedrock".to_string(),
target: bcode_model::ModelCatalogSupportHint {
provider: "bedrock".to_string(),
auth_mode: "bearer_token".to_string(),
api_surface: "messages".to_string(),
integration: Some("bcode".to_string()),
},
authority: bcode_model::ModelListAuthority::Authoritative,
};
assert_eq!(
resolve_request_max_output_tokens(
&catalog,
&policy,
"global.anthropic.claude-fable-5-1",
None,
)
.await,
Some(128_000)
);
assert_eq!(
resolve_request_max_output_tokens(
&catalog,
&policy,
"global.anthropic.claude-fable-5-1",
Some(64_000),
)
.await,
Some(64_000)
);
}
#[test]
fn resolved_target_owns_output_limit_after_alias_or_default_resolution() {
let mut target = ResolvedModelRequestTarget {
provider_plugin_id: Some("bcode.bedrock".to_string()),
requested_model_id: None,
model_id: "global.anthropic.claude-fable-5-1".to_string(),
provider_context: bcode_model::ProviderRequestContext::default(),
cache: bcode_model::ModelCacheInfo::default(),
max_output_tokens: Some(128_000),
pricing: None,
catalog_provider_id: Some("bedrock".to_string()),
catalog_identity: None,
pricing_target: None,
};
let mut context = bcode_model::ProviderRequestContext::default();
assert_eq!(target.effective_max_output_tokens(&context), Some(128_000));
context.settings.insert(
"model_metadata.global.anthropic.claude-fable-5-1.max_output_tokens".to_string(),
"64000".to_string(),
);
assert_eq!(target.effective_max_output_tokens(&context), Some(64_000));
target.max_output_tokens = None;
assert_eq!(target.effective_max_output_tokens(&context), Some(64_000));
}
#[tokio::test]
async fn catalog_surface_overrides_stale_context_and_unknown_models_keep_context() {
let catalog = bcode_model_catalog::ModelCatalogResolver::embedded();
let policy = bcode_model::ModelCatalogPolicy::ExpandAll {
provider_id: "bedrock".to_string(),
};
let known = resolve_request_api_surface(
&catalog,
&policy,
"openai.gpt-5.6-sol",
None,
Some(bcode_model::ModelApiSurface::Messages),
)
.await;
let unknown = resolve_request_api_surface(
&catalog,
&policy,
"openai.unknown-future-model",
None,
Some(bcode_model::ModelApiSurface::Messages),
)
.await;
assert_eq!(known, Some(bcode_model::ModelApiSurface::Responses));
assert_eq!(unknown, Some(bcode_model::ModelApiSurface::Messages));
}
}