g1t agents: model menu and optional AI Gateway routing
- runner: models offered are configured as Balanced, Deep and Fast, each mapped to a provider model server-side; AI_GATEWAY_ID routes model traffic through a Cloudflare AI Gateway - site: model choice when starting a run - docs: choosing a model, and how traffic is routed
6 files+122−140/6 viewed
| 13 | 13 | ## Starting a run | |
| 14 | 14 | ||
| 15 | 15 | 1. Open an intent on a repository. | |
| 16 | − | 2. In **Run g1t agents**, choose how many agents to race. | |
| 16 | + | 2. In **Run g1t agents**, choose how many agents to race and which model | |
| 17 | + | they use. | |
| 17 | 18 | 3. Optionally add guidance for this run, on top of the intent's brief. | |
| 18 | 19 | 4. Choose **Run**. | |
| 19 | 20 | ||
| 36 | 37 | want. Shipping lands it on `main` and closes the intent. See | |
| 37 | 38 | [shipping](/concepts/overview/#shipping) for what happens when `main` has moved. | |
| 38 | 39 | ||
| 39 | − | ## What runs behind it | |
| 40 | + | ## Choosing a model | |
| 41 | + | ||
| 42 | + | When you start a run you pick how much model to spend on it: | |
| 40 | 43 | ||
| 41 | − | Which model and tooling a g1t agent uses is decided by g1t, and later by | |
| 42 | − | workspace settings. An agent's attempt carries the label `g1t-agent`. | |
| 44 | + | | Choice | Use it for | | |
| 45 | + | | --- | --- | | |
| 46 | + | | **Balanced** | Most tasks. The default. | | |
| 47 | + | | **Deep** | Hard problems that need the strongest reasoning. Slower and costlier. | | |
| 48 | + | | **Fast** | Small, well-defined changes. | | |
| 49 | + | ||
| 50 | + | These are g1t's names, not a vendor's. Which model stands behind each one is | |
| 51 | + | g1t's choice and can change without your intents or automations changing. | |
| 52 | + | An agent's attempt carries the label `g1t-agent`. | |
| 53 | + | ||
| 54 | + | ## How model traffic is routed | |
| 55 | + | ||
| 56 | + | g1t agents can send every model request through | |
| 57 | + | [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/). The | |
| 58 | + | gateway is where an operator sees each request, caps spend, caches, and | |
| 59 | + | sets a fallback to another provider if one is down, without changing | |
| 60 | + | anything in g1t. | |
| 61 | + | ||
| 62 | + | If you run your own copy of g1t, two settings on the runner control this: | |
| 63 | + | ||
| 64 | + | | Setting | What it does | | |
| 65 | + | | --- | --- | | |
| 66 | + | | `AGENT_MODELS` | The choices offered, in order, each mapped to a provider's model. | | |
| 67 | + | | `AI_GATEWAY_ID` | The gateway to route through. Empty sends requests to the provider directly. | | |
| 43 | 68 | ||
| 44 | 69 | ## Limits in the preview | |
| 45 | 70 |
| 48 | 48 | return { | |
| 49 | 49 | ...detail, | |
| 50 | 50 | viewer, | |
| 51 | − | canRunHosted: await env.RUNNER.available(viewer), | |
| 51 | + | agentModels: await env.RUNNER.models(viewer), | |
| 52 | 52 | }; | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 62 | 62 | const result = await env.RUNNER.run(user, intentId, { | |
| 63 | 63 | count: Number(form.get("count")), | |
| 64 | 64 | instructions: String(form.get("instructions") ?? ""), | |
| 65 | + | model: String(form.get("model") ?? "") || undefined, | |
| 65 | 66 | }); | |
| 66 | 67 | return result.ok ? null : { error: result.error.message }; | |
| 67 | 68 | } | |
| 84 | 85 | actionData, | |
| 85 | 86 | params, | |
| 86 | 87 | }: Route.ComponentProps) { | |
| 87 | − | const { intent, attempts, viewer, canRunHosted } = loaderData; | |
| 88 | + | const { intent, attempts, viewer, agentModels } = loaderData; | |
| 88 | 89 | ||
| 89 | 90 | // Follow running attempts without a manual reload. | |
| 90 | 91 | const revalidator = useRevalidator(); | |
| 211 | 212 | </section> | |
| 212 | 213 | )} | |
| 213 | 214 | ||
| 214 | − | {open && canRunHosted && ( | |
| 215 | + | {open && agentModels.length > 0 && ( | |
| 215 | 216 | <section className="rounded-xl border border-accent/30 bg-accent/5 p-4"> | |
| 216 | 217 | <h3 className="flex items-center gap-2 text-sm font-medium"> | |
| 217 | 218 | <Sparkles size={15} className="text-accent" /> | |
| 238 | 239 | ))} | |
| 239 | 240 | </select> | |
| 240 | 241 | </label> | |
| 242 | + | <label className="flex items-center justify-between gap-3 text-sm"> | |
| 243 | + | <span className="text-muted">Model</span> | |
| 244 | + | <select | |
| 245 | + | name="model" | |
| 246 | + | className="rounded-md border border-line bg-bg px-2 py-1 text-sm" | |
| 247 | + | > | |
| 248 | + | {agentModels.map((model) => ( | |
| 249 | + | <option key={model.id} value={model.id} title={model.description}> | |
| 250 | + | {model.label} | |
| 251 | + | </option> | |
| 252 | + | ))} | |
| 253 | + | </select> | |
| 254 | + | </label> | |
| 241 | 255 | <Textarea | |
| 242 | 256 | name="instructions" | |
| 243 | 257 | rows={2} |
| 67 | 67 | let token = var("G1T_TOKEN")?; | |
| 68 | 68 | let secrets = std::iter::once(token.clone()) | |
| 69 | 69 | .chain(std::env::var("ANTHROPIC_API_KEY")) | |
| 70 | + | .chain(std::env::var("AI_GATEWAY_TOKEN")) | |
| 70 | 71 | .filter(|secret| !secret.is_empty()) | |
| 71 | 72 | .collect(); | |
| 72 | 73 | Ok(Reporter { |
| 2 | 2 | import type { Result } from "./result"; | |
| 3 | 3 | import type { Attempt } from "./work"; | |
| 4 | 4 | ||
| 5 | + | /** A model a g1t agent can run on, as offered to the person starting it. */ | |
| 6 | + | export type AgentModel = { | |
| 7 | + | /** Stable name used in requests, e.g. `balanced`. */ | |
| 8 | + | id: string; | |
| 9 | + | label: string; | |
| 10 | + | description: string; | |
| 11 | + | }; | |
| 12 | + | ||
| 5 | 13 | export type RunHostedInput = { | |
| 6 | 14 | /** How many agents to race on the intent, each in its own sandbox. */ | |
| 7 | 15 | count: number; | |
| 8 | 16 | /** Extra guidance appended to the intent's brief for these runs. */ | |
| 9 | 17 | instructions?: string; | |
| 18 | + | /** One of the offered model ids; the first offered when absent. */ | |
| 19 | + | model?: string; | |
| 10 | 20 | }; | |
| 11 | 21 | ||
| 12 | 22 | /** Hosted agents: sandboxes on g1t that work on an intent. */ | |
| 13 | 23 | export interface RunnerApi { | |
| 14 | − | /** Whether this viewer may start hosted agents. */ | |
| 15 | − | available(viewer: Viewer): Promise<boolean>; | |
| 24 | + | /** The models this viewer may run g1t agents on; empty if they may not. */ | |
| 25 | + | models(viewer: Viewer): Promise<AgentModel[]>; | |
| 16 | 26 | /** | |
| 17 | 27 | * Starts `count` attempts on the intent, each run by an agent in its own | |
| 18 | 28 | * sandbox. Returns as soon as the sandboxes are starting; progress shows |
| 2 | 2 | import { WorkerEntrypoint } from "cloudflare:workers"; | |
| 3 | 3 | ||
| 4 | 4 | import { | |
| 5 | + | type AgentModel, | |
| 5 | 6 | type Attempt, | |
| 6 | 7 | type Intent, | |
| 7 | 8 | type Result, | |
| 27 | 28 | * key above, so this stays an allowlist until accounts bring their own. | |
| 28 | 29 | */ | |
| 29 | 30 | HOSTED_AGENT_USERS: string; | |
| 31 | + | /** | |
| 32 | + | * The models offered, as JSON: `[{ id, label, description, model }]`. | |
| 33 | + | * `model` is the provider's model name and is never shown to users. | |
| 34 | + | */ | |
| 35 | + | AGENT_MODELS: string; | |
| 36 | + | /** | |
| 37 | + | * A Cloudflare AI Gateway id. When set, model traffic goes through that | |
| 38 | + | * gateway, which is where logging, spend limits, caching and fallback | |
| 39 | + | * between providers are configured. Empty sends it to the provider | |
| 40 | + | * directly. | |
| 41 | + | */ | |
| 42 | + | AI_GATEWAY_ID: string; | |
| 43 | + | CLOUDFLARE_ACCOUNT_ID: string; | |
| 44 | + | /** Secret. Needed only if the gateway requires authentication. */ | |
| 45 | + | AI_GATEWAY_TOKEN?: string; | |
| 30 | 46 | } | |
| 31 | 47 | ||
| 32 | 48 | const MAX_AGENTS_PER_RUN = 5; | |
| 63 | 79 | } | |
| 64 | 80 | } | |
| 65 | 81 | ||
| 82 | + | type ConfiguredModel = AgentModel & { model: string }; | |
| 83 | + | ||
| 84 | + | /** Where the sandbox sends model requests, and what it sends with them. */ | |
| 85 | + | function modelEnv(env: RunnerEnv, model: ConfiguredModel): Record<string, string> { | |
| 86 | + | const vars: Record<string, string> = { | |
| 87 | + | ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY!, | |
| 88 | + | ANTHROPIC_MODEL: model.model, | |
| 89 | + | }; | |
| 90 | + | if (env.AI_GATEWAY_ID) { | |
| 91 | + | vars.ANTHROPIC_BASE_URL = `https://gateway.ai.cloudflare.com/v1/${env.CLOUDFLARE_ACCOUNT_ID}/${env.AI_GATEWAY_ID}/anthropic`; | |
| 92 | + | if (env.AI_GATEWAY_TOKEN) { | |
| 93 | + | vars.AI_GATEWAY_TOKEN = env.AI_GATEWAY_TOKEN; | |
| 94 | + | vars.ANTHROPIC_CUSTOM_HEADERS = `cf-aig-authorization: Bearer ${env.AI_GATEWAY_TOKEN}`; | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | return vars; | |
| 98 | + | } | |
| 99 | + | ||
| 66 | 100 | function buildPrompt(intent: Intent, instructions: string): string { | |
| 67 | 101 | const parts = [ | |
| 68 | 102 | "You are a coding agent working in the git repository checked out in the current directory.", | |
| 90 | 124 | return new Response("Not found\n", { status: 404 }); | |
| 91 | 125 | } | |
| 92 | 126 | ||
| 93 | − | async available(viewer: Viewer): Promise<boolean> { | |
| 127 | + | private configuredModels(): ConfiguredModel[] { | |
| 128 | + | return JSON.parse(this.env.AGENT_MODELS); | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | private allowed(viewer: Viewer): boolean { | |
| 94 | 132 | if (!viewer || !this.env.ANTHROPIC_API_KEY) return false; | |
| 95 | 133 | return this.env.HOSTED_AGENT_USERS.split(",") | |
| 96 | 134 | .map((name) => name.trim()) | |
| 97 | 135 | .includes(viewer.username); | |
| 98 | 136 | } | |
| 99 | 137 | ||
| 138 | + | async models(viewer: Viewer): Promise<AgentModel[]> { | |
| 139 | + | if (!this.allowed(viewer)) return []; | |
| 140 | + | return this.configuredModels().map(({ id, label, description }) => ({ | |
| 141 | + | id, | |
| 142 | + | label, | |
| 143 | + | description, | |
| 144 | + | })); | |
| 145 | + | } | |
| 146 | + | ||
| 100 | 147 | async run( | |
| 101 | 148 | actor: User, | |
| 102 | 149 | intentId: string, | |
| 103 | 150 | input: RunHostedInput, | |
| 104 | 151 | ): Promise<Result<Attempt[]>> { | |
| 105 | − | if (!(await this.available(actor))) { | |
| 106 | − | return fail("forbidden", "Hosted agents are not enabled for your account."); | |
| 152 | + | if (!this.allowed(actor)) { | |
| 153 | + | return fail("forbidden", "g1t agents are not enabled for your account."); | |
| 107 | 154 | } | |
| 155 | + | const models = this.configuredModels(); | |
| 156 | + | const model = input.model | |
| 157 | + | ? models.find((candidate) => candidate.id === input.model) | |
| 158 | + | : models[0]; | |
| 159 | + | if (!model) return fail("invalid", "That model is not available."); | |
| 108 | 160 | const count = Math.min(Math.max(Math.trunc(input.count) || 1, 1), MAX_AGENTS_PER_RUN); | |
| 109 | 161 | const identity = identityClient(this.env.IDENTITY); | |
| 110 | 162 | ||
| 140 | 192 | GIT_REMOTE: `https://g1t.sh/${attempt.fork.namespace}/${attempt.fork.name}.git`, | |
| 141 | 193 | COMMIT_MESSAGE: found.value.intent.title, | |
| 142 | 194 | PROMPT: buildPrompt(found.value.intent, input.instructions?.trim() ?? ""), | |
| 143 | − | ANTHROPIC_API_KEY: this.env.ANTHROPIC_API_KEY!, | |
| 195 | + | ...modelEnv(this.env, model), | |
| 144 | 196 | }, | |
| 145 | 197 | }); | |
| 146 | 198 | } |
| 25 | 25 | { "binding": "WORK", "service": "g1t-work" } | |
| 26 | 26 | ], | |
| 27 | 27 | "vars": { | |
| 28 | − | "HOSTED_AGENT_USERS": "syntaqx" | |
| 28 | + | "HOSTED_AGENT_USERS": "syntaqx", | |
| 29 | + | // What a person can choose when starting g1t agents. The first is the | |
| 30 | + | // default. "model" is the provider's name for it and stays server-side. | |
| 31 | + | "AGENT_MODELS": "[{\"id\":\"balanced\",\"label\":\"Balanced\",\"description\":\"Good at most tasks.\",\"model\":\"claude-sonnet-5-5\"},{\"id\":\"deep\",\"label\":\"Deep\",\"description\":\"Strongest reasoning; slower and costlier.\",\"model\":\"claude-opus-5-5\"},{\"id\":\"fast\",\"label\":\"Fast\",\"description\":\"Quick and cheap, for small changes.\",\"model\":\"claude-haiku-4-5-20251001\"}]", | |
| 32 | + | // Set to a Cloudflare AI Gateway id to route model traffic through it. | |
| 33 | + | "AI_GATEWAY_ID": "", | |
| 34 | + | "CLOUDFLARE_ACCOUNT_ID": "1e6f2cffa3f445920836e8ebe446bb58" | |
| 29 | 35 | }, | |
| 30 | 36 | "observability": { "enabled": true } | |
| 31 | 37 | } |