@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.
- package/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- 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
|
+
[](https://github.com/pedrofariasx/qwenproxy/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://hono.dev/)
|
|
8
|
+
[](https://playwright.dev/)
|
|
9
|
+
[](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 }
|