use super::error::ChatError;
use super::wire::ChatResponse;
use super::OpenRouterChatClient;
use crate::errors::AppError;
use crate::retry::AttemptOutcome;
#[derive(Debug)]
pub struct ChatCompletion {
pub value: serde_json::Value,
pub cost_usd: f64,
pub finish_reason: Option<String>,
pub prompt_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
}
impl OpenRouterChatClient {
pub(super) fn finish_completion(
&self,
response: ChatResponse,
finish_reason: Option<String>,
prompt_tokens: Option<u32>,
completion_tokens: Option<u32>,
) -> Result<ChatCompletion, ChatError> {
let content = response
.choices
.into_iter()
.next()
.and_then(|c| c.message.content)
.filter(|c| !c.trim().is_empty())
.ok_or_else(|| {
AppError::Validation(crate::i18n::validation::model_no_structured_content(
&self.model,
))
})
.map_err(|e| {
ChatError::with_diagnostics(
e,
finish_reason.clone(),
prompt_tokens,
completion_tokens,
AttemptOutcome::Transient,
)
})?;
let value = crate::json_repair::repair_to_value(&content).map_err(|e| {
ChatError::with_diagnostics(
AppError::Validation(crate::i18n::validation::model_json_parse_failed(
&self.model,
&e,
)),
finish_reason.clone(),
prompt_tokens,
completion_tokens,
AttemptOutcome::Transient,
)
})?;
if !value.is_object() {
return Err(ChatError::with_diagnostics(
AppError::Validation(crate::i18n::validation::model_non_object_json(
&self.model,
json_shape_name(&value),
)),
finish_reason,
prompt_tokens,
completion_tokens,
AttemptOutcome::Transient,
));
}
let cost = response.usage.and_then(|u| u.cost).unwrap_or(0.0);
Ok(ChatCompletion {
value,
cost_usd: cost,
finish_reason,
prompt_tokens,
completion_tokens,
})
}
}
pub(super) fn grow_max_tokens(current: Option<u32>) -> u32 {
let base = current.unwrap_or(crate::constants::ENRICH_INITIAL_MAX_TOKENS);
base.saturating_mul(crate::constants::ENRICH_MAX_TOKENS_GROWTH_FACTOR)
.min(crate::constants::ENRICH_MAX_TOKENS_CEILING)
}
fn json_shape_name(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}