Abadawi/send has image to router#308321
Conversation
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.
There was a problem hiding this comment.
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_imageto the router when images are present. - Add
RouterDecisionErrorwith parsed error codes from HTTP 400 responses to enable more specific fallback reasons (e.g.noVisionModels). - Update automode routing tests to validate
has_imagebehavior andno_vision_modelsfallback 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)); |
There was a problem hiding this comment.
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.
| 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)); |
| const errorText = await response.text().catch(() => ''); | ||
| let errorCode: string | undefined; | ||
| try { | ||
| errorCode = JSON.parse(errorText).error; |
There was a problem hiding this comment.
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.
| 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; | |
| } |
| @@ -731,22 +710,14 @@ describe('AutomodeService', () => { | |||
|
|
|||
| await automodeService.resolveAutoModeEndpoint(imageRequest as ChatRequest, [gpt4oEndpoint, claudeEndpoint]); | |||
|
|
|||
There was a problem hiding this comment.
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.
| expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith( | |
| expect.anything(), | |
| expect.objectContaining({ type: RequestType.ModelRouter }) | |
| ); |
| expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| body: expect.stringContaining('"has_image":true') | ||
| }), | ||
| expect.objectContaining({ type: RequestType.ModelRouter }) | ||
| ); |
There was a problem hiding this comment.
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.
| 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); |
Delegate image routing to server-side auto-intent-service
The server-side auto-intent-service now handles
has_imagevia a vision-aware pre-filter instead of returning HTTP 400. Remove the client-sidehasImageearly return in_tryRouterSelection()and sendhas_image: truein the router request body when images are present.Changes
automodeService.ts: removehasImagebail-out, passhasImageto routerrouterDecisionFetcher.ts: accepthasImageparam, includehas_imagein request body, parse error code from 400 responsesautomodeService.ts: handleno_vision_modelserror as a distinct fallback reason;_applyVisionFallbackremains as safety netautomodeService.spec.ts: replace skip-router test with send-has_imagetest, addno_vision_modelserror handling test, update transient fallback test