1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
// Provider entity types (knowledge/foundations/providers.md)
//
// A Provider is an org-scoped instance of a driver: a configured vendor
// account (credentials, endpoint) that powers services like chat. DriverId
// names the driver implementation a provider uses.
use serde::{Deserialize, Serialize};
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
/// Open string identifier retained as the 0.17.x integration-kind name.
///
/// Despite the legacy type name, this is not a built-in-provider enum: any
/// normalized string is valid. The associated constants keep source
/// compatibility for the 0.17.x runtime adapter and persisted HTTP shapes.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DriverId(std::borrow::Cow<'static, str>);
impl std::fmt::Display for DriverId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl DriverId {
#[allow(non_upper_case_globals)]
pub const OpenAI: Self = Self(std::borrow::Cow::Borrowed("openai"));
#[allow(non_upper_case_globals)]
pub const OpenRouter: Self = Self(std::borrow::Cow::Borrowed("openrouter"));
#[allow(non_upper_case_globals)]
pub const AzureOpenAI: Self = Self(std::borrow::Cow::Borrowed("azure_openai"));
#[allow(non_upper_case_globals)]
pub const OpenAICompletions: Self = Self(std::borrow::Cow::Borrowed("openai_completions"));
#[allow(non_upper_case_globals)]
pub const Anthropic: Self = Self(std::borrow::Cow::Borrowed("anthropic"));
#[allow(non_upper_case_globals)]
pub const Gemini: Self = Self(std::borrow::Cow::Borrowed("gemini"));
#[allow(non_upper_case_globals)]
pub const LlmSim: Self = Self(std::borrow::Cow::Borrowed("llmsim"));
#[allow(non_upper_case_globals)]
pub const Bedrock: Self = Self(std::borrow::Cow::Borrowed("bedrock"));
#[allow(non_upper_case_globals)]
pub const Mai: Self = Self(std::borrow::Cow::Borrowed("mai"));
#[allow(non_upper_case_globals)]
pub const Fireworks: Self = Self(std::borrow::Cow::Borrowed("fireworks"));
#[allow(non_upper_case_globals)]
pub const Meta: Self = Self(std::borrow::Cow::Borrowed("meta"));
#[allow(non_upper_case_globals)]
pub const Cloudflare: Self = Self(std::borrow::Cow::Borrowed("cloudflare"));
#[allow(non_upper_case_globals)]
pub const Vercel: Self = Self(std::borrow::Cow::Borrowed("vercel"));
#[allow(non_upper_case_globals)]
pub const Mistral: Self = Self(std::borrow::Cow::Borrowed("mistral"));
/// Construct an external driver id from its canonical wire id.
///
/// The id is normalized to lowercase so registration and lookup match
/// case-insensitively, consistent with built-in parsing.
pub fn external(id: impl AsRef<str>) -> Self {
Self(std::borrow::Cow::Owned(
id.as_ref().trim().to_ascii_lowercase(),
))
}
/// Return the canonical string identifier for this provider.
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
/// The driver whose vendor serves `base_url`, by host.
///
/// A configured base URL is often the only thing an embedder has — it comes
/// from a settings file, an env var, or a request — and picking the driver
/// from it is otherwise a hand-rolled `host.contains("openrouter")` in
/// every consumer. Matching is on the host only, so a vendor's path
/// variations and proxy ports do not change the answer.
///
/// `None` means the host is not one Everruns recognizes, which is the
/// common and correct case for a self-hosted or gateway endpoint: those
/// speak an OpenAI-compatible wire, so the caller picks
/// [`DriverId::OpenAICompletions`] itself rather than having a vendor
/// guessed for it.
///
/// ```
/// use everruns_contracts::DriverId;
///
/// assert_eq!(
/// DriverId::for_base_url("https://openrouter.ai/api/v1"),
/// Some(DriverId::OpenRouter)
/// );
/// assert_eq!(DriverId::for_base_url("http://127.0.0.1:8081/v1"), None);
///
/// // A host that accepts any base URL picks the vendor driver when there
/// // is one, and the OpenAI-compatible wire otherwise.
/// let pick = |url: &str| DriverId::for_base_url(url).unwrap_or(DriverId::OpenAICompletions);
/// assert_eq!(pick("https://api.anthropic.com"), DriverId::Anthropic);
/// assert_eq!(pick("http://localhost:11434/v1"), DriverId::OpenAICompletions);
/// ```
pub fn for_base_url(base_url: &str) -> Option<DriverId> {
let host = url::Url::parse(base_url.trim())
.ok()?
.host_str()?
.to_ascii_lowercase();
let host = host.strip_prefix("www.").unwrap_or(&host);
let matches = |domain: &str| host == domain || host.ends_with(&format!(".{domain}"));
if matches("openrouter.ai") {
return Some(DriverId::OpenRouter);
}
if matches("openai.azure.com") || matches("azure.com") {
return Some(DriverId::AzureOpenAI);
}
if matches("openai.com") {
return Some(DriverId::OpenAI);
}
if matches("anthropic.com") {
return Some(DriverId::Anthropic);
}
if matches("googleapis.com") {
return Some(DriverId::Gemini);
}
if matches("fireworks.ai") {
return Some(DriverId::Fireworks);
}
if host == "api.mistral.ai" {
return Some(DriverId::Mistral);
}
// Matched on the gateway host only. `vercel.app` serves everything
// Vercel hosts, so a broader match would claim ordinary customer
// origins as LLM endpoints. Cloudflare has no arm at all: its AI
// endpoint is account-scoped under `api.cloudflare.com`, which serves
// the whole Cloudflare API, so the host alone does not identify an LLM
// endpoint.
if host == "ai-gateway.vercel.sh" {
return Some(DriverId::Vercel);
}
None
}
/// The vendor's canonical API base URL, for drivers that have one.
///
/// `None` for drivers whose endpoint is per-deployment (Azure, Bedrock) or
/// that have no HTTP endpoint at all.
pub fn default_base_url(&self) -> Option<&'static str> {
match self.as_str() {
"openai" | "openai_completions" => Some("https://api.openai.com/v1"),
"openrouter" => Some("https://openrouter.ai/api/v1"),
"anthropic" => Some("https://api.anthropic.com"),
"gemini" => Some("https://generativelanguage.googleapis.com"),
"fireworks" => Some("https://api.fireworks.ai/inference/v1"),
"mistral" => Some("https://api.mistral.ai/v1"),
// Cloudflare is absent on purpose: its base URL embeds the account
// and gateway ids, so there is no vendor-wide default.
"vercel" => Some("https://ai-gateway.vercel.sh/v1"),
_ => None,
}
}
/// Default trace-link URL templates for this driver, as
/// `(generation_url_template, session_url_template)`.
///
/// These are best-effort defaults for vendors that expose an observability
/// dashboard. They are only *defaults*: an org overrides them per provider
/// (`ProviderTraceConfig`) and must opt in via `enabled`, since most vendors
/// retain prompt/completion content only when logging is explicitly turned
/// on. Templates support the `{response_id}`, `{session_id}`, `{turn_id}`
/// and `{model}` placeholders.
///
/// OpenRouter stores logged generations on its **Logs** page
/// (<https://openrouter.ai/logs>, gated behind the account's
/// "Input & Output Logging" Observability setting). OpenRouter does not
/// document a public deep-link by generation id, so the generation template
/// passes the id best-effort; worst case it lands on the Logs page where the
/// generation can be found by recency.
pub fn default_trace_templates(&self) -> (Option<String>, Option<String>) {
if self == &DriverId::OpenRouter {
(
Some("https://openrouter.ai/logs?id={response_id}".to_string()),
Some("https://openrouter.ai/logs".to_string()),
)
} else {
(None, None)
}
}
}
impl std::str::FromStr for DriverId {
// Parsing never fails: unknown ids become `External`.
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Normalize once so built-in matching and the External id share the
// same lowercased form; casing variance never yields duplicate ids.
Ok(DriverId::external(s))
}
}
impl Serialize for DriverId {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for DriverId {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
if s.trim().is_empty() {
return Err(serde::de::Error::custom("provider type cannot be empty"));
}
if s != s.trim() {
return Err(serde::de::Error::custom(
"provider type cannot have leading or trailing whitespace",
));
}
// FromStr is infallible (unknown ids become External).
Ok(s.parse().unwrap_or_else(|_| unreachable!()))
}
}
#[cfg(test)]
mod tests {
use super::DriverId;
#[test]
fn built_in_driver_ids_share_literal_wire_and_display_contracts() {
for (driver, wire) in [
(DriverId::OpenAI, "openai"),
(DriverId::OpenRouter, "openrouter"),
(DriverId::AzureOpenAI, "azure_openai"),
(DriverId::OpenAICompletions, "openai_completions"),
(DriverId::Anthropic, "anthropic"),
(DriverId::Gemini, "gemini"),
(DriverId::LlmSim, "llmsim"),
(DriverId::Bedrock, "bedrock"),
(DriverId::Mai, "mai"),
(DriverId::Fireworks, "fireworks"),
(DriverId::Meta, "meta"),
(DriverId::Cloudflare, "cloudflare"),
(DriverId::Vercel, "vercel"),
(DriverId::Mistral, "mistral"),
] {
assert_eq!(driver.as_str(), wire);
assert_eq!(driver.to_string(), wire);
assert_eq!(wire.parse::<DriverId>().unwrap(), driver);
assert_eq!(
serde_json::to_value(&driver).unwrap(),
serde_json::json!(wire)
);
assert_eq!(
serde_json::from_value::<DriverId>(serde_json::json!(wire)).unwrap(),
driver
);
}
}
#[test]
fn wire_ids_reject_empty_and_padded_values() {
for value in ["", " ", "\t\n"] {
let error = serde_json::from_value::<DriverId>(serde_json::json!(value)).unwrap_err();
assert!(error.to_string().contains("provider type cannot be empty"));
}
let error = serde_json::from_value::<DriverId>(serde_json::json!(" openai ")).unwrap_err();
assert!(error.to_string().contains("leading or trailing whitespace"));
}
#[test]
fn wire_ids_normalize_case_and_accept_extensions() {
assert_eq!("OpenAI".parse::<DriverId>().unwrap(), DriverId::OpenAI);
assert_eq!(DriverId::external("OpenAI-Codex").as_str(), "openai-codex");
let driver = serde_json::from_str::<DriverId>(r#""Custom-Driver""#).unwrap();
assert_eq!(driver.as_str(), "custom-driver");
assert_eq!(
serde_json::to_string(&driver).unwrap(),
r#""custom-driver""#
);
assert_eq!(" Custom-Driver ".parse::<DriverId>().unwrap(), driver);
let mut ids = std::collections::HashSet::new();
ids.insert(driver);
ids.insert(DriverId::external("CUSTOM-DRIVER"));
assert_eq!(ids.len(), 1, "normalization must preserve hash identity");
}
#[test]
fn a_vendor_base_url_selects_its_driver_by_host() {
for (url, expected) in [
("https://api.openai.com/v1", DriverId::OpenAI),
("https://openrouter.ai/api/v1", DriverId::OpenRouter),
("https://api.anthropic.com", DriverId::Anthropic),
(
"https://generativelanguage.googleapis.com/v1beta",
DriverId::Gemini,
),
("https://api.fireworks.ai/inference/v1", DriverId::Fireworks),
("https://api.mistral.ai/v1", DriverId::Mistral),
(
"https://contoso.openai.azure.com/openai",
DriverId::AzureOpenAI,
),
] {
assert_eq!(DriverId::for_base_url(url), Some(expected.clone()), "{url}");
}
}
#[test]
fn an_unrecognized_host_is_left_to_the_caller() {
// Self-hosted and gateway endpoints speak an OpenAI-compatible wire;
// guessing a vendor for them would be wrong, not helpful.
for url in [
"http://127.0.0.1:8081/v1",
"https://llm.internal.example/v1",
"not a url",
"",
] {
assert_eq!(DriverId::for_base_url(url), None, "{url}");
}
}
#[test]
fn a_lookalike_host_is_not_the_vendor() {
// Suffix matching is on domain boundaries, so a host that merely ends
// in the vendor's name does not match it.
assert_eq!(DriverId::for_base_url("https://notopenai.com/v1"), None);
// Mistral matches on the API host alone: `mistral.ai` also serves the
// marketing site and Le Chat, which are not LLM endpoints.
assert_eq!(DriverId::for_base_url("https://mistral.ai/v1"), None);
assert_eq!(
DriverId::for_base_url("https://eu.api.openai.com/v1"),
Some(DriverId::OpenAI)
);
}
#[test]
fn a_driver_with_a_canonical_endpoint_reports_it() {
assert_eq!(
DriverId::OpenRouter.default_base_url(),
Some("https://openrouter.ai/api/v1")
);
assert_eq!(
DriverId::Mistral.default_base_url(),
Some("https://api.mistral.ai/v1")
);
// Per-deployment endpoints have no canonical default to report.
assert_eq!(DriverId::AzureOpenAI.default_base_url(), None);
assert_eq!(DriverId::Bedrock.default_base_url(), None);
}
}
// `Arc<str>` does not implement `ToSchema`, so the schema is written by hand.
// It is a plain string at the wire level regardless of the variant.
#[cfg(feature = "openapi")]
impl utoipa::ToSchema for DriverId {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("DriverId")
}
}
#[cfg(feature = "openapi")]
impl utoipa::PartialSchema for DriverId {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
utoipa::openapi::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::SchemaType::new(
utoipa::openapi::schema::Type::String,
))
.description(Some(
"LLM provider type. Built-in: openai, openrouter, azure_openai, \
openai_completions, anthropic, gemini, llmsim, bedrock, mai, fireworks, meta, \
mistral, cloudflare, vercel. \
Any other string is treated as an embedder-defined external provider.",
))
.build()
.into()
}
}
/// Configuration for linking from the chat UI to a provider's observability
/// dashboard ("trace"/"logs").
///
/// This is provider-agnostic: any driver with a dashboard can supply default
/// templates (see [`DriverId::default_trace_templates`]), and an org enables
/// links per provider once it has confirmed logging is on for that account.
/// URL templates support the `{response_id}`, `{session_id}`, `{turn_id}` and
/// `{model}` placeholders, so the same mechanism works for OpenRouter today and
/// for third-party observability backends (Langfuse, Helicone, ...) via an
/// override.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ProviderTraceConfig {
/// Whether trace links should be shown for this provider. Defaults to
/// `false`: vendors typically do not retain trace content unless logging is
/// explicitly enabled, so the org opts in once that is set up.
pub enabled: bool,
/// URL template for a single generation's trace, e.g.
/// `"https://openrouter.ai/logs?id={response_id}"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generation_url_template: Option<String>,
/// URL template for a session's grouped trace, e.g.
/// `"https://openrouter.ai/logs"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_url_template: Option<String>,
}
/// One extra HTTP header sent with every request to a provider connection.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ProviderRequestHeader {
/// Header name, e.g. `x-gateway-tenant`.
pub name: String,
/// Header value, sent verbatim.
pub value: String,
}
/// Per-connection request options: what an org wants added to every outbound
/// request to this provider, beyond endpoint and credentials.
///
/// These are connection-level on purpose. A gateway header or a diagnostics
/// opt-in describes the *service* an org talks to, not one agent's behavior, so
/// it belongs next to the base URL and credentials rather than on every agent.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct ProviderRequestOptions {
/// Extra HTTP headers added to every request to this provider. They
/// override the driver's and the connection's own headers by name, but
/// connection-level headers (`host`, `content-length`, ...) are ignored.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub headers: Vec<ProviderRequestHeader>,
/// Ask the provider to explain unexpected prompt-cache misses. Honored by
/// drivers with a diagnostics protocol (today: Anthropic's
/// `cache-diagnosis` beta); ignored elsewhere.
#[serde(default)]
pub cache_diagnostics: bool,
}
impl ProviderRequestOptions {
/// Whether these options change anything about an outbound request.
pub fn is_empty(&self) -> bool {
self.headers.is_empty() && !self.cache_diagnostics
}
/// The headers as the `(name, value)` pairs drivers merge onto a request.
pub fn header_pairs(&self) -> Vec<(String, String)> {
self.headers
.iter()
.map(|header| (header.name.clone(), header.value.clone()))
.collect()
}
}