Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Positron Assistant: Web compatibility #6506

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
84 changes: 6 additions & 78 deletions extensions/positron-assistant/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@
{
"command": "positron-assistant.addModelConfiguration",
"title": "%commands.addModelConfiguration.title%",
"category": "%commands.addModelConfiguration.category%",
"enablement": "isDevelopment"
"category": "%commands.category%"
},
{
"command": "positron-assistant.configureModels",
"title": "%commands.configureModels.title%",
"category": "%commands.category%"
}
],
"languageModels": [
Expand All @@ -53,82 +57,6 @@
"type": "boolean",
"default": false,
"description": "%configuration.enable.description%"
},
"positron.assistant.models": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "%configuration.models.id.description%"
},
"name": {
"type": "string",
"description": "%configuration.models.name.description%"
},
"model": {
"type": "string",
"description": "%configuration.models.model.description%"
},
"baseUrl": {
"type": "string",
"description": "%configuration.models.baseUrl.description%"
},
"numCtx": {
"type": "number",
"description": "%configuration.models.numCtx.description%"
},
"toolCalls": {
"type": "boolean",
"description": "%configuration.models.toolCalls.description%"
},
"project": {
"type": "string",
"description": "%configuration.models.project.description%"
},
"location": {
"type": "string",
"description": "%configuration.models.location.description%"
},
"resourceName": {
"type": "string",
"description": "%configuration.models.resourceName.description%"
},
"provider": {
"type": "string",
"enum": [
"anthropic",
"azure",
"echo",
"error",
"google",
"mistral",
"ollama",
"openai",
"openai-legacy",
"openrouter",
"bedrock",
"vertex",
"vertex-legacy"
],
"description": "%configuration.models.provider.description%"
},
"type": {
"enum": [
"chat",
"completion"
],
"description": "%configuration.models.type.description%"
}
},
"required": [
"name",
"provider"
],
"additionalProperties": false
},
"description": "%configuration.models.description%"
}
}
}
Expand Down
17 changes: 3 additions & 14 deletions extensions/positron-assistant/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@
"displayName": "Positron Assistant",
"description": "Provides default assistant and language models for Positron.",
"commands.addModelConfiguration.title": "Add Language Model",
"commands.addModelConfiguration.category": "Positron Assistant",
"configuration.enable.description": "Enable Positron Assistant (experimental)",
"configuration.models.description": "Array of language model configurations.",
"configuration.models.id.description": "Identifier for the model provider, used for API key storage.",
"configuration.models.name.description": "Display name for the model.",
"configuration.models.model.description": "The specific model to use.",
"configuration.models.baseUrl.description": "URL prefix for API calls.",
"configuration.models.numCtx.description": "Context window size.",
"configuration.models.toolCalls.description": "Should the LLM make tool calls?",
"configuration.models.project.description": "Google Cloud Project ID.",
"configuration.models.location.description": "Google Cloud Location.",
"configuration.models.resourceName.description": "Azure resource name.",
"configuration.models.provider.description": "The language model provider.",
"configuration.models.type.description": "Type of model; chat participant or code completion."
"commands.configureModels.title": "Configure Language Models",
"commands.category": "Positron Assistant",
"configuration.enable.description": "Enable Positron Assistant (experimental)"
}
153 changes: 129 additions & 24 deletions extensions/positron-assistant/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,69 @@ interface StoredModelConfig extends Omit<positron.ai.LanguageModelConfig, 'apiKe
id: string;
}

/**
* Interface for storing and retrieving secrets.
*/
export interface SecretStorage {
store(key: string, value: string): Thenable<void>;
get(key: string): Thenable<string | undefined>;
delete(key: string): Thenable<void>;
}

/**
* Implementation of SecretStorage that uses VS Code's secret storage API.
*
* This class should be used in desktop mode to store secrets securely.
*/
export class EncryptedSecretStorage implements SecretStorage {
constructor(private context: vscode.ExtensionContext) { }
store(key: string, value: string): Thenable<void> {
return this.context.secrets.store(key, value);
}
get(key: string): Thenable<string | undefined> {
return this.context.secrets.get(key);
}
delete(key: string): Thenable<void> {
return this.context.secrets.delete(key);
}
}

/**
* Implementation of SecretStorage that uses VS Code's global storage API.
*
* This class stores secrets **insecurely** using VS Code's global storage API.
* It is used in web mode, where there is no durable secret storage.
*
* This class should be replaced with one that uses a secure storage mechanism,
* or just removed altogether when Positron gains secure storage capabilities.
*/
export class GlobalSecretStorage implements SecretStorage {
constructor(private context: vscode.ExtensionContext) { }
store(key: string, value: string): Thenable<void> {
return this.context.globalState.update(key, value);
}
get(key: string): Thenable<string | undefined> {
return Promise.resolve(this.context.globalState.get(key));
}
delete(key: string): Thenable<void> {
return this.context.globalState.update(key, undefined);
}
}

export interface ModelConfig extends StoredModelConfig {
apiKey: string;
}

export function getStoredModels(): StoredModelConfig[] {
const config = vscode.workspace.getConfiguration('positron.assistant');
const storedConfigs = config.get<StoredModelConfig[]>('models') || [];
return storedConfigs;
export function getStoredModels(context: vscode.ExtensionContext): StoredModelConfig[] {
return context.globalState.get('positron.assistant.models') || [];
}

export async function getModelConfigurations(context: vscode.ExtensionContext): Promise<ModelConfig[]> {
const storedConfigs = getStoredModels();
export async function getModelConfigurations(context: vscode.ExtensionContext, storage: SecretStorage): Promise<ModelConfig[]> {
const storedConfigs = getStoredModels(context);

const fullConfigs: ModelConfig[] = await Promise.all(
storedConfigs.map(async (config) => {
const apiKey = await context.secrets.get(`apiKey-${config.id}`);
const apiKey = await storage.get(`apiKey-${config.id}`);
return {
...config,
apiKey: apiKey || ''
Expand All @@ -38,7 +85,69 @@ export async function getModelConfigurations(context: vscode.ExtensionContext):
return fullConfigs;
}

export async function showConfigurationDialog(context: vscode.ExtensionContext) {
export async function showModelList(context: vscode.ExtensionContext, storage: SecretStorage) {
// Create a quickpick with all configured models
const modelConfigs = await getModelConfigurations(context, storage);
const quickPick = vscode.window.createQuickPick();

// Create sections for chat and completion models
const chatModels = modelConfigs.filter(config =>
config.type === 'chat'
);
const completionModels = modelConfigs.filter(config =>
config.type === 'completion'
);

const items: Array<vscode.QuickPickItem> = [
{
label: vscode.l10n.t('Chat Models'),
kind: vscode.QuickPickItemKind.Separator
},
...chatModels.map((config) => ({
label: config.name,
detail: config.model
})),
{
label: vscode.l10n.t('Completion Models'),
kind: vscode.QuickPickItemKind.Separator
},
...completionModels.map((config) => ({
label: config.name,
detail: config.model,
description: config.baseUrl
})),
{
label: '',
kind: vscode.QuickPickItemKind.Separator
},
{
label: vscode.l10n.t('Add a Language Model'),
description: vscode.l10n.t('Add a new language model configuration'),
}
];

vscode.window.showQuickPick(items, {
placeHolder: vscode.l10n.t('Select a language model'),
canPickMany: false,

}).then(async (selected) => {
if (!selected) {
return;
}
if (selected.description === vscode.l10n.t('Add a new language model configuration')) {
showConfigurationDialog(context, storage);
} else {
const selectedConfig = modelConfigs.find((config) => config.name === selected.label);
if (selectedConfig) {
vscode.window.showInformationMessage(
vscode.l10n.t(`Selected language model: {0}`, selectedConfig.name)
);
}
}
});
}

export async function showConfigurationDialog(context: vscode.ExtensionContext, storage: SecretStorage) {
// Gather model sources
const sources = [...languageModels, ...completionModels].map((provider) => provider.source);

Expand Down Expand Up @@ -67,12 +176,11 @@ export async function showConfigurationDialog(context: vscode.ExtensionContext)

// Store API key in secret storage
if (apiKey) {
await context.secrets.store(`apiKey-${id}`, apiKey);
await storage.store(`apiKey-${id}`, apiKey);
}

// Get existing configurations
const config = vscode.workspace.getConfiguration('positron.assistant');
const existingConfigs = config.get<StoredModelConfig[]>('models') || [];
const existingConfigs: Array<StoredModelConfig> = context.globalState.get('positron.assistant.models') || [];

// Add new configuration
const newConfig: StoredModelConfig = {
Expand All @@ -83,11 +191,10 @@ export async function showConfigurationDialog(context: vscode.ExtensionContext)
...otherConfig,
};

// Update settings.json
await config.update(
'models',
[...existingConfigs, newConfig],
vscode.ConfigurationTarget.Global
// Update global state
await context.globalState.update(
'positron.assistant.models',
[...existingConfigs, newConfig]
);

vscode.window.showInformationMessage(
Expand All @@ -97,16 +204,14 @@ export async function showConfigurationDialog(context: vscode.ExtensionContext)

}

export async function deleteConfiguration(context: vscode.ExtensionContext, id: string) {
const config = vscode.workspace.getConfiguration('positron.assistant');
const existingConfigs = config.get<StoredModelConfig[]>('models') || [];
export async function deleteConfiguration(context: vscode.ExtensionContext, storage: SecretStorage, id: string) {
const existingConfigs: Array<StoredModelConfig> = context.globalState.get('positron.assistant.models') || [];
const updatedConfigs = existingConfigs.filter(config => config.id !== id);

await config.update(
'models',
updatedConfigs,
vscode.ConfigurationTarget.Global
await context.globalState.update(
'positron.assistant.models',
updatedConfigs
);

await context.secrets.delete(`apiKey-${id}`);
await storage.delete(`apiKey-${id}`);
}
Loading
Loading