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 codebase is provided for personal use and self-hosting only.
Redistribution of the codebase, whether in original or modified form, is not permitted without prior written consent from the author.
You may fork and modify the repository solely for the purpose of running and self-hosting your own instance.
Any other form of distribution, sublicensing, or commercial use is strictly prohibited unless explicitly authorized.
- Built on Cloudflare Workers
- Uses Hono web framework
- Inspired by the official Qwen Code CLI
- OAuth patterns adapted from Qwen Code CLI implementation