Skip to content

Commit 9825ef1

Browse files
committed
Hide coding-plan costs from live provider identity not launch URL
/model updates providerName without rewriting baseURL, so a URL-only coding-plan hide was stale after switches onto or off Z.AI. Live zai identity hides; a present non-zai identity shows; URL match remains the no-name fallback. Also pin the Codex-to-metered prompt $ refresh.
1 parent 4e9df5e commit 9825ef1

5 files changed

Lines changed: 126 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1717

1818
- Codex ChatGPT subscription sessions no longer show a public-rate dollar
1919
cost estimate. Hide follows the live provider identity after `/model`
20-
switches, not the launch base URL. Context usage and `/cost` still work;
21-
`/cost` reports the cost as covered by ChatGPT subscription. Metered
22-
OpenAI API endpoints keep dollar estimates.
20+
switches, not the launch base URL. Coding-plan (Z.AI) hide uses the same
21+
live-identity rule. Context usage and `/cost` still work; `/cost`
22+
reports Codex cost as covered by ChatGPT subscription. Metered OpenAI
23+
API endpoints keep dollar estimates.
2324

2425
## [0.3.11] - 2026-08-31
2526

src/cost/cost-summary.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ describe("buildCostSummary", () => {
5454
expect(summary.costHiddenReason).toBe("coding-plan");
5555
});
5656

57+
it("hides cost for a live coding-plan identity even when baseURL is still metered", () => {
58+
const summary = buildCostSummary({
59+
...baseInput,
60+
providerName: "zai",
61+
baseURL: "https://api.openai.com/v1",
62+
});
63+
expect(summary.costHiddenReason).toBe("coding-plan");
64+
});
65+
66+
it("shows cost for a live metered identity even when baseURL is still a coding-plan endpoint", () => {
67+
const summary = buildCostSummary({
68+
...baseInput,
69+
modelId: "glm-5.1",
70+
providerName: "openai",
71+
baseURL: "https://api.z.ai/api/coding/paas/v4",
72+
});
73+
expect(summary.costHiddenReason).toBeNull();
74+
});
75+
5776
it("hides cost for a Codex ChatGPT subscription identity", () => {
5877
const summary = buildCostSummary({
5978
...baseInput,

src/cost/cost-visibility.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
costHiddenReason,
66
isChatGPTSubscriptionBaseURL,
77
isCodingPlanBaseURL,
8+
isCodingPlanProviderName,
89
isFreeModelId,
910
} from "./cost-visibility.js";
1011
import type { PricingCache } from "./pricing-fetcher.js";
@@ -66,6 +67,14 @@ describe("isCodingPlanBaseURL", () => {
6667
});
6768
});
6869

70+
describe("isCodingPlanProviderName", () => {
71+
it("matches the first-class Z.AI Coding Plan catalog id", () => {
72+
expect(isCodingPlanProviderName("zai")).toBe(true);
73+
expect(isCodingPlanProviderName("openai")).toBe(false);
74+
expect(isCodingPlanProviderName("codex/default")).toBe(false);
75+
});
76+
});
77+
6978
describe("isChatGPTSubscriptionBaseURL", () => {
7079
it("detects the Codex ChatGPT subscription inference base URL", () => {
7180
expect(isChatGPTSubscriptionBaseURL(CODEX_BASE_URL)).toBe(true);
@@ -124,6 +133,28 @@ describe("costHiddenReason", () => {
124133
).toBe("coding-plan");
125134
});
126135

136+
it("hides on live coding-plan provider identity even when baseURL is still the metered API", () => {
137+
expect(
138+
costHiddenReason({
139+
modelId: "glm-5.1",
140+
providerName: "zai",
141+
baseURL: "https://api.openai.com/v1",
142+
pricingCache,
143+
}),
144+
).toBe("coding-plan");
145+
});
146+
147+
it("shows cost on live non-coding-plan identity even when baseURL is still a coding-plan endpoint", () => {
148+
expect(
149+
costHiddenReason({
150+
modelId: "glm-5.1",
151+
providerName: "openai",
152+
baseURL: "https://api.z.ai/api/coding/paas/v4",
153+
pricingCache,
154+
}),
155+
).toBeNull();
156+
});
157+
127158
it("hides for a Codex ChatGPT subscription base URL even when the model has public rates", () => {
128159
expect(
129160
costHiddenReason({

src/cost/cost-visibility.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export function isCodingPlanBaseURL(baseURL: string | undefined): boolean {
2626
}
2727
}
2828

29+
// First-class Z.AI Coding Plan catalog id. Live /model identity uses this
30+
// name, not the launch baseURL, so a switch onto or off zai updates $ now.
31+
export function isCodingPlanProviderName(name: string): boolean {
32+
return name === "zai";
33+
}
34+
2935
// Codex OAuth bills against the user's ChatGPT subscription via
3036
// chatgpt.com/backend-api. Public per-token rates for the same model ids do
3137
// not apply there, so dollar estimates must be suppressed. Matched against
@@ -60,8 +66,10 @@ export function isFreeModelByPricing(cache: PricingCache | null, modelId: string
6066
export interface CostVisibilityInput {
6167
baseURL?: string | undefined;
6268
// Live /model identity. When set, it wins over a stale launch baseURL for
63-
// the ChatGPT-subscription hide: Codex names hide even on api.openai.com,
64-
// non-Codex names show even on CODEX_BASE_URL. Undefined falls back to URL.
69+
// ChatGPT-subscription and coding-plan hides: Codex names hide even on
70+
// api.openai.com, zai names hide even on a metered URL; a present
71+
// non-matching name shows even when launch URL would hide. Undefined
72+
// falls back to URL.
6573
providerName?: string | undefined;
6674
modelId: string;
6775
providerFree?: boolean | undefined;
@@ -71,6 +79,13 @@ export interface CostVisibilityInput {
7179
export type CostHiddenReason =
7280
"provider-free" | "coding-plan" | "chatgpt-subscription" | "free-model" | "zero-priced";
7381

82+
function isCodingPlanSession(input: CostVisibilityInput): boolean {
83+
if (input.providerName !== undefined) {
84+
return isCodingPlanProviderName(input.providerName);
85+
}
86+
return isCodingPlanBaseURL(input.baseURL);
87+
}
88+
7489
function isChatGPTSubscriptionSession(input: CostVisibilityInput): boolean {
7590
if (input.providerName !== undefined) {
7691
return isCodexProviderName(input.providerName);
@@ -79,13 +94,14 @@ function isChatGPTSubscriptionSession(input: CostVisibilityInput): boolean {
7994
}
8095

8196
// Non-null when the dollar cost should be suppressed: a manual provider
82-
// override, a coding-plan endpoint, a ChatGPT/Codex subscription (live
83-
// provider identity, else Codex URL), a free-named model, or a model the
84-
// pricing registry reports as zero-cost. The reason is carried to the
85-
// display so /cost can say which condition hid the figure.
97+
// override, a coding-plan session (live zai identity, else /coding URL), a
98+
// ChatGPT/Codex subscription (live provider identity, else Codex URL), a
99+
// free-named model, or a model the pricing registry reports as zero-cost.
100+
// The reason is carried to the display so /cost can say which condition hid
101+
// the figure.
86102
export function costHiddenReason(input: CostVisibilityInput): CostHiddenReason | null {
87103
if (input.providerFree === true) return "provider-free";
88-
if (isCodingPlanBaseURL(input.baseURL)) return "coding-plan";
104+
if (isCodingPlanSession(input)) return "coding-plan";
89105
if (isChatGPTSubscriptionSession(input)) return "chatgpt-subscription";
90106
if (isFreeModelId(input.modelId)) return "free-model";
91107
return isFreeModelByPricing(input.pricingCache, input.modelId) ? "zero-priced" : null;

src/tui/runner-host.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,55 @@ describe("bottom border cost run", () => {
497497
}
498498
});
499499

500+
test("selecting a metered model from Codex shows prompt $ without waiting for inference", async () => {
501+
const harness = await createHarness({ width: 80, height: 24 });
502+
let provider = "codex/abk-labs";
503+
const host = await mountRunnerHost({
504+
title: "test",
505+
eventEmitter: new EventEmitter(),
506+
send: () => {},
507+
interrupt: () => {},
508+
providers: {
509+
"codex/abk-labs": { models: ["gpt-5.5"] },
510+
xai: { models: ["grok-4"] },
511+
},
512+
onModelSelect: (id) => {
513+
const sep = id.indexOf(":");
514+
if (sep <= 0) return;
515+
provider = id.slice(0, sep);
516+
},
517+
commands: [],
518+
onCommand: () => {},
519+
chrome: () => ({ agents: [] }),
520+
subscribeChrome: () => () => {},
521+
subAgentSessions: () => [],
522+
createRenderer: async () => harness.renderer,
523+
readCostSummary: () => ({
524+
...fakeCostSummary(),
525+
costHiddenReason: provider.startsWith("codex/") ? "chatgpt-subscription" : null,
526+
}),
527+
showPromptCost: () => true,
528+
});
529+
try {
530+
expect(ruleOf(host.shell.promptBottomRule)).not.toContain("$0.42");
531+
532+
expect(host.openSurface("models")).toBe(true);
533+
const items = host.shell.overlayItems;
534+
const meteredIndex = items.findIndex((label) => label.includes("[xai]"));
535+
expect(meteredIndex).toBeGreaterThanOrEqual(0);
536+
moveOverlaySelection(host.shell, meteredIndex);
537+
acceptOverlaySelection(host.shell);
538+
539+
expect(provider).toBe("xai");
540+
expect(ruleOf(host.shell.promptBottomRule)).toContain("$0.42");
541+
expect(host.shell.costContext?.costLabel ?? null).toBe("$0.42");
542+
expect(ruleOf(host.shell.promptBottomRule)).toContain("10%");
543+
} finally {
544+
host.dispose();
545+
harness.destroy();
546+
}
547+
});
548+
500549
test("session.clear paints the context meter unknown immediately", async () => {
501550
const harness = await createHarness({ width: 80, height: 24 });
502551
const emitter = new EventEmitter();

0 commit comments

Comments
 (0)