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
3 files+78−60/3 viewed
| 688 | 688 | and shaded by how many were an agent's (**Agent use**, less to more). Choose a box to | |
| 689 | 689 | open what is under it. Work with no milestone or work item has its own | |
| 690 | 690 | 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. | |
| 691 | 695 | 3. **What's moving and what's waiting**: each team's last two weeks, day | |
| 692 | 696 | by day, by the projects the team works on (each project, when the | |
| 693 | 697 | workspace has no teams). Above the line are pull requests opened and |
| 12 | 12 | * Imports only types. | |
| 13 | 13 | */ | |
| 14 | 14 | import type { | |
| 15 | + | ProviderCosts, | |
| 15 | 16 | RepoPath, | |
| 16 | 17 | SignalAuthor, | |
| 17 | 18 | SignalDay, | |
| ⋯ | |||
| 1050 | 1051 | .slice(0, max) | |
| 1051 | 1052 | .map((pull) => ({ ...pull, repo: repos[pull.repoId]!, to: pullPath(repos[pull.repoId]!, pull.number) })); | |
| 1052 | 1053 | } | |
| 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 | + | } | |
| 2 | 2 | import { type ReactNode, Suspense } from "react"; | |
| 3 | 3 | import { Await, Link, data } from "react-router"; | |
| 4 | 4 | ||
| 5 | − | import { type User, hasCodeAccess } from "@g1t/contracts"; | |
| 5 | + | import { COST_PROVIDERS, type CostProvider, type User, hasCodeAccess, isCostProvider } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/workspace-overview"; | |
| 8 | 8 | ||
| 9 | 9 | import { UsageCard } from "../../components/usage-card"; | |
| 10 | − | import { identity } from "../../lib/services.server"; | |
| 10 | + | import { billing, identity } from "../../lib/services.server"; | |
| 11 | 11 | import { loadOnboarding } from "../../lib/onboarding.server"; | |
| 12 | 12 | import { OnboardingOnPage } from "../../components/onboarding"; | |
| 13 | 13 | import { getViewer, roleIn } from "../../lib/session.server"; | |
| ⋯ | |||
| 24 | 24 | import { SpendFlow } from "../../components/spend-flow"; | |
| 25 | 25 | import { isFailed, part } from "../../lib/partial"; | |
| 26 | 26 | 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"; | |
| 28 | 28 | import { type SignalData, loadGroups, loadSignalData } from "../../lib/signals.server"; | |
| 29 | 29 | ||
| 30 | 30 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| ⋯ | |||
| 76 | 76 | async function loadGround(viewer: User, slug: string, now: number) { | |
| 77 | 77 | const owned = await loadRepos(viewer, slug).catch(() => null); | |
| 78 | 78 | 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; | |
| 81 | 89 | } | |
| 82 | 90 | ||
| 83 | 91 | function shapeGround(found: SignalData, groups: Awaited<ReturnType<typeof loadGroups>>, now: number) { | |
| ⋯ | |||
| 122 | 130 | }; | |
| 123 | 131 | } | |
| 124 | 132 | ||
| 125 | − | type Ground = ReturnType<typeof shapeGround>; | |
| 133 | + | type Ground = ReturnType<typeof shapeGround> & { providers: ReturnType<typeof providerQuarter> }; | |
| 126 | 134 | ||
| 127 | 135 | function Tile({ icon, label, value, to, action }: { icon: ReactNode; label: string; value: string; to: string; action: string }) { | |
| 128 | 136 | return ( | |
| ⋯ | |||
| 302 | 310 | : "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." | |
| 303 | 311 | } | |
| 304 | 312 | > | |
| 313 | + | <ProvidersLine slug={slug} providers={ground.providers} threaded={ground.quarterSpend} /> | |
| 305 | 314 | <Boundary what="Spend, threaded to the work"> | |
| 306 | 315 | <SpendFlow flow={ground.flow} sized={ground.sized} /> | |
| 307 | 316 | </Boundary> | |
| ⋯ | |||
| 361 | 370 | </div> | |
| 362 | 371 | ); | |
| 363 | 372 | } | |
| 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'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'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.