Skip to content

Commit aa590b2

Browse files
authored
Handle smart image routing in auto (#3504)
1 parent 4614ff5 commit aa590b2

File tree

3 files changed

+53
-7
lines changed

3 files changed

+53
-7
lines changed

src/extension/conversation/vscode-node/languageModelAccess.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ export class LanguageModelAccess extends Disposable implements IExtensionContrib
278278
},
279279
isUserSelectable: endpoint.showInModelPicker,
280280
capabilities: {
281-
imageInput: endpoint.supportsVision,
281+
imageInput: endpoint instanceof AutoChatEndpoint ? true : endpoint.supportsVision,
282282
toolCalling: endpoint.supportsToolCalls,
283283
}
284284
};

src/extension/prompts/node/panel/image.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,14 @@ export class HistoricalImage extends PromptElement<HistoricalImageProps, unknown
4444
constructor(
4545
props: HistoricalImageProps,
4646
@IPromptEndpoint private readonly promptEndpoint: IPromptEndpoint,
47+
@IAuthenticationService private readonly authService: IAuthenticationService,
4748
) {
4849
super(props);
4950
}
5051

5152
override async render(_state: unknown, sizing: PromptSizing) {
5253
// If the model doesn't support vision, omit historical images
53-
if (!this.promptEndpoint.supportsVision) {
54+
if (!this.promptEndpoint.supportsVision || !this.authService.copilotToken?.isEditorPreviewFeaturesEnabled()) {
5455
return undefined;
5556
}
5657

@@ -77,7 +78,7 @@ export class Image extends PromptElement<ImageProps, unknown> {
7778
const fillerUri: Uri = this.props.reference ?? Uri.parse('Attached Image');
7879

7980
try {
80-
if (!this.promptEndpoint.supportsVision) {
81+
if (!this.promptEndpoint.supportsVision || !this.authService.copilotToken?.isEditorPreviewFeaturesEnabled()) {
8182
if (this.props.omitReferences) {
8283
return;
8384
}

src/platform/endpoint/node/automodeService.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ class AutoModeTokenBank extends Disposable {
9494
: 'copilotchat.autoModelHint';
9595

9696
const autoModeHint = this._expService.getTreatmentVariable<string>(expName) || 'auto';
97+
console.log(`AutoModeService: Using auto mode hint '${autoModeHint}' for location '${this._location}'.`);
9798

9899
const response = await this._capiClientService.makeRequest<Response>({
99100
json: {
100-
'auto_mode': { 'model_hints': [autoModeHint] }
101+
'auto_mode': { 'model_hints': ['grok-code-fast-1'] }
101102
},
102103
headers,
103104
method: 'POST'
@@ -229,6 +230,8 @@ export class AutomodeService extends Disposable implements IAutomodeService {
229230
throw new Error(errorMsg);
230231
}
231232
}
233+
selectedModel = this._applyVisionFallback(chatRequest, selectedModel, reserveToken.available_models, knownEndpoints);
234+
232235
const existingEndpoints = entry?.endpoints || [];
233236
let autoEndpoint = existingEndpoints.find(e => e.model === selectedModel.model);
234237
if (!autoEndpoint) {
@@ -259,7 +262,15 @@ export class AutomodeService extends Disposable implements IAutomodeService {
259262
}
260263
entry.endpoints = [this._instantiationService.createInstance(AutoChatEndpoint, newModel, entryToken.session_token, entryToken.discounted_costs?.[newModel.model] || 0, this._calculateDiscountRange(entryToken.discounted_costs))];
261264
}
262-
return entry.endpoints[0];
265+
// Apply vision fallback even on cached entries, since the cached model may not support images
266+
const cachedEndpoint = entry.endpoints[0];
267+
const fallbackEndpoint = this._applyVisionFallback(chatRequest, cachedEndpoint, entryToken.available_models, knownEndpoints);
268+
if (fallbackEndpoint !== cachedEndpoint) {
269+
const autoEndpoint = this._instantiationService.createInstance(AutoChatEndpoint, fallbackEndpoint, entryToken.session_token, entryToken.discounted_costs?.[fallbackEndpoint.model] || 0, this._calculateDiscountRange(entryToken.discounted_costs));
270+
entry.endpoints[0] = autoEndpoint;
271+
return autoEndpoint;
272+
}
273+
return cachedEndpoint;
263274
}
264275

265276
// No cached entry, use the reserve token
@@ -269,18 +280,38 @@ export class AutomodeService extends Disposable implements IAutomodeService {
269280
reserveTokenBank.debugName = conversationId;
270281

271282
const reserveToken = await reserveTokenBank.getToken();
272-
const selectedModel = knownEndpoints.find(e => e.model === reserveToken.selected_model);
283+
let selectedModel = knownEndpoints.find(e => e.model === reserveToken.selected_model);
273284
if (!selectedModel) {
274285
const errorMsg = `Auto mode failed: selected model '${reserveToken.selected_model}' not found in known endpoints.`;
275286
this._logService.error(errorMsg);
276287
throw new Error(errorMsg);
277288
}
289+
selectedModel = this._applyVisionFallback(chatRequest, selectedModel, reserveToken.available_models, knownEndpoints);
278290
const autoEndpoint = this._instantiationService.createInstance(AutoChatEndpoint, selectedModel, reserveToken.session_token, reserveToken.discounted_costs?.[selectedModel.model] || 0, this._calculateDiscountRange(reserveToken.discounted_costs));
279291

280292
this._autoModelCache.set(conversationId, { endpoints: [autoEndpoint], tokenBank: reserveTokenBank });
281293
return autoEndpoint;
282294
}
283295

296+
/**
297+
* If the request contains an image and the selected model doesn't support vision,
298+
* fall back to the first vision-capable model from the available models.
299+
*/
300+
private _applyVisionFallback(chatRequest: ChatRequest | undefined, selectedModel: IChatEndpoint, availableModels: string[], knownEndpoints: IChatEndpoint[]): IChatEndpoint {
301+
if (!hasImage(chatRequest) || selectedModel.supportsVision) {
302+
return selectedModel;
303+
}
304+
const visionModel = availableModels
305+
.map(model => knownEndpoints.find(e => e.model === model))
306+
.find(endpoint => endpoint?.supportsVision);
307+
if (visionModel) {
308+
this._logService.trace(`Selected model '${selectedModel.model}' does not support vision, falling back to '${visionModel.model}'.`);
309+
return visionModel;
310+
}
311+
this._logService.warn(`Request contains an image but no vision-capable model is available.`);
312+
return selectedModel;
313+
}
314+
284315
private _calculateDiscountRange(discounts: Record<string, number> | undefined): { low: number; high: number } {
285316
if (!discounts) {
286317
return { low: 0, high: 0 };
@@ -311,5 +342,19 @@ function getConversationId(chatRequest: ChatRequest | undefined): string {
311342
if (!chatRequest) {
312343
return 'unknown';
313344
}
314-
return (chatRequest?.toolInvocationToken as { sessionId: string })?.sessionId || 'unknown';
345+
return chatRequest?.sessionId || 'unknown';
346+
}
347+
348+
function hasImage(chatRequest: ChatRequest | undefined): boolean {
349+
if (!chatRequest || !chatRequest.references) {
350+
return false;
351+
}
352+
return chatRequest.references.some(ref => {
353+
const value = ref.value;
354+
return typeof value === 'object' &&
355+
value !== null &&
356+
'mimeType' in value &&
357+
typeof value.mimeType === 'string'
358+
&& value.mimeType.startsWith('image/');
359+
});
315360
}

0 commit comments

Comments
 (0)