开放模式
只需要 qwen_token;ssxmod_itna_value
受限模式
搭配盐值,更安全 salt_value;qwen_token;ssxmod_itna_value

qwen_token & ssxmod_itna_value 获取方式

源码
复制粘贴到deno playgroud 部署即可
/**
* Qwen API to OpenAI Standard - Single File Deno Deploy/Playground Script
*
* @version 3.9
* @description Fixed duplicate image URLs in stream response
*
* FIXES in v3.9:
* - Prevent duplicate image URLs in streaming response
* - Track already sent image URLs
* - Only send unique image URLs once
* - Better handling of image generation completion
*/
import { Application, Router, Context, Middleware } from "https://deno.land/x/[email protected]/mod.ts";
// ============================================================================
// Logger Class - Enhanced Logging System
// ============================================================================
class Logger {
private formatTimestamp(): string {
return new Date().toISOString();
}
private formatDuration(startTime: number): string {
return `${Date.now() - startTime}ms`;
}
private sanitizeHeaders(headers: Headers): Record<string, string> {
const sanitized: Record<string, string> = {};
headers.forEach((value, key) => {
if (key.toLowerCase() === 'authorization') {
sanitized[key] = value.substring(0, 20) + '...';
} else if (key.toLowerCase() === 'cookie') {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = value;
}
});
return sanitized;
}
info(message: string, data?: any) {
console.log(`[${this.formatTimestamp()}] INFO: ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
error(message: string, error?: any, data?: any) {
console.error(`[${this.formatTimestamp()}] ERROR: ${message}`, {
error: error?.message || error,
stack: error?.stack,
...data
});
}
debug(message: string, data?: any) {
if (Deno.env.get("DEBUG") === "true") {
console.log(`[${this.formatTimestamp()}] DEBUG: ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
}
request(ctx: Context, startTime: number) {
const duration = this.formatDuration(startTime);
const logData = {
timestamp: this.formatTimestamp(),
method: ctx.request.method,
path: ctx.request.url.pathname,
query: Object.fromEntries(ctx.request.url.searchParams),
status: ctx.response.status,
duration,
headers: this.sanitizeHeaders(ctx.request.headers),
ip: ctx.request.ip,
userAgent: ctx.request.headers.get('user-agent'),
};
const level = ctx.response.status >= 400 ? 'ERROR' : 'INFO';
console.log(`[${this.formatTimestamp()}] ${level}: ${ctx.request.method} ${ctx.request.url.pathname} - ${ctx.response.status} (${duration})`, logData);
}
streamChunk(type: string, content: any) {
console.log(`[${this.formatTimestamp()}] STREAM: ${type}`, {
contentLength: content?.length || 0,
preview: typeof content === 'string' ? content.substring(0, 200) : JSON.stringify(content).substring(0, 200)
});
}
}
const logger = new Logger();
// ============================================================================
// Chat Session Cache - Single session per (IP, Token) combination
// Key format: ${ip}_${tokenHash}
// ============================================================================
const chatSessionCache = new Map<string, { chatId: string, parentId: string | null }>();
// Helper function to generate session key from IP and token
function generateSessionKey(ip: string, token: string): string {
// Use a simple hash of the token to avoid storing full tokens in memory
let hash = 0;
for (let i = 0; i < token.length; i++) {
const char = token.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
const tokenHash = Math.abs(hash).toString(36);
return `${ip}_${tokenHash}`;
}
// ============================================================================
// Configuration from Environment Variables
// ============================================================================
const config = {
salt: Deno.env.get("SALT") || "",
useDenoEnv: Deno.env.get("USE_DENO_ENV")?.toLowerCase() === 'true',
qwenTokenEnv: Deno.env.get("QWEN_TOKEN") || "",
ssxmodItnaEnv: Deno.env.get("SSXMOD_ITNA_VALUE") || "",
debug: Deno.env.get("DEBUG")?.toLowerCase() === 'true',
};
// ============================================================================
// Core Conversion Logic
// ============================================================================
const QWEN_API_BASE_URL = "https://chat.qwen.ai/api/v2/chat/completions";
const QWEN_CHAT_NEW_URL = "https://chat.qwen.ai/api/v2/chats/new";
// Helper function to create a new chat session
async function createNewChat(token: string, model: string, chatType: string): Promise<string | null> {
try {
logger.info(`Creating new chat session`, { model, chatType });
const response = await fetch(QWEN_CHAT_NEW_URL, {
method: "POST",
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"title": "New Chat",
"models": [model],
"chat_mode": "normal",
"chat_type": chatType,
"timestamp": Date.now()
})
});
if (!response.ok) {
const errorText = await response.text();
logger.error(`Failed to create new chat`, { status: response.status, error: errorText });
return null;
}
const data = await response.json();
const chatId = data?.data?.id || null;
if (chatId) {
logger.info(`Successfully created new chat`, { chatId });
} else {
logger.error(`No chat ID in response`, data);
}
return chatId;
} catch (error) {
logger.error(`Error creating new chat`, error);
return null;
}
}
// Helper function to extract images from conversation history
function extractImagesFromHistory(messages: any[]): string[] {
const images: string[] = [];
// Iterate through all messages to find images
for (const message of messages) {
if (!message) continue;
// Handle assistant messages with markdown images
if (message.role === 'assistant' && typeof message.content === 'string') {
// Extract markdown image URLs: 
const markdownImageRegex = /!\[.*?\]\((.*?)\)/g;
const matches = message.content.matchAll(markdownImageRegex);
for (const match of matches) {
if (match[1]) {
images.push(match[1]);
}
}
}
// Handle user messages with various content formats
if (message.role === 'user') {
// String content might contain markdown images
if (typeof message.content === 'string') {
const markdownImageRegex = /!\[.*?\]\((.*?)\)/g;
const matches = message.content.matchAll(markdownImageRegex);
for (const match of matches) {
if (match[1]) {
images.push(match[1]);
}
}
}
// Array content with image_url objects
else if (Array.isArray(message.content)) {
for (const item of message.content) {
if (item.type === 'image_url' && item.image_url?.url) {
images.push(item.image_url.url);
} else if (item.type === 'image' && item.image) {
images.push(item.image);
}
}
}
}
}
// Return last 3 images (most recent)
return images.slice(-3);
}
// Helper function to calculate aspect ratio dynamically
function calculateAspectRatio(size: string): string {
const [width, height] = size.split('x').map(Number);
if (!width || !height) {
logger.error(`Invalid size format: ${size}, defaulting to 1:1`);
return "1:1";
}
const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
const divisor = gcd(width, height);
const aspectRatio = `${width/divisor}:${height/divisor}`;
logger.info(`Calculated aspect ratio for ${size}: ${aspectRatio}`);
return aspectRatio;
}
// Transform OpenAI request to Qwen format
// Returns transformed request and chat_id if needed
async function transformOpenAIRequestToQwen(openAIRequest: any, token: string, ip: string): Promise<{ request: any, chatId: string | null, sessionKey?: string, numImages?: number, updateParentId?: (newParentId: string) => void }> {
// Validate request
if (!openAIRequest.messages || !Array.isArray(openAIRequest.messages)) {
throw new Error("Invalid request: messages array is required");
}
if (openAIRequest.messages.length === 0) {
throw new Error("Invalid request: messages array cannot be empty");
}
const model = openAIRequest.model || "qwen-max";
let chat_type = 't2t'; // default text-to-text
// Determine chat type based on model suffix
if (model.endsWith('-image')) chat_type = 't2i';
if (model.endsWith('-image_edit')) chat_type = 'image_edit';
if (model.endsWith('-video')) chat_type = 't2v';
// Remove known suffixes for base model name
const qwenModel = model.replace(/-(search|thinking|image|image_edit|video)$/, '');
logger.info(`Transforming OpenAI request`, {
originalModel: model,
qwenModel,
chatType: chat_type,
messageCount: openAIRequest.messages.length
});
// Handle image editing requests
if (chat_type === 'image_edit') {
const lastUserMessage = openAIRequest.messages?.filter((m: any) => m.role === 'user').pop();
if (!lastUserMessage) {
throw new Error("No user message found for image editing.");
}
// Session management for image editing (similar to text chat)
const sessionKey = generateSessionKey(ip, token);
const userMessageCount = openAIRequest.messages.filter((m: any) => m.role === 'user').length;
let chatId: string;
let parentId: string | null = null;
if (userMessageCount === 1) {
// New image editing conversation
const newChatId = await createNewChat(token, qwenModel, 'image_edit');
if (!newChatId) {
throw new Error("Failed to create chat session for image editing");
}
chatId = newChatId;
parentId = null;
chatSessionCache.set(sessionKey + '_image_edit', { chatId, parentId });
logger.info(`Created new image_edit chat`, { chatId, userMsgCount: userMessageCount });
} else {
// Multi-turn image editing - reuse session
const cached = chatSessionCache.get(sessionKey + '_image_edit');
if (cached) {
chatId = cached.chatId;
parentId = cached.parentId;
logger.info(`Reusing image_edit chat`, { chatId, parentId, userMsgCount: userMessageCount });
} else {
const newChatId = await createNewChat(token, qwenModel, 'image_edit');
if (!newChatId) {
throw new Error("Failed to create chat session for image editing");
}
chatId = newChatId;
parentId = null;
chatSessionCache.set(sessionKey + '_image_edit', { chatId, parentId });
logger.info(`Created new image_edit chat (no cache)`, { chatId, userMsgCount: userMessageCount });
}
}
// Extract text content from last user message
let textContent = "";
const currentMessageImages: string[] = [];
if (typeof lastUserMessage.content === 'string') {
textContent = lastUserMessage.content;
} else if (Array.isArray(lastUserMessage.content)) {
for (const item of lastUserMessage.content) {
if (item.type === 'text') {
textContent += item.text || item.content || '';
} else if (item.type === 'image_url' && item.image_url?.url) {
currentMessageImages.push(item.image_url.url);
} else if (item.type === 'image' && item.image) {
currentMessageImages.push(item.image);
}
}
}
// Extract images from conversation history (including assistant responses)
const historyImages = extractImagesFromHistory(openAIRequest.messages.slice(0, -1));
// Combine current message images with history images (current takes priority)
const allImages = [...currentMessageImages, ...historyImages];
const imagesToUse = allImages.slice(-3); // Use last 3 images max
logger.info(`Image editing context`, {
currentMessageImages: currentMessageImages.length,
historyImages: historyImages.length,
totalImages: allImages.length,
usingImages: imagesToUse.length
});
// Build files array for the request
const files: any[] = [];
// If we have images, add them
if (imagesToUse.length > 0) {
// Use the most recent image as the primary edit target
files.push({
type: "image",
url: imagesToUse[imagesToUse.length - 1]
});
logger.info(`Using image for editing: ${imagesToUse[imagesToUse.length - 1]}`);
} else {
// If no images in context, this becomes a text-to-image request
logger.info(`No images found in context, switching to t2i mode`);
}
// Get number of images to generate (default to 1)
const numImages = openAIRequest.n || 1;
// Build request with chat_id and parent_id
const transformedRequest = {
"stream": true,
"incremental_output": true,
"chat_id": chatId,
"chat_mode": "normal",
"model": qwenModel,
"parent_id": parentId,
// Note: Qwen API doesn't support 'num' parameter - always returns 2 images by default
"messages": [{
"role": "user",
"content": textContent || "Generate an image",
"files": files,
"chat_type": files.length > 0 ? "image_edit" : "t2i",
"feature_config": {
"thinking_enabled": false,
"output_schema": "phase"
},
"extra": {
"meta": {
"subChatType": files.length > 0 ? "image_edit" : "t2i"
}
},
"sub_chat_type": files.length > 0 ? "image_edit" : "t2i"
}],
"timestamp": Date.now()
};
logger.info(`Transformed to image editing request`, {
model: qwenModel,
chatId,
parentId,
fileCount: files.length,
actualChatType: transformedRequest.messages[0].chat_type,
num: numImages
});
return {
request: transformedRequest,
chatId,
sessionKey: sessionKey + '_image_edit',
numImages, // Pass through for client-side filtering
updateParentId: (newParentId: string) => {
const session = chatSessionCache.get(sessionKey + '_image_edit');
if (session) {
session.parentId = newParentId;
logger.info(`Updated image_edit chat parentId`, { chatId: session.chatId, newParentId });
}
}
};
}
// Handle image generation requests
if (chat_type === 't2i') {
const lastUserMessage = openAIRequest.messages?.filter((m: any) => m.role === 'user').pop();
if (!lastUserMessage) {
throw new Error("No user message found for image generation.");
}
// Session management for image generation (similar to text chat)
const sessionKey = generateSessionKey(ip, token);
const userMessageCount = openAIRequest.messages.filter((m: any) => m.role === 'user').length;
let chatId: string;
let parentId: string | null = null;
if (userMessageCount === 1) {
// New image generation conversation
const newChatId = await createNewChat(token, qwenModel, 't2i');
if (!newChatId) {
throw new Error("Failed to create chat session for image generation");
}
chatId = newChatId;
parentId = null;
chatSessionCache.set(sessionKey + '_image', { chatId, parentId });
logger.info(`Created new image chat`, { chatId, userMsgCount: userMessageCount });
} else {
// Multi-turn image generation - reuse session
const cached = chatSessionCache.get(sessionKey + '_image');
if (cached) {
chatId = cached.chatId;
parentId = cached.parentId;
logger.info(`Reusing image chat`, { chatId, parentId, userMsgCount: userMessageCount });
} else {
const newChatId = await createNewChat(token, qwenModel, 't2i');
if (!newChatId) {
throw new Error("Failed to create chat session for image generation");
}
chatId = newChatId;
parentId = null;
chatSessionCache.set(sessionKey + '_image', { chatId, parentId });
logger.info(`Created new image chat (no cache)`, { chatId, userMsgCount: userMessageCount });
}
}
// Extract text content
let textContent = "";
if (typeof lastUserMessage.content === 'string') {
textContent = lastUserMessage.content;
} else if (Array.isArray(lastUserMessage.content)) {
for (const item of lastUserMessage.content) {
if (item.type === 'text') {
textContent += item.text || item.content || '';
}
}
}
// Size parameter - only support aspect ratios, not specific resolutions
// Note: Qwen API only accepts aspect ratios, not exact pixel dimensions
const sizeParam = openAIRequest.size || "1:1";
// Size mapping to official Qwen aspect ratios
// Supports both ratio strings and common size references
const sizeMap: Record<string, string> = {
// Direct ratio strings (recommended)
"1:1": "1:1",
"3:4": "3:4",
"4:3": "4:3",
"16:9": "16:9",
"9:16": "9:16",
// Common reference sizes (for compatibility)
"1024x1024": "1:1",
"768x1024": "3:4",
"1024x768": "4:3",
"1792x1024": "16:9",
"1024x1792": "9:16",
};
let qwenSize = sizeMap[sizeParam] || "1:1"; // Default to 1:1 if unknown
// Get number of images to generate (default to 1)
const numImages = openAIRequest.n || 1;
// Build request with chat_id and parent_id
const transformedRequest = {
"stream": true,
"chat_id": chatId,
"chat_mode": "normal",
"model": qwenModel,
"parent_id": parentId,
"size": qwenSize,
// Note: Qwen API doesn't support 'num' parameter - always returns 2 images by default
"messages": [{
"role": "user",
"content": textContent || "Generate an image",
"files": [],
"chat_type": "t2i",
"feature_config": {
"output_schema": "phase"
}
}],
};
logger.info(`Transformed to image generation request`, {
size: qwenSize,
model: qwenModel,
chatId,
parentId,
requestedImages: numImages // Client requested count (will filter on response)
});
return {
request: transformedRequest,
chatId,
sessionKey: sessionKey + '_image',
numImages, // Pass through for client-side filtering
updateParentId: (newParentId: string) => {
const session = chatSessionCache.get(sessionKey + '_image');
if (session) {
session.parentId = newParentId;
logger.info(`Updated image chat parentId`, { chatId: session.chatId, newParentId });
}
}
};
}
// Single session mode: Each (IP, Token) has one active session
const sessionKey = generateSessionKey(ip, token);
let chatId: string;
let parentId: string | null = null;
if (openAIRequest.messages.length === 1) {
// New conversation - create new chat (overwrite old session)
const newChatId = await createNewChat(token, qwenModel, 't2t');
if (!newChatId) {
throw new Error("Failed to create chat session");
}
chatId = newChatId;
parentId = null;
// Store or overwrite session for this user
chatSessionCache.set(sessionKey, { chatId, parentId });
logger.info(`Created new chat (single session mode)`, {
chatId,
sessionKey: sessionKey.substring(0, 20),
msgCount: 1
});
} else {
// Multi-turn - reuse existing session for this user
const cached = chatSessionCache.get(sessionKey);
if (cached) {
chatId = cached.chatId;
parentId = cached.parentId;
logger.info(`Reusing chat (single session mode)`, {
chatId,
parentId,
sessionKey: sessionKey.substring(0, 20),
msgCount: openAIRequest.messages.length
});
} else {
// No cached session - create new one
const newChatId = await createNewChat(token, qwenModel, 't2t');
if (!newChatId) {
throw new Error("Failed to create chat session");
}
chatId = newChatId;
parentId = null;
chatSessionCache.set(sessionKey, { chatId, parentId });
logger.info(`Created new chat (no cached session)`, {
chatId,
sessionKey: sessionKey.substring(0, 20),
msgCount: openAIRequest.messages.length
});
}
}
// Only send the last user message
const lastUserMsg = openAIRequest.messages.filter((m: any) => m.role === 'user').pop();
if (!lastUserMsg) {
throw new Error("No user message found");
}
// Determine actual chat_type for the message
const actualChatType = model.includes('-search') ? 'search' : 't2t';
const transformedRequest = {
"stream": true,
"incremental_output": true,
"chat_id": chatId,
"chat_mode": "normal",
"model": qwenModel,
"parent_id": parentId,
"messages": [{
"role": "user",
"content": typeof lastUserMsg.content === 'string' ? lastUserMsg.content : JSON.stringify(lastUserMsg.content),
"files": [],
"chat_type": actualChatType,
"feature_config": {
"thinking_enabled": model.includes('-thinking'),
"output_schema": "phase"
}
}],
"timestamp": Date.now()
};
logger.info(`Transformed to text request`, {
thinkingEnabled: model.includes('-thinking'),
searchEnabled: model.includes('-search'),
chatType: actualChatType,
chatId,
parentId
});
return {
request: transformedRequest,
chatId,
sessionKey, // Return sessionKey to update parentId later
updateParentId: (newParentId: string) => {
const session = chatSessionCache.get(sessionKey);
if (session) {
session.parentId = newParentId;
logger.info(`Updated parentId`, { chatId: session.chatId, newParentId, sessionKey: sessionKey.substring(0, 20) });
}
}
};
}
// Stream transformer with enhanced error handling and duplicate prevention
function createQwenToOpenAIStreamTransformer(maxImages: number = 1): TransformStream<Uint8Array, Uint8Array> {
const decoder = new TextDecoder();
const encoder = new TextEncoder();
let buffer = '';
const MAX_BUFFER_SIZE = 100000; // 100KB limit
const messageId = crypto.randomUUID();
let chunkCount = 0;
let rawDataCount = 0;
let errorDetected = false;
// Track sent image URLs to prevent duplicates and limit count
const sentImageUrls = new Set<string>();
let lastImageUrl: string | null = null;
let imageGenPhaseStarted = false;
let imageGenPhaseFinished = false;
return new TransformStream({
transform(chunk, controller) {
const rawChunk = decoder.decode(chunk, { stream: true });
buffer += rawChunk;
// Log raw data for debugging
if (config.debug || rawDataCount < 3) {
logger.debug(`Raw stream data (chunk ${++rawDataCount})`, {
length: rawChunk.length,
preview: rawChunk.substring(0, 500),
hasData: rawChunk.includes('data:'),
hasNewlines: rawChunk.includes('\n'),
});
}
// Buffer overflow protection
if (buffer.length > MAX_BUFFER_SIZE) {
logger.error(`Buffer overflow detected (size: ${buffer.length}), clearing buffer`);
buffer = '';
return;
}
// Check for error responses
if (!errorDetected && buffer.includes('"success":false')) {
try {
const errorJson = JSON.parse(buffer);
if (errorJson.success === false) {
logger.error("Upstream API returned error", errorJson);
const errorMessage = errorJson.data?.details || errorJson.data?.code || "Unknown error from Qwen API";
const openAIError = {
id: `chatcmpl-${messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "qwen-proxy",
choices: [{
index: 0,
delta: {
content: `Error: ${errorMessage}\nRequest ID: ${errorJson.request_id}`
},
finish_reason: "stop",
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(openAIError)}\n\n`));
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
errorDetected = true;
buffer = '';
return;
}
} catch (e) {
// Not a complete JSON yet, continue buffering
}
}
// Parse lines with multiple delimiter support
let lines: string[] = [];
if (buffer.includes('\n\n')) {
lines = buffer.split('\n\n');
buffer = lines.pop() || '';
} else if (buffer.includes('\n')) {
lines = buffer.split('\n');
const lastLine = lines[lines.length - 1];
if (lastLine && !lastLine.startsWith('data:')) {
buffer = lines.pop() || '';
} else {
buffer = '';
}
}
for (const line of lines) {
if (!line || line.trim() === '') continue;
let dataStr = line;
if (line.startsWith('data:')) {
dataStr = line.substring(5).trim();
} else if (line.startsWith('data: ')) {
dataStr = line.substring(6).trim();
}
if (dataStr === '[DONE]') {
logger.debug('Received [DONE] signal');
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
continue;
}
try {
const qwenChunk = JSON.parse(dataStr);
// Check for errors in chunk
if (qwenChunk.success === false) {
logger.error("Error in stream chunk", qwenChunk);
const errorMessage = qwenChunk.data?.details || qwenChunk.data?.code || "Stream error";
const openAIError = {
id: `chatcmpl-${messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "qwen-proxy",
choices: [{
index: 0,
delta: { content: `Error: ${errorMessage}` },
finish_reason: "stop",
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(openAIError)}\n\n`));
errorDetected = true;
continue;
}
// Log first few chunks for debugging
if (chunkCount < 3 || config.debug) {
logger.debug(`Parsed Qwen chunk ${chunkCount}`, qwenChunk);
}
// Extract content from various response structures
let content = "";
let isFinished = false;
if (qwenChunk.choices && qwenChunk.choices.length > 0) {
const choice = qwenChunk.choices[0];
const delta = choice.delta || choice.message;
if (delta) {
content = delta.content || "";
// Handle image generation phase
if (delta.phase === 'image_gen') {
if (!imageGenPhaseStarted) {
imageGenPhaseStarted = true;
logger.info(`Image generation phase started`);
}
// Check if this is an image URL
if (content && content.startsWith('https://')) {
// Check if we've already sent this URL or reached max count
if (!sentImageUrls.has(content)) {
// Check if we've reached the max image count
if (sentImageUrls.size >= maxImages) {
// Skip - already sent enough images
logger.info(`Skipping image - reached max count`, {
url: content.substring(0, 80),
maxImages,
currentCount: sentImageUrls.size
});
content = "";
} else {
// Add and send this image
sentImageUrls.add(content);
lastImageUrl = content;
content = ``;
// Log dimensions if available (for debugging)
if (delta.extra?.output_image_hw && Array.isArray(delta.extra.output_image_hw)) {
const dimensions = delta.extra.output_image_hw[0];
if (dimensions && dimensions.length === 2) {
logger.info(`Image generated with dimensions`, {
url: lastImageUrl,
width: dimensions[0],
height: dimensions[1]
});
}
}
logger.info(`New image URL detected and formatted`, {
url: lastImageUrl,
totalImagesSent: sentImageUrls.size,
maxImages
});
}
} else {
// Skip duplicate image URL
logger.debug(`Skipping duplicate image URL`, { url: content });
content = "";
}
} else if (content === "") {
// Empty content during image_gen phase, skip
logger.debug(`Skipping empty content during image_gen phase`);
}
}
// Handle image editing responses
else if ((delta.chat_type === 't2i' || delta.chat_type === 'image_edit') && content.startsWith('https://')) {
if (!sentImageUrls.has(content)) {
sentImageUrls.add(content);
lastImageUrl = content;
content = ``;
// Log dimensions if available (for debugging)
if (delta.extra?.output_image_hw && Array.isArray(delta.extra.output_image_hw)) {
const dimensions = delta.extra.output_image_hw[0];
if (dimensions && dimensions.length === 2) {
logger.info(`Image generated with dimensions`, {
url: lastImageUrl,
width: dimensions[0],
height: dimensions[1],
chatType: delta.chat_type
});
}
}
logger.info(`Image URL detected and formatted`, {
url: lastImageUrl,
chatType: delta.chat_type
});
} else {
logger.debug(`Skipping duplicate image URL`, { url: content });
content = "";
}
}
// Check if phase is finished
if (delta.status === 'finished') {
isFinished = true;
if (imageGenPhaseStarted) {
imageGenPhaseFinished = true;
logger.info(`Image generation phase finished`, {
totalImages: sentImageUrls.size
});
}
}
isFinished = isFinished || choice.finish_reason === 'stop';
}
} else if (qwenChunk.content) {
content = qwenChunk.content;
// Check if it's an image URL
if (content.startsWith('https://') && content.includes('cdn.qwenlm.ai')) {
if (!sentImageUrls.has(content)) {
sentImageUrls.add(content);
lastImageUrl = content;
content = ``;
logger.info(`Image URL detected in content field`, { url: lastImageUrl });
} else {
logger.debug(`Skipping duplicate image URL from content field`, { url: content });
content = "";
}
}
isFinished = qwenChunk.status === 'finished' || qwenChunk.finish_reason === 'stop';
} else if (qwenChunk.result || qwenChunk.data) {
const data = qwenChunk.result || qwenChunk.data;
if (typeof data === 'string') {
content = data;
} else if (data.content) {
content = data.content;
}
}
// Send OpenAI formatted chunk only if there's content or it's finished
if (content || isFinished) {
const openAIChunk = {
id: `chatcmpl-${messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "qwen-proxy",
choices: [{
index: 0,
delta: { content: content },
finish_reason: isFinished ? 'stop' : null,
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(openAIChunk)}\n\n`));
chunkCount++;
if (chunkCount % 10 === 0 || chunkCount <= 3) {
logger.info(`Processed ${chunkCount} stream chunks`);
}
}
} catch (e) {
// Handle non-JSON text
if (dataStr && dataStr.trim() && !dataStr.startsWith('{')) {
logger.debug(`Treating as raw text: ${dataStr.substring(0, 100)}`);
const openAIChunk = {
id: `chatcmpl-${messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "qwen-proxy",
choices: [{
index: 0,
delta: { content: dataStr },
finish_reason: null,
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(openAIChunk)}\n\n`));
chunkCount++;
} else {
logger.debug("Could not parse chunk", {
error: e.message,
dataStr: dataStr.substring(0, 200)
});
}
}
}
},
flush(controller) {
// Process remaining buffer
if (buffer.trim() && !errorDetected) {
logger.debug(`Processing remaining buffer on flush: ${buffer.substring(0, 200)}`);
try {
const remaining = buffer.trim();
const possibleError = JSON.parse(remaining);
if (possibleError.success === false) {
logger.error("Error in final buffer", possibleError);
const errorMessage = possibleError.data?.details || possibleError.data?.code || "Final buffer error";
const openAIError = {
id: `chatcmpl-${messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "qwen-proxy",
choices: [{
index: 0,
delta: { content: `Error: ${errorMessage}` },
finish_reason: "stop",
}],
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(openAIError)}\n\n`));
}
} catch (e) {
logger.debug(`Could not parse remaining buffer: ${e.message}`);
}
}
if (!errorDetected) {
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
}
logger.info(`Stream completed`, {
totalChunks: chunkCount,
totalImages: sentImageUrls.size,
errorDetected,
imageGenPhaseStarted,
imageGenPhaseFinished
});
},
});
}
// ============================================================================
// Oak Application Setup
// ============================================================================
const app = new Application();
const router = new Router();
// Global error handling middleware
app.use(async (ctx, next) => {
const startTime = Date.now();
try {
await next();
} catch (err: any) {
logger.error(`Unhandled error in request ${ctx.request.method} ${ctx.request.url}`, err);
ctx.response.status = err.status || 500;
ctx.response.body = {
error: err.message || "Internal Server Error",
timestamp: new Date().toISOString(),
path: ctx.request.url.pathname
};
} finally {
logger.request(ctx, startTime);
}
});
// Authentication middleware
const authMiddleware: Middleware = async (ctx, next) => {
// Skip auth for root endpoint
if (ctx.request.url.pathname === '/') {
await next();
return;
}
logger.info(`Processing authentication for ${ctx.request.url.pathname}`);
ctx.state = ctx.state || {};
if (config.useDenoEnv) {
// Server-side authentication mode
const authHeader = ctx.request.headers.get("Authorization");
if (config.salt) {
const clientToken = authHeader?.replace(/^Bearer\s+/, '');
if (clientToken !== config.salt) {
logger.error("Authentication failed: Invalid salt value", { provided: clientToken?.substring(0, 10) });
ctx.response.status = 401;
ctx.response.body = {
error: "Invalid salt value.",
format: "Server is in environment mode. Use: Authorization: Bearer your_salt_value",
salt_required: true
};
return;
}
}
ctx.state.qwenToken = config.qwenTokenEnv;
ctx.state.ssxmodItna = config.ssxmodItnaEnv;
logger.info("Authentication successful (server-side mode)");
} else {
// Client-side authentication mode
const authHeader = ctx.request.headers.get("Authorization");
const clientToken = authHeader?.replace(/^Bearer\s+/, '');
if (!clientToken) {
const expectedFormat = config.salt ? "Bearer salt;qwen_token;ssxmod_itna" : "Bearer qwen_token;ssxmod_itna";
logger.error("Authentication failed: No token provided");
ctx.response.status = 401;
ctx.response.body = {
error: "Unauthorized.",
format: `Use: ${expectedFormat}`,
salt_required: !!config.salt
};
return;
}
const parts = clientToken.split(';');
let qwenToken: string;
let ssxmodItna: string;
if (config.salt) {
if (parts.length < 2) {
logger.error("Authentication failed: Invalid token format");
ctx.response.status = 401;
ctx.response.body = {
error: "Invalid token format.",
format: "Use: Bearer salt;qwen_token;ssxmod_itna",
salt_required: true
};
return;
}
if (parts[0]?.trim() !== config.salt) {
logger.error("Authentication failed: Invalid salt", { provided: parts[0]?.substring(0, 10) });
ctx.response.status = 401;
ctx.response.body = { error: "Invalid salt value." };
return;
}
qwenToken = parts[1]?.trim();
ssxmodItna = parts[2]?.trim() || '';
} else {
qwenToken = parts[0]?.trim();
ssxmodItna = parts[1]?.trim() || '';
}
if (!qwenToken) {
logger.error("Authentication failed: Qwen token is missing");
ctx.response.status = 401;
ctx.response.body = { error: "Qwen token is required." };
return;
}
ctx.state.qwenToken = qwenToken;
ctx.state.ssxmodItna = ssxmodItna;
logger.info("Authentication successful (client-side mode)");
}
await next();
};
// Apply authentication middleware
app.use(authMiddleware);
// ============================================================================
// Routes
// ============================================================================
// Home page
router.get("/", (ctx: Context) => {
logger.info("Serving home page");
let saltStatus = config.salt ? "🔒 受限访问模式" : "🎯 开放访问模式";
let authFormat: string;
let authMode: string;
if (config.useDenoEnv) {
authMode = "服务器端认证 (环境变量)";
authFormat = config.salt
? "Authorization: Bearer your_salt_value"
: "Authorization header can be anything (e.g., Bearer dummy)";
} else {
// authMode = "客户端认证 (请求头)";
authFormat = config.salt
? "Authorization: Bearer salt_value;qwen_token;ssxmod_itna_value"
: "Authorization: Bearer qwen_token;ssxmod_itna_value";
}
const htmlContent = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen API Proxy</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="font-sans min-h-screen flex items-center justify-center p-5 bg-gradient-to-br from-indigo-500 to-purple-600">
<div class="w-full max-w-lg rounded-2xl bg-white/95 p-10 text-center shadow-2xl backdrop-blur-md">
<div class="mb-3 flex items-center justify-center gap-2">
<div class="h-2 w-2 animate-pulse rounded-full bg-emerald-500"></div>
<div class="text-lg font-semibold text-gray-800">服务运行正常</div>
</div>
<div class="mb-8 text-sm leading-relaxed text-gray-500">欲买桂花同载酒,终不似,少年游</div>
<div class="mb-8 text-left">
<div class="mb-4 text-base font-semibold text-gray-700">API 端点</div>
<div class="flex items-center justify-between border-b border-gray-100 py-3">
<span class="text-sm text-gray-500">模型列表</span>
<code class="font-mono rounded-md border border-gray-200 bg-gray-50 px-2 py-1 text-xs text-gray-800">/v1/models</code>
</div>
<div class="flex items-center justify-between py-3">
<span class="text-sm text-gray-500">聊天完成</span>
<code class="font-mono rounded-md border border-gray-200 bg-gray-50 px-2 py-1 text-xs text-gray-800">/v1/chat/completions</code>
</div>
</div>
<div class="rounded-xl border border-slate-200 bg-slate-50 p-5 text-center">
<div class="mb-2 text-sm font-semibold text-gray-700">认证方式</div>
<div class="mb-1 text-xs font-medium text-emerald-600">${saltStatus}</div>
<div class="font-mono break-all rounded-md border border-gray-300 bg-white px-3 py-2 text-left text-[12px] leading-snug text-gray-600">${authFormat}</div>
</div>
<div class="mt-6 text-xs font-medium text-indigo-500">Qwen API Proxy v3.9</div>
</div>
</body>
</html>`;
ctx.response.body = htmlContent;
ctx.response.headers.set("Content-Type", "text/html; charset=utf-8");
});
// Models endpoint - ADDED image_edit suffix
router.get("/v1/models", async (ctx: Context) => {
const token = ctx.state?.qwenToken;
if (!token) {
logger.error("Models endpoint: No Qwen token available");
ctx.response.status = 401;
ctx.response.body = { error: "Authentication failed. No Qwen token available." };
return;
}
try {
logger.info("Fetching models from Qwen API");
const response = await fetch('https://chat.qwen.ai/api/models', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
}
const originalModels = (await response.json()).data;
const processedModels: any[] = [];
for (const model of originalModels) {
processedModels.push(model);
// Add variant models based on capabilities with uppercase suffixes in display name
if (model?.info?.meta?.abilities?.thinking) {
const thinkingModel = { ...model, id: `${model.id}-thinking` };
if (thinkingModel.name) thinkingModel.name = `${thinkingModel.name}-THINKING`;
if (thinkingModel.display_name) thinkingModel.display_name = `${thinkingModel.display_name}-THINKING`;
processedModels.push(thinkingModel);
}
if (model?.info?.meta?.chat_type?.includes('search')) {
const searchModel = { ...model, id: `${model.id}-search` };
if (searchModel.name) searchModel.name = `${searchModel.name}-SEARCH`;
if (searchModel.display_name) searchModel.display_name = `${searchModel.display_name}-SEARCH`;
processedModels.push(searchModel);
}
if (model?.info?.meta?.chat_type?.includes('t2i')) {
const imageModel = { ...model, id: `${model.id}-image` };
if (imageModel.name) imageModel.name = `${imageModel.name}-IMAGE`;
if (imageModel.display_name) imageModel.display_name = `${imageModel.display_name}-IMAGE`;
// Add vision tag for client identification
if (!imageModel.tags) imageModel.tags = [];
if (!imageModel.tags.includes('vision')) imageModel.tags.push('vision');
if (!imageModel.tags.includes('image-generation')) imageModel.tags.push('image-generation');
processedModels.push(imageModel);
// Also add image_edit variant for models that support image generation
const imageEditModel = { ...model, id: `${model.id}-image_edit` };
if (imageEditModel.name) imageEditModel.name = `${imageEditModel.name}-IMAGE_EDIT`;
if (imageEditModel.display_name) imageEditModel.display_name = `${imageEditModel.display_name}-IMAGE_EDIT`;
// Add vision tag for client identification
if (!imageEditModel.tags) imageEditModel.tags = [];
if (!imageEditModel.tags.includes('vision')) imageEditModel.tags.push('vision');
if (!imageEditModel.tags.includes('image-editing')) imageEditModel.tags.push('image-editing');
processedModels.push(imageEditModel);
}
// Some models might specifically support image_edit without t2i
if (model?.info?.meta?.chat_type?.includes('image_edit')) {
if (!processedModels.some(m => m.id === `${model.id}-image_edit`)) {
const imageEditModel = { ...model, id: `${model.id}-image_edit` };
if (imageEditModel.name) imageEditModel.name = `${imageEditModel.name}-IMAGE_EDIT`;
if (imageEditModel.display_name) imageEditModel.display_name = `${imageEditModel.display_name}-IMAGE_EDIT`;
// Add vision tag for client identification
if (!imageEditModel.tags) imageEditModel.tags = [];
if (!imageEditModel.tags.includes('vision')) imageEditModel.tags.push('vision');
if (!imageEditModel.tags.includes('image-editing')) imageEditModel.tags.push('image-editing');
processedModels.push(imageEditModel);
}
}
}
logger.info(`Successfully fetched ${originalModels.length} models, processed to ${processedModels.length} models`);
ctx.response.body = { object: "list", data: processedModels };
} catch (err: any) {
logger.error("Error fetching models", err);
ctx.response.status = 502;
ctx.response.body = {
error: "Failed to fetch models from upstream API.",
details: err.message
};
}
});
// Chat completions endpoint - Updated with image_edit support
router.post("/v1/chat/completions", async (ctx: Context) => {
const token = ctx.state?.qwenToken;
const ssxmodItna = ctx.state?.ssxmodItna;
const requestId = crypto.randomUUID();
logger.info(`Starting chat completion request`, { requestId });
if (!token) {
logger.error("Chat completions: No Qwen token available", { requestId });
ctx.response.status = 401;
ctx.response.body = { error: "Authentication failed. No Qwen token available." };
return;
}
try {
const openAIRequest = await ctx.request.body({ type: "json" }).value;
const clientIp = ctx.request.ip || 'unknown';
logger.info(`Received OpenAI request`, {
requestId,
model: openAIRequest.model,
messageCount: openAIRequest.messages?.length,
hasSize: !!openAIRequest.size,
hasN: !!openAIRequest.n,
n: openAIRequest.n,
stream: openAIRequest.stream,
clientIp
});
// Transform request and potentially create chat session
const { request: qwenRequest, chatId, numImages, updateParentId } = await transformOpenAIRequestToQwen(openAIRequest, token, clientIp);
// Build URL - add chat_id as query param if we have one
let apiUrl = QWEN_API_BASE_URL;
if (chatId) {
apiUrl = `${QWEN_API_BASE_URL}?chat_id=${chatId}`;
}
// Log transformed request for debugging
logger.debug(`Qwen request payload`, qwenRequest);
logger.info(`Using API URL: ${apiUrl}`);
const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36',
'source': 'web',
'x-request-id': requestId,
};
if (ssxmodItna) {
headers['Cookie'] = `ssxmod_itna=${ssxmodItna}`;
}
logger.info(`Sending request to Qwen API`, {
requestId,
headers: Object.keys(headers),
url: apiUrl,
hasChatId: !!chatId,
modelType: openAIRequest.model?.includes('-image_edit') ? 'image_edit' :
openAIRequest.model?.includes('-image') ? 'image' : 'text'
});
const upstreamResponse = await fetch(apiUrl, {
method: "POST",
headers: headers,
body: JSON.stringify(qwenRequest),
});
if (!upstreamResponse.ok || !upstreamResponse.body) {
const errorBody = await upstreamResponse.text();
logger.error(`Upstream API error`, {
requestId,
status: upstreamResponse.status,
statusText: upstreamResponse.statusText,
errorBody: errorBody.substring(0, 500),
url: apiUrl
});
ctx.response.status = upstreamResponse.status;
ctx.response.body = {
error: "Upstream API request failed",
details: errorBody,
requestId
};
return;
}
logger.info(`Successfully received upstream response, starting stream transformation`, { requestId });
// Create a stream to extract response_id and update parent_id
const responseIdExtractor = new TransformStream({
transform(chunk, controller) {
// Extract response_id from the first chunk if available
if (updateParentId) {
try {
const text = new TextDecoder().decode(chunk);
const match = text.match(/"response_id"\s*:\s*"([^"]+)"/);
if (match && match[1]) {
updateParentId(match[1]);
logger.info(`Extracted response_id`, { responseId: match[1] });
}
} catch (e) {
// Ignore parse errors
}
}
controller.enqueue(chunk);
}
});
const transformedStream = upstreamResponse.body
.pipeThrough(responseIdExtractor)
.pipeThrough(createQwenToOpenAIStreamTransformer(numImages || 1));
ctx.response.body = transformedStream;
ctx.response.headers.set("Content-Type", "text/event-stream");
ctx.response.headers.set("Cache-Control", "no-cache");
ctx.response.headers.set("Connection", "keep-alive");
ctx.response.headers.set("X-Request-Id", requestId);
logger.info(`Stream response started`, { requestId });
} catch (err: any) {
logger.error("Error in chat completions proxy", err, { requestId });
ctx.response.status = 500;
ctx.response.body = {
error: "Internal Server Error",
details: err.message,
requestId
};
}
});
// Health check endpoint
router.get("/health", (ctx: Context) => {
logger.info("Health check requested");
ctx.response.body = {
status: "healthy",
timestamp: new Date().toISOString(),
version: "3.9",
config: {
saltEnabled: !!config.salt,
serverSideAuth: config.useDenoEnv,
debugMode: config.debug
}
};
});
// Debug test stream endpoint
router.get("/debug/test-stream", (ctx: Context) => {
ctx.response.headers.set("Content-Type", "text/event-stream");
ctx.response.headers.set("Cache-Control", "no-cache");
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(`data: {"test": "message1"}\n\n`));
controller.enqueue(encoder.encode(`data: {"test": "message2"}\n\n`));
controller.enqueue(encoder.encode(`data: [DONE]\n\n`));
controller.close();
}
});
ctx.response.body = stream;
});
// ============================================================================
// Application Setup and Start
// ============================================================================
app.use(router.routes());
app.use(router.allowedMethods());
// 404 handler
app.use((ctx) => {
logger.error(`404 Not Found: ${ctx.request.url.pathname}`);
ctx.response.status = 404;
ctx.response.body = {
error: "Not Found",
path: ctx.request.url.pathname,
timestamp: new Date().toISOString()
};
});
// Startup logging
console.log("=".repeat(60));
console.log("🚀 Starting Qwen API Proxy Server v3.9...");
console.log("=".repeat(60));
if (config.debug) {
console.log("🐛 DEBUG MODE ENABLED - Verbose logging active");
}
if (config.useDenoEnv) {
console.log("🔒 SERVER-SIDE AUTH ENABLED (USE_DENO_ENV=true)");
if (!config.qwenTokenEnv) {
console.error("❌ FATAL: USE_DENO_ENV is true, but QWEN_TOKEN environment variable is not set.");
Deno.exit(1);
}
console.log("✅ QWEN_TOKEN loaded from environment.");
if (config.ssxmodItnaEnv) console.log("🍪 SSXMOD_ITNA_VALUE loaded from environment.");
if (config.salt) {
console.log(`🔐 SALT protection enabled: ${config.salt.substring(0,3)}***`);
console.log("💡 Clients should provide: Authorization: Bearer <SALT_VALUE>");
} else {
console.log("💡 No SALT required. Clients can use any Authorization header.");
}
} else {
console.log("👤 CLIENT-SIDE AUTH ENABLED (Header-based)");
if (config.salt) {
console.log("🔒 SALT PROTECTION ENABLED - Restricted access mode");
console.log("💡 Clients should provide: Authorization: Bearer salt_value;qwen_token;ssxmod_itna_value");
} else {
console.log("🎯 OPEN ACCESS MODE - No salt protection");
console.log("💡 Clients should provide: Authorization: Bearer qwen_token;ssxmod_itna_value");
}
}
console.log("=".repeat(60));
console.log("✅ FIXES IN v3.9:");
console.log(" 🎯 Prevents duplicate image URLs in stream");
console.log(" • Tracks sent image URLs with Set");
console.log(" • Skips duplicate URLs in image_gen phase");
console.log(" • Only sends unique image URLs once");
console.log(" • Logs image generation phase status");
console.log("=".repeat(60));
console.log("📝 Features:");
console.log(" 🎨 Image generation (model-image suffix)");
console.log(" ✏️ Image editing with context awareness");
console.log(" 📸 Extracts images from conversation history");
console.log(" 🖼️ Supports last 3 images from context");
console.log(" 💭 Thinking mode (model-thinking suffix)");
console.log(" 🔍 Search mode (model-search suffix)");
console.log("=".repeat(60));
// Start the server
Deno.serve((req) => app.handle(req));
console.log("✅ Server is ready to accept connections.");
console.log("📊 Visit the root URL for API documentation.");
console.log("💡 Set DEBUG=true environment variable for verbose logging.");
console.log("=".repeat(60));
logger.info("Server initialization complete");


感谢分享