FN-7757: add missing openai-codex model pricing entries to fix empty estimated costs
Fixes the dashboard showing empty/dash estimated costs for runs on newer OpenAI Codex models, whose pricing was missing from the static model-pricing table. - Add pricing entries for gpt-5.3-codex-spark, gpt-5.4, gpt-5.4-mini, and gpt-5.5 under the openai-codex provider, sourced from the pinned pi-ai 0.80.5 model catalog. - Correct the previously-guessed gpt-5.6-luna/sol/terra rates to match their actual distinct published rates instead of all sharing the gpt-5.3-codex rate. - Update/add tests across model-pricing, team-analytics, token-analytics, and workflow-analytics to cover the new/corrected pricing entries. - Add dashboard tests (TaskSummaryTab prior attempts, CommandCenter mobile scroll, TokensArea, task-planner chat metrics) verifying costs render correctly instead of falling back to unavailable. - Add a patch changeset documenting the estimated-cost fix. Files changed: $(cat /tmp/diffstat.txt) Fusion-Task-Id: FN-7757 Fusion-Task-Lineage: e0f2e9d5-e922-4878-82d0-40c7d9899fdb Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7757-estimated-cost.md
Normal file
7
.changeset/fn-7757-estimated-cost.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix empty estimated cost on the dashboard so priced runs show a dollar amount.
|
||||
category: fix
|
||||
dev: Root-caused model-identity → pricing-key resolution; cost stays read-time derived in costFor/token-analytics.
|
||||
@@ -65,17 +65,25 @@ describe("model-pricing", () => {
|
||||
expect(result.usd).toBeCloseTo(3.25, 2);
|
||||
});
|
||||
|
||||
it("prices OpenAI Codex GPT-5.6 codenamed models instead of reporting unavailable", () => {
|
||||
// FN-7742: gpt-5.6-luna / gpt-5.6-sol / gpt-5.6-terra mirror the gpt-5.3-codex rate
|
||||
// ($1.25/1M input, $10/1M output). 1,000,000 input + 200,000 output = 1.25 + 2.00 = 3.25
|
||||
for (const model of ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]) {
|
||||
it("prices current OpenAI Codex catalog models instead of reporting unavailable", () => {
|
||||
const cases = [
|
||||
["gpt-5.3-codex-spark", 4.55],
|
||||
["gpt-5.4", 5.5],
|
||||
["gpt-5.4-mini", 1.65],
|
||||
["gpt-5.5", 11],
|
||||
["gpt-5.6-luna", 2.2],
|
||||
["gpt-5.6-sol", 11],
|
||||
["gpt-5.6-terra", 5.5],
|
||||
] as const;
|
||||
|
||||
for (const [model, expectedUsd] of cases) {
|
||||
const result = costFor(
|
||||
{ ...ZERO, inputTokens: 1_000_000, outputTokens: 200_000 },
|
||||
{ provider: "openai-codex", model },
|
||||
);
|
||||
expect(result.unavailable).toBe(false);
|
||||
expect(result.usd).not.toBeNull();
|
||||
expect(result.usd).toBeCloseTo(3.25, 2);
|
||||
expect(result.usd).toBeCloseTo(expectedUsd, 2);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -90,8 +90,8 @@ describe("team-analytics", () => {
|
||||
outputTokens: 1_000_000,
|
||||
totalTokens: 2_000_000,
|
||||
tokenUsageLastUsedAt: "2026-03-02T00:00:00.000Z",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
});
|
||||
insertTask(db, {
|
||||
id: "a-done",
|
||||
@@ -143,7 +143,7 @@ describe("team-analytics", () => {
|
||||
tasksInReview: 0,
|
||||
});
|
||||
expect(byAgent.get("agent-a")?.tokens.totalTokens).toBe(2_000_000);
|
||||
expect(byAgent.get("agent-a")?.cost).toEqual({ usd: 12.5, unavailable: false, stale: false });
|
||||
expect(byAgent.get("agent-a")?.cost).toEqual({ usd: 35, unavailable: false, stale: false });
|
||||
expect(byAgent.get("agent-b")).toMatchObject({
|
||||
agentName: "Beta",
|
||||
role: "reviewer",
|
||||
|
||||
@@ -660,6 +660,47 @@ describe("token-analytics", () => {
|
||||
expect(result.series?.[0].cost).toEqual({ usd: 12.5, unavailable: true, stale: false });
|
||||
});
|
||||
|
||||
it("prices current OpenAI Codex runtime identities across token analytics surfaces", () => {
|
||||
const usage = { inputTokens: 1_000_000, outputTokens: 200_000, cachedTokens: 500_000, cacheWriteTokens: 100_000 };
|
||||
const expected = costFor(usage, { provider: "openai-codex", model: "gpt-5.5" });
|
||||
expect(expected).toEqual({ usd: 11.25, unavailable: false, stale: false });
|
||||
|
||||
insertTask(db, {
|
||||
id: "codex-current",
|
||||
inputTokens: usage.inputTokens,
|
||||
outputTokens: usage.outputTokens,
|
||||
cachedTokens: usage.cachedTokens,
|
||||
cacheWriteTokens: usage.cacheWriteTokens,
|
||||
totalTokens: 1_800_000,
|
||||
lastUsedAt: "2026-03-01T00:00:00.000Z",
|
||||
tokenUsageModelProvider: "openai-codex",
|
||||
tokenUsageModelId: "gpt-5.5",
|
||||
tokenUsagePerModel: [
|
||||
{
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
...usage,
|
||||
totalTokens: 1_800_000,
|
||||
lastUsedAt: "2026-03-01T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o-mini",
|
||||
nodeId: "node-codex",
|
||||
agentId: "agent-codex",
|
||||
});
|
||||
|
||||
const byModel = aggregateTokenAnalytics(db, { groupBy: "model" });
|
||||
expect(byModel.cost).toEqual(expected);
|
||||
expect(byModel.groups.find((group) => group.key === "gpt-5.5")?.cost).toEqual(expected);
|
||||
|
||||
const byProvider = aggregateTokenAnalytics(db, { groupBy: "provider" });
|
||||
expect(byProvider.groups.find((group) => group.key === "openai-codex")?.cost).toEqual(expected);
|
||||
|
||||
const byDay = aggregateTokenAnalytics(db, { granularity: "day" });
|
||||
expect(byDay.series?.[0].cost).toEqual(expected);
|
||||
});
|
||||
|
||||
it("prices resolved-model token usage costs from the usage snapshot across analytics surfaces", () => {
|
||||
const usage = { inputTokens: 1_000_000, outputTokens: 1_000_000, cachedTokens: 0, cacheWriteTokens: 0 };
|
||||
const expected = costFor(usage, { provider: "openai", model: "gpt-4o" });
|
||||
|
||||
@@ -95,8 +95,8 @@ describe("workflow-analytics", () => {
|
||||
outputTokens: 1_000_000,
|
||||
totalTokens: 2_000_000,
|
||||
tokenUsageLastUsedAt: "2026-03-02T00:00:00.000Z",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
modifiedFiles: ["src/custom.ts", "src/shared.ts"],
|
||||
updatedAt: "2026-03-02T00:00:00.000Z",
|
||||
});
|
||||
@@ -149,7 +149,7 @@ describe("workflow-analytics", () => {
|
||||
tasksInReview: 0,
|
||||
});
|
||||
expect(byWorkflow.get("WF-custom")?.tokens.totalTokens).toBe(2_000_000);
|
||||
expect(byWorkflow.get("WF-custom")?.cost).toEqual({ usd: 12.5, unavailable: false, stale: false });
|
||||
expect(byWorkflow.get("WF-custom")?.cost).toEqual({ usd: 35, unavailable: false, stale: false });
|
||||
expect(byWorkflow.get("builtin:quick-fix")).toMatchObject({
|
||||
workflowName: "Quick fix",
|
||||
isBuiltin: true,
|
||||
|
||||
@@ -294,36 +294,56 @@ export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = {
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
/*
|
||||
* FNXC:CommandCenter 2026-07-09-00:00:
|
||||
* FN-7742 bumped the bundled pi SDK to 0.80.5 and inspected its generated model catalogs
|
||||
* directly (node_modules/@earendil-works/pi-ai/dist/providers/{openai,openai-codex}.models.js)
|
||||
* rather than guessing. There is no `gpt-5.6-codex` id in either catalog — OpenAI dropped the
|
||||
* separate `-codex`-suffixed tier naming starting at the 5.4 generation. GPT-5.6 instead ships
|
||||
* as three codenamed variants (`gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) that are present
|
||||
* under BOTH the `openai` and `openai-codex` provider catalogs, so they are the faithful
|
||||
* equivalent of the prior `gpt-5.x-codex` entries. Rates mirror the existing gpt-5.3-codex rate
|
||||
* (no confirmed different published OpenAI rate for these codenamed models) so Command Center
|
||||
* token cost does not show `unavailable` for pi-sourced GPT-5.6 codex runs.
|
||||
* FNXC:CommandCenter 2026-07-09-21:30:
|
||||
* FN-7757 found that pi-ai 0.80.5 now persists OpenAI Codex model ids from the generated `openai-codex.models.js` catalog (`gpt-5.3-codex-spark`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, and codenamed GPT-5.6 variants). The old static table covered only earlier `gpt-5.x-codex` ids, so lookupPricing returned unavailable and every dashboard cost surface rendered `—`. Keep this block aligned with the pinned runtime catalog rates while preserving unknown-model `—` semantics.
|
||||
*/
|
||||
"openai-codex:gpt-5.3-codex-spark": {
|
||||
inputPer1M: 1.75,
|
||||
outputPer1M: 14,
|
||||
cacheReadPer1M: 0.175,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.4": {
|
||||
inputPer1M: 2.5,
|
||||
outputPer1M: 15,
|
||||
cacheReadPer1M: 0.25,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.4-mini": {
|
||||
inputPer1M: 0.75,
|
||||
outputPer1M: 4.5,
|
||||
cacheReadPer1M: 0.075,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.5": {
|
||||
inputPer1M: 5,
|
||||
outputPer1M: 30,
|
||||
cacheReadPer1M: 0.5,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.6-luna": {
|
||||
inputPer1M: 1.25,
|
||||
outputPer1M: 10,
|
||||
cacheReadPer1M: 0.125,
|
||||
cacheWritePer1M: 1.25,
|
||||
inputPer1M: 1,
|
||||
outputPer1M: 6,
|
||||
cacheReadPer1M: 0.1,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.6-sol": {
|
||||
inputPer1M: 1.25,
|
||||
outputPer1M: 10,
|
||||
cacheReadPer1M: 0.125,
|
||||
cacheWritePer1M: 1.25,
|
||||
inputPer1M: 5,
|
||||
outputPer1M: 30,
|
||||
cacheReadPer1M: 0.5,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:gpt-5.6-terra": {
|
||||
inputPer1M: 1.25,
|
||||
outputPer1M: 10,
|
||||
cacheReadPer1M: 0.125,
|
||||
cacheWritePer1M: 1.25,
|
||||
inputPer1M: 2.5,
|
||||
outputPer1M: 15,
|
||||
cacheReadPer1M: 0.25,
|
||||
cacheWritePer1M: 0,
|
||||
source: "openai.com/api/pricing",
|
||||
},
|
||||
"openai-codex:codex-mini-latest": {
|
||||
|
||||
@@ -13,6 +13,42 @@ function doneTaskWithResults(workflowStepResults: unknown[]) {
|
||||
}
|
||||
|
||||
describe("TaskSummaryTab prior-attempts history (FN-7727)", () => {
|
||||
it("renders current OpenAI Codex token costs as dollars instead of the unavailable sentinel", () => {
|
||||
render(
|
||||
<TaskSummaryTab
|
||||
task={makeTask({
|
||||
column: "done",
|
||||
tokenUsage: {
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 200_000,
|
||||
cachedTokens: 500_000,
|
||||
cacheWriteTokens: 100_000,
|
||||
totalTokens: 1_800_000,
|
||||
firstUsedAt: "2026-07-01T10:00:00.000Z",
|
||||
lastUsedAt: "2026-07-01T10:30:00.000Z",
|
||||
perModel: [
|
||||
{
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 200_000,
|
||||
cachedTokens: 500_000,
|
||||
cacheWriteTokens: 100_000,
|
||||
totalTokens: 1_800_000,
|
||||
firstUsedAt: "2026-07-01T10:00:00.000Z",
|
||||
lastUsedAt: "2026-07-01T10:30:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const section = screen.getByTestId("task-summary-token-cost-section");
|
||||
expect(section).toHaveTextContent("$11.25");
|
||||
expect(section).not.toHaveTextContent("—");
|
||||
});
|
||||
|
||||
it("renders a collapsed prior-attempts affordance for a step with history", () => {
|
||||
render(
|
||||
<TaskSummaryTab
|
||||
|
||||
@@ -87,7 +87,7 @@ function populatedTokenFixture() {
|
||||
cost: { usd: 9, unavailable: false, stale: false },
|
||||
groups: [
|
||||
{
|
||||
key: "gpt-4o",
|
||||
key: "gpt-5.5",
|
||||
inputTokens: 600,
|
||||
outputTokens: 300,
|
||||
cachedTokens: 100,
|
||||
@@ -413,6 +413,8 @@ describe("CommandCenter mobile scroll regression (FN-6595)", () => {
|
||||
render(<CommandCenter />);
|
||||
|
||||
await screen.findByTestId("command-center-overview-charts");
|
||||
expect(screen.getByTestId("command-center-stat-tokens")).toHaveTextContent("$9.00");
|
||||
expect(screen.getByTestId("command-center-stat-tokens")).not.toHaveTextContent("—");
|
||||
expect(screen.getByTestId("command-center-overview-chart-tokens")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-overview-pie")).toBeTruthy();
|
||||
expect(screen.getByTestId("cc-overview-line")).toBeTruthy();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { costFor } from "@fusion/core";
|
||||
import { TokensArea } from "../TokensArea";
|
||||
import type { DateRange } from "../../DateRangePicker";
|
||||
|
||||
@@ -179,6 +180,28 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("TokensArea provider model icons", () => {
|
||||
it("renders current OpenAI Codex priced costs as dollars instead of the unavailable sentinel", async () => {
|
||||
const usage = { inputTokens: 1_000_000, outputTokens: 200_000, cachedTokens: 500_000, cacheWriteTokens: 100_000 };
|
||||
const cost = costFor(usage, { provider: "openai-codex", model: "gpt-5.5" });
|
||||
expect(cost).toEqual({ usd: 11.25, unavailable: false, stale: false });
|
||||
|
||||
apiMock.mockResolvedValue({
|
||||
from: "2026-06-08",
|
||||
to: null,
|
||||
groupBy: "model",
|
||||
totals: { ...usage, totalTokens: 1_800_000, nTasks: 1 },
|
||||
cost,
|
||||
series: [{ bucket: "2026-06-18", ...usage, totalTokens: 1_800_000, nTasks: 1, cost }],
|
||||
groups: [{ key: "gpt-5.5", ...usage, totalTokens: 1_800_000, nTasks: 1, cost }],
|
||||
});
|
||||
render(<TokensArea range={range7d} />);
|
||||
|
||||
await screen.findByTestId("cc-area-tokens");
|
||||
expect(screen.getByTestId("cc-tokens-cost")).toHaveTextContent("$11.25");
|
||||
expect(screen.getByTestId("cc-tokens-cost")).not.toHaveTextContent("—");
|
||||
expect(screen.getByTestId("cc-tokens-row-gpt-5.5")).toHaveTextContent("$11.25");
|
||||
});
|
||||
|
||||
it("renders inferred provider icons in the per-model table and Tokens by model bars", async () => {
|
||||
render(<TokensArea range={range7d} />);
|
||||
|
||||
|
||||
@@ -150,6 +150,27 @@ describe("formatTaskPlannerChatMetrics", () => {
|
||||
expect(result.summaryText).toContain("estimated cost $0.0085");
|
||||
});
|
||||
|
||||
it("prices current OpenAI Codex runtime identities in task-planner chat metrics", () => {
|
||||
const result = formatTaskPlannerChatMetrics(makeTask({
|
||||
tokenUsage: {
|
||||
inputTokens: 1_000_000,
|
||||
outputTokens: 200_000,
|
||||
cachedTokens: 500_000,
|
||||
cacheWriteTokens: 100_000,
|
||||
totalTokens: 1_800_000,
|
||||
firstUsedAt: "2026-07-01T10:00:00.000Z",
|
||||
lastUsedAt: "2026-07-01T10:30:00.000Z",
|
||||
modelProvider: "openai-codex",
|
||||
modelId: "gpt-5.5",
|
||||
},
|
||||
}), { nowMs: Date.parse("2026-07-01T10:31:00.000Z") });
|
||||
|
||||
expect(result.metrics.tokens.cost).toEqual({ usd: 11.25, costUnavailable: false, pricingStale: false });
|
||||
expect(result.metrics.tokens.perModel[0].cost).toEqual({ usd: 11.25, costUnavailable: false, pricingStale: false });
|
||||
expect(result.summaryText).toContain("estimated cost $11.2500");
|
||||
expect(result.summaryText).not.toContain("cost unavailable");
|
||||
});
|
||||
|
||||
it("marks unpriced and stale model costs unavailable instead of reporting zero", () => {
|
||||
const result = formatTaskPlannerChatMetrics(makeTask({
|
||||
tokenUsage: {
|
||||
|
||||
Reference in New Issue
Block a user