Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
129 linesCodeBlameRaw

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.

The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.1//! A workspace's AI providers: where each one's API is, how it takes its
2//! key, and checking that the key works by asking the provider for its
3//! models.
4
5use g1t_contracts::integrations::{ConnectionConfig, Provider};
6use worker::{Method, Result};
7
8use crate::http;
9
10/// Where requests go: without `/v1` for Anthropic's API, with the version
11/// for OpenAI's (`…/v1`, or Gemini's `…/v1beta/openai`).
12pub fn base_url(provider: Provider, config: &ConnectionConfig) -> String {
13 let given = config.base_url.as_deref().unwrap_or_default().trim_end_matches('/');
14 match provider {
15 Provider::AnthropicEndpoint => given.trim_end_matches("/v1").to_owned(),
16 Provider::OpenaiEndpoint => given.to_owned(),
17 // Azure's v1 API, on the workspace's own resource.
18 Provider::AzureOpenai => format!("{}/openai/v1", given.trim_end_matches("/openai/v1").trim_end_matches("/openai")),
19 _ => provider.spec().base_url.to_owned(),
20 }
21}
22
23/// The header the key goes in; `authorization` means `Bearer <key>`.
24pub fn auth_header(provider: Provider, config: &ConnectionConfig) -> String {
25 match provider {
26 Provider::AnthropicEndpoint | Provider::OpenaiEndpoint => config
27 .auth_header
28 .clone()
29 .unwrap_or_else(|| provider.spec().auth_header.to_owned()),
30 _ => provider.spec().auth_header.to_owned(),
31 }
32}
33
34/// Whether a model id is one for chat: not embeddings, images,
35/// speech or moderation.
36fn for_chat(id: &str) -> bool {
37 let id = id.to_ascii_lowercase();
38 !["embed", "tts", "whisper", "dall-e", "image", "moderation", "audio", "transcribe", "realtime", "search", "aqa", "imagen", "veo"]
39 .iter()
40 .any(|word| id.contains(word))
41}
42
43/// Asks the provider for its models with the key. `Ok` with what to say and
44/// the models it offers; `Err` with what went wrong.
45pub async fn test(
46 provider: Provider,
47 config: &ConnectionConfig,
48 key: Option<&str>,
49) -> Result<std::result::Result<(String, Vec<String>), String>> {
50 let base = base_url(provider, config);
51 let url = match provider.api() {
52 "anthropic" => format!("{base}/v1/models?limit=100"),
53 _ => format!("{base}/models"),
54 };
55 let header = auth_header(provider, config);
56 let bearer = key.map(|key| format!("Bearer {key}"));
57 let mut headers = vec![("anthropic-version", "2023-06-01")];
58 if let Some(key) = key {
59 if header == "authorization" {
60 headers.push(("authorization", bearer.as_deref().unwrap_or_default()));
61 } else {
62 headers.push((header.as_str(), key));
63 }
64 }
65 let answer = http::send(Method::Get, &url, &headers, None).await?;
66 let system = provider.label();
67 if answer.ok() {
68 let mut models: Vec<String> = answer.json()["data"]
69 .as_array()
70 .map(|data| {
71 data.iter()
72 .filter_map(|model| model["id"].as_str())
73 // Gemini names models `models/gemini-…`.
74 .map(|id| id.trim_start_matches("models/").to_owned())
75 .filter(|id| for_chat(id))
76 .collect()
77 })
78 .unwrap_or_default();
79 models.sort();
80 models.truncate(1000);
81 let message = match models.len() {
82 0 => format!("{system} accepted the key."),
83 count => format!("{system} accepted the key and offers {count} models."),
84 };
85 return Ok(Ok((message, models)));
86 }
87 // A proxy may answer messages but not list models: it was reached, but
88 // the key could not be checked.
89 if answer.status == 404 && matches!(provider, Provider::AnthropicEndpoint | Provider::OpenaiEndpoint) {
90 return Ok(Ok((
91 "Reached the endpoint. It does not list models, so the key could not be checked.".to_owned(),
92 Vec::new(),
93 )));
94 }
95 Ok(Err(answer.problem(system)))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn each_provider_has_its_address_and_header() {
104 let config = ConnectionConfig {
105 base_url: Some("https://llm.acme.dev/v1/".to_owned()),
106 ..ConnectionConfig::default()
107 };
108 assert_eq!(base_url(Provider::Openai, &config), "https://api.openai.com/v1");
109 assert_eq!(base_url(Provider::OpenaiEndpoint, &config), "https://llm.acme.dev/v1");
110 assert_eq!(base_url(Provider::AnthropicEndpoint, &config), "https://llm.acme.dev");
111 assert_eq!(auth_header(Provider::Gemini, &config), "authorization");
112 assert_eq!(auth_header(Provider::Anthropic, &config), "x-api-key");
113 assert_eq!(auth_header(Provider::AzureOpenai, &config), "api-key");
114 assert_eq!(base_url(Provider::Groq, &config), "https://api.groq.com/openai/v1");
115 let azure = ConnectionConfig {
116 base_url: Some("https://acme.openai.azure.com/".to_owned()),
117 ..ConnectionConfig::default()
118 };
119 assert_eq!(base_url(Provider::AzureOpenai, &azure), "https://acme.openai.azure.com/openai/v1");
120 }
121
122 #[test]
123 fn only_models_that_can_chat_are_offered() {
124 assert!(for_chat("gpt-5"));
125 assert!(for_chat("gemini-2.5-pro"));
126 assert!(!for_chat("text-embedding-3-large"));
127 assert!(!for_chat("gpt-image-1"));
128 }
129}