Skip to main content

harness_core/
model.rs

1use crate::{Context, error::ModelError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4
5/// Information about a configured model — uniform across providers.
6///
7/// `handle` is the user-chosen logical identifier (used in logs, metrics,
8/// and `harness.toml` selectors); `model` is the wire-protocol model id
9/// sent to the provider.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ModelInfo {
12    pub handle: String,
13    pub provider: String,
14    pub model: String,
15    pub context_window: u32,
16    pub input_cost_usd_per_million_tokens: Option<f64>,
17    pub output_cost_usd_per_million_tokens: Option<f64>,
18    pub supports_tool_use: bool,
19    pub supports_streaming: bool,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ModelOutput {
24    pub text: Option<String>,
25    pub tool_calls: Vec<ToolCall>,
26    pub usage: Usage,
27    pub stop_reason: StopReason,
28    /// Provider-specific reasoning trace (DeepSeek `reasoning_content`,
29    /// Anthropic `thinking` blocks). Pushed back to the API verbatim on
30    /// subsequent calls; required by providers that gate on it.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub reasoning: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ToolCall {
37    pub id: String,
38    pub name: String,
39    pub args: serde_json::Value,
40}
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct Usage {
44    pub input_tokens: u32,
45    pub output_tokens: u32,
46    pub cached_input_tokens: u32,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51#[non_exhaustive]
52pub enum StopReason {
53    EndTurn,
54    ToolUse,
55    MaxTokens,
56    StopSequence,
57    Other,
58}
59
60/// Streaming delta — incremental output from the model.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[non_exhaustive]
63pub enum ModelDelta {
64    Text(String),
65    ToolCallStart { id: String, name: String },
66    ToolCallArgs { id: String, partial_json: String },
67    ToolCallEnd { id: String },
68    Usage(Usage),
69    Stop(StopReason),
70}
71
72#[async_trait]
73pub trait Model: Send + Sync + 'static {
74    async fn complete(&self, ctx: &Context) -> Result<ModelOutput, ModelError>;
75
76    /// Streaming is optional; default implementation falls back to `complete`.
77    async fn stream(
78        &self,
79        ctx: &Context,
80    ) -> Result<futures::stream::BoxStream<'static, Result<ModelDelta, ModelError>>, ModelError>
81    {
82        let out = self.complete(ctx).await?;
83        let deltas: Vec<Result<ModelDelta, ModelError>> = out
84            .text
85            .into_iter()
86            .map(|t| Ok(ModelDelta::Text(t)))
87            .chain(std::iter::once(Ok(ModelDelta::Stop(out.stop_reason))))
88            .collect();
89        Ok(Box::pin(futures::stream::iter(deltas)))
90    }
91
92    fn info(&self) -> ModelInfo;
93}