Skip to main content

stakpak_api/
lib.rs

1use async_trait::async_trait;
2use futures_util::Stream;
3use models::*;
4use reqwest::header::HeaderMap;
5use rmcp::model::Content;
6use stakpak_shared::models::integrations::openai::{
7    ChatCompletionResponse, ChatCompletionStreamResponse, ChatMessage, Tool,
8};
9use uuid::Uuid;
10
11pub mod client;
12pub mod commands;
13pub mod local;
14pub mod models;
15pub mod stakpak;
16pub mod storage;
17
18// Re-export unified AgentClient as the primary client
19pub use client::{AgentClient, AgentClientConfig, DEFAULT_STAKPAK_ENDPOINT, StakpakConfig};
20
21// Re-export Model types from stakai
22pub use stakai::{Model, ModelCost, ModelLimit};
23
24// Re-export storage types
25pub use storage::{
26    BackendInfo, BackendKind, BoxedSessionStorage, Checkpoint, CheckpointState, CheckpointSummary,
27    CreateCheckpointRequest, CreateSessionRequest as StorageCreateSessionRequest,
28    CreateSessionResult, ListCheckpointsQuery, ListCheckpointsResult, ListSessionsQuery,
29    ListSessionsResult, LocalStorage, Session, SessionStats, SessionStatus, SessionStorage,
30    SessionSummary, SessionVisibility, StakpakStorage, StorageError,
31    UpdateSessionRequest as StorageUpdateSessionRequest,
32};
33
34/// Find a model by ID string
35///
36/// Parses the model string and searches the model cache:
37/// - Format "provider/model_id" searches within that specific provider
38/// - Plain "model_id" searches all providers
39///
40/// When `use_stakpak` is true, the model is transformed for Stakpak API routing.
41pub fn find_model(model_str: &str, use_stakpak: bool) -> Option<Model> {
42    const PROVIDERS: &[&str] = &["anthropic", "openai", "google"];
43
44    let (provider_hint, model_id) = parse_model_string(model_str);
45
46    // Search with provider hint first, then fall back to searching all
47    let model = provider_hint
48        .and_then(|p| find_in_provider(p, model_id))
49        .or_else(|| {
50            PROVIDERS
51                .iter()
52                .find_map(|&p| find_in_provider(p, model_id))
53        })?;
54
55    Some(if use_stakpak {
56        transform_for_stakpak(model)
57    } else {
58        model
59    })
60}
61
62/// Parse "provider/model_id" or plain "model_id"
63#[allow(clippy::string_slice)] // idx from find('/') on same string, '/' is ASCII
64fn parse_model_string(s: &str) -> (Option<&str>, &str) {
65    match s.find('/') {
66        Some(idx) => {
67            let provider = &s[..idx];
68            let model_id = &s[idx + 1..];
69            let normalized = match provider {
70                "gemini" => "google",
71                p => p,
72            };
73            (Some(normalized), model_id)
74        }
75        None => (None, s),
76    }
77}
78
79/// Find a model by ID within a specific provider
80fn find_in_provider(provider_id: &str, model_id: &str) -> Option<Model> {
81    let models = stakai::load_models_for_provider(provider_id).ok()?;
82
83    // Try exact match first
84    if let Some(model) = models.iter().find(|m| m.id == model_id) {
85        return Some(model.clone());
86    }
87
88    // Try prefix match (e.g., "gpt-5.2-2026-01-15" matches catalog's "gpt-5.2")
89    // Find the longest matching prefix
90    let mut best_match: Option<&Model> = None;
91    let mut best_len = 0;
92
93    for model in &models {
94        if model_id.starts_with(&model.id) && model.id.len() > best_len {
95            best_match = Some(model);
96            best_len = model.id.len();
97        }
98    }
99
100    best_match.cloned()
101}
102
103/// Transform a model for Stakpak API routing
104///
105/// Changes the model's provider to "stakpak" and prefixes the model ID
106/// with the original provider name for routing purposes.
107pub fn transform_for_stakpak(model: Model) -> Model {
108    Model {
109        id: format!("{}/{}", model.provider, model.id),
110        provider: "stakpak".into(),
111        name: model.name,
112        reasoning: model.reasoning,
113        cost: model.cost,
114        limit: model.limit,
115        release_date: model.release_date,
116    }
117}
118
119/// Unified agent provider trait.
120///
121/// Extends `SessionStorage` so that any `AgentProvider` can also manage
122/// sessions and checkpoints.  This avoids passing two separate trait
123/// objects through the CLI call-chain.
124#[async_trait]
125pub trait AgentProvider: SessionStorage + Send + Sync {
126    // Account
127    async fn get_my_account(&self) -> Result<GetMyAccountResponse, String>;
128    async fn get_billing_info(
129        &self,
130        account_username: &str,
131    ) -> Result<stakpak_shared::models::billing::BillingResponse, String>;
132
133    // Rulebooks
134    async fn list_rulebooks(&self) -> Result<Vec<ListRuleBook>, String>;
135    async fn get_rulebook_by_uri(&self, uri: &str) -> Result<RuleBook, String>;
136    async fn create_rulebook(
137        &self,
138        uri: &str,
139        description: &str,
140        content: &str,
141        tags: Vec<String>,
142        visibility: Option<RuleBookVisibility>,
143    ) -> Result<CreateRuleBookResponse, String>;
144    async fn delete_rulebook(&self, uri: &str) -> Result<(), String>;
145
146    // Chat
147    async fn chat_completion(
148        &self,
149        model: Model,
150        messages: Vec<ChatMessage>,
151        tools: Option<Vec<Tool>>,
152        session_id: Option<Uuid>,
153        metadata: Option<serde_json::Value>,
154    ) -> Result<ChatCompletionResponse, String>;
155    async fn chat_completion_stream(
156        &self,
157        model: Model,
158        messages: Vec<ChatMessage>,
159        tools: Option<Vec<Tool>>,
160        headers: Option<HeaderMap>,
161        session_id: Option<Uuid>,
162        metadata: Option<serde_json::Value>,
163    ) -> Result<
164        (
165            std::pin::Pin<
166                Box<dyn Stream<Item = Result<ChatCompletionStreamResponse, ApiStreamError>> + Send>,
167            >,
168            Option<String>,
169        ),
170        String,
171    >;
172    async fn cancel_stream(&self, request_id: String) -> Result<(), String>;
173
174    // Search Docs
175    async fn search_docs(&self, input: &SearchDocsRequest) -> Result<Vec<Content>, String>;
176
177    // Memory
178    async fn memorize_session(&self, checkpoint_id: Uuid) -> Result<(), String>;
179    async fn search_memory(&self, input: &SearchMemoryRequest) -> Result<Vec<Content>, String>;
180
181    // Slack
182    async fn slack_read_messages(
183        &self,
184        input: &SlackReadMessagesRequest,
185    ) -> Result<Vec<Content>, String>;
186    async fn slack_read_replies(
187        &self,
188        input: &SlackReadRepliesRequest,
189    ) -> Result<Vec<Content>, String>;
190    async fn slack_send_message(
191        &self,
192        input: &SlackSendMessageRequest,
193    ) -> Result<Vec<Content>, String>;
194
195    // Models
196    async fn list_models(&self) -> Vec<Model>;
197}