Skip to main content

systemprompt_models/
providers.rs

1//! Client-facing families a catalog model is advertised to.
2//!
3//! [`ClientFamily`] names the *client* side of the gateway: which host family
4//! lists a model in its picker (Claude Code and Desktop, OpenAI-protocol
5//! hosts such as Codex and `OpenCode`, Gemini-protocol hosts). It is
6//! independent of the wire the gateway speaks upstream, because the gateway
7//! transcodes every inbound wire to every outbound one. A model's
8//! `clients: [...]` list is its whole visibility rule, and `clients: []` is
9//! "not advertised" — the model still dispatches when asked for by id.
10//!
11//! [`without_context_variant`] strips the client-side context-window marker
12//! from a requested model id, and [`with_context_variant`] adds it, so the
13//! bridge profile and the gateway agree on one catalog entry.
14//!
15//! Copyright (c) systemprompt.io — Business Source License 1.1.
16//! See <https://systemprompt.io> for licensing details.
17
18use serde::{Deserialize, Serialize};
19
20#[derive(
21    Debug,
22    Clone,
23    Copy,
24    PartialEq,
25    Eq,
26    PartialOrd,
27    Ord,
28    Hash,
29    Serialize,
30    Deserialize,
31    schemars::JsonSchema,
32)]
33#[serde(rename_all = "lowercase")]
34pub enum ClientFamily {
35    Claude,
36    #[serde(rename = "openai")]
37    OpenAi,
38    Gemini,
39}
40
41impl ClientFamily {
42    pub const ALL: [Self; 3] = [Self::Claude, Self::OpenAi, Self::Gemini];
43
44    #[must_use]
45    pub const fn as_tag(self) -> &'static str {
46        match self {
47            Self::Claude => "claude",
48            Self::OpenAi => "openai",
49            Self::Gemini => "gemini",
50        }
51    }
52
53    #[must_use]
54    pub fn from_tag(tag: &str) -> Option<Self> {
55        Self::ALL.into_iter().find(|family| family.as_tag() == tag)
56    }
57}
58
59impl std::fmt::Display for ClientFamily {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_str(self.as_tag())
62    }
63}
64
65// Why: Claude Code budgets a gateway model at 200k unless the user picks its
66// `[1m]` variant (`claude-sonnet-5[1m]`). The suffix is a client-side context
67// marker, not a vendor id: it resolves to the base catalog entry and never
68// reaches the upstream.
69pub const CONTEXT_VARIANT_SUFFIX: &str = "[1m]";
70
71pub const LONG_CONTEXT_WINDOW: u32 = 1_000_000;
72
73#[must_use]
74pub fn without_context_variant(requested: &str) -> &str {
75    requested
76        .strip_suffix(CONTEXT_VARIANT_SUFFIX)
77        .unwrap_or(requested)
78}
79
80#[must_use]
81pub fn has_context_variant(requested: &str) -> bool {
82    requested.ends_with(CONTEXT_VARIANT_SUFFIX)
83}
84
85#[must_use]
86pub fn with_context_variant(model: &str) -> String {
87    format!("{}{CONTEXT_VARIANT_SUFFIX}", without_context_variant(model))
88}