From f3de02c6777dfbb7d5e6a2a3ea228996a95d656e Mon Sep 17 00:00:00 2001 From: vritant24 Date: Tue, 6 Oct 2026 09:23:51 -0700 Subject: [PATCH] byok: default statefulResponses to true for custom Responses endpoints The Open Responses spec requires servers to honor previous_response_id or fail with previous_response_not_found, which the existing full-history retry already handles. Chain by default and let stateless servers that silently ignore the ID (e.g. DeepSeek) opt out with statefulResponses: false. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extensions/copilot/package.json | 4 ++-- .../byok/vscode-node/customEndpointProvider.ts | 10 +++++----- .../test/customEndpointProvider.spec.ts | 16 ++++++++-------- .../platform/endpoint/common/endpointProvider.ts | 5 +++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index 770c1ca39650..7a07cd6fd6cd 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -2158,8 +2158,8 @@ }, "statefulResponses": { "type": "boolean", - "default": false, - "markdownDescription": "Whether the Responses API server retains prior responses. When `true`, requests chain via `previous_response_id` and send only new history; leave `false` for stateless servers (for example DeepSeek), which require the full history on every request. Ignored when `zeroDataRetentionEnabled` is `true`." + "default": true, + "markdownDescription": "Whether the Responses API server retains prior responses. When `true`, requests chain via `previous_response_id` and send only new history. Set to `false` for stateless servers that ignore `previous_response_id` (for example DeepSeek), so the full history is sent on every request. Ignored when `zeroDataRetentionEnabled` is `true`." }, "supportsReasoningEffort": { "type": "array", diff --git a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts index 176bc0f2731c..aa6449c01c72 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/customEndpointProvider.ts @@ -208,8 +208,8 @@ export class CustomEndpointBYOKModelProvider extends AbstractOpenAICompatibleLMP * conflicting credentials. * 4. Omits the Responses API `store` property when Zero Data Retention was not * explicitly configured, allowing custom implementations to use their own default. - * 5. Sends full Responses history instead of chaining via `previous_response_id` - * unless the user declares the server stateful with `statefulResponses: true`. + * 5. Chains Responses requests via `previous_response_id` per the Open Responses spec, + * unless the user marks the server stateless with `statefulResponses: false`. */ export class CustomEndpointOAIEndpoint extends OpenAIEndpoint { /** @@ -261,11 +261,11 @@ export class CustomEndpointOAIEndpoint extends OpenAIEndpoint { } /** - * Many OpenAI-compatible Responses implementations (e.g. DeepSeek) are stateless and - * ignore `previous_response_id`, so only chain when the user opts in via `statefulResponses`. + * Spec-compliant Responses servers honor `previous_response_id`, but some (e.g. DeepSeek) + * silently ignore it, so users can opt out with `statefulResponses: false`. */ protected override get supportsStatefulResponses(): boolean { - return this.modelMetadata.statefulResponses === true; + return this.modelMetadata.statefulResponses !== false; } override createRequestBody(options: ICreateEndpointBodyOptions): IEndpointBody { diff --git a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts index ae0113179190..93ad35674b75 100644 --- a/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts +++ b/extensions/copilot/src/extension/byok/vscode-node/test/customEndpointProvider.spec.ts @@ -236,7 +236,7 @@ describe('CustomEndpointBYOKModelProvider', () => { }; } - it('omits store and previous_response_id after cloning a Custom Endpoint Responses endpoint when zeroDataRetentionEnabled is omitted', async () => { + it('omits store but keeps previous_response_id after cloning a Custom Endpoint Responses endpoint when both flags are omitted', async () => { const endpoint = (await createConfiguredResponsesEndpoint()).cloneWithTokenOverride(64000); const body = createResponsesBody(endpoint); @@ -247,12 +247,12 @@ describe('CustomEndpointBYOKModelProvider', () => { }).toEqual({ storePresent: false, store: undefined, - previousResponseId: undefined, + previousResponseId: customResponsesMarker, }); }); - it('issue #331636: sends the full tool-call history to stateless Custom Endpoint Responses servers', async () => { - const endpoint = await createConfiguredResponsesEndpoint(); + it('issue #331636: sends the full tool-call history when statefulResponses is false', async () => { + const endpoint = await createConfiguredResponsesEndpoint(undefined, false); const body = endpoint.createRequestBody({ debugName: 'test', messages: [ @@ -280,8 +280,8 @@ describe('CustomEndpointBYOKModelProvider', () => { }); }); - it('enables store but not previous_response_id when zeroDataRetentionEnabled is false without statefulResponses', async () => { - const endpoint = await createConfiguredResponsesEndpoint(false); + it('enables store but not previous_response_id when zeroDataRetentionEnabled and statefulResponses are false', async () => { + const endpoint = await createConfiguredResponsesEndpoint(false, false); const body = createResponsesBody(endpoint); expect({ @@ -295,8 +295,8 @@ describe('CustomEndpointBYOKModelProvider', () => { }); }); - it('chains via previous_response_id when statefulResponses is true', async () => { - const endpoint = await createConfiguredResponsesEndpoint(false, true); + it('enables store and previous_response_id when zeroDataRetentionEnabled is false and statefulResponses is omitted', async () => { + const endpoint = await createConfiguredResponsesEndpoint(false); const body = createResponsesBody(endpoint); expect({ diff --git a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts index aefbf0ab783c..d47b5b7a9468 100644 --- a/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts +++ b/extensions/copilot/src/platform/endpoint/common/endpointProvider.ts @@ -156,8 +156,9 @@ export type IChatModelInformation = IModelAPIResponse & { modelOptions?: Readonly; zeroDataRetentionEnabled?: boolean; /** - * BYOK Custom Endpoint opt-in declaring that the Responses API server retains prior - * responses, so requests may chain via `previous_response_id` and send only post-marker history. + * BYOK Custom Endpoint setting declaring whether the Responses API server retains prior + * responses, so requests may chain via `previous_response_id` and send only post-marker + * history. Unset is treated as `true`; set `false` for stateless servers. */ statefulResponses?: boolean; /**