A free allowance on g1t's models, so anyone can try its agents
Until now g1t's hosted models were open only to listed workspaces, so anyone else, judges included, saw an empty forge until they pasted in a model key of their own. Every workspace now gets $1 of model cost on g1t's own models, free, out of one $40 pool, until the end of finalist judging (2026-10-22, 11:59 PM Pacific). Billing works it out from the runs it already records (`trial`: TRIAL_WORKSPACE_MICROS, TRIAL_TOTAL_MICROS, TRIAL_UNTIL; empty turns it off), and the runner counts an open allowance as hosted access, so agents, checks, the merge queue and workflow runners all start. Listed workspaces are not counted against the pool. When a workspace's share, or the pool, is spent, or the date passes, it closes, and every page that starts work says why and links to Integrations. - Mission control's checklist shows "Try g1t's agents free"; once the allowance is gone, "Connect a model" says it was used up or ended. - Integrations says how much is left and until when. - The landing page gains "Watch agents at work" (hello's merged pull requests, no sign-up) and says every new workspace gets $1 free until October 22; that line goes away on its own after. - The quickstart, usage guide (new "The free allowance" section), models, agents, Actions and overview pages and llms.txt say so. Verified as g1t-reviewer, a workspace with no provider: assigned an issue, the agent opened a pull request, its checks and its GitHub Actions workflow passed on g1t's runners, it was merged, and Integrations shows $0.80 of $1.00 left.
| 140 | 140 | member of the workspace chooses to merge anyway. | |
| 141 | 141 | ||
| 142 | 142 | Checks run in repositories of workspaces that can use g1t's agents: those | |
| 143 | − | with [their own model provider](/guides/models/), and those g1t's hosted | |
| 144 | − | models are open to. See [the preview](/guides/usage-and-billing/#the-preview). | |
| 143 | + | with [their own model provider](/guides/models/), and those on | |
| 144 | + | [the free allowance](/guides/usage-and-billing/#the-free-allowance) of | |
| 145 | + | g1t's hosted models. | |
| 145 | 146 | ||
| 146 | 147 | ## Review | |
| 147 | 148 | ||
| 231 | 232 | - **Milestones.** | |
| 232 | 233 | - **g1t agents for everyone.** g1t can put its own agents on an issue, each | |
| 233 | 234 | in a sandbox. A workspace that connects its own model provider can use | |
| 234 | − | them today; g1t's hosted models are open to selected workspaces until | |
| 235 | − | payments go live. | |
| 236 | − | See [the preview](/guides/usage-and-billing/#the-preview). | |
| 235 | + | them today, and until October 22 every workspace gets a free $1 of agent | |
| 236 | + | time on g1t's own models, no key needed. | |
| 237 | + | See [the free allowance](/guides/usage-and-billing/#the-free-allowance). |
| 118 | 118 | ||
| 119 | 119 | ## Who may run workflows | |
| 120 | 120 | ||
| 121 | − | Workflows run in every workspace that uses g1t's agents: one with its own | |
| 122 | − | [model provider](/guides/models/) connected, or one g1t has opened its | |
| 123 | − | hosted models to. They are free while g1t is being built out. Elsewhere a | |
| 124 | − | run is recorded with its jobs failed and the reason. | |
| 121 | + | Workflows run in every workspace that can use g1t's agents: one with its | |
| 122 | + | own [model provider](/guides/models/) connected, or one on | |
| 123 | + | [the free allowance](/guides/usage-and-billing/#the-free-allowance) while | |
| 124 | + | it lasts. They are free while g1t is being built out. Elsewhere a run is | |
| 125 | + | recorded with its jobs failed and the reason, and the Actions page says so | |
| 126 | + | before the first run. | |
| 125 | 127 | ||
| 126 | 128 | ## From the API | |
| 127 | 129 |
| 224 | 224 | ||
| 225 | 225 | - g1t's agents, and the sandboxes that run acceptance checks and the merge | |
| 226 | 226 | queue, work in any workspace that has | |
| 227 | − | [its own model provider](/guides/models/). g1t's hosted models are open to | |
| 228 | − | selected workspaces until payments go live. See | |
| 229 | − | [the preview](/guides/usage-and-billing/#the-preview). | |
| 227 | + | [its own model provider](/guides/models/), and, until October 22, in any | |
| 228 | + | workspace on its free $1 of g1t's own models. See | |
| 229 | + | [the free allowance](/guides/usage-and-billing/#the-free-allowance). | |
| 230 | 230 | - An agent is given one fork and the issue. Its credential, though, is your | |
| 231 | 231 | account's for the length of the run; credentials limited to the pull | |
| 232 | 232 | request are planned. |
| 7 | 7 | ||
| 8 | 8 | - **g1t's hosted models.** g1t chooses the model for each kind of work, pays | |
| 9 | 9 | the provider, and charges your workspace's credit what it cost plus a | |
| 10 | − | margin. Open to selected workspaces until payments go live, then to all. | |
| 10 | + | margin. Until October 22 every workspace gets $1 of it free (see | |
| 11 | + | [the free allowance](/guides/usage-and-billing/#the-free-allowance)); | |
| 12 | + | once payments go live, open to all. | |
| 11 | 13 | - **Your own providers.** Connect as many as you use, then choose, for each | |
| 12 | 14 | kind of work, which provider and model it runs on. Each provider bills | |
| 13 | 15 | you directly. Open to every workspace now. |
| 15 | 15 | model, and a workspace pays for the runs on its repositories from credit it | |
| 16 | 16 | buys in advance. There is no subscription and no seat price. | |
| 17 | 17 | ||
| 18 | + | ## The free allowance | |
| 19 | + | ||
| 20 | + | So anyone can try g1t's agents without a key of their own, every workspace | |
| 21 | + | gets **$1 of model cost on g1t's own models**, free, until **October 22, | |
| 22 | + | 2026** (11:59 PM Pacific). That is roughly 10 to 25 agent runs: changes, | |
| 23 | + | reviews and revisions. The allowance covers the GitHub Actions runners | |
| 24 | + | too, while it lasts. | |
| 25 | + | ||
| 26 | + | - Mission control and **Settings → Integrations** show what is left. | |
| 27 | + | - When it is used up, agents and workflow runs stop starting, and the | |
| 28 | + | pages that start them say so and link to Integrations. Connect your own | |
| 29 | + | model provider there and everything carries on at once. | |
| 30 | + | - The allowance draws on one shared pool. If the pool runs out before | |
| 31 | + | October 22, the allowance ends for everyone early. | |
| 32 | + | - Workspaces on their own provider never use it. | |
| 33 | + | ||
| 18 | 34 | ## What is charged | |
| 19 | 35 | ||
| 20 | 36 | | | Charged | | |
| 124 | 140 | workspace's agents, acceptance checks and merge queue work at once. Your | |
| 125 | 141 | provider bills you for the models; g1t charges nothing for now, and $0.10 | |
| 126 | 142 | a run once pricing starts. | |
| 127 | − | - **g1t's hosted models, for selected workspaces:** while payments are in | |
| 128 | − | test mode, g1t's own models are open only to workspaces it has opened | |
| 129 | − | them to. When payments go live, every workspace can use them, paid from | |
| 130 | − | its credit. | |
| 143 | + | - **g1t's hosted models:** while payments are in test mode, every | |
| 144 | + | workspace can use g1t's own models on | |
| 145 | + | [the free allowance](#the-free-allowance) ($1 each, until October 22), | |
| 146 | + | and a few g1t has opened them to without limit. When payments go live, | |
| 147 | + | every workspace can use them, paid from its credit. | |
| 131 | 148 | ||
| 132 | 149 | Each workspace decides where its model spend goes. A workspace that can use | |
| 133 | 150 | neither sees a message saying so, with the way to connect its own provider. |
| 57 | 57 | Add **acceptance checks**, commands that must pass, to hold the work to | |
| 58 | 58 | them. | |
| 59 | 59 | ||
| 60 | − | 5. **Connect a model** | |
| 60 | + | 5. **Use the free allowance, or connect a model** | |
| 61 | 61 | ||
| 62 | 62 | g1t's agents, and the runners your workflows use, need a model to work | |
| 63 | − | with. In your workspace's **Settings → Integrations**, connect your | |
| 64 | − | Anthropic or OpenAI key, or any compatible endpoint. Your provider bills | |
| 65 | − | you for the model; g1t itself costs nothing while it is being built out. | |
| 66 | − | Mission control and the issue page link there until it is done. See | |
| 67 | − | [model providers](/guides/models/). | |
| 63 | + | with. Until October 22, every new workspace gets **$1 of agent time on | |
| 64 | + | g1t's own models**, no key needed: enough for a good handful of issues. | |
| 65 | + | Mission control shows what is left. See | |
| 66 | + | [the free allowance](/guides/usage-and-billing/#the-free-allowance). | |
| 67 | + | ||
| 68 | + | For more, or to choose models, connect your Anthropic or OpenAI key, or | |
| 69 | + | any compatible endpoint, in your workspace's **Settings → | |
| 70 | + | Integrations**. Your provider bills you for the model; g1t itself costs | |
| 71 | + | nothing while it is being built out. See [model providers](/guides/models/). | |
| 68 | 72 | ||
| 69 | 73 | 6. **Put an agent on it** | |
| 70 | 74 |
| 9 | 9 | import { RepoList } from "./repo-list"; | |
| 10 | 10 | import { ButtonLink, CopyLine } from "./ui"; | |
| 11 | 11 | ||
| 12 | + | /** When the free allowance on g1t's models ends (billing's TRIAL_UNTIL). */ | |
| 13 | + | const TRIAL_ENDS = Date.parse("2026-10-23T06:59:59Z"); | |
| 14 | + | ||
| 12 | 15 | /** A small label above a heading, in the mono face. */ | |
| 13 | 16 | function Eyebrow({ children }: { children: ReactNode }) { | |
| 14 | 17 | return <p className="font-mono text-xs tracking-[0.2em] text-merged uppercase">{children}</p>; | |
| 156 | 159 | Get started | |
| 157 | 160 | <ArrowRight size={15} /> | |
| 158 | 161 | </ButtonLink> | |
| 159 | − | <ButtonLink to="https://docs.g1t.sh/quickstart/" variant="quiet" large> | |
| 160 | − | Read the docs | |
| 162 | + | <ButtonLink to="/syntaqx/hello/pulls?state=closed" variant="quiet" large> | |
| 163 | + | Watch agents at work | |
| 161 | 164 | </ButtonLink> | |
| 162 | 165 | </div> | |
| 166 | + | {Date.now() < TRIAL_ENDS && ( | |
| 167 | + | <p className="mx-auto mt-5 max-w-xl animate-fade-up text-sm text-fg-soft/70 text-balance"> | |
| 168 | + | Free to try: every new workspace gets $1 of agent time on g1t's models, no key needed, until | |
| 169 | + | October 22. Bring your own model for more. | |
| 170 | + | </p> | |
| 171 | + | )} | |
| 163 | 172 | </div> | |
| 164 | 173 | <div className="relative mx-auto max-w-5xl px-4 pb-4"> | |
| 165 | 174 | <ConvergeArt className="w-full" /> |
| 23 | 23 | ||
| 24 | 24 | const REFRESH_MS = 5000; | |
| 25 | 25 | ||
| 26 | + | const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`; | |
| 27 | + | ||
| 26 | 28 | export function meta({}: Route.MetaArgs) { | |
| 27 | 29 | return [ | |
| 28 | 30 | { title: "g1t — Git for AI scale" }, | |
| 70 | 72 | viewer, | |
| 71 | 73 | repos, | |
| 72 | 74 | canRunAgents: models == null || models.hosted || models.own != null, | |
| 75 | + | // Its free allowance on g1t's models, when that is what it runs on. | |
| 76 | + | trial: models?.own == null ? (models?.trial ?? null) : null, | |
| 73 | 77 | assigned: assigned.flatMap((issue) => { | |
| 74 | 78 | const repo = known.get(issue.repoId); | |
| 75 | 79 | return repo ? [{ issue, repo }] : []; | |
| 247 | 251 | } | |
| 248 | 252 | ||
| 249 | 253 | export default function Home({ loaderData }: Route.ComponentProps) { | |
| 250 | − | const { viewer, repos, active, assigned, canRunAgents } = loaderData; | |
| 254 | + | const { viewer, repos, active, assigned, canRunAgents, trial } = loaderData; | |
| 251 | 255 | ||
| 252 | 256 | const needsYou = active.filter((item) => item.lifecycle?.stage === "needs_you"); | |
| 253 | 257 | const ready = active.filter((item) => item.lifecycle?.stage === "ready"); | |
| 286 | 290 | to: workspace ? `/new?workspace=${workspace}` : "/new", | |
| 287 | 291 | action: "Add", | |
| 288 | 292 | }, | |
| 289 | − | canRunAgents | |
| 293 | + | trial?.open | |
| 290 | 294 | ? { | |
| 295 | + | done: true, | |
| 296 | + | title: "Try g1t's agents free", | |
| 297 | + | about: `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its free ${dollars(trial.limitMicros)} left on g1t's models${trial.endsAt ? `, until ${new Date(trial.endsAt).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone: "America/Los_Angeles" })}` : ""}: assign issues and watch agents land them. Connect your own model under Integrations any time for more.`, | |
| 298 | + | to: workspace ? `/${workspace}/-/integrations` : null, | |
| 299 | + | action: "Connect", | |
| 300 | + | } | |
| 301 | + | : canRunAgents | |
| 302 | + | ? { | |
| 291 | 303 | // Nothing to pay while g1t is being built out. | |
| 292 | 304 | done: Boolean(shell?.free) || shell?.creditMicros == null || shell.creditMicros > 0, | |
| 293 | 305 | title: "Add agent credit", | |
| 298 | 310 | : { | |
| 299 | 311 | done: false, | |
| 300 | 312 | title: "Connect a model", | |
| 301 | − | about: | |
| 302 | − | "Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint; g1t itself costs nothing while it is being built out.", | |
| 313 | + | about: `${ | |
| 314 | + | trial?.reason === "used" | |
| 315 | + | ? "This workspace has used its free allowance on g1t's models. " | |
| 316 | + | : trial?.reason === "pool" || trial?.reason === "ended" | |
| 317 | + | ? "The free allowance on g1t's models has ended. " | |
| 318 | + | : "" | |
| 319 | + | }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint; g1t itself costs nothing while it is being built out.`, | |
| 303 | 320 | to: workspace ? `/${workspace}/-/integrations` : null, | |
| 304 | 321 | action: "Connect", | |
| 305 | 322 | }, |
| 66 | 66 | repos: listed.map((repo) => `${repo.namespace}/${repo.name}`), | |
| 67 | 67 | adding: isProvider(adding) ? adding : null, | |
| 68 | 68 | hostedOpen: access.hosted, | |
| 69 | + | trial: access.trial, | |
| 69 | 70 | routes: routes.ok ? routes.value : [], | |
| 70 | 71 | feeMicros: account.ok ? account.value.orchestrationFeeMicros : 100_000, | |
| 71 | 72 | free: account.ok ? Boolean(account.value.status.free) : false, | |
| 175 | 176 | } | |
| 176 | 177 | ||
| 177 | 178 | export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) { | |
| 178 | − | const { slug, role, connections, deliveries, repos: repoNames, adding, feeMicros, free, marginPercent, hostedOpen, routes } = | |
| 179 | + | const { slug, role, connections, deliveries, repos: repoNames, adding, feeMicros, free, marginPercent, hostedOpen, trial, routes } = | |
| 179 | 180 | loaderData; | |
| 180 | 181 | const owner = role === "owner"; | |
| 181 | 182 | const busy = useNavigation().state === "submitting"; | |
| 199 | 200 | <AlertTriangle size={16} /> | |
| 200 | 201 | </span> | |
| 201 | 202 | <p className="text-sm text-muted"> | |
| 202 | − | <span className="font-medium text-fg">Choose how your agents reach a model.</span> g1t's hosted models | |
| 203 | − | are not open to {slug} yet. Connect a provider of your own below and your agents start at once, | |
| 204 | − | billed by that provider. | |
| 203 | + | <span className="font-medium text-fg">Choose how your agents reach a model.</span>{" "} | |
| 204 | + | {trial?.reason === "used" | |
| 205 | + | ? `${slug} has used its free allowance on g1t's models.` | |
| 206 | + | : trial?.reason === "pool" || trial?.reason === "ended" | |
| 207 | + | ? "The free allowance on g1t's models has ended." | |
| 208 | + | : `g1t's hosted models are not open to ${slug}.`}{" "} | |
| 209 | + | Connect a provider of your own below and your agents start at once, billed by that provider. | |
| 205 | 210 | </p> | |
| 206 | 211 | </div> | |
| 207 | 212 | )} | |
| 209 | 214 | {modelConnections.length === 0 && hostedOpen && ( | |
| 210 | 215 | <p className="flex items-center gap-2 rounded-xl border border-line bg-surface px-4 py-3 text-sm text-muted"> | |
| 211 | 216 | <CheckCircle2 size={15} className="shrink-0 text-merged" /> | |
| 212 | − | All work runs on g1t's models, charged to your credit at cost plus {marginPercent}%. Connect a provider of | |
| 213 | − | your own to choose models and pay for them there. | |
| 217 | + | {trial?.open | |
| 218 | + | ? `All work runs on g1t's models, free: ${slug} has ${dollars(Math.max(0, trial.limitMicros - trial.usedMicros))} of its ${dollars(trial.limitMicros)} allowance left${trial.endsAt ? `, until ${new Date(trial.endsAt).toLocaleDateString("en-US", { month: "long", day: "numeric", timeZone: "America/Los_Angeles" })}` : ""}. Connect a provider of your own for more, or to choose models.` | |
| 219 | + | : free | |
| 220 | + | ? "All work runs on g1t's models, free while g1t is being built out. Connect a provider of your own to choose models and pay for them there." | |
| 221 | + | : `All work runs on g1t's models, charged to your credit at cost plus ${marginPercent}%. Connect a provider of your own to choose models and pay for them there.`} | |
| 214 | 222 | </p> | |
| 215 | 223 | )} | |
| 216 | 224 | {modelConnections.length > 0 && ( |
| 154 | 154 | ||
| 155 | 155 | Each workspace decides how its agents reach a model: its own provider | |
| 156 | 156 | (connected under Integrations, billed by the provider) works for any | |
| 157 | − | workspace; g1t's hosted models are open to selected workspaces until | |
| 158 | − | payments go live. Without either, these calls answer with a message saying | |
| 159 | − | so. | |
| 157 | + | workspace; until October 22, 2026, every workspace also gets $1 of free | |
| 158 | + | agent time on g1t's hosted models, no key needed. Without either, these | |
| 159 | + | calls answer with a message saying so. | |
| 160 | 160 | ||
| 161 | 161 | - **Hand off an outcome:** `POST {repo}/plans` with `brief`: what should be | |
| 162 | 162 | true when the work is done. A planner reads the repository and proposes |
| 34 | 34 | pub free: bool, | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + | /// `trial`: the free allowance on g1t's hosted models for a workspace that | |
| 38 | + | /// is not otherwise open to them, so people can try g1t's agents without a | |
| 39 | + | /// key of their own. Each workspace gets a few dollars of model cost, out | |
| 40 | + | /// of one pool, until an end date. Returns `Trial`. | |
| 41 | + | #[derive(Debug, Serialize, Deserialize)] | |
| 42 | + | #[serde(rename_all = "camelCase")] | |
| 43 | + | pub struct TrialArgs { | |
| 44 | + | pub workspace: String, | |
| 45 | + | /// Workspaces open to hosted models anyway, whose use is not counted | |
| 46 | + | /// against the pool. | |
| 47 | + | #[serde(default)] | |
| 48 | + | pub exempt: Vec<String>, | |
| 49 | + | } | |
| 50 | + | ||
| 51 | + | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 52 | + | #[serde(rename_all = "camelCase")] | |
| 53 | + | pub struct Trial { | |
| 54 | + | /// Whether its agents may use g1t's hosted models on the allowance now. | |
| 55 | + | pub open: bool, | |
| 56 | + | /// What its runs on g1t's models have cost, in millionths of a dollar. | |
| 57 | + | pub used_micros: i64, | |
| 58 | + | pub limit_micros: i64, | |
| 59 | + | /// RFC 3339; when the allowance ends for everyone. | |
| 60 | + | pub ends_at: Option<String>, | |
| 61 | + | /// Why it is closed: `off` (no allowance), `ended`, `used` (this | |
| 62 | + | /// workspace's is spent) or `pool` (everyone's is). | |
| 63 | + | pub reason: Option<String>, | |
| 64 | + | } | |
| 65 | + | ||
| 37 | 66 | /// A workspace's standing. | |
| 38 | 67 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 39 | 68 | #[serde(rename_all = "camelCase")] |
| 67 | 67 | * buys credit; each run deducts its cost plus g1t's margin; with no credit, | |
| 68 | 68 | * no agent starts. | |
| 69 | 69 | */ | |
| 70 | + | /** | |
| 71 | + | * The free allowance on g1t's hosted models for a workspace not otherwise | |
| 72 | + | * open to them: a few dollars of model cost each, out of one pool, until a | |
| 73 | + | * date. Mirrors `Trial` in `crates/contracts/src/billing.rs`. | |
| 74 | + | */ | |
| 75 | + | export type Trial = { | |
| 76 | + | open: boolean; | |
| 77 | + | usedMicros: number; | |
| 78 | + | limitMicros: number; | |
| 79 | + | endsAt: string | null; | |
| 80 | + | /** Why it is closed: `off`, `ended`, `used` (this workspace's) or `pool` (everyone's). */ | |
| 81 | + | reason: "off" | "ended" | "used" | "pool" | null; | |
| 82 | + | }; | |
| 83 | + | ||
| 70 | 84 | export interface BillingApi { | |
| 71 | 85 | status(): Promise<BillingStatus>; | |
| 72 | 86 | /** Members of the workspace only. */ | |
| 88 | 102 | * opened for it. A failure, with the reason to show, when it has no credit. | |
| 89 | 103 | */ | |
| 90 | 104 | canStart(workspace: string): Promise<Result<boolean>>; | |
| 105 | + | /** A workspace's free allowance on g1t's hosted models; `exempt` are open to them anyway. */ | |
| 106 | + | trial(workspace: string, exempt: string[]): Promise<Trial>; | |
| 91 | 107 | /** | |
| 92 | 108 | * Asks whether a workspace may start an agent and opens the run it will be | |
| 93 | 109 | * charged for. Null when billing is off; a failure when there is no credit. |
| 186 | 186 | call("checkout", { actor, workspace, amountCents, returnUrl }), | |
| 187 | 187 | confirm: (workspace, viewer, session) => call("confirm", { workspace, viewer, session }), | |
| 188 | 188 | canStart: (workspace) => call("can_start", { workspace }), | |
| 189 | + | trial: (workspace, exempt) => call("trial", { workspace, exempt }), | |
| 189 | 190 | startRun: (run) => call("start_run", run), | |
| 190 | 191 | }; | |
| 191 | 192 | } |
| 1 | + | import type { Trial } from "./billing"; | |
| 1 | 2 | import type { User, Viewer } from "./identity"; | |
| 2 | 3 | import type { Result } from "./result"; | |
| 3 | 4 | import type { RepoPath } from "./repos"; | |
| 21 | 22 | export type ModelAccess = { | |
| 22 | 23 | /** The workspace's own model connection, by name, if it has one. */ | |
| 23 | 24 | own: string | null; | |
| 24 | − | /** Whether g1t's hosted models are open to it. */ | |
| 25 | + | /** Whether g1t's hosted models are open to it, on its free allowance or otherwise. */ | |
| 25 | 26 | hosted: boolean; | |
| 27 | + | /** Its free allowance, when that is how it reaches g1t's hosted models; null when it needs none. */ | |
| 28 | + | trial: Trial | null; | |
| 26 | 29 | }; | |
| 27 | 30 | ||
| 28 | 31 | export interface RunnerApi { |
| 126 | 126 | orchestration_fee_micros: i64, | |
| 127 | 127 | /// While g1t is being built out, nothing is charged (`FREE_WHILE_BUILDING`). | |
| 128 | 128 | free: bool, | |
| 129 | + | /// The free allowance on g1t's hosted models, when there is one. | |
| 130 | + | trial: Option<TrialConfig>, | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | /// `TRIAL_WORKSPACE_MICROS`, `TRIAL_TOTAL_MICROS` and `TRIAL_UNTIL`. | |
| 134 | + | struct TrialConfig { | |
| 135 | + | per_workspace_micros: i64, | |
| 136 | + | total_micros: i64, | |
| 137 | + | /// RFC 3339, in UTC. | |
| 138 | + | until: String, | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | #[derive(serde::Deserialize)] | |
| 142 | + | struct Sum { | |
| 143 | + | micros: Option<i64>, | |
| 129 | 144 | } | |
| 130 | 145 | ||
| 131 | 146 | impl Billing { | |
| 468 | 483 | })) | |
| 469 | 484 | } | |
| 470 | 485 | ||
| 486 | + | /// A workspace's free allowance on g1t's hosted models: what its runs | |
| 487 | + | /// there have cost against its share, and the pool everyone draws on. | |
| 488 | + | async fn trial(&self, a: TrialArgs) -> Result<Trial> { | |
| 489 | + | let workspace = a.workspace.to_lowercase(); | |
| 490 | + | let Some(config) = &self.trial else { | |
| 491 | + | return Ok(Trial { | |
| 492 | + | open: false, | |
| 493 | + | used_micros: 0, | |
| 494 | + | limit_micros: 0, | |
| 495 | + | ends_at: None, | |
| 496 | + | reason: Some("off".to_owned()), | |
| 497 | + | }); | |
| 498 | + | }; | |
| 499 | + | let used = self | |
| 500 | + | .db | |
| 501 | + | .prepare( | |
| 502 | + | "SELECT SUM(cost_micros) AS micros FROM ledger | |
| 503 | + | WHERE kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' AND workspace = ?", | |
| 504 | + | ) | |
| 505 | + | .bind(&[workspace.as_str().into()])? | |
| 506 | + | .first::<Sum>(None) | |
| 507 | + | .await? | |
| 508 | + | .and_then(|sum| sum.micros) | |
| 509 | + | .unwrap_or_default(); | |
| 510 | + | // Everyone's, but for the workspaces open to hosted models anyway. | |
| 511 | + | let exempt: Vec<String> = a.exempt.iter().map(|name| name.trim().to_lowercase()).collect(); | |
| 512 | + | let marks = vec!["?"; exempt.len().max(1)].join(", "); | |
| 513 | + | let mut values: Vec<JsValue> = exempt.iter().map(|name| JsValue::from(name.as_str())).collect(); | |
| 514 | + | if values.is_empty() { | |
| 515 | + | values.push(JsValue::from("")); | |
| 516 | + | } | |
| 517 | + | let pooled = self | |
| 518 | + | .db | |
| 519 | + | .prepare(format!( | |
| 520 | + | "SELECT SUM(cost_micros) AS micros FROM ledger | |
| 521 | + | WHERE kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' AND workspace NOT IN ({marks})" | |
| 522 | + | )) | |
| 523 | + | .bind(&values)? | |
| 524 | + | .first::<Sum>(None) | |
| 525 | + | .await? | |
| 526 | + | .and_then(|sum| sum.micros) | |
| 527 | + | .unwrap_or_default(); | |
| 528 | + | let reason = if rfc3339(now_ms()) >= config.until { | |
| 529 | + | Some("ended") | |
| 530 | + | } else if used >= config.per_workspace_micros { | |
| 531 | + | Some("used") | |
| 532 | + | } else if pooled >= config.total_micros { | |
| 533 | + | Some("pool") | |
| 534 | + | } else { | |
| 535 | + | None | |
| 536 | + | }; | |
| 537 | + | Ok(Trial { | |
| 538 | + | open: reason.is_none(), | |
| 539 | + | used_micros: used, | |
| 540 | + | limit_micros: config.per_workspace_micros, | |
| 541 | + | ends_at: Some(config.until.clone()), | |
| 542 | + | reason: reason.map(str::to_owned), | |
| 543 | + | }) | |
| 544 | + | } | |
| 545 | + | ||
| 471 | 546 | async fn can_start(&self, a: CanStartArgs) -> Result<Outcome<bool>> { | |
| 472 | 547 | if self.stripe.is_none() { | |
| 473 | 548 | return Ok(Outcome::Ok(true)); | |
| 610 | 685 | .and_then(|fee| fee.to_string().parse().ok()) | |
| 611 | 686 | .unwrap_or(100_000), | |
| 612 | 687 | free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"), | |
| 688 | + | trial: { | |
| 689 | + | let number = |name: &str| env.var(name).ok().and_then(|v| v.to_string().parse::<i64>().ok()); | |
| 690 | + | match ( | |
| 691 | + | number("TRIAL_WORKSPACE_MICROS"), | |
| 692 | + | number("TRIAL_TOTAL_MICROS"), | |
| 693 | + | env.var("TRIAL_UNTIL").ok().map(|v| v.to_string()), | |
| 694 | + | ) { | |
| 695 | + | (Some(per_workspace_micros), Some(total_micros), Some(until)) | |
| 696 | + | if per_workspace_micros > 0 && !until.is_empty() => | |
| 697 | + | { | |
| 698 | + | Some(TrialConfig { | |
| 699 | + | per_workspace_micros, | |
| 700 | + | total_micros, | |
| 701 | + | until, | |
| 702 | + | }) | |
| 703 | + | } | |
| 704 | + | _ => None, | |
| 705 | + | } | |
| 706 | + | }, | |
| 613 | 707 | }; | |
| 614 | 708 | match method.as_str() { | |
| 615 | 709 | "status" => reply(&billing.status()), | |
| 619 | 713 | "checkout" => reply(&billing.checkout(args(body)?).await?), | |
| 620 | 714 | "confirm" => reply(&billing.confirm(args(body)?).await?), | |
| 621 | 715 | "can_start" => reply(&billing.can_start(args(body)?).await?), | |
| 716 | + | "trial" => reply(&billing.trial(args(body)?).await?), | |
| 622 | 717 | "start_run" => reply(&billing.start_run(args(body)?).await?), | |
| 623 | 718 | "finish_run" => reply(&billing.finish_run(args(body)?).await?), | |
| 624 | 719 | _ => Response::error("Unknown method", 404), |
| 29 | 29 | // While g1t is being built out, workspaces pay nothing: runs are | |
| 30 | 30 | // recorded with what they cost, and nothing is charged. Set to | |
| 31 | 31 | // "false" when pricing starts. | |
| 32 | − | "FREE_WHILE_BUILDING": "true" | |
| 32 | + | "FREE_WHILE_BUILDING": "true", | |
| 33 | + | // A free allowance on g1t's hosted models, so people can try g1t's | |
| 34 | + | // agents without a key of their own: each workspace may use this | |
| 35 | + | // much model cost ($1), out of one pool for everyone ($40), until | |
| 36 | + | // the end of finalist judging (2026-10-22, 11:59 PM Pacific). Any | |
| 37 | + | // of them empty turns it off. | |
| 38 | + | "TRIAL_WORKSPACE_MICROS": "1000000", | |
| 39 | + | "TRIAL_TOTAL_MICROS": "40000000", | |
| 40 | + | "TRIAL_UNTIL": "2026-10-23T06:59:59Z" | |
| 33 | 41 | }, | |
| 34 | 42 | // Secret: STRIPE_SECRET_KEY. Without it nothing is charged and the | |
| 35 | 43 | // runner decides who may start agents some other way. |
| 562 | 562 | * How a workspace's agents reach a model, as the workspace decided: its | |
| 563 | 563 | * own provider, which it pays, or g1t's hosted models, which its credit | |
| 564 | 564 | * pays for. Hosted models are open to every workspace once billing takes | |
| 565 | − | * real money, and before that to those listed. Null when it can use | |
| 566 | − | * neither yet. | |
| 565 | + | * real money; before that to those listed, and to any other on its free | |
| 566 | + | * allowance while that lasts. Null when it can use neither yet. | |
| 567 | 567 | */ | |
| 568 | 568 | async modelAccess(namespace: string): Promise<ModelAccess> { | |
| 569 | − | if (!this.modelsReachable()) return { own: null, hosted: false }; | |
| 569 | + | if (!this.modelsReachable()) return { own: null, hosted: false, trial: null }; | |
| 570 | + | const billing = billingClient(this.env.BILLING); | |
| 570 | 571 | const [own, status] = await Promise.all([ | |
| 571 | 572 | integrationsClient(this.env.INTEGRATIONS) | |
| 572 | 573 | .modelProvider(namespace) | |
| 573 | 574 | .catch(() => null), | |
| 574 | − | billingClient(this.env.BILLING).status(), | |
| 575 | + | billing.status(), | |
| 575 | 576 | ]); | |
| 576 | − | return { | |
| 577 | − | own: own?.name ?? null, | |
| 578 | − | hosted: this.previewListed(namespace) || (status.enabled && status.live), | |
| 579 | − | }; | |
| 577 | + | if (this.previewListed(namespace) || (status.enabled && status.live)) { | |
| 578 | + | return { own: own?.name ?? null, hosted: true, trial: null }; | |
| 579 | + | } | |
| 580 | + | const exempt = this.env.HOSTED_AGENT_WORKSPACES.split(",") | |
| 581 | + | .map((name) => name.trim().toLowerCase()) | |
| 582 | + | .filter((name) => name && name !== "*"); | |
| 583 | + | const trial = await billing.trial(namespace, exempt).catch(() => null); | |
| 584 | + | return { own: own?.name ?? null, hosted: Boolean(trial?.open), trial }; | |
| 580 | 585 | } | |
| 581 | 586 | ||
| 582 | 587 | /** | |
| 598 | 603 | error: { | |
| 599 | 604 | code: "forbidden", | |
| 600 | 605 | message: | |
| 601 | − | "Workflows run on g1t's runners for workspaces that use g1t's agents: connect your own model provider under Integrations, free while g1t is being built out.", | |
| 606 | + | "Workflows run on g1t's runners for workspaces that can use g1t's agents, and this one has no model to use: its free allowance on g1t's models is used up or over. Connect your own model provider under Integrations; g1t is free while it is being built out.", | |
| 602 | 607 | }, | |
| 603 | 608 | }; | |
| 604 | 609 | } | |
| 1033 | 1038 | if (!(await this.workspaceAllowed(repo.namespace))) { | |
| 1034 | 1039 | return fail( | |
| 1035 | 1040 | "forbidden", | |
| 1036 | − | `g1t's hosted models are not open to the ${repo.namespace} workspace yet. An owner can connect the workspace's own model provider under Integrations, and its agents start at once.`, | |
| 1041 | + | `The ${repo.namespace} workspace has no model for its agents: its free allowance on g1t's models is used up or over. An owner can connect the workspace's own model provider under Integrations, and its agents start at once.`, | |
| 1037 | 1042 | ); | |
| 1038 | 1043 | } | |
| 1039 | 1044 | if (!(await this.allowed(actor, repo))) { |