Skip to content

Commit 341c312

Browse files
committed
Parse Codex priority pricing from OpenAI docs
1 parent 0a780b9 commit 341c312

2 files changed

Lines changed: 56 additions & 14 deletions

File tree

src/pricing.mjs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ const STANDARD_LONG_CONTEXT_THRESHOLD_TOKENS = 272_000;
2222
const PRIORITY_LONG_CONTEXT_EXCLUSION_THRESHOLD_TOKENS = 128_000;
2323
const STANDARD_LONG_CONTEXT_TIERS = new Map([
2424
['gpt-5.5', standardLongContextTiers(10, 1, 45)],
25+
['gpt-5.5-pro', standardLongContextTiers(60, 60, 270)],
2526
['gpt-5.4', standardLongContextTiers(5, 0.5, 22.5)],
27+
['gpt-5.4-pro', standardLongContextTiers(60, 60, 270)],
2628
]);
2729

2830
const BUNDLED_STANDARD_PRICING = {
2931
'gpt-5.5': officialPrice(5, 0.5, 30),
32+
'gpt-5.5-pro': officialPrice(30, null, 180),
3033
'gpt-5.4': officialPrice(2.5, 0.25, 15),
3134
'gpt-5.4-mini': officialPrice(0.75, 0.075, 4.5),
3235
'gpt-5.4-nano': officialPrice(0.2, 0.02, 1.25),
36+
'gpt-5.4-pro': officialPrice(30, null, 180),
3337
'gpt-5.2': officialPrice(1.75, 0.175, 14),
3438
'gpt-5.2-codex': officialPrice(1.75, 0.175, 14),
3539
'gpt-5.3-codex': officialPrice(1.75, 0.175, 14),
@@ -183,21 +187,17 @@ export function parseOpenAIDevPricingHtml(html, options = {}) {
183187
}
184188
}
185189

186-
if (tier === 'standard') {
187-
for (const component of components) {
188-
if (component.exportName !== 'GroupedPricingTable') {
189-
continue;
190-
}
191-
if (!component.props.includes('"Category"') || !component.props.includes('"Cached input"')) {
192-
continue;
193-
}
194-
if (!component.props.includes('"ChatGPT"') && !component.props.includes('"Codex"')) {
195-
continue;
196-
}
197-
if (component.props.includes('"Deep research"') && component.props.includes('"Embedding"')) {
198-
addRowsFromProps(component.props, out, 'openai-official');
199-
}
190+
for (const component of components) {
191+
if (component.exportName !== 'GroupedPricingTable') {
192+
continue;
200193
}
194+
if ((component.pane ?? (tier === 'standard' ? 'standard' : null)) !== tier) {
195+
continue;
196+
}
197+
if (!isTokenGroupedPricingTable(component.props)) {
198+
continue;
199+
}
200+
addRowsFromProps(component.props, out, tier === 'priority' ? 'openai-priority-official' : 'openai-official');
201201
}
202202

203203
for (const [model, price] of Object.entries(out)) {
@@ -933,14 +933,27 @@ function extractAstroComponents(html) {
933933
const components = [];
934934
const re = /component-export="([^"]+)"[^>]*props="([^"]+)"/g;
935935
for (const match of html.matchAll(re)) {
936+
const prefix = html.slice(Math.max(0, match.index - 5000), match.index);
937+
const paneMatches = [...prefix.matchAll(/data-content-switcher-pane="true"\s+data-value="([^"]+)"/g)];
936938
components.push({
937939
exportName: match[1],
938940
props: decodeHtmlAttribute(match[2]),
941+
pane: paneMatches.at(-1)?.[1] ?? null,
939942
});
940943
}
941944
return components;
942945
}
943946

947+
function isTokenGroupedPricingTable(props) {
948+
return (
949+
props.includes('"Category"') &&
950+
props.includes('"Model"') &&
951+
props.includes('"Input"') &&
952+
props.includes('"Cached input"') &&
953+
props.includes('"Output"')
954+
);
955+
}
956+
944957
function readAstroStringField(props, field) {
945958
const match = props.match(new RegExp(`"${escapeRegex(field)}":\\[0,"([^"]+)"\\]`));
946959
return match?.[1];

test/pricing.test.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ assert.deepEqual(bundledGpt55Standard.price.tiered, [
3636
{ kind: 'cache_read_input_token_cost', thresholdTokens: 272_000, costPerMToken: 1 },
3737
{ kind: 'output_cost_per_token', thresholdTokens: 272_000, costPerMToken: 45 },
3838
]);
39+
const bundledGpt55ProStandard = bundledStandard.getPricing('gpt-5.5-pro');
40+
assert.equal(bundledGpt55ProStandard.price.cachedInputCostPerMToken, 30);
41+
assert.deepEqual(bundledGpt55ProStandard.price.tiered, [
42+
{ kind: 'input_cost_per_token', thresholdTokens: 272_000, costPerMToken: 60 },
43+
{ kind: 'cache_read_input_token_cost', thresholdTokens: 272_000, costPerMToken: 60 },
44+
{ kind: 'output_cost_per_token', thresholdTokens: 272_000, costPerMToken: 270 },
45+
]);
3946
assert.deepEqual(bundledStandard.metadata.billingThresholds, [272_000]);
4047
assertClose(
4148
calculateCostFromUsageOrEvents(
@@ -51,13 +58,35 @@ assert.deepEqual(bundledGpt54Standard.price.tiered, [
5158
{ kind: 'cache_read_input_token_cost', thresholdTokens: 272_000, costPerMToken: 0.5 },
5259
{ kind: 'output_cost_per_token', thresholdTokens: 272_000, costPerMToken: 22.5 },
5360
]);
61+
const bundledGpt54ProStandard = bundledStandard.getPricing('gpt-5.4-pro');
62+
assert.equal(bundledGpt54ProStandard.price.cachedInputCostPerMToken, 30);
63+
assert.deepEqual(bundledGpt54ProStandard.price.tiered, [
64+
{ kind: 'input_cost_per_token', thresholdTokens: 272_000, costPerMToken: 60 },
65+
{ kind: 'cache_read_input_token_cost', thresholdTokens: 272_000, costPerMToken: 60 },
66+
{ kind: 'output_cost_per_token', thresholdTokens: 272_000, costPerMToken: 270 },
67+
]);
5468

5569
const parsedStandard = parseOpenAIDevPricingHtml(
5670
'<div component-export="TextTokenPricingTables" props="&quot;tier&quot;:[0,&quot;standard&quot;],[0,&quot;GPT-5.5&quot;],[0,5],[0,0.5],[0,30]"></div>',
5771
{ tier: 'standard' },
5872
);
5973
assert.deepEqual(parsedStandard['gpt-5.5'].tiered, bundledGpt55Standard.price.tiered);
6074

75+
const groupedPricingHtml = [
76+
'<div data-content-switcher-pane="true" data-value="standard">',
77+
'<astro-island component-export="GroupedPricingTable" props="&quot;headings&quot;:[1,[[0,&quot;Category&quot;],[0,&quot;Model&quot;],[0,&quot;Input&quot;],[0,&quot;Cached input&quot;],[0,&quot;Output&quot;]]],&quot;groups&quot;:[1,[[0,{&quot;model&quot;:[0,&quot;Codex&quot;],&quot;rows&quot;:[1,[[1,[[0,&quot;gpt-5.3-codex&quot;],[0,1.75],[0,0.175],[0,14]]]]]}]]]}"></astro-island>',
78+
'</div>',
79+
'<div data-content-switcher-pane="true" data-value="priority">',
80+
'<astro-island component-export="GroupedPricingTable" props="&quot;headings&quot;:[1,[[0,&quot;Category&quot;],[0,&quot;Model&quot;],[0,&quot;Input&quot;],[0,&quot;Cached input&quot;],[0,&quot;Output&quot;]]],&quot;groups&quot;:[1,[[0,{&quot;model&quot;:[0,&quot;Codex&quot;],&quot;rows&quot;:[1,[[1,[[0,&quot;gpt-5.3-codex&quot;],[0,3.5],[0,0.35],[0,28]]]]]}]]]}"></astro-island>',
81+
'</div>',
82+
].join('');
83+
const parsedGroupedStandard = parseOpenAIDevPricingHtml(groupedPricingHtml, { tier: 'standard' });
84+
assert.equal(parsedGroupedStandard['gpt-5.3-codex'].inputCostPerMToken, 1.75);
85+
const parsedGroupedPriority = parseOpenAIDevPricingHtml(groupedPricingHtml, { tier: 'priority' });
86+
assert.equal(parsedGroupedPriority['gpt-5.3-codex'].inputCostPerMToken, 3.5);
87+
assert.equal(parsedGroupedPriority['gpt-5.3-codex'].cachedInputCostPerMToken, 0.35);
88+
assert.equal(parsedGroupedPriority['gpt-5.3-codex'].outputCostPerMToken, 28);
89+
6190
const oldOpenAiCacheFile = path.join(root, 'old-openai-cache.json');
6291
await writeFile(
6392
oldOpenAiCacheFile,

0 commit comments

Comments
 (0)