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: 2 additions & 2 deletions extensions/positron-assistant/src/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export class AnthropicLanguageModel implements positron.ai.LanguageModelChatProv
id: 'anthropic-api',
displayName: 'Anthropic'
},
supportedOptions: ['apiKey', 'apiKeyEnvVar'],
supportedOptions: ['apiKey', 'autoconfigure'],
defaults: {
name: DEFAULT_ANTHROPIC_MODEL_NAME,
model: DEFAULT_ANTHROPIC_MODEL_MATCH + '-latest',
toolCalls: true,
apiKeyEnvVar: { key: 'ANTHROPIC_API_KEY', signedIn: false },
autoconfigure: { type: positron.ai.LanguageModelAutoconfigureType.EnvVariable, key: 'ANTHROPIC_API_KEY', signedIn: false }
},
};

Expand Down
48 changes: 32 additions & 16 deletions extensions/positron-assistant/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as positron from 'positron';
import { randomUUID } from 'crypto';
import { getLanguageModels } from './models';
import { completionModels } from './completion';
import { clearTokenUsage, disposeModels, log, registerModel } from './extension';
import { clearTokenUsage, disposeModels, getAutoconfiguredModels, log, registerModel } from './extension';
import { CopilotService } from './copilot.js';
import { PositronAssistantApi } from './api.js';
import { PositLanguageModel } from './posit.js';
Expand Down Expand Up @@ -153,35 +153,51 @@ export async function showConfigurationDialog(context: vscode.ExtensionContext,

// Gather model sources; ignore disabled providers
const enabledProviders = await getEnabledProviders();
// Models in persistent storage
const registeredModels = context.globalState.get<Array<StoredModelConfig>>('positron.assistant.models');
const sources = [...getLanguageModels(), ...completionModels]
// Auto-configured models (e.g., env var based or managed credentials) stored in memory
const autoconfiguredModels = getAutoconfiguredModels();
const sources: positron.ai.LanguageModelSource[] = [...getLanguageModels(), ...completionModels]
.map((provider) => {
const isRegistered = registeredModels?.find((modelConfig) => modelConfig.provider === provider.source.provider.id);
return {
// Get model data from `registeredModels` (for manually configured models; stored in persistent storage)
// or `autoconfiguredModels` (for auto-configured models; e.g., env var based or managed credentials)
const isRegistered = registeredModels?.find((modelConfig) => modelConfig.provider === provider.source.provider.id) || autoconfiguredModels.find((modelConfig) => modelConfig.provider === provider.source.provider.id);
// Update source data with actual model configuration status if found
// Otherwise, use defaults from provider
const source: positron.ai.LanguageModelSource = {
...provider.source,
signedIn: !!isRegistered,
defaults: isRegistered
? { ...provider.source.defaults, ...isRegistered }
: provider.source.defaults
};
return source;
})
.filter((source) => {
// If no specific set of providers was specified, include all
return enabledProviders.length === 0 || enabledProviders.includes(source.provider.id);
})
.map((source) => {
// Resolve environment variables in apiKeyEnvVar
if ('apiKeyEnvVar' in source.defaults && source.defaults.apiKeyEnvVar) {
const envVarName = (source.defaults as any).apiKeyEnvVar.key;
const envVarValue = process.env[envVarName];

return {
...source,
defaults: {
...source.defaults,
apiKeyEnvVar: { key: envVarName, signedIn: !!envVarValue }
},
};
// Handle autoconfigurable providers
if ('autoconfigure' in source.defaults && source.defaults.autoconfigure) {
// Resolve environment variables
if (source.defaults.autoconfigure.type === positron.ai.LanguageModelAutoconfigureType.EnvVariable) {
const envVarName = source.defaults.autoconfigure.key;
const envVarValue = process.env[envVarName];

return {
...source,
defaults: {
...source.defaults,
autoconfigure: { type: positron.ai.LanguageModelAutoconfigureType.EnvVariable, key: envVarName, signedIn: !!envVarValue }
},
};
} else if (source.defaults.autoconfigure.type === positron.ai.LanguageModelAutoconfigureType.Custom) {
// No special handling for custom autoconfiguration at this time
// The custom autoconfiguration logic should handle everything
// and is retrieved from `autoconfiguredModels` above
return source;
}
}
return source;
});
Expand Down
6 changes: 6 additions & 0 deletions extensions/positron-assistant/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as path from 'path';
import { DocumentSelector } from 'vscode-languageclient';
import * as vscode from 'vscode';

/** The extension root directory. */
export const EXTENSION_ROOT_DIR = path.join(__dirname, '..');
Expand Down Expand Up @@ -49,3 +50,8 @@ export const MAX_CONTEXT_VARIABLES = 400;

/** Max number of models to attempt connecting to when checking auth for a provider */
export const DEFAULT_MAX_CONNECTION_ATTEMPTS = 3;

/**
* Determines if the Posit Web environment is detected.
*/
export const IS_RUNNING_ON_PWB = !!process.env.RS_SERVER_URL && vscode.env.uiKind === vscode.UIKind.Web;
28 changes: 24 additions & 4 deletions extensions/positron-assistant/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import * as positron from 'positron';
import { EncryptedSecretStorage, expandConfigToSource, getEnabledProviders, getModelConfiguration, getModelConfigurations, getStoredModels, GlobalSecretStorage, logStoredModels, ModelConfig, SecretStorage, showConfigurationDialog, StoredModelConfig } from './config';
import { createModelConfigsFromEnv, newLanguageModelChatProvider } from './models';
import { createAutomaticModelConfigs, newLanguageModelChatProvider } from './models';
import { registerMappedEditsProvider } from './edits';
import { ParticipantService, registerParticipants } from './participants';
import { newCompletionProvider, registerHistoryTracking } from './completion';
Expand All @@ -32,6 +32,16 @@ let modelDisposables: ModelDisposable[] = [];
let assistantEnabled = false;
let tokenTracker: TokenTracker;

const autoconfiguredModels: ModelConfig[] = [];

/**
* Get all models which were automatically configured (e.g., via environment variables or managed credentials).
* @returns A list of models that were automatically configured
*/
export function getAutoconfiguredModels(): ModelConfig[] {
return [...autoconfiguredModels];
}

/** A chat or completion model provider disposable with associated configuration. */
class ModelDisposable implements vscode.Disposable {
constructor(
Expand Down Expand Up @@ -108,6 +118,7 @@ export async function registerModels(context: vscode.ExtensionContext, storage:
// Dispose of existing models
disposeModels();

let autoModelConfigs: ModelConfig[];
let modelConfigs: ModelConfig[] = [];
try {
// Refresh the set of enabled providers
Expand All @@ -123,10 +134,10 @@ export async function registerModels(context: vscode.ExtensionContext, storage:
return enabled;
});

// Add any configs that should automatically work when the right environment variables are set
const modelConfigsFromEnv = createModelConfigsFromEnv();
// Add any configs that should automatically work when the right conditions are met
autoModelConfigs = await createAutomaticModelConfigs();
// we add in the config if we don't already have it configured
for (const config of modelConfigsFromEnv) {
for (const config of autoModelConfigs) {
if (!modelConfigs.find(c => c.provider === config.provider)) {
modelConfigs.push(config);
}
Expand All @@ -143,6 +154,15 @@ export async function registerModels(context: vscode.ExtensionContext, storage:
try {
await registerModelWithAPI(config, context, storage);
registeredModels.push(config);
if (autoModelConfigs.includes(config)) {
// In addition, track auto-configured models separately
// at a module level so that we can expose them via
// getAutoconfiguredModels()
// This is needed since auto-configured models are not
// stored in persistent storage like manually configured models
// are, and configuration data needs to be retrieved from memory.
autoconfiguredModels.push(config);
}
} catch (e) {
vscode.window.showErrorMessage(`${e}`);
}
Expand Down
88 changes: 75 additions & 13 deletions extensions/positron-assistant/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import * as positron from 'positron';
import * as ai from 'ai';
import { getMaxConnectionAttempts, getProviderTimeoutMs, ModelConfig, SecretStorage } from './config';
import { getMaxConnectionAttempts, getProviderTimeoutMs, getEnabledProviders, ModelConfig, SecretStorage } from './config';
import { AnthropicProvider, createAnthropic } from '@ai-sdk/anthropic';
import { AzureOpenAIProvider, createAzure } from '@ai-sdk/azure';
import { createVertex, GoogleVertexProvider } from '@ai-sdk/google-vertex';
Expand All @@ -19,12 +19,13 @@ import { processMessages, toAIMessage } from './utils';
import { AmazonBedrockProvider, createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { AnthropicLanguageModel, DEFAULT_ANTHROPIC_MODEL_MATCH, DEFAULT_ANTHROPIC_MODEL_NAME } from './anthropic';
import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js';
import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT, IS_RUNNING_ON_PWB } from './constants.js';
import { log, recordRequestTokenUsage, recordTokenUsage } from './extension.js';
import { TokenUsage } from './tokens.js';
import { BedrockClient, FoundationModelSummary, InferenceProfileSummary, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock';
import { PositLanguageModel } from './posit.js';
import { applyModelFilters } from './modelFilters';
import { autoconfigureWithManagedCredentials, AWS_MANAGED_CREDENTIALS } from './pwb';

/**
* Models used by chat participants and for vscode.lm.* API functionality.
Expand Down Expand Up @@ -263,7 +264,21 @@ class EchoLanguageModel implements positron.ai.LanguageModelChatProvider {
//#endregion
//#region Language Models

/**
* Result of an autoconfiguration attempt.
* - Signed in indicates whether the model is configured and ready to use.
* - Message provides additional information to be displayed to user in the configuration modal, if signed in.
*/
export type AutoconfigureResult = {
signedIn: false;
} | {
signedIn: true;
message: string;
};

abstract class AILanguageModel implements positron.ai.LanguageModelChatProvider {
public static source: positron.ai.LanguageModelSource;

public readonly name;
public readonly provider;
public readonly id;
Expand Down Expand Up @@ -669,6 +684,13 @@ abstract class AILanguageModel implements positron.ai.LanguageModelChatProvider
}
return this._config.model === id;
}

/**
* Autoconfigures the language model, if supported.
* May implement functionality such as checking for environment variables or assessing managed credentials.
* @returns A promise that resolves to the autoconfigure result.
*/
static autoconfigure?: () => Promise<AutoconfigureResult>;
}

class AnthropicAILanguageModel extends AILanguageModel implements positron.ai.LanguageModelChatProvider {
Expand All @@ -683,12 +705,12 @@ class AnthropicAILanguageModel extends AILanguageModel implements positron.ai.La
id: 'anthropic-api',
displayName: 'Anthropic'
},
supportedOptions: ['apiKey', 'apiKeyEnvVar'],
supportedOptions: ['apiKey', 'autoconfigure'],
defaults: {
name: DEFAULT_ANTHROPIC_MODEL_NAME,
model: DEFAULT_ANTHROPIC_MODEL_MATCH + '-latest',
toolCalls: true,
apiKeyEnvVar: { key: 'ANTHROPIC_API_KEY', signedIn: false },
autoconfigure: { type: positron.ai.LanguageModelAutoconfigureType.EnvVariable, key: 'ANTHROPIC_API_KEY', signedIn: false },
},
};

Expand Down Expand Up @@ -1035,11 +1057,12 @@ export class AWSLanguageModel extends AILanguageModel implements positron.ai.Lan
id: 'amazon-bedrock',
displayName: 'Amazon Bedrock'
},
supportedOptions: ['toolCalls'],
supportedOptions: ['toolCalls', 'autoconfigure'],
defaults: {
name: 'Claude 4 Sonnet Bedrock',
model: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
toolCalls: true,
autoconfigure: { type: positron.ai.LanguageModelAutoconfigureType.Custom, message: 'Automatically configured using AWS credentials', signedIn: false },
},
};
bedrockClient: BedrockClient;
Expand Down Expand Up @@ -1245,6 +1268,14 @@ export class AWSLanguageModel extends AILanguageModel implements positron.ai.Lan
return undefined;
}

static override async autoconfigure(): Promise<AutoconfigureResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about making the LanguageModelAutoconfigureType.Custom more specific to Workbench and refactoring this function so that the environment variables are passed with keys? This would make it straightforward to add autoconfigure functionality for other Workbench creds relying on environment variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to keep this method as agnostic as possible, to allow for the possibility of other autoconfiguration mechanisms in the future (in addition to Managed Creds).

Perhaps a happy medium is a helper function to check for Managed Creds that other providers can use?

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds reasonable. Since we're including logic to check if we're running in PWB, can we rename autoconfigure to something that indicates this is only for Workbench?

return autoconfigureWithManagedCredentials(
AWS_MANAGED_CREDENTIALS,
AWSLanguageModel.source.provider.id,
AWSLanguageModel.source.provider.displayName
);
}

}

//#endregion
Expand Down Expand Up @@ -1282,31 +1313,62 @@ export function getLanguageModels() {
*
* @returns The model configurations that are configured by the environment.
*/
export function createModelConfigsFromEnv(): ModelConfig[] {
export async function createAutomaticModelConfigs(): Promise<ModelConfig[]> {
const models = getLanguageModels();
const modelConfigs: ModelConfig[] = [];

models.forEach(model => {
if ('apiKeyEnvVar' in model.source.defaults) {
const key = model.source.defaults.apiKeyEnvVar?.key;
for (const model of models) {
if (!('autoconfigure' in model.source.defaults)) {
// Not an autoconfigurable model
continue;
}

if (model.source.defaults.autoconfigure.type === positron.ai.LanguageModelAutoconfigureType.EnvVariable) {
// Handle environment variable based auto-configuration
const key = model.source.defaults.autoconfigure.key;
// pragma: allowlist nextline secret
const apiKey = key ? process.env[key] : undefined;

if (key && apiKey) {
const modelConfig = {
const modelConfig: ModelConfig = {
id: `${model.source.provider.id}`,
provider: model.source.provider.id,
type: positron.PositronLanguageModelType.Chat,
name: model.source.provider.displayName,
model: model.source.defaults.model,
apiKey: apiKey,
// pragma: allowlist nextline secret
apiKeyEnvVar: 'apiKeyEnvVar' in model.source.defaults ? model.source.defaults.apiKeyEnvVar : undefined,
autoconfigure: {
type: positron.ai.LanguageModelAutoconfigureType.EnvVariable,
key: key,
signedIn: true,
}
};
modelConfigs.push(modelConfig);
}
} else if (model.source.defaults.autoconfigure.type === positron.ai.LanguageModelAutoconfigureType.Custom) {
// Handle custom auto-configuration
if ('autoconfigure' in model && model.autoconfigure) {
const result = await model.autoconfigure();
if (result.signedIn) {
const modelConfig: ModelConfig = {
id: `${model.source.provider.id}`,
provider: model.source.provider.id,
type: positron.PositronLanguageModelType.Chat,
name: model.source.provider.displayName,
model: model.source.defaults.model,
apiKey: undefined,
// pragma: allowlist nextline secret
autoconfigure: {
type: positron.ai.LanguageModelAutoconfigureType.Custom,
message: result.message,
signedIn: true
}
};
modelConfigs.push(modelConfig);
}
}
}
});
}

return modelConfigs;
}
Expand Down
Loading
Loading