pub mod anthropic;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolSpec {
pub name: String,
pub description: String,
pub input_schema: Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: Value,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolResult {
pub id: String,
pub ok: bool,
pub content: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Message {
System(String),
User(Vec<ContentBlock>),
Assistant(Vec<ContentBlock>),
ToolResult(ToolResult),
}
impl Message {
pub fn user_text(content: impl Into<String>) -> Self {
Self::User(vec![ContentBlock::Text(content.into())])
}
pub fn assistant_text(content: impl Into<String>) -> Self {
Self::Assistant(vec![ContentBlock::Text(content.into())])
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ContentBlock {
Text(String),
Image(ImageContent),
ToolCall(ToolCall),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ImageContent {
pub data: String,
pub mime_type: String,
}
#[derive(Clone, Debug)]
pub struct ModelRequest {
pub messages: Vec<Message>,
pub tools: Vec<ToolSpec>,
pub prompt_cache_key: Option<String>,
}
#[derive(Clone, Debug)]
pub enum ModelResponse {
Assistant(Vec<ContentBlock>),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ModelUsage {
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
pub cache_read_tokens: Option<u64>,
pub cache_write_tokens: Option<u64>,
pub total_tokens: Option<u64>,
pub context_window: Option<u64>,
pub cost_usd_micros: Option<u64>,
}
impl ModelUsage {
pub fn total_input_tokens(&self) -> Option<u64> {
let has_input = self.input_tokens.is_some()
|| self.cache_read_tokens.is_some()
|| self.cache_write_tokens.is_some();
let total = self
.input_tokens
.unwrap_or_default()
.saturating_add(self.cache_read_tokens.unwrap_or_default())
.saturating_add(self.cache_write_tokens.unwrap_or_default());
has_input.then_some(total)
}
}
#[derive(Clone, Debug)]
pub enum ModelEvent {
OutputDelta(String),
ReasoningDelta(String),
WebSearch(String),
Usage(ModelUsage),
}
#[derive(Debug, Error)]
pub enum ModelError {
#[error("missing OpenAI API key; run /login openai in the TUI or set OPENAI_API_KEY as a CI/dev override")]
MissingApiKey,
#[error("missing Codex OAuth credentials; run /login openai-codex in the TUI or set CODEX_ACCESS_TOKEN as a CI/dev override")]
MissingCodexAuth,
#[error("missing Anthropic API key; run /login anthropic in the TUI or set ANTHROPIC_API_KEY as a CI/dev override")]
MissingAnthropicApiKey,
#[error("missing GitHub Copilot credentials; run /login github-copilot in the TUI or set GITHUB_COPILOT_TOKEN as a CI/dev override")]
MissingGithubCopilotAuth,
#[error("credential store error: {0}")]
Credentials(String),
#[error("request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("request failed: HTTP {status}: {body}")]
HttpStatus {
status: reqwest::StatusCode,
body: String,
},
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("provider stream interrupted")]
Interrupted,
#[error("provider returned invalid response: {0}")]
InvalidResponse(String),
#[error("unsupported provider '{0}'")]
UnsupportedProvider(String),
}
impl ModelError {
pub fn credentials(error: impl std::fmt::Display) -> Self {
Self::Credentials(error.to_string())
}
}
#[async_trait::async_trait(?Send)]
pub trait ModelProvider: Send + Sync {
async fn send_turn(&self, request: ModelRequest) -> Result<ModelResponse, ModelError>;
async fn send_turn_stream(
&self,
request: ModelRequest,
on_event: &mut dyn FnMut(ModelEvent) -> Result<(), ModelError>,
) -> Result<ModelResponse, ModelError> {
let response = self.send_turn(request).await?;
let ModelResponse::Assistant(blocks) = response;
for block in blocks.iter() {
if let ContentBlock::Text(text) = block {
on_event(ModelEvent::OutputDelta(text.clone()))?;
}
}
Ok(ModelResponse::Assistant(blocks))
}
}
pub type DynModelProvider = Box<dyn ModelProvider>;
pub use anthropic::AnthropicProvider;