Skip to content

Abadawi/send has image to router#308321

Open
abadawi591 wants to merge 4 commits intomicrosoft:mainfrom
abadawi591:abadawi/send-has-image-to-router
Open

Abadawi/send has image to router#308321
abadawi591 wants to merge 4 commits intomicrosoft:mainfrom
abadawi591:abadawi/send-has-image-to-router

Conversation

@abadawi591
Copy link
Copy Markdown

Delegate image routing to server-side auto-intent-service

The server-side auto-intent-service now handles has_image via a vision-aware pre-filter instead of returning HTTP 400. Remove the client-side hasImage early return in _tryRouterSelection() and send has_image: true in the router request body when images are present.

Changes

  • automodeService.ts: remove hasImage bail-out, pass hasImage to router
  • routerDecisionFetcher.ts: accept hasImage param, include has_image in request body, parse error code from 400 responses
  • automodeService.ts: handle no_vision_models error as a distinct fallback reason; _applyVisionFallback remains as safety net
  • automodeService.spec.ts: replace skip-router test with send-has_image test, add no_vision_models error handling test, update transient fallback test

The server-side auto-intent-service now handles has_image via a
vision-aware pre-filter instead of returning HTTP 400. Remove the
client-side hasImage early return in _tryRouterSelection() and send
has_image: true in the router request body when images are present.

Changes:
- automodeService.ts: remove hasImage bail-out, pass hasImage to router
- routerDecisionFetcher.ts: accept hasImage param, include has_image in
  request body, parse error code from 400 responses
- automodeService.ts: handle no_vision_models error as a distinct
  fallback reason; _applyVisionFallback remains as safety net
- automodeService.spec.ts: replace skip-router test with send-has_image
  test, add no_vision_models error handling test, update transient
  fallback test
Replace (err as any).errorCode pattern with a typed RouterDecisionError
class that carries the errorCode as a proper property.
Add emptyPrompt and emptyCandidateList to the comment so telemetry
documentation matches runtime behavior.
Verify the router was actually called and the error was logged with
the noVisionModels fallback reason, not just that the model is gpt-4o.
Copilot AI review requested due to automatic review settings April 7, 2026 20:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Copilot extension’s auto-mode routing flow so image-bearing chat turns are routed server-side (by sending has_image) instead of being short-circuited client-side, and adds a dedicated fallback classification when the router reports no available vision models.

Changes:

  • Remove the client-side image-based router bail-out and always send has_image to the router when images are present.
  • Add RouterDecisionError with parsed error codes from HTTP 400 responses to enable more specific fallback reasons (e.g. noVisionModels).
  • Update automode routing tests to validate has_image behavior and no_vision_models fallback handling.
Show a summary per file
File Description
extensions/copilot/src/platform/endpoint/node/automodeService.ts Sends hasImage into router requests and maps no_vision_models to a distinct fallback reason.
extensions/copilot/src/platform/endpoint/node/routerDecisionFetcher.ts Adds has_image to the router request body and introduces RouterDecisionError with parsed error codes.
extensions/copilot/src/platform/endpoint/node/test/automodeService.spec.ts Updates/extends tests to cover routing for image turns and the no_vision_models fallback case.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 4

turn_number: (entry?.turnCount ?? 0) + 1,
};
const result = await this._routerDecisionFetcher.getRouterDecision(prompt, token.session_token, token.available_models, undefined, contextSignals, chatRequest?.sessionId, chatRequest?.id);
const result = await this._routerDecisionFetcher.getRouterDecision(prompt, token.session_token, token.available_models, undefined, contextSignals, chatRequest?.sessionId, chatRequest?.id, hasImage(chatRequest));
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

In the router call, conversationId is computed earlier as sessionResource?.toString() ?? sessionId, but here chatRequest?.sessionId is passed to getRouterDecision() for the conversationId argument. This can cause router telemetry (and any downstream logging keyed by that argument) to miss the sessionResource-based id. Pass the already-computed conversationId variable instead to keep IDs consistent with contextSignals.session_id and cache keys.

Suggested change
const result = await this._routerDecisionFetcher.getRouterDecision(prompt, token.session_token, token.available_models, undefined, contextSignals, chatRequest?.sessionId, chatRequest?.id, hasImage(chatRequest));
const result = await this._routerDecisionFetcher.getRouterDecision(prompt, token.session_token, token.available_models, undefined, contextSignals, conversationId, chatRequest?.id, hasImage(chatRequest));

Copilot uses AI. Check for mistakes.
const errorText = await response.text().catch(() => '');
let errorCode: string | undefined;
try {
errorCode = JSON.parse(errorText).error;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

errorCode = JSON.parse(errorText).error; assigns an untyped value into a string | undefined. If the server ever returns a non-string error, the comparison in AutomodeService will silently fail and misclassify the fallback reason. Consider validating typeof parsed.error === 'string' before assigning.

Suggested change
errorCode = JSON.parse(errorText).error;
const parsed = JSON.parse(errorText);
if (typeof parsed === 'object' && parsed !== null && 'error' in parsed && typeof parsed.error === 'string') {
errorCode = parsed.error;
}

Copilot uses AI. Check for mistakes.
@@ -731,22 +710,14 @@ describe('AutomodeService', () => {

await automodeService.resolveAutoModeEndpoint(imageRequest as ChatRequest, [gpt4oEndpoint, claudeEndpoint]);

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This test resets the router mock call history right after turn 1, but never asserts that the router was actually called for the image request (the behavior the test name/comment claims). Add an assertion on turn 1 (before mockClear()) to ensure the test would fail if the router were skipped again.

Suggested change
expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ type: RequestType.ModelRouter })
);

Copilot uses AI. Check for mistakes.
Comment on lines +757 to +762
expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('"has_image":true')
}),
expect.objectContaining({ type: RequestType.ModelRouter })
);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The assertion checks for a raw substring '"has_image":true' in the request body, which is brittle (JSON spacing / serialization order changes can break it). Prefer parsing the JSON body (or matching via a predicate) and asserting has_image === true.

Suggested change
expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.stringContaining('"has_image":true')
}),
expect.objectContaining({ type: RequestType.ModelRouter })
);
const routerCall = (mockCAPIClientService.makeRequest as ReturnType<typeof vi.fn>).mock.calls.find(([, opts]) => opts?.type === RequestType.ModelRouter);
expect(routerCall).toBeDefined();
const [routerRequestBody] = routerCall!;
expect(JSON.parse(routerRequestBody.body).has_image).toBe(true);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants