1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5
6use agent_runtime_identity_contract::{
7 CredentialCapability, RuntimeIdentityId, TenantProjectScope,
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13pub const SERVICE_NAME: &str = "agent-model-infra";
14pub const AUDIENCE: &str = "agent-model-infra";
15pub const DEFAULT_PORT: u16 = 5130;
16
17macro_rules! identifier {
18 ($name:ident, $label:literal, $max:expr) => {
19 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20 #[serde(transparent)]
21 pub struct $name(String);
22
23 impl $name {
24 pub fn new(value: impl Into<String>) -> Result<Self, String> {
25 let value = value.into();
26 validate_identifier($label, &value, $max)?;
27 Ok(Self(value))
28 }
29
30 pub fn as_str(&self) -> &str {
31 &self.0
32 }
33 }
34
35 impl fmt::Display for $name {
36 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37 self.0.fmt(formatter)
38 }
39 }
40 };
41}
42
43identifier!(ModelProfileId, "model_profile_id", 128);
44identifier!(ModelConfigId, "model_config_id", 128);
45identifier!(InvocationId, "invocation_id", 255);
46
47#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(transparent)]
49pub struct SecretRef(String);
50
51impl SecretRef {
52 pub fn new(value: impl Into<String>) -> Result<Self, String> {
53 let value = value.into();
54 if !value.starts_with("secret://") || value.len() > 512 || value.len() <= 9 {
55 return Err("credential_ref must be a secret:// reference of at most 512 bytes".into());
56 }
57 Ok(Self(value))
58 }
59
60 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63}
64
65impl fmt::Debug for SecretRef {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 formatter.write_str("SecretRef([REDACTED])")
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum ModelApiFamily {
74 OpenAi,
75 Claude,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum ModelProfileStatus {
81 Draft,
82 Active,
83 Disabled,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum ProviderKind {
89 OpenAiCompatible,
90 Anthropic,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum Modality {
96 Text,
97 Image,
98 Audio,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct ModelCapabilities {
104 pub input_modalities: BTreeSet<Modality>,
105 pub output_modalities: BTreeSet<Modality>,
106 pub tools: bool,
107 pub structured_output: bool,
108 pub reasoning: bool,
109 pub logprobs: bool,
110 pub token_count: bool,
111 pub realtime: bool,
112}
113
114impl Default for ModelCapabilities {
115 fn default() -> Self {
116 Self {
117 input_modalities: BTreeSet::from([Modality::Text]),
118 output_modalities: BTreeSet::from([Modality::Text]),
119 tools: true,
120 structured_output: false,
121 reasoning: false,
122 logprobs: false,
123 token_count: true,
124 realtime: false,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase", deny_unknown_fields)]
131pub struct ProviderEndpoint {
132 pub id: String,
133 pub base_url: String,
134 pub priority: u16,
135 pub weight: u16,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub region: Option<String>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct ModelPolicy {
143 pub connect_timeout_ms: u64,
144 pub first_event_timeout_ms: u64,
145 pub total_timeout_ms: u64,
146 pub stream_idle_timeout_ms: u64,
147 pub max_output_tokens: u32,
148 pub max_request_bytes: usize,
149 pub max_stream_event_bytes: usize,
150 pub max_concurrency: usize,
151}
152
153impl Default for ModelPolicy {
154 fn default() -> Self {
155 Self {
156 connect_timeout_ms: 3_000,
157 first_event_timeout_ms: 30_000,
158 total_timeout_ms: 300_000,
159 stream_idle_timeout_ms: 60_000,
160 max_output_tokens: 8_192,
161 max_request_bytes: 2 * 1024 * 1024,
162 max_stream_event_bytes: 256 * 1024,
163 max_concurrency: 64,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase", deny_unknown_fields)]
170pub struct ModelProfile {
171 pub id: ModelProfileId,
172 pub scope: TenantProjectScope,
173 pub display_name: String,
174 pub public_model_id: String,
175 pub api_families: BTreeSet<ModelApiFamily>,
176 pub capabilities: ModelCapabilities,
177 pub provider: ProviderKind,
178 pub provider_model: String,
179 pub endpoints: Vec<ProviderEndpoint>,
180 pub credential_ref: SecretRef,
181 pub credential_configured: bool,
182 pub policy: ModelPolicy,
183 pub status: ModelProfileStatus,
184 pub revision: u64,
185 pub created_at: DateTime<Utc>,
186 pub updated_at: DateTime<Utc>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase", deny_unknown_fields)]
191pub struct CreateModelProfileRequest {
192 pub id: ModelProfileId,
193 pub display_name: String,
194 pub public_model_id: String,
195 pub api_families: BTreeSet<ModelApiFamily>,
196 #[serde(default)]
197 pub capabilities: ModelCapabilities,
198 pub provider: ProviderKind,
199 pub provider_model: String,
200 pub endpoints: Vec<ProviderEndpoint>,
201 pub credential_ref: SecretRef,
202 #[serde(default)]
203 pub policy: ModelPolicy,
204 pub idempotency_key: String,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct UpdateModelProfileRequest {
210 pub expected_revision: u64,
211 pub display_name: String,
212 pub public_model_id: String,
213 pub api_families: BTreeSet<ModelApiFamily>,
214 pub capabilities: ModelCapabilities,
215 pub provider: ProviderKind,
216 pub provider_model: String,
217 pub endpoints: Vec<ProviderEndpoint>,
218 pub credential_ref: SecretRef,
219 pub policy: ModelPolicy,
220 pub idempotency_key: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase", deny_unknown_fields)]
227pub struct ModelConnectionConfig {
228 pub provider: ProviderKind,
229 pub base_url: String,
230 pub api_models: Vec<String>,
231 pub default_model: String,
232 pub api_families: BTreeSet<ModelApiFamily>,
233 #[serde(default)]
234 pub capabilities: ModelCapabilities,
235 #[serde(default)]
236 pub policy: ModelPolicy,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase", deny_unknown_fields)]
241pub struct AgentModelConfig {
242 pub id: ModelConfigId,
243 pub scope: TenantProjectScope,
244 pub agent_id: agent_registry_contract::AgentId,
245 pub display_name: String,
246 pub connection: ModelConnectionConfig,
247 pub api_key_configured: bool,
248 pub enabled: bool,
249 pub revision: u64,
250 pub created_at: DateTime<Utc>,
251 pub updated_at: DateTime<Utc>,
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase", deny_unknown_fields)]
256pub struct BuiltinModelConfig {
257 pub id: ModelConfigId,
258 pub display_name: String,
259 pub connection: ModelConnectionConfig,
260 pub api_key_configured: bool,
261 pub enabled: bool,
262 pub is_default: bool,
263 pub revision: u64,
264 pub created_at: DateTime<Utc>,
265 pub updated_at: DateTime<Utc>,
266}
267
268#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(transparent)]
270pub struct WriteOnlyApiKey(String);
271
272impl WriteOnlyApiKey {
273 pub fn new(value: impl Into<String>) -> Result<Self, String> {
274 let value = value.into();
275 if value.trim().is_empty() || value.len() > 16 * 1024 {
276 return Err("api_key must be 1..=16384 bytes".into());
277 }
278 Ok(Self(value))
279 }
280
281 pub fn expose(&self) -> &str {
282 &self.0
283 }
284}
285
286impl fmt::Debug for WriteOnlyApiKey {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 formatter.write_str("WriteOnlyApiKey([REDACTED])")
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase", deny_unknown_fields)]
294pub struct UpsertAgentModelConfigRequest {
295 pub display_name: String,
296 pub connection: ModelConnectionConfig,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub api_key: Option<WriteOnlyApiKey>,
299 #[serde(default = "default_true")]
300 pub enabled: bool,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub expected_revision: Option<u64>,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
306#[serde(rename_all = "camelCase", deny_unknown_fields)]
307pub struct UpsertBuiltinModelConfigRequest {
308 pub display_name: String,
309 pub connection: ModelConnectionConfig,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub api_key: Option<WriteOnlyApiKey>,
312 #[serde(default = "default_true")]
313 pub enabled: bool,
314 #[serde(default)]
315 pub is_default: bool,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub expected_revision: Option<u64>,
318}
319
320fn default_true() -> bool {
321 true
322}
323
324pub const PLATFORM_ADMIN_ROLE: &str = "platform_admin";
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase", deny_unknown_fields)]
328pub struct PlatformAdminUser {
329 pub username: String,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub user_id: Option<String>,
332 pub role: String,
333 pub active: bool,
334 pub created_at: DateTime<Utc>,
335 pub updated_at: DateTime<Utc>,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase", deny_unknown_fields)]
340pub struct CreatePlatformAdminUserRequest {
341 pub username: String,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase", deny_unknown_fields)]
346pub struct UpdatePlatformAdminUserRequest {
347 pub active: bool,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct PlatformAdminUserPage {
353 pub items: Vec<PlatformAdminUser>,
354}
355
356pub fn parse_admin_username(value: &str) -> Result<String, String> {
357 let value = value.trim();
358 validate_identifier("username", value, 255)?;
359 Ok(value.to_owned())
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase", deny_unknown_fields)]
364pub struct ProfileCommandRequest {
365 pub expected_revision: u64,
366 pub idempotency_key: String,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase", deny_unknown_fields)]
371pub struct ListModelProfilesRequest {
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub status: Option<ModelProfileStatus>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub after: Option<ModelProfileId>,
376 #[serde(default = "default_list_limit")]
377 pub limit: usize,
378}
379
380impl Default for ListModelProfilesRequest {
381 fn default() -> Self {
382 Self {
383 status: None,
384 after: None,
385 limit: default_list_limit(),
386 }
387 }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase", deny_unknown_fields)]
392pub struct ListModelProfilesResponse {
393 pub items: Vec<ModelProfile>,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub next: Option<ModelProfileId>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct ModelActor {
400 pub scope: TenantProjectScope,
401 pub runtime_identity_id: RuntimeIdentityId,
402 pub capabilities: Vec<CredentialCapability>,
403 pub credential_expires_at_seconds: Option<u64>,
404 pub relations: agent_registry_contract::ResourceRelations,
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "snake_case")]
409pub enum CanonicalRole {
410 Developer,
411 System,
412 User,
413 Assistant,
414 Tool,
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418#[serde(tag = "type", rename_all = "snake_case")]
419pub enum CanonicalContent {
420 Text {
421 text: String,
422 },
423 Image {
424 source: Value,
425 },
426 ToolUse {
427 id: String,
428 name: String,
429 input: Value,
430 },
431 ToolResult {
432 tool_use_id: String,
433 content: Value,
434 },
435 Audio {
436 data: String,
437 format: String,
438 },
439 Thinking {
440 text: String,
441 },
442}
443
444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct CanonicalMessage {
447 pub role: CanonicalRole,
448 pub content: Vec<CanonicalContent>,
449}
450
451#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct ToolDefinition {
454 pub name: String,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub description: Option<String>,
457 pub input_schema: Value,
458}
459
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct CanonicalModelRequest {
463 pub invocation_id: InvocationId,
464 pub public_model_id: String,
465 pub source_api: ModelApiFamily,
466 pub messages: Vec<CanonicalMessage>,
467 #[serde(default)]
468 pub tools: Vec<ToolDefinition>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub tool_choice: Option<Value>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub response_format: Option<Value>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub temperature: Option<f32>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub max_output_tokens: Option<u32>,
477 #[serde(default)]
478 pub stop_sequences: Vec<String>,
479 #[serde(default)]
480 pub metadata: BTreeMap<String, String>,
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
484#[serde(rename_all = "camelCase")]
485pub struct TokenUsage {
486 pub input_tokens: u64,
487 pub output_tokens: u64,
488}
489
490#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
491#[serde(rename_all = "camelCase")]
492pub struct CanonicalModelResponse {
493 pub invocation_id: InvocationId,
494 pub provider_request_id: Option<String>,
495 pub output: Vec<CanonicalContent>,
496 pub finish_reason: String,
497 pub usage: TokenUsage,
498}
499
500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
501#[serde(tag = "type", rename_all = "snake_case")]
502pub enum CanonicalModelEvent {
503 Started {
504 provider_request_id: Option<String>,
505 },
506 TextDelta {
507 delta: String,
508 },
509 ToolUseStart {
510 index: usize,
511 id: String,
512 name: String,
513 },
514 ToolArgumentsDelta {
515 index: usize,
516 delta: String,
517 },
518 Usage {
519 usage: TokenUsage,
520 },
521 Completed {
522 finish_reason: String,
523 },
524 Failed {
525 code: String,
526 message: String,
527 },
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
531#[serde(rename_all = "snake_case")]
532pub enum InvocationChannel {
533 Http,
534 Sse,
535 Websocket,
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
539#[serde(rename_all = "snake_case")]
540pub enum InvocationState {
541 Accepted,
542 Connecting,
543 Streaming,
544 Completed,
545 Failed,
546 Cancelled,
547}
548
549#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(rename_all = "snake_case")]
551pub enum ModelCredentialSource {
552 Agent,
553 Builtin,
554 #[default]
555 LegacyProfile,
556}
557
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct ModelInvocation {
561 pub id: InvocationId,
562 pub scope: TenantProjectScope,
563 pub runtime_identity_id: RuntimeIdentityId,
564 #[serde(default)]
565 pub relations: agent_registry_contract::ResourceRelations,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
567 pub agent_id: Option<agent_registry_contract::AgentId>,
568 #[serde(default)]
569 pub requested_model_id: String,
570 #[serde(default)]
571 pub resolved_model_id: String,
572 #[serde(default)]
573 pub credential_source: ModelCredentialSource,
574 pub profile_id: ModelProfileId,
575 pub profile_revision: u64,
576 pub api_family: ModelApiFamily,
577 pub channel: InvocationChannel,
578 pub state: InvocationState,
579 pub request_digest: String,
580 pub provider_request_id: Option<String>,
581 pub usage: Option<TokenUsage>,
582 pub finish_reason: Option<String>,
583 pub error_code: Option<String>,
584 pub created_at: DateTime<Utc>,
585 pub completed_at: Option<DateTime<Utc>>,
586}
587
588#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct ModelUsageSummary {
591 pub invocation_count: u64,
592 pub completed_count: u64,
593 pub failed_count: u64,
594 pub input_tokens: u64,
595 pub output_tokens: u64,
596 pub builtin_invocation_count: u64,
597 pub agent_invocation_count: u64,
598}
599
600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
601#[serde(rename_all = "camelCase")]
602pub struct ModelUsageLedgerEntry {
603 pub invocation_id: InvocationId,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub agent_id: Option<agent_registry_contract::AgentId>,
606 pub requested_model_id: String,
607 pub resolved_model_id: String,
608 pub credential_source: ModelCredentialSource,
609 pub state: InvocationState,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub usage: Option<TokenUsage>,
612 pub created_at: DateTime<Utc>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub completed_at: Option<DateTime<Utc>>,
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub enum ModelErrorKind {
619 Invalid,
620 Unauthenticated,
621 Forbidden,
622 NotFound,
623 Conflict,
624 RateLimited,
625 Timeout,
626 Overloaded,
627 Upstream,
628 Internal,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
632pub struct ModelError {
633 pub kind: ModelErrorKind,
634 pub code: String,
635 pub message: String,
636 pub param: Option<String>,
637 pub retryable: bool,
638 pub outcome_unknown: bool,
639}
640
641impl ModelError {
642 pub fn new(kind: ModelErrorKind, code: &str, message: impl Into<String>) -> Self {
643 Self {
644 kind,
645 code: code.into(),
646 message: message.into(),
647 param: None,
648 retryable: false,
649 outcome_unknown: false,
650 }
651 }
652
653 pub fn invalid(code: &str, message: impl Into<String>, param: Option<&str>) -> Self {
654 let mut error = Self::new(ModelErrorKind::Invalid, code, message);
655 error.param = param.map(str::to_owned);
656 error
657 }
658}
659
660impl fmt::Display for ModelError {
661 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
662 self.message.fmt(formatter)
663 }
664}
665
666impl std::error::Error for ModelError {}
667
668pub type ModelResult<T> = Result<T, ModelError>;
669
670pub fn capability(profile_id: &ModelProfileId, action: &str) -> String {
671 format!("model-profile:{profile_id}:{action}")
672}
673
674pub fn validate_identifier(label: &str, value: &str, max: usize) -> Result<(), String> {
675 if value.is_empty() || value.len() > max {
676 return Err(format!("{label} must contain 1..={max} bytes"));
677 }
678 if !value.bytes().all(|byte| {
679 byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'.' | b'-' | b'_' | b'@')
680 }) {
681 return Err(format!("{label} contains unsupported characters"));
682 }
683 Ok(())
684}
685
686fn default_list_limit() -> usize {
687 50
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn secret_debug_never_reveals_reference() {
696 let secret = SecretRef::new("secret://model-infra/prod").unwrap();
697 assert_eq!(format!("{secret:?}"), "SecretRef([REDACTED])");
698 }
699
700 #[test]
701 fn profile_body_cannot_override_scope() {
702 let result = serde_json::from_value::<CreateModelProfileRequest>(serde_json::json!({
703 "id":"echo", "tenantId":"attacker"
704 }));
705 assert!(result.is_err());
706 }
707
708 #[test]
709 fn admin_username_accepts_sky_gateway_usernames() {
710 assert_eq!(parse_admin_username(" jinc.cen ").unwrap(), "jinc.cen");
711 assert!(parse_admin_username("").is_err());
712 assert!(parse_admin_username("bad name").is_err());
713 }
714}