Compare changes
Choose two branches to see what one has that the other does not, then open a pull request for it.
1 commit
17 files+267−500/17 viewed
| 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))) { | |