hazo_llm_api 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/layout/index.d.ts +7 -0
- package/dist/components/layout/index.d.ts.map +1 -0
- package/dist/components/layout/index.js +7 -0
- package/dist/components/layout/index.js.map +1 -0
- package/dist/components/layout/layout.d.ts +21 -0
- package/dist/components/layout/layout.d.ts.map +1 -0
- package/dist/components/layout/layout.js +18 -0
- package/dist/components/layout/layout.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config/config_parser.d.ts +131 -0
- package/dist/lib/config/config_parser.d.ts.map +1 -0
- package/dist/lib/config/config_parser.js +297 -0
- package/dist/lib/config/config_parser.js.map +1 -0
- package/dist/lib/config/index.d.ts +8 -0
- package/dist/lib/config/index.d.ts.map +1 -0
- package/dist/lib/config/index.js +22 -0
- package/dist/lib/config/index.js.map +1 -0
- package/dist/lib/config/provider_loader.d.ts +113 -0
- package/dist/lib/config/provider_loader.d.ts.map +1 -0
- package/dist/lib/config/provider_loader.js +169 -0
- package/dist/lib/config/provider_loader.js.map +1 -0
- package/dist/lib/database/index.d.ts +8 -0
- package/dist/lib/database/index.d.ts.map +1 -0
- package/dist/lib/database/index.js +10 -0
- package/dist/lib/database/index.js.map +1 -0
- package/dist/lib/database/init_database.d.ts +118 -0
- package/dist/lib/database/init_database.d.ts.map +1 -0
- package/dist/lib/database/init_database.js +591 -0
- package/dist/lib/database/init_database.js.map +1 -0
- package/dist/lib/database/utils.d.ts +53 -0
- package/dist/lib/database/utils.d.ts.map +1 -0
- package/dist/lib/database/utils.js +87 -0
- package/dist/lib/database/utils.js.map +1 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +17 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/llm_api/chain_helpers.d.ts +117 -0
- package/dist/lib/llm_api/chain_helpers.d.ts.map +1 -0
- package/dist/lib/llm_api/chain_helpers.js +445 -0
- package/dist/lib/llm_api/chain_helpers.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_image.d.ts +26 -0
- package/dist/lib/llm_api/hazo_llm_image_image.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_image.js +94 -0
- package/dist/lib/llm_api/hazo_llm_image_image.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_image_text.d.ts +26 -0
- package/dist/lib/llm_api/hazo_llm_image_image_text.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_image_text.js +222 -0
- package/dist/lib/llm_api/hazo_llm_image_image_text.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_text.d.ts +20 -0
- package/dist/lib/llm_api/hazo_llm_image_text.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_image_text.js +78 -0
- package/dist/lib/llm_api/hazo_llm_image_text.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_prompt_chain.d.ts +20 -0
- package/dist/lib/llm_api/hazo_llm_prompt_chain.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_prompt_chain.js +368 -0
- package/dist/lib/llm_api/hazo_llm_prompt_chain.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_image.d.ts +20 -0
- package/dist/lib/llm_api/hazo_llm_text_image.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_image.js +69 -0
- package/dist/lib/llm_api/hazo_llm_text_image.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_image_text.d.ts +26 -0
- package/dist/lib/llm_api/hazo_llm_text_image_text.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_image_text.js +154 -0
- package/dist/lib/llm_api/hazo_llm_text_image_text.js.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_text.d.ts +20 -0
- package/dist/lib/llm_api/hazo_llm_text_text.d.ts.map +1 -0
- package/dist/lib/llm_api/hazo_llm_text_text.js +91 -0
- package/dist/lib/llm_api/hazo_llm_text_text.js.map +1 -0
- package/dist/lib/llm_api/index.d.ts +223 -0
- package/dist/lib/llm_api/index.d.ts.map +1 -0
- package/dist/lib/llm_api/index.js +1220 -0
- package/dist/lib/llm_api/index.js.map +1 -0
- package/dist/lib/llm_api/provider_helper.d.ts +163 -0
- package/dist/lib/llm_api/provider_helper.d.ts.map +1 -0
- package/dist/lib/llm_api/provider_helper.js +346 -0
- package/dist/lib/llm_api/provider_helper.js.map +1 -0
- package/dist/lib/llm_api/types.d.ts +667 -0
- package/dist/lib/llm_api/types.d.ts.map +1 -0
- package/dist/lib/llm_api/types.js +49 -0
- package/dist/lib/llm_api/types.js.map +1 -0
- package/dist/lib/prompts/get_prompt.d.ts +76 -0
- package/dist/lib/prompts/get_prompt.d.ts.map +1 -0
- package/dist/lib/prompts/get_prompt.js +342 -0
- package/dist/lib/prompts/get_prompt.js.map +1 -0
- package/dist/lib/prompts/index.d.ts +9 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/index.js +9 -0
- package/dist/lib/prompts/index.js.map +1 -0
- package/dist/lib/prompts/prompt_cache.d.ts +151 -0
- package/dist/lib/prompts/prompt_cache.d.ts.map +1 -0
- package/dist/lib/prompts/prompt_cache.js +276 -0
- package/dist/lib/prompts/prompt_cache.js.map +1 -0
- package/dist/lib/prompts/substitute_variables.d.ts +38 -0
- package/dist/lib/prompts/substitute_variables.d.ts.map +1 -0
- package/dist/lib/prompts/substitute_variables.js +171 -0
- package/dist/lib/prompts/substitute_variables.js.map +1 -0
- package/dist/lib/providers/gemini/gemini_client.d.ts +25 -0
- package/dist/lib/providers/gemini/gemini_client.d.ts.map +1 -0
- package/dist/lib/providers/gemini/gemini_client.js +235 -0
- package/dist/lib/providers/gemini/gemini_client.js.map +1 -0
- package/dist/lib/providers/gemini/gemini_provider.d.ts +111 -0
- package/dist/lib/providers/gemini/gemini_provider.d.ts.map +1 -0
- package/dist/lib/providers/gemini/gemini_provider.js +431 -0
- package/dist/lib/providers/gemini/gemini_provider.js.map +1 -0
- package/dist/lib/providers/gemini/index.d.ts +8 -0
- package/dist/lib/providers/gemini/index.d.ts.map +1 -0
- package/dist/lib/providers/gemini/index.js +8 -0
- package/dist/lib/providers/gemini/index.js.map +1 -0
- package/dist/lib/providers/index.d.ts +8 -0
- package/dist/lib/providers/index.d.ts.map +1 -0
- package/dist/lib/providers/index.js +8 -0
- package/dist/lib/providers/index.js.map +1 -0
- package/dist/lib/providers/qwen/index.d.ts +8 -0
- package/dist/lib/providers/qwen/index.d.ts.map +1 -0
- package/dist/lib/providers/qwen/index.js +8 -0
- package/dist/lib/providers/qwen/index.js.map +1 -0
- package/dist/lib/providers/qwen/qwen_client.d.ts +154 -0
- package/dist/lib/providers/qwen/qwen_client.d.ts.map +1 -0
- package/dist/lib/providers/qwen/qwen_client.js +1002 -0
- package/dist/lib/providers/qwen/qwen_client.js.map +1 -0
- package/dist/lib/providers/qwen/qwen_provider.d.ts +139 -0
- package/dist/lib/providers/qwen/qwen_provider.d.ts.map +1 -0
- package/dist/lib/providers/qwen/qwen_provider.js +304 -0
- package/dist/lib/providers/qwen/qwen_provider.js.map +1 -0
- package/dist/lib/providers/registry.d.ts +66 -0
- package/dist/lib/providers/registry.d.ts.map +1 -0
- package/dist/lib/providers/registry.js +158 -0
- package/dist/lib/providers/registry.js.map +1 -0
- package/dist/lib/providers/types.d.ts +145 -0
- package/dist/lib/providers/types.d.ts.map +1 -0
- package/dist/lib/providers/types.js +37 -0
- package/dist/lib/providers/types.js.map +1 -0
- package/dist/server.d.ts +27 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +50 -0
- package/dist/server.js.map +1 -0
- package/package.json +12 -1
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen API Client Module
|
|
3
|
+
*
|
|
4
|
+
* Handles communication with Alibaba Cloud DashScope Qwen API.
|
|
5
|
+
* Uses OpenAI-compatible chat completions endpoint.
|
|
6
|
+
*/
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Qwen API Client
|
|
9
|
+
// =============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Call the Qwen API with messages and optional image data
|
|
12
|
+
* @param api_url - The Qwen API endpoint URL
|
|
13
|
+
* @param api_key - The API key for authentication
|
|
14
|
+
* @param model - The model name to use
|
|
15
|
+
* @param messages - Array of messages (system + user)
|
|
16
|
+
* @param logger - Logger instance
|
|
17
|
+
* @param generation_config - Optional generation configuration parameters
|
|
18
|
+
* @returns LLM response with generated text or error
|
|
19
|
+
*/
|
|
20
|
+
export async function call_qwen_api(api_url, api_key, model, messages, logger, generation_config) {
|
|
21
|
+
const file_name = 'qwen_client.ts';
|
|
22
|
+
try {
|
|
23
|
+
// Build the request body
|
|
24
|
+
const request_body = {
|
|
25
|
+
model,
|
|
26
|
+
messages,
|
|
27
|
+
};
|
|
28
|
+
// Add generation config parameters if provided
|
|
29
|
+
if (generation_config) {
|
|
30
|
+
if (generation_config.temperature !== undefined) {
|
|
31
|
+
request_body.temperature = generation_config.temperature;
|
|
32
|
+
}
|
|
33
|
+
if (generation_config.max_tokens !== undefined) {
|
|
34
|
+
request_body.max_tokens = generation_config.max_tokens;
|
|
35
|
+
}
|
|
36
|
+
if (generation_config.top_p !== undefined) {
|
|
37
|
+
request_body.top_p = generation_config.top_p;
|
|
38
|
+
}
|
|
39
|
+
if (generation_config.top_k !== undefined) {
|
|
40
|
+
request_body.top_k = generation_config.top_k;
|
|
41
|
+
}
|
|
42
|
+
if (generation_config.n !== undefined) {
|
|
43
|
+
request_body.n = generation_config.n;
|
|
44
|
+
}
|
|
45
|
+
if (generation_config.stop !== undefined && generation_config.stop.length > 0) {
|
|
46
|
+
request_body.stop = generation_config.stop;
|
|
47
|
+
}
|
|
48
|
+
if (generation_config.presence_penalty !== undefined) {
|
|
49
|
+
request_body.presence_penalty = generation_config.presence_penalty;
|
|
50
|
+
}
|
|
51
|
+
if (generation_config.frequency_penalty !== undefined) {
|
|
52
|
+
request_body.frequency_penalty = generation_config.frequency_penalty;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Log the API call
|
|
56
|
+
logger.debug('Calling Qwen API', {
|
|
57
|
+
file: file_name,
|
|
58
|
+
line: 125,
|
|
59
|
+
data: {
|
|
60
|
+
api_url,
|
|
61
|
+
model,
|
|
62
|
+
message_count: messages.length,
|
|
63
|
+
has_generation_config: !!generation_config,
|
|
64
|
+
generation_config: generation_config || 'none (using defaults)',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Make the API request
|
|
68
|
+
const response = await fetch(api_url, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Authorization': `Bearer ${api_key}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(request_body),
|
|
75
|
+
});
|
|
76
|
+
// Parse the response
|
|
77
|
+
const response_data = await response.json();
|
|
78
|
+
// Check for errors
|
|
79
|
+
if (!response.ok || response_data.error) {
|
|
80
|
+
const error_message = response_data.error?.message || `HTTP ${response.status}`;
|
|
81
|
+
logger.error('Qwen API returned error', {
|
|
82
|
+
file: file_name,
|
|
83
|
+
line: 152,
|
|
84
|
+
data: {
|
|
85
|
+
status: response.status,
|
|
86
|
+
error: response_data.error,
|
|
87
|
+
request_model: model,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: error_message,
|
|
93
|
+
raw_response: response_data,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Extract the generated text from response
|
|
97
|
+
const generated_text = extract_text_from_response(response_data, logger);
|
|
98
|
+
if (generated_text) {
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
text: generated_text,
|
|
102
|
+
raw_response: response_data,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.warn('No text content in Qwen response', {
|
|
107
|
+
file: file_name,
|
|
108
|
+
line: 177,
|
|
109
|
+
data: { raw_response: response_data },
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: 'No text content in response',
|
|
114
|
+
raw_response: response_data,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const error_message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
logger.error('Failed to call Qwen API', {
|
|
121
|
+
file: file_name,
|
|
122
|
+
line: 188,
|
|
123
|
+
data: { error: error_message, model },
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: error_message,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Response Parsing Functions
|
|
133
|
+
// =============================================================================
|
|
134
|
+
/**
|
|
135
|
+
* Extract text content from Qwen API response
|
|
136
|
+
* @param response - The Qwen API response
|
|
137
|
+
* @param logger - Logger instance
|
|
138
|
+
* @returns The generated text or null if not found
|
|
139
|
+
*/
|
|
140
|
+
function extract_text_from_response(response, logger) {
|
|
141
|
+
const file_name = 'qwen_client.ts';
|
|
142
|
+
if (!response.choices || response.choices.length === 0) {
|
|
143
|
+
logger.debug('No choices in Qwen response', {
|
|
144
|
+
file: file_name,
|
|
145
|
+
line: 210,
|
|
146
|
+
});
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const first_choice = response.choices[0];
|
|
150
|
+
if (!first_choice.message || !first_choice.message.content) {
|
|
151
|
+
logger.debug('No message content in first choice', {
|
|
152
|
+
file: file_name,
|
|
153
|
+
line: 218,
|
|
154
|
+
});
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return first_choice.message.content;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the default Qwen API URL
|
|
161
|
+
* @returns The default DashScope API URL
|
|
162
|
+
*/
|
|
163
|
+
export function get_qwen_api_url() {
|
|
164
|
+
return 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get the DashScope image generation API URL
|
|
168
|
+
* @param model - The model name (e.g., qwen-image-plus)
|
|
169
|
+
* @returns The DashScope image generation API URL
|
|
170
|
+
*/
|
|
171
|
+
export function get_qwen_image_api_url(model) {
|
|
172
|
+
// DashScope image generation endpoint format
|
|
173
|
+
// Try the standard text-to-image endpoint first
|
|
174
|
+
return 'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis';
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get the DashScope image editing API URL
|
|
178
|
+
* @param model - The model name (e.g., qwen-image-edit)
|
|
179
|
+
* @returns The DashScope image editing API URL
|
|
180
|
+
*/
|
|
181
|
+
export function get_qwen_image_edit_api_url(model) {
|
|
182
|
+
// DashScope image editing uses MultiModalConversation API
|
|
183
|
+
// Uses the same chat completions endpoint as other multimodal operations
|
|
184
|
+
return 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions';
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build Qwen messages array from prompt and optional images
|
|
188
|
+
* @param prompt - The text prompt
|
|
189
|
+
* @param system_instruction - Optional system instruction
|
|
190
|
+
* @param b64_data - Optional array of base64 encoded images
|
|
191
|
+
* @returns Array of Qwen messages
|
|
192
|
+
*/
|
|
193
|
+
export function build_qwen_messages(prompt, system_instruction, b64_data) {
|
|
194
|
+
const messages = [];
|
|
195
|
+
// Add system message if provided
|
|
196
|
+
if (system_instruction) {
|
|
197
|
+
messages.push({
|
|
198
|
+
role: 'system',
|
|
199
|
+
content: system_instruction,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Build user message content
|
|
203
|
+
if (b64_data && b64_data.length > 0) {
|
|
204
|
+
// Multi-modal message with images
|
|
205
|
+
const content = [];
|
|
206
|
+
// Add images first
|
|
207
|
+
for (const img of b64_data) {
|
|
208
|
+
content.push({
|
|
209
|
+
type: 'image_url',
|
|
210
|
+
image_url: {
|
|
211
|
+
url: `data:${img.mime_type};base64,${img.data}`,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Add text prompt
|
|
216
|
+
content.push({
|
|
217
|
+
type: 'text',
|
|
218
|
+
text: prompt,
|
|
219
|
+
});
|
|
220
|
+
messages.push({
|
|
221
|
+
role: 'user',
|
|
222
|
+
content,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Text-only message
|
|
227
|
+
messages.push({
|
|
228
|
+
role: 'user',
|
|
229
|
+
content: prompt,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return messages;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Call the DashScope image generation API
|
|
236
|
+
* @param api_url - The DashScope image generation API endpoint URL
|
|
237
|
+
* @param api_key - The API key for authentication
|
|
238
|
+
* @param model - The model name to use (e.g., qwen-image-plus)
|
|
239
|
+
* @param prompt - The text prompt for image generation
|
|
240
|
+
* @param logger - Logger instance
|
|
241
|
+
* @param generation_config - Optional generation configuration parameters
|
|
242
|
+
* @returns LLM response with generated image or error
|
|
243
|
+
*/
|
|
244
|
+
export async function call_qwen_image_api(api_url, api_key, model, prompt, logger, generation_config) {
|
|
245
|
+
const file_name = 'qwen_client.ts';
|
|
246
|
+
try {
|
|
247
|
+
// Build the request body for image generation
|
|
248
|
+
// DashScope image generation API format
|
|
249
|
+
// Allowed sizes: 1664*928, 1472*1140, 1328*1328, 1140*1472, 928*1664
|
|
250
|
+
const request_body = {
|
|
251
|
+
model,
|
|
252
|
+
input: {
|
|
253
|
+
prompt,
|
|
254
|
+
},
|
|
255
|
+
parameters: {
|
|
256
|
+
// Use square format as default (1328*1328)
|
|
257
|
+
size: '1328*1328',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
// Add generation config parameters if provided
|
|
261
|
+
// Note: DashScope image generation may support different parameters
|
|
262
|
+
// For now, we'll use basic parameters
|
|
263
|
+
if (generation_config) {
|
|
264
|
+
const params = {};
|
|
265
|
+
if (generation_config.n !== undefined) {
|
|
266
|
+
params.n = generation_config.n;
|
|
267
|
+
}
|
|
268
|
+
// Add other parameters as needed for image generation
|
|
269
|
+
if (Object.keys(params).length > 0) {
|
|
270
|
+
request_body.parameters = params;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Log the API call
|
|
274
|
+
logger.debug('Calling DashScope image generation API', {
|
|
275
|
+
file: file_name,
|
|
276
|
+
line: 380,
|
|
277
|
+
data: {
|
|
278
|
+
api_url,
|
|
279
|
+
model,
|
|
280
|
+
prompt_length: prompt.length,
|
|
281
|
+
has_generation_config: !!generation_config,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
// Make the API request
|
|
285
|
+
// DashScope image generation requires async mode
|
|
286
|
+
const response = await fetch(api_url, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'Authorization': `Bearer ${api_key}`,
|
|
291
|
+
'X-DashScope-Async': 'enable', // Enable async mode
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify(request_body),
|
|
294
|
+
});
|
|
295
|
+
let parsed_response;
|
|
296
|
+
const response_text = await response.text();
|
|
297
|
+
try {
|
|
298
|
+
parsed_response = JSON.parse(response_text);
|
|
299
|
+
// Log the initial response to see what we get
|
|
300
|
+
logger.debug('DashScope image generation initial response', {
|
|
301
|
+
file: file_name,
|
|
302
|
+
line: 450,
|
|
303
|
+
data: {
|
|
304
|
+
status: response.status,
|
|
305
|
+
response_preview: response_text.substring(0, 1000),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// If not JSON, use the text as error message
|
|
311
|
+
parsed_response = { error: { message: response_text } };
|
|
312
|
+
}
|
|
313
|
+
// Check for errors
|
|
314
|
+
if (!response.ok || parsed_response.error || parsed_response.code) {
|
|
315
|
+
const error_message = parsed_response.error?.message ||
|
|
316
|
+
parsed_response.message ||
|
|
317
|
+
`HTTP ${response.status}: ${response_text.substring(0, 500)}`;
|
|
318
|
+
logger.error('DashScope image generation API returned error', {
|
|
319
|
+
file: file_name,
|
|
320
|
+
line: 420,
|
|
321
|
+
data: {
|
|
322
|
+
status: response.status,
|
|
323
|
+
status_text: response.statusText,
|
|
324
|
+
error: parsed_response.error,
|
|
325
|
+
code: parsed_response.code,
|
|
326
|
+
message: parsed_response.message,
|
|
327
|
+
request_model: model,
|
|
328
|
+
request_body: JSON.stringify(request_body),
|
|
329
|
+
full_response: response_text.substring(0, 1000), // Limit to first 1000 chars
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
error: error_message,
|
|
335
|
+
raw_response: parsed_response,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Handle async task - if task_id is returned, we need to poll for results
|
|
339
|
+
const task_id = parsed_response.output?.task_id || parsed_response.task_id;
|
|
340
|
+
if (task_id && !parsed_response.output?.results) {
|
|
341
|
+
// Async mode - poll for results
|
|
342
|
+
logger.info('DashScope returned async task_id, polling for results', {
|
|
343
|
+
file: file_name,
|
|
344
|
+
line: 440,
|
|
345
|
+
data: { task_id },
|
|
346
|
+
});
|
|
347
|
+
// Poll the task status endpoint
|
|
348
|
+
// DashScope uses: /api/v1/tasks/{task_id}
|
|
349
|
+
const status_url = `https://dashscope-intl.aliyuncs.com/api/v1/tasks/${task_id}`;
|
|
350
|
+
let attempts = 0;
|
|
351
|
+
const max_attempts = 60; // Poll for up to 60 seconds (image generation can take time)
|
|
352
|
+
while (attempts < max_attempts) {
|
|
353
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds between polls
|
|
354
|
+
const status_response = await fetch(status_url, {
|
|
355
|
+
headers: {
|
|
356
|
+
'Authorization': `Bearer ${api_key}`,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
if (!status_response.ok) {
|
|
360
|
+
const error_text = await status_response.text();
|
|
361
|
+
logger.warn('Task status check failed', {
|
|
362
|
+
file: file_name,
|
|
363
|
+
line: 510,
|
|
364
|
+
data: { status: status_response.status, error: error_text },
|
|
365
|
+
});
|
|
366
|
+
attempts++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const status_data_raw = await status_response.json();
|
|
370
|
+
const status_data = status_data_raw;
|
|
371
|
+
// Check task status FIRST - if it's "FAILED" or "CANCELED", return error immediately
|
|
372
|
+
const task_status = status_data_raw.output?.task_status ||
|
|
373
|
+
status_data_raw.task_status ||
|
|
374
|
+
status_data_raw.status;
|
|
375
|
+
// Log the status response for debugging (less frequently to reduce log noise)
|
|
376
|
+
if (attempts % 5 === 0 || task_status === 'FAILED' || task_status === 'SUCCEEDED') {
|
|
377
|
+
logger.debug('Polling task status response', {
|
|
378
|
+
file: file_name,
|
|
379
|
+
line: 530,
|
|
380
|
+
data: {
|
|
381
|
+
task_id,
|
|
382
|
+
attempt: attempts + 1,
|
|
383
|
+
max_attempts,
|
|
384
|
+
response_status: status_response.status,
|
|
385
|
+
task_status,
|
|
386
|
+
response_data: JSON.stringify(status_data_raw).substring(0, 500), // First 500 chars
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
// Check for FAILED or CANCELED status immediately
|
|
391
|
+
if (task_status === 'FAILED' || task_status === 'CANCELED') {
|
|
392
|
+
// Extract error message from output if available
|
|
393
|
+
const output = status_data_raw.output;
|
|
394
|
+
const error_msg = output?.message ||
|
|
395
|
+
status_data.error?.message ||
|
|
396
|
+
status_data.message ||
|
|
397
|
+
`Task ${task_status.toLowerCase()}`;
|
|
398
|
+
logger.error('Image generation task failed', {
|
|
399
|
+
file: file_name,
|
|
400
|
+
line: 545,
|
|
401
|
+
data: { task_id, task_status, error_code: output?.code, error_message: error_msg },
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
error: error_msg,
|
|
406
|
+
raw_response: status_data,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// Check if task is complete and has results
|
|
410
|
+
// DashScope may return results in different structures
|
|
411
|
+
if (status_data.output?.results && status_data.output.results.length > 0) {
|
|
412
|
+
// Results are ready
|
|
413
|
+
parsed_response.output = status_data.output;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
// Also check for direct results in response (some API versions)
|
|
417
|
+
if (status_data_raw.results &&
|
|
418
|
+
Array.isArray(status_data_raw.results) &&
|
|
419
|
+
status_data_raw.results.length > 0) {
|
|
420
|
+
parsed_response.output = {
|
|
421
|
+
results: status_data_raw.results,
|
|
422
|
+
};
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
// If task is SUCCEEDED but no results yet, continue polling
|
|
426
|
+
if (task_status === 'SUCCEEDED') {
|
|
427
|
+
// Results should be available, but if not, continue polling
|
|
428
|
+
attempts++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// Check for task failure in error fields
|
|
432
|
+
if (status_data.error || status_data.code) {
|
|
433
|
+
const error_msg = status_data.error?.message || status_data.message || 'Task failed';
|
|
434
|
+
return {
|
|
435
|
+
success: false,
|
|
436
|
+
error: error_msg,
|
|
437
|
+
raw_response: status_data,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
// If task is still PENDING or RUNNING, continue polling
|
|
441
|
+
if (task_status === 'PENDING' || task_status === 'RUNNING' || !task_status) {
|
|
442
|
+
attempts++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// Default: continue polling
|
|
446
|
+
attempts++;
|
|
447
|
+
}
|
|
448
|
+
if (attempts >= max_attempts) {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
error: 'Image generation timed out - task did not complete in time',
|
|
452
|
+
raw_response: parsed_response,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Extract image URL from response
|
|
457
|
+
const results = parsed_response.output?.results;
|
|
458
|
+
if (results && results.length > 0 && results[0].url) {
|
|
459
|
+
// Fetch the image from the URL and convert to base64
|
|
460
|
+
const image_url = results[0].url;
|
|
461
|
+
const image_response = await fetch(image_url);
|
|
462
|
+
if (!image_response.ok) {
|
|
463
|
+
return {
|
|
464
|
+
success: false,
|
|
465
|
+
error: `Failed to fetch generated image: HTTP ${image_response.status}`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const image_array_buffer = await image_response.arrayBuffer();
|
|
469
|
+
// Convert ArrayBuffer to base64 in chunks to avoid stack overflow
|
|
470
|
+
const uint8_array = new Uint8Array(image_array_buffer);
|
|
471
|
+
const chunk_size = 8192; // Process in 8KB chunks
|
|
472
|
+
let binary_string = '';
|
|
473
|
+
for (let i = 0; i < uint8_array.length; i += chunk_size) {
|
|
474
|
+
const chunk = uint8_array.slice(i, i + chunk_size);
|
|
475
|
+
binary_string += String.fromCharCode(...chunk);
|
|
476
|
+
}
|
|
477
|
+
const image_base64 = btoa(binary_string);
|
|
478
|
+
// Get content type from response headers
|
|
479
|
+
const content_type = image_response.headers.get('content-type') || 'image/png';
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
image_b64: image_base64,
|
|
483
|
+
image_mime_type: content_type,
|
|
484
|
+
raw_response: parsed_response,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
logger.warn('No image URL in DashScope response', {
|
|
488
|
+
file: file_name,
|
|
489
|
+
line: 470,
|
|
490
|
+
data: { raw_response: parsed_response },
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
error: 'No image URL in response',
|
|
495
|
+
raw_response: parsed_response,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const error_message = error instanceof Error ? error.message : String(error);
|
|
500
|
+
logger.error('Failed to call DashScope image generation API', {
|
|
501
|
+
file: file_name,
|
|
502
|
+
line: 480,
|
|
503
|
+
data: { error: error_message, model },
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
error: error_message,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Call the DashScope image editing API
|
|
513
|
+
* @param api_url - The DashScope image editing API endpoint URL
|
|
514
|
+
* @param api_key - The API key for authentication
|
|
515
|
+
* @param model - The model name to use (e.g., qwen-image-edit)
|
|
516
|
+
* @param prompt - The text prompt describing the transformation
|
|
517
|
+
* @param input_images - Array of base64 encoded input images with mime types (1-3 images supported)
|
|
518
|
+
* @param logger - Logger instance
|
|
519
|
+
* @param generation_config - Optional generation configuration parameters
|
|
520
|
+
* @returns LLM response with edited image or error
|
|
521
|
+
*/
|
|
522
|
+
export async function call_qwen_image_edit_api(api_url, api_key, model, prompt, input_images, logger, generation_config) {
|
|
523
|
+
const file_name = 'qwen_client.ts';
|
|
524
|
+
try {
|
|
525
|
+
// Check if this is the multimodal generation endpoint (different format)
|
|
526
|
+
const is_multimodal_endpoint = api_url.includes('/multimodal-generation/');
|
|
527
|
+
let request_body;
|
|
528
|
+
if (is_multimodal_endpoint) {
|
|
529
|
+
// Multimodal generation endpoint uses input format with images array
|
|
530
|
+
// Support multiple images (1-3 images as per API requirements)
|
|
531
|
+
const content = [];
|
|
532
|
+
// Add all images first
|
|
533
|
+
for (const img of input_images) {
|
|
534
|
+
content.push({
|
|
535
|
+
image: `data:${img.mime_type};base64,${img.data}`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// Add text prompt after images
|
|
539
|
+
content.push({
|
|
540
|
+
text: prompt,
|
|
541
|
+
});
|
|
542
|
+
request_body = {
|
|
543
|
+
model,
|
|
544
|
+
input: {
|
|
545
|
+
messages: [
|
|
546
|
+
{
|
|
547
|
+
role: 'user',
|
|
548
|
+
content,
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
parameters: {
|
|
553
|
+
result_format: 'message',
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// Chat completions endpoint uses messages format
|
|
559
|
+
// Support multiple images
|
|
560
|
+
const messages = build_qwen_messages(prompt, undefined, input_images);
|
|
561
|
+
request_body = {
|
|
562
|
+
model,
|
|
563
|
+
messages,
|
|
564
|
+
result_format: 'message', // Required for multimodal responses with images
|
|
565
|
+
stream: false, // Disable streaming for image editing
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// Add generation config parameters if provided
|
|
569
|
+
if (generation_config) {
|
|
570
|
+
if (is_multimodal_endpoint) {
|
|
571
|
+
// For multimodal endpoint, add to parameters object
|
|
572
|
+
const params = request_body.parameters;
|
|
573
|
+
if (generation_config.temperature !== undefined) {
|
|
574
|
+
params.temperature = generation_config.temperature;
|
|
575
|
+
}
|
|
576
|
+
if (generation_config.max_tokens !== undefined) {
|
|
577
|
+
params.max_tokens = generation_config.max_tokens;
|
|
578
|
+
}
|
|
579
|
+
if (generation_config.top_p !== undefined) {
|
|
580
|
+
params.top_p = generation_config.top_p;
|
|
581
|
+
}
|
|
582
|
+
if (generation_config.top_k !== undefined) {
|
|
583
|
+
params.top_k = generation_config.top_k;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// For chat completions endpoint, add to root
|
|
588
|
+
if (generation_config.temperature !== undefined) {
|
|
589
|
+
request_body.temperature = generation_config.temperature;
|
|
590
|
+
}
|
|
591
|
+
if (generation_config.max_tokens !== undefined) {
|
|
592
|
+
request_body.max_tokens = generation_config.max_tokens;
|
|
593
|
+
}
|
|
594
|
+
if (generation_config.top_p !== undefined) {
|
|
595
|
+
request_body.top_p = generation_config.top_p;
|
|
596
|
+
}
|
|
597
|
+
if (generation_config.top_k !== undefined) {
|
|
598
|
+
request_body.top_k = generation_config.top_k;
|
|
599
|
+
}
|
|
600
|
+
if (generation_config.n !== undefined) {
|
|
601
|
+
request_body.n = generation_config.n;
|
|
602
|
+
}
|
|
603
|
+
if (generation_config.stop !== undefined && generation_config.stop.length > 0) {
|
|
604
|
+
request_body.stop = generation_config.stop;
|
|
605
|
+
}
|
|
606
|
+
if (generation_config.presence_penalty !== undefined) {
|
|
607
|
+
request_body.presence_penalty = generation_config.presence_penalty;
|
|
608
|
+
}
|
|
609
|
+
if (generation_config.frequency_penalty !== undefined) {
|
|
610
|
+
request_body.frequency_penalty = generation_config.frequency_penalty;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Log the API call
|
|
615
|
+
logger.debug('Calling DashScope image editing API', {
|
|
616
|
+
file: file_name,
|
|
617
|
+
line: 730,
|
|
618
|
+
data: {
|
|
619
|
+
api_url,
|
|
620
|
+
model,
|
|
621
|
+
prompt_length: prompt.length,
|
|
622
|
+
image_count: input_images.length,
|
|
623
|
+
image_mime_types: input_images.map(img => img.mime_type),
|
|
624
|
+
has_generation_config: !!generation_config,
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
// Make the API request
|
|
628
|
+
// DashScope image editing uses chat completions endpoint
|
|
629
|
+
const response = await fetch(api_url, {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers: {
|
|
632
|
+
'Content-Type': 'application/json',
|
|
633
|
+
'Authorization': `Bearer ${api_key}`,
|
|
634
|
+
},
|
|
635
|
+
body: JSON.stringify(request_body),
|
|
636
|
+
});
|
|
637
|
+
// Parse the response - handle both chat completions and multimodal generation formats
|
|
638
|
+
const raw_response = await response.json();
|
|
639
|
+
// Log the initial response
|
|
640
|
+
logger.debug('DashScope image editing initial response', {
|
|
641
|
+
file: file_name,
|
|
642
|
+
line: 854,
|
|
643
|
+
data: {
|
|
644
|
+
status: response.status,
|
|
645
|
+
response_preview: JSON.stringify(raw_response).substring(0, 1000),
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
// Check for errors (handle both response formats)
|
|
649
|
+
// For multimodal endpoint, successful responses have 'output' field, errors have 'error' or 'code' field
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
// HTTP error status
|
|
652
|
+
const error_data = raw_response.error;
|
|
653
|
+
const error_message = error_data?.message ||
|
|
654
|
+
raw_response?.message ||
|
|
655
|
+
`HTTP ${response.status}`;
|
|
656
|
+
logger.error('DashScope image editing API returned HTTP error', {
|
|
657
|
+
file: file_name,
|
|
658
|
+
line: 870,
|
|
659
|
+
data: {
|
|
660
|
+
status: response.status,
|
|
661
|
+
error: error_data,
|
|
662
|
+
request_model: model,
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
success: false,
|
|
667
|
+
error: error_message,
|
|
668
|
+
raw_response: raw_response,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// Check for API-level errors in response body
|
|
672
|
+
// Only check for actual error fields, not the entire response object
|
|
673
|
+
const error_field = raw_response.error;
|
|
674
|
+
const error_code = raw_response.code;
|
|
675
|
+
const has_error = error_field !== undefined || (error_code !== undefined && error_code !== '200' && error_code !== 'Success');
|
|
676
|
+
if (has_error) {
|
|
677
|
+
const error_message = error_field?.message ||
|
|
678
|
+
raw_response?.message ||
|
|
679
|
+
`API returned error code: ${error_code}`;
|
|
680
|
+
logger.error('DashScope image editing API returned error', {
|
|
681
|
+
file: file_name,
|
|
682
|
+
line: 895,
|
|
683
|
+
data: {
|
|
684
|
+
status: response.status,
|
|
685
|
+
error: error_field || { code: error_code, message: raw_response?.message },
|
|
686
|
+
request_model: model,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: error_message,
|
|
692
|
+
raw_response: raw_response,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
// Handle multimodal generation endpoint response format (wraps in output object)
|
|
696
|
+
let response_data;
|
|
697
|
+
let raw_response_data = raw_response;
|
|
698
|
+
if (is_multimodal_endpoint && raw_response.output) {
|
|
699
|
+
// Multimodal generation endpoint wraps response in output object
|
|
700
|
+
const multimodal_response = raw_response;
|
|
701
|
+
response_data = multimodal_response.output;
|
|
702
|
+
raw_response_data = multimodal_response; // Keep full response for logging
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Chat completions format (direct response)
|
|
706
|
+
response_data = raw_response;
|
|
707
|
+
}
|
|
708
|
+
// Extract image from response
|
|
709
|
+
// For image editing, the response content may contain image URLs or base64 data
|
|
710
|
+
if (!response_data.choices || response_data.choices.length === 0) {
|
|
711
|
+
logger.warn('No choices in DashScope image editing response', {
|
|
712
|
+
file: file_name,
|
|
713
|
+
line: 843,
|
|
714
|
+
data: {
|
|
715
|
+
raw_response: response_data,
|
|
716
|
+
response_keys: Object.keys(response_data || {}),
|
|
717
|
+
full_response: JSON.stringify(response_data, null, 2).substring(0, 2000),
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
return {
|
|
721
|
+
success: false,
|
|
722
|
+
error: 'No content in response - API returned no choices. Check logs for full response details.',
|
|
723
|
+
raw_response: response_data,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const first_choice = response_data.choices[0];
|
|
727
|
+
const content = first_choice.message?.content;
|
|
728
|
+
if (!content) {
|
|
729
|
+
logger.warn('No content in first choice', {
|
|
730
|
+
file: file_name,
|
|
731
|
+
line: 920,
|
|
732
|
+
data: {
|
|
733
|
+
raw_response: response_data,
|
|
734
|
+
first_choice: first_choice,
|
|
735
|
+
message: first_choice.message,
|
|
736
|
+
full_response: JSON.stringify(response_data, null, 2).substring(0, 2000),
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
success: false,
|
|
741
|
+
error: 'No content in response - message has no content. Check logs for full response details.',
|
|
742
|
+
raw_response: response_data,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
// Handle multimodal generation endpoint format - content is array with {image: "url"} objects
|
|
746
|
+
if (Array.isArray(content)) {
|
|
747
|
+
const content_array = content;
|
|
748
|
+
for (const item of content_array) {
|
|
749
|
+
// Check for multimodal format: {image: "url"} (multimodal generation endpoint)
|
|
750
|
+
if (item.image && typeof item.image === 'string') {
|
|
751
|
+
const image_url = item.image;
|
|
752
|
+
try {
|
|
753
|
+
const image_response = await fetch(image_url);
|
|
754
|
+
if (!image_response.ok) {
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
error: `Failed to fetch edited image: HTTP ${image_response.status}`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const image_array_buffer = await image_response.arrayBuffer();
|
|
761
|
+
const uint8_array = new Uint8Array(image_array_buffer);
|
|
762
|
+
const chunk_size = 8192;
|
|
763
|
+
let binary_string = '';
|
|
764
|
+
for (let i = 0; i < uint8_array.length; i += chunk_size) {
|
|
765
|
+
const chunk = uint8_array.slice(i, i + chunk_size);
|
|
766
|
+
binary_string += String.fromCharCode(...chunk);
|
|
767
|
+
}
|
|
768
|
+
const image_base64 = btoa(binary_string);
|
|
769
|
+
const content_type = image_response.headers.get('content-type') || 'image/png';
|
|
770
|
+
return {
|
|
771
|
+
success: true,
|
|
772
|
+
image_b64: image_base64,
|
|
773
|
+
image_mime_type: content_type,
|
|
774
|
+
raw_response: response_data,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
catch (fetch_error) {
|
|
778
|
+
logger.error('Failed to fetch image from URL', {
|
|
779
|
+
file: file_name,
|
|
780
|
+
line: 960,
|
|
781
|
+
data: { error: fetch_error instanceof Error ? fetch_error.message : String(fetch_error), url: image_url },
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
error: 'Failed to fetch image from URL in response',
|
|
786
|
+
raw_response: response_data,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Check for chat completions format: {image_url: {url: "..."}}
|
|
791
|
+
if (item.image_url?.url) {
|
|
792
|
+
const image_url = item.image_url.url;
|
|
793
|
+
// Handle data URLs
|
|
794
|
+
if (image_url.startsWith('data:')) {
|
|
795
|
+
const base64_match = image_url.match(/data:image\/([^;]+);base64,([A-Za-z0-9+/=]+)/);
|
|
796
|
+
if (base64_match) {
|
|
797
|
+
const mime_type = `image/${base64_match[1]}`;
|
|
798
|
+
const base64_data = base64_match[2];
|
|
799
|
+
return {
|
|
800
|
+
success: true,
|
|
801
|
+
image_b64: base64_data,
|
|
802
|
+
image_mime_type: mime_type,
|
|
803
|
+
raw_response: response_data,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
// Fetch from URL
|
|
809
|
+
try {
|
|
810
|
+
const image_response = await fetch(image_url);
|
|
811
|
+
if (!image_response.ok) {
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
error: `Failed to fetch edited image: HTTP ${image_response.status}`,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
const image_array_buffer = await image_response.arrayBuffer();
|
|
818
|
+
const uint8_array = new Uint8Array(image_array_buffer);
|
|
819
|
+
const chunk_size = 8192;
|
|
820
|
+
let binary_string = '';
|
|
821
|
+
for (let i = 0; i < uint8_array.length; i += chunk_size) {
|
|
822
|
+
const chunk = uint8_array.slice(i, i + chunk_size);
|
|
823
|
+
binary_string += String.fromCharCode(...chunk);
|
|
824
|
+
}
|
|
825
|
+
const image_base64 = btoa(binary_string);
|
|
826
|
+
const content_type = image_response.headers.get('content-type') || 'image/png';
|
|
827
|
+
return {
|
|
828
|
+
success: true,
|
|
829
|
+
image_b64: image_base64,
|
|
830
|
+
image_mime_type: content_type,
|
|
831
|
+
raw_response: response_data,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
catch (fetch_error) {
|
|
835
|
+
logger.error('Failed to fetch image from URL', {
|
|
836
|
+
file: file_name,
|
|
837
|
+
line: 1000,
|
|
838
|
+
data: { error: fetch_error instanceof Error ? fetch_error.message : String(fetch_error), url: image_url },
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
success: false,
|
|
842
|
+
error: 'Failed to fetch image from URL in response',
|
|
843
|
+
raw_response: response_data,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Check if content is a string (might contain image URL or base64)
|
|
851
|
+
if (typeof content === 'string') {
|
|
852
|
+
// Try to extract image URL or base64 from content
|
|
853
|
+
// Image URLs typically start with http:// or https://
|
|
854
|
+
const url_match = content.match(/https?:\/\/[^\s]+/);
|
|
855
|
+
if (url_match) {
|
|
856
|
+
const image_url = url_match[0];
|
|
857
|
+
try {
|
|
858
|
+
const image_response = await fetch(image_url);
|
|
859
|
+
if (!image_response.ok) {
|
|
860
|
+
return {
|
|
861
|
+
success: false,
|
|
862
|
+
error: `Failed to fetch edited image: HTTP ${image_response.status}`,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const image_array_buffer = await image_response.arrayBuffer();
|
|
866
|
+
const uint8_array = new Uint8Array(image_array_buffer);
|
|
867
|
+
const chunk_size = 8192;
|
|
868
|
+
let binary_string = '';
|
|
869
|
+
for (let i = 0; i < uint8_array.length; i += chunk_size) {
|
|
870
|
+
const chunk = uint8_array.slice(i, i + chunk_size);
|
|
871
|
+
binary_string += String.fromCharCode(...chunk);
|
|
872
|
+
}
|
|
873
|
+
const image_base64 = btoa(binary_string);
|
|
874
|
+
const content_type = image_response.headers.get('content-type') || 'image/png';
|
|
875
|
+
return {
|
|
876
|
+
success: true,
|
|
877
|
+
image_b64: image_base64,
|
|
878
|
+
image_mime_type: content_type,
|
|
879
|
+
raw_response: response_data,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
catch (fetch_error) {
|
|
883
|
+
logger.error('Failed to fetch image from URL', {
|
|
884
|
+
file: file_name,
|
|
885
|
+
line: 890,
|
|
886
|
+
data: { error: fetch_error instanceof Error ? fetch_error.message : String(fetch_error), url: image_url },
|
|
887
|
+
});
|
|
888
|
+
return {
|
|
889
|
+
success: false,
|
|
890
|
+
error: 'Failed to fetch image from URL in response',
|
|
891
|
+
raw_response: response_data,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Check if content contains base64 image data
|
|
896
|
+
const base64_match = content.match(/data:image\/([^;]+);base64,([A-Za-z0-9+/=]+)/);
|
|
897
|
+
if (base64_match) {
|
|
898
|
+
const mime_type = `image/${base64_match[1]}`;
|
|
899
|
+
const base64_data = base64_match[2];
|
|
900
|
+
return {
|
|
901
|
+
success: true,
|
|
902
|
+
image_b64: base64_data,
|
|
903
|
+
image_mime_type: mime_type,
|
|
904
|
+
raw_response: response_data,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
// If no image found, return text response
|
|
908
|
+
return {
|
|
909
|
+
success: true,
|
|
910
|
+
text: content,
|
|
911
|
+
raw_response: response_data,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
// If content is an array (multimodal), look for image_url
|
|
915
|
+
if (Array.isArray(content)) {
|
|
916
|
+
const content_array = content;
|
|
917
|
+
for (const item of content_array) {
|
|
918
|
+
if (item.type === 'image_url' && item.image_url?.url) {
|
|
919
|
+
const image_url = item.image_url.url;
|
|
920
|
+
// Handle data URLs
|
|
921
|
+
if (image_url.startsWith('data:')) {
|
|
922
|
+
const base64_match = image_url.match(/data:image\/([^;]+);base64,([A-Za-z0-9+/=]+)/);
|
|
923
|
+
if (base64_match) {
|
|
924
|
+
const mime_type = `image/${base64_match[1]}`;
|
|
925
|
+
const base64_data = base64_match[2];
|
|
926
|
+
return {
|
|
927
|
+
success: true,
|
|
928
|
+
image_b64: base64_data,
|
|
929
|
+
image_mime_type: mime_type,
|
|
930
|
+
raw_response: response_data,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// Fetch from URL
|
|
936
|
+
try {
|
|
937
|
+
const image_response = await fetch(image_url);
|
|
938
|
+
if (!image_response.ok) {
|
|
939
|
+
return {
|
|
940
|
+
success: false,
|
|
941
|
+
error: `Failed to fetch edited image: HTTP ${image_response.status}`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const image_array_buffer = await image_response.arrayBuffer();
|
|
945
|
+
const uint8_array = new Uint8Array(image_array_buffer);
|
|
946
|
+
const chunk_size = 8192;
|
|
947
|
+
let binary_string = '';
|
|
948
|
+
for (let i = 0; i < uint8_array.length; i += chunk_size) {
|
|
949
|
+
const chunk = uint8_array.slice(i, i + chunk_size);
|
|
950
|
+
binary_string += String.fromCharCode(...chunk);
|
|
951
|
+
}
|
|
952
|
+
const image_base64 = btoa(binary_string);
|
|
953
|
+
const content_type = image_response.headers.get('content-type') || 'image/png';
|
|
954
|
+
return {
|
|
955
|
+
success: true,
|
|
956
|
+
image_b64: image_base64,
|
|
957
|
+
image_mime_type: content_type,
|
|
958
|
+
raw_response: response_data,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
catch (fetch_error) {
|
|
962
|
+
logger.error('Failed to fetch image from URL', {
|
|
963
|
+
file: file_name,
|
|
964
|
+
line: 950,
|
|
965
|
+
data: { error: fetch_error instanceof Error ? fetch_error.message : String(fetch_error), url: image_url },
|
|
966
|
+
});
|
|
967
|
+
return {
|
|
968
|
+
success: false,
|
|
969
|
+
error: 'Failed to fetch image from URL in response',
|
|
970
|
+
raw_response: response_data,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// If we get here, no image was found
|
|
978
|
+
logger.warn('No image found in DashScope image editing response', {
|
|
979
|
+
file: file_name,
|
|
980
|
+
line: 960,
|
|
981
|
+
data: { raw_response: response_data },
|
|
982
|
+
});
|
|
983
|
+
return {
|
|
984
|
+
success: false,
|
|
985
|
+
error: 'No image found in response',
|
|
986
|
+
raw_response: response_data,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
const error_message = error instanceof Error ? error.message : String(error);
|
|
991
|
+
logger.error('Failed to call DashScope image editing API', {
|
|
992
|
+
file: file_name,
|
|
993
|
+
line: 970,
|
|
994
|
+
data: { error: error_message, model },
|
|
995
|
+
});
|
|
996
|
+
return {
|
|
997
|
+
success: false,
|
|
998
|
+
error: error_message,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
//# sourceMappingURL=qwen_client.js.map
|