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