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:
gsxdsm
2026-07-09 21:40:58 -07:00
parent f7c6f560c0
commit b4b183fcd0
10 changed files with 192 additions and 34 deletions

View 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.

View File

@@ -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);
}
});

View File

@@ -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",

View File

@@ -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" });

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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

View File

@@ -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();

View File

@@ -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} />);

View File

@@ -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: {