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
273 linesCodeBlameRaw
1import type { User, Viewer } from "./identity";
2import type { Tested } from "./integrations";
3import type { RepoPath } from "./repos";
4import type { Result } from "./result";
5
6/**
7 * A workspace's AI: the AI providers it keeps keys for, and what Ask g1t
8 * answers with. Kept by the integrations service, but not an integration: a
9 * workspace's AI is one of its own settings, at `/:workspace/-/ai`. Mirrors
10 * `crates/contracts/src/ai.rs`, which says what each does.
11 */
12export type AiProvider =
13 | "anthropic"
14 | "openai"
15 | "gemini"
16 | "xai"
17 | "mistral"
18 | "deepseek"
19 | "azure_openai"
20 | "openrouter"
21 | "groq"
22 | "together"
23 | "fireworks"
24 | "cerebras"
25 | "anthropic_endpoint"
26 | "openai_endpoint";
27
28/**
29 * Where an AI provider's models come from, as people choose them: a lab's
30 * own API, a platform serving many labs' models, or a model the workspace
31 * hosts itself (or its own gateway).
32 */
33export type AiGroup = "labs" | "platforms" | "self_hosted";
34
35/** What g1t knows about an AI provider. */
36export type AiSpec = {
37 label: string;
38 group: AiGroup;
39 /** The API it speaks. */
40 api: "anthropic" | "openai";
41 /**
42 * Where its API is, with the version for OpenAI's API and without it for
43 * Anthropic's. Empty when the workspace gives its own.
44 */
45 baseUrl: string;
46 /** The header the key goes in; `authorization` means `Bearer <key>`. */
47 authHeader: "x-api-key" | "authorization" | "api-key";
48};
49
50/** Every AI provider, in the order people are shown them. */
51export const AI_PROVIDERS: Record<AiProvider, AiSpec> = {
52 anthropic: { label: "Anthropic", group: "labs", api: "anthropic", baseUrl: "https://api.anthropic.com", authHeader: "x-api-key" },
53 openai: { label: "OpenAI", group: "labs", api: "openai", baseUrl: "https://api.openai.com/v1", authHeader: "authorization" },
54 gemini: {
55 label: "Google Gemini",
56 group: "labs",
57 api: "openai",
58 baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
59 authHeader: "authorization",
60 },
61 xai: { label: "xAI", group: "labs", api: "openai", baseUrl: "https://api.x.ai/v1", authHeader: "authorization" },
62 mistral: { label: "Mistral", group: "labs", api: "openai", baseUrl: "https://api.mistral.ai/v1", authHeader: "authorization" },
63 deepseek: { label: "DeepSeek", group: "labs", api: "openai", baseUrl: "https://api.deepseek.com/v1", authHeader: "authorization" },
64 azure_openai: { label: "Azure OpenAI", group: "platforms", api: "openai", baseUrl: "", authHeader: "api-key" },
65 openrouter: { label: "OpenRouter", group: "platforms", api: "openai", baseUrl: "https://openrouter.ai/api/v1", authHeader: "authorization" },
66 groq: { label: "Groq", group: "platforms", api: "openai", baseUrl: "https://api.groq.com/openai/v1", authHeader: "authorization" },
67 together: { label: "Together AI", group: "platforms", api: "openai", baseUrl: "https://api.together.xyz/v1", authHeader: "authorization" },
68 fireworks: {
69 label: "Fireworks AI",
70 group: "platforms",
71 api: "openai",
72 baseUrl: "https://api.fireworks.ai/inference/v1",
73 authHeader: "authorization",
74 },
75 cerebras: { label: "Cerebras", group: "platforms", api: "openai", baseUrl: "https://api.cerebras.ai/v1", authHeader: "authorization" },
76 anthropic_endpoint: { label: "Anthropic-compatible endpoint", group: "self_hosted", api: "anthropic", baseUrl: "", authHeader: "x-api-key" },
77 openai_endpoint: { label: "OpenAI-compatible endpoint", group: "self_hosted", api: "openai", baseUrl: "", authHeader: "authorization" },
78};
79
80/** Whether the workspace gives the provider's address, rather than g1t knowing it. */
81export function aiOwnAddress(provider: AiProvider): boolean {
82 return AI_PROVIDERS[provider].baseUrl === "";
83}
84
85export function isAiProvider(value: string): value is AiProvider {
86 return Object.hasOwn(AI_PROVIDERS, value);
87}
88
89/** An AI provider's settings: everything but its key. */
90export type AiConfig = {
91 /**
92 * For an endpoint or an Azure OpenAI resource: its address over
93 * `https://`. An Anthropic-compatible one without `/v1`, an
94 * OpenAI-compatible one up to and including the version.
95 */
96 baseUrl?: string;
97 /** For an endpoint: `x-api-key` or `authorization` (Bearer). */
98 authHeader?: string;
99 /** The model the team uses on it; for Azure OpenAI, the deployment. */
100 model?: string;
101 /**
102 * For Anthropic: the Console workspace the key belongs to, as its Console
103 * address names it (`wrkspc_…`), so links to the sessions it starts open
104 * there. Without it, the account's Default workspace.
105 */
106 consoleWorkspace?: string;
107};
108
109/** One of a workspace's AI providers, as any member sees it. */
110export type AiConnection = {
111 id: string;
112 workspace: string;
113 provider: AiProvider;
114 group: AiGroup;
115 name: string;
116 config: AiConfig;
117 /** `…3f9a`. The key itself is never shown again. */
118 secretHint: string | null;
119 createdBy: string;
120 createdAt: string;
121 lastUsedAt: string | null;
122 /** The last thing that went wrong talking to it, until it next works. */
123 lastError: string | null;
124 /** The models it offered when last checked. */
125 models: string[];
126};
127
128export type AddAiInput = {
129 provider: AiProvider;
130 name?: string;
131 config?: AiConfig;
132 /** Its API key. Optional for an endpoint that needs none. */
133 secret?: string;
134};
135
136export type UpdateAiInput = {
137 name?: string;
138 config?: AiConfig;
139 /** A new key, replacing the old one. */
140 secret?: string;
141};
142
143// --- Ask g1t -----------------------------------------------------------------
144// A conversation with g1t from the top bar and with @g1t in Chat, answered by
145// the AI provider and model an owner chose, on the workspace's own key. g1t
146// hosts no model for it and bills nothing for it.
147
148/** The most messages one ask sends: the conversation's latest turns. */
149export const ASK_MAX_MESSAGES = 40;
150/** The longest one message may be, in characters. */
151export const ASK_MAX_MESSAGE_CHARS = 16_000;
152/** The longest a whole conversation may be, in characters. */
153export const ASK_MAX_TOTAL_CHARS = 64_000;
154/** The most tokens one answer may run to. */
155export const ASK_MAX_OUTPUT_TOKENS = 4_096;
156
157/** The model name that chooses Auto in `setAskSettings`. */
158export const ASK_AUTO_MODEL = "auto";
159
160/** What Ask answers with in a workspace, or why it cannot yet. */
161export type AskSetup = {
162 /** The workspace's AI provider Ask answers with: an `AiConnection`'s id. */
163 provider_id: string | null;
164 /** Its name, as its owner named it. */
165 provider_name: string | null;
166 provider: AiProvider | null;
167 /** The model chosen. With `auto`, null, or the provider's own model when it lists none. */
168 model: string | null;
169 /**
170 * Auto: each question goes to the provider's model that suits it (a fast
171 * one for a lookup, a stronger one for a question that needs several reads
172 * or reasoning). The default.
173 */
174 auto?: boolean;
175 /** Why Ask cannot answer, for the person asking. Null when it can. */
176 problem: string | null;
177};
178
179export type AskRole = "user" | "assistant";
180export type AskMessage = { role: AskRole; content: string };
181
182/** Where the person asking is, so the answer is about their work. */
183export type AskPage = {
184 path?: string | null;
185 repo?: RepoPath | null;
186 issue?: number | null;
187 pull?: number | null;
188 /** The browser's IANA time zone, such as `America/Denver`: what "today" means when their profile names none. */
189 time_zone?: string | null;
190};
191
192/** The most rounds of tool calls one answer takes before it must answer. */
193export const ASK_MAX_TOOL_ROUNDS = 10;
194/** The most tool calls one answer makes, across its rounds. */
195export const ASK_MAX_TOOL_CALLS = 30;
196/** The most characters of one tool's result the model is given; more is trimmed, with a note. */
197export const ASK_MAX_TOOL_RESULT_CHARS = 16_000;
198/** The most characters of tool results one answer gives the model in all. */
199export const ASK_MAX_TOOL_TOTAL_CHARS = 120_000;
200/** How long one answer may spend calling tools, in seconds. */
201export const ASK_MAX_TOOL_SECONDS = 120;
202/** The most sources one answer lists. */
203export const ASK_MAX_SOURCES = 12;
204
205/**
206 * Where a step of an answer is. `refused`: Ask did not run it (it only
207 * reads and hands work off, or the answer had used its tool calls).
208 */
209export type AskToolStatus = "running" | "done" | "failed" | "refused";
210
211/** A page on g1t an answer drew on; `cited` when the answer links to it. */
212export type AskSource = { title: string; url: string; cited: boolean };
213
214/**
215 * One line of an `ask` answer, as newline-delimited JSON: `meta`, then
216 * `text` as it comes and a `tool` line as each step starts and ends (the
217 * same `id` twice), then `sources` and `done`, or `error` at any point.
218 */
219export type AskEvent =
220 | { type: "meta"; provider: AiProvider; provider_label: string; model: string; /** Auto chose `model`. */ auto?: boolean }
221 | { type: "text"; text: string }
222 | {
223 type: "tool";
224 id: string;
225 /** The tool, such as `pull_request`, and its action, such as `list`. */
226 name: string;
227 action: string | null;
228 /** What it does, for a person: "Read pull requests in acme/web". */
229 label: string;
230 status: AskToolStatus;
231 summary: string | null;
232 link: string | null;
233 }
234 | { type: "sources"; sources: AskSource[] }
235 | {
236 /**
237 * Work Ask proposes to hand to a session in the person's own harness:
238 * on an issue, or a new one with `title` and `body`. Nothing has
239 * started; the person starts it from the card the answer shows.
240 */
241 type: "proposal";
242 id: string;
243 repo: string;
244 issue: number | null;
245 title: string | null;
246 body: string | null;
247 mode: "plan" | "implement" | "review";
248 instruction: string | null;
249 }
250 | { type: "done"; stop_reason: string | null }
251 | { type: "error"; message: string };
252
253/** A workspace's AI, through the integrations service's `ai_*` and Ask methods. */
254export interface AiApi {
255 /** The workspace's AI providers. Members only. */
256 list(workspace: string, viewer: Viewer): Promise<Result<AiConnection[]>>;
257 /** Adds an AI provider, after checking its key. Owners only. */
258 add(actor: User, workspace: string, input: AddAiInput): Promise<Result<AiConnection>>;
259 /** Changes only the fields given. Owners only. */
260 update(actor: User, workspace: string, id: string, input: UpdateAiInput): Promise<Result<AiConnection>>;
261 remove(actor: User, workspace: string, id: string): Promise<Result<boolean>>;
262 test(actor: User, workspace: string, id: string): Promise<Result<Tested>>;
263 /** What Ask answers with. Members only. */
264 askSettings(workspace: string, viewer: Viewer): Promise<Result<AskSetup>>;
265 /** Chooses Ask's AI provider and model; a null provider turns it off. Owners only. */
266 setAskSettings(actor: User, workspace: string, providerId: string | null, model: string | null): Promise<Result<AskSetup>>;
267 /**
268 * One answer, as the service's response: newline-delimited `AskEvent`s,
269 * streamed. Members only. Call it on the binding itself, not through
270 * anything that reads the whole body first. `signal` stops the answer.
271 */
272 ask(actor: User, workspace: string, messages: AskMessage[], page: AskPage, signal?: AbortSignal): Promise<Response>;
273}