feat(proxy): add scoped proxy configuration and docs runbooks
- add scope-aware proxy schema and runtime wiring for providers/channels/tools - add agent callable proxy_config tool for fast proxy setup - standardize docs system with index, template, and playbooks
This commit is contained in:
parent
13ee9e6398
commit
ce104bed45
36 changed files with 2025 additions and 323 deletions
|
|
@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct AnthropicProvider {
|
||||
credential: Option<String>,
|
||||
base_url: String,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -161,11 +160,6 @@ impl AnthropicProvider {
|
|||
.filter(|k| !k.is_empty())
|
||||
.map(ToString::to_string),
|
||||
base_url,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +398,10 @@ impl AnthropicProvider {
|
|||
tool_calls,
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.anthropic", 120, 10)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -433,7 +431,7 @@ impl Provider for AnthropicProvider {
|
|||
};
|
||||
|
||||
let mut request = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(format!("{}/v1/messages", self.base_url))
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("content-type", "application/json")
|
||||
|
|
@ -480,7 +478,7 @@ impl Provider for AnthropicProvider {
|
|||
};
|
||||
|
||||
let req = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(format!("{}/v1/messages", self.base_url))
|
||||
.header("anthropic-version", "2023-06-01")
|
||||
.header("content-type", "application/json")
|
||||
|
|
@ -502,7 +500,7 @@ impl Provider for AnthropicProvider {
|
|||
async fn warmup(&self) -> anyhow::Result<()> {
|
||||
if let Some(credential) = self.credential.as_ref() {
|
||||
let mut request = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(format!("{}/v1/messages", self.base_url))
|
||||
.header("anthropic-version", "2023-06-01");
|
||||
request = self.apply_auth(request, credential);
|
||||
|
|
@ -594,7 +592,9 @@ mod tests {
|
|||
let provider = AnthropicProvider::new(None);
|
||||
let request = provider
|
||||
.apply_auth(
|
||||
provider.client.get("https://api.anthropic.com/v1/models"),
|
||||
provider
|
||||
.http_client()
|
||||
.get("https://api.anthropic.com/v1/models"),
|
||||
"sk-ant-oat01-test-token",
|
||||
)
|
||||
.build()
|
||||
|
|
@ -622,7 +622,9 @@ mod tests {
|
|||
let provider = AnthropicProvider::new(None);
|
||||
let request = provider
|
||||
.apply_auth(
|
||||
provider.client.get("https://api.anthropic.com/v1/models"),
|
||||
provider
|
||||
.http_client()
|
||||
.get("https://api.anthropic.com/v1/models"),
|
||||
"sk-ant-api-key",
|
||||
)
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ pub struct OpenAiCompatibleProvider {
|
|||
/// When false, do not fall back to /v1/responses on chat completions 404.
|
||||
/// GLM/Zhipu does not support the responses API.
|
||||
supports_responses_fallback: bool,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
/// How the provider expects the API key to be sent.
|
||||
|
|
@ -49,11 +48,6 @@ impl OpenAiCompatibleProvider {
|
|||
credential: credential.map(ToString::to_string),
|
||||
auth_header: auth_style,
|
||||
supports_responses_fallback: true,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,14 +65,13 @@ impl OpenAiCompatibleProvider {
|
|||
credential: credential.map(ToString::to_string),
|
||||
auth_header: auth_style,
|
||||
supports_responses_fallback: false,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.compatible", 120, 10)
|
||||
}
|
||||
|
||||
/// Build the full URL for chat completions, detecting if base_url already includes the path.
|
||||
/// This allows custom providers with non-standard endpoints (e.g., VolcEngine ARK uses
|
||||
/// `/api/coding/v3/chat/completions` instead of `/v1/chat/completions`).
|
||||
|
|
@ -513,7 +506,7 @@ impl OpenAiCompatibleProvider {
|
|||
let url = self.responses_url();
|
||||
|
||||
let response = self
|
||||
.apply_auth_header(self.client.post(&url).json(&request), credential)
|
||||
.apply_auth_header(self.http_client().post(&url).json(&request), credential)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
|
@ -578,7 +571,7 @@ impl Provider for OpenAiCompatibleProvider {
|
|||
let url = self.chat_completions_url();
|
||||
|
||||
let response = self
|
||||
.apply_auth_header(self.client.post(&url).json(&request), credential)
|
||||
.apply_auth_header(self.http_client().post(&url).json(&request), credential)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
|
@ -660,7 +653,7 @@ impl Provider for OpenAiCompatibleProvider {
|
|||
|
||||
let url = self.chat_completions_url();
|
||||
let response = self
|
||||
.apply_auth_header(self.client.post(&url).json(&request), credential)
|
||||
.apply_auth_header(self.http_client().post(&url).json(&request), credential)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
|
@ -760,7 +753,7 @@ impl Provider for OpenAiCompatibleProvider {
|
|||
|
||||
let url = self.chat_completions_url();
|
||||
let response = self
|
||||
.apply_auth_header(self.client.post(&url).json(&request), credential)
|
||||
.apply_auth_header(self.http_client().post(&url).json(&request), credential)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
|
|
@ -900,7 +893,7 @@ impl Provider for OpenAiCompatibleProvider {
|
|||
};
|
||||
|
||||
let url = self.chat_completions_url();
|
||||
let client = self.client.clone();
|
||||
let client = self.http_client();
|
||||
let auth_header = self.auth_header.clone();
|
||||
|
||||
// Use a channel to bridge the async HTTP response to the stream
|
||||
|
|
@ -967,7 +960,7 @@ impl Provider for OpenAiCompatibleProvider {
|
|||
// the goal is TLS handshake and HTTP/2 negotiation.
|
||||
let url = self.chat_completions_url();
|
||||
let _ = self
|
||||
.apply_auth_header(self.client.get(&url), credential)
|
||||
.apply_auth_header(self.http_client().get(&url), credential)
|
||||
.send()
|
||||
.await?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,6 @@ pub struct CopilotProvider {
|
|||
/// Mutex ensures only one caller refreshes tokens at a time,
|
||||
/// preventing duplicate device flow prompts or redundant API calls.
|
||||
refresh_lock: Arc<Mutex<Option<CachedApiKey>>>,
|
||||
http: Client,
|
||||
token_dir: PathBuf,
|
||||
}
|
||||
|
||||
|
|
@ -204,15 +203,14 @@ impl CopilotProvider {
|
|||
.filter(|token| !token.is_empty())
|
||||
.map(String::from),
|
||||
refresh_lock: Arc::new(Mutex::new(None)),
|
||||
http: Client::builder()
|
||||
.timeout(Duration::from_secs(120))
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
token_dir,
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.copilot", 120, 10)
|
||||
}
|
||||
|
||||
/// Required headers for Copilot API requests (editor identification).
|
||||
const COPILOT_HEADERS: [(&str, &str); 4] = [
|
||||
("Editor-Version", "vscode/1.85.1"),
|
||||
|
|
@ -326,7 +324,7 @@ impl CopilotProvider {
|
|||
};
|
||||
|
||||
let mut req = self
|
||||
.http
|
||||
.http_client()
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.json(&request);
|
||||
|
|
@ -438,7 +436,7 @@ impl CopilotProvider {
|
|||
/// Run GitHub OAuth device code flow.
|
||||
async fn device_code_login(&self) -> anyhow::Result<String> {
|
||||
let response: DeviceCodeResponse = self
|
||||
.http
|
||||
.http_client()
|
||||
.post(GITHUB_DEVICE_CODE_URL)
|
||||
.header("Accept", "application/json")
|
||||
.json(&serde_json::json!({
|
||||
|
|
@ -467,7 +465,7 @@ impl CopilotProvider {
|
|||
tokio::time::sleep(poll_interval).await;
|
||||
|
||||
let token_response: AccessTokenResponse = self
|
||||
.http
|
||||
.http_client()
|
||||
.post(GITHUB_ACCESS_TOKEN_URL)
|
||||
.header("Accept", "application/json")
|
||||
.json(&serde_json::json!({
|
||||
|
|
@ -502,7 +500,7 @@ impl CopilotProvider {
|
|||
|
||||
/// Exchange a GitHub access token for a Copilot API key.
|
||||
async fn exchange_for_api_key(&self, access_token: &str) -> anyhow::Result<ApiKeyInfo> {
|
||||
let mut request = self.http.get(GITHUB_API_KEY_URL);
|
||||
let mut request = self.http_client().get(GITHUB_API_KEY_URL);
|
||||
for (header, value) in &Self::COPILOT_HEADERS {
|
||||
request = request.header(*header, *value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ use std::path::PathBuf;
|
|||
/// Gemini provider supporting multiple authentication methods.
|
||||
pub struct GeminiProvider {
|
||||
auth: Option<GeminiAuth>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
/// Resolved credential — the variant determines both the HTTP auth method
|
||||
|
|
@ -161,11 +160,6 @@ impl GeminiProvider {
|
|||
|
||||
Self {
|
||||
auth: resolved_auth,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,6 +273,10 @@ impl GeminiProvider {
|
|||
}
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.gemini", 120, 10)
|
||||
}
|
||||
|
||||
fn build_generate_content_request(
|
||||
&self,
|
||||
auth: &GeminiAuth,
|
||||
|
|
@ -286,6 +284,7 @@ impl GeminiProvider {
|
|||
request: &GenerateContentRequest,
|
||||
model: &str,
|
||||
) -> reqwest::RequestBuilder {
|
||||
let req = self.http_client().post(url).json(request);
|
||||
match auth {
|
||||
GeminiAuth::OAuthToken(token) => {
|
||||
// Internal API expects the model in the request body envelope
|
||||
|
|
@ -317,12 +316,12 @@ impl GeminiProvider {
|
|||
.collect(),
|
||||
}),
|
||||
};
|
||||
self.client
|
||||
self.http_client()
|
||||
.post(url)
|
||||
.json(&internal_request)
|
||||
.bearer_auth(token)
|
||||
}
|
||||
_ => self.client.post(url).json(request),
|
||||
_ => req,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -408,7 +407,7 @@ impl Provider for GeminiProvider {
|
|||
"https://generativelanguage.googleapis.com/v1beta/models".to_string()
|
||||
};
|
||||
|
||||
let mut request = self.client.get(&url);
|
||||
let mut request = self.http_client().get(&url);
|
||||
if let GeminiAuth::OAuthToken(token) = auth {
|
||||
request = request.bearer_auth(token);
|
||||
}
|
||||
|
|
@ -470,17 +469,13 @@ mod tests {
|
|||
fn auth_source_explicit_key() {
|
||||
let provider = GeminiProvider {
|
||||
auth: Some(GeminiAuth::ExplicitKey("key".into())),
|
||||
client: Client::new(),
|
||||
};
|
||||
assert_eq!(provider.auth_source(), "config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_source_none_without_credentials() {
|
||||
let provider = GeminiProvider {
|
||||
auth: None,
|
||||
client: Client::new(),
|
||||
};
|
||||
let provider = GeminiProvider { auth: None };
|
||||
assert_eq!(provider.auth_source(), "none");
|
||||
}
|
||||
|
||||
|
|
@ -488,7 +483,6 @@ mod tests {
|
|||
fn auth_source_oauth() {
|
||||
let provider = GeminiProvider {
|
||||
auth: Some(GeminiAuth::OAuthToken("ya29.mock".into())),
|
||||
client: Client::new(),
|
||||
};
|
||||
assert_eq!(provider.auth_source(), "Gemini CLI OAuth");
|
||||
}
|
||||
|
|
@ -534,7 +528,6 @@ mod tests {
|
|||
fn oauth_request_uses_bearer_auth_header() {
|
||||
let provider = GeminiProvider {
|
||||
auth: Some(GeminiAuth::OAuthToken("ya29.mock-token".into())),
|
||||
client: Client::new(),
|
||||
};
|
||||
let auth = GeminiAuth::OAuthToken("ya29.mock-token".into());
|
||||
let url = GeminiProvider::build_generate_content_url("gemini-2.0-flash", &auth);
|
||||
|
|
@ -570,7 +563,6 @@ mod tests {
|
|||
fn api_key_request_does_not_set_bearer_header() {
|
||||
let provider = GeminiProvider {
|
||||
auth: Some(GeminiAuth::ExplicitKey("api-key-123".into())),
|
||||
client: Client::new(),
|
||||
};
|
||||
let auth = GeminiAuth::ExplicitKey("api-key-123".into());
|
||||
let url = GeminiProvider::build_generate_content_url("gemini-2.0-flash", &auth);
|
||||
|
|
@ -689,10 +681,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn warmup_without_key_is_noop() {
|
||||
let provider = GeminiProvider {
|
||||
auth: None,
|
||||
client: Client::new(),
|
||||
};
|
||||
let provider = GeminiProvider { auth: None };
|
||||
let result = provider.warmup().await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pub struct GlmProvider {
|
|||
api_key_id: String,
|
||||
api_key_secret: String,
|
||||
base_url: String,
|
||||
client: Client,
|
||||
/// Cached JWT token + expiry timestamp (ms)
|
||||
token_cache: Mutex<Option<(String, u64)>>,
|
||||
}
|
||||
|
|
@ -90,11 +89,6 @@ impl GlmProvider {
|
|||
api_key_id: id,
|
||||
api_key_secret: secret,
|
||||
base_url: "https://api.z.ai/api/paas/v4".to_string(),
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
token_cache: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
|
@ -149,6 +143,10 @@ impl GlmProvider {
|
|||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.glm", 120, 10)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -185,7 +183,7 @@ impl Provider for GlmProvider {
|
|||
let url = format!("{}/chat/completions", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.json(&request)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct OllamaProvider {
|
||||
base_url: String,
|
||||
api_key: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
// ─── Request Structures ───────────────────────────────────────────────────────
|
||||
|
|
@ -76,11 +75,6 @@ impl OllamaProvider {
|
|||
.trim_end_matches('/')
|
||||
.to_string(),
|
||||
api_key,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +85,10 @@ impl OllamaProvider {
|
|||
.is_some_and(|host| matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1"))
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.ollama", 300, 10)
|
||||
}
|
||||
|
||||
fn resolve_request_details(&self, model: &str) -> anyhow::Result<(String, bool)> {
|
||||
let requests_cloud = model.ends_with(":cloud");
|
||||
let normalized_model = model.strip_suffix(":cloud").unwrap_or(model).to_string();
|
||||
|
|
@ -139,7 +137,7 @@ impl OllamaProvider {
|
|||
temperature
|
||||
);
|
||||
|
||||
let mut request_builder = self.client.post(&url).json(&request);
|
||||
let mut request_builder = self.http_client().post(&url).json(&request);
|
||||
|
||||
if should_auth {
|
||||
if let Some(key) = self.api_key.as_ref() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct OpenAiProvider {
|
||||
base_url: String,
|
||||
credential: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -148,11 +147,6 @@ impl OpenAiProvider {
|
|||
.map(|u| u.trim_end_matches('/').to_string())
|
||||
.unwrap_or_else(|| "https://api.openai.com/v1".to_string()),
|
||||
credential: credential.map(ToString::to_string),
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -254,6 +248,10 @@ impl OpenAiProvider {
|
|||
|
||||
ProviderChatResponse { text, tool_calls }
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.openai", 120, 10)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -290,7 +288,7 @@ impl Provider for OpenAiProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(format!("{}/chat/completions", self.base_url))
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.json(&request)
|
||||
|
|
@ -331,7 +329,7 @@ impl Provider for OpenAiProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post(format!("{}/chat/completions", self.base_url))
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.json(&native_request)
|
||||
|
|
@ -358,7 +356,7 @@ impl Provider for OpenAiProvider {
|
|||
|
||||
async fn warmup(&self) -> anyhow::Result<()> {
|
||||
if let Some(credential) = self.credential.as_ref() {
|
||||
self.client
|
||||
self.http_client()
|
||||
.get(format!("{}/models", self.base_url))
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.send()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
pub struct OpenRouterProvider {
|
||||
credential: Option<String>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -113,11 +112,6 @@ impl OpenRouterProvider {
|
|||
pub fn new(credential: Option<&str>) -> Self {
|
||||
Self {
|
||||
credential: credential.map(ToString::to_string),
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,6 +219,10 @@ impl OpenRouterProvider {
|
|||
tool_calls,
|
||||
}
|
||||
}
|
||||
|
||||
fn http_client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("provider.openrouter", 120, 10)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -233,7 +231,7 @@ impl Provider for OpenRouterProvider {
|
|||
// Hit a lightweight endpoint to establish TLS + HTTP/2 connection pool.
|
||||
// This prevents the first real chat request from timing out on cold start.
|
||||
if let Some(credential) = self.credential.as_ref() {
|
||||
self.client
|
||||
self.http_client()
|
||||
.get("https://openrouter.ai/api/v1/auth/key")
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.send()
|
||||
|
|
@ -274,7 +272,7 @@ impl Provider for OpenRouterProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post("https://openrouter.ai/api/v1/chat/completions")
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.header(
|
||||
|
|
@ -324,7 +322,7 @@ impl Provider for OpenRouterProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post("https://openrouter.ai/api/v1/chat/completions")
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.header(
|
||||
|
|
@ -372,7 +370,7 @@ impl Provider for OpenRouterProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post("https://openrouter.ai/api/v1/chat/completions")
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.header(
|
||||
|
|
@ -460,7 +458,7 @@ impl Provider for OpenRouterProvider {
|
|||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.http_client()
|
||||
.post("https://openrouter.ai/api/v1/chat/completions")
|
||||
.header("Authorization", format!("Bearer {credential}"))
|
||||
.header(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue