qwenproxy-cli 1.0.0 → 1.0.2

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 (109) hide show
  1. package/LICENSE +14 -14
  2. package/README.md +906 -906
  3. package/bin/qwenproxy.js +5 -1
  4. package/package.json +77 -78
  5. package/src/api/error-classifier.ts +159 -159
  6. package/src/api/error-helpers.ts +118 -118
  7. package/src/api/models.ts +261 -261
  8. package/src/api/server.ts +860 -859
  9. package/src/cache/memory-cache.ts +385 -385
  10. package/src/clean-cache.ts +204 -204
  11. package/src/core/account-concurrency.ts +671 -671
  12. package/src/core/account-manager.ts +301 -297
  13. package/src/core/account-priority.ts +163 -163
  14. package/src/core/accounts.ts +186 -186
  15. package/src/core/config.ts +383 -383
  16. package/src/core/crypto-utils.ts +79 -79
  17. package/src/core/database.ts +276 -276
  18. package/src/core/errors.ts +118 -118
  19. package/src/core/logger.ts +269 -269
  20. package/src/core/memory-usage.ts +84 -84
  21. package/src/core/metrics.ts +291 -291
  22. package/src/core/model-alias.ts +77 -77
  23. package/src/core/model-registry.ts +544 -544
  24. package/src/core/mutex.ts +119 -119
  25. package/src/core/paths.ts +199 -199
  26. package/src/core/prompt-limits.ts +214 -214
  27. package/src/core/reasoning-effort.ts +102 -102
  28. package/src/core/stream-registry.ts +96 -96
  29. package/src/core/waf-isolation.ts +117 -117
  30. package/src/core/watchdog.ts +195 -195
  31. package/src/delete-chats.ts +23 -23
  32. package/src/index.ts +65 -64
  33. package/src/login.ts +147 -147
  34. package/src/reset-cooldowns.ts +11 -11
  35. package/src/routes/anthropic/index.ts +355 -355
  36. package/src/routes/anthropic/translate.ts +522 -522
  37. package/src/routes/anthropic/types.ts +154 -154
  38. package/src/routes/anthropic/validation.ts +144 -144
  39. package/src/routes/chat/account.ts +1817 -1817
  40. package/src/routes/chat/context.ts +241 -241
  41. package/src/routes/chat/errors.ts +85 -85
  42. package/src/routes/chat/helpers.ts +268 -268
  43. package/src/routes/chat/index.ts +618 -618
  44. package/src/routes/chat/media.ts +285 -285
  45. package/src/routes/chat/retry-policy.ts +754 -754
  46. package/src/routes/chat/stop.ts +98 -98
  47. package/src/routes/chat/streaming.ts +2710 -2710
  48. package/src/routes/chat/validation.ts +526 -526
  49. package/src/routes/chat.ts +2 -2
  50. package/src/routes/completions.ts +290 -290
  51. package/src/routes/images.ts +139 -139
  52. package/src/routes/responses/adapter.ts +503 -503
  53. package/src/routes/responses/index.ts +405 -405
  54. package/src/routes/responses/state.ts +230 -230
  55. package/src/routes/responses/streaming.ts +528 -528
  56. package/src/routes/responses/types.ts +285 -285
  57. package/src/routes/responses/validation.ts +202 -202
  58. package/src/routes/upload.ts +731 -731
  59. package/src/routes/videos.ts +214 -214
  60. package/src/services/auth-playwright.ts +173 -173
  61. package/src/services/captcha-coordinator.ts +161 -161
  62. package/src/services/captcha-solver.ts +553 -553
  63. package/src/services/chat-cleanup.ts +80 -80
  64. package/src/services/context-meter.ts +317 -317
  65. package/src/services/fingerprint.ts +242 -242
  66. package/src/services/human-behavior.ts +173 -173
  67. package/src/services/media-generation.ts +1748 -1748
  68. package/src/services/playwright.ts +2878 -2800
  69. package/src/services/qwen-chat-pool.ts +345 -345
  70. package/src/services/qwen-errors.ts +133 -133
  71. package/src/services/qwen-headers.ts +79 -79
  72. package/src/services/qwen-thread-state.ts +393 -393
  73. package/src/services/qwen-url.ts +19 -19
  74. package/src/services/qwen.ts +3126 -3126
  75. package/src/services/session-keeper.ts +88 -88
  76. package/src/services/token-estimation-metrics.ts +118 -118
  77. package/src/sync/claude-code.ts +75 -75
  78. package/src/sync/codex.ts +123 -123
  79. package/src/sync/index.ts +362 -362
  80. package/src/sync/omp.ts +105 -105
  81. package/src/sync/opencode.ts +214 -214
  82. package/src/sync/types.ts +53 -53
  83. package/src/sync/utils.ts +27 -27
  84. package/src/sync-clients.ts +189 -189
  85. package/src/tools/instructions.ts +137 -137
  86. package/src/tools/manifest.ts +81 -81
  87. package/src/tools/parser.ts +2989 -2989
  88. package/src/tools/toolcall-tags.ts +142 -142
  89. package/src/tui/app.ts +259 -264
  90. package/src/tui/index.ts +61 -61
  91. package/src/tui/markdown.ts +258 -258
  92. package/src/tui/proxy-client.ts +331 -326
  93. package/src/tui/screen.ts +294 -278
  94. package/src/tui/server-manager.ts +270 -270
  95. package/src/tui/theme.ts +432 -432
  96. package/src/tui/types.ts +33 -33
  97. package/src/tui/views/accounts-view.ts +656 -656
  98. package/src/tui/views/chat-view.ts +1018 -823
  99. package/src/tui/views/logs-view.ts +479 -413
  100. package/src/tui/views/status-view.ts +204 -204
  101. package/src/tui/views/storage-view.ts +304 -291
  102. package/src/tui/views/sync-view.ts +409 -409
  103. package/src/types/ali-oss.d.ts +32 -32
  104. package/src/update-cli.ts +121 -0
  105. package/src/utils/context-truncation.ts +84 -84
  106. package/src/utils/json.ts +380 -380
  107. package/src/utils/session-id.ts +37 -37
  108. package/src/utils/tool-call-guard.ts +84 -84
  109. package/src/utils/types.ts +109 -109
package/README.md CHANGED
@@ -1,907 +1,907 @@
1
- <p align="center">
2
-
3
- <img src="docs/banner.webp" alt="QwenProxy" width="100%">
4
-
5
- </p>
6
-
7
- Gateway e API de alta performance compatível com **OpenAI** e **Anthropic** que conecta clientes e agentes (Codex, Claude Code CLI, Grok, Cursor) ao **Qwen (`chat.qwen.ai`)** com suporte a múltiplas contas, failover inteligente, tool calling robusto, thread-native, geração de fotos e vídeos, **Responses API completa com memória persistente** e sessões persistentes. Inclui Playwright com stealth, retries para erros transitórios, variantes públicas base/`-fast`/`-thinking`, cache comprimido, registro de capabilities por modelo e observabilidade.
8
-
9
- [![CI](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml/badge.svg)](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml)
10
- [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue)](https://www.typescriptlang.org/)
11
- [![Hono](https://img.shields.io/badge/Hono-4.13-green)](https://hono.dev/)
12
- [![Playwright](https://img.shields.io/badge/Playwright-1.62-blueviolet)](https://playwright.dev/)
13
- [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](LICENSE)
14
- [![GitHub Sponsors](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/johngbl)
15
- [![Ko-fi](https://img.shields.io/badge/Donate-Ko--fi-ff5e5b?logo=kofi&logoColor=white)](https://ko-fi.com/johngbl)
16
-
17
- ## ❤️ Apoie o projeto
18
-
19
- Se o **QwenProxy** está sendo útil para você ou sua equipe e você deseja incentivar o desenvolvimento contínuo, novas integrações, testes ao vivo e atualizações rápidas, considere apoiar voluntariamente:
20
-
21
- <a href="https://github.com/sponsors/johngbl" target="_blank"><img src="https://img.shields.io/badge/Sponsor%20no%20GitHub-ea4aaa?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors"></a> <a href="https://ko-fi.com/johngbl" target="_blank"><img src="https://img.shields.io/badge/Apoiar%20via%20Ko--fi-ff5e5b?style=for-the-badge&logo=kofi&logoColor=white" alt="Ko-fi"></a>
22
-
23
- Toda contribuição é muito bem-vinda e ajuda a cobrir custos de infraestrutura e contas de teste!
24
-
25
- ---
26
-
27
- ## Principais funcionalidades
28
-
29
- - **Compatibilidade OpenAI &amp; Anthropic** — `/v1/chat/completions`, `/v1/completions` (legado), `/v1/models`, `/v1/messages` (**Anthropic Messages API** nativa com suporte total a **Claude Code CLI** e `@anthropic-ai/sdk`), `/v1/messages/count_tokens` e **Responses API** `/v1/responses`.
30
- - **Responses API completa** — SSE com `event:` + `data:` + `sequence_number`, memória persistente via `previous_response_id` (SQLite durável), `last_response_id`, multimodal (`input_image`/`input_file`), reasoning effort normalization, lifecycle events de reasoning e usage real do upstream.
31
- - **Thread-native** — Reutiliza sessão/pai no Qwen; preservação de contexto entre turns
32
- - **Dois modos de conversa** — `thread` (default, reutiliza chat e envia delta) e `temp` (novo chat temporário `chat_mode:"local"` por request, envia histórico completo; zero `chat_in_progress` e zero chats órfãos)
33
- - **Playwright + stealth** — Headers reais (`bx-ua`, `bx-umidtoken`, `bx-v`) por conta; fingerprint estável e cleanup de processos.
34
- - **Transporte Qwen via Chromium** — No fluxo principal de chat, modelos, criação de sessão, personalização, completion e stop usam o contexto Playwright; o completion lê o `ReadableStream` incrementalmente e preserva o SSE sem bufferizar a resposta inteira.
35
- - **Startup rápido multi-conta** — Sobe com a **primeira conta pronta**; as demais continuam preparando em background.
36
- - **Retries resilientes** — 502/503/504, erros de rede (`fetch failed`), anti-bot, quota e `invalid_input` com recriação de chat.
37
- - **Parser de tools robusto** — stream fragmentado, JSON malformado, fuzzy de nomes (`readFile` → `read_file`), JSON duplamente escapado e `</tool_call>` case-insensitive.
38
- - **Personalization sync** — system + tools completos são sincronizados em `/settings/personalization` via `POST /api/v2/users/user/settings/update`; o cache por conteúdo evita updates repetidos e instruções acima do limite seguem inline; aplica settings seguras (`largeTextAsFile=false`, memory off, tools internas off).
39
- - **Senhas criptografadas at-rest** no SQLite.
40
- - **Uploads multimodais** — imagens, vídeo, áudio e documentos via OSS do Qwen.
41
- - **Modelos atuais** — catálogo live da família `qwen3.x` (incluindo `qwen3.8-max`) + variantes sintéticas `-fast`/`-thinking` para todos os modelos + registro de capabilities (vision, thinking, modalities)
42
- - **Thinking nativo** — raciocínio chega via `phase: thinking_summary` do upstream, sem sanitização de tags; o modelo é instruído a nunca emitir `<think>` no conteúdo visível
43
- - **Observabilidade** — `/health`, `/metrics` (Prometheus), watchdog e logs com emojis.
44
- - **Deploy simples** — `npm`, Docker e graceful shutdown.
45
- - **Geração de fotos e vídeos** — `/v1/images/generations` e `/v1/videos/generations` com modelos de ponta (`qwen-image-3.0-pro`, `qwen-image-3.0`, `wan2.7-image-pro`, `wan3.0-video` até 30s 1080P, `z-image-turbo`). Intercepta também pelo chat completions devolvendo Markdown renderizável.
46
- - **Logs padronizados e unificados** — Exatamente 1 par limpo (`📥 Incoming` e `📤 Request`) por turno em todos os protocolos (`[Chat]`, `[Anthropic]`, `[Responses]`, `[Completions]`).
47
-
48
- ---
49
-
50
- ## Arquitetura
51
-
52
- ```mermaid
53
- flowchart TD
54
- Client["Cliente OpenAI / Claude Code / Codex / Grok"] -->|HTTP| Proxy["QwenProxy - Hono"]
55
- Proxy --> Chat["/v1/chat/completions"]
56
- Proxy --> Anthropic["/v1/messages"]
57
- Proxy --> Completions["/v1/completions (legado)"]
58
- Proxy --> Responses["/v1/responses"]
59
- Proxy --> Media["/v1/images | /v1/videos"]
60
- Proxy --> Models["/v1/models"]
61
- Proxy --> Upload["/v1/upload"]
62
- Anthropic --> Chat
63
- Completions --> Chat
64
- Responses --> Chat
65
- Responses --> Effort["Effort normalization"]
66
- Responses --> State[("SQLite responses_store")]
67
- Chat --> Context["Thread-native context"]
68
- Chat --> Accounts["Account manager"]
69
- Accounts --> DB[("SQLite encrypted")]
70
- Accounts --> Playwright["Playwright + Stealth"]
71
- Playwright --> Fingerprint["Fingerprint / session keeper"]
72
- Chat --> Parser["Tool-call parser"]
73
- Chat --> Personalization["Settings + personalization sync"]
74
- Chat --> BrowserTransport["Playwright page fetch + SSE bridge"]
75
- BrowserTransport --> Qwen["chat.qwen.ai"]
76
- Media --> BrowserTransport
77
- Upload --> OSS["Qwen OSS"]
78
- ```
79
-
80
- ---
81
-
82
- ### Autenticação
83
-
84
- Se `API_KEY` estiver definido, as rotas `/v1/*` (e `/metrics`) exigem uma das formas:
85
-
86
- - `Authorization: Bearer <API_KEY>` (OpenAI / Responses)
87
- - `x-api-key: <API_KEY>` (clients bearer-style)
88
-
89
- QwenProxy usa **Playwright por padrão**. Cada conta abre uma sessão real de browser para capturar cookies e headers anti-bot.
90
-
91
- ```env
92
- PLAYWRIGHT_HEADLESS=true
93
- PLAYWRIGHT_BROWSER=chromium
94
- ```
95
-
96
- **Requisitos:**
97
-
98
- ```bash
99
- npx playwright install chromium
100
- ```
101
-
102
- Senhas das contas são armazenadas **criptografadas** no SQLite (`data/`).
103
-
104
- ### Transporte upstream e streaming
105
-
106
- No fluxo textual principal, as chamadas ao Qwen são feitas pelo `fetch` executado dentro da página Chromium da conta. Isso mantém cookies, User-Agent, TLS, Origin e fingerprint no mesmo contexto do navegador.
107
-
108
- A resposta de completion é consumida com `ReadableStream.getReader()` e encaminhada em chunks ao Bridge. O corpo SSE não é acumulado inteiro antes de ser entregue ao cliente. Personalization usa a página `/settings/personalization`; os demais endpoints de chat usam o mesmo contexto autenticado.
109
-
110
- ---
111
-
112
- ## Modelos e contexto
113
-
114
- Modelos e janelas de contexto são sincronizados em tempo real pelo catálogo `/api/models` do Qwen, separadamente para cada conta. O QwenProxy não mantém uma tabela de nomes/capabilities: modelos novos aparecem automaticamente em `/v1/models`, e o objeto `info.meta` recebido do upstream é preservado.
115
-
116
- Exemplos do catálogo atual (podem mudar sem release do proxy):
117
-
118
-
119
- | Modelo | Contexto | Output máximo | Thinking | Vision |
120
- | ------------------------- | -------------: | -------------: | :--------: | :------: |
121
- | `qwen3.8-max` | 1.000.000 | 131.072 | ✅ | ✅ |
122
- | `qwen3.7-plus` | 1.000.000 | 65.536 | ✅ | ✅ |
123
- | `qwen3.7-max` | 1.000.000 | 65.536 | ✅ | ❌ |
124
- | **Fallback desconhecido** | **1.048.576** | **65.536** | — | — |
125
-
126
-
127
- O fallback é usado somente quando a conta ainda não sincronizou o catálogo ou o endpoint upstream está indisponível. Depois da sincronização, contexto, output, thinking, modalidades, `think_skip`, `chat_type`, `mcp`, status ativo e demais metadata vêm do Qwen.
128
-
129
- > **Nota:** O endpoint `/v1/models` retorna capabilities dinâmicas (formato OpenAI).
130
-
131
- ### Capabilities
132
-
133
- Cada modelo tem um registro `ModelCapabilities` em `src/core/model-registry.ts`:
134
-
135
- ```ts
136
- interface ModelCapabilities {
137
- maxOutputTokens: number;
138
- maxThinkingTokens: number;
139
- supportsThinking: boolean;
140
- supportsVision: boolean;
141
- canSkipThinking: boolean;
142
- supportsDocument: boolean;
143
- supportsAudio: boolean;
144
- supportsVideo: boolean;
145
- supportsCitations: boolean;
146
- supportsCodeExecution: boolean;
147
- supportsStructuredOutputs: boolean;
148
- modalities: string[];
149
- chatTypes: string[];
150
- mcp: string[];
151
- isActive: boolean;
152
- }
153
- ```
154
-
155
- **Destaque `qwen3.8-max`**: modelo flagship com suporte a visão (o `qwen3.7-max` não suporta). Permite desativar thinking (`canSkipThinking: true`).
156
-
157
- ### Variantes sintéticas
158
-
159
- - modelo base — modo **Auto** (o Qwen decide se raciocina), ex.: `qwen3.7-plus`
160
- - `-fast` — Fast com thinking desativado, ex.: `qwen3.7-plus-fast`
161
- - `-thinking` — Thinking forçado, ex.: `qwen3.7-plus-thinking`
162
-
163
- As variantes usam a mesma janela de contexto e o mesmo modelo upstream do modelo base; o modo de raciocínio é selecionado pelo `feature_config` do Qwen (Auto/Fast/Thinking). O ID antigo `-no-thinking` não é publicado; é apenas normalizado internamente para `-fast` por compatibilidade legada.
164
-
165
- ### `reasoning_effort` no Chat Completions
166
-
167
- O campo OpenAI `reasoning_effort` (`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`) também é aceito em `/v1/chat/completions`:
168
-
169
- - `low`/`none`/`minimal` → força Fast (thinking OFF) quando o modelo **não** tem sufixo
170
- - `medium`/`high`/`xhigh`/`max` → mantém Auto (o Qwen decide, como hoje)
171
- - **Precedência:** um sufixo explícito no modelo (`-fast`/`-thinking`) sempre vence o `reasoning_effort`; ausente o campo, comportamento idêntico ao anterior (no-op)
172
-
173
- ---
174
-
175
- ## Responses API (`/v1/responses`)
176
-
177
- Implementação completa da OpenAI Responses API com extensões para clientes agentic (Codex, Grok CLI, Cursor).
178
-
179
- ### Features
180
-
181
-
182
- | Feature | Descrição |
183
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
- | **SSE fiel** | `event: <type>` + `data: {...}` com `sequence_number` incremental em todos os eventos |
185
- | **Memória persistente** | `previous_response_id` com store SQLite durável (sobrevive restarts, TTL 7 dias) |
186
- | `**last_response_id**` | Retornado em toda response para encadeamento pelo cliente |
187
- | **Reasoning effort** | `reasoning.effort` aceita qualquer string; normaliza `xhigh`/`max`/`fast`/`none`/numérico para thinking ON/OFF |
188
- | **Multimodal** | `input_image` → `image_url`, `input_file` → `file_url` no chat interno |
189
- | **Usage real** | `stream_options.include_usage: true`; upstream sobrescreve estimativas; `input_tokens_details` e `output_tokens_details` **sempre** presentes (fix Grok/serde) |
190
- | **Reasoning lifecycle** | `reasoning_summary_part.added` → `reasoning_summary_text.delta` → `reasoning_summary_text.done` → `reasoning_summary_part.done` |
191
- | **Error envelope** | Formato OpenAI: `{ error: { message, type, param, code } }` |
192
- | **Store** | `store: false` desativa persistência; GET/DELETE `/v1/responses/:id` para recuperar/remover |
193
-
194
-
195
- ### Reasoning effort mapping
196
-
197
-
198
- | Client effort | Normalizado | Qwen `feature_config` |
199
- | ------------------------------------------------------ | ----------- | -------------------------------------------------------------------- |
200
- | `max`, `high`, `xhigh`, `thinking`, `ultra`, `deep` | high | `thinking_enabled: true`, `thinking_mode: "Thinking"` |
201
- | `medium`, `med`, `default` | medium | thinking ON (mesmo que high) |
202
- | `fast`, `none`, `low`, `off`, `minimal`, `no-thinking` | low | `thinking_enabled: false`, `thinking_mode: "Fast"` e modelo `*-fast` |
203
- | numérico 0–33 | low | thinking OFF |
204
- | numérico 34–66 | medium | thinking ON |
205
- | numérico 67–100 | high | thinking ON |
206
-
207
-
208
- > **Nota:** effort `low` sempre seleciona a variante pública `*-fast`; o catálogo pode informar `think_skip`, mas esse metadado não limita a publicação da variante.
209
-
210
- ### Exemplo: Responses API com memória
211
-
212
- ```bash
213
- # Primeira request
214
- curl http://localhost:7936/v1/responses \
215
- -H "Authorization: Bearer local" \
216
- -H "Content-Type: application/json" \
217
- -d '{"model":"qwen3.8-max","input":"Meu nome é João","stream":true}'
218
-
219
- # Resposta inclui last_response_id: "resp_abc123..."
220
-
221
- # Segunda request com memória
222
- curl http://localhost:7936/v1/responses \
223
- -H "Authorization: Bearer local" \
224
- -H "Content-Type: application/json" \
225
- -d '{"model":"qwen3.8-max","input":"Qual meu nome?","previous_response_id":"resp_abc123...","stream":true}'
226
- ```
227
-
228
- ### Exemplo: effort com Codex/Grok
229
-
230
- ```bash
231
- curl http://localhost:7936/v1/responses \
232
- -H "Authorization: Bearer local" \
233
- -H "Content-Type: application/json" \
234
- -d '{"model":"qwen3.7-max","input":"hi","reasoning":{"effort":"xhigh"},"max_output_tokens":30}'
235
- ```
236
-
237
- ---
238
-
239
- ## Pré-requisitos
240
-
241
-
242
- | Dependência | Versão mínima | Observação |
243
- | ----------- | -------------: | ------------------------------------ |
244
- | Node.js | 22+ | Conforme `engines` do `package.json` |
245
- | npm | 9+ | Incluído com Node |
246
- | Playwright | - | `npx playwright install chromium` |
247
- | Docker | opcional | Deploy em container |
248
-
249
-
250
- ---
251
-
252
- ## Instalação e Execução
253
-
254
- O QwenProxy pode ser instalado globalmente, executado instantaneamente via `npx`/`bunx`, ou clonado localmente:
255
-
256
- ### Opção 1: Instalação Global (Recomendado)
257
- Instale uma única vez para ter acesso ao comando rápido **`qpx`** de qualquer lugar do terminal:
258
- ```bash
259
- # Via npm:
260
- npm install -g qwenproxy-cli
261
-
262
- # Ou via pnpm:
263
- pnpm add -g qwenproxy-cli
264
-
265
- # Ou via bun:
266
- bun add -g qwenproxy-cli
267
- ```
268
- Após instalar, basta abrir o terminal e digitar:
269
- ```bash
270
- qpx
271
- # ou: qwenproxy
272
- ```
273
- *(Abre diretamente o dashboard interativo da TUI com o servidor e proxy integrados).*
274
-
275
- ### Opção 2: Execução Instantânea (Zero Instalação)
276
- Experimente ou execute pontualmente sem instalar nada permanentemente:
277
- ```bash
278
- npx qwenproxy-cli
279
- # ou: bunx qwenproxy-cli
280
- ```
281
-
282
- ### Opção 3: Clonando o Código (Desenvolvimento)
283
- ```bash
284
- git clone https://github.com/johngbl/qwenproxy.git
285
- cd qwenproxy
286
- npm install
287
- npm run tui # Abre a TUI interativa
288
- # ou: npm start # Inicia apenas o servidor HTTP headless
289
- ```
290
-
291
- ### Opção 4: Via Docker
292
- ```bash
293
- docker-compose up -d
294
- ```
295
- ---
296
-
297
- ## Início rápido
298
-
299
- Crie um `.env` na raiz (use `.env.example` como base).
300
-
301
- ### Exemplo mínimo
302
-
303
- ```env
304
- QWEN_ACCOUNTS=user1@example.com:senha1;user2@example.com:senha2
305
- API_KEY=sua-chave-local
306
- HOST=127.0.0.1
307
- ```
308
-
309
- > **Dica:** use `;` como separador de contas (`,` legado ainda funciona).
310
- > Senhas com `:`, `#` e espaços são aceitas.
311
-
312
- ### Iniciar
313
-
314
- ```bash
315
- npm start
316
- ```
317
-
318
- > **Nota:** o servidor não inicia sem pelo menos uma conta configurada (via `.env`/`QWEN_ACCOUNTS`, `npm run login` ou banco de contas).
319
-
320
- ### Startup multi-conta
321
-
322
- 1. Prepara as contas em sequência, reutilizando o profile persistente quando ele já está autenticado.
323
- 2. Se o profile não tiver uma sessão válida, autentica com as credenciais da conta e salva a sessão em `data/qwen_profiles/<accountId>`.
324
- 3. O servidor sobe após a primeira conta ficar pronta e continua preparando as demais em background.
325
- 4. Com `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS=2` (padrão), 2 contextos ficam abertos após o warmup ({principal + reserva}, cobrindo o failover comum); contextos extras (uso simultâneo ou failover) fecham ao ficar idle. O watchdog RSS fecha contextos idle sob pressão de RAM.
326
- 5. Use `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP=false` para voltar ao modo econômico, preparando as contas adicionais somente quando forem necessárias.
327
-
328
- Exemplo de log:
329
-
330
- ```text
331
- ✅ [Server] Account ready (1/6): us***@example.com
332
- 🪶 [Server] Preparing 5 standby account(s) in background
333
- ✅ [Server] Account ready (2/6): us***@example.com
334
- ...
335
-
336
- +----------------------------------------------------------+
337
- | QwenProxy |
338
- | OpenAI & Anthropic Compatible API |
339
- | Endpoint http://127.0.0.1:7936/v1 |
340
- | Accounts 1/6 warm |
341
- | Status ● Online |
342
- +----------------------------------------------------------+
343
- ```
344
-
345
- ---
346
-
347
- ## Testes
348
-
349
- ```bash
350
- npm test # mock + live
351
- npm run test:mock # suite mock (sem browser real de contas)
352
- npm run test:live # stress/concurrency reais
353
- npm run typecheck # tipos
354
- ```
355
-
356
- ---
357
-
358
- ## Variáveis de ambiente
359
-
360
- ### Rede e segurança
361
-
362
-
363
- | Variável | Default | Descrição |
364
- | --------- | --------- | -------------------------------- |
365
- | `PORT` | `7936` | Porta HTTP (padrão QWEN: 7936). Configurável via .env |
366
- | `HOST` | `0.0.0.0` | Bind host. Local: `127.0.0.1` |
367
- | `API_KEY` | vazio | Protege `/v1/*` com Bearer token |
368
-
369
-
370
- ### Contas e sessão
371
-
372
-
373
- | Variável | Default | Descrição |
374
- | ----------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
375
- | `QWEN_ACCOUNTS` | vazio | `email1:senha1;email2:senha2` |
376
- | `DELETE_ALL_CHATS_ON_SHUTDOWN` | `false` | Limpa chats no shutdown |
377
- | `QWEN_PERSONALIZATION_FROM_REQUEST` | `true` | Envia system + tools via `/settings/personalization` |
378
- | `QWEN_PERSONALIZATION_VERIFY_GET` | `true` | Confirma personalization com GET |
379
- | `QWEN_MAX_PERSONALIZATION_BYTES` | `200000` | Teto UTF-8 para personalization por request; acima disso as instruções seguem inline |
380
- | `QWEN_CHAT_POOL_SIZE` | `1` | Warm pool de chats por modelo; fica desativado quando personalization por request está ativa |
381
- | `QWEN_CHAT_POOL_MODELS` | `qwen3.7-plus` | Modelos aquecidos no warm pool |
382
- | `QWEN_CHAT_MODE` | `thread` | Modo de conversa: `thread` (reutiliza o chat upstream via `parent_id` e envia o delta) ou `temp` (cria um chat temporário `chat_mode:"local"` a cada request e envia o histórico completo). Override por request via header `X-QwenProxy-Chat-Mode: thread/temp` |
383
-
384
-
385
- ### Playwright / processos
386
-
387
-
388
- | Variável | Default | Descrição |
389
- | ------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
390
- | `PLAYWRIGHT_HEADLESS` | `true` | Browser sem janela |
391
- | `PLAYWRIGHT_BROWSER` | `chromium` | `chromium` / `chrome` / `edge` |
392
- | `PLAYWRIGHT_INIT_BATCH_SIZE` | `1` | Contas em paralelo no background init |
393
- | `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP` | `true` | Prepara todas as contas no boot (`false` = só quando necessárias) |
394
- | `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS` | `2` | Contextos idle mantidos quentes ({principal + reserva}); streams ativos nunca são fechados; uso simultâneo abre mais. Contas em cooldown (rate limit) ficam idle e são evictadas |
395
- | `PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS` | `10000` | Timeout de close antes do kill |
396
- | `PLAYWRIGHT_IDLE_CONTEXT_TTL_MS` | `60000` | Fecha contextos idle acima do cap (`0` desativa) |
397
- | `PLAYWRIGHT_JS_HEAP_MB` | `256` | Cap V8 do Chromium (`--max-old-space-size`) |
398
- | `PLAYWRIGHT_LOW_MEMORY_FLAGS` | `true` | Flags de baixa RAM (heap cap, cache mínimo, renderer limit) |
399
- | `OSS_MULTIPART_THRESHOLD_MB` | `5` | Acima disso usa multipart OSS; abaixo `putStream` |
400
- | `SESSION_KEEP_ALIVE_ENABLED` | `false` | Keep-alive opt-in (evita Chromes permanentes) |
401
- | `SESSION_KEEP_ALIVE_INTERVAL_MS` | `180000` | Intervalo do ciclo de keep-alive/cleanup |
402
- | `SESSION_KEEP_ALIVE_IDLE_MS` | `120000` | Idle mínimo para keep-alive |
403
- | `SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS` | `480000` | Intervalo de navegação leve |
404
-
405
-
406
- ### CAPTCHA automático
407
-
408
-
409
- | Variável | Default | Descrição |
410
- | ------------------------------- | -------- | -------------------------------------------------------------------------------------- |
411
- | `CAPTCHA_SOLVER_ENABLED` | `true` | Solver Baxia/TMD ativo por padrão; use `false` somente como desligamento de emergência |
412
- | `CAPTCHA_SOLVER_MAX_ATTEMPTS` | `3` | Máximo de arrastos por challenge |
413
- | `CAPTCHA_SOLVER_TIMEOUT_MS` | `15000` | Tempo para o iframe Baxia aparecer |
414
- | `CAPTCHA_SOLVER_RETRY_DELAY_MS` | `1000` | Espera entre tentativas do slider |
415
- | `CAPTCHA_SOLVER_SETTLE_MS` | `2000` | Tempo para confirmar cookies/DOM após o arrasto |
416
- | `CAPTCHA_ACCOUNT_COOLDOWN_MS` | `120000` | Cooldown da conta quando o desafio não pôde ser resolvido; `0` desliga |
417
-
418
-
419
- ### Headers anti-bot
420
-
421
-
422
- | Variável | Default | Descrição |
423
- | ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
424
- | `USER_AGENT` | Chrome 149 Windows | UA fallback |
425
- | `QWEN_BX_V` | `2.5.37` | `bx-v` fallback; `bx-ua`/`bx-umidtoken` **não** são enviados como headers (o cliente real os carrega como cookies WAF) |
426
- | `QWEN_SEND_BX_UA` | `false` | `true` restaura o comportamento legado de injetar `bx-ua`/`bx-umidtoken` capturados como headers |
427
-
428
-
429
- Fingerprint estável por conta (UA, locale, viewport, hardware/WebGL) é aplicado automaticamente.
430
-
431
- ### Delays e retry
432
-
433
-
434
- | Variável | Default | Descrição |
435
- | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
436
- | `RETRY_BASE_DELAY_MS` | `1000` | Base do exponential backoff |
437
- | `RETRY_MAX_DELAY_MS` | `10000` | Cap do backoff |
438
- | `RETRY_MAX_ATTEMPTS` | `3` | Tentativas por request (create-stream + mid-stream) |
439
- | `RETRY_MAX_ACCOUNT_SWITCHES` | `2` | Máximo de trocas de conta por request |
440
- | `RETRY_ON_UNKNOWN_UPSTREAM` | `true` | Retry/troca automática em erros upstream desconhecidos (denylist só para erros locais terminais) |
441
- | `RETRY_AUTO_MALFORMED_TOOLS` | `true` | Auto-retry quando todos os tool calls da resposta vêm malformados |
442
- | `RETRY_AUTO_MALFORMED_TOOLS_MAX` | `2` | Máximo de retries de tool calls malformados por resposta |
443
- | `MAX_TOOL_CALLS_PER_TURN` | `8` | Teto de tool calls por turno (0 desativa); calls duplicadas idênticas também são descartadas |
444
- | `CHAT_IN_PROGRESS_RETRY_DELAY_MS` | `2000` | Espera antes de repetir no mesmo chat após `chat_in_progress` |
445
- | `CHAT_IN_PROGRESS_BUSY_MS` | `4000` | Janela busy da conta após `chat_in_progress` (absorve o settle do upstream) |
446
- | `MID_STREAM_FAILOVER_THRESHOLD` | `2` | Falhas de rede mid-stream nesta janela marcam a conta temporarily busy |
447
- | `MID_STREAM_FAILOVER_BUSY_MS` | `60000` | Duração do busy após o threshold mid-stream |
448
- | `ACQUIRE_DEADLINE_MS` | `120000` | Deadline por tentativa de acquire do stream (falha visível → troca de conta) |
449
- | `ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS` | `120000` | Cap de espera na fila "sem deadline" de contas |
450
- | `ACCOUNT_LEASE_MAX_DURATION_MS` | `600000` | Vida máxima de uma lease de conta |
451
- | `ACCOUNT_INIT_FAILURE_COOLDOWN_MS` | `300000` | Cooldown após falha de init de conta |
452
-
453
-
454
- ### Timeouts
455
-
456
-
457
- | Variável | Default | Descrição |
458
- | -------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
459
- | `HTTP_TIMEOUT` | `10000` | HTTP genérico |
460
- | `CHAT_TIMEOUT` | `120000` | Timeout de chat |
461
- | `NAVIGATION_TIMEOUT` | `60000` | Navegação Playwright |
462
- | `PAGE_TIMEOUT` | `60000` | Operações de página |
463
- | `HEADERS_TIMEOUT` | `60000` | Captura de headers |
464
- | `TIME_TO_FIRST_BYTE` | `60000` | Janela de primeiro byte (teto com piso de 15s no metadata) |
465
- | `IDLE_STREAM_TIMEOUT` | `60000` | Stream sem dados (modelos não-reasoning) |
466
- | `TOTAL_REQUEST_TIMEOUT` | `600000` | Teto de geração |
467
- | `REASONING_MODEL_TIMEOUT` | `180000` | Silêncio mid-stream para modelos reasoning (chunks fluidos resetam; zero bytes por 3min = morto) |
468
- | `QWEN_FIRST_CHUNK_TIMEOUT` | `180000` | Deadline do PRIMEIRO chunk (thought = 0 bytes por 3min aborta retryável) |
469
-
470
-
471
- **Nota:** timeouts dinâmicos de payload: **modelos reasoning** usam `REASONING_MODEL_TIMEOUT` (180s default) + 30s por MB; **modelos não-reasoning** usam `IDLE_STREAM_TIMEOUT` (60s default) + 30s por MB.
472
-
473
- ### Cache e contexto
474
-
475
-
476
- | Variável | Default | Descrição |
477
- | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
478
- | `CACHE_TTL` | `3600` | TTL do cache (s) |
479
- | `CACHE_COMPRESSION_ENABLED` | `true` | Compressão Brotli |
480
- | `QWEN_MAX_PROMPT_BYTES` | `0` | Teto opcional local UTF-8 do prompt (`0` desativa); não é a janela de tokens. O payload total continua limitado a 50 MiB |
481
- | `CONTEXT_METER_ENABLED` | `true` | Medição do histórico completo, delta/replay, payload Qwen e percentuais de contexto; já vem ativa por padrão |
482
- | `CONTEXT_METER_WINDOW_TOKENS` | `0` | Janela usada pelo medidor (`0` usa a janela real registrada para o modelo) |
483
- | `CONTEXT_METER_REPORT_USAGE` | `true` | Reporta em `usage.prompt_tokens` o valor real `input_tokens` do Qwen quando disponível; só usa a estimativa como fallback |
484
-
485
-
486
- O medidor de contexto é padrão e não exige nenhuma variável no `.env`. Ele não é um tokenizer nativo do Zed/Cline nem substitui o tokenizer privado do Qwen: calcula uma estimativa local do histórico completo recebido pelo proxy, registra o prompt delta/replay efetivamente enviado e preserva `usage.context_meter` com `measurementSource=qwen` quando o Qwen devolve `input_tokens`, ou `measurementSource=local_estimate` quando não devolve. A janela do modelo é sincronizada automaticamente pelo `/api/models`, e são emitidos headers `X-QwenProxy-Context-*` e logs estruturados. As três variáveis podem ser usadas somente como overrides avançados; por padrão o valor real do Qwen é preferido e a estimativa só é fallback.
487
-
488
- ### Observabilidade
489
-
490
-
491
- | Variável | Default | Descrição |
492
- | --------------------- | -------- | ------------------------------------------------------------------------------- |
493
- | `CHAT_REQUEST_LOG` | `false` | Logs detalhados de request |
494
- | `LOG_LEVEL` | `warn` | Nível do logger (`debug`/`info`/`warn`/`error`); `TOOLCALL_DEBUG=1` força debug |
495
- | `METRICS_INTERVAL` | `10000` | Intervalo de métricas |
496
- | `WATCHDOG_INTERVAL` | `5000` | Intervalo do watchdog |
497
- | `RAM_WARNING` | `80` | % RSS warning (RSS / totalmem) |
498
- | `RAM_CRITICAL` | `95` | % RSS critical (RSS / totalmem) |
499
- | `RATE_LIMIT_REQUESTS` | `5000` | Header estático `x-ratelimit-limit-requests` (não impõe quota) |
500
- | `RATE_LIMIT_TOKENS` | `200000` | Header estático `x-ratelimit-limit-tokens` (não impõe quota) |
501
-
502
-
503
- ---
504
-
505
- ## Retries e resiliência
506
-
507
- O proxy tenta recuperar erros transitórios sem quebrar thread-native/tools:
508
-
509
-
510
- | Situação | Comportamento |
511
- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
512
- | `502` / `503` / `504` | Retry com delay curto |
513
- | `fetch failed`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND` | Retry de rede |
514
- | Anti-bot (`FAIL_SYS_USER_VALIDATE`, captcha, WAF HTML, etc.) | Com solver Baxia habilitado: preserva a página, tenta resolver uma vez, atualiza headers e repete na mesma conta; sem solver, mantém o retry simples |
515
- | Quota / rate limit | Cooldown categorizado (`RateLimited`, `RateLimitTemporary`, …) |
516
- | `invalid_input` (“entrada ou anexo inválido”) | Retry forçando **novo chat** + contexto completo |
517
- | Chat not exist / session stale | Força novo chat na sessão lógica |
518
- | Tool calls malformados (todos inválidos) | Reparo local do JSON; se não resolver, auto-retry na mesma conta com novo chat e correção enviada ao modelo (até `RETRY_AUTO_MALFORMED_TOOLS_MAX`) |
519
- | `INVALID_FIRST_MSG` / histórico corrompido | Novo chat + contexto completo na **mesma conta** (a corrupção é da cadeia de parent, não da conta); o thread lógico contaminado é invalidado |
520
-
521
-
522
- Settings seguras aplicadas no sync de personalization (sem reescrever tudo da conta):
523
-
524
- ```json
525
- {
526
- "ui": { "autoTags": false, "largeTextAsFile": false, "splitLargeChunks": false },
527
- "mcp_remind": false,
528
- "memory": { "enable_memory": false, "enable_history_memory": false },
529
- "tools_enabled": { "web_search": false, "code_interpreter": false }
530
- }
531
- ```
532
-
533
- ---
534
-
535
- ## Anti-bot
536
-
537
- Detecta, entre outros:
538
-
539
- - `FAIL_SYS_USER_VALIDATE`
540
- - `RGV587_ERROR`
541
- - mensagens de captcha / human verification
542
-
543
- **Fluxo:**
544
-
545
- 1. Identifica o WAF/captcha sem expor o HTML do desafio ao cliente
546
- 2. Se `CAPTCHA_SOLVER_ENABLED=true`, detecta o diálogo Baxia já visível e procura o iframe aninhado, o iframe legado ou o documento NC diretamente na página da mesma conta
547
- 3. Se nada estiver visível — o caso normal, porque o completion roda como `fetch` em background e o WAF responde o documento de punish ao XHR em vez de renderizar algo — extrai a URL do desafio do corpo da resposta e abre essa URL na própria página da conta; sem URL utilizável, recarrega a página de chat para forçar o desafio a aparecer. Só a origem configurada em `QWEN_BASE_URL` pode ser aberta
548
- 4. Executa o slider com limite de tentativas e volta a página para `/c/new-chat`
549
- 5. Após sucesso, captura novamente cookies/headers e repete a requisição original na mesma conta
550
- 6. Se o solver falhar, a conta entra em cooldown por `CAPTCHA_ACCOUNT_COOLDOWN_MS` e a requisição é encaminhada para **uma** outra conta; percorrer o pool inteiro apenas faria o WAF desafiar todas as contas
551
- 7. Uma recuperação que falhou é ignorada por 30s na mesma conta, para o retry loop não gastar o orçamento do solver em cada tentativa
552
-
553
- Com Playwright, cada conta usa fingerprint e headers capturados do browser real.
554
-
555
- O solver Baxia/TMD fica ativo por padrão e cobre o slider NC visível em iframe ou documento direto. O proxy não salva HTML, screenshot, cookies ou tokens do challenge; desafios não suportados continuam no fluxo sanitizado de retry na mesma conta.
556
-
557
- ---
558
-
559
- ## Compatibilidade real das rotas
560
-
561
- O README descreve o uso operacional. Para detalhes técnicos da API (schemas, exemplos, headers), veja:
562
-
563
- - [`docs/openapi.yaml`](docs/openapi.yaml) — OpenAPI 3.1 spec com todas as rotas (Chat, Completions, Responses, Models, Upload, Health)
564
-
565
- > **Nota:** A spec OpenAPI é mantida atualizada com as mudanças recentes (auth Bearer + x-api-key, health heap detalhado).
566
-
567
- ---
568
-
569
- ## Endpoints
570
-
571
- ### OpenAI Compatible
572
-
573
-
574
- | Rota | Método | Descrição |
575
- | --------------------------- | ------ | ----------------------------------------- |
576
- | `/v1/chat/completions` | POST | Chat completions (stream + non-stream) |
577
- | `/v1/completions` | POST | Completions legado (adapter sobre o chat) |
578
- | `/v1/chat/completions/stop` | POST | Abortar geração |
579
- | `/v1/models` | GET | Listar modelos |
580
- | `/v1/models/:id` | GET | Modelo específico |
581
- | `/v1/responses` | POST | OpenAI Responses API |
582
- | `/v1/responses/:id` | GET | Recuperar response armazenada |
583
- | `/v1/responses/:id` | DELETE | Deletar response |
584
-
585
-
586
- ### Anthropic Compatible (Claude Code CLI / Anthropic SDK)
587
-
588
-
589
- | Rota | Método | Descrição |
590
- | --------------------------- | ------ | ------------------------------------------------------------- |
591
- | `/v1/messages` | POST | Anthropic Messages API (stream, thinking, tools, Claude Code) |
592
- | `/v1/messages/count_tokens` | POST | Contagem de tokens compatível com Anthropic |
593
-
594
-
595
- ### Geração de Mídia (Fotos e Vídeos)
596
-
597
-
598
- | Rota | Método | Descrição |
599
- | -------------------------- | ------ | ------------------------------------------------------------------------------------ |
600
- | `/v1/images/generations` | POST | Geração de fotos/imagens (`qwen-image-3.0-pro`, `wan2.7-image-pro`, `z-image-turbo`) |
601
- | `/v1/videos/generations` | POST | Geração de vídeos (`wan3.0-video` até 30s em 1080P, `wan2.7-t2v` com áudio) |
602
- | `/v1/tasks/status/:taskId` | GET | Consulta de status e download da tarefa de vídeo |
603
-
604
-
605
- ### Utilidades
606
-
607
-
608
- | Rota | Método | Descrição |
609
- | ------------ | ------ | ------------------------------------------------- |
610
- | `/health` | GET | Health check |
611
- | `/metrics` | GET | Prometheus (protegido por API key se configurada) |
612
- | `/v1/upload` | POST | Upload multimodal |
613
-
614
-
615
- > Rotas sem o prefixo `/v1` (ex.: `/chat/completions`) são redirecionadas com 308 preservando método e corpo. Respostas incluem headers OpenAI (`openai-version`, `openai-processing-ms`, `x-ratelimit-*`).
616
-
617
- ---
618
-
619
- ## Exemplos de uso
620
-
621
- ### OpenAI SDK (Node.js)
622
-
623
- ```typescript
624
- import OpenAI from "openai";
625
-
626
- const client = new OpenAI({
627
- baseURL: "http://localhost:7936/v1",
628
- apiKey: "sua-api-key",
629
- });
630
-
631
- const completion = await client.chat.completions.create({
632
- model: "qwen3.7-plus",
633
- messages: [{ role: "user", content: "Hello!" }],
634
- });
635
-
636
- console.log(completion.choices[0].message.content);
637
- ```
638
-
639
- ### Anthropic SDK / Claude Code CLI
640
-
641
- O proxy é 100% compatível com o **Claude Code CLI** e o **Anthropic SDK**:
642
-
643
- ```bash
644
- # Configuração para Claude Code CLI
645
- export ANTHROPIC_BASE_URL="http://localhost:7936"
646
- export ANTHROPIC_API_KEY="sua-api-key"
647
-
648
- # Iniciar Claude Code
649
- claude
650
- ```
651
-
652
- ```typescript
653
- import Anthropic from "@anthropic-ai/sdk";
654
-
655
- const anthropic = new Anthropic({
656
- baseURL: "http://localhost:7936",
657
- apiKey: "sua-api-key",
658
- });
659
-
660
- const message = await anthropic.messages.create({
661
- model: "claude-3-7-sonnet-20250219", // ou "qwen3.8-max"
662
- max_tokens: 1024,
663
- messages: [{ role: "user", content: "Olá!" }],
664
- });
665
-
666
- console.log(message.content[0]);
667
- ```
668
-
669
- ### OpenAI Responses API (Codex / Grok CLI)
670
-
671
- ```typescript
672
- import OpenAI from "openai";
673
-
674
- const client = new OpenAI({
675
- baseURL: "http://localhost:7936/v1",
676
- apiKey: "sua-api-key",
677
- });
678
-
679
- // Streaming com reasoning effort
680
- const stream = await client.responses.create({
681
- model: "qwen3.8-max",
682
- input: "Explique computação quântica",
683
- reasoning: { effort: "high" },
684
- stream: true,
685
- });
686
-
687
- for await (const event of stream) {
688
- if (event.type === "response.output_text.delta") {
689
- process.stdout.write(event.delta);
690
- }
691
- }
692
- ```
693
-
694
- ### Geração de Imagens (`/v1/images/generations`)
695
-
696
- ```bash
697
- curl http://localhost:7936/v1/images/generations \
698
- -H "Content-Type: application/json" \
699
- -H "Authorization: Bearer sua-api-key" \
700
- -d '{
701
- "model": "qwen-image-3.0-pro",
702
- "prompt": "A futuristic cyberpunk city in the rain, ultra-detailed, cinematic lighting",
703
- "size": "16:9"
704
- }'
705
- ```
706
-
707
- ### Geração de Vídeos (`/v1/videos/generations`)
708
-
709
- ```bash
710
- curl http://localhost:7936/v1/videos/generations \
711
- -H "Content-Type: application/json" \
712
- -H "Authorization: Bearer sua-api-key" \
713
- -d '{
714
- "model": "wan3.0-video",
715
- "prompt": "Drone shot flying over a misty pine forest at sunrise",
716
- "size": "16:9",
717
- "wait": true
718
- }'
719
- ```
720
-
721
- ---
722
-
723
- ### cURL
724
-
725
- ```bash
726
- curl http://localhost:7936/v1/chat/completions \
727
- -H "Content-Type: application/json" \
728
- -H "Authorization: Bearer sua-api-key" \
729
- -d '{
730
- "model": "qwen3.7-plus",
731
- "messages": [{"role": "user", "content": "Hello!"}],
732
- "stream": true
733
- }'
734
- ```
735
-
736
- ### Grok CLI (config)
737
-
738
- ```toml
739
- [model.qwen38-max]
740
- api_backend = "responses"
741
- base_url = "http://127.0.0.1:7936/v1"
742
- ```
743
-
744
- ---
745
-
746
- ## Tool calling
747
-
748
- O parser suporta:
749
-
750
- - tags `<tool_call>...</tool_call>` e variantes Qwen `<tool_calls>...</tool_call(s)>` (fechamentos case-insensitive)
751
- - formato Hermes/XML (`<parameter name="...">`)
752
- - JSON malformado / recovery (aspas/braces faltando)
753
- - JSON **duplamente escapado** em arguments
754
- - stream fragmentado / tool call sem open tag
755
- - **fuzzy match** seguro de nomes (`readFile` → `read_file`) quando há match único
756
- - tool names não declarados: podem ser preservados como texto literal (evita quebrar exemplos)
757
- - **auto-retry**: se todos os tool calls vierem malformados, o proxy tenta reparo local e, se necessário, repete a geração em novo chat informando o erro ao modelo (até `RETRY_AUTO_MALFORMED_TOOLS_MAX`)
758
-
759
- Tools internas da conta Qwen (web_search, code interpreter, etc.) ficam desligadas; o proxy usa as tools do cliente.
760
-
761
- ---
762
-
763
- ## Modelos
764
-
765
- O proxy envia o id do modelo ao Qwen **como está**. Apenas os sufixos de raciocínio são normalizados antes de subir (via `stripThinkingSuffix`):
766
-
767
- - `qwen3.7-plus` → base (Auto: o Qwen decide)
768
- - `qwen3.7-plus-fast` → base + thinking OFF
769
- - `qwen3.7-plus-thinking` → base + thinking ON
770
- - `qwen3.7-plus-no-thinking` → base + thinking OFF (compat legado)
771
-
772
- ---
773
-
774
- ## Deploy com Docker
775
-
776
- ```yaml
777
- services:
778
- qwenproxy:
779
- build: .
780
- container_name: qwenproxy
781
- ports:
782
- - "${PORT:-7936}:7936"
783
- env_file:
784
- - .env
785
- volumes:
786
- - ./data:/app/data
787
- restart: unless-stopped
788
- logging:
789
- driver: "json-file"
790
- options:
791
- max-size: "10m"
792
- max-file: "3"
793
- ```
794
-
795
- O container ajusta permissões de `data/db` e `data/qwen_profiles` no startup.
796
-
797
- ---
798
-
799
- ## Estrutura do projeto
800
-
801
- ```
802
- QwenProxy/
803
- ├── src/
804
- │ ├── api/ # Server Hono, models, errors
805
- │ ├── benchmarks/ # Baseline de latência do proxy
806
- │ ├── cache/ # Memory cache + Brotli
807
- │ ├── core/ # Config, accounts, DB, metrics, cooldowns, model-registry
808
- │ ├── routes/
809
- │ │ ├── chat/ # Completions, streaming, account acquire, retry-policy
810
- │ │ └── responses/ # OpenAI Responses API (state, streaming, adapter)
811
- │ ├── services/
812
- │ │ ├── playwright.ts # Browser + headers + cleanup
813
- │ │ ├── qwen.ts # Upstream Qwen + personalization + idle timeout
814
- │ │ ├── session-keeper.ts
815
- │ │ ├── fingerprint.ts
816
- │ │ └── human-behavior.ts
817
- │ ├── tools/ # Parser e instruções de tools
818
- │ ├── tests/
819
- │ └── utils/
820
- ├── data/ # SQLite, key e profiles (gitignored)
821
- ├── Dockerfile
822
- ├── docker-compose.yml
823
- └── package.json
824
- ```
825
-
826
- ---
827
-
828
- ## Scripts úteis
829
-
830
-
831
- | Comando | Descrição |
832
- | ------------------- | ----------------------------------------------------------------------- |
833
- | `npm start` | Iniciar o servidor QwenProxy |
834
- | `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
835
- | `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
836
- | `npm run clean:all` | Limpar caches + remover navegadores órfãos do Playwright (+4GB no SSD) |
837
- | `npm run reset` | Zerar cooldowns de contas no banco de dados |
838
- | `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
839
- | `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
840
- | `npm test` | Executar suíte de testes completa |
841
- | `npm run typecheck` | Checagem estrita de tipos do TypeScript |
842
-
843
- ---
844
-
845
- ## Scripts de instalação, início e atualização
846
-
847
- A pasta `scripts/` contém atalhos para instalar, iniciar e atualizar o projeto sem digitar os comandos manualmente.
848
-
849
-
850
- | Script | Windows | Linux/macOS | O que faz |
851
- | ----------- | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------- |
852
- | Instalador | `scripts\install.bat` | `./scripts/install.sh` | Verifica Node 22+, roda `npm install`, cria `.env` a partir de `.env.example` se não existir |
853
- | Iniciador | `scripts\start.bat` | `./scripts/start.sh` | Verifica dependências e `.env`, inicia o servidor com `npm start` |
854
- | Atualizador | `scripts\update.bat` | `./scripts/update.sh` | `git pull` (se for repositório), `npm install` e `npx playwright install chromium` |
855
-
856
-
857
- No Linux/macOS, dê permissão de execução na primeira vez:
858
-
859
- ```bash
860
- chmod +x scripts/*.sh
861
- ```
862
-
863
- ---
864
-
865
- ## Troubleshooting
866
-
867
-
868
- | Problema | Solução |
869
- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
870
- | Anti-bot / captcha | Solver Baxia automático por padrão (`CAPTCHA_SOLVER_ENABLED=true`); se falhar, a conta entra em cooldown (`CAPTCHA_ACCOUNT_COOLDOWN_MS`) e a request roda em outra conta |
871
- | Quota exceeded | Mais contas ou esperar cooldown |
872
- | `502 Bad Gateway` / `fetch failed` | Normalmente upstream/rede; o proxy faz retry automático |
873
- | `invalid_input` (anexo inválido) | Retry com chat novo; settings `largeTextAsFile=false` ajudam |
874
- | `context_length_exceeded` | O proxy bloqueou o prompt localmente antes de qualquer retry; reduza/resuma o histórico ou ajuste `QWEN_MAX_PROMPT_BYTES` |
875
- | HTML/WAF no lugar do stream | O Bridge identifica o desafio e aciona o solver; se persistir, reduza o tamanho/frequência do payload e verifique a sessão |
876
- | `Model not found` | Use um id do catálogo de `/v1/models` (ex.: `qwen3.8-max`) |
877
- | Vários Chromes abertos / RAM alta | `SESSION_KEEP_ALIVE_ENABLED=false`, idle cleanup on, `PLAYWRIGHT_INIT_BATCH_SIZE=1`, `PLAYWRIGHT_JS_HEAP_MB`, watchdog RSS fecha idle sob pressão |
878
- | Watchdog “RAM critical” falso | Baseado em RSS (`memory.rss.usage_percent`); confira `/health` |
879
- | Timeout em requests grandes | Aumente `TOTAL_REQUEST_TIMEOUT` / `REASONING_MODEL_TIMEOUT` |
880
- | `stream_aborted` em modelo reasoning | Idle timeout: zero bytes por `REASONING_MODEL_TIMEOUT` (180s default) fecha o stream retryável; aumente se necessário |
881
- | `canSkipThinking: false` | O catálogo não informa `think_skip`; a variante pública `-fast` continua disponível e usa o payload Fast do Qwen |
882
- | Grok CLI `missing field input_tokens_details` | Corrigido: usage sempre inclui `input_tokens_details` e `output_tokens_details` |
883
- | Responses `previous_response_id` not found | Store SQLite com TTL 7 dias; verifique se `store: false` não foi enviado |
884
- | Playwright não inicia | `npx playwright install chromium` |
885
- | Porta em uso | Altere `PORT` no `.env` |
886
- | Sessão expirada | `npm run login` ou deixe o refresh automático reautenticar |
887
- | API aberta em `0.0.0.0` sem key | Defina `API_KEY` e/ou `HOST=127.0.0.1` |
888
-
889
-
890
- ---
891
-
892
- ## Créditos e Agradecimentos
893
-
894
- O **QwenProxy** é desenvolvido e mantido por **johngbl**, construído sobre as fundações de código aberto originalmente criadas por **Pedro Farias** sob a licença [ISC](LICENSE).
895
-
896
- ---
897
- ## Disclaimer
898
-
899
- **Software fornecido *as is*, sem qualquer garantia (expressa ou implícita), incluindo as de comerciabilidade, adequação a um fim, funcionamento contínuo, correção de erros ou suporte.**
900
-
901
- - **Sem afiliação:** o QwenProxy não é afiliado, endossado nem patrocinado pela Alibaba/Qwen, OpenAI, Anthropic ou qualquer provedor citado. Marcas pertencem aos seus titulares.
902
- - **Uso por sua conta e risco:** destinado a fins educacionais e de estudo técnico. Cabe exclusivamente a você cumprir os Termos de Uso e limites do serviço upstream, usar contas e credenciais próprias e verificar a legalidade do uso na sua jurisdição.
903
- - **Responsabilidade integral do usuário:** você é o único responsável por bloqueio, suspensão ou encerramento de contas, desafios anti-bot, perda de dados ou histórico, custos imprevistos, indisponibilidade e por qualquer prompt, saída, arquivo ou resultado de mídia gerado.
904
- - **Exclusão total do mantenedor:** nos limites máximos da lei, o autor e os contribuidores **não respondem** por quaisquer danos diretos, indiretos, incidentais, especiais ou consequenciais (perda de lucros, dados, contas, quota ou receita) decorrentes do uso ou da incapacidade de uso deste software, nem por mudanças, quebras ou bloqueios do serviço upstream, que podem ocorrer a qualquer momento e sem aviso prévio.
905
- - **Sem prestação de serviço:** projeto voluntário, sem SLA e sem obrigação de atualizar ou corrigir.
906
-
1
+ <p align="center">
2
+
3
+ <img src="docs/banner.webp" alt="QwenProxy" width="100%">
4
+
5
+ </p>
6
+
7
+ Gateway e API de alta performance compatível com **OpenAI** e **Anthropic** que conecta clientes e agentes (Codex, Claude Code CLI, Grok, Cursor) ao **Qwen (`chat.qwen.ai`)** com suporte a múltiplas contas, failover inteligente, tool calling robusto, thread-native, geração de fotos e vídeos, **Responses API completa com memória persistente** e sessões persistentes. Inclui Playwright com stealth, retries para erros transitórios, variantes públicas base/`-fast`/`-thinking`, cache comprimido, registro de capabilities por modelo e observabilidade.
8
+
9
+ [![CI](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml/badge.svg)](https://github.com/johngbl/QwenProxy/actions/workflows/ci.yml)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-7.0-blue)](https://www.typescriptlang.org/)
11
+ [![Hono](https://img.shields.io/badge/Hono-4.13-green)](https://hono.dev/)
12
+ [![Playwright](https://img.shields.io/badge/Playwright-1.62-blueviolet)](https://playwright.dev/)
13
+ [![License: ISC](https://img.shields.io/badge/License-ISC-yellow.svg)](LICENSE)
14
+ [![GitHub Sponsors](https://img.shields.io/badge/sponsor-GitHub%20Sponsors-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/johngbl)
15
+ [![Ko-fi](https://img.shields.io/badge/Donate-Ko--fi-ff5e5b?logo=kofi&logoColor=white)](https://ko-fi.com/johngbl)
16
+
17
+ ## ❤️ Apoie o projeto
18
+
19
+ Se o **QwenProxy** está sendo útil para você ou sua equipe e você deseja incentivar o desenvolvimento contínuo, novas integrações, testes ao vivo e atualizações rápidas, considere apoiar voluntariamente:
20
+
21
+ <a href="https://github.com/sponsors/johngbl" target="_blank"><img src="https://img.shields.io/badge/Sponsor%20no%20GitHub-ea4aaa?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors"></a> <a href="https://ko-fi.com/johngbl" target="_blank"><img src="https://img.shields.io/badge/Apoiar%20via%20Ko--fi-ff5e5b?style=for-the-badge&logo=kofi&logoColor=white" alt="Ko-fi"></a>
22
+
23
+ Toda contribuição é muito bem-vinda e ajuda a cobrir custos de infraestrutura e contas de teste!
24
+
25
+ ---
26
+
27
+ ## Principais funcionalidades
28
+
29
+ - **Compatibilidade OpenAI &amp; Anthropic** — `/v1/chat/completions`, `/v1/completions` (legado), `/v1/models`, `/v1/messages` (**Anthropic Messages API** nativa com suporte total a **Claude Code CLI** e `@anthropic-ai/sdk`), `/v1/messages/count_tokens` e **Responses API** `/v1/responses`.
30
+ - **Responses API completa** — SSE com `event:` + `data:` + `sequence_number`, memória persistente via `previous_response_id` (SQLite durável), `last_response_id`, multimodal (`input_image`/`input_file`), reasoning effort normalization, lifecycle events de reasoning e usage real do upstream.
31
+ - **Thread-native** — Reutiliza sessão/pai no Qwen; preservação de contexto entre turns
32
+ - **Dois modos de conversa** — `thread` (default, reutiliza chat e envia delta) e `temp` (novo chat temporário `chat_mode:"local"` por request, envia histórico completo; zero `chat_in_progress` e zero chats órfãos)
33
+ - **Playwright + stealth** — Headers reais (`bx-ua`, `bx-umidtoken`, `bx-v`) por conta; fingerprint estável e cleanup de processos.
34
+ - **Transporte Qwen via Chromium** — No fluxo principal de chat, modelos, criação de sessão, personalização, completion e stop usam o contexto Playwright; o completion lê o `ReadableStream` incrementalmente e preserva o SSE sem bufferizar a resposta inteira.
35
+ - **Startup rápido multi-conta** — Sobe com a **primeira conta pronta**; as demais continuam preparando em background.
36
+ - **Retries resilientes** — 502/503/504, erros de rede (`fetch failed`), anti-bot, quota e `invalid_input` com recriação de chat.
37
+ - **Parser de tools robusto** — stream fragmentado, JSON malformado, fuzzy de nomes (`readFile` → `read_file`), JSON duplamente escapado e `</tool_call>` case-insensitive.
38
+ - **Personalization sync** — system + tools completos são sincronizados em `/settings/personalization` via `POST /api/v2/users/user/settings/update`; o cache por conteúdo evita updates repetidos e instruções acima do limite seguem inline; aplica settings seguras (`largeTextAsFile=false`, memory off, tools internas off).
39
+ - **Senhas criptografadas at-rest** no SQLite.
40
+ - **Uploads multimodais** — imagens, vídeo, áudio e documentos via OSS do Qwen.
41
+ - **Modelos atuais** — catálogo live da família `qwen3.x` (incluindo `qwen3.8-max`) + variantes sintéticas `-fast`/`-thinking` para todos os modelos + registro de capabilities (vision, thinking, modalities)
42
+ - **Thinking nativo** — raciocínio chega via `phase: thinking_summary` do upstream, sem sanitização de tags; o modelo é instruído a nunca emitir `<think>` no conteúdo visível
43
+ - **Observabilidade** — `/health`, `/metrics` (Prometheus), watchdog e logs com emojis.
44
+ - **Deploy simples** — `npm`, Docker e graceful shutdown.
45
+ - **Geração de fotos e vídeos** — `/v1/images/generations` e `/v1/videos/generations` com modelos de ponta (`qwen-image-3.0-pro`, `qwen-image-3.0`, `wan2.7-image-pro`, `wan3.0-video` até 30s 1080P, `z-image-turbo`). Intercepta também pelo chat completions devolvendo Markdown renderizável.
46
+ - **Logs padronizados e unificados** — Exatamente 1 par limpo (`📥 Incoming` e `📤 Request`) por turno em todos os protocolos (`[Chat]`, `[Anthropic]`, `[Responses]`, `[Completions]`).
47
+
48
+ ---
49
+
50
+ ## Arquitetura
51
+
52
+ ```mermaid
53
+ flowchart TD
54
+ Client["Cliente OpenAI / Claude Code / Codex / Grok"] -->|HTTP| Proxy["QwenProxy - Hono"]
55
+ Proxy --> Chat["/v1/chat/completions"]
56
+ Proxy --> Anthropic["/v1/messages"]
57
+ Proxy --> Completions["/v1/completions (legado)"]
58
+ Proxy --> Responses["/v1/responses"]
59
+ Proxy --> Media["/v1/images | /v1/videos"]
60
+ Proxy --> Models["/v1/models"]
61
+ Proxy --> Upload["/v1/upload"]
62
+ Anthropic --> Chat
63
+ Completions --> Chat
64
+ Responses --> Chat
65
+ Responses --> Effort["Effort normalization"]
66
+ Responses --> State[("SQLite responses_store")]
67
+ Chat --> Context["Thread-native context"]
68
+ Chat --> Accounts["Account manager"]
69
+ Accounts --> DB[("SQLite encrypted")]
70
+ Accounts --> Playwright["Playwright + Stealth"]
71
+ Playwright --> Fingerprint["Fingerprint / session keeper"]
72
+ Chat --> Parser["Tool-call parser"]
73
+ Chat --> Personalization["Settings + personalization sync"]
74
+ Chat --> BrowserTransport["Playwright page fetch + SSE bridge"]
75
+ BrowserTransport --> Qwen["chat.qwen.ai"]
76
+ Media --> BrowserTransport
77
+ Upload --> OSS["Qwen OSS"]
78
+ ```
79
+
80
+ ---
81
+
82
+ ### Autenticação
83
+
84
+ Se `API_KEY` estiver definido, as rotas `/v1/*` (e `/metrics`) exigem uma das formas:
85
+
86
+ - `Authorization: Bearer <API_KEY>` (OpenAI / Responses)
87
+ - `x-api-key: <API_KEY>` (clients bearer-style)
88
+
89
+ QwenProxy usa **Playwright por padrão**. Cada conta abre uma sessão real de browser para capturar cookies e headers anti-bot.
90
+
91
+ ```env
92
+ PLAYWRIGHT_HEADLESS=true
93
+ PLAYWRIGHT_BROWSER=chromium
94
+ ```
95
+
96
+ **Requisitos:**
97
+
98
+ ```bash
99
+ npx playwright install chromium
100
+ ```
101
+
102
+ Senhas das contas são armazenadas **criptografadas** no SQLite (`data/`).
103
+
104
+ ### Transporte upstream e streaming
105
+
106
+ No fluxo textual principal, as chamadas ao Qwen são feitas pelo `fetch` executado dentro da página Chromium da conta. Isso mantém cookies, User-Agent, TLS, Origin e fingerprint no mesmo contexto do navegador.
107
+
108
+ A resposta de completion é consumida com `ReadableStream.getReader()` e encaminhada em chunks ao Bridge. O corpo SSE não é acumulado inteiro antes de ser entregue ao cliente. Personalization usa a página `/settings/personalization`; os demais endpoints de chat usam o mesmo contexto autenticado.
109
+
110
+ ---
111
+
112
+ ## Modelos e contexto
113
+
114
+ Modelos e janelas de contexto são sincronizados em tempo real pelo catálogo `/api/models` do Qwen, separadamente para cada conta. O QwenProxy não mantém uma tabela de nomes/capabilities: modelos novos aparecem automaticamente em `/v1/models`, e o objeto `info.meta` recebido do upstream é preservado.
115
+
116
+ Exemplos do catálogo atual (podem mudar sem release do proxy):
117
+
118
+
119
+ | Modelo | Contexto | Output máximo | Thinking | Vision |
120
+ | ------------------------- | -------------: | -------------: | :--------: | :------: |
121
+ | `qwen3.8-max` | 1.000.000 | 131.072 | ✅ | ✅ |
122
+ | `qwen3.7-plus` | 1.000.000 | 65.536 | ✅ | ✅ |
123
+ | `qwen3.7-max` | 1.000.000 | 65.536 | ✅ | ❌ |
124
+ | **Fallback desconhecido** | **1.048.576** | **65.536** | — | — |
125
+
126
+
127
+ O fallback é usado somente quando a conta ainda não sincronizou o catálogo ou o endpoint upstream está indisponível. Depois da sincronização, contexto, output, thinking, modalidades, `think_skip`, `chat_type`, `mcp`, status ativo e demais metadata vêm do Qwen.
128
+
129
+ > **Nota:** O endpoint `/v1/models` retorna capabilities dinâmicas (formato OpenAI).
130
+
131
+ ### Capabilities
132
+
133
+ Cada modelo tem um registro `ModelCapabilities` em `src/core/model-registry.ts`:
134
+
135
+ ```ts
136
+ interface ModelCapabilities {
137
+ maxOutputTokens: number;
138
+ maxThinkingTokens: number;
139
+ supportsThinking: boolean;
140
+ supportsVision: boolean;
141
+ canSkipThinking: boolean;
142
+ supportsDocument: boolean;
143
+ supportsAudio: boolean;
144
+ supportsVideo: boolean;
145
+ supportsCitations: boolean;
146
+ supportsCodeExecution: boolean;
147
+ supportsStructuredOutputs: boolean;
148
+ modalities: string[];
149
+ chatTypes: string[];
150
+ mcp: string[];
151
+ isActive: boolean;
152
+ }
153
+ ```
154
+
155
+ **Destaque `qwen3.8-max`**: modelo flagship com suporte a visão (o `qwen3.7-max` não suporta). Permite desativar thinking (`canSkipThinking: true`).
156
+
157
+ ### Variantes sintéticas
158
+
159
+ - modelo base — modo **Auto** (o Qwen decide se raciocina), ex.: `qwen3.7-plus`
160
+ - `-fast` — Fast com thinking desativado, ex.: `qwen3.7-plus-fast`
161
+ - `-thinking` — Thinking forçado, ex.: `qwen3.7-plus-thinking`
162
+
163
+ As variantes usam a mesma janela de contexto e o mesmo modelo upstream do modelo base; o modo de raciocínio é selecionado pelo `feature_config` do Qwen (Auto/Fast/Thinking). O ID antigo `-no-thinking` não é publicado; é apenas normalizado internamente para `-fast` por compatibilidade legada.
164
+
165
+ ### `reasoning_effort` no Chat Completions
166
+
167
+ O campo OpenAI `reasoning_effort` (`none`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`) também é aceito em `/v1/chat/completions`:
168
+
169
+ - `low`/`none`/`minimal` → força Fast (thinking OFF) quando o modelo **não** tem sufixo
170
+ - `medium`/`high`/`xhigh`/`max` → mantém Auto (o Qwen decide, como hoje)
171
+ - **Precedência:** um sufixo explícito no modelo (`-fast`/`-thinking`) sempre vence o `reasoning_effort`; ausente o campo, comportamento idêntico ao anterior (no-op)
172
+
173
+ ---
174
+
175
+ ## Responses API (`/v1/responses`)
176
+
177
+ Implementação completa da OpenAI Responses API com extensões para clientes agentic (Codex, Grok CLI, Cursor).
178
+
179
+ ### Features
180
+
181
+
182
+ | Feature | Descrição |
183
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
184
+ | **SSE fiel** | `event: <type>` + `data: {...}` com `sequence_number` incremental em todos os eventos |
185
+ | **Memória persistente** | `previous_response_id` com store SQLite durável (sobrevive restarts, TTL 7 dias) |
186
+ | `**last_response_id**` | Retornado em toda response para encadeamento pelo cliente |
187
+ | **Reasoning effort** | `reasoning.effort` aceita qualquer string; normaliza `xhigh`/`max`/`fast`/`none`/numérico para thinking ON/OFF |
188
+ | **Multimodal** | `input_image` → `image_url`, `input_file` → `file_url` no chat interno |
189
+ | **Usage real** | `stream_options.include_usage: true`; upstream sobrescreve estimativas; `input_tokens_details` e `output_tokens_details` **sempre** presentes (fix Grok/serde) |
190
+ | **Reasoning lifecycle** | `reasoning_summary_part.added` → `reasoning_summary_text.delta` → `reasoning_summary_text.done` → `reasoning_summary_part.done` |
191
+ | **Error envelope** | Formato OpenAI: `{ error: { message, type, param, code } }` |
192
+ | **Store** | `store: false` desativa persistência; GET/DELETE `/v1/responses/:id` para recuperar/remover |
193
+
194
+
195
+ ### Reasoning effort mapping
196
+
197
+
198
+ | Client effort | Normalizado | Qwen `feature_config` |
199
+ | ------------------------------------------------------ | ----------- | -------------------------------------------------------------------- |
200
+ | `max`, `high`, `xhigh`, `thinking`, `ultra`, `deep` | high | `thinking_enabled: true`, `thinking_mode: "Thinking"` |
201
+ | `medium`, `med`, `default` | medium | thinking ON (mesmo que high) |
202
+ | `fast`, `none`, `low`, `off`, `minimal`, `no-thinking` | low | `thinking_enabled: false`, `thinking_mode: "Fast"` e modelo `*-fast` |
203
+ | numérico 0–33 | low | thinking OFF |
204
+ | numérico 34–66 | medium | thinking ON |
205
+ | numérico 67–100 | high | thinking ON |
206
+
207
+
208
+ > **Nota:** effort `low` sempre seleciona a variante pública `*-fast`; o catálogo pode informar `think_skip`, mas esse metadado não limita a publicação da variante.
209
+
210
+ ### Exemplo: Responses API com memória
211
+
212
+ ```bash
213
+ # Primeira request
214
+ curl http://localhost:7936/v1/responses \
215
+ -H "Authorization: Bearer local" \
216
+ -H "Content-Type: application/json" \
217
+ -d '{"model":"qwen3.8-max","input":"Meu nome é João","stream":true}'
218
+
219
+ # Resposta inclui last_response_id: "resp_abc123..."
220
+
221
+ # Segunda request com memória
222
+ curl http://localhost:7936/v1/responses \
223
+ -H "Authorization: Bearer local" \
224
+ -H "Content-Type: application/json" \
225
+ -d '{"model":"qwen3.8-max","input":"Qual meu nome?","previous_response_id":"resp_abc123...","stream":true}'
226
+ ```
227
+
228
+ ### Exemplo: effort com Codex/Grok
229
+
230
+ ```bash
231
+ curl http://localhost:7936/v1/responses \
232
+ -H "Authorization: Bearer local" \
233
+ -H "Content-Type: application/json" \
234
+ -d '{"model":"qwen3.7-max","input":"hi","reasoning":{"effort":"xhigh"},"max_output_tokens":30}'
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Pré-requisitos
240
+
241
+
242
+ | Dependência | Versão mínima | Observação |
243
+ | ----------- | -------------: | ------------------------------------ |
244
+ | Node.js | 22+ | Conforme `engines` do `package.json` |
245
+ | npm | 9+ | Incluído com Node |
246
+ | Playwright | - | `npx playwright install chromium` |
247
+ | Docker | opcional | Deploy em container |
248
+
249
+
250
+ ---
251
+
252
+ ## Instalação e Execução
253
+
254
+ O QwenProxy pode ser instalado globalmente, executado instantaneamente via `npx`/`bunx`, ou clonado localmente:
255
+
256
+ ### Opção 1: Instalação Global (Recomendado)
257
+ Instale uma única vez para ter acesso ao comando rápido **`qpx`** de qualquer lugar do terminal:
258
+ ```bash
259
+ # Via npm:
260
+ npm install -g qwenproxy-cli
261
+
262
+ # Ou via pnpm:
263
+ pnpm add -g qwenproxy-cli
264
+
265
+ # Ou via bun:
266
+ bun add -g qwenproxy-cli
267
+ ```
268
+ Após instalar, basta abrir o terminal e digitar:
269
+ ```bash
270
+ qpx
271
+ # ou: qwenproxy
272
+ ```
273
+ *(Abre diretamente o dashboard interativo da TUI com o servidor e proxy integrados).*
274
+
275
+ ### Opção 2: Execução Instantânea (Zero Instalação)
276
+ Experimente ou execute pontualmente sem instalar nada permanentemente:
277
+ ```bash
278
+ npx qwenproxy-cli
279
+ # ou: bunx qwenproxy-cli
280
+ ```
281
+
282
+ ### Opção 3: Clonando o Código (Desenvolvimento)
283
+ ```bash
284
+ git clone https://github.com/johngbl/qwenproxy.git
285
+ cd qwenproxy
286
+ npm install
287
+ npm run tui # Abre a TUI interativa
288
+ # ou: npm start # Inicia apenas o servidor HTTP headless
289
+ ```
290
+
291
+ ### Opção 4: Via Docker
292
+ ```bash
293
+ docker-compose up -d
294
+ ```
295
+ ---
296
+
297
+ ## Início rápido
298
+
299
+ Crie um `.env` na raiz (use `.env.example` como base).
300
+
301
+ ### Exemplo mínimo
302
+
303
+ ```env
304
+ QWEN_ACCOUNTS=user1@example.com:senha1;user2@example.com:senha2
305
+ API_KEY=sua-chave-local
306
+ HOST=127.0.0.1
307
+ ```
308
+
309
+ > **Dica:** use `;` como separador de contas (`,` legado ainda funciona).
310
+ > Senhas com `:`, `#` e espaços são aceitas.
311
+
312
+ ### Iniciar
313
+
314
+ ```bash
315
+ npm start
316
+ ```
317
+
318
+ > **Nota:** o servidor não inicia sem pelo menos uma conta configurada (via `.env`/`QWEN_ACCOUNTS`, `npm run login` ou banco de contas).
319
+
320
+ ### Startup multi-conta
321
+
322
+ 1. Prepara as contas em sequência, reutilizando o profile persistente quando ele já está autenticado.
323
+ 2. Se o profile não tiver uma sessão válida, autentica com as credenciais da conta e salva a sessão em `data/qwen_profiles/<accountId>`.
324
+ 3. O servidor sobe após a primeira conta ficar pronta e continua preparando as demais em background.
325
+ 4. Com `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS=2` (padrão), 2 contextos ficam abertos após o warmup ({principal + reserva}, cobrindo o failover comum); contextos extras (uso simultâneo ou failover) fecham ao ficar idle. O watchdog RSS fecha contextos idle sob pressão de RAM.
326
+ 5. Use `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP=false` para voltar ao modo econômico, preparando as contas adicionais somente quando forem necessárias.
327
+
328
+ Exemplo de log:
329
+
330
+ ```text
331
+ ✅ [Server] Account ready (1/6): us***@example.com
332
+ 🪶 [Server] Preparing 5 standby account(s) in background
333
+ ✅ [Server] Account ready (2/6): us***@example.com
334
+ ...
335
+
336
+ +----------------------------------------------------------+
337
+ | QwenProxy |
338
+ | OpenAI & Anthropic Compatible API |
339
+ | Endpoint http://127.0.0.1:7936/v1 |
340
+ | Accounts 1/6 warm |
341
+ | Status ● Online |
342
+ +----------------------------------------------------------+
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Testes
348
+
349
+ ```bash
350
+ npm test # mock + live
351
+ npm run test:mock # suite mock (sem browser real de contas)
352
+ npm run test:live # stress/concurrency reais
353
+ npm run typecheck # tipos
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Variáveis de ambiente
359
+
360
+ ### Rede e segurança
361
+
362
+
363
+ | Variável | Default | Descrição |
364
+ | --------- | --------- | -------------------------------- |
365
+ | `PORT` | `7936` | Porta HTTP (padrão QWEN: 7936). Configurável via .env |
366
+ | `HOST` | `0.0.0.0` | Bind host. Local: `127.0.0.1` |
367
+ | `API_KEY` | vazio | Protege `/v1/*` com Bearer token |
368
+
369
+
370
+ ### Contas e sessão
371
+
372
+
373
+ | Variável | Default | Descrição |
374
+ | ----------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
375
+ | `QWEN_ACCOUNTS` | vazio | `email1:senha1;email2:senha2` |
376
+ | `DELETE_ALL_CHATS_ON_SHUTDOWN` | `false` | Limpa chats no shutdown |
377
+ | `QWEN_PERSONALIZATION_FROM_REQUEST` | `true` | Envia system + tools via `/settings/personalization` |
378
+ | `QWEN_PERSONALIZATION_VERIFY_GET` | `true` | Confirma personalization com GET |
379
+ | `QWEN_MAX_PERSONALIZATION_BYTES` | `200000` | Teto UTF-8 para personalization por request; acima disso as instruções seguem inline |
380
+ | `QWEN_CHAT_POOL_SIZE` | `1` | Warm pool de chats por modelo; fica desativado quando personalization por request está ativa |
381
+ | `QWEN_CHAT_POOL_MODELS` | `qwen3.7-plus` | Modelos aquecidos no warm pool |
382
+ | `QWEN_CHAT_MODE` | `thread` | Modo de conversa: `thread` (reutiliza o chat upstream via `parent_id` e envia o delta) ou `temp` (cria um chat temporário `chat_mode:"local"` a cada request e envia o histórico completo). Override por request via header `X-QwenProxy-Chat-Mode: thread/temp` |
383
+
384
+
385
+ ### Playwright / processos
386
+
387
+
388
+ | Variável | Default | Descrição |
389
+ | ------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
390
+ | `PLAYWRIGHT_HEADLESS` | `true` | Browser sem janela |
391
+ | `PLAYWRIGHT_BROWSER` | `chromium` | `chromium` / `chrome` / `edge` |
392
+ | `PLAYWRIGHT_INIT_BATCH_SIZE` | `1` | Contas em paralelo no background init |
393
+ | `PLAYWRIGHT_PREPARE_ALL_ON_STARTUP` | `true` | Prepara todas as contas no boot (`false` = só quando necessárias) |
394
+ | `PLAYWRIGHT_MAX_ACTIVE_CONTEXTS` | `2` | Contextos idle mantidos quentes ({principal + reserva}); streams ativos nunca são fechados; uso simultâneo abre mais. Contas em cooldown (rate limit) ficam idle e são evictadas |
395
+ | `PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS` | `10000` | Timeout de close antes do kill |
396
+ | `PLAYWRIGHT_IDLE_CONTEXT_TTL_MS` | `60000` | Fecha contextos idle acima do cap (`0` desativa) |
397
+ | `PLAYWRIGHT_JS_HEAP_MB` | `256` | Cap V8 do Chromium (`--max-old-space-size`) |
398
+ | `PLAYWRIGHT_LOW_MEMORY_FLAGS` | `true` | Flags de baixa RAM (heap cap, cache mínimo, renderer limit) |
399
+ | `OSS_MULTIPART_THRESHOLD_MB` | `5` | Acima disso usa multipart OSS; abaixo `putStream` |
400
+ | `SESSION_KEEP_ALIVE_ENABLED` | `false` | Keep-alive opt-in (evita Chromes permanentes) |
401
+ | `SESSION_KEEP_ALIVE_INTERVAL_MS` | `180000` | Intervalo do ciclo de keep-alive/cleanup |
402
+ | `SESSION_KEEP_ALIVE_IDLE_MS` | `120000` | Idle mínimo para keep-alive |
403
+ | `SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS` | `480000` | Intervalo de navegação leve |
404
+
405
+
406
+ ### CAPTCHA automático
407
+
408
+
409
+ | Variável | Default | Descrição |
410
+ | ------------------------------- | -------- | -------------------------------------------------------------------------------------- |
411
+ | `CAPTCHA_SOLVER_ENABLED` | `true` | Solver Baxia/TMD ativo por padrão; use `false` somente como desligamento de emergência |
412
+ | `CAPTCHA_SOLVER_MAX_ATTEMPTS` | `3` | Máximo de arrastos por challenge |
413
+ | `CAPTCHA_SOLVER_TIMEOUT_MS` | `15000` | Tempo para o iframe Baxia aparecer |
414
+ | `CAPTCHA_SOLVER_RETRY_DELAY_MS` | `1000` | Espera entre tentativas do slider |
415
+ | `CAPTCHA_SOLVER_SETTLE_MS` | `2000` | Tempo para confirmar cookies/DOM após o arrasto |
416
+ | `CAPTCHA_ACCOUNT_COOLDOWN_MS` | `120000` | Cooldown da conta quando o desafio não pôde ser resolvido; `0` desliga |
417
+
418
+
419
+ ### Headers anti-bot
420
+
421
+
422
+ | Variável | Default | Descrição |
423
+ | ----------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
424
+ | `USER_AGENT` | Chrome 149 Windows | UA fallback |
425
+ | `QWEN_BX_V` | `2.5.37` | `bx-v` fallback; `bx-ua`/`bx-umidtoken` **não** são enviados como headers (o cliente real os carrega como cookies WAF) |
426
+ | `QWEN_SEND_BX_UA` | `false` | `true` restaura o comportamento legado de injetar `bx-ua`/`bx-umidtoken` capturados como headers |
427
+
428
+
429
+ Fingerprint estável por conta (UA, locale, viewport, hardware/WebGL) é aplicado automaticamente.
430
+
431
+ ### Delays e retry
432
+
433
+
434
+ | Variável | Default | Descrição |
435
+ | ----------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
436
+ | `RETRY_BASE_DELAY_MS` | `1000` | Base do exponential backoff |
437
+ | `RETRY_MAX_DELAY_MS` | `10000` | Cap do backoff |
438
+ | `RETRY_MAX_ATTEMPTS` | `3` | Tentativas por request (create-stream + mid-stream) |
439
+ | `RETRY_MAX_ACCOUNT_SWITCHES` | `2` | Máximo de trocas de conta por request |
440
+ | `RETRY_ON_UNKNOWN_UPSTREAM` | `true` | Retry/troca automática em erros upstream desconhecidos (denylist só para erros locais terminais) |
441
+ | `RETRY_AUTO_MALFORMED_TOOLS` | `true` | Auto-retry quando todos os tool calls da resposta vêm malformados |
442
+ | `RETRY_AUTO_MALFORMED_TOOLS_MAX` | `2` | Máximo de retries de tool calls malformados por resposta |
443
+ | `MAX_TOOL_CALLS_PER_TURN` | `8` | Teto de tool calls por turno (0 desativa); calls duplicadas idênticas também são descartadas |
444
+ | `CHAT_IN_PROGRESS_RETRY_DELAY_MS` | `2000` | Espera antes de repetir no mesmo chat após `chat_in_progress` |
445
+ | `CHAT_IN_PROGRESS_BUSY_MS` | `4000` | Janela busy da conta após `chat_in_progress` (absorve o settle do upstream) |
446
+ | `MID_STREAM_FAILOVER_THRESHOLD` | `2` | Falhas de rede mid-stream nesta janela marcam a conta temporarily busy |
447
+ | `MID_STREAM_FAILOVER_BUSY_MS` | `60000` | Duração do busy após o threshold mid-stream |
448
+ | `ACQUIRE_DEADLINE_MS` | `120000` | Deadline por tentativa de acquire do stream (falha visível → troca de conta) |
449
+ | `ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS` | `120000` | Cap de espera na fila "sem deadline" de contas |
450
+ | `ACCOUNT_LEASE_MAX_DURATION_MS` | `600000` | Vida máxima de uma lease de conta |
451
+ | `ACCOUNT_INIT_FAILURE_COOLDOWN_MS` | `300000` | Cooldown após falha de init de conta |
452
+
453
+
454
+ ### Timeouts
455
+
456
+
457
+ | Variável | Default | Descrição |
458
+ | -------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
459
+ | `HTTP_TIMEOUT` | `10000` | HTTP genérico |
460
+ | `CHAT_TIMEOUT` | `120000` | Timeout de chat |
461
+ | `NAVIGATION_TIMEOUT` | `60000` | Navegação Playwright |
462
+ | `PAGE_TIMEOUT` | `60000` | Operações de página |
463
+ | `HEADERS_TIMEOUT` | `60000` | Captura de headers |
464
+ | `TIME_TO_FIRST_BYTE` | `60000` | Janela de primeiro byte (teto com piso de 15s no metadata) |
465
+ | `IDLE_STREAM_TIMEOUT` | `60000` | Stream sem dados (modelos não-reasoning) |
466
+ | `TOTAL_REQUEST_TIMEOUT` | `600000` | Teto de geração |
467
+ | `REASONING_MODEL_TIMEOUT` | `180000` | Silêncio mid-stream para modelos reasoning (chunks fluidos resetam; zero bytes por 3min = morto) |
468
+ | `QWEN_FIRST_CHUNK_TIMEOUT` | `180000` | Deadline do PRIMEIRO chunk (thought = 0 bytes por 3min aborta retryável) |
469
+
470
+
471
+ **Nota:** timeouts dinâmicos de payload: **modelos reasoning** usam `REASONING_MODEL_TIMEOUT` (180s default) + 30s por MB; **modelos não-reasoning** usam `IDLE_STREAM_TIMEOUT` (60s default) + 30s por MB.
472
+
473
+ ### Cache e contexto
474
+
475
+
476
+ | Variável | Default | Descrição |
477
+ | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
478
+ | `CACHE_TTL` | `3600` | TTL do cache (s) |
479
+ | `CACHE_COMPRESSION_ENABLED` | `true` | Compressão Brotli |
480
+ | `QWEN_MAX_PROMPT_BYTES` | `0` | Teto opcional local UTF-8 do prompt (`0` desativa); não é a janela de tokens. O payload total continua limitado a 50 MiB |
481
+ | `CONTEXT_METER_ENABLED` | `true` | Medição do histórico completo, delta/replay, payload Qwen e percentuais de contexto; já vem ativa por padrão |
482
+ | `CONTEXT_METER_WINDOW_TOKENS` | `0` | Janela usada pelo medidor (`0` usa a janela real registrada para o modelo) |
483
+ | `CONTEXT_METER_REPORT_USAGE` | `true` | Reporta em `usage.prompt_tokens` o valor real `input_tokens` do Qwen quando disponível; só usa a estimativa como fallback |
484
+
485
+
486
+ O medidor de contexto é padrão e não exige nenhuma variável no `.env`. Ele não é um tokenizer nativo do Zed/Cline nem substitui o tokenizer privado do Qwen: calcula uma estimativa local do histórico completo recebido pelo proxy, registra o prompt delta/replay efetivamente enviado e preserva `usage.context_meter` com `measurementSource=qwen` quando o Qwen devolve `input_tokens`, ou `measurementSource=local_estimate` quando não devolve. A janela do modelo é sincronizada automaticamente pelo `/api/models`, e são emitidos headers `X-QwenProxy-Context-*` e logs estruturados. As três variáveis podem ser usadas somente como overrides avançados; por padrão o valor real do Qwen é preferido e a estimativa só é fallback.
487
+
488
+ ### Observabilidade
489
+
490
+
491
+ | Variável | Default | Descrição |
492
+ | --------------------- | -------- | ------------------------------------------------------------------------------- |
493
+ | `CHAT_REQUEST_LOG` | `false` | Logs detalhados de request |
494
+ | `LOG_LEVEL` | `warn` | Nível do logger (`debug`/`info`/`warn`/`error`); `TOOLCALL_DEBUG=1` força debug |
495
+ | `METRICS_INTERVAL` | `10000` | Intervalo de métricas |
496
+ | `WATCHDOG_INTERVAL` | `5000` | Intervalo do watchdog |
497
+ | `RAM_WARNING` | `80` | % RSS warning (RSS / totalmem) |
498
+ | `RAM_CRITICAL` | `95` | % RSS critical (RSS / totalmem) |
499
+ | `RATE_LIMIT_REQUESTS` | `5000` | Header estático `x-ratelimit-limit-requests` (não impõe quota) |
500
+ | `RATE_LIMIT_TOKENS` | `200000` | Header estático `x-ratelimit-limit-tokens` (não impõe quota) |
501
+
502
+
503
+ ---
504
+
505
+ ## Retries e resiliência
506
+
507
+ O proxy tenta recuperar erros transitórios sem quebrar thread-native/tools:
508
+
509
+
510
+ | Situação | Comportamento |
511
+ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
512
+ | `502` / `503` / `504` | Retry com delay curto |
513
+ | `fetch failed`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND` | Retry de rede |
514
+ | Anti-bot (`FAIL_SYS_USER_VALIDATE`, captcha, WAF HTML, etc.) | Com solver Baxia habilitado: preserva a página, tenta resolver uma vez, atualiza headers e repete na mesma conta; sem solver, mantém o retry simples |
515
+ | Quota / rate limit | Cooldown categorizado (`RateLimited`, `RateLimitTemporary`, …) |
516
+ | `invalid_input` (“entrada ou anexo inválido”) | Retry forçando **novo chat** + contexto completo |
517
+ | Chat not exist / session stale | Força novo chat na sessão lógica |
518
+ | Tool calls malformados (todos inválidos) | Reparo local do JSON; se não resolver, auto-retry na mesma conta com novo chat e correção enviada ao modelo (até `RETRY_AUTO_MALFORMED_TOOLS_MAX`) |
519
+ | `INVALID_FIRST_MSG` / histórico corrompido | Novo chat + contexto completo na **mesma conta** (a corrupção é da cadeia de parent, não da conta); o thread lógico contaminado é invalidado |
520
+
521
+
522
+ Settings seguras aplicadas no sync de personalization (sem reescrever tudo da conta):
523
+
524
+ ```json
525
+ {
526
+ "ui": { "autoTags": false, "largeTextAsFile": false, "splitLargeChunks": false },
527
+ "mcp_remind": false,
528
+ "memory": { "enable_memory": false, "enable_history_memory": false },
529
+ "tools_enabled": { "web_search": false, "code_interpreter": false }
530
+ }
531
+ ```
532
+
533
+ ---
534
+
535
+ ## Anti-bot
536
+
537
+ Detecta, entre outros:
538
+
539
+ - `FAIL_SYS_USER_VALIDATE`
540
+ - `RGV587_ERROR`
541
+ - mensagens de captcha / human verification
542
+
543
+ **Fluxo:**
544
+
545
+ 1. Identifica o WAF/captcha sem expor o HTML do desafio ao cliente
546
+ 2. Se `CAPTCHA_SOLVER_ENABLED=true`, detecta o diálogo Baxia já visível e procura o iframe aninhado, o iframe legado ou o documento NC diretamente na página da mesma conta
547
+ 3. Se nada estiver visível — o caso normal, porque o completion roda como `fetch` em background e o WAF responde o documento de punish ao XHR em vez de renderizar algo — extrai a URL do desafio do corpo da resposta e abre essa URL na própria página da conta; sem URL utilizável, recarrega a página de chat para forçar o desafio a aparecer. Só a origem configurada em `QWEN_BASE_URL` pode ser aberta
548
+ 4. Executa o slider com limite de tentativas e volta a página para `/c/new-chat`
549
+ 5. Após sucesso, captura novamente cookies/headers e repete a requisição original na mesma conta
550
+ 6. Se o solver falhar, a conta entra em cooldown por `CAPTCHA_ACCOUNT_COOLDOWN_MS` e a requisição é encaminhada para **uma** outra conta; percorrer o pool inteiro apenas faria o WAF desafiar todas as contas
551
+ 7. Uma recuperação que falhou é ignorada por 30s na mesma conta, para o retry loop não gastar o orçamento do solver em cada tentativa
552
+
553
+ Com Playwright, cada conta usa fingerprint e headers capturados do browser real.
554
+
555
+ O solver Baxia/TMD fica ativo por padrão e cobre o slider NC visível em iframe ou documento direto. O proxy não salva HTML, screenshot, cookies ou tokens do challenge; desafios não suportados continuam no fluxo sanitizado de retry na mesma conta.
556
+
557
+ ---
558
+
559
+ ## Compatibilidade real das rotas
560
+
561
+ O README descreve o uso operacional. Para detalhes técnicos da API (schemas, exemplos, headers), veja:
562
+
563
+ - [`docs/openapi.yaml`](docs/openapi.yaml) — OpenAPI 3.1 spec com todas as rotas (Chat, Completions, Responses, Models, Upload, Health)
564
+
565
+ > **Nota:** A spec OpenAPI é mantida atualizada com as mudanças recentes (auth Bearer + x-api-key, health heap detalhado).
566
+
567
+ ---
568
+
569
+ ## Endpoints
570
+
571
+ ### OpenAI Compatible
572
+
573
+
574
+ | Rota | Método | Descrição |
575
+ | --------------------------- | ------ | ----------------------------------------- |
576
+ | `/v1/chat/completions` | POST | Chat completions (stream + non-stream) |
577
+ | `/v1/completions` | POST | Completions legado (adapter sobre o chat) |
578
+ | `/v1/chat/completions/stop` | POST | Abortar geração |
579
+ | `/v1/models` | GET | Listar modelos |
580
+ | `/v1/models/:id` | GET | Modelo específico |
581
+ | `/v1/responses` | POST | OpenAI Responses API |
582
+ | `/v1/responses/:id` | GET | Recuperar response armazenada |
583
+ | `/v1/responses/:id` | DELETE | Deletar response |
584
+
585
+
586
+ ### Anthropic Compatible (Claude Code CLI / Anthropic SDK)
587
+
588
+
589
+ | Rota | Método | Descrição |
590
+ | --------------------------- | ------ | ------------------------------------------------------------- |
591
+ | `/v1/messages` | POST | Anthropic Messages API (stream, thinking, tools, Claude Code) |
592
+ | `/v1/messages/count_tokens` | POST | Contagem de tokens compatível com Anthropic |
593
+
594
+
595
+ ### Geração de Mídia (Fotos e Vídeos)
596
+
597
+
598
+ | Rota | Método | Descrição |
599
+ | -------------------------- | ------ | ------------------------------------------------------------------------------------ |
600
+ | `/v1/images/generations` | POST | Geração de fotos/imagens (`qwen-image-3.0-pro`, `wan2.7-image-pro`, `z-image-turbo`) |
601
+ | `/v1/videos/generations` | POST | Geração de vídeos (`wan3.0-video` até 30s em 1080P, `wan2.7-t2v` com áudio) |
602
+ | `/v1/tasks/status/:taskId` | GET | Consulta de status e download da tarefa de vídeo |
603
+
604
+
605
+ ### Utilidades
606
+
607
+
608
+ | Rota | Método | Descrição |
609
+ | ------------ | ------ | ------------------------------------------------- |
610
+ | `/health` | GET | Health check |
611
+ | `/metrics` | GET | Prometheus (protegido por API key se configurada) |
612
+ | `/v1/upload` | POST | Upload multimodal |
613
+
614
+
615
+ > Rotas sem o prefixo `/v1` (ex.: `/chat/completions`) são redirecionadas com 308 preservando método e corpo. Respostas incluem headers OpenAI (`openai-version`, `openai-processing-ms`, `x-ratelimit-*`).
616
+
617
+ ---
618
+
619
+ ## Exemplos de uso
620
+
621
+ ### OpenAI SDK (Node.js)
622
+
623
+ ```typescript
624
+ import OpenAI from "openai";
625
+
626
+ const client = new OpenAI({
627
+ baseURL: "http://localhost:7936/v1",
628
+ apiKey: "sua-api-key",
629
+ });
630
+
631
+ const completion = await client.chat.completions.create({
632
+ model: "qwen3.7-plus",
633
+ messages: [{ role: "user", content: "Hello!" }],
634
+ });
635
+
636
+ console.log(completion.choices[0].message.content);
637
+ ```
638
+
639
+ ### Anthropic SDK / Claude Code CLI
640
+
641
+ O proxy é 100% compatível com o **Claude Code CLI** e o **Anthropic SDK**:
642
+
643
+ ```bash
644
+ # Configuração para Claude Code CLI
645
+ export ANTHROPIC_BASE_URL="http://localhost:7936"
646
+ export ANTHROPIC_API_KEY="sua-api-key"
647
+
648
+ # Iniciar Claude Code
649
+ claude
650
+ ```
651
+
652
+ ```typescript
653
+ import Anthropic from "@anthropic-ai/sdk";
654
+
655
+ const anthropic = new Anthropic({
656
+ baseURL: "http://localhost:7936",
657
+ apiKey: "sua-api-key",
658
+ });
659
+
660
+ const message = await anthropic.messages.create({
661
+ model: "claude-3-7-sonnet-20250219", // ou "qwen3.8-max"
662
+ max_tokens: 1024,
663
+ messages: [{ role: "user", content: "Olá!" }],
664
+ });
665
+
666
+ console.log(message.content[0]);
667
+ ```
668
+
669
+ ### OpenAI Responses API (Codex / Grok CLI)
670
+
671
+ ```typescript
672
+ import OpenAI from "openai";
673
+
674
+ const client = new OpenAI({
675
+ baseURL: "http://localhost:7936/v1",
676
+ apiKey: "sua-api-key",
677
+ });
678
+
679
+ // Streaming com reasoning effort
680
+ const stream = await client.responses.create({
681
+ model: "qwen3.8-max",
682
+ input: "Explique computação quântica",
683
+ reasoning: { effort: "high" },
684
+ stream: true,
685
+ });
686
+
687
+ for await (const event of stream) {
688
+ if (event.type === "response.output_text.delta") {
689
+ process.stdout.write(event.delta);
690
+ }
691
+ }
692
+ ```
693
+
694
+ ### Geração de Imagens (`/v1/images/generations`)
695
+
696
+ ```bash
697
+ curl http://localhost:7936/v1/images/generations \
698
+ -H "Content-Type: application/json" \
699
+ -H "Authorization: Bearer sua-api-key" \
700
+ -d '{
701
+ "model": "qwen-image-3.0-pro",
702
+ "prompt": "A futuristic cyberpunk city in the rain, ultra-detailed, cinematic lighting",
703
+ "size": "16:9"
704
+ }'
705
+ ```
706
+
707
+ ### Geração de Vídeos (`/v1/videos/generations`)
708
+
709
+ ```bash
710
+ curl http://localhost:7936/v1/videos/generations \
711
+ -H "Content-Type: application/json" \
712
+ -H "Authorization: Bearer sua-api-key" \
713
+ -d '{
714
+ "model": "wan3.0-video",
715
+ "prompt": "Drone shot flying over a misty pine forest at sunrise",
716
+ "size": "16:9",
717
+ "wait": true
718
+ }'
719
+ ```
720
+
721
+ ---
722
+
723
+ ### cURL
724
+
725
+ ```bash
726
+ curl http://localhost:7936/v1/chat/completions \
727
+ -H "Content-Type: application/json" \
728
+ -H "Authorization: Bearer sua-api-key" \
729
+ -d '{
730
+ "model": "qwen3.7-plus",
731
+ "messages": [{"role": "user", "content": "Hello!"}],
732
+ "stream": true
733
+ }'
734
+ ```
735
+
736
+ ### Grok CLI (config)
737
+
738
+ ```toml
739
+ [model.qwen38-max]
740
+ api_backend = "responses"
741
+ base_url = "http://127.0.0.1:7936/v1"
742
+ ```
743
+
744
+ ---
745
+
746
+ ## Tool calling
747
+
748
+ O parser suporta:
749
+
750
+ - tags `<tool_call>...</tool_call>` e variantes Qwen `<tool_calls>...</tool_call(s)>` (fechamentos case-insensitive)
751
+ - formato Hermes/XML (`<parameter name="...">`)
752
+ - JSON malformado / recovery (aspas/braces faltando)
753
+ - JSON **duplamente escapado** em arguments
754
+ - stream fragmentado / tool call sem open tag
755
+ - **fuzzy match** seguro de nomes (`readFile` → `read_file`) quando há match único
756
+ - tool names não declarados: podem ser preservados como texto literal (evita quebrar exemplos)
757
+ - **auto-retry**: se todos os tool calls vierem malformados, o proxy tenta reparo local e, se necessário, repete a geração em novo chat informando o erro ao modelo (até `RETRY_AUTO_MALFORMED_TOOLS_MAX`)
758
+
759
+ Tools internas da conta Qwen (web_search, code interpreter, etc.) ficam desligadas; o proxy usa as tools do cliente.
760
+
761
+ ---
762
+
763
+ ## Modelos
764
+
765
+ O proxy envia o id do modelo ao Qwen **como está**. Apenas os sufixos de raciocínio são normalizados antes de subir (via `stripThinkingSuffix`):
766
+
767
+ - `qwen3.7-plus` → base (Auto: o Qwen decide)
768
+ - `qwen3.7-plus-fast` → base + thinking OFF
769
+ - `qwen3.7-plus-thinking` → base + thinking ON
770
+ - `qwen3.7-plus-no-thinking` → base + thinking OFF (compat legado)
771
+
772
+ ---
773
+
774
+ ## Deploy com Docker
775
+
776
+ ```yaml
777
+ services:
778
+ qwenproxy:
779
+ build: .
780
+ container_name: qwenproxy
781
+ ports:
782
+ - "${PORT:-7936}:7936"
783
+ env_file:
784
+ - .env
785
+ volumes:
786
+ - ./data:/app/data
787
+ restart: unless-stopped
788
+ logging:
789
+ driver: "json-file"
790
+ options:
791
+ max-size: "10m"
792
+ max-file: "3"
793
+ ```
794
+
795
+ O container ajusta permissões de `data/db` e `data/qwen_profiles` no startup.
796
+
797
+ ---
798
+
799
+ ## Estrutura do projeto
800
+
801
+ ```
802
+ QwenProxy/
803
+ ├── src/
804
+ │ ├── api/ # Server Hono, models, errors
805
+ │ ├── benchmarks/ # Baseline de latência do proxy
806
+ │ ├── cache/ # Memory cache + Brotli
807
+ │ ├── core/ # Config, accounts, DB, metrics, cooldowns, model-registry
808
+ │ ├── routes/
809
+ │ │ ├── chat/ # Completions, streaming, account acquire, retry-policy
810
+ │ │ └── responses/ # OpenAI Responses API (state, streaming, adapter)
811
+ │ ├── services/
812
+ │ │ ├── playwright.ts # Browser + headers + cleanup
813
+ │ │ ├── qwen.ts # Upstream Qwen + personalization + idle timeout
814
+ │ │ ├── session-keeper.ts
815
+ │ │ ├── fingerprint.ts
816
+ │ │ └── human-behavior.ts
817
+ │ ├── tools/ # Parser e instruções de tools
818
+ │ ├── tests/
819
+ │ └── utils/
820
+ ├── data/ # SQLite, key e profiles (gitignored)
821
+ ├── Dockerfile
822
+ ├── docker-compose.yml
823
+ └── package.json
824
+ ```
825
+
826
+ ---
827
+
828
+ ## Scripts úteis
829
+
830
+
831
+ | Comando | Descrição |
832
+ | ------------------- | ----------------------------------------------------------------------- |
833
+ | `npm start` | Iniciar o servidor QwenProxy |
834
+ | `npm run sync` | Sincronizar clientes (Claude Code, Codex, OpenCode, OMP) com backup |
835
+ | `npm run clean` | Limpar caches temporários dos perfis Chromium (~4.5MB por conta) |
836
+ | `npm run clean:all` | Limpar caches + remover navegadores órfãos e versões antigas no SSD |
837
+ | `npm run reset` | Zerar cooldowns de contas no banco de dados |
838
+ | `npm run login` | Adicionar/autenticar novas contas visualmente no navegador |
839
+ | `npm run purge` | Limpar chats remotos do Qwen nas contas configuradas |
840
+ | `npm test` | Executar suíte de testes completa |
841
+ | `npm run typecheck` | Checagem estrita de tipos do TypeScript |
842
+
843
+ ---
844
+
845
+ ## Scripts de instalação, início e atualização
846
+
847
+ A pasta `scripts/` contém atalhos para instalar, iniciar e atualizar o projeto sem digitar os comandos manualmente.
848
+
849
+
850
+ | Script | Windows | Linux/macOS | O que faz |
851
+ | ----------- | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------- |
852
+ | Instalador | `scripts\install.bat` | `./scripts/install.sh` | Verifica Node 22+, roda `npm install`, cria `.env` a partir de `.env.example` se não existir |
853
+ | Iniciador | `scripts\start.bat` | `./scripts/start.sh` | Verifica dependências e `.env`, inicia o servidor com `npm start` |
854
+ | Atualizador | `scripts\update.bat` | `./scripts/update.sh` | `git pull` (se for repositório), `npm install` e `npx playwright install chromium` |
855
+
856
+
857
+ No Linux/macOS, dê permissão de execução na primeira vez:
858
+
859
+ ```bash
860
+ chmod +x scripts/*.sh
861
+ ```
862
+
863
+ ---
864
+
865
+ ## Troubleshooting
866
+
867
+
868
+ | Problema | Solução |
869
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
870
+ | Anti-bot / captcha | Solver Baxia automático por padrão (`CAPTCHA_SOLVER_ENABLED=true`); se falhar, a conta entra em cooldown (`CAPTCHA_ACCOUNT_COOLDOWN_MS`) e a request roda em outra conta |
871
+ | Quota exceeded | Mais contas ou esperar cooldown |
872
+ | `502 Bad Gateway` / `fetch failed` | Normalmente upstream/rede; o proxy faz retry automático |
873
+ | `invalid_input` (anexo inválido) | Retry com chat novo; settings `largeTextAsFile=false` ajudam |
874
+ | `context_length_exceeded` | O proxy bloqueou o prompt localmente antes de qualquer retry; reduza/resuma o histórico ou ajuste `QWEN_MAX_PROMPT_BYTES` |
875
+ | HTML/WAF no lugar do stream | O Bridge identifica o desafio e aciona o solver; se persistir, reduza o tamanho/frequência do payload e verifique a sessão |
876
+ | `Model not found` | Use um id do catálogo de `/v1/models` (ex.: `qwen3.8-max`) |
877
+ | Vários Chromes abertos / RAM alta | `SESSION_KEEP_ALIVE_ENABLED=false`, idle cleanup on, `PLAYWRIGHT_INIT_BATCH_SIZE=1`, `PLAYWRIGHT_JS_HEAP_MB`, watchdog RSS fecha idle sob pressão |
878
+ | Watchdog “RAM critical” falso | Baseado em RSS (`memory.rss.usage_percent`); confira `/health` |
879
+ | Timeout em requests grandes | Aumente `TOTAL_REQUEST_TIMEOUT` / `REASONING_MODEL_TIMEOUT` |
880
+ | `stream_aborted` em modelo reasoning | Idle timeout: zero bytes por `REASONING_MODEL_TIMEOUT` (180s default) fecha o stream retryável; aumente se necessário |
881
+ | `canSkipThinking: false` | O catálogo não informa `think_skip`; a variante pública `-fast` continua disponível e usa o payload Fast do Qwen |
882
+ | Grok CLI `missing field input_tokens_details` | Corrigido: usage sempre inclui `input_tokens_details` e `output_tokens_details` |
883
+ | Responses `previous_response_id` not found | Store SQLite com TTL 7 dias; verifique se `store: false` não foi enviado |
884
+ | Playwright não inicia | `npx playwright install chromium` |
885
+ | Porta em uso | Altere `PORT` no `.env` |
886
+ | Sessão expirada | `npm run login` ou deixe o refresh automático reautenticar |
887
+ | API aberta em `0.0.0.0` sem key | Defina `API_KEY` e/ou `HOST=127.0.0.1` |
888
+
889
+
890
+ ---
891
+
892
+ ## Créditos e Agradecimentos
893
+
894
+ O **QwenProxy** é desenvolvido e mantido por **johngbl**, construído sobre as fundações de código aberto originalmente criadas por **Pedro Farias** sob a licença [ISC](LICENSE).
895
+
896
+ ---
897
+ ## Disclaimer
898
+
899
+ **Software fornecido *as is*, sem qualquer garantia (expressa ou implícita), incluindo as de comerciabilidade, adequação a um fim, funcionamento contínuo, correção de erros ou suporte.**
900
+
901
+ - **Sem afiliação:** o QwenProxy não é afiliado, endossado nem patrocinado pela Alibaba/Qwen, OpenAI, Anthropic ou qualquer provedor citado. Marcas pertencem aos seus titulares.
902
+ - **Uso por sua conta e risco:** destinado a fins educacionais e de estudo técnico. Cabe exclusivamente a você cumprir os Termos de Uso e limites do serviço upstream, usar contas e credenciais próprias e verificar a legalidade do uso na sua jurisdição.
903
+ - **Responsabilidade integral do usuário:** você é o único responsável por bloqueio, suspensão ou encerramento de contas, desafios anti-bot, perda de dados ou histórico, custos imprevistos, indisponibilidade e por qualquer prompt, saída, arquivo ou resultado de mídia gerado.
904
+ - **Exclusão total do mantenedor:** nos limites máximos da lei, o autor e os contribuidores **não respondem** por quaisquer danos diretos, indiretos, incidentais, especiais ou consequenciais (perda de lucros, dados, contas, quota ou receita) decorrentes do uso ou da incapacidade de uso deste software, nem por mudanças, quebras ou bloqueios do serviço upstream, que podem ocorrer a qualquer momento e sem aviso prévio.
905
+ - **Sem prestação de serviço:** projeto voluntário, sem SLA e sem obrigação de atualizar ou corrigir.
906
+
907
907
  **Ao baixar, compilar ou executar este software você declara ter lido e aceito integralmente este aviso, isentando o mantenedor de qualquer obrigação, reclamação, ação, custo ou despesa (incluindo honorários advocatícios). Se não concordar, não utilize.** Acompanha e não substitui a licença [ISC](LICENSE).