Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Usage while free is shown at cost; agents get rustfmt and clippy | 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 | ||
| 19 | use serde::{Deserialize, Serialize}; | |
| 20 | ||
| 21 | use crate::repos::RepoPath; | |
| 22 | use 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")] | |
| 27 | pub 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. | |
| 63 | pub 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 | ||
| 78 | const 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 | ||
| 90 | const 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. | |
| 103 | pub 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 | ||
| 125 | impl 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. | |
| 171 | pub const MODEL_TASKS: [&str; 5] = ["default", "implement", "review", "plan", "update"]; | |
| 172 | ||
| Merge branch 'model-routing' | 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. | |
| 176 | pub const MODEL_TIERS: [&str; 3] = ["small", "large", "frontier"]; | |
| 177 | ||
| Usage while free is shown at cost; agents get rustfmt and clippy | 178 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 179 | #[serde(rename_all = "snake_case")] | |
| 180 | pub 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")] | |
| 194 | pub 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>, | |
| AI Gateway: OpenAI's format, open models, and your own providers | 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>>, | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 244 | } |
| 245 | ||
| AI Gateway: OpenAI's format, open models, and your own providers | 246 | impl 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. | |
| 260 | pub const GATEWAY_MODELS_MAX: usize = 100; | |
| 261 | pub const GATEWAY_MODEL_LEN: usize = 200; | |
| 262 | ||
| 263 | /// Tidies a connection's AI Gateway models, or says what is wrong. | |
| 264 | pub 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 | ||
| Usage while free is shown at cost; agents get rustfmt and clippy | 287 | fn yes() -> bool { |
| 288 | true | |
| 289 | } | |
| 290 | ||
| 291 | impl 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, | |
| AI Gateway: OpenAI's format, open models, and your own providers | 305 | gateway_models: None, |
| Usage while free is shown at cost; agents get rustfmt and clippy | 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")] | |
| 314 | pub 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")] | |
| 339 | pub 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>, | |
| Merge branch 'model-routing' | 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. | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 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")] | |
| 353 | pub 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")] | |
| 370 | pub 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")] | |
| 387 | pub 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")] | |
| 403 | pub 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>, | |
| Prices keep themselves current with what g1t pays | 413 | /// Names the run in AI Gateway's logs (`metadata.session`), so billing |
| Merge branch 'model-routing' | 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. | |
| Prices keep themselves current with what g1t pays | 417 | #[serde(default)] |
| 418 | pub id: String, | |
| Merge branch 'model-routing' | 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>, | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 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")] | |
| 429 | pub 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, | |
| Prices keep themselves current with what g1t pays | 447 | /// The session's id; see `ModelSession::id`. |
| 448 | #[serde(default)] | |
| 449 | pub session: String, | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 450 | /// For `g1t`: the tier the run was routed to, `small` or `large`. |
| 451 | #[serde(default)] | |
| 452 | pub tier: Option<String>, | |
| Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix | 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>, | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 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>, | |
| Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006) | 467 | /// The most the run may spend on models, in millionths of a dollar: the |
| 468 | /// lower of its project's cost cap and its plan's. The proxy refuses | |
| 469 | /// the run's requests once it has spent this. `None` until the sandbox | |
| 470 | /// sets it (`cap_model_sessions`), and for the AI Gateway. | |
| 471 | #[serde(default)] | |
| 472 | pub cap_micros: Option<i64>, | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 473 | } |
| 474 | ||
| 475 | // --- Methods ----------------------------------------------------------------- | |
| 476 | ||
| 477 | /// `list`. Returns `Outcome<Vec<Connection>>`. Members only. | |
| 478 | #[derive(Debug, Serialize, Deserialize)] | |
| 479 | pub struct ListArgs { | |
| 480 | pub workspace: String, | |
| 481 | pub viewer: Viewer, | |
| 482 | } | |
| 483 | ||
| 484 | /// `connect`. Returns `Outcome<Connected>`. Owners only. | |
| 485 | #[derive(Debug, Serialize, Deserialize)] | |
| 486 | #[serde(rename_all = "camelCase")] | |
| 487 | pub struct ConnectArgs { | |
| 488 | pub actor: User, | |
| 489 | pub workspace: String, | |
| 490 | pub provider: Provider, | |
| 491 | #[serde(default)] | |
| 492 | pub name: Option<String>, | |
| 493 | #[serde(default)] | |
| 494 | pub config: ConnectionConfig, | |
| 495 | /// The API key or token g1t uses to call it. | |
| 496 | #[serde(default)] | |
| 497 | pub secret: Option<String>, | |
| 498 | /// What it signs its requests to g1t with: Sentry's client secret. | |
| 499 | /// Made by g1t for Datadog and webhooks, and shown once. For a model | |
| 500 | /// endpoint behind an authenticated Cloudflare AI Gateway, the gateway's | |
| 501 | /// token. | |
| 502 | #[serde(default)] | |
| 503 | pub signing_secret: Option<String>, | |
| 504 | } | |
| 505 | ||
| 506 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 507 | #[serde(rename_all = "camelCase")] | |
| 508 | pub struct Connected { | |
| 509 | pub connection: Connection, | |
| 510 | /// A signing secret g1t made, shown this once. | |
| 511 | pub signing_secret: Option<String>, | |
| 512 | } | |
| 513 | ||
| 514 | /// `update`: only the fields given change. Returns `Outcome<Connection>`. | |
| 515 | /// Owners only. | |
| 516 | #[derive(Debug, Serialize, Deserialize)] | |
| 517 | #[serde(rename_all = "camelCase")] | |
| 518 | pub struct UpdateArgs { | |
| 519 | pub actor: User, | |
| 520 | pub workspace: String, | |
| 521 | pub id: String, | |
| 522 | #[serde(default)] | |
| 523 | pub name: Option<String>, | |
| 524 | #[serde(default)] | |
| 525 | pub config: Option<ConnectionConfig>, | |
| 526 | #[serde(default)] | |
| 527 | pub secret: Option<String>, | |
| 528 | #[serde(default)] | |
| 529 | pub signing_secret: Option<String>, | |
| 530 | } | |
| 531 | ||
| 532 | /// `disconnect` and `test`. `disconnect` returns `Outcome<bool>`; `test` | |
| 533 | /// returns `Outcome<Tested>`. Owners only. | |
| 534 | #[derive(Debug, Serialize, Deserialize)] | |
| 535 | pub struct ConnectionArgs { | |
| 536 | pub actor: User, | |
| 537 | pub workspace: String, | |
| 538 | pub id: String, | |
| 539 | } | |
| 540 | ||
| 541 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 542 | pub struct Tested { | |
| 543 | pub ok: bool, | |
| 544 | pub message: String, | |
| 545 | } | |
| 546 | ||
| 547 | /// `deliveries`: the latest requests a connection received, newest first. | |
| 548 | /// Returns `Outcome<Vec<Delivery>>`. Members only. | |
| 549 | #[derive(Debug, Serialize, Deserialize)] | |
| 550 | pub struct DeliveriesArgs { | |
| 551 | pub workspace: String, | |
| 552 | pub viewer: Viewer, | |
| 553 | pub id: String, | |
| 554 | } | |
| 555 | ||
| 556 | /// `receive`: a request an outside system sent to a connection's address. | |
| 557 | /// Returns `Received`. Anyone can send one; only a signed one is acted on. | |
| 558 | #[derive(Debug, Serialize, Deserialize)] | |
| 559 | pub struct ReceiveArgs { | |
| 560 | pub id: String, | |
| 561 | /// Header names in lowercase. | |
| 562 | pub headers: std::collections::HashMap<String, String>, | |
| 563 | pub body: String, | |
| 564 | } | |
| 565 | ||
| 566 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 567 | pub struct Received { | |
| 568 | /// The HTTP status to answer with. | |
| 569 | pub status: u16, | |
| 570 | pub message: String, | |
| 571 | } | |
| 572 | ||
| 573 | /// `resolve`: fetches one outside reference. Returns `Outcome<ContextItem>`. | |
| 574 | /// Members of the workspace only. | |
| 575 | #[derive(Debug, Serialize, Deserialize)] | |
| 576 | pub struct ResolveArgs { | |
| 577 | pub workspace: String, | |
| 578 | pub viewer: Viewer, | |
| 579 | /// `TECH-1234`, or a Jira, Linear or Sentry address. | |
| 580 | pub reference: String, | |
| 581 | } | |
| 582 | ||
| 583 | /// `references`: every outside reference in `text` that one of the | |
| 584 | /// workspace's connections answers for, fetched. Returns `Vec<ContextItem>`. | |
| 585 | /// For g1t's own agents, about work in that workspace. | |
| 586 | #[derive(Debug, Serialize, Deserialize)] | |
| 587 | pub struct ReferencesArgs { | |
| 588 | pub workspace: String, | |
| 589 | pub text: String, | |
| 590 | #[serde(default)] | |
| 591 | pub limit: Option<u32>, | |
| 592 | } | |
| 593 | ||
| Each agent has an Abilities tab: g1t's built-ins, always on within the asker's access; its computer, coming; each connected integration's actions one row each, read, import, comment or resolve, with a level for each, alone, alone when the person asked for it, ask first or never, and whose connection it runs on; and MCP servers an owner adds. Reading is alone, writing inside g1t follows today's choices, anything that leaves g1t asks first, and production deploys can't go above ask. The agents service enforces every level: ask first posts a card to allow or deny and parks a session until it's answered, a refusal names its rule in the transcript and the audit log, and a missing ability posts a request to owners. The agent abilities guide says how. | 594 | /// `comment`: for g1t's agents, a comment on the item `reference` names in |
| 595 | /// the system that knows it (Linear, Jira or Sentry), on the workspace's | |
| 596 | /// connection, with `link` (a g1t address) at the end. The actor is the | |
| 597 | /// person the agent acts for. Returns `Outcome<ContextItem>`. | |
| 598 | #[derive(Debug, Serialize, Deserialize)] | |
| 599 | pub struct CommentArgs { | |
| 600 | pub actor: User, | |
| 601 | pub workspace: String, | |
| 602 | pub reference: String, | |
| 603 | pub text: String, | |
| 604 | pub link: String, | |
| 605 | } | |
| 606 | ||
| 607 | /// `close`: for g1t's agents, marks a Sentry issue resolved with a note and | |
| 608 | /// `link`. Only Sentry items can be closed. Returns `Outcome<ContextItem>`. | |
| 609 | #[derive(Debug, Serialize, Deserialize)] | |
| 610 | pub struct CloseArgs { | |
| 611 | pub actor: User, | |
| 612 | pub workspace: String, | |
| 613 | pub reference: String, | |
| 614 | pub text: String, | |
| 615 | pub link: String, | |
| 616 | } | |
| 617 | ||
| Usage while free is shown at cost; agents get rustfmt and clippy | 618 | /// `import`: opens an issue from a ticket. Returns `Outcome<Imported>`. |
| 619 | #[derive(Debug, Serialize, Deserialize)] | |
| 620 | pub struct ImportArgs { | |
| 621 | pub actor: User, | |
| 622 | pub repo: RepoPath, | |
| 623 | pub reference: String, | |
| 624 | /// Put a g1t agent on it. | |
| 625 | #[serde(default)] | |
| 626 | pub assign: bool, | |
| 627 | } | |
| 628 | ||
| 629 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 630 | pub struct Imported { | |
| 631 | pub number: u32, | |
| 632 | pub item: ContextItem, | |
| 633 | /// False when the ticket had been imported already, and `number` is | |
| 634 | /// that issue. | |
| 635 | pub created: bool, | |
| 636 | } | |
| 637 | ||
| 638 | /// `links`: what an issue is tied to outside g1t. Returns `Vec<Link>`. | |
| 639 | /// Callers must have checked the viewer may see the issue. | |
| 640 | #[derive(Debug, Serialize, Deserialize)] | |
| 641 | #[serde(rename_all = "camelCase")] | |
| 642 | pub struct LinksArgs { | |
| 643 | pub repo: RepoPath, | |
| 644 | pub number: u32, | |
| 645 | } | |
| 646 | ||
| 647 | /// `open_model_session`: where one run's model requests go, by the | |
| 648 | /// workspace's routes. Returns `Outcome<ModelSession>`: a failure, with the | |
| 649 | /// reason to show, when the route goes nowhere it can use. | |
| 650 | #[derive(Debug, Serialize, Deserialize)] | |
| 651 | #[serde(rename_all = "camelCase")] | |
| 652 | pub struct OpenModelSessionArgs { | |
| 653 | pub workspace: String, | |
| 654 | pub repo: RepoPath, | |
| 655 | pub number: u32, | |
| 656 | pub task: String, | |
| 657 | /// Whether g1t's hosted models are open to the workspace. The runner | |
| 658 | /// decides that; this service only follows the routes. | |
| 659 | #[serde(default = "yes")] | |
| 660 | pub hosted_open: bool, | |
| Merge branch 'model-routing' | 661 | /// `small`, `large` or `frontier`: the tier the runner routed the run |
| 662 | /// to on g1t's hosted models, tagged on its requests at the gateway. | |
| 663 | /// Kept only when the run goes to g1t's models, where a tier the | |
| 664 | /// workspace chose for the work takes its place. | |
| Auto model routing: the cheapest tier that can do each piece of work, a retry goes up a tier, and each run records its tier | 665 | #[serde(default)] |
| 666 | pub tier: Option<String>, | |
| Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix | 667 | /// The person the run is for, by username, so usage can be shown per |
| 668 | /// person. Null when nobody asked; `g1t`, the agent, is kept as null. | |
| 669 | #[serde(default)] | |
| 670 | pub requested_by: Option<String>, | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 671 | } |
| 672 | ||
| 673 | /// `routes`: a workspace's model routes, one per kind of work that has its | |
| 674 | /// own. Returns `Outcome<Vec<ModelRoute>>`. Members only. | |
| 675 | #[derive(Debug, Serialize, Deserialize)] | |
| 676 | pub struct RoutesArgs { | |
| 677 | pub workspace: String, | |
| 678 | pub viewer: Viewer, | |
| 679 | } | |
| 680 | ||
| 681 | /// `set_routes`: replaces a workspace's model routes. A kind of work left | |
| 682 | /// out follows `default`; with no `default`, g1t's hosted models where they | |
| 683 | /// are open. Returns `Outcome<Vec<ModelRoute>>`. Owners only. | |
| 684 | #[derive(Debug, Serialize, Deserialize)] | |
| 685 | pub struct SetRoutesArgs { | |
| 686 | pub actor: User, | |
| 687 | pub workspace: String, | |
| 688 | pub routes: Vec<ModelRoute>, | |
| 689 | } | |
| 690 | ||
| 691 | /// `model_upstream`: what a model session's token stands for, or null when | |
| 692 | /// it is unknown or expired. Returns `Option<ModelUpstream>`. | |
| 693 | #[derive(Debug, Serialize, Deserialize)] | |
| 694 | pub struct ModelUpstreamArgs { | |
| 695 | pub token: String, | |
| 696 | } | |
| 697 | ||
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 698 | /// `gateway_upstream`: where a workspace's AI Gateway requests go when it |
| 699 | /// has its own Anthropic key: its first Anthropic or Anthropic-compatible | |
| 700 | /// model provider, with the credentials to forward them. Null sends them to | |
| 701 | /// g1t's models. Returns `Option<ModelUpstream>`, with `task` `gateway`. | |
| 702 | #[derive(Debug, Serialize, Deserialize)] | |
| 703 | pub struct GatewayUpstreamArgs { | |
| 704 | pub workspace: String, | |
| 705 | } | |
| 706 | ||
| AI Gateway: OpenAI's format, open models, and your own providers | 707 | /// `gateway_providers`: the workspace's own model providers, in the order |
| 708 | /// they were connected, with what the AI Gateway needs to send requests to | |
| 709 | /// each and which models it takes. Returns `Vec<GatewayProvider>`. For the | |
| 710 | /// model proxy only: it carries keys. | |
| 711 | #[derive(Debug, Serialize, Deserialize)] | |
| 712 | pub struct GatewayProvidersArgs { | |
| 713 | pub workspace: String, | |
| 714 | } | |
| 715 | ||
| 716 | /// One of a workspace's own model providers, for the AI Gateway. | |
| 717 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 718 | #[serde(rename_all = "camelCase")] | |
| 719 | pub struct GatewayProvider { | |
| 720 | /// The connection's id and name. | |
| 721 | pub id: String, | |
| 722 | pub name: String, | |
| 723 | /// The provider, by name: `anthropic`, `openai`, `openai_endpoint`… | |
| 724 | pub provider: String, | |
| 725 | /// The API it speaks: `anthropic` or `openai`. | |
| 726 | pub api: String, | |
| 727 | /// OpenAI's own API (or Azure's), which shapes requests its own way. | |
| 728 | pub official: bool, | |
| 729 | /// Where requests go: without `/v1` for Anthropic's API, with it for OpenAI's. | |
| 730 | pub base_url: String, | |
| 731 | pub api_key: Option<String>, | |
| 732 | /// `x-api-key`, `authorization` (as `Bearer`) or `api-key`. | |
| 733 | pub auth_header: String, | |
| 734 | /// For an endpoint behind an authenticated Cloudflare AI Gateway. | |
| 735 | #[serde(default)] | |
| 736 | pub gateway_token: Option<String>, | |
| 737 | /// Which models it takes: ids and `*` prefixes (see | |
| 738 | /// `ConnectionConfig::gateway_models`). | |
| 739 | pub patterns: Vec<String>, | |
| 740 | /// The models the provider listed when last checked. | |
| 741 | #[serde(default)] | |
| 742 | pub models: Vec<String>, | |
| 743 | } | |
| 744 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 745 | /// `close_model_sessions`: ends the model sessions whose tokens hash to |
| 746 | /// these (SHA-256, lowercase hex), so a run's model token stops working | |
| 747 | /// when its run does rather than when it would lapse. Returns how many | |
| 748 | /// were open. | |
| 749 | #[derive(Debug, Serialize, Deserialize)] | |
| 750 | pub struct CloseModelSessionsArgs { | |
| 751 | #[serde(alias = "tokenHashes")] | |
| 752 | pub token_hashes: Vec<String>, | |
| 753 | } | |
| 754 | ||
| Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006) | 755 | /// `cap_model_sessions`: sets the most the runs whose model tokens hash to |
| 756 | /// these (SHA-256, lowercase hex) may spend on models, in millionths of a | |
| 757 | /// dollar, which the model proxy holds them to. The sandbox sets it once | |
| 758 | /// it knows the run's guardrails and plan; zero or less clears it. Returns | |
| 759 | /// how many open sessions it set. | |
| 760 | #[derive(Debug, Serialize, Deserialize)] | |
| 761 | pub struct CapModelSessionsArgs { | |
| 762 | #[serde(alias = "tokenHashes")] | |
| 763 | pub token_hashes: Vec<String>, | |
| 764 | #[serde(alias = "capMicros")] | |
| 765 | pub cap_micros: i64, | |
| 766 | } | |
| 767 | ||
| Usage while free is shown at cost; agents get rustfmt and clippy | 768 | /// `model_provider`: the workspace's own model connection, if it has one. |
| 769 | /// Returns `Option<Connection>`. | |
| 770 | #[derive(Debug, Serialize, Deserialize)] | |
| 771 | pub struct ModelProviderArgs { | |
| 772 | pub workspace: String, | |
| 773 | } | |
| 774 | ||
| 775 | #[cfg(test)] | |
| 776 | mod tests { | |
| 777 | use super::*; | |
| 778 | ||
| 779 | #[test] | |
| 780 | fn every_provider_is_named_once_and_found_again() { | |
| 781 | let mut names: Vec<&str> = PROVIDERS.iter().map(|spec| spec.name).collect(); | |
| 782 | for provider in Provider::all() { | |
| 783 | assert_eq!(Provider::parse(provider.name()), Some(provider)); | |
| 784 | } | |
| 785 | names.sort(); | |
| 786 | names.dedup(); | |
| 787 | assert_eq!(names.len(), PROVIDERS.len()); | |
| 788 | } | |
| 789 | ||
| 790 | #[test] | |
| AI Gateway: OpenAI's format, open models, and your own providers | 791 | fn gateway_models_default_to_claude_on_anthropic_and_nothing_elsewhere() { |
| 792 | let config = ConnectionConfig::default(); | |
| 793 | assert_eq!(config.gateway_patterns(Provider::Anthropic), ["claude-*"]); | |
| 794 | assert_eq!(config.gateway_patterns(Provider::AnthropicEndpoint), ["claude-*"]); | |
| 795 | assert!(config.gateway_patterns(Provider::Openai).is_empty()); | |
| 796 | assert!(config.gateway_patterns(Provider::OpenaiEndpoint).is_empty()); | |
| 797 | let chosen = ConnectionConfig { gateway_models: Some(vec!["gpt-*".into()]), ..ConnectionConfig::default() }; | |
| 798 | assert_eq!(chosen.gateway_patterns(Provider::Openai), ["gpt-*"]); | |
| 799 | let none = ConnectionConfig { gateway_models: Some(Vec::new()), ..ConnectionConfig::default() }; | |
| 800 | assert!(none.gateway_patterns(Provider::Anthropic).is_empty()); | |
| 801 | } | |
| 802 | ||
| 803 | #[test] | |
| 804 | fn gateway_models_are_ids_or_prefixes() { | |
| 805 | let tidy = tidy_gateway_models(&[" gpt-5.5 ".into(), "".into(), "ollama/*".into(), "gpt-5.5".into(), "*".into()]).unwrap(); | |
| 806 | assert_eq!(tidy, ["gpt-5.5", "ollama/*", "*"]); | |
| 807 | assert!(tidy_gateway_models(&["gpt *".into()]).is_err()); | |
| 808 | assert!(tidy_gateway_models(&["g*t".into()]).is_err()); | |
| 809 | assert!(tidy_gateway_models(&["**".into()]).is_err()); | |
| 810 | assert!(tidy_gateway_models(&["x".repeat(201)]).is_err()); | |
| 811 | let many: Vec<String> = (0..101).map(|n| format!("m{n}")).collect(); | |
| 812 | assert!(tidy_gateway_models(&many).is_err()); | |
| 813 | } | |
| 814 | ||
| 815 | #[test] | |
| Usage while free is shown at cost; agents get rustfmt and clippy | 816 | fn model_providers_say_how_to_reach_them() { |
| 817 | for spec in PROVIDERS.iter().filter(|spec| spec.kind == ProviderKind::Models) { | |
| 818 | assert!(spec.api == "anthropic" || spec.api == "openai", "{}", spec.name); | |
| 819 | assert!(!spec.auth_header.is_empty(), "{}", spec.name); | |
| 820 | assert!(spec.base_url.is_empty() || spec.base_url.starts_with("https://"), "{}", spec.name); | |
| 821 | } | |
| 822 | } | |
| 823 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.