rpi_extensions/
provider_hooks.rs1use std::sync::Arc;
15
16use rpi_ai::types::AssistantMessage;
17use rpi_ai::Context;
18use rpi_ai::{Model, ProviderHooks, SimpleStreamOptions, SimpleStreamOptionsPatch};
19use rpi_plugin_sdk::EventTag;
20
21use crate::loader::ExtensionSession;
22use crate::registry::RegistrySnapshot;
23use crate::translate::dispatch_data_event;
24use crate::PluginKeepalive;
25
26pub struct ExtensionProviderHooks {
30 snapshot: Arc<RegistrySnapshot>,
31 _keepalive: Arc<PluginKeepalive>,
32}
33
34impl ExtensionProviderHooks {
35 pub fn from_session(session: &ExtensionSession) -> Option<Self> {
39 let snapshot = session.snapshot_arc()?;
40 let subscribed = [
41 EventTag::BeforeProviderRequest,
42 EventTag::BeforeProviderHeaders,
43 EventTag::AfterProviderResponse,
44 ]
45 .iter()
46 .any(|t| !snapshot.handlers_for(*t).is_empty());
47 if !subscribed {
48 return None;
49 }
50 Some(Self {
51 snapshot,
52 _keepalive: session.keepalive(),
53 })
54 }
55}
56
57impl ProviderHooks for ExtensionProviderHooks {
58 fn before_request(
62 &self,
63 model: &Model,
64 _ctx: &Context,
65 opts: &SimpleStreamOptions,
66 ) -> Option<SimpleStreamOptionsPatch> {
67 let request = serde_json::json!({
68 "model": model.id,
69 "provider": model.provider,
70 "baseUrl": model.base_url,
71 "reasoning": model.reasoning,
72 "apiKey": opts.api_key,
73 "timeoutMs": opts.timeout.map(|d| d.as_millis() as u64),
74 "headers": opts.headers,
75 "metadata": opts.metadata,
76 "maxTokens": opts.max_tokens,
77 "temperature": opts.temperature,
78 });
79 dispatch_data_event(
80 &self.snapshot,
81 EventTag::BeforeProviderRequest,
82 &request.to_string(),
83 );
84 let headers = serde_json::json!({ "headers": opts.headers });
85 dispatch_data_event(
86 &self.snapshot,
87 EventTag::BeforeProviderHeaders,
88 &headers.to_string(),
89 );
90 None
91 }
92
93 fn after_response(&self, _model: &Model, message: &AssistantMessage) {
96 let json = serde_json::to_value(message).unwrap_or(serde_json::json!({}));
97 dispatch_data_event(
98 &self.snapshot,
99 EventTag::AfterProviderResponse,
100 &json.to_string(),
101 );
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use rpi_plugin_sdk::{EventTag, StablePluginEvent};
109 use std::sync::atomic::{AtomicUsize, Ordering};
110 use std::sync::Mutex;
111
112 static PROVIDER_HITS: AtomicUsize = AtomicUsize::new(0);
113 static PROVIDER_LOCK: Mutex<()> = Mutex::new(());
114
115 extern "C" fn counting_provider_handler(
116 ev: StablePluginEvent,
117 _ud: *mut std::ffi::c_void,
118 ) -> i32 {
119 let s = unsafe { ev.payload.data.data.to_string_lossy() };
125 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
126 if v.get("model").is_some() {
130 assert!(v.get("model").is_some(), "request event must carry model");
131 }
132 PROVIDER_HITS.fetch_add(1, Ordering::SeqCst);
133 }
134 0
135 }
136
137 #[test]
141 fn provider_hooks_dispatch_to_subscribers() {
142 let _guard = PROVIDER_LOCK.lock().unwrap();
143 PROVIDER_HITS.store(0, Ordering::SeqCst);
144
145 let mut registry = crate::registry::ExtensionRegistry::new();
146 let handler: rpi_plugin_sdk::EventHandlerFn = counting_provider_handler;
147 registry.register_event_handler(
148 EventTag::BeforeProviderRequest,
149 handler,
150 std::ptr::null_mut(),
151 );
152 registry.register_event_handler(
153 EventTag::BeforeProviderHeaders,
154 handler,
155 std::ptr::null_mut(),
156 );
157 let snapshot = Arc::new(registry.snapshot());
158
159 assert!(dispatch_data_event(
161 &snapshot,
162 EventTag::BeforeProviderRequest,
163 r#"{"model":"m"}"#,
164 ));
165 assert_eq!(PROVIDER_HITS.load(Ordering::SeqCst), 1);
166
167 let hooks = ExtensionProviderHooks {
173 snapshot: Arc::clone(&snapshot),
174 _keepalive: Arc::new(crate::PluginKeepalive::new(Vec::new(), None)),
175 };
176 let model = rpi_ai::Model::new(
177 "m",
178 "m",
179 rpi_ai::Api::AnthropicMessages,
180 "anthropic",
181 "https://api.anthropic.com",
182 );
183 let ctx = rpi_ai::Context::default();
184 let opts = rpi_ai::SimpleStreamOptions::default();
185 let patch = hooks.before_request(&model, &ctx, &opts);
186 assert!(patch.is_none(), "observer-only v1: no patch returned");
187 assert_eq!(PROVIDER_HITS.load(Ordering::SeqCst), 3);
188 }
189
190 #[test]
193 fn provider_hooks_no_subscribers_is_noop() {
194 let registry = crate::registry::ExtensionRegistry::new();
195 let snapshot = Arc::new(registry.snapshot());
196 assert!(!dispatch_data_event(
197 &snapshot,
198 EventTag::AfterProviderResponse,
199 "{}",
200 ));
201 }
202}