feat: simplify LlmProvider trait to single call_llm method

Replace the three-method LlmProvider trait (generate_search_pass,
generate_rewrite_pass, supports_web_search) and ProviderCapabilities
with a single call_llm method. Update all three provider implementations
(Gemini, OpenAI, Anthropic) and all callers in synthesis.rs,
source_scraper.rs, and api_keys.rs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent e4b76fb06a
commit a2fe3f3310

@ -142,7 +142,7 @@ pub async fn test_key(
let test_model = get_default_model_for_provider(&state, &provider).await?; let test_model = get_default_model_for_provider(&state, &provider).await?;
let result = llm_provider let result = llm_provider
.generate_rewrite_pass( .call_llm(
&test_model, &test_model,
"You are a test assistant. Respond in JSON as instructed.", "You are a test assistant. Respond in JSON as instructed.",
"Say hello in one word.", "Say hello in one word.",

@ -1,8 +1,8 @@
//! Anthropic LLM provider implementation. //! Anthropic LLM provider implementation.
//! //!
//! Implements the `LlmProvider` trait using the Anthropic Messages API. //! Implements the `LlmProvider` trait using the Anthropic Messages API.
//! - **Pass 1 (search)**: Messages API with `web_search_20250305` tool //! Uses the Messages API without web search tools; structured output is
//! - **Pass 2 (rewrite)**: Messages API without tools, JSON via prompt instructions //! enforced via schema instructions embedded in the system prompt.
use async_trait::async_trait; use async_trait::async_trait;
use serde_json::Value; use serde_json::Value;
@ -16,9 +16,6 @@ const ANTHROPIC_VERSION: &str = "2023-06-01";
/// Default max tokens for Anthropic responses. /// Default max tokens for Anthropic responses.
const DEFAULT_MAX_TOKENS: u32 = 16384; const DEFAULT_MAX_TOKENS: u32 = 16384;
/// Maximum web search uses per request.
const WEB_SEARCH_MAX_USES: u32 = 10;
/// Anthropic provider. /// Anthropic provider.
/// ///
/// Holds the API key and an HTTP client for making requests /// Holds the API key and an HTTP client for making requests
@ -36,19 +33,20 @@ impl AnthropicProvider {
http_client, http_client,
} }
} }
}
#[async_trait]
impl LlmProvider for AnthropicProvider {
fn provider_id(&self) -> &str {
"anthropic"
}
/// Execute a request to the Anthropic Messages API. async fn call_llm(
///
/// Sends a POST to `https://api.anthropic.com/v1/messages` with the
/// appropriate headers and body. When `include_web_search` is true,
/// the `web_search_20250305` tool is included.
async fn call_messages_api(
&self, &self,
model: &str, model: &str,
system_prompt: &str, system_prompt: &str,
user_prompt: &str, user_prompt: &str,
response_schema: &Value, response_schema: &Value,
include_web_search: bool,
) -> Result<Value, AppError> { ) -> Result<Value, AppError> {
// Anthropic doesn't have native JSON schema enforcement like OpenAI/Gemini. // Anthropic doesn't have native JSON schema enforcement like OpenAI/Gemini.
// We embed the schema in the system prompt to instruct Claude to respond with // We embed the schema in the system prompt to instruct Claude to respond with
@ -62,7 +60,7 @@ impl AnthropicProvider {
let full_system_prompt = format!("{}{}", system_prompt, schema_instruction); let full_system_prompt = format!("{}{}", system_prompt, schema_instruction);
let mut body = serde_json::json!({ let body = serde_json::json!({
"model": model, "model": model,
"max_tokens": DEFAULT_MAX_TOKENS, "max_tokens": DEFAULT_MAX_TOKENS,
"system": full_system_prompt, "system": full_system_prompt,
@ -72,14 +70,6 @@ impl AnthropicProvider {
}] }]
}); });
if include_web_search {
body["tools"] = serde_json::json!([{
"type": "web_search_20250305",
"name": "web_search",
"max_uses": WEB_SEARCH_MAX_USES
}]);
}
let response = self let response = self
.http_client .http_client
.post("https://api.anthropic.com/v1/messages") .post("https://api.anthropic.com/v1/messages")
@ -115,39 +105,6 @@ impl AnthropicProvider {
} }
} }
#[async_trait]
impl LlmProvider for AnthropicProvider {
fn provider_id(&self) -> &str {
"anthropic"
}
async fn generate_search_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
self.call_messages_api(model, system_prompt, user_prompt, response_schema, true)
.await
}
async fn generate_rewrite_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
self.call_messages_api(model, system_prompt, user_prompt, response_schema, false)
.await
}
fn supports_web_search(&self) -> bool {
true
}
}
/// Extract the text content from an Anthropic Messages API response. /// Extract the text content from an Anthropic Messages API response.
/// ///
/// The response structure is: /// The response structure is:
@ -155,7 +112,6 @@ impl LlmProvider for AnthropicProvider {
/// { /// {
/// "content": [ /// "content": [
/// { "type": "text", "text": "..." }, /// { "type": "text", "text": "..." },
/// { "type": "web_search_tool_result", ... },
/// { "type": "text", "text": "{...json...}" } /// { "type": "text", "text": "{...json...}" }
/// ] /// ]
/// } /// }
@ -281,7 +237,6 @@ mod tests {
fn anthropic_provider_metadata() { fn anthropic_provider_metadata() {
let provider = AnthropicProvider::new("test-key".into(), reqwest::Client::new()); let provider = AnthropicProvider::new("test-key".into(), reqwest::Client::new());
assert_eq!(provider.provider_id(), "anthropic"); assert_eq!(provider.provider_id(), "anthropic");
assert!(provider.supports_web_search());
} }
// ── Content extraction ────────────────────────────────────── // ── Content extraction ──────────────────────────────────────
@ -307,30 +262,13 @@ mod tests {
} }
#[test] #[test]
fn extract_content_with_web_search_results() { fn extract_content_with_multiple_text_blocks() {
// When web_search is used, the response may contain tool results interleaved with text // When there are multiple text blocks, we take the last one
let response = serde_json::json!({ let response = serde_json::json!({
"content": [ "content": [
{ {
"type": "text", "type": "text",
"text": "Let me search for that information." "text": "Let me think about that."
},
{
"type": "server_tool_use",
"id": "srvtoolu_123",
"name": "web_search",
"input": { "query": "AI news this week" }
},
{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_123",
"content": [
{
"type": "web_search_result",
"url": "https://example.com/ai-news",
"title": "AI News"
}
]
}, },
{ {
"type": "text", "type": "text",

@ -60,21 +60,18 @@ mod tests {
fn factory_creates_gemini_provider() { fn factory_creates_gemini_provider() {
let provider = create_provider("gemini", "test-key".into()).unwrap(); let provider = create_provider("gemini", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "gemini"); assert_eq!(provider.provider_id(), "gemini");
assert!(provider.supports_web_search());
} }
#[test] #[test]
fn factory_creates_openai_provider() { fn factory_creates_openai_provider() {
let provider = create_provider("openai", "test-key".into()).unwrap(); let provider = create_provider("openai", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "openai"); assert_eq!(provider.provider_id(), "openai");
assert!(provider.supports_web_search());
} }
#[test] #[test]
fn factory_creates_anthropic_provider() { fn factory_creates_anthropic_provider() {
let provider = create_provider("anthropic", "test-key".into()).unwrap(); let provider = create_provider("anthropic", "test-key".into()).unwrap();
assert_eq!(provider.provider_id(), "anthropic"); assert_eq!(provider.provider_id(), "anthropic");
assert!(provider.supports_web_search());
} }
#[test] #[test]

@ -1,8 +1,7 @@
//! Google Gemini LLM provider implementation. //! Google Gemini LLM provider implementation.
//! //!
//! Implements the `LlmProvider` trait using the Gemini REST API. //! Implements the `LlmProvider` trait using the Gemini REST API.
//! Supports both web search grounding (Pass 1) and plain structured //! Uses the `generateContent` endpoint with structured JSON output.
//! output (Pass 2) via the `generateContent` endpoint.
use async_trait::async_trait; use async_trait::async_trait;
use serde_json::Value; use serde_json::Value;
@ -75,56 +74,25 @@ impl LlmProvider for GeminiProvider {
"gemini" "gemini"
} }
async fn generate_search_pass( async fn call_llm(
&self, &self,
model: &str, model: &str,
system_prompt: &str, system_prompt: &str,
user_prompt: &str, user_prompt: &str,
response_schema: &Value, response_schema: &Value,
) -> Result<Value, AppError> { ) -> Result<Value, AppError> {
let body = build_request_body( let body = build_request_body(system_prompt, user_prompt, response_schema);
system_prompt,
user_prompt,
response_schema,
true, // include googleSearch tool
);
self.generate_content(model, &body).await
}
async fn generate_rewrite_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
let body = build_request_body(
system_prompt,
user_prompt,
response_schema,
false, // no tools for rewrite
);
self.generate_content(model, &body).await self.generate_content(model, &body).await
} }
fn supports_web_search(&self) -> bool {
true
}
} }
/// Build the JSON request body for the Gemini `generateContent` endpoint. /// Build the JSON request body for the Gemini `generateContent` endpoint.
///
/// When `include_search` is true, the `googleSearch` tool is included
/// to enable web search grounding (Pass 1).
fn build_request_body( fn build_request_body(
system_prompt: &str, system_prompt: &str,
user_prompt: &str, user_prompt: &str,
response_schema: &Value, response_schema: &Value,
include_search: bool,
) -> Value { ) -> Value {
let mut body = serde_json::json!({ serde_json::json!({
"contents": [{ "contents": [{
"role": "user", "role": "user",
"parts": [{ "parts": [{
@ -141,15 +109,7 @@ fn build_request_body(
"responseSchema": response_schema, "responseSchema": response_schema,
"maxOutputTokens": 16384 "maxOutputTokens": 16384
} }
}); })
if include_search {
body["tools"] = serde_json::json!([{
"googleSearch": {}
}]);
}
body
} }
/// Extract the text content from a Gemini API response. /// Extract the text content from a Gemini API response.
@ -222,7 +182,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn build_request_body_with_search() { fn build_request_body_structure() {
let schema = serde_json::json!({ let schema = serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -233,7 +193,7 @@ mod tests {
} }
}); });
let body = build_request_body("system prompt", "user prompt", &schema, true); let body = build_request_body("system prompt", "user prompt", &schema);
// Verify contents // Verify contents
assert_eq!( assert_eq!(
@ -251,9 +211,8 @@ mod tests {
"system prompt" "system prompt"
); );
// Verify tools (googleSearch present) // No tools key
assert!(body["tools"].is_array()); assert!(body.get("tools").is_none());
assert!(body["tools"][0].get("googleSearch").is_some());
// Verify generation config // Verify generation config
assert_eq!( assert_eq!(
@ -263,15 +222,6 @@ mod tests {
assert!(body["generationConfig"]["responseSchema"].is_object()); assert!(body["generationConfig"]["responseSchema"].is_object());
} }
#[test]
fn build_request_body_without_search() {
let schema = serde_json::json!({"type": "object"});
let body = build_request_body("sys", "user", &schema, false);
// No tools key when search is disabled
assert!(body.get("tools").is_none());
}
#[test] #[test]
fn extract_content_valid_response() { fn extract_content_valid_response() {
let response = serde_json::json!({ let response = serde_json::json!({
@ -385,12 +335,11 @@ mod tests {
} }
#[test] #[test]
fn gemini_provider_supports_web_search() { fn gemini_provider_metadata() {
let provider = GeminiProvider::new( let provider = GeminiProvider::new(
"test-key".into(), "test-key".into(),
reqwest::Client::new(), reqwest::Client::new(),
); );
assert!(provider.supports_web_search());
assert_eq!(provider.provider_id(), "gemini"); assert_eq!(provider.provider_id(), "gemini");
} }
} }

@ -14,64 +14,27 @@ use serde_json::Value;
use crate::errors::AppError; use crate::errors::AppError;
/// Capabilities advertised by an LLM provider.
#[derive(Debug, Clone)]
pub struct ProviderCapabilities {
/// Whether the provider supports native web search grounding.
pub supports_web_search: bool,
/// Whether the provider supports structured output via JSON schema.
pub supports_structured_output: bool,
}
/// Trait defining the contract for LLM provider implementations. /// Trait defining the contract for LLM provider implementations.
/// ///
/// Each provider (Gemini, OpenAI, Anthropic) implements this trait /// Each provider (Gemini, OpenAI, Anthropic) implements this trait
/// to provide a unified interface for the synthesis generation pipeline. /// to provide a unified interface for structured LLM calls.
///
/// The pipeline uses two passes:
/// - **Search pass**: Generates content with web search grounding (if supported)
/// - **Rewrite pass**: Rewrites/consolidates content with structured output
#[async_trait] #[async_trait]
pub trait LlmProvider: Send + Sync { pub trait LlmProvider: Send + Sync {
/// Returns the provider identifier (e.g., "gemini", "openai", "anthropic"). /// Returns the provider identifier (e.g., "gemini", "openai", "anthropic").
fn provider_id(&self) -> &str; fn provider_id(&self) -> &str;
/// Generate content with web search grounding (Pass 1). /// Call the LLM with a prompt and expected JSON schema.
///
/// For providers that support native web search (e.g., Gemini with googleSearch),
/// this pass retrieves and structures information from the web.
///
/// # Arguments
/// * `model` — The model identifier (e.g., "gemini-2.5-pro")
/// * `system_prompt` — System-level instructions for the model
/// * `user_prompt` — The user's prompt with search criteria
/// * `response_schema` — JSON Schema defining the expected response structure
async fn generate_search_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError>;
/// Generate content without web search (Pass 2).
///
/// Used for rewriting, consolidating, or reformatting content
/// with structured output but no web search tools.
/// ///
/// # Arguments /// # Arguments
/// * `model` — The model identifier /// * `model` — The model identifier (e.g., "gpt-4o-mini")
/// * `system_prompt` — System-level instructions for the model /// * `system_prompt` — System-level instructions
/// * `user_prompt` — The user's prompt (typically includes content from Pass 1) /// * `user_prompt` — The user's prompt
/// * `response_schema` — JSON Schema defining the expected response structure /// * `response_schema` — JSON Schema defining the expected response structure
async fn generate_rewrite_pass( async fn call_llm(
&self, &self,
model: &str, model: &str,
system_prompt: &str, system_prompt: &str,
user_prompt: &str, user_prompt: &str,
response_schema: &Value, response_schema: &Value,
) -> Result<Value, AppError>; ) -> Result<Value, AppError>;
/// Whether this provider supports native web search grounding.
fn supports_web_search(&self) -> bool;
} }

@ -1,8 +1,7 @@
//! OpenAI LLM provider implementation. //! OpenAI LLM provider implementation.
//! //!
//! Implements the `LlmProvider` trait using two OpenAI APIs: //! Implements the `LlmProvider` trait using the OpenAI Responses API (`/v1/responses`)
//! - **Pass 1 (search)**: Responses API (`/v1/responses`) with `web_search_preview` tool //! with structured JSON output via `json_schema` text format.
//! - **Pass 2 (rewrite)**: Chat Completions API (`/v1/chat/completions`) with structured output
use async_trait::async_trait; use async_trait::async_trait;
use serde_json::Value; use serde_json::Value;
@ -13,7 +12,7 @@ use crate::errors::AppError;
/// OpenAI provider. /// OpenAI provider.
/// ///
/// Holds the API key and an HTTP client for making requests /// Holds the API key and an HTTP client for making requests
/// to the OpenAI Responses and Chat Completions APIs. /// to the OpenAI Responses API.
pub struct OpenAiProvider { pub struct OpenAiProvider {
api_key: String, api_key: String,
http_client: reqwest::Client, http_client: reqwest::Client,
@ -27,20 +26,22 @@ impl OpenAiProvider {
http_client, http_client,
} }
} }
}
#[async_trait]
impl LlmProvider for OpenAiProvider {
fn provider_id(&self) -> &str {
"openai"
}
/// Execute a request to the OpenAI Responses API (Pass 1). async fn call_llm(
///
/// Uses the Responses API with `web_search_preview` tool for grounded search results
/// and structured output via `json_schema` text format.
async fn call_responses_api(
&self, &self,
model: &str, model: &str,
system_prompt: &str, system_prompt: &str,
user_prompt: &str, user_prompt: &str,
response_schema: &Value, response_schema: &Value,
include_web_search: bool,
) -> Result<Value, AppError> { ) -> Result<Value, AppError> {
let mut body = serde_json::json!({ let body = serde_json::json!({
"model": model, "model": model,
"instructions": system_prompt, "instructions": system_prompt,
"input": user_prompt, "input": user_prompt,
@ -55,12 +56,6 @@ impl OpenAiProvider {
} }
}); });
if include_web_search {
body["tools"] = serde_json::json!([{
"type": "web_search_preview"
}]);
}
let response = self let response = self
.http_client .http_client
.post("https://api.openai.com/v1/responses") .post("https://api.openai.com/v1/responses")
@ -93,106 +88,6 @@ impl OpenAiProvider {
extract_responses_api_content(&response_body) extract_responses_api_content(&response_body)
} }
/// Execute a request to the OpenAI Chat Completions API (Pass 2).
///
/// Uses the Chat Completions API with `json_schema` response format
/// for structured output without web search.
async fn call_chat_completions_api(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
let body = serde_json::json!({
"model": model,
"messages": [
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": user_prompt
}
],
"max_tokens": 16384,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "synthesis",
"strict": true,
"schema": response_schema
}
}
});
let response = self
.http_client
.post("https://api.openai.com/v1/chat/completions")
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await
.map_err(|e| {
let kind = if e.is_timeout() {
"timeout"
} else if e.is_connect() {
"connection error"
} else {
"network error"
};
tracing::error!("OpenAI Chat Completions API request failed: {}", kind);
AppError::Internal(anyhow::anyhow!("Failed to connect to OpenAI API"))
})?;
let status = response.status();
let response_body: Value = response.json().await.map_err(|e| {
tracing::error!("Failed to parse OpenAI response body: {}", e);
AppError::Internal(anyhow::anyhow!("Failed to parse OpenAI API response"))
})?;
if !status.is_success() {
return Err(map_openai_error(status.as_u16(), &response_body));
}
extract_chat_completions_content(&response_body)
}
}
#[async_trait]
impl LlmProvider for OpenAiProvider {
fn provider_id(&self) -> &str {
"openai"
}
async fn generate_search_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
self.call_responses_api(model, system_prompt, user_prompt, response_schema, true)
.await
}
async fn generate_rewrite_pass(
&self,
model: &str,
system_prompt: &str,
user_prompt: &str,
response_schema: &Value,
) -> Result<Value, AppError> {
self.call_chat_completions_api(model, system_prompt, user_prompt, response_schema)
.await
}
fn supports_web_search(&self) -> bool {
true
}
} }
/// Extract the text content from an OpenAI Responses API response. /// Extract the text content from an OpenAI Responses API response.
@ -257,34 +152,6 @@ fn extract_responses_api_content(response: &Value) -> Result<Value, AppError> {
))) )))
} }
/// Extract the text content from an OpenAI Chat Completions API response.
///
/// The response structure is:
/// ```json
/// { "choices": [{ "message": { "content": "..." } }] }
/// ```
fn extract_chat_completions_content(response: &Value) -> Result<Value, AppError> {
let text = response
.get("choices")
.and_then(|c| c.get(0))
.and_then(|c| c.get("message"))
.and_then(|m| m.get("content"))
.and_then(|t| t.as_str())
.ok_or_else(|| {
tracing::error!("Unexpected OpenAI Chat Completions response structure");
AppError::Internal(anyhow::anyhow!(
"OpenAI Chat Completions API returned an unexpected response structure"
))
})?;
serde_json::from_str(text).map_err(|e| {
tracing::error!("Failed to parse OpenAI Chat Completions JSON output: {}", e);
AppError::Internal(anyhow::anyhow!(
"OpenAI returned invalid JSON in structured output"
))
})
}
/// Map OpenAI API error responses to appropriate `AppError` variants. /// Map OpenAI API error responses to appropriate `AppError` variants.
/// ///
/// Handles common error codes without exposing internal details. /// Handles common error codes without exposing internal details.
@ -325,13 +192,12 @@ fn map_openai_error(status: u16, body: &Value) -> AppError {
mod tests { mod tests {
use super::*; use super::*;
// ── Request body tests ────────────────────────────────────── // ── Provider metadata ───────────────────────────────────────
#[test] #[test]
fn openai_provider_metadata() { fn openai_provider_metadata() {
let provider = OpenAiProvider::new("test-key".into(), reqwest::Client::new()); let provider = OpenAiProvider::new("test-key".into(), reqwest::Client::new());
assert_eq!(provider.provider_id(), "openai"); assert_eq!(provider.provider_id(), "openai");
assert!(provider.supports_web_search());
} }
// ── Responses API response parsing ────────────────────────── // ── Responses API response parsing ──────────────────────────
@ -420,52 +286,6 @@ mod tests {
assert!(extract_responses_api_content(&response).is_err()); assert!(extract_responses_api_content(&response).is_err());
} }
// ── Chat Completions response parsing ───────────────────────
#[test]
fn extract_chat_completions_content_valid() {
let response = serde_json::json!({
"choices": [{
"message": {
"role": "assistant",
"content": "{\"category_0\": [{\"title\": \"Rewritten\", \"url\": \"https://example.com\", \"summary\": \"Rewritten summary\"}]}"
},
"finish_reason": "stop"
}]
});
let result = extract_chat_completions_content(&response).unwrap();
assert!(result["category_0"].is_array());
assert_eq!(
result["category_0"][0]["title"].as_str().unwrap(),
"Rewritten"
);
}
#[test]
fn extract_chat_completions_content_missing_choices() {
let response = serde_json::json!({});
assert!(extract_chat_completions_content(&response).is_err());
}
#[test]
fn extract_chat_completions_content_empty_choices() {
let response = serde_json::json!({"choices": []});
assert!(extract_chat_completions_content(&response).is_err());
}
#[test]
fn extract_chat_completions_content_invalid_json() {
let response = serde_json::json!({
"choices": [{
"message": {
"content": "this is not json"
}
}]
});
assert!(extract_chat_completions_content(&response).is_err());
}
// ── Error mapping tests ───────────────────────────────────── // ── Error mapping tests ─────────────────────────────────────
#[test] #[test]

@ -160,7 +160,7 @@ pub async fn extract_article_links_with_llm(
let (system, user) = build_link_extraction_prompt(&head_html, &body_html); let (system, user) = build_link_extraction_prompt(&head_html, &body_html);
let schema = build_link_extraction_schema(); let schema = build_link_extraction_schema();
match provider.generate_rewrite_pass(model, &system, &user, &schema).await { match provider.call_llm(model, &system, &user, &schema).await {
Ok(llm_response) => { Ok(llm_response) => {
let urls: Vec<String> = llm_response let urls: Vec<String> = llm_response
.get("urls") .get("urls")

@ -510,7 +510,7 @@ async fn run_generation_inner(
let llm_start = std::time::Instant::now(); let llm_start = std::time::Instant::now();
let class_response = provider let class_response = provider
.generate_rewrite_pass( .call_llm(
&model_research, &model_research,
&class_system, &class_system,
&class_user, &class_user,
@ -669,7 +669,7 @@ async fn run_generation_inner(
let llm_start = std::time::Instant::now(); let llm_start = std::time::Instant::now();
let raw_results = provider let raw_results = provider
.generate_search_pass(&model_research, &system_prompt, &user_prompt, &search_schema) .call_llm(&model_research, &system_prompt, &user_prompt, &search_schema)
.await?; .await?;
let llm_duration = llm_start.elapsed().as_millis() as u64; let llm_duration = llm_start.elapsed().as_millis() as u64;
log_llm_call(&state.pool, user_id, job_id, "search", &model_research, log_llm_call(&state.pool, user_id, job_id, "search", &model_research,
@ -815,7 +815,7 @@ async fn run_generation_inner(
let llm_start = std::time::Instant::now(); let llm_start = std::time::Instant::now();
let class_response = provider let class_response = provider
.generate_rewrite_pass( .call_llm(
&model_research, &model_research,
&class_system, &class_system,
&class_user, &class_user,
@ -916,7 +916,7 @@ async fn run_generation_inner(
let llm_start = std::time::Instant::now(); let llm_start = std::time::Instant::now();
let final_results = provider let final_results = provider
.generate_rewrite_pass(&model_writing, &rewrite_system, &rewrite_user, &rewrite_schema) .call_llm(&model_writing, &rewrite_system, &rewrite_user, &rewrite_schema)
.await?; .await?;
let llm_duration = llm_start.elapsed().as_millis() as u64; let llm_duration = llm_start.elapsed().as_millis() as u64;
log_llm_call(&state.pool, user_id, job_id, "rewrite", &model_writing, log_llm_call(&state.pool, user_id, job_id, "rewrite", &model_writing,
@ -1765,7 +1765,7 @@ async fn scrape_single_article_with_llm(
); );
let schema = crate::services::llm::schema::build_article_extraction_schema(); let schema = crate::services::llm::schema::build_article_extraction_schema();
match provider.generate_rewrite_pass(&model, &system, &user, &schema).await { match provider.call_llm(&model, &system, &user, &schema).await {
Ok(response) => { Ok(response) => {
let title = response.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string(); let title = response.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();
let body = response.get("body_text").and_then(|b| b.as_str()).unwrap_or("").to_string(); let body = response.get("body_text").and_then(|b| b.as_str()).unwrap_or("").to_string();

Loading…
Cancel
Save