Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.13.0] - Unreleased

### 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
- Quick Fixes: Enable local language model fix now adds or updates `languageModel.enabled: true` for supported plugins only

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot this should be included in a new section called Added, we didn't fix it, we added it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in b9fb471. I moved that quick-fix entry into a new Added section under 1.13.0 and left the diagnostics item under Fixed. Screenshot: https://github.com/user-attachments/assets/6fa9af5d-9a9e-4b4c-8485-70dac16a1b01


## [1.12.0] - 2026-01-29

### Added:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/code-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function registerLanguageModelFixes(context: vscode.ExtensionContext) {
}

const fix = new vscode.CodeAction(
'Add languageModel configuration',
'Enable local language model',
vscode.CodeActionKind.QuickFix,
);

Expand Down
2 changes: 1 addition & 1 deletion src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
15 changes: 8 additions & 7 deletions src/data/plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -220,7 +219,8 @@
"config": {
"name": "devproxy-plugin-open-api-spec-generator-config",
"required": false
}
},
"usesLanguageModel": true
},
"RateLimitingPlugin": {
"instance": "devproxy-plugin-rate-limiting",
Expand All @@ -244,7 +244,8 @@
"config": {
"name": "devproxy-plugin-typespec-generator-config",
"required": false
}
},
"usesLanguageModel": true
},
"UrlDiscoveryPlugin": {
"instance": "devproxy-plugin-url-discovery"
Expand Down
4 changes: 2 additions & 2 deletions src/data/plugins.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
},
Expand Down
6 changes: 3 additions & 3 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/test/code-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Expand Down Expand Up @@ -506,7 +506,7 @@ suite('Language Model Code Action Logic', () => {
const docContent = `{
"plugins": [
{
"name": "LanguageModelFailurePlugin"
"name": "OpenAIMockResponsePlugin"
}
],
"languageModel": {
Expand All @@ -526,7 +526,7 @@ suite('Language Model Code Action Logic', () => {
const docContent = `{
"plugins": [
{
"name": "LanguageModelFailurePlugin"
"name": "OpenAIMockResponsePlugin"
}
],
"languageModel": {
Expand All @@ -546,7 +546,7 @@ suite('Language Model Code Action Logic', () => {
const docContent = `{
"plugins": [
{
"name": "LanguageModelFailurePlugin"
"name": "OpenAIMockResponsePlugin"
}
]
}`;
Expand Down
19 changes: 10 additions & 9 deletions src/test/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
8 changes: 2 additions & 6 deletions src/test/examples/config-language-model-disabled.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
16 changes: 4 additions & 12 deletions src/test/examples/config-language-model-enabled.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
18 changes: 5 additions & 13 deletions src/test/examples/config-language-model-required.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
15 changes: 14 additions & 1 deletion src/test/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { DevProxyInstall } from '../types';
Expand All @@ -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);
}

Expand Down
28 changes: 7 additions & 21 deletions src/test/shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand All @@ -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);
});
});
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type PluginSnippets = {
[key: string]: {
instance: string;
config?: PluginConfig;
requiresLanguageModel?: boolean;
usesLanguageModel?: boolean;
};
};

Expand Down