provider_agent/backend/
openrouter.rs1use async_trait::async_trait;
9
10use super::http::{build_client, get_json, parse_openai_models, probe, stream_chat_completions};
11use super::{
12 Backend, BackendError, BackendHealth, BackendModel, BackendResult, Job, JobResult, JobSink,
13};
14
15const BASE_URL: &str = "https://openrouter.ai/api/v1";
16
17pub struct OpenRouterBackend {
18 id: String,
19 api_key: String,
20 client: reqwest::Client,
21}
22
23impl OpenRouterBackend {
24 pub fn from_env(api_key_env: &str) -> BackendResult<Self> {
27 let api_key = std::env::var(api_key_env)
28 .ok()
29 .filter(|s| !s.trim().is_empty())
30 .ok_or(BackendError::MissingApiKey("openrouter"))?;
31 Ok(Self {
32 id: "openrouter".to_string(),
33 api_key,
34 client: build_client(),
35 })
36 }
37}
38
39#[async_trait]
40impl Backend for OpenRouterBackend {
41 fn kind(&self) -> &'static str {
42 "openrouter"
43 }
44
45 fn id(&self) -> &str {
46 &self.id
47 }
48
49 async fn list_models(&self) -> BackendResult<Vec<BackendModel>> {
50 let url = format!("{BASE_URL}/models");
51 let v = get_json(&self.client, &url, Some(&self.api_key)).await?;
52 Ok(parse_openai_models(&v, false))
53 }
54
55 async fn health(&self) -> BackendResult<BackendHealth> {
56 let url = format!("{BASE_URL}/models");
57 match probe(&self.client, &url, Some(&self.api_key)).await {
58 Ok(latency_ms) => Ok(BackendHealth {
59 reachable: true,
60 latency_ms: Some(latency_ms),
61 last_error: None,
62 }),
63 Err(e) => Ok(BackendHealth {
64 reachable: false,
65 latency_ms: None,
66 last_error: Some(e.to_string()),
67 }),
68 }
69 }
70
71 async fn execute(&self, job: &Job, sink: &mut dyn JobSink) -> BackendResult<JobResult> {
72 let endpoint = format!("{BASE_URL}/chat/completions");
73 stream_chat_completions(&self.client, &endpoint, Some(&self.api_key), job, sink).await
74 }
75}