use car_secrets::{SecretRef, SecretStore};
#[cfg(test)]
use std::cell::Cell;
#[cfg(test)]
use std::sync::MutexGuard;
use std::sync::{Mutex, OnceLock};
use crate::schema::{
ApiProtocol, CostModel, GenerateParam, ModelCapability, ModelSchema, ModelSource,
PerformanceEnvelope, ProprietaryAuth, ProprietaryProtocol, TrustTier,
};
pub const API_KEY_ENV: &str = "OPENROUTER_API_KEY";
pub const OAUTH_KEYCHAIN_KEY: &str = car_secrets::OPENROUTER_OAUTH_KEY;
pub const DEFAULT_API_BASE: &str = "https://openrouter.ai/api";
fn oauth_keychain_service() -> String {
std::env::var("CAR_OPENROUTER_OAUTH_KEYCHAIN_SERVICE")
.unwrap_or_else(|_| car_secrets::DEFAULT_SERVICE.to_string())
}
fn oauth_secret_ref() -> SecretRef {
SecretRef::new(oauth_keychain_service(), OAUTH_KEYCHAIN_KEY)
}
pub fn is_reserved_oauth_secret(service: Option<&str>, key: &str) -> bool {
service.unwrap_or(car_secrets::DEFAULT_SERVICE) == oauth_keychain_service()
&& key == OAUTH_KEYCHAIN_KEY
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CredentialSource {
Env,
Pasted,
Oauth,
}
impl CredentialSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Env => "env",
Self::Pasted => "pasted",
Self::Oauth => "oauth",
}
}
}
pub fn credential_source() -> Option<CredentialSource> {
#[cfg(test)]
CREDENTIAL_SOURCE_CALLS.with(|calls| calls.set(calls.get() + 1));
#[cfg(test)]
if let Some(value) = test_credential_override()
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone()
{
return value.map(|_| CredentialSource::Env);
}
if environment_key_exists() {
return Some(CredentialSource::Env);
}
if pasted_key_exists() {
return Some(CredentialSource::Pasted);
}
oauth_key_exists().then_some(CredentialSource::Oauth)
}
#[cfg(test)]
thread_local! {
static CREDENTIAL_SOURCE_CALLS: Cell<usize> = const { Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_credential_source_call_count() {
CREDENTIAL_SOURCE_CALLS.with(|calls| calls.set(0));
}
#[cfg(test)]
pub(crate) fn credential_source_call_count() -> usize {
CREDENTIAL_SOURCE_CALLS.with(Cell::get)
}
pub fn environment_key_exists() -> bool {
std::env::var(API_KEY_ENV).is_ok_and(|value| !value.trim().is_empty())
}
pub fn resolve_credential() -> Option<(String, CredentialSource)> {
#[cfg(test)]
if let Some(value) = test_credential_override()
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone()
{
return value.map(|key| (key, CredentialSource::Env));
}
if let Ok(value) = std::env::var(API_KEY_ENV) {
if !value.trim().is_empty() {
return Some((value, CredentialSource::Env));
}
}
let store = SecretStore::new();
if store.is_available() {
if let Ok(value) = store.get(&SecretRef::with_default_service(API_KEY_ENV)) {
if !value.trim().is_empty() {
return Some((value, CredentialSource::Pasted));
}
}
if let Ok(value) = store.get(&oauth_secret_ref()) {
if !value.trim().is_empty() {
return Some((value, CredentialSource::Oauth));
}
}
}
None
}
#[cfg(test)]
fn test_credential_override() -> &'static Mutex<Option<Option<String>>> {
static OVERRIDE: OnceLock<Mutex<Option<Option<String>>>> = OnceLock::new();
OVERRIDE.get_or_init(|| Mutex::new(None))
}
#[cfg(test)]
fn test_credential_serial_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
#[cfg(test)]
pub(crate) struct TestCredentialScope {
_guard: MutexGuard<'static, ()>,
}
#[cfg(test)]
impl Drop for TestCredentialScope {
fn drop(&mut self) {
clear_test_credential();
}
}
#[cfg(test)]
pub(crate) fn test_credential_scope() -> TestCredentialScope {
TestCredentialScope {
_guard: test_credential_serial_lock()
.lock()
.unwrap_or_else(|p| p.into_inner()),
}
}
#[cfg(test)]
pub(crate) fn test_environment_scope() -> tokio::sync::MutexGuard<'static, ()> {
test_environment_lock().blocking_lock()
}
#[cfg(test)]
pub(crate) async fn test_environment_scope_async() -> tokio::sync::MutexGuard<'static, ()> {
test_environment_lock().lock().await
}
#[cfg(test)]
fn test_environment_lock() -> &'static tokio::sync::Mutex<()> {
static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
}
#[cfg(test)]
pub(crate) fn set_test_credential(value: Option<&str>) {
*test_credential_override()
.lock()
.unwrap_or_else(|p| p.into_inner()) = Some(value.map(str::to_string));
}
#[cfg(test)]
pub(crate) fn clear_test_credential() {
*test_credential_override()
.lock()
.unwrap_or_else(|p| p.into_inner()) = None;
}
pub fn pasted_key_exists() -> bool {
SecretStore::new()
.status(&SecretRef::with_default_service(API_KEY_ENV))
.map(|status| status.exists)
.unwrap_or(false)
}
pub fn oauth_key_exists() -> bool {
SecretStore::new()
.status(&oauth_secret_ref())
.map(|status| status.exists)
.unwrap_or(false)
}
pub fn store_oauth_credential(value: &str) -> Result<(), car_secrets::SecretError> {
SecretStore::new().put(&oauth_secret_ref(), value)
}
pub fn delete_oauth_credential() -> Result<(), car_secrets::SecretError> {
match SecretStore::new().delete(&oauth_secret_ref()) {
Ok(()) | Err(car_secrets::SecretError::NotFound { .. }) => Ok(()),
Err(error) => Err(error),
}
}
#[derive(Clone, Copy)]
struct CuratedModel {
upstream_id: &'static str,
managed_alias: &'static str,
family: &'static str,
snapshot: &'static str,
context_length: usize,
max_output_tokens: usize,
input_per_mtok: f64,
output_per_mtok: f64,
cache_read_input_per_mtok: Option<f64>,
cache_write_input_per_mtok: Option<f64>,
high_context_pricing: Option<CuratedPricingTier>,
capabilities: &'static [ModelCapability],
tags: &'static [&'static str],
supported_params: &'static [GenerateParam],
}
#[derive(Clone, Copy)]
struct CuratedPricingTier {
min_prompt_tokens: usize,
input_per_mtok: f64,
output_per_mtok: f64,
cache_read_input_per_mtok: Option<f64>,
cache_write_input_per_mtok: Option<f64>,
}
use ModelCapability as C;
const CODE_REASON_VISION: &[ModelCapability] = &[
C::Generate,
C::Code,
C::Reasoning,
C::ToolUse,
C::Summarize,
C::Vision,
];
const CODE_REASON: &[ModelCapability] =
&[C::Generate, C::Code, C::Reasoning, C::ToolUse, C::Summarize];
const CODE_REASON_MULTI_TOOL: &[ModelCapability] = &[
C::Generate,
C::Code,
C::Reasoning,
C::ToolUse,
C::MultiToolCall,
C::Summarize,
];
const CODE: &[ModelCapability] = &[C::Generate, C::Code, C::ToolUse, C::Summarize];
use GenerateParam as P;
const PARAMS_OPENAI_REASONING: &[GenerateParam] =
&[P::MaxTokens, P::ResponseFormat, P::ExtendedThinking];
const PARAMS_ANTHROPIC_REASONING: &[GenerateParam] = &[
P::Temperature,
P::MaxTokens,
P::ResponseFormat,
P::ExtendedThinking,
];
const PARAMS_GEMINI_REASONING: &[GenerateParam] = &[
P::Temperature,
P::MaxTokens,
P::ResponseFormat,
P::ExtendedThinking,
];
const PARAMS_STANDARD_REASONING: &[GenerateParam] = &[
P::Temperature,
P::MaxTokens,
P::ResponseFormat,
P::ExtendedThinking,
];
const PARAMS_QWEN_REASONING: &[GenerateParam] = &[
P::Temperature,
P::MaxTokens,
P::ResponseFormat,
P::ExtendedThinking,
];
const PARAMS_STANDARD: &[GenerateParam] = &[P::Temperature, P::MaxTokens, P::ResponseFormat];
const CURATED: &[CuratedModel] = &[
CuratedModel {
upstream_id: "openai/gpt-5.4",
managed_alias: "frontier-general",
family: "gpt-5.4",
snapshot: "2026-07-22",
context_length: 1_050_000,
max_output_tokens: 128_000,
input_per_mtok: 2.5,
output_per_mtok: 15.0,
cache_read_input_per_mtok: Some(0.25),
cache_write_input_per_mtok: None,
high_context_pricing: Some(CuratedPricingTier {
min_prompt_tokens: 272_000,
input_per_mtok: 5.0,
output_per_mtok: 22.5,
cache_read_input_per_mtok: Some(0.5),
cache_write_input_per_mtok: None,
}),
capabilities: CODE_REASON_VISION,
tags: &["frontier"],
supported_params: PARAMS_OPENAI_REASONING,
},
CuratedModel {
upstream_id: "anthropic/claude-opus-4.6",
managed_alias: "frontier-deep",
family: "claude-4.6",
snapshot: "2026-07-22",
context_length: 1_000_000,
max_output_tokens: 128_000,
input_per_mtok: 5.0,
output_per_mtok: 25.0,
cache_read_input_per_mtok: Some(0.5),
cache_write_input_per_mtok: Some(6.25),
high_context_pricing: None,
capabilities: CODE_REASON_VISION,
tags: &["frontier"],
supported_params: PARAMS_ANTHROPIC_REASONING,
},
CuratedModel {
upstream_id: "google/gemini-3.1-pro-preview",
managed_alias: "frontier-multimodal",
family: "gemini-3.1",
snapshot: "2026-07-22",
context_length: 1_048_576,
max_output_tokens: 65_536,
input_per_mtok: 2.0,
output_per_mtok: 12.0,
cache_read_input_per_mtok: Some(0.2),
cache_write_input_per_mtok: Some(0.375),
high_context_pricing: Some(CuratedPricingTier {
min_prompt_tokens: 200_000,
input_per_mtok: 4.0,
output_per_mtok: 18.0,
cache_read_input_per_mtok: Some(0.4),
cache_write_input_per_mtok: Some(0.375),
}),
capabilities: CODE_REASON_VISION,
tags: &["frontier", "preview"],
supported_params: PARAMS_GEMINI_REASONING,
},
CuratedModel {
upstream_id: "anthropic/claude-sonnet-4.6",
managed_alias: "balanced-general",
family: "claude-4.6",
snapshot: "2026-07-22",
context_length: 1_000_000,
max_output_tokens: 128_000,
input_per_mtok: 3.0,
output_per_mtok: 15.0,
cache_read_input_per_mtok: Some(0.3),
cache_write_input_per_mtok: Some(3.75),
high_context_pricing: None,
capabilities: CODE_REASON_VISION,
tags: &["balanced"],
supported_params: PARAMS_ANTHROPIC_REASONING,
},
CuratedModel {
upstream_id: "moonshotai/kimi-k2.5",
managed_alias: "open-multimodal",
family: "kimi-k2.5",
snapshot: "2026-07-22",
context_length: 262_144,
max_output_tokens: 262_144,
input_per_mtok: 0.57,
output_per_mtok: 2.85,
cache_read_input_per_mtok: Some(0.095),
cache_write_input_per_mtok: None,
high_context_pricing: None,
capabilities: CODE_REASON_VISION,
tags: &["cheap", "open-weight"],
supported_params: PARAMS_STANDARD_REASONING,
},
CuratedModel {
upstream_id: "qwen/qwen3.5-plus-02-15",
managed_alias: "open-long-context",
family: "qwen3.5",
snapshot: "2026-07-22",
context_length: 1_000_000,
max_output_tokens: 65_536,
input_per_mtok: 0.26,
output_per_mtok: 1.56,
cache_read_input_per_mtok: None,
cache_write_input_per_mtok: None,
high_context_pricing: Some(CuratedPricingTier {
min_prompt_tokens: 256_000,
input_per_mtok: 0.325,
output_per_mtok: 1.95,
cache_read_input_per_mtok: None,
cache_write_input_per_mtok: None,
}),
capabilities: CODE_REASON_VISION,
tags: &["cheap", "open-weight"],
supported_params: PARAMS_QWEN_REASONING,
},
CuratedModel {
upstream_id: "deepseek/deepseek-v3.2",
managed_alias: "open-reasoning",
family: "deepseek-v3.2",
snapshot: "2026-07-22",
context_length: 163_840,
max_output_tokens: 65_536,
input_per_mtok: 0.269,
output_per_mtok: 0.4,
cache_read_input_per_mtok: Some(0.1345),
cache_write_input_per_mtok: None,
high_context_pricing: None,
capabilities: CODE_REASON,
tags: &["cheap", "open-weight"],
supported_params: PARAMS_STANDARD_REASONING,
},
CuratedModel {
upstream_id: "minimax/minimax-m2.5",
managed_alias: "open-fast",
family: "minimax-m2.5",
snapshot: "2026-07-22",
context_length: 204_800,
max_output_tokens: 196_608,
input_per_mtok: 0.15,
output_per_mtok: 0.9,
cache_read_input_per_mtok: Some(0.05),
cache_write_input_per_mtok: None,
high_context_pricing: None,
capabilities: CODE_REASON_MULTI_TOOL,
tags: &["cheap", "open-weight"],
supported_params: PARAMS_STANDARD_REASONING,
},
CuratedModel {
upstream_id: "openai/gpt-5.3-codex",
managed_alias: "coding-frontier",
family: "gpt-5.3-codex",
snapshot: "2026-07-22",
context_length: 400_000,
max_output_tokens: 128_000,
input_per_mtok: 1.75,
output_per_mtok: 14.0,
cache_read_input_per_mtok: Some(0.175),
cache_write_input_per_mtok: None,
high_context_pricing: None,
capabilities: CODE_REASON_VISION,
tags: &["code"],
supported_params: PARAMS_OPENAI_REASONING,
},
CuratedModel {
upstream_id: "qwen/qwen3-coder-next",
managed_alias: "coding-efficient",
family: "qwen3-coder",
snapshot: "2026-07-22",
context_length: 262_144,
max_output_tokens: 262_144,
input_per_mtok: 0.11,
output_per_mtok: 0.8,
cache_read_input_per_mtok: Some(0.07),
cache_write_input_per_mtok: None,
high_context_pricing: None,
capabilities: CODE,
tags: &["code", "cheap", "open-weight"],
supported_params: PARAMS_STANDARD,
},
CuratedModel {
upstream_id: "anthropic/claude-opus-4.8",
managed_alias: "frontier-deep-next",
family: "claude-4.8",
snapshot: "2026-07-31",
context_length: 1_000_000,
max_output_tokens: 128_000,
input_per_mtok: 5.0,
output_per_mtok: 25.0,
cache_read_input_per_mtok: Some(0.5),
cache_write_input_per_mtok: Some(6.25),
high_context_pricing: None,
capabilities: CODE_REASON_VISION,
tags: &["frontier"],
supported_params: PARAMS_ANTHROPIC_REASONING,
},
];
fn schema(model: CuratedModel, gateway: bool, parslee_api_base: &str) -> ModelSchema {
let id = if gateway {
format!("parslee/openrouter/{}", model.managed_alias)
} else {
format!("openrouter/{}", model.upstream_id)
};
let name = if gateway {
id.clone()
} else {
model.upstream_id.to_string()
};
let mut tags = vec!["builtin".to_string(), "openrouter".to_string()];
tags.extend(model.tags.iter().map(|tag| (*tag).to_string()));
if gateway {
tags.extend(["parslee".to_string(), "managed".to_string()]);
} else {
tags.extend([
"personal-key".to_string(),
"openrouter-baseline".to_string(),
]);
}
let source = if gateway {
ModelSource::Proprietary {
provider: "parslee".to_string(),
endpoint: parslee_api_base.to_string(),
auth: ProprietaryAuth::OAuth2Pkce {
authority: parslee_api_base.to_string(),
client_id: "parslee-car".to_string(),
scopes: vec!["inference:invoke".to_string(), "models:list".to_string()],
},
protocol: ProprietaryProtocol {
chat_path: "/api/v1/orgs/{orgId}/inference/responses".to_string(),
content_type: "application/json".to_string(),
streaming: true,
extra_headers: Default::default(),
},
}
} else {
ModelSource::RemoteApi {
endpoint: DEFAULT_API_BASE.to_string(),
api_key_env: API_KEY_ENV.to_string(),
api_key_envs: Vec::new(),
api_version: None,
protocol: ApiProtocol::OpenRouter,
}
};
ModelSchema {
id,
name,
provider: if gateway { "parslee" } else { "openrouter" }.to_string(),
family: model.family.to_string(),
version: model.snapshot.to_string(),
capabilities: model.capabilities.to_vec(),
context_length: model.context_length,
max_output_tokens: Some(model.max_output_tokens),
param_count: String::new(),
quantization: None,
performance: PerformanceEnvelope::default(),
cost: CostModel {
input_per_mtok: Some(model.input_per_mtok),
output_per_mtok: Some(model.output_per_mtok),
cache_read_input_per_mtok: model.cache_read_input_per_mtok,
cache_write_input_per_mtok: model.cache_write_input_per_mtok,
pricing_tiers: model
.high_context_pricing
.into_iter()
.map(|tier| crate::schema::TokenPricingTier {
min_prompt_tokens: tier.min_prompt_tokens,
prices: crate::schema::TokenPrices {
input_per_mtok: Some(tier.input_per_mtok),
output_per_mtok: Some(tier.output_per_mtok),
cache_read_input_per_mtok: tier.cache_read_input_per_mtok,
cache_write_input_per_mtok: tier.cache_write_input_per_mtok,
},
})
.collect(),
size_mb: None,
ram_mb: None,
},
source,
tags,
supported_params: if gateway {
vec![P::MaxTokens]
} else {
model.supported_params.to_vec()
},
public_benchmarks: Vec::new(),
trust_tier: TrustTier::Curated,
deprecated: false,
available: false,
weights_ready: true,
}
}
pub fn canonical_managed_gateway_selector(model: &ModelSchema) -> Option<&str> {
let canonical_id = is_curated_managed_gateway_alias(&model.id);
(canonical_id
&& model.provider == "parslee"
&& matches!(
&model.source,
ModelSource::Proprietary {
provider,
endpoint,
auth:
ProprietaryAuth::OAuth2Pkce {
authority,
client_id,
scopes,
},
protocol,
} if provider == "parslee"
&& !endpoint.is_empty()
&& endpoint.trim_end_matches('/') == authority.trim_end_matches('/')
&& client_id == "parslee-car"
&& scopes.as_slice() == ["inference:invoke", "models:list"]
&& protocol.chat_path == "/api/v1/orgs/{orgId}/inference/responses"
&& protocol.content_type == "application/json"
&& protocol.streaming
&& protocol.extra_headers.is_empty()
))
.then_some(model.id.as_str())
}
pub fn is_curated_managed_gateway_alias(id: &str) -> bool {
id.strip_prefix("parslee/openrouter/")
.is_some_and(|alias| CURATED.iter().any(|entry| entry.managed_alias == alias))
}
const GATEWAY_UNCONFIGURED_TTL: std::time::Duration = std::time::Duration::from_secs(15 * 60);
fn gateway_unconfigured_at() -> &'static Mutex<Option<std::time::Instant>> {
static AT: OnceLock<Mutex<Option<std::time::Instant>>> = OnceLock::new();
AT.get_or_init(|| Mutex::new(None))
}
pub fn note_gateway_unconfigured() {
let mut guard = match gateway_unconfigured_at().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*guard = Some(std::time::Instant::now());
}
pub fn gateway_unconfigured() -> bool {
let guard = match gateway_unconfigured_at().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.is_some_and(|at| at.elapsed() < GATEWAY_UNCONFIGURED_TTL)
}
pub fn clear_gateway_unconfigured() {
let mut guard = match gateway_unconfigured_at().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*guard = None;
}
pub(crate) fn is_managed_gateway_schema(model: &ModelSchema) -> bool {
let Some(canonical_id) = canonical_managed_gateway_selector(model) else {
return false;
};
let has_tag = |expected: &str| model.tags.iter().any(|tag| tag == expected);
model.name == canonical_id
&& ["builtin", "openrouter", "parslee", "managed"]
.into_iter()
.all(has_tag)
}
pub fn curated_model_count() -> usize {
CURATED.len()
}
pub fn curated_schemas() -> Vec<ModelSchema> {
let parslee_api_base = car_auth::api_base(None);
CURATED
.iter()
.flat_map(|model| {
[
schema(*model, false, &parslee_api_base),
schema(*model, true, &parslee_api_base),
]
})
.collect()
}
pub fn builtin_schemas() -> Vec<ModelSchema> {
curated_schemas()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn managed_aliases_follow_the_configured_parslee_api_base() {
let _environment = test_environment_scope();
unsafe {
std::env::set_var(
car_auth::PARSLEE_API_BASE_KEY,
"https://staging-api.parslee.ai/",
);
}
let managed: Vec<_> = curated_schemas()
.into_iter()
.filter(|schema| schema.provider == "parslee")
.collect();
assert_eq!(managed.len(), CURATED.len());
for schema in managed {
let ModelSource::Proprietary { endpoint, auth, .. } = schema.source else {
panic!("managed alias must use the Parslee proprietary transport");
};
assert_eq!(endpoint, "https://staging-api.parslee.ai");
let ProprietaryAuth::OAuth2Pkce { authority, .. } = auth else {
panic!("managed alias must use the Parslee OAuth connection");
};
assert_eq!(authority, "https://staging-api.parslee.ai");
}
unsafe {
std::env::remove_var(car_auth::PARSLEE_API_BASE_KEY);
}
}
#[test]
fn managed_alias_schema_keeps_the_production_default() {
let schema = schema(CURATED[0], true, car_auth::DEFAULT_API_BASE);
let ModelSource::Proprietary { endpoint, auth, .. } = schema.source else {
panic!("managed alias must use the Parslee proprietary transport");
};
assert_eq!(endpoint, car_auth::DEFAULT_API_BASE);
let ProprietaryAuth::OAuth2Pkce { authority, .. } = auth else {
panic!("managed alias must use the Parslee OAuth connection");
};
assert_eq!(authority, car_auth::DEFAULT_API_BASE);
}
#[test]
fn managed_aliases_match_the_gateway_contract() {
let managed: Vec<_> = curated_schemas()
.into_iter()
.filter(|schema| schema.provider == "parslee")
.collect();
assert!(
managed.iter().all(is_managed_gateway_schema),
"every reviewed alias must satisfy the managed gateway predicate"
);
let ids: Vec<_> = managed.into_iter().map(|schema| schema.id).collect();
assert_eq!(
ids,
[
"parslee/openrouter/frontier-general",
"parslee/openrouter/frontier-deep",
"parslee/openrouter/frontier-multimodal",
"parslee/openrouter/balanced-general",
"parslee/openrouter/open-multimodal",
"parslee/openrouter/open-long-context",
"parslee/openrouter/open-reasoning",
"parslee/openrouter/open-fast",
"parslee/openrouter/coding-frontier",
"parslee/openrouter/coding-efficient",
"parslee/openrouter/frontier-deep-next",
]
);
}
#[test]
fn managed_gateway_predicate_rejects_allowlisted_id_with_wrong_selector_name() {
let mut schema = curated_schemas()
.into_iter()
.find(|schema| schema.id == "parslee/openrouter/frontier-general")
.expect("managed frontier alias");
schema.name = "attacker-controlled-upstream-selector".into();
assert!(
!is_managed_gateway_schema(&schema),
"an allowlisted id cannot make a different outbound selector trusted"
);
}
#[test]
fn managed_aliases_deserialize_the_gateway_models_fixture_without_upstream_ids() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../tests/fixtures/parslee-openrouter-models.json"
))
.expect("gateway fixture must be valid JSON");
assert_eq!(fixture["object"], "list");
let rows = fixture["data"].as_array().expect("data array");
let ids: Vec<_> = rows
.iter()
.map(|row| row["id"].as_str().expect("opaque id"))
.collect();
let projected: Vec<_> = curated_schemas()
.into_iter()
.filter(|schema| schema.provider == "parslee")
.map(|schema| schema.id)
.collect();
assert_eq!(ids, projected);
for row in rows {
assert_eq!(row["object"], "model");
assert_eq!(row["owned_by"], "parslee");
assert_eq!(row["provider"], "openrouter");
assert!(row.get("upstream_model").is_none());
assert!(!row.to_string().contains("openai/"));
assert!(!row.to_string().contains("anthropic/"));
assert!(row["capabilities"].is_array());
assert!(row["tags"].is_array());
}
}
#[test]
fn managed_aliases_advertise_only_parameters_the_parslee_transport_forwards() {
let managed: Vec<_> = builtin_schemas()
.into_iter()
.filter(|schema| schema.provider == "parslee")
.collect();
assert_eq!(managed.len(), CURATED.len());
for schema in managed {
assert_eq!(
schema.supported_params,
vec![P::MaxTokens],
"{} advertises a request control the CAR-to-Parslee transport drops",
schema.id
);
}
}
#[test]
fn projections_are_bounded_distinct_and_price_identical() {
let schemas = curated_schemas();
assert_eq!(schemas.len(), CURATED.len() * 2);
for model in CURATED {
let personal = schemas
.iter()
.find(|s| s.id == format!("openrouter/{}", model.upstream_id))
.unwrap();
let gateway = schemas
.iter()
.find(|s| s.id == format!("parslee/openrouter/{}", model.managed_alias))
.unwrap();
assert_eq!(personal.cost.input_per_mtok, gateway.cost.input_per_mtok);
assert_eq!(personal.cost.output_per_mtok, gateway.cost.output_per_mtok);
assert_eq!(
personal.cost.cache_read_input_per_mtok,
gateway.cost.cache_read_input_per_mtok
);
assert_eq!(
personal.cost.cache_write_input_per_mtok,
gateway.cost.cache_write_input_per_mtok
);
assert_eq!(personal.cost.pricing_tiers, gateway.cost.pricing_tiers);
assert_eq!(personal.capabilities, gateway.capabilities);
}
}
#[test]
fn only_calibrated_models_are_frontier() {
let frontier: Vec<_> = curated_schemas()
.into_iter()
.filter(|s| s.provider == "openrouter" && s.tags.iter().any(|t| t == "frontier"))
.collect();
assert_eq!(frontier.len(), 4);
assert!(frontier
.iter()
.all(|s| !s.tags.iter().any(|t| t == "cheap")));
}
#[test]
fn personal_registry_is_exactly_the_reviewed_rows() {
let personal: Vec<_> = curated_schemas()
.into_iter()
.filter(|schema| schema.provider == "openrouter")
.collect();
let ids: Vec<_> = personal.iter().map(|schema| schema.id.as_str()).collect();
assert_eq!(
ids,
[
"openrouter/openai/gpt-5.4",
"openrouter/anthropic/claude-opus-4.6",
"openrouter/google/gemini-3.1-pro-preview",
"openrouter/anthropic/claude-sonnet-4.6",
"openrouter/moonshotai/kimi-k2.5",
"openrouter/qwen/qwen3.5-plus-02-15",
"openrouter/deepseek/deepseek-v3.2",
"openrouter/minimax/minimax-m2.5",
"openrouter/openai/gpt-5.3-codex",
"openrouter/qwen/qwen3-coder-next",
"openrouter/anthropic/claude-opus-4.8",
]
);
assert!(personal.iter().all(|schema| {
schema.trust_tier == TrustTier::Curated
&& schema.tags.iter().any(|tag| tag == "openrouter")
&& schema.tags.iter().any(|tag| tag == "personal-key")
&& !schema.tags.iter().any(|tag| tag == "dynamic")
}));
}
#[test]
fn personal_capabilities_and_params_match_reviewed_chat_transport_contract() {
let handler = crate::protocol::handler_for(ApiProtocol::OpenRouter);
assert!(!handler.supports_audio());
assert!(!handler.supports_video());
let expected = [
("openrouter/openai/gpt-5.4", CODE_REASON_VISION),
("openrouter/anthropic/claude-opus-4.6", CODE_REASON_VISION),
(
"openrouter/google/gemini-3.1-pro-preview",
CODE_REASON_VISION,
),
("openrouter/anthropic/claude-sonnet-4.6", CODE_REASON_VISION),
("openrouter/moonshotai/kimi-k2.5", CODE_REASON_VISION),
("openrouter/qwen/qwen3.5-plus-02-15", CODE_REASON_VISION),
("openrouter/deepseek/deepseek-v3.2", CODE_REASON),
("openrouter/minimax/minimax-m2.5", CODE_REASON_MULTI_TOOL),
("openrouter/openai/gpt-5.3-codex", CODE_REASON_VISION),
("openrouter/qwen/qwen3-coder-next", CODE),
("openrouter/anthropic/claude-opus-4.8", CODE_REASON_VISION),
];
let personal: Vec<_> = curated_schemas()
.into_iter()
.filter(|schema| schema.provider == "openrouter")
.collect();
for (id, capabilities) in expected {
let schema = personal.iter().find(|schema| schema.id == id).unwrap();
assert_eq!(
schema.capabilities, capabilities,
"{id} capability metadata drifted from the reviewed model contract"
);
assert!(!schema.has_capability(C::VideoUnderstanding));
assert!(!schema.has_capability(C::AudioUnderstanding));
assert!(!schema.has_capability(C::ImageGeneration));
assert!(!schema.has_capability(C::VideoGeneration));
}
let transport_params = [
P::Temperature,
P::MaxTokens,
P::ResponseFormat,
P::ExtendedThinking,
];
for schema in personal {
assert!(
schema
.supported_params
.iter()
.all(|parameter| transport_params.contains(parameter)),
"{} advertises a request parameter CAR's OpenRouter Chat adapter does not send",
schema.id
);
}
}
#[test]
fn current_catalog_prices_include_tiers_and_per_model_cache_rates() {
let schemas = curated_schemas();
let gpt = schemas
.iter()
.find(|schema| schema.id == "openrouter/openai/gpt-5.4")
.unwrap();
assert_eq!(gpt.cost.prices_for(271_999).input_per_mtok, Some(2.5));
assert_eq!(gpt.cost.prices_for(272_000).input_per_mtok, Some(5.0));
assert_eq!(gpt.cost.prices_for(272_000).output_per_mtok, Some(22.5));
assert_eq!(
gpt.cost.prices_for(272_000).cache_read_input_per_mtok,
Some(0.5)
);
let gemini = schemas
.iter()
.find(|schema| schema.id == "openrouter/google/gemini-3.1-pro-preview")
.unwrap();
assert_eq!(
gemini.cost.prices_for(199_999).cache_write_input_per_mtok,
Some(0.375)
);
assert_eq!(gemini.cost.prices_for(200_000).input_per_mtok, Some(4.0));
assert_eq!(gemini.cost.prices_for(200_000).output_per_mtok, Some(18.0));
assert_eq!(
gemini.cost.prices_for(200_000).cache_read_input_per_mtok,
Some(0.4)
);
let kimi = schemas
.iter()
.find(|schema| schema.id == "openrouter/moonshotai/kimi-k2.5")
.unwrap();
assert_eq!(kimi.cost.cache_read_input_per_mtok, Some(0.095));
assert_eq!(kimi.cost.cache_write_input_per_mtok, None);
}
#[test]
fn no_curated_cache_read_rate_exceeds_its_input_rate() {
for schema in curated_schemas() {
let thresholds = std::iter::once(0).chain(
schema
.cost
.pricing_tiers
.iter()
.map(|tier| tier.min_prompt_tokens),
);
for threshold in thresholds {
let prices = schema.cost.prices_for(threshold);
let (Some(input), Some(cache_read)) =
(prices.input_per_mtok, prices.cache_read_input_per_mtok)
else {
continue;
};
assert!(
cache_read <= input,
"{} prices a cache read at {cache_read}/MTok above its {input}/MTok input \
rate at prompt threshold {threshold}. The `≤` ceiling that \
`estimated_usd_bounded` puts on a MISSING cache-read rate assumes this \
never happens — if a provider now surcharges cached reads, that \
substitution needs both direction flags the way cache write does.",
schema.id
);
}
}
}
#[test]
fn claude_opus_4_8_matches_the_live_openrouter_catalog_row() {
let schemas = curated_schemas();
let personal = schemas
.iter()
.find(|schema| schema.id == "openrouter/anthropic/claude-opus-4.8")
.expect("built-in curated row, no runtime registration");
assert_eq!(personal.provider, "openrouter");
assert_eq!(personal.context_length, 1_000_000);
assert_eq!(personal.max_output_tokens, Some(128_000));
assert_eq!(personal.cost.input_per_mtok, Some(5.0));
assert_eq!(personal.cost.output_per_mtok, Some(25.0));
assert_eq!(personal.cost.cache_read_input_per_mtok, Some(0.5));
assert_eq!(personal.cost.cache_write_input_per_mtok, Some(6.25));
assert!(personal.cost.pricing_tiers.is_empty());
assert_eq!(personal.version, "2026-07-31");
let alias = schemas
.iter()
.find(|schema| schema.id == "parslee/openrouter/frontier-deep-next")
.expect("matching managed alias row");
assert!(is_managed_gateway_schema(alias));
assert!(!serde_json::to_string(alias).unwrap().contains("opus-4.8"));
}
#[test]
fn the_ten_preexisting_rows_keep_their_ids_prices_and_sizes() {
let expected: &[(&str, f64, f64, Option<f64>, Option<f64>, usize, usize)] = &[
(
"openrouter/openai/gpt-5.4",
2.5,
15.0,
Some(0.25),
None,
1_050_000,
128_000,
),
(
"openrouter/anthropic/claude-opus-4.6",
5.0,
25.0,
Some(0.5),
Some(6.25),
1_000_000,
128_000,
),
(
"openrouter/google/gemini-3.1-pro-preview",
2.0,
12.0,
Some(0.2),
Some(0.375),
1_048_576,
65_536,
),
(
"openrouter/anthropic/claude-sonnet-4.6",
3.0,
15.0,
Some(0.3),
Some(3.75),
1_000_000,
128_000,
),
(
"openrouter/moonshotai/kimi-k2.5",
0.57,
2.85,
Some(0.095),
None,
262_144,
262_144,
),
(
"openrouter/qwen/qwen3.5-plus-02-15",
0.26,
1.56,
None,
None,
1_000_000,
65_536,
),
(
"openrouter/deepseek/deepseek-v3.2",
0.269,
0.4,
Some(0.1345),
None,
163_840,
65_536,
),
(
"openrouter/minimax/minimax-m2.5",
0.15,
0.9,
Some(0.05),
None,
204_800,
196_608,
),
(
"openrouter/openai/gpt-5.3-codex",
1.75,
14.0,
Some(0.175),
None,
400_000,
128_000,
),
(
"openrouter/qwen/qwen3-coder-next",
0.11,
0.8,
Some(0.07),
None,
262_144,
262_144,
),
];
let schemas = curated_schemas();
assert_eq!(
expected.len() + 1,
curated_model_count(),
"this list must cover every curated row except the one added after it"
);
for (id, input, output, cache_read, cache_write, context, max_output) in expected {
let schema = schemas
.iter()
.find(|schema| &schema.id == id)
.unwrap_or_else(|| panic!("{id} must not be removed or renamed"));
assert_eq!(schema.context_length, *context, "{id} context window");
assert_eq!(
schema.max_output_tokens,
Some(*max_output),
"{id} max output tokens"
);
assert_eq!(schema.cost.input_per_mtok, Some(*input), "{id} input price");
assert_eq!(
schema.cost.output_per_mtok,
Some(*output),
"{id} output price"
);
assert_eq!(
schema.cost.cache_read_input_per_mtok, *cache_read,
"{id} cache-read price"
);
assert_eq!(
schema.cost.cache_write_input_per_mtok, *cache_write,
"{id} cache-write price"
);
assert_eq!(schema.version, "2026-07-22", "{id} snapshot date");
}
let qwen = schemas
.iter()
.find(|schema| schema.id == "openrouter/qwen/qwen3.5-plus-02-15")
.unwrap();
assert_eq!(qwen.cost.prices_for(255_999).input_per_mtok, Some(0.26));
assert_eq!(qwen.cost.prices_for(255_999).output_per_mtok, Some(1.56));
assert_eq!(qwen.cost.prices_for(256_000).input_per_mtok, Some(0.325));
assert_eq!(qwen.cost.prices_for(256_000).output_per_mtok, Some(1.95));
assert_eq!(
qwen.cost.prices_for(256_000).cache_read_input_per_mtok,
None
);
assert_eq!(
qwen.cost.prices_for(256_000).cache_write_input_per_mtok,
None
);
}
}