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
56 changes: 56 additions & 0 deletions extensions/positron-assistant/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,62 @@
"experimental"
]
},
"positron.assistant.configuredModels": {
"type": "object",
"default": {},
"markdownDescription": "%configuration.configuredModels.description%",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Display name for the model"
},
"identifier": {
"type": "string",
"description": "Model identifier for API calls"
},
"maxInputTokens": {
"type": "number",
"minimum": 512,
"description": "Maximum input tokens for this model"
},
"maxOutputTokens": {
"type": "number",
"minimum": 512,
"description": "Maximum output tokens for this model"
}
},
"required": [
"name",
"identifier"
]
}
},
"examples": [
{
"openai-compatible": [
{
"name": "Claude Sonnet 4.5",
"identifier": "claude-sonnet-4-5",
"maxInputTokens": 200000,
"maxOutputTokens": 64000
},
{
"name": "GPT-4 Turbo",
"identifier": "gpt-4-turbo",
"maxInputTokens": 8192,
"maxOutputTokens": 2048
}
]
}
],
"tags": [
"experimental"
]
},
"positron.assistant.notebookSuggestions.model": {
"type": "array",
"default": [
Expand Down
3 changes: 2 additions & 1 deletion extensions/positron-assistant/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@
"configuration.preferredModel.description": "A preferred model ID or name (partial match supported) to use if available in Positron Assistant for the current provider.\n\nTakes precedence over `#positron.assistant.defaultModels#`.\n\nRequires a restart to take effect.\n\nExamples:\n- `Claude Sonnet 4.5` prefers the model named 'Claude Sonnet 4.5'\n- `GPT-5` prefers the model named 'GPT-5'",
"configuration.defaultModels.description": "A mapping of provider IDs to default model IDs or names (partial match supported) to use for that provider in Positron Assistant.\n\n`#positron.assistant.preferredModel#` takes precedence over this setting.\n\nRequires a restart to take effect.\n\nExample: Item `anthropic-api` and Value `Claude Sonnet 4.5` sets the default model for Anthropic to 'Claude Sonnet 4.5'",
"configuration.notebookSuggestions.model.description": "An ordered array of model patterns to try when generating AI suggestions for Positron Notebooks. Patterns are tried in order until a match is found.\n\nEach pattern supports partial matching on model ID or name (case-insensitive). Default is `[\"haiku\", \"mini\"]` which tries to find a model with \"haiku\" in the name first, then tries \"mini\" if not found. If no patterns match or the array is empty, falls back to the current chat session model, then the current provider's model, then the first available model.\n\nExamples:\n- `[\"haiku\", \"mini\"]` (default) tries to find a model with \"haiku\" in the name first, then tries \"mini\" if not found\n- `[\"Claude Sonnet 4.5\", \"GPT-5\"]` tries \"Claude Sonnet 4.5\" first, then \"GPT-5\"\n- `[]` disables pattern matching and uses the default fallback behavior",
"configuration.providerVariables.bedrock.description": "Variables used to configure advanced settings for Bedrock in Positron Assistant.\n\nRequires a restart to take effect.\n\nExample: to set the AWS region and profile for Amazon Bedrock, add items with keys `AWS_REGION` and `AWS_PROFILE`."
"configuration.providerVariables.bedrock.description": "Variables used to configure advanced settings for Bedrock in Positron Assistant.\n\nRequires a restart to take effect.\n\nExample: to set the AWS region and profile for Amazon Bedrock, add items with keys `AWS_REGION` and `AWS_PROFILE`.",
"configuration.configuredModels.description": "Configure custom language models for each provider. These models will be used instead of retrieving the model listing from the provider.\n\nUse provider IDs as keys, with arrays of model configurations as values.\n\nExample:\n```json\n{\n \"anthropic-api\": [\n {\n \"name\": \"Claude Sonnet 4.5\", // required: display name\n \"identifier\": \"claude-sonnet-4-5\", // required: API model ID\n \"maxInputTokens\": 200000, // optional: input token limit\n \"maxOutputTokens\": 8192 // optional: output token limit\n }\n ]\n}\n```"
}
164 changes: 80 additions & 84 deletions extensions/positron-assistant/src/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { isChatImagePart, isCacheBreakpointPart, parseCacheBreakpoint, processMe
import { DEFAULT_MAX_TOKEN_INPUT, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js';
import { log, recordTokenUsage, recordRequestTokenUsage } from './extension.js';
import { TokenUsage } from './tokens.js';
import { availableModels } from './models.js';
import { getAllModelDefinitions } from './modelDefinitions.js';
import { LanguageModelDataPartMimeType } from './types.js';
import { applyModelFilters } from './modelFilters.js';
import { createModelInfo, markDefaultModel } from './modelResolutionHelpers.js';

export const DEFAULT_ANTHROPIC_MODEL_NAME = 'Claude Sonnet 4';
export const DEFAULT_ANTHROPIC_MODEL_MATCH = 'claude-sonnet-4';
Expand Down Expand Up @@ -90,10 +91,10 @@ export class AnthropicLanguageModel implements positron.ai.LanguageModelChatProv
}

async provideLanguageModelChatInformation(_options: { silent: boolean }, token: vscode.CancellationToken): Promise<vscode.LanguageModelChatInformation[]> {
log.trace(`[${this.providerName}] Preparing language model chat information...`);
log.debug(`[${this.providerName}] Preparing language model chat information...`);
const models = await this.resolveModels(token) ?? [];

log.trace(`[${this.providerName}] Resolved ${models.length} models.`);
log.debug(`[${this.providerName}] Resolved ${models.length} models.`);
return this.filterModels(models);
}

Expand Down Expand Up @@ -234,15 +235,74 @@ export class AnthropicLanguageModel implements positron.ai.LanguageModelChatProv
return applyModelFilters(models, this.provider, this.providerName);
}

private isDefaultUserModel(id: string, name?: string): boolean {
const config = vscode.workspace.getConfiguration('positron.assistant');
const defaultModels = config.get<Record<string, string>>('defaultModels') || {};
if ('anthropic-api' in defaultModels) {
if (id.includes(defaultModels['anthropic-api']) || name?.includes(defaultModels['anthropic-api'])) {
return true;
private retrieveModelsFromConfig(): vscode.LanguageModelChatInformation[] | undefined {
const configuredModels = getAllModelDefinitions(this.provider);
if (configuredModels.length === 0) {
return undefined;
}

log.info(`[${this.providerName}] Using ${configuredModels.length} configured models.`);

const modelListing = configuredModels.map((modelDef) =>
createModelInfo({
id: modelDef.identifier,
name: modelDef.name,
family: this.provider,
version: '',
provider: this.provider,
providerName: this.providerName,
capabilities: this.capabilities,
defaultMaxInput: modelDef.maxInputTokens,
defaultMaxOutput: modelDef.maxOutputTokens
})
);

return markDefaultModel(modelListing, this.provider, DEFAULT_ANTHROPIC_MODEL_MATCH);
}

private async retrieveModelsFromApi(): Promise<vscode.LanguageModelChatInformation[] | undefined> {
try {
const modelListing: vscode.LanguageModelChatInformation[] = [];
const knownAnthropicModels = getAllModelDefinitions(this.provider);
let hasMore = true;
let nextPageToken: string | undefined;

log.trace(`[${this.providerName}] Fetching models from Anthropic API...`);

while (hasMore) {
const modelsPage = nextPageToken
? await this._client.models.list({ after_id: nextPageToken })
: await this._client.models.list();

modelsPage.data.forEach(model => {
const knownModel = knownAnthropicModels?.find(m => model.id.startsWith(m.identifier));

modelListing.push(
createModelInfo({
id: model.id,
name: model.display_name,
family: this.provider,
version: model.created_at,
provider: this.provider,
providerName: this.providerName,
capabilities: this.capabilities,
defaultMaxInput: knownModel?.maxInputTokens,
defaultMaxOutput: knownModel?.maxOutputTokens
})
);
});

hasMore = modelsPage.has_more;
if (hasMore && modelsPage.data.length > 0) {
nextPageToken = modelsPage.data[modelsPage.data.length - 1].id;
}
}

return markDefaultModel(modelListing, this.provider, DEFAULT_ANTHROPIC_MODEL_MATCH);
} catch (error) {
log.warn(`[${this.providerName}] Failed to fetch models from Anthropic API: ${error}`);
return undefined;
}
return id.includes(DEFAULT_ANTHROPIC_MODEL_MATCH);
}

private onContentBlock(block: Anthropic.ContentBlock, progress: vscode.Progress<vscode.LanguageModelResponsePart2>): void {
Expand Down Expand Up @@ -301,85 +361,21 @@ export class AnthropicLanguageModel implements positron.ai.LanguageModelChatProv
}

async resolveModels(token: vscode.CancellationToken): Promise<vscode.LanguageModelChatInformation[] | undefined> {
const modelListing: vscode.LanguageModelChatInformation[] = [];
const knownAnthropicModels = availableModels.get(this.provider);
const userSetMaxInputTokens: Record<string, number> = vscode.workspace.getConfiguration('positron.assistant').get('maxInputTokens', {});
const userSetMaxOutputTokens: Record<string, number> = vscode.workspace.getConfiguration('positron.assistant').get('maxOutputTokens', {});
let hasMore = true;
let nextPageToken: string | undefined;

log.trace(`[${this.providerName}] Fetching models from Anthropic API...`);

while (hasMore) {
const modelsPage = nextPageToken
? await this._client.models.list({ after_id: nextPageToken })
: await this._client.models.list();

modelsPage.data.forEach(model => {
const knownModel = knownAnthropicModels?.find(m => model.id.startsWith(m.identifier));

const knownModelMaxInputTokens = knownModel?.maxInputTokens;
const maxInputTokens = userSetMaxInputTokens[model.id] ?? knownModelMaxInputTokens ?? DEFAULT_MAX_TOKEN_INPUT;

const knownModelMaxOutputTokens = knownModel?.maxOutputTokens;
const maxOutputTokens = userSetMaxOutputTokens[model.id] ?? knownModelMaxOutputTokens ?? DEFAULT_MAX_TOKEN_OUTPUT;

modelListing.push({
id: model.id,
name: model.display_name,
family: this.provider,
version: model.created_at,
maxInputTokens: maxInputTokens,
maxOutputTokens: maxOutputTokens,
capabilities: this.capabilities,
isDefault: this.isDefaultUserModel(model.id, model.display_name),
isUserSelectable: true,
});
});
log.debug(`[${this.providerName}] Resolving models...`);

hasMore = modelsPage.has_more;
if (hasMore && modelsPage.data.length > 0) {
nextPageToken = modelsPage.data[modelsPage.data.length - 1].id;
}
const configuredModels = this.retrieveModelsFromConfig();
if (configuredModels) {
this.modelListing = configuredModels;
return configuredModels;
}

if (modelListing.length === 0) {
modelListing.push({
id: this.id,
name: this.name,
family: this.provider,
version: this._context?.extension.packageJSON.version ?? '',
maxInputTokens: this.maxInputTokens,
maxOutputTokens: this.maxOutputTokens,
capabilities: this.capabilities,
isDefault: true,
isUserSelectable: true,
});
const apiModels = await this.retrieveModelsFromApi();
if (apiModels) {
this.modelListing = apiModels;
return apiModels;
}

// Mark models as default, ensuring only one default per provider
let hasDefault = false;
for (let i = 0; i < modelListing.length; i++) {
const model = modelListing[i];
if (!hasDefault && this.isDefaultUserModel(model.id, model.name)) {
modelListing[i] = { ...model, isDefault: true };
hasDefault = true;
} else {
modelListing[i] = { ...model, isDefault: false };
}
}

// If no models match the default ID, make the first model the default.
if (modelListing.length > 0 && !hasDefault) {
modelListing[0] = {
...modelListing[0],
isDefault: true,
};
}

this.modelListing = modelListing;

return modelListing;
return undefined;
}
}

Expand Down
3 changes: 3 additions & 0 deletions extensions/positron-assistant/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const DEFAULT_MAX_TOKEN_INPUT = 100_000;
/** The default max token output if a model's maximum is unknown */
export const DEFAULT_MAX_TOKEN_OUTPUT = 4_096;

/** The minimum allowed token limit for input/output tokens */
export const MIN_TOKEN_LIMIT = 512;

/** Tag used by tools to indicate a workspace must be open in order to use the tool */
export const TOOL_TAG_REQUIRES_WORKSPACE = 'requires-workspace';

Expand Down
4 changes: 3 additions & 1 deletion extensions/positron-assistant/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { registerPromptManagement } from './promptRender.js';
import { collectDiagnostics } from './diagnostics.js';
import { BufferedLogOutputChannel } from './logBuffer.js';
import { resetAssistantState } from './reset.js';
import { verifyProvidersInConfiguredModels } from './modelDefinitions.js';

const hasChatModelsContextKey = 'positron-assistant.hasChatModels';

Expand Down Expand Up @@ -437,7 +438,7 @@ export function getRequestTokenUsage(requestId: string): { tokens: TokenUsage; p
return requestTokenUsage.get(requestId);
}

export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
// Create the log output channel.
context.subscriptions.push(log);

Expand All @@ -458,6 +459,7 @@ export function activate(context: vscode.ExtensionContext) {
positron.ai.addLanguageModelConfig(expandConfigToSource(stored));
});
}
await verifyProvidersInConfiguredModels();
} catch (error) {
const msg = error instanceof Error ? error.message : JSON.stringify(error);
vscode.window.showErrorMessage(
Expand Down
Loading
Loading