rig_core/client/
signed_in.rs1use std::future::Future;
6use std::sync::{Arc, Mutex};
7
8use crate::catalog::{Catalog, ModelSpec};
9use crate::completion::UnsupportedOption;
10use crate::effect::{EffectKind, HandlerDescriptor, family};
11use crate::error::{ErrorKind, ErrorReport};
12use crate::http_client::DynHttpClient;
13use crate::operation::Completion;
14use crate::providers::internal::auth::{AuthContext, AuthError};
15use crate::providers::registry::{ConnectError, ConnectOptions};
16use crate::providers::{chatgpt, copilot};
17use crate::serve::adapters::ModelAdapter;
18use crate::serve::{Dispatch, Reply, Serve};
19use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
20
21pub trait CredentialSource: WasmCompatSend + WasmCompatSync + 'static {
23 fn auth_context(
26 &self,
27 http: &DynHttpClient,
28 ) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend;
29}
30
31impl CredentialSource for chatgpt::auth::Authenticator {
32 fn auth_context(
33 &self,
34 http: &DynHttpClient,
35 ) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend {
36 chatgpt::auth::Authenticator::auth_context(self, http)
37 }
38}
39
40impl CredentialSource for copilot::auth::Authenticator {
41 fn auth_context(
42 &self,
43 http: &DynHttpClient,
44 ) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend {
45 copilot::auth::Authenticator::auth_context(self, http)
46 }
47}
48
49pub struct SignedInModel<A> {
72 spec: ModelSpec,
73 credentials: A,
74 http: DynHttpClient,
75 unsigned: ModelAdapter<Completion>,
78 connected: Mutex<Option<(AuthContext, Arc<ModelAdapter<Completion>>)>>,
80}
81
82impl<A: CredentialSource> SignedInModel<A> {
83 pub fn new(spec: ModelSpec, credentials: A, http: DynHttpClient) -> Result<Self, ConnectError> {
89 let unsigned = Catalog::builtin()
90 .connect_with(&spec, ConnectOptions::new().api_key("").http(http.clone()))?;
91 Ok(Self {
92 unsigned: ModelAdapter::new(spec.reference(), unsigned),
93 spec,
94 credentials,
95 http,
96 connected: Mutex::default(),
97 })
98 }
99
100 async fn model(&self) -> Result<Arc<ModelAdapter<Completion>>, ErrorReport> {
103 let context = self
104 .credentials
105 .auth_context(&self.http)
106 .await
107 .map_err(|error| {
108 ErrorReport::new(
109 ErrorKind::Provider,
110 format!("the sign-in gave no credential ({error}); sign in again"),
111 )
112 .with_retryable(false)
113 })?;
114 if let Ok(connected) = self.connected.lock()
115 && let Some((held, model)) = connected.as_ref()
116 && *held == context
117 {
118 return Ok(Arc::clone(model));
119 }
120 let mut options = ConnectOptions::new()
121 .api_key(context.api_key.clone())
122 .http(self.http.clone());
123 if let Some(account_id) = &context.account_id {
124 options = options.account_id(account_id);
125 }
126 if let Some(api_base) = &context.api_base {
127 options = options.base_url(api_base);
128 }
129 let model = Catalog::builtin()
130 .connect_with(&self.spec, options)
131 .map_err(|error| ErrorReport::new(ErrorKind::Internal, error.to_string()))?;
132 let model = Arc::new(ModelAdapter::new(self.spec.reference(), model));
133 if let Ok(mut connected) = self.connected.lock() {
134 *connected = Some((context, Arc::clone(&model)));
135 }
136 Ok(model)
137 }
138}
139
140impl<A: CredentialSource> Serve for SignedInModel<A> {
141 type Family = family::Completion;
142
143 fn descriptor(&self) -> HandlerDescriptor {
144 self.unsigned.descriptor()
145 }
146
147 fn check(&self, kind: &EffectKind) -> Vec<UnsupportedOption> {
150 self.unsigned.check(kind)
151 }
152
153 async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
154 match self.model().await {
155 Ok(model) => model.serve(kind, dispatch).await,
156 Err(report) => Reply::Outcome(Err(report)),
157 }
158 }
159}