Skip to content

Commit 3d25b5f

Browse files
authored
Add warning when configuration requires languageModel.enabled. Closes #302 (#316)
1 parent 4cd3b7d commit 3d25b5f

10 files changed

+402
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- Code actions: Support for JSONC files
1515
- Code lens: Support for JSONC files
16+
- Diagnostics: Added warnings for plugins that require `languageModel.enabled: true` when not configured
17+
- Code actions: Added quick-fix to automatically add or update `languageModel.enabled: true` configuration
18+
- Commands: Added `dev-proxy-toolkit.addLanguageModelConfig` command to add language model configuration
19+
- Plugin constants: Added `requiresLanguageModel` property to identify plugins requiring language model functionality
1620
- Snippets: Added `devproxy-plugin-language-model-failure` - LanguageModelFailurePlugin instance
1721
- Snippets: Added `devproxy-plugin-language-model-failure-config` - LanguageModelFailurePlugin config section
1822
- Snippets: Added `devproxy-plugin-language-model-rate-limiting` - LanguageModelRateLimitingPlugin instance

src/codeactions.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as vscode from 'vscode';
22
import {DevProxyInstall} from './types';
3+
import parse from 'json-to-ast';
4+
import { getASTNode, getRangeFromASTNode } from './helpers';
35

46
export const registerCodeActions = (context: vscode.ExtensionContext) => {
57
const devProxyInstall =
@@ -15,6 +17,7 @@ export const registerCodeActions = (context: vscode.ExtensionContext) => {
1517

1618
registerInvalidSchemaFixes(devProxyVersion, context);
1719
registerDeprecatedPluginPathFixes(context);
20+
registerLanguageModelFixes(context);
1821
};
1922

2023
function registerInvalidSchemaFixes(
@@ -130,3 +133,126 @@ function registerDeprecatedPluginPathFixes(context: vscode.ExtensionContext) {
130133
),
131134
);
132135
}
136+
137+
function registerLanguageModelFixes(context: vscode.ExtensionContext) {
138+
const languageModelMissing: vscode.CodeActionProvider = {
139+
provideCodeActions: (document, range, context, token) => {
140+
// Check if the current range intersects with a missing language model diagnostic
141+
const currentDiagnostic = context.diagnostics.find(diagnostic => {
142+
return (
143+
diagnostic.code === 'missingLanguageModel' &&
144+
diagnostic.range.intersection(range)
145+
);
146+
});
147+
148+
// Only provide language model actions if user is on a missing language model diagnostic
149+
if (!currentDiagnostic) {
150+
return [];
151+
}
152+
153+
const fixes: vscode.CodeAction[] = [];
154+
155+
// Fix to add languageModel configuration
156+
const addLanguageModelFix = new vscode.CodeAction(
157+
'Add languageModel configuration',
158+
vscode.CodeActionKind.QuickFix,
159+
);
160+
161+
addLanguageModelFix.edit = new vscode.WorkspaceEdit();
162+
163+
try {
164+
// Parse the document using json-to-ast for accurate insertion
165+
const documentNode = parse(document.getText()) as parse.ObjectNode;
166+
167+
// Check if languageModel already exists
168+
const existingLanguageModel = getASTNode(
169+
documentNode.children,
170+
'Identifier',
171+
'languageModel'
172+
);
173+
174+
if (existingLanguageModel) {
175+
// languageModel exists but enabled might be false or missing
176+
const languageModelObjectNode = existingLanguageModel.value as parse.ObjectNode;
177+
const enabledNode = getASTNode(
178+
languageModelObjectNode.children,
179+
'Identifier',
180+
'enabled'
181+
);
182+
183+
if (enabledNode) {
184+
// Replace the enabled value
185+
addLanguageModelFix.edit.replace(
186+
document.uri,
187+
getRangeFromASTNode(enabledNode.value),
188+
'true'
189+
);
190+
} else {
191+
// Add enabled property
192+
const insertPosition = new vscode.Position(
193+
languageModelObjectNode.loc!.end.line - 1,
194+
languageModelObjectNode.loc!.end.column - 1
195+
);
196+
addLanguageModelFix.edit.insert(
197+
document.uri,
198+
insertPosition,
199+
'\n "enabled": true'
200+
);
201+
}
202+
} else {
203+
// Add new languageModel object
204+
// Find the last property to insert after it
205+
const lastProperty = documentNode.children[documentNode.children.length - 1] as parse.PropertyNode;
206+
const insertPosition = new vscode.Position(
207+
lastProperty.loc!.end.line - 1,
208+
lastProperty.loc!.end.column
209+
);
210+
211+
addLanguageModelFix.edit.insert(
212+
document.uri,
213+
insertPosition,
214+
',\n "languageModel": {\n "enabled": true\n }'
215+
);
216+
}
217+
} catch (error) {
218+
// Fallback to simple text-based insertion
219+
const documentText = document.getText();
220+
const lines = documentText.split('\n');
221+
222+
// Find where to insert the languageModel config
223+
let insertLine = lines.length - 1;
224+
for (let i = lines.length - 1; i >= 0; i--) {
225+
if (lines[i].includes('}')) {
226+
insertLine = i;
227+
break;
228+
}
229+
}
230+
231+
const hasContentBefore = lines.slice(0, insertLine).some(line =>
232+
line.trim() && !line.trim().startsWith('{') && !line.trim().startsWith('/*') && !line.trim().startsWith('*')
233+
);
234+
235+
const languageModelConfig = hasContentBefore ?
236+
',\n "languageModel": {\n "enabled": true\n }' :
237+
' "languageModel": {\n "enabled": true\n }';
238+
239+
const insertPosition = new vscode.Position(insertLine, 0);
240+
addLanguageModelFix.edit.insert(document.uri, insertPosition, languageModelConfig + '\n');
241+
}
242+
243+
addLanguageModelFix.isPreferred = true;
244+
fixes.push(addLanguageModelFix);
245+
246+
return fixes;
247+
},
248+
};
249+
250+
// Code action for missing language model configuration
251+
context.subscriptions.push(
252+
vscode.languages.registerCodeActionsProvider('json', languageModelMissing),
253+
);
254+
255+
context.subscriptions.push(
256+
vscode.languages.registerCodeActionsProvider('jsonc', languageModelMissing),
257+
);
258+
}

src/commands.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as vscode from 'vscode';
22
import { pluginDocs } from './constants';
33
import { VersionPreference } from './enums';
4-
import { executeCommand, isConfigFile, openUpgradeDocumentation, upgradeDevProxyWithPackageManager } from './helpers';
4+
import { executeCommand, isConfigFile, openUpgradeDocumentation, upgradeDevProxyWithPackageManager, getASTNode, getRangeFromASTNode } from './helpers';
55
import { isDevProxyRunning, getDevProxyExe } from './detect';
6+
import parse from 'json-to-ast';
67

78
export const registerCommands = (context: vscode.ExtensionContext, configuration: vscode.WorkspaceConfiguration) => {
89
const versionPreference = configuration.get('version') as VersionPreference;
@@ -78,6 +79,101 @@ export const registerCommands = (context: vscode.ExtensionContext, configuration
7879
)
7980
);
8081

82+
context.subscriptions.push(
83+
vscode.commands.registerCommand(
84+
'dev-proxy-toolkit.addLanguageModelConfig',
85+
async (uri: vscode.Uri) => {
86+
const document = await vscode.workspace.openTextDocument(uri);
87+
const edit = new vscode.WorkspaceEdit();
88+
89+
try {
90+
// Parse the document using json-to-ast for accurate insertion
91+
const documentNode = parse(document.getText()) as parse.ObjectNode;
92+
93+
// Check if languageModel already exists
94+
const existingLanguageModel = getASTNode(
95+
documentNode.children,
96+
'Identifier',
97+
'languageModel'
98+
);
99+
100+
if (existingLanguageModel) {
101+
// languageModel exists but enabled might be false or missing
102+
const languageModelObjectNode = existingLanguageModel.value as parse.ObjectNode;
103+
const enabledNode = getASTNode(
104+
languageModelObjectNode.children,
105+
'Identifier',
106+
'enabled'
107+
);
108+
109+
if (enabledNode) {
110+
// Replace the enabled value
111+
edit.replace(
112+
uri,
113+
getRangeFromASTNode(enabledNode.value),
114+
'true'
115+
);
116+
} else {
117+
// Add enabled property
118+
const insertPosition = new vscode.Position(
119+
languageModelObjectNode.loc!.end.line - 1,
120+
languageModelObjectNode.loc!.end.column - 1
121+
);
122+
edit.insert(
123+
uri,
124+
insertPosition,
125+
'\n "enabled": true'
126+
);
127+
}
128+
} else {
129+
// Add new languageModel object
130+
// Find the last property to insert after it
131+
const lastProperty = documentNode.children[documentNode.children.length - 1] as parse.PropertyNode;
132+
const insertPosition = new vscode.Position(
133+
lastProperty.loc!.end.line - 1,
134+
lastProperty.loc!.end.column
135+
);
136+
137+
edit.insert(
138+
uri,
139+
insertPosition,
140+
',\n "languageModel": {\n "enabled": true\n }'
141+
);
142+
}
143+
} catch (error) {
144+
// Fallback to simple text-based insertion
145+
const documentText = document.getText();
146+
const lines = documentText.split('\n');
147+
148+
// Find where to insert the languageModel config
149+
let insertLine = lines.length - 1;
150+
for (let i = lines.length - 1; i >= 0; i--) {
151+
if (lines[i].includes('}')) {
152+
insertLine = i;
153+
break;
154+
}
155+
}
156+
157+
const hasContentBefore = lines.slice(0, insertLine).some(line =>
158+
line.trim() && !line.trim().startsWith('{') && !line.trim().startsWith('/*') && !line.trim().startsWith('*')
159+
);
160+
161+
const languageModelConfig = hasContentBefore ?
162+
',\n "languageModel": {\n "enabled": true\n }' :
163+
' "languageModel": {\n "enabled": true\n }';
164+
165+
const insertPosition = new vscode.Position(insertLine, 0);
166+
edit.insert(uri, insertPosition, languageModelConfig + '\n');
167+
}
168+
169+
await vscode.workspace.applyEdit(edit);
170+
await document.save();
171+
172+
vscode.window.showInformationMessage('Language model configuration added');
173+
}
174+
)
175+
);
176+
81177
context.subscriptions.push(
82178
vscode.commands.registerCommand('dev-proxy-toolkit.upgrade', async () => {
83179
const platform = process.platform;

src/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,16 @@ export const pluginSnippets: PluginSnippets = {
116116
config: {
117117
name: 'devproxy-plugin-language-model-failure-config',
118118
required: true,
119-
}
119+
},
120+
requiresLanguageModel: true,
120121
},
121122
LanguageModelRateLimitingPlugin: {
122123
instance: 'devproxy-plugin-language-model-rate-limiting',
123124
config: {
124125
name: 'devproxy-plugin-language-model-rate-limiting-config',
125126
required: true,
126-
}
127+
},
128+
requiresLanguageModel: true,
127129
},
128130
LatencyPlugin: {
129131
instance: 'devproxy-plugin-latency',

src/diagnostics.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const updateConfigFileDiagnostics = (
2222
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);
2323
checkPlugins(pluginsNode, diagnostics, documentNode, devProxyInstall);
2424
checkConfigSection(documentNode, diagnostics);
25+
checkLanguageModelRequirements(documentNode, diagnostics);
2526

2627
collection.set(document.uri, diagnostics);
2728
};
@@ -495,3 +496,76 @@ function checkDeprecatedPluginPath(
495496
}
496497
});
497498
}
499+
500+
function checkLanguageModelRequirements(
501+
documentNode: parse.ObjectNode,
502+
diagnostics: vscode.Diagnostic[],
503+
) {
504+
const pluginsNode = getPluginsNode(documentNode);
505+
506+
if (!pluginsNode || pluginsNode.value.type !== 'Array') {
507+
return;
508+
}
509+
510+
const pluginNodes = (pluginsNode.value as parse.ArrayNode)
511+
.children as parse.ObjectNode[];
512+
513+
// Check if languageModel is enabled
514+
const languageModelNode = getASTNode(
515+
documentNode.children,
516+
'Identifier',
517+
'languageModel'
518+
);
519+
let isLanguageModelEnabled = false;
520+
521+
if (languageModelNode && languageModelNode.value.type === 'Object') {
522+
const languageModelObjectNode = languageModelNode.value as parse.ObjectNode;
523+
const enabledNode = getASTNode(
524+
languageModelObjectNode.children,
525+
'Identifier',
526+
'enabled'
527+
);
528+
if (enabledNode && enabledNode.value.type === 'Literal') {
529+
isLanguageModelEnabled = (enabledNode.value as parse.LiteralNode).value as boolean;
530+
}
531+
}
532+
533+
// Check each plugin that requires language model
534+
pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
535+
const pluginNameNode = getASTNode(
536+
pluginNode.children,
537+
'Identifier',
538+
'name',
539+
);
540+
541+
if (!pluginNameNode) {
542+
return;
543+
}
544+
545+
const pluginName = (pluginNameNode.value as parse.LiteralNode).value as string;
546+
const pluginSnippet = pluginSnippets[pluginName];
547+
548+
if (!pluginSnippet?.requiresLanguageModel) {
549+
return;
550+
}
551+
552+
// Check if plugin is enabled
553+
const enabledNode = getASTNode(
554+
pluginNode.children,
555+
'Identifier',
556+
'enabled',
557+
);
558+
const isPluginEnabled = enabledNode ?
559+
(enabledNode.value as parse.LiteralNode).value as boolean : false;
560+
561+
if (isPluginEnabled && !isLanguageModelEnabled) {
562+
const diagnostic = new vscode.Diagnostic(
563+
getRangeFromASTNode(pluginNameNode.value),
564+
`${pluginName} requires languageModel.enabled to be set to true.`,
565+
vscode.DiagnosticSeverity.Warning,
566+
);
567+
diagnostic.code = 'missingLanguageModel';
568+
diagnostics.push(diagnostic);
569+
}
570+
});
571+
}

src/test/.devproxy/devproxyrc.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.2/rc.schema.json",
3+
"plugins": [
4+
{
5+
"name": "LanguageModelFailurePlugin",
6+
"enabled": true,
7+
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
8+
"configSection": "languageModelFailurePlugin"
9+
}
10+
],
11+
"urlsToWatch": [
12+
"https://*.openai.com/*"
13+
],
14+
"languageModelFailurePlugin": {
15+
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v1.0.0/languagemodelfailureplugin.schema.json",
16+
"failures": ["rate-limit-reached", "quota-exceeded"]
17+
},
18+
"logLevel": "information",
19+
"newVersionNotification": "stable",
20+
"showSkipMessages": true,
21+
"languageModel": {
22+
"enabled": true
23+
}
24+
}

0 commit comments

Comments
 (0)