diff --git a/CHANGELOG.md b/CHANGELOG.md index 2686ff5..ec0e8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.13.0] - Unreleased +### Added: + +- Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only + +### Fixed: + +- Diagnostics: Language model diagnostic now correctly targets plugins that can use a local language model (OpenAIMockResponsePlugin, OpenApiSpecGeneratorPlugin, TypeSpecGeneratorPlugin) and shows as an informational hint instead of a warning + ## [1.12.0] - 2026-01-29 ### Added: diff --git a/README.md b/README.md index 31cf367..91258ff 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Real-time validation of your configuration files. Click any diagnostic code to v | `invalidSchema` | Schema version doesn't match installed Dev Proxy | | `invalidConfigSection` | Config section not used by any plugin | | `deprecatedPluginPath` | Using old plugin DLL path (pre-v0.29) | -| `missingLanguageModel` | Plugin requires language model configuration | +| `missingLanguageModel` | Plugin can use a local language model to enhance its output | | `noEnabledPlugins` | No plugins are enabled | | `reporterPosition` | Reporter plugin should be last | | `summaryWithoutReporter` | Summary plugin needs a reporter | @@ -196,7 +196,7 @@ One-click fixes for common issues: - **Update schema** - Match schema to installed Dev Proxy version (config file or config sections) - **Update plugin path** - Fix deprecated `dev-proxy-plugins.dll` paths (single or all at once) - **Remove unknown property** - Remove properties not defined in config section schema -- **Add languageModel configuration** - Enable language model for AI plugins +- **Enable local language model** - Add or update `languageModel.enabled: true` for plugins that support it - **Add plugin configuration** - Add optional config section for plugins that support it - **Add missing config section** - Create config section when plugin references one that doesn't exist diff --git a/src/code-actions.ts b/src/code-actions.ts index 593a49a..26e389f 100644 --- a/src/code-actions.ts +++ b/src/code-actions.ts @@ -269,7 +269,7 @@ function registerLanguageModelFixes(context: vscode.ExtensionContext) { } const fix = new vscode.CodeAction( - 'Add languageModel configuration', + 'Enable local language model', vscode.CodeActionKind.QuickFix, ); diff --git a/src/data/index.ts b/src/data/index.ts index 155df5b..8942864 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -48,7 +48,7 @@ export function getPluginDocUrl(name: string): string | undefined { */ export function getLanguageModelPlugins(): string[] { return Object.entries(pluginSnippets) - .filter(([_, config]) => config.requiresLanguageModel) + .filter(([_, config]) => config.usesLanguageModel) .map(([name]) => name); } diff --git a/src/data/plugins.json b/src/data/plugins.json index 3e04f90..471274d 100644 --- a/src/data/plugins.json +++ b/src/data/plugins.json @@ -133,16 +133,14 @@ "config": { "name": "devproxy-plugin-language-model-failure-config", "required": true - }, - "requiresLanguageModel": true + } }, "LanguageModelRateLimitingPlugin": { "instance": "devproxy-plugin-language-model-rate-limiting", "config": { "name": "devproxy-plugin-language-model-rate-limiting-config", "required": true - }, - "requiresLanguageModel": true + } }, "LatencyPlugin": { "instance": "devproxy-plugin-latency", @@ -203,7 +201,8 @@ "instance": "devproxy-plugin-odsp-search-guidance" }, "OpenAIMockResponsePlugin": { - "instance": "devproxy-plugin-openai-mock-response" + "instance": "devproxy-plugin-openai-mock-response", + "usesLanguageModel": true }, "OpenAITelemetryPlugin": { "instance": "devproxy-plugin-openai-telemetry", @@ -220,7 +219,8 @@ "config": { "name": "devproxy-plugin-open-api-spec-generator-config", "required": false - } + }, + "usesLanguageModel": true }, "RateLimitingPlugin": { "instance": "devproxy-plugin-rate-limiting", @@ -244,7 +244,8 @@ "config": { "name": "devproxy-plugin-typespec-generator-config", "required": false - } + }, + "usesLanguageModel": true }, "UrlDiscoveryPlugin": { "instance": "devproxy-plugin-url-discovery" diff --git a/src/data/plugins.schema.json b/src/data/plugins.schema.json index df4d313..8e8ca5c 100644 --- a/src/data/plugins.schema.json +++ b/src/data/plugins.schema.json @@ -49,9 +49,9 @@ } } }, - "requiresLanguageModel": { + "usesLanguageModel": { "type": "boolean", - "description": "Whether the plugin requires a language model to be configured" + "description": "Whether the plugin can use a local language model to enhance its output" } } }, diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 6775433..b4007f6 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -583,7 +583,7 @@ function checkLanguageModelRequirements( const pluginName = (pluginNameNode.value as parse.LiteralNode).value as string; const pluginSnippet = pluginSnippets[pluginName]; - if (!pluginSnippet?.requiresLanguageModel) { + if (!pluginSnippet?.usesLanguageModel) { return; } @@ -599,8 +599,8 @@ function checkLanguageModelRequirements( if (isPluginEnabled && !isLanguageModelEnabled) { const diagnostic = new vscode.Diagnostic( getRangeFromASTNode(pluginNameNode.value), - `${pluginName} requires languageModel.enabled to be set to true.`, - vscode.DiagnosticSeverity.Warning, + `${pluginName} can use a local language model to enhance its output. Add languageModel configuration to enable this.`, + vscode.DiagnosticSeverity.Information, ); diagnostic.code = getDiagnosticCode(DiagnosticCodes.missingLanguageModel); diagnostics.push(diagnostic); diff --git a/src/test/code-actions.test.ts b/src/test/code-actions.test.ts index cbccd23..d64b741 100644 --- a/src/test/code-actions.test.ts +++ b/src/test/code-actions.test.ts @@ -372,7 +372,7 @@ suite('Code Actions', () => { vscode.CodeActionKind.QuickFix.value ); - const lmFix = codeActions?.find(a => a.title === 'Add languageModel configuration'); + const lmFix = codeActions?.find(a => a.title === 'Enable local language model'); assert.strictEqual(lmFix, undefined, 'Should not provide fix without diagnostic'); }); }); @@ -506,7 +506,7 @@ suite('Language Model Code Action Logic', () => { const docContent = `{ "plugins": [ { - "name": "LanguageModelFailurePlugin" + "name": "OpenAIMockResponsePlugin" } ], "languageModel": { @@ -526,7 +526,7 @@ suite('Language Model Code Action Logic', () => { const docContent = `{ "plugins": [ { - "name": "LanguageModelFailurePlugin" + "name": "OpenAIMockResponsePlugin" } ], "languageModel": { @@ -546,7 +546,7 @@ suite('Language Model Code Action Logic', () => { const docContent = `{ "plugins": [ { - "name": "LanguageModelFailurePlugin" + "name": "OpenAIMockResponsePlugin" } ] }`; diff --git a/src/test/data.test.ts b/src/test/data.test.ts index 941a9ab..83fdc43 100644 --- a/src/test/data.test.ts +++ b/src/test/data.test.ts @@ -62,16 +62,17 @@ suite('getLanguageModelPlugins', () => { assert.ok(Array.isArray(plugins), 'Should return an array'); }); - test('should include known language model plugins', () => { + test('should include plugins that use a language model', () => { const plugins = getLanguageModelPlugins(); - assert.ok( - plugins.includes('LanguageModelFailurePlugin'), - 'Should include LanguageModelFailurePlugin' - ); - assert.ok( - plugins.includes('LanguageModelRateLimitingPlugin'), - 'Should include LanguageModelRateLimitingPlugin' - ); + assert.ok(plugins.includes('OpenAIMockResponsePlugin'), 'Should include OpenAIMockResponsePlugin'); + assert.ok(plugins.includes('OpenApiSpecGeneratorPlugin'), 'Should include OpenApiSpecGeneratorPlugin'); + assert.ok(plugins.includes('TypeSpecGeneratorPlugin'), 'Should include TypeSpecGeneratorPlugin'); + }); + + test('should not include language model failure and rate limiting plugins', () => { + const plugins = getLanguageModelPlugins(); + assert.ok(!plugins.includes('LanguageModelFailurePlugin')); + assert.ok(!plugins.includes('LanguageModelRateLimitingPlugin')); }); test('should not include non-language-model plugins', () => { diff --git a/src/test/examples/config-language-model-disabled.json b/src/test/examples/config-language-model-disabled.json index 2703a8b..b7841fd 100644 --- a/src/test/examples/config-language-model-disabled.json +++ b/src/test/examples/config-language-model-disabled.json @@ -2,18 +2,14 @@ "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.0/rc.schema.json", "plugins": [ { - "name": "LanguageModelFailurePlugin", + "name": "OpenAIMockResponsePlugin", "enabled": true, - "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", - "configSection": "devproxy-plugin-language-model-failure-config" + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" } ], "urlsToWatch": ["https://api.openai.com/*"], "logLevel": "information", "languageModel": { "enabled": false - }, - "devproxy-plugin-language-model-failure-config": { - "failureRate": 10 } } diff --git a/src/test/examples/config-language-model-enabled.json b/src/test/examples/config-language-model-enabled.json index 6641015..0c21b24 100644 --- a/src/test/examples/config-language-model-enabled.json +++ b/src/test/examples/config-language-model-enabled.json @@ -2,27 +2,19 @@ "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.0/rc.schema.json", "plugins": [ { - "name": "LanguageModelFailurePlugin", + "name": "OpenAIMockResponsePlugin", "enabled": true, - "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", - "configSection": "devproxy-plugin-language-model-failure-config" + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" }, { - "name": "LanguageModelRateLimitingPlugin", + "name": "OpenApiSpecGeneratorPlugin", "enabled": true, - "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", - "configSection": "devproxy-plugin-language-model-rate-limiting-config" + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" } ], "urlsToWatch": ["https://api.openai.com/*"], "logLevel": "information", "languageModel": { "enabled": true - }, - "devproxy-plugin-language-model-failure-config": { - "failureRate": 10 - }, - "devproxy-plugin-language-model-rate-limiting-config": { - "requestsPerMinute": 100 } } diff --git a/src/test/examples/config-language-model-required.json b/src/test/examples/config-language-model-required.json index f615726..3b22e57 100644 --- a/src/test/examples/config-language-model-required.json +++ b/src/test/examples/config-language-model-required.json @@ -2,24 +2,16 @@ "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.0/rc.schema.json", "plugins": [ { - "name": "LanguageModelFailurePlugin", + "name": "OpenAIMockResponsePlugin", "enabled": true, - "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", - "configSection": "devproxy-plugin-language-model-failure-config" + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" }, { - "name": "LanguageModelRateLimitingPlugin", + "name": "OpenApiSpecGeneratorPlugin", "enabled": true, - "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", - "configSection": "devproxy-plugin-language-model-rate-limiting-config" + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll" } ], "urlsToWatch": ["https://api.openai.com/*"], - "logLevel": "information", - "devproxy-plugin-language-model-failure-config": { - "failureRate": 10 - }, - "devproxy-plugin-language-model-rate-limiting-config": { - "requestsPerMinute": 100 - } + "logLevel": "information" } diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 58ed363..843b521 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import { DevProxyInstall } from '../types'; @@ -8,7 +9,19 @@ import { DevProxyInstall } from '../types'; * avoiding the need to copy them to out/ during build. */ export function getFixturePath(fileName: string): string { - // process.cwd() is the workspace root when running tests via VS Code test runner + const candidateRoots = [ + process.cwd(), + path.resolve(__dirname, '..', '..'), + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath, + ].filter((root): root is string => Boolean(root)); + + for (const root of candidateRoots) { + const fixturePath = path.resolve(root, 'src', 'test', 'examples', fileName); + if (fs.existsSync(fixturePath)) { + return fixturePath; + } + } + return path.resolve(process.cwd(), 'src', 'test', 'examples', fileName); } diff --git a/src/test/shell.test.ts b/src/test/shell.test.ts index 7c6749b..310a757 100644 --- a/src/test/shell.test.ts +++ b/src/test/shell.test.ts @@ -3,8 +3,6 @@ * Tests for pure utility functions in shell.ts. */ import * as assert from 'assert'; -import * as sinon from 'sinon'; -import * as fs from 'fs'; import { getPackageIdentifier, resolveDevProxyExecutable } from '../utils/shell'; import { PackageManager, @@ -44,15 +42,7 @@ suite('getPackageIdentifier', () => { }); suite('resolveDevProxyExecutable', () => { - let existsSyncStub: sinon.SinonStub; - - setup(() => { - existsSyncStub = sinon.stub(fs, 'existsSync'); - }); - - teardown(() => { - sinon.restore(); - }); + const NONEXISTENT_DEVPROXY_COMMAND = 'devproxy-command-that-does-not-exist-for-tests'; test('should return custom path when provided and non-empty', async () => { const customPath = '/custom/path/to/devproxy'; @@ -67,22 +57,18 @@ suite('resolveDevProxyExecutable', () => { }); test('should ignore empty custom path and proceed with auto-detection', async () => { - // With empty custom path and no auto-detection success, should return bare command - existsSyncStub.returns(false); - const result = await resolveDevProxyExecutable('devproxy', ''); + const result = await resolveDevProxyExecutable(NONEXISTENT_DEVPROXY_COMMAND, ''); // Will fall through to bare command since nothing else succeeds - assert.strictEqual(result, 'devproxy'); + assert.strictEqual(result, NONEXISTENT_DEVPROXY_COMMAND); }); test('should ignore whitespace-only custom path', async () => { - existsSyncStub.returns(false); - const result = await resolveDevProxyExecutable('devproxy', ' '); - assert.strictEqual(result, 'devproxy'); + const result = await resolveDevProxyExecutable(NONEXISTENT_DEVPROXY_COMMAND, ' '); + assert.strictEqual(result, NONEXISTENT_DEVPROXY_COMMAND); }); test('should handle undefined custom path', async () => { - existsSyncStub.returns(false); - const result = await resolveDevProxyExecutable('devproxy', undefined); - assert.strictEqual(result, 'devproxy'); + const result = await resolveDevProxyExecutable(NONEXISTENT_DEVPROXY_COMMAND, undefined); + assert.strictEqual(result, NONEXISTENT_DEVPROXY_COMMAND); }); }); diff --git a/src/types.ts b/src/types.ts index 4d3b675..f242cb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ export type PluginSnippets = { [key: string]: { instance: string; config?: PluginConfig; - requiresLanguageModel?: boolean; + usesLanguageModel?: boolean; }; };