docs/design/custom-api-key-auth-wizard-prd.md
Improve the /auth -> API Key -> Custom API Key experience by replacing the current documentation-only screen with an in-terminal setup wizard for custom API providers.
Qwen Code supports multiple API protocols through authType / modelProviders keys, including openai, anthropic, and gemini. Therefore, the custom setup wizard should start by asking users to select the protocol, then collect endpoint, key, and model information for that protocol.
The wizard guides users through:
Select Protocol -> Enter Base URL -> Enter API Key -> Enter Model IDs -> Review JSON -> Save + authenticate
This keeps the custom API key setup inside Qwen Code, reduces the need to manually edit settings.json, and makes the final configuration transparent by showing the generated JSON before saving.
Today, selecting Custom API Key in /auth shows a static information screen:
Custom Configuration
You can configure your API key and models in settings.json
Refer to the documentation for setup instructions
https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/
Esc to go back
This requires users to leave the CLI, read documentation, understand settings.json, manually configure modelProviders, choose an envKey, add API keys, and then return to Qwen Code. Users have reported that this flow is difficult and disconnected from the rest of the /auth experience.
The current ModelStudio Standard API key path already provides a guided setup flow:
Alibaba Cloud ModelStudio Standard API Key
└─ Select Region
└─ Enter API Key
└─ Enter Model IDs
└─ Save + authenticate
Custom API key setup should offer a similar guided experience, while also respecting that Qwen Code supports multiple provider protocols.
The custom API key path is currently a dead end inside /auth:
/auth
└─ Select Authentication Method
├─ Alibaba Cloud Coding Plan
├─ API Key
│ └─ Select API Key Type
│ ├─ Alibaba Cloud ModelStudio Standard API Key
│ │ ├─ Select Region
│ │ ├─ Enter API Key
│ │ ├─ Enter Model IDs
│ │ └─ Save + authenticate
│ │
│ └─ Custom API Key
│ └─ Documentation-only screen
│
└─ Qwen OAuth
This causes several usability issues:
/auth.authType, baseUrl, envKey, modelProviders, model.name, and security.auth.selectedType./auth.modelProviders: openai, anthropic, and gemini.baseUrl as the custom-provider equivalent of region.envKey from the selected protocol and input baseUrl.settings.json.env, consistent with the current Qwen-managed credential pattern.modelProviders entries.envKey.generationConfig, capabilities, or per-model overrides to the wizard.baseUrl in the first version; users select the protocol explicitly.settings.json.The wizard should initially expose these protocol options:
openai
anthropic
gemini
Each protocol maps directly to a modelProviders key and security.auth.selectedType value.
| Protocol option | Auth type / modelProviders key | Notes |
|---|---|---|
| OpenAI-compatible | openai | OpenAI, OpenRouter, Fireworks, local OpenAI-compatible servers, internal gateways |
| Anthropic-compatible | anthropic | Anthropic-compatible endpoints |
| Gemini-compatible | gemini | Gemini-compatible endpoints |
/auth tree/auth
└─ Select Authentication Method
├─ Alibaba Cloud Coding Plan
│ └─ Select Region
│ └─ Enter API Key
│ └─ Save + authenticate
│
├─ API Key
│ └─ Select API Key Type
│ ├─ Alibaba Cloud ModelStudio Standard API Key
│ │ ├─ Select Region
│ │ ├─ Enter API Key
│ │ ├─ Enter Model IDs
│ │ └─ Save + authenticate
│ │
│ └─ Custom API Key
│ ├─ Select Protocol
│ ├─ Enter Base URL
│ ├─ Enter API Key
│ ├─ Enter Model IDs
│ ├─ Review generated JSON
│ └─ Save + authenticate
│
└─ Qwen OAuth
api-key-type-select
│
└─ CUSTOM_API_KEY
│
▼
custom-protocol-select
│ Enter
▼
custom-base-url-input
│ Enter
│ generate envKey from protocol + baseUrl
▼
custom-api-key-input
│ Enter
▼
custom-model-id-input
│ Enter
▼
custom-review-json
│ Enter
▼
save settings + refreshAuth(selectedProtocol)
custom-review-json
Esc -> custom-model-id-input
custom-model-id-input
Esc -> custom-api-key-input
custom-api-key-input
Esc -> custom-base-url-input
custom-base-url-input
Esc -> custom-protocol-select
custom-protocol-select
Esc -> api-key-type-select
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Select Protocol │
│ │
│ ◉ OpenAI-compatible │
│ OpenAI, OpenRouter, Fireworks, vLLM, Ollama, LM Studio │
│ │
│ ○ Anthropic-compatible │
│ Anthropic-compatible endpoints │
│ │
│ ○ Gemini-compatible │
│ Gemini-compatible endpoints │
│ │
│ Enter to select, ↑↓ to navigate, Esc to go back │
└──────────────────────────────────────────────────────────────┘
The selected protocol determines:
modelProviders key to update.security.auth.selectedType value to persist.refreshAuth() auth type used after saving.baseUrl is the custom-provider equivalent of region selection. It should come before API key entry because it determines which endpoint the API key belongs to.
For OpenAI-compatible:
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Base URL │
│ │
│ Protocol: OpenAI-compatible │
│ │
│ Enter the OpenAI-compatible API endpoint. │
│ │
│ Base URL: https://openrouter.ai/api/v1_ │
│ │
│ Examples: │
│ OpenAI: https://api.openai.com/v1 │
│ OpenRouter: https://openrouter.ai/api/v1 │
│ Fireworks: https://api.fireworks.ai/inference/v1 │
│ Ollama: http://localhost:11434/v1 │
│ LM Studio: http://localhost:1234/v1 │
│ │
│ Enter to continue, Esc to go back │
└──────────────────────────────────────────────────────────────┘
For Anthropic-compatible:
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Base URL │
│ │
│ Protocol: Anthropic-compatible │
│ │
│ Enter the Anthropic-compatible API endpoint. │
│ │
│ Base URL: https://api.anthropic.com/v1_ │
│ │
│ Enter to continue, Esc to go back │
└──────────────────────────────────────────────────────────────┘
For Gemini-compatible:
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Base URL │
│ │
│ Protocol: Gemini-compatible │
│ │
│ Enter the Gemini-compatible API endpoint. │
│ │
│ Base URL: https://generativelanguage.googleapis.com_ │
│ │
│ Enter to continue, Esc to go back │
└──────────────────────────────────────────────────────────────┘
Validation:
http:// or https://.On valid submit:
envKey from selected protocol and baseUrl.┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · API Key │
│ │
│ Protocol: OpenAI-compatible │
│ Endpoint: https://openrouter.ai/api/v1 │
│ │
│ Enter the API key for this endpoint. │
│ │
│ API key: sk-or-v1-••••••••••••••••_ │
│ │
│ Enter to continue, Esc to go back │
└──────────────────────────────────────────────────────────────┘
Validation:
Notes:
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Model IDs │
│ │
│ Protocol: OpenAI-compatible │
│ Endpoint: https://openrouter.ai/api/v1 │
│ │
│ Enter one or more model IDs, separated by commas. │
│ │
│ Model IDs: qwen/qwen3-coder,openai/gpt-4.1_ │
│ │
│ Enter to continue, Esc to go back │
└──────────────────────────────────────────────────────────────┘
Validation:
Model naming:
id and name should be the same.Example:
Input:
qwen/qwen3-coder, openai/gpt-4.1, qwen/qwen3-coder
Normalized:
qwen/qwen3-coder, openai/gpt-4.1
Before saving, show the generated JSON snippet that will be written or merged into settings.json.
OpenAI-compatible example:
┌──────────────────────────────────────────────────────────────┐
│ Custom API Key · Review │
│ │
│ The following JSON will be saved to settings.json: │
│ │
│ { │
│ "env": { │
│ "QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1":│
│ "sk-••••••••••••••••" │
│ }, │
│ "modelProviders": { │
│ "openai": [ │
│ { │
│ "id": "qwen/qwen3-coder", │
│ "name": "qwen/qwen3-coder", │
│ "baseUrl": "https://openrouter.ai/api/v1", │
│ "envKey": "QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1"│
│ } │
│ ] │
│ }, │
│ "security": { │
│ "auth": { │
│ "selectedType": "openai" │
│ } │
│ }, │
│ "model": { │
│ "name": "qwen/qwen3-coder" │
│ } │
│ } │
│ │
│ Enter to save, Esc to go back │
└──────────────────────────────────────────────────────────────┘
Anthropic-compatible example:
{
"env": {
"QWEN_CUSTOM_API_KEY_ANTHROPIC_HTTPS_API_ANTHROPIC_COM_V1": "sk-••••"
},
"modelProviders": {
"anthropic": [
{
"id": "claude-sonnet-4-5",
"name": "claude-sonnet-4-5",
"baseUrl": "https://api.anthropic.com/v1",
"envKey": "QWEN_CUSTOM_API_KEY_ANTHROPIC_HTTPS_API_ANTHROPIC_COM_V1"
}
]
},
"security": {
"auth": {
"selectedType": "anthropic"
}
},
"model": {
"name": "claude-sonnet-4-5"
}
}
The displayed JSON should:
modelProviders key.security.auth.selectedType.envKey.baseUrl.id === name for each model.model.name set to the first normalized model ID.If the JSON is too wide for the current terminal, wrapping is acceptable. The goal is transparency, not copy-paste-perfect formatting.
On Enter from the review screen:
save:
env[generatedEnvKey] = apiKey
modelProviders[selectedProtocol] = [
...new custom configs using generatedEnvKey,
...existing configs whose envKey !== generatedEnvKey
]
security.auth.selectedType = selectedProtocol
model.name = firstModelId
reloadModelProvidersConfig()
refreshAuth(selectedProtocol)
Success message:
Custom API Key authenticated successfully. Settings updated with generated env key and model provider config.
Tip: Use /model to switch between configured models.
Failure message should preserve the existing authentication failure pattern, with additional user-facing hints if possible:
Failed to authenticate. Message: <error>
Please check:
- Base URL is compatible with the selected protocol
- API key is valid for this endpoint
- Model ID exists for this provider
The wizard should not ask users to enter an envKey.
Qwen-managed API keys are stored in settings.json.env, so the env key should be generated automatically under a Qwen-specific namespace. This avoids collisions with user-managed shell environment variables and prevents multiple custom endpoints from overwriting each other.
QWEN_CUSTOM_API_KEY_${PROTOCOL}_${NORMALIZED_BASE_URL}
Including the protocol avoids collisions when the same endpoint is used under different protocol adapters.
Protocol: openai
Base URL: https://api.openai.com/v1
-> QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_API_OPENAI_COM_V1
Protocol: openai
Base URL: https://openrouter.ai/api/v1
-> QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1
Protocol: anthropic
Base URL: https://api.anthropic.com/v1
-> QWEN_CUSTOM_API_KEY_ANTHROPIC_HTTPS_API_ANTHROPIC_COM_V1
Protocol: gemini
Base URL: https://generativelanguage.googleapis.com
-> QWEN_CUSTOM_API_KEY_GEMINI_HTTPS_GENERATIVELANGUAGE_GOOGLEAPIS_COM
Protocol: openai
Base URL: http://localhost:11434/v1
-> QWEN_CUSTOM_API_KEY_OPENAI_HTTP_LOCALHOST_11434_V1
protocol
-> trim
-> uppercase
-> replace every non A-Z / 0-9 character with _
baseUrl
-> trim
-> uppercase
-> replace every non A-Z / 0-9 character with _
-> collapse consecutive _ characters
-> remove leading/trailing _
return QWEN_CUSTOM_API_KEY_${NORMALIZED_PROTOCOL}_${NORMALIZED_BASE_URL}
Pseudo-code:
function generateCustomApiKeyEnvKey(protocol: string, baseUrl: string): string {
const normalize = (value: string) =>
value
.trim()
.toUpperCase()
.replace(/[^A-Z0-9]+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
return `QWEN_CUSTOM_API_KEY_${normalize(protocol)}_${normalize(baseUrl)}`;
}
Given user input:
Protocol: openai
Base URL: https://openrouter.ai/api/v1
API key: sk-or-v1-xxx
Model IDs: qwen/qwen3-coder,openai/gpt-4.1
The wizard should produce:
{
"env": {
"QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1": "sk-or-v1-xxx"
},
"modelProviders": {
"openai": [
{
"id": "qwen/qwen3-coder",
"name": "qwen/qwen3-coder",
"baseUrl": "https://openrouter.ai/api/v1",
"envKey": "QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1"
},
{
"id": "openai/gpt-4.1",
"name": "openai/gpt-4.1",
"baseUrl": "https://openrouter.ai/api/v1",
"envKey": "QWEN_CUSTOM_API_KEY_OPENAI_HTTPS_OPENROUTER_AI_API_V1"
}
]
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "qwen/qwen3-coder"
}
}
For anthropic, the same structure is used, except:
modelProviders.anthropic
security.auth.selectedType = anthropic
refreshAuth(anthropic)
For gemini, the same structure is used, except:
modelProviders.gemini
security.auth.selectedType = gemini
refreshAuth(gemini)
Use the same persist-scope strategy as model selection and the existing API-key flows:
getPersistScopeForModelSelection(settings)
This keeps behavior consistent with existing modelProviders ownership rules.
Before writing, back up the target settings file, consistent with existing Coding Plan and ModelStudio Standard flows.
After writing settings.json.env[generatedEnvKey], immediately sync:
process.env[generatedEnvKey] = apiKey
This ensures refreshAuth(selectedProtocol) can use the newly entered key in the same session.
For the generated env key:
generatedEnvKey = QWEN_CUSTOM_API_KEY_${PROTOCOL}_${NORMALIZED_BASE_URL}
Update modelProviders[selectedProtocol] as follows:
newConfigs = normalizedModelIds.map(modelId => ({
id: modelId,
name: modelId,
baseUrl,
envKey: generatedEnvKey,
}))
existingConfigs = settings.merged.modelProviders?.[selectedProtocol] ?? []
preservedConfigs = existingConfigs.filter(config =>
config.envKey !== generatedEnvKey
)
updatedConfigs = [
...newConfigs,
...preservedConfigs,
]
Rationale:
baseUrl replaces old models for that endpoint.baseUrl uses a different env key and does not overwrite previous custom endpoints.The protocol must be one of:
openai
anthropic
gemini
Base URL cannot be empty.
Base URL must start with http:// or https://.
API key cannot be empty.
Model IDs cannot be empty.
Use the existing failure mechanism where possible, but the user-facing error should help users recover:
Failed to authenticate. Message: <message>
Please check:
- Base URL is compatible with the selected protocol
- API key is valid for this endpoint
- Model ID exists for this provider
The wizard should still expose the existing model providers documentation for advanced users.
Recommended placement:
Suggested copy:
Need advanced generationConfig or capabilities? See:
https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/
Expected AuthDialog view levels:
type ViewLevel =
| 'main'
| 'region-select'
| 'api-key-input'
| 'api-key-type-select'
| 'alibaba-standard-region-select'
| 'alibaba-standard-api-key-input'
| 'alibaba-standard-model-id-input'
| 'custom-protocol-select'
| 'custom-base-url-input'
| 'custom-api-key-input'
| 'custom-model-id-input'
| 'custom-review-json';
Expected custom protocol type:
type CustomApiProtocol =
| AuthType.USE_OPENAI
| AuthType.USE_ANTHROPIC
| AuthType.USE_GEMINI;
Expected new state in AuthDialog:
const [customProtocol, setCustomProtocol] = useState<CustomApiProtocol>(
AuthType.USE_OPENAI,
);
const [customProtocolIndex, setCustomProtocolIndex] = useState<number>(0);
const [customBaseUrl, setCustomBaseUrl] = useState('');
const [customBaseUrlError, setCustomBaseUrlError] = useState<string | null>(
null,
);
const [customApiKey, setCustomApiKey] = useState('');
const [customApiKeyError, setCustomApiKeyError] = useState<string | null>(null);
const [customModelIds, setCustomModelIds] = useState('');
const [customModelIdsError, setCustomModelIdsError] = useState<string | null>(
null,
);
Expected new UI action:
handleCustomApiKeySubmit: (
protocol: CustomApiProtocol,
baseUrl: string,
apiKey: string,
modelIdsInput: string,
) => Promise<void>;
Expected helper functions:
generateCustomApiKeyEnvKey(protocol: string, baseUrl: string): string
normalizeCustomModelIds(modelIdsInput: string): string[]
maskApiKey(apiKey: string): string
/auth -> API Key -> Custom API Key opens the custom wizard instead of the documentation-only page.settings.json.env[generatedEnvKey].generatedEnvKey is derived from selected protocol and baseUrl using the Qwen private namespace.modelProviders[selectedProtocol] receives one entry per normalized model ID.id === name.security.auth.selectedType is set to the selected protocol.model.name is set to the first normalized model ID.modelProviders[selectedProtocol] with a different envKey are preserved.modelProviders[selectedProtocol] with the same generated envKey are replaced.modelProviders protocol keys are preserved.process.env before auth refresh.refreshAuth(selectedProtocol).AuthDialog tests to cover the custom wizard path.http://localhost:11434/v1 allow empty or placeholder API keys for servers that do not require authentication?For the first version, recommended defaults are:
openai, anthropic, and gemini.