diff --git a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts index a79db29b3..478794a59 100644 --- a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts @@ -52,7 +52,7 @@ import { AIChatStream } from '../../utils/Streaming.js'; import { BytePlusProvider } from './BytePlusProvider.js'; import { BYTEPLUS_MODELS } from './models.js'; -// ── OpenAI SDK mock ───────────────────────────────────────────────── +// -- OpenAI SDK mock ------------------------------------------------- // // `vi.hoisted` lets us share spies between the (hoisted) factory and // the test body so each test can stub `chat.completions.create` with @@ -80,7 +80,7 @@ vi.mock('openai', () => { return { OpenAI: OpenAICtor, default: { OpenAI: OpenAICtor } }; }); -// ── Test harness ──────────────────────────────────────────────────── +// -- Test harness ---------------------------------------------------- let server: PuterServer; let recordSpy: MockInstance; @@ -148,7 +148,7 @@ afterEach(() => { vi.restoreAllMocks(); }); -// ── Construction ──────────────────────────────────────────────────── +// -- Construction ---------------------------------------------------- describe('BytePlusProvider construction', () => { it('points the OpenAI SDK at the ModelArk base URL with the configured key', () => { @@ -171,7 +171,7 @@ describe('BytePlusProvider construction', () => { }); }); -// ── Model catalog ─────────────────────────────────────────────────── +// -- Model catalog --------------------------------------------------- describe('BytePlusProvider model catalog', () => { it('returns seed-2-0-lite-260428 as the default', () => { @@ -209,7 +209,7 @@ describe('BytePlusProvider model catalog', () => { }); }); -// ── Request shape ─────────────────────────────────────────────────── +// -- Request shape --------------------------------------------------- describe('BytePlusProvider.complete request shape', () => { const baseCompletion = { @@ -357,7 +357,7 @@ describe('BytePlusProvider.complete request shape', () => { }); }); -// ── Model resolution ──────────────────────────────────────────────── +// -- Model resolution ------------------------------------------------ describe('BytePlusProvider model resolution', () => { const baseCompletion = { @@ -428,7 +428,7 @@ describe('BytePlusProvider model resolution', () => { }); }); -// ── Non-stream completion + reasoning_content normalisation ───────── +// -- Non-stream completion + reasoning_content normalisation --------- describe('BytePlusProvider.complete non-stream output', () => { it('returns the first choice and runs the metered usage calculator', async () => { @@ -600,7 +600,7 @@ describe('BytePlusProvider.complete non-stream output', () => { }); }); -// ── Streaming deltas ──────────────────────────────────────────────── +// -- Streaming deltas ------------------------------------------------ describe('BytePlusProvider.complete streaming', () => { it('streams text deltas through to text events and meters final usage', async () => { @@ -781,7 +781,7 @@ describe('BytePlusProvider.complete streaming', () => { }); }); -// ── Error mapping ─────────────────────────────────────────────────── +// -- Error mapping --------------------------------------------------- describe('BytePlusProvider.complete error mapping', () => { it('rethrows errors raised by the OpenAI client unchanged', async () => { @@ -803,7 +803,7 @@ describe('BytePlusProvider.complete error mapping', () => { }); }); -// ── Moderation ────────────────────────────────────────────────────── +// -- Moderation ------------------------------------------------------ describe('BytePlusProvider.checkModeration', () => { it('throws — BytePlus provider does not implement moderation', () => { diff --git a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.ts b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.ts index fab9e3f06..b28b64f4b 100644 --- a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.ts +++ b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.ts @@ -166,7 +166,9 @@ export class BytePlusProvider implements IChatProvider { completion, }); - this.#normalizeReasoningContent(result); + // Ark's deep-reasoning models return `reasoning_content` (DeepSeek + // wire convention); expose it under `reasoning` like other providers. + OpenAIUtil.normalizeReasoningContent(result); return result; } @@ -175,21 +177,4 @@ export class BytePlusProvider implements IChatProvider { ): ReturnType { throw new Error('Method not implemented.'); } - - // Ark's deep-reasoning models return `reasoning_content` (DeepSeek wire - // convention); expose it under the `reasoning` key like other providers. - #normalizeReasoningContent( - result: Awaited>, - ) { - if (!('message' in result) || !result.message) return; - - const message = result.message as Record; - if ( - message.reasoning === undefined && - message.reasoning_content !== undefined - ) { - message.reasoning = message.reasoning_content; - } - delete message.reasoning_content; - } } diff --git a/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.ts b/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.ts index 2bc7b776f..714c38bbf 100644 --- a/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.ts +++ b/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.ts @@ -180,7 +180,7 @@ export class ZAIProvider implements IChatProvider { completion, }); - this.#normalizeReasoningContent(result); + OpenAIUtil.normalizeReasoningContent(result); return result; } @@ -189,32 +189,4 @@ export class ZAIProvider implements IChatProvider { ): ReturnType { throw new Error('Method not implemented.'); } - - #normalizeReasoningContent( - result: Awaited>, - ) { - if (!('message' in result) || !result.message) return; - - const message = result.message as Record; - if ( - message.reasoning === undefined && - message.reasoning_content !== undefined - ) { - message.reasoning = message.reasoning_content; - } - delete message.reasoning_content; - - if (!Array.isArray(message.content)) return; - - for (const contentPart of message.content) { - const part = asRecord(contentPart); - if ( - part.reasoning === undefined && - part.reasoning_content !== undefined - ) { - part.reasoning = part.reasoning_content; - } - delete part.reasoning_content; - } - } } diff --git a/src/backend/drivers/ai-chat/utils/OpenAIUtil.js b/src/backend/drivers/ai-chat/utils/OpenAIUtil.js index 00e7c3569..26acf35dd 100644 --- a/src/backend/drivers/ai-chat/utils/OpenAIUtil.js +++ b/src/backend/drivers/ai-chat/utils/OpenAIUtil.js @@ -265,6 +265,36 @@ export const extractMeteredUsage = (usage) => { }; }; +// Renames one object's DeepSeek-wire `reasoning_content` to the `reasoning` +// key Puter exposes, without clobbering an existing `reasoning`. +const renameReasoningContent = (obj) => { + if (obj.reasoning === undefined && obj.reasoning_content !== undefined) { + obj.reasoning = obj.reasoning_content; + } + delete obj.reasoning_content; +}; + +/** + * Normalize a non-streaming completion result whose provider follows the + * DeepSeek wire convention (`reasoning_content` on the message and content + * parts) to Puter's `reasoning` key. The streaming path already does this in + * create_chat_stream_handler. + */ +export const normalizeReasoningContent = (result) => { + if (!result || typeof result !== 'object') return; + if (!('message' in result) || !result.message) return; + + const message = result.message; + renameReasoningContent(message); + + if (!Array.isArray(message.content)) return; + for (const part of message.content) { + if (part && typeof part === 'object' && !Array.isArray(part)) { + renameReasoningContent(part); + } + } +}; + export const create_chat_stream_handler = ({ deviations, completion, usage_calculator }) => async ({ chatStream }) => { diff --git a/src/backend/drivers/ai-image/ImageGenerationDriver.ts b/src/backend/drivers/ai-image/ImageGenerationDriver.ts index 63c0f672c..6628b8ba3 100644 --- a/src/backend/drivers/ai-image/ImageGenerationDriver.ts +++ b/src/backend/drivers/ai-image/ImageGenerationDriver.ts @@ -335,19 +335,22 @@ export class ImageGenerationDriver extends PuterDriver { } // Falls back to the shared `byteplus` (ai-chat) key; `apiBaseUrl` - // selects the ModelArk region, same as the chat provider. - const byteplusCfg = (providers['byteplus-image-generation'] ?? - providers['byteplus']) as Record | undefined; - const byteplusKey = readKey( - providers['byteplus-image-generation'], - providers['byteplus'], - ); + // selects the ModelArk region, same as the chat provider. Each field + // falls through independently so a partial image-specific block can't + // pair its missing apiBaseUrl with the shared block's key (or vice + // versa) and point a region-scoped key at the wrong endpoint. + const byteplusImageCfg = providers['byteplus-image-generation'] as + Record | undefined; + const byteplusSharedCfg = providers['byteplus'] as + Record | undefined; + const byteplusKey = readKey(byteplusImageCfg, byteplusSharedCfg); if (byteplusKey) { this.#providers['byteplus-image-generation'] = new BytePlusImageProvider( { apiKey: byteplusKey, - apiBaseUrl: byteplusCfg?.apiBaseUrl as + apiBaseUrl: (byteplusImageCfg?.apiBaseUrl ?? + byteplusSharedCfg?.apiBaseUrl) as string | undefined, }, m, diff --git a/src/backend/drivers/ai-image/inputImage.ts b/src/backend/drivers/ai-image/inputImage.ts index ba97031fc..e31ab7863 100644 --- a/src/backend/drivers/ai-image/inputImage.ts +++ b/src/backend/drivers/ai-image/inputImage.ts @@ -20,8 +20,8 @@ /** * Shared helpers for `input_images` (image-to-image) handling across image * providers. `input_images` is the canonical, cross-provider field; an entry - * may be a public URL, a data-URI, or raw base64. Providers whose upstream - * API needs base64 use these helpers to normalize URLs server-side (via the + * may be a public URL, a data-URI, or raw base64. Providers whose upstream API + * needs base64 use these helpers to normalize URLs server-side (via the * SSRF-guarded `secureFetch`); providers that accept URLs natively (Replicate, * xAI) pass them through untouched. */ @@ -35,9 +35,20 @@ export function isHttpUrl(s: string): boolean { } /** - * Resolve the single input image for providers that only support one. - * Throws 400 if `input_images` carries more than one entry. Returns the - * chosen image string (URL / data-URI / raw base64) or undefined. + * Normalize an input-image string for providers that accept URLs natively: + * http(s) URLs and data-URIs pass through untouched, raw base64 is wrapped with + * `mimeHint` (default image/png). + */ +export function toUrlOrDataUri(img: string, mimeHint?: string): string { + return isHttpUrl(img) || img.startsWith('data:') + ? img + : `data:${mimeHint ?? 'image/png'};base64,${img}`; +} + +/** + * Resolve the single input image for providers that only support one. Throws + * 400 if `input_images` carries more than one entry. Returns the chosen image + * string (URL / data-URI / raw base64) or undefined. */ export function resolveSingleInputImage( params: Pick, @@ -84,10 +95,9 @@ export async function fetchImageAsBase64( } /** - * Normalize any input-image string to a base64 data-URI: - * • http(s) URL → fetched via secureFetch - * • data-URI → returned as-is - * • raw base64 → wrapped with `mimeHint` (default image/png) + * Normalize any input-image string to a base64 data-URI: • http(s) URL → + * fetched via secureFetch • data-URI → returned as-is • raw base64 → wrapped + * with `mimeHint` (default image/png) */ export async function toBase64DataUri( img: string, diff --git a/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.test.ts b/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.test.ts index ee8b3871a..ebac4f7ef 100644 --- a/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.test.ts +++ b/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.test.ts @@ -47,7 +47,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { BYTEPLUS_IMAGE_GENERATION_MODELS } from './models.js'; import { BytePlusImageProvider } from './BytePlusImageProvider.js'; -// ── OpenAI SDK mock ───────────────────────────────────────────────── +// -- OpenAI SDK mock ------------------------------------------------- const { generateMock, openAICtor } = vi.hoisted(() => ({ generateMock: vi.fn(), @@ -68,7 +68,7 @@ vi.mock('openai', () => { return { OpenAI: OpenAICtor, default: { OpenAI: OpenAICtor } }; }); -// ── Test harness ──────────────────────────────────────────────────── +// -- Test harness ---------------------------------------------------- let server: PuterServer; let hasCreditsSpy: MockInstance; @@ -114,7 +114,7 @@ const sampleResponse = { data: [{ url: 'https://ark.example/img/1' }] }; const findModel = (id: string) => BYTEPLUS_IMAGE_GENERATION_MODELS.find((m) => m.id === id)!; -// ── Construction ──────────────────────────────────────────────────── +// -- Construction ---------------------------------------------------- describe('BytePlusImageProvider construction', () => { it('defaults the OpenAI SDK to the ap-southeast ModelArk base URL', () => { @@ -146,7 +146,7 @@ describe('BytePlusImageProvider construction', () => { }); }); -// ── Model catalog ─────────────────────────────────────────────────── +// -- Model catalog --------------------------------------------------- describe('BytePlusImageProvider model catalog', () => { it('returns seedream-5-0-lite as the default', () => { @@ -162,7 +162,7 @@ describe('BytePlusImageProvider model catalog', () => { }); }); -// ── test_mode / validation / credit gate ──────────────────────────── +// -- test_mode / validation / credit gate ---------------------------- describe('BytePlusImageProvider.generate gates', () => { it('returns the canned sample URL in test_mode without side effects', async () => { @@ -200,7 +200,7 @@ describe('BytePlusImageProvider.generate gates', () => { }); }); -// ── Model resolution ──────────────────────────────────────────────── +// -- Model resolution ------------------------------------------------ describe('BytePlusImageProvider.generate model resolution', () => { it('falls back to the default model for unknown ids', async () => { @@ -237,7 +237,7 @@ describe('BytePlusImageProvider.generate model resolution', () => { }); }); -// ── Request shape ─────────────────────────────────────────────────── +// -- Request shape --------------------------------------------------- describe('BytePlusImageProvider.generate request shape', () => { it('sends the tier keyword size (Ark default 2K), url format and no watermark', async () => { @@ -306,6 +306,7 @@ describe('BytePlusImageProvider.generate request shape', () => { generateMock.mockResolvedValueOnce(sampleResponse); await withTestActor(() => makeProvider().generate({ + model: 'seedream-4-0', prompt: 'hi', ratio: { w: 2048, h: 1024 }, }), @@ -313,6 +314,34 @@ describe('BytePlusImageProvider.generate request shape', () => { expect(generateMock.mock.calls[0]![0].size).toBe('2048x1024'); }); + it('rejects explicit pixel dimensions below a 2K-only model minimum', async () => { + // The default model (seedream-5-0-lite) only accepts >= 3,686,400 px, + // so a size that passes the generic floor still fails pre-flight + // instead of round-tripping to Ark for a 400. + await expect( + withTestActor(() => + makeProvider().generate({ + prompt: 'hi', + ratio: { w: 2048, h: 1024 }, + }), + ), + ).rejects.toMatchObject({ statusCode: 400 }); + expect(generateMock).not.toHaveBeenCalled(); + }); + + it('reduces w:h to lowest terms when mapping an aspect ratio', async () => { + generateMock.mockResolvedValueOnce(sampleResponse); + await withTestActor(() => + makeProvider().generate({ + model: 'seedream-4-0', + prompt: 'hi', + quality: '1k', + ratio: { w: 32, h: 18 }, + }), + ); + expect(generateMock.mock.calls[0]![0].size).toBe('1424x800'); + }); + it('rejects explicit pixel dimensions outside Ark limits', async () => { await expect( withTestActor(() => @@ -340,7 +369,7 @@ describe('BytePlusImageProvider.generate request shape', () => { }); }); -// ── Input images ──────────────────────────────────────────────────── +// -- Input images ---------------------------------------------------- describe('BytePlusImageProvider.generate input images', () => { const PNG = 'data:image/png;base64,iVBORw0KGgo='; @@ -390,7 +419,7 @@ describe('BytePlusImageProvider.generate input images', () => { }); }); -// ── Metering ──────────────────────────────────────────────────────── +// -- Metering -------------------------------------------------------- describe('BytePlusImageProvider.generate metering', () => { it('meters flat per-image models at their catalog rate', async () => { @@ -488,7 +517,7 @@ describe('BytePlusImageProvider.generate metering', () => { }); }); -// ── Response handling ─────────────────────────────────────────────── +// -- Response handling ----------------------------------------------- describe('BytePlusImageProvider.generate response handling', () => { it('falls back to a data URI when the response carries b64_json', async () => { diff --git a/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.ts b/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.ts index 08b52f0b6..d9c8962b6 100644 --- a/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.ts +++ b/src/backend/drivers/ai-image/providers/byteplus/BytePlusImageProvider.ts @@ -26,7 +26,7 @@ import type { IImageModel, IImageProvider, } from '../../types.js'; -import { isHttpUrl } from '../../inputImage.js'; +import { toUrlOrDataUri } from '../../inputImage.js'; import { BYTEPLUS_IMAGE_GENERATION_MODELS, SEEDREAM_RESOLUTION_MAP, @@ -38,6 +38,9 @@ const DEFAULT_MODEL = 'seedream-5-0-lite-260128'; // [1280x720, 2048x2048x1.1025] and aspect ratio within [1/16, 16]. const MIN_TOTAL_PIXELS = 921_600; const MAX_TOTAL_PIXELS = 4_624_220; +// Models restricted to the 2K tier (see SEEDREAM_2K_ONLY in models.ts) +// enforce this higher minimum on explicit sizes too. +const MIN_TOTAL_PIXELS_2K_ONLY = 3_686_400; // dola-seedream-5-0-pro's price break: ≤ 2.61MP bills the "1.5K or lower" // rate, above it the higher rate. const PRO_TIER_BREAK_PIXELS = 2_610_000; @@ -135,7 +138,7 @@ export class BytePlusImageProvider implements IImageProvider { const inputImageCount = input_images?.length ?? 0; const tier = this.#normalizeTier(quality, selectedModel); - const size = this.#resolveSize(tier, ratio); + const size = this.#resolveSize(tier, selectedModel, ratio); // The pro model bills by output pixel count; everything else is a // flat per-image rate. @@ -184,7 +187,7 @@ export class BytePlusImageProvider implements IImageProvider { const image = inputImageCount > 0 ? input_images!.map((img) => - this.#toImageRef(img, input_image_mime_type), + toUrlOrDataUri(img, input_image_mime_type), ) : undefined; @@ -239,14 +242,6 @@ export class BytePlusImageProvider implements IImageProvider { return url; } - // Ark accepts a public URL or a `data:image/...;base64,` URI; wrap raw - // base64 payloads so they're valid. - #toImageRef(img: string, mimeHint?: string): string { - return isHttpUrl(img) || img.startsWith('data:') - ? img - : `data:${mimeHint ?? 'image/png'};base64,${img}`; - } - /** * Pick the tier to request. Models with a minimum output-pixel count reject * the smaller tiers outright (and the aspect-ratio table maps them to @@ -255,8 +250,16 @@ export class BytePlusImageProvider implements IImageProvider { */ #normalizeTier(quality: string | undefined, model: IImageModel): Tier { const q = (quality ?? '').toLowerCase(); - // Ark's default size is 2K when unspecified. - const requested: Tier = isTier(q) ? q : '2k'; + // OpenAI-style quality names other Puter providers accept map onto + // the nearest Ark tier so e.g. 'low' isn't silently billed at the + // 2K rate; anything else falls back to Ark's own default of 2K. + const synonym: Record = { + low: '1k', + medium: '1.5k', + high: '2k', + hd: '2k', + }; + const requested: Tier = isTier(q) ? q : (synonym[q] ?? '2k'); const allowed = TIERS.filter( (t) => model.allowedQualityLevels?.includes(t) ?? true, @@ -278,12 +281,23 @@ export class BytePlusImageProvider implements IImageProvider { * documented `WxH` for (aspect, tier) * - Otherwise → the tier keyword (`1K`/`1.5K`/`2K`, method 1) */ - #resolveSize(tier: Tier, ratio?: { w: number; h: number }): string { + #resolveSize( + tier: Tier, + model: IImageModel, + ratio?: { w: number; h: number }, + ): string { if (ratio?.w && ratio?.h) { const pixels = ratio.w * ratio.h; if (pixels >= MIN_TOTAL_PIXELS) { + // 2K-only models enforce a higher minimum on explicit sizes + // too — fail fast with the real constraint instead of letting + // Ark 400 the request after the round-trip. + const minPixels = model.allowedQualityLevels?.includes('1k') + ? MIN_TOTAL_PIXELS + : MIN_TOTAL_PIXELS_2K_ONLY; const aspect = ratio.w / ratio.h; if ( + pixels < minPixels || pixels > MAX_TOTAL_PIXELS || aspect < 1 / 16 || aspect > 16 @@ -291,14 +305,20 @@ export class BytePlusImageProvider implements IImageProvider { throw new HttpError( 400, `Requested size ${ratio.w}x${ratio.h} is outside BytePlus limits ` + - `(total pixels ≤ ${MAX_TOTAL_PIXELS}, aspect ratio within [1/16, 16])`, + `for ${model.id} (total pixels within [${minPixels}, ${MAX_TOTAL_PIXELS}], ` + + 'aspect ratio within [1/16, 16])', { legacyCode: 'bad_request' }, ); } return `${ratio.w}x${ratio.h}`; } - const mapped = - SEEDREAM_RESOLUTION_MAP[`${ratio.w}:${ratio.h}`]?.[tier]; + // Reduce w:h to lowest terms so any spelling of a supported + // aspect (8:6, 32:18, ...) finds its documented tier size. + const gcd = (a: number, b: number): number => + b === 0 ? a : gcd(b, a % b); + const d = gcd(Math.round(ratio.w), Math.round(ratio.h)) || 1; + const key = `${Math.round(ratio.w) / d}:${Math.round(ratio.h) / d}`; + const mapped = SEEDREAM_RESOLUTION_MAP[key]?.[tier]; if (mapped) return `${mapped.w}x${mapped.h}`; } return { '1k': '1K', '1.5k': '1.5K', '2k': '2K' }[tier]; diff --git a/src/backend/drivers/ai-video/VideoGenerationDriver.ts b/src/backend/drivers/ai-video/VideoGenerationDriver.ts index 72f589324..58906717f 100644 --- a/src/backend/drivers/ai-video/VideoGenerationDriver.ts +++ b/src/backend/drivers/ai-video/VideoGenerationDriver.ts @@ -222,11 +222,16 @@ export class VideoGenerationDriver extends PuterDriver { ? args.resolution : undefined; + // Case-insensitive so '4K' matches a catalog entry spelled '4k'; + // the matched catalog spelling (not the caller's) is forwarded. const normalizedResolution = - requestedResolution && - model.dimensions.includes(requestedResolution) - ? requestedResolution - : model.dimensions[0]; + (requestedResolution && + model.dimensions.find( + (d) => + d.toLowerCase() === + requestedResolution.toLowerCase(), + )) || + model.dimensions[0]; args.size = normalizedResolution; args.resolution = normalizedResolution; } @@ -295,19 +300,22 @@ export class VideoGenerationDriver extends PuterDriver { } // Falls back to the shared `byteplus` (ai-chat) key; `apiBaseUrl` - // selects the ModelArk region, same as the chat provider. - const byteplusCfg = (providers['byteplus-video-generation'] ?? - providers['byteplus']) as Record | undefined; - const byteplusKey = readKey( - providers['byteplus-video-generation'], - providers['byteplus'], - ); + // selects the ModelArk region, same as the chat provider. Each field + // falls through independently so a partial video-specific block can't + // pair its missing apiBaseUrl with the shared block's key (or vice + // versa) and point a region-scoped key at the wrong endpoint. + const byteplusVideoCfg = providers['byteplus-video-generation'] as + Record | undefined; + const byteplusSharedCfg = providers['byteplus'] as + Record | undefined; + const byteplusKey = readKey(byteplusVideoCfg, byteplusSharedCfg); if (byteplusKey) { this.#providers['byteplus-video-generation'] = new BytePlusVideoProvider( { apiKey: byteplusKey, - apiBaseUrl: byteplusCfg?.apiBaseUrl as + apiBaseUrl: (byteplusVideoCfg?.apiBaseUrl ?? + byteplusSharedCfg?.apiBaseUrl) as string | undefined, }, m, diff --git a/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.test.ts b/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.test.ts index 60fff1766..a3b2dc38d 100644 --- a/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.test.ts +++ b/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.test.ts @@ -46,7 +46,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { BYTEPLUS_VIDEO_GENERATION_MODELS } from './models.js'; import { BytePlusVideoProvider } from './BytePlusVideoProvider.js'; -// ── Test harness ──────────────────────────────────────────────────── +// -- Test harness ---------------------------------------------------- let server: PuterServer; let fetchSpy: MockInstance; @@ -121,7 +121,7 @@ const sentBody = (callIndex = 0): Record => const findModel = (id: string) => BYTEPLUS_VIDEO_GENERATION_MODELS.find((m) => m.id === id)!; -// ── Construction / catalog ────────────────────────────────────────── +// -- Construction / catalog ------------------------------------------ describe('BytePlusVideoProvider construction and catalog', () => { it('throws when no apiKey is supplied', () => { @@ -152,7 +152,7 @@ describe('BytePlusVideoProvider construction and catalog', () => { }); }); -// ── Gates ─────────────────────────────────────────────────────────── +// -- Gates ----------------------------------------------------------- describe('BytePlusVideoProvider.generate gates', () => { it('returns the canned sample URL in test_mode without network calls', async () => { @@ -183,7 +183,7 @@ describe('BytePlusVideoProvider.generate gates', () => { }); }); -// ── Request shape ─────────────────────────────────────────────────── +// -- Request shape --------------------------------------------------- describe('BytePlusVideoProvider.generate request shape', () => { it('POSTs a create-task request with text content and defaults', async () => { @@ -362,7 +362,7 @@ describe('BytePlusVideoProvider.generate request shape', () => { }); }); -// ── Polling / outcomes ────────────────────────────────────────────── +// -- Polling / outcomes ---------------------------------------------- describe('BytePlusVideoProvider.generate polling and outcomes', () => { it('keeps polling while the task is queued/running', async () => { @@ -400,7 +400,7 @@ describe('BytePlusVideoProvider.generate polling and outcomes', () => { }); }); -// ── Metering ──────────────────────────────────────────────────────── +// -- Metering -------------------------------------------------------- describe('BytePlusVideoProvider.generate metering', () => { it('bills the tokens the task reports at the resolution rate', async () => { diff --git a/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.ts b/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.ts index 854eda09c..ff0cac016 100644 --- a/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.ts +++ b/src/backend/drivers/ai-video/providers/byteplus/BytePlusVideoProvider.ts @@ -279,7 +279,14 @@ export class BytePlusVideoProvider extends VideoProvider { { legacyCode: 'bad_request' }, ); } - for (const img of referenceImages!.slice(0, MAX_REFERENCE_IMAGES)) { + if (referenceImages!.length > MAX_REFERENCE_IMAGES) { + throw new HttpError( + 400, + `${modelId} accepts at most ${MAX_REFERENCE_IMAGES} reference image(s)`, + { legacyCode: 'bad_request' }, + ); + } + for (const img of referenceImages!) { if (typeof img !== 'string' || !img.trim()) continue; content.push({ type: 'image_url', diff --git a/src/backend/drivers/ai-video/providers/byteplus/models.ts b/src/backend/drivers/ai-video/providers/byteplus/models.ts index ecb6c9e52..5fad94af2 100644 --- a/src/backend/drivers/ai-video/providers/byteplus/models.ts +++ b/src/backend/drivers/ai-video/providers/byteplus/models.ts @@ -90,9 +90,7 @@ export const BYTEPLUS_VIDEO_GENERATION_MODELS: IVideoModel[] = [ 'default-duration-per-video': 76, }, durationSeconds: seconds(5, 4, 15), - // '4K' repeats Ark's documented casing so the driver's strict - // `includes` match accepts either spelling. - dimensions: ['720p', '480p', '1080p', '4k', '4K'], + dimensions: ['720p', '480p', '1080p', '4k'], fps: FPS, defaultUsageKey: 'byteplus-video-generation:dreamina-seedance-2-0-260128:video_tokens:720p',