Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
854557a
chat: fix signed-in model prompt on startup
dmitrivMS Aug 14, 2026
07c969b
chat: fix: stop capitalizing localized customization group headers
ulugbekna Aug 17, 2026
38be91b
mcp: fix: make Show Output clickable on a failing MCP server
ulugbekna Aug 17, 2026
0fff929
mcp: fix: show one row per agent MCP server, not one per customization
ulugbekna Aug 17, 2026
a17634a
Merge pull request #330928 from microsoft/dev/dmitriv/fix-signed-in-c…
vritant24 Aug 17, 2026
518720d
fix languages.getDiagnostics() problem-matchers diagnostics duplicati…
n-gist Aug 17, 2026
4ccdb71
sessions: Synchronize live last turn changes (#331244)
sandy081 Aug 17, 2026
90dde50
chat: describe BYOK Responses failures that omit error details
vritant24 Aug 17, 2026
bd96693
chat: localize BYOK failure descriptions and drop unrelated coverage
vritant24 Aug 17, 2026
93344fb
Do not present a failed tool call as a successful one
RyanEwen Aug 13, 2026
dd4ee31
Condense comments per review
RyanEwen Aug 13, 2026
c517c08
Name the failure instead of dropping the label
RyanEwen Aug 17, 2026
be2aecc
agentHost: make startup session discovery registry-first (#331176)
benibenj Aug 17, 2026
8c62922
Merge pull request #331281 from microsoft/vritant24/fix-330408-blank-…
vritant24 Aug 17, 2026
1e3ed44
agentHost: Scope server tools to exact chats (#331307)
sandy081 Aug 17, 2026
d02bfbb
Show Anthropic refusals correctly (#331315)
bhavyaus Aug 17, 2026
89634ad
Allow dictation without Copilot entitlement (#331321)
meganrogge Aug 17, 2026
624addc
Merge pull request #330707 from RyanEwen/fix/browser-tool-failure-rep…
connor4312 Aug 17, 2026
dee145b
Scope dictation indicator to owning chat (#331221)
meganrogge Aug 17, 2026
a85c9b1
fix chat jump on pet window blur (#331278)
justschen Aug 17, 2026
52a0628
Automations: Render quick chats like workspace chats (#331306)
benvillalobos Aug 17, 2026
9db4d18
agentHost: Speed up rename chat tool (#331324)
sandy081 Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion extensions/copilot/src/extension/byok/node/openAIEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ function hydrateBYOKErrorMessages(response: ChatResponse): ChatResponse {
type: response.type,
requestId: response.requestId,
serverRequestId: response.serverRequestId,
reason: JSON.stringify(response.streamError),
// A stream error carrying no message has no diagnostic value, so keep the
// original reason rather than replacing it with a hollow serialized struct.
reason: response.streamError.message ? JSON.stringify(response.streamError) : response.reason,
};
} else if (response.type === ChatFetchResponseType.RateLimited) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,65 @@ describe('OpenAIEndpoint - Reasoning Properties', () => {
expect(parentRequestSpy.mock.calls[0][0].ignoreStatefulMarker).toBe(false);
});

// Regression for https://github.com/microsoft/vscode/issues/330408
//
// BYOK promotes the serialized stream error to the whole user-facing reason. A
// message-less error must not displace the reason the fetcher already computed,
// otherwise the user is left with an empty struct and nothing to act on.
it('issue #330408: keeps the original reason when a stream error carries no message', async () => {
const endpoint = instaService.createInstance(OpenAIEndpoint,
modelMetadata,
'test-api-key',
'https://api.openai.com/v1/responses');
const parentResponse: ChatResponse = {
type: ChatFetchResponseType.Failed,
requestId: 'request-id',
serverRequestId: 'server-request-id',
reason: 'Server error. Stream terminated',
streamError: { code: 0, message: '', metadata: {} },
};
vi.spyOn(ChatEndpoint.prototype, 'makeChatRequest2').mockResolvedValue(parentResponse);

const response = await endpoint.makeChatRequest2(
createMakeRequestOptions([
{
role: Raw.ChatRole.User,
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'hello' }]
}
]),
CancellationToken.None,
);

expect(response.type === ChatFetchResponseType.Failed && response.reason).toBe('Server error. Stream terminated');
});

it('surfaces the serialized stream error when it carries a message', async () => {
const endpoint = instaService.createInstance(OpenAIEndpoint,
modelMetadata,
'test-api-key',
'https://api.openai.com/v1/responses');
const parentResponse: ChatResponse = {
type: ChatFetchResponseType.Failed,
requestId: 'request-id',
serverRequestId: 'server-request-id',
reason: 'Server error. Stream terminated',
streamError: { code: 0, message: 'something broke', metadata: { code: 'server_error' } },
};
vi.spyOn(ChatEndpoint.prototype, 'makeChatRequest2').mockResolvedValue(parentResponse);

const response = await endpoint.makeChatRequest2(
createMakeRequestOptions([
{
role: Raw.ChatRole.User,
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'hello' }]
}
]),
CancellationToken.None,
);

expect(response.type === ChatFetchResponseType.Failed && response.reason).toBe('{"code":0,"message":"something broke","metadata":{"code":"server_error"}}');
});

it('disables marker reuse and store for ZDR Responses requests', () => {
const endpoint = instaService.createInstance(OpenAIEndpoint,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
case ChatFetchResponseType.QuotaExceeded:
case ChatFetchResponseType.Canceled:
case ChatFetchResponseType.OffTopic:
case ChatFetchResponseType.Refusal:
return false;
default:
return response.type !== ChatFetchResponseType.Success;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,11 @@ describe('ToolCallingLoop autopilot', () => {
expect(loop.testShouldAutoRetry(mockResponse(ChatFetchResponseType.OffTopic))).toBe(false);
});

it('should not retry on Refusal', () => {
const loop = createLoop('autopilot');
expect(loop.testShouldAutoRetry(mockResponse(ChatFetchResponseType.Refusal))).toBe(false);
});

it('should not retry on Success', () => {
const loop = createLoop('autoApprove');
expect(loop.testShouldAutoRetry(mockResponse(ChatFetchResponseType.Success))).toBe(false);
Expand Down
7 changes: 7 additions & 0 deletions extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,13 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
requestId: requestId,
serverRequestId: result.requestId.headerRequestId,
};
case FinishedCompletionReason.Refusal:
return {
type: ChatFetchResponseType.Refusal,
reason: 'Model declined to respond.',
requestId: requestId,
serverRequestId: result.requestId.headerRequestId,
};
case FinishedCompletionReason.Length:
return {
type: ChatFetchResponseType.Length,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,12 @@ export class DefaultIntentRequestHandler {
this.turn.setResponse(TurnStatus.Filtered, undefined, baseModelTelemetry.properties.messageId, chatResult);
return chatResult;
}
case ChatFetchResponseType.Refusal: {
const errorDetails = await this.getErrorDetails(fetchResult);
const chatResult = { errorDetails, metadata: metadataFragment };
this.turn.setResponse(TurnStatus.Filtered, undefined, baseModelTelemetry.properties.messageId, chatResult);
return chatResult;
}
case ChatFetchResponseType.PromptFiltered: {
const errorDetails = await this.getErrorDetails(fetchResult);
const chatResult = { errorDetails, metadata: { ...metadataFragment, filterReason: FilterReason.Prompt } };
Expand Down
1 change: 1 addition & 0 deletions extensions/copilot/src/extension/xtab/node/xtabProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,7 @@ export function mapChatFetcherErrorToNoNextEditReason(fetchError: ChatFetchError
case ChatFetchResponseType.OffTopic:
case ChatFetchResponseType.Filtered:
case ChatFetchResponseType.PromptFiltered:
case ChatFetchResponseType.Refusal:
case ChatFetchResponseType.Length:
case ChatFetchResponseType.RateLimited:
case ChatFetchResponseType.QuotaExceeded:
Expand Down
11 changes: 11 additions & 0 deletions extensions/copilot/src/platform/chat/common/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export enum ChatFetchResponseType {
Filtered = 'filtered',
FilteredRetry = 'filteredRetry',
PromptFiltered = 'promptFiltered',
Refusal = 'refusal',
Length = 'length',
RateLimited = 'rateLimited',
QuotaExceeded = 'quotaExceeded',
Expand Down Expand Up @@ -142,6 +143,10 @@ export type ChatFetchError =
* We requested conversation, but the prompt was filtered by RAI.
*/
| { type: ChatFetchResponseType.PromptFiltered; reason: string; reasonDetail?: string; category: FilterReason; requestId: string; serverRequestId: string | undefined }
/**
* We requested conversation, but the model declined to answer.
*/
| { type: ChatFetchResponseType.Refusal; reason: string; reasonDetail?: string; requestId: string; serverRequestId: string | undefined }
/**
* We requested conversation, but the response was too long.
*/
Expand Down Expand Up @@ -452,6 +457,12 @@ function getErrorDetailsFromChatFetchErrorInner(fetchResult: ChatFetchError, cop
level: ChatErrorLevel.Info,
};
break;
case ChatFetchResponseType.Refusal:
details = {
message: l10n.t(`Sorry, the model declined to complete this request. Please rephrase your prompt.`),
level: ChatErrorLevel.Info,
};
break;
case ChatFetchResponseType.AgentUnauthorized:
details = { message: l10n.t(`Sorry, something went wrong.`) };
break;
Expand Down
50 changes: 22 additions & 28 deletions extensions/copilot/src/platform/endpoint/node/messagesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ContentBlockParam, DocumentBlockParam, ImageBlockParam, MessageParam, RedactedThinkingBlockParam, TextBlockParam, ThinkingBlockParam, ToolReferenceBlockParam, ToolResultBlockParam } from '@anthropic-ai/sdk/resources';
import { ContentBlockParam, DocumentBlockParam, ImageBlockParam, MessageParam, RedactedThinkingBlockParam, RefusalStopDetails, TextBlockParam, ThinkingBlockParam, ToolReferenceBlockParam, ToolResultBlockParam } from '@anthropic-ai/sdk/resources';
import { Raw } from '@vscode/prompt-tsx';
import { Response } from '../../../platform/networking/common/fetcherService';
import { AsyncIterableObject } from '../../../util/vs/base/common/async';
Expand Down Expand Up @@ -137,11 +137,7 @@ interface AnthropicStreamEvent {
signature?: string;
stop_reason?: string;
stop_sequence?: string;
stop_details?: {
category?: string;
explanation?: string;
type?: string;
};
stop_details?: RefusalStopDetails | null;
};
copilot_annotations?: {
IPCodeCitations?: AnthropicIPCodeCitation[];
Expand Down Expand Up @@ -790,7 +786,7 @@ interface AnthropicCompletionState {
function mapStopReason(stopReason: string | null | undefined): FinishedCompletionReason {
switch (stopReason) {
case 'refusal':
return FinishedCompletionReason.ClientDone;
return FinishedCompletionReason.Refusal;
case 'max_tokens':
case 'model_context_window_exceeded':
return FinishedCompletionReason.Length;
Expand Down Expand Up @@ -889,6 +885,7 @@ type AnthropicNonStreamingResponse =
)[];
model: string;
stop_reason: string | null;
stop_details?: RefusalStopDetails | null;
usage: {
input_tokens: number;
output_tokens: number;
Expand Down Expand Up @@ -987,23 +984,9 @@ export async function processNonStreamingResponseFromMessagesEndpoint(
}
}

// Report text and tool calls to finishedCb so callers that rely on
// the callback (e.g. for OTEL tracing, progress, langModelServer SSE
// forwarding) see the complete response — matching the streaming path.
const delta: IResponseDelta = {
text: textContent,
...(toolCalls.length > 0 ? {
copilotToolCalls: toolCalls.map(tc => ({
id: tc.id,
name: tc.name,
arguments: tc.arguments,
})),
} : {}),
};
await finishCallback(textContent, 0, delta);

if (parsed.stop_reason === 'refusal') {
logService.warn(`[messagesAPI] non-streaming: Refusal received for model ${parsed.model}`);
const category = parsed.stop_details?.category ?? 'unknown';
logService.warn(`[messagesAPI] non-streaming: Refusal received: category='${category}' for model ${parsed.model}`);

/* __GDPR__
"messagesApi.refusal" : {
Expand All @@ -1018,11 +1001,24 @@ export async function processNonStreamingResponseFromMessagesEndpoint(
{
requestId,
model: parsed.model,
category: 'unknown',
category,
}
);
}

// There are no incremental deltas here, so callback-only consumers need the whole response.
const delta: IResponseDelta = {
text: textContent,
...(toolCalls.length > 0 ? {
copilotToolCalls: toolCalls.map(tc => ({
id: tc.id,
name: tc.name,
arguments: tc.arguments,
})),
} : {}),
};
await finishCallback(textContent, 0, delta);

const usage = parsed.usage;
const completion = buildAnthropicCompletion({
model: parsed.model,
Expand Down Expand Up @@ -1091,7 +1087,7 @@ export class AnthropicMessagesProcessor {
private copilotUsage?: { total_nano_aiu: number };
private contextManagementResponse?: ContextManagementResponse;
private stopReason: string | undefined;
private stopDetails?: { category?: string; explanation?: string; type?: string };
private stopDetails?: RefusalStopDetails;

constructor(
private readonly telemetryData: TelemetryData,
Expand Down Expand Up @@ -1292,7 +1288,7 @@ export class AnthropicMessagesProcessor {
if (chunk.context_management) {
this.contextManagementResponse = chunk.context_management;
// Report context management via delta so it gets logged to request logger
return onProgress({
onProgress({
text: '',
contextManagement: chunk.context_management
});
Expand Down Expand Up @@ -1404,5 +1400,3 @@ export class AnthropicMessagesProcessor {
}
}
}


63 changes: 56 additions & 7 deletions extensions/copilot/src/platform/endpoint/node/responsesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as l10n from '@vscode/l10n';
import { Raw } from '@vscode/prompt-tsx';
import type { OpenAI } from 'openai';
import { Response } from '../../../platform/networking/common/fetcherService';
Expand Down Expand Up @@ -1102,20 +1103,68 @@ function extractFilterReasonFromContentFilters(filters: CapiContentFilterEntry[]
return undefined;
}

/**
* Identifying details for the terminal Responses event that carried an error.
* Used to describe failures whose error object omits the fields the API
* contract requires.
*/
interface IResponsesErrorContext {
/** The terminal SSE event that carried the error, e.g. `response.failed`. */
readonly eventType: string;
/** `id` from the response envelope, so a user report stays correlatable upstream. */
readonly responseId?: string;
/** `status` from the response envelope, when present. */
readonly responseStatus?: string;
}

function toResponsesErrorContext(eventType: string, response?: Pick<OpenAI.Responses.Response, 'id' | 'status'>): IResponsesErrorContext {
return {
eventType,
responseId: response?.id || undefined,
responseStatus: response?.status || undefined,
};
}

/**
* Describe a terminal error that carries no usable message. Names only the
* event, status, response id, and provider code so the failure stays
* diagnosable and correlatable without exposing prompt content.
*/
function describeUninformativeResponsesError(code: string | undefined, context: IResponsesErrorContext): string {
// Structured diagnostic identifiers, kept verbatim so they stay greppable and
// pasteable into a provider support request.
const details = [
`event: ${context.eventType}`,
...(context.responseStatus ? [`status: ${context.responseStatus}`] : []),
...(context.responseId ? [`response: ${context.responseId}`] : []),
].join(', ');
return code
? l10n.t("The model provider reported a failed response with code '{0}' and no error message ({1}).", code, details)
: l10n.t("The model provider reported a failed response without any error details ({0}).", details);
}

/**
* Map a Responses-API `response.error` (string-coded per the OpenAI SDK) onto
* our {@link APIErrorResponse} shape (numeric `code`). We can't preserve the
* string code in `code`, so we stash it in `metadata.code` for BYOK diagnostics
* (which `JSON.stringify` the whole struct).
* (which `JSON.stringify` the whole struct). Providers do terminate streams with
* an error object that omits `code`/`message` entirely, so those are described
* rather than serialized as an empty struct that tells the user nothing.
*/
function mapResponsesApiError(err: OpenAI.Responses.ResponseError | null | undefined): APIErrorResponse | undefined {
function mapResponsesApiError(err: OpenAI.Responses.ResponseError | null | undefined, context: IResponsesErrorContext): APIErrorResponse | undefined {
if (!err) {
return undefined;
}
const code = typeof err.code === 'string' && err.code ? err.code : undefined;
const message = typeof err.message === 'string' && err.message ? err.message : undefined;
return {
code: 0,
message: err.message ?? '',
metadata: { code: err.code },
message: message ?? describeUninformativeResponsesError(code, context),
// Omit absent keys so `JSON.stringify` cannot collapse metadata to `{}`.
metadata: {
...(code ? { code } : {}),
...(context.responseId ? { responseId: context.responseId } : {}),
},
};
}

Expand Down Expand Up @@ -1206,7 +1255,7 @@ export class OpenAIResponsesProcessor {
return this.buildTerminalCompletion(
{ output: [] } as unknown as CapiResponseTerminalEvent['response'],
FinishedCompletionReason.ServerError,
{ error: mapResponsesApiError({ code: chunk.code, message: chunk.message } as OpenAI.Responses.ResponseError) }
{ error: mapResponsesApiError({ code: chunk.code, message: chunk.message } as OpenAI.Responses.ResponseError, toResponsesErrorContext('error')) }
);
case 'response.output_text.delta': {
const capiChunk: CapiResponsesTextDeltaEvent = chunk;
Expand Down Expand Up @@ -1428,13 +1477,13 @@ export class OpenAIResponsesProcessor {
}
return this.buildTerminalCompletion(incomplete, finishReason, {
filterReason,
error: mapResponsesApiError(incomplete.error),
error: mapResponsesApiError(incomplete.error, toResponsesErrorContext('response.incomplete', incomplete)),
});
}
case 'response.failed': {
const failed = chunk.response as CapiResponseTerminalEvent['response'];
return this.buildTerminalCompletion(failed, FinishedCompletionReason.ServerError, {
error: mapResponsesApiError(failed.error),
error: mapResponsesApiError(failed.error, toResponsesErrorContext('response.failed', failed)),
});
}
}
Expand Down
Loading
Loading