{"record":{"id":"454d2fa2886871d5","repo":"CherryHQ/cherry-studio","slug":"image-generation-job-no-async-transport-for-sd","errorCode":null,"errorMessage":"Image generation job: no async transport for '${sdkConfig.providerId}' (model '${sdkConfig.modelId}')","messagePattern":"Image generation job: no async transport for '(.+?)' \\(model '(.+?)'\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/provider/custom/tasks/imageGenerationJobHandler.ts","lineNumber":92,"sourceCode":"    // `recovery: 'abandon'` no run outlives the process, so persisting it would be\n    // a write nobody reads — and would imply a durability this handler does not have.\n    const captureContext = createAiUsageCaptureContext({\n      providerId: provider.id,\n      providerName: provider.name,\n      modelId: sdkConfig.modelId,\n      modelName: model.name,\n      pricing: model.pricing,\n      trustProviderReportedCost: provider.apiFeatures.reportsActualCost,\n      reportedCostCurrency: provider.reportedCostCurrency,\n      credentialReceipt,\n      source: input.source ?? null,\n      messageRef: null\n    })\n    const usageStartedAt = Date.now()\n\n    const transport = resolveImageTransport(sdkConfig.providerId, sdkConfig.modelId, sdkConfig.providerSettings)\n    if (!transport) {\n      throw new Error(\n        `Image generation job: no async transport for '${sdkConfig.providerId}' (model '${sdkConfig.modelId}')`\n      )\n    }\n\n    // No persisted-task resume branch: `recovery: 'abandon'` means a job never\n    // outlives the process that enqueued it, so every execution starts at submit.\n    let urls: string[]\n    const submit = await transport.submit(await buildSubmitInput(input, sdkConfig.modelId, ctx.signal))\n    if (submit.imageUrls) {\n      urls = submit.imageUrls\n    } else if (submit.taskId) {\n      urls = await pollUntilDone(transport, submit.taskId, ctx)\n    } else {\n      // A malformed submit response (neither URLs nor a task id) must fail the\n      // job rather than silently complete with zero files (a paid no-op).\n      throw new Error(`Image generation submit for '${sdkConfig.modelId}' returned neither imageUrls nor a taskId`)\n    }\n","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/provider/custom/tasks/imageGenerationJobHandler.ts#L74-L110","documentation":"Thrown in imageGenerationJobHandler.execute() when resolveImageTransport(providerId, modelId, settings) returns null. The imageTransportRegistry only registers poll-capable transports for ppio, dashscope, modelscope, and dmxapi-bespoke (gated by dmxapiUsesCustomTransport). A null means the provider/model is not in that registry — either a provider without an async transport was routed into the job path, or the registry's `supports` predicate rejected the model.","triggerScenarios":"An image-generation job is enqueued for a provider that has no async transport: ollama, ovms, silicon, moonshot, newapi, or a dmxapi model not matched by dmxapiUsesCustomTransport. Also if hasImageTransport returned true at enqueue time but the predicate disagrees at execute (registry drift), or providerId is unknown to the registry.","commonSituations":"Job path selected for a single-shot/sync provider (ollama/ovms/silicon) that should use the in-SDK doGenerate path instead; new provider added without registering a transport; DMXAPI model outside the bespoke set routed to the job path.","solutions":["Gate the job path on hasImageTransport(providerId, modelId) before enqueue — single-shot providers must stay on the synchronous doGenerate path.","If the provider genuinely needs async submit/poll, register a TransportRegistration in imageTransportRegistry.ts (supports + build).","For DMXAPI, confirm dmxapiUsesCustomTransport(modelId) returns true for that model family.","Surface a user-facing 'this model does not support async image generation' message instead of the generic throw."],"exampleFix":"// before\nconst transport = resolveImageTransport(providerId, modelId, settings)\nif (!transport) throw new Error(`... no async transport ...`)\n// after — fail earlier at enqueue with an actionable message\nif (!hasImageTransport(providerId, modelId)) {\n  throw new Error(`Model '${modelId}' (provider '${providerId}') has no async image transport; use the synchronous path`)\n}","handlingStrategy":"validation","validationCode":"// Gate the job path on transport existence before enqueue\nif (!hasImageTransport(providerId, modelId)) {\n  throw new Error(`No async image transport for '${providerId}/${modelId}'; use the synchronous path`)\n}","typeGuard":"export function isNoAsyncTransportError(e: unknown): boolean {\n  return e instanceof Error && /no async transport for/.test(e.message)\n}","tryCatchPattern":"try {\n  await job.execute(ctx)\n} catch (e) {\n  if (isNoAsyncTransportError(e)) {\n    // route to the synchronous doGenerate path instead\n  }\n  throw e\n}","preventionTips":["Gate the job path on hasImageTransport(providerId, modelId) at enqueue.","Register a TransportRegistration for any provider that needs async submit/poll.","Keep single-shot providers (ollama/ovms/silicon) on the synchronous in-SDK path."],"tags":["job-handler","transport-registry","configuration","image-generation","misroute"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}