use std::num::NonZeroU32;
use promptforge_tool_picker::{Catalog, ToolDescriptor, ToolId as PickerToolId};
use serde_json::Value;
use crate::Result;
use crate::dialects::ToolDialectId;
mod error;
mod ids;
mod options;
mod resolver;
mod transport;
pub use error::{CompletionError, CompletionErrorKind};
pub use ids::{ModelCatalogError, ModelId, ModelIdError};
pub use options::{CompletionOptions, ModelDescriptor, TemperatureError, ThinkingMode};
pub(crate) use options::{
ModelBinding, ModelBindings, ModelInvocation, ModelNeedOpts, Temperature,
};
pub(crate) use resolver::PickerModelResolver;
pub use transport::fetch_model_catalog;
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct ModelCatalog {
models: Vec<ModelDescriptor>,
}
impl ModelCatalog {
pub fn new(
models: impl IntoIterator<Item = ModelDescriptor>,
) -> std::result::Result<ModelCatalog, ModelCatalogError> {
let models: Vec<ModelDescriptor> = models.into_iter().collect();
for (index, model) in models.iter().enumerate() {
if models[..index].iter().any(|prior| prior.id() == model.id()) {
return Err(ModelCatalogError::DuplicateId {
server: model.id().server().to_owned(),
name: model.id().name().to_owned(),
});
}
}
Ok(Self { models })
}
pub(crate) fn from_validated(models: Vec<ModelDescriptor>) -> ModelCatalog {
Self { models }
}
#[must_use]
pub fn empty() -> Self {
Self::from_validated(Vec::new())
}
#[must_use]
pub fn models(&self) -> &[ModelDescriptor] {
&self.models
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.models.is_empty()
}
#[must_use]
pub fn get(&self, id: &ModelId) -> Option<&ModelDescriptor> {
self.models.iter().find(|model| model.id() == id)
}
#[must_use]
pub fn contains(&self, id: &ModelId) -> bool {
self.get(id).is_some()
}
#[must_use]
pub(crate) fn filtered(&self, opts: &ModelNeedOpts) -> Vec<&ModelDescriptor> {
self.models
.iter()
.filter(|model| satisfies_constraints(model, opts))
.collect()
}
}
pub(crate) fn picker_catalog_from<'a>(
models: impl IntoIterator<Item = &'a ModelDescriptor>,
) -> Catalog {
Catalog::new(
models
.into_iter()
.map(|model| {
ToolDescriptor::new(
model_to_picker_id(model.id()),
model.description().to_owned(),
Value::Object(serde_json::Map::new()),
)
})
.collect(),
)
}
const PICKER_MODEL_LABEL: &str = "model";
const PICKER_ID_SEPARATOR: char = '\u{1e}';
fn model_to_picker_id(id: &ModelId) -> PickerToolId {
PickerToolId::new(
format!("{}{}{}", id.server(), PICKER_ID_SEPARATOR, id.name()),
PICKER_MODEL_LABEL,
)
}
pub(crate) fn model_from_picker_id(id: &PickerToolId) -> ModelId {
match id.server().split_once(PICKER_ID_SEPARATOR) {
Some((server, name)) if !server.is_empty() && !name.is_empty() => {
ModelId::from_validated(server, name)
}
_ => ModelId::from_validated(id.server(), id.name()),
}
}
pub(crate) trait ModelResolver: Send + Sync {
fn resolve(&self, description: &str, opts: &ModelNeedOpts) -> Result<ResolvedModel>;
}
impl<F> ModelResolver for F
where
F: Fn(&str, &ModelNeedOpts) -> Result<ResolvedModel> + Send + Sync,
{
fn resolve(&self, description: &str, opts: &ModelNeedOpts) -> Result<ResolvedModel> {
self(description, opts)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ResolvedModel {
pub(crate) id: ModelId,
pub(crate) invocation: ModelInvocation,
pub(crate) tool_dialect: ToolDialectId,
pub(crate) context: NonZeroU32,
}
fn satisfies_constraints(model: &ModelDescriptor, opts: &ModelNeedOpts) -> bool {
if let Some(min_context) = opts.context
&& model.context() < min_context
{
return false;
}
match opts.thinking {
Some(true) => matches!(
model.thinking(),
ThinkingMode::Switchable | ThinkingMode::Always
),
Some(false) => matches!(
model.thinking(),
ThinkingMode::Switchable | ThinkingMode::Never
),
None => true,
}
}
#[cfg(test)]
mod tests;