Skip to content
Closed
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
347 changes: 347 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions QWEN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,166 @@ Only write high-value comments if at all. Avoid talking to the user through comm
## General style requirements

Use hyphens instead of underscores in flag names (e.g. `my-flag` instead of `my_flag`).

## Codebase Architecture Map

### Project Structure Overview

Qwen Code is a monorepo with the following architecture:

```
qwen-code/
├── packages/
│ ├── core/ # Core business logic and AI integrations
│ ├── cli/ # Terminal UI and command processing
│ └── vscode-ide-companion/ # VSCode extension
├── scripts/ # Build and development automation
├── bundle/ # Distribution artifacts
└── package.json # Root workspace configuration
```

### Core Package (`packages/core/`)

**Purpose**: Central business logic, AI model integrations, authentication, and tool system.

#### Key Modules:

**Authentication & Configuration:**

- `src/core/contentGenerator.ts` - Content generator factory and `AuthType` enum
- `src/config/config.ts` - Central `Config` class for system configuration
- `src/config/models.ts` - Default model constants (`DEFAULT_GEMINI_MODEL`)
- `src/code_assist/oauth2.ts` - Google OAuth2 implementation

**AI Provider Integrations:**

- `src/core/openaiContentGenerator.ts` - OpenAI API integration
- `src/core/openrouterContentGenerator.ts` - OpenRouter API integration
- `src/core/geminiChat.ts` - Gemini API integration
- `src/core/client.ts` - Main client orchestration

**Tool System:**

- `src/tools/tool-registry.ts` - Tool discovery and registration
- `src/tools/` - File operations, shell commands, web tools, memory tools

**Services:**

- `src/services/fileDiscoveryService.ts` - File discovery with gitignore support
- `src/services/gitService.ts` - Git operations
- `src/services/loopDetectionService.ts` - Infinite loop prevention

**Utilities:**

- `src/utils/paths.ts` - Path manipulation (`GEMINI_DIR = '.qwen'`)
- `src/utils/memoryDiscovery.ts` - Memory management
- `src/telemetry/` - OpenTelemetry integration

### CLI Package (`packages/cli/`)

**Purpose**: Terminal user interface, authentication dialogs, and command processing.

#### Key Modules:

**Entry Points:**

- `index.ts` - Global CLI entry point
- `src/gemini.tsx` - Main React application bootstrap
- `src/nonInteractiveCli.ts` - Headless mode handler

**Configuration Management:**

- `src/config/config.ts` - CLI argument parsing with yargs
- `src/config/settings.ts` - Hierarchical settings loading (system/user/workspace)
- `src/config/auth.ts` - Authentication method validation
- `src/config/extension.ts` - Extension system
- `src/config/sandboxConfig.ts` - Sandbox configuration

**User Interface (React + Ink):**

- `src/ui/App.tsx` - Main UI orchestrator
- `src/ui/components/AuthDialog.tsx` - Multi-provider authentication selection
- `src/ui/components/OpenAIKeyPrompt.tsx` - OpenAI configuration
- `src/ui/components/OpenRouterKeyPrompt.tsx` - OpenRouter configuration
- `src/ui/hooks/useGeminiStream.ts` - Core AI response streaming

**Command System:**

- `src/services/CommandService.ts` - Command management
- `src/ui/commands/` - Slash command implementations
- `src/ui/hooks/slashCommandProcessor.ts` - Command parsing

**Theme System:**

- `src/ui/themes/` - Color schemes and theming

### Root Level Structure

**Build System:**

- `esbuild.config.js` - Single-file bundling to `bundle/gemini.js`
- `scripts/build.js` - Workspace build orchestration
- `scripts/bundle.js` - Asset copying and bundling
- `package.json` - Workspace management and CLI distribution via `"bin": {"qwen": "bundle/gemini.js"}`

**Distribution:**

- `bundle/gemini.js` - Standalone executable
- `bundle/*.sb` - macOS sandbox configuration files

### Authentication Flow Architecture

**Environment Loading Priority:**

1. Current directory `.qwen/.env`
2. Current directory `.env`
3. Parent directories (recursive)
4. Home directory `.qwen/.env`
5. Home directory `.env`

**Auth Type Selection Priority:**

1. Saved user preference (`settings.selectedAuthType`)
2. `GEMINI_DEFAULT_AUTH_TYPE` environment variable
3. Auto-detection based on available credentials:
- `OPENROUTER_API_KEY` → `AuthType.USE_OPENROUTER`
- `OPENAI_API_KEY` → `AuthType.USE_OPENAI`
- `GEMINI_API_KEY` → `AuthType.USE_GEMINI`
4. Fallback: Interactive auth dialog

**Configuration Flow:**

```
Environment Loading → Auth Type Selection → Content Generator Creation → API Calls
```

### Key Configuration Points

**Default Model Selection:**

- File: `packages/core/src/config/models.ts:7`
- Current: `DEFAULT_GEMINI_MODEL = 'qwen3-coder-max'`

**CLI Model Priority:**

- File: `packages/cli/src/config/config.ts:75`
- Order: `OPENROUTER_MODEL || GEMINI_MODEL || DEFAULT_GEMINI_MODEL`

**Auth Selection (Interactive):**

- File: `packages/cli/src/ui/components/AuthDialog.tsx:57-78`
- Priority: Settings → ENV vars → API key detection → Default

**Auth Selection (Non-Interactive):**

- File: `packages/cli/src/gemini.tsx:342-350`
- Priority: `OPENROUTER_API_KEY` → `OPENAI_API_KEY` → `USE_GEMINI`

### Global Installation Flow

1. `npm run build` - TypeScript compilation
2. `npm run bundle` - esbuild creates `bundle/gemini.js`
3. `npm install -g` - Installs `qwen` command globally
4. `qwen` command executes `bundle/gemini.js`

This architecture supports both development (monorepo with hot reloading) and distribution (single executable) workflows.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ qwen
### Terminal-Bench

| Agent | Model | Accuracy |
|-----------|--------------------|----------|
| --------- | ------------------ | -------- |
| Qwen Code | Qwen3-Coder-480A35 | 37.5 |

## Project Structure
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/config/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export const validateAuthMethod = (authMethod: string): string | null => {
return null;
}

if (authMethod === AuthType.USE_OPENROUTER) {
if (!process.env.OPENROUTER_API_KEY) {
return 'OPENROUTER_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.';
}
return null;
}

return 'Invalid auth method selected.';
};

Expand All @@ -59,3 +66,11 @@ export const setOpenAIBaseUrl = (baseUrl: string): void => {
export const setOpenAIModel = (model: string): void => {
process.env.OPENAI_MODEL = model;
};

export const setOpenRouterApiKey = (apiKey: string): void => {
process.env.OPENROUTER_API_KEY = apiKey;
};

export const setOpenRouterModel = (model: string): void => {
process.env.OPENROUTER_MODEL = model;
};
5 changes: 4 additions & 1 deletion packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export async function parseArguments(): Promise<CliArgs> {
alias: 'm',
type: 'string',
description: `Model`,
default: process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
default:
process.env.OPENROUTER_MODEL ||
process.env.GEMINI_MODEL ||
DEFAULT_GEMINI_MODEL,
})
.option('prompt', {
alias: 'p',
Expand Down
29 changes: 25 additions & 4 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export async function main() {
'selectedAuthType',
AuthType.CLOUD_SHELL,
);
} else {
// Default to OpenRouter for all users
settings.setValue(
SettingScope.User,
'selectedAuthType',
AuthType.USE_OPENROUTER,
);
}
}

Expand Down Expand Up @@ -323,16 +330,30 @@ async function validateNonInterActiveAuth(
nonInteractiveConfig: Config,
) {
// making a special case for the cli. many headless environments might not have a settings.json set
// so if GEMINI_API_KEY is set, we'll use that. However since the oauth things are interactive anyway, we'll
// so if any API key is set, we'll use that. However since the oauth things are interactive anyway, we'll
// still expect that exists
if (!selectedAuthType && !process.env.GEMINI_API_KEY) {
if (
!selectedAuthType &&
!process.env.GEMINI_API_KEY &&
!process.env.OPENROUTER_API_KEY &&
!process.env.OPENAI_API_KEY
) {
console.error(
`Please set an Auth method in your ${USER_SETTINGS_PATH} OR specify GEMINI_API_KEY env variable file before running`,
`Please set an Auth method in your ${USER_SETTINGS_PATH} OR specify GEMINI_API_KEY, OPENROUTER_API_KEY, or OPENAI_API_KEY env variable file before running`,
);
process.exit(1);
}

selectedAuthType = selectedAuthType || AuthType.USE_GEMINI;
// Auto-select auth type based on available credentials
if (!selectedAuthType) {
if (process.env.OPENROUTER_API_KEY) {
selectedAuthType = AuthType.USE_OPENROUTER;
} else if (process.env.OPENAI_API_KEY) {
selectedAuthType = AuthType.USE_OPENAI;
} else {
selectedAuthType = AuthType.USE_GEMINI;
}
}
const err = validateAuthMethod(selectedAuthType);
if (err != null) {
console.error(err);
Expand Down
46 changes: 43 additions & 3 deletions packages/cli/src/ui/components/AuthDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import {
setOpenAIApiKey,
setOpenAIBaseUrl,
setOpenAIModel,
setOpenRouterApiKey,
setOpenRouterModel,
} from '../../config/auth.js';
import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js';
import { OpenRouterKeyPrompt } from './OpenRouterKeyPrompt.js';

interface AuthDialogProps {
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
Expand Down Expand Up @@ -45,7 +48,11 @@ export function AuthDialog({
initialErrorMessage || null,
);
const [showOpenAIKeyPrompt, setShowOpenAIKeyPrompt] = useState(false);
const items = [{ label: 'OpenAI', value: AuthType.USE_OPENAI }];
const [showOpenRouterKeyPrompt, setShowOpenRouterKeyPrompt] = useState(false);
const items = [
{ label: 'OpenAI', value: AuthType.USE_OPENAI },
{ label: 'OpenRouter', value: AuthType.USE_OPENROUTER },
];

const initialAuthIndex = items.findIndex((item) => {
if (settings.merged.selectedAuthType) {
Expand All @@ -59,6 +66,10 @@ export function AuthDialog({
return item.value === defaultAuthType;
}

if (process.env.OPENROUTER_API_KEY) {
return item.value === AuthType.USE_OPENROUTER;
}

if (process.env.GEMINI_API_KEY) {
return item.value === AuthType.USE_GEMINI;
}
Expand All @@ -72,6 +83,12 @@ export function AuthDialog({
if (authMethod === AuthType.USE_OPENAI && !process.env.OPENAI_API_KEY) {
setShowOpenAIKeyPrompt(true);
setErrorMessage(null);
} else if (
authMethod === AuthType.USE_OPENROUTER &&
!process.env.OPENROUTER_API_KEY
) {
setShowOpenRouterKeyPrompt(true);
setErrorMessage(null);
} else {
setErrorMessage(error);
}
Expand All @@ -98,9 +115,23 @@ export function AuthDialog({
setErrorMessage('OpenAI API key is required to use OpenAI authentication.');
};

const handleOpenRouterKeySubmit = (apiKey: string, model: string) => {
setOpenRouterApiKey(apiKey);
setOpenRouterModel(model);
setShowOpenRouterKeyPrompt(false);
onSelect(AuthType.USE_OPENROUTER, SettingScope.User);
};

const handleOpenRouterKeyCancel = () => {
setShowOpenRouterKeyPrompt(false);
setErrorMessage(
'OpenRouter API key is required to use OpenRouter authentication.',
);
};

useInput((_input, key) => {
// 当显示 OpenAIKeyPrompt 时,不处理输入事件
if (showOpenAIKeyPrompt) {
// 当显示 OpenAIKeyPrompt 或 OpenRouterKeyPrompt 时,不处理输入事件
if (showOpenAIKeyPrompt || showOpenRouterKeyPrompt) {
return;
}

Expand Down Expand Up @@ -130,6 +161,15 @@ export function AuthDialog({
);
}

if (showOpenRouterKeyPrompt) {
return (
<OpenRouterKeyPrompt
onSubmit={handleOpenRouterKeySubmit}
onCancel={handleOpenRouterKeyCancel}
/>
);
}

return (
<Box
borderStyle="round"
Expand Down
Loading