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
52 changes: 51 additions & 1 deletion src/server/providers/byteplus-pricing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
sharePublishedAliases,
} from './byteplus-pricing.ts'
import type { ByteplusDoc } from './byteplus-pricing.ts'
import liveImages from './fixtures/byteplus-pricing-images.json'
import { pricingDoc } from './fixtures/byteplus-pricing-doc.ts'
import type { PricingTable } from './fixtures/byteplus-pricing-doc.ts'
import { overlayModelFacts } from './reasoning-config.ts'
Expand Down Expand Up @@ -583,7 +584,7 @@ const IMAGE_TABLE: PricingTable = {
[
'dola-seedream-5-0-pro-260628',
'First image: Free From the second image: $0.003 Pricing varies by image generation scenario.',
'Single-image generation: Up to 2.61 million pixels (1.5K or lower): $0.045 More than 2.61 million pixels (higher than 1.5K): $0.09 Layer decomposition: Up to 2.61 million pixels (1.5K or lower): $0.0225 More than 2.61 million pixels (higher than 1.5K): $0.045',
'Single-image generation: P ≤ 2.61 million (1.5K or lower): $0.045 P > 2.61 million (higher than 1.5K): $0.09 Layer decomposition: P ≤ 2.61 million (1.5K or lower): $0.0225 P > 2.61 million (higher than 1.5K): $0.045',
],
['seedream-5-0-lite-260128', 'Free', '$0.035'],
['dola-seedream-5-0-flash-260915', 'Free', '$0.018'],
Expand Down Expand Up @@ -764,6 +765,55 @@ describe('byteplus video and image tables', () => {
})
})

describe('byteplus image table as published 2026-11-13 (#640)', () => {
// The image table cut verbatim from the live doc: the "Output image"
// header now carries a note line, and Seedream 5.0 pro's tiers read
// "P ≤ 2.61 million".
const page: ByteplusDoc = liveImages

it('reads the flat rows under the annotated header', () => {
expect(parseByteplusImages(page)).toEqual(
new Map([
['dola-seedream-5-0-flash-260915', 0.018],
['seedream-5-0-lite-260128', 0.035],
['seedream-4-5-251128', 0.04],
['seedream-4-0-250828', 0.03],
['seededit-3-0-i2i-250628', 0.03],
]),
)
})

it('reads Seedream 5.0 pro tiers and leaves Seedream 5.5 unpriced', () => {
expect(parseByteplusPixelTiers(page)).toEqual(
new Map([
[
'dola-seedream-5-0-pro-260628',
{
extraInput: 0.003,
maxLowPixels: 2.61e6,
lowLevel: '1.5K',
low: 0.045,
high: 0.09,
},
],
]),
)
})

it('still parses nothing from a table with no output column', () => {
const none = pricingDoc([
{
rows: [
['Model ID', 'Input image'],
['seedream-4-0-250828', 'Free'],
],
},
])
expect(parseByteplusImages(none).size).toBe(0)
expect(parseByteplusPixelTiers(none).size).toBe(0)
})
})

/** The tutorial's model and pixel tables as the live page words them. */
const GUIDE = doc(
[
Expand Down
68 changes: 42 additions & 26 deletions src/server/providers/byteplus-pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,29 +838,52 @@ export function parseByteplusVideo(
return out
}

interface ImageRow {
id: string
input: string
output: string
cells: Array<string>
}

/**
* Image table: `Model ID ‖ Input image ‖ Output image`, USD per image
* (`$0.035`). Only flat rows with free input images are read here; Seedream 5.0
* pro's pixel-tiered row is `parseByteplusPixelTiers`. Only the first
* matching table is read.
* Image table rows: `Model ID ‖ Input image ‖ Output image`. Since
* 2026-11-12 (#640) the output header carries a note line ("P is the total
* pixel count of a single output image."), so that column is the one header
* starting "Output image". Only the first matching table is read.
*/
function imageRows(doc: ByteplusDoc): Array<ImageRow> {
for (const { keys, rows } of tablesWith(doc, ['model id', 'input image'])) {
const outputs = keys.flatMap((key, at) =>
/^output image\b/.test(key) ? [at] : [],
)
const [output] = outputs
if (output === undefined || outputs.length > 1) continue
return rows.map((cells) => ({
id: (cells[keys.indexOf('model id')] ?? '').split(' ')[0] ?? '',
input: (cells[keys.indexOf('input image')] ?? '').trim(),
output: (cells[output] ?? '').trim(),
cells,
}))
}
return []
}

/**
* Flat image rows, USD per output image (`$0.035`) with free input images.
* Seedream 5.0 pro's pixel-tiered row is `parseByteplusPixelTiers`; any
* other row (Seedream 5.5's paid inputs and bounded pixel tiers) is
* unpriced and warned.
*/
export function parseByteplusImages(doc: ByteplusDoc): Map<string, number> {
const out = new Map<string, number>()
const [table] = tablesWith(doc, ['model id', 'input image', 'output image'])
if (!table) return out
const at = (key: string) => table.keys.indexOf(key)
for (const row of table.rows) {
const id = (row[at('model id')] ?? '').split(' ')[0] ?? ''
for (const { id, input, output, cells } of imageRows(doc)) {
if (!/^[a-z0-9-]+$/.test(id)) continue
const input = (row[at('input image')] ?? '').trim()
const output = dollars((row[at('output image')] ?? '').trim())
if (!/^free$/i.test(input) || output === null) {
// Pixel-tiered rows belong to `parseByteplusPixelTiers`.
const tiered = pixelTierRates(input, row[at('output image')] ?? '')
if (!tiered) warnUnread('image', id, row)
const rate = dollars(output)
if (!/^free$/i.test(input) || rate === null) {
if (!pixelTierRates(input, output)) warnUnread('image', id, cells)
continue
}
out.set(id, output)
out.set(id, rate)
}
return out
}
Expand Down Expand Up @@ -1009,7 +1032,7 @@ const PIXEL_TIER_INPUT =
// Layer decomposition is not read: the page says one request's layers can
// land in different tiers, so the card refuses that mode.
const PIXEL_TIER_OUTPUT =
/^Single-image generation: Up to ([\d.]+) million pixels \(([\d.]+)K or lower\): \$([\d.]+) More than \1 million pixels \(higher than \2K\): \$([\d.]+) Layer decomposition: /i
/^Single-image generation: P ≤ ([\d.]+) million \(([\d.]+)K or lower\): \$([\d.]+) P > \1 million \(higher than \2K\): \$([\d.]+) Layer decomposition: /i

/** One image-table row as pixel tiers, or null for any other wording. */
function pixelTierRates(
Expand All @@ -1034,15 +1057,8 @@ export function parseByteplusPixelTiers(
doc: ByteplusDoc,
): Map<string, ByteplusPixelTiers> {
const out = new Map<string, ByteplusPixelTiers>()
const [table] = tablesWith(doc, ['model id', 'input image', 'output image'])
if (!table) return out
const at = (key: string) => table.keys.indexOf(key)
for (const row of table.rows) {
const id = (row[at('model id')] ?? '').split(' ')[0] ?? ''
const rates = pixelTierRates(
row[at('input image')] ?? '',
row[at('output image')] ?? '',
)
for (const { id, input, output } of imageRows(doc)) {
const rates = pixelTierRates(input, output)
if (/^[a-z0-9-]+$/.test(id) && rates) out.set(id, rates)
}
return out
Expand Down
Loading
Loading