Skip to main content

systemprompt_loader/vertex_discovery/
classify.rs

1//! Deciding which Vertex listing entries are models we can actually serve.
2//!
3//! A Model Garden listing mixes three populations under one JSON shape:
4//! serverless models Google hosts and bills per token; *deployable
5//! checkpoints*, which are weights plus a serving container and are callable
6//! only after you stand up an endpoint yourself; and, under
7//! `publishers/google`, every non-chat modality Google sells — embeddings,
8//! speech, image, video, robotics. There is no field that separates them by
9//! modality.
10//!
11//! So the rule is in two halves. Shape rules out what cannot be called at all
12//! (a checkpoint with a `deploy` action; a partner entry that is not `MaaS`).
13//! The rate card rules in what we are willing to serve — it is the only place
14//! that knows `gemini-2.5-flash` is chat and `gemini-embedding-001` is not.
15//!
16//! Shape (`is_serverless`): Google's own publisher is served serverlessly
17//! across the board, so shape says nothing there and everything is a
18//! candidate. A partner model qualifies only as `MaaS` — the `-maas` suffix
19//! Vertex gives every serverless partner model, the third-party OSS category,
20//! and no actions of its own. Anything with a `deploy` action is a checkpoint.
21//!
22//! Pricing (`classify_discovered`) keys the card by the listing endpoint's
23//! wire and knows nothing about who listed the model;
24//! every [`CatalogSource`](super::source::CatalogSource) is judged by exactly
25//! this rule, on the provider-agnostic [`DiscoveredModel`] shape.
26//!
27//! Copyright (c) systemprompt.io — Business Source License 1.1.
28//! See <https://systemprompt.io> for licensing details.
29
30use serde::Deserialize;
31use systemprompt_manifest::services::{VertexRateCard, VertexRateCardEntry};
32use systemprompt_wire::WireProtocol;
33
34use super::source::{DiscoveredModel, LaunchStage};
35
36const MAAS_SUFFIX: &str = "-maas";
37
38const THIRD_PARTY_OSS: &str = "THIRD_PARTY_OWNED_OSS";
39
40const GOOGLE_PUBLISHER: &str = "google";
41
42const GA: &str = "GA";
43
44/// One entry of `publisherModels`, named
45/// `publishers/{publisher}/models/{model}`; the rate card names an upstream as
46/// `{publisher}/{model}`.
47///
48/// Unknown fields are ignored on purpose: the listing carries presentation
49/// data (notebook links, container specs, regional availability) that grows
50/// without notice, and a boot-time reader that fails on a new field would turn
51/// a Google release note into an outage.
52#[derive(Debug, Clone, Default, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct PublisherModel {
55    pub name: String,
56
57    #[serde(default)]
58    pub version_id: String,
59
60    #[serde(default)]
61    pub launch_stage: String,
62
63    #[serde(default)]
64    // JSON: Vertex AI `supportedActions` — undocumented action map, only probed for keys.
65    pub supported_actions: Option<serde_json::Value>,
66
67    #[serde(default)]
68    pub open_source_category: Option<String>,
69}
70
71impl PublisherModel {
72    #[must_use]
73    pub fn publisher(&self) -> &str {
74        let mut parts = self.name.split('/');
75        if parts.next() == Some("publishers") {
76            parts.next().unwrap_or_default()
77        } else {
78            ""
79        }
80    }
81
82    #[must_use]
83    pub fn model_name(&self) -> &str {
84        self.name.rsplit('/').next().unwrap_or(&self.name)
85    }
86
87    #[must_use]
88    pub fn upstream(&self) -> String {
89        format!("{}/{}", self.publisher(), self.model_name())
90    }
91
92    #[must_use]
93    pub fn is_generally_available(&self) -> bool {
94        self.launch_stage == GA
95    }
96
97    #[must_use]
98    pub fn discovered(&self) -> DiscoveredModel {
99        DiscoveredModel {
100            upstream: self.upstream(),
101            launch_stage: if self.is_generally_available() {
102                LaunchStage::GenerallyAvailable
103            } else {
104                LaunchStage::Preview
105            },
106            serverless: is_serverless(self),
107        }
108    }
109
110    fn is_deployable_checkpoint(&self) -> bool {
111        self.supported_actions
112            .as_ref()
113            .is_some_and(|actions| actions.get("deploy").is_some())
114    }
115
116    fn declares_actions(&self) -> bool {
117        match self.supported_actions.as_ref() {
118            None | Some(serde_json::Value::Null) => false,
119            Some(serde_json::Value::Object(actions)) => !actions.is_empty(),
120            Some(_) => true,
121        }
122    }
123}
124
125#[must_use]
126pub fn is_serverless(model: &PublisherModel) -> bool {
127    if model.is_deployable_checkpoint() {
128        return false;
129    }
130    if model.publisher() == GOOGLE_PUBLISHER {
131        return true;
132    }
133    model.model_name().ends_with(MAAS_SUFFIX)
134        && model.open_source_category.as_deref() == Some(THIRD_PARTY_OSS)
135        && !model.declares_actions()
136}
137
138/// What discovery decided about one listing entry.
139///
140/// Not serverless (needs a deploy, or not a `MaaS` partner model); callable
141/// but unpriced by the rate card, so never served; priced but not GA with a
142/// card entry that does not opt into previews; or priced and publishable.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum Classification {
145    NotServerless,
146    Unpriced,
147    PreviewWithheld,
148    Publish,
149}
150
151#[must_use]
152pub fn classify<'a>(
153    model: &PublisherModel,
154    card: &'a VertexRateCard,
155    wire: WireProtocol,
156) -> (Classification, Option<&'a VertexRateCardEntry>) {
157    classify_discovered(&model.discovered(), card, wire)
158}
159
160#[must_use]
161pub fn classify_discovered<'a>(
162    model: &DiscoveredModel,
163    card: &'a VertexRateCard,
164    wire: WireProtocol,
165) -> (Classification, Option<&'a VertexRateCardEntry>) {
166    if !model.serverless {
167        return (Classification::NotServerless, None);
168    }
169    let Some(entry) = card.lookup(&model.upstream).filter(|e| e.wire == wire) else {
170        return (Classification::Unpriced, None);
171    };
172    if !model.launch_stage.is_generally_available() && !entry.allow_preview {
173        return (Classification::PreviewWithheld, Some(entry));
174    }
175    (Classification::Publish, Some(entry))
176}