use allama::adapter::chat::Prepared;
use allama::prelude::*;
use allama::{
CapabilitySupport, ChatCapability, ChatValidation, FinishReason, ProviderData,
ProviderIdentity, ProviderOptions, Reasoning, RequestField, SingleResponseStream,
UnsupportedPolicy, Usage, ValidationCoverage,
};
use serde::{Deserialize, Serialize};
const NAMESPACE: &str = "echo";
const PROVIDER_NAME: &str = "echo";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct EchoOptions {
repeat: Option<u8>,
}
impl ProviderOptions for EchoOptions {
const NAMESPACE: &'static str = NAMESPACE;
}
struct EchoProvider;
impl ProviderIdentity for EchoProvider {
fn provider_name(&self) -> &str {
PROVIDER_NAME
}
fn reads_namespace(&self, namespace: &str) -> bool {
namespace == NAMESPACE
}
fn downcast_any(&self, id: std::any::TypeId) -> Option<&(dyn std::any::Any + Send + Sync)> {
allama::expose(self, id)
}
}
impl ChatProvider for EchoProvider {
type Stream = SingleResponseStream;
async fn chat(&self, request: &ChatRequest) -> allama::Result<ChatResponse> {
let mut prepared = allama::adapter::chat::prepare(self, request)?;
let text = encode(&mut prepared)?;
prepared.mark_sent();
let model = prepared.request().model.clone();
if !model.starts_with("echo-") {
return Err(Error::model_not_found(format!("unknown model `{model}`"))
.authored()
.with_provider(PROVIDER_NAME));
}
let reasoning = Reasoning::new("echoing").with_provider_data(ProviderData::new().with_raw(
NAMESPACE,
"signature",
serde_json::json!("sig-1"),
));
let response = ChatResponse::new(vec![
ContentBlock::Reasoning(reasoning),
ContentBlock::text(text),
])
.finish_reason(FinishReason::Stop)
.finish_reason_raw("end")
.usage(
Usage::new()
.input_tokens(3)
.output_tokens(5)
.reasoning_tokens(1)
.with_derived_total(),
)
.model(model)
.provider(PROVIDER_NAME)
.request_id("req-local");
Ok(prepared.finish(response))
}
async fn chat_stream(&self, request: &ChatRequest) -> allama::Result<Self::Stream> {
Ok(self.chat(request).await?.into_stream())
}
fn chat_capability(&self, _model: &str, capability: ChatCapability) -> CapabilitySupport {
match capability {
ChatCapability::Streaming | ChatCapability::ReasoningOutput => {
CapabilitySupport::Supported
}
ChatCapability::NativeStreaming
| ChatCapability::StructuredOutput
| ChatCapability::ReasoningConfig => CapabilitySupport::Unsupported,
_ => CapabilitySupport::Unknown,
}
}
fn validate_chat_request(&self, request: &ChatRequest) -> allama::Result<ChatValidation> {
let mut prepared = allama::adapter::chat::prepare_dry_run(self, request)?;
encode(&mut prepared)?;
Ok(prepared.into_validation(ValidationCoverage::Encoding))
}
fn validate_chat_stream_request(
&self,
request: &ChatRequest,
) -> allama::Result<ChatValidation> {
let mut prepared = allama::adapter::chat::prepare_dry_run(self, request)?;
prepared.preflight_stream(self);
encode(&mut prepared)?;
Ok(prepared.into_validation(ValidationCoverage::Encoding))
}
}
fn encode(prepared: &mut Prepared<'_>) -> allama::Result<String> {
let (request, degradations) = prepared.parts_mut();
if request.seed.is_some() {
degradations
.unsupported_hint(RequestField::Seed, "echo has no seed; `seed` was not sent")?;
}
if matches!(request.response_format, Some(ResponseFormat::Json)) {
return Err(degradations.unsupported(
RequestField::ResponseFormat,
"echo returns plain text; JSON mode is not supported",
));
}
let options: EchoOptions = request.resolve_option()?;
let last_user = request
.messages
.iter()
.rev()
.find_map(|m| m.as_user())
.map(|m| {
m.content
.to_parts()
.iter()
.filter_map(ContentPart::as_text)
.collect::<String>()
})
.unwrap_or_default();
Ok(last_user.repeat(usize::from(options.repeat.unwrap_or(1))))
}
#[tokio::main]
async fn main() -> allama::Result<()> {
let provider = EchoProvider;
let request = ChatRequest::new("echo-1")
.user("hi ")
.seed(7)
.reasoning(ReasoningEffort::High)
.with_options_json(NAMESPACE, serde_json::json!({"repeat": 2}));
let validation = provider.validate_chat_request(&request)?;
println!("validation warnings: {}", validation.warnings().len());
let response = provider.chat(&request).await?;
println!("text: {}", response.text_or_empty());
for warning in &response.warnings {
println!("warning: {warning}");
}
let strict = request.clone().unsupported_policy(UnsupportedPolicy::Error);
println!(
"strict policy: {}",
provider.chat(&strict).await.unwrap_err()
);
let json = ChatRequest::new("echo-1")
.user("hi")
.response_format(ResponseFormat::json_schema(
serde_json::json!({"type": "object"}),
));
println!(
"semantic field: {}",
provider.chat(&json).await.unwrap_err()
);
let unknown = ChatRequest::new("other").user("hi");
println!(
"error mapping: {}",
provider.chat(&unknown).await.unwrap_err()
);
Ok(())
}