Skip to main content

systemprompt_models/bridge/
profile.rs

1//! Wire contract for `GET /v1/bridge/profile`.
2//!
3//! The desktop bridge (`bin/bridge`) fetches this to render host configuration
4//! and to decide which provider models each host advertises. The server
5//! (`crates/entry/api`) produces it and the bridge consumes it through these
6//! exact types, so the two sides cannot drift.
7//!
8//! Every field is derived in [`build`] from
9//! [`ProviderRegistry::advertised_providers`], the single bearer of the
10//! advertisement rule ([`ApiSurface::is_advertised`]). A `surface: backend`
11//! provider is therefore structurally absent from both `providers` and the
12//! flat `models` front door. The flat `models` list is the *whole* advertised
13//! set, not one family's projection: the gateway transcodes every inbound wire
14//! to every provider wire, so every advertised model is reachable from every
15//! host. `providers` carries the per-provider split the bridge uses to build
16//! the narrower per-host views (Claude Desktop being the only host that
17//! narrows).
18//!
19//! Copyright (c) systemprompt.io — Business Source License 1.1.
20//! See <https://systemprompt.io> for licensing details.
21
22use serde::{Deserialize, Serialize};
23
24use crate::services::{ApiSurface, ProviderRegistry};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct BridgeProfileResponse {
28    pub inference_gateway_base_url: String,
29    pub auth_scheme: String,
30    #[serde(default)]
31    pub models: Vec<String>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub default_model: Option<String>,
34    #[serde(default)]
35    pub organization_uuid: Option<String>,
36    #[serde(default)]
37    pub providers: Vec<ProviderHealth>,
38}
39
40pub const KNOWN_HOSTS: &[&str] = &[
41    "claude-code",
42    "claude-desktop",
43    "codex-cli",
44    "hermes",
45    "opencode",
46];
47
48/// A provider whose credential secret is absent is flagged
49/// (`configured = false`) rather than dropped silently.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ProviderHealth {
52    pub name: String,
53    pub surface: ApiSurface,
54    pub configured: bool,
55    #[serde(default)]
56    pub models: Vec<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub config_issue: Option<String>,
59}
60
61pub fn provider_health(
62    registry: &ProviderRegistry,
63    secret_present: impl Fn(&str) -> bool,
64) -> Vec<ProviderHealth> {
65    registry
66        .advertised_providers()
67        .map(|entry| {
68            let secret = entry.api_key_secret.as_str();
69            let configured = secret_present(secret);
70            ProviderHealth {
71                name: entry.name.as_str().to_owned(),
72                surface: entry.surface,
73                configured,
74                models: entry
75                    .models
76                    .iter()
77                    .flat_map(|m| {
78                        std::iter::once(m.id.as_str().to_owned())
79                            .chain(m.aliases.iter().map(|a| a.as_str().to_owned()))
80                    })
81                    .collect(),
82                config_issue: (!configured)
83                    .then(|| format!("API key secret '{secret}' is not configured")),
84            }
85        })
86        .collect()
87}
88
89#[derive(Debug, Clone)]
90pub struct BridgeProfileParams<'a> {
91    pub inference_gateway_base_url: String,
92    pub auth_scheme: String,
93    pub organization_uuid: Option<String>,
94    pub default_model: Option<String>,
95    pub registry: &'a ProviderRegistry,
96}
97
98#[must_use]
99pub fn build(
100    params: BridgeProfileParams<'_>,
101    secret_present: impl Fn(&str) -> bool,
102) -> BridgeProfileResponse {
103    let BridgeProfileParams {
104        inference_gateway_base_url,
105        auth_scheme,
106        organization_uuid,
107        default_model,
108        registry,
109    } = params;
110    BridgeProfileResponse {
111        inference_gateway_base_url,
112        auth_scheme,
113        models: registry.advertised_model_ids(&[]),
114        default_model,
115        organization_uuid,
116        providers: provider_health(registry, secret_present),
117    }
118}