30 KiB
Custom API Key Auth Wizard PRD
Summary
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.
Background
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.
Problem Statement
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:
- Users cannot finish custom provider setup from
/auth. - Users need to understand low-level settings concepts before they can authenticate.
- Users may not know which fields are required:
authType,baseUrl,envKey,modelProviders,model.name, andsecurity.auth.selectedType. - Users may accidentally conflict with existing environment variables or overwrite existing provider configuration.
- Users do not get immediate authentication feedback after editing settings manually.
Goals
- Let users configure a custom API provider completely inside
/auth. - Support the main protocols Qwen Code supports in
modelProviders:openai,anthropic, andgemini. - Keep the flow close to the existing ModelStudio Standard flow.
- Treat
baseUrlas the custom-provider equivalent ofregion. - Automatically generate a Qwen-managed private
envKeyfrom the selected protocol and inputbaseUrl. - Store the API key under
settings.json.env, consistent with the current Qwen-managed credential pattern. - Avoid conflicts with user shell environment variables by using a Qwen-specific generated key name.
- Show the generated JSON before saving so users can review the exact settings changes.
- Preserve unrelated existing
modelProvidersentries. - Authenticate immediately after saving and show success or failure feedback.
Non-goals
- Do not require users to manually enter
envKey. - Do not introduce provider name as a separate concept.
- Do not add advanced
generationConfig,capabilities, or per-model overrides to the wizard. - Do not remove the documentation link entirely; it should remain available for advanced configuration.
- Do not change the existing Coding Plan or ModelStudio Standard API key flows.
- Do not attempt to auto-detect protocol from
baseUrlin the first version; users select the protocol explicitly.
Target Users
- Users who bring their own custom API endpoint.
- Users configuring providers such as OpenAI-compatible APIs, Anthropic-compatible APIs, Gemini-compatible APIs, vLLM, Ollama, LM Studio, or internal gateways.
- Users who prefer setting up authentication from the CLI rather than manually editing
settings.json.
Supported Protocols
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 |
User Experience Overview
Updated /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
Custom API Key state machine
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)
Escape behavior
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
Detailed Interaction Design
Step 1: Select Protocol
┌──────────────────────────────────────────────────────────────┐
│ 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:
- The
modelProviderskey to update. - The
security.auth.selectedTypevalue to persist. - The protocol label shown on later screens.
- The
refreshAuth()auth type used after saving.
Step 2: Enter Base URL
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:
- Required.
- Must start with
http://orhttps://. - Trim leading and trailing whitespace.
- Preserve the normalized string as entered, except trimming.
On valid submit:
- Generate the Qwen-managed
envKeyfrom selected protocol andbaseUrl. - Move to API key input.
Step 3: Enter API Key
┌──────────────────────────────────────────────────────────────┐
│ 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:
- Required.
- Trim leading and trailing whitespace.
Notes:
- The input may initially use the existing text input behavior for consistency with nearby flows.
- The review screen should mask the API key.
Step 4: Enter Model IDs
┌──────────────────────────────────────────────────────────────┐
│ 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:
- Required.
- Split by comma.
- Trim each model ID.
- Remove empty entries.
- Deduplicate entries while preserving order.
- At least one model ID must remain.
Model naming:
idandnameshould be the same.- No separate provider name is requested from the user.
Example:
Input:
qwen/qwen3-coder, openai/gpt-4.1, qwen/qwen3-coder
Normalized:
qwen/qwen3-coder, openai/gpt-4.1
Step 5: Review JSON
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:
- Use the selected protocol as the
modelProviderskey. - Use the selected protocol as
security.auth.selectedType. - Use the actual generated
envKey. - Mask the API key.
- Use the user-entered
baseUrl. - Use
id === namefor each model. - Show
model.nameset 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.
Step 6: Save and Authenticate
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
Env Key Generation
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.
Format
QWEN_CUSTOM_API_KEY_${PROTOCOL}_${NORMALIZED_BASE_URL}
Including the protocol avoids collisions when the same endpoint is used under different protocol adapters.
Examples
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
Normalization rule
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)}`;
}
Settings Write Design
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)
Persist scope
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.
Backup
Before writing, back up the target settings file, consistent with existing Coding Plan and ModelStudio Standard flows.
Process env sync
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.
Model provider merge rule
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:
- Reconfiguring the same protocol +
baseUrlreplaces old models for that endpoint. - Configuring a different protocol or
baseUrluses a different env key and does not overwrite previous custom endpoints. - Coding Plan, ModelStudio Standard, and other user configs are preserved unless they use the same generated env key under the same protocol.
- New configs are placed first so the newly configured models are immediately visible and selected by default.
Error Handling
Protocol validation error
The protocol must be one of:
openai
anthropic
gemini
Base URL validation error
Base URL cannot be empty.
Base URL must start with http:// or https://.
API key validation error
API key cannot be empty.
Model IDs validation error
Model IDs cannot be empty.
Authentication failure
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
Documentation Link
The wizard should still expose the existing model providers documentation for advanced users.
Recommended placement:
- On the review screen footer, or
- As secondary text on the base URL screen.
Suggested copy:
Need advanced generationConfig or capabilities? See:
https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/
Implementation Notes
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
Acceptance Criteria
UX
- Selecting
/auth -> API Key -> Custom API Keyopens the custom wizard instead of the documentation-only page. - The first custom wizard step asks for protocol.
- The second step asks for Base URL and displays the selected protocol.
- The third step asks for API key and displays the selected protocol and endpoint.
- The fourth step asks for model IDs and displays the selected protocol and endpoint.
- The review step displays the generated JSON, including masked API key, selected protocol, and generated env key.
- Pressing Enter on the review step saves settings and attempts authentication.
- Pressing Esc navigates back one step at a time.
Settings
- The API key is written to
settings.json.env[generatedEnvKey]. generatedEnvKeyis derived from selected protocol andbaseUrlusing the Qwen private namespace.modelProviders[selectedProtocol]receives one entry per normalized model ID.- Each custom model entry uses
id === name. security.auth.selectedTypeis set to the selected protocol.model.nameis set to the first normalized model ID.- Existing entries under
modelProviders[selectedProtocol]with a differentenvKeyare preserved. - Existing entries under
modelProviders[selectedProtocol]with the same generatedenvKeyare replaced. - Entries under other
modelProvidersprotocol keys are preserved.
Authentication
- The generated env key is synced to
process.envbefore auth refresh. - The app reloads model provider config before
refreshAuth(selectedProtocol). - Successful auth closes the auth dialog and shows a success message.
- Failed auth keeps the user in the auth flow and shows an actionable error.
Tests
- Add or update
AuthDialogtests to cover the custom wizard path. - Add tests for protocol selection.
- Add tests for env key generation from protocol and base URL.
- Add tests for model ID normalization and deduplication.
- Add tests for settings merge behavior:
- same generated env key replaces old custom entries under the same protocol;
- different env keys are preserved;
- other protocol keys are preserved;
- Coding Plan and ModelStudio Standard entries are preserved.
- Add tests for generated JSON preview content where practical.
Open Questions
- Should the API key input be masked during typing, or only masked on the review screen?
- Should local endpoints such as
http://localhost:11434/v1allow empty or placeholder API keys for servers that do not require authentication? - Should the generated JSON preview show only the patch being applied, or the resulting full relevant settings subtree after merge?
- Should Vertex AI be included in this custom API key wizard, or remain outside because its auth setup differs from simple API-key providers?
For the first version, recommended defaults are:
- Support
openai,anthropic, andgemini. - Use existing input behavior during typing.
- Require non-empty API key for consistency with API-key auth flows.
- Show the patch-style JSON that will be saved or updated.
- Keep Vertex AI out of the custom API key wizard until a separate product decision is made.