feat(providers): add OVHcloud AI Endpoints as native provider
Route OVHcloud through OpenAiProvider (with proper tool_call_id serialization) instead of OpenAiCompatibleProvider, fixing tool-call round-trips against vLLM-based endpoints. - Add base_url field and with_base_url() constructor to OpenAiProvider - Replace all hardcoded api.openai.com URLs with self.base_url - Pass api_url through for the openai provider arm - Register ovhcloud/ovh provider with env var OVH_AI_ENDPOINTS_ACCESS_TOKEN
This commit is contained in:
parent
80b60d7b70
commit
6062888d1b
2 changed files with 28 additions and 4 deletions
|
|
@ -324,6 +324,7 @@ fn resolve_provider_credential(name: &str, credential_override: Option<&str>) ->
|
||||||
"opencode" | "opencode-zen" => vec!["OPENCODE_API_KEY"],
|
"opencode" | "opencode-zen" => vec!["OPENCODE_API_KEY"],
|
||||||
"vercel" | "vercel-ai" => vec!["VERCEL_API_KEY"],
|
"vercel" | "vercel-ai" => vec!["VERCEL_API_KEY"],
|
||||||
"cloudflare" | "cloudflare-ai" => vec!["CLOUDFLARE_API_KEY"],
|
"cloudflare" | "cloudflare-ai" => vec!["CLOUDFLARE_API_KEY"],
|
||||||
|
"ovhcloud" | "ovh" => vec!["OVH_AI_ENDPOINTS_ACCESS_TOKEN"],
|
||||||
"astrai" => vec!["ASTRAI_API_KEY"],
|
"astrai" => vec!["ASTRAI_API_KEY"],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
@ -405,7 +406,7 @@ pub fn create_provider_with_url(
|
||||||
// ── Primary providers (custom implementations) ───────
|
// ── Primary providers (custom implementations) ───────
|
||||||
"openrouter" => Ok(Box::new(openrouter::OpenRouterProvider::new(key))),
|
"openrouter" => Ok(Box::new(openrouter::OpenRouterProvider::new(key))),
|
||||||
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))),
|
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))),
|
||||||
"openai" => Ok(Box::new(openai::OpenAiProvider::new(key))),
|
"openai" => Ok(Box::new(openai::OpenAiProvider::with_base_url(api_url, key))),
|
||||||
// Ollama uses api_url for custom base URL (e.g. remote Ollama instance)
|
// Ollama uses api_url for custom base URL (e.g. remote Ollama instance)
|
||||||
"ollama" => Ok(Box::new(ollama::OllamaProvider::new(api_url, key))),
|
"ollama" => Ok(Box::new(ollama::OllamaProvider::new(api_url, key))),
|
||||||
"gemini" | "google" | "google-gemini" => {
|
"gemini" | "google" | "google-gemini" => {
|
||||||
|
|
@ -527,6 +528,12 @@ pub fn create_provider_with_url(
|
||||||
"Astrai", "https://as-trai.com/v1", key, AuthStyle::Bearer,
|
"Astrai", "https://as-trai.com/v1", key, AuthStyle::Bearer,
|
||||||
))),
|
))),
|
||||||
|
|
||||||
|
// ── Cloud AI endpoints ───────────────────────────────
|
||||||
|
"ovhcloud" | "ovh" => Ok(Box::new(openai::OpenAiProvider::with_base_url(
|
||||||
|
Some("https://oai.endpoints.kepler.ai.cloud.ovh.net/v1"),
|
||||||
|
key,
|
||||||
|
))),
|
||||||
|
|
||||||
// ── Bring Your Own Provider (custom URL) ───────────
|
// ── Bring Your Own Provider (custom URL) ───────────
|
||||||
// Format: "custom:https://your-api.com" or "custom:http://localhost:1234"
|
// Format: "custom:https://your-api.com" or "custom:http://localhost:1234"
|
||||||
name if name.starts_with("custom:") => {
|
name if name.starts_with("custom:") => {
|
||||||
|
|
@ -900,6 +907,12 @@ pub fn list_providers() -> Vec<ProviderInfo> {
|
||||||
aliases: &["nvidia-nim", "build.nvidia.com"],
|
aliases: &["nvidia-nim", "build.nvidia.com"],
|
||||||
local: false,
|
local: false,
|
||||||
},
|
},
|
||||||
|
ProviderInfo {
|
||||||
|
name: "ovhcloud",
|
||||||
|
display_name: "OVHcloud AI Endpoints",
|
||||||
|
aliases: &["ovh"],
|
||||||
|
local: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1413,6 +1426,7 @@ mod tests {
|
||||||
"copilot",
|
"copilot",
|
||||||
"nvidia",
|
"nvidia",
|
||||||
"astrai",
|
"astrai",
|
||||||
|
"ovhcloud",
|
||||||
];
|
];
|
||||||
for name in providers {
|
for name in providers {
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub struct OpenAiProvider {
|
pub struct OpenAiProvider {
|
||||||
|
base_url: String,
|
||||||
credential: Option<String>,
|
credential: Option<String>,
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +137,16 @@ impl NativeResponseMessage {
|
||||||
|
|
||||||
impl OpenAiProvider {
|
impl OpenAiProvider {
|
||||||
pub fn new(credential: Option<&str>) -> Self {
|
pub fn new(credential: Option<&str>) -> Self {
|
||||||
|
Self::with_base_url(None, credential)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a provider with an optional custom base URL.
|
||||||
|
/// Defaults to `https://api.openai.com/v1` when `base_url` is `None`.
|
||||||
|
pub fn with_base_url(base_url: Option<&str>, credential: Option<&str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
base_url: base_url
|
||||||
|
.map(|u| u.trim_end_matches('/').to_string())
|
||||||
|
.unwrap_or_else(|| "https://api.openai.com/v1".to_string()),
|
||||||
credential: credential.map(ToString::to_string),
|
credential: credential.map(ToString::to_string),
|
||||||
client: Client::builder()
|
client: Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(120))
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
|
|
@ -281,7 +291,7 @@ impl Provider for OpenAiProvider {
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
.post("https://api.openai.com/v1/chat/completions")
|
.post(format!("{}/chat/completions", self.base_url))
|
||||||
.header("Authorization", format!("Bearer {credential}"))
|
.header("Authorization", format!("Bearer {credential}"))
|
||||||
.json(&request)
|
.json(&request)
|
||||||
.send()
|
.send()
|
||||||
|
|
@ -322,7 +332,7 @@ impl Provider for OpenAiProvider {
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
.post("https://api.openai.com/v1/chat/completions")
|
.post(format!("{}/chat/completions", self.base_url))
|
||||||
.header("Authorization", format!("Bearer {credential}"))
|
.header("Authorization", format!("Bearer {credential}"))
|
||||||
.json(&native_request)
|
.json(&native_request)
|
||||||
.send()
|
.send()
|
||||||
|
|
@ -349,7 +359,7 @@ impl Provider for OpenAiProvider {
|
||||||
async fn warmup(&self) -> anyhow::Result<()> {
|
async fn warmup(&self) -> anyhow::Result<()> {
|
||||||
if let Some(credential) = self.credential.as_ref() {
|
if let Some(credential) = self.credential.as_ref() {
|
||||||
self.client
|
self.client
|
||||||
.get("https://api.openai.com/v1/models")
|
.get(format!("{}/models", self.base_url))
|
||||||
.header("Authorization", format!("Bearer {credential}"))
|
.header("Authorization", format!("Bearer {credential}"))
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue