refactor: introduce LlmClient trait (no-op)
Preparation for a second LLM backend (OpenRouter) and hybrid vision-local / chat-remote mode. Shared wire types (ChatMessage, Tool, ToolCall, etc.) move into a new src/ai/llm_client.rs and are re-exported from ollama.rs so existing imports keep working. OllamaClient now implements LlmClient. No behavior change; callers still hold the concrete OllamaClient. Caller migration to Arc<dyn LlmClient> is deferred to the PR that wires hybrid backend routing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
702aa8078c
commit
0073409b3d
5 files changed
+197
-92
No files matched your search
@@ -0,0 +1,140 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Provider-agnostic surface for LLM backends (Ollama, OpenRouter, …).
|
||||
///
|
||||
/// Impls translate these canonical shapes at the wire boundary: tool-call
|
||||
/// arguments stay as `serde_json::Value` in memory and are stringified only
|
||||
/// when a provider requires it (OpenAI-compatible APIs do), and `images`
|
||||
/// stays as base64 strings here and is rewritten into content-parts where
|
||||
/// needed.
|
||||
// First consumer lands in a later PR (OpenRouter impl + hybrid mode routing).
|
||||
#[allow(dead_code)]
|
||||
#[async_trait]
|
||||
pub trait LlmClient: Send + Sync {
|
||||
/// Single-shot text generation. Optional system prompt and optional
|
||||
/// base64 images (ignored by providers without vision support).
|
||||
async fn generate(
|
||||
&self,
|
||||
prompt: &str,
|
||||
system: Option<&str>,
|
||||
images: Option<Vec<String>>,
|
||||
) -> Result<String>;
|
||||
|
||||
/// Multi-turn chat with tool definitions. Returns the assistant message
|
||||
/// (which may contain tool_calls) plus optional prompt/eval token counts.
|
||||
async fn chat_with_tools(
|
||||
&self,
|
||||
messages: Vec<ChatMessage>,
|
||||
tools: Vec<Tool>,
|
||||
) -> Result<(ChatMessage, Option<i32>, Option<i32>)>;
|
||||
|
||||
/// Batch embedding generation. Dimensionality is provider/model specific.
|
||||
async fn generate_embeddings(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>>;
|
||||
|
||||
/// One-shot vision description of an image. Used to convert images into
|
||||
/// plain text for the hybrid-mode conversation flow.
|
||||
async fn describe_image(&self, image_base64: &str) -> Result<String>;
|
||||
|
||||
/// Enumerate available models with their capabilities.
|
||||
async fn list_models(&self) -> Result<Vec<ModelCapabilities>>;
|
||||
|
||||
/// Look up capabilities for a single model.
|
||||
async fn model_capabilities(&self, model: &str) -> Result<ModelCapabilities>;
|
||||
|
||||
/// Primary model identifier this client was constructed with.
|
||||
fn primary_model(&self) -> &str;
|
||||
}
|
||||
|
||||
/// Tool definition sent to the model (OpenAI-compatible function schema).
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Tool {
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String, // always "function"
|
||||
pub function: ToolFunction,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct ToolFunction {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: serde_json::Value,
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
pub fn function(name: &str, description: &str, parameters: serde_json::Value) -> Self {
|
||||
Self {
|
||||
tool_type: "function".to_string(),
|
||||
function: ToolFunction {
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
parameters,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A message in the chat conversation history.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ChatMessage {
|
||||
pub role: String, // "system" | "user" | "assistant" | "tool"
|
||||
/// Empty string (not null) when tool_calls is present — Ollama quirk.
|
||||
#[serde(default)]
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
/// Base64 images — only on user messages to vision-capable models.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub images: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
pub fn system(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "system".to_string(),
|
||||
content: content.into(),
|
||||
tool_calls: None,
|
||||
images: None,
|
||||
}
|
||||
}
|
||||
pub fn user(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "user".to_string(),
|
||||
content: content.into(),
|
||||
tool_calls: None,
|
||||
images: None,
|
||||
}
|
||||
}
|
||||
pub fn tool_result(content: impl Into<String>) -> Self {
|
||||
Self {
|
||||
role: "tool".to_string(),
|
||||
content: content.into(),
|
||||
tool_calls: None,
|
||||
images: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool call returned by the model in an assistant message.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ToolCall {
|
||||
pub function: ToolCallFunction,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ToolCallFunction {
|
||||
pub name: String,
|
||||
/// Canonical shape: native JSON. Providers that use JSON-encoded-string
|
||||
/// arguments (OpenAI-compatible) translate at their wire boundary.
|
||||
pub arguments: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ModelCapabilities {
|
||||
pub name: String,
|
||||
pub has_vision: bool,
|
||||
pub has_tool_calling: bool,
|
||||
}
|
||||
Reference in new issue
Block a user