blob: 15e1b2d16786168d702503027375746b5e4c9355 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Compiled API provider policies use explicit definitions and credential contracts.
use crate::facts::{self, AuthKind, ProviderFacts};
use futures_util::future::BoxFuture;
use maka_agent_facts::configuration::ApiProtocol as Wire;
use maka_agent_facts::configuration::ModelInfo;
use maka_agent_facts::configuration::ModelOverride;
use maka_agent_ports::provider::Connection;
use maka_agent_ports::provider::Definition;
use maka_agent_ports::provider::Descriptor;
use maka_agent_ports::provider::Error;
use maka_agent_ports::provider::Model;
use maka_agent_ports::provider::Provider;
use maka_agent_ports::provider::Resolve;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
mod authentication;
mod discovery;
mod effects;
mod inventory;
mod metadata;
mod options;
mod output;
mod preferred;
mod route;
pub const ID: &str = "maka.providers";
/// Catalog metadata and configured execution choices remain distinct: `info`
/// contains model facts; thinking policy includes the user's overrides.
pub struct ModelMetadata {
pub info: ModelInfo,
pub thinking_levels: Vec<maka_agent_facts::execution::ThinkingLevel>,
pub default_thinking_level: Option<maka_agent_facts::execution::ThinkingLevel>,
}
pub struct ApiProviders;
impl ApiProviders {
/// A compiled API implementation for a native application owner. Availability
/// does not activate legacy contributions or grant execution authority.
pub fn definition(
id: &str,
snapshot: Arc<crate::catalog::Snapshot>,
) -> Result<Definition, String> {
Self::provider(id, snapshot)?.definition()
}
/// Selectable model metadata, checked by the same preparation policy as
/// execution. No credentials or network access are needed. An invalid
/// configured default is an error, never silently replaced by another level.
pub fn model_metadata(
id: &str,
snapshot: Arc<crate::catalog::Snapshot>,
connection: Connection,
model: ModelInfo,
overrides: Option<ModelOverride>,
) -> Result<ModelMetadata, String> {
let provider = Self::provider(id, snapshot)?;
let mut metadata = provider.model_metadata(model.clone(), overrides.as_ref());
let mut request = Resolve {
connection,
model,
overrides,
thinking_level: None,
};
provider
.prepare(request.clone())
.map_err(|error| error.to_string())?;
metadata.thinking_levels.retain(|level| {
request.thinking_level = Some(*level);
provider.prepare(request.clone()).is_ok()
});
Ok(metadata)
}
fn provider(id: &str, snapshot: Arc<crate::catalog::Snapshot>) -> Result<ApiProvider, String> {
let (id, facts) = facts::all()
.map_err(|error| error.to_string())?
.iter()
.find(|(name, facts)| {
name.as_str() == id && !facts.retired && facts.auth_kind != AuthKind::OauthToken
})
.ok_or("API provider is unavailable")?;
Ok(ApiProvider {
id: id.as_str(),
facts,
snapshot,
})
}
/// Available compiled API policies, without activating legacy contributions.
pub fn names() -> Result<Vec<String>, String> {
Ok(facts::all()
.map_err(|error| error.to_string())?
.iter()
.filter(|(_, facts)| !facts.retired && facts.auth_kind != AuthKind::OauthToken)
.map(|(name, _)| name.clone())
.collect())
}
}
#[derive(Clone)]
struct ApiProvider {
id: &'static str,
facts: &'static ProviderFacts,
snapshot: Arc<crate::catalog::Snapshot>,
}
#[derive(Deserialize, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Configuration {
#[schemars(length(min = 1))]
base_url: String,
}
impl Configuration {
fn read(connection: &Connection) -> Result<Self, Error> {
let mut configuration: Self = serde_json::from_value(connection.configuration.clone())
.map_err(|_| Error::InvalidConfiguration)?;
configuration.base_url = maka_agent_facts::configuration::validation::normalize_base_url(
&configuration.base_url,
)
.map_err(|_| Error::InvalidConfiguration)?;
Ok(configuration)
}
}
/// Pure policy input, not a Host catalog or a credential handle.
struct Selection<'a> {
provider: &'a str,
connection: &'a Connection,
base_url: &'a str,
model: &'a ModelInfo,
overrides: Option<&'a ModelOverride>,
}
impl ApiProvider {
fn definition(&self) -> Result<Definition, String> {
let authentication =
authentication::methods(self.facts.auth_kind).map_err(|error| error.to_string())?;
Definition::new(
Descriptor {
label: match self.id {
"openai-compatible" => "Chat Completions".into(),
"openai-responses-compatible" => "Responses".into(),
"anthropic-compatible" => "Anthropic Messages".into(),
_ => self.facts.label.clone(),
},
custom_endpoint: self.facts.base_url.is_empty(),
configuration_schema: serde_json::to_value(schemars::schema_for!(Configuration))
.map_err(|error| error.to_string())?,
configuration_defaults: if self.facts.base_url.is_empty() {
json!({})
} else {
json!({"baseUrl":self.facts.base_url})
},
authentication,
anonymous: matches!(
self.facts.auth_kind,
AuthKind::None | AuthKind::OptionalApiKey
),
discovery: true,
},
Arc::new(self.clone()),
)
.map_err(|error| error.to_string())
}
fn model_metadata(&self, model: ModelInfo, overrides: Option<&ModelOverride>) -> ModelMetadata {
let reported = metadata::enrich(&self.snapshot, self.id, model);
let info =
overrides.map_or_else(|| reported.clone(), |overrides| overrides.apply(&reported));
let thinking_levels = overrides
.and_then(|o| o.thinking_levels.clone())
.or_else(|| info.thinking_levels.clone())
.unwrap_or_default();
let default_thinking_level = overrides.and_then(|o| o.default_thinking_level).or(info
.default_thinking_level
.filter(|level| thinking_levels.contains(level)));
ModelMetadata {
info,
thinking_levels,
default_thinking_level,
}
}
fn prepare(&self, request: Resolve) -> Result<Model, Error> {
let configuration = Configuration::read(&request.connection)?;
let ModelMetadata {
info,
thinking_levels,
default_thinking_level,
} = self.model_metadata(request.model, request.overrides.as_ref());
let selection = Selection {
provider: self.id,
connection: &request.connection,
base_url: &configuration.base_url,
model: &info,
overrides: request.overrides.as_ref(),
};
let route = route::resolve(&selection, self.facts)?;
route.check_execution()?;
let level = request.thinking_level.or(default_thinking_level);
if level.is_some_and(|level| !thinking_levels.contains(&level)) {
return Err(Error::UnsupportedThinkingLevel);
}
let options = options::resolve(&selection, self.facts, &thinking_levels, level, &route)?;
let output = output::resolve(&selection, route.wire, &options)?;
let adapter = request
.overrides
.as_ref()
.and_then(|o| o.adapter.clone())
.unwrap_or_else(|| {
match route.wire {
Wire::OpenaiChatCompletions => "chat-completions",
Wire::OpenaiResponses => "responses",
Wire::AnthropicMessages => "anthropic-messages",
}
.into()
});
let model = Model {
adapter,
protocol: route.kind,
base_url: route.base_url,
info,
thinking_levels,
provider_options: options,
main_output_limit: output,
};
model.validate().map_err(|_| Error::InvalidPreparedModel)?;
Ok(model)
}
}
impl Provider for ApiProvider {
fn resolve(&self, request: Resolve) -> BoxFuture<'_, Result<Model, Error>> {
Box::pin(async move { self.prepare(request) })
}
fn authorize(
&self,
connection: Connection,
credential: Option<maka_agent_facts::provider::Credential>,
_: String,
) -> BoxFuture<'_, Result<maka_agent_ports::model::Credentials, Error>> {
Box::pin(async move {
Configuration::read(&connection)?;
authentication::authorize(self.facts.auth_kind, credential)
})
}
fn authenticate(
&self,
request: maka_agent_ports::provider::authentication::Authenticate,
_: maka_agent_ports::provider::Context,
) -> BoxFuture<'_, Result<maka_agent_facts::provider::Credential, Error>> {
Box::pin(async move {
Configuration::read(&request.connection)?;
authentication::authenticate(request)
})
}
fn discover(
&self,
request: maka_agent_ports::provider::Discovery,
context: maka_agent_ports::provider::Context,
) -> BoxFuture<'_, Result<Vec<ModelInfo>, Error>> {
Box::pin(async move { effects::discover(self, request, context).await })
}
fn verify(
&self,
request: maka_agent_ports::provider::Verification,
context: maka_agent_ports::provider::Context,
) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async move { effects::verify(self, request, context).await })
}
}