Files
OpenQode/bin/goose-ultra-final/electron/qwen-api.js

193 lines
5.7 KiB
JavaScript

/**
* Qwen API Bridge for Goose Ultra
*
* Uses the SAME token infrastructure as QwenOAuth (qwen-oauth.mjs)
* Token location: ~/.qwen/oauth_creds.json
*/
import fs from 'fs';
import path from 'path';
import https from 'https';
import os from 'os';
import crypto from 'crypto';
const QWEN_CHAT_API = 'https://chat.qwen.ai/api/v1/chat/completions';
const getOauthCredPath = () => path.join(os.homedir(), '.qwen', 'oauth_creds.json');
const normalizeModel = (model) => {
const m = String(model || '').trim();
const map = {
'qwen-coder-plus': 'coder-model',
'qwen-plus': 'coder-model',
'qwen-turbo': 'coder-model',
'coder-model': 'coder-model',
};
return map[m] || 'coder-model';
};
export function loadTokens() {
const tokenPath = getOauthCredPath();
try {
if (fs.existsSync(tokenPath)) {
const data = JSON.parse(fs.readFileSync(tokenPath, 'utf-8'));
if (data.access_token) {
console.log('[QwenAPI] Loaded tokens from:', tokenPath);
return {
access_token: data.access_token,
refresh_token: data.refresh_token,
token_type: data.token_type || 'Bearer',
expiry_date: Number(data.expiry_date || 0),
resource_url: data.resource_url,
};
}
}
} catch (e) {
console.error('[QwenAPI] Token load error:', e.message);
}
console.warn('[QwenAPI] No valid tokens found at', tokenPath);
return null;
}
function isTokenValid(tokens) {
const expiry = Number(tokens?.expiry_date || 0);
if (!expiry) return true;
return expiry > Date.now() + 30_000;
}
function getApiEndpoint(tokens) {
if (tokens?.resource_url) {
return `https://${tokens.resource_url}/v1/chat/completions`;
}
return QWEN_CHAT_API;
}
// Track active request to prevent stream interleaving
let activeRequest = null;
export function abortActiveChat() {
if (activeRequest) {
console.log('[QwenAPI] Aborting previous request...');
try {
activeRequest.destroy();
} catch (e) {
console.warn('[QwenAPI] Abort warning:', e.message);
}
activeRequest = null;
}
}
export async function streamChat(messages, model = 'qwen-coder-plus', onChunk, onComplete, onError, onStatus) {
// Abort any existing request to prevent interleaving
abortActiveChat();
const log = (msg) => {
console.log('[QwenAPI]', msg);
if (onStatus) onStatus(msg);
};
log('Loading tokens...');
const tokens = loadTokens();
if (!tokens?.access_token) {
log('Error: No tokens found.');
console.error('[QwenAPI] Authentication missing. No valid tokens found.');
onError(new Error('AUTHENTICATION_REQUIRED: Please run OpenQode > Option 4, then /auth in Qwen CLI.'));
return;
}
if (!isTokenValid(tokens)) {
log('Error: Tokens expired.');
console.error('[QwenAPI] Token expired.');
onError(new Error('TOKEN_EXPIRED: Please run OpenQode > Option 4 and /auth again.'));
return;
}
const endpoint = getApiEndpoint(tokens);
const url = new URL(endpoint);
const requestId = crypto.randomUUID();
const body = JSON.stringify({
model: normalizeModel(model),
messages: messages,
stream: true
});
log(`Connecting to ${url.hostname}...`);
console.log(`[QwenAPI] Calling ${url.href} with model ${normalizeModel(model)}`);
const options = {
hostname: url.hostname,
port: 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${tokens.access_token}`,
'x-request-id': requestId,
'Content-Length': Buffer.byteLength(body)
}
};
const req = https.request(options, (res) => {
activeRequest = req;
let fullResponse = '';
if (res.statusCode !== 200) {
let errBody = '';
res.on('data', (c) => errBody += c.toString());
res.on('end', () => {
onError(new Error(`API Error ${res.statusCode}: ${errBody}`));
});
return;
}
res.setEncoding('utf8');
let buffer = '';
res.on('data', (chunk) => {
buffer += chunk;
// split by double newline or newline
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line
for (const line of lines) {
const trimmed = line.trim();
// Check prefix
if (!trimmed.startsWith('data: ')) continue;
const data = trimmed.replace('data: ', '').trim();
if (data === '[DONE]') {
onComplete(fullResponse);
return;
}
try {
const parsed = JSON.parse(data);
// Qwen strict response matching
const choice = parsed.choices?.[0];
const content = choice?.delta?.content || choice?.message?.content || '';
if (content) {
fullResponse += content;
onChunk(content);
}
} catch (e) {
// Ignore parsing errors for intermediate crumbs
}
}
});
res.on('end', () => {
onComplete(fullResponse);
});
});
req.on('error', (e) => {
console.error('[QwenAPI] Request error:', e.message);
onError(e);
});
req.write(body);
req.end();
}