mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 17:07:50 +08:00
agentHost: revert reserved integration id change; tag Codex proxy with vscode_codex UA (#322600)
* Revert "Merge pull request #321964 from microsoft/fix-agenthost-reserved-integration-id" This reverts commitb82871d0b1, reversing changes made to18ab318cd2. * agentHost: do not pass an integration id into CAPIClient The 'code-oss' integration id is reserved and rejected by @vscode/copilot-api's CAPIClient constructor. Stop passing it in and drop the now-unnecessary getIntegrationId() test overrides; the Copilot-Integration-Id header still defaults via the library and the per-request suppressIntegrationId opt-in is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: tag Codex proxy requests with a vscode_codex user-agent Mirror the Claude proxy and oaiLanguageModelServer.ts by transforming the inbound codex user-agent (replacing the client-name portion with the vscode_codex prefix) and forwarding it to CAPI on /v1/responses requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: tag Codex agent model-list fetch with a vscode_codex user-agent The Codex agent's _refreshModels() call to ICopilotApiService.models() did not set a User-Agent, unlike the Claude agent (which sends vscode_claude_code/<version>). Add a matching vscode_codex/<version> header so the model-list fetch is identifiable server-side, consistent with the Codex proxy's responses requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: simplify CopilotApiService test construction Address PR review: the anonymous CopilotApiService subclasses no longer override anything after the getIntegrationId() removal, so replace them with direct construction for readability and cleaner stack traces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
185495af39
commit
cebc749d20
Vendored
+1
@@ -26,6 +26,7 @@ declare module '@vscode/copilot-api' {
|
||||
json?: unknown;
|
||||
method?: 'GET' | 'POST' | 'PUT';
|
||||
signal?: IAbortSignal;
|
||||
suppressIntegrationId?: boolean;
|
||||
}
|
||||
|
||||
export type MakeRequestOptions = Omit<FetchOptions, 'callSite'> & {
|
||||
|
||||
@@ -321,7 +321,7 @@ export class ClaudeAgent extends Disposable implements IAgent {
|
||||
}
|
||||
try {
|
||||
const userAgent = `${USER_AGENT_PREFIX}/${this._productService.version}`;
|
||||
const all = await this._copilotApiService.models(tokenAtStart, { headers: { 'User-Agent': userAgent } });
|
||||
const all = await this._copilotApiService.models(tokenAtStart, { headers: { 'User-Agent': userAgent }, suppressIntegrationId: true });
|
||||
// Stale-write guard: if `authenticate()` rotated the token
|
||||
// while we were awaiting the model list, a newer refresh has
|
||||
// already published the right value — don't overwrite it.
|
||||
|
||||
@@ -253,7 +253,7 @@ export class ClaudeProxyService extends LoopbackProxyServer<IClaudeProxyState, s
|
||||
const headers = buildOutboundHeaders(req.headers);
|
||||
let models: CCAModel[];
|
||||
try {
|
||||
models = await this._copilotApiService.models(runtime.state.githubToken, { headers });
|
||||
models = await this._copilotApiService.models(runtime.state.githubToken, { headers, suppressIntegrationId: true });
|
||||
} catch (err) {
|
||||
this._writeUpstreamErrorResponse(res, err);
|
||||
return;
|
||||
@@ -391,7 +391,7 @@ export class ClaudeProxyService extends LoopbackProxyServer<IClaudeProxyState, s
|
||||
originalSdkModelId: string,
|
||||
sessionId: string | undefined,
|
||||
): Promise<void> {
|
||||
const options: ICopilotApiServiceRequestOptions = { headers, signal: entry.ac.signal };
|
||||
const options: ICopilotApiServiceRequestOptions = { headers, signal: entry.ac.signal, suppressIntegrationId: true };
|
||||
let message: Anthropic.Message;
|
||||
try {
|
||||
message = await this._copilotApiService.messages(runtime.state.githubToken, body, options);
|
||||
@@ -427,7 +427,7 @@ export class ClaudeProxyService extends LoopbackProxyServer<IClaudeProxyState, s
|
||||
_originalSdkModelId: string,
|
||||
sessionId: string | undefined,
|
||||
): Promise<void> {
|
||||
const options: ICopilotApiServiceRequestOptions = { headers, signal: entry.ac.signal };
|
||||
const options: ICopilotApiServiceRequestOptions = { headers, signal: entry.ac.signal, suppressIntegrationId: true };
|
||||
let stream: AsyncGenerator<Anthropic.MessageStreamEvent>;
|
||||
try {
|
||||
stream = this._copilotApiService.messages(runtime.state.githubToken, body, options);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { ILogService } from '../../../log/common/log.js';
|
||||
import { IProductService } from '../../../product/common/productService.js';
|
||||
import { createSchema, platformSessionSchema, schemaProperty, type SessionMode } from '../../common/agentHostSchema.js';
|
||||
import { getReasoningEffortDescription, getReasoningEffortLabel } from '../../common/reasoningEffort.js';
|
||||
import { AgentHostCodexAgentBinaryArgsEnvVar, AgentHostCodexAgentCodexHomeEnvVar, AgentHostCodexAgentSdkRootEnvVar, AgentSession, AgentSignal, GITHUB_COPILOT_PROTECTED_RESOURCE, GITHUB_REPO_PROTECTED_RESOURCE, IAgent, IAgentCreateSessionConfig, IAgentCreateSessionResult, IAgentDescriptor, IAgentMaterializeSessionEvent, IAgentModelInfo, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IMcpNotification, type AgentProvider } from '../../common/agentService.js';
|
||||
@@ -93,6 +94,14 @@ const CLIENT_INFO = {
|
||||
|
||||
const CODEX_THINKING_LEVEL_KEY = 'thinkingLevel';
|
||||
|
||||
/**
|
||||
* User-agent prefix applied to the Codex agent's outbound CAPI calls (e.g. the
|
||||
* model-list fetch) so the traffic is identifiable server-side. Mirrors
|
||||
* `claudeAgent.ts` and the `vscode_codex` prefix used by `codexProxyService.ts`
|
||||
* and `oaiLanguageModelServer.ts`.
|
||||
*/
|
||||
const USER_AGENT_PREFIX = 'vscode_codex';
|
||||
|
||||
const CODEX_REASONING_EFFORTS: readonly ReasoningEffort[] = ['minimal', 'low', 'medium', 'high'];
|
||||
|
||||
/**
|
||||
@@ -544,6 +553,7 @@ export class CodexAgent extends Disposable implements IAgent {
|
||||
@ICodexProxyService private readonly _codexProxyService: ICodexProxyService,
|
||||
@IAgentConfigurationService private readonly _configurationService: IAgentConfigurationService,
|
||||
@IAgentSdkDownloader private readonly _agentSdkDownloader: IAgentSdkDownloader,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
@@ -719,7 +729,8 @@ export class CodexAgent extends Disposable implements IAgent {
|
||||
|
||||
private async _refreshModels(token: string): Promise<void> {
|
||||
try {
|
||||
const all = await this._copilotApiService.models(token);
|
||||
const userAgent = `${USER_AGENT_PREFIX}/${this._productService.version}`;
|
||||
const all = await this._copilotApiService.models(token, { headers: { 'User-Agent': userAgent }, suppressIntegrationId: true });
|
||||
if (this._githubToken !== token) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,14 @@ type ICodexProxyRuntime = ILoopbackProxyRuntime<ICodexProxyState>;
|
||||
|
||||
const PROXY_USER_FACING_NAME = 'CodexProxyService';
|
||||
|
||||
/**
|
||||
* User-agent prefix applied to outbound CAPI requests so the codex proxy's
|
||||
* traffic is identifiable server-side. Mirrors `oaiLanguageModelServer.ts`
|
||||
* in the Copilot Chat extension, which tags Codex requests with the same
|
||||
* prefix.
|
||||
*/
|
||||
const USER_AGENT_PREFIX = 'vscode_codex';
|
||||
|
||||
/**
|
||||
* When set to an absolute directory path, every `/v1/responses` request body
|
||||
* and its full upstream response stream are written to that directory as
|
||||
@@ -274,9 +282,11 @@ export class CodexProxyService extends LoopbackProxyServer<ICodexProxyState, str
|
||||
// whatever `runtime.state.githubToken` has been rotated to.
|
||||
const dispatchedToken = runtime.state.githubToken;
|
||||
|
||||
const headers = buildOutboundHeaders(req.headers);
|
||||
|
||||
try {
|
||||
this._logService.info(`[${PROXY_USER_FACING_NAME}] forwarding to CAPI responses...`);
|
||||
const upstream = await this._copilotApiService.responses(dispatchedToken, body, { signal: entry.ac.signal });
|
||||
const upstream = await this._copilotApiService.responses(dispatchedToken, body, { headers, signal: entry.ac.signal, suppressIntegrationId: true });
|
||||
const contentType = upstream.headers.get('content-type') ?? 'application/json';
|
||||
const upstreamHeaders = [...upstream.headers.entries()].map(([k, v]) => `${k}: ${v}`).join(', ');
|
||||
this._logService.info(`[${PROXY_USER_FACING_NAME}] <<< CAPI response: status=${upstream.status}, contentType=${contentType}, headers=[${upstreamHeaders}]`);
|
||||
@@ -346,3 +356,38 @@ export class CodexProxyService extends LoopbackProxyServer<ICodexProxyState, str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the headers forwarded to {@link ICopilotApiService.responses} from the
|
||||
* inbound codex request. Currently forwards `user-agent` (transformed via
|
||||
* {@link transformUserAgent}); the remaining outbound headers are supplied by
|
||||
* the API service itself.
|
||||
*/
|
||||
function buildOutboundHeaders(inbound: http.IncomingHttpHeaders): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
const userAgent = inbound['user-agent'];
|
||||
if (typeof userAgent === 'string' && userAgent.length > 0) {
|
||||
out['User-Agent'] = transformUserAgent(userAgent);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an incoming user-agent string by replacing the client name portion
|
||||
* (before the first `/`) with {@link USER_AGENT_PREFIX}. This mirrors the
|
||||
* transform in `oaiLanguageModelServer.ts` in the Copilot Chat extension,
|
||||
* ensuring all Codex requests are tagged with a consistent prefix for
|
||||
* server-side identification.
|
||||
*
|
||||
* Examples:
|
||||
* - `codex/1.2.3` → `vscode_codex/1.2.3`
|
||||
* - `OpenAI/Python/1.0` → `vscode_codex/Python/1.0`
|
||||
* - `unknown` → `vscode_codex/unknown`
|
||||
*/
|
||||
function transformUserAgent(userAgent: string): string {
|
||||
const slashIndex = userAgent.indexOf('/');
|
||||
if (slashIndex === -1) {
|
||||
return `${USER_AGENT_PREFIX}/${userAgent}`;
|
||||
}
|
||||
return `${USER_AGENT_PREFIX}${userAgent.substring(slashIndex)}`;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,19 @@ import { COPILOT_LICENSE_AGREEMENT } from '../../../endpoint/common/licenseAgree
|
||||
export interface ICopilotApiServiceRequestOptions {
|
||||
readonly headers?: Readonly<Record<string, string>>;
|
||||
readonly signal?: AbortSignal;
|
||||
|
||||
/**
|
||||
* Suppress the `Copilot-Integration-Id` header on this request.
|
||||
*
|
||||
* When unset, `@vscode/copilot-api` derives the integration id from the
|
||||
* discovered Copilot SKU: a `no_auth_limited_copilot` SKU maps to
|
||||
* `vscode-nl`, which the CAPI backend treats as the limited/no-auth
|
||||
* integration and refuses premium models such as `claude-opus-4.7`.
|
||||
* Setting this to `true` omits the header so CAPI authorizes against the
|
||||
* token's real entitlement. Mirrors the Copilot Chat extension's
|
||||
* `ClaudeStreamingPassThroughEndpoint.getEndpointFetchOptions()`.
|
||||
*/
|
||||
readonly suppressIntegrationId?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,6 +535,9 @@ export class CopilotApiService implements ICopilotApiService {
|
||||
...options?.headers,
|
||||
'Authorization': `Bearer ${githubToken}`,
|
||||
},
|
||||
// Opt-in per request — see
|
||||
// `ICopilotApiServiceRequestOptions.suppressIntegrationId`.
|
||||
suppressIntegrationId: options?.suppressIntegrationId,
|
||||
signal: options?.signal,
|
||||
},
|
||||
{ type: RequestType.Models },
|
||||
@@ -566,6 +582,9 @@ export class CopilotApiService implements ICopilotApiService {
|
||||
'X-Request-Id': requestId,
|
||||
'OpenAI-Intent': 'conversation',
|
||||
},
|
||||
// Opt-in per request — see
|
||||
// `ICopilotApiServiceRequestOptions.suppressIntegrationId`.
|
||||
suppressIntegrationId: options?.suppressIntegrationId,
|
||||
body,
|
||||
signal: options?.signal,
|
||||
},
|
||||
@@ -750,6 +769,7 @@ export class CopilotApiService implements ICopilotApiService {
|
||||
// paths already omit it). Thread a real per-turn initiator here if
|
||||
// that signal ever becomes available at the proxy boundary.
|
||||
},
|
||||
suppressIntegrationId: options?.suppressIntegrationId,
|
||||
body,
|
||||
signal: options?.signal,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import type * as http from 'http';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { NullLogService } from '../../../../log/common/log.js';
|
||||
import {
|
||||
type ICopilotApiService,
|
||||
type ICopilotApiServiceRequestOptions,
|
||||
} from '../../../node/shared/copilotApiService.js';
|
||||
import { CodexProxyService } from '../../../node/codex/codexProxyService.js';
|
||||
|
||||
// #region Test fakes
|
||||
|
||||
interface IResponsesCall {
|
||||
githubToken: string;
|
||||
body: string;
|
||||
options: ICopilotApiServiceRequestOptions | undefined;
|
||||
}
|
||||
|
||||
class FakeCopilotApiService implements ICopilotApiService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
readonly responsesCalls: IResponsesCall[] = [];
|
||||
|
||||
messages(): never {
|
||||
throw new Error('messages not used by Codex proxy tests');
|
||||
}
|
||||
|
||||
async countTokens(): Promise<never> {
|
||||
throw new Error('countTokens not used by Codex proxy tests');
|
||||
}
|
||||
|
||||
async models(): Promise<never> {
|
||||
throw new Error('models not used by Codex proxy tests');
|
||||
}
|
||||
|
||||
async responses(githubToken: string, body: string, options?: ICopilotApiServiceRequestOptions): Promise<Response> {
|
||||
this.responsesCalls.push({ githubToken, body, options });
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('event: response.completed\ndata: {}\n\n'));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(stream, { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
||||
}
|
||||
|
||||
async utilityChatCompletion(): Promise<never> {
|
||||
throw new Error('utilityChatCompletion not used by Codex proxy tests');
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region HTTP helpers
|
||||
|
||||
let _httpModule: typeof http | undefined;
|
||||
async function getHttp(): Promise<typeof http> {
|
||||
if (!_httpModule) {
|
||||
_httpModule = await import('http');
|
||||
}
|
||||
return _httpModule;
|
||||
}
|
||||
|
||||
function postResponses(url: string, init: { headers?: Record<string, string>; body?: string }): Promise<{ status: number; body: string }> {
|
||||
return getHttp().then(httpMod => new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const req = httpMod.request({
|
||||
hostname: u.hostname,
|
||||
port: u.port,
|
||||
path: u.pathname + u.search,
|
||||
method: 'POST',
|
||||
headers: init.headers,
|
||||
}, res => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', c => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
||||
res.on('end', () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString('utf8') }));
|
||||
res.on('error', reject);
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (init.body !== undefined) {
|
||||
req.write(init.body);
|
||||
}
|
||||
req.end();
|
||||
}));
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
const TOKEN = 'gh-test-token';
|
||||
|
||||
suite('CodexProxyService', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
async function withProxy(fn: (handle: { baseUrl: string; nonce: string }, fake: FakeCopilotApiService) => Promise<void>): Promise<void> {
|
||||
const fake = new FakeCopilotApiService();
|
||||
const service = new CodexProxyService(new NullLogService(), fake);
|
||||
const handle = await service.start(TOKEN);
|
||||
try {
|
||||
await fn(handle, fake);
|
||||
} finally {
|
||||
handle.dispose();
|
||||
service.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
test('forwards transformed user-agent to CAPI responses', async () => {
|
||||
await withProxy(async (handle, fake) => {
|
||||
await postResponses(`${handle.baseUrl}/v1/responses`, {
|
||||
headers: { 'Authorization': `Bearer ${handle.nonce}`, 'User-Agent': 'codex/1.2.3' },
|
||||
body: JSON.stringify({ model: 'gpt-5', stream: true, input: [] }),
|
||||
});
|
||||
assert.strictEqual(fake.responsesCalls.at(-1)?.options?.headers?.['User-Agent'], 'vscode_codex/1.2.3');
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the suffix when transforming a multi-segment user-agent', async () => {
|
||||
await withProxy(async (handle, fake) => {
|
||||
await postResponses(`${handle.baseUrl}/v1/responses`, {
|
||||
headers: { 'Authorization': `Bearer ${handle.nonce}`, 'User-Agent': 'OpenAI/Python/1.0' },
|
||||
body: JSON.stringify({ model: 'gpt-5', stream: true, input: [] }),
|
||||
});
|
||||
assert.strictEqual(fake.responsesCalls.at(-1)?.options?.headers?.['User-Agent'], 'vscode_codex/Python/1.0');
|
||||
});
|
||||
});
|
||||
|
||||
test('omits User-Agent when the inbound request has none', async () => {
|
||||
await withProxy(async (handle, fake) => {
|
||||
await postResponses(`${handle.baseUrl}/v1/responses`, {
|
||||
headers: { 'Authorization': `Bearer ${handle.nonce}` },
|
||||
body: JSON.stringify({ model: 'gpt-5', stream: true, input: [] }),
|
||||
});
|
||||
assert.strictEqual(fake.responsesCalls.at(-1)?.options?.headers?.['User-Agent'], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes
|
||||
import { CodexSessionConfigKey, collaborationModeKind, narrowAdditionalDirectories, narrowApprovalPolicy, narrowBoolean, narrowPersonality, narrowReasoningEffort, narrowReasoningSummary, narrowSandboxMode, narrowWebSearchMode } from '../../../node/codex/codexSessionConfigKeys.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { ISessionDataService } from '../../../common/sessionDataService.js';
|
||||
import { CodexAgent } from '../../../node/codex/codexAgent.js';
|
||||
import { ICodexProxyService } from '../../../node/codex/codexProxyService.js';
|
||||
@@ -24,6 +25,7 @@ function createAgent(disposables: Pick<DisposableStore, 'add'>): CodexAgent {
|
||||
instantiationService.stub(ICodexProxyService, { _serviceBrand: undefined });
|
||||
instantiationService.stub(IAgentConfigurationService, { _serviceBrand: undefined });
|
||||
instantiationService.stub(IAgentSdkDownloader, { _serviceBrand: undefined });
|
||||
instantiationService.stub(IProductService, { _serviceBrand: undefined, version: '1.0.0-test' } as IProductService);
|
||||
instantiationService.stub(ILogService, new NullLogService());
|
||||
return disposables.add(instantiationService.createInstance(CodexAgent));
|
||||
}
|
||||
|
||||
@@ -587,18 +587,23 @@ suite('CopilotApiService', () => {
|
||||
assert.strictEqual(headers['OpenAI-Intent'], 'messages-proxy');
|
||||
});
|
||||
|
||||
test('sends a derived Copilot-Integration-Id header by default', async () => {
|
||||
test('suppressIntegrationId opt-in controls the Copilot-Integration-Id header', async () => {
|
||||
const { fetch: fetchFn, captured } = routingFetch(
|
||||
() => anthropicResponse([{ type: 'text', text: 'ok' }]),
|
||||
);
|
||||
const service = createService(fetchFn);
|
||||
|
||||
// @vscode/copilot-api derives the integration id from the license /
|
||||
// SKU / build state and sends it on every request.
|
||||
// Default (no opt-in): @vscode/copilot-api derives and sends the header.
|
||||
await service.messages('gh-tok', baseRequest);
|
||||
const headers = captured().init?.headers as Record<string, string>;
|
||||
const withHeader = captured().init?.headers as Record<string, string>;
|
||||
|
||||
assert.ok(headers['Copilot-Integration-Id'], 'integration id should be present');
|
||||
// Opt-in: the header is omitted entirely so CAPI authorizes against
|
||||
// the token's real entitlement instead of the derived integration id.
|
||||
await service.messages('gh-tok', baseRequest, { suppressIntegrationId: true });
|
||||
const suppressed = captured().init?.headers as Record<string, string>;
|
||||
|
||||
assert.ok(withHeader['Copilot-Integration-Id'], 'integration id should be present by default');
|
||||
assert.strictEqual(suppressed['Copilot-Integration-Id'], undefined, 'integration id should be suppressed when opted in');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1585,16 +1590,21 @@ suite('CopilotApiService', () => {
|
||||
assert.strictEqual(capturedHeaders?.['Authorization'], 'Bearer gh-tok');
|
||||
});
|
||||
|
||||
test('sends a derived Copilot-Integration-Id header by default', async () => {
|
||||
test('suppressIntegrationId opt-in controls the Copilot-Integration-Id header', async () => {
|
||||
const { fetch: fetchFn, captured } = routingFetch(() => modelsResponse([]));
|
||||
const service = createService(fetchFn);
|
||||
|
||||
// @vscode/copilot-api derives the integration id from the license /
|
||||
// SKU / build state and sends it on every request.
|
||||
// Default (no opt-in): @vscode/copilot-api derives and sends the header.
|
||||
await service.models('gh-tok');
|
||||
const headers = captured().init?.headers as Record<string, string>;
|
||||
const withHeader = captured().init?.headers as Record<string, string>;
|
||||
|
||||
assert.ok(headers['Copilot-Integration-Id'], 'integration id should be present');
|
||||
// Opt-in: the header is omitted entirely so CAPI authorizes against
|
||||
// the token's real entitlement instead of the derived integration id.
|
||||
await service.models('gh-tok', { suppressIntegrationId: true });
|
||||
const suppressed = captured().init?.headers as Record<string, string>;
|
||||
|
||||
assert.ok(withHeader['Copilot-Integration-Id'], 'integration id should be present by default');
|
||||
assert.strictEqual(suppressed['Copilot-Integration-Id'], undefined, 'integration id should be suppressed when opted in');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user