Release v1.01 Enhanced: Vi Control, TUI Gen5, Core Stability
This commit is contained in:
192
bin/goose-ultra-final/electron/qwen-api.js
Normal file
192
bin/goose-ultra-final/electron/qwen-api.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user