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
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ import { ICommandService } from '../../commands/node/commandService';
import { getAgentForIntent, Intent } from '../../common/constants';
import { IConversationStore } from '../../conversationStore/node/conversationStore';
import { IIntentService } from '../../intents/node/intentService';
import { isAutoModel } from '../../../platform/endpoint/node/autoChatEndpoint';
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { UnknownIntent } from '../../intents/node/unknownIntent';
import { formatModelDetails } from '../../../platform/chat/common/chatModelDetails';
import { formatAutoModeDetails, formatModelDetails } from '../../../platform/chat/common/chatModelDetails';
import { ContributedToolName } from '../../tools/common/toolNames';
import { ChatVariablesCollection } from '../common/chatVariablesCollection';
import { Conversation, getGlobalContextCacheKey, GlobalContextMessageMetadata, ICopilotChatResult, ICopilotChatResultIn, normalizeSummariesOnRounds, RenderedUserMessageMetadata, Turn, TurnStatus, TurnTokenUsageMetadata } from '../common/conversation';
Expand Down Expand Up @@ -87,6 +89,7 @@ export class ChatParticipantRequestHandler {
@IAuthenticationService private readonly _authService: IAuthenticationService,
@IAuthenticationChatUpgradeService private readonly _authenticationUpgradeService: IAuthenticationChatUpgradeService,
@IChatQuotaService private readonly _chatQuotaService: IChatQuotaService,
@IExperimentationService private readonly _experimentationService: IExperimentationService,
) {
this.location = this.getLocation(request);

Expand Down Expand Up @@ -259,7 +262,11 @@ export class ChatParticipantRequestHandler {
result = await chatResult;
const endpoint = await this._endpointProvider.getChatEndpoint(this.request);
const creditsUsed = this._chatQuotaService.getCreditsForTurn(this.turn.id);
if (this._authService.copilotToken?.isNoAuthUser) {
const hideAutoModelName = isAutoModel(endpoint) === 1
&& this._experimentationService.getTreatmentVariable<boolean>('copilotchat.hideAutoModelName') === true;
if (hideAutoModelName) {
result.details = formatAutoModeDetails(creditsUsed, endpoint.multiplier);
} else if (this._authService.copilotToken?.isNoAuthUser) {
result.details = endpoint.name;
} else {
result.details = formatModelDetails(endpoint.name, endpoint.multiplier, creditsUsed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,47 @@ import { ConfigKey } from '../../../platform/configuration/common/configurationS
import { ChatEndpointFamily } from '../../../platform/endpoint/common/endpointProvider';
import { ExtensionContributedChatEndpoint } from '../../../platform/endpoint/vscode-node/extChatEndpoint';
import { IChatEndpoint } from '../../../platform/networking/common/networking';
import { Delayer } from '../../../util/vs/base/common/async';
import { MicrotaskDelay } from '../../../util/vs/base/common/symbols';
import { ProductionEndpointProvider } from './endpointProviderImpl';

export class ScenarioAutomationEndpointProviderImpl extends ProductionEndpointProvider {
/**
* Cached first-non-copilot model. Resolved lazily on first use and invalidated when the
* registered chat-model set changes. Without this cache, `getChatEndpoint` would call
* `lm.selectChatModels()` (empty selector) on every invocation — which fans out across
* all registered vendors and re-resolves each one. In long automation runs that run at
* several Hz for the entire turn, this can dominate renderer/CDP traffic.
*/
private _firstNonCopilotModelPromise: Promise<LanguageModelChat | undefined> | undefined;
private _invalidateDelayer: Delayer<void> | undefined;
private _changeListenerInstalled = false;

override async getChatEndpoint(requestOrFamilyOrModel: LanguageModelChat | ChatRequest | ChatEndpointFamily): Promise<IChatEndpoint> {
const isProxyingCAPI = !!this._configService.getConfig(ConfigKey.Shared.DebugOverrideCAPIUrl) || !!this._configService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl);
if (this._authService.copilotToken?.isNoAuthUser && !isProxyingCAPI) {
// When using no auth in scenario automation, we want to force using a custom model / non-copilot for all requests
const getFirstNonCopilotModel = async () => {
const allModels = await lm.selectChatModels();
const firstNonCopilotModel = allModels.find(m => m.vendor !== 'copilot');
const firstNonCopilotModel = await this._resolveFirstNonCopilotModel();
if (firstNonCopilotModel) {
this._logService.trace(`Using custom contributed chat model`);
this._logService.trace(`ScenarioAutomation: using BYOK model ${firstNonCopilotModel.vendor}/${firstNonCopilotModel.id}`);
return this._instantiationService.createInstance(ExtensionContributedChatEndpoint, firstNonCopilotModel);
} else {
this._logService.error(`ScenarioAutomation: no non-copilot models registered`);
throw new Error('No custom contributed chat models found.');
}
};

// Check if we have a hard-coded family which indicates a copilot model
if (typeof requestOrFamilyOrModel === 'string') {
this._logService.trace(`ScenarioAutomation: redirecting family '${requestOrFamilyOrModel}' to BYOK`);
return getFirstNonCopilotModel();
}

// Check if a copilot model was explicitly requested in the picker
const model = 'model' in requestOrFamilyOrModel ? requestOrFamilyOrModel.model : requestOrFamilyOrModel;
if (model.vendor === 'copilot') {
this._logService.trace(`ScenarioAutomation: redirecting copilot model '${model.id}' to BYOK`);
return getFirstNonCopilotModel();
}
}
Expand All @@ -44,10 +59,46 @@ export class ScenarioAutomationEndpointProviderImpl extends ProductionEndpointPr
// In scenario automation, some model families (e.g. copilot-utility-small → gpt-4o-mini) may
// not be available via the capi proxy. Fall back to copilot-utility.
if (typeof requestOrFamilyOrModel === 'string') {
this._logService.trace(`ScenarioAutomation: failed to resolve model family '${requestOrFamilyOrModel}', falling back to copilot-utility`);
this._logService.warn(`ScenarioAutomation: failed to resolve model family '${requestOrFamilyOrModel}', falling back to copilot-utility: ${error}`);
return super.getChatEndpoint('copilot-utility');
}
throw error;
}
}

private _resolveFirstNonCopilotModel(): Promise<LanguageModelChat | undefined> {
this._ensureChangeListener();
if (!this._firstNonCopilotModelPromise) {
this._firstNonCopilotModelPromise = (async () => {
try {
const allModels = await lm.selectChatModels();
const found = allModels.find(m => m.vendor !== 'copilot');
this._logService.info(`ScenarioAutomation: resolved BYOK model ${found ? `${found.vendor}/${found.id}` : '<none>'} from ${allModels.length} registered model(s)`);
return found;
} catch (err) {
this._logService.warn(`ScenarioAutomation: selectChatModels failed; clearing cache: ${err}`);
this._firstNonCopilotModelPromise = undefined;
throw err;
}
})();
}
return this._firstNonCopilotModelPromise;
}

private _ensureChangeListener(): void {
if (this._changeListenerInstalled) {
return;
}
this._changeListenerInstalled = true;
// Coalesce bursts of model-set changes (e.g. when a BYOK provider activates and
// publishes several utility-alias models in quick succession) into a single
// invalidation so we don't churn the cache.
this._invalidateDelayer = this._register(new Delayer<void>(MicrotaskDelay));
this._register(lm.onDidChangeChatModels(() => {
this._invalidateDelayer!.trigger(() => {
this._logService.info(`ScenarioAutomation: chat model set changed; invalidating cached BYOK model`);
this._firstNonCopilotModelPromise = undefined;
}).catch(() => { /* cancelled on dispose */ });
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ export function formatModelDetailsWithCredits(modelName: string, creditsUsed: nu
export function formatModelDetailsWithMultiplier(modelName: string, multiplier: number | undefined): string {
return multiplier !== undefined ? l10n.t('{0} \u2022 {1}x', modelName, multiplier) : modelName;
}

/**
* Formats model details for auto mode when the model name should be hidden.
* Shows "Auto • N credits" or "Auto • Nx" instead of the actual model name.
*/
export function formatAutoModeDetails(creditsUsed: number | undefined, multiplier: number | undefined): string {
return formatModelDetails(l10n.t('Auto'), multiplier, creditsUsed);
}
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"@microsoft/dev-tunnels-management": "^1.3.41",
"@microsoft/dev-tunnels-ssh": "^3.12.22",
"@microsoft/dev-tunnels-ssh-tcp": "^3.12.22",
"@microsoft/mxc-sdk": "0.3.0",
"@microsoft/mxc-sdk": "0.6.0",
"@parcel/watcher": "^2.5.6",
"@types/semver": "^7.5.8",
"@vscode/codicons": "^0.0.46-14",
Expand Down
8 changes: 4 additions & 4 deletions remote/package-lock.json

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

2 changes: 1 addition & 1 deletion remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@github/copilot-sdk": "1.0.0-beta.8",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@microsoft/mxc-sdk": "0.3.0",
"@microsoft/mxc-sdk": "0.6.0",
"@parcel/watcher": "^2.5.6",
"@vscode/copilot-api": "^0.4.2",
"@vscode/deviceid": "^0.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {

// Playwright
const agentNetworkFilterService = this._register(new AgentNetworkFilterService(accessor.get(IConfigurationService)));
const playwrightChannel = this._register(new PlaywrightChannel(this.server, accessor.get(IMainProcessService), accessor.get(ILogService), agentNetworkFilterService));
const playwrightChannel = this._register(new PlaywrightChannel(this.server, accessor.get(IMainProcessService), accessor.get(ILogService), agentNetworkFilterService, accessor.get(ITelemetryService)));
this.server.registerChannel('playwright', playwrightChannel);

// Local Git
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
return connection;
}

async addManagedConnection(entry: IRemoteAgentHostEntry, connection: IAgentConnection, transportDisposable?: IDisposable): Promise<IRemoteAgentHostConnectionInfo> {
async addManagedConnection(entry: IRemoteAgentHostEntry, connection: IAgentConnection, transportDisposable?: IDisposable, status = RemoteAgentHostConnectionStatus.connected): Promise<IRemoteAgentHostConnectionInfo> {
if (!this._configurationService.getValue<boolean>(RemoteAgentHostsEnabledSettingId)) {
throw new Error('Remote agent host connections are not enabled.');
}
Expand All @@ -311,7 +311,7 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
// Create a connection entry wrapping the pre-connected client
const protocolClient = connection as RemoteAgentHostProtocolClient;
store.add(protocolClient);
const connEntry: IConnectionEntry = { store, client: protocolClient, transportDisposable, connected: true, status: RemoteAgentHostConnectionStatus.connected };
const connEntry: IConnectionEntry = { store, client: protocolClient, transportDisposable, connected: RemoteAgentHostConnectionStatus.isConnected(status), status };
this._entries.set(address, connEntry);
this._names.set(address, entry.name);
this._registeredEntries.set(address, entry);
Expand Down Expand Up @@ -340,7 +340,7 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
name: entry.name,
clientId: protocolClient.clientId,
defaultDirectory: protocolClient.defaultDirectory,
status: RemoteAgentHostConnectionStatus.connected,
status,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,3 @@ export function toContainerCustomization(entry: IPersistedCustomizationConfigEnt
enabled: true,
};
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import product from '../../product/common/product.js';
import { Registry } from '../../registry/common/platform.js';
import {
AgentHostClaudeAgentSdkPathSettingId,
AgentHostCodexAgentBinaryArgsSettingId,
AgentHostCodexAgentBinaryPathSettingId,
AgentHostCodexAgentCodexHomeSettingId,
AgentHostOTelCaptureContentSettingId,
AgentHostOTelDbSpanExporterEnabledSettingId,
AgentHostOTelEnabledSettingId,
Expand Down Expand Up @@ -46,6 +49,28 @@ configurationRegistry.registerConfiguration({
tags: ['experimental', 'advanced'],
included: product.quality !== 'stable',
},
[AgentHostCodexAgentBinaryPathSettingId]: {
type: 'string',
description: nls.localize('chat.agentHost.codexAgent.path', "Experimental, for local testing only. Absolute path to a locally-installed `codex` binary. When set, the Codex agent provider is registered inside the agent host and `codex app-server` is spawned from this path. Requires `#chat.agentHost.enabled#`. The agent host process must be restarted for changes to take effect."),
default: '',
tags: ['experimental', 'advanced'],
included: product.quality !== 'stable',
},
[AgentHostCodexAgentCodexHomeSettingId]: {
type: 'string',
description: nls.localize('chat.agentHost.codexAgent.codexHome', "Optional override for `$CODEX_HOME`. Controls where the codex binary reads config and writes rollouts. When empty, codex uses its default (`~/.codex`)."),
default: '',
tags: ['experimental', 'advanced'],
included: product.quality !== 'stable',
},
[AgentHostCodexAgentBinaryArgsSettingId]: {
type: 'array',
items: { type: 'string' },
description: nls.localize('chat.agentHost.codexAgent.binaryArgs', "Additional command-line arguments passed to `codex app-server`. Primarily useful for debugging (for example, `--log-level=debug`)."),
default: [],
tags: ['experimental', 'advanced'],
included: product.quality !== 'stable',
},
[AgentHostOTelEnabledSettingId]: {
type: 'boolean',
markdownDescription: nls.localize('chat.agentHost.otel.enabled', "When enabled, the agent host emits OpenTelemetry traces from the Copilot SDK. Requires `#chat.agentHost.enabled#`. Either configure `#chat.agentHost.otel.otlpEndpoint#` to ship traces to an external collector or enable `#chat.agentHost.otel.dbSpanExporter.enabled#` to capture them locally."),
Expand Down
1 change: 0 additions & 1 deletion src/vs/platform/agentHost/common/agentPluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,3 @@ export interface IAgentPluginManager {
*/
syncCustomizations(clientId: string, customizations: ClientPluginCustomization[], progress?: (status: Customization) => void): Promise<ISyncedCustomization[]>;
}

40 changes: 40 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,46 @@ export const AgentHostClaudeAgentSdkPathSettingId = 'chat.agentHost.claudeAgent.
*/
export const AgentHostClaudeSdkPathEnvVar = 'VSCODE_AGENT_HOST_CLAUDE_SDK_PATH';

// -- Codex agent settings --------------------------------------------------------
//
// Codex is opt-in via `chat.agentHost.codexAgent.path`. The setting points at
// an absolute path to the `codex` binary; the agent host spawns
// `<path> app-server` as a long-lived child process and speaks JSON-RPC over
// stdio. The binary is not bundled; users install codex themselves (typically
// via `npm install -g @openai/codex` or a platform package manager).

/**
* Absolute path to a locally-installed `codex` binary. When non-empty, the
* Codex agent provider is registered inside the agent host. Empty (the
* default) disables the provider entirely.
*/
export const AgentHostCodexAgentBinaryPathSettingId = 'chat.agentHost.codexAgent.path';

/**
* Optional override for `$CODEX_HOME`. When set, the codex app-server child
* process inherits this value, controlling where rollouts and config live.
*/
export const AgentHostCodexAgentCodexHomeSettingId = 'chat.agentHost.codexAgent.codexHome';

/**
* Additional command-line arguments passed to `codex app-server`. Mainly for
* debugging (e.g. `--log-level=debug`).
*/
export const AgentHostCodexAgentBinaryArgsSettingId = 'chat.agentHost.codexAgent.binaryArgs';

/**
* Environment variable the agent host process reads to locate the codex
* binary. Forwarded by the starters from
* {@link AgentHostCodexAgentBinaryPathSettingId}.
*/
export const AgentHostCodexAgentBinaryPathEnvVar = 'VSCODE_AGENT_HOST_CODEX_APP_SERVER_PATH';

/** Forwarded `$CODEX_HOME`. */
export const AgentHostCodexAgentCodexHomeEnvVar = 'CODEX_HOME';

/** Forwarded extra args for `codex app-server` (JSON-encoded string[]). */
export const AgentHostCodexAgentBinaryArgsEnvVar = 'VSCODE_AGENT_HOST_CODEX_APP_SERVER_ARGS';

// -- OpenTelemetry settings ------------------------------------------------------
//
// The `chat.agentHost.otel.*` namespace surfaces the same exporter knobs the CLI
Expand Down
1 change: 0 additions & 1 deletion src/vs/platform/agentHost/common/customAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,3 @@ export function resolveAgentHostAgent(
}
return storedAgentUri ? agents.find(a => a.uri === storedAgentUri) : undefined;
}

7 changes: 6 additions & 1 deletion src/vs/platform/agentHost/common/remoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,13 @@ export interface IRemoteAgentHostService {
* Callers should put any teardown that needs to happen on entry removal
* (e.g. closing the shared-process tunnel, dropping renderer-side handles)
* into this disposable, so a single removal path tears down the whole stack.
*
* `status` defaults to `connected`. Pass `incompatible` when the managed
* transport is alive but the protocol handshake rejected the client version;
* this keeps recovery actions (such as server upgrade) addressable without
* exposing the connection as ready for session traffic.
*/
addManagedConnection(entry: IRemoteAgentHostEntry, connection: IAgentConnection, transportDisposable?: IDisposable): Promise<IRemoteAgentHostConnectionInfo>;
addManagedConnection(entry: IRemoteAgentHostEntry, connection: IAgentConnection, transportDisposable?: IDisposable, status?: RemoteAgentHostConnectionStatus): Promise<IRemoteAgentHostConnectionInfo>;

/**
* Force the protocol client at `address` (if any) to treat its
Expand Down
Loading
Loading