mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-21 16:26:20 +00:00
Compare commits
13 Commits
fix/vscode
...
docs/code-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2852f48a4a | ||
|
|
886f914fb3 | ||
|
|
90365af2f8 | ||
|
|
cbef5ffd89 | ||
|
|
985f65f8fa | ||
|
|
9b9c5fadd5 | ||
|
|
372c67cad4 | ||
|
|
af3864b5de | ||
|
|
1e3791f30a | ||
|
|
9bf626d051 | ||
|
|
a35af6550f | ||
|
|
d6607e134e | ||
|
|
9024a41723 |
@@ -5,11 +5,13 @@ Qwen Code supports two authentication methods. Pick the one that matches how you
|
|||||||
- **Qwen OAuth (recommended)**: sign in with your `qwen.ai` account in a browser.
|
- **Qwen OAuth (recommended)**: sign in with your `qwen.ai` account in a browser.
|
||||||
- **OpenAI-compatible API**: use an API key (OpenAI or any OpenAI-compatible provider / endpoint).
|
- **OpenAI-compatible API**: use an API key (OpenAI or any OpenAI-compatible provider / endpoint).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Option 1: Qwen OAuth (recommended & free) 👍
|
## Option 1: Qwen OAuth (recommended & free) 👍
|
||||||
|
|
||||||
Use this if you want the simplest setup and you’re using Qwen models.
|
Use this if you want the simplest setup and you're using Qwen models.
|
||||||
|
|
||||||
- **How it works**: on first start, Qwen Code opens a browser login page. After you finish, credentials are cached locally so you usually won’t need to log in again.
|
- **How it works**: on first start, Qwen Code opens a browser login page. After you finish, credentials are cached locally so you usually won't need to log in again.
|
||||||
- **Requirements**: a `qwen.ai` account + internet access (at least for the first login).
|
- **Requirements**: a `qwen.ai` account + internet access (at least for the first login).
|
||||||
- **Benefits**: no API key management, automatic credential refresh.
|
- **Benefits**: no API key management, automatic credential refresh.
|
||||||
- **Cost & quota**: free, with a quota of **60 requests/minute** and **2,000 requests/day**.
|
- **Cost & quota**: free, with a quota of **60 requests/minute** and **2,000 requests/day**.
|
||||||
@@ -24,15 +26,54 @@ qwen
|
|||||||
|
|
||||||
Use this if you want to use OpenAI models or any provider that exposes an OpenAI-compatible API (e.g. OpenAI, Azure OpenAI, OpenRouter, ModelScope, Alibaba Cloud Bailian, or a self-hosted compatible endpoint).
|
Use this if you want to use OpenAI models or any provider that exposes an OpenAI-compatible API (e.g. OpenAI, Azure OpenAI, OpenRouter, ModelScope, Alibaba Cloud Bailian, or a self-hosted compatible endpoint).
|
||||||
|
|
||||||
### Quick start (interactive, recommended for local use)
|
### Recommended: Coding Plan (subscription-based) 🚀
|
||||||
|
|
||||||
When you choose the OpenAI-compatible option in the CLI, it will prompt you for:
|
Use this if you want predictable costs with higher usage quotas for the qwen3-coder-plus model.
|
||||||
|
|
||||||
- **API key**
|
> [!IMPORTANT]
|
||||||
- **Base URL** (default: `https://api.openai.com/v1`)
|
>
|
||||||
- **Model** (default: `gpt-4o`)
|
> Coding Plan is only available for users in China mainland (Beijing region).
|
||||||
|
|
||||||
> **Note:** the CLI may display the key in plain text for verification. Make sure your terminal is not being recorded or shared.
|
- **How it works**: subscribe to the Coding Plan with a fixed monthly fee, then configure Qwen Code to use the dedicated endpoint and your subscription API key.
|
||||||
|
- **Requirements**: an active Coding Plan subscription from [Alibaba Cloud Bailian](https://bailian.console.aliyun.com/cn-beijing/?tab=globalset#/efm/coding_plan).
|
||||||
|
- **Benefits**: higher usage quotas, predictable monthly costs, access to latest qwen3-coder-plus model.
|
||||||
|
- **Cost & quota**: varies by plan (see table below).
|
||||||
|
|
||||||
|
#### Coding Plan Pricing & Quotas
|
||||||
|
|
||||||
|
| Feature | Lite Basic Plan | Pro Advanced Plan |
|
||||||
|
| :------------------ | :-------------------- | :-------------------- |
|
||||||
|
| **Price** | ¥40/month | ¥200/month |
|
||||||
|
| **5-Hour Limit** | Up to 1,200 requests | Up to 6,000 requests |
|
||||||
|
| **Weekly Limit** | Up to 9,000 requests | Up to 45,000 requests |
|
||||||
|
| **Monthly Limit** | Up to 18,000 requests | Up to 90,000 requests |
|
||||||
|
| **Supported Model** | qwen3-coder-plus | qwen3-coder-plus |
|
||||||
|
|
||||||
|
#### Quick Setup for Coding Plan
|
||||||
|
|
||||||
|
When you select the OpenAI-compatible option in the CLI, enter these values:
|
||||||
|
|
||||||
|
- **API key**: `sk-sp-xxxxx`
|
||||||
|
- **Base URL**: `https://coding.dashscope.aliyuncs.com/v1`
|
||||||
|
- **Model**: `qwen3-coder-plus`
|
||||||
|
|
||||||
|
> **Note**: Coding Plan API keys have the format `sk-sp-xxxxx`, which is different from standard Alibaba Cloud API keys.
|
||||||
|
|
||||||
|
#### Configure via Environment Variables
|
||||||
|
|
||||||
|
Set these environment variables to use Coding Plan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY="your-coding-plan-api-key" # Format: sk-sp-xxxxx
|
||||||
|
export OPENAI_BASE_URL="https://coding.dashscope.aliyuncs.com/v1"
|
||||||
|
export OPENAI_MODEL="qwen3-coder-plus"
|
||||||
|
```
|
||||||
|
|
||||||
|
For more details about Coding Plan, including subscription options and troubleshooting, see the [full Coding Plan documentation](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3005961).
|
||||||
|
|
||||||
|
### Other OpenAI-compatible Providers
|
||||||
|
|
||||||
|
If you are using other providers (OpenAI, Azure, local LLMs, etc.), use the following configuration methods.
|
||||||
|
|
||||||
### Configure via command-line arguments
|
### Configure via command-line arguments
|
||||||
|
|
||||||
|
|||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -17310,7 +17310,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "1.30.0",
|
"@google/genai": "1.30.0",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
@@ -17947,7 +17947,7 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@qwen-code/qwen-code-core",
|
"name": "@qwen-code/qwen-code-core",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.36.1",
|
"@anthropic-ai/sdk": "^0.36.1",
|
||||||
@@ -21408,7 +21408,7 @@
|
|||||||
},
|
},
|
||||||
"packages/test-utils": {
|
"packages/test-utils": {
|
||||||
"name": "@qwen-code/qwen-code-test-utils",
|
"name": "@qwen-code/qwen-code-test-utils",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -21420,7 +21420,7 @@
|
|||||||
},
|
},
|
||||||
"packages/vscode-ide-companion": {
|
"packages/vscode-ide-companion": {
|
||||||
"name": "qwen-code-vscode-ide-companion",
|
"name": "qwen-code-vscode-ide-companion",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"license": "LICENSE",
|
"license": "LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.0"
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env node scripts/start.js",
|
"start": "cross-env node scripts/start.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"description": "Qwen Code",
|
"description": "Qwen Code",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.0"
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "1.30.0",
|
"@google/genai": "1.30.0",
|
||||||
|
|||||||
@@ -874,11 +874,10 @@ export async function loadCliConfig(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
// ACP mode check: must include both --acp (current) and --experimental-acp (deprecated).
|
||||||
!interactive &&
|
// Without this check, edit, write_file, run_shell_command would be excluded in ACP mode.
|
||||||
!argv.experimentalAcp &&
|
const isAcpMode = argv.acp || argv.experimentalAcp;
|
||||||
inputFormat !== InputFormat.STREAM_JSON
|
if (!interactive && !isAcpMode && inputFormat !== InputFormat.STREAM_JSON) {
|
||||||
) {
|
|
||||||
switch (approvalMode) {
|
switch (approvalMode) {
|
||||||
case ApprovalMode.PLAN:
|
case ApprovalMode.PLAN:
|
||||||
case ApprovalMode.DEFAULT:
|
case ApprovalMode.DEFAULT:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code-core",
|
"name": "@qwen-code/qwen-code-core",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"description": "Qwen Code Core",
|
"description": "Qwen Code Core",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ export class Config {
|
|||||||
private toolRegistry!: ToolRegistry;
|
private toolRegistry!: ToolRegistry;
|
||||||
private promptRegistry!: PromptRegistry;
|
private promptRegistry!: PromptRegistry;
|
||||||
private subagentManager!: SubagentManager;
|
private subagentManager!: SubagentManager;
|
||||||
private skillManager!: SkillManager;
|
private skillManager: SkillManager | null = null;
|
||||||
private fileSystemService: FileSystemService;
|
private fileSystemService: FileSystemService;
|
||||||
private contentGeneratorConfig!: ContentGeneratorConfig;
|
private contentGeneratorConfig!: ContentGeneratorConfig;
|
||||||
private contentGeneratorConfigSources: ContentGeneratorConfigSources = {};
|
private contentGeneratorConfigSources: ContentGeneratorConfigSources = {};
|
||||||
@@ -672,8 +672,10 @@ export class Config {
|
|||||||
}
|
}
|
||||||
this.promptRegistry = new PromptRegistry();
|
this.promptRegistry = new PromptRegistry();
|
||||||
this.subagentManager = new SubagentManager(this);
|
this.subagentManager = new SubagentManager(this);
|
||||||
this.skillManager = new SkillManager(this);
|
if (this.getExperimentalSkills()) {
|
||||||
await this.skillManager.startWatching();
|
this.skillManager = new SkillManager(this);
|
||||||
|
await this.skillManager.startWatching();
|
||||||
|
}
|
||||||
|
|
||||||
// Load session subagents if they were provided before initialization
|
// Load session subagents if they were provided before initialization
|
||||||
if (this.sessionSubagents.length > 0) {
|
if (this.sessionSubagents.length > 0) {
|
||||||
@@ -1439,7 +1441,7 @@ export class Config {
|
|||||||
return this.subagentManager;
|
return this.subagentManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSkillManager(): SkillManager {
|
getSkillManager(): SkillManager | null {
|
||||||
return this.skillManager;
|
return this.skillManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,28 +270,28 @@ export function createContentGeneratorConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createContentGenerator(
|
export async function createContentGenerator(
|
||||||
config: ContentGeneratorConfig,
|
generatorConfig: ContentGeneratorConfig,
|
||||||
gcConfig: Config,
|
config: Config,
|
||||||
isInitialAuth?: boolean,
|
isInitialAuth?: boolean,
|
||||||
): Promise<ContentGenerator> {
|
): Promise<ContentGenerator> {
|
||||||
const validation = validateModelConfig(config, false);
|
const validation = validateModelConfig(generatorConfig, false);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
throw new Error(validation.errors.map((e) => e.message).join('\n'));
|
throw new Error(validation.errors.map((e) => e.message).join('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.authType === AuthType.USE_OPENAI) {
|
const authType = generatorConfig.authType;
|
||||||
// Import OpenAIContentGenerator dynamically to avoid circular dependencies
|
if (!authType) {
|
||||||
|
throw new Error('ContentGeneratorConfig must have an authType');
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseGenerator: ContentGenerator;
|
||||||
|
|
||||||
|
if (authType === AuthType.USE_OPENAI) {
|
||||||
const { createOpenAIContentGenerator } = await import(
|
const { createOpenAIContentGenerator } = await import(
|
||||||
'./openaiContentGenerator/index.js'
|
'./openaiContentGenerator/index.js'
|
||||||
);
|
);
|
||||||
|
baseGenerator = createOpenAIContentGenerator(generatorConfig, config);
|
||||||
// Always use OpenAIContentGenerator, logging is controlled by enableOpenAILogging flag
|
} else if (authType === AuthType.QWEN_OAUTH) {
|
||||||
const generator = createOpenAIContentGenerator(config, gcConfig);
|
|
||||||
return new LoggingContentGenerator(generator, gcConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.authType === AuthType.QWEN_OAUTH) {
|
|
||||||
// Import required classes dynamically
|
|
||||||
const { getQwenOAuthClient: getQwenOauthClient } = await import(
|
const { getQwenOAuthClient: getQwenOauthClient } = await import(
|
||||||
'../qwen/qwenOAuth2.js'
|
'../qwen/qwenOAuth2.js'
|
||||||
);
|
);
|
||||||
@@ -300,44 +300,38 @@ export async function createContentGenerator(
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the Qwen OAuth client (now includes integrated token management)
|
|
||||||
// If this is initial auth, require cached credentials to detect missing credentials
|
|
||||||
const qwenClient = await getQwenOauthClient(
|
const qwenClient = await getQwenOauthClient(
|
||||||
gcConfig,
|
config,
|
||||||
isInitialAuth ? { requireCachedCredentials: true } : undefined,
|
isInitialAuth ? { requireCachedCredentials: true } : undefined,
|
||||||
);
|
);
|
||||||
|
baseGenerator = new QwenContentGenerator(
|
||||||
// Create the content generator with dynamic token management
|
qwenClient,
|
||||||
const generator = new QwenContentGenerator(qwenClient, config, gcConfig);
|
generatorConfig,
|
||||||
return new LoggingContentGenerator(generator, gcConfig);
|
config,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${error instanceof Error ? error.message : String(error)}`,
|
`${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else if (authType === AuthType.USE_ANTHROPIC) {
|
||||||
|
|
||||||
if (config.authType === AuthType.USE_ANTHROPIC) {
|
|
||||||
const { createAnthropicContentGenerator } = await import(
|
const { createAnthropicContentGenerator } = await import(
|
||||||
'./anthropicContentGenerator/index.js'
|
'./anthropicContentGenerator/index.js'
|
||||||
);
|
);
|
||||||
|
baseGenerator = createAnthropicContentGenerator(generatorConfig, config);
|
||||||
const generator = createAnthropicContentGenerator(config, gcConfig);
|
} else if (
|
||||||
return new LoggingContentGenerator(generator, gcConfig);
|
authType === AuthType.USE_GEMINI ||
|
||||||
}
|
authType === AuthType.USE_VERTEX_AI
|
||||||
|
|
||||||
if (
|
|
||||||
config.authType === AuthType.USE_GEMINI ||
|
|
||||||
config.authType === AuthType.USE_VERTEX_AI
|
|
||||||
) {
|
) {
|
||||||
const { createGeminiContentGenerator } = await import(
|
const { createGeminiContentGenerator } = await import(
|
||||||
'./geminiContentGenerator/index.js'
|
'./geminiContentGenerator/index.js'
|
||||||
);
|
);
|
||||||
const generator = createGeminiContentGenerator(config, gcConfig);
|
baseGenerator = createGeminiContentGenerator(generatorConfig, config);
|
||||||
return new LoggingContentGenerator(generator, gcConfig);
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Error creating contentGenerator: Unsupported authType: ${authType}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
return new LoggingContentGenerator(baseGenerator, config, generatorConfig);
|
||||||
`Error creating contentGenerator: Unsupported authType: ${config.authType}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
import { GenerateContentResponse } from '@google/genai';
|
import { GenerateContentResponse } from '@google/genai';
|
||||||
import type { Config } from '../../config/config.js';
|
import type { Config } from '../../config/config.js';
|
||||||
import type { ContentGenerator } from '../contentGenerator.js';
|
import type { ContentGenerator } from '../contentGenerator.js';
|
||||||
|
import { AuthType } from '../contentGenerator.js';
|
||||||
import { LoggingContentGenerator } from './index.js';
|
import { LoggingContentGenerator } from './index.js';
|
||||||
import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js';
|
import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js';
|
||||||
import {
|
import {
|
||||||
@@ -50,14 +51,17 @@ const convertGeminiResponseToOpenAISpy = vi
|
|||||||
choices: [],
|
choices: [],
|
||||||
} as OpenAI.Chat.ChatCompletion);
|
} as OpenAI.Chat.ChatCompletion);
|
||||||
|
|
||||||
const createConfig = (overrides: Record<string, unknown> = {}): Config =>
|
const createConfig = (overrides: Record<string, unknown> = {}): Config => {
|
||||||
({
|
const configContent = {
|
||||||
getContentGeneratorConfig: () => ({
|
authType: 'openai',
|
||||||
authType: 'openai',
|
enableOpenAILogging: false,
|
||||||
enableOpenAILogging: false,
|
...overrides,
|
||||||
...overrides,
|
};
|
||||||
}),
|
return {
|
||||||
}) as Config;
|
getContentGeneratorConfig: () => configContent,
|
||||||
|
getAuthType: () => configContent.authType as AuthType | undefined,
|
||||||
|
} as Config;
|
||||||
|
};
|
||||||
|
|
||||||
const createWrappedGenerator = (
|
const createWrappedGenerator = (
|
||||||
generateContent: ContentGenerator['generateContent'],
|
generateContent: ContentGenerator['generateContent'],
|
||||||
@@ -124,13 +128,17 @@ describe('LoggingContentGenerator', () => {
|
|||||||
),
|
),
|
||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
|
const generatorConfig = {
|
||||||
|
model: 'test-model',
|
||||||
|
authType: AuthType.USE_OPENAI,
|
||||||
|
enableOpenAILogging: true,
|
||||||
|
openAILoggingDir: 'logs',
|
||||||
|
schemaCompliance: 'openapi_30' as const,
|
||||||
|
};
|
||||||
const generator = new LoggingContentGenerator(
|
const generator = new LoggingContentGenerator(
|
||||||
wrapped,
|
wrapped,
|
||||||
createConfig({
|
createConfig(),
|
||||||
enableOpenAILogging: true,
|
generatorConfig,
|
||||||
openAILoggingDir: 'logs',
|
|
||||||
schemaCompliance: 'openapi_30',
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
@@ -225,9 +233,15 @@ describe('LoggingContentGenerator', () => {
|
|||||||
vi.fn().mockRejectedValue(error),
|
vi.fn().mockRejectedValue(error),
|
||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
|
const generatorConfig = {
|
||||||
|
model: 'test-model',
|
||||||
|
authType: AuthType.USE_OPENAI,
|
||||||
|
enableOpenAILogging: true,
|
||||||
|
};
|
||||||
const generator = new LoggingContentGenerator(
|
const generator = new LoggingContentGenerator(
|
||||||
wrapped,
|
wrapped,
|
||||||
createConfig({ enableOpenAILogging: true }),
|
createConfig(),
|
||||||
|
generatorConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
@@ -293,9 +307,15 @@ describe('LoggingContentGenerator', () => {
|
|||||||
})(),
|
})(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const generatorConfig = {
|
||||||
|
model: 'test-model',
|
||||||
|
authType: AuthType.USE_OPENAI,
|
||||||
|
enableOpenAILogging: true,
|
||||||
|
};
|
||||||
const generator = new LoggingContentGenerator(
|
const generator = new LoggingContentGenerator(
|
||||||
wrapped,
|
wrapped,
|
||||||
createConfig({ enableOpenAILogging: true }),
|
createConfig(),
|
||||||
|
generatorConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
@@ -345,9 +365,15 @@ describe('LoggingContentGenerator', () => {
|
|||||||
})(),
|
})(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const generatorConfig = {
|
||||||
|
model: 'test-model',
|
||||||
|
authType: AuthType.USE_OPENAI,
|
||||||
|
enableOpenAILogging: true,
|
||||||
|
};
|
||||||
const generator = new LoggingContentGenerator(
|
const generator = new LoggingContentGenerator(
|
||||||
wrapped,
|
wrapped,
|
||||||
createConfig({ enableOpenAILogging: true }),
|
createConfig(),
|
||||||
|
generatorConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import {
|
|||||||
logApiRequest,
|
logApiRequest,
|
||||||
logApiResponse,
|
logApiResponse,
|
||||||
} from '../../telemetry/loggers.js';
|
} from '../../telemetry/loggers.js';
|
||||||
import type { ContentGenerator } from '../contentGenerator.js';
|
import type {
|
||||||
|
ContentGenerator,
|
||||||
|
ContentGeneratorConfig,
|
||||||
|
} from '../contentGenerator.js';
|
||||||
import { isStructuredError } from '../../utils/quotaErrorDetection.js';
|
import { isStructuredError } from '../../utils/quotaErrorDetection.js';
|
||||||
import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js';
|
import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js';
|
||||||
import { OpenAILogger } from '../../utils/openaiLogger.js';
|
import { OpenAILogger } from '../../utils/openaiLogger.js';
|
||||||
@@ -50,9 +53,11 @@ export class LoggingContentGenerator implements ContentGenerator {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly wrapped: ContentGenerator,
|
private readonly wrapped: ContentGenerator,
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
|
generatorConfig: ContentGeneratorConfig,
|
||||||
) {
|
) {
|
||||||
const generatorConfig = this.config.getContentGeneratorConfig();
|
// Extract fields needed for initialization from passed config
|
||||||
if (generatorConfig?.enableOpenAILogging) {
|
// (config.getContentGeneratorConfig() may not be available yet during refreshAuth)
|
||||||
|
if (generatorConfig.enableOpenAILogging) {
|
||||||
this.openaiLogger = new OpenAILogger(generatorConfig.openAILoggingDir);
|
this.openaiLogger = new OpenAILogger(generatorConfig.openAILoggingDir);
|
||||||
this.schemaCompliance = generatorConfig.schemaCompliance;
|
this.schemaCompliance = generatorConfig.schemaCompliance;
|
||||||
}
|
}
|
||||||
@@ -89,7 +94,7 @@ export class LoggingContentGenerator implements ContentGenerator {
|
|||||||
model,
|
model,
|
||||||
durationMs,
|
durationMs,
|
||||||
prompt_id,
|
prompt_id,
|
||||||
this.config.getContentGeneratorConfig()?.authType,
|
this.config.getAuthType(),
|
||||||
usageMetadata,
|
usageMetadata,
|
||||||
responseText,
|
responseText,
|
||||||
),
|
),
|
||||||
@@ -126,7 +131,7 @@ export class LoggingContentGenerator implements ContentGenerator {
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
durationMs,
|
durationMs,
|
||||||
prompt_id,
|
prompt_id,
|
||||||
this.config.getContentGeneratorConfig()?.authType,
|
this.config.getAuthType(),
|
||||||
errorType,
|
errorType,
|
||||||
errorStatus,
|
errorStatus,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export class SkillManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.watchStarted = true;
|
this.watchStarted = true;
|
||||||
|
await this.ensureUserSkillsDir();
|
||||||
await this.refreshCache();
|
await this.refreshCache();
|
||||||
this.updateWatchersFromCache();
|
this.updateWatchersFromCache();
|
||||||
}
|
}
|
||||||
@@ -486,29 +487,14 @@ export class SkillManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateWatchersFromCache(): void {
|
private updateWatchersFromCache(): void {
|
||||||
const desiredPaths = new Set<string>();
|
const watchTargets = new Set<string>(
|
||||||
|
(['project', 'user'] as const)
|
||||||
for (const level of ['project', 'user'] as const) {
|
.map((level) => this.getSkillsBaseDir(level))
|
||||||
const baseDir = this.getSkillsBaseDir(level);
|
.filter((baseDir) => fsSync.existsSync(baseDir)),
|
||||||
const parentDir = path.dirname(baseDir);
|
);
|
||||||
if (fsSync.existsSync(parentDir)) {
|
|
||||||
desiredPaths.add(parentDir);
|
|
||||||
}
|
|
||||||
if (fsSync.existsSync(baseDir)) {
|
|
||||||
desiredPaths.add(baseDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
const levelSkills = this.skillsCache?.get(level) || [];
|
|
||||||
for (const skill of levelSkills) {
|
|
||||||
const skillDir = path.dirname(skill.filePath);
|
|
||||||
if (fsSync.existsSync(skillDir)) {
|
|
||||||
desiredPaths.add(skillDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const existingPath of this.watchers.keys()) {
|
for (const existingPath of this.watchers.keys()) {
|
||||||
if (!desiredPaths.has(existingPath)) {
|
if (!watchTargets.has(existingPath)) {
|
||||||
void this.watchers
|
void this.watchers
|
||||||
.get(existingPath)
|
.get(existingPath)
|
||||||
?.close()
|
?.close()
|
||||||
@@ -522,7 +508,7 @@ export class SkillManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const watchPath of desiredPaths) {
|
for (const watchPath of watchTargets) {
|
||||||
if (this.watchers.has(watchPath)) {
|
if (this.watchers.has(watchPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -557,4 +543,16 @@ export class SkillManager {
|
|||||||
void this.refreshCache().then(() => this.updateWatchersFromCache());
|
void this.refreshCache().then(() => this.updateWatchersFromCache());
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureUserSkillsDir(): Promise<void> {
|
||||||
|
const baseDir = this.getSkillsBaseDir('user');
|
||||||
|
try {
|
||||||
|
await fs.mkdir(baseDir, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to create user skills directory at ${baseDir}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class SkillTool extends BaseDeclarativeTool<SkillParams, ToolResult> {
|
|||||||
false, // canUpdateOutput
|
false, // canUpdateOutput
|
||||||
);
|
);
|
||||||
|
|
||||||
this.skillManager = config.getSkillManager();
|
this.skillManager = config.getSkillManager()!;
|
||||||
this.skillManager.addChangeListener(() => {
|
this.skillManager.addChangeListener(() => {
|
||||||
void this.refreshSkills();
|
void this.refreshSkills();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code-test-utils",
|
"name": "@qwen-code/qwen-code-test-utils",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user