use std::future::Future;
use std::sync::{Arc, Mutex};
use crate::catalog::{Catalog, ModelSpec};
use crate::completion::UnsupportedOption;
use crate::effect::{EffectKind, HandlerDescriptor, family};
use crate::error::{ErrorKind, ErrorReport};
use crate::http_client::DynHttpClient;
use crate::operation::Completion;
use crate::providers::internal::auth::{AuthContext, AuthError};
use crate::providers::registry::{ConnectError, ConnectOptions};
use crate::providers::{chatgpt, copilot};
use crate::serve::adapters::ModelAdapter;
use crate::serve::{Dispatch, Reply, Serve};
use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
pub trait CredentialSource: WasmCompatSend + WasmCompatSync + 'static {
fn auth_context(
&self,
http: &DynHttpClient,
) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend;
}
impl CredentialSource for chatgpt::auth::Authenticator {
fn auth_context(
&self,
http: &DynHttpClient,
) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend {
chatgpt::auth::Authenticator::auth_context(self, http)
}
}
impl CredentialSource for copilot::auth::Authenticator {
fn auth_context(
&self,
http: &DynHttpClient,
) -> impl Future<Output = Result<AuthContext, AuthError>> + WasmCompatSend {
copilot::auth::Authenticator::auth_context(self, http)
}
}
pub struct SignedInModel<A> {
spec: ModelSpec,
credentials: A,
http: DynHttpClient,
unsigned: ModelAdapter<Completion>,
connected: Mutex<Option<(AuthContext, Arc<ModelAdapter<Completion>>)>>,
}
impl<A: CredentialSource> SignedInModel<A> {
pub fn new(spec: ModelSpec, credentials: A, http: DynHttpClient) -> Result<Self, ConnectError> {
let unsigned = Catalog::builtin()
.connect_with(&spec, ConnectOptions::new().api_key("").http(http.clone()))?;
Ok(Self {
unsigned: ModelAdapter::new(spec.reference(), unsigned),
spec,
credentials,
http,
connected: Mutex::default(),
})
}
async fn model(&self) -> Result<Arc<ModelAdapter<Completion>>, ErrorReport> {
let context = self
.credentials
.auth_context(&self.http)
.await
.map_err(|error| {
ErrorReport::new(
ErrorKind::Provider,
format!("the sign-in gave no credential ({error}); sign in again"),
)
.with_retryable(false)
})?;
if let Ok(connected) = self.connected.lock()
&& let Some((held, model)) = connected.as_ref()
&& *held == context
{
return Ok(Arc::clone(model));
}
let mut options = ConnectOptions::new()
.api_key(context.api_key.clone())
.http(self.http.clone());
if let Some(account_id) = &context.account_id {
options = options.account_id(account_id);
}
if let Some(api_base) = &context.api_base {
options = options.base_url(api_base);
}
let model = Catalog::builtin()
.connect_with(&self.spec, options)
.map_err(|error| ErrorReport::new(ErrorKind::Internal, error.to_string()))?;
let model = Arc::new(ModelAdapter::new(self.spec.reference(), model));
if let Ok(mut connected) = self.connected.lock() {
*connected = Some((context, Arc::clone(&model)));
}
Ok(model)
}
}
impl<A: CredentialSource> Serve for SignedInModel<A> {
type Family = family::Completion;
fn descriptor(&self) -> HandlerDescriptor {
self.unsigned.descriptor()
}
fn check(&self, kind: &EffectKind) -> Vec<UnsupportedOption> {
self.unsigned.check(kind)
}
async fn serve(&self, kind: EffectKind, dispatch: Dispatch) -> Reply {
match self.model().await {
Ok(model) => model.serve(kind, dispatch).await,
Err(report) => Reply::Outcome(Err(report)),
}
}
}