diff --git a/.changeset/small-cougars-reflect.md b/.changeset/small-cougars-reflect.md new file mode 100644 index 000000000000..3f9ea17a0ec5 --- /dev/null +++ b/.changeset/small-cougars-reflect.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/prodia': patch +--- + +feat(provider/prodia): add price param to API requests diff --git a/packages/prodia/src/prodia-image-model.test.ts b/packages/prodia/src/prodia-image-model.test.ts index f285fe91ebc9..8d5b1d966866 100644 --- a/packages/prodia/src/prodia-image-model.test.ts +++ b/packages/prodia/src/prodia-image-model.test.ts @@ -64,13 +64,14 @@ const defaultJobResult = { state: { current: 'completed' }, config: { prompt, seed: 42 }, metrics: { elapsed: 2.5, ips: 10.5 }, + price: { product: 'flux-fast.schnell', dollars: 0.0025 }, }; describe('ProdiaImageModel', () => { const multipartResponse = createMultipartResponse(defaultJobResult); const server = createTestServer({ - 'https://api.example.com/v2/job': { + 'https://api.example.com/v2/job?price=true': { response: { type: 'binary', body: multipartResponse.body, @@ -259,7 +260,9 @@ describe('ProdiaImageModel', () => { }); expect(server.calls[0].requestMethod).toBe('POST'); - expect(server.calls[0].requestUrl).toBe('https://api.example.com/v2/job'); + expect(server.calls[0].requestUrl).toBe( + 'https://api.example.com/v2/job?price=true', + ); }); it('sends Accept: multipart/form-data header', async () => { @@ -357,6 +360,7 @@ describe('ProdiaImageModel', () => { iterationsPerSecond: 10.5, createdAt: '2025-01-01T00:00:00Z', updatedAt: '2025-01-01T00:00:05Z', + dollars: 0.0025, }, ], }); @@ -370,7 +374,7 @@ describe('ProdiaImageModel', () => { }; const response = createMultipartResponse(minimalJobResult); - server.urls['https://api.example.com/v2/job'].response = { + server.urls['https://api.example.com/v2/job?price=true'].response = { type: 'binary', body: response.body, headers: { @@ -426,7 +430,7 @@ describe('ProdiaImageModel', () => { }); it('handles API errors', async () => { - server.urls['https://api.example.com/v2/job'].response = { + server.urls['https://api.example.com/v2/job?price=true'].response = { type: 'error', status: 400, body: JSON.stringify({ @@ -451,13 +455,130 @@ describe('ProdiaImageModel', () => { ).rejects.toMatchObject({ message: 'Prompt cannot be empty', statusCode: 400, - url: 'https://api.example.com/v2/job', + url: 'https://api.example.com/v2/job?price=true', + }); + }); + + it('includes dollars in metadata when price is present', async () => { + const jobResultWithPrice = { + id: 'job-789', + state: { current: 'completed' }, + config: { prompt }, + price: { product: 'flux-fast.schnell', dollars: 0.005 }, + }; + const response = createMultipartResponse(jobResultWithPrice); + + server.urls['https://api.example.com/v2/job?price=true'].response = { + type: 'binary', + body: response.body, + headers: { + 'content-type': response.contentType, + }, + }; + + const model = createBasicModel(); + + const result = await model.doGenerate({ + prompt, + files: undefined, + mask: undefined, + n: 1, + size: undefined, + seed: undefined, + aspectRatio: undefined, + providerOptions: {}, + }); + + expect(result.providerMetadata?.prodia).toStrictEqual({ + images: [ + { + jobId: 'job-789', + dollars: 0.005, + }, + ], + }); + }); + + it('omits dollars from metadata when price is absent', async () => { + const jobResultWithoutPrice = { + id: 'job-790', + state: { current: 'completed' }, + config: { prompt }, + }; + const response = createMultipartResponse(jobResultWithoutPrice); + + server.urls['https://api.example.com/v2/job?price=true'].response = { + type: 'binary', + body: response.body, + headers: { + 'content-type': response.contentType, + }, + }; + + const model = createBasicModel(); + + const result = await model.doGenerate({ + prompt, + files: undefined, + mask: undefined, + n: 1, + size: undefined, + seed: undefined, + aspectRatio: undefined, + providerOptions: {}, + }); + + expect(result.providerMetadata?.prodia).toStrictEqual({ + images: [ + { + jobId: 'job-790', + }, + ], + }); + }); + + it('omits dollars from metadata when price is null', async () => { + const jobResultWithNullPrice = { + id: 'job-791', + state: { current: 'completed' }, + config: { prompt }, + price: null, + }; + const response = createMultipartResponse(jobResultWithNullPrice); + + server.urls['https://api.example.com/v2/job?price=true'].response = { + type: 'binary', + body: response.body, + headers: { + 'content-type': response.contentType, + }, + }; + + const model = createBasicModel(); + + const result = await model.doGenerate({ + prompt, + files: undefined, + mask: undefined, + n: 1, + size: undefined, + seed: undefined, + aspectRatio: undefined, + providerOptions: {}, + }); + + expect(result.providerMetadata?.prodia).toStrictEqual({ + images: [ + { + jobId: 'job-791', + }, + ], }); }); it('includes timestamp, headers, and modelId in response metadata', async () => { const response = createMultipartResponse(defaultJobResult); - server.urls['https://api.example.com/v2/job'].response = { + server.urls['https://api.example.com/v2/job?price=true'].response = { type: 'binary', body: response.body, headers: { diff --git a/packages/prodia/src/prodia-image-model.ts b/packages/prodia/src/prodia-image-model.ts index 19c65f65d325..8cd1f7872ba5 100644 --- a/packages/prodia/src/prodia-image-model.ts +++ b/packages/prodia/src/prodia-image-model.ts @@ -114,7 +114,7 @@ export class ProdiaImageModel implements ImageModelV3 { ); const { value: multipartResult, responseHeaders } = await postToApi({ - url: `${this.config.baseURL}/job`, + url: `${this.config.baseURL}/job?price=true`, headers: { ...combinedHeaders, Accept: 'multipart/form-data; image/png', @@ -155,6 +155,9 @@ export class ProdiaImageModel implements ImageModelV3 { ...(jobResult.updated_at != null && { updatedAt: jobResult.updated_at, }), + ...(jobResult.price?.dollars != null && { + dollars: jobResult.price.dollars, + }), }, ], }, @@ -255,6 +258,12 @@ const prodiaJobResultSchema = z.object({ ips: z.number().optional(), }) .optional(), + price: z + .object({ + product: z.string(), + dollars: z.number(), + }) + .nullish(), }); type ProdiaJobResult = z.infer; diff --git a/packages/prodia/src/prodia-provider.test.ts b/packages/prodia/src/prodia-provider.test.ts index b17ad389806c..e71f8a0894cb 100644 --- a/packages/prodia/src/prodia-provider.test.ts +++ b/packages/prodia/src/prodia-provider.test.ts @@ -43,7 +43,7 @@ const defaultJobResult = { const multipartResponse = createMultipartResponse(defaultJobResult); const server = createTestServer({ - 'https://api.example.com/v2/job': { + 'https://api.example.com/v2/job?price=true': { response: { type: 'binary', body: multipartResponse.body, @@ -91,7 +91,9 @@ describe('Prodia provider', () => { providerOptions: {}, }); - expect(server.calls[0].requestUrl).toBe('https://api.example.com/v2/job'); + expect(server.calls[0].requestUrl).toBe( + 'https://api.example.com/v2/job?price=true', + ); expect(server.calls[0].requestMethod).toBe('POST'); expect(server.calls[0].requestHeaders.authorization).toBe( 'Bearer test-api-key',