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+114−330/17 viewed
| 537 | 537 | "A workspace's integrations: its own model provider, the alert sources that open issues (Sentry, Datadog, webhooks), and the trackers whose tickets agents can read (Jira, Linear). Secrets are never returned. Members only." | |
| 538 | 538 | } | |
| 539 | 539 | Op::ConnectIntegration => { | |
| 540 | − | "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, with g1t charging a flat orchestration fee per run; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only." | |
| 540 | + | "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, and free on g1t while it is being built out; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only." | |
| 541 | 541 | } | |
| 542 | 542 | Op::DisconnectIntegration => { | |
| 543 | 543 | "Remove an integration and its secrets. Agents already running on a model provider being removed stop reaching it. Owners only." |
| 114 | 114 | ||
| 115 | 115 | ## Who may run workflows | |
| 116 | 116 | ||
| 117 | − | While g1t is in preview, workflows run in the workspaces g1t has opened | |
| 118 | − | them to. Elsewhere a run is recorded with its jobs failed and the reason. | |
| 117 | + | Workflows run in every workspace that uses g1t's agents: one with its own | |
| 118 | + | [model provider](/guides/models/) connected, or one g1t has opened its | |
| 119 | + | hosted models to. They are free while g1t is being built out. Elsewhere a | |
| 120 | + | run is recorded with its jobs failed and the reason. | |
| 119 | 121 | ||
| 120 | 122 | ## From the API | |
| 121 | 123 |
| 205 | 205 | ||
| 206 | 206 | ## What it costs | |
| 207 | 207 | ||
| 208 | − | A workspace pays for the g1t agents that work on its repositories, from | |
| 209 | − | credit an owner buys in advance: each run is charged what the model cost, | |
| 210 | − | plus 20%. Acceptance checks are free. With no credit, agents do not start. | |
| 208 | + | While g1t is being built out, its agents cost nothing: runs are recorded | |
| 209 | + | with what they cost, and nothing is charged. Once pricing starts, a | |
| 210 | + | workspace will pay for the g1t agents that work on its repositories from | |
| 211 | + | credit an owner buys in advance: each run charged what the model cost, plus | |
| 212 | + | 20%, and acceptance checks free. | |
| 211 | 213 | The workspace's **Usage** page shows what its agents have cost, by day, | |
| 212 | 214 | kind of work, repository, model and pull request. See | |
| 213 | 215 | [usage and billing](/guides/usage-and-billing/). |
| 83 | 83 | ||
| 84 | 84 | ## What it costs | |
| 85 | 85 | ||
| 86 | − | On your own providers, they bill you for the models, and g1t charges your | |
| 87 | − | credit a flat **$0.10 per run** for the sandbox and orchestration. A | |
| 86 | + | While g1t is being built out, g1t charges nothing for runs on your own | |
| 87 | + | providers: they bill you for the models, and that is all. Once pricing | |
| 88 | + | starts, g1t will charge your credit a flat **$0.10 per run** for the | |
| 89 | + | sandbox and orchestration. A | |
| 88 | 90 | change, a review, a revision, a catch-up and a plan are each a run. The | |
| 89 | 91 | statement marks these runs "on your own model provider" and names the | |
| 90 | 92 | model and provider; the Usage page shows what they cost at the provider, |
| 3 | 3 | description: What g1t agents cost, how a workspace pays for them, and what is free. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | + | > **Free while g1t is being built out.** For now, using g1t costs nothing: | |
| 7 | + | > agents, reviews, checks and workflows. Bring your own model provider and | |
| 8 | + | > its usage is billed by that provider, not by g1t. Runs are still recorded | |
| 9 | + | > with what they cost, so the Usage page shows what you are using. This is | |
| 10 | + | > for now, not forever: the pricing below is how g1t will charge once it | |
| 11 | + | > starts, and we will say so well before anything is charged. | |
| 12 | + | ||
| 6 | 13 | Hosting repositories, issues, pull requests, review and your own agent cost | |
| 7 | 14 | nothing on g1t. What costs money is g1t's own agents: each run uses a | |
| 8 | 15 | model, and a workspace pays for the runs on its repositories from credit it |
| 63 | 63 | } | null; | |
| 64 | 64 | /** The workspace's agent credit, if billing is on and they may see it. */ | |
| 65 | 65 | creditMicros: number | null; | |
| 66 | + | /** Whether g1t charges nothing for now, while it is being built out. */ | |
| 67 | + | free?: boolean; | |
| 66 | 68 | /** What its agents have cost since the start of the month. */ | |
| 67 | 69 | monthSpentMicros: number | null; | |
| 68 | 70 | }; | |
| 190 | 192 | </span> | |
| 191 | 193 | <span className="mt-2 flex items-baseline justify-between"> | |
| 192 | 194 | <span className="font-mono text-sm tabular-nums">${(spent / MICROS_PER_DOLLAR).toFixed(2)}</span> | |
| 193 | − | {left != null && ( | |
| 194 | − | <span className={`text-xs ${left <= 0 ? "text-warn" : "text-faint"}`}> | |
| 195 | − | ${(left / MICROS_PER_DOLLAR).toFixed(2)} left | |
| 196 | − | </span> | |
| 195 | + | {shell.free ? ( | |
| 196 | + | <span className="text-xs text-accent">Free for now</span> | |
| 197 | + | ) : ( | |
| 198 | + | left != null && ( | |
| 199 | + | <span className={`text-xs ${left <= 0 ? "text-warn" : "text-faint"}`}> | |
| 200 | + | ${(left / MICROS_PER_DOLLAR).toFixed(2)} left | |
| 201 | + | </span> | |
| 202 | + | ) | |
| 197 | 203 | )} | |
| 198 | 204 | </span> | |
| 199 | 205 | <span className="mt-2 block h-1 overflow-hidden rounded-full bg-raised"> |
| 109 | 109 | pulls: counts.value.pulls, | |
| 110 | 110 | } | |
| 111 | 111 | : null, | |
| 112 | + | // While g1t is being built out nothing is charged, so no credit is shown. | |
| 112 | 113 | creditMicros: | |
| 113 | − | account?.ok && account.value.status.enabled ? account.value.balanceMicros : null, | |
| 114 | + | account?.ok && account.value.status.enabled && !account.value.status.free ? account.value.balanceMicros : null, | |
| 115 | + | free: account?.ok ? Boolean(account.value.status.free) : false, | |
| 114 | 116 | monthSpentMicros: usage?.ok ? usage.value.spentMicros : null, | |
| 115 | 117 | }; | |
| 116 | 118 | } |
| 281 | 281 | action: "Add", | |
| 282 | 282 | }, | |
| 283 | 283 | { | |
| 284 | − | done: shell?.creditMicros == null || shell.creditMicros > 0, | |
| 284 | + | // Nothing to pay while g1t is being built out. | |
| 285 | + | done: Boolean(shell?.free) || shell?.creditMicros == null || shell.creditMicros > 0, | |
| 285 | 286 | title: "Add agent credit", | |
| 286 | 287 | about: "g1t's agents are paid for from the workspace's credit, at what the model costs plus 20%.", | |
| 287 | 288 | to: workspace ? `/${workspace}/-/billing` : null, |
| 78 | 78 | return ( | |
| 79 | 79 | <div className="grid gap-10 lg:grid-cols-[1fr_20rem]"> | |
| 80 | 80 | <div className="min-w-0"> | |
| 81 | + | {status.free && ( | |
| 82 | + | <div className="mb-8 rounded-xl border border-accent/30 bg-accent/5 p-5"> | |
| 83 | + | <h2 className="font-medium">Free while g1t is being built out</h2> | |
| 84 | + | <p className="mt-1.5 max-w-2xl text-sm text-muted"> | |
| 85 | + | While we build g1t out, using it costs nothing: agents, reviews, checks and workflows. Bring your own | |
| 86 | + | model provider under Integrations and its usage is billed by that provider, not by g1t. Runs are still | |
| 87 | + | recorded with what they cost, so Usage shows what you are using. This is for now, not forever: pricing | |
| 88 | + | will come later, and we will say so well before anything is charged. | |
| 89 | + | </p> | |
| 90 | + | </div> | |
| 91 | + | )} | |
| 81 | 92 | <h2 className="font-medium">Agent credit</h2> | |
| 82 | 93 | <p className="mt-1 max-w-2xl text-sm text-muted"> | |
| 83 | 94 | g1t agents that work on this workspace's repositories are paid for from |
| 215 | 215 | as it uses, with each | |
| 216 | 216 | kind of work routed to one of them or to g1t's hosted models | |
| 217 | 217 | (`PUT /workspaces/{workspace}/model-routes`). Those providers bill the | |
| 218 | − | workspace and g1t charges $0.10 a run. Sandboxes never hold a key. | |
| 218 | + | workspace; g1t charges nothing while it is being built out (later, $0.10 | |
| 219 | + | a run). Sandboxes never hold a key. | |
| 219 | 220 | - **Alerts** (`sentry`, `datadog`, `webhook`): each problem opens one issue | |
| 220 | 221 | in a chosen repository, optionally with an agent put on it at once. | |
| 221 | 222 | Senders sign requests to `https://api.g1t.sh/hooks/{integration}`. |
| 27 | 27 | /// False while the payment provider is in its test mode, where cards | |
| 28 | 28 | /// are not real. | |
| 29 | 29 | pub live: bool, | |
| 30 | + | /// True while g1t is being built out: runs are recorded, with what | |
| 31 | + | /// they cost, but nothing is charged and no credit is needed. Not a | |
| 32 | + | /// promise that it stays free. | |
| 33 | + | #[serde(default)] | |
| 34 | + | pub free: bool, | |
| 30 | 35 | } | |
| 31 | 36 | ||
| 32 | 37 | /// A workspace's standing. |
| 14 | 14 | enabled: boolean; | |
| 15 | 15 | /** False while the card processor is in its test mode, where cards are not real. */ | |
| 16 | 16 | live: boolean; | |
| 17 | + | /** | |
| 18 | + | * True while g1t is being built out: runs are recorded with what they | |
| 19 | + | * cost, but nothing is charged and no credit is needed. Not forever. | |
| 20 | + | */ | |
| 21 | + | free?: boolean; | |
| 17 | 22 | }; | |
| 18 | 23 | ||
| 19 | 24 | /** A workspace's standing. */ |
| 6 | 6 | use g1t_actions::workflow::{self, Trigger, Workflow}; | |
| 7 | 7 | use g1t_contracts::actions::{DispatchArgs, WorkflowRun}; | |
| 8 | 8 | use g1t_contracts::events::Event; | |
| 9 | − | use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernameArgs, UsernamesArgs}; | |
| 9 | + | use g1t_contracts::identity::{AGENT_ID, AGENT_NAME, UsernamesArgs}; | |
| 10 | 10 | use g1t_contracts::repos::{Commit, CompareArgs, Comparison, LogArgs, Repo, RepoPath}; | |
| 11 | 11 | use g1t_contracts::work::{IssueDetail, PullDetail, ViewArgs}; | |
| 12 | − | use g1t_contracts::{FailureCode, Outcome, User, Viewer, new_id}; | |
| 12 | + | use g1t_contracts::{FailureCode, Outcome, User, new_id}; | |
| 13 | 13 | use g1t_kit::now_ms; | |
| 14 | 14 | use serde_json::{Map, Value, json}; | |
| 15 | 15 | use worker::Result; | |
| 51 | 51 | Ok(names.get(id).cloned()) | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | − | /// Whether someone belongs to the workspace, so their pull requests' | |
| 55 | − | /// runs get the secrets. | |
| 56 | − | async fn insider(&self, author: &User, namespace: &str) -> Result<bool> { | |
| 57 | − | if author.id == AGENT_ID || author.is_member(&namespace.to_lowercase()) { | |
| 54 | + | /// Whether a pull request's author belongs to the workspace, so its | |
| 55 | + | /// runs get the secrets and a token. On a private repository only | |
| 56 | + | /// members can open one at all. | |
| 57 | + | async fn insider(&self, author: &User, repo: &Repo, ws: &User) -> Result<bool> { | |
| 58 | + | let slug = repo.namespace.to_lowercase(); | |
| 59 | + | if repo.is_private || author.id == AGENT_ID || author.is_member(&slug) { | |
| 58 | 60 | return Ok(true); | |
| 59 | 61 | } | |
| 60 | − | let found: Viewer = g1t_kit::call(&self.identity, "user_by_username", &UsernameArgs { username: author.username.clone() }).await?; | |
| 61 | − | Ok(found.is_some_and(|user| user.is_member(&namespace.to_lowercase()))) | |
| 62 | + | // Stored authors carry no memberships: ask the workspace. | |
| 63 | + | let members: Outcome<Vec<g1t_contracts::identity::Member>> = g1t_kit::call( | |
| 64 | + | &self.identity, | |
| 65 | + | "list_members", | |
| 66 | + | &g1t_contracts::identity::ListMembersArgs { slug, viewer: Some(ws.clone()) }, | |
| 67 | + | ) | |
| 68 | + | .await?; | |
| 69 | + | Ok(members.into_result().unwrap_or_default().iter().any(|m| m.username.eq_ignore_ascii_case(&author.username))) | |
| 62 | 70 | } | |
| 63 | 71 | ||
| 64 | 72 | async fn commits(&self, repo: &Repo, actor: &User, after: &str, before: Option<&str>) -> Result<Vec<Commit>> { | |
| 195 | 203 | "user": review.map(|r| payload::user(&r.author.username)), | |
| 196 | 204 | }); | |
| 197 | 205 | } | |
| 198 | − | let trusted = self.insider(&pull.author, &repo.namespace).await?; | |
| 206 | + | let trusted = self.insider(&pull.author, repo, ws).await?; | |
| 199 | 207 | let head_ref = payload::head_ref(pull); | |
| 200 | 208 | if event_name == "pull_request_target" { | |
| 201 | 209 | // In the base's context: its workflows, its head. |
| 124 | 124 | margin_percent: u32, | |
| 125 | 125 | /// Charged for a run on the workspace's own model provider. | |
| 126 | 126 | orchestration_fee_micros: i64, | |
| 127 | + | /// While g1t is being built out, nothing is charged (`FREE_WHILE_BUILDING`). | |
| 128 | + | free: bool, | |
| 127 | 129 | } | |
| 128 | 130 | ||
| 129 | 131 | impl Billing { | |
| 131 | 133 | Status { | |
| 132 | 134 | enabled: self.stripe.is_some(), | |
| 133 | 135 | live: self.stripe.as_ref().is_some_and(Stripe::live), | |
| 136 | + | free: self.free, | |
| 134 | 137 | } | |
| 135 | 138 | } | |
| 136 | 139 | ||
| 443 | 446 | ||
| 444 | 447 | /// A refusal if the workspace has no credit to start an agent with. | |
| 445 | 448 | async fn out_of_credit<T>(&self, workspace: &str) -> Result<Option<Outcome<T>>> { | |
| 449 | + | // While g1t is being built out, no one needs credit. | |
| 450 | + | if self.free { | |
| 451 | + | return Ok(None); | |
| 452 | + | } | |
| 446 | 453 | let balance = self | |
| 447 | 454 | .row(workspace) | |
| 448 | 455 | .await? | |
| 531 | 538 | } | |
| 532 | 539 | // On the workspace's own provider, the model was paid for there: | |
| 533 | 540 | // g1t charges its fee, and keeps the provider's cost to show. | |
| 534 | − | let charge = if run.own_provider() { | |
| 541 | + | let charge = if self.free { | |
| 542 | + | // Recorded, with what it cost, but not charged. | |
| 543 | + | 0 | |
| 544 | + | } else if run.own_provider() { | |
| 535 | 545 | self.orchestration_fee_micros | |
| 536 | 546 | } else { | |
| 537 | 547 | charge_micros(a.cost_usd, self.margin_percent) | |
| 545 | 555 | if run.own_provider() { | |
| 546 | 556 | description.push_str(", on your own model provider"); | |
| 547 | 557 | } | |
| 558 | + | if self.free { | |
| 559 | + | description.push_str(" (free while g1t is being built out)"); | |
| 560 | + | } | |
| 548 | 561 | self.enter( | |
| 549 | 562 | &run.workspace, | |
| 550 | 563 | EntryKind::Usage, | |
| 592 | 605 | .ok() | |
| 593 | 606 | .and_then(|fee| fee.to_string().parse().ok()) | |
| 594 | 607 | .unwrap_or(100_000), | |
| 608 | + | free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"), | |
| 595 | 609 | }; | |
| 596 | 610 | match method.as_str() { | |
| 597 | 611 | "status" => reply(&billing.status()), |
| 25 | 25 | // What a run on a workspace's own model provider is charged, in | |
| 26 | 26 | // millionths of a dollar: the sandbox and the orchestration around | |
| 27 | 27 | // it, with the model paid for at the provider. $0.10. | |
| 28 | − | "ORCHESTRATION_FEE_MICROS": "100000" | |
| 28 | + | "ORCHESTRATION_FEE_MICROS": "100000", | |
| 29 | + | // While g1t is being built out, workspaces pay nothing: runs are | |
| 30 | + | // recorded with what they cost, and nothing is charged. Set to | |
| 31 | + | // "false" when pricing starts. | |
| 32 | + | "FREE_WHILE_BUILDING": "true" | |
| 29 | 33 | }, | |
| 30 | 34 | // Secret: STRIPE_SECRET_KEY. Without it nothing is charged and the | |
| 31 | 35 | // runner decides who may start agents some other way. |
| 555 | 555 | repo: RepoPath; | |
| 556 | 556 | timeoutMinutes: number; | |
| 557 | 557 | }): Promise<Result<true>> { | |
| 558 | − | const status = await billingClient(this.env.BILLING).status(); | |
| 559 | − | if (!(this.previewListed(args.repo.namespace) || (status.enabled && status.live))) { | |
| 558 | + | // The same workspaces that may use g1t's sandboxes for agents. | |
| 559 | + | if (!(await this.workspaceAllowed(args.repo.namespace))) { | |
| 560 | 560 | return { | |
| 561 | 561 | ok: false, | |
| 562 | 562 | error: { | |
| 563 | 563 | code: "forbidden", | |
| 564 | − | message: "Workflows run on g1t's hosted runners, which are not open to this workspace yet.", | |
| 564 | + | message: | |
| 565 | + | "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.", | |
| 565 | 566 | }, | |
| 566 | 567 | }; | |
| 567 | 568 | } |
| 627 | 627 | .map(|s| { | |
| 628 | 628 | let run = s.target_url.as_deref().and_then(|url| url.rsplit('/').next()).unwrap_or_default(); | |
| 629 | 629 | format!( | |
| 630 | − | "- {} ({}): run `{run}`", | |
| 630 | + | "- {} ({}): run `{run}`, {}", | |
| 631 | 631 | s.context, | |
| 632 | − | s.description.as_deref().unwrap_or("failed") | |
| 632 | + | s.description.as_deref().unwrap_or("failed"), | |
| 633 | + | s.target_url.as_deref().unwrap_or_default() | |
| 633 | 634 | ) | |
| 634 | 635 | }) | |
| 635 | 636 | .collect(); | |
| 637 | + | // Named outright: the agent cannot guess it from its fork. | |
| 638 | + | let repo = g1t_kit::call::<_, Option<RepoPath>>( | |
| 639 | + | &self.repos, | |
| 640 | + | "path_by_id", | |
| 641 | + | &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() }, | |
| 642 | + | ) | |
| 643 | + | .await? | |
| 644 | + | .map(|path| format!("{}/{}", path.namespace, path.name)) | |
| 645 | + | .unwrap_or_default(); | |
| 636 | 646 | Ok(format!( | |
| 637 | − | "These GitHub Actions workflows failed on your latest commit:\n\n{}\n\n\ | |
| 638 | − | Read why with the `get_workflow_run` tool (this repository, and the run's id), \ | |
| 647 | + | "These GitHub Actions workflows failed on your latest commit to {repo}:\n\n{}\n\n\ | |
| 648 | + | Read why with the `get_workflow_run` tool (repo `{repo}` and the run's id), \ | |
| 639 | 649 | then `get_job_logs` for the job that failed. Fix the cause in the code, not the workflow, \ | |
| 640 | 650 | unless the workflow itself is wrong.", | |
| 641 | 651 | failed.join("\n") |