@pedrofariasx/qwenproxy 1.1.0

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.
Files changed (59) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +292 -0
  3. package/bin/qwenproxy.mjs +11 -0
  4. package/package.json +56 -0
  5. package/src/api/models.ts +183 -0
  6. package/src/api/server.ts +126 -0
  7. package/src/cache/memory-cache.ts +186 -0
  8. package/src/core/account-manager.ts +132 -0
  9. package/src/core/accounts.ts +78 -0
  10. package/src/core/config.ts +91 -0
  11. package/src/core/database.ts +92 -0
  12. package/src/core/logger.ts +96 -0
  13. package/src/core/metrics.ts +169 -0
  14. package/src/core/model-registry.ts +30 -0
  15. package/src/core/stream-registry.ts +40 -0
  16. package/src/core/watchdog.ts +130 -0
  17. package/src/index.ts +7 -0
  18. package/src/linter/extraction-engine.ts +165 -0
  19. package/src/linter/index.ts +258 -0
  20. package/src/linter/repair-normalize.ts +245 -0
  21. package/src/linter/safety-gate.ts +219 -0
  22. package/src/linter/streaming-state-machine.ts +252 -0
  23. package/src/linter/structural-parser.ts +352 -0
  24. package/src/linter/types.ts +74 -0
  25. package/src/login.ts +228 -0
  26. package/src/routes/chat.ts +801 -0
  27. package/src/routes/upload.ts +700 -0
  28. package/src/services/playwright.ts +778 -0
  29. package/src/services/qwen.ts +500 -0
  30. package/src/tests/advanced.test.ts +227 -0
  31. package/src/tests/agenticStress.test.ts +360 -0
  32. package/src/tests/concurrency.test.ts +103 -0
  33. package/src/tests/concurrentChat.test.ts +71 -0
  34. package/src/tests/delta.test.ts +63 -0
  35. package/src/tests/index.test.ts +356 -0
  36. package/src/tests/jsonFix.test.ts +98 -0
  37. package/src/tests/linter.test.ts +151 -0
  38. package/src/tests/parallel.test.ts +42 -0
  39. package/src/tests/parser.test.ts +89 -0
  40. package/src/tests/rotation.test.ts +45 -0
  41. package/src/tests/streamingOptimizations.test.ts +328 -0
  42. package/src/tests/structureVerification.test.ts +176 -0
  43. package/src/tools/ast.ts +15 -0
  44. package/src/tools/coercion.ts +67 -0
  45. package/src/tools/confidence.ts +48 -0
  46. package/src/tools/detector.ts +40 -0
  47. package/src/tools/executor.ts +236 -0
  48. package/src/tools/parser.ts +446 -0
  49. package/src/tools/pipeline.ts +122 -0
  50. package/src/tools/registry-runtime.ts +34 -0
  51. package/src/tools/registry.ts +142 -0
  52. package/src/tools/repair.ts +42 -0
  53. package/src/tools/schema.ts +285 -0
  54. package/src/tools/types.ts +104 -0
  55. package/src/tools/validator.ts +33 -0
  56. package/src/utils/context-truncation.ts +61 -0
  57. package/src/utils/json.ts +114 -0
  58. package/src/utils/qwen-stream-parser.ts +286 -0
  59. package/src/utils/types.ts +101 -0
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2026 Pedro Farias
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # QwenProxy
2
+
3
+ Proxy API local compatível com OpenAI que roteia requisições para os modelos do **Qwen (chat.qwen.ai)** via automação de navegador com Playwright. Suporte a múltiplas contas com rotação automática, execução de ferramentas, modo de pensamento (reasoning), persistência de sessão e armazenamento em SQLite.
4
+
5
+ [![CI](https://github.com/pedrofariasx/qwenproxy/actions/workflows/ci.yml/badge.svg)](https://github.com/pedrofariasx/qwenproxy/actions/workflows/ci.yml)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue)](https://www.typescriptlang.org/)
7
+ [![Hono](https://img.shields.io/badge/Hono-4.12-green)](https://hono.dev/)
8
+ [![Playwright](https://img.shields.io/badge/Playwright-1.60-blueviolet)](https://playwright.dev/)
9
+ [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](LICENSE)
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ - **OpenAI API Compatible** — Interface compatível com `/v1/chat/completions` e `/v1/models`.
16
+ - **Multi-Account** — Gerencie múltiplas contas Qwen com rotação round-robin e cooldown automático.
17
+ - **SQLite Storage** — Contas salvas em banco de dados SQLite (WAL mode) para performance e confiabilidade.
18
+ - **Reasoning Support** — Suporte completo ao modo de pensamento (thinking) dos modelos Qwen.
19
+ - **Tool Execution** — Sistema de execução de ferramentas locais integrado ao fluxo do chat.
20
+ - **Session Persistence** — Perfil de navegador persistente por conta em `qwen_profiles/`.
21
+ - **Auto-Login** — Login automático via credenciais com recuperação de sessão.
22
+ - **Browser Selection** — Escolha entre Chromium, Chrome, Firefox, Edge ou WebKit.
23
+ - **Monitoring** — Health check, métricas Prometheus e watchdog integrados.
24
+ - **Docker Ready** — Deploy para VPS com Docker, volumes persistentes e graceful shutdown.
25
+
26
+ ---
27
+
28
+ ## Arquitetura
29
+
30
+ ```mermaid
31
+ graph TD
32
+ Client[Cliente OpenAI/SDK] -->|HTTP| Proxy[QwenProxy - Hono]
33
+ Proxy -->|/v1/chat/completions| Handler[Chat Handler]
34
+ Proxy -->|/v1/models| Models[Models API]
35
+ Handler --> AccountMgr[Account Manager]
36
+ AccountMgr -->|Round-Robin| Accounts[(SQLite)]
37
+ AccountMgr --> Playwright[Playwright Service]
38
+ Playwright --> Browser1[Browser - Conta 1]
39
+ Playwright --> Browser2[Browser - Conta 2]
40
+ Playwright --> BrowserN[Browser - Conta N]
41
+ Handler --> QwenAPI[chat.qwen.ai]
42
+ Handler --> Tools[Tool Executor]
43
+
44
+ subgraph "Persistência"
45
+ Accounts
46
+ Profiles[qwen_profiles/]
47
+ end
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Pré-requisitos
53
+
54
+ | Dependência | Versão Mínima | Instalação |
55
+ |------------|--------------|-----------|
56
+ | Node.js | v20.x | [nvm](https://github.com/nvm-sh/nvm) |
57
+ | npm | v9.x | Incluído com Node.js |
58
+ | Playwright | - | `npx playwright install` |
59
+ | Docker (opcional) | v24.x | [Docker Docs](https://docs.docker.com/get-docker/) |
60
+
61
+ ---
62
+
63
+ ## Instalação
64
+
65
+ ### Via npm
66
+
67
+ ```bash
68
+ git clone https://github.com/pedrofariasx/qwenproxy.git
69
+ cd qwenproxy
70
+ npm install
71
+ npx playwright install
72
+ ```
73
+
74
+ ### Via Docker
75
+
76
+ ```bash
77
+ docker-compose up -d
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Configuração
83
+
84
+ Crie o arquivo `.env` na raiz do projeto (veja `.env.example`):
85
+
86
+ ```env
87
+ # Porta do servidor (default: 3000)
88
+ PORT=3000
89
+
90
+ # Chave de API para proteger os endpoints (opcional)
91
+ API_KEY=sua-chave-secreta-aqui
92
+
93
+ # Credenciais Qwen para login automático (modo single-account)
94
+ QWEN_EMAIL=seu-email@exemplo.com
95
+ QWEN_PASSWORD=sua-senha-aqui
96
+
97
+ # Navegador (chromium, firefox, chrome, edge)
98
+ BROWSER=chromium
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Gerenciamento de Contas
104
+
105
+ As contas são armazenadas em SQLite (`data/qwenproxy.db`). Use o CLI interativo para gerenciar:
106
+
107
+ ```bash
108
+ # Abrir o gerenciador de contas
109
+ npm run login
110
+
111
+ # Com navegador específico
112
+ npm run login:firefox
113
+ npm run login:chrome
114
+ npm run login:edge
115
+ ```
116
+
117
+ O menu interativo permite:
118
+ - **[A]** Adicionar conta com credenciais (email + senha)
119
+ - **[M]** Adicionar conta via login manual no navegador
120
+ - **[R]** Remover uma conta
121
+ - **[L]** Login em todas as contas (inicializar sessões)
122
+
123
+ > Na primeira execução, se existir um `accounts.json` antigo, as contas serão migradas automaticamente para SQLite.
124
+
125
+ ---
126
+
127
+ ## Uso
128
+
129
+ ### Iniciar o servidor
130
+
131
+ ```bash
132
+ npm start # Chromium (padrão)
133
+ npm run start:chrome # Google Chrome
134
+ npm run start:firefox # Firefox
135
+ npm run start:edge # Microsoft Edge
136
+ ```
137
+
138
+ O servidor inicia em `http://localhost:3000` com as seguintes rotas:
139
+
140
+ | Rota | Método | Descrição |
141
+ |------|--------|-----------|
142
+ | `/v1/chat/completions` | POST | Chat completions (streaming + non-streaming) |
143
+ | `/v1/chat/completions/stop` | POST | Abortar uma geração ativa |
144
+ | `/v1/models` | GET | Listar modelos disponíveis |
145
+ | `/v1/models/:model` | GET | Informações de um modelo específico |
146
+ | `/health` | GET | Health check com status do sistema |
147
+ | `/metrics` | GET | Métricas no formato Prometheus |
148
+
149
+ ---
150
+
151
+ ## Exemplos de Integração
152
+
153
+ ### OpenAI SDK (Node.js)
154
+
155
+ ```typescript
156
+ import OpenAI from 'openai';
157
+
158
+ const openai = new OpenAI({
159
+ baseURL: 'http://localhost:3000/v1',
160
+ apiKey: process.env.API_KEY || 'sk-no-key-required'
161
+ });
162
+
163
+ const completion = await openai.chat.completions.create({
164
+ model: 'qwen-plus',
165
+ messages: [{ role: 'user', content: 'Explique como funciona o Playwright.' }]
166
+ });
167
+
168
+ console.log(completion.choices[0].message.content);
169
+ ```
170
+
171
+ ### cURL
172
+
173
+ ```bash
174
+ curl http://localhost:3000/v1/chat/completions \
175
+ -H "Content-Type: application/json" \
176
+ -H "Authorization: Bearer sua-chave" \
177
+ -d '{
178
+ "model": "qwen-plus",
179
+ "messages": [{"role": "user", "content": "Hello!"}],
180
+ "stream": true
181
+ }'
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Deploy com Docker
187
+
188
+ ### docker-compose.yml
189
+
190
+ ```yaml
191
+ services:
192
+ qwenproxy:
193
+ build: .
194
+ container_name: qwenproxy
195
+ ports:
196
+ - "${PORT:-3000}:3000"
197
+ env_file:
198
+ - .env
199
+ volumes:
200
+ - ./data:/app/data # Banco SQLite
201
+ - ./qwen_profiles:/app/qwen_profiles # Sessões dos navegadores
202
+ restart: unless-stopped
203
+ ```
204
+
205
+ ### Volumes persistentes
206
+
207
+ | Volume | Conteúdo |
208
+ |--------|----------|
209
+ | `./data` | Banco SQLite com as contas (`qwenproxy.db`) |
210
+ | `./qwen_profiles` | Perfis de navegador por conta (cookies, sessões) |
211
+
212
+ ---
213
+
214
+ ## Estrutura do Projeto
215
+
216
+ ```
217
+ qwenproxy/
218
+ ├── src/
219
+ │ ├── index.ts # Entry point
220
+ │ ├── login.ts # CLI de gerenciamento de contas
221
+ │ ├── api/
222
+ │ │ ├── models.ts # Endpoints /v1/models
223
+ │ │ └── server.ts # Servidor Hono + startup
224
+ │ ├── cache/
225
+ │ │ └── memory-cache.ts # Cache em memória com TTL
226
+ │ ├── core/
227
+ │ │ ├── account-manager.ts # Rotação round-robin + cooldowns
228
+ │ │ ├── accounts.ts # CRUD de contas (SQLite)
229
+ │ │ ├── config.ts # Configuração com Zod
230
+ │ │ ├── database.ts # Conexão e migrations SQLite
231
+ │ │ ├── logger.ts # Logger estruturado
232
+ │ │ ├── metrics.ts # Coleta de métricas
233
+ │ │ ├── model-registry.ts # Registro de modelos e context windows
234
+ │ │ ├── stream-registry.ts # Tracking de streams ativos
235
+ │ │ └── watchdog.ts # Health monitoring
236
+ │ ├── linter/
237
+ │ │ ├── bar.ts # Facade
238
+ │ │ ├── extraction-engine.ts # Extraction engine
239
+ │ │ ├── foo.ts # Exports
240
+ │ │ ├── index.ts # Main public API
241
+ │ │ ├── repair-normalize.ts # Repair and normalize
242
+ │ │ ├── safety-gate.ts # Safety gate
243
+ │ │ ├── streaming-state-machine.ts # Streaming state machine
244
+ │ │ ├── structural-parser.ts # Structural parser
245
+ │ │ └── types.ts # Types
246
+ │ ├── routes/
247
+ │ │ └── chat.ts # Handler /v1/chat/completions
248
+ │ ├── services/
249
+ │ │ ├── playwright.ts # Automação de navegador
250
+ │ │ └── qwen.ts # Integração com API do Qwen
251
+ │ ├── tests/ # Testes automatizados
252
+ │ ├── tools/
253
+ │ │ ├── executor.ts # Execução de ferramentas
254
+ │ │ ├── parser.ts # Parser de <tool_call> tags
255
+ │ │ ├── registry.ts # Registro de tools
256
+ │ │ ├── schema.ts # Validação JSON Schema
257
+ │ │ └── types.ts # Tipos do sistema de tools
258
+ │ └── utils/
259
+ │ ├── context-truncation.ts # Truncamento de contexto
260
+ │ ├── json.ts # Parser JSON robusto
261
+ │ └── types.ts # Re-exports de tipos
262
+ ├── data/ # Banco SQLite (gitignored)
263
+ ├── qwen_profiles/ # Perfis de navegador por conta (gitignored)
264
+ ├── Dockerfile
265
+ ├── docker-compose.yml
266
+ └── package.json
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Troubleshooting
272
+
273
+ | Problema | Solução |
274
+ |----------|---------|
275
+ | Porta em uso | Altere `PORT` no `.env` ou encerre o processo na porta 3000 |
276
+ | Navegador não abre | Execute `npx playwright install` |
277
+ | Sessão expirada | Execute `npm run login` para renovar cookies |
278
+ | Rate limit em todas as contas | Adicione mais contas via `npm run login` |
279
+ | Banco corrompido | Apague `data/qwenproxy.db` e re-adicione as contas |
280
+
281
+ ---
282
+
283
+ ## Disclaimer
284
+
285
+ > Este projeto é fornecido estritamente para fins educacionais e de pesquisa.
286
+
287
+ Os autores não incentivam ou endossam:
288
+ - Violação dos Termos de Serviço da plataforma Qwen.
289
+ - Automação não autorizada em larga escala.
290
+ - Uso para atividades maliciosas.
291
+
292
+ **Use por sua conta e risco.**
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const script = path.join(__dirname, '..', 'src', 'index.ts');
8
+
9
+ const args = process.argv.slice(2);
10
+ const proc = spawn('node', ['--import', 'tsx', script, ...args], { stdio: 'inherit' });
11
+ proc.on('close', (code) => process.exit(code ?? 0));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@pedrofariasx/qwenproxy",
3
+ "version": "1.1.0",
4
+ "description": "Local OpenAI-compatible proxy API that routes requests to Qwen (chat.qwen.ai) via Playwright browser automation.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "npx tsx src/index.ts",
8
+ "start:firefox": "npx tsx src/index.ts --browser=firefox",
9
+ "start:chrome": "npx tsx src/index.ts --browser=chrome",
10
+ "start:edge": "npx tsx src/index.ts --browser=edge",
11
+ "login": "npx tsx src/login.ts",
12
+ "login:firefox": "npx tsx src/login.ts --browser=firefox",
13
+ "login:chrome": "npx tsx src/login.ts --browser=chrome",
14
+ "login:edge": "npx tsx src/login.ts --browser=edge",
15
+ "test": "tsx --test src/**/*.test.ts",
16
+ "typecheck": "tsc --noEmit"
17
+ },
18
+ "keywords": [],
19
+ "author": "Pedro Farias",
20
+ "license": "ISC",
21
+ "dependencies": {
22
+ "@hono/node-server": "^2.0.3",
23
+ "ajv": "^8.20.0",
24
+ "ali-oss": "^6.23.0",
25
+ "better-sqlite3": "^12.10.0",
26
+ "dotenv": "^17.4.2",
27
+ "hono": "^4.12.21",
28
+ "playwright": "^1.60.0",
29
+ "tsx": "^4.22.3",
30
+ "uuid": "^14.0.0",
31
+ "zod": "^4.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@semantic-release/changelog": "^6.0.3",
35
+ "@semantic-release/exec": "^7.1.0",
36
+ "@semantic-release/git": "^10.0.1",
37
+ "@types/ajv": "^0.0.5",
38
+ "@types/ali-oss": "^6.23.3",
39
+ "@types/better-sqlite3": "^7.6.13",
40
+ "@types/node": "^25.9.1",
41
+ "@types/uuid": "^11.0.0",
42
+ "semantic-release": "^25.0.3",
43
+ "typescript": "^6.0.3"
44
+ },
45
+ "bin": {
46
+ "qwenproxy": "./bin/qwenproxy.mjs"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "files": [
52
+ "src/**/*",
53
+ "bin/**/*"
54
+ ],
55
+ "type": "module"
56
+ }
@@ -0,0 +1,183 @@
1
+ import { Hono } from 'hono'
2
+ import { config } from '../core/config.js'
3
+ import { getBasicHeaders } from '../services/playwright.js'
4
+ import { loadAccounts } from '../core/accounts.js'
5
+ import { getAccountCooldownInfo } from '../core/account-manager.js'
6
+ import { cache } from '../cache/memory-cache.js'
7
+
8
+ const app = new Hono()
9
+
10
+ app.get('/v1/models', async (c) => {
11
+ try {
12
+ let accountId: string | undefined
13
+ try {
14
+ const accounts = loadAccounts()
15
+ const account = accounts.find(a => !getAccountCooldownInfo(a.id)) || accounts[0]
16
+ if (account) {
17
+ accountId = account.id
18
+ }
19
+ } catch (e) {
20
+ console.warn('Failed to retrieve account for models endpoint:', e)
21
+ }
22
+
23
+ const cacheKey = `models:${accountId || 'global'}` as any
24
+ const cached = await cache.get<any>(cacheKey)
25
+ if (cached) {
26
+ return c.json(cached)
27
+ }
28
+
29
+ const { cookie, userAgent, bxV } = await getBasicHeaders(accountId)
30
+ const response = await fetch(`${config.qwen.baseUrl}/api/models`, {
31
+ headers: {
32
+ 'Accept': 'application/json, text/plain, */*',
33
+ 'Accept-Language': 'pt-BR,pt;q=0.9',
34
+ 'Connection': 'keep-alive',
35
+ 'Referer': `${config.qwen.baseUrl}/c/demo`,
36
+ 'Sec-Fetch-Dest': 'empty',
37
+ 'Sec-Fetch-Mode': 'cors',
38
+ 'Sec-Fetch-Site': 'same-origin',
39
+ 'User-Agent': userAgent,
40
+ 'X-Request-Id': crypto.randomUUID(),
41
+ 'source': 'web',
42
+ 'bx-v': bxV,
43
+ 'sec-ch-ua': '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
44
+ 'sec-ch-ua-mobile': '?0',
45
+ 'sec-ch-ua-platform': '"Linux"',
46
+ 'Timezone': new Date().toString(),
47
+ 'Cookie': cookie,
48
+ },
49
+ })
50
+
51
+ if (!response.ok) {
52
+ throw new Error(`Failed to fetch models: ${response.status}`)
53
+ }
54
+
55
+ const data = await response.json()
56
+
57
+ const models = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
58
+
59
+ const formatted = {
60
+ object: 'list',
61
+ data: [
62
+ ...models.map((model: any) => ({
63
+ id: model.id,
64
+ name: model.name,
65
+ object: 'model',
66
+ owned_by: model.owned_by,
67
+ created: model.info?.created_at || Date.now(),
68
+ context_window: model.info?.meta?.max_context_length,
69
+ capabilities: model.info?.meta?.capabilities,
70
+ })),
71
+ ...models.map((model: any) => ({
72
+ id: `${model.id}-no-thinking`,
73
+ name: `${model.name} (No Thinking)`,
74
+ object: 'model',
75
+ owned_by: model.owned_by,
76
+ created: model.info?.created_at || Date.now(),
77
+ context_window: model.info?.meta?.max_context_length,
78
+ capabilities: model.info?.meta?.capabilities,
79
+ })),
80
+ ],
81
+ }
82
+
83
+ // Cache the formatted models list for 5 minutes (300 seconds)
84
+ await cache.set(cacheKey, formatted, 300)
85
+
86
+ return c.json(formatted)
87
+ } catch (error: any) {
88
+ console.error('Error fetching models:', error)
89
+ return c.json({ error: error.message }, 500)
90
+ }
91
+ })
92
+
93
+ app.get('/v1/models/:model', async (c) => {
94
+ try {
95
+ const modelId = c.req.param('model')
96
+
97
+ let accountId: string | undefined
98
+ try {
99
+ const accounts = loadAccounts()
100
+ const account = accounts.find(a => !getAccountCooldownInfo(a.id)) || accounts[0]
101
+ if (account) {
102
+ accountId = account.id
103
+ }
104
+ } catch (e) {
105
+ console.warn('Failed to retrieve account for model endpoint:', e)
106
+ }
107
+
108
+ const cacheKey = `models:${accountId || 'global'}` as any
109
+ let formattedList = await cache.get<any>(cacheKey)
110
+ let models = formattedList?.data || []
111
+
112
+ if (models.length === 0) {
113
+ const { cookie, userAgent, bxV } = await getBasicHeaders(accountId)
114
+ const response = await fetch(`${config.qwen.baseUrl}/api/models`, {
115
+ headers: {
116
+ 'Accept': 'application/json, text/plain, */*',
117
+ 'Accept-Language': 'pt-BR,pt;q=0.9',
118
+ 'Connection': 'keep-alive',
119
+ 'Referer': `${config.qwen.baseUrl}/c/demo`,
120
+ 'Sec-Fetch-Dest': 'empty',
121
+ 'Sec-Fetch-Mode': 'cors',
122
+ 'Sec-Fetch-Site': 'same-origin',
123
+ 'User-Agent': userAgent,
124
+ 'X-Request-Id': crypto.randomUUID(),
125
+ 'source': 'web',
126
+ 'bx-v': bxV,
127
+ 'sec-ch-ua': '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
128
+ 'sec-ch-ua-mobile': '?0',
129
+ 'sec-ch-ua-platform': '"Linux"',
130
+ 'Timezone': new Date().toString(),
131
+ 'Cookie': cookie,
132
+ },
133
+ })
134
+
135
+ if (!response.ok) {
136
+ throw new Error(`Failed to fetch models: ${response.status}`)
137
+ }
138
+
139
+ const data = await response.json()
140
+ const rawModels = Array.isArray(data?.data) ? data.data : Array.isArray(data) ? data : []
141
+
142
+ const formatted = {
143
+ object: 'list',
144
+ data: [
145
+ ...rawModels.map((model: any) => ({
146
+ id: model.id,
147
+ name: model.name,
148
+ object: 'model',
149
+ owned_by: model.owned_by,
150
+ created: model.info?.created_at || Date.now(),
151
+ context_window: model.info?.meta?.max_context_length,
152
+ capabilities: model.info?.meta?.capabilities,
153
+ })),
154
+ ...rawModels.map((model: any) => ({
155
+ id: `${model.id}-no-thinking`,
156
+ name: `${model.name} (No Thinking)`,
157
+ object: 'model',
158
+ owned_by: model.owned_by,
159
+ created: model.info?.created_at || Date.now(),
160
+ context_window: model.info?.meta?.max_context_length,
161
+ capabilities: model.info?.meta?.capabilities,
162
+ })),
163
+ ],
164
+ }
165
+
166
+ await cache.set(cacheKey, formatted, 300)
167
+ models = formatted.data
168
+ }
169
+
170
+ const model = models.find((m: any) => m.id === modelId)
171
+
172
+ if (!model) {
173
+ return c.json({ error: 'Model not found' }, 404)
174
+ }
175
+
176
+ return c.json(model)
177
+ } catch (error: any) {
178
+ console.error('Error fetching model:', error)
179
+ return c.json({ error: error.message }, 500)
180
+ }
181
+ })
182
+
183
+ export { app }
@@ -0,0 +1,126 @@
1
+ import { Hono } from 'hono'
2
+ import { serve } from '@hono/node-server'
3
+ import { config } from '../core/config.js'
4
+ import { metrics } from '../core/metrics.js'
5
+ import { cache } from '../cache/memory-cache.js'
6
+ import { Watchdog } from '../core/watchdog.js'
7
+ import { app as modelsApp } from './models.js'
8
+ import { chatCompletions, chatCompletionsStop } from '../routes/chat.js'
9
+ import { uploadFile } from '../routes/upload.js'
10
+
11
+ const app = new Hono()
12
+
13
+ let watchdog: Watchdog
14
+ let server: any
15
+
16
+ app.use('*', async (c, next) => {
17
+ metrics.increment('requests.total')
18
+ const start = Date.now()
19
+ await next()
20
+ const duration = Date.now() - start
21
+ metrics.histogram('latency.request', duration)
22
+ c.header('X-Response-Time', `${duration}ms`)
23
+ })
24
+
25
+ app.use('/v1/*', async (c, next) => {
26
+ const apiKey = process.env.API_KEY || config.apiKey
27
+ if (apiKey) {
28
+ const auth = c.req.header('Authorization')
29
+ if (!auth?.startsWith('Bearer ')) {
30
+ return c.json({ error: 'Missing or invalid Authorization header' }, 401)
31
+ }
32
+ const token = auth.slice(7)
33
+ if (token !== apiKey) {
34
+ return c.json({ error: 'Invalid API key' }, 401)
35
+ }
36
+ }
37
+ await next()
38
+ })
39
+
40
+ app.route('', modelsApp)
41
+ app.post('/v1/chat/completions', chatCompletions)
42
+ app.post('/v1/chat/completions/stop', chatCompletionsStop)
43
+ app.post('/v1/upload', uploadFile)
44
+
45
+ app.get('/health', async (c) => {
46
+ const status = await watchdog?.getStatus()
47
+ return c.json({
48
+ status: status?.overall || 'unknown',
49
+ timestamp: Date.now(),
50
+ metrics: {
51
+ cache: await cache?.getStats(),
52
+ },
53
+ })
54
+ })
55
+
56
+ app.get('/metrics', (c) => {
57
+ return c.text(metrics.formatPrometheus(), {
58
+ headers: { 'Content-Type': 'text/plain; version=0.0.4' },
59
+ })
60
+ })
61
+
62
+ app.onError((err, c) => {
63
+ metrics.increment('requests.errors')
64
+ console.error('API Error:', err)
65
+ return c.json({ error: err.message }, 500)
66
+ })
67
+
68
+ app.notFound((c) => c.json({ error: 'Not found' }, 404))
69
+
70
+ export async function startServer(): Promise<void> {
71
+ await cache.connect()
72
+
73
+ const { loadAccounts } = await import('../core/accounts.ts')
74
+ const accounts = loadAccounts()
75
+
76
+ const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.ts')
77
+
78
+ await initPlaywright(config.browser.headless)
79
+
80
+ if (accounts.length > 0) {
81
+ console.log(`[Server] Pre-warming ${accounts.length} configured account(s) in parallel...`)
82
+ await Promise.all(
83
+ accounts.map(account =>
84
+ initPlaywrightForAccount(account, config.browser.headless).catch((err: any) => {
85
+ console.error(`[Server] Failed to initialize account ${account.email}:`, err.message)
86
+ })
87
+ )
88
+ )
89
+ console.log('[Server] Pre-fetching headers for all accounts in background...')
90
+ const { warmAllPools } = await import('../services/qwen.ts')
91
+ warmAllPools(accounts.map(a => a.id)).catch(() => {})
92
+ }
93
+
94
+ watchdog = new Watchdog()
95
+ watchdog.start()
96
+
97
+ metrics.startCollection()
98
+
99
+ server = serve({
100
+ fetch: app.fetch,
101
+ port: config.server.port,
102
+ hostname: config.server.host,
103
+ }, (info) => {
104
+ console.log(`Server listening on http://${info.address}:${info.port}`)
105
+ })
106
+
107
+ const shutdown = async (signal: string) => {
108
+ console.log(`Received ${signal}, shutting down gracefully...`)
109
+ watchdog.stop()
110
+ metrics.stopCollection()
111
+ await cache.close()
112
+ const { closePlaywright } = await import('../services/playwright.js')
113
+ await closePlaywright()
114
+ const { cleanupAllAccountMutexes } = await import('../routes/chat.js')
115
+ cleanupAllAccountMutexes()
116
+ const { closeDatabase } = await import('../core/database.ts')
117
+ closeDatabase()
118
+ server?.close()
119
+ process.exit(0)
120
+ }
121
+
122
+ process.on('SIGINT', () => shutdown('SIGINT'))
123
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
124
+ }
125
+
126
+ export { app }