vtcode_llm/providers/
poolside.rs1use serde_json::Value;
2use vtcode_config::constants::{env_vars, models, urls};
3
4use super::openai_compat::{OpenAiCompatCore, OpenAiCompatSpec, impl_openai_compat_provider};
5
6pub struct PoolsideSpec;
7
8fn no_reasoning(_message: &Value, _choice: &Value) -> Option<String> {
9 None
10}
11
12impl OpenAiCompatSpec for PoolsideSpec {
13 const NAME: &'static str = "Poolside";
14 const KEY: &'static str = "poolside";
15 const API_KEY_ENV: &'static str = "POOLSIDE_API_KEY";
16 const DEFAULT_MODEL: &'static str = models::poolside::DEFAULT_MODEL;
17 const DEFAULT_BASE_URL: &'static str = urls::POOLSIDE_API_BASE;
18 const BASE_URL_ENV: Option<&'static str> = Some(env_vars::POOLSIDE_BASE_URL);
19 const LISTED_MODELS: &'static [&'static str] = models::poolside::SUPPORTED_MODELS;
20 const VALIDATION_ALLOWLIST: Option<&'static [&'static str]> = Some(models::poolside::SUPPORTED_MODELS);
21
22 const SUPPRESS_SAMPLING_WHEN_REASONING: bool = false;
23 const STREAM_OPTIONS_INCLUDE_USAGE: bool = true;
24 const INCLUDE_USER_ID: bool = true;
25 const STREAM_REASONING_FIELDS: &'static [&'static str] = &[];
26 const DELTA_ORDER: super::shared::OpenAiDeltaOrder = super::shared::OpenAiDeltaOrder::ContentFirst;
27 const RESPONSE_REASONING_EXTRACTOR: Option<super::openai_compat::ReasoningExtractor> = Some(no_reasoning);
30
31 fn resolve_api_key(api_key: Option<String>) -> String {
32 api_key
33 .or_else(|| std::env::var(Self::API_KEY_ENV).ok().filter(|key| !key.trim().is_empty()))
34 .unwrap_or_default()
35 }
36
37 fn response_cache_metrics(core: &OpenAiCompatCore<Self>) -> bool {
38 core.prompt_cache_enabled
39 }
40
41 fn stream_cache_metrics(_core: &OpenAiCompatCore<Self>) -> bool {
42 true
43 }
44}
45
46impl_openai_compat_provider!(PoolsideProvider, PoolsideSpec, {
47 fn supports_streaming(&self) -> bool {
48 true
49 }
50
51 fn supports_structured_output(&self, _model: &str) -> bool {
52 true
53 }
54
55 fn supports_reasoning(&self, _model: &str) -> bool {
56 true
57 }
58
59 fn effective_context_size(&self, model: &str) -> usize {
60 crate::provider::catalog_context_window("poolside", model, 131_072)
61 }
62});
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::provider::{LLMRequest, Message, ToolChoice};
68 use std::sync::Arc;
69 use vtcode_config::types::ReasoningEffortLevel;
70
71 fn provider() -> PoolsideProvider {
72 PoolsideProvider::from_config(
73 Some("test-key".to_string()),
74 Some("malibu-latest".to_string()),
75 Some("https://example.test/v1".to_string()),
76 None,
77 None,
78 None,
79 None,
80 )
81 }
82
83 fn base_request() -> LLMRequest {
84 LLMRequest {
85 messages: vec![Message::user("hello".to_string())].into(),
86 system_prompt: Some(Arc::from("system guidance")),
87 model: "malibu-latest".to_string(),
88 max_tokens: Some(512),
89 temperature: Some(0.5),
90 top_p: Some(0.25),
91 stream: true,
92 tool_choice: Some(ToolChoice::Auto),
93 metadata: Some(serde_json::json!({"user_id": "user-42"})),
94 ..Default::default()
95 }
96 }
97
98 #[test]
99 fn golden_payload_basic_shape() {
100 let payload = provider().core.convert_request(&base_request()).unwrap();
101
102 assert_eq!(payload["model"], "malibu-latest");
103 let messages = payload["messages"].as_array().unwrap();
104 assert_eq!(messages.len(), 2);
105 assert_eq!(messages[0]["role"], "system");
106 assert_eq!(messages[0]["content"], "system guidance");
107 assert_eq!(messages[1]["role"], "user");
108 assert_eq!(messages[1]["content"], "hello");
109 assert_eq!(payload["max_tokens"], 512);
110 assert_eq!(payload["temperature"], 0.5);
111 assert_eq!(payload["top_p"], 0.25);
112 assert_eq!(payload["stream"], true);
113 assert_eq!(payload["stream_options"]["include_usage"], true);
114 assert_eq!(payload["tool_choice"], "auto");
115 assert_eq!(payload["user_id"], "user-42");
116 }
117
118 #[test]
119 fn golden_payload_reasoning_does_not_suppress_sampling() {
120 let mut request = base_request();
121 request.reasoning_effort = Some(ReasoningEffortLevel::High);
122 let payload = provider().core.convert_request(&request).unwrap();
123 assert_eq!(payload["temperature"], 0.5);
124 assert_eq!(payload["top_p"], 0.25);
125 assert!(payload.get("reasoning_effort").is_none());
126 }
127
128 #[test]
129 fn golden_payload_omits_empty_system_prompt() {
130 let mut request = base_request();
131 request.system_prompt = Some(Arc::from(" "));
132 request.stream = false;
133 let payload = provider().core.convert_request(&request).unwrap();
134 let messages = payload["messages"].as_array().unwrap();
135 assert_eq!(messages.len(), 1);
136 assert_eq!(messages[0]["role"], "user");
137 assert!(payload.get("stream").is_none());
138 assert!(payload.get("stream_options").is_none());
139 }
140}