blob: 34299894a2485cc159777a341f1fe50f90b331dc [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.
*/
mod diagnostics;
pub(crate) mod oauth;
use futures_util::future::BoxFuture;
use maka_agent_engine::{ModelSource, PreparedModel, RunError};
use maka_agent_facts::composition::SourceRevision;
use maka_agent_facts::configuration::ModelCapabilities;
use maka_agent_facts::configuration::ModelInfo;
use maka_agent_facts::configuration::ModelSelection;
use maka_agent_facts::context::ModelRequestContext;
use maka_agent_facts::execution::EditingTools;
use maka_agent_facts::execution::ModelBinding;
use maka_agent_facts::execution::ThinkingLevel;
use maka_agent_facts::execution::ToolMode;
use maka_agent_ports::provider::Connection;
use maka_agent_ports::provider::Definition;
use maka_agent_ports::provider::Model;
use maka_agent_ports::provider::Resolve;
use maka_config::{ConfigurationStore, ConnectionObservation};
use maka_model::{ProviderAuth, ProviderConfig};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
use tracing::Instrument;
/// Only parameters actually supplied to execution. Catalog labels, tests and
/// other models are observations, not a mutable authority over an accepted run.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ExecutionBasis {
adapter: String,
protocol: maka_agent_facts::model::request::ProviderKind,
base_url: String,
capabilities: ModelCapabilities,
options: Value,
main_output_limit: Option<u64>,
body_overlay: Option<Value>,
}
impl ExecutionBasis {
fn new(model: &Model, row: &maka_agent_facts::configuration::ConnectionCatalogEntry) -> Self {
Self {
adapter: model.adapter.clone(),
protocol: model.protocol.clone(),
base_url: model.base_url.clone(),
capabilities: model.info.capabilities.unwrap_or_default(),
options: model.provider_options.clone(),
main_output_limit: model.main_output_limit,
body_overlay: row.request_body_overlay.clone(),
}
}
}
/// Accepted route facts, without credentials or live activation handles.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct AcceptedModel {
pub binding: ModelBinding,
pub thinking_level: Option<ThinkingLevel>,
/// Execution policy is captured at message admission. Later model settings
/// do not reinterpret queued work.
pub tool_mode: ToolMode,
#[serde(
default,
skip_serializing_if = "maka_agent_facts::execution::CodeLanguage::is_javascript"
)]
pub code_language: maka_agent_facts::execution::CodeLanguage,
/// None captures an unfiltered tool selection.
#[serde(deserialize_with = "Option::deserialize")]
pub editing_tools: Option<EditingTools>,
context: ModelRequestContext,
/// Public defaults at admission only; account reports and overrides stay live.
#[serde(deserialize_with = "Option::deserialize")]
public_model: Option<ModelInfo>,
provider: maka_agent_facts::provider::Identity,
provider_configuration: Value,
basis: ExecutionBasis,
}
#[derive(Clone)]
pub(crate) struct Credentials {
pub executor: maka_model::ModelExecutor,
pub refresh: Arc<oauth::Authority>,
}
pub(crate) struct Source {
configuration: Arc<ConfigurationStore>,
credentials: Credentials,
pub accepted: AcceptedModel,
definition: Arc<Definition>,
session: String,
revision: SourceRevision,
}
impl Source {
pub async fn select(
configuration: Arc<ConfigurationStore>,
catalog: Arc<maka_providers::catalog::Catalog>,
credentials: Credentials,
selection: ModelSelection,
thinking_level: Option<ThinkingLevel>,
session: String,
mut revision: SourceRevision,
) -> Result<(Self, PreparedModel), RunError> {
let span = tracing::warn_span!("agent_model_preparation", session_id = %session,
connection_id = %selection.connection_id, activation = %revision.activation);
async move {
let public_guard = catalog.snapshot.read().await;
let snapshot = public_guard.as_ref().map_err(|_| unavailable())?.clone();
let material = configuration
.observe_selected_model(selection.clone())
.await
.map_err(|error| {
diagnostics::configuration("selected_model_observation", &error, Some(&selection.connection_id));
unavailable()
})?
.ok_or_else(unavailable)?;
drop(public_guard);
let row = &material.connection;
let public_model = snapshot.get(&row.provider.name, &selection.model_id).cloned();
let snapshot = Arc::new(maka_providers::catalog::Snapshot::for_model(
&row.provider.name, &selection.model_id, public_model.clone(),
).map_err(|_| unavailable())?);
let definition =
Arc::new(maka_providers::definition(&row.provider, snapshot).map_err(|_| {
tracing::warn!(stage = "provider_definition", cause = "provider_definition_unavailable",
provider = %row.provider.name, "Model preparation failed");
unavailable()
})?);
let model = resolve(&definition, row, &selection.model_id, thinking_level).await?;
revision.name = row.provider.name.clone();
let source = Self {
configuration,
credentials,
accepted: AcceptedModel {
context: request_context(row, &model.info)?,
public_model,
editing_tools: Some(editing_tools(row, &selection.model_id, &model.base_url)),
tool_mode: ToolMode::for_model(
&selection.model_id,
&model.base_url,
row.model_overrides
.as_ref()
.and_then(|models| models.get(&selection.model_id))
.and_then(|overrides| overrides.code_mode),
),
code_language: row.model_overrides.as_ref()
.and_then(|models| models.get(&selection.model_id))
.and_then(|overrides| overrides.code_mode_language)
.unwrap_or_default(),
binding: ModelBinding {
connection_id: row.connection_id.clone(),
connection_slug: row.slug.clone(),
model: selection.model_id,
},
thinking_level,
provider: row.provider.clone(),
provider_configuration: row.configuration.clone(),
basis: ExecutionBasis::new(&model, row),
},
definition,
session,
revision,
};
let prepared = source.prepare(material, model).await?;
Ok((source, prepared))
}.instrument(span).await
}
pub fn restore(
configuration: Arc<ConfigurationStore>,
credentials: Credentials,
accepted: AcceptedModel,
session: String,
mut revision: SourceRevision,
) -> Result<Self, RunError> {
let _span = tracing::warn_span!("agent_model_preparation", session_id = %session,
connection_id = %accepted.binding.connection_id, activation = %revision.activation)
.entered();
let snapshot = maka_providers::catalog::Snapshot::for_model(
&accepted.provider.name,
&accepted.binding.model,
accepted.public_model.clone(),
)
.map_err(|_| unavailable())?;
let definition =
Arc::new(maka_providers::definition(&accepted.provider, Arc::new(snapshot)).map_err(|_| {
tracing::warn!(stage = "restore_provider_definition", cause = "provider_definition_unavailable",
provider = %accepted.provider.name, "Model preparation failed");
unavailable()
})?);
revision.name = accepted.provider.name.clone();
Ok(Self {
configuration,
credentials,
accepted,
definition,
session,
revision,
})
}
async fn prepare(
&self,
material: ConnectionObservation,
model: Model,
) -> Result<PreparedModel, RunError> {
let capabilities = model.info.capabilities.unwrap_or_default();
if capabilities.chat == Some(false) {
tracing::warn!(
stage = "model_capabilities",
cause = "chat_unsupported",
"Model preparation failed"
);
return Err(unavailable());
}
let row = &material.connection;
let network = maka_network::Policy::from_host_settings(
&material.network.proxy,
material.network.password.as_deref(),
)
.map_err(|error| {
diagnostics::network(
"network_policy",
&error,
&self.accepted.binding.connection_id,
);
unavailable()
})?;
let credential = match material.credential {
Some(snapshot) if snapshot.credential().refresh_at.is_some() => Some(
self.credentials
.refresh
.bind(snapshot)
.map_err(|_| RunError::InvalidInput("agent_authentication_unavailable".into()))?
.resolve_native(
self.accepted.provider.clone(),
self.definition.clone(),
maka_agent_ports::provider::Context {
transport: self.credentials.executor.transport(&network).map_err(
|error| {
diagnostics::model(
"credential_transport",
&error,
&self.accepted.binding.connection_id,
);
unavailable()
},
)?,
cancellation: tokio_util::sync::CancellationToken::new(),
interaction: None,
},
)
.await
.map_err(|_| RunError::InvalidInput("agent_authentication_unavailable".into()))?
.credential()
.clone(),
),
Some(snapshot) => Some(snapshot.credential().clone()),
None => None,
};
let credentials = self
.definition
.authorize(connection(row), credential, self.session.clone())
.await
.map_err(|error| {
diagnostics::provider(
"provider_authorize",
&error,
&row.connection_id,
&row.provider.name,
);
RunError::InvalidInput("agent_authentication_unavailable".into())
})?;
let auth = match credentials {
maka_agent_ports::model::Credentials::ApiKey(key) => ProviderAuth::ApiKey(key),
maka_agent_ports::model::Credentials::RequestHeaders(headers) => {
ProviderAuth::RequestHeaders(headers)
}
};
Ok(PreparedModel {
provider_id: self.accepted.provider.name.clone(),
provider: ProviderConfig {
adapter: Some(model.adapter),
capabilities,
kind: model.protocol,
model: self.accepted.binding.model.clone(),
base_url: model.base_url,
auth,
headers: material.request_headers,
network,
body_overlay: row
.request_body_overlay
.as_ref()
.map(|value| value.as_object().expect("validated overlay").clone()),
},
options: model.provider_options,
context: Some(self.accepted.context.clone()),
main_output_limit: model.main_output_limit,
supports_vision: capabilities.vision.unwrap_or(false),
revision: self.revision.clone(),
})
}
}
fn request_context(
row: &maka_agent_facts::configuration::ConnectionCatalogEntry,
model: &ModelInfo,
) -> Result<ModelRequestContext, RunError> {
if matches!((model.context_window, model.input_limit), (Some(capacity), Some(input)) if input > capacity)
{
tracing::warn!(stage = "request_context", cause = "input_limit_exceeds_context", connection_id = %row.connection_id, "Model preparation failed");
return Err(unavailable());
}
let input = match (model.context_window, model.input_limit) {
(Some(capacity), Some(input)) => Some(capacity.min(input)),
(capacity, input) => capacity.or(input),
};
let context = ModelRequestContext {
provider_id: row.provider.name.clone(),
context_window: input,
model_context_window: model.context_window,
declared_window: row
.model_overrides
.as_ref()
.and_then(|models| models.get(&model.id))
.and_then(|profile| profile.compaction_threshold)
.or_else(|| input.map(|limit| ((u128::from(limit) * 85 / 100) as u64).max(1))),
};
context.validate().map_err(|_| {
tracing::warn!(stage = "request_context", cause = "request_context_invalid", connection_id = %row.connection_id, "Model preparation failed");
unavailable()
})?;
Ok(context)
}
impl Source {
fn preparation_span(&self) -> tracing::Span {
tracing::warn_span!("agent_model_preparation", session_id = %self.session,
connection_id = %self.accepted.binding.connection_id, activation = %self.revision.activation,
provider = %self.accepted.provider.name)
}
async fn observe(&self) -> Result<(ConnectionObservation, Model), RunError> {
let material = self
.configuration
.observe_model(self.accepted.binding.clone())
.await
.map_err(|error| {
diagnostics::configuration(
"accepted_model_observation",
&error,
Some(&self.accepted.binding.connection_id),
);
unavailable()
})?
.ok_or_else(|| {
tracing::warn!(
stage = "accepted_model_observation",
cause = "accepted_source_unavailable",
"Model preparation failed"
);
unavailable()
})?;
let row = &material.connection;
if row.provider != self.accepted.provider {
tracing::warn!(
stage = "accepted_model_observation",
cause = "accepted_provider_changed",
"Model preparation failed"
);
return Err(unavailable());
}
if row.configuration != self.accepted.provider_configuration {
tracing::warn!(
stage = "accepted_model_observation",
cause = "accepted_configuration_changed",
"Model preparation failed"
);
return Err(unavailable());
}
let model = resolve(
&self.definition,
row,
&self.accepted.binding.model,
self.accepted.thinking_level,
)
.await?;
if ExecutionBasis::new(&model, row) != self.accepted.basis {
tracing::warn!(
stage = "accepted_model_observation",
cause = "accepted_execution_basis_changed",
"Model preparation failed"
);
return Err(unavailable());
}
Ok((material, model))
}
/// The original accepted route, with the least declared reasoning for a short auxiliary task.
pub(crate) async fn capture_auxiliary(&self) -> Result<PreparedModel, RunError> {
async {
let (material, model) = self.observe().await?;
let least = ThinkingLevel::ALL
.into_iter()
.find(|level| model.thinking_levels.contains(level));
let model = match least {
Some(level) => {
resolve(
&self.definition,
&material.connection,
&self.accepted.binding.model,
Some(level),
)
.await?
}
None => model,
};
self.prepare(material, model).await
}
.instrument(self.preparation_span())
.await
}
}
impl ModelSource for Source {
fn capture(&self) -> BoxFuture<'_, Result<PreparedModel, RunError>> {
Box::pin(
async move {
let (material, model) = self.observe().await?;
self.prepare(material, model).await
}
.instrument(self.preparation_span()),
)
}
}
/// Resolve tool compatibility without preparing credentials or starting execution.
pub(crate) async fn candidate_editing_tools(
configuration: &Arc<ConfigurationStore>,
catalog: &maka_providers::catalog::Catalog,
selection: ModelSelection,
thinking_level: Option<ThinkingLevel>,
) -> Result<EditingTools, RunError> {
let public_guard = catalog.snapshot.read().await;
let snapshot = public_guard.as_ref().map_err(|_| unavailable())?.clone();
let material = configuration
.observe_selected_model(selection.clone())
.await
.map_err(|error| {
diagnostics::configuration(
"candidate_model_observation",
&error,
Some(&selection.connection_id),
);
unavailable()
})?
.ok_or_else(unavailable)?;
drop(public_guard);
let row = &material.connection;
let definition = maka_providers::definition(&row.provider, snapshot).map_err(|_| {
tracing::warn!(stage = "provider_definition", cause = "provider_definition_unavailable", connection_id = %row.connection_id,
provider = %row.provider.name, "Model preparation failed");
unavailable()
})?;
let model = resolve(&definition, row, &selection.model_id, thinking_level).await?;
Ok(editing_tools(row, &selection.model_id, &model.base_url))
}
fn editing_tools(
row: &maka_agent_facts::configuration::ConnectionCatalogEntry,
model: &str,
base_url: &str,
) -> EditingTools {
EditingTools::for_model(
model,
base_url,
row.model_overrides
.as_ref()
.and_then(|models| models.get(model))
.and_then(|overrides| overrides.apply_patch),
)
}
async fn resolve(
definition: &Definition,
row: &maka_agent_facts::configuration::ConnectionCatalogEntry,
model: &str,
thinking_level: Option<ThinkingLevel>,
) -> Result<Model, RunError> {
definition
.prepare(Resolve {
connection: connection(row),
model: row
.models
.iter()
.find(|info| info.id == model)
.cloned()
.unwrap_or_else(|| ModelInfo::new(model)),
overrides: row
.model_overrides
.as_ref()
.and_then(|models| models.get(model))
.cloned(),
thinking_level,
})
.await
.map_err(|error| {
diagnostics::provider(
"provider_prepare",
&error,
&row.connection_id,
&row.provider.name,
);
unavailable()
})
}
fn connection(row: &maka_agent_facts::configuration::ConnectionCatalogEntry) -> Connection {
Connection {
id: row.connection_id.clone(),
revision: row.revision,
configuration: row.configuration.clone(),
}
}
fn unavailable() -> RunError {
RunError::InvalidInput("agent_model_unavailable".into())
}
/// Capture the actual Root rate decision for every physical provider request.
pub(crate) struct Prices(pub Arc<ConfigurationStore>);
impl maka_agent_engine::pricing::Pricing for Prices {
fn quote<'a>(
&'a self,
provider: &'a str,
model: &'a str,
) -> BoxFuture<'a, Result<maka_agent_facts::pricing::Quote, RunError>> {
Box::pin(
async move {
self.0
.quote_model(provider.into(), model.into())
.await
.map_err(|error| {
diagnostics::configuration("model_pricing", &error, None);
match error {
maka_config::ConfigError::CommitUnknown => {
maka_agent_facts::event::CommitError::OutcomeUnknown(
error.to_string(),
)
.into()
}
_ => RunError::Internal("model pricing unavailable".into()),
}
})
}
.instrument(tracing::warn_span!(
"agent_model_pricing",
provider,
model_id = model
)),
)
}
}