Skip to content

Commit e350f50

Browse files
authored
feat(image-generator): refresh provider model catalogs and validate requests (#9653)
* feat(image-generator): refresh provider model catalogs and validate requests * fix(image-generator): preserve saved catalog settings * fix(image-generator): retain dropdown literal types in regression tests
1 parent 329af08 commit e350f50

12 files changed

Lines changed: 5661 additions & 930 deletions

File tree

‎apps/sim/blocks/blocks/image_generator.ts‎

Lines changed: 103 additions & 530 deletions
Large diffs are not rendered by default.

‎apps/sim/lib/internal/image/operations.test.ts‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const falInput = {
4949

5050
describe('image operations', () => {
5151
beforeEach(() => {
52+
mocks.fetch.mockReset()
53+
mockSecureFetchWithPinnedIP.mockReset()
5254
vi.stubGlobal('fetch', mocks.fetch)
5355
mockInterruptibleSleep.mockResolvedValue(undefined)
5456
mockUploadCopilotFile.mockResolvedValue({ url: 'https://sim.test/generated.png' })
@@ -116,4 +118,98 @@ describe('image operations', () => {
116118
expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
117119
expect(mockUploadCopilotFile).not.toHaveBeenCalled()
118120
})
121+
122+
it('rejects foreign fal queue hosts before sending credentials', async () => {
123+
mocks.fetch.mockResolvedValueOnce(
124+
Response.json({
125+
request_id: 'foreign-job',
126+
status_url: 'https://untrusted.example/status',
127+
})
128+
)
129+
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(Response.json({ status: 'FAILED' }))
130+
131+
const response = await executeImageGeneration(falInput, {
132+
userId: 'user-1',
133+
requestId: 'foreign-queue',
134+
})
135+
136+
expect(response.status).toBe(500)
137+
expect(await response.json()).toMatchObject({ error: 'Invalid Fal.ai queue URL' })
138+
expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
139+
})
140+
141+
it('retrieves nested fal jobs from the documented application request path', async () => {
142+
mocks.fetch.mockResolvedValueOnce(Response.json({ request_id: 'nested-job' }))
143+
mockSecureFetchWithPinnedIP.mockImplementation(async (url: string) => {
144+
if (url === 'https://queue.fal.run/fal-ai/bytedance/requests/nested-job/status') {
145+
return Response.json({ status: 'COMPLETED' })
146+
}
147+
if (url === 'https://queue.fal.run/fal-ai/bytedance/requests/nested-job') {
148+
return Response.json({ images: [{ url: 'data:image/png;base64,cG5n' }] })
149+
}
150+
return new Response('Not found', { status: 404 })
151+
})
152+
153+
const response = await executeImageGeneration(
154+
{ ...falInput, model: 'seedream-v4.5' },
155+
{
156+
userId: 'user-1',
157+
requestId: 'nested-queue',
158+
}
159+
)
160+
161+
expect(response.status).toBe(200)
162+
expect((await response.json()).imageUrl).toBe('https://sim.test/generated.png')
163+
})
164+
165+
it.each(['openai', 'gemini'])(
166+
'does not purchase a fallback for an unknown %s model',
167+
async (provider) => {
168+
mocks.fetch.mockResolvedValueOnce(new Response('Unexpected generation', { status: 502 }))
169+
const response = await executeImageGeneration(
170+
{ ...falInput, provider, model: 'unknown-model' },
171+
{
172+
userId: 'user-1',
173+
requestId: 'unknown-model',
174+
}
175+
)
176+
177+
expect(response.status).toBe(400)
178+
expect(mocks.fetch).not.toHaveBeenCalled()
179+
}
180+
)
181+
182+
it('retains the completed fal receipt when delivery is interrupted', async () => {
183+
const controller = new AbortController()
184+
mocks.fetch.mockResolvedValueOnce(Response.json({ request_id: 'paid-job' }))
185+
mocks.getFalAICostMetadata.mockImplementation(async ({ signal }: { signal?: AbortSignal }) => {
186+
signal?.throwIfAborted()
187+
return {
188+
requestId: 'paid-job',
189+
endpointId: 'fal-ai/nano-banana-2',
190+
costDollars: 0.1,
191+
source: 'billing_events',
192+
}
193+
})
194+
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(Response.json({ status: 'COMPLETED' }))
195+
mockSecureFetchWithPinnedIP.mockImplementationOnce(async () => {
196+
controller.abort()
197+
return Response.json({ status: 'COMPLETED' })
198+
})
199+
200+
await expect(
201+
executeImageGeneration(
202+
{ ...falInput, useHostedCostTracking: true },
203+
{
204+
userId: 'user-1',
205+
requestId: 'completed-job',
206+
signal: controller.signal,
207+
}
208+
)
209+
).rejects.toMatchObject({ name: 'AbortError' })
210+
expect(mocks.getFalAICostMetadata).toHaveBeenCalledWith(
211+
expect.objectContaining({ requestId: 'paid-job' })
212+
)
213+
expect(mocks.getFalAICostMetadata.mock.calls[0][0].signal).toBeUndefined()
214+
})
119215
})

0 commit comments

Comments
 (0)