Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ export function modelCanUseImageURL(model: LanguageModelChat | IChatEndpoint): b
* The model supports native PDF document processing via document content parts.
*/
export function modelSupportsPDFDocuments(model: LanguageModelChat | IChatEndpoint): boolean {
return isAnthropicFamily(model);
return isAnthropicFamily(model) || isGpt5PlusFamily(model) || isHiddenModelM(model);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions extensions/copilot/src/platform/endpoint/node/responsesApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,12 +446,19 @@ function rawMessagesToResponseAPI(modelId: string, messages: readonly Raw.ChatMe
detail: c.imageUrl.detail || 'auto',
image_url: c.imageUrl.url,
}));
const asFiles = message.content
.filter((c): c is RawDocumentContentPart => c.type === Raw.ChatCompletionContentPartKind.Document)
.map(rawDocumentToResponsesInputFile)
.filter(isDefined);

// todod@connor4312: hack while responses API only supports text output from tools
input.push({ type: 'function_call_output', call_id: message.toolCallId, output: asText });
if (asImages.length) {
input.push({ role: 'user', content: [{ type: 'input_text', text: 'Image associated with the above tool call:' }, ...asImages] });
}
if (asFiles.length) {
input.push({ role: 'user', content: [{ type: 'input_text', text: 'PDF associated with the above tool call:' }, ...asFiles] });
}
}
}
break;
Expand Down Expand Up @@ -520,12 +527,28 @@ function getLatestCompactionMessageIndex(messages: readonly Raw.ChatMessage[]):
return undefined;
}

type RawDocumentContentPart = Extract<Raw.ChatCompletionContentPart, { type: Raw.ChatCompletionContentPartKind.Document }>;

function rawDocumentToResponsesInputFile(part: RawDocumentContentPart): OpenAI.Responses.ResponseInputFile | undefined {
if (part.documentData.mediaType !== 'application/pdf') {
return undefined;
}

return {
type: 'input_file',
filename: 'document.pdf',
file_data: `data:${part.documentData.mediaType};base64,${part.documentData.data}`,
};
}

function rawContentToResponsesContent(part: Raw.ChatCompletionContentPart): OpenAI.Responses.ResponseInputContent | undefined {
switch (part.type) {
case Raw.ChatCompletionContentPartKind.Text:
return { type: 'input_text', text: part.text };
case Raw.ChatCompletionContentPartKind.Image:
return { type: 'input_image', detail: part.imageUrl.detail || 'auto', image_url: part.imageUrl.url };
case Raw.ChatCompletionContentPartKind.Document:
return rawDocumentToResponsesInputFile(part);
case Raw.ChatCompletionContentPartKind.Opaque: {
const maybeCast = part.value as OpenAI.Responses.ResponseInputContent;
if (maybeCast.type === 'input_text' || maybeCast.type === 'input_image' || maybeCast.type === 'input_file') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ChatLocation } from '../../../chat/common/commonTypes';
import { ILogService } from '../../../log/common/logService';
import { isOpenAIContextManagementResponse } from '../../../networking/common/fetch';
import { IChatEndpoint, ICreateEndpointBodyOptions } from '../../../networking/common/networking';
import { ChatCompletion, openAIContextManagementCompactionType, OpenAIContextManagementResponse, FilterReason, FinishedCompletionReason } from '../../../networking/common/openai';
import { ChatCompletion, FilterReason, FinishedCompletionReason, openAIContextManagementCompactionType, OpenAIContextManagementResponse } from '../../../networking/common/openai';
import { IToolDeferralService } from '../../../networking/common/toolDeferralService';
import { IChatWebSocketManager, NullChatWebSocketManager } from '../../../networking/node/chatWebSocketManager';
import { TelemetryData } from '../../../telemetry/common/telemetryData';
Expand Down Expand Up @@ -365,6 +365,68 @@ describe('createResponsesRequestBody', () => {
})).toBe(1234);
});

it('converts PDF document content parts to Responses input_file', () => {
const services = createPlatformServices();
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const endpoint = { ...testEndpoint, family: 'gpt-5.4', supportsVision: true };
const base64Data = 'JVBERi0xLjQKMSAwIG9iago8PC9UeXBlIC9DYXRhbG9n';
const messages: Raw.ChatMessage[] = [{
role: Raw.ChatRole.User,
content: [{
type: Raw.ChatCompletionContentPartKind.Document,
documentData: { data: base64Data, mediaType: 'application/pdf' },
}],
}];

const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(servicesAccessor, createRequestOptions(messages, false), endpoint.model, endpoint));

expect(body.input?.[0]).toMatchObject({
role: 'user',
content: [{
type: 'input_file',
filename: 'document.pdf',
file_data: `data:application/pdf;base64,${base64Data}`,
}],
});

accessor.dispose();
services.dispose();
});

it('preserves PDF document tool results as Responses input_file follow-up content', () => {
const services = createPlatformServices();
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const base64Data = 'JVBERi0xLjQK';
const messages: Raw.ChatMessage[] = [
{
role: Raw.ChatRole.Assistant,
content: [],
toolCalls: [{ id: 'call_pdf', type: 'function', function: { name: 'read_file', arguments: '{"path":"doc.pdf"}' } }],
},
{
role: Raw.ChatRole.Tool,
toolCallId: 'call_pdf',
content: [{ type: Raw.ChatCompletionContentPartKind.Document, documentData: { data: base64Data, mediaType: 'application/pdf' } }],
},
];

const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(servicesAccessor, createRequestOptions(messages, false), testEndpoint.model, testEndpoint));

expect(body.input?.[1]).toMatchObject({ type: 'function_call_output', call_id: 'call_pdf', output: '' });
expect(body.input?.[2]).toMatchObject({
role: 'user',
content: [
{ type: 'input_text', text: 'PDF associated with the above tool call:' },
{ type: 'input_file', filename: 'document.pdf', file_data: `data:application/pdf;base64,${base64Data}` },
],
});

accessor.dispose();
services.dispose();
});

it('still slices websocket requests by stateful marker index when compaction is disabled', () => {
const services = createPlatformServices();
const wsManager = new NullChatWebSocketManager();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@ describe('modelSupportsPDFDocuments', () => {
expect(modelSupportsPDFDocuments(fakeModel('Anthropic-custom'))).toBe(true);
});

test('returns false for non-Anthropic families', () => {
test('returns true for gpt-5 plus families', () => {
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.4'))).toBe(true);
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.4-mini'))).toBe(true);
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.5'))).toBe(true);
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.5-mini'))).toBe(true);
expect(modelSupportsPDFDocuments(fakeModel('gpt-4'))).toBe(false);
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.1'))).toBe(false);
expect(modelSupportsPDFDocuments(fakeModel('gpt-5.1'))).toBe(true);
});

test('returns false for other families', () => {
expect(modelSupportsPDFDocuments(fakeModel('gemini-2.0-flash'))).toBe(false);
expect(modelSupportsPDFDocuments(fakeModel('o4-mini'))).toBe(false);
});
Expand Down
24 changes: 0 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/vs/platform/sandbox/browser/sandboxHelperService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class NullSandboxHelperService implements ISandboxHelperService {
// or block sandbox flows on an unavailable host-side capability.
return {
bubblewrapInstalled: true,
bubblewrapUsable: true,
socatInstalled: true,
};
}
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/sandbox/common/sandboxHelperService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export const ISandboxHelperService = createDecorator<ISandboxHelperService>('san

export interface ISandboxDependencyStatus {
readonly bubblewrapInstalled: boolean;
readonly bubblewrapUsable: boolean;
readonly socatInstalled: boolean;
readonly bubblewrapError?: string;
readonly supportsUbuntuAppArmorRemediation?: boolean;
}

export interface IWindowsMxcFilesystemPolicy {
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/sandbox/common/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const enum AgentSandboxSettingId {
AgentSandboxEnabled = 'chat.agent.sandbox.enabled',
AgentSandboxWindowsEnabled = 'chat.agent.sandbox.enabledWindows',
AgentSandboxAllowUnsandboxedCommands = 'chat.agent.sandbox.allowUnsandboxedCommands',
AgentSandboxRetryWithAllowNetworkRequests = 'chat.agent.sandbox.retryWithAllowNetworkRequests',
AgentSandboxAutoApproveUnsandboxedCommands = 'chat.agent.sandbox.autoApproveUnsandboxedCommands',
AgentSandboxAllowAutoApprove = 'chat.agent.sandbox.allowAutoApprove',
AgentSandboxLinuxFileSystem = 'chat.agent.sandbox.fileSystem.linux',
Expand Down
Loading
Loading