Commit

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

syntaqxcommitted Parentffd58d5Browse files
6 files+122−140/6 viewed
+29−4
1313 ## Starting a run
1414
1515 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.
1718 3. Optionally add guidance for this run, on top of the intent's brief.
1819 4. Choose **Run**.
1920
3637 want. Shipping lands it on `main` and closes the intent. See
3738 [shipping](/concepts/overview/#shipping) for what happens when `main` has moved.
3839
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:
4043
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. |
4368
4469 ## Limits in the preview
4570
+17−3
4848 return {
4949 ...detail,
5050 viewer,
51− canRunHosted: await env.RUNNER.available(viewer),
51+ agentModels: await env.RUNNER.models(viewer),
5252 };
5353 }
5454
6262 const result = await env.RUNNER.run(user, intentId, {
6363 count: Number(form.get("count")),
6464 instructions: String(form.get("instructions") ?? ""),
65+ model: String(form.get("model") ?? "") || undefined,
6566 });
6667 return result.ok ? null : { error: result.error.message };
6768 }
8485 actionData,
8586 params,
8687 }: Route.ComponentProps) {
87− const { intent, attempts, viewer, canRunHosted } = loaderData;
88+ const { intent, attempts, viewer, agentModels } = loaderData;
8889
8990 // Follow running attempts without a manual reload.
9091 const revalidator = useRevalidator();
211212 </section>
212213 )}
213214
214− {open && canRunHosted && (
215+ {open && agentModels.length > 0 && (
215216 <section className="rounded-xl border border-accent/30 bg-accent/5 p-4">
216217 <h3 className="flex items-center gap-2 text-sm font-medium">
217218 <Sparkles size={15} className="text-accent" />
238239 ))}
239240 </select>
240241 </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>
241255 <Textarea
242256 name="instructions"
243257 rows={2}
+1−0
6767 let token = var("G1T_TOKEN")?;
6868 let secrets = std::iter::once(token.clone())
6969 .chain(std::env::var("ANTHROPIC_API_KEY"))
70+ .chain(std::env::var("AI_GATEWAY_TOKEN"))
7071 .filter(|secret| !secret.is_empty())
7172 .collect();
7273 Ok(Reporter {
+12−2
22 import type { Result } from "./result";
33 import type { Attempt } from "./work";
44
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+
513 export type RunHostedInput = {
614 /** How many agents to race on the intent, each in its own sandbox. */
715 count: number;
816 /** Extra guidance appended to the intent's brief for these runs. */
917 instructions?: string;
18+ /** One of the offered model ids; the first offered when absent. */
19+ model?: string;
1020 };
1121
1222 /** Hosted agents: sandboxes on g1t that work on an intent. */
1323 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[]>;
1626 /**
1727 * Starts `count` attempts on the intent, each run by an agent in its own
1828 * sandbox. Returns as soon as the sandboxes are starting; progress shows
+56−4
22 import { WorkerEntrypoint } from "cloudflare:workers";
33
44 import {
5+ type AgentModel,
56 type Attempt,
67 type Intent,
78 type Result,
2728 * key above, so this stays an allowlist until accounts bring their own.
2829 */
2930 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;
3046 }
3147
3248 const MAX_AGENTS_PER_RUN = 5;
6379 }
6480 }
6581
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+
66100 function buildPrompt(intent: Intent, instructions: string): string {
67101 const parts = [
68102 "You are a coding agent working in the git repository checked out in the current directory.",
90124 return new Response("Not found\n", { status: 404 });
91125 }
92126
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 {
94132 if (!viewer || !this.env.ANTHROPIC_API_KEY) return false;
95133 return this.env.HOSTED_AGENT_USERS.split(",")
96134 .map((name) => name.trim())
97135 .includes(viewer.username);
98136 }
99137
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+
100147 async run(
101148 actor: User,
102149 intentId: string,
103150 input: RunHostedInput,
104151 ): 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.");
107154 }
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.");
108160 const count = Math.min(Math.max(Math.trunc(input.count) || 1, 1), MAX_AGENTS_PER_RUN);
109161 const identity = identityClient(this.env.IDENTITY);
110162
140192 GIT_REMOTE: `https://g1t.sh/${attempt.fork.namespace}/${attempt.fork.name}.git`,
141193 COMMIT_MESSAGE: found.value.intent.title,
142194 PROMPT: buildPrompt(found.value.intent, input.instructions?.trim() ?? ""),
143− ANTHROPIC_API_KEY: this.env.ANTHROPIC_API_KEY!,
195+ ...modelEnv(this.env, model),
144196 },
145197 });
146198 }
+7−1
2525 { "binding": "WORK", "service": "g1t-work" }
2626 ],
2727 "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"
2935 },
3036 "observability": { "enabled": true }
3137 }