Add OpenRouter platform: wire-served metadata (provider 8)

The 11th registry row and the first THIRD-PARTY wire_serves_metadata=true
provider: id "openrouter", OPENROUTER_API_KEY > auth.json "openrouter"
scope, https://openrouter.ai/api/v1 (note /api/v1) with
KIGI_OPENROUTER_BASE_URL override, Bearer, OpenAI listing +
ChatCompletions + Passthrough.

OpenRouter's public /models serves context_length for every model
(verified live: 340/340), so it needs NO enrichment: models_dev_id=None,
wire_serves_metadata=true, restrict_to_enriched=false. An OpenRouter-only
user makes zero models.dev calls; context comes straight from the listing.
Slashed ids (anthropic/claude-opus-4.8) round-trip through the managed key
via the first-slash split; the native id rides the wire. The e2e pins all
of this with the models.dev refresh disabled.

Review-confirmed defect fixed (and independently re-verified with live
curls): OpenRouter's /models is PUBLIC — GET /models returns 200 for ANY
key — so login key-validation would false-accept a bad key, deferring the
failure to the first chat 401. New spec field key_validation_path lets a
public-listing platform validate against an auth-requiring endpoint;
OpenRouter uses /key (401s for bad keys). Reusable for Vercel (also
public). Tests pin the /key validation and no regression to the default
/models path.

Gate caught a fixture regression: the kimi_import test used openrouter.ai
to represent a CUSTOM provider, which now correctly dedupes to the builtin
OpenRouter — moved the fixture to a reserved llm.example.test host that no
future platform can shadow.
This commit is contained in:
2026-07-21 12:41:14 -04:00
parent 7a2cd8a726
commit e7dbb98207
5 changed files with 257 additions and 15 deletions
@@ -394,7 +394,11 @@ pub(crate) async fn authenticate_platform_api_key(
let Some(key) = key else {
return Err(auth_err(missing_platform_key_error(platform)));
};
let url = format!("{}/models", platform.base_url().trim_end_matches('/'));
let url = format!(
"{}{}",
platform.base_url().trim_end_matches('/'),
platform.key_validation_path()
);
let request = match platform.key_header() {
kigi_models::PlatformKeyHeader::Bearer => crate::http::shared_client()
.get(&url)
@@ -598,7 +602,8 @@ mod tests {
"groq",
"mistral",
"fireworks",
"google"
"google",
"openrouter"
]
);
assert_eq!(default_id(&built), Some(XAI_API_KEY_METHOD_ID));
@@ -631,7 +636,8 @@ mod tests {
"groq",
"mistral",
"fireworks",
"google"
"google",
"openrouter"
]
);
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
@@ -657,7 +663,8 @@ mod tests {
"groq",
"mistral",
"fireworks",
"google"
"google",
"openrouter"
]
);
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
@@ -686,7 +693,8 @@ mod tests {
"groq",
"mistral",
"fireworks",
"google"
"google",
"openrouter"
]
);
assert_eq!(default_id(&built), None);
@@ -816,4 +824,63 @@ mod tests {
"the key must never leak into errors"
);
}
/// OpenRouter's `/models` is PUBLIC (200 for any key), so validation must
/// hit its auth-requiring `/key` endpoint instead — otherwise a bad key
/// false-accepts at login. The mock serves `/models` 200 always; a bad
/// key must still be rejected (proving `/models` is NOT what's validated).
#[tokio::test]
#[serial]
async fn openrouter_validates_against_key_endpoint_not_public_models() {
use wiremock::matchers::{method, path};
let server = wiremock::MockServer::start().await;
// Public listing: 200 for anyone. If validation used this, a bad key
// would pass.
wiremock::Mock::given(method("GET"))
.and(path("/models"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "data": [] })),
)
.mount(&server)
.await;
// Auth-required key endpoint: 401 for a bad key.
wiremock::Mock::given(method("GET"))
.and(path("/key"))
.respond_with(wiremock::ResponseTemplate::new(401))
.expect(1)
.mount(&server)
.await;
let _base = EnvGuard::set(kigi_models::OPENROUTER_BASE_URL_ENV, &server.uri());
let err =
authenticate_platform_api_key(kigi_models::PlatformId::OpenRouter, Some("sk-or-bad"))
.await
.expect_err("a bad key must be rejected via /key, not accepted via /models");
assert_eq!(
err.message,
"Invalid API key for openrouter \u{2014} check your key on openrouter.ai"
);
}
/// A valid OpenRouter key: `/key` returns 200 → accepted.
#[tokio::test]
#[serial]
async fn openrouter_valid_key_succeeds_via_key_endpoint() {
use wiremock::matchers::{header, method, path};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(method("GET"))
.and(path("/key"))
.and(header("Authorization", "Bearer sk-or-good"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "data": { "label": "k" } })),
)
.expect(1)
.mount(&server)
.await;
let _base = EnvGuard::set(kigi_models::OPENROUTER_BASE_URL_ENV, &server.uri());
authenticate_platform_api_key(kigi_models::PlatformId::OpenRouter, Some("sk-or-good"))
.await
.expect("200 from /key must validate the key");
}
}