1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Deterministic bridge-model selection from live provider catalogs.
//!
//! A cross-protocol bridge has to name an upstream model: a client speaking the
//! Anthropic Messages dialect sends `claude-…`, which means nothing to a Codex,
//! Qwen or Gemini upstream. That model used to come from a per-provider
//! constant baked into the router, which could advertise or route to models
//! that were renamed, withdrawn, or never entitled for the account (issue #192).
//!
//! Selection now reads the account's live catalog. When no compatible model
//! exists the request fails with `model_selection_required` rather than
//! silently substituting a source-code constant.
//!
//! # Policy
//!
//! The operator chooses how a model is picked from the catalog with
//! `--bridge-model-policy` / `BRIDGE_MODEL_POLICY`. Every policy is a total
//! order over the catalog, so the same catalog always yields the same choice.
use std::fmt;
/// How to choose an upstream model from a live catalog.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BridgeModelPolicy {
/// The catalog's first entry in lexicographic order.
///
/// Deterministic and provider-neutral: it encodes no opinion about which
/// vendor names are "better", which is what let the old constants drift.
#[default]
FirstAdvertised,
/// The catalog's last entry in lexicographic order.
///
/// Vendors commonly suffix newer models with higher version strings, so
/// this usually selects the newest generation without naming one.
LastAdvertised,
}
impl BridgeModelPolicy {
/// Parse an operator-facing policy name.
///
/// # Errors
///
/// Returns the accepted names when `value` is not one of them.
pub fn parse(value: &str) -> Result<Self, String> {
match value.trim().to_ascii_lowercase().replace('_', "-").as_str() {
"" | "first" | "first-advertised" => Ok(Self::FirstAdvertised),
"last" | "last-advertised" => Ok(Self::LastAdvertised),
other => Err(format!(
"unknown bridge model policy '{other}'; expected 'first-advertised' or 'last-advertised'"
)),
}
}
/// The operator-facing name of this policy.
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::FirstAdvertised => "first-advertised",
Self::LastAdvertised => "last-advertised",
}
}
/// Apply the policy to a catalog.
///
/// `catalog` is sorted before selection so the result depends only on the
/// set of advertised models, not on the order the provider returned them.
#[must_use]
pub fn choose(self, catalog: &[String]) -> Option<String> {
let mut sorted: Vec<&String> = catalog.iter().filter(|id| !id.is_empty()).collect();
sorted.sort();
match self {
Self::FirstAdvertised => sorted.first().map(|id| (*id).clone()),
Self::LastAdvertised => sorted.last().map(|id| (*id).clone()),
}
}
}
/// Why a bridged request could not be given an upstream model.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelSelectionRequired {
/// Provider whose catalog was consulted.
pub provider: String,
/// Why no model could be chosen.
pub reason: SelectionFailure,
}
/// The specific reason a catalog yielded no usable model.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionFailure {
/// No live discovery has succeeded for this account yet.
NotDiscovered,
/// A catalog exists but the credential is missing or revoked.
CredentialUnavailable,
/// The credential works but advertises no models.
EmptyCatalog,
}
impl fmt::Display for ModelSelectionRequired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let provider = &self.provider;
match self.reason {
SelectionFailure::NotDiscovered => write!(
f,
"no model can be selected for {provider}: its live catalog has not been \
discovered yet. Authorize the {provider} subscription, or set an explicit \
--bridge-model that the account advertises."
),
SelectionFailure::CredentialUnavailable => write!(
f,
"no model can be selected for {provider}: its credential is missing or has \
been rejected, so its last known catalog is not usable. Re-authorize the \
{provider} subscription."
),
SelectionFailure::EmptyCatalog => write!(
f,
"no model can be selected for {provider}: its live catalog advertises no \
models for this account."
),
}
}
}
/// The error code clients receive when selection fails.
pub const MODEL_SELECTION_REQUIRED: &str = "model_selection_required";
#[cfg(test)]
mod tests {
use super::*;
fn catalog(ids: &[&str]) -> Vec<String> {
ids.iter().map(|id| (*id).to_string()).collect()
}
// Deliberately synthetic names: no test here may depend on a real vendor
// model id, which is the regression issue #192 guards against.
const SYNTHETIC: [&str; 3] = ["aurora-1-mini", "aurora-2-base", "borealis-9-ultra"];
#[test]
fn first_advertised_is_lexicographically_first() {
assert_eq!(
BridgeModelPolicy::FirstAdvertised.choose(&catalog(&SYNTHETIC)),
Some("aurora-1-mini".to_string())
);
}
#[test]
fn last_advertised_is_lexicographically_last() {
assert_eq!(
BridgeModelPolicy::LastAdvertised.choose(&catalog(&SYNTHETIC)),
Some("borealis-9-ultra".to_string())
);
}
/// The choice must not depend on the order the provider listed models in.
#[test]
fn selection_is_independent_of_catalog_order() {
let forward = catalog(&["borealis-9-ultra", "aurora-1-mini", "aurora-2-base"]);
let reverse = catalog(&["aurora-2-base", "borealis-9-ultra", "aurora-1-mini"]);
assert_eq!(
BridgeModelPolicy::FirstAdvertised.choose(&forward),
BridgeModelPolicy::FirstAdvertised.choose(&reverse)
);
assert_eq!(
BridgeModelPolicy::LastAdvertised.choose(&forward),
BridgeModelPolicy::LastAdvertised.choose(&reverse)
);
}
#[test]
fn an_empty_catalog_selects_nothing() {
assert_eq!(BridgeModelPolicy::FirstAdvertised.choose(&[]), None);
assert_eq!(BridgeModelPolicy::LastAdvertised.choose(&[]), None);
// Blank entries are not models.
assert_eq!(
BridgeModelPolicy::FirstAdvertised.choose(&catalog(&["", ""])),
None
);
}
#[test]
fn policies_parse_from_operator_spelling() {
assert_eq!(
BridgeModelPolicy::parse("first-advertised"),
Ok(BridgeModelPolicy::FirstAdvertised)
);
assert_eq!(
BridgeModelPolicy::parse("LAST_ADVERTISED"),
Ok(BridgeModelPolicy::LastAdvertised)
);
assert_eq!(
BridgeModelPolicy::parse(""),
Ok(BridgeModelPolicy::FirstAdvertised)
);
assert!(BridgeModelPolicy::parse("cheapest-ever").is_err());
}
#[test]
fn each_failure_reason_names_the_provider_and_a_remedy() {
for reason in [
SelectionFailure::NotDiscovered,
SelectionFailure::CredentialUnavailable,
SelectionFailure::EmptyCatalog,
] {
let message = ModelSelectionRequired {
provider: "examplecorp".to_string(),
reason,
}
.to_string();
assert!(message.contains("examplecorp"), "{message}");
assert!(message.contains("no model can be selected"), "{message}");
}
}
}