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
267 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
103/** One of a workspace's AI providers, as any member sees it. */
104export type AiConnection = {
105 id: string;
106 workspace: string;
107 provider: AiProvider;
108 group: AiGroup;
109 name: string;
110 config: AiConfig;
111 /** `…3f9a`. The key itself is never shown again. */
112 secretHint: string | null;
113 createdBy: string;
114 createdAt: string;
115 lastUsedAt: string | null;
116 /** The last thing that went wrong talking to it, until it next works. */
117 lastError: string | null;
118 /** The models it offered when last checked. */
119 models: string[];
120};
121
122export type AddAiInput = {
123 provider: AiProvider;
124 name?: string;
125 config?: AiConfig;
126 /** Its API key. Optional for an endpoint that needs none. */
127 secret?: string;
128};
129
130export type UpdateAiInput = {
131 name?: string;
132 config?: AiConfig;
133 /** A new key, replacing the old one. */
134 secret?: string;
135};
136
137// --- Ask g1t -----------------------------------------------------------------
138// A conversation with g1t from the top bar and with @g1t in Chat, answered by
139// the AI provider and model an owner chose, on the workspace's own key. g1t
140// hosts no model for it and bills nothing for it.
141
142/** The most messages one ask sends: the conversation's latest turns. */
143export const ASK_MAX_MESSAGES = 40;
144/** The longest one message may be, in characters. */
145export const ASK_MAX_MESSAGE_CHARS = 16_000;
146/** The longest a whole conversation may be, in characters. */
147export const ASK_MAX_TOTAL_CHARS = 64_000;
148/** The most tokens one answer may run to. */
149export const ASK_MAX_OUTPUT_TOKENS = 4_096;
150
151/** The model name that chooses Auto in `setAskSettings`. */
152export const ASK_AUTO_MODEL = "auto";
153
154/** What Ask answers with in a workspace, or why it cannot yet. */
155export type AskSetup = {
156 /** The workspace's AI provider Ask answers with: an `AiConnection`'s id. */
157 provider_id: string | null;
158 /** Its name, as its owner named it. */
159 provider_name: string | null;
160 provider: AiProvider | null;
161 /** The model chosen. With `auto`, null, or the provider's own model when it lists none. */
162 model: string | null;
163 /**
164 * Auto: each question goes to the provider's model that suits it (a fast
165 * one for a lookup, a stronger one for a question that needs several reads
166 * or reasoning). The default.
167 */
168 auto?: boolean;
169 /** Why Ask cannot answer, for the person asking. Null when it can. */
170 problem: string | null;
171};
172
173export type AskRole = "user" | "assistant";
174export type AskMessage = { role: AskRole; content: string };
175
176/** Where the person asking is, so the answer is about their work. */
177export type AskPage = {
178 path?: string | null;
179 repo?: RepoPath | null;
180 issue?: number | null;
181 pull?: number | null;
182 /** The browser's IANA time zone, such as `America/Denver`: what "today" means when their profile names none. */
183 time_zone?: string | null;
184};
185
186/** The most rounds of tool calls one answer takes before it must answer. */
187export const ASK_MAX_TOOL_ROUNDS = 10;
188/** The most tool calls one answer makes, across its rounds. */
189export const ASK_MAX_TOOL_CALLS = 30;
190/** The most characters of one tool's result the model is given; more is trimmed, with a note. */
191export const ASK_MAX_TOOL_RESULT_CHARS = 16_000;
192/** The most characters of tool results one answer gives the model in all. */
193export const ASK_MAX_TOOL_TOTAL_CHARS = 120_000;
194/** How long one answer may spend calling tools, in seconds. */
195export const ASK_MAX_TOOL_SECONDS = 120;
196/** The most sources one answer lists. */
197export const ASK_MAX_SOURCES = 12;
198
199/**
200 * Where a step of an answer is. `refused`: Ask did not run it (it only
201 * reads and hands work off, or the answer had used its tool calls).
202 */
203export type AskToolStatus = "running" | "done" | "failed" | "refused";
204
205/** A page on g1t an answer drew on; `cited` when the answer links to it. */
206export type AskSource = { title: string; url: string; cited: boolean };
207
208/**
209 * One line of an `ask` answer, as newline-delimited JSON: `meta`, then
210 * `text` as it comes and a `tool` line as each step starts and ends (the
211 * same `id` twice), then `sources` and `done`, or `error` at any point.
212 */
213export type AskEvent =
214 | { type: "meta"; provider: AiProvider; provider_label: string; model: string; /** Auto chose `model`. */ auto?: boolean }
215 | { type: "text"; text: string }
216 | {
217 type: "tool";
218 id: string;
219 /** The tool, such as `pull_request`, and its action, such as `list`. */
220 name: string;
221 action: string | null;
222 /** What it does, for a person: "Read pull requests in acme/web". */
223 label: string;
224 status: AskToolStatus;
225 summary: string | null;
226 link: string | null;
227 }
228 | { type: "sources"; sources: AskSource[] }
229 | {
230 /**
231 * Work Ask proposes to hand to a session in the person's own harness:
232 * on an issue, or a new one with `title` and `body`. Nothing has
233 * started; the person starts it from the card the answer shows.
234 */
235 type: "proposal";
236 id: string;
237 repo: string;
238 issue: number | null;
239 title: string | null;
240 body: string | null;
241 mode: "plan" | "implement" | "review";
242 instruction: string | null;
243 }
244 | { type: "done"; stop_reason: string | null }
245 | { type: "error"; message: string };
246
247/** A workspace's AI, through the integrations service's `ai_*` and Ask methods. */
248export interface AiApi {
249 /** The workspace's AI providers. Members only. */
250 list(workspace: string, viewer: Viewer): Promise<Result<AiConnection[]>>;
251 /** Adds an AI provider, after checking its key. Owners only. */
252 add(actor: User, workspace: string, input: AddAiInput): Promise<Result<AiConnection>>;
253 /** Changes only the fields given. Owners only. */
254 update(actor: User, workspace: string, id: string, input: UpdateAiInput): Promise<Result<AiConnection>>;
255 remove(actor: User, workspace: string, id: string): Promise<Result<boolean>>;
256 test(actor: User, workspace: string, id: string): Promise<Result<Tested>>;
257 /** What Ask answers with. Members only. */
258 askSettings(workspace: string, viewer: Viewer): Promise<Result<AskSetup>>;
259 /** Chooses Ask's AI provider and model; a null provider turns it off. Owners only. */
260 setAskSettings(actor: User, workspace: string, providerId: string | null, model: string | null): Promise<Result<AskSetup>>;
261 /**
262 * One answer, as the service's response: newline-delimited `AskEvent`s,
263 * streamed. Members only. Call it on the binding itself, not through
264 * anything that reads the whole body first. `signal` stops the answer.
265 */
266 ask(actor: User, workspace: string, messages: AskMessage[], page: AskPage, signal?: AbortSignal): Promise<Response>;
267}