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    },
51    Done,
52}
53
54/// I1: per-slot attribution of Anthropic cache_read_input_tokens. Each field is `None` when that
55/// slot did not carry a `cache_control` breakpoint on the request. Mirrors the Node SDK shape.
56#[derive(Debug, Clone, Default)]
57pub struct CacheReadBySlot {
58    pub system: Option<u32>,
59    pub tools: Option<u32>,
60    pub messages: Option<u32>,
61}
62
63#[async_trait]
64pub trait LLMProvider: Send + Sync {
65    /// Optional per-run state for protocol-native continuation (e.g. Responses API).
66    fn create_run_state(&self) -> Option<ProviderRunState> {
67        None
68    }
69
70    /// Per-model runtime policy. Overridden by RuntimeOptions fields when set.
71    fn runtime_policy(&self) -> RuntimePolicy {
72        RuntimePolicy::default()
73    }
74
75    fn peek_provider_replay(
76        &self,
77        _content: &str,
78        _tool_calls: &[ToolCall],
79    ) -> Option<ProviderReplay> {
80        None
81    }
82
83    fn seed_provider_replay(
84        &self,
85        _content: &str,
86        _tool_calls: &[ToolCall],
87        _replay: &ProviderReplay,
88    ) {
89    }
90
91    fn commit_stream_replay(&self, _content: &str, _tool_calls: &[ToolCall]) {}
92
93    /// Non-streaming completion — default collects from `stream`.
94    async fn complete(
95        &self,
96        context: &RenderedContext,
97        tools: &[ToolSchema],
98        extensions: Option<&serde_json::Value>,
99    ) -> crate::Result<Message> {
100        let mut stream = self.stream(context, tools, extensions, None).await?;
101        collect_message_from_stream(&mut stream).await
102    }
103
104    async fn stream(
105        &self,
106        context: &RenderedContext,
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}
112
113pub async fn collect_message_from_stream(
114    stream: &mut (dyn Stream<Item = crate::Result<StreamEvent>> + Send + Unpin),
115) -> crate::Result<Message> {
116    let mut content = String::new();
117    let mut tool_calls = Vec::new();
118    while let Some(evt) = stream.next().await {
119        match evt? {
120            StreamEvent::TextDelta { delta } => content.push_str(&delta),
121            StreamEvent::ThinkingDelta { .. } => {}
122            StreamEvent::ToolCall {
123                id,
124                name,
125                arguments,
126            } => {
127                tool_calls.push(ToolCall {
128                    id: CompactString::new(&id),
129                    name: CompactString::new(&name),
130                    arguments,
131                });
132            }
133            StreamEvent::Usage { .. } | StreamEvent::Done => {}
134        }
135    }
136    Ok(Message {
137        role: Role::Assistant,
138        content: Content::Text(content),
139        tool_calls,
140        token_count: None,
141    })
142}
143
144/// Token consumption for a single LLM call.
145#[derive(Debug, Clone, Default)]
146pub struct TokenUsage {
147    /// Full prompt size: uncached input + cache reads + cache writes.
148    pub input_tokens: u32,
149    pub output_tokens: u32,
150    /// Prompt tokens served from cache (billed ~0.1x). Subset of input_tokens.
151    pub cache_read_input_tokens: u32,
152    /// Prompt tokens written to cache (billed ~1.25x). Subset of input_tokens.
153    pub cache_creation_input_tokens: u32,
154}
155
156impl TokenUsage {
157    pub fn total_tokens(&self) -> u32 {
158        self.input_tokens + self.output_tokens
159    }
160}
161
162/// A tool specification in provider-facing format (parameters as a parsed JSON value).
163#[derive(Debug, Clone)]
164pub struct ProviderToolSpec {
165    pub name: String,
166    pub description: String,
167    pub parameters: serde_json::Value,
168}