use std::collections::HashMap;
use std::pin::Pin;
use futures::Stream;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ts_rs::TS;
use crate::error::AiMuxError;
use crate::language_model::LanguageModel;
use crate::language_model_message::convert_to_language_model_prompt;
use crate::message::{ModelMessage, ModelPrompt};
use crate::options::{CallOptions, ResponseFormat, ToolChoice};
use crate::result::{GenerateContent, GenerateResult, StreamResult};
use crate::stream_part::StreamPart;
use crate::tool::Tool;
use crate::types::{FinishReason, ReasoningEffort, Usage, Warning};
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct GenerateTextOptions {
pub max_output_tokens: Option<u32>,
pub temperature: Option<f64>,
pub stop_sequences: Option<Vec<String>>,
pub top_p: Option<f64>,
pub top_k: Option<f64>,
pub presence_penalty: Option<f64>,
pub frequency_penalty: Option<f64>,
pub response_format: Option<ResponseFormat>,
pub seed: Option<u64>,
pub tools: Option<Vec<Tool>>,
pub tool_choice: Option<ToolChoice>,
pub headers: Option<HashMap<String, String>>,
pub provider_options: Option<HashMap<String, Value>>,
pub reasoning: Option<ReasoningEffort>,
pub instructions: Option<String>,
pub body_overrides: Option<Value>,
pub max_retries: Option<u32>,
pub timeout: Option<crate::options::TimeoutConfiguration>,
#[serde(skip)]
#[ts(skip)]
pub abort_signal: Option<crate::shared::AbortSignal>,
}
impl GenerateTextOptions {
pub fn into_call_options(
self,
prompt: crate::language_model_message::LanguageModelPrompt,
) -> CallOptions {
CallOptions {
prompt,
max_output_tokens: self.max_output_tokens,
temperature: self.temperature,
stop_sequences: self.stop_sequences,
top_p: self.top_p,
top_k: self.top_k,
presence_penalty: self.presence_penalty,
frequency_penalty: self.frequency_penalty,
response_format: self.response_format,
seed: self.seed,
tools: self.tools,
tool_choice: self.tool_choice.unwrap_or_default(),
headers: self.headers,
provider_options: self.provider_options,
reasoning: self.reasoning,
body_overrides: self.body_overrides,
max_retries: self.max_retries,
timeout: self.timeout,
abort_signal: self.abort_signal,
}
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct GenerateTextResult {
pub text: String,
pub tool_calls: Vec<crate::tool::ToolCall>,
pub finish_reason: FinishReason,
pub usage: Usage,
pub warnings: Vec<Warning>,
pub raw: GenerateResult,
}
pub struct StreamTextResult {
pub stream: Pin<Box<dyn Stream<Item = Result<StreamPart, AiMuxError>> + Send>>,
}
impl std::fmt::Debug for StreamTextResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamTextResult")
.field("stream", &"<stream>")
.finish()
}
}
impl StreamTextResult {
pub async fn text(self) -> Result<String, AiMuxError> {
use futures::StreamExt;
let mut result = String::new();
let mut stream = self.stream;
while let Some(part) = stream.next().await {
match part? {
StreamPart::TextDelta { delta, .. } => result.push_str(&delta),
StreamPart::Finish { .. } => break,
StreamPart::Error { error } => return Err(error),
_ => {}
}
}
Ok(result)
}
}
pub async fn generate_text(
model: &dyn LanguageModel,
prompt: impl Into<ModelPrompt>,
options: GenerateTextOptions,
) -> Result<GenerateTextResult, AiMuxError> {
let (messages, instructions) = split_prompt(prompt.into(), options.instructions.as_deref());
let lm_prompt = convert_to_language_model_prompt(&messages, instructions);
let call_options = options.into_call_options(lm_prompt);
let result = model.do_generate(&call_options).await?;
let mut text = String::new();
let mut tool_calls = Vec::new();
for content in &result.content {
match content {
GenerateContent::Text { text: t, .. } => text.push_str(t),
GenerateContent::ToolCall {
tool_call_id,
tool_name,
input,
thought_signature,
..
} => {
tool_calls.push(crate::tool::ToolCall {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
input: input.clone(),
provider_executed: None,
dynamic: None,
thought_signature: thought_signature.clone(),
});
}
GenerateContent::Source { .. } => {}
GenerateContent::Reasoning { .. } => {}
GenerateContent::File { .. } => {}
GenerateContent::ToolResult { .. } => {}
}
}
Ok(GenerateTextResult {
text,
tool_calls,
finish_reason: result.finish_reason.clone(),
usage: result.usage.clone(),
warnings: result.warnings.clone(),
raw: result,
})
}
pub async fn stream_text(
model: &dyn LanguageModel,
prompt: impl Into<ModelPrompt>,
options: GenerateTextOptions,
) -> Result<StreamTextResult, AiMuxError> {
let (messages, instructions) = split_prompt(prompt.into(), options.instructions.as_deref());
let lm_prompt = convert_to_language_model_prompt(&messages, instructions);
let call_options = options.into_call_options(lm_prompt);
let result: StreamResult = model.do_stream(&call_options).await?;
Ok(StreamTextResult {
stream: result.stream,
})
}
fn split_prompt(
prompt: ModelPrompt,
instructions: Option<&str>,
) -> (Vec<ModelMessage>, Option<&str>) {
match prompt {
ModelPrompt::Text(text) => (vec![ModelMessage::user(text)], instructions),
ModelPrompt::Messages(msgs) => (msgs, instructions),
}
}