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
+10 -3
View File
@@ -6894,7 +6894,7 @@ pub(crate) mod tests {
#[test]
fn pending_menu_items_lists_interactive_methods_plus_quit() {
let items = pending_menu_items(&fresh_user_auth_methods(), None);
assert_eq!(items.len(), 11, "10 login rows + Quit, got {items:?}");
assert_eq!(items.len(), 12, "11 login rows + Quit, got {items:?}");
assert!(
matches!(&items[0], PendingMenuItem::Login { label } if label == "Kimi Code (OAuth)"),
"row 0 must be the OAuth login, got {:?}",
@@ -6964,7 +6964,14 @@ pub(crate) mod tests {
label: "Google Gemini (API key)".into(),
}
);
assert_eq!(items[10], PendingMenuItem::Quit);
assert_eq!(
items[10],
PendingMenuItem::ApiKey {
target: PlatformLogin(kigi_shell::models::PlatformId::OpenRouter),
label: "OpenRouter (API key)".into(),
}
);
assert_eq!(items[11], PendingMenuItem::Quit);
// The non-interactive methods must never appear as rows.
let byok = kigi_shell::agent::auth_method::build_auth_methods(
kigi_shell::agent::auth_method::AuthMethodsBuildInputs {
@@ -6975,7 +6982,7 @@ pub(crate) mod tests {
);
assert_eq!(
pending_menu_items(&byok.methods, None).len(),
11,
12,
"xai.api_key / cached_token must not add rows"
);
}