Transform Qwen Code CLI into OpenAI-compatible endpoints using Cloudflare Workers. Access advanced AI capabilities through a standardized API interface, powered by OAuth2 authentication and seamless integration with the Qwen Code ecosystem.
- π OAuth2 Authentication - Uses your Qwen Code CLI credentials seamlessly
- π― OpenAI-Compatible API - Drop-in replacement for OpenAI endpoints
- π OpenAI SDK Support - Works with official OpenAI SDKs and libraries
- π Third-party Integration - Compatible with Open WebUI, Cline, and more
- π‘οΈ API Key Security - Optional authentication layer for endpoint access
- β‘ Cloudflare Workers - Global edge deployment with low latency
- π Smart Token Management - Automatic token refresh with KV storage
- π‘ Real-time Streaming - Server-sent events for live responses
- ποΈ Clean Architecture - Well-structured, maintainable codebase
- π Debug Logging - Comprehensive logging for troubleshooting
- Qwen Account with Code CLI access
- Cloudflare Account with Workers enabled
- Wrangler CLI installed (
npm install -g wrangler
)
You need OAuth2 credentials from the official Qwen Code CLI.
-
Install Qwen Code CLI:
npm install -g @qwen-code/qwen-code@latest
-
Start Qwen Code and authenticate:
qwen
Select your preferred authentication method when prompted.
-
Locate the credentials file:
Windows:
C:\Users\USERNAME\.qwen\oauth_creds.json
macOS/Linux:
~/.qwen/oauth_creds.json
-
Copy the credentials: The file contains JSON in this format:
{ "access_token": "your_access_token_here", "refresh_token": "your_refresh_token_here", "expiry_date": 1700000000000, "resource_url": "https://your-endpoint.com/v1", "token_type": "Bearer" }
# Create a KV namespace for token caching
wrangler kv namespace create "QWEN_KV"
Note the namespace ID returned and update wrangler.toml
:
kv_namespaces = [
{ binding = "QWEN_KV", id = "your-kv-namespace-id" }
]
Create a .dev.vars
file:
# Required: Qwen Code CLI authentication JSON
QWEN_CLI_AUTH={"access_token":"your_access_token","refresh_token":"your_refresh_token","expiry_date":1700000000000,"resource_url":"https://your-endpoint.com/v1","token_type":"Bearer"}
# Optional: API key for client authentication (if set, users must provide it in Authorization header)
# OPENAI_API_KEY=sk-your-secret-key-here
# Optional: Default model override
# OPENAI_MODEL=qwen3-coder-plus
# Optional: Custom base URL (will use resource_url from OAuth if available)
# OPENAI_BASE_URL=https://api-inference.modelscope.cn/v1
For production, set the secrets:
wrangler secret put QWEN_CLI_AUTH
# Enter your OAuth credentials JSON
# Install dependencies
npm install
# Deploy to Cloudflare Workers
npm run deploy
# Or run locally for development
npm run dev
The service will be available at https://your-worker.your-subdomain.workers.dev
Variable | Required | Description |
---|---|---|
QWEN_CLI_AUTH |
β | OAuth2 credentials JSON from Qwen Code CLI |
OPENAI_API_KEY |
β | API key for client authentication |
OPENAI_MODEL |
β | Default model override |
OPENAI_BASE_URL |
β | Custom base URL (uses OAuth resource_url if available) |
- When
OPENAI_API_KEY
is set, all/v1/*
endpoints require authentication - Clients must include the header:
Authorization: Bearer <your-api-key>
- Recommended format:
sk-
followed by a random string - Without this variable, endpoints are publicly accessible (not recommended for production)
- Automatic Refresh: Tokens are automatically refreshed when expired
- KV Persistence: Refreshed tokens are stored in Cloudflare KV
- Fallback Logic: KV cache β environment β refresh β retry
- Debug Logging: Comprehensive token source tracking
Binding | Purpose |
---|---|
QWEN_KV |
OAuth token caching and refresh storage |
https://your-worker.your-subdomain.workers.dev
POST /v1/chat/completions
Authorization: Bearer sk-your-api-key-here (if OPENAI_API_KEY is set)
Content-Type: application/json
{
"model": "qwen3-coder-plus",
"messages": [
{
"role": "system",
"content": "You are a helpful coding assistant."
},
{
"role": "user",
"content": "Write a Python function to calculate fibonacci numbers"
}
],
"stream": true,
"temperature": 0.7,
"max_tokens": 1000
}
GET /v1/models
Authorization: Bearer sk-your-api-key-here (if OPENAI_API_KEY is set)
Response:
{
"object": "list",
"data": [
{
"id": "qwen3-coder-plus",
"object": "model",
"created": 1700000000,
"owned_by": "qwen"
},
{
"id": "qwen3-coder-flash",
"object": "model",
"created": 1700000000,
"owned_by": "qwen"
}
]
}
GET /health
No authentication required
Response:
{
"status": "ok",
"uptime": 1700000000,
"version": "qwen-worker-1.0.0"
}
from openai import OpenAI
# Initialize with your worker endpoint
client = OpenAI(
base_url="https://your-worker.workers.dev/v1",
api_key="sk-your-secret-api-key-here" # Only if OPENAI_API_KEY is set
)
# Chat completion
response = client.chat.completions.create(
model="qwen3-coder-plus",
messages=[
{"role": "system", "content": "You are a helpful coding assistant."},
{"role": "user", "content": "Write a binary search algorithm in Python"}
],
temperature=0.2,
max_tokens=500,
stream=True
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://your-worker.workers.dev/v1',
apiKey: 'sk-your-secret-api-key-here', // Only if OPENAI_API_KEY is set
});
const stream = await openai.chat.completions.create({
model: 'qwen3-coder-plus',
messages: [
{ role: 'user', content: 'Explain async/await in JavaScript' }
],
stream: true,
temperature: 0.7,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
process.stdout.write(content);
}
# Chat completion (non-streaming)
curl -X POST https://your-worker.workers.dev/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-secret-api-key-here" \
-d '{
"model": "qwen3-coder-plus",
"messages": [
{"role": "user", "content": "Hello! How are you?"}
],
"temperature": 0.7
}'
# Chat completion (streaming)
curl -N -X POST https://your-worker.workers.dev/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-secret-api-key-here" \
-d '{
"model": "qwen3-coder-flash",
"messages": [
{"role": "user", "content": "Write a TypeScript hello world"}
],
"stream": true
}'
# List available models
curl https://your-worker.workers.dev/v1/models \
-H "Authorization: Bearer sk-your-secret-api-key-here"
# Health check
curl https://your-worker.workers.dev/health
-
Add as OpenAI-compatible endpoint:
- Base URL:
https://your-worker.workers.dev/v1
- API Key:
sk-your-secret-api-key-here
(only ifOPENAI_API_KEY
is set)
- Base URL:
-
Auto-discovery: Open WebUI will automatically discover available models through the
/v1/models
endpoint.
graph TD
A[Client Request] --> B[Cloudflare Worker]
B --> C[API Key Validation]
C --> D{Valid API Key?}
D -->|No| E[401 Unauthorized]
D -->|Yes| F{Token in KV Cache?}
F -->|Yes| G[Use Cached Token]
F -->|No| H[Check Environment Token]
H --> I{Token Valid?}
I -->|Yes| J[Cache & Use Token]
I -->|No| K[Refresh Token via Qwen API]
K --> L[Cache New Token]
G --> M[Call Qwen API]
J --> M
L --> M
M --> N{Streaming?}
N -->|Yes| O[Forward SSE Stream]
N -->|No| P[Return JSON Response]
O --> Q[Client Receives Stream]
P --> Q
The wrapper acts as a secure translation layer, managing OAuth2 authentication automatically while providing OpenAI-compatible responses.
401 Authentication Error
- Verify your
OPENAI_API_KEY
is correctly set (if using API key auth) - Check if client is sending
Authorization: Bearer <key>
header - Ensure the API key format is valid
OAuth Token Issues
- Check if your
QWEN_CLI_AUTH
credentials are valid - Ensure the refresh token hasn't expired
- Verify the JSON format matches the expected structure
KV Storage Issues
- Confirm KV namespace is correctly configured in
wrangler.toml
- Check KV namespace permissions in Cloudflare dashboard
- Verify the binding name matches (
QWEN_KV
)
Streaming Problems
- Check the worker logs for streaming-related errors
- Ensure the upstream Qwen API supports streaming for the requested model
- Verify the resource_url in your OAuth credentials
The worker provides comprehensive debug logging:
# Run locally to see logs
npm run dev
Look for these log patterns:
=== New chat completion request ===
Environment loaded: { hasKv: true, hasCliAuth: true, ... }
Authentication passed
loadInitialCredentials called with json: present
Token validity check: { hasAccessToken: true, expiryDate: ..., tokenValid: true }
Base URL resolved: https://your-endpoint.com/v1
Making upstream request...
Captured usage in stream: {...}
# Health check with detailed info
curl https://your-worker.workers.dev/health
# Check if KV credentials are loaded (logs will show)
curl https://your-worker.workers.dev/v1/models
src/
βββ types/ # TypeScript type definitions
β βββ bindings.ts # Cloudflare bindings
β βββ openai.ts # OpenAI API types
β βββ qwen.ts # Qwen-specific types
β βββ common.ts # Shared utilities
βββ config/ # Configuration management
β βββ constants.ts # App constants
β βββ validation.ts # Request validation
β βββ index.ts # Config exports
βββ services/ # Business logic services
β βββ qwenOAuthKvClient.ts # OAuth client with KV storage
β βββ qwenProxy.ts # HTTP proxy to Qwen API
β βββ openaiMapper.ts # Request/response mapping
β βββ auth.ts # API key authentication
β βββ credentials.ts # Legacy (can be removed)
βββ routes/ # HTTP route handlers
β βββ chat.ts # Chat completions endpoint
β βββ health.ts # Health check endpoint
β βββ models.ts # Models listing endpoint
βββ index.ts # Hono bootstrap
-
OAuth Client (
services/qwenOAuthKvClient.ts
)- Manages Qwen OAuth tokens with KV persistence
- Automatic token refresh when expired
- Bootstrap from environment credentials
-
Request Mapping (
services/openaiMapper.ts
)- Transforms OpenAI requests to Qwen-compatible format
- Maps sampling parameters (temperature, top_p, etc.)
- Validates request structure
-
HTTP Proxy (
services/qwenProxy.ts
)- Calls Qwen API with proper authentication
- Resolves base URL from OAuth resource_url
- Handles both streaming and non-streaming responses
-
Authentication (
services/auth.ts
)- Optional API key validation for endpoint access
- Bearer token format validation
- Fork the repository:
https://github.com/gewoonjaap/qwen-code-cli-wrapper
- Create a feature branch:
git checkout -b feature-name
- Make your changes and add tests
- Run linting:
npm run lint
- Test thoroughly:
npm test
- Commit your changes:
git commit -am 'Add feature'
- Push to the branch:
git push origin feature-name
- Submit a pull request
git clone https://github.com/gewoonjaap/qwen-code-cli-wrapper.git
cd qwen-code-cli-wrapper
npm install
cp .dev.vars.example .dev.vars
# Edit .dev.vars with your Qwen OAuth credentials
npm run dev
npm run dev # Start development server
npm run deploy # Deploy to Cloudflare Workers
npm run lint # Run ESLint and TypeScript checks
npm run format # Format code with Prettier
npm test # Run test suite
npm run build # Build the project
This project is licensed under the MIT License - see the LICENSE file for details.
- Built on Cloudflare Workers
- Uses Hono web framework
- Inspired by the official Qwen Code CLI
- OAuth patterns adapted from Qwen Code CLI implementation