Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/users/configuration/model-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ This auth type supports not only OpenAI's official API but also any OpenAI-compa
"maxRetries": 3,
"enableCacheControl": true,
"contextWindowSize": 128000,
"modalities": {
"image": true
},
"customHeaders": {
"X-Client-Request-ID": "req-123"
},
Expand Down Expand Up @@ -275,7 +278,7 @@ export VLLM_API_KEY="not-needed"
```

> [!note]
>
>
> The `extra_body` parameter is **only supported for OpenAI-compatible providers** (`openai`, `qwen-oauth`). It is ignored for Anthropic, Gemini, and Vertex AI providers.

## Bailian Coding Plan
Expand Down Expand Up @@ -388,7 +391,7 @@ The effective auth/model/credential values are chosen per field using the follow
\*When present, CLI auth flags override settings. Otherwise, `security.auth.selectedType` or the implicit default determine the auth type. Qwen OAuth and OpenAI are the only auth types surfaced without extra configuration.

> [!warning]
>
>
> **Deprecation of `security.auth.apiKey` and `security.auth.baseUrl`:** Directly configuring API credentials via `security.auth.apiKey` and `security.auth.baseUrl` in `settings.json` is deprecated. These settings were used in historical versions for credentials entered through the UI, but the credential input flow was removed in version 0.10.1. These fields will be fully removed in a future release. **It is strongly recommended to migrate to `modelProviders`** for all model and credential configurations. Use `envKey` in `modelProviders` to reference environment variables for secure credential management instead of hardcoding credentials in settings files.

## Generation Config Layering: The Impermeable Provider Layer
Expand Down Expand Up @@ -522,7 +525,7 @@ The snapshot:
## Selection Persistence and Recommendations

> [!important]
>
>
> Define `modelProviders` in the user-scope `~/.qwen/settings.json` whenever possible and avoid persisting credential overrides in any scope. Keeping the provider catalog in user settings prevents merge/override conflicts between project and user scopes and ensures `/auth` and `/model` updates always write back to a consistent scope.

- `/model` and `/auth` persist `model.name` (where applicable) and `security.auth.selectedType` to the closest writable scope that already defines `modelProviders`; otherwise they fall back to the user scope. This keeps workspace/user files in sync with the active provider catalog.
Expand Down
31 changes: 19 additions & 12 deletions docs/users/configuration/settings.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,17 @@ export default {
'(default)': '(Standard)',
'(set)': '(gesetzt)',
'(not set)': '(nicht gesetzt)',
Modality: 'Modalität',
'Context Window': 'Kontextfenster',
text: 'Text',
'text-only': 'nur Text',
image: 'Bild',
pdf: 'PDF',
audio: 'Audio',
video: 'Video',
'not set': 'nicht gesetzt',
none: 'keine',
unknown: 'unbekannt',
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
"Modell konnte nicht auf '{{modelId}}' umgestellt werden.\n\n{{error}}",
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,17 @@ export default {
'(default)': '(default)',
'(set)': '(set)',
'(not set)': '(not set)',
Modality: 'Modality',
'Context Window': 'Context Window',
text: 'text',
'text-only': 'text-only',
image: 'image',
pdf: 'pdf',
audio: 'audio',
video: 'video',
'not set': 'not set',
none: 'none',
unknown: 'unknown',
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
"Failed to switch model to '{{modelId}}'.\n\n{{error}}",
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,17 @@ export default {
// Dialogs - Model
'Select Model': 'モデルを選択',
'(Press Esc to close)': '(Esc で閉じる)',
Modality: 'モダリティ',
'Context Window': 'コンテキストウィンドウ',
text: 'テキスト',
'text-only': 'テキストのみ',
image: '画像',
pdf: 'PDF',
audio: '音声',
video: '動画',
'not set': '未設定',
none: 'なし',
unknown: '不明',
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
'Qwen 3.5 Plus — 効率的なハイブリッドモデル、業界トップクラスのコーディング性能',
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,17 @@ export default {
'(default)': '(padrão)',
'(set)': '(definido)',
'(not set)': '(não definido)',
Modality: 'Modalidade',
'Context Window': 'Janela de Contexto',
text: 'texto',
'text-only': 'somente texto',
image: 'imagem',
pdf: 'PDF',
audio: 'áudio',
video: 'vídeo',
'not set': 'não definido',
none: 'nenhum',
unknown: 'desconhecido',
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
"Falha ao trocar o modelo para '{{modelId}}'.\n\n{{error}}",
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,17 @@ export default {
'(default)': '(по умолчанию)',
'(set)': '(установлено)',
'(not set)': '(не задано)',
Modality: 'Модальность',
'Context Window': 'Контекстное окно',
text: 'текст',
'text-only': 'только текст',
image: 'изображение',
pdf: 'PDF',
audio: 'аудио',
video: 'видео',
'not set': 'не задано',
none: 'нет',
unknown: 'неизвестно',
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
"Не удалось переключиться на модель '{{modelId}}'.\n\n{{error}}",
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,17 @@ export default {
'(default)': '(默认)',
'(set)': '(已设置)',
'(not set)': '(未设置)',
Modality: '模态',
'Context Window': '上下文窗口',
text: '文本',
'text-only': '纯文本',
image: '图像',
pdf: 'PDF',
audio: '音频',
video: '视频',
'not set': '未设置',
none: '无',
unknown: '未知',
"Failed to switch model to '{{modelId}}'.\n\n{{error}}":
"无法切换到模型 '{{modelId}}'.\n\n{{error}}",
'Qwen 3.5 Plus — efficient hybrid model with leading coding performance':
Expand Down
37 changes: 35 additions & 2 deletions packages/cli/src/ui/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,43 @@
*/

import { Box } from 'ink';
import { Header } from './Header.js';
import { AuthType } from '@qwen-code/qwen-code-core';
import { Header, AuthDisplayType } from './Header.js';
import { Tips } from './Tips.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { isCodingPlanConfig } from '../../constants/codingPlan.js';

interface AppHeaderProps {
version: string;
}

/**
* Determine the auth display type based on auth type and configuration.
*/
function getAuthDisplayType(
authType?: AuthType,
baseUrl?: string,
apiKeyEnvKey?: string,
): AuthDisplayType {
if (!authType) {
return AuthDisplayType.UNKNOWN;
}

// Check if it's a Coding Plan config
if (isCodingPlanConfig(baseUrl, apiKeyEnvKey)) {
return AuthDisplayType.CODING_PLAN;
}

switch (authType) {
case AuthType.QWEN_OAUTH:
return AuthDisplayType.QWEN_OAUTH;
default:
return AuthDisplayType.API_KEY;
}
}

export const AppHeader = ({ version }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
Expand All @@ -27,12 +54,18 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
const showBanner = !config.getScreenReader();
const showTips = !(settings.merged.ui?.hideTips || config.getScreenReader());

const authDisplayType = getAuthDisplayType(
authType,
contentGeneratorConfig?.baseUrl,
contentGeneratorConfig?.apiKeyEnvKey,
);

return (
<Box flexDirection="column">
{showBanner && (
<Header
version={version}
authType={authType}
authDisplayType={authDisplayType}
model={model}
workingDirectory={targetDir}
/>
Expand Down
59 changes: 21 additions & 38 deletions packages/cli/src/ui/components/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,95 +6,78 @@

import { render } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthType } from '@qwen-code/qwen-code-core';
import { Header } from './Header.js';
import { Header, AuthDisplayType } from './Header.js';
import * as useTerminalSize from '../hooks/useTerminalSize.js';

vi.mock('../hooks/useTerminalSize.js');
const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);

const defaultProps = {
version: '1.0.0',
authType: AuthType.QWEN_OAUTH,
authDisplayType: AuthDisplayType.QWEN_OAUTH,
model: 'qwen-coder-plus',
workingDirectory: '/home/user/projects/test',
};

describe('<Header />', () => {
beforeEach(() => {
// Default to wide terminal (shows both logo and info panel)
useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 });
});

it('renders the ASCII logo on wide terminal', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
// Check that parts of the shortAsciiLogo are rendered
expect(lastFrame()).toContain('██╔═══██╗');
});

it('hides the ASCII logo on narrow terminal', () => {
useTerminalSizeMock.mockReturnValue({ columns: 60, rows: 24 });
const { lastFrame } = render(<Header {...defaultProps} />);
// Should not contain the logo but still show the info panel
expect(lastFrame()).not.toContain('██╔═══██╗');
expect(lastFrame()).toContain('>_ Qwen Code');
});

it('renders custom ASCII art when provided on wide terminal', () => {
const customArt = 'CUSTOM ART';
const { lastFrame } = render(
<Header {...defaultProps} customAsciiArt={customArt} />,
);
expect(lastFrame()).toContain(customArt);
});

it('displays the version number', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('v1.0.0');
});

it('displays Qwen Code title with >_ prefix', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('>_ Qwen Code');
});

it('displays auth type and model', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('Qwen OAuth');
expect(lastFrame()).toContain('qwen-coder-plus');
});

it('displays working directory', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('/home/user/projects/test');
});

it('renders a custom working directory display', () => {
it('displays Coding Plan auth type', () => {
const { lastFrame } = render(
<Header {...defaultProps} workingDirectory="custom display" />,
<Header
{...defaultProps}
authDisplayType={AuthDisplayType.CODING_PLAN}
/>,
);
expect(lastFrame()).toContain('custom display');
expect(lastFrame()).toContain('Coding Plan');
});

it('displays working directory without branch name', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
// Branch name is no longer shown in header
expect(lastFrame()).toContain('/home/user/projects/test');
expect(lastFrame()).not.toContain('(main*)');
it('displays API Key auth type', () => {
const { lastFrame } = render(
<Header {...defaultProps} authDisplayType={AuthDisplayType.API_KEY} />,
);
expect(lastFrame()).toContain('API Key');
});

it('formats home directory with tilde', () => {
it('displays Unknown when auth type is not set', () => {
const { lastFrame } = render(
<Header {...defaultProps} workingDirectory="/Users/testuser/projects" />,
<Header {...defaultProps} authDisplayType={undefined} />,
);
// The actual home dir replacement depends on os.homedir()
// Just verify the path is shown
expect(lastFrame()).toContain('projects');
expect(lastFrame()).toContain('Unknown');
});

it('displays working directory', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
expect(lastFrame()).toContain('/home/user/projects/test');
});

it('renders with border around info panel', () => {
const { lastFrame } = render(<Header {...defaultProps} />);
// Check for border characters (round border style uses these)
expect(lastFrame()).toContain('╭');
expect(lastFrame()).toContain('╯');
});
Expand Down
Loading