Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

The workspace overview says what providers reported beside what g1t threads to the work

With a provider's cost report connected, the spend section shows what Anthropic or OpenAI reported for the quarter and how much of it is threaded to the work; without one, it asks an owner to connect one. The provider's figure holds everything the organization ran, so it is shown beside g1t's, never added to it. Part of #157

G1t-Session:
ses_01m57128wre9bvvj1y4z4b8dk0
syntaqxcommitted Parent001fb6cBrowse files
3 files+78−60/3 viewed
+4−0
688688 and shaded by how many were an agent's (**Agent use**, less to more). Choose a box to
689689 open what is under it. Work with no milestone or work item has its own
690690 box. When usage can't be read, boxes are sized by pull requests.
691+ Above it, with a [provider's cost report](/guides/usage-and-billing/)
692+ connected, is what Anthropic or OpenAI reported for the quarter and how
693+ much of it is threaded to the work. Their figure holds everything the
694+ organization ran, so it is never added to g1t's.
691695 3. **What's moving and what's waiting**: each team's last two weeks, day
692696 by day, by the projects the team works on (each project, when the
693697 workspace has no teams). Above the line are pull requests opened and
+24−0
1212 * Imports only types.
1313 */
1414 import type {
15+ ProviderCosts,
1516 RepoPath,
1617 SignalAuthor,
1718 SignalDay,
10501051 .slice(0, max)
10511052 .map((pull) => ({ ...pull, repo: repos[pull.repoId]!, to: pullPath(repos[pull.repoId]!, pull.number) }));
10521053 }
1054+
1055+// --- What providers reported ------------------------------------------------------
1056+
1057+export type ProviderQuarter = {
1058+ /** Providers connected, with what each reported since the quarter began, the most first. */
1059+ providers: { provider: string; micros: number }[];
1060+ total: number;
1061+ /** Every connection's last read worked and reaches back to the quarter. */
1062+ complete: boolean;
1063+};
1064+
1065+/**
1066+ * What the connected providers' cost reports say the organization spent
1067+ * since `quarter` began: their whole bill, agents' work g1t sees and
1068+ * everything else. Null when none is connected.
1069+ */
1070+export function providerQuarter(costs: ProviderCosts | null, quarter: string): ProviderQuarter | null {
1071+ if (!costs || costs.connections.length === 0) return null;
1072+ const by = new Map<string, number>(costs.connections.map((connection) => [connection.provider, 0]));
1073+ for (const day of costs.days) if (day.day >= quarter) by.set(day.provider, (by.get(day.provider) ?? 0) + day.micros);
1074+ const providers = [...by].map(([provider, micros]) => ({ provider, micros })).sort((a, b) => b.micros - a.micros);
1075+ return { providers, total: providers.reduce((sum, row) => sum + row.micros, 0), complete: costs.complete };
1076+}
+50−6
22 import { type ReactNode, Suspense } from "react";
33 import { Await, Link, data } from "react-router";
44
5−import { type User, hasCodeAccess } from "@g1t/contracts";
5+import { COST_PROVIDERS, type CostProvider, type User, hasCodeAccess, isCostProvider } from "@g1t/contracts";
66
77 import type { Route } from "./+types/workspace-overview";
88
99 import { UsageCard } from "../../components/usage-card";
10−import { identity } from "../../lib/services.server";
10+import { billing, identity } from "../../lib/services.server";
1111 import { loadOnboarding } from "../../lib/onboarding.server";
1212 import { OnboardingOnPage } from "../../components/onboarding";
1313 import { getViewer, roleIn } from "../../lib/session.server";
2424 import { SpendFlow } from "../../components/spend-flow";
2525 import { isFailed, part } from "../../lib/partial";
2626 import { loadRepos } from "../../lib/home.server";
27−import { dailySignal, dayList, dollars, harnessWords, namesWords, newGround, newHabits, quarterSpend, rhythm, silos, spendFlow, spoken, spreading, waitingRows } from "../../lib/signals";
27+import { dailySignal, dayList, dollars, harnessWords, namesWords, newGround, newHabits, providerQuarter, quarterSpend, signalsRange, rhythm, silos, spendFlow, spoken, spreading, waitingRows } from "../../lib/signals";
2828 import { type SignalData, loadGroups, loadSignalData } from "../../lib/signals.server";
2929
3030 export function meta({ params, ...args }: Route.MetaArgs) {
7676 async function loadGround(viewer: User, slug: string, now: number) {
7777 const owned = await loadRepos(viewer, slug).catch(() => null);
7878 if (!owned) return null;
79− const [found, groups] = await Promise.all([loadSignalData(viewer, slug, now, owned).catch(() => null), loadGroups(viewer, slug, owned).catch(() => null)]);
80− return found ? shapeGround(found, groups, now) : null;
79+ const [found, groups, providers] = await Promise.all([
80+ loadSignalData(viewer, slug, now, owned).catch(() => null),
81+ loadGroups(viewer, slug, owned).catch(() => null),
82+ // What the connected providers say was spent: beside the rest, never in the way of it.
83+ billing
84+ .providerCosts(slug, viewer, signalsRange(now).quarter)
85+ .then((result) => (result.ok ? result.value : null))
86+ .catch(() => null),
87+ ]);
88+ return found ? { ...shapeGround(found, groups, now), providers: providerQuarter(providers, found.range.quarter) } : null;
8189 }
8290
8391 function shapeGround(found: SignalData, groups: Awaited<ReturnType<typeof loadGroups>>, now: number) {
122130 };
123131 }
124132
125−type Ground = ReturnType<typeof shapeGround>;
133+type Ground = ReturnType<typeof shapeGround> & { providers: ReturnType<typeof providerQuarter> };
126134
127135 function Tile({ icon, label, value, to, action }: { icon: ReactNode; label: string; value: string; to: string; action: string }) {
128136 return (
302310 : "Usage isn't known for this quarter, so each box is sized by its pull requests and shaded by how many were an agent's. Choose a box to open what is under it."
303311 }
304312 >
313+ <ProvidersLine slug={slug} providers={ground.providers} threaded={ground.quarterSpend} />
305314 <Boundary what="Spend, threaded to the work">
306315 <SpendFlow flow={ground.flow} sized={ground.sized} />
307316 </Boundary>
361370 </div>
362371 );
363372 }
373+
374+const providerName = (provider: string) => (isCostProvider(provider) ? COST_PROVIDERS[provider as CostProvider].label : provider);
375+
376+/**
377+ * What the connected providers reported for the quarter, beside what g1t
378+ * threads to the work: their bill holds everything the organization ran,
379+ * so it is the larger figure, never added to the other.
380+ */
381+function ProvidersLine({ slug, providers, threaded }: { slug: string; providers: Ground["providers"]; threaded: number | null }) {
382+ if (!providers) {
383+ return (
384+ <p className="-mt-2 mb-5 text-sm text-muted">
385+ <Link to={`/${slug}/-/provider-costs`} className="text-accent hover:underline">
386+ Connect a provider&apos;s cost report
387+ </Link>{" "}
388+ to see everything your organization spent with Anthropic or OpenAI, not only what g1t can tie to the work.
389+ </p>
390+ );
391+ }
392+ return (
393+ <div className="-mt-2 mb-5 flex flex-wrap items-baseline gap-x-5 gap-y-1 rounded-xl border border-line bg-bg px-4 py-3 text-sm">
394+ <span className="text-fg">
395+ <span className="font-semibold">{dollars(providers.total)}</span> reported by your providers this quarter
396+ </span>
397+ <span className="font-mono text-xs text-muted">{providers.providers.map((row) => `${providerName(row.provider)} ${dollars(row.micros)}`).join(" · ")}</span>
398+ {threaded != null && threaded > 0 && providers.total > 0 && (
399+ <span className="text-xs text-muted">{Math.min(100, Math.round((threaded / providers.total) * 100))}% of it is threaded to the work below</span>
400+ )}
401+ {!providers.complete && <span className="text-xs text-faint">Some of the quarter isn&apos;t read yet</span>}
402+ <Link to={`/${slug}/-/provider-costs`} className="ml-auto text-xs text-muted hover:text-fg">
403+ Provider costs
404+ </Link>
405+ </div>
406+ );
407+}

Comments

Sign in to comment.