use rig::client::CompletionClient;
use rig::completion::Prompt;
use rig::providers::{anthropic, chatgpt, gemini, ollama, openai, openrouter};
use crate::agent::prompt;
use crate::session::SessionMessage;
use super::anthropic_http::AnthropicHttpClient;
use super::codex_http::CodexHttpClient;
use super::summarize;
const OPENAI_CODEX_OAUTH_DEFAULT_MODEL: &str = "gpt-5.5";
pub enum AnyClient {
OpenRouter(openrouter::Client),
OpenAI(openai::CompletionsClient),
ChatGptOpenAI(openai::Client<CodexHttpClient>),
OpenAICodex(chatgpt::Client),
Anthropic(anthropic::Client),
AnthropicOauth(anthropic::Client<AnthropicHttpClient>),
Gemini(gemini::Client),
DeepSeek(openai::CompletionsClient),
Glm(openai::CompletionsClient),
Ollama(ollama::Client),
Custom(openai::CompletionsClient),
}
impl AnyClient {
pub fn completion_model(&self, name: impl Into<String>) -> AnyModel {
let name = name.into();
match self {
AnyClient::OpenRouter(c) => AnyModel::OpenRouter(c.completion_model(name)),
AnyClient::OpenAI(c) => AnyModel::OpenAI(c.completion_model(name)),
AnyClient::ChatGptOpenAI(c) => {
AnyModel::ChatGptOpenAI(c.completion_model(codex_model_name(name)))
}
AnyClient::OpenAICodex(c) => {
AnyModel::OpenAICodex(c.completion_model(codex_model_name(name)))
}
AnyClient::Anthropic(c) => AnyModel::Anthropic(c.completion_model(name)),
AnyClient::AnthropicOauth(c) => AnyModel::AnthropicOauth(c.completion_model(name)),
AnyClient::Gemini(c) => AnyModel::Gemini(c.completion_model(name)),
AnyClient::DeepSeek(c) => AnyModel::DeepSeek(c.completion_model(name)),
AnyClient::Glm(c) => AnyModel::Glm(c.completion_model(name)),
AnyClient::Ollama(c) => AnyModel::Ollama(c.completion_model(name)),
AnyClient::Custom(c) => AnyModel::Custom(c.completion_model(name)),
}
}
}
pub(crate) fn build_compaction_prompt(
messages: &[SessionMessage],
previous_summary: Option<&str>,
instructions: Option<&str>,
) -> anyhow::Result<String> {
let conversation = summarize::serialize_conversation(messages);
let instructions_block = match instructions {
Some(text) if !text.trim().is_empty() => format!(
"FOCUS TOPIC: \"{}\"\n\
The user has requested that this compaction PRIORITISE preserving \
all information related to the focus topic above. For content \
related to \"{}\", include full detail — exact values, file paths, \
command outputs, error messages, and decisions. For content NOT \
related to the focus topic, summarise more aggressively. The \
focus topic sections should receive roughly 60-70% of the \
summary token budget. Even for the focus topic, NEVER preserve \
API keys, tokens, passwords, or credentials — use [REDACTED].",
text.trim(),
text.trim(),
),
_ => "(none)".to_string(),
};
let prev_summary_value = previous_summary.unwrap_or("(none)");
if prompt::input_contains_compaction_delimiter(&[
&conversation,
prev_summary_value,
&instructions_block,
]) {
tracing::warn!(
"compaction input contains the untrusted-material delimiter — \
skipping compaction this turn to avoid prompt-injection risk"
);
anyhow::bail!("compaction aborted: input contains reserved delimiter string");
}
Ok(prompt::COMPACTION_PROMPT
.replace("{conversation}", &conversation)
.replace("{previous_summary}", prev_summary_value)
.replace("{instructions}", &instructions_block))
}
pub(crate) async fn run_compaction(model: AnyModel, prompt_text: String) -> anyhow::Result<String> {
let response = summarize::summarize_with_model(model, prompt_text).await?;
Ok(prompt::strip_compaction_delimiters(&response))
}
fn codex_model_name(name: String) -> String {
if name == super::default_model_for("openai") {
OPENAI_CODEX_OAUTH_DEFAULT_MODEL.to_string()
} else {
name
}
}
#[derive(Clone)]
pub enum AnyModel {
OpenRouter(openrouter::completion::CompletionModel),
OpenAI(openai::completion::CompletionModel),
ChatGptOpenAI(openai::responses_api::ResponsesCompletionModel<CodexHttpClient>),
OpenAICodex(chatgpt::ResponsesCompletionModel),
Anthropic(anthropic::completion::CompletionModel),
AnthropicOauth(
anthropic::completion::CompletionModel<super::anthropic_http::AnthropicHttpClient>,
),
Gemini(gemini::completion::CompletionModel),
DeepSeek(openai::completion::CompletionModel),
Glm(openai::completion::CompletionModel),
Ollama(ollama::CompletionModel),
Custom(openai::completion::CompletionModel),
}
impl AnyModel {
pub async fn btw_query(&self, prompt: String) -> anyhow::Result<String> {
self.btw_query_with(prompt, None).await
}
pub async fn btw_query_with(
&self,
prompt: String,
preamble: Option<&str>,
) -> anyhow::Result<String> {
let preamble = preamble.unwrap_or("Answer the user's question concisely.");
use crate::agent::recovery::{RecoveryPolicy, run_with_retry};
let policy = RecoveryPolicy::default();
macro_rules! one_shot {
($m:expr) => {{
let m = $m.clone();
run_with_retry(&policy, "btw_query", || {
let agent = rig::agent::AgentBuilder::new(m.clone())
.preamble(preamble)
.build();
let prompt = prompt.clone();
async move { agent.prompt(prompt).await }
})
.await
.map_err(anyhow::Error::from)
}};
}
match self {
AnyModel::OpenRouter(m) => one_shot!(m),
AnyModel::OpenAI(m) => one_shot!(m),
AnyModel::ChatGptOpenAI(m) => one_shot!(m),
AnyModel::OpenAICodex(m) => one_shot!(m),
AnyModel::Anthropic(m) => one_shot!(m),
AnyModel::AnthropicOauth(m) => one_shot!(m),
AnyModel::Gemini(m) => one_shot!(m),
AnyModel::DeepSeek(m) => one_shot!(m),
AnyModel::Glm(m) => one_shot!(m),
AnyModel::Ollama(m) => one_shot!(m),
AnyModel::Custom(m) => one_shot!(m),
}
}
pub fn build_stream_fn(
&self,
tools: Vec<rig::completion::ToolDefinition>,
chunk_timeout: std::time::Duration,
provider_name: Option<String>,
) -> crate::agent::agent_loop::StreamFn {
self.build_stream_fn_with_filter(tools, chunk_timeout, provider_name, None)
}
pub fn build_stream_fn_with_filter(
&self,
tools: Vec<rig::completion::ToolDefinition>,
chunk_timeout: std::time::Duration,
provider_name: Option<String>,
tool_def_filter: Option<
std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
>,
) -> crate::agent::agent_loop::StreamFn {
crate::provider::stream_dispatch::dispatch_stream_fn! {
match self;
AnyModel(m) => m.clone(),
tools = tools,
timeout = Some(chunk_timeout),
provider = provider_name,
filter = tool_def_filter,
}
}
pub fn name(&self) -> String {
match self {
AnyModel::OpenRouter(m) => m.model.clone(),
AnyModel::OpenAI(m) => m.model.clone(),
AnyModel::ChatGptOpenAI(m) => m.model.clone(),
AnyModel::OpenAICodex(m) => m.model.clone(),
AnyModel::Anthropic(m) => m.model.clone(),
AnyModel::AnthropicOauth(m) => m.model.clone(),
AnyModel::Gemini(m) => m.model.clone(),
AnyModel::DeepSeek(m) => m.model.clone(),
AnyModel::Glm(m) => m.model.clone(),
AnyModel::Ollama(m) => m.model.clone(),
AnyModel::Custom(m) => m.model.clone(),
}
}
}
#[cfg(test)]
pub(crate) fn filter_tool_names<'a>(
all: impl Iterator<Item = &'a str>,
allowed: &[&str],
) -> Vec<String> {
all.filter(|n| allowed.contains(n))
.map(String::from)
.collect()
}