Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
64 changes: 63 additions & 1 deletion src/server/providers/adapters/meta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,69 @@ describe('Meta native docs', () => {
models,
{ url: META_PRICING, hash: 'fixture', extractedAt: '2026-10-12' },
),
).toThrow('unknown pricing table')
).toThrow('unknown pricing table in Muse Wonder')
})
it('reads token prices when Contributor is the first column', () => {
// Trimmed from https://dev.meta.ai/docs/pricing-rate-limits on 2026-11-10.
// Decorative attributes removed; cell text is the live table.
const html = `<h1 id="pricing">Pricing</h1>
<h3 id="muse-wonder-pricing">Muse Wonder</h3>
<p>Model: <code>muse-wonder-1.0</code>.</p>
<table><thead><tr><th>Usage</th><th>Contributor mode (per 1M tokens)</th><th>Standard mode (per 1M tokens)</th></tr></thead><tbody>
<tr><td>Cached input</td><td>Not available</td><td>$0.20</td></tr>
<tr><td>Input</td><td>Not available</td><td>$4.00</td></tr>
<tr><td>Output</td><td>Not available</td><td>$20.00</td></tr>
</tbody></table>
<h3 id="muse-spark-pricing">Muse Spark</h3>
<p>Models: <code>muse-spark-1.3</code>, <code>muse-spark-1.2</code>, and <code>muse-spark-1.1</code> in Standard mode; <code>muse-spark-1.3-contributor</code> and <code>muse-spark-1.2-contributor</code> in Contributor mode. Muse Spark 1.1 is Standard mode only.</p>
<table><thead><tr><th>Usage</th><th>Contributor mode (per 1M tokens)</th><th>Standard mode (per 1M tokens)</th></tr></thead><tbody>
<tr><td>Cached input</td><td>$0.002</td><td>$0.15</td></tr>
<tr><td>Input</td><td>$0.10</td><td>$1.25</td></tr>
<tr><td>Output</td><td>$0.20</td><td>$4.25</td></tr>
</tbody></table>`
const models = parseMetaModels(docs[META_MODELS] ?? '', pages)
const cards = parseMetaPricing(html, models, {
url: META_PRICING,
hash: 'live-2026-11-10',
extractedAt: '2026-11-10',
})
expect(cards.get('muse-wonder-1.0')).toMatchObject({
tables: {
rate: {
base: {
input_tokens: 4 / 1e6,
output_tokens: 20 / 1e6,
cache_read_tokens: 0.2 / 1e6,
},
},
},
})
expect(cards.get('muse-spark-1.3')).toMatchObject({
tables: {
rate: {
base: {
input_tokens: 1.25 / 1e6,
output_tokens: 4.25 / 1e6,
cache_read_tokens: 0.15 / 1e6,
},
},
},
})
expect(cards.get('muse-spark-1.3-contributor')).toMatchObject({
tables: {
rate: {
base: {
input_tokens: 0.1 / 1e6,
output_tokens: 0.2 / 1e6,
cache_read_tokens: 0.002 / 1e6,
},
},
},
})
expect(cards.get('muse-spark-1.1')).toEqual(cards.get('muse-spark-1.3'))
expect(cards.get('muse-spark-1.2-contributor')).toEqual(
cards.get('muse-spark-1.3-contributor'),
)
})
it('fails when a pay-as-you-go chat ID gets no token price', () => {
const models = parseMetaModels(docs[META_MODELS] ?? '', pages)
Expand Down
62 changes: 44 additions & 18 deletions src/server/providers/meta-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,29 @@ export function parseMetaModels(
fail('model page listed no chat models')
return out
}
type TokenMode = 'Standard' | 'Contributor'
/** Mode columns are named. Meta publishes them in either order. */
function tokenModeColumns(
head: Array<string>,
): Record<TokenMode, number> | undefined {
if (head[0] !== 'Usage' || head.length !== 3) return
const found = new Map<TokenMode, number>()
for (let index = 1; index < head.length; index++) {
const cell = head[index]
const mode: TokenMode | undefined =
cell === 'Standard mode (per 1M tokens)'
? 'Standard'
: cell === 'Contributor mode (per 1M tokens)'
? 'Contributor'
: undefined
if (!mode || found.has(mode)) return
found.set(mode, index)
}
const standard = found.get('Standard')
const contributor = found.get('Contributor')
if (standard === undefined || contributor === undefined) return
return { Standard: standard, Contributor: contributor }
}
export function parseMetaPricing(
html: string,
models: Array<ModelInfo>,
Expand All @@ -246,13 +269,11 @@ export function parseMetaPricing(
)
for (const section of sections(html, 3)) {
const rows = tables(section.body)[0]
if (
rows?.[0]?.join('|') ===
'Usage|Standard mode (per 1M tokens)|Contributor mode (per 1M tokens)'
) {
const tokenCols = tokenModeColumns(rows?.[0] ?? [])
if (rows && tokenCols) {
// "Models: a, b in Standard mode; c in Contributor mode." A section
// that names no mode must have only one priced column.
const columns = new Map<string, number>()
const columns = new Map<string, TokenMode | undefined>()
for (const clause of plain(section.body.split(/<table\b/)[0] ?? '').split(
';',
)) {
Expand All @@ -269,7 +290,9 @@ export function parseMetaPricing(
if (columns.has(id)) fail('duplicate model price')
columns.set(
id,
modes[0] === 'Contributor' ? 2 : modes[0] === 'Standard' ? 1 : 0,
modes[0] === 'Contributor' || modes[0] === 'Standard'
? modes[0]
: undefined,
)
}
}
Expand All @@ -279,29 +302,31 @@ export function parseMetaPricing(
Input: 'input_tokens',
Output: 'output_tokens',
}
const rates: Record<number, Record<string, number>> = { 1: {}, 2: {} }
const rates: Record<TokenMode, Record<string, number>> = {
Standard: {},
Contributor: {},
}
for (const row of rows.slice(1)) {
const key = meters[row[0] ?? '']
if (!key || row.length !== 3) fail('unreadable token price')
for (const col of [1, 2]) {
const cell = row[col] ?? ''
for (const mode of ['Standard', 'Contributor'] as const) {
const cell = row[tokenCols[mode]] ?? ''
if (cell === 'Not available') continue
const price = /^\$(\d+(?:\.\d+)?)$/.exec(cell)
if (!price) fail('unreadable token price')
const target = rates[col]
if (target) target[key] = Number(price[1]) / 1e6
rates[mode][key] = Number(price[1]) / 1e6
}
}
const priced = [1, 2].filter(
(col) => Object.keys(rates[col] ?? {}).length,
const priced = (['Standard', 'Contributor'] as const).filter(
(mode) => Object.keys(rates[mode]).length,
)
for (const [id, stated] of columns) {
const col = stated || (priced.length === 1 ? priced[0] : undefined)
if (!col) fail('token price mode unstated')
const mode = stated ?? (priced.length === 1 ? priced[0] : undefined)
if (!mode) fail('token price mode unstated')
const tier = tiers.get(id)
if (tier && col !== (tier === 'Contributor' ? 2 : 1))
if (tier && mode !== tier)
fail('token price mode disagrees with model tier')
const colRates = rates[col] ?? {}
const colRates = rates[mode]
if (!('input_tokens' in colRates) || !('output_tokens' in colRates))
fail('incomplete token pricing')
if (cards.has(id)) fail('duplicate model price')
Expand Down Expand Up @@ -344,7 +369,8 @@ export function parseMetaPricing(
const p = /flat \$(\d+(?:\.\d+)?) per generated image/.exec(text)
if (!p) fail('unreadable image price')
rates.generated_images = Number(p[1])
} else if (rows?.[0]?.[0] === 'Usage') fail('unknown pricing table')
} else if (rows?.[0]?.[0] === 'Usage')
fail(`unknown pricing table in ${section.name}`)
else continue
const card = compileTokenCard(rates, [], source)
if (!card) fail('uncompilable unit pricing')
Expand Down
Loading