style: Cleaned up some comments and imports
This commit is contained in:
+4
-3
@@ -348,13 +348,14 @@ clients:
|
|||||||
api_base: https://api.mistral.ai/v1
|
api_base: https://api.mistral.ai/v1
|
||||||
api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
api_key: '{{MISTRAL_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
|
|
||||||
# See https://docs.x.ai/docs — OAuth via SuperGrok / X Premium+ subscription
|
# See https://docs.x.ai/docs - OAuth via SuperGrok / X Premium+ subscription
|
||||||
- type: openai-compatible
|
- type: openai-compatible
|
||||||
name: xai
|
name: xai
|
||||||
api_base: https://api.x.ai/v1
|
api_base: https://api.x.ai/v1
|
||||||
api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
api_key: '{{XAI_API_KEY}}' # You can either hard-code or inject secrets from the Coyote vault
|
||||||
# auth: oauth # OR: OAuth (requires SuperGrok/X Premium+); endpoints from models.yaml
|
auth: null # When set to 'oauth', Coyote will use OAuth instead of an API key
|
||||||
# After enabling `auth: oauth`, run: coyote --authenticate xai
|
# Authenticate with `coyote --authenticate` or `.authenticate` in the REPL
|
||||||
|
# Note: Oauth requires SuperGrok/X Premium+ subscription
|
||||||
|
|
||||||
# Example: private OpenAI-compatible gateway with client_credentials OAuth
|
# Example: private OpenAI-compatible gateway with client_credentials OAuth
|
||||||
# - type: openai-compatible
|
# - type: openai-compatible
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@ use super::{
|
|||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use crate::utils::{estimate_token_length, strip_think_tag};
|
use crate::utils::{estimate_token_length, strip_think_tag};
|
||||||
|
|
||||||
|
use super::oauth::OAuthConfig;
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -358,7 +359,7 @@ impl ModelData {
|
|||||||
pub struct ProviderModels {
|
pub struct ProviderModels {
|
||||||
pub provider: String,
|
pub provider: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub oauth: Option<super::oauth::OAuthConfig>,
|
pub oauth: Option<OAuthConfig>,
|
||||||
pub models: Vec<ModelData>,
|
pub models: Vec<ModelData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-24
@@ -1,5 +1,6 @@
|
|||||||
use super::ClientConfig;
|
|
||||||
use super::access_token::{is_valid_access_token, set_access_token};
|
use super::access_token::{is_valid_access_token, set_access_token};
|
||||||
|
use super::openai_compatible_oauth::OpenAICompatibleOAuthProvider;
|
||||||
|
use super::{ClientConfig, ProviderModels};
|
||||||
use crate::config::paths;
|
use crate::config::paths;
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@@ -33,11 +34,6 @@ pub enum OAuthFlow {
|
|||||||
ClientCredentials,
|
ClientCredentials,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime OAuth configuration merged from `models.yaml` provider defaults
|
|
||||||
/// and user config `clients[i].oauth` overrides.
|
|
||||||
///
|
|
||||||
/// Every field except `client_id`, `token_url`, and `flow` is optional so that
|
|
||||||
/// user config can override individual fields without restating the entire block.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OAuthConfig {
|
pub struct OAuthConfig {
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
@@ -69,9 +65,6 @@ fn default_true() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OAuthConfig {
|
impl OAuthConfig {
|
||||||
/// Merge a user override into `self` field-by-field. User values win.
|
|
||||||
/// Uses `json_patch::merge`-like semantics (see `common.rs:apply_patch`) —
|
|
||||||
/// `None` in override means "keep base"; explicit values replace.
|
|
||||||
pub fn merge(mut self, override_cfg: OAuthConfig) -> Self {
|
pub fn merge(mut self, override_cfg: OAuthConfig) -> Self {
|
||||||
self.client_id = override_cfg.client_id;
|
self.client_id = override_cfg.client_id;
|
||||||
self.token_url = override_cfg.token_url;
|
self.token_url = override_cfg.token_url;
|
||||||
@@ -106,8 +99,10 @@ impl OAuthConfig {
|
|||||||
self.extra_request_headers
|
self.extra_request_headers
|
||||||
.extend(override_cfg.extra_request_headers);
|
.extend(override_cfg.extra_request_headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.echo_pkce_in_token_exchange = override_cfg.echo_pkce_in_token_exchange;
|
self.echo_pkce_in_token_exchange = override_cfg.echo_pkce_in_token_exchange;
|
||||||
self.include_state_in_token_exchange = override_cfg.include_state_in_token_exchange;
|
self.include_state_in_token_exchange = override_cfg.include_state_in_token_exchange;
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,6 +336,7 @@ async fn run_client_credentials_flow(
|
|||||||
client_name,
|
client_name,
|
||||||
provider.provider_name()
|
provider.provider_name()
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,7 +498,7 @@ fn listen_for_oauth_callback(redirect_uri: &str) -> Result<(String, String)> {
|
|||||||
|
|
||||||
println!("Waiting for OAuth callback on {redirect_uri} ...");
|
println!("Waiting for OAuth callback on {redirect_uri} ...");
|
||||||
println!(
|
println!(
|
||||||
"(If the browser shows a 'paste this code' page, ignore it — Coyote captures the callback automatically.)\n"
|
"(If the browser shows a 'paste this code' page, ignore it. Coyote captures the callback automatically.)\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
let listener = TcpListener::bind(format!("{host}:{port}"))?;
|
let listener = TcpListener::bind(format!("{host}:{port}"))?;
|
||||||
@@ -570,13 +566,9 @@ pub fn get_oauth_provider(provider_type: &str) -> Option<Box<dyn OAuthProvider>>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an OAuthProvider for a given client, resolving config-driven providers
|
|
||||||
/// (openai-compatible) from a merged `models.yaml` + user-config OAuthConfig.
|
|
||||||
///
|
|
||||||
/// For first-class providers (claude/gemini/openai), delegates to `get_oauth_provider`.
|
|
||||||
pub fn get_oauth_provider_for_client(
|
pub fn get_oauth_provider_for_client(
|
||||||
client_config: &ClientConfig,
|
client_config: &ClientConfig,
|
||||||
all_provider_models: &[super::ProviderModels],
|
all_provider_models: &[ProviderModels],
|
||||||
) -> Option<Box<dyn OAuthProvider>> {
|
) -> Option<Box<dyn OAuthProvider>> {
|
||||||
let (client_name, provider_type, auth) = client_config_info(client_config);
|
let (client_name, provider_type, auth) = client_config_info(client_config);
|
||||||
if auth != Some("oauth") {
|
if auth != Some("oauth") {
|
||||||
@@ -596,12 +588,10 @@ pub fn get_oauth_provider_for_client(
|
|||||||
(None, Some(u)) => u,
|
(None, Some(u)) => u,
|
||||||
(Some(b), Some(u)) => b.merge(u),
|
(Some(b), Some(u)) => b.merge(u),
|
||||||
};
|
};
|
||||||
Some(Box::new(
|
Some(Box::new(OpenAICompatibleOAuthProvider {
|
||||||
super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
config: merged,
|
||||||
config: merged,
|
client_name: client_name.to_string(),
|
||||||
client_name: client_name.to_string(),
|
}))
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
_ => get_oauth_provider(provider_type),
|
_ => get_oauth_provider(provider_type),
|
||||||
}
|
}
|
||||||
@@ -778,7 +768,9 @@ extra_authorize_params:
|
|||||||
referrer: coyote
|
referrer: coyote
|
||||||
echo_pkce_in_token_exchange: true
|
echo_pkce_in_token_exchange: true
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
|
||||||
assert_eq!(cfg.client_id, "xai-client");
|
assert_eq!(cfg.client_id, "xai-client");
|
||||||
assert_eq!(cfg.token_url, "https://auth.x.ai/oauth2/token");
|
assert_eq!(cfg.token_url, "https://auth.x.ai/oauth2/token");
|
||||||
assert_eq!(cfg.scopes.len(), 3);
|
assert_eq!(cfg.scopes.len(), 3);
|
||||||
@@ -799,14 +791,18 @@ echo_pkce_in_token_exchange: true
|
|||||||
#[test]
|
#[test]
|
||||||
fn oauth_flow_defaults_to_pkce_when_missing() {
|
fn oauth_flow_defaults_to_pkce_when_missing() {
|
||||||
let yaml = "client_id: x\ntoken_url: y";
|
let yaml = "client_id: x\ntoken_url: y";
|
||||||
|
|
||||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
|
||||||
assert!(matches!(cfg.flow, OAuthFlow::Pkce));
|
assert!(matches!(cfg.flow, OAuthFlow::Pkce));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oauth_flow_client_credentials_parses() {
|
fn oauth_flow_client_credentials_parses() {
|
||||||
let yaml = "client_id: x\ntoken_url: y\nflow: client_credentials";
|
let yaml = "client_id: x\ntoken_url: y\nflow: client_credentials";
|
||||||
|
|
||||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
|
||||||
assert!(matches!(cfg.flow, OAuthFlow::ClientCredentials));
|
assert!(matches!(cfg.flow, OAuthFlow::ClientCredentials));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,6 +840,7 @@ echo_pkce_in_token_exchange: true
|
|||||||
let cc = make_openai_compat_client("acme", Some("oauth"), Some(user));
|
let cc = make_openai_compat_client("acme", Some("oauth"), Some(user));
|
||||||
|
|
||||||
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
||||||
|
|
||||||
assert_eq!(provider.client_id(), "user-id");
|
assert_eq!(provider.client_id(), "user-id");
|
||||||
assert_eq!(provider.token_url(), "https://user.example/token");
|
assert_eq!(provider.token_url(), "https://user.example/token");
|
||||||
assert!(provider.echo_pkce_in_token_exchange());
|
assert!(provider.echo_pkce_in_token_exchange());
|
||||||
@@ -860,6 +857,7 @@ echo_pkce_in_token_exchange: true
|
|||||||
let cc = make_openai_compat_client("bundled-only", Some("oauth"), None);
|
let cc = make_openai_compat_client("bundled-only", Some("oauth"), None);
|
||||||
|
|
||||||
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
let provider = get_oauth_provider_for_client(&cc, &models).unwrap();
|
||||||
|
|
||||||
assert_eq!(provider.client_id(), "base-id");
|
assert_eq!(provider.client_id(), "base-id");
|
||||||
assert_eq!(provider.token_url(), "https://base.example/token");
|
assert_eq!(provider.token_url(), "https://base.example/token");
|
||||||
}
|
}
|
||||||
@@ -874,12 +872,14 @@ echo_pkce_in_token_exchange: true
|
|||||||
let cc = make_openai_compat_client("inline-only", Some("oauth"), Some(user));
|
let cc = make_openai_compat_client("inline-only", Some("oauth"), Some(user));
|
||||||
|
|
||||||
let provider = get_oauth_provider_for_client(&cc, &[]).unwrap();
|
let provider = get_oauth_provider_for_client(&cc, &[]).unwrap();
|
||||||
|
|
||||||
assert_eq!(provider.client_id(), "inline-id");
|
assert_eq!(provider.client_id(), "inline-id");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_oauth_provider_for_client_returns_none_when_no_config_anywhere() {
|
fn get_oauth_provider_for_client_returns_none_when_no_config_anywhere() {
|
||||||
let cc = make_openai_compat_client("nothing", Some("oauth"), None);
|
let cc = make_openai_compat_client("nothing", Some("oauth"), None);
|
||||||
|
|
||||||
assert!(get_oauth_provider_for_client(&cc, &[]).is_none());
|
assert!(get_oauth_provider_for_client(&cc, &[]).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,7 +887,9 @@ echo_pkce_in_token_exchange: true
|
|||||||
fn get_oauth_provider_for_client_returns_none_when_auth_not_oauth() {
|
fn get_oauth_provider_for_client_returns_none_when_auth_not_oauth() {
|
||||||
let base = base_config();
|
let base = base_config();
|
||||||
let models = vec![make_provider_models("api-key-client", Some(base))];
|
let models = vec![make_provider_models("api-key-client", Some(base))];
|
||||||
|
|
||||||
let cc = make_openai_compat_client("api-key-client", None, None);
|
let cc = make_openai_compat_client("api-key-client", None, None);
|
||||||
|
|
||||||
assert!(get_oauth_provider_for_client(&cc, &models).is_none());
|
assert!(get_oauth_provider_for_client(&cc, &models).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,10 +897,12 @@ echo_pkce_in_token_exchange: true
|
|||||||
fn openai_compatible_provider_joins_scopes_with_spaces() {
|
fn openai_compatible_provider_joins_scopes_with_spaces() {
|
||||||
let mut cfg = base_config();
|
let mut cfg = base_config();
|
||||||
cfg.scopes = vec!["one".into(), "two".into(), "three".into()];
|
cfg.scopes = vec!["one".into(), "two".into(), "three".into()];
|
||||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
|
||||||
|
let provider = OpenAICompatibleOAuthProvider {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
client_name: "test".into(),
|
client_name: "test".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(provider.scopes(), "one two three");
|
assert_eq!(provider.scopes(), "one two three");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -907,10 +911,12 @@ echo_pkce_in_token_exchange: true
|
|||||||
let mut cfg = base_config();
|
let mut cfg = base_config();
|
||||||
cfg.redirect_uri = Some("https://custom.example/cb".into());
|
cfg.redirect_uri = Some("https://custom.example/cb".into());
|
||||||
cfg.redirect_port = Some(9999);
|
cfg.redirect_port = Some(9999);
|
||||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
|
||||||
|
let provider = OpenAICompatibleOAuthProvider {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
client_name: "test".into(),
|
client_name: "test".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
provider.fixed_redirect_uri().as_deref(),
|
provider.fixed_redirect_uri().as_deref(),
|
||||||
Some("https://custom.example/cb")
|
Some("https://custom.example/cb")
|
||||||
@@ -922,10 +928,12 @@ echo_pkce_in_token_exchange: true
|
|||||||
let mut cfg = base_config();
|
let mut cfg = base_config();
|
||||||
cfg.redirect_uri = None;
|
cfg.redirect_uri = None;
|
||||||
cfg.redirect_port = None;
|
cfg.redirect_port = None;
|
||||||
let provider = super::super::openai_compatible_oauth::OpenAICompatibleOAuthProvider {
|
|
||||||
|
let provider = OpenAICompatibleOAuthProvider {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
client_name: "test".into(),
|
client_name: "test".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(provider.uses_localhost_redirect());
|
assert!(provider.uses_localhost_redirect());
|
||||||
assert!(provider.fixed_redirect_uri().is_none());
|
assert!(provider.fixed_redirect_uri().is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use anyhow::{Context, Result, anyhow, bail};
|
|||||||
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
use reqwest::{Client as ReqwestClient, RequestBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
use oauth::OAuthConfig;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct OpenAICompatibleConfig {
|
pub struct OpenAICompatibleConfig {
|
||||||
@@ -14,7 +15,7 @@ pub struct OpenAICompatibleConfig {
|
|||||||
pub api_base: Option<String>,
|
pub api_base: Option<String>,
|
||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub auth: Option<String>,
|
pub auth: Option<String>,
|
||||||
pub oauth: Option<Box<super::oauth::OAuthConfig>>,
|
pub oauth: Option<Box<OAuthConfig>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub models: Vec<ModelData>,
|
pub models: Vec<ModelData>,
|
||||||
pub patch: Option<RequestPatch>,
|
pub patch: Option<RequestPatch>,
|
||||||
@@ -43,6 +44,7 @@ impl Client for OpenAICompatibleClient {
|
|||||||
) -> Result<ChatCompletionsOutput> {
|
) -> Result<ChatCompletionsOutput> {
|
||||||
let request_data = prepare_chat_completions(self, client, data).await?;
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
let builder = self.request_builder(client, request_data);
|
let builder = self.request_builder(client, request_data);
|
||||||
|
|
||||||
openai_chat_completions(builder, self.model()).await
|
openai_chat_completions(builder, self.model()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ impl Client for OpenAICompatibleClient {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let request_data = prepare_chat_completions(self, client, data).await?;
|
let request_data = prepare_chat_completions(self, client, data).await?;
|
||||||
let builder = self.request_builder(client, request_data);
|
let builder = self.request_builder(client, request_data);
|
||||||
|
|
||||||
openai_chat_completions_streaming(builder, handler, self.model()).await
|
openai_chat_completions_streaming(builder, handler, self.model()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +67,7 @@ impl Client for OpenAICompatibleClient {
|
|||||||
) -> Result<EmbeddingsOutput> {
|
) -> Result<EmbeddingsOutput> {
|
||||||
let request_data = prepare_embeddings(self, client, data).await?;
|
let request_data = prepare_embeddings(self, client, data).await?;
|
||||||
let builder = self.request_builder(client, request_data);
|
let builder = self.request_builder(client, request_data);
|
||||||
|
|
||||||
openai_embeddings(builder, self.model()).await
|
openai_embeddings(builder, self.model()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +78,7 @@ impl Client for OpenAICompatibleClient {
|
|||||||
) -> Result<RerankOutput> {
|
) -> Result<RerankOutput> {
|
||||||
let request_data = prepare_rerank(self, client, data).await?;
|
let request_data = prepare_rerank(self, client, data).await?;
|
||||||
let builder = self.request_builder(client, request_data);
|
let builder = self.request_builder(client, request_data);
|
||||||
|
|
||||||
generic_rerank(builder, self.model()).await
|
generic_rerank(builder, self.model()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +92,9 @@ async fn prepare_chat_completions(
|
|||||||
let url = format!("{api_base}/chat/completions");
|
let url = format!("{api_base}/chat/completions");
|
||||||
let body = openai_build_chat_completions_body(data, &self_.model);
|
let body = openai_build_chat_completions_body(data, &self_.model);
|
||||||
let mut request_data = RequestData::new(url, body);
|
let mut request_data = RequestData::new(url, body);
|
||||||
|
|
||||||
apply_auth(self_, client, &mut request_data).await?;
|
apply_auth(self_, client, &mut request_data).await?;
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +107,9 @@ async fn prepare_embeddings(
|
|||||||
let url = format!("{api_base}/embeddings");
|
let url = format!("{api_base}/embeddings");
|
||||||
let body = openai_build_embeddings_body(data, &self_.model);
|
let body = openai_build_embeddings_body(data, &self_.model);
|
||||||
let mut request_data = RequestData::new(url, body);
|
let mut request_data = RequestData::new(url, body);
|
||||||
|
|
||||||
apply_auth(self_, client, &mut request_data).await?;
|
apply_auth(self_, client, &mut request_data).await?;
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +126,9 @@ async fn prepare_rerank(
|
|||||||
};
|
};
|
||||||
let body = generic_build_rerank_body(data, &self_.model);
|
let body = generic_build_rerank_body(data, &self_.model);
|
||||||
let mut request_data = RequestData::new(url, body);
|
let mut request_data = RequestData::new(url, body);
|
||||||
|
|
||||||
apply_auth(self_, client, &mut request_data).await?;
|
apply_auth(self_, client, &mut request_data).await?;
|
||||||
|
|
||||||
Ok(request_data)
|
Ok(request_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +160,7 @@ async fn apply_auth(
|
|||||||
client_name
|
client_name
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let ready = oauth::prepare_oauth_access_token(client, &*provider, client_name).await?;
|
let ready = oauth::prepare_oauth_access_token(client, &*provider, client_name).await?;
|
||||||
if !ready {
|
if !ready {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -157,8 +169,10 @@ async fn apply_auth(
|
|||||||
client_name
|
client_name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = get_access_token(client_name)?;
|
let token = get_access_token(client_name)?;
|
||||||
request_data.bearer_auth(token);
|
request_data.bearer_auth(token);
|
||||||
|
|
||||||
for (key, value) in provider.extra_request_headers() {
|
for (key, value) in provider.extra_request_headers() {
|
||||||
request_data.header(key, value);
|
request_data.header(key, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,16 +52,6 @@ impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
|||||||
self.config.redirect_uri.is_none() && self.config.redirect_port.is_none()
|
self.config.redirect_uri.is_none() && self.config.redirect_port.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fixed_redirect_uri(&self) -> Option<String> {
|
|
||||||
if let Some(uri) = &self.config.redirect_uri {
|
|
||||||
return Some(uri.clone());
|
|
||||||
}
|
|
||||||
if let Some(port) = self.config.redirect_port {
|
|
||||||
return Some(format!("http://127.0.0.1:{port}/callback"));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extra_token_headers(&self) -> Vec<(&str, &str)> {
|
fn extra_token_headers(&self) -> Vec<(&str, &str)> {
|
||||||
self.config
|
self.config
|
||||||
.extra_token_headers
|
.extra_token_headers
|
||||||
@@ -78,6 +68,20 @@ impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fixed_redirect_uri(&self) -> Option<String> {
|
||||||
|
if let Some(uri) = &self.config.redirect_uri {
|
||||||
|
return Some(uri.clone());
|
||||||
|
}
|
||||||
|
if let Some(port) = self.config.redirect_port {
|
||||||
|
return Some(format!("http://127.0.0.1:{port}/callback"));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn include_state_in_token_exchange(&self) -> bool {
|
||||||
|
self.config.include_state_in_token_exchange
|
||||||
|
}
|
||||||
|
|
||||||
fn flow(&self) -> OAuthFlow {
|
fn flow(&self) -> OAuthFlow {
|
||||||
self.config.flow
|
self.config.flow
|
||||||
}
|
}
|
||||||
@@ -85,8 +89,4 @@ impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
|||||||
fn echo_pkce_in_token_exchange(&self) -> bool {
|
fn echo_pkce_in_token_exchange(&self) -> bool {
|
||||||
self.config.echo_pkce_in_token_exchange
|
self.config.echo_pkce_in_token_exchange
|
||||||
}
|
}
|
||||||
|
|
||||||
fn include_state_in_token_exchange(&self) -> bool {
|
|
||||||
self.config.include_state_in_token_exchange
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -63,7 +63,7 @@ use indoc::formatdoc;
|
|||||||
use inquire::{Confirm, Select};
|
use inquire::{Confirm, Select};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
@@ -596,7 +596,7 @@ impl Config {
|
|||||||
})
|
})
|
||||||
.with_context(|| "Failed to load config from str")?;
|
.with_context(|| "Failed to load config from str")?;
|
||||||
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
for cc in &config.clients {
|
for cc in &config.clients {
|
||||||
let (name, _, _) = oauth::client_config_info(cc);
|
let (name, _, _) = oauth::client_config_info(cc);
|
||||||
if !seen.insert(name.to_string()) {
|
if !seen.insert(name.to_string()) {
|
||||||
|
|||||||
+3
-2
@@ -785,8 +785,8 @@ fn resolve_oauth_client(
|
|||||||
0 => bail!("No OAuth-capable clients configured."),
|
0 => bail!("No OAuth-capable clients configured."),
|
||||||
1 => find_by_name(&candidates[0]).unwrap(),
|
1 => find_by_name(&candidates[0]).unwrap(),
|
||||||
_ => {
|
_ => {
|
||||||
let choice = Select::new("Select a client to authenticate:", candidates.clone())
|
let choice =
|
||||||
.prompt()?;
|
Select::new("Select a client to authenticate:", candidates.clone()).prompt()?;
|
||||||
find_by_name(&choice)
|
find_by_name(&choice)
|
||||||
.ok_or_else(|| anyhow!("Selected client '{choice}' not found"))?
|
.ok_or_else(|| anyhow!("Selected client '{choice}' not found"))?
|
||||||
}
|
}
|
||||||
@@ -800,5 +800,6 @@ fn resolve_oauth_client(
|
|||||||
"Could not build OAuth provider for '{name}' (no oauth config in models.yaml or user config)"
|
"Could not build OAuth provider for '{name}' (no oauth config in models.yaml or user config)"
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok((name, provider))
|
Ok((name, provider))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user