| 1 | import type { User, Viewer } from "./identity"; |
| 2 | import type { Tested } from "./integrations"; |
| 3 | import type { RepoPath } from "./repos"; |
| 4 | import 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 | */ |
| 12 | export 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 | */ |
| 33 | export type AiGroup = "labs" | "platforms" | "self_hosted"; |
| 34 | |
| 35 | /** What g1t knows about an AI provider. */ |
| 36 | export 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. */ |
| 51 | export 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. */ |
| 81 | export function aiOwnAddress(provider: AiProvider): boolean { |
| 82 | return AI_PROVIDERS[provider].baseUrl === ""; |
| 83 | } |
| 84 | |
| 85 | export 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. */ |
| 90 | export 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. */ |
| 110 | export 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 | |
| 128 | export 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 | |
| 136 | export 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. */ |
| 149 | export const ASK_MAX_MESSAGES = 40; |
| 150 | /** The longest one message may be, in characters. */ |
| 151 | export const ASK_MAX_MESSAGE_CHARS = 16_000; |
| 152 | /** The longest a whole conversation may be, in characters. */ |
| 153 | export const ASK_MAX_TOTAL_CHARS = 64_000; |
| 154 | /** The most tokens one answer may run to. */ |
| 155 | export const ASK_MAX_OUTPUT_TOKENS = 4_096; |
| 156 | |
| 157 | /** The model name that chooses Auto in `setAskSettings`. */ |
| 158 | export const ASK_AUTO_MODEL = "auto"; |
| 159 | |
| 160 | /** What Ask answers with in a workspace, or why it cannot yet. */ |
| 161 | export 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 | |
| 179 | export type AskRole = "user" | "assistant"; |
| 180 | export type AskMessage = { role: AskRole; content: string }; |
| 181 | |
| 182 | /** Where the person asking is, so the answer is about their work. */ |
| 183 | export 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. */ |
| 193 | export const ASK_MAX_TOOL_ROUNDS = 10; |
| 194 | /** The most tool calls one answer makes, across its rounds. */ |
| 195 | export 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. */ |
| 197 | export const ASK_MAX_TOOL_RESULT_CHARS = 16_000; |
| 198 | /** The most characters of tool results one answer gives the model in all. */ |
| 199 | export const ASK_MAX_TOOL_TOTAL_CHARS = 120_000; |
| 200 | /** How long one answer may spend calling tools, in seconds. */ |
| 201 | export const ASK_MAX_TOOL_SECONDS = 120; |
| 202 | /** The most sources one answer lists. */ |
| 203 | export 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 | */ |
| 209 | export type AskToolStatus = "running" | "done" | "failed" | "refused"; |
| 210 | |
| 211 | /** A page on g1t an answer drew on; `cited` when the answer links to it. */ |
| 212 | export 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 | */ |
| 219 | export 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. */ |
| 254 | export 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 | } |