@@ -109,7 +109,7 @@ const WORKFLOW_DEFINITIONS_FILE = path.join(CONFIG_DIR, 'codexmate-workflows.jso
109109const WORKFLOW_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-workflow-runs.jsonl');
110110const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
111111const DEFAULT_QWEN_LOCAL_MODEL = 'coder-model';
112- const DEFAULT_QWEN_OAUTH_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode /v1' ;
112+ const DEFAULT_QWEN_OAUTH_BASE_URL = 'https://portal.qwen.ai /v1';
113113const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
114114const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
115115const CODEX_BACKUP_NAME = 'codex-config';
@@ -513,6 +513,24 @@ let g_modelsCache = new Map();
513513let g_modelsInFlight = new Map();
514514let g_builtinProxyRuntime = null;
515515const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
516+ const DEFAULT_QWEN_LOCAL_MODELS = Object.freeze(['coder-model']);
517+ const QWEN_PROXY_DEFAULT_HEADERS = Object.freeze({
518+ 'X-Stainless-Runtime-Version': 'v22.17.0',
519+ 'User-Agent': 'QwenCode/0.0.0 codexmate-local-proxy',
520+ 'X-Stainless-Lang': 'js',
521+ 'Accept-Language': '*',
522+ 'X-Dashscope-Cachecontrol': 'enable',
523+ 'X-Stainless-Os': process.platform === 'darwin' ? 'MacOS' : process.platform,
524+ 'X-Dashscope-Authtype': 'qwen-oauth',
525+ 'X-Stainless-Arch': process.arch,
526+ 'X-Stainless-Runtime': 'node',
527+ 'X-Stainless-Retry-Count': '0',
528+ 'Accept-Encoding': 'gzip, deflate',
529+ 'X-Stainless-Package-Version': '5.11.0',
530+ 'Sec-Fetch-Mode': 'cors',
531+ 'Connection': 'keep-alive',
532+ 'X-Dashscope-Useragent': 'QwenCode/0.0.0 codexmate-local-proxy'
533+ });
516534
517535function isBuiltinProxyProvider(providerName) {
518536 return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase();
@@ -1261,7 +1279,9 @@ function normalizeQwenOAuthResourceUrl(value) {
12611279 const raw = typeof value === 'string' ? value.trim() : '';
12621280 if (!raw) return DEFAULT_QWEN_OAUTH_BASE_URL;
12631281 const withProtocol = /^[a-z]+:\/\//i.test(raw) ? raw : `https://${raw}`;
1264- return normalizeBaseUrl ( withProtocol ) || DEFAULT_QWEN_OAUTH_BASE_URL ;
1282+ const normalized = normalizeBaseUrl(withProtocol);
1283+ if (!normalized) return DEFAULT_QWEN_OAUTH_BASE_URL;
1284+ return /\/v1$/i.test(normalized) ? normalized : `${normalized}/v1`;
12651285}
12661286
12671287function readQwenOAuthCredentials() {
@@ -6068,7 +6088,7 @@ function buildLocalProviderConfigEntry(overrides = {}) {
60686088 return {
60696089 name: DEFAULT_LOCAL_PROVIDER_NAME,
60706090 base_url: buildBuiltinProxyProviderBaseUrl(settings),
6071- wire_api : 'responses ' ,
6091+ wire_api: 'chat/completions ',
60726092 requires_openai_auth: false,
60736093 preferred_auth_method: '',
60746094 request_max_retries: 4,
@@ -6434,6 +6454,53 @@ function resolveBuiltinProxyRequestUpstream(upstream) {
64346454 };
64356455}
64366456
6457+ function buildQwenLocalModelsPayload() {
6458+ return {
6459+ object: 'list',
6460+ data: DEFAULT_QWEN_LOCAL_MODELS.map((id) => ({
6461+ id,
6462+ object: 'model',
6463+ created: 0,
6464+ owned_by: 'qwen-cli-oauth'
6465+ }))
6466+ };
6467+ }
6468+
6469+ function sendJsonResponse(res, statusCode, payload) {
6470+ const body = JSON.stringify(payload);
6471+ res.writeHead(statusCode, {
6472+ 'Content-Type': 'application/json; charset=utf-8',
6473+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
6474+ });
6475+ res.end(body, 'utf-8');
6476+ }
6477+
6478+ function buildQwenProxyHeaders(req, token) {
6479+ const incoming = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
6480+ const headers = { ...incoming };
6481+ delete headers.host;
6482+ delete headers.connection;
6483+ delete headers['content-length'];
6484+ delete headers.authorization;
6485+ delete headers['x-api-key'];
6486+
6487+ const acceptHeader = typeof incoming.accept === 'string' ? incoming.accept.toLowerCase() : '';
6488+ const wantsStream = acceptHeader.includes('text/event-stream');
6489+ for (const [key, value] of Object.entries(QWEN_PROXY_DEFAULT_HEADERS)) {
6490+ headers[key] = value;
6491+ }
6492+ headers.Authorization = /^bearer\s+/i.test(token || '') ? token : `Bearer ${token}`;
6493+ headers.Accept = wantsStream ? 'text/event-stream' : 'application/json';
6494+ headers['Content-Type'] = typeof incoming['content-type'] === 'string' && incoming['content-type'].trim()
6495+ ? incoming['content-type']
6496+ : 'application/json';
6497+ headers['x-codexmate-proxy'] = '1';
6498+ if (!headers['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
6499+ headers['x-forwarded-for'] = req.socket.remoteAddress;
6500+ }
6501+ return headers;
6502+ }
6503+
64376504function createBuiltinProxyServer(settings, upstream) {
64386505 const connections = new Set();
64396506 const timeoutMs = settings.timeoutMs;
@@ -6478,12 +6545,21 @@ function createBuiltinProxyServer(settings, upstream) {
64786545
64796546 const liveUpstream = resolveBuiltinProxyRequestUpstream(upstream);
64806547 if (liveUpstream.error) {
6481- const body = JSON . stringify ( { error : liveUpstream . error } ) ;
6482- res . writeHead ( 503 , {
6483- 'Content-Type' : 'application/json; charset=utf-8' ,
6484- 'Content-Length' : Buffer . byteLength ( body , 'utf-8' )
6548+ sendJsonResponse(res, 503, { error: liveUpstream.error });
6549+ return;
6550+ }
6551+
6552+ if (liveUpstream.authSource === 'qwen-oauth' && incomingPath === '/v1/models') {
6553+ sendJsonResponse(res, 200, buildQwenLocalModelsPayload());
6554+ return;
6555+ }
6556+
6557+ if (liveUpstream.authSource === 'qwen-oauth'
6558+ && incomingPath !== '/v1/chat/completions'
6559+ && incomingPath !== '/v1/models') {
6560+ sendJsonResponse(res, 404, {
6561+ error: 'local provider 仅支持 /v1/models 和 /v1/chat/completions'
64856562 });
6486- res . end ( body , 'utf-8' ) ;
64876563 return;
64886564 }
64896565
@@ -6515,26 +6591,30 @@ function createBuiltinProxyServer(settings, upstream) {
65156591 return;
65166592 }
65176593
6518- const requestHeaders = { ...req . headers } ;
6519- delete requestHeaders . host ;
6520- delete requestHeaders . connection ;
6521- delete requestHeaders [ 'content-length' ] ;
6522- if ( liveUpstream . authHeader ) {
6523- requestHeaders . authorization = liveUpstream . authHeader ;
6524- }
6525- if ( liveUpstream . extraHeaders && typeof liveUpstream . extraHeaders === 'object' ) {
6526- for ( const [ key , value ] of Object . entries ( liveUpstream . extraHeaders ) ) {
6527- if ( ! key ) continue ;
6528- if ( value === undefined || value === null || value === '' ) {
6529- delete requestHeaders [ key . toLowerCase ( ) ] ;
6530- continue ;
6594+ const requestHeaders = liveUpstream.authSource === 'qwen-oauth'
6595+ ? buildQwenProxyHeaders(req, liveUpstream.authHeader || '')
6596+ : { ...req.headers };
6597+ if (liveUpstream.authSource !== 'qwen-oauth') {
6598+ delete requestHeaders.host;
6599+ delete requestHeaders.connection;
6600+ delete requestHeaders['content-length'];
6601+ if (liveUpstream.authHeader) {
6602+ requestHeaders.authorization = liveUpstream.authHeader;
6603+ }
6604+ if (liveUpstream.extraHeaders && typeof liveUpstream.extraHeaders === 'object') {
6605+ for (const [key, value] of Object.entries(liveUpstream.extraHeaders)) {
6606+ if (!key) continue;
6607+ if (value === undefined || value === null || value === '') {
6608+ delete requestHeaders[key.toLowerCase()];
6609+ continue;
6610+ }
6611+ requestHeaders[key] = value;
65316612 }
6532- requestHeaders [ key ] = value ;
65336613 }
6534- }
6535- requestHeaders [ 'x-codexmate-proxy ' ] = '1' ;
6536- if ( ! requestHeaders [ 'x-forwarded-for' ] && req . socket && req . socket . remoteAddress ) {
6537- requestHeaders [ 'x-forwarded-for' ] = req . socket . remoteAddress ;
6614+ requestHeaders['x-codexmate-proxy'] = '1';
6615+ if (! requestHeaders['x-forwarded-for '] && req.socket && req.socket.remoteAddress) {
6616+ requestHeaders['x-forwarded-for'] = req.socket. remoteAddress;
6617+ }
65386618 }
65396619
65406620 const transport = targetUrl.protocol === 'https:' ? https : http;
0 commit comments