Skip to main content

swink_agent_adapters/
remote_presets.rs

1use std::sync::Arc;
2
3#[cfg(feature = "gemini")]
4use swink_agent::ApiVersion;
5use swink_agent::{CatalogPreset, ModelConnection, ProviderKind, StreamFn, model_catalog};
6use thiserror::Error;
7
8#[cfg(feature = "anthropic")]
9use crate::AnthropicStreamFn;
10#[cfg(feature = "bedrock")]
11use crate::BedrockStreamFn;
12#[cfg(feature = "gemini")]
13use crate::GeminiStreamFn;
14#[cfg(feature = "mistral")]
15use crate::MistralStreamFn;
16#[cfg(feature = "openai")]
17use crate::OpenAiStreamFn;
18#[cfg(feature = "xai")]
19use crate::XAiStreamFn;
20#[cfg(feature = "azure")]
21use crate::{AzureAuth, AzureStreamFn};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct RemotePresetKey {
25    pub provider_key: &'static str,
26    pub preset_id: &'static str,
27}
28
29impl RemotePresetKey {
30    #[must_use]
31    pub const fn new(provider_key: &'static str, preset_id: &'static str) -> Self {
32        Self {
33            provider_key,
34            preset_id,
35        }
36    }
37}
38
39#[derive(Debug, Error, PartialEq, Eq)]
40pub enum RemoteModelConnectionError {
41    #[error("Unknown remote preset {provider_key}.{preset_id}")]
42    UnknownPreset {
43        provider_key: &'static str,
44        preset_id: &'static str,
45    },
46    #[error("No remote preset found for model_id \"{model_id}\"")]
47    UnknownModelId { model_id: String },
48    #[error("{provider_key}.{preset_id} is not a remote preset")]
49    NotRemotePreset {
50        provider_key: String,
51        preset_id: String,
52    },
53    #[error(
54        "Missing {env_var} for {preset}. Set it in your environment or .env before launching the example."
55    )]
56    MissingCredential { preset: String, env_var: String },
57    #[error(
58        "Missing {env_var} for {preset}. Set it in your environment or .env before launching the example."
59    )]
60    MissingBaseUrl { preset: String, env_var: String },
61    #[error(
62        "Missing {env_var} for {preset}. Set it in your environment or .env before launching the example."
63    )]
64    MissingRegion { preset: String, env_var: String },
65    #[error(
66        "Missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY for {preset}. Set AWS credentials in your environment or .env before launching the example."
67    )]
68    MissingAwsCredentials { preset: String },
69    #[error("Unsupported provider \"{provider_key}\" — no adapter feature enabled")]
70    UnsupportedProvider { provider_key: String },
71}
72
73/// Returns `true` if the adapter for the given provider key is compiled in.
74///
75/// Uses `#[cfg(feature = "...")]` checks so the answer is a compile-time
76/// constant for each provider. Provider keys that don't map to any adapter
77/// feature (e.g. `"local"`) always return `false`.
78#[must_use]
79#[allow(clippy::match_like_matches_macro)] // arms evaluate different cfg! flags, not a set membership check
80pub fn is_provider_compiled(provider_key: &str) -> bool {
81    match provider_key {
82        "anthropic" => cfg!(feature = "anthropic"),
83        "openai" => cfg!(feature = "openai"),
84        "google" => cfg!(feature = "gemini"),
85        "azure" => cfg!(feature = "azure"),
86        "xai" => cfg!(feature = "xai"),
87        "mistral" => cfg!(feature = "mistral"),
88        "bedrock" => cfg!(feature = "bedrock"),
89        _ => false,
90    }
91}
92
93/// Returns remote presets filtered to only those whose provider adapter is
94/// compiled in. Use [`all_remote_presets`] to enumerate the full catalog
95/// regardless of compiled adapter support.
96#[must_use]
97pub fn remote_presets(provider_key: Option<&str>) -> Vec<CatalogPreset> {
98    all_remote_presets(provider_key)
99        .into_iter()
100        .filter(|p| is_provider_compiled(&p.provider_key))
101        .collect()
102}
103
104/// Returns all remote presets from the catalog, regardless of feature flags.
105///
106/// Useful for discovery UIs that want to show available models even when the
107/// corresponding adapter is not compiled in.
108#[must_use]
109pub fn all_remote_presets(provider_key: Option<&str>) -> Vec<CatalogPreset> {
110    let catalog = model_catalog();
111    catalog
112        .providers
113        .iter()
114        .filter(|provider| provider.kind == ProviderKind::Remote)
115        .filter(|provider| provider_key.is_none_or(|key| provider.key == key))
116        .flat_map(|provider| {
117            provider
118                .presets
119                .iter()
120                .filter_map(|preset| catalog.preset(&provider.key, &preset.id))
121        })
122        .collect()
123}
124
125pub fn build_remote_connection(
126    key: RemotePresetKey,
127) -> Result<ModelConnection, RemoteModelConnectionError> {
128    let preset = required_catalog_preset(key)?;
129    build_connection_from_preset(
130        &preset,
131        preset
132            .credential_env_var
133            .as_deref()
134            .and_then(|env_var| std::env::var(env_var).ok()),
135        preset
136            .base_url_env_var
137            .as_deref()
138            .and_then(|env_var| std::env::var(env_var).ok())
139            .as_deref(),
140    )
141}
142
143/// Builds a [`ModelConnection`] for a preset key using an explicitly provided
144/// credential instead of reading it from the process environment.
145///
146/// This is useful for embedders that manage secrets in an external store and
147/// want to avoid process-global environment mutation.
148pub fn build_remote_connection_with_credential(
149    key: RemotePresetKey,
150    api_key: Option<String>,
151    base_url: Option<&str>,
152) -> Result<ModelConnection, RemoteModelConnectionError> {
153    let preset = required_catalog_preset(key)?;
154    build_connection_from_preset(&preset, api_key, base_url)
155}
156
157#[allow(unreachable_code, unused_variables)]
158pub fn build_connection_from_preset(
159    preset: &CatalogPreset,
160    api_key: Option<String>,
161    base_url: Option<&str>,
162) -> Result<ModelConnection, RemoteModelConnectionError> {
163    if preset.provider_kind != ProviderKind::Remote {
164        return Err(RemoteModelConnectionError::NotRemotePreset {
165            provider_key: preset.provider_key.clone(),
166            preset_id: preset.preset_id.clone(),
167        });
168    }
169
170    let provider_key = preset.provider_key.as_str();
171
172    let api_key = if provider_key == "bedrock" {
173        String::new()
174    } else {
175        let env_var = preset.credential_env_var.clone().ok_or_else(|| {
176            RemoteModelConnectionError::UnsupportedProvider {
177                provider_key: provider_key.to_string(),
178            }
179        })?;
180        match api_key {
181            Some(value) if !value.trim().is_empty() => value,
182            _ => {
183                return Err(RemoteModelConnectionError::MissingCredential {
184                    preset: preset.display_name.clone(),
185                    env_var,
186                });
187            }
188        }
189    };
190
191    let resolved_base_url = || {
192        base_url
193            .map(str::to_string)
194            .or_else(|| preset.default_base_url.clone())
195            .ok_or_else(|| RemoteModelConnectionError::MissingBaseUrl {
196                preset: preset.display_name.clone(),
197                env_var: preset
198                    .base_url_env_var
199                    .clone()
200                    .unwrap_or_else(|| "BASE_URL".to_string()),
201            })
202    };
203    let stream_fn: Arc<dyn StreamFn> = match provider_key {
204        #[cfg(feature = "anthropic")]
205        "anthropic" => Arc::new(AnthropicStreamFn::new(resolved_base_url()?, &api_key)),
206        #[cfg(feature = "openai")]
207        "openai" => Arc::new(OpenAiStreamFn::new(resolved_base_url()?, &api_key)),
208        #[cfg(feature = "gemini")]
209        "google" => Arc::new(GeminiStreamFn::new(
210            resolved_base_url()?,
211            &api_key,
212            preset.api_version.clone().unwrap_or(ApiVersion::V1beta),
213        )),
214        #[cfg(feature = "azure")]
215        #[allow(clippy::redundant_clone)]
216        // Clone needed when multiple adapter features enabled
217        "azure" => Arc::new(AzureStreamFn::new(
218            resolved_base_url()?,
219            AzureAuth::ApiKey(api_key.clone()),
220        )),
221        #[cfg(feature = "xai")]
222        "xai" => Arc::new(XAiStreamFn::new(resolved_base_url()?, &api_key)),
223        #[cfg(feature = "mistral")]
224        "mistral" => Arc::new(MistralStreamFn::new(resolved_base_url()?, &api_key)),
225        #[cfg(feature = "bedrock")]
226        "bedrock" => {
227            let region_env_var = preset
228                .region_env_var
229                .clone()
230                .unwrap_or_else(|| "AWS_REGION".to_string());
231            let region = std::env::var(&region_env_var).map_err(|_| {
232                RemoteModelConnectionError::MissingRegion {
233                    preset: preset.display_name.clone(),
234                    env_var: region_env_var,
235                }
236            })?;
237            let access_key_id = std::env::var("AWS_ACCESS_KEY_ID").map_err(|_| {
238                RemoteModelConnectionError::MissingAwsCredentials {
239                    preset: preset.display_name.clone(),
240                }
241            })?;
242            let secret_access_key = std::env::var("AWS_SECRET_ACCESS_KEY").map_err(|_| {
243                RemoteModelConnectionError::MissingAwsCredentials {
244                    preset: preset.display_name.clone(),
245                }
246            })?;
247            let session_token = std::env::var("AWS_SESSION_TOKEN").ok();
248            Arc::new(BedrockStreamFn::new(
249                region,
250                access_key_id,
251                secret_access_key,
252                session_token,
253            ))
254        }
255        _ => {
256            return Err(RemoteModelConnectionError::UnsupportedProvider {
257                provider_key: provider_key.to_string(),
258            });
259        }
260    };
261    Ok(ModelConnection::new(preset.model_spec(), stream_fn))
262}
263
264/// Looks up a remote preset by its `model_id` (e.g. `"claude-sonnet-4-6"`).
265///
266/// This is the primary entry point for finding a preset — callers write
267/// `preset("claude-sonnet-4-6")` instead of constructing a `RemotePresetKey`
268/// and looking up the catalog manually.
269#[must_use]
270pub fn preset(model_id: &str) -> Option<CatalogPreset> {
271    remote_presets(None)
272        .into_iter()
273        .find(|p| p.model_id == model_id)
274}
275
276/// Builds a [`ModelConnection`] for a model identified by its `model_id`
277/// (e.g. `"claude-sonnet-4-6"`, `"gpt-5.4"`).
278///
279/// This is the simplest way to get a connection — it resolves the preset from
280/// the catalog by `model_id`, reads credentials from the environment, and
281/// constructs the appropriate provider-specific `StreamFn`.
282///
283/// # Errors
284///
285/// Returns [`RemoteModelConnectionError`] if the model is not found, is not a
286/// remote preset, or required credentials are missing from the environment.
287pub fn build_remote_connection_for_model(
288    model_id: &str,
289) -> Result<ModelConnection, RemoteModelConnectionError> {
290    let catalog_preset =
291        preset(model_id).ok_or_else(|| RemoteModelConnectionError::UnknownModelId {
292            model_id: model_id.to_string(),
293        })?;
294    build_connection_from_preset(
295        &catalog_preset,
296        catalog_preset
297            .credential_env_var
298            .as_deref()
299            .and_then(|env_var| std::env::var(env_var).ok()),
300        catalog_preset
301            .base_url_env_var
302            .as_deref()
303            .and_then(|env_var| std::env::var(env_var).ok())
304            .as_deref(),
305    )
306}
307
308fn required_catalog_preset(
309    key: RemotePresetKey,
310) -> Result<CatalogPreset, RemoteModelConnectionError> {
311    model_catalog()
312        .preset(key.provider_key, key.preset_id)
313        .ok_or(RemoteModelConnectionError::UnknownPreset {
314            provider_key: key.provider_key,
315            preset_id: key.preset_id,
316        })
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    // ── all_remote_presets (unfiltered) ──────────────────────────────────
324
325    #[test]
326    fn all_remote_presets_are_loaded_from_catalog() {
327        let all = all_remote_presets(None);
328        assert!(!all.is_empty(), "catalog should have remote presets");
329    }
330
331    #[test]
332    fn every_remote_provider_has_at_least_one_unfiltered_preset() {
333        let catalog = model_catalog();
334        for provider in &catalog.providers {
335            if provider.kind == ProviderKind::Remote {
336                let presets = all_remote_presets(Some(&provider.key));
337                assert!(
338                    !presets.is_empty(),
339                    "remote provider '{}' should have presets in the catalog",
340                    provider.key
341                );
342            }
343        }
344    }
345
346    #[test]
347    fn all_catalog_remote_presets_resolvable_by_provider_and_preset_id() {
348        let catalog = model_catalog();
349        for p in all_remote_presets(None) {
350            let found = catalog
351                .preset(&p.provider_key, &p.preset_id)
352                .unwrap_or_else(|| {
353                    panic!(
354                        "catalog.preset('{}', '{}') must resolve for model_id '{}'",
355                        p.provider_key, p.preset_id, p.model_id
356                    )
357                });
358            assert_eq!(found.model_id, p.model_id);
359        }
360    }
361
362    // ── is_provider_compiled ────────────────────────────────────────────
363
364    #[test]
365    fn is_provider_compiled_returns_false_for_unknown_provider() {
366        assert!(!is_provider_compiled("nonexistent"));
367        assert!(!is_provider_compiled("local"));
368        assert!(!is_provider_compiled(""));
369    }
370
371    #[test]
372    fn is_provider_compiled_matches_feature_gates() {
373        // Each assertion matches the compile-time cfg for the corresponding feature.
374        assert_eq!(
375            is_provider_compiled("anthropic"),
376            cfg!(feature = "anthropic")
377        );
378        assert_eq!(is_provider_compiled("openai"), cfg!(feature = "openai"));
379        assert_eq!(is_provider_compiled("google"), cfg!(feature = "gemini"));
380        assert_eq!(is_provider_compiled("azure"), cfg!(feature = "azure"));
381        assert_eq!(is_provider_compiled("xai"), cfg!(feature = "xai"));
382        assert_eq!(is_provider_compiled("mistral"), cfg!(feature = "mistral"));
383        assert_eq!(is_provider_compiled("bedrock"), cfg!(feature = "bedrock"));
384    }
385
386    // ── remote_presets (filtered) ───────────────────────────────────────
387
388    #[test]
389    fn remote_presets_only_contains_compiled_providers() {
390        for p in remote_presets(None) {
391            assert!(
392                is_provider_compiled(&p.provider_key),
393                "remote_presets() returned preset '{}' for provider '{}' which is not compiled",
394                p.preset_id,
395                p.provider_key
396            );
397        }
398    }
399
400    #[test]
401    fn remote_presets_subset_of_all_remote_presets() {
402        let filtered = remote_presets(None);
403        let all = all_remote_presets(None);
404        assert!(
405            filtered.len() <= all.len(),
406            "filtered ({}) must be <= all ({})",
407            filtered.len(),
408            all.len()
409        );
410        // Every filtered preset must also appear in the unfiltered list.
411        for p in &filtered {
412            assert!(
413                all.iter()
414                    .any(|a| a.model_id == p.model_id && a.provider_key == p.provider_key),
415                "filtered preset '{}.{}' not found in all_remote_presets",
416                p.provider_key,
417                p.preset_id
418            );
419        }
420    }
421
422    #[cfg(not(any(
423        feature = "anthropic",
424        feature = "openai",
425        feature = "gemini",
426        feature = "azure",
427        feature = "xai",
428        feature = "mistral",
429        feature = "bedrock",
430    )))]
431    #[test]
432    fn remote_presets_empty_when_no_adapters_compiled() {
433        let presets = remote_presets(None);
434        assert!(
435            presets.is_empty(),
436            "remote_presets() should be empty with no adapter features, got {} presets",
437            presets.len()
438        );
439    }
440
441    #[cfg(all(feature = "xai", not(feature = "openai")))]
442    #[test]
443    fn xai_feature_does_not_mark_openai_as_compiled() {
444        assert!(is_provider_compiled("xai"));
445        assert!(!is_provider_compiled("openai"));
446        assert!(
447            remote_presets(None)
448                .into_iter()
449                .all(|preset| preset.provider_key != "openai"),
450            "xai-only builds must not expose OpenAI presets as compiled",
451        );
452    }
453
454    // ── preset() (filtered) ────────────────────────────────────────────
455
456    #[test]
457    fn preset_only_finds_compiled_providers() {
458        // Take every model_id from the full catalog and verify that preset()
459        // only returns it when the provider is compiled.
460        for p in all_remote_presets(None) {
461            let result = preset(&p.model_id);
462            if is_provider_compiled(&p.provider_key) {
463                // May still be None if an earlier provider claimed this model_id.
464                // That's fine — we just verify it doesn't return an uncompiled one.
465                if let Some(found) = &result {
466                    assert!(
467                        is_provider_compiled(&found.provider_key),
468                        "preset('{}') returned uncompiled provider '{}'",
469                        p.model_id,
470                        found.provider_key
471                    );
472                }
473            }
474        }
475    }
476
477    #[test]
478    fn preset_returns_none_for_nonexistent_model() {
479        assert!(preset("nonexistent-model-xyz").is_none());
480    }
481
482    // ── preset key resolution ──────────────────────────────────────────
483
484    #[test]
485    fn preset_key_resolves_via_catalog() {
486        let key = RemotePresetKey::new("anthropic", "sonnet_46");
487        let catalog_preset = required_catalog_preset(key).unwrap();
488        assert_eq!(catalog_preset.model_id, "claude-sonnet-4-6");
489    }
490
491    // ── feature-gated connection tests ──────────────────────────────────
492
493    #[cfg(feature = "anthropic")]
494    #[test]
495    fn preset_finds_anthropic_when_compiled() {
496        let sonnet = preset("claude-sonnet-4-6").expect("sonnet preset should exist");
497        assert_eq!(sonnet.provider_key, "anthropic");
498        assert_eq!(sonnet.preset_id, "sonnet_46");
499    }
500
501    #[cfg(feature = "openai")]
502    #[test]
503    fn preset_finds_openai_when_compiled() {
504        let gpt = preset("gpt-5.4").expect("gpt-5.4 preset should exist");
505        assert_eq!(gpt.provider_key, "openai");
506    }
507
508    #[cfg(feature = "anthropic")]
509    #[test]
510    fn remote_connection_uses_catalog_model_spec() {
511        let key = RemotePresetKey::new("anthropic", "sonnet_46");
512        let preset = required_catalog_preset(key).unwrap();
513        let connection =
514            build_connection_from_preset(&preset, Some("test-key".to_string()), None).unwrap();
515        assert_eq!(connection.model_spec(), &preset.model_spec());
516    }
517
518    #[cfg(feature = "openai")]
519    #[test]
520    fn remote_preset_requires_key() {
521        let preset = preset("gpt-5.4").unwrap();
522        let err = match build_connection_from_preset(&preset, None, None) {
523            Ok(_) => panic!("expected missing credential error"),
524            Err(err) => err,
525        };
526        assert_eq!(
527            err,
528            RemoteModelConnectionError::MissingCredential {
529                preset: "OpenAI GPT-5.4".to_string(),
530                env_var: "OPENAI_API_KEY".to_string(),
531            }
532        );
533    }
534
535    #[cfg(feature = "anthropic")]
536    #[test]
537    fn explicit_credential_builds_remote_connection_without_env_lookup() {
538        let key = RemotePresetKey::new("anthropic", "sonnet_46");
539        let connection =
540            build_remote_connection_with_credential(key, Some("test-key".to_string()), None)
541                .unwrap();
542        assert_eq!(connection.model_spec().provider, "anthropic");
543        assert_eq!(connection.model_spec().model_id, "claude-sonnet-4-6");
544    }
545
546    #[cfg(feature = "anthropic")]
547    #[test]
548    fn explicit_credential_reports_missing_key() {
549        let key = RemotePresetKey::new("anthropic", "sonnet_46");
550        let err = match build_remote_connection_with_credential(key, None, None) {
551            Ok(_) => panic!("expected missing credential error"),
552            Err(err) => err,
553        };
554        assert_eq!(
555            err,
556            RemoteModelConnectionError::MissingCredential {
557                preset: "Anthropic Sonnet 4.6".to_string(),
558                env_var: "ANTHROPIC_API_KEY".to_string(),
559            }
560        );
561    }
562
563    #[cfg(feature = "bedrock")]
564    #[test]
565    fn bedrock_explicit_credential_path_does_not_require_api_key() {
566        let key = RemotePresetKey::new("bedrock", "anthropic_claude_sonnet_45");
567        let err = match build_remote_connection_with_credential(key, None, None) {
568            Ok(_) => panic!("expected missing region or AWS credentials"),
569            Err(err) => err,
570        };
571        assert!(
572            matches!(
573                err,
574                RemoteModelConnectionError::MissingRegion { .. }
575                    | RemoteModelConnectionError::MissingAwsCredentials { .. }
576            ),
577            "bedrock should skip API-key validation, got {err:?}",
578        );
579    }
580
581    // Explicit-but-empty credential must be rejected the same way `None` is.
582    // `Some("")` and `Some("   ")` can sneak past a naive `is_some()` check,
583    // so pin the contract: trimmed-empty explicit keys error as MissingCredential.
584    #[cfg(feature = "anthropic")]
585    #[test]
586    fn build_with_credential_rejects_explicit_empty_string() {
587        let key = RemotePresetKey::new("anthropic", "sonnet_46");
588        for candidate in [String::new(), "   ".to_string(), "\t\n".to_string()] {
589            let err = match build_remote_connection_with_credential(key, Some(candidate), None) {
590                Ok(_) => panic!("empty/whitespace explicit credential must error"),
591                Err(err) => err,
592            };
593            assert!(
594                matches!(err, RemoteModelConnectionError::MissingCredential { .. }),
595                "expected MissingCredential, got {err:?}"
596            );
597        }
598    }
599
600    // Unknown preset must short-circuit with UnknownPreset before any credential
601    // or env-var work happens — protects callers from misleading MissingCredential
602    // errors when the real problem is a bad preset key.
603    #[test]
604    fn build_with_credential_rejects_unknown_preset() {
605        let key = RemotePresetKey::new("anthropic", "nonexistent_preset_xyz");
606        let err =
607            match build_remote_connection_with_credential(key, Some("irrelevant".into()), None) {
608                Ok(_) => panic!("unknown preset must error"),
609                Err(err) => err,
610            };
611        assert_eq!(
612            err,
613            RemoteModelConnectionError::UnknownPreset {
614                provider_key: "anthropic",
615                preset_id: "nonexistent_preset_xyz",
616            }
617        );
618    }
619
620    // Bedrock uses SigV4, not a bearer token — `Some("")` must still route
621    // through the bedrock branch (which reads AWS env) and never return
622    // MissingCredential. Complements the `None` test above.
623    #[cfg(feature = "bedrock")]
624    #[test]
625    fn bedrock_ignores_explicit_empty_api_key() {
626        let key = RemotePresetKey::new("bedrock", "anthropic_claude_sonnet_45");
627        if let Err(err) = build_remote_connection_with_credential(key, Some(String::new()), None) {
628            assert!(
629                !matches!(err, RemoteModelConnectionError::MissingCredential { .. }),
630                "bedrock must not surface MissingCredential when api_key is Some(\"\"); got {err:?}"
631            );
632        }
633    }
634
635    #[test]
636    fn build_remote_connection_for_model_rejects_unknown() {
637        let result = build_remote_connection_for_model("nonexistent-xyz");
638        assert!(result.is_err());
639        let err = result.err().unwrap();
640        assert_eq!(
641            err,
642            RemoteModelConnectionError::UnknownModelId {
643                model_id: "nonexistent-xyz".to_string(),
644            }
645        );
646    }
647
648    #[test]
649    fn preset_by_model_id_returns_a_match_for_every_filtered_model_id() {
650        let mut seen = std::collections::HashSet::new();
651        for p in remote_presets(None) {
652            if seen.insert(p.model_id.clone()) {
653                assert!(
654                    preset(&p.model_id).is_some(),
655                    "preset('{}') must return Some for a compiled catalog model_id",
656                    p.model_id
657                );
658            }
659        }
660    }
661
662    #[cfg(feature = "anthropic")]
663    #[test]
664    fn preset_finds_representative_anthropic_model() {
665        let p = preset("claude-sonnet-4-6").expect("anthropic preset should exist");
666        assert_eq!(p.provider_key, "anthropic");
667    }
668
669    #[cfg(feature = "openai")]
670    #[test]
671    fn preset_finds_representative_openai_model() {
672        let p = preset("gpt-5.4").expect("openai preset should exist");
673        assert_eq!(p.provider_key, "openai");
674    }
675
676    #[cfg(feature = "gemini")]
677    #[test]
678    fn preset_finds_representative_gemini_model() {
679        let p = preset("gemini-3-flash-preview").expect("gemini preset should exist");
680        assert_eq!(p.provider_key, "google");
681    }
682
683    #[cfg(feature = "mistral")]
684    #[test]
685    fn preset_finds_representative_mistral_model() {
686        let p = preset("mistral-large-latest").expect("mistral preset should exist");
687        assert_eq!(p.provider_key, "mistral");
688    }
689}