Skip to main content

deepstrike_sdk/providers/
mod.rs

1use async_trait::async_trait;
2use compact_str::CompactString;
3use deepstrike_core::context::renderer::RenderedContext;
4use deepstrike_core::runtime::session::ProviderReplay;
5use deepstrike_core::types::message::{Content, Message, Role, ToolCall, ToolSchema};
6use futures::{Stream, StreamExt};
7
8pub mod anthropic;
9pub mod openai;
10pub mod provider_error;
11pub mod request_plan;
12
13pub use provider_error::{ProviderError, ProviderErrorKind};
14
15pub use request_plan::{
16    CostObservation, NormalizedProviderUsage, PricingRates, PricingSnapshot,
17    ProviderRequestEndpoint, ProviderRequestPlan, ProviderUsage, RecordedPromptMeasurement,
18    RequestPlanError, UnpricedReason, measurement_for_plan, normalize_provider_usage,
19    price_provider_usage, record_prompt_measurement,
20};
21
22/// Opaque per-run state owned by the provider (e.g. OpenAI Responses continuation).
23pub type ProviderRunState = serde_json::Value;
24
25/// Per-model execution policy returned by providers.
26/// Three-layer merge in RuntimeRunner: RuntimeOptions > provider > defaults.
27#[derive(Debug, Clone, Default)]
28pub struct RuntimePolicy {
29    pub max_turns: Option<u32>,
30    pub timeout_ms: Option<u64>,
31}
32
33/// Stream event emitted by providers.
34#[derive(Debug, Clone)]
35pub enum StreamEvent {
36    TextDelta {
37        delta: String,
38    },
39    ThinkingDelta {
40        delta: String,
41    },
42    ToolCall {
43        id: String,
44        name: String,
45        arguments: serde_json::Value,
46    },
47    /// Token usage from the provider (e.g. OpenAI `stream_options.include_usage`).
48    Usage {
49        total_tokens: u32,
50        /// Full prompt size: uncached input + cache reads + cache writes.
51        input_tokens: u32,
52        output_tokens: u32,
53        /// Prompt tokens served from cache (billed ~0.1x). Subset of input_tokens.
54        cache_read_input_tokens: u32,
55        /// Prompt tokens written to cache (billed ~1.25x). Subset of input_tokens.
56        cache_creation_input_tokens: u32,
57        /// I1: pro-rata per-slot attribution of `cache_read_input_tokens` (Anthropic only).
58        /// `None` when the provider doesn't honor `cache_control` or when no breakpoints were
59        /// placed. Estimated (Anthropic returns a single scalar) — see helper docs.
60        cache_read_input_tokens_by_slot: Option<CacheReadBySlot>,
61        /// Provider stop reason — `max_tokens` (Anthropic) / `length` (OpenAI) flag an output-cap
62        /// truncation that drives the kernel's max-output-tokens recovery. `None` when not reported.
63        stop_reason: Option<String>,
64    },
65    Done,
66}
67
68/// I1: per-slot attribution of Anthropic cache_read_input_tokens. Each field is `None` when that
69/// slot did not carry a `cache_control` breakpoint on the request. Mirrors the Node SDK shape.
70#[derive(Debug, Clone, Default)]
71pub struct CacheReadBySlot {
72    pub system: Option<u32>,
73    pub tools: Option<u32>,
74    pub messages: Option<u32>,
75}
76
77#[async_trait]
78pub trait LLMProvider: Send + Sync {
79    /// Optional per-run state for protocol-native continuation (e.g. Responses API).
80    fn create_run_state(&self) -> Option<ProviderRunState> {
81        None
82    }
83
84    /// Per-model runtime policy. Overridden by RuntimeOptions fields when set.
85    fn runtime_policy(&self) -> RuntimePolicy {
86        RuntimePolicy::default()
87    }
88
89    fn peek_provider_replay(
90        &self,
91        _content: &str,
92        _tool_calls: &[ToolCall],
93    ) -> Option<ProviderReplay> {
94        None
95    }
96
97    fn seed_provider_replay(
98        &self,
99        _content: &str,
100        _tool_calls: &[ToolCall],
101        _replay: &ProviderReplay,
102    ) {
103    }
104
105    fn commit_stream_replay(&self, _content: &str, _tool_calls: &[ToolCall]) {}
106
107    /// Non-streaming completion — default collects from `stream`.
108    async fn complete(
109        &self,
110        context: &RenderedContext,
111        tools: &[ToolSchema],
112        extensions: Option<&serde_json::Value>,
113    ) -> crate::Result<Message> {
114        let mut stream = self.stream(context, tools, extensions, None).await?;
115        collect_message_from_stream(&mut stream).await
116    }
117
118    async fn stream(
119        &self,
120        context: &RenderedContext,
121        tools: &[ToolSchema],
122        extensions: Option<&serde_json::Value>,
123        state: Option<&ProviderRunState>,
124    ) -> crate::Result<Box<dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin>>;
125}
126
127pub async fn collect_message_from_stream(
128    stream: &mut (dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin),
129) -> crate::Result<Message> {
130    let mut content = String::new();
131    let mut tool_calls = Vec::new();
132    while let Some(evt) = stream.next().await {
133        match evt? {
134            StreamEvent::TextDelta { delta } => content.push_str(&delta),
135            StreamEvent::ThinkingDelta { .. } => {}
136            StreamEvent::ToolCall {
137                id,
138                name,
139                arguments,
140            } => {
141                tool_calls.push(ToolCall {
142                    id: CompactString::new(&id),
143                    name: CompactString::new(&name),
144                    arguments,
145                });
146            }
147            StreamEvent::Usage { .. } | StreamEvent::Done => {}
148        }
149    }
150    Ok(Message {
151        role: Role::Assistant,
152        content: Content::Text(content),
153        tool_calls,
154        token_count: None,
155    })
156}
157
158/// Token consumption for a single LLM call.
159#[derive(Debug, Clone, Default)]
160pub struct TokenUsage {
161    /// Full prompt size: uncached input + cache reads + cache writes.
162    pub input_tokens: u32,
163    pub output_tokens: u32,
164    /// Prompt tokens served from cache (billed ~0.1x). Subset of input_tokens.
165    pub cache_read_input_tokens: u32,
166    /// Prompt tokens written to cache (billed ~1.25x). Subset of input_tokens.
167    pub cache_creation_input_tokens: u32,
168}
169
170impl TokenUsage {
171    pub fn total_tokens(&self) -> u32 {
172        self.input_tokens + self.output_tokens
173    }
174}
175
176/// A tool specification in provider-facing format (parameters as a parsed JSON value).
177#[derive(Debug, Clone)]
178pub struct ProviderToolSpec {
179    pub name: String,
180    pub description: String,
181    pub parameters: serde_json::Value,
182}