Skip to content

Commit e7cc5f5

Browse files
committed
Agent Host changes for lramos15/agents/auto-tiers-routing-profiles-optimization
1 parent 1dbbce1 commit e7cc5f5

20 files changed

Lines changed: 549 additions & 91 deletions

File tree

extensions/copilot/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4389,7 +4389,7 @@
43894389
"null"
43904390
],
43914391
"default": null,
4392-
"markdownDescription": "Overrides the routing tier that the `Auto` model requests, ignoring both the tier picked in the model picker and the tier inline chat defaults to. Accepts `eco`, `balanced`, `max`, or `fast`. Used by evals.\n\n**Note**: This is an advanced debugging setting.",
4392+
"markdownDescription": "Overrides the routing tier that the `Auto` model requests, ignoring both the tier picked in the model picker and the tier inline chat defaults to. Accepts `efficiency`, `balance`, `intelligence`, or `fast`. Used by evals.\n\n**Note**: This is an advanced debugging setting.",
43934393
"tags": [
43944394
"advanced"
43954395
]

extensions/copilot/src/extension/conversation/common/languageModelAccess.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ export function buildReasoningEffortSchemaProperty(effortLevels: readonly string
9090
*/
9191
export function getAutoModeTierLabel(tier: string): string {
9292
switch (tier) {
93-
case 'eco': return l10n.t('Efficiency');
94-
case 'balanced': return l10n.t('Balance');
95-
case 'max': return l10n.t('Intelligence');
93+
case 'efficiency': return l10n.t('Efficiency');
94+
case 'balance': return l10n.t('Balance');
95+
case 'intelligence': return l10n.t('Intelligence');
9696
case 'fast': return l10n.t('Fast');
9797
default: return tier.charAt(0).toUpperCase() + tier.slice(1);
9898
}
@@ -104,9 +104,9 @@ export function getAutoModeTierLabel(tier: string): string {
104104
*/
105105
export function getAutoModeTierDescription(tier: string): string {
106106
switch (tier) {
107-
case 'eco': return l10n.t('Cheaper models for everyday tasks');
108-
case 'balanced': return l10n.t('Balances capability and cost');
109-
case 'max': return l10n.t('Most capable models, higher cost');
107+
case 'efficiency': return l10n.t('Cheaper models for everyday tasks');
108+
case 'balance': return l10n.t('Balances capability and cost');
109+
case 'intelligence': return l10n.t('Most capable models, higher cost');
110110
case 'fast': return l10n.t('Lowest latency models');
111111
default: return tier;
112112
}

extensions/copilot/src/extension/conversation/vscode-node/test/languageModelAccess.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ suite('reasoning effort schema', () => {
663663
});
664664

665665
test('Kimi K3 prefers high when available', () => {
666-
assert.strictEqual(pickDefaultReasoningEffort(['low', 'high', 'max'], 'kimi-k3'), 'high');
666+
assert.strictEqual(pickDefaultReasoningEffort(['low', 'high', 'intelligence'], 'kimi-k3'), 'high');
667667
});
668668

669669
test('non-claude family prefers medium when available', () => {
@@ -698,10 +698,10 @@ suite('auto mode tier schema', () => {
698698
assert.deepStrictEqual(buildAutoModeTierSchemaProperty(selectableAutoModeTiers, defaultAutoModeTier), {
699699
type: 'string',
700700
title: 'Optimize for',
701-
enum: ['eco', 'balanced', 'max'],
701+
enum: ['efficiency', 'balance', 'intelligence'],
702702
enumItemLabels: ['Efficiency', 'Balance', 'Intelligence'],
703703
enumDescriptions: ['Cheaper models for everyday tasks', 'Balances capability and cost', 'Most capable models, higher cost'],
704-
default: 'balanced',
704+
default: 'balance',
705705
group: 'navigation',
706706
});
707707
});

extensions/copilot/src/platform/endpoint/common/autoModeTiers.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* Routing profiles accepted by `POST /auto`. A tier is picked per session and
88
* biases which models the router may choose from.
99
*/
10-
export const autoModeTiers = ['eco', 'balanced', 'max', 'fast'] as const;
10+
export const autoModeTiers = ['efficiency', 'balance', 'intelligence', 'fast'] as const;
1111

1212
export type AutoModeTier = typeof autoModeTiers[number];
1313

@@ -17,17 +17,37 @@ export type AutoModeTier = typeof autoModeTiers[number];
1717
* offered as a choice. It remains reachable through the internal
1818
* {@link ConfigKey.Advanced.AutoModeTierOverride} setting.
1919
*/
20-
export const selectableAutoModeTiers: readonly AutoModeTier[] = ['eco', 'balanced', 'max'];
20+
export const selectableAutoModeTiers: readonly AutoModeTier[] = ['efficiency', 'balance', 'intelligence'];
2121

2222
/** The tier used when the user has not picked one. */
23-
export const defaultAutoModeTier: AutoModeTier = 'balanced';
23+
export const defaultAutoModeTier: AutoModeTier = 'balance';
2424

2525
/** The tier inline chat defaults to; latency matters more than routing depth there. */
2626
export const inlineChatAutoModeTier: AutoModeTier = 'fast';
2727

2828
/** Key the selected tier is stored under in the Auto model's configuration. */
2929
export const AUTO_MODE_TIER_PROPERTY = 'tier';
3030

31+
/**
32+
* Tier names retired in favour of the current ones. `POST /auto` still accepts
33+
* them, but the agent runtime rejects them at its session boundary, so one
34+
* vocabulary is used everywhere and these are mapped forward on read.
35+
*/
36+
const retiredAutoModeTiers: Readonly<Record<string, AutoModeTier>> = {
37+
eco: 'efficiency',
38+
balanced: 'balance',
39+
max: 'intelligence',
40+
};
41+
42+
/**
43+
* Maps a retired tier name to its current one, leaving anything else untouched.
44+
* Only raw inputs that bypass the picker schema need this, notably the override
45+
* setting; a persisted picker value is already filtered against the live schema.
46+
*/
47+
export function normalizeAutoModeTier(value: string): string {
48+
return retiredAutoModeTiers[value] ?? value;
49+
}
50+
3151
/**
3252
* Narrows an untrusted value (persisted model configuration, or configuration
3353
* supplied by a third-party extension through the `vscode.lm` API) to a tier the

extensions/copilot/src/platform/endpoint/node/autoV2Fetcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export class AutoV2Fetcher {
151151
"conversationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The conversation ID in which the selection was made." },
152152
"vscodeRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The VS Code chat request id in which the selection was made." },
153153
"selectedModel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model the server selected for this prompt." },
154-
"tier": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The routing profile requested for this selection, e.g. eco, balanced, max, fast. Empty when none was requested." },
154+
"tier": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The routing profile requested for this selection, e.g. efficiency, balance, intelligence, fast. Empty when none was requested." },
155155
"e2eLatencyMs": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "The end-to-end latency of the auto request in milliseconds, including network overhead." },
156156
"scoreReasoning": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Hydra per-dimension score for reasoning. -1 if not present in the response." },
157157
"scoreCodeGen": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Hydra per-dimension score for code generation. -1 if not present in the response." },

extensions/copilot/src/platform/endpoint/node/automodeService.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { IChatEndpoint } from '../../networking/common/networking';
1818
import { IRequestLogger } from '../../requestLogger/common/requestLogger';
1919
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
2020
import { ITelemetryService } from '../../telemetry/common/telemetry';
21-
import { AUTO_MODE_TIER_PROPERTY, autoModeTiers, defaultAutoModeTier, inlineChatAutoModeTier, isSelectableAutoModeTier, type AutoModeTier } from '../common/autoModeTiers';
21+
import { AUTO_MODE_TIER_PROPERTY, autoModeTiers, defaultAutoModeTier, inlineChatAutoModeTier, isSelectableAutoModeTier, normalizeAutoModeTier, type AutoModeTier } from '../common/autoModeTiers';
2222
import { ICAPIClientService } from '../common/capiClient';
2323
import type { IChatModelCapabilities, IChatModelInformation } from '../common/endpointProvider';
2424
import { AutoChatEndpoint } from './autoChatEndpoint';
@@ -397,16 +397,19 @@ export class AutomodeService extends Disposable implements IAutomodeService {
397397
*
398398
* Only a non-default selection counts as explicit: the workbench materializes
399399
* the schema default into `modelConfiguration` and strips a pick of the
400-
* default back out when storing it, so a `balanced` entry cannot be told
401-
* apart from "never picked" — reading it as a selection would make the inline
402-
* pin below unreachable.
400+
* default back out when storing it, so a `balance` entry cannot be told apart
401+
* from "never picked" — reading it as a selection would make the inline pin
402+
* below unreachable.
403403
*/
404404
private _resolveTier(chatRequest: IAutoModeRoutingRequest | undefined): AutoModeTier | undefined {
405405
const override = this._configurationService.getConfig(ConfigKey.Advanced.AutoModeTierOverride);
406406
if (override) {
407+
// Normalized because the override is a raw string setting, so unlike a picker value it never
408+
// passes through the schema filter that drops retired names.
409+
const normalized = normalizeAutoModeTier(override);
407410
// The override is internal, so unlike the picker it may select `fast`.
408-
if ((autoModeTiers as readonly string[]).includes(override)) {
409-
return override as AutoModeTier;
411+
if ((autoModeTiers as readonly string[]).includes(normalized)) {
412+
return normalized as AutoModeTier;
410413
}
411414
this._logService.warn(`[AutomodeService] Ignoring auto tier override '${override}' — not one of [${autoModeTiers.join(', ')}].`);
412415
}

extensions/copilot/src/platform/endpoint/node/test/automodeService.spec.ts

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,10 @@ describe('AutomodeService', () => {
390390
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
391391

392392
await Promise.all([
393-
route('session-a', 'max'),
394-
route('session-b', 'max'),
395-
route('session-a', 'eco'),
396-
route('session-a', 'max', [{ value: { mimeType: 'image/png' } }]),
393+
route('session-a', 'intelligence'),
394+
route('session-b', 'intelligence'),
395+
route('session-a', 'efficiency'),
396+
route('session-a', 'intelligence', [{ value: { mimeType: 'image/png' } }]),
397397
]);
398398

399399
expect(autoCalls()).toHaveLength(4);
@@ -632,10 +632,10 @@ describe('AutomodeService', () => {
632632
location: ChatLocation.Editor,
633633
prompt: 'test prompt',
634634
sessionId: 'session-auto-inline-tier',
635-
modelConfiguration: { tier: 'max' },
635+
modelConfiguration: { tier: 'intelligence' },
636636
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
637637

638-
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'max' }]);
638+
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'intelligence' }]);
639639
});
640640

641641
it('sends the tier picked in the model configuration', async () => {
@@ -648,10 +648,10 @@ describe('AutomodeService', () => {
648648
location: ChatLocation.Panel,
649649
prompt: 'test prompt',
650650
sessionId: 'session-auto-tier',
651-
modelConfiguration: { tier: 'max' },
651+
modelConfiguration: { tier: 'intelligence' },
652652
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
653653

654-
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'max' }]);
654+
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'intelligence' }]);
655655
});
656656

657657
it('falls back to the default tier when the configured tier is not user selectable', async () => {
@@ -667,7 +667,7 @@ describe('AutomodeService', () => {
667667
modelConfiguration: { tier: 'fast' },
668668
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
669669

670-
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'balanced' }]);
670+
expect(autoRequestBodies()).toEqual([{ prompt: 'test prompt', tier: 'balance' }]);
671671
});
672672

673673
it('re-routes the conversation when the tier changes', async () => {
@@ -680,35 +680,35 @@ describe('AutomodeService', () => {
680680
location: ChatLocation.Panel,
681681
prompt: 'test prompt',
682682
sessionId: 'session-auto-tier-change',
683-
modelConfiguration: { tier: 'eco' },
683+
modelConfiguration: { tier: 'efficiency' },
684684
} as unknown as ChatRequest;
685685

686686
await automodeService.resolveAutoModeEndpoint(chatRequest, [mockChatEndpoint, gpt4oEndpoint]);
687687
await automodeService.resolveAutoModeEndpoint({ ...chatRequest, prompt: 'second turn' } as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
688-
await automodeService.resolveAutoModeEndpoint({ ...chatRequest, prompt: 'third turn', modelConfiguration: { tier: 'max' } } as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
688+
await automodeService.resolveAutoModeEndpoint({ ...chatRequest, prompt: 'third turn', modelConfiguration: { tier: 'intelligence' } } as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
689689

690-
expect(autoRequestBodies().map(b => b.tier)).toEqual(['eco', 'max']);
690+
expect(autoRequestBodies().map(b => b.tier)).toEqual(['efficiency', 'intelligence']);
691691
});
692692

693693
it('lets the tier override win over the picker and the inline chat pin', async () => {
694694
const gpt4oEndpoint = createEndpoint('gpt-4o', 'OpenAI');
695695
mockAuto(autoResponse('gpt-4o'));
696696

697-
setTierOverride('eco');
697+
setTierOverride('efficiency');
698698
automodeService = createService();
699699
await automodeService.resolveAutoModeEndpoint({
700700
location: ChatLocation.Panel,
701701
prompt: 'panel turn',
702702
sessionId: 'session-override-panel',
703-
modelConfiguration: { tier: 'max' },
703+
modelConfiguration: { tier: 'intelligence' },
704704
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
705705
await automodeService.resolveAutoModeEndpoint({
706706
location: ChatLocation.Editor,
707707
prompt: 'inline turn',
708708
sessionId: 'session-override-inline',
709709
} as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
710710

711-
expect(autoRequestBodies().map(b => b.tier)).toEqual(['eco', 'eco']);
711+
expect(autoRequestBodies().map(b => b.tier)).toEqual(['efficiency', 'efficiency']);
712712
});
713713

714714
// The override is an internal/eval knob, so unlike the picker it may target
@@ -741,10 +741,10 @@ describe('AutomodeService', () => {
741741
location: ChatLocation.Panel,
742742
prompt: 'panel turn',
743743
sessionId: 'session-override-bogus',
744-
modelConfiguration: { tier: 'max' },
744+
modelConfiguration: { tier: 'intelligence' },
745745
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
746746

747-
expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'max' }]);
747+
expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'intelligence' }]);
748748
});
749749

750750
it('announces tier support when the setting changes', async () => {
@@ -755,7 +755,7 @@ describe('AutomodeService', () => {
755755
const listener = automodeService.onDidChangeAutoModeTierSupport(() => announced++);
756756
await configurationService.setConfig(ConfigKey.Advanced.AutoModeTiersEnabled, true);
757757
// An unrelated change must not re-announce.
758-
await configurationService.setConfig(ConfigKey.Advanced.AutoModeTierOverride, 'max');
758+
await configurationService.setConfig(ConfigKey.Advanced.AutoModeTierOverride, 'intelligence');
759759
listener.dispose();
760760

761761
expect({ announced, supported: automodeService.areAutoModeTiersSupported() }).toEqual({ announced: 1, supported: true });
@@ -771,18 +771,18 @@ describe('AutomodeService', () => {
771771
location: ChatLocation.Panel,
772772
prompt: 'first turn',
773773
sessionId: 'session-auto-tier-error',
774-
modelConfiguration: { tier: 'eco' },
774+
modelConfiguration: { tier: 'efficiency' },
775775
} as unknown as ChatRequest;
776776
const first = await automodeService.resolveAutoModeEndpoint(chatRequest, [mockChatEndpoint, gpt4oEndpoint]);
777777
expect(first.model).toBe('gpt-4o');
778778

779-
// The tier changes and the re-route fails: the eco endpoint must not be
779+
// The tier changes and the re-route fails: the efficiency endpoint must not be
780780
// handed back as though it satisfied the new tier.
781781
mockAuto({ error: 'server_error' }, 500);
782782
await expect(automodeService.resolveAutoModeEndpoint({
783783
...chatRequest,
784784
prompt: 'second turn',
785-
modelConfiguration: { tier: 'max' },
785+
modelConfiguration: { tier: 'intelligence' },
786786
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint])).rejects.toThrow();
787787
});
788788

@@ -798,15 +798,15 @@ describe('AutomodeService', () => {
798798
location: ChatLocation.Panel,
799799
prompt: 'first turn',
800800
sessionId: 'session-auto-tier-discount',
801-
modelConfiguration: { tier: 'eco' },
801+
modelConfiguration: { tier: 'efficiency' },
802802
} as unknown as ChatRequest;
803803
await automodeService.resolveAutoModeEndpoint(chatRequest, [mockChatEndpoint, gpt4oEndpoint]);
804804

805805
mockAuto(autoResponse('gpt-4o', { discounted_costs: { 'gpt-4o': 0.9 } }));
806806
await automodeService.resolveAutoModeEndpoint({
807807
...chatRequest,
808808
prompt: 'second turn',
809-
modelConfiguration: { tier: 'max' },
809+
modelConfiguration: { tier: 'intelligence' },
810810
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
811811

812812
const discounts = (mockInstantiationService.createInstance as ReturnType<typeof vi.fn>).mock.calls.map(c => c[3]);
@@ -825,7 +825,7 @@ describe('AutomodeService', () => {
825825
location,
826826
prompt: 'test prompt',
827827
sessionId: `session-tiers-off-${location}`,
828-
modelConfiguration: { tier: 'max' },
828+
modelConfiguration: { tier: 'intelligence' },
829829
} as unknown as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
830830
}
831831

@@ -843,15 +843,32 @@ describe('AutomodeService', () => {
843843
const gpt4oEndpoint = createEndpoint('gpt-4o', 'OpenAI');
844844
mockAuto(autoResponse('gpt-4o'));
845845

846-
setTierOverride('max');
846+
setTierOverride('intelligence');
847847
automodeService = createService();
848848
await automodeService.resolveAutoModeEndpoint({
849849
location: ChatLocation.Panel,
850850
prompt: 'panel turn',
851851
sessionId: 'session-override-tiers-off',
852852
} as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
853853

854-
expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'max' }]);
854+
expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'intelligence' }]);
855+
});
856+
857+
// The override is a raw string setting, so a config left on a retired name by an
858+
// eval or an internal user must keep working rather than silently fall back.
859+
it('maps a retired tier name in the override to its current one', async () => {
860+
const gpt4oEndpoint = createEndpoint('gpt-4o', 'OpenAI');
861+
mockAuto(autoResponse('gpt-4o'));
862+
863+
setTierOverride('eco');
864+
automodeService = createService();
865+
await automodeService.resolveAutoModeEndpoint({
866+
location: ChatLocation.Panel,
867+
prompt: 'panel turn',
868+
sessionId: 'session-override-retired',
869+
} as ChatRequest, [mockChatEndpoint, gpt4oEndpoint]);
870+
871+
expect(autoRequestBodies()).toEqual([{ prompt: 'panel turn', tier: 'efficiency' }]);
855872
});
856873
});
857874

@@ -875,7 +892,7 @@ describe('AutomodeService', () => {
875892
for (let i = 0; i < 50; i++) {
876893
await route(`session-${i}`, `turn ${i}`);
877894
}
878-
await route('session-49', 'retiered turn', 'max');
895+
await route('session-49', 'retiered turn', 'intelligence');
879896

880897
const callsBefore = autoCalls().length;
881898
await route('session-0', 'follow up');

0 commit comments

Comments
 (0)