qwen-image-adapter.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /**
  2. * Qwen Image (Alibaba Cloud / DashScope) Image Generation Adapter
  3. *
  4. * Uses DashScope multimodal generation API (synchronous, no polling needed).
  5. * Endpoint: https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation
  6. *
  7. * Supported models:
  8. * - qwen-image-max (highest quality)
  9. * - z-image-turbo (fast, good quality)
  10. *
  11. * API docs: https://help.aliyun.com/zh/model-studio/developer-reference/text-to-image
  12. */
  13. import type {
  14. ImageGenerationConfig,
  15. ImageGenerationOptions,
  16. ImageGenerationResult,
  17. } from '../types';
  18. const DEFAULT_MODEL = 'qwen-image-max';
  19. const DEFAULT_BASE_URL = 'https://dashscope.aliyuncs.com';
  20. /**
  21. * Map our width x height to DashScope size format "WxH".
  22. * Common sizes: 1024*1024, 1280*720, 1664*928, 1120*1440, etc.
  23. */
  24. function resolveDashScopeSize(options: ImageGenerationOptions): string {
  25. const w = options.width || 1024;
  26. const h = options.height || 576;
  27. return `${w}*${h}`;
  28. }
  29. /**
  30. * Lightweight connectivity test — validates API key by making a minimal
  31. * request. 401/403 means key invalid; other errors mean key is valid.
  32. */
  33. export async function testQwenImageConnectivity(
  34. config: ImageGenerationConfig,
  35. ): Promise<{ success: boolean; message: string }> {
  36. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  37. try {
  38. const response = await fetch(
  39. `${baseUrl}/api/v1/services/aigc/multimodal-generation/generation`,
  40. {
  41. method: 'POST',
  42. headers: {
  43. 'Content-Type': 'application/json',
  44. Authorization: `Bearer ${config.apiKey}`,
  45. },
  46. body: JSON.stringify({
  47. model: config.model || DEFAULT_MODEL,
  48. input: { messages: [{ role: 'user', content: [{ text: '' }] }] },
  49. parameters: { size: '1*1' },
  50. }),
  51. },
  52. );
  53. if (response.status === 401 || response.status === 403) {
  54. const text = await response.text();
  55. return {
  56. success: false,
  57. message: `Qwen Image auth failed (${response.status}): ${text}`,
  58. };
  59. }
  60. return { success: true, message: 'Connected to Qwen Image' };
  61. } catch (err) {
  62. return { success: false, message: `Qwen Image connectivity error: ${err}` };
  63. }
  64. }
  65. export async function generateWithQwenImage(
  66. config: ImageGenerationConfig,
  67. options: ImageGenerationOptions,
  68. ): Promise<ImageGenerationResult> {
  69. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  70. const response = await fetch(`${baseUrl}/api/v1/services/aigc/multimodal-generation/generation`, {
  71. method: 'POST',
  72. headers: {
  73. 'Content-Type': 'application/json',
  74. Authorization: `Bearer ${config.apiKey}`,
  75. },
  76. body: JSON.stringify({
  77. model: config.model || DEFAULT_MODEL,
  78. input: {
  79. messages: [
  80. {
  81. role: 'user',
  82. content: [
  83. {
  84. text: options.prompt,
  85. },
  86. ],
  87. },
  88. ],
  89. },
  90. parameters: {
  91. negative_prompt: options.negativePrompt || undefined,
  92. prompt_extend: true,
  93. watermark: false,
  94. size: resolveDashScopeSize(options),
  95. },
  96. }),
  97. });
  98. if (!response.ok) {
  99. const text = await response.text();
  100. throw new Error(`Qwen Image generation failed (${response.status}): ${text}`);
  101. }
  102. const data = await response.json();
  103. // DashScope multimodal generation response format:
  104. // { output: { choices: [{ message: { content: [{ image: "url" }] } }] } }
  105. const choices = data.output?.choices;
  106. if (!choices || choices.length === 0) {
  107. // Check for error in response
  108. if (data.code || data.message) {
  109. throw new Error(`Qwen Image error: ${data.code} - ${data.message}`);
  110. }
  111. throw new Error('Qwen Image returned empty response');
  112. }
  113. const content = choices[0]?.message?.content;
  114. const imageContent = content?.find((c: { image?: string }) => c.image);
  115. if (!imageContent?.image) {
  116. throw new Error('Qwen Image response missing image URL');
  117. }
  118. return {
  119. url: imageContent.image,
  120. width: options.width || 1024,
  121. height: options.height || 576,
  122. };
  123. }