Skip to content

g1t/crates/contracts/src/integrations.rs

780 lines29,591 bytesCodeBlame
1//! The integrations service: a workspace's connections to systems outside
2//! g1t, and everything that crosses between them.
3//!
4//! - **Models.** A workspace connects as many model providers as it uses
5//! (Anthropic, OpenAI, Gemini, and anything compatible with either API)
6//! and routes each kind of work to one of them, or to g1t's hosted models.
7//! Sandboxes never hold a key: they hold a token for one run, and the
8//! model proxy puts the credentials on each request, translating to
9//! OpenAI's API where the provider speaks it.
10//! - **Alerts.** Sentry, Datadog or any signed webhook opens an issue in a
11//! repository, once per problem however often it fires, and can put an
12//! agent on it.
13//! - **Trackers.** A Jira or Linear key, such as `TECH-1234`, resolves to the
14//! ticket: agents read it, people import it as an issue, and when the work
15//! lands the ticket is told.
16//!
17//! Mirrors `packages/contracts/src/integrations.ts`.
18
19use serde::{Deserialize, Serialize};
20
21use crate::repos::RepoPath;
22use crate::{User, Viewer};
23
24/// Which outside system a connection is to.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Provider {
28 // Model providers: the labs.
29 Anthropic,
30 Openai,
31 /// Through Gemini's OpenAI-compatible endpoint.
32 Gemini,
33 Xai,
34 Mistral,
35 Deepseek,
36 // Model providers: platforms that serve many labs' models.
37 /// A deployment on the workspace's own Azure OpenAI resource.
38 AzureOpenai,
39 Openrouter,
40 Groq,
41 Together,
42 Fireworks,
43 Cerebras,
44 // Model providers: anything else.
45 /// Any endpoint that speaks Anthropic's Messages API: the workspace's
46 /// own Cloudflare AI Gateway, LiteLLM, a proxy in front of Bedrock or
47 /// Vertex, or a self-hosted model.
48 AnthropicEndpoint,
49 /// Any endpoint that speaks OpenAI's Chat Completions API: vLLM,
50 /// Ollama behind a tunnel, LiteLLM, a gateway.
51 OpenaiEndpoint,
52 // Alerts.
53 Sentry,
54 Datadog,
55 /// Anything that can send a signed JSON request.
56 Webhook,
57 // Trackers.
58 Jira,
59 Linear,
60}
61
62/// What g1t knows about a provider.
63pub struct Spec {
64 pub provider: Provider,
65 pub name: &'static str,
66 pub label: &'static str,
67 pub kind: ProviderKind,
68 /// For a model provider: the API it speaks, `anthropic` or `openai`.
69 pub api: &'static str,
70 /// For a model provider with a fixed address: where its API is, with
71 /// the version for OpenAI's API and without it for Anthropic's. Empty
72 /// when the connection gives its own.
73 pub base_url: &'static str,
74 /// The header the key goes in; `authorization` means `Bearer <key>`.
75 pub auth_header: &'static str,
76}
77
78const fn model(provider: Provider, name: &'static str, label: &'static str, api: &'static str, base_url: &'static str, auth_header: &'static str) -> Spec {
79 Spec {
80 provider,
81 name,
82 label,
83 kind: ProviderKind::Models,
84 api,
85 base_url,
86 auth_header,
87 }
88}
89
90const fn other(provider: Provider, name: &'static str, label: &'static str, kind: ProviderKind) -> Spec {
91 Spec {
92 provider,
93 name,
94 label,
95 kind,
96 api: "",
97 base_url: "",
98 auth_header: "",
99 }
100}
101
102/// Every provider, in the order people are shown them.
103pub const PROVIDERS: [Spec; 19] = [
104 model(Provider::Anthropic, "anthropic", "Anthropic", "anthropic", "https://api.anthropic.com", "x-api-key"),
105 model(Provider::Openai, "openai", "OpenAI", "openai", "https://api.openai.com/v1", "authorization"),
106 model(Provider::Gemini, "gemini", "Google Gemini", "openai", "https://generativelanguage.googleapis.com/v1beta/openai", "authorization"),
107 model(Provider::Xai, "xai", "xAI", "openai", "https://api.x.ai/v1", "authorization"),
108 model(Provider::Mistral, "mistral", "Mistral", "openai", "https://api.mistral.ai/v1", "authorization"),
109 model(Provider::Deepseek, "deepseek", "DeepSeek", "openai", "https://api.deepseek.com/v1", "authorization"),
110 model(Provider::AzureOpenai, "azure_openai", "Azure OpenAI", "openai", "", "api-key"),
111 model(Provider::Openrouter, "openrouter", "OpenRouter", "openai", "https://openrouter.ai/api/v1", "authorization"),
112 model(Provider::Groq, "groq", "Groq", "openai", "https://api.groq.com/openai/v1", "authorization"),
113 model(Provider::Together, "together", "Together AI", "openai", "https://api.together.xyz/v1", "authorization"),
114 model(Provider::Fireworks, "fireworks", "Fireworks AI", "openai", "https://api.fireworks.ai/inference/v1", "authorization"),
115 model(Provider::Cerebras, "cerebras", "Cerebras", "openai", "https://api.cerebras.ai/v1", "authorization"),
116 model(Provider::AnthropicEndpoint, "anthropic_endpoint", "Anthropic-compatible endpoint", "anthropic", "", "x-api-key"),
117 model(Provider::OpenaiEndpoint, "openai_endpoint", "OpenAI-compatible endpoint", "openai", "", "authorization"),
118 other(Provider::Sentry, "sentry", "Sentry", ProviderKind::Alerts),
119 other(Provider::Datadog, "datadog", "Datadog", ProviderKind::Alerts),
120 other(Provider::Webhook, "webhook", "Webhook", ProviderKind::Alerts),
121 other(Provider::Jira, "jira", "Jira", ProviderKind::Tracker),
122 other(Provider::Linear, "linear", "Linear", ProviderKind::Tracker),
123];
124
125impl Provider {
126 pub fn spec(self) -> &'static Spec {
127 PROVIDERS
128 .iter()
129 .find(|spec| spec.provider == self)
130 .expect("every provider is in the catalogue")
131 }
132
133 pub fn all() -> impl Iterator<Item = Provider> {
134 PROVIDERS.iter().map(|spec| spec.provider)
135 }
136
137 pub fn name(self) -> &'static str {
138 self.spec().name
139 }
140
141 pub fn parse(name: &str) -> Option<Provider> {
142 PROVIDERS.iter().find(|spec| spec.name == name).map(|spec| spec.provider)
143 }
144
145 /// What people call it.
146 pub fn label(self) -> &'static str {
147 self.spec().label
148 }
149
150 pub fn kind(self) -> ProviderKind {
151 self.spec().kind
152 }
153
154 /// Whether it sends g1t requests, at the connection's own address.
155 pub fn receives(self) -> bool {
156 self.kind() == ProviderKind::Alerts
157 }
158
159 /// For a model provider, the API it speaks: `anthropic` or `openai`.
160 pub fn api(self) -> &'static str {
161 self.spec().api
162 }
163
164 /// Whether the connection gives the address, rather than g1t knowing it.
165 pub fn own_address(self) -> bool {
166 self.kind() == ProviderKind::Models && self.spec().base_url.is_empty()
167 }
168}
169
170/// The kinds of work a model is chosen for, and `default` for the rest.
171pub const MODEL_TASKS: [&str; 5] = ["default", "implement", "review", "plan", "update"];
172
173/// The tiers g1t routes its hosted models' work to, cheapest first: `small`
174/// (fast), `large` (standard) and `frontier` (most capable). A route to
175/// g1t's models may name one instead of leaving the choice to Auto.
176pub const MODEL_TIERS: [&str; 3] = ["small", "large", "frontier"];
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum ProviderKind {
181 /// Where agents' model requests go. A workspace can have several and
182 /// routes each kind of work to one.
183 Models,
184 /// Problems that become issues.
185 Alerts,
186 /// Tickets that agents read and people import.
187 Tracker,
188}
189
190/// A connection's settings: everything about it except its secrets. Each
191/// provider uses the fields that apply to it.
192#[derive(Clone, Debug, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct ConnectionConfig {
195 /// For alerts: the repository issues are opened in, `owner/name`. For a
196 /// tracker: where an imported ticket goes when no repository is named.
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub repo: Option<String>,
199 /// For alerts: put a g1t agent on each issue opened.
200 #[serde(default)]
201 pub assign: bool,
202 /// For alerts: the label put on each issue opened. `bug` when unset.
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub label: Option<String>,
205 /// Tell the outside system when the work lands: resolve the Sentry
206 /// issue, comment on the ticket.
207 #[serde(default = "yes")]
208 pub write_back: bool,
209 /// Sentry: the organization's slug.
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub organization: Option<String>,
212 /// The system's address, for Jira (`https://acme.atlassian.net`) or a
213 /// Sentry that is not sentry.io.
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub site: Option<String>,
216 /// Jira: the account the API token belongs to.
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub email: Option<String>,
219 /// Jira project keys or Linear team keys this connection answers for,
220 /// such as `TECH`. Empty answers for every key.
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub keys: Vec<String>,
223 /// Your own endpoint: its base URL, without `/v1`.
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub base_url: Option<String>,
226 /// Your own endpoint: send the key as `x-api-key` (the default) or as
227 /// `authorization: Bearer`.
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub auth_header: Option<String>,
230 /// Models: the model used when a route to this connection names none.
231 /// Required for providers that speak OpenAI's API; for Anthropic, g1t's
232 /// choice for the kind of work when unset.
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub model: Option<String>,
235 /// Models: which AI Gateway requests go to this connection, by the
236 /// model they name. Each is a model id (`gpt-5.5`), or a prefix ending
237 /// in `*` (`gpt-*`, `ollama/*`, or `*` for every model). A prefix that
238 /// ends in `/*` is taken off before the request is sent, so
239 /// `ollama/llama3.3` reaches the endpoint as `llama3.3`. Absent: Claude
240 /// models (`claude-*`) on an Anthropic key or Anthropic-compatible
241 /// endpoint, and nothing on the others. Empty: nothing.
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub gateway_models: Option<Vec<String>>,
244}
245
246impl ConnectionConfig {
247 /// The AI Gateway models this connection takes: its own list, or its
248 /// provider's default.
249 pub fn gateway_patterns(&self, provider: Provider) -> Vec<String> {
250 match &self.gateway_models {
251 Some(models) => models.clone(),
252 None if provider.kind() == ProviderKind::Models && provider.api() == "anthropic" => vec!["claude-*".to_owned()],
253 None => Vec::new(),
254 }
255 }
256}
257
258/// The most models a connection can list for the AI Gateway, and how long
259/// each may be.
260pub const GATEWAY_MODELS_MAX: usize = 100;
261pub const GATEWAY_MODEL_LEN: usize = 200;
262
263/// Tidies a connection's AI Gateway models, or says what is wrong.
264pub fn tidy_gateway_models(models: &[String]) -> std::result::Result<Vec<String>, String> {
265 let mut tidy: Vec<String> = Vec::new();
266 for model in models {
267 let model = model.trim();
268 if model.is_empty() {
269 continue;
270 }
271 if model.len() > GATEWAY_MODEL_LEN || model.chars().any(|c| c.is_whitespace() || c.is_control()) {
272 return Err(format!("{model} is not a model id: one word of at most {GATEWAY_MODEL_LEN} characters, such as gpt-5.5 or gpt-*."));
273 }
274 if model.contains('*') && !model.ends_with('*') || model.matches('*').count() > 1 {
275 return Err(format!("{model}: a * can only end a model id, as in gpt-*."));
276 }
277 if !tidy.iter().any(|seen| seen == model) {
278 tidy.push(model.to_owned());
279 }
280 }
281 if tidy.len() > GATEWAY_MODELS_MAX {
282 return Err(format!("A connection can take at most {GATEWAY_MODELS_MAX} AI Gateway models."));
283 }
284 Ok(tidy)
285}
286
287fn yes() -> bool {
288 true
289}
290
291impl Default for ConnectionConfig {
292 fn default() -> Self {
293 ConnectionConfig {
294 repo: None,
295 assign: false,
296 label: None,
297 write_back: true,
298 organization: None,
299 site: None,
300 email: None,
301 keys: Vec::new(),
302 base_url: None,
303 auth_header: None,
304 model: None,
305 gateway_models: None,
306 }
307 }
308}
309
310/// A connection, as anyone in the workspace sees it. Secrets are never
311/// shown after they are saved; `secretHint` is enough to tell keys apart.
312#[derive(Clone, Debug, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct Connection {
315 pub id: String,
316 pub workspace: String,
317 pub provider: Provider,
318 pub kind: ProviderKind,
319 pub name: String,
320 pub config: ConnectionConfig,
321 /// The last four characters of the saved key, such as `…3f9a`.
322 pub secret_hint: Option<String>,
323 /// For a provider that sends g1t requests: where it sends them.
324 pub webhook_url: Option<String>,
325 pub created_by: String,
326 /// RFC 3339.
327 pub created_at: String,
328 pub last_used_at: Option<String>,
329 /// The last thing that went wrong talking to it, until it next works.
330 pub last_error: Option<String>,
331 /// For a model provider: the models it offered when last checked.
332 #[serde(default)]
333 pub models: Vec<String>,
334}
335
336/// Where one kind of work's model requests go in a workspace.
337#[derive(Clone, Debug, Serialize, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub struct ModelRoute {
340 /// One of [`MODEL_TASKS`].
341 pub task: String,
342 /// The workspace's own model connection, or `None` for g1t's hosted
343 /// models.
344 pub connection_id: Option<String>,
345 /// The model at that connection; its default model when `None`. On
346 /// g1t's hosted models, one of [`MODEL_TIERS`], or `None` for Auto.
347 pub model: Option<String>,
348}
349
350/// One request an outside system sent, and what g1t did with it.
351#[derive(Clone, Debug, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct Delivery {
354 pub id: String,
355 /// RFC 3339.
356 pub received_at: String,
357 /// What it was about, in the sender's terms: `issue.created`.
358 pub event: String,
359 /// `opened`, `updated`, `reopened`, `ignored` or `refused`.
360 pub outcome: String,
361 pub detail: String,
362 /// The issue it opened or updated, `owner/name#number`.
363 pub issue: Option<String>,
364}
365
366/// Something outside g1t, fetched as it is now. Its text was written outside
367/// g1t, so it is reference material and never instructions.
368#[derive(Clone, Debug, Serialize, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ContextItem {
371 pub provider: Provider,
372 /// `TECH-1234`, or the Sentry issue's short id.
373 pub key: String,
374 pub title: String,
375 pub url: String,
376 /// Its status in that system: `In Progress`, `unresolved`.
377 pub status: Option<String>,
378 /// Its description, as plain text, shortened if long.
379 pub body: String,
380 /// RFC 3339: when g1t fetched it.
381 pub fetched_at: String,
382}
383
384/// An issue's tie to something outside g1t.
385#[derive(Clone, Debug, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct Link {
388 pub provider: Provider,
389 pub connection_id: String,
390 pub key: String,
391 pub title: String,
392 pub url: String,
393 /// How many times an alert has fired for it.
394 pub count: u32,
395 /// RFC 3339.
396 pub first_seen: String,
397 pub last_seen: String,
398}
399
400/// Where a workspace's agents' model requests go.
401#[derive(Clone, Debug, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct ModelSession {
404 /// What the sandbox sends instead of a key. Lives as long as one run.
405 pub token: String,
406 /// `g1t` when g1t pays the provider and charges the workspace,
407 /// `workspace` when the workspace's own account does.
408 pub billed_to: String,
409 /// The connection's name, when it is the workspace's own.
410 pub provider_name: Option<String>,
411 /// The model to use instead of g1t's choice, if the connection names one.
412 pub model: Option<String>,
413 /// Names the run in AI Gateway's logs (`metadata.session`), so billing
414 /// can charge each run what the gateway priced its requests at, and its
415 /// tokens in billing's count (the agent rate). Not a secret: it cannot
416 /// be turned back into the token.
417 #[serde(default)]
418 pub id: String,
419 /// The tier the workspace chose for this work on g1t's models instead
420 /// of Auto: the run goes there. `None` for Auto and on its own
421 /// providers.
422 #[serde(default)]
423 pub tier_choice: Option<String>,
424}
425
426/// What the model proxy needs to forward one run's requests.
427#[derive(Clone, Debug, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct ModelUpstream {
430 /// `g1t`, `anthropic` or `endpoint`.
431 pub route: String,
432 /// The API the provider speaks: `anthropic` or `openai`, which the proxy
433 /// translates to.
434 pub api: String,
435 /// The model every request of the run is sent to, when the route names
436 /// one.
437 pub model: Option<String>,
438 /// For `openai`: OpenAI's own API, which shapes requests its own way.
439 pub official: bool,
440 /// Which provider it is, by name, so the proxy can meet its quirks.
441 #[serde(default)]
442 pub provider: String,
443 pub workspace: String,
444 pub repo: String,
445 pub number: u32,
446 pub task: String,
447 /// The session's id; see `ModelSession::id`.
448 #[serde(default)]
449 pub session: String,
450 /// For `g1t`: the tier the run was routed to, `small` or `large`.
451 #[serde(default)]
452 pub tier: Option<String>,
453 /// The person the run is for, by username: who asked g1t for the work.
454 /// Null when nobody did. Never `g1t`, the agent itself.
455 #[serde(default)]
456 pub requested_by: Option<String>,
457 /// For `endpoint`: where to send requests.
458 pub base_url: Option<String>,
459 /// For `anthropic` and `endpoint`: the workspace's key.
460 pub api_key: Option<String>,
461 /// `x-api-key` or `authorization`.
462 pub auth_header: Option<String>,
463 /// For an endpoint behind an authenticated Cloudflare AI Gateway: the
464 /// gateway's own token, sent as `cf-aig-authorization`.
465 #[serde(default)]
466 pub gateway_token: Option<String>,
467}
468
469// --- Methods -----------------------------------------------------------------
470
471/// `list`. Returns `Outcome<Vec<Connection>>`. Members only.
472#[derive(Debug, Serialize, Deserialize)]
473pub struct ListArgs {
474 pub workspace: String,
475 pub viewer: Viewer,
476}
477
478/// `connect`. Returns `Outcome<Connected>`. Owners only.
479#[derive(Debug, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct ConnectArgs {
482 pub actor: User,
483 pub workspace: String,
484 pub provider: Provider,
485 #[serde(default)]
486 pub name: Option<String>,
487 #[serde(default)]
488 pub config: ConnectionConfig,
489 /// The API key or token g1t uses to call it.
490 #[serde(default)]
491 pub secret: Option<String>,
492 /// What it signs its requests to g1t with: Sentry's client secret.
493 /// Made by g1t for Datadog and webhooks, and shown once. For a model
494 /// endpoint behind an authenticated Cloudflare AI Gateway, the gateway's
495 /// token.
496 #[serde(default)]
497 pub signing_secret: Option<String>,
498}
499
500#[derive(Clone, Debug, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct Connected {
503 pub connection: Connection,
504 /// A signing secret g1t made, shown this once.
505 pub signing_secret: Option<String>,
506}
507
508/// `update`: only the fields given change. Returns `Outcome<Connection>`.
509/// Owners only.
510#[derive(Debug, Serialize, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct UpdateArgs {
513 pub actor: User,
514 pub workspace: String,
515 pub id: String,
516 #[serde(default)]
517 pub name: Option<String>,
518 #[serde(default)]
519 pub config: Option<ConnectionConfig>,
520 #[serde(default)]
521 pub secret: Option<String>,
522 #[serde(default)]
523 pub signing_secret: Option<String>,
524}
525
526/// `disconnect` and `test`. `disconnect` returns `Outcome<bool>`; `test`
527/// returns `Outcome<Tested>`. Owners only.
528#[derive(Debug, Serialize, Deserialize)]
529pub struct ConnectionArgs {
530 pub actor: User,
531 pub workspace: String,
532 pub id: String,
533}
534
535#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct Tested {
537 pub ok: bool,
538 pub message: String,
539}
540
541/// `deliveries`: the latest requests a connection received, newest first.
542/// Returns `Outcome<Vec<Delivery>>`. Members only.
543#[derive(Debug, Serialize, Deserialize)]
544pub struct DeliveriesArgs {
545 pub workspace: String,
546 pub viewer: Viewer,
547 pub id: String,
548}
549
550/// `receive`: a request an outside system sent to a connection's address.
551/// Returns `Received`. Anyone can send one; only a signed one is acted on.
552#[derive(Debug, Serialize, Deserialize)]
553pub struct ReceiveArgs {
554 pub id: String,
555 /// Header names in lowercase.
556 pub headers: std::collections::HashMap<String, String>,
557 pub body: String,
558}
559
560#[derive(Clone, Debug, Serialize, Deserialize)]
561pub struct Received {
562 /// The HTTP status to answer with.
563 pub status: u16,
564 pub message: String,
565}
566
567/// `resolve`: fetches one outside reference. Returns `Outcome<ContextItem>`.
568/// Members of the workspace only.
569#[derive(Debug, Serialize, Deserialize)]
570pub struct ResolveArgs {
571 pub workspace: String,
572 pub viewer: Viewer,
573 /// `TECH-1234`, or a Jira, Linear or Sentry address.
574 pub reference: String,
575}
576
577/// `references`: every outside reference in `text` that one of the
578/// workspace's connections answers for, fetched. Returns `Vec<ContextItem>`.
579/// For g1t's own agents, about work in that workspace.
580#[derive(Debug, Serialize, Deserialize)]
581pub struct ReferencesArgs {
582 pub workspace: String,
583 pub text: String,
584 #[serde(default)]
585 pub limit: Option<u32>,
586}
587
588/// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`.
589#[derive(Debug, Serialize, Deserialize)]
590pub struct ImportArgs {
591 pub actor: User,
592 pub repo: RepoPath,
593 pub reference: String,
594 /// Put a g1t agent on it.
595 #[serde(default)]
596 pub assign: bool,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
600pub struct Imported {
601 pub number: u32,
602 pub item: ContextItem,
603 /// False when the ticket had been imported already, and `number` is
604 /// that issue.
605 pub created: bool,
606}
607
608/// `links`: what an issue is tied to outside g1t. Returns `Vec<Link>`.
609/// Callers must have checked the viewer may see the issue.
610#[derive(Debug, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612pub struct LinksArgs {
613 pub repo: RepoPath,
614 pub number: u32,
615}
616
617/// `open_model_session`: where one run's model requests go, by the
618/// workspace's routes. Returns `Outcome<ModelSession>`: a failure, with the
619/// reason to show, when the route goes nowhere it can use.
620#[derive(Debug, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct OpenModelSessionArgs {
623 pub workspace: String,
624 pub repo: RepoPath,
625 pub number: u32,
626 pub task: String,
627 /// Whether g1t's hosted models are open to the workspace. The runner
628 /// decides that; this service only follows the routes.
629 #[serde(default = "yes")]
630 pub hosted_open: bool,
631 /// `small`, `large` or `frontier`: the tier the runner routed the run
632 /// to on g1t's hosted models, tagged on its requests at the gateway.
633 /// Kept only when the run goes to g1t's models, where a tier the
634 /// workspace chose for the work takes its place.
635 #[serde(default)]
636 pub tier: Option<String>,
637 /// The person the run is for, by username, so usage can be shown per
638 /// person. Null when nobody asked; `g1t`, the agent, is kept as null.
639 #[serde(default)]
640 pub requested_by: Option<String>,
641}
642
643/// `routes`: a workspace's model routes, one per kind of work that has its
644/// own. Returns `Outcome<Vec<ModelRoute>>`. Members only.
645#[derive(Debug, Serialize, Deserialize)]
646pub struct RoutesArgs {
647 pub workspace: String,
648 pub viewer: Viewer,
649}
650
651/// `set_routes`: replaces a workspace's model routes. A kind of work left
652/// out follows `default`; with no `default`, g1t's hosted models where they
653/// are open. Returns `Outcome<Vec<ModelRoute>>`. Owners only.
654#[derive(Debug, Serialize, Deserialize)]
655pub struct SetRoutesArgs {
656 pub actor: User,
657 pub workspace: String,
658 pub routes: Vec<ModelRoute>,
659}
660
661/// `model_upstream`: what a model session's token stands for, or null when
662/// it is unknown or expired. Returns `Option<ModelUpstream>`.
663#[derive(Debug, Serialize, Deserialize)]
664pub struct ModelUpstreamArgs {
665 pub token: String,
666}
667
668/// `gateway_upstream`: where a workspace's AI Gateway requests go when it
669/// has its own Anthropic key: its first Anthropic or Anthropic-compatible
670/// model provider, with the credentials to forward them. Null sends them to
671/// g1t's models. Returns `Option<ModelUpstream>`, with `task` `gateway`.
672#[derive(Debug, Serialize, Deserialize)]
673pub struct GatewayUpstreamArgs {
674 pub workspace: String,
675}
676
677/// `gateway_providers`: the workspace's own model providers, in the order
678/// they were connected, with what the AI Gateway needs to send requests to
679/// each and which models it takes. Returns `Vec<GatewayProvider>`. For the
680/// model proxy only: it carries keys.
681#[derive(Debug, Serialize, Deserialize)]
682pub struct GatewayProvidersArgs {
683 pub workspace: String,
684}
685
686/// One of a workspace's own model providers, for the AI Gateway.
687#[derive(Clone, Debug, Serialize, Deserialize)]
688#[serde(rename_all = "camelCase")]
689pub struct GatewayProvider {
690 /// The connection's id and name.
691 pub id: String,
692 pub name: String,
693 /// The provider, by name: `anthropic`, `openai`, `openai_endpoint`…
694 pub provider: String,
695 /// The API it speaks: `anthropic` or `openai`.
696 pub api: String,
697 /// OpenAI's own API (or Azure's), which shapes requests its own way.
698 pub official: bool,
699 /// Where requests go: without `/v1` for Anthropic's API, with it for OpenAI's.
700 pub base_url: String,
701 pub api_key: Option<String>,
702 /// `x-api-key`, `authorization` (as `Bearer`) or `api-key`.
703 pub auth_header: String,
704 /// For an endpoint behind an authenticated Cloudflare AI Gateway.
705 #[serde(default)]
706 pub gateway_token: Option<String>,
707 /// Which models it takes: ids and `*` prefixes (see
708 /// `ConnectionConfig::gateway_models`).
709 pub patterns: Vec<String>,
710 /// The models the provider listed when last checked.
711 #[serde(default)]
712 pub models: Vec<String>,
713}
714
715/// `close_model_sessions`: ends the model sessions whose tokens hash to
716/// these (SHA-256, lowercase hex), so a run's model token stops working
717/// when its run does rather than when it would lapse. Returns how many
718/// were open.
719#[derive(Debug, Serialize, Deserialize)]
720pub struct CloseModelSessionsArgs {
721 #[serde(alias = "tokenHashes")]
722 pub token_hashes: Vec<String>,
723}
724
725/// `model_provider`: the workspace's own model connection, if it has one.
726/// Returns `Option<Connection>`.
727#[derive(Debug, Serialize, Deserialize)]
728pub struct ModelProviderArgs {
729 pub workspace: String,
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 #[test]
737 fn every_provider_is_named_once_and_found_again() {
738 let mut names: Vec<&str> = PROVIDERS.iter().map(|spec| spec.name).collect();
739 for provider in Provider::all() {
740 assert_eq!(Provider::parse(provider.name()), Some(provider));
741 }
742 names.sort();
743 names.dedup();
744 assert_eq!(names.len(), PROVIDERS.len());
745 }
746
747 #[test]
748 fn gateway_models_default_to_claude_on_anthropic_and_nothing_elsewhere() {
749 let config = ConnectionConfig::default();
750 assert_eq!(config.gateway_patterns(Provider::Anthropic), ["claude-*"]);
751 assert_eq!(config.gateway_patterns(Provider::AnthropicEndpoint), ["claude-*"]);
752 assert!(config.gateway_patterns(Provider::Openai).is_empty());
753 assert!(config.gateway_patterns(Provider::OpenaiEndpoint).is_empty());
754 let chosen = ConnectionConfig { gateway_models: Some(vec!["gpt-*".into()]), ..ConnectionConfig::default() };
755 assert_eq!(chosen.gateway_patterns(Provider::Openai), ["gpt-*"]);
756 let none = ConnectionConfig { gateway_models: Some(Vec::new()), ..ConnectionConfig::default() };
757 assert!(none.gateway_patterns(Provider::Anthropic).is_empty());
758 }
759
760 #[test]
761 fn gateway_models_are_ids_or_prefixes() {
762 let tidy = tidy_gateway_models(&[" gpt-5.5 ".into(), "".into(), "ollama/*".into(), "gpt-5.5".into(), "*".into()]).unwrap();
763 assert_eq!(tidy, ["gpt-5.5", "ollama/*", "*"]);
764 assert!(tidy_gateway_models(&["gpt *".into()]).is_err());
765 assert!(tidy_gateway_models(&["g*t".into()]).is_err());
766 assert!(tidy_gateway_models(&["**".into()]).is_err());
767 assert!(tidy_gateway_models(&["x".repeat(201)]).is_err());
768 let many: Vec<String> = (0..101).map(|n| format!("m{n}")).collect();
769 assert!(tidy_gateway_models(&many).is_err());
770 }
771
772 #[test]
773 fn model_providers_say_how_to_reach_them() {
774 for spec in PROVIDERS.iter().filter(|spec| spec.kind == ProviderKind::Models) {
775 assert!(spec.api == "anthropic" || spec.api == "openai", "{}", spec.name);
776 assert!(!spec.auth_header.is_empty(), "{}", spec.name);
777 assert!(spec.base_url.is_empty() || spec.base_url.starts_with("https://"), "{}", spec.name);
778 }
779 }
780}