Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ UPDATE_SIGNING_KEY ?=
UPDATE_PUBLIC_KEY ?= desktop/update/updater-public.pem
RELEASES_DIR ?= $(CURDIR)/.task/release-assets

.PHONY: fmt vet lint lint-core lint-desktop test build source-hygiene core-boundary core-check security-check check clean desktop-bindings desktop-bindings-check desktop-taskfile-check desktop-frontend-install desktop-frontend-check desktop-build release-tools-check release-build release-build-macos release-build-linux-amd64 release-finalize release-linux-smoke verify-update-e2e desktop-check docs-install docs-dev docs-build docs-preview docs-check ci-check ci-core-check ci-desktop-check ci-security-check ci-release-build-macos ci-release-build-linux-amd64 ci-release-finalize ci-verify-update-e2e
.PHONY: fmt vet lint lint-core lint-desktop test build pricing-check source-hygiene core-boundary core-check security-check check clean desktop-bindings desktop-bindings-check desktop-taskfile-check desktop-frontend-install desktop-frontend-check desktop-build release-tools-check release-build release-build-macos release-build-linux-amd64 release-finalize release-linux-smoke verify-update-e2e desktop-check docs-install docs-dev docs-build docs-preview docs-check ci-check ci-core-check ci-desktop-check ci-security-check ci-release-build-macos ci-release-build-linux-amd64 ci-release-finalize ci-verify-update-e2e

fmt:
$(GOLANGCI_LINT) fmt $(GO_PKGS)
Expand All @@ -63,6 +63,9 @@ lint-desktop:
test:
go test $(CORE_PKGS)

pricing-check:
go test ./internal/pricing

build:
mkdir -p $(BIN_DIR)
go build -o $(BIN_DIR)/$(BINARY) $(CMD)
Expand All @@ -73,7 +76,7 @@ source-hygiene:
core-boundary:
go test ./internal/architecture

core-check: source-hygiene lint-core core-boundary test build
core-check: source-hygiene pricing-check lint-core core-boundary test build

security-check: $(GOVULNCHECK) $(ACTIONLINT)
$(GOVULNCHECK) ./...
Expand Down
13 changes: 13 additions & 0 deletions desktop/backend/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/strahe/profiledeck/internal/doctor"
"github.com/strahe/profiledeck/internal/grokbuild"
grokconfig "github.com/strahe/profiledeck/internal/grokbuild/config"
"github.com/strahe/profiledeck/internal/pricing"
"github.com/strahe/profiledeck/internal/profile"
"github.com/strahe/profiledeck/internal/profiletarget"
"github.com/strahe/profiledeck/internal/provider"
Expand Down Expand Up @@ -1112,6 +1113,18 @@ func (s *UsageService) Summary(ctx context.Context, providerID string) (usage.Us
return s.application.Usage().Summary(ctx, usage.UsageSummaryRequest{ProviderID: providerID})
}

func (s *UsageService) PricingStatus(ctx context.Context) (pricing.Status, error) {
return s.application.Pricing().Status(ctx)
}

func (s *UsageService) CheckPricing(ctx context.Context) (pricing.Status, error) {
return s.application.Pricing().Check(ctx, true)
}

func (s *UsageService) SetAutomaticPricing(ctx context.Context, enabled bool) (pricing.Status, error) {
return s.application.Pricing().SetAutomatic(ctx, enabled)
}

func (s *UsageService) AutoSyncStatus(
ctx context.Context,
providerID string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as pricing$0 from "../../internal/pricing/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as usage$0 from "../../internal/usage/models.js";
Expand All @@ -17,10 +20,22 @@ export function AutoSyncStatus(providerID: string): $CancellablePromise<$models.
return $Call.ByID(2973972616, providerID);
}

export function CheckPricing(): $CancellablePromise<pricing$0.Status> {
return $Call.ByID(2910122128);
}

export function PricingStatus(): $CancellablePromise<pricing$0.Status> {
return $Call.ByID(2879249746);
}

export function Report(providerID: string, rangeValue: string): $CancellablePromise<usage$0.UsageReportResult> {
return $Call.ByID(2463093428, providerID, rangeValue);
}

export function SetAutomaticPricing(enabled: boolean): $CancellablePromise<pricing$0.Status> {
return $Call.ByID(2883654161, enabled);
}

export function Summary(providerID: string): $CancellablePromise<usage$0.UsageSummaryResult> {
return $Call.ByID(2239958706, providerID);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

export type {
Status
} from "./models.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

export interface Status {
"automatic": boolean;
"catalog_version": number;
"last_checked_at_unix_ms": number;
"last_updated_at_unix_ms": number;
"last_error"?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export interface UsagePricingInfo {
"source_url": string;
"verified_at": string;
"historical_repricing": boolean;
"catalog_version": number;
"multiple_versions": boolean;
}

export enum UsageRangePreset {
Expand Down
2 changes: 1 addition & 1 deletion desktop/frontend/src/features/usage/UsageModelTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<Table.Cell class="font-mono text-xs font-medium">{model.model}</Table.Cell>
<Table.Cell class="text-right tabular-nums">
<div class="flex items-center justify-end gap-2">
<span>{formatCurrency(model.summary.known_estimated_cost_usd)}</span>
<span>{model.summary.estimated_cost_event_count + model.summary.partial_cost_event_count > 0 ? formatCurrency(model.summary.known_estimated_cost_usd) : "—"}</span>
{#if model.summary.cost_status !== "estimated"}<Badge variant="outline">{model.summary.pricing_coverage > 0 ? $_("usage.pricing.partial") : $_("usage.pricing.unknown")}</Badge>{/if}
</div>
</Table.Cell>
Expand Down
5 changes: 4 additions & 1 deletion desktop/frontend/src/features/usage/UsagePage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
<Card.Description>{$_("usage.apiEquivalentCost")}</Card.Description>
{#if report.summary.cost_status !== "estimated"}<Badge variant="outline">{report.summary.pricing_coverage > 0 ? $_("usage.pricing.partial") : $_("usage.pricing.unknown")}</Badge>{/if}
</div>
<Card.Title class="text-xl tabular-nums">{formatCurrency(report.summary.known_estimated_cost_usd)}</Card.Title>
<Card.Title class="text-xl tabular-nums">{report.summary.estimated_cost_event_count + report.summary.partial_cost_event_count > 0 ? formatCurrency(report.summary.known_estimated_cost_usd) : "—"}</Card.Title>
<p class="text-xs text-muted-foreground">
{#if report.summary.cost_status === "estimated"}
{$_("usage.pricing.apiEstimate")}
Expand All @@ -350,6 +350,9 @@
{$_("usage.pricing.coverage", { values: { coverage: formatPercent(report.summary.pricing_coverage) } })}
{/if}
</p>
{#if report.pricing.multiple_versions}
<p class="text-xs text-muted-foreground">{$_("usage.pricing.multipleVersions")}</p>
{/if}
</Card.Header>
</Card.Root>
{#if providerID === "grok-build"}
Expand Down
17 changes: 16 additions & 1 deletion desktop/frontend/src/features/usage/UsageTrendChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@
return Number.isFinite(value) ? Math.max(0, value) : 0;
}

function tooltipCost(point: UsageTrendPoint): string {
if (metric === "reported") {
if (point.summary.event_count > 0 && point.summary.reported_cost_event_count + point.summary.partial_reported_cost_event_count === 0) return "—";
return formatReportedCurrency(point.summary.known_reported_cost_usd);
}
if (point.summary.event_count > 0 && point.summary.estimated_cost_event_count + point.summary.partial_cost_event_count === 0) return "—";
return formatCurrency(point.summary.known_estimated_cost_usd);
}

function chartTitle(): string {
if (metric === "cost") return translate("usage.chart.costTitle");
if (metric === "reported") return translate("usage.chart.reportedCostTitle");
Expand Down Expand Up @@ -190,6 +199,9 @@

function bucketAriaLabel(point: UsageTrendPoint): string {
if (metric === "cost") {
if (point.summary.event_count > 0 && point.summary.estimated_cost_event_count + point.summary.partial_cost_event_count === 0) {
return translate("usage.chart.unknownCostBucketAria", { bucket: fullBucketLabel(point.start_unix_ms) });
}
if (point.summary.partial_cost_event_count > 0) {
return translate("usage.chart.partialCostBucketAria", {
bucket: fullBucketLabel(point.start_unix_ms),
Expand All @@ -204,6 +216,9 @@
});
}
if (metric === "reported") {
if (point.summary.event_count > 0 && point.summary.reported_cost_event_count + point.summary.partial_reported_cost_event_count === 0) {
return translate("usage.chart.unknownReportedCostBucketAria", { bucket: fullBucketLabel(point.start_unix_ms) });
}
if (point.summary.partial_reported_cost_event_count > 0) {
return translate("usage.chart.partialReportedCostBucketAria", {
bucket: fullBucketLabel(point.start_unix_ms),
Expand Down Expand Up @@ -385,7 +400,7 @@
{#if metric !== "tokens"}
<div class="mt-1 flex items-center justify-between gap-4">
<span>{metric === "reported" ? $_("usage.chart.knownReportedCost") : $_("usage.chart.knownCost")}</span>
<span class="font-mono tabular-nums">{metric === "reported" ? formatReportedCurrency(tooltipPoint.summary.known_reported_cost_usd) : formatCurrency(tooltipPoint.summary.known_estimated_cost_usd)}</span>
<span class="font-mono tabular-nums">{tooltipCost(tooltipPoint)}</span>
</div>
{#if metric === "cost" && tooltipPoint.summary.partial_cost_event_count > 0}
<div class="mt-1 text-muted-foreground">{$_("usage.chart.partialEstimate")}</div>
Expand Down
2 changes: 2 additions & 0 deletions desktop/frontend/src/features/workspace/GlobalSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import type { State as AgentState } from "../../../bindings/github.com/strahe/profiledeck/internal/agent/models";
import { currentDesktopLocale, type DesktopLanguage } from "$lib/i18n";
import AppBackupSettings from "./AppBackupSettings.svelte";
import PricingSettings from "./PricingSettings.svelte";

let {
section,
Expand Down Expand Up @@ -272,6 +273,7 @@
</Field.FieldGroup>
{/if}
</SectionCard>
<PricingSettings />
{:else}
<AppBackupSettings {automaticBackups} {databaseHealthy} {onAutomaticBackupsChange} />
{/if}
Expand Down
87 changes: 87 additions & 0 deletions desktop/frontend/src/features/workspace/PricingSettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
import { UsageService } from "../../../bindings/github.com/strahe/profiledeck/desktop/backend";
import type { Status } from "../../../bindings/github.com/strahe/profiledeck/internal/pricing/models";
import SectionCard from "$lib/components/app/SectionCard.svelte";
import SettingsRow from "$lib/components/app/SettingsRow.svelte";
import { Button } from "$lib/components/ui/button";
import * as Field from "$lib/components/ui/field";
import { Spinner } from "$lib/components/ui/spinner";
import * as Switch from "$lib/components/ui/switch";

let status = $state<Status | null>(null);
let busy = $state("");
let failed = $state(false);
let checkResult = $state<"updated" | "current" | null>(null);

async function load() {
try {
status = await UsageService.PricingStatus();
failed = false;
} catch {
failed = true;
}
}

onMount(() => {
void load();
const timer = setInterval(() => { if (!busy) void load(); }, 60_000);
return () => clearInterval(timer);
});

async function check() {
busy = "check";
const previousVersion = status?.catalog_version ?? 0;
try {
status = await UsageService.CheckPricing();
failed = false;
checkResult = status.last_error ? null : status.catalog_version > previousVersion ? "updated" : "current";
} catch {
failed = true;
checkResult = null;
} finally {
busy = "";
}
}

async function setAutomatic(enabled: boolean) {
busy = "automatic";
checkResult = null;
try {
status = await UsageService.SetAutomaticPricing(enabled);
failed = false;
} catch {
failed = true;
} finally {
busy = "";
}
}

function description(): string {
if (failed) return $_("settings.pricing.settingsUnavailable");
if (status?.last_error) return $_("settings.pricing.checkFailed");
if (checkResult) return $_(`settings.pricing.${checkResult}`);
if (!status?.last_checked_at_unix_ms) return $_("settings.pricing.neverChecked");
return $_("settings.pricing.lastChecked", { values: { time: new Date(status.last_checked_at_unix_ms).toLocaleString() } });
}
</script>

<SectionCard title={$_("settings.pricing.title")} description={$_("settings.pricing.description")}>
<Field.FieldGroup>
<SettingsRow label={$_("settings.pricing.automatic.label")} description={$_("settings.pricing.automatic.description")} forID="automatic-pricing">
{#snippet control()}
{#if busy === "automatic"}<Spinner />{/if}
<Switch.Root id="automatic-pricing" checked={status?.automatic ?? true} disabled={!status || !!busy} onCheckedChange={setAutomatic} />
{/snippet}
</SettingsRow>
<SettingsRow label={$_("settings.pricing.status")} description={description()}>
{#snippet control()}
<Button size="sm" variant="outline" disabled={!status || !!busy} onclick={check}>
{#if busy === "check"}<Spinner />{/if}
{$_("settings.pricing.check")}
</Button>
{/snippet}
</SettingsRow>
</Field.FieldGroup>
</SectionCard>
Loading
Loading