Skip to main content

deepstrike_sdk/providers/
mod.rs

1use async_trait::async_trait;
2use compact_str::CompactString;
3use deepstrike_core::context::renderer::InternalRenderedContext;
4use deepstrike_core::runtime::session::ProviderReplay;
5use deepstrike_core::types::message::{Content, CoreMessage, 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    /// Stable, credential-free route evidence used to freeze Context execution identity.
80    /// Custom providers may override this with their model, protocol and endpoint identity.
81    fn context_route(&self) -> serde_json::Value {
82        serde_json::json!({ "kind": "opaque", "implementation": std::any::type_name::<Self>() })
83    }
84
85    /// Freeze the material covered by a Context request fingerprint before I/O.
86    /// The default scope is logical adapter input. Providers with hidden encoding state must
87    /// override this and `stream_prepared` to consume the same frozen encoded request.
88    fn prepare_context_request(
89        &self,
90        context: &InternalRenderedContext,
91        tools: &[ToolSchema],
92        extensions: Option<&serde_json::Value>,
93        state: Option<&ProviderRunState>,
94    ) -> crate::Result<serde_json::Value> {
95        Ok(serde_json::json!({
96            "scope": "adapter_input", "context": context, "tools": tools,
97            "extensions": extensions, "state": state,
98        }))
99    }
100
101    /// Dispatch previously frozen material. The default supports stateless logical adapters;
102    /// encoded-request providers override this to avoid re-encoding after evidence is recorded.
103    async fn stream_prepared(
104        &self,
105        _prepared: &serde_json::Value,
106        context: &InternalRenderedContext,
107        tools: &[ToolSchema],
108        extensions: Option<&serde_json::Value>,
109        state: Option<&ProviderRunState>,
110    ) -> crate::Result<Box<dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin>> {
111        self.stream(context, tools, extensions, state).await
112    }
113
114    /// Optional per-run state for protocol-native continuation (e.g. Responses API).
115    fn create_run_state(&self) -> Option<ProviderRunState> {
116        None
117    }
118
119    /// Per-model runtime policy. Overridden by RuntimeOptions fields when set.
120    fn runtime_policy(&self) -> RuntimePolicy {
121        RuntimePolicy::default()
122    }
123
124    fn peek_provider_replay(
125        &self,
126        _content: &str,
127        _tool_calls: &[ToolCall],
128    ) -> Option<ProviderReplay> {
129        None
130    }
131
132    fn seed_provider_replay(
133        &self,
134        _content: &str,
135        _tool_calls: &[ToolCall],
136        _replay: &ProviderReplay,
137    ) {
138    }
139
140    fn commit_stream_replay(&self, _content: &str, _tool_calls: &[ToolCall]) {}
141
142    /// Non-streaming completion — default collects from `stream`.
143    async fn complete(
144        &self,
145        context: &InternalRenderedContext,
146        tools: &[ToolSchema],
147        extensions: Option<&serde_json::Value>,
148    ) -> crate::Result<CoreMessage> {
149        let mut stream = self.stream(context, tools, extensions, None).await?;
150        collect_message_from_stream(&mut stream).await
151    }
152
153    async fn stream(
154        &self,
155        context: &InternalRenderedContext,
156        tools: &[ToolSchema],
157        extensions: Option<&serde_json::Value>,
158        state: Option<&ProviderRunState>,
159    ) -> crate::Result<Box<dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin>>;
160}
161
162pub async fn collect_message_from_stream(
163    stream: &mut (dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin),
164) -> crate::Result<CoreMessage> {
165    let mut content = String::new();
166    let mut tool_calls = Vec::new();
167    while let Some(evt) = stream.next().await {
168        match evt? {
169            StreamEvent::TextDelta { delta } => content.push_str(&delta),
170            StreamEvent::ThinkingDelta { .. } => {}
171            StreamEvent::ToolCall {
172                id,
173                name,
174                arguments,
175            } => {
176                tool_calls.push(ToolCall {
177                    id: CompactString::new(&id),
178                    name: CompactString::new(&name),
179                    arguments,
180                });
181            }
182            StreamEvent::Usage { .. } | StreamEvent::Done => {}
183        }
184    }
185    Ok(CoreMessage {
186        role: Role::Assistant,
187        content: Content::Text(content),
188        tool_calls,
189    })
190}
191
192/// Token consumption for a single LLM call.
193#[derive(Debug, Clone, Default)]
194pub struct TokenUsage {
195    /// Full prompt size: uncached input + cache reads + cache writes.
196    pub input_tokens: u32,
197    pub output_tokens: u32,
198    /// Prompt tokens served from cache (billed ~0.1x). Subset of input_tokens.
199    pub cache_read_input_tokens: u32,
200    /// Prompt tokens written to cache (billed ~1.25x). Subset of input_tokens.
201    pub cache_creation_input_tokens: u32,
202}
203
204impl TokenUsage {
205    pub fn total_tokens(&self) -> u32 {
206        self.input_tokens + self.output_tokens
207    }
208}
209
210/// A tool specification in provider-facing format (parameters as a parsed JSON value).
211#[derive(Debug, Clone)]
212pub struct ProviderToolSpec {
213    pub name: String,
214    pub description: String,
215    pub parameters: serde_json::Value,
216}