-
Notifications
You must be signed in to change notification settings - Fork 39k
Abadawi/send has image to router #308321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Abadawi/send has image to router #308321
Changes from all commits
155c2ad
a55ff3a
9b084c3
75af0f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -32,6 +32,12 @@ export interface RoutingContextSignals { | |||||||||||
| prompt_char_count?: number; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| export class RouterDecisionError extends Error { | ||||||||||||
| constructor(message: string, public readonly errorCode?: string) { | ||||||||||||
| super(message); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Fetches routing decisions from a classification API to determine which model should handle a query. | ||||||||||||
| * | ||||||||||||
|
|
@@ -48,12 +54,15 @@ export class RouterDecisionFetcher { | |||||||||||
| ) { | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| async getRouterDecision(query: string, autoModeToken: string, availableModels: string[], stickyThreshold?: number, contextSignals?: RoutingContextSignals, conversationId?: string, vscodeRequestId?: string): Promise<RouterDecisionResponse> { | ||||||||||||
| async getRouterDecision(query: string, autoModeToken: string, availableModels: string[], stickyThreshold?: number, contextSignals?: RoutingContextSignals, conversationId?: string, vscodeRequestId?: string, hasImage?: boolean): Promise<RouterDecisionResponse> { | ||||||||||||
| const startTime = Date.now(); | ||||||||||||
| const requestBody: Record<string, unknown> = { prompt: query, available_models: availableModels, ...contextSignals }; | ||||||||||||
| if (stickyThreshold !== undefined) { | ||||||||||||
| requestBody.sticky_threshold = stickyThreshold; | ||||||||||||
| } | ||||||||||||
| if (hasImage) { | ||||||||||||
| requestBody.has_image = true; | ||||||||||||
| } | ||||||||||||
| const copilotToken = (await this._authService.getCopilotToken()).token; | ||||||||||||
| const abortController = new AbortController(); | ||||||||||||
| const timeout = setTimeout(() => abortController.abort(), 1000); | ||||||||||||
|
|
@@ -73,7 +82,12 @@ export class RouterDecisionFetcher { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if (!response.ok) { | ||||||||||||
| throw new Error(`Router decision request failed with status ${response.status}: ${response.statusText}`); | ||||||||||||
| const errorText = await response.text().catch(() => ''); | ||||||||||||
| let errorCode: string | undefined; | ||||||||||||
| try { | ||||||||||||
| errorCode = JSON.parse(errorText).error; | ||||||||||||
|
||||||||||||
| 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; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -688,40 +688,19 @@ describe('AutomodeService', () => { | |||||||||||||||||||||
| expect(routerCallCount2).toBe(1); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| it('should skip router on new turn after a transient fallback reason without invalidation', async () => { | ||||||||||||||||||||||
| it('should skip router on subsequent turns after image request routed on first turn', async () => { | ||||||||||||||||||||||
| enableRouter(); | ||||||||||||||||||||||
| const gpt4oEndpoint = createEndpoint('gpt-4o', 'OpenAI', { supportsVision: true }); | ||||||||||||||||||||||
| const claudeEndpoint = createEndpoint('claude-sonnet', 'Anthropic'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| (mockCAPIClientService.makeRequest as ReturnType<typeof vi.fn>).mockImplementation((_body: any, opts: any) => { | ||||||||||||||||||||||
| if (opts?.type === RequestType.ModelRouter) { | ||||||||||||||||||||||
| return Promise.resolve({ | ||||||||||||||||||||||
| ok: true, | ||||||||||||||||||||||
| status: 200, | ||||||||||||||||||||||
| headers: createMockHeaders(), | ||||||||||||||||||||||
| text: vi.fn().mockResolvedValue(JSON.stringify({ | ||||||||||||||||||||||
| predicted_label: 'needs_reasoning', | ||||||||||||||||||||||
| confidence: 0.9, | ||||||||||||||||||||||
| latency_ms: 30, | ||||||||||||||||||||||
| chosen_model: 'claude-sonnet', | ||||||||||||||||||||||
| candidate_models: ['claude-sonnet'], | ||||||||||||||||||||||
| scores: { needs_reasoning: 0.9, no_reasoning: 0.1 }, | ||||||||||||||||||||||
| sticky_override: false | ||||||||||||||||||||||
| })) | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return Promise.resolve( | ||||||||||||||||||||||
| makeMockTokenResponse({ | ||||||||||||||||||||||
| available_models: ['claude-sonnet', 'gpt-4o'], | ||||||||||||||||||||||
| expires_at: Math.floor(Date.now() / 1000) + 3600, | ||||||||||||||||||||||
| session_token: 'test-token', | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| mockRouterResponse( | ||||||||||||||||||||||
| ['gpt-4o', 'claude-sonnet'], | ||||||||||||||||||||||
| { chosen_model: 'gpt-4o', candidate_models: ['gpt-4o'] } | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| automodeService = createService(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Turn 1: image request — router is skipped (transient fallback) | ||||||||||||||||||||||
| // Turn 1: image request — router IS called now | ||||||||||||||||||||||
| const imageRequest: Partial<ChatRequest> = { | ||||||||||||||||||||||
| location: ChatLocation.Panel, | ||||||||||||||||||||||
| prompt: 'describe this image', | ||||||||||||||||||||||
|
|
@@ -731,22 +710,14 @@ describe('AutomodeService', () => { | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| await automodeService.resolveAutoModeEndpoint(imageRequest as ChatRequest, [gpt4oEndpoint, claudeEndpoint]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
||||||||||||||||||||||
| expect(mockCAPIClientService.makeRequest).toHaveBeenCalledWith( | |
| expect.anything(), | |
| expect.objectContaining({ type: RequestType.ModelRouter }) | |
| ); |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the router call,
conversationIdis computed earlier assessionResource?.toString() ?? sessionId, but herechatRequest?.sessionIdis passed togetRouterDecision()for theconversationIdargument. This can cause router telemetry (and any downstream logging keyed by that argument) to miss the sessionResource-based id. Pass the already-computedconversationIdvariable instead to keep IDs consistent withcontextSignals.session_idand cache keys.