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

- Code actions: Support for JSONC files
- Code lens: Support for JSONC files
- Diagnostics: Added warnings for plugins that require `languageModel.enabled: true` when not configured
- Code actions: Added quick-fix to automatically add or update `languageModel.enabled: true` configuration
- Commands: Added `dev-proxy-toolkit.addLanguageModelConfig` command to add language model configuration
- Plugin constants: Added `requiresLanguageModel` property to identify plugins requiring language model functionality
- Snippets: Added `devproxy-plugin-language-model-failure` - LanguageModelFailurePlugin instance
- Snippets: Added `devproxy-plugin-language-model-failure-config` - LanguageModelFailurePlugin config section
- Snippets: Added `devproxy-plugin-language-model-rate-limiting` - LanguageModelRateLimitingPlugin instance
Expand Down
126 changes: 126 additions & 0 deletions src/codeactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as vscode from 'vscode';
import {DevProxyInstall} from './types';
import parse from 'json-to-ast';
import { getASTNode, getRangeFromASTNode } from './helpers';

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

registerInvalidSchemaFixes(devProxyVersion, context);
registerDeprecatedPluginPathFixes(context);
registerLanguageModelFixes(context);
};

function registerInvalidSchemaFixes(
Expand Down Expand Up @@ -130,3 +133,126 @@ function registerDeprecatedPluginPathFixes(context: vscode.ExtensionContext) {
),
);
}

function registerLanguageModelFixes(context: vscode.ExtensionContext) {
const languageModelMissing: vscode.CodeActionProvider = {
provideCodeActions: (document, range, context, token) => {
// Check if the current range intersects with a missing language model diagnostic
const currentDiagnostic = context.diagnostics.find(diagnostic => {
return (
diagnostic.code === 'missingLanguageModel' &&
diagnostic.range.intersection(range)
);
});

// Only provide language model actions if user is on a missing language model diagnostic
if (!currentDiagnostic) {
return [];
}

const fixes: vscode.CodeAction[] = [];

// Fix to add languageModel configuration
const addLanguageModelFix = new vscode.CodeAction(
'Add languageModel configuration',
vscode.CodeActionKind.QuickFix,
);

addLanguageModelFix.edit = new vscode.WorkspaceEdit();

try {
// Parse the document using json-to-ast for accurate insertion
const documentNode = parse(document.getText()) as parse.ObjectNode;

// Check if languageModel already exists
const existingLanguageModel = getASTNode(
documentNode.children,
'Identifier',
'languageModel'
);

if (existingLanguageModel) {
// languageModel exists but enabled might be false or missing
const languageModelObjectNode = existingLanguageModel.value as parse.ObjectNode;
const enabledNode = getASTNode(
languageModelObjectNode.children,
'Identifier',
'enabled'
);

if (enabledNode) {
// Replace the enabled value
addLanguageModelFix.edit.replace(
document.uri,
getRangeFromASTNode(enabledNode.value),
'true'
);
} else {
// Add enabled property
const insertPosition = new vscode.Position(
languageModelObjectNode.loc!.end.line - 1,
languageModelObjectNode.loc!.end.column - 1
);
addLanguageModelFix.edit.insert(
document.uri,
insertPosition,
'\n "enabled": true'
);
}
} else {
// Add new languageModel object
// Find the last property to insert after it
const lastProperty = documentNode.children[documentNode.children.length - 1] as parse.PropertyNode;
const insertPosition = new vscode.Position(
lastProperty.loc!.end.line - 1,
lastProperty.loc!.end.column
);

addLanguageModelFix.edit.insert(
document.uri,
insertPosition,
',\n "languageModel": {\n "enabled": true\n }'
);
}
} catch (error) {
// Fallback to simple text-based insertion
const documentText = document.getText();
const lines = documentText.split('\n');

// Find where to insert the languageModel config
let insertLine = lines.length - 1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].includes('}')) {
insertLine = i;
break;
}
}

const hasContentBefore = lines.slice(0, insertLine).some(line =>
line.trim() && !line.trim().startsWith('{') && !line.trim().startsWith('/*') && !line.trim().startsWith('*')
);

const languageModelConfig = hasContentBefore ?
',\n "languageModel": {\n "enabled": true\n }' :
' "languageModel": {\n "enabled": true\n }';

const insertPosition = new vscode.Position(insertLine, 0);
addLanguageModelFix.edit.insert(document.uri, insertPosition, languageModelConfig + '\n');
}

addLanguageModelFix.isPreferred = true;
fixes.push(addLanguageModelFix);

return fixes;
},
};

// Code action for missing language model configuration
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider('json', languageModelMissing),
);

context.subscriptions.push(
vscode.languages.registerCodeActionsProvider('jsonc', languageModelMissing),
);
}
98 changes: 97 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as vscode from 'vscode';
import { pluginDocs } from './constants';
import { VersionPreference } from './enums';
import { executeCommand, isConfigFile, openUpgradeDocumentation, upgradeDevProxyWithPackageManager } from './helpers';
import { executeCommand, isConfigFile, openUpgradeDocumentation, upgradeDevProxyWithPackageManager, getASTNode, getRangeFromASTNode } from './helpers';
import { isDevProxyRunning, getDevProxyExe } from './detect';
import parse from 'json-to-ast';

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

context.subscriptions.push(
vscode.commands.registerCommand(
'dev-proxy-toolkit.addLanguageModelConfig',
async (uri: vscode.Uri) => {
const document = await vscode.workspace.openTextDocument(uri);
const edit = new vscode.WorkspaceEdit();

try {
// Parse the document using json-to-ast for accurate insertion
const documentNode = parse(document.getText()) as parse.ObjectNode;

// Check if languageModel already exists
const existingLanguageModel = getASTNode(
documentNode.children,
'Identifier',
'languageModel'
);

if (existingLanguageModel) {
// languageModel exists but enabled might be false or missing
const languageModelObjectNode = existingLanguageModel.value as parse.ObjectNode;
const enabledNode = getASTNode(
languageModelObjectNode.children,
'Identifier',
'enabled'
);

if (enabledNode) {
// Replace the enabled value
edit.replace(
uri,
getRangeFromASTNode(enabledNode.value),
'true'
);
} else {
// Add enabled property
const insertPosition = new vscode.Position(
languageModelObjectNode.loc!.end.line - 1,
languageModelObjectNode.loc!.end.column - 1
);
edit.insert(
uri,
insertPosition,
'\n "enabled": true'
);
}
} else {
// Add new languageModel object
// Find the last property to insert after it
const lastProperty = documentNode.children[documentNode.children.length - 1] as parse.PropertyNode;
const insertPosition = new vscode.Position(
lastProperty.loc!.end.line - 1,
lastProperty.loc!.end.column
);

edit.insert(
uri,
insertPosition,
',\n "languageModel": {\n "enabled": true\n }'
);
}
} catch (error) {
// Fallback to simple text-based insertion
const documentText = document.getText();
const lines = documentText.split('\n');

// Find where to insert the languageModel config
let insertLine = lines.length - 1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].includes('}')) {
insertLine = i;
break;
}
}

const hasContentBefore = lines.slice(0, insertLine).some(line =>
line.trim() && !line.trim().startsWith('{') && !line.trim().startsWith('/*') && !line.trim().startsWith('*')
);

const languageModelConfig = hasContentBefore ?
',\n "languageModel": {\n "enabled": true\n }' :
' "languageModel": {\n "enabled": true\n }';

const insertPosition = new vscode.Position(insertLine, 0);
edit.insert(uri, insertPosition, languageModelConfig + '\n');
}

await vscode.workspace.applyEdit(edit);
await document.save();

vscode.window.showInformationMessage('Language model configuration added');
}
)
);

context.subscriptions.push(
vscode.commands.registerCommand('dev-proxy-toolkit.upgrade', async () => {
const platform = process.platform;
Expand Down
6 changes: 4 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,16 @@ export const pluginSnippets: PluginSnippets = {
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
74 changes: 74 additions & 0 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const updateConfigFileDiagnostics = (
checkSchemaCompatibility(documentNode, devProxyInstall, diagnostics);
checkPlugins(pluginsNode, diagnostics, documentNode, devProxyInstall);
checkConfigSection(documentNode, diagnostics);
checkLanguageModelRequirements(documentNode, diagnostics);

collection.set(document.uri, diagnostics);
};
Expand Down Expand Up @@ -495,3 +496,76 @@ function checkDeprecatedPluginPath(
}
});
}

function checkLanguageModelRequirements(
documentNode: parse.ObjectNode,
diagnostics: vscode.Diagnostic[],
) {
const pluginsNode = getPluginsNode(documentNode);

if (!pluginsNode || pluginsNode.value.type !== 'Array') {
return;
}

const pluginNodes = (pluginsNode.value as parse.ArrayNode)
.children as parse.ObjectNode[];

// Check if languageModel is enabled
const languageModelNode = getASTNode(
documentNode.children,
'Identifier',
'languageModel'
);
let isLanguageModelEnabled = false;

if (languageModelNode && languageModelNode.value.type === 'Object') {
const languageModelObjectNode = languageModelNode.value as parse.ObjectNode;
const enabledNode = getASTNode(
languageModelObjectNode.children,
'Identifier',
'enabled'
);
if (enabledNode && enabledNode.value.type === 'Literal') {
isLanguageModelEnabled = (enabledNode.value as parse.LiteralNode).value as boolean;
}
}

// Check each plugin that requires language model
pluginNodes.forEach((pluginNode: parse.ObjectNode) => {
const pluginNameNode = getASTNode(
pluginNode.children,
'Identifier',
'name',
);

if (!pluginNameNode) {
return;
}

const pluginName = (pluginNameNode.value as parse.LiteralNode).value as string;
const pluginSnippet = pluginSnippets[pluginName];

if (!pluginSnippet?.requiresLanguageModel) {
return;
}

// Check if plugin is enabled
const enabledNode = getASTNode(
pluginNode.children,
'Identifier',
'enabled',
);
const isPluginEnabled = enabledNode ?
(enabledNode.value as parse.LiteralNode).value as boolean : false;

if (isPluginEnabled && !isLanguageModelEnabled) {
const diagnostic = new vscode.Diagnostic(
getRangeFromASTNode(pluginNameNode.value),
`${pluginName} requires languageModel.enabled to be set to true.`,
vscode.DiagnosticSeverity.Warning,
);
diagnostic.code = 'missingLanguageModel';
diagnostics.push(diagnostic);
}
});
}
24 changes: 24 additions & 0 deletions src/test/.devproxy/devproxyrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.2/rc.schema.json",
"plugins": [
{
"name": "LanguageModelFailurePlugin",
"enabled": true,
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
"configSection": "languageModelFailurePlugin"
}
],
"urlsToWatch": [
"https://*.openai.com/*"
],
"languageModelFailurePlugin": {
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v1.0.0/languagemodelfailureplugin.schema.json",
"failures": ["rate-limit-reached", "quota-exceeded"]
},
"logLevel": "information",
"newVersionNotification": "stable",
"showSkipMessages": true,
"languageModel": {
"enabled": true
}
}
Loading
Loading