Add Groq platform (provider 4)
The 7th registry row and the first pure-pattern cycle: id "groq", GROQ_API_KEY > auth.json "groq" scope, https://api.groq.com/openai/v1 with KIGI_GROQ_BASE_URL override, OpenAI listing + ChatCompletions + Passthrough dialect (Groq accepts the OpenAI-style reasoning_effort scalar verbatim), enrichment-backed metadata with the tool-calling restriction (the listing carries whisper/tts/guard noise — 8 of 15 enrichment entries are non-chat). Review verdict: faithful pattern repeat, zero blocking findings — row facts verified against live Groq docs, registry integrity at 7, e2e proven strong on both axes (restriction + dialect mapping). Added the recommended pin: managed keys split on the FIRST slash, so Groq's provider-native slashed ids (openai/gpt-oss-120b and 10 more) round-trip as groq/openai/gpt-oss-120b → (Groq, openai/gpt-oss-120b). Also fixes a pre-existing test race this cycle surfaced: the enterprise- endpoints test asserted moonshot's fixed base while non-serial, racing serial tests that legitimately point KIGI_MOONSHOT_CN_BASE_URL at wiremock; now serial + env-unset like its documented siblings. Two display-only advisories logged as tracked debt (Groq's delta.reasoning field invisible in the TUI; platform-generic rate-limit copy).
This commit is contained in:
@@ -1123,6 +1123,89 @@ mod tests {
|
||||
assert_eq!(cfg.chat_compat, kigi_sampling_types::ChatCompat::Kimi);
|
||||
}
|
||||
|
||||
/// Groq-cycle e2e: pure pattern — polluted listing restricted to
|
||||
/// tool-calling enrichment models, Passthrough dialect (OpenAI-style
|
||||
/// reasoning_effort untouched on this wire).
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial_test::serial]
|
||||
async fn groq_listing_restricts_and_maps_passthrough_dialect() {
|
||||
let platform_server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/models"))
|
||||
.and(wiremock::matchers::header("Authorization", "Bearer gsk-1"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
|
||||
serde_json::json!({ "data": [
|
||||
{ "id": "llama-3.3-70b-versatile", "object": "model" },
|
||||
{ "id": "whisper-large-v3", "object": "model" }
|
||||
]}),
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&platform_server)
|
||||
.await;
|
||||
let modelsdev_server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/api.json"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
|
||||
serde_json::json!({ "groq": { "models": {
|
||||
"llama-3.3-70b-versatile": {
|
||||
"limit": {"context": 131072, "output": 32768},
|
||||
"tool_call": true
|
||||
},
|
||||
"whisper-large-v3": { "limit": {"context": 448} }
|
||||
}}}),
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&modelsdev_server)
|
||||
.await;
|
||||
let cache_dir = tempfile::tempdir().unwrap();
|
||||
let _base =
|
||||
kigi_test_support::EnvGuard::set(kigi_models::GROQ_BASE_URL_ENV, platform_server.uri());
|
||||
let _mdev = kigi_test_support::EnvGuard::set(
|
||||
crate::agent::enrichment_fetch::MODELS_DEV_URL_ENV,
|
||||
format!("{}/api.json", modelsdev_server.uri()),
|
||||
);
|
||||
let _mdev_cache = kigi_test_support::EnvGuard::set(
|
||||
crate::agent::enrichment_fetch::MODELS_DEV_CACHE_DIR_ENV,
|
||||
cache_dir.path(),
|
||||
);
|
||||
let endpoints = crate::agent::config::EndpointsConfig::default();
|
||||
let keys = crate::agent::models::PlatformApiKeys::test_single(
|
||||
kigi_models::PlatformId::Groq,
|
||||
"gsk-1",
|
||||
);
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
fetch_platform_models_blocking(&endpoints, None, &keys)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("fetch must succeed");
|
||||
assert_eq!(
|
||||
result
|
||||
.models
|
||||
.iter()
|
||||
.map(|m| m.id.as_deref().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["groq/llama-3.3-70b-versatile"],
|
||||
"whisper (enrichment-known, not tool-calling) must be dropped"
|
||||
);
|
||||
let entry = &result.models[0];
|
||||
assert_eq!(entry.context_window.get(), 131_072);
|
||||
assert_eq!(entry.max_completion_tokens, Some(32_768));
|
||||
let model_entry = crate::agent::config::ModelEntry::from_config_entry(entry);
|
||||
let creds = crate::agent::config::ResolvedCredentials {
|
||||
api_key: Some("gsk-1".into()),
|
||||
base_url: entry.base_url.clone(),
|
||||
auth_type: kigi_chat_state::AuthType::ApiKey,
|
||||
auth_scheme: Default::default(),
|
||||
};
|
||||
let cfg = crate::agent::config::sampling_config_for_model(&model_entry, creds, None);
|
||||
assert_eq!(
|
||||
cfg.chat_compat,
|
||||
kigi_sampling_types::ChatCompat::Passthrough,
|
||||
"groq entries must leave OpenAI-style bodies untouched"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_env_keys_parses_strings_and_rejects_non_strings() {
|
||||
use crate::agent::config::EnvKeys;
|
||||
|
||||
Reference in New Issue
Block a user