Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ As usage grows, the platform needs stronger derived data pipelines, performance
- [ ] [P1] Break-even analysis for API vs seat-based pricing with per-engineer recommendations
- [x] [P1] Cost per workflow archetype and cost per engineering outcome
- [x] [P1] Workflow compare mode for archetype and fingerprint performance
- [ ] [P1] Model-choice opportunity scoring for overspend reduction
- [x] [P1] Model-choice opportunity scoring for overspend reduction
- [ ] [P2] Budget policy simulation by team, project, and billing model

## AI Synthesis & Explorer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { render, screen } from "@testing-library/react"

import { ModelChoiceOpportunityTable } from "@/components/finops/model-choice-opportunity-table"

describe("ModelChoiceOpportunityTable", () => {
it("renders an empty state when no opportunities exist", () => {
render(<ModelChoiceOpportunityTable opportunities={[]} totalSavingsMonthly={0} />)

expect(
screen.getByText("No clear model downshift opportunities found for this period yet."),
).toBeInTheDocument()
})

it("renders scored opportunities", () => {
render(
<ModelChoiceOpportunityTable
totalSavingsMonthly={14.2}
opportunities={[
{
workflow_archetype: "debugging",
current_model: "claude-opus-4-20250514",
recommended_model: "claude-sonnet-4-5-20250929",
current_session_count: 8,
supporting_session_count: 12,
current_success_rate: 0.82,
recommended_success_rate: 0.84,
current_avg_cost: 4.8,
recommended_avg_cost: 1.1,
period_savings_estimate: 22.4,
monthly_savings_estimate: 14.2,
confidence: "high",
rationale: "Peers get similar results with Sonnet for debugging work.",
},
]}
/>,
)

expect(screen.getByText("Model choice opportunities")).toBeInTheDocument()
expect(
screen.getByText("claude-opus-4-20250514 -> claude-sonnet-4-5-20250929"),
).toBeInTheDocument()
expect(screen.getByText("Debugging")).toBeInTheDocument()
expect(screen.getByText("High confidence")).toBeInTheDocument()
expect(screen.getAllByText("$14.20")).toHaveLength(2)
})
})
125 changes: 125 additions & 0 deletions frontend/src/components/finops/model-choice-opportunity-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { formatCost, formatLabel, formatPercent } from "@/lib/utils"
import type { ModelChoiceOpportunity } from "@/types/api"

interface ModelChoiceOpportunityTableProps {
opportunities: ModelChoiceOpportunity[]
totalSavingsMonthly: number
}

const confidenceVariant: Record<string, "default" | "secondary" | "outline"> = {
high: "default",
medium: "secondary",
low: "outline",
}

export function ModelChoiceOpportunityTable({
opportunities,
totalSavingsMonthly,
}: ModelChoiceOpportunityTableProps) {
return (
<Card>
<CardHeader className="pb-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<CardTitle className="text-sm font-medium">Model choice opportunities</CardTitle>
<p className="mt-1 text-sm text-muted-foreground">
Places where a cheaper model appears to preserve outcomes for the same workflow.
</p>
</div>
<div className="rounded-xl border border-border/70 bg-muted/40 px-3 py-2 text-right">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Potential Monthly Savings
</p>
<p className="mt-1 font-display text-2xl tracking-tight">
{formatCost(totalSavingsMonthly)}
</p>
</div>
</div>
</CardHeader>
<CardContent>
{opportunities.length === 0 ? (
<p className="text-sm text-muted-foreground">
No clear model downshift opportunities found for this period yet.
</p>
) : (
<div className="space-y-3">
{opportunities.map((opportunity) => (
<div
key={`${opportunity.workflow_archetype}:${opportunity.current_model}:${opportunity.recommended_model}`}
className="rounded-2xl border border-border/70 bg-card/70 p-4"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">
{formatLabel(opportunity.workflow_archetype)}
</Badge>
<Badge
variant={confidenceVariant[opportunity.confidence] ?? "outline"}
>
{formatLabel(opportunity.confidence)} confidence
</Badge>
</div>
<p className="text-base font-semibold">
{opportunity.current_model} {"->"} {opportunity.recommended_model}
</p>
<p className="text-sm text-muted-foreground">{opportunity.rationale}</p>
</div>
<div className="text-right">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
Monthly Savings
</p>
<p className="mt-1 text-xl font-semibold">
{formatCost(opportunity.monthly_savings_estimate)}
</p>
</div>
</div>

<div className="mt-4 grid gap-3 sm:grid-cols-4">
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Success
</p>
<p className="mt-1 text-sm">
{formatPercent(opportunity.current_success_rate)}
{" -> "}
{formatPercent(opportunity.recommended_success_rate)}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Avg Cost
</p>
<p className="mt-1 text-sm">
{opportunity.current_avg_cost != null
? formatCost(opportunity.current_avg_cost)
: "-"}
{" -> "}
{opportunity.recommended_avg_cost != null
? formatCost(opportunity.recommended_avg_cost)
: "-"}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Current Sessions
</p>
<p className="mt-1 text-sm">{opportunity.current_session_count}</p>
</div>
<div>
<p className="text-xs uppercase tracking-wide text-muted-foreground">
Benchmark Sessions
</p>
<p className="mt-1 text-sm">{opportunity.supporting_session_count}</p>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
)
}
6 changes: 6 additions & 0 deletions frontend/src/components/finops/modeling-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ChartTooltip } from "@/components/charts/chart-tooltip"
import { CHART_COLORS, AXIS_TICK_STYLE } from "@/lib/chart-colors"
import { useCostModeling } from "@/hooks/use-api-queries"
import { CardSkeleton, ChartSkeleton, TableSkeleton } from "@/components/shared/loading-skeleton"
import { ModelChoiceOpportunityTable } from "@/components/finops/model-choice-opportunity-table"
import { formatCost, cn } from "@/lib/utils"

const TIER_COLORS: Record<string, string> = {
Expand Down Expand Up @@ -136,6 +137,11 @@ export function ModelingTab({ teamId, startDate, endDate }: ModelingTabProps) {
</div>
)}

<ModelChoiceOpportunityTable
opportunities={data.model_choice_opportunities}
totalSavingsMonthly={data.total_model_choice_savings_monthly}
/>

{/* Efficient Frontier Chart */}
{chartData.length > 0 && (
<Card>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,22 @@ export interface PlanAllocationSummary {
total_monthly_cost: number
}

export interface ModelChoiceOpportunity {
workflow_archetype: string
current_model: string
recommended_model: string
current_session_count: number
supporting_session_count: number
current_success_rate: number | null
recommended_success_rate: number | null
current_avg_cost: number | null
recommended_avg_cost: number | null
period_savings_estimate: number
monthly_savings_estimate: number
confidence: string
rationale: string
}

export interface CostModelingResponse {
period_days: number
plan_tiers: PlanTier[]
Expand All @@ -1916,6 +1932,8 @@ export interface CostModelingResponse {
total_api_cost_monthly: number
total_optimal_cost_monthly: number
total_savings_monthly: number
model_choice_opportunities: ModelChoiceOpportunity[]
total_model_choice_savings_monthly: number
}

export interface ForecastPoint {
Expand Down
18 changes: 18 additions & 0 deletions src/primer/common/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2303,6 +2303,22 @@ class PlanAllocationSummary(BaseModel):
total_monthly_cost: float


class ModelChoiceOpportunity(BaseModel):
workflow_archetype: str
current_model: str
recommended_model: str
current_session_count: int
supporting_session_count: int
current_success_rate: float | None = None
recommended_success_rate: float | None = None
current_avg_cost: float | None = None
recommended_avg_cost: float | None = None
period_savings_estimate: float
monthly_savings_estimate: float
confidence: str
rationale: str


class CostModelingResponse(BaseModel):
period_days: int
plan_tiers: list[PlanTier]
Expand All @@ -2311,6 +2327,8 @@ class CostModelingResponse(BaseModel):
total_api_cost_monthly: float
total_optimal_cost_monthly: float
total_savings_monthly: float
model_choice_opportunities: list[ModelChoiceOpportunity] = []
total_model_choice_savings_monthly: float = 0.0


# --- FinOps: Forecasting ---
Expand Down
Loading
Loading