Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1fe52b1
Add ai-lib submodule and ai-config dependency to authentication
melissa-barca Jul 14, 2026
42f9977
Map provider settings to a providers.json config
melissa-barca Jul 14, 2026
6605727
Add one-shot providers.json migration with overwrite mode
melissa-barca Jul 14, 2026
757ee2c
Bump ai-lib for nodenext-compatible ai-config dist
melissa-barca Jul 15, 2026
584a2a1
Build ai-config with tsc directly to avoid its tsx prebuild
melissa-barca Jul 15, 2026
06ee8bc
Add providers.json migration command and first-launch prompt
melissa-barca Jul 15, 2026
65a8cb1
Deprecate provider settings migrated to providers.json
melissa-barca Jul 15, 2026
b5c2377
Remove positron.assistant.models.include setting
melissa-barca Jul 15, 2026
7983e1a
Fix migration guards for invalid files and empty settings
melissa-barca Jul 15, 2026
2d38a7b
Validate the full settings mapping against the ai-config schema
melissa-barca Jul 15, 2026
a7031e5
Run ai-config's own build from the extension postinstall
melissa-barca Jul 15, 2026
5b162c5
Migrate model overrides to providers.json custom models
melissa-barca Jul 15, 2026
b25bff1
Migrate SNOWFLAKE_HOME to providers.json and deprecate the setting
melissa-barca Jul 16, 2026
7f9243b
Migrate providers.json settings automatically instead of prompting
melissa-barca Jul 16, 2026
17eabbe
Shorten providers.json deprecation messages
melissa-barca Jul 16, 2026
63725f6
Log migrated provider IDs and link to the log from the toast
melissa-barca Jul 16, 2026
a1079d2
Log each migrated setting as source -> providers.json path
melissa-barca Jul 16, 2026
5649cdf
Log the migrated value alongside each setting mapping
melissa-barca Jul 16, 2026
863984a
Clarify the migration toast wording
melissa-barca Jul 16, 2026
d40d35f
Bump ai-lib to the merged model-capabilities release
melissa-barca Jul 16, 2026
26d1b0b
Add activation event for the migrate settings command
melissa-barca Jul 16, 2026
e708f4d
Bump ai-lib to the merged husky prepare guard
melissa-barca Jul 17, 2026
0e7fb29
Remove the dead migrateApiKey command
melissa-barca Jul 17, 2026
62b4f1e
Migrate the Foundry base URL verbatim
melissa-barca Jul 20, 2026
1656bef
Validate the migrated config and report schema failures
melissa-barca Jul 20, 2026
258ad69
save automatic migration and setting deprecation for the future
melissa-barca Jul 20, 2026
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "extensions/positron-r/ark"]
path = extensions/positron-r/ark
url = https://github.com/posit-dev/ark.git
[submodule "ai-lib"]
path = ai-lib
url = https://github.com/posit-dev/ai-lib.git
1 change: 1 addition & 0 deletions ai-lib
Submodule ai-lib added at ed13e0
1 change: 1 addition & 0 deletions build/npm/dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export let dirs = [
'build/vite',
'extensions',
// --- Start Positron ---
'ai-lib/packages/ai-config',
'extensions/authentication',
'extensions/next-edit-suggestions',
'extensions/open-remote-ssh',
Expand Down
29 changes: 29 additions & 0 deletions extensions/authentication/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 10 additions & 19 deletions extensions/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
"onAuthenticationRequest:google-cloud",
"onAuthenticationRequest:deepseek-api",
"onCommand:authentication.configureProviders",
"onCommand:authentication.migrateApiKey"
"onCommand:authentication.migrateSettingsToProvidersJson"
],
"main": "./out/extension.js",
"scripts": {
"postinstall": "npm --prefix ../../ai-lib run build -w ai-config"
},
"dependencies": {
"@aws-sdk/credential-providers": "^3.734.0",
"@aws-sdk/types": "^3.734.0",
"ai-config": "file:../../ai-lib/packages/ai-config",
"google-auth-library": "^9.15.1"
},
"devDependencies": {
Expand Down Expand Up @@ -80,6 +84,11 @@
"command": "authentication.configureProviders",
"title": "%commands.configureProviders.title%",
"category": "%commands.category%"
},
{
"command": "authentication.migrateSettingsToProvidersJson",
"title": "%commands.migrateSettingsToProvidersJson.title%",
"category": "%commands.category%"
}
],
"configuration": {
Expand Down Expand Up @@ -703,24 +712,6 @@
"items": {
"type": "string"
}
},
"positron.assistant.models.include": {
"type": "array",
"default": [],
"markdownDescription": "%configuration.models.include.description%",
"items": {
"type": "string"
},
"examples": [
[
"claude",
"gpt"
],
[
"Claude Sonnet 4.5",
"GPT-4o"
]
]
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion extensions/authentication/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"configuration.models.overrides.msFoundry.description": "Model overrides for Microsoft Foundry provider.\n\nThese models will be used instead of retrieving the model listing from the provider. Each model must have a `name` (display name) and `identifier` (model ID for API calls). Optionally specify `maxInputTokens` and `maxOutputTokens`.\n\nRequires a restart to take effect.",
"configuration.models.overrides.google.description": "Model overrides for Google Gemini provider.\n\nThese models will be used instead of retrieving the model listing from the provider. Each model must have a `name` (display name) and `identifier` (model ID for API calls). Optionally specify `maxInputTokens` and `maxOutputTokens`.\n\nRequires a restart to take effect.",
"configuration.aiExcludes.description": "A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from AI features. Files matching these patterns will not have their contents sent to AI providers for inline completions or chat context. Patterns without a `/` will match against the filename only (e.g., `*.py` matches all Python files). Use `**/` prefix for explicit recursive matching.",
"configuration.models.include.description": "Enforce strict restrictions on which language models are available. When configured, **only models matching these patterns will be available**\u2014other models will be excluded entirely.\n\nMatching is case-insensitive and supports wildcards (`*`) and regular expressions for advanced filtering.\n\n**Note:** This setting does not affect GitHub Copilot models.\n\nExamples:\n- `claude`: Only Claude models are available\n- `GPT-4o`: Only this specific model is available\n- `^claude.*opus`: Only Claude Opus models (matched by regex) are available",
"configuration.deprecation.providersJson": "This setting is deprecated. Use `~/.posit/ai/providers.json` instead.",
"configuration.deprecation.inferenceProfileRegion": "This setting is deprecated and no longer read. Use a custom model in `~/.posit/ai/providers.json` instead.",
"commands.configureProviders.title": "Configure Language Model Providers",
"commands.migrateSettingsToProvidersJson.title": "Migrate AI Provider Settings to providers.json",
"commands.category": "Authentication"
}
10 changes: 0 additions & 10 deletions extensions/authentication/src/configDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ export function registerAuthProvider(
}
}

/**
* Get the auth provider for a given provider ID.
* Used by the migrateApiKey command.
*/
export function getAuthProvider(
providerId: string
): AuthProvider | undefined {
return authProviders.get(providerId);
}

/**
* Update a provider's signedIn and autoconfigure state from its current sessions.
* The caller is responsible for fetching sessions via the appropriate mechanism.
Expand Down
4 changes: 2 additions & 2 deletions extensions/authentication/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import * as fs from 'fs';
import { log } from './log';
import { migrateAwsSettings } from './migration/aws';
import { migrateSnowflakeSettings } from './migration/snowflake';
import { registerMigrateApiKeyCommand } from './migration/apiKey';
import { registerProvidersJsonMigration } from './migration/providersJsonUi';
import { AuthProviderLogger } from './authProviderLogger';
import { applyPwbPositAIDefault } from './pwbDefaults';

Expand Down Expand Up @@ -142,7 +142,7 @@ export async function activate(context: vscode.ExtensionContext) {
}
),
);
registerMigrateApiKeyCommand(context);
registerProvidersJsonMigration(context);

return { getLogs: () => log.formatEntriesForDiagnostics() };
}
Expand Down
34 changes: 0 additions & 34 deletions extensions/authentication/src/migration/apiKey.ts

This file was deleted.

146 changes: 146 additions & 0 deletions extensions/authentication/src/migration/migrateToProvidersJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2026 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'fs/promises';
import * as vscode from 'vscode';
import { log } from '../log';
import {
buildProvidersConfigFromSettings,
InferCapabilitiesFn,
MigrationSettingsReader,
} from './providersJson';

export type MigrationResult =
| { outcome: 'migrated'; settingCount: number }
| { outcome: 'skipped-populated' }
| { outcome: 'nothing-to-migrate' };

export interface RunMigrationOptions {
/** Replace a populated providers block instead of skipping (manual command only). */
overwrite: boolean;
/** Override providers.json path (tests). */
configPath?: string;
/** Override the settings source (tests). */
reader?: MigrationSettingsReader;
/** Override capability inference (tests). */
inferCapabilities?: InferCapabilitiesFn;
}

/** Reads explicitly-set GLOBAL values only; defaults and workspace scopes are ignored. */
export function createGlobalSettingsReader(): MigrationSettingsReader {
return {
globalValue: <T,>(key: string) =>
vscode.workspace.getConfiguration().inspect<T>(key)?.globalValue,
};
}

/**
* Zero-value capability synthesizer for presence checks. Capabilities only
* shape the values written into custom models, never whether a setting
* migrates, so hasMigratableSettings can stay synchronous instead of
* dynamically importing ai-config's real inferModelCapabilities.
*/
const PRESENCE_CHECK_CAPABILITIES: InferCapabilitiesFn = () => ({
maxContextLength: 0,
supportsTools: false,
supportsImages: false,
supportsToolResultImages: false,
supportsWebSearch: false,
});

/** True when the settings hold values the migration would actually write (empty values are filtered). */
export function hasMigratableSettings(
reader: MigrationSettingsReader = createGlobalSettingsReader()
): boolean {
return buildProvidersConfigFromSettings(reader, PRESENCE_CHECK_CAPABILITIES) !== undefined;
}

/**
* True when the user's providers.json file already carries provider config,
* or holds content the migration must not silently replace. ai-config's read
* path coerces unparseable or schema-invalid files to an empty config, which
* would make a hand-edited file with one typo look unpopulated; this check
* deliberately reads the raw file and validates it with ai-config's schema
* so such files count as populated.
*/
export async function userProvidersFileIsPopulated(configPath?: string): Promise<boolean> {
const { PROVIDERS_CONFIG_PATH, providersConfigSchema } = await import('ai-config/node');
const filePath = configPath ?? PROVIDERS_CONFIG_PATH;
let raw: string;
try {
raw = await fs.readFile(filePath, 'utf-8');
} catch {
return false;
}
if (raw.trim() === '') {
return false;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
log.warn(`[migration] ${filePath} is not valid JSON; treating it as populated`);
return true;
}
const result = providersConfigSchema.safeParse(parsed);
if (!result.success) {
log.warn(`[migration] ${filePath} does not match the providers schema; treating it as populated`);
return true;
}
const providers = result.data.providers;
return !!providers && Object.keys(providers).length > 0;
}

/**
* One-shot migration: writes the mapped config through mutateProvidersConfig.
* The populated-file check runs BEFORE the mutator so unparseable files (which
* the mutator's read coerces to an empty config) and no-op skips never touch
* the file, and again INSIDE the mutator so the parseable case stays guarded
* under ai-config's cross-process lock.
*/
export async function runMigration(opts: RunMigrationOptions): Promise<MigrationResult> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we make a version of this accessible from the CLI by adding a subcommand to the Positron binary (like @sharon-wang) was talking about?

Then in the Workbench migration script I could hand Positron an enforced-settings.json and it could give me the migrated providers.json (so we don't need to duplicate all this translation logic inside Workbench).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do you think you could file a separate issue for this? I'm not sure if we'll be able to get it in 2026.08 (I'm out next week) but we might be able to get it in a patch for Workbench 2026.08

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sure thing!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue here: #15023

const reader = opts.reader ?? createGlobalSettingsReader();
const { mutateProvidersConfig, inferModelCapabilities, providersConfigSchema } = await import('ai-config/node');
const mapped = buildProvidersConfigFromSettings(reader, opts.inferCapabilities ?? inferModelCapabilities);
if (!mapped) {
log.info('[migration] No provider settings to migrate');
return { outcome: 'nothing-to-migrate' };
}

// The builder assembles loosely-typed blocks; validate the assembled config
// against ai-config's schema before writing so a bad mapping fails loudly
// here instead of writing malformed providers.json.
const config = providersConfigSchema.parse(mapped.config);

if (!opts.overwrite && await userProvidersFileIsPopulated(opts.configPath)) {
log.info('[migration] providers.json already has provider config; skipped');
return { outcome: 'skipped-populated' };
}

let skippedPopulated = false;
await mutateProvidersConfig(
current => {
if (!opts.overwrite && current.providers && Object.keys(current.providers).length > 0) {
skippedPopulated = true;
return current;
}
return { ...current, providers: config.providers };
},
{
configPath: opts.configPath,
logger: { debug: (m: string) => log.debug(m), warn: (m: string) => log.warn(m) },
}
);

if (skippedPopulated) {
log.info('[migration] providers.json already has provider config; skipped');
return { outcome: 'skipped-populated' };
}
log.info(`[migration] Migrated ${mapped.settingCount} setting(s) to providers.json:`);
for (const { source, destination, value } of mapped.migrations) {
log.info(`[migration] ${source} -> ${destination} = ${value}`);
}
return { outcome: 'migrated', settingCount: mapped.settingCount };
}
Loading
Loading