{"repo": "bchhngsaygez/deepwhale", "files": {"README.vi.md": {"f542d69a7df4ba2b8d35ca696abec8ddf47e05ef": {"t": 1785746525.068422, "n": 6809, "h": "976b08c1deec3342", "c": "# DeepWhale — Cầu Nối DeepSeek Web sang API\n\n[](https://www.python.org/)\n[](LICENSE)\n[](https://hub.docker.com/)\n\n> [English](README.md) | **Tiếng Việt**\n\n**DeepWhale** chuyển đổi giao diện web của [DeepSeek Chat](https://chat.deepseek.com) thành một **điểm cuối API tương thích OpenAI** hoàn chỉnh, kèm theo **bảng điều khiển quản trị** để quản lý, kiểm tra và giám sát theo thời gian thực.\n\nCông cụ sử dụng kỹ thuật lấy dấu vân tay trình duyệt qua `cloakbrowser` (Playwright) để vượt qua Cloudflare và các hệ thống chống bot, cho phép truy cập lập trình đáng tin cậy vào các mô hình của DeepSeek. \n\n**LƯU Ý**:Tôi không hiểu tại sao kho mã nguồn (repo) này của mình lại chạy cực kỳ mượt mà trên Cline, Roo Code, Continue.dev và các công cụ tương tự, nhưng khi chuyển sang dùng các AI agent chạy trên dòng lệnh (CLI), nó lại hoàn toàn không chịu hoạt động. Nó không làm được bất cứ việc gì cả—từ tạo file, viết code cho đến tạo thư mục—tất cả đều thất bại. Nếu bạn biết cách khắc phục vấn đề này, vui lòng để lại gợi ý hoặc giải pháp trong mục \"Issues\".\n\n---\n\n## Tính Năng\n\n- **API Tương Thích OpenAI** — Thay thế trực tiếp cho `https://api.openai.com/v1`. Hoạt động với mọi OpenAI client (Cline, Roo Code, Continue.dev, OpenRouter, v.v.).\n- **Bảng Điều Khiển Quản Trị** — Giao diện web hiện đại với chuyển đổi ngôn ngữ (Tiếng Việt / English), nhật ký thời gian thực, trò chuyện đa lượt, kiểm tra API tương tác và quản lý máy chủ/tài khoản.\n- **Vượt Cloudflare** — Sử dụng `cloakbrowser` với kỹ thuật lấy dấu vân tay Chromium thực. Không cần trích xuất cookie thủ công hay giải CAPTCHA.\n- **Giải PoW Tự Động** — Tự động giải các thử thách PoW của DeepSeek bằng Web Workers trong trình duyệt, với phương án dự phòng Python thuần túy.\n- **Luân Chuyển Tài Khoản** — Cấu hình nhiều tài khoản DeepSeek; máy chủ luân chuyển theo vòng tròn, tự động bỏ qua các lần đăng nhập thất bại.\n- **Tự Phục Hồi** — Phát hiện token hết hạn hoặc không hợp lệ và tự động xác thực lại.\n- **Streaming & Không Streaming** — Hỗ trợ SSE streaming và JSON tiêu chuẩn. Tự động tiếp tục cho phản hồi dài (tới 8 lượt).\n- **Hỗ Trợ Suy Nghĩ/Lý Luận** — Các mô hình như `deepseek-reasoner` hiển thị nội dung suy nghĩ trong phản hồi.\n- **Bí Danh Mô Hình** — Ánh xạ bất kỳ tên mô hình nào (`gpt-4o`, `o1`, `qwen-plus`, v.v.) sang mô hình DeepSeek.\n- **Trò Chuyện Đa Lượt** — Giao diện trò chuyện tích hợp với lựa chọn mô hình và chế độ suy nghĩ.\n- **Nhật Ký Thời Gian Thực** — Luồng nhật ký SSE trực tiếp với mã màu theo danh mục và tự động cuộn.\n- **Hỗ Trợ CORS** — Truy cập API từ các công cụ và giao diện web.\n\n---\n\n## Khởi Động Nhanh (Docker)\n\n```bash\n# Sao chép và vào thư mục\ngit clone https://github.com/bchhngsaygez/deepwhale.git\ncd deepwhale\n\n# Tạo file .env\ncp .env.example .env\n# Chỉnh sửa .env với email/mật khẩu DeepSeek của bạn\n\n# Khởi động với Docker Compose\ndocker compose up -d\n```\n\nMở `http://localhost:5001/admin/login` (mật khẩu mặc định: `admin`).\n\n---\n\n## Cài Đặt Thủ Công\n\n### Yêu Cầu\n\n- Python 3.11+\n- pip\n- ~2 GB dung lượng ổ đĩa trống (cho Chromium)\n\n### Cài Đặt\n\n```bash\npip install -r requirements.txt\n```\n\nTải bản dựng Chromium cloakbrowser cho nền tảng của bạn từ [trang phát hành CloakBrowser](https://github.com/CloakHQ/cloakbrowser/releases).\n\n### Cấu Hình\n\n```ini\n# .env\nDEEPSEEK_EMAIL=email_cua_ban@example.com\nDEEPSEEK_PASSWORD=mat_khau_cua_ban\nAPI_KEY=sk-my-secret-key-1\nPORT=5001\nHOST=0.0.0.0\nADMIN_PASSWORD=admin\nFLASK_SECRET=bi-mat-ngau-nhien-cua-ban\n```\n\n### Khởi Động Máy Chủ\n\n```bash\npython server.py\n```\n\n### Kiểm Tra Qua Dòng Lệnh\n\n```bash\ncurl http://localhost:5001/v1/models \\\n -H \"Authorization: Bearer sk-my-secret-key-1\"\n\ncurl http://localhost:5001/v1/chat/completions \\\n -H \"Authorization: Bearer sk-my-secret-key-1\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [{\"role\": \"user\", \"content\": \"Xin chào!\"}]\n }'\n```\n\n---\n\n## Các Điểm Cuối API\n\n| Điểm Cuối | Phương Thức | Xác Thực | Mô Tả |\n|---|---|---|---|\n| `/v1/models` | GET | Có | Danh sách mô hình |\n| `/v1/chat/completions` | POST | Có | Trò chuyện (tương thích OpenAI) |\n| `/chat/completions` | POST | Có | Bí danh không có tiền tố phiên bản |\n| `/healthz`, `/readyz` | GET | Không | Kiểm tra sức khỏe |\n| `/admin/login` | GET/POST | Không | Xác thực quản trị |\n| `/admin/dashboard` | GET | Quản trị | Bảng điều khiển web |\n| `/admin/api/config` | GET/POST | Quản trị | Cấu hình máy chủ |\n| `/admin/api/logs` | GET | Quản trị | Nhật ký đã ghi lại |\n| `/admin/api/logs/stream` | GET | Quản trị | Luồng nhật ký trực tiếp (SSE) |\n| `/admin/api/accounts-status` | GET | Quản trị | Trạng thái xác thực tài khoản |\n\n---\n\n## Bảng Điều Khiển Quản Trị\n\nBảng điều khiển có năm phần truy cập từ thanh bên:\n\n| Phần | Mô Tả |\n|---|---|\n| **Tổng Quan** | Trạng thái máy chủ, số lượng mô hình, trạng thái tài khoản, thời gian hoạt động |\n| **Trò Chuyện** | Đối thoại đa lượt với DeepSeek, chọn mô hình, chế độ suy nghĩ |\n| **Kiểm Tra** | Lệnh curl có sẵn với nút sao chép + trình xây dựng yêu cầu HTTP tương tác |\n| **Nhật Ký** | Xem nhật ký thời gian thực qua SSE, mã màu, tự động cuộn |\n| **Cài Đặt** | Cấu hình máy chủ (API key, cổng, địa chỉ, mật khẩu) và quản lý tài khoản |\n\nChuyển đổi ngôn ngữ giữa **Tiếng Việt** và **English** có sẵn ở góc trên bên phải của bảng điều khiển.\n\n---\n\n## Mô Hình Hỗ Trợ\n\n| ID Mô Hình | Loại | Mô Tả |\n|---|---|---|\n| `deepseek-v4-flash` | Mặc định | Mô hình đa năng nhanh |\n| `deepseek-v4-pro` | Chuyên gia | Suy luận chất lượng cao hơn |\n| `deepseek-chat` | Mặc định | Mô hình trò chuyện kế thừa |\n| `deepseek-reasoner` | Chuyên gia | Có đầu ra suy nghĩ/lý luận |\n| `deepseek-r1` | Chuyên gia | DeepSeek R1 (bật suy nghĩ) |\n| `deepseek-v3` | Mặc định | DeepSeek V3 | \n\n---\n\n## Cấu Trúc Dự Án\n\n```\ndeepwhale/\n├── server.py # Flask API + bảng điều khiển quản trị\n├── deepseek_client.py # Phiên trình duyệt, đăng nhập, gọi API\n├── pow_solver.py # Trình giải thử thách PoW\n├── utils.py # Tiện ích dùng chung (UTF-8, tải env)\n├── test_client.py # Kịch bản kiểm tra\n├── templates/\n│ ├── login.html # Đăng nhập quản trị (giao diện tối, i18n)\n│ └── dashboard.html # Bảng điều khiển (i18n VI/EN)\n├── Dockerfile # Triển khai container\n├── docker-compose.yml # Thiết lập Docker Compose\n├── requirements.txt # Phụ thuộc Python\n├── .env.example # Mẫu biến môi trường\n└── README.md # Tài liệu (Tiếng Anh)\n```\n\n---\n\n## Giấy Phép\n\n[MIT](LICENSE) \n\n>⭐Nếu bạn thấy kho lưu trữ này hữu ích, vui lòng tặng nó một dấu sao (star).Cảm ơn vì bạn đã sử dụng!⭐\n"}, "d2a5176ae64de197ba8327c405ea2bef2b08e3d9": {"t": 1785765289.2376459, "n": 6809, "h": "976b08c1deec3342", "c": "# DeepWhale — Cầu Nối DeepSeek Web sang API\n\n[](https://www.python.org/)\n[](LICENSE)\n[](https://hub.docker.com/)\n\n> [English](README.md) | **Tiếng Việt**\n\n**DeepWhale** chuyển đổi giao diện web của [DeepSeek Chat](https://chat.deepseek.com) thành một **điểm cuối API tương thích OpenAI** hoàn chỉnh, kèm theo **bảng điều khiển quản trị** để quản lý, kiểm tra và giám sát theo thời gian thực.\n\nCông cụ sử dụng kỹ thuật lấy dấu vân tay trình duyệt qua `cloakbrowser` (Playwright) để vượt qua Cloudflare và các hệ thống chống bot, cho phép truy cập lập trình đáng tin cậy vào các mô hình của DeepSeek. \n\n**LƯU Ý**:Tôi không hiểu tại sao kho mã nguồn (repo) này của mình lại chạy cực kỳ mượt mà trên Cline, Roo Code, Continue.dev và các công cụ tương tự, nhưng khi chuyển sang dùng các AI agent chạy trên dòng lệnh (CLI), nó lại hoàn toàn không chịu hoạt động. Nó không làm được bất cứ việc gì cả—từ tạo file, viết code cho đến tạo thư mục—tất cả đều thất bại. Nếu bạn biết cách khắc phục vấn đề này, vui lòng để lại gợi ý hoặc giải pháp trong mục \"Issues\".\n\n---\n\n## Tính Năng\n\n- **API Tương Thích OpenAI** — Thay thế trực tiếp cho `https://api.openai.com/v1`. Hoạt động với mọi OpenAI client (Cline, Roo Code, Continue.dev, OpenRouter, v.v.).\n- **Bảng Điều Khiển Quản Trị** — Giao diện web hiện đại với chuyển đổi ngôn ngữ (Tiếng Việt / English), nhật ký thời gian thực, trò chuyện đa lượt, kiểm tra API tương tác và quản lý máy chủ/tài khoản.\n- **Vượt Cloudflare** — Sử dụng `cloakbrowser` với kỹ thuật lấy dấu vân tay Chromium thực. Không cần trích xuất cookie thủ công hay giải CAPTCHA.\n- **Giải PoW Tự Động** — Tự động giải các thử thách PoW của DeepSeek bằng Web Workers trong trình duyệt, với phương án dự phòng Python thuần túy.\n- **Luân Chuyển Tài Khoản** — Cấu hình nhiều tài khoản DeepSeek; máy chủ luân chuyển theo vòng tròn, tự động bỏ qua các lần đăng nhập thất bại.\n- **Tự Phục Hồi** — Phát hiện token hết hạn hoặc không hợp lệ và tự động xác thực lại.\n- **Streaming & Không Streaming** — Hỗ trợ SSE streaming và JSON tiêu chuẩn. Tự động tiếp tục cho phản hồi dài (tới 8 lượt).\n- **Hỗ Trợ Suy Nghĩ/Lý Luận** — Các mô hình như `deepseek-reasoner` hiển thị nội dung suy nghĩ trong phản hồi.\n- **Bí Danh Mô Hình** — Ánh xạ bất kỳ tên mô hình nào (`gpt-4o`, `o1`, `qwen-plus`, v.v.) sang mô hình DeepSeek.\n- **Trò Chuyện Đa Lượt** — Giao diện trò chuyện tích hợp với lựa chọn mô hình và chế độ suy nghĩ.\n- **Nhật Ký Thời Gian Thực** — Luồng nhật ký SSE trực tiếp với mã màu theo danh mục và tự động cuộn.\n- **Hỗ Trợ CORS** — Truy cập API từ các công cụ và giao diện web.\n\n---\n\n## Khởi Động Nhanh (Docker)\n\n```bash\n# Sao chép và vào thư mục\ngit clone https://github.com/bchhngsaygez/deepwhale.git\ncd deepwhale\n\n# Tạo file .env\ncp .env.example .env\n# Chỉnh sửa .env với email/mật khẩu DeepSeek của bạn\n\n# Khởi động với Docker Compose\ndocker compose up -d\n```\n\nMở `http://localhost:5001/admin/login` (mật khẩu mặc định: `admin`).\n\n---\n\n## Cài Đặt Thủ Công\n\n### Yêu Cầu\n\n- Python 3.11+\n- pip\n- ~2 GB dung lượng ổ đĩa trống (cho Chromium)\n\n### Cài Đặt\n\n```bash\npip install -r requirements.txt\n```\n\nTải bản dựng Chromium cloakbrowser cho nền tảng của bạn từ [trang phát hành CloakBrowser](https://github.com/CloakHQ/cloakbrowser/releases).\n\n### Cấu Hình\n\n```ini\n# .env\nDEEPSEEK_EMAIL=email_cua_ban@example.com\nDEEPSEEK_PASSWORD=mat_khau_cua_ban\nAPI_KEY=sk-my-secret-key-1\nPORT=5001\nHOST=0.0.0.0\nADMIN_PASSWORD=admin\nFLASK_SECRET=bi-mat-ngau-nhien-cua-ban\n```\n\n### Khởi Động Máy Chủ\n\n```bash\npython server.py\n```\n\n### Kiểm Tra Qua Dòng Lệnh\n\n```bash\ncurl http://localhost:5001/v1/models \\\n -H \"Authorization: Bearer sk-my-secret-key-1\"\n\ncurl http://localhost:5001/v1/chat/completions \\\n -H \"Authorization: Bearer sk-my-secret-key-1\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [{\"role\": \"user\", \"content\": \"Xin chào!\"}]\n }'\n```\n\n---\n\n## Các Điểm Cuối API\n\n| Điểm Cuối | Phương Thức | Xác Thực | Mô Tả |\n|---|---|---|---|\n| `/v1/models` | GET | Có | Danh sách mô hình |\n| `/v1/chat/completions` | POST | Có | Trò chuyện (tương thích OpenAI) |\n| `/chat/completions` | POST | Có | Bí danh không có tiền tố phiên bản |\n| `/healthz`, `/readyz` | GET | Không | Kiểm tra sức khỏe |\n| `/admin/login` | GET/POST | Không | Xác thực quản trị |\n| `/admin/dashboard` | GET | Quản trị | Bảng điều khiển web |\n| `/admin/api/config` | GET/POST | Quản trị | Cấu hình máy chủ |\n| `/admin/api/logs` | GET | Quản trị | Nhật ký đã ghi lại |\n| `/admin/api/logs/stream` | GET | Quản trị | Luồng nhật ký trực tiếp (SSE) |\n| `/admin/api/accounts-status` | GET | Quản trị | Trạng thái xác thực tài khoản |\n\n---\n\n## Bảng Điều Khiển Quản Trị\n\nBảng điều khiển có năm phần truy cập từ thanh bên:\n\n| Phần | Mô Tả |\n|---|---|\n| **Tổng Quan** | Trạng thái máy chủ, số lượng mô hình, trạng thái tài khoản, thời gian hoạt động |\n| **Trò Chuyện** | Đối thoại đa lượt với DeepSeek, chọn mô hình, chế độ suy nghĩ |\n| **Kiểm Tra** | Lệnh curl có sẵn với nút sao chép + trình xây dựng yêu cầu HTTP tương tác |\n| **Nhật Ký** | Xem nhật ký thời gian thực qua SSE, mã màu, tự động cuộn |\n| **Cài Đặt** | Cấu hình máy chủ (API key, cổng, địa chỉ, mật khẩu) và quản lý tài khoản |\n\nChuyển đổi ngôn ngữ giữa **Tiếng Việt** và **English** có sẵn ở góc trên bên phải của bảng điều khiển.\n\n---\n\n## Mô Hình Hỗ Trợ\n\n| ID Mô Hình | Loại | Mô Tả |\n|---|---|---|\n| `deepseek-v4-flash` | Mặc định | Mô hình đa năng nhanh |\n| `deepseek-v4-pro` | Chuyên gia | Suy luận chất lượng cao hơn |\n| `deepseek-chat` | Mặc định | Mô hình trò chuyện kế thừa |\n| `deepseek-reasoner` | Chuyên gia | Có đầu ra suy nghĩ/lý luận |\n| `deepseek-r1` | Chuyên gia | DeepSeek R1 (bật suy nghĩ) |\n| `deepseek-v3` | Mặc định | DeepSeek V3 | \n\n---\n\n## Cấu Trúc Dự Án\n\n```\ndeepwhale/\n├── server.py # Flask API + bảng điều khiển quản trị\n├── deepseek_client.py # Phiên trình duyệt, đăng nhập, gọi API\n├── pow_solver.py # Trình giải thử thách PoW\n├── utils.py # Tiện ích dùng chung (UTF-8, tải env)\n├── test_client.py # Kịch bản kiểm tra\n├── templates/\n│ ├── login.html # Đăng nhập quản trị (giao diện tối, i18n)\n│ └── dashboard.html # Bảng điều khiển (i18n VI/EN)\n├── Dockerfile # Triển khai container\n├── docker-compose.yml # Thiết lập Docker Compose\n├── requirements.txt # Phụ thuộc Python\n├── .env.example # Mẫu biến môi trường\n└── README.md # Tài liệu (Tiếng Anh)\n```\n\n---\n\n## Giấy Phép\n\n[MIT](LICENSE) \n\n>⭐Nếu bạn thấy kho lưu trữ này hữu ích, vui lòng tặng nó một dấu sao (star).Cảm ơn vì bạn đã sử dụng!⭐\n"}, "2d2764b96f195876e553c4aa1a876fb3b0cf5078": {"t": 1785765290.6249917, "n": 6993, "h": "8b1a0621a658b3f0", "c": "# DeepWhale — Cầu Nối DeepSeek Web sang API\n\n[](https://www.python.org/)\n[](LICENSE)\n[](https://hub.docker.com/)\n\n> [English](README.md) | **Tiếng Việt**\n\n**DeepWhale** chuyển đổi giao diện web của [DeepSeek Chat](https://chat.deepseek.com) thành một **điểm cuối API tương thích OpenAI** hoàn chỉnh, kèm theo **bảng điều khiển quản trị** để quản lý, kiểm tra và giám sát theo thời gian thực.\n\nCông cụ sử dụng kỹ thuật lấy dấu vân tay trình duyệt qua `cloakbrowser` (Playwright) để vượt qua Cloudflare và các hệ thống chống bot, cho phép truy cập lập trình đáng tin cậy vào các mô hình của DeepSeek. \n\n**LƯU Ý**:Tôi không hiểu tại sao kho mã nguồn (repo) này của mình lại chạy cực kỳ mượt mà trên Cline, Roo Code, Continue.dev và các công cụ tương tự, nhưng khi chuyển sang dùng các AI agent chạy trên dòng lệnh (CLI), nó lại hoàn toàn không chịu hoạt động. Nó không làm được bất cứ việc gì cả—từ tạo file, viết code cho đến tạo thư mục—tất cả đều thất bại. Nếu bạn biết cách khắc phục vấn đề này, vui lòng để lại gợi ý hoặc giải pháp trong mục \"Issues\".\n\n---\n\n## Tính Năng\n\n- **API Tương Thích OpenAI** — Thay thế trực tiếp cho `https://api.openai.com/v1`. Hoạt động với mọi OpenAI client (Cline, Roo Code, Continue.dev, OpenRouter, v.v.).\n- **Bảng Điều Khiển Quản Trị** — Giao diện web hiện đại với chuyển đổi ngôn ngữ (Tiếng Việt / English), nhật ký thời gian thực, trò chuyện đa lượt, kiểm tra API tương tác và quản lý máy chủ/tài khoản.\n- **Vượt Cloudflare** — Sử dụng `cloakbrowser` với kỹ thuật lấy dấu vân tay Chromium thực. Không cần trích xuất cookie thủ công hay giải CAPTCHA.\n- **Giải PoW Tự Động** — Tự động giải các thử thách PoW của DeepSeek bằng Web Workers trong trình duyệt, với phương án dự phòng Python thuần túy.\n- **Luân Chuyển Tài Khoản** — Cấu hình nhiều tài khoản DeepSeek; máy chủ luân chuyển theo vòng tròn, tự động bỏ qua các lần đăng nhập thất bại.\n- **Tự Phục Hồi** — Phát hiện token hết hạn hoặc không hợp lệ và tự động xác thực lại.\n- **Streaming & Không Streaming** — Hỗ trợ SSE streaming và JSON tiêu chuẩn. Tự động tiếp tục cho phản hồi dài (tới 8 lượt).\n- **Hỗ Trợ Suy Nghĩ/Lý Luận** — Các mô hình như `deepseek-reasoner` hiển thị nội dung suy nghĩ trong phản hồi.\n- **Bí Danh Mô Hình** — Ánh xạ bất kỳ tên mô hình nào (`gpt-4o`, `o1`, `qwen-plus`, v.v.) sang mô hình DeepSeek.\n- **Trò Chuyện Đa Lượt** — Giao diện trò chuyện tích hợp với lựa chọn mô hình và chế độ suy nghĩ.\n- **Nhật Ký Thời Gian Thực** — Luồng nhật ký SSE trực tiếp với mã màu theo danh mục và tự động cuộn.\n- **Hỗ Trợ CORS** — Truy cập API từ các công cụ và giao diện web.\n\n---\n\n## Khởi Động Nhanh (Docker)\n\n```bash\n# Sao chép và vào thư mục\ngit clone https://github.com/bchhngsaygez/deepwhale.git\ncd deepwhale\n\n# Tạo file .env\ncp .env.example .env\n# Chỉnh sửa .env với email/mật khẩu DeepSeek của bạn\n\n# Khởi động với Docker Compose\ndocker compose up -d\n```\n\nMở `http://localhost:5001/admin/login` (mật khẩu mặc định: `admin`).\n\n---\n\n## Cài Đặt Thủ Công\n\n### Yêu Cầu\n\n- Python 3.11+\n- pip\n- ~2 GB dung lượng ổ đĩa trống (cho Chromium)\n\n### Cài Đặt\n\n```bash\npip install -r requirements.txt\n```\n\nTải bản dựng Chromium cloakbrowser cho nền tảng của bạn từ [trang phát hành CloakBrowser](https://github.com/CloakHQ/cloakbrowser/releases).\n\n### Cấu Hình\n\n```ini\n# .env\nDEEPSEEK_EMAIL=email_cua_ban@example.com\nDEEPSEEK_PASSWORD=mat_khau_cua_ban\nAPI_KEY=sk-my-secret-key-1\nPORT=5001\nHOST=0.0.0.0\nADMIN_PASSWORD=admin\nFLASK_SECRET=bi-mat-ngau-nhien-cua-ban\n```\n\n### Khởi Động Máy Chủ\n\n```bash\npython server.py\n```\n\n### Kiểm Tra Qua Dòng Lệnh\n\n```bash\ncurl http://localhost:5001/v1/models \\\n -H \"Authorization: Bearer sk-my-secret-key-1\"\n\ncurl http://localhost:5001/v1/chat/completions \\\n -H \"Authorization: Bearer sk-my-secret-key-1\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [{\"role\": \"user\", \"content\": \"Xin chào!\"}]\n }'\n```\n\n---\n\n## Các Điểm Cuối API\n\n| Điểm Cuối | Phương Thức | Xác Thực | Mô Tả |\n|---|---|---|---|\n| `/v1/models` | GET | Có | Danh sách mô hình |\n| `/v1/chat/completions` | POST | Có | Trò chuyện (tương thích OpenAI) |\n| `/chat/completions` | POST | Có | Bí danh không có tiền tố phiên bản |\n| `/healthz`, `/readyz` | GET | Không | Kiểm tra sức khỏe |\n| `/admin/login` | GET/POST | Không | Xác thực quản trị |\n| `/admin/dashboard` | GET | Quản trị | Bảng điều khiển web |\n| `/admin/api/config` | GET/POST | Quản trị | Cấu hình máy chủ |\n| `/admin/api/logs` | GET | Quản trị | Nhật ký đã ghi lại |\n| `/admin/api/logs/stream` | GET | Quản trị | Luồng nhật ký trực tiếp (SSE) |\n| `/admin/api/accounts-status` | GET | Quản trị | Trạng thái xác thực tài khoản |\n\n---\n\n## Bảng Điều Khiển Quản Trị\n\nBảng điều khiển có năm phần truy cập từ thanh bên:\n\n| Phần | Mô Tả |\n|---|---|\n| **Tổng Quan** | Trạng thái máy chủ, số lượng mô hình, trạng thái tài khoản, thời gian hoạt động |\n| **Trò Chuyện** | Đối thoại đa lượt với DeepSeek, chọn mô hình, chế độ suy nghĩ |\n| **Kiểm Tra** | Lệnh curl có sẵn với nút sao chép + trình xây dựng yêu cầu HTTP tương tác |\n| **Nhật Ký** | Xem nhật ký thời gian thực qua SSE, mã màu, tự động cuộn |\n| **Cài Đặt** | Cấu hình máy chủ (API key, cổng, địa chỉ, mật khẩu) và quản lý tài khoản |\n\nChuyển đổi ngôn ngữ giữa **Tiếng Việt** và **English** có sẵn ở góc trên bên phải của bảng điều khiển.\n\n---\n\n## Mô Hình Hỗ Trợ\n\n| ID Mô Hình | Loại | Mô Tả |\n|---|---|---|\n| `deepseek-v4-flash` | Mặc định | Mô hình đa năng nhanh |\n| `deepseek-v4-pro` | Chuyên gia | Suy luận chất lượng cao hơn |\n| `deepseek-chat` | Mặc định | Mô hình trò chuyện kế thừa |\n| `deepseek-reasoner` | Chuyên gia | Có đầu ra suy nghĩ/lý luận |\n| `deepseek-r1` | Chuyên gia | DeepSeek R1 (bật suy nghĩ) |\n| `deepseek-v3` | Mặc định | DeepSeek V3 |\n\nBí danh tích hợp: `gpt-4o`, `gpt-4`, `gpt-3.5-turbo` → `deepseek-v4-flash`; `o3` → `deepseek-v4-pro`; `o1` → `deepseek-reasoner`; `qwen-plus`, `qwen-turbo`, v.v. → `deepseek-v4-flash`.\n\n---\n\n## Cấu Trúc Dự Án\n\n```\ndeepwhale/\n├── server.py # Flask API + bảng điều khiển quản trị\n├── deepseek_client.py # Phiên trình duyệt, đăng nhập, gọi API\n├── pow_solver.py # Trình giải thử thách PoW\n├── utils.py # Tiện ích dùng chung (UTF-8, tải env)\n├── test_client.py # Kịch bản kiểm tra\n├── templates/\n│ ├── login.html # Đăng nhập quản trị (giao diện tối, i18n)\n│ └── dashboard.html # Bảng điều khiển (i18n VI/EN)\n├── Dockerfile # Triển khai container\n├── docker-compose.yml # Thiết lập Docker Compose\n├── requirements.txt # Phụ thuộc Python\n├── .env.example # Mẫu biến môi trường\n└── README.md # Tài liệu (Tiếng Anh)\n```\n\n---\n\n## Giấy Phép\n\n[MIT](LICENSE) \n\n>⭐Nếu bạn thấy kho lưu trữ này hữu ích, vui lòng tặng nó một dấu sao (star).Cảm ơn vì bạn đã sử dụng!⭐\n"}, "8dca05e110a3166c7c36ca0c6e91dbbd7f8a1b8d": {"t": 1785765292.0622272, "n": 6966, "h": "8a412c3c88536828", "c": "# DeepWhale — Cầu Nối DeepSeek Web sang API\n\n[](https://www.python.org/)\n[](LICENSE)\n[](https://hub.docker.com/)\n\n> [English](README.md) | **Tiếng Việt**\n\n**DeepWhale** chuyển đổi giao diện web của [DeepSeek Chat](https://chat.deepseek.com) thành một **điểm cuối API tương thích OpenAI** hoàn chỉnh, kèm theo **bảng điều khiển quản trị** để quản lý, kiểm tra và giám sát theo thời gian thực.\n\nCông cụ sử dụng kỹ thuật lấy dấu vân tay trình duyệt qua `cloakbrowser` (Playwright) để vượt qua Cloudflare và các hệ thống chống bot, cho phép truy cập lập trình đáng tin cậy vào các mô hình của DeepSeek. \n\n**LƯU Ý**:Tôi không biết bằng cách nào nó lại không thể sài đc trên OpenClaude,khi hỏi nó tạo file hay viết code,nó sẽ bị ngắt đoạn và ko thực hiện hành động,tôi chỉ mới test trên Cline,Roo Code,Continue.dev và OpenClaude,và những CLI khác như OpenCode,Blackbox,... thì tôi chưa test,nhưng lỗi đã kể trên xuất hiện rõ nhất ở OpenClaude,còn những CLI khác thì có vẻ vẫn bình thường.Nếu bạn biết lí do hãy trả lời ở phần Issues\n\n---\n\n## Tính Năng\n\n- **API Tương Thích OpenAI** — Thay thế trực tiếp cho `https://api.openai.com/v1`. Hoạt động với mọi OpenAI client (Cline, Roo Code, Continue.dev, OpenRouter, v.v.).\n- **Bảng Điều Khiển Quản Trị** — Giao diện web hiện đại với chuyển đổi ngôn ngữ (Tiếng Việt / English), nhật ký thời gian thực, trò chuyện đa lượt, kiểm tra API tương tác và quản lý máy chủ/tài khoản.\n- **Vượt Cloudflare** — Sử dụng `cloakbrowser` với kỹ thuật lấy dấu vân tay Chromium thực. Không cần trích xuất cookie thủ công hay giải CAPTCHA.\n- **Giải PoW Tự Động** — Tự động giải các thử thách PoW của DeepSeek bằng Web Workers trong trình duyệt, với phương án dự phòng Python thuần túy.\n- **Luân Chuyển Tài Khoản** — Cấu hình nhiều tài khoản DeepSeek; máy chủ luân chuyển theo vòng tròn, tự động bỏ qua các lần đăng nhập thất bại.\n- **Tự Phục Hồi** — Phát hiện token hết hạn hoặc không hợp lệ và tự động xác thực lại.\n- **Streaming & Không Streaming** — Hỗ trợ SSE streaming và JSON tiêu chuẩn. Tự động tiếp tục cho phản hồi dài (tới 8 lượt).\n- **Hỗ Trợ Suy Nghĩ/Lý Luận** — Các mô hình như `deepseek-reasoner` hiển thị nội dung suy nghĩ trong phản hồi.\n- **Bí Danh Mô Hình** — Ánh xạ bất kỳ tên mô hình nào (`gpt-4o`, `o1`, `qwen-plus`, v.v.) sang mô hình DeepSeek.\n- **Trò Chuyện Đa Lượt** — Giao diện trò chuyện tích hợp với lựa chọn mô hình và chế độ suy nghĩ.\n- **Nhật Ký Thời Gian Thực** — Luồng nhật ký SSE trực tiếp với mã màu theo danh mục và tự động cuộn.\n- **Hỗ Trợ CORS** — Truy cập API từ các công cụ và giao diện web.\n\n---\n\n## Khởi Động Nhanh (Docker)\n\n```bash\n# Sao chép và vào thư mục\ngit clone https://github.com/bchhngsaygez/deepwhale.git\ncd deepwhale\n\n# Tạo file .env\ncp .env.example .env\n# Chỉnh sửa .env với email/mật khẩu DeepSeek của bạn\n\n# Khởi động với Docker Compose\ndocker compose up -d\n```\n\nMở `http://localhost:5001/admin/login` (mật khẩu mặc định: `admin`).\n\n---\n\n## Cài Đặt Thủ Công\n\n### Yêu Cầu\n\n- Python 3.11+\n- pip\n- ~2 GB dung lượng ổ đĩa trống (cho Chromium)\n\n### Cài Đặt\n\n```bash\npip install -r requirements.txt\n```\n\nTải bản dựng Chromium cloakbrowser cho nền tảng của bạn từ [trang phát hành CloakBrowser](https://github.com/CloakHQ/cloakbrowser/releases).\n\n### Cấu Hình\n\n```ini\n# .env\nDEEPSEEK_EMAIL=email_cua_ban@example.com\nDEEPSEEK_PASSWORD=mat_khau_cua_ban\nAPI_KEY=sk-my-secret-key-1\nPORT=5001\nHOST=0.0.0.0\nADMIN_PASSWORD=admin\nFLASK_SECRET=bi-mat-ngau-nhien-cua-ban\n```\n\n### Khởi Động Máy Chủ\n\n```bash\npython server.py\n```\n\n### Kiểm Tra Qua Dòng Lệnh\n\n```bash\ncurl http://localhost:5001/v1/models \\\n -H \"Authorization: Bearer sk-my-secret-key-1\"\n\ncurl http://localhost:5001/v1/chat/completions \\\n -H \"Authorization: Bearer sk-my-secret-key-1\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"model\": \"deepseek-v4-flash\",\n \"messages\": [{\"role\": \"user\", \"content\": \"Xin chào!\"}]\n }'\n```\n\n---\n\n## Các Điểm Cuối API\n\n| Điểm Cuối | Phương Thức | Xác Thực | Mô Tả |\n|---|---|---|---|\n| `/v1/models` | GET | Có | Danh sách mô hình |\n| `/v1/chat/completions` | POST | Có | Trò chuyện (tương thích OpenAI) |\n| `/chat/completions` | POST | Có | Bí danh không có tiền tố phiên bản |\n| `/healthz`, `/readyz` | GET | Không | Kiểm tra sức khỏe |\n| `/admin/login` | GET/POST | Không | Xác thực quản trị |\n| `/admin/dashboard` | GET | Quản trị | Bảng điều khiển web |\n| `/admin/api/config` | GET/POST | Quản trị | Cấu hình máy chủ |\n| `/admin/api/logs` | GET | Quản trị | Nhật ký đã ghi lại |\n| `/admin/api/logs/stream` | GET | Quản trị | Luồng nhật ký trực tiếp (SSE) |\n| `/admin/api/accounts-status` | GET | Quản trị | Trạng thái xác thực tài khoản |\n\n---\n\n## Bảng Điều Khiển Quản Trị\n\nBảng điều khiển có năm phần truy cập từ thanh bên:\n\n| Phần | Mô Tả |\n|---|---|\n| **Tổng Quan** | Trạng thái máy chủ, số lượng mô hình, trạng thái tài khoản, thời gian hoạt động |\n| **Trò Chuyện** | Đối thoại đa lượt với DeepSeek, chọn mô hình, chế độ suy nghĩ |\n| **Kiểm Tra** | Lệnh curl có sẵn với nút sao chép + trình xây dựng yêu cầu HTTP tương tác |\n| **Nhật Ký** | Xem nhật ký thời gian thực qua SSE, mã màu, tự động cuộn |\n| **Cài Đặt** | Cấu hình máy chủ (API key, cổng, địa chỉ, mật khẩu) và quản lý tài khoản |\n\nChuyển đổi ngôn ngữ giữa **Tiếng Việt** và **English** có sẵn ở góc trên bên phải của bảng điều khiển.\n\n---\n\n## Mô Hình Hỗ Trợ\n\n| ID Mô Hình | Loại | Mô Tả |\n|---|---|---|\n| `deepseek-v4-flash` | Mặc định | Mô hình đa năng nhanh |\n| `deepseek-v4-pro` | Chuyên gia | Suy luận chất lượng cao hơn |\n| `deepseek-chat` | Mặc định | Mô hình trò chuyện kế thừa |\n| `deepseek-reasoner` | Chuyên gia | Có đầu ra suy nghĩ/lý luận |\n| `deepseek-r1` | Chuyên gia | DeepSeek R1 (bật suy nghĩ) |\n| `deepseek-v3` | Mặc định | DeepSeek V3 |\n\nBí danh tích hợp: `gpt-4o`, `gpt-4`, `gpt-3.5-turbo` → `deepseek-v4-flash`; `o3` → `deepseek-v4-pro`; `o1` → `deepseek-reasoner`; `qwen-plus`, `qwen-turbo`, v.v. → `deepseek-v4-flash`.\n\n---\n\n## Cấu Trúc Dự Án\n\n```\ndeepwhale/\n├── server.py # Flask API + bảng điều khiển quản trị\n├── deepseek_client.py # Phiên trình duyệt, đăng nhập, gọi API\n├── pow_solver.py # Trình giải thử thách PoW\n├── utils.py # Tiện ích dùng chung (UTF-8, tải env)\n├── test_client.py # Kịch bản kiểm tra\n├── templates/\n│ ├── login.html # Đăng nhập quản trị (giao diện tối, i18n)\n│ └── dashboard.html # Bảng điều khiển (i18n VI/EN)\n├── Dockerfile # Triển khai container\n├── docker-compose.yml # Thiết lập Docker Compose\n├── requirements.txt # Phụ thuộc Python\n├── .env.example # Mẫu biến môi trường\n└── README.md # Tài liệu (Tiếng Anh)\n```\n\n---\n\n## Giấy Phép\n\n[MIT](LICENSE) \n\n>⭐Nếu bạn thấy kho lưu trữ này hữu ích, vui lòng tặng nó một dấu sao (star).Cảm ơn vì bạn đã sử dụng!⭐\n"}}, "server.py": {"f542d69a7df4ba2b8d35ca696abec8ddf47e05ef": {"t": 1785746560.6760428, "n": 31609, "h": "f29e11df5c5c6ff0", "c": "import sys\nimport os\n\nfrom utils import setup_utf8, load_env, get_secret_key\n\nsetup_utf8()\nload_env()\n\nimport json\nimport time\nimport uuid\nimport threading\nimport io\nimport re\nimport secrets\nfrom collections import deque\nfrom flask import Flask, request, Response, jsonify, session, render_template, redirect, url_for, stream_with_context\n\nfrom deepseek_client import (\n login, create_session, get_pow,\n call_completion, call_continue,\n delete_session, parse_sse_lines,\n collect_response, make_session, get_model_type,\n)\n\nAPI_KEYS = []\nAPI_KEYS_LOCK = threading.RLock()\n\ndef load_api_keys():\n global API_KEYS\n with API_KEYS_LOCK:\n keys_json = os.environ.get(\"API_KEYS_JSON\", \"\")\n if keys_json:\n try:\n parsed = json.loads(keys_json)\n if isinstance(parsed, list) and len(parsed) > 0:\n API_KEYS = parsed\n return\n except json.JSONDecodeError:\n pass\n legacy = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n if legacy:\n API_KEYS = [{\"id\": \"default\", \"name\": \"Default\", \"key\": legacy, \"enabled\": True}]\n _persist_api_keys_mem()\n\ndef _persist_api_keys_mem():\n with API_KEYS_LOCK:\n os.environ[\"API_KEYS_JSON\"] = json.dumps(API_KEYS)\n if API_KEYS:\n os.environ[\"API_KEY\"] = API_KEYS[0][\"key\"]\n\nload_api_keys()\n\nACCOUNTS = []\naccounts_env = os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\")\nif accounts_env:\n for acc_str in accounts_env.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\n \"email\": parts[0].strip(),\n \"password\": parts[1].strip(),\n \"token\": None\n })\n\nif not ACCOUNTS:\n email = os.environ.get(\"DEEPSEEK_EMAIL\", \"\").strip()\n password = os.environ.get(\"DEEPSEEK_PASSWORD\", \"\").strip()\n if not email or not password:\n raise ValueError(\"ERROR: DEEPSEEK_EMAIL or DEEPSEEK_PASSWORD not configured in .env file!\")\n ACCOUNTS.append({\n \"email\": email,\n \"password\": password,\n \"token\": None,\n })\n\nAVAILABLE_MODELS = [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n \"deepseek-r1\",\n \"deepseek-v3\",\n]\n\nMODEL_ALIASES = {}\n\n_account_lock = threading.Lock()\n_current_account_index = 0\n\ndef get_active_token() -> str:\n global _current_account_index\n with _account_lock:\n if not ACCOUNTS:\n raise RuntimeError(\"No DeepSeek accounts configured!\")\n\n for _ in range(len(ACCOUNTS)):\n acc = ACCOUNTS[_current_account_index]\n if not acc.get(\"token\"):\n try:\n print(f\"[auth] Logging in account #{_current_account_index + 1}: {acc.get('email')}\")\n token = login(\n email=acc.get(\"email\"),\n password=acc.get(\"password\")\n )\n acc[\"token\"] = token\n print(f\"[auth] Login OK for account #{_current_account_index + 1}: {token[:20]}...\")\n except Exception as e:\n print(f\"[auth] Account #{_current_account_index + 1} ({acc.get('email')}) login failed: {e}\")\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n continue\n\n token = acc[\"token\"]\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n return token\n\n raise RuntimeError(\"All configured DeepSeek accounts failed to log in!\")\n\ndef invalidate_token(token: str = None):\n with _account_lock:\n if token:\n for acc in ACCOUNTS:\n if acc.get(\"token\") == token:\n print(f\"[auth] Invalidating token for account: {acc.get('email')}\")\n acc[\"token\"] = None\n break\n else:\n for acc in ACCOUNTS:\n acc[\"token\"] = None\n\nLOG_BUFFER = deque(maxlen=2000)\nLOG_LOCK = threading.Lock()\n\nclass LogCapture(io.StringIO):\n def __init__(self, original):\n super().__init__()\n self._original = original\n def write(self, msg):\n if msg.strip():\n ts = time.strftime(\"%H:%M:%S\", time.localtime())\n with LOG_LOCK:\n LOG_BUFFER.append((ts, msg.rstrip()))\n self._original.write(msg)\n def flush(self):\n self._original.flush()\n\n_original_stdout = sys.stdout\nsys.stdout = LogCapture(_original_stdout)\n_original_stderr = sys.stderr\nsys.stderr = LogCapture(_original_stderr)\n\nCHAT_SESSIONS = {}\nCHAT_LOCK = threading.Lock()\n\ndef get_chat_session(chat_id: str):\n with CHAT_LOCK:\n if chat_id not in CHAT_SESSIONS:\n token = get_active_token()\n sess = make_session()\n sid = create_session(token, session=sess)\n CHAT_SESSIONS[chat_id] = {\n \"session_id\": sid,\n \"messages\": [],\n \"token\": token,\n \"http_session\": sess,\n \"created_at\": time.time(),\n }\n return CHAT_SESSIONS[chat_id]\n\napp = Flask(__name__)\napp.secret_key = get_secret_key()\napp.config['PERMANENT_SESSION_LIFETIME'] = 86400\n\nADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n\ndef require_admin():\n if not session.get(\"admin_authenticated\"):\n return redirect(url_for(\"admin_login_page\"))\n return None\n\nENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), \".env\")\n\ndef save_env_file(updates: dict):\n lines = []\n if os.path.exists(ENV_PATH):\n with open(ENV_PATH, \"r\", encoding=\"utf-8\") as f:\n lines = f.readlines()\n remaining = dict(updates)\n new_lines = []\n for line in lines:\n stripped = line.strip()\n if not stripped or stripped.startswith(\"#\"):\n new_lines.append(line)\n continue\n if \"=\" in stripped:\n key = stripped.split(\"=\", 1)[0].strip()\n if key in remaining:\n val = remaining.pop(key)\n new_lines.append(f'{key}=\"{val}\"\\n')\n continue\n new_lines.append(line)\n for key, val in remaining.items():\n new_lines.append(f'{key}=\"{val}\"\\n')\n with open(ENV_PATH, \"w\", encoding=\"utf-8\") as f:\n f.writelines(new_lines)\n for key, val in updates.items():\n os.environ[key] = val\n\n@app.after_request\ndef after_request(response):\n response.headers.add('Access-Control-Allow-Origin', '*')\n response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')\n response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')\n return response\n\n\ndef get_caller_key():\n auth = request.headers.get(\"Authorization\", \"\")\n if auth.lower().startswith(\"bearer \"):\n key = auth[7:].strip()\n if key:\n return key\n key = request.headers.get(\"X-Api-Key\", \"\").strip()\n if key:\n return key\n return None\n\ndef require_auth():\n key = get_caller_key()\n if not key:\n return jsonify({\"error\": {\"message\": \"Missing API key\", \"type\": \"invalid_request_error\"}}), 401\n with API_KEYS_LOCK:\n valid = any(k[\"key\"] == key and k.get(\"enabled\", True) for k in API_KEYS)\n if not valid:\n return jsonify({\"error\": {\"message\": \"Invalid or disabled API key\", \"type\": \"invalid_request_error\"}}), 401\n return None\n\n\ndef build_prompt(messages: list) -> str:\n parts = []\n for msg in messages:\n role = msg.get(\"role\", \"user\")\n content = msg.get(\"content\", \"\")\n\n if isinstance(content, list):\n texts = [\n item.get(\"text\", \"\")\n for item in content\n if isinstance(item, dict) and item.get(\"type\") == \"text\"\n ]\n content = \"\\n\".join(texts)\n elif not isinstance(content, str):\n content = str(content)\n\n if role == \"system\":\n parts.append(f\"\\n{content}\\n\")\n elif role == \"user\":\n parts.append(f\"Human: {content}\")\n elif role == \"assistant\":\n parts.append(f\"Assistant: {content}\")\n\n parts.append(\"Assistant:\")\n return \"\\n\\n\".join(parts)\n\ndef resolve_model(model: str) -> str:\n return MODEL_ALIASES.get(model.strip().lower(), model.strip())\n\n\ndef make_chunk(completion_id: str, model: str, delta: dict,\n finish_reason=None) -> str:\n obj = {\n \"id\": completion_id,\n \"object\": \"chat.completion.chunk\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"delta\": delta,\n \"finish_reason\": finish_reason,\n }],\n }\n return f\"data: {json.dumps(obj)}\\n\\n\"\n\n\ndef stream_generator(token: str, prompt: str, model: str,\n thinking_enabled: bool, completion_id: str):\n sess = make_session()\n yield make_chunk(completion_id, model, {\"role\": \"assistant\", \"content\": \"\"})\n\n session_id = None\n msg_id = 0\n last_status = \"\"\n\n try:\n session_id = create_session(token, session=sess)\n pow_resp = get_pow(token, session=sess)\n\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled,\n pow_response=pow_resp, http_session=sess,\n )\n\n def consume(lines_gen):\n nonlocal msg_id, last_status\n for chunk in parse_sse_lines(lines_gen):\n if chunk.get(\"response_message_id\"):\n msg_id = int(chunk[\"response_message_id\"])\n\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n\n if \"status\" in p and isinstance(v, str):\n last_status = v\n if \"auto_continue\" in p and v is True:\n last_status = \"AUTO_CONTINUE\"\n\n if isinstance(v, str) and \"content\" in p:\n yield make_chunk(completion_id, model, {\"content\": v})\n\n yield from consume(lines)\n\n for rnd in range(8):\n if last_status.upper() not in (\"INCOMPLETE\", \"AUTO_CONTINUE\"):\n break\n if msg_id <= 0:\n break\n print(f\"[auto_continue] round {rnd+1}, msg_id={msg_id}\")\n pow2 = get_pow(token, session=sess)\n cont = call_continue(token, session_id, msg_id,\n pow_response=pow2, http_session=sess)\n last_status = \"\"\n yield from consume(cont)\n\n yield make_chunk(completion_id, model, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n\n except Exception as e:\n invalidate_token(token)\n err = {\"error\": {\"type\": \"api_error\", \"message\": str(e)}}\n yield f\"data: {json.dumps(err)}\\n\\n\"\n\n\n@app.get(\"/healthz\")\n@app.get(\"/readyz\")\ndef health():\n return jsonify({\"status\": \"ok\"})\n\n\n@app.get(\"/v1/models\")\n@app.get(\"/models\")\ndef list_models():\n err = require_auth()\n if err:\n return err\n data = [\n {\"id\": m, \"object\": \"model\", \"created\": 1700000000, \"owned_by\": \"deepseek\"}\n for m in AVAILABLE_MODELS\n ]\n return jsonify({\"object\": \"list\", \"data\": data})\n\n\n@app.get(\"/v1/chat/completions\")\n@app.get(\"/chat/completions\")\ndef chat_completions_info():\n return jsonify({\n \"message\": \"This endpoint requires a POST request with a JSON body.\",\n \"usage\": 'curl -X POST http://localhost:5001/v1/chat/completions -H \"Authorization: Bearer YOUR_API_KEY\" -H \"Content-Type: application/json\" -d \\'{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}\\'',\n \"endpoints\": {\n \"/v1/chat/completions\": \"POST - Chat completion\",\n \"/v1/models\": \"GET - List models\",\n \"/healthz\": \"GET - Health check\",\n }\n })\n\n@app.post(\"/v1/chat/completions\")\n@app.post(\"/chat/completions\")\ndef chat_completions():\n err = require_auth()\n if err:\n return err\n\n body = request.get_json(force=True, silent=True) or {}\n model = resolve_model(body.get(\"model\", \"deepseek-v4-flash\"))\n msgs = body.get(\"messages\", [])\n stream = bool(body.get(\"stream\", False))\n thinking_flag = body.get(\"thinking\", None)\n\n if not msgs:\n return jsonify({\"error\": {\"message\": \"messages required\"}}), 400\n\n prompt = build_prompt(msgs)\n\n thinking_enabled = bool(thinking_flag) if thinking_flag is not None \\\n else (get_model_type(model) == \"reasoner\")\n\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": {\"message\": f\"Auth failed: {e}\"}}), 500\n\n if stream:\n return Response(\n stream_generator(token, prompt, model, thinking_enabled, completion_id),\n mimetype=\"text/event-stream\",\n headers={\n \"Cache-Control\": \"no-cache\",\n \"X-Accel-Buffering\": \"no\",\n \"Connection\": \"keep-alive\",\n },\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": {\"message\": str(e)}}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n\n resp = {\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n }\n if result.get(\"thinking\"):\n resp[\"choices\"][0][\"message\"][\"thinking\"] = result[\"thinking\"]\n\n return jsonify(resp)\n\n\n@app.route(\"/admin/login\", methods=[\"GET\"])\ndef admin_login_page():\n return render_template(\"login.html\")\n\n@app.route(\"/admin/login\", methods=[\"POST\"])\ndef admin_login():\n data = request.get_json(force=True, silent=True) or {}\n pw = data.get(\"password\", \"\")\n if pw == ADMIN_PASSWORD:\n session[\"admin_authenticated\"] = True\n session.permanent = True\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Invalid password\"}), 401\n\n@app.route(\"/admin/logout\")\ndef admin_logout():\n session.pop(\"admin_authenticated\", None)\n return redirect(url_for(\"admin_login_page\"))\n\n@app.route(\"/admin/dashboard\")\ndef admin_dashboard():\n err = require_admin()\n if err:\n return err\n return render_template(\"dashboard.html\")\n\n@app.route(\"/admin/api/config\", methods=[\"GET\"])\ndef admin_get_config():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe_keys = [{\"id\": k[\"id\"], \"name\": k[\"name\"], \"key\": k[\"key\"], \"enabled\": k.get(\"enabled\", True)} for k in API_KEYS]\n return jsonify({\n \"api_key\": os.environ.get(\"API_KEY\", \"\"),\n \"api_keys\": safe_keys,\n \"port\": os.environ.get(\"PORT\", \"5001\"),\n \"host\": os.environ.get(\"HOST\", \"0.0.0.0\"),\n \"email\": os.environ.get(\"DEEPSEEK_EMAIL\", \"\"),\n \"password\": os.environ.get(\"DEEPSEEK_PASSWORD\", \"\"),\n \"accounts_raw\": os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\"),\n })\n\n@app.route(\"/admin/api/config\", methods=[\"POST\"])\ndef admin_save_config():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n updates = {}\n global ADMIN_PASSWORD\n if \"admin_password\" in data and data[\"admin_password\"].strip():\n updates[\"ADMIN_PASSWORD\"] = data[\"admin_password\"].strip()\n ADMIN_PASSWORD = data[\"admin_password\"].strip()\n if \"api_key\" in data and data[\"api_key\"].strip():\n updates[\"API_KEY\"] = data[\"api_key\"].strip()\n with API_KEYS_LOCK:\n found = False\n for k in API_KEYS:\n if k[\"id\"] == \"default\":\n k[\"key\"] = data[\"api_key\"].strip()\n found = True\n break\n if not found:\n API_KEYS.insert(0, {\"id\": \"default\", \"name\": \"Default\", \"key\": data[\"api_key\"].strip(), \"enabled\": True})\n _persist_api_keys_mem()\n if \"port\" in data and str(data[\"port\"]).strip():\n updates[\"PORT\"] = str(data[\"port\"]).strip()\n if \"host\" in data and data[\"host\"].strip():\n updates[\"HOST\"] = data[\"host\"].strip()\n if \"email\" in data and data[\"email\"].strip():\n updates[\"DEEPSEEK_EMAIL\"] = data[\"email\"].strip()\n if \"password\" in data and data[\"password\"].strip():\n updates[\"DEEPSEEK_PASSWORD\"] = data[\"password\"].strip()\n if \"accounts\" in data:\n val = data[\"accounts\"].strip()\n updates[\"DEEPSEEK_ACCOUNTS\"] = val\n ACCOUNTS.clear()\n if val:\n for acc_str in val.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\"email\": parts[0].strip(), \"password\": parts[1].strip(), \"token\": None})\n if updates:\n save_env_file(updates)\n return jsonify({\"message\": \"Configuration saved\"})\n return jsonify({\"error\": \"No changes provided\"}), 400\n\n@app.route(\"/admin/api/api-keys\", methods=[\"GET\"])\ndef admin_get_api_keys():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe = [\n {\"id\": k[\"id\"], \"name\": k[\"name\"], \"key_preview\": k[\"key\"][:12] + \"...\", \"enabled\": k.get(\"enabled\", True)}\n for k in API_KEYS\n ]\n return jsonify({\"keys\": safe})\n\n@app.route(\"/admin/api/api-keys\", methods=[\"POST\"])\ndef admin_add_api_key():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n name = data.get(\"name\", \"\").strip()\n if not name:\n return jsonify({\"error\": \"Key name is required\"}), 400\n custom_key = data.get(\"key\", \"\").strip()\n with API_KEYS_LOCK:\n if any(k[\"name\"] == name for k in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{name}' already exists\"}), 400\n if custom_key:\n if any(k[\"key\"] == custom_key for k in API_KEYS):\n return jsonify({\"error\": \"An API key with this value already exists\"}), 400\n if len(custom_key) < 6:\n return jsonify({\"error\": \"API key must be at least 6 characters\"}), 400\n new_key = custom_key or (\"sk-\" + secrets.token_hex(24))\n entry = {\"id\": secrets.token_hex(8), \"name\": name, \"key\": new_key, \"enabled\": True}\n API_KEYS.append(entry)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True, \"key\": {\"id\": entry[\"id\"], \"name\": entry[\"name\"], \"key\": entry[\"key\"], \"enabled\": True}})\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"PATCH\"])\ndef admin_update_api_key(key_id):\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n with API_KEYS_LOCK:\n for k in API_KEYS:\n if k[\"id\"] == key_id:\n if \"name\" in data and data[\"name\"].strip():\n new_name = data[\"name\"].strip()\n if any(kk[\"name\"] == new_name and kk[\"id\"] != key_id for kk in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{new_name}' already exists\"}), 400\n k[\"name\"] = new_name\n if \"enabled\" in data:\n k[\"enabled\"] = bool(data[\"enabled\"])\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"DELETE\"])\ndef admin_delete_api_key(key_id):\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n for i, k in enumerate(API_KEYS):\n if k[\"id\"] == key_id:\n API_KEYS.pop(i)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/test\", methods=[\"POST\"])\ndef admin_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n prompt_text = data.get(\"prompt\", \"Hello!\")\n use_stream = bool(data.get(\"stream\", False))\n model = \"deepseek-v4-flash\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": f\"Auth failed: {e}\"}), 500\n\n prompt = f\"Human: {prompt_text}\\n\\nAssistant:\"\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n if use_stream:\n def gen():\n yield from stream_generator(token, prompt, model, False, completion_id)\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=False, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": str(e)}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n return jsonify({\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n })\n\n\n@app.route(\"/admin/api/accounts-status\")\ndef admin_accounts_status():\n err = require_admin()\n if err:\n return err\n with _account_lock:\n accounts_info = []\n for acc in ACCOUNTS:\n accounts_info.append({\n \"email\": acc.get(\"email\", \"\"),\n \"has_token\": acc.get(\"token\") is not None,\n \"token_prefix\": (acc.get(\"token\") or \"\")[:12] + \"...\" if acc.get(\"token\") else None,\n })\n return jsonify({\"accounts\": accounts_info, \"count\": len(accounts_info)})\n\n\n@app.route(\"/admin/api/chat\", methods=[\"POST\"])\ndef admin_chat():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n message = data.get(\"message\", \"\").strip()\n model = data.get(\"model\", \"deepseek-v4-flash\")\n use_stream = bool(data.get(\"stream\", False))\n thinking = bool(data.get(\"thinking\", False))\n context_messages = data.get(\"context_messages\")\n if context_messages is not None:\n context_messages = int(context_messages)\n\n if not message:\n return jsonify({\"error\": \"Message is required\"}), 400\n\n try:\n chat = get_chat_session(chat_id)\n chat[\"messages\"].append({\"role\": \"user\", \"content\": message})\n msgs = chat[\"messages\"]\n if context_messages is not None and context_messages >= 0:\n msgs = msgs[-context_messages:] if context_messages > 0 else [msgs[-1]]\n prompt = build_prompt(msgs)\n token = chat[\"token\"]\n resolved = resolve_model(model)\n\n if use_stream:\n def gen(token=token):\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n yield make_chunk(completion_id, resolved, {\"role\": \"assistant\", \"content\": \"\"})\n session_id = chat[\"session_id\"]\n try:\n pow_resp = get_pow(token, session=chat[\"http_session\"])\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=resolved, thinking=thinking,\n pow_response=pow_resp, http_session=chat[\"http_session\"],\n )\n full_text = \"\"\n for chunk in parse_sse_lines(lines):\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n if isinstance(v, str) and \"content\" in p:\n full_text += v\n yield make_chunk(completion_id, resolved, {\"content\": v})\n yield make_chunk(completion_id, resolved, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": full_text})\n except Exception as e:\n yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n result = collect_response(\n token=token, session_id=chat[\"session_id\"], prompt=prompt,\n model=resolved, thinking=thinking, http_session=chat[\"http_session\"],\n )\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")})\n return jsonify({\n \"text\": result.get(\"text\", \"\"),\n \"thinking\": result.get(\"thinking\", \"\"),\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n@app.route(\"/admin/api/chat/reset\", methods=[\"POST\"])\ndef admin_chat_reset():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n with CHAT_LOCK:\n if chat_id in CHAT_SESSIONS:\n try:\n delete_session(CHAT_SESSIONS[chat_id][\"token\"], CHAT_SESSIONS[chat_id][\"session_id\"])\n except:\n pass\n del CHAT_SESSIONS[chat_id]\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/logs\")\ndef admin_get_logs():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n return jsonify({\"logs\": list(LOG_BUFFER)})\n\n@app.route(\"/admin/api/logs/stream\")\ndef admin_log_stream():\n err = require_admin()\n if err:\n return err\n def generate():\n last_count = 0\n while True:\n with LOG_LOCK:\n current = list(LOG_BUFFER)\n if len(current) > last_count:\n new_entries = current[last_count:]\n last_count = len(current)\n for ts, msg in new_entries:\n yield f\"data: {json.dumps({'ts': ts, 'msg': msg})}\\n\\n\"\n time.sleep(0.5)\n return Response(\n stream_with_context(generate()),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"Connection\": \"keep-alive\"},\n )\n\n@app.route(\"/admin/api/logs/clear\", methods=[\"POST\"])\ndef admin_log_clear():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n LOG_BUFFER.clear()\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/curl-test\", methods=[\"POST\"])\ndef admin_curl_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n method = data.get(\"method\", \"GET\").upper()\n path = data.get(\"path\", \"/v1/models\")\n headers = data.get(\"headers\", {})\n body = data.get(\"body\", \"\")\n\n test_headers = {}\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n for k, v in headers.items():\n test_headers[k] = v\n if \"Authorization\" not in test_headers and \"authorization\" not in {k.lower() for k in test_headers}:\n test_headers[\"Authorization\"] = f\"Bearer {api_key}\"\n\n from urllib.parse import urlparse\n parsed = urlparse(path)\n if parsed.netloc:\n full_url = path\n else:\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n full_url = f\"http://localhost:{port}{path}\"\n\n import http.client\n import urllib.parse\n\n try:\n parsed = urllib.parse.urlparse(full_url)\n conn = http.client.HTTPConnection(parsed.netloc, timeout=30)\n conn.request(method, parsed.path + (\"?\" + parsed.query if parsed.query else \"\"),\n body=body if body else None,\n headers=test_headers)\n resp = conn.getresponse()\n resp_body = resp.read().decode(\"utf-8\", errors=\"replace\")\n if len(resp_body) > 10000:\n resp_body = resp_body[:10000] + \"\\n... (truncated)\"\n return jsonify({\n \"status\": resp.status,\n \"reason\": resp.reason,\n \"headers\": dict(resp.getheaders()),\n \"body\": resp_body,\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n\n@app.errorhandler(404)\ndef not_found(e):\n return jsonify({\n \"message\": \"Endpoint not found. Here are the available endpoints:\",\n \"endpoints\": {\n \"GET /v1/models\": \"List available models\",\n \"POST /v1/chat/completions\": \"Chat completion (OpenAI-compatible)\",\n \"GET /healthz\": \"Health check\",\n \"GET /readyz\": \"Readiness check\",\n \"GET /admin/login\": \"Admin login page\",\n \"GET /admin/dashboard\": \"Admin dashboard\",\n },\n \"docs\": \"https://github.com/bchhngsaygez/deepwhale\"\n }), 404\n\nif __name__ == \"__main__\":\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n\n print(\"=\" * 50)\n print(\"DeepWhale (Flask) - OpenAI Compatible DeepSeek Bridge\")\n print(\"=\" * 50)\n print(f\"Endpoint: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/chat/completions\")\n print(f\"Models: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/models\")\n print(f\"API Key: {api_key}\")\n print(\"=\" * 50)\n print(f\"Admin: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/admin/login\")\n admin_pw = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n print(f\"Admin PW: {admin_pw}\")\n print(\"=\" * 50)\n print(\"[info] Starting browser and logging into DeepSeek automatically in the background...\")\n threading.Thread(target=get_active_token, daemon=True).start()\n print(\"=\" * 50)\n\n app.run(\n host=host,\n port=port,\n threaded=True,\n debug=False,\n )\n"}, "7194cd5eaf9590b275b32383a2554a0e70ae3b90": {"t": 1785765298.1133678, "n": 31609, "h": "f29e11df5c5c6ff0", "c": "import sys\nimport os\n\nfrom utils import setup_utf8, load_env, get_secret_key\n\nsetup_utf8()\nload_env()\n\nimport json\nimport time\nimport uuid\nimport threading\nimport io\nimport re\nimport secrets\nfrom collections import deque\nfrom flask import Flask, request, Response, jsonify, session, render_template, redirect, url_for, stream_with_context\n\nfrom deepseek_client import (\n login, create_session, get_pow,\n call_completion, call_continue,\n delete_session, parse_sse_lines,\n collect_response, make_session, get_model_type,\n)\n\nAPI_KEYS = []\nAPI_KEYS_LOCK = threading.RLock()\n\ndef load_api_keys():\n global API_KEYS\n with API_KEYS_LOCK:\n keys_json = os.environ.get(\"API_KEYS_JSON\", \"\")\n if keys_json:\n try:\n parsed = json.loads(keys_json)\n if isinstance(parsed, list) and len(parsed) > 0:\n API_KEYS = parsed\n return\n except json.JSONDecodeError:\n pass\n legacy = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n if legacy:\n API_KEYS = [{\"id\": \"default\", \"name\": \"Default\", \"key\": legacy, \"enabled\": True}]\n _persist_api_keys_mem()\n\ndef _persist_api_keys_mem():\n with API_KEYS_LOCK:\n os.environ[\"API_KEYS_JSON\"] = json.dumps(API_KEYS)\n if API_KEYS:\n os.environ[\"API_KEY\"] = API_KEYS[0][\"key\"]\n\nload_api_keys()\n\nACCOUNTS = []\naccounts_env = os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\")\nif accounts_env:\n for acc_str in accounts_env.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\n \"email\": parts[0].strip(),\n \"password\": parts[1].strip(),\n \"token\": None\n })\n\nif not ACCOUNTS:\n email = os.environ.get(\"DEEPSEEK_EMAIL\", \"\").strip()\n password = os.environ.get(\"DEEPSEEK_PASSWORD\", \"\").strip()\n if not email or not password:\n raise ValueError(\"ERROR: DEEPSEEK_EMAIL or DEEPSEEK_PASSWORD not configured in .env file!\")\n ACCOUNTS.append({\n \"email\": email,\n \"password\": password,\n \"token\": None,\n })\n\nAVAILABLE_MODELS = [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n \"deepseek-r1\",\n \"deepseek-v3\",\n]\n\nMODEL_ALIASES = {}\n\n_account_lock = threading.Lock()\n_current_account_index = 0\n\ndef get_active_token() -> str:\n global _current_account_index\n with _account_lock:\n if not ACCOUNTS:\n raise RuntimeError(\"No DeepSeek accounts configured!\")\n\n for _ in range(len(ACCOUNTS)):\n acc = ACCOUNTS[_current_account_index]\n if not acc.get(\"token\"):\n try:\n print(f\"[auth] Logging in account #{_current_account_index + 1}: {acc.get('email')}\")\n token = login(\n email=acc.get(\"email\"),\n password=acc.get(\"password\")\n )\n acc[\"token\"] = token\n print(f\"[auth] Login OK for account #{_current_account_index + 1}: {token[:20]}...\")\n except Exception as e:\n print(f\"[auth] Account #{_current_account_index + 1} ({acc.get('email')}) login failed: {e}\")\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n continue\n\n token = acc[\"token\"]\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n return token\n\n raise RuntimeError(\"All configured DeepSeek accounts failed to log in!\")\n\ndef invalidate_token(token: str = None):\n with _account_lock:\n if token:\n for acc in ACCOUNTS:\n if acc.get(\"token\") == token:\n print(f\"[auth] Invalidating token for account: {acc.get('email')}\")\n acc[\"token\"] = None\n break\n else:\n for acc in ACCOUNTS:\n acc[\"token\"] = None\n\nLOG_BUFFER = deque(maxlen=2000)\nLOG_LOCK = threading.Lock()\n\nclass LogCapture(io.StringIO):\n def __init__(self, original):\n super().__init__()\n self._original = original\n def write(self, msg):\n if msg.strip():\n ts = time.strftime(\"%H:%M:%S\", time.localtime())\n with LOG_LOCK:\n LOG_BUFFER.append((ts, msg.rstrip()))\n self._original.write(msg)\n def flush(self):\n self._original.flush()\n\n_original_stdout = sys.stdout\nsys.stdout = LogCapture(_original_stdout)\n_original_stderr = sys.stderr\nsys.stderr = LogCapture(_original_stderr)\n\nCHAT_SESSIONS = {}\nCHAT_LOCK = threading.Lock()\n\ndef get_chat_session(chat_id: str):\n with CHAT_LOCK:\n if chat_id not in CHAT_SESSIONS:\n token = get_active_token()\n sess = make_session()\n sid = create_session(token, session=sess)\n CHAT_SESSIONS[chat_id] = {\n \"session_id\": sid,\n \"messages\": [],\n \"token\": token,\n \"http_session\": sess,\n \"created_at\": time.time(),\n }\n return CHAT_SESSIONS[chat_id]\n\napp = Flask(__name__)\napp.secret_key = get_secret_key()\napp.config['PERMANENT_SESSION_LIFETIME'] = 86400\n\nADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n\ndef require_admin():\n if not session.get(\"admin_authenticated\"):\n return redirect(url_for(\"admin_login_page\"))\n return None\n\nENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), \".env\")\n\ndef save_env_file(updates: dict):\n lines = []\n if os.path.exists(ENV_PATH):\n with open(ENV_PATH, \"r\", encoding=\"utf-8\") as f:\n lines = f.readlines()\n remaining = dict(updates)\n new_lines = []\n for line in lines:\n stripped = line.strip()\n if not stripped or stripped.startswith(\"#\"):\n new_lines.append(line)\n continue\n if \"=\" in stripped:\n key = stripped.split(\"=\", 1)[0].strip()\n if key in remaining:\n val = remaining.pop(key)\n new_lines.append(f'{key}=\"{val}\"\\n')\n continue\n new_lines.append(line)\n for key, val in remaining.items():\n new_lines.append(f'{key}=\"{val}\"\\n')\n with open(ENV_PATH, \"w\", encoding=\"utf-8\") as f:\n f.writelines(new_lines)\n for key, val in updates.items():\n os.environ[key] = val\n\n@app.after_request\ndef after_request(response):\n response.headers.add('Access-Control-Allow-Origin', '*')\n response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')\n response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')\n return response\n\n\ndef get_caller_key():\n auth = request.headers.get(\"Authorization\", \"\")\n if auth.lower().startswith(\"bearer \"):\n key = auth[7:].strip()\n if key:\n return key\n key = request.headers.get(\"X-Api-Key\", \"\").strip()\n if key:\n return key\n return None\n\ndef require_auth():\n key = get_caller_key()\n if not key:\n return jsonify({\"error\": {\"message\": \"Missing API key\", \"type\": \"invalid_request_error\"}}), 401\n with API_KEYS_LOCK:\n valid = any(k[\"key\"] == key and k.get(\"enabled\", True) for k in API_KEYS)\n if not valid:\n return jsonify({\"error\": {\"message\": \"Invalid or disabled API key\", \"type\": \"invalid_request_error\"}}), 401\n return None\n\n\ndef build_prompt(messages: list) -> str:\n parts = []\n for msg in messages:\n role = msg.get(\"role\", \"user\")\n content = msg.get(\"content\", \"\")\n\n if isinstance(content, list):\n texts = [\n item.get(\"text\", \"\")\n for item in content\n if isinstance(item, dict) and item.get(\"type\") == \"text\"\n ]\n content = \"\\n\".join(texts)\n elif not isinstance(content, str):\n content = str(content)\n\n if role == \"system\":\n parts.append(f\"\\n{content}\\n\")\n elif role == \"user\":\n parts.append(f\"Human: {content}\")\n elif role == \"assistant\":\n parts.append(f\"Assistant: {content}\")\n\n parts.append(\"Assistant:\")\n return \"\\n\\n\".join(parts)\n\ndef resolve_model(model: str) -> str:\n return MODEL_ALIASES.get(model.strip().lower(), model.strip())\n\n\ndef make_chunk(completion_id: str, model: str, delta: dict,\n finish_reason=None) -> str:\n obj = {\n \"id\": completion_id,\n \"object\": \"chat.completion.chunk\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"delta\": delta,\n \"finish_reason\": finish_reason,\n }],\n }\n return f\"data: {json.dumps(obj)}\\n\\n\"\n\n\ndef stream_generator(token: str, prompt: str, model: str,\n thinking_enabled: bool, completion_id: str):\n sess = make_session()\n yield make_chunk(completion_id, model, {\"role\": \"assistant\", \"content\": \"\"})\n\n session_id = None\n msg_id = 0\n last_status = \"\"\n\n try:\n session_id = create_session(token, session=sess)\n pow_resp = get_pow(token, session=sess)\n\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled,\n pow_response=pow_resp, http_session=sess,\n )\n\n def consume(lines_gen):\n nonlocal msg_id, last_status\n for chunk in parse_sse_lines(lines_gen):\n if chunk.get(\"response_message_id\"):\n msg_id = int(chunk[\"response_message_id\"])\n\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n\n if \"status\" in p and isinstance(v, str):\n last_status = v\n if \"auto_continue\" in p and v is True:\n last_status = \"AUTO_CONTINUE\"\n\n if isinstance(v, str) and \"content\" in p:\n yield make_chunk(completion_id, model, {\"content\": v})\n\n yield from consume(lines)\n\n for rnd in range(8):\n if last_status.upper() not in (\"INCOMPLETE\", \"AUTO_CONTINUE\"):\n break\n if msg_id <= 0:\n break\n print(f\"[auto_continue] round {rnd+1}, msg_id={msg_id}\")\n pow2 = get_pow(token, session=sess)\n cont = call_continue(token, session_id, msg_id,\n pow_response=pow2, http_session=sess)\n last_status = \"\"\n yield from consume(cont)\n\n yield make_chunk(completion_id, model, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n\n except Exception as e:\n invalidate_token(token)\n err = {\"error\": {\"type\": \"api_error\", \"message\": str(e)}}\n yield f\"data: {json.dumps(err)}\\n\\n\"\n\n\n@app.get(\"/healthz\")\n@app.get(\"/readyz\")\ndef health():\n return jsonify({\"status\": \"ok\"})\n\n\n@app.get(\"/v1/models\")\n@app.get(\"/models\")\ndef list_models():\n err = require_auth()\n if err:\n return err\n data = [\n {\"id\": m, \"object\": \"model\", \"created\": 1700000000, \"owned_by\": \"deepseek\"}\n for m in AVAILABLE_MODELS\n ]\n return jsonify({\"object\": \"list\", \"data\": data})\n\n\n@app.get(\"/v1/chat/completions\")\n@app.get(\"/chat/completions\")\ndef chat_completions_info():\n return jsonify({\n \"message\": \"This endpoint requires a POST request with a JSON body.\",\n \"usage\": 'curl -X POST http://localhost:5001/v1/chat/completions -H \"Authorization: Bearer YOUR_API_KEY\" -H \"Content-Type: application/json\" -d \\'{\"model\":\"deepseek-v4-flash\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}\\'',\n \"endpoints\": {\n \"/v1/chat/completions\": \"POST - Chat completion\",\n \"/v1/models\": \"GET - List models\",\n \"/healthz\": \"GET - Health check\",\n }\n })\n\n@app.post(\"/v1/chat/completions\")\n@app.post(\"/chat/completions\")\ndef chat_completions():\n err = require_auth()\n if err:\n return err\n\n body = request.get_json(force=True, silent=True) or {}\n model = resolve_model(body.get(\"model\", \"deepseek-v4-flash\"))\n msgs = body.get(\"messages\", [])\n stream = bool(body.get(\"stream\", False))\n thinking_flag = body.get(\"thinking\", None)\n\n if not msgs:\n return jsonify({\"error\": {\"message\": \"messages required\"}}), 400\n\n prompt = build_prompt(msgs)\n\n thinking_enabled = bool(thinking_flag) if thinking_flag is not None \\\n else (get_model_type(model) == \"reasoner\")\n\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": {\"message\": f\"Auth failed: {e}\"}}), 500\n\n if stream:\n return Response(\n stream_generator(token, prompt, model, thinking_enabled, completion_id),\n mimetype=\"text/event-stream\",\n headers={\n \"Cache-Control\": \"no-cache\",\n \"X-Accel-Buffering\": \"no\",\n \"Connection\": \"keep-alive\",\n },\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": {\"message\": str(e)}}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n\n resp = {\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n }\n if result.get(\"thinking\"):\n resp[\"choices\"][0][\"message\"][\"thinking\"] = result[\"thinking\"]\n\n return jsonify(resp)\n\n\n@app.route(\"/admin/login\", methods=[\"GET\"])\ndef admin_login_page():\n return render_template(\"login.html\")\n\n@app.route(\"/admin/login\", methods=[\"POST\"])\ndef admin_login():\n data = request.get_json(force=True, silent=True) or {}\n pw = data.get(\"password\", \"\")\n if pw == ADMIN_PASSWORD:\n session[\"admin_authenticated\"] = True\n session.permanent = True\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Invalid password\"}), 401\n\n@app.route(\"/admin/logout\")\ndef admin_logout():\n session.pop(\"admin_authenticated\", None)\n return redirect(url_for(\"admin_login_page\"))\n\n@app.route(\"/admin/dashboard\")\ndef admin_dashboard():\n err = require_admin()\n if err:\n return err\n return render_template(\"dashboard.html\")\n\n@app.route(\"/admin/api/config\", methods=[\"GET\"])\ndef admin_get_config():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe_keys = [{\"id\": k[\"id\"], \"name\": k[\"name\"], \"key\": k[\"key\"], \"enabled\": k.get(\"enabled\", True)} for k in API_KEYS]\n return jsonify({\n \"api_key\": os.environ.get(\"API_KEY\", \"\"),\n \"api_keys\": safe_keys,\n \"port\": os.environ.get(\"PORT\", \"5001\"),\n \"host\": os.environ.get(\"HOST\", \"0.0.0.0\"),\n \"email\": os.environ.get(\"DEEPSEEK_EMAIL\", \"\"),\n \"password\": os.environ.get(\"DEEPSEEK_PASSWORD\", \"\"),\n \"accounts_raw\": os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\"),\n })\n\n@app.route(\"/admin/api/config\", methods=[\"POST\"])\ndef admin_save_config():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n updates = {}\n global ADMIN_PASSWORD\n if \"admin_password\" in data and data[\"admin_password\"].strip():\n updates[\"ADMIN_PASSWORD\"] = data[\"admin_password\"].strip()\n ADMIN_PASSWORD = data[\"admin_password\"].strip()\n if \"api_key\" in data and data[\"api_key\"].strip():\n updates[\"API_KEY\"] = data[\"api_key\"].strip()\n with API_KEYS_LOCK:\n found = False\n for k in API_KEYS:\n if k[\"id\"] == \"default\":\n k[\"key\"] = data[\"api_key\"].strip()\n found = True\n break\n if not found:\n API_KEYS.insert(0, {\"id\": \"default\", \"name\": \"Default\", \"key\": data[\"api_key\"].strip(), \"enabled\": True})\n _persist_api_keys_mem()\n if \"port\" in data and str(data[\"port\"]).strip():\n updates[\"PORT\"] = str(data[\"port\"]).strip()\n if \"host\" in data and data[\"host\"].strip():\n updates[\"HOST\"] = data[\"host\"].strip()\n if \"email\" in data and data[\"email\"].strip():\n updates[\"DEEPSEEK_EMAIL\"] = data[\"email\"].strip()\n if \"password\" in data and data[\"password\"].strip():\n updates[\"DEEPSEEK_PASSWORD\"] = data[\"password\"].strip()\n if \"accounts\" in data:\n val = data[\"accounts\"].strip()\n updates[\"DEEPSEEK_ACCOUNTS\"] = val\n ACCOUNTS.clear()\n if val:\n for acc_str in val.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\"email\": parts[0].strip(), \"password\": parts[1].strip(), \"token\": None})\n if updates:\n save_env_file(updates)\n return jsonify({\"message\": \"Configuration saved\"})\n return jsonify({\"error\": \"No changes provided\"}), 400\n\n@app.route(\"/admin/api/api-keys\", methods=[\"GET\"])\ndef admin_get_api_keys():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe = [\n {\"id\": k[\"id\"], \"name\": k[\"name\"], \"key_preview\": k[\"key\"][:12] + \"...\", \"enabled\": k.get(\"enabled\", True)}\n for k in API_KEYS\n ]\n return jsonify({\"keys\": safe})\n\n@app.route(\"/admin/api/api-keys\", methods=[\"POST\"])\ndef admin_add_api_key():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n name = data.get(\"name\", \"\").strip()\n if not name:\n return jsonify({\"error\": \"Key name is required\"}), 400\n custom_key = data.get(\"key\", \"\").strip()\n with API_KEYS_LOCK:\n if any(k[\"name\"] == name for k in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{name}' already exists\"}), 400\n if custom_key:\n if any(k[\"key\"] == custom_key for k in API_KEYS):\n return jsonify({\"error\": \"An API key with this value already exists\"}), 400\n if len(custom_key) < 6:\n return jsonify({\"error\": \"API key must be at least 6 characters\"}), 400\n new_key = custom_key or (\"sk-\" + secrets.token_hex(24))\n entry = {\"id\": secrets.token_hex(8), \"name\": name, \"key\": new_key, \"enabled\": True}\n API_KEYS.append(entry)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True, \"key\": {\"id\": entry[\"id\"], \"name\": entry[\"name\"], \"key\": entry[\"key\"], \"enabled\": True}})\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"PATCH\"])\ndef admin_update_api_key(key_id):\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n with API_KEYS_LOCK:\n for k in API_KEYS:\n if k[\"id\"] == key_id:\n if \"name\" in data and data[\"name\"].strip():\n new_name = data[\"name\"].strip()\n if any(kk[\"name\"] == new_name and kk[\"id\"] != key_id for kk in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{new_name}' already exists\"}), 400\n k[\"name\"] = new_name\n if \"enabled\" in data:\n k[\"enabled\"] = bool(data[\"enabled\"])\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"DELETE\"])\ndef admin_delete_api_key(key_id):\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n for i, k in enumerate(API_KEYS):\n if k[\"id\"] == key_id:\n API_KEYS.pop(i)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/test\", methods=[\"POST\"])\ndef admin_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n prompt_text = data.get(\"prompt\", \"Hello!\")\n use_stream = bool(data.get(\"stream\", False))\n model = \"deepseek-v4-flash\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": f\"Auth failed: {e}\"}), 500\n\n prompt = f\"Human: {prompt_text}\\n\\nAssistant:\"\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n if use_stream:\n def gen():\n yield from stream_generator(token, prompt, model, False, completion_id)\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=False, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": str(e)}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n return jsonify({\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n })\n\n\n@app.route(\"/admin/api/accounts-status\")\ndef admin_accounts_status():\n err = require_admin()\n if err:\n return err\n with _account_lock:\n accounts_info = []\n for acc in ACCOUNTS:\n accounts_info.append({\n \"email\": acc.get(\"email\", \"\"),\n \"has_token\": acc.get(\"token\") is not None,\n \"token_prefix\": (acc.get(\"token\") or \"\")[:12] + \"...\" if acc.get(\"token\") else None,\n })\n return jsonify({\"accounts\": accounts_info, \"count\": len(accounts_info)})\n\n\n@app.route(\"/admin/api/chat\", methods=[\"POST\"])\ndef admin_chat():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n message = data.get(\"message\", \"\").strip()\n model = data.get(\"model\", \"deepseek-v4-flash\")\n use_stream = bool(data.get(\"stream\", False))\n thinking = bool(data.get(\"thinking\", False))\n context_messages = data.get(\"context_messages\")\n if context_messages is not None:\n context_messages = int(context_messages)\n\n if not message:\n return jsonify({\"error\": \"Message is required\"}), 400\n\n try:\n chat = get_chat_session(chat_id)\n chat[\"messages\"].append({\"role\": \"user\", \"content\": message})\n msgs = chat[\"messages\"]\n if context_messages is not None and context_messages >= 0:\n msgs = msgs[-context_messages:] if context_messages > 0 else [msgs[-1]]\n prompt = build_prompt(msgs)\n token = chat[\"token\"]\n resolved = resolve_model(model)\n\n if use_stream:\n def gen(token=token):\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n yield make_chunk(completion_id, resolved, {\"role\": \"assistant\", \"content\": \"\"})\n session_id = chat[\"session_id\"]\n try:\n pow_resp = get_pow(token, session=chat[\"http_session\"])\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=resolved, thinking=thinking,\n pow_response=pow_resp, http_session=chat[\"http_session\"],\n )\n full_text = \"\"\n for chunk in parse_sse_lines(lines):\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n if isinstance(v, str) and \"content\" in p:\n full_text += v\n yield make_chunk(completion_id, resolved, {\"content\": v})\n yield make_chunk(completion_id, resolved, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": full_text})\n except Exception as e:\n yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n result = collect_response(\n token=token, session_id=chat[\"session_id\"], prompt=prompt,\n model=resolved, thinking=thinking, http_session=chat[\"http_session\"],\n )\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")})\n return jsonify({\n \"text\": result.get(\"text\", \"\"),\n \"thinking\": result.get(\"thinking\", \"\"),\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n@app.route(\"/admin/api/chat/reset\", methods=[\"POST\"])\ndef admin_chat_reset():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n with CHAT_LOCK:\n if chat_id in CHAT_SESSIONS:\n try:\n delete_session(CHAT_SESSIONS[chat_id][\"token\"], CHAT_SESSIONS[chat_id][\"session_id\"])\n except:\n pass\n del CHAT_SESSIONS[chat_id]\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/logs\")\ndef admin_get_logs():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n return jsonify({\"logs\": list(LOG_BUFFER)})\n\n@app.route(\"/admin/api/logs/stream\")\ndef admin_log_stream():\n err = require_admin()\n if err:\n return err\n def generate():\n last_count = 0\n while True:\n with LOG_LOCK:\n current = list(LOG_BUFFER)\n if len(current) > last_count:\n new_entries = current[last_count:]\n last_count = len(current)\n for ts, msg in new_entries:\n yield f\"data: {json.dumps({'ts': ts, 'msg': msg})}\\n\\n\"\n time.sleep(0.5)\n return Response(\n stream_with_context(generate()),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"Connection\": \"keep-alive\"},\n )\n\n@app.route(\"/admin/api/logs/clear\", methods=[\"POST\"])\ndef admin_log_clear():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n LOG_BUFFER.clear()\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/curl-test\", methods=[\"POST\"])\ndef admin_curl_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n method = data.get(\"method\", \"GET\").upper()\n path = data.get(\"path\", \"/v1/models\")\n headers = data.get(\"headers\", {})\n body = data.get(\"body\", \"\")\n\n test_headers = {}\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n for k, v in headers.items():\n test_headers[k] = v\n if \"Authorization\" not in test_headers and \"authorization\" not in {k.lower() for k in test_headers}:\n test_headers[\"Authorization\"] = f\"Bearer {api_key}\"\n\n from urllib.parse import urlparse\n parsed = urlparse(path)\n if parsed.netloc:\n full_url = path\n else:\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n full_url = f\"http://localhost:{port}{path}\"\n\n import http.client\n import urllib.parse\n\n try:\n parsed = urllib.parse.urlparse(full_url)\n conn = http.client.HTTPConnection(parsed.netloc, timeout=30)\n conn.request(method, parsed.path + (\"?\" + parsed.query if parsed.query else \"\"),\n body=body if body else None,\n headers=test_headers)\n resp = conn.getresponse()\n resp_body = resp.read().decode(\"utf-8\", errors=\"replace\")\n if len(resp_body) > 10000:\n resp_body = resp_body[:10000] + \"\\n... (truncated)\"\n return jsonify({\n \"status\": resp.status,\n \"reason\": resp.reason,\n \"headers\": dict(resp.getheaders()),\n \"body\": resp_body,\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n\n@app.errorhandler(404)\ndef not_found(e):\n return jsonify({\n \"message\": \"Endpoint not found. Here are the available endpoints:\",\n \"endpoints\": {\n \"GET /v1/models\": \"List available models\",\n \"POST /v1/chat/completions\": \"Chat completion (OpenAI-compatible)\",\n \"GET /healthz\": \"Health check\",\n \"GET /readyz\": \"Readiness check\",\n \"GET /admin/login\": \"Admin login page\",\n \"GET /admin/dashboard\": \"Admin dashboard\",\n },\n \"docs\": \"https://github.com/bchhngsaygez/deepwhale\"\n }), 404\n\nif __name__ == \"__main__\":\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n\n print(\"=\" * 50)\n print(\"DeepWhale (Flask) - OpenAI Compatible DeepSeek Bridge\")\n print(\"=\" * 50)\n print(f\"Endpoint: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/chat/completions\")\n print(f\"Models: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/models\")\n print(f\"API Key: {api_key}\")\n print(\"=\" * 50)\n print(f\"Admin: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/admin/login\")\n admin_pw = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n print(f\"Admin PW: {admin_pw}\")\n print(\"=\" * 50)\n print(\"[info] Starting browser and logging into DeepSeek automatically in the background...\")\n threading.Thread(target=get_active_token, daemon=True).start()\n print(\"=\" * 50)\n\n app.run(\n host=host,\n port=port,\n threaded=True,\n debug=False,\n )\n"}, "8dca05e110a3166c7c36ca0c6e91dbbd7f8a1b8d": {"t": 1785765299.688539, "n": 30410, "h": "39b243a152ac27e5", "c": "import sys\nimport os\n\nfrom utils import setup_utf8, load_env, get_secret_key\n\nsetup_utf8()\nload_env()\n\nimport json\nimport time\nimport uuid\nimport threading\nimport io\nimport re\nimport secrets\nfrom collections import deque\nfrom flask import Flask, request, Response, jsonify, session, render_template, redirect, url_for, stream_with_context\n\nfrom deepseek_client import (\n login, create_session, get_pow,\n call_completion, call_continue,\n delete_session, parse_sse_lines,\n collect_response, make_session, get_model_type,\n)\n\nAPI_KEYS = []\nAPI_KEYS_LOCK = threading.RLock()\n\ndef load_api_keys():\n global API_KEYS\n with API_KEYS_LOCK:\n keys_json = os.environ.get(\"API_KEYS_JSON\", \"\")\n if keys_json:\n try:\n parsed = json.loads(keys_json)\n if isinstance(parsed, list) and len(parsed) > 0:\n API_KEYS = parsed\n return\n except json.JSONDecodeError:\n pass\n legacy = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n if legacy:\n API_KEYS = [{\"id\": \"default\", \"name\": \"Default\", \"key\": legacy, \"enabled\": True}]\n _persist_api_keys_mem()\n\ndef _persist_api_keys_mem():\n with API_KEYS_LOCK:\n os.environ[\"API_KEYS_JSON\"] = json.dumps(API_KEYS)\n if API_KEYS:\n os.environ[\"API_KEY\"] = API_KEYS[0][\"key\"]\n\nload_api_keys()\n\nACCOUNTS = []\naccounts_env = os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\")\nif accounts_env:\n for acc_str in accounts_env.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\n \"email\": parts[0].strip(),\n \"password\": parts[1].strip(),\n \"token\": None\n })\n\nif not ACCOUNTS:\n email = os.environ.get(\"DEEPSEEK_EMAIL\", \"\").strip()\n password = os.environ.get(\"DEEPSEEK_PASSWORD\", \"\").strip()\n if not email or not password:\n raise ValueError(\"ERROR: DEEPSEEK_EMAIL or DEEPSEEK_PASSWORD not configured in .env file!\")\n ACCOUNTS.append({\n \"email\": email,\n \"password\": password,\n \"token\": None,\n })\n\nAVAILABLE_MODELS = [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\",\n \"deepseek-chat\",\n \"deepseek-reasoner\",\n \"deepseek-r1\",\n \"deepseek-v3\",\n]\n\nMODEL_ALIASES = {}\n\n_account_lock = threading.Lock()\n_current_account_index = 0\n\ndef get_active_token() -> str:\n global _current_account_index\n with _account_lock:\n if not ACCOUNTS:\n raise RuntimeError(\"No DeepSeek accounts configured!\")\n\n for _ in range(len(ACCOUNTS)):\n acc = ACCOUNTS[_current_account_index]\n if not acc.get(\"token\"):\n try:\n print(f\"[auth] Logging in account #{_current_account_index + 1}: {acc.get('email')}\")\n token = login(\n email=acc.get(\"email\"),\n password=acc.get(\"password\")\n )\n acc[\"token\"] = token\n print(f\"[auth] Login OK for account #{_current_account_index + 1}: {token[:20]}...\")\n except Exception as e:\n print(f\"[auth] Account #{_current_account_index + 1} ({acc.get('email')}) login failed: {e}\")\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n continue\n\n token = acc[\"token\"]\n _current_account_index = (_current_account_index + 1) % len(ACCOUNTS)\n return token\n\n raise RuntimeError(\"All configured DeepSeek accounts failed to log in!\")\n\ndef invalidate_token(token: str = None):\n with _account_lock:\n if token:\n for acc in ACCOUNTS:\n if acc.get(\"token\") == token:\n print(f\"[auth] Invalidating token for account: {acc.get('email')}\")\n acc[\"token\"] = None\n break\n else:\n for acc in ACCOUNTS:\n acc[\"token\"] = None\n\nLOG_BUFFER = deque(maxlen=2000)\nLOG_LOCK = threading.Lock()\n\nclass LogCapture(io.StringIO):\n def __init__(self, original):\n super().__init__()\n self._original = original\n def write(self, msg):\n if msg.strip():\n ts = time.strftime(\"%H:%M:%S\", time.localtime())\n with LOG_LOCK:\n LOG_BUFFER.append((ts, msg.rstrip()))\n self._original.write(msg)\n def flush(self):\n self._original.flush()\n\n_original_stdout = sys.stdout\nsys.stdout = LogCapture(_original_stdout)\n_original_stderr = sys.stderr\nsys.stderr = LogCapture(_original_stderr)\n\nCHAT_SESSIONS = {}\nCHAT_LOCK = threading.Lock()\n\ndef get_chat_session(chat_id: str):\n with CHAT_LOCK:\n if chat_id not in CHAT_SESSIONS:\n token = get_active_token()\n sess = make_session()\n sid = create_session(token, session=sess)\n CHAT_SESSIONS[chat_id] = {\n \"session_id\": sid,\n \"messages\": [],\n \"token\": token,\n \"http_session\": sess,\n \"created_at\": time.time(),\n }\n return CHAT_SESSIONS[chat_id]\n\napp = Flask(__name__)\napp.secret_key = get_secret_key()\napp.config['PERMANENT_SESSION_LIFETIME'] = 86400\n\nADMIN_PASSWORD = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n\ndef require_admin():\n if not session.get(\"admin_authenticated\"):\n return redirect(url_for(\"admin_login_page\"))\n return None\n\nENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), \".env\")\n\ndef save_env_file(updates: dict):\n lines = []\n if os.path.exists(ENV_PATH):\n with open(ENV_PATH, \"r\", encoding=\"utf-8\") as f:\n lines = f.readlines()\n remaining = dict(updates)\n new_lines = []\n for line in lines:\n stripped = line.strip()\n if not stripped or stripped.startswith(\"#\"):\n new_lines.append(line)\n continue\n if \"=\" in stripped:\n key = stripped.split(\"=\", 1)[0].strip()\n if key in remaining:\n val = remaining.pop(key)\n new_lines.append(f'{key}=\"{val}\"\\n')\n continue\n new_lines.append(line)\n for key, val in remaining.items():\n new_lines.append(f'{key}=\"{val}\"\\n')\n with open(ENV_PATH, \"w\", encoding=\"utf-8\") as f:\n f.writelines(new_lines)\n for key, val in updates.items():\n os.environ[key] = val\n\n@app.after_request\ndef after_request(response):\n response.headers.add('Access-Control-Allow-Origin', '*')\n response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')\n response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')\n return response\n\n\ndef get_caller_key():\n auth = request.headers.get(\"Authorization\", \"\")\n if auth.lower().startswith(\"bearer \"):\n key = auth[7:].strip()\n if key:\n return key\n key = request.headers.get(\"X-Api-Key\", \"\").strip()\n if key:\n return key\n return None\n\ndef require_auth():\n key = get_caller_key()\n if not key:\n return jsonify({\"error\": {\"message\": \"Missing API key\", \"type\": \"invalid_request_error\"}}), 401\n with API_KEYS_LOCK:\n valid = any(k[\"key\"] == key and k.get(\"enabled\", True) for k in API_KEYS)\n if not valid:\n return jsonify({\"error\": {\"message\": \"Invalid or disabled API key\", \"type\": \"invalid_request_error\"}}), 401\n return None\n\n\ndef build_prompt(messages: list) -> str:\n parts = []\n for msg in messages:\n role = msg.get(\"role\", \"user\")\n content = msg.get(\"content\", \"\")\n\n if isinstance(content, list):\n texts = [\n item.get(\"text\", \"\")\n for item in content\n if isinstance(item, dict) and item.get(\"type\") == \"text\"\n ]\n content = \"\\n\".join(texts)\n elif not isinstance(content, str):\n content = str(content)\n\n if role == \"system\":\n parts.append(f\"\\n{content}\\n\")\n elif role == \"user\":\n parts.append(f\"Human: {content}\")\n elif role == \"assistant\":\n parts.append(f\"Assistant: {content}\")\n\n parts.append(\"Assistant:\")\n return \"\\n\\n\".join(parts)\n\ndef resolve_model(model: str) -> str:\n return MODEL_ALIASES.get(model.strip().lower(), model.strip())\n\n\ndef make_chunk(completion_id: str, model: str, delta: dict,\n finish_reason=None) -> str:\n obj = {\n \"id\": completion_id,\n \"object\": \"chat.completion.chunk\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"delta\": delta,\n \"finish_reason\": finish_reason,\n }],\n }\n return f\"data: {json.dumps(obj)}\\n\\n\"\n\n\ndef stream_generator(token: str, prompt: str, model: str,\n thinking_enabled: bool, completion_id: str):\n sess = make_session()\n yield make_chunk(completion_id, model, {\"role\": \"assistant\", \"content\": \"\"})\n\n session_id = None\n msg_id = 0\n last_status = \"\"\n\n try:\n session_id = create_session(token, session=sess)\n pow_resp = get_pow(token, session=sess)\n\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled,\n pow_response=pow_resp, http_session=sess,\n )\n\n def consume(lines_gen):\n nonlocal msg_id, last_status\n for chunk in parse_sse_lines(lines_gen):\n if chunk.get(\"response_message_id\"):\n msg_id = int(chunk[\"response_message_id\"])\n\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n\n if \"status\" in p and isinstance(v, str):\n last_status = v\n if \"auto_continue\" in p and v is True:\n last_status = \"AUTO_CONTINUE\"\n\n if isinstance(v, str) and \"content\" in p:\n yield make_chunk(completion_id, model, {\"content\": v})\n\n yield from consume(lines)\n\n for rnd in range(8):\n if last_status.upper() not in (\"INCOMPLETE\", \"AUTO_CONTINUE\"):\n break\n if msg_id <= 0:\n break\n print(f\"[auto_continue] round {rnd+1}, msg_id={msg_id}\")\n pow2 = get_pow(token, session=sess)\n cont = call_continue(token, session_id, msg_id,\n pow_response=pow2, http_session=sess)\n last_status = \"\"\n yield from consume(cont)\n\n yield make_chunk(completion_id, model, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n\n except Exception as e:\n invalidate_token(token)\n err = {\"error\": {\"type\": \"api_error\", \"message\": str(e)}}\n yield f\"data: {json.dumps(err)}\\n\\n\"\n\n\n@app.get(\"/healthz\")\n@app.get(\"/readyz\")\ndef health():\n return jsonify({\"status\": \"ok\"})\n\n\n@app.get(\"/v1/models\")\n@app.get(\"/models\")\ndef list_models():\n err = require_auth()\n if err:\n return err\n data = [\n {\"id\": m, \"object\": \"model\", \"created\": 1700000000, \"owned_by\": \"deepseek\"}\n for m in AVAILABLE_MODELS\n ]\n return jsonify({\"object\": \"list\", \"data\": data})\n\n\n@app.post(\"/v1/chat/completions\")\n@app.post(\"/chat/completions\")\ndef chat_completions():\n err = require_auth()\n if err:\n return err\n\n body = request.get_json(force=True, silent=True) or {}\n model = resolve_model(body.get(\"model\", \"deepseek-v4-flash\"))\n msgs = body.get(\"messages\", [])\n stream = bool(body.get(\"stream\", False))\n thinking_flag = body.get(\"thinking\", None)\n\n if not msgs:\n return jsonify({\"error\": {\"message\": \"messages required\"}}), 400\n\n prompt = build_prompt(msgs)\n\n thinking_enabled = bool(thinking_flag) if thinking_flag is not None \\\n else (get_model_type(model) == \"reasoner\")\n\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": {\"message\": f\"Auth failed: {e}\"}}), 500\n\n if stream:\n return Response(\n stream_generator(token, prompt, model, thinking_enabled, completion_id),\n mimetype=\"text/event-stream\",\n headers={\n \"Cache-Control\": \"no-cache\",\n \"X-Accel-Buffering\": \"no\",\n \"Connection\": \"keep-alive\",\n },\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=thinking_enabled, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": {\"message\": str(e)}}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n\n resp = {\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n }\n if result.get(\"thinking\"):\n resp[\"choices\"][0][\"message\"][\"thinking\"] = result[\"thinking\"]\n\n return jsonify(resp)\n\n\n@app.route(\"/admin/login\", methods=[\"GET\"])\ndef admin_login_page():\n return render_template(\"login.html\")\n\n@app.route(\"/admin/login\", methods=[\"POST\"])\ndef admin_login():\n data = request.get_json(force=True, silent=True) or {}\n pw = data.get(\"password\", \"\")\n if pw == ADMIN_PASSWORD:\n session[\"admin_authenticated\"] = True\n session.permanent = True\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Invalid password\"}), 401\n\n@app.route(\"/admin/logout\")\ndef admin_logout():\n session.pop(\"admin_authenticated\", None)\n return redirect(url_for(\"admin_login_page\"))\n\n@app.route(\"/admin/dashboard\")\ndef admin_dashboard():\n err = require_admin()\n if err:\n return err\n return render_template(\"dashboard.html\")\n\n@app.route(\"/admin/api/config\", methods=[\"GET\"])\ndef admin_get_config():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe_keys = [{\"id\": k[\"id\"], \"name\": k[\"name\"], \"key\": k[\"key\"], \"enabled\": k.get(\"enabled\", True)} for k in API_KEYS]\n return jsonify({\n \"api_key\": os.environ.get(\"API_KEY\", \"\"),\n \"api_keys\": safe_keys,\n \"port\": os.environ.get(\"PORT\", \"5001\"),\n \"host\": os.environ.get(\"HOST\", \"0.0.0.0\"),\n \"email\": os.environ.get(\"DEEPSEEK_EMAIL\", \"\"),\n \"password\": os.environ.get(\"DEEPSEEK_PASSWORD\", \"\"),\n \"accounts_raw\": os.environ.get(\"DEEPSEEK_ACCOUNTS\", \"\"),\n })\n\n@app.route(\"/admin/api/config\", methods=[\"POST\"])\ndef admin_save_config():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n updates = {}\n global ADMIN_PASSWORD\n if \"admin_password\" in data and data[\"admin_password\"].strip():\n updates[\"ADMIN_PASSWORD\"] = data[\"admin_password\"].strip()\n ADMIN_PASSWORD = data[\"admin_password\"].strip()\n if \"api_key\" in data and data[\"api_key\"].strip():\n updates[\"API_KEY\"] = data[\"api_key\"].strip()\n with API_KEYS_LOCK:\n found = False\n for k in API_KEYS:\n if k[\"id\"] == \"default\":\n k[\"key\"] = data[\"api_key\"].strip()\n found = True\n break\n if not found:\n API_KEYS.insert(0, {\"id\": \"default\", \"name\": \"Default\", \"key\": data[\"api_key\"].strip(), \"enabled\": True})\n _persist_api_keys_mem()\n if \"port\" in data and str(data[\"port\"]).strip():\n updates[\"PORT\"] = str(data[\"port\"]).strip()\n if \"host\" in data and data[\"host\"].strip():\n updates[\"HOST\"] = data[\"host\"].strip()\n if \"email\" in data and data[\"email\"].strip():\n updates[\"DEEPSEEK_EMAIL\"] = data[\"email\"].strip()\n if \"password\" in data and data[\"password\"].strip():\n updates[\"DEEPSEEK_PASSWORD\"] = data[\"password\"].strip()\n if \"accounts\" in data:\n val = data[\"accounts\"].strip()\n updates[\"DEEPSEEK_ACCOUNTS\"] = val\n ACCOUNTS.clear()\n if val:\n for acc_str in val.split(\",\"):\n acc_str = acc_str.strip()\n if \":\" in acc_str:\n parts = acc_str.split(\":\", 1)\n ACCOUNTS.append({\"email\": parts[0].strip(), \"password\": parts[1].strip(), \"token\": None})\n if updates:\n save_env_file(updates)\n return jsonify({\"message\": \"Configuration saved\"})\n return jsonify({\"error\": \"No changes provided\"}), 400\n\n@app.route(\"/admin/api/api-keys\", methods=[\"GET\"])\ndef admin_get_api_keys():\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n safe = [\n {\"id\": k[\"id\"], \"name\": k[\"name\"], \"key_preview\": k[\"key\"][:12] + \"...\", \"enabled\": k.get(\"enabled\", True)}\n for k in API_KEYS\n ]\n return jsonify({\"keys\": safe})\n\n@app.route(\"/admin/api/api-keys\", methods=[\"POST\"])\ndef admin_add_api_key():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n name = data.get(\"name\", \"\").strip()\n if not name:\n return jsonify({\"error\": \"Key name is required\"}), 400\n custom_key = data.get(\"key\", \"\").strip()\n with API_KEYS_LOCK:\n if any(k[\"name\"] == name for k in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{name}' already exists\"}), 400\n if custom_key:\n if any(k[\"key\"] == custom_key for k in API_KEYS):\n return jsonify({\"error\": \"An API key with this value already exists\"}), 400\n if len(custom_key) < 6:\n return jsonify({\"error\": \"API key must be at least 6 characters\"}), 400\n new_key = custom_key or (\"sk-\" + secrets.token_hex(24))\n entry = {\"id\": secrets.token_hex(8), \"name\": name, \"key\": new_key, \"enabled\": True}\n API_KEYS.append(entry)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True, \"key\": {\"id\": entry[\"id\"], \"name\": entry[\"name\"], \"key\": entry[\"key\"], \"enabled\": True}})\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"PATCH\"])\ndef admin_update_api_key(key_id):\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n with API_KEYS_LOCK:\n for k in API_KEYS:\n if k[\"id\"] == key_id:\n if \"name\" in data and data[\"name\"].strip():\n new_name = data[\"name\"].strip()\n if any(kk[\"name\"] == new_name and kk[\"id\"] != key_id for kk in API_KEYS):\n return jsonify({\"error\": f\"Key with name '{new_name}' already exists\"}), 400\n k[\"name\"] = new_name\n if \"enabled\" in data:\n k[\"enabled\"] = bool(data[\"enabled\"])\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/api-keys/\", methods=[\"DELETE\"])\ndef admin_delete_api_key(key_id):\n err = require_admin()\n if err:\n return err\n with API_KEYS_LOCK:\n for i, k in enumerate(API_KEYS):\n if k[\"id\"] == key_id:\n API_KEYS.pop(i)\n _persist_api_keys_mem()\n save_env_file({\"API_KEYS_JSON\": json.dumps(API_KEYS)})\n return jsonify({\"ok\": True})\n return jsonify({\"error\": \"Key not found\"}), 404\n\n@app.route(\"/admin/api/test\", methods=[\"POST\"])\ndef admin_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n prompt_text = data.get(\"prompt\", \"Hello!\")\n use_stream = bool(data.get(\"stream\", False))\n model = \"deepseek-v4-flash\"\n\n try:\n token = get_active_token()\n except Exception as e:\n return jsonify({\"error\": f\"Auth failed: {e}\"}), 500\n\n prompt = f\"Human: {prompt_text}\\n\\nAssistant:\"\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n\n if use_stream:\n def gen():\n yield from stream_generator(token, prompt, model, False, completion_id)\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n sess = make_session()\n session_id = None\n try:\n session_id = create_session(token, session=sess)\n result = collect_response(\n token=token, session_id=session_id, prompt=prompt,\n model=model, thinking=False, http_session=sess,\n )\n except Exception as e:\n invalidate_token(token)\n return jsonify({\"error\": str(e)}), 500\n\n prompt_tokens = len(prompt) // 4\n completion_tokens = len(result.get(\"text\", \"\")) // 4\n return jsonify({\n \"id\": completion_id,\n \"object\": \"chat.completion\",\n \"created\": int(time.time()),\n \"model\": model,\n \"choices\": [{\n \"index\": 0,\n \"message\": {\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")},\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n }],\n \"usage\": {\n \"prompt_tokens\": prompt_tokens,\n \"completion_tokens\": completion_tokens,\n \"total_tokens\": prompt_tokens + completion_tokens,\n },\n })\n\n\n@app.route(\"/admin/api/accounts-status\")\ndef admin_accounts_status():\n err = require_admin()\n if err:\n return err\n with _account_lock:\n accounts_info = []\n for acc in ACCOUNTS:\n accounts_info.append({\n \"email\": acc.get(\"email\", \"\"),\n \"has_token\": acc.get(\"token\") is not None,\n \"token_prefix\": (acc.get(\"token\") or \"\")[:12] + \"...\" if acc.get(\"token\") else None,\n })\n return jsonify({\"accounts\": accounts_info, \"count\": len(accounts_info)})\n\n\n@app.route(\"/admin/api/chat\", methods=[\"POST\"])\ndef admin_chat():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n message = data.get(\"message\", \"\").strip()\n model = data.get(\"model\", \"deepseek-v4-flash\")\n use_stream = bool(data.get(\"stream\", False))\n thinking = bool(data.get(\"thinking\", False))\n context_messages = data.get(\"context_messages\")\n if context_messages is not None:\n context_messages = int(context_messages)\n\n if not message:\n return jsonify({\"error\": \"Message is required\"}), 400\n\n try:\n chat = get_chat_session(chat_id)\n chat[\"messages\"].append({\"role\": \"user\", \"content\": message})\n msgs = chat[\"messages\"]\n if context_messages is not None and context_messages >= 0:\n msgs = msgs[-context_messages:] if context_messages > 0 else [msgs[-1]]\n prompt = build_prompt(msgs)\n token = chat[\"token\"]\n resolved = resolve_model(model)\n\n if use_stream:\n def gen(token=token):\n completion_id = f\"chatcmpl-{uuid.uuid4().hex[:24]}\"\n yield make_chunk(completion_id, resolved, {\"role\": \"assistant\", \"content\": \"\"})\n session_id = chat[\"session_id\"]\n try:\n pow_resp = get_pow(token, session=chat[\"http_session\"])\n lines = call_completion(\n token=token, session_id=session_id, prompt=prompt,\n model=resolved, thinking=thinking,\n pow_response=pow_resp, http_session=chat[\"http_session\"],\n )\n full_text = \"\"\n for chunk in parse_sse_lines(lines):\n p = chunk.get(\"p\", \"\")\n v = chunk.get(\"v\")\n if isinstance(v, str) and \"content\" in p:\n full_text += v\n yield make_chunk(completion_id, resolved, {\"content\": v})\n yield make_chunk(completion_id, resolved, {}, finish_reason=\"stop\")\n yield \"data: [DONE]\\n\\n\"\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": full_text})\n except Exception as e:\n yield f\"data: {json.dumps({'error': str(e)})}\\n\\n\"\n return Response(\n gen(),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\", \"Connection\": \"keep-alive\"},\n )\n\n result = collect_response(\n token=token, session_id=chat[\"session_id\"], prompt=prompt,\n model=resolved, thinking=thinking, http_session=chat[\"http_session\"],\n )\n chat[\"messages\"].append({\"role\": \"assistant\", \"content\": result.get(\"text\", \"\")})\n return jsonify({\n \"text\": result.get(\"text\", \"\"),\n \"thinking\": result.get(\"thinking\", \"\"),\n \"finish_reason\": result.get(\"finish_reason\", \"stop\"),\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n@app.route(\"/admin/api/chat/reset\", methods=[\"POST\"])\ndef admin_chat_reset():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n chat_id = data.get(\"chat_id\", \"default\")\n with CHAT_LOCK:\n if chat_id in CHAT_SESSIONS:\n try:\n delete_session(CHAT_SESSIONS[chat_id][\"token\"], CHAT_SESSIONS[chat_id][\"session_id\"])\n except:\n pass\n del CHAT_SESSIONS[chat_id]\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/logs\")\ndef admin_get_logs():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n return jsonify({\"logs\": list(LOG_BUFFER)})\n\n@app.route(\"/admin/api/logs/stream\")\ndef admin_log_stream():\n err = require_admin()\n if err:\n return err\n def generate():\n last_count = 0\n while True:\n with LOG_LOCK:\n current = list(LOG_BUFFER)\n if len(current) > last_count:\n new_entries = current[last_count:]\n last_count = len(current)\n for ts, msg in new_entries:\n yield f\"data: {json.dumps({'ts': ts, 'msg': msg})}\\n\\n\"\n time.sleep(0.5)\n return Response(\n stream_with_context(generate()),\n mimetype=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"Connection\": \"keep-alive\"},\n )\n\n@app.route(\"/admin/api/logs/clear\", methods=[\"POST\"])\ndef admin_log_clear():\n err = require_admin()\n if err:\n return err\n with LOG_LOCK:\n LOG_BUFFER.clear()\n return jsonify({\"ok\": True})\n\n\n@app.route(\"/admin/api/curl-test\", methods=[\"POST\"])\ndef admin_curl_test():\n err = require_admin()\n if err:\n return err\n data = request.get_json(force=True, silent=True) or {}\n method = data.get(\"method\", \"GET\").upper()\n path = data.get(\"path\", \"/v1/models\")\n headers = data.get(\"headers\", {})\n body = data.get(\"body\", \"\")\n\n test_headers = {}\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n for k, v in headers.items():\n test_headers[k] = v\n if \"Authorization\" not in test_headers and \"authorization\" not in {k.lower() for k in test_headers}:\n test_headers[\"Authorization\"] = f\"Bearer {api_key}\"\n\n from urllib.parse import urlparse\n parsed = urlparse(path)\n if parsed.netloc:\n full_url = path\n else:\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n full_url = f\"http://localhost:{port}{path}\"\n\n import http.client\n import urllib.parse\n\n try:\n parsed = urllib.parse.urlparse(full_url)\n conn = http.client.HTTPConnection(parsed.netloc, timeout=30)\n conn.request(method, parsed.path + (\"?\" + parsed.query if parsed.query else \"\"),\n body=body if body else None,\n headers=test_headers)\n resp = conn.getresponse()\n resp_body = resp.read().decode(\"utf-8\", errors=\"replace\")\n if len(resp_body) > 10000:\n resp_body = resp_body[:10000] + \"\\n... (truncated)\"\n return jsonify({\n \"status\": resp.status,\n \"reason\": resp.reason,\n \"headers\": dict(resp.getheaders()),\n \"body\": resp_body,\n })\n except Exception as e:\n return jsonify({\"error\": str(e)}), 500\n\n\nif __name__ == \"__main__\":\n host = os.environ.get(\"HOST\", \"0.0.0.0\")\n port = int(os.environ.get(\"PORT\", \"5001\"))\n api_key = os.environ.get(\"API_KEY\", \"sk-my-secret-key-1\")\n\n print(\"=\" * 50)\n print(\"DeepWhale (Flask) - OpenAI Compatible DeepSeek Bridge\")\n print(\"=\" * 50)\n print(f\"Endpoint: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/chat/completions\")\n print(f\"Models: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/v1/models\")\n print(f\"API Key: {api_key}\")\n print(\"=\" * 50)\n print(f\"Admin: http://{host if host != '0.0.0.0' else 'localhost'}:{port}/admin/login\")\n admin_pw = os.environ.get(\"ADMIN_PASSWORD\", \"admin\")\n print(f\"Admin PW: {admin_pw}\")\n print(\"=\" * 50)\n print(\"[info] Starting browser and logging into DeepSeek automatically in the background...\")\n threading.Thread(target=get_active_token, daemon=True).start()\n print(\"=\" * 50)\n\n app.run(\n host=host,\n port=port,\n threaded=True,\n debug=False,\n )\n"}}}}