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
13 changes: 0 additions & 13 deletions src/vs/platform/agentHost/common/agentHostCustomizationConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ export const enum AgentHostConfigKey {
* TODO: revisit magic key in config; refine into a dedicated typed channel. https://github.com/microsoft/vscode/issues/313812
*/
DefaultShell = 'defaultShell',
/**
* When true (the default), the Claude provider routes all Anthropic
* `messages` traffic through the local Copilot-CAPI proxy (Copilot-routed
* Claude). When false, the Claude Agent SDK talks to Anthropic directly on
* the user's own credentials (BYO Anthropic — Phase 19).
*/
ClaudeUseCopilotProxy = 'claudeUseCopilotProxy',
/**
* Experimentation flag for conditional agent-window auth. When true, a
* session type that is usable without GitHub (e.g. Claude in native mode with
Expand Down Expand Up @@ -98,12 +91,6 @@ export const agentHostCustomizationConfigSchema = createSchema({
title: localize('agentHost.config.defaultShell.title', "Default Shell"),
description: localize('agentHost.config.defaultShell.description', "Absolute path to the shell executable used by host-managed terminals. Normally pushed by the connected VS Code client from `terminal.integrated.agentHostProfile.<os>` (falling back to `terminal.integrated.defaultProfile.<os>`); when unset, the agent host falls back to the system shell. Only the path is supported; `args` and `env` from the workbench profile are not piped through yet. The workbench only pushes this for the local agent host — remote agent host operators should set this directly in the remote machine's `agent-host-config.json`."),
}),
[AgentHostConfigKey.ClaudeUseCopilotProxy]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.claudeUseCopilotProxy.title', "Route Claude Through Copilot"),
description: localize('agentHost.config.claudeUseCopilotProxy.description', "When enabled (the default), the Claude agent routes all requests through GitHub Copilot. When disabled, Claude talks to Anthropic directly using your own credentials (API key or Claude subscription)."),
default: true,
}),
[AgentHostConfigKey.AllowSignedOutWhenUsable]: schemaProperty<boolean>({
type: 'boolean',
title: localize('agentHost.config.allowSignedOutWhenUsable.title', "Allow Signed-Out Agent Window"),
Expand Down
27 changes: 17 additions & 10 deletions src/vs/platform/agentHost/node/claude/claudeAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,27 +478,23 @@ export class ClaudeAgent extends Disposable implements IAgent {
// waiting for `authenticate()`. Without this a signed-out window with a local
// Claude setup would show an empty picker. `queueMicrotask` runs it off the
// ctor stack. The per-session transport is derived on demand at materialize
// (see {@link _defaultTransportMode}), so a `claudeUseCopilotProxy` change
// needs no reactive re-resolve — the next session simply reads it live.
// (see {@link _defaultTransportMode}), so a sign-in state change needs no
// reactive re-resolve — the next session simply reads it live.
queueMicrotask(() => { void this._startModelRefresh(); });
}

/**
* The fallback transport for a session whose model names no provider (model-less
* or a bare/legacy id). Read on demand at materialize — never cached — from live
* availability: a started {@link _proxyHandle} means Copilot is serveable now, a
* local Claude setup means native is. The precedence (explicit
* `claudeUseCopilotProxy` override; else sign-in state and local setup) is
* delegated to the pure {@link resolveClaudeTransportMode}. A provider-qualified
* model bypasses this and routes on its own provider.
* local Claude setup means native is. The precedence (sign-in state, then local
* setup) is delegated to the pure {@link resolveClaudeTransportMode}. A
* provider-qualified model bypasses this and routes on its own provider.
*/
private _defaultTransportMode(): ClaudeTransportMode {
// An absent `claudeUseCopilotProxy` stays `undefined` so the pure function
// can tell an explicit override from "fall through to the sign-in rules".
const explicitProxy = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.ClaudeUseCopilotProxy);
const allowSignedOutWhenUsable = this._configurationService.getRootValue(agentHostCustomizationConfigSchema, AgentHostConfigKey.AllowSignedOutWhenUsable) === true;
const hasExistingSetup = allowSignedOutWhenUsable && detectExistingClaudeSetup(this._environmentService.userHome.fsPath);
return resolveClaudeTransportMode({ explicitProxy, allowSignedOutWhenUsable, hasGitHubToken: this._proxyHandle !== undefined, hasExistingSetup });
return resolveClaudeTransportMode({ allowSignedOutWhenUsable, hasGitHubToken: this._proxyHandle !== undefined, hasExistingSetup });
}

// #region Descriptor + auth
Expand Down Expand Up @@ -680,6 +676,17 @@ export class ClaudeAgent extends Disposable implements IAgent {
* one source erroring; only when *every* source we attempted fails do we keep
* the last known-good catalog instead of blanking, so a transient double
* failure never wipes the picker.
*
* Gating the native half on {@link detectExistingClaudeSetup} is deliberate and
* load-bearing, not just an optimization. `supportedModels()` returns a *static*
* list of models the SDK understands — it is not an entitlement or credential
* check, and it answers even with no `ANTHROPIC_API_KEY`, no
* `CLAUDE_CODE_OAUTH_TOKEN` and an empty `HOME`. Publishing it unconditionally
* would advertise models for an agent that cannot serve a single request, which
* reads downstream as "usable without GitHub" and would hold the Agents window
* open on an agent that fails on its first turn. An empty catalog is the honest
* signal: it surfaces as "no models" (`SessionTypeAuthRequirement.Unusable`)
* rather than a sign-in prompt that would not help.
*/
private async _refreshModels(): Promise<void> {
const tokenAtStart = this._githubToken;
Expand Down
17 changes: 12 additions & 5 deletions src/vs/platform/agentHost/node/claude/claudeModelSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,18 @@ export function resolveClaudeSessionTransport(inputs: {
* so re-stamping it to a transport token would misroute a model-selected
* `create_session`. The transport/group token lives only in `_meta`.
*
* Proxy models come first to preserve the picker's `models[0]`-is-default
* convention for the common (Copilot) case. Every other field is passed through
* untouched. Either list may be empty — one source failing to fetch contributes
* nothing but must never blank the other — so merging an empty side just yields
* the other side's qualified models.
* Array order is *not* what picks the session default. The picker re-buckets the
* flat list by the `_meta` vendor token and renders group-by-group, so which
* model is pre-selected follows the group ordering — verified end-to-end: with
* both halves populated the Anthropic group sorts first and
* `@provider=anthropic:default` is pre-selected, i.e. the default routes native
* and bills the user's own Anthropic account. Do not reason about the default
* from the order here. (Making that choice explicit rather than emergent needs a
* default/sticky model preference, which does not exist yet.)
*
* Every other field is passed through untouched. Either list may be empty — one
* source failing to fetch contributes nothing but must never blank the other —
* so merging an empty side just yields the other side's qualified models.
*/
export function mergeClaudeModelCatalogs(proxy: readonly IAgentModelInfo[], native: readonly IAgentModelInfo[]): IAgentModelInfo[] {
return [
Expand Down
55 changes: 33 additions & 22 deletions src/vs/platform/agentHost/node/claude/claudeTransportMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@ import { vObj, vOptionalProp, vString, type ValidatorType } from '../../../../ba
export type ClaudeTransportMode = 'proxy' | 'native';

/**
* The four precedence inputs {@link resolveClaudeTransportMode} decides over.
* The three precedence inputs {@link resolveClaudeTransportMode} decides over.
*/
export interface IClaudeTransportModeInputs {
/**
* User/workspace-set value of `claudeUseCopilotProxy`, or `undefined` when
* unset — the distinction between an explicit choice and the default is what
* makes an explicit setting a hard override.
*/
readonly explicitProxy: boolean | undefined;
/** Whether the experimentation flag enabling signed-out-when-usable is on. */
readonly allowSignedOutWhenUsable: boolean;
/** Whether a GitHub Copilot token has been captured (i.e. signed in). */
Expand All @@ -33,25 +27,42 @@ export interface IClaudeTransportModeInputs {
}

/**
* Pure decision (ADR 0001, "D4"): which transport should the Claude provider
* use right now? Precedence, highest first:
* Which transport should the Claude provider fall back to right now? Pure
* decision; precedence, highest first:
*
* 1. An explicit `claudeUseCopilotProxy` setting is a HARD override.
* 2. Feature flag off means today's default behavior (always proxy).
* 3. Signed in to GitHub prefers Copilot (proxy).
* 4. Signed out but with the user's own Claude credentials uses native (no GitHub).
* 5. Nothing usable falls back to proxy, which surfaces as requires-GitHub and drives the
* window sign-in gate.
* 1. Feature flag off means today's default behavior (always proxy).
* 2. Signed in to GitHub prefers Copilot (proxy).
* 3. Signed out but with the user's own Claude credentials uses native (no GitHub).
* 4. Nothing usable still falls back to proxy — the safe end, since attempting
* native with no credential would fail inside the SDK rather than at a
* surface that can explain itself.
*
* Native mode drops the GitHub Copilot protected resource, so getting this
* decision right is what lets a signed-out user with their own credentials run
* without being forced to sign in.
* This is only the *fallback* for a session whose model names no provider. A
* provider-qualified model routes on its own provider
* (`resolveClaudeSessionTransport`), so getting this decision right is what lets
* a signed-out user with their own credentials start working without being
* forced to sign in.
*
* The result is **not** an input to the Agents window's sign-in gate, and
* resolving to `proxy` does not by itself make the session type "require
* GitHub". Claude advertises the Copilot protected resource as `required: false`
* unconditionally, so `resolveAgentAuthRequirement` separates "usable" from
* "unusable" on the *model count* instead: in case 4 neither half of the merged
* catalog can be enumerated, the published catalog is empty, and the type
* resolves to `Unusable` — surfacing as "no models". The proxy fallback only
* bites at use time, when a model-less/bare session actually materializes with no
* proxy handle and `_ensureAuthenticated` raises `AHP_AUTH_REQUIRED`.
*
* There is deliberately no host-global setting to *prefer* a transport. Since
* the picker offers both providers' models side by side, transport is downstream
* of the model the user picked; a flag would keep disagreeing with what the
* picker shows (it could not stop a Copilot-routed model from being offered or
* chosen, because neither model enumeration nor the advertised protected
* resources would consult it). Expressing a preference is a *model*-selection
* concern — a default/sticky model — not a transport one.
*/
export function resolveClaudeTransportMode(inputs: IClaudeTransportModeInputs): ClaudeTransportMode {
const { explicitProxy, allowSignedOutWhenUsable, hasGitHubToken, hasExistingSetup } = inputs;
if (explicitProxy !== undefined) {
return explicitProxy ? 'proxy' : 'native';
}
const { allowSignedOutWhenUsable, hasGitHubToken, hasExistingSetup } = inputs;
if (!allowSignedOutWhenUsable) {
return 'proxy';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ suite('AgentHostGitHubEndpointService', () => {
disposables.add(service.onDidChange(() => fires++));

// An unrelated root-config change must NOT fire.
configService.updateRootConfig({ [AgentHostConfigKey.ClaudeUseCopilotProxy]: false });
configService.updateRootConfig({ [AgentHostConfigKey.AllowSignedOutWhenUsable]: true });
assert.strictEqual(fires, 0);

// Setting the enterprise URI fires once and repoints the endpoints.
Expand Down
Loading
Loading