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