copilotApiService.ts ×30

Frontier kind: Code frontier

unlabeled · c_2df40c39245a

1239 tests · 19884 LOC · 82 files · introduces 0 tests · 639 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
31 ranges639 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1551 ranges19884 lines · 82 files · Browse complete extent
All tests (intent)
1239 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 639 introduced LOC across 31 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/shared/copilotApiService.ts 627 introduced LOC · 30 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- copilotApiService.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type Anthropic from '@anthropic-ai/sdk';
7 > import { CAPIClient, RequestType, type CCAModel, type IExtensionInformation } from '@vscode/copilot-api';
8 > import { generateUuid } from '../../../../base/common/uuid.js';
9 > import { getDevDeviceId, getMachineId } from '../../../../base/node/id.js';
10 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
11 > import { IAgentHostGitHubEndpointService } from '../agentHostGitHubEndpointService.js';
12 > import { ILogService } from '../../../log/common/log.js';
13 > import { IProductService } from '../../../product/common/productService.js';
14 > import { COPILOT_LICENSE_AGREEMENT } from '../../../endpoint/common/licenseAgreement.js';
15 > import { parseCopilotTokenFields } from '../copilot/copilotTokenFields.js';
16 >
17 > // #region Types
18 >
19 > /**
20 > * Per-call transport options for all {@link ICopilotApiService} methods.
21 > *
22 > * `headers` are merged into the outgoing CAPI request before security-
23 > * sensitive headers (`Authorization`, `Content-Type`, `X-Request-Id`,
24 > * `OpenAI-Intent`), so callers cannot override those.
25 > *
26 > * `signal` propagates to the outgoing API request but **not** to the
27 > * shared token mint. The mint is deduped across concurrent callers, so
28 > * a single caller's abort must not cancel it for everyone.
29 > */
30 > export interface ICopilotApiServiceRequestOptions {
31 > readonly headers?: Readonly<Record<string, string>>;
32 > readonly signal?: AbortSignal;
33 >
34 > /**
35 > * Suppress the `Copilot-Integration-Id` header on this request.
36 > *
37 > * When unset, `@vscode/copilot-api` derives the integration id from the
38 > * discovered Copilot SKU: a `no_auth_limited_copilot` SKU maps to
39 > * `vscode-nl`, which the CAPI backend treats as the limited/no-auth
40 > * integration and refuses premium models such as `claude-opus-4.7`.
41 > * Setting this to `true` omits the header so CAPI authorizes against the
42 > * token's real entitlement. Mirrors the Copilot Chat extension's
43 > * `ClaudeStreamingPassThroughEndpoint.getEndpointFetchOptions()`.
44 > */
45 > readonly suppressIntegrationId?: boolean;
46 > }
47 >
48 > /**
49 > * One chat message in a {@link ICopilotUtilityChatCompletionRequest}.
50 > * Mirrors the OpenAI Chat Completions message shape CAPI accepts.
51 > */
52 > export interface ICopilotUtilityChatMessage {
53 > readonly role: 'system' | 'user' | 'assistant';
54 > readonly content: string;
55 > }
56 >
57 > /**
58 > * Inputs for {@link ICopilotApiService.utilityChatCompletion}.
59 > *
60 > * Callers own prompt construction — typically a `'system'` rules message
61 > * followed by one or more `'user'` messages, matching the Copilot Chat
62 > * extension's `copilot-utility-small` prompts (see
63 > * `GitCommitMessagePrompt`'s `SystemMessage` + `UserMessage` pair). This
64 > * service forwards the messages and returns the assistant text.
65 > *
66 > * `temperature` defaults to `0.1` (matching the Copilot Chat extension's
67 > * default `IConversationOptions.temperature`). All other parameters
68 > * (`top_p`, model family) are fixed defaults inside the service — callers
69 > * should not need to tune them for utility flows. `max_tokens` is left
70 > * unset so CAPI applies its per-model default, matching what the
71 > * extension's `copilot-utility-small` endpoint sends today.
72 > */
73 > export interface ICopilotUtilityChatCompletionRequest {
74 > readonly messages: readonly ICopilotUtilityChatMessage[];
75 > readonly temperature?: number;
76 > }
77 >
78 > /**
79 > * Subset of the GitHub `copilot_internal/user` response we care about.
80 > * The full payload carries entitlement info; we only need `endpoints` (for
81 > * routing CAPI requests) and `access_type_sku` (which `CAPIClient.updateDomains`
82 > * stamps onto requests).
83 > */
84 > interface ICopilotUserResponse {
85 > readonly login?: string;
86 > readonly copilotignore_enabled?: boolean;
87 > readonly endpoints?: {
88 > readonly api?: string;
89 > readonly telemetry?: string;
90 > readonly proxy?: string;
91 > readonly 'origin-tracker'?: string;
92 > };
93 > readonly access_type_sku?: string;
94 > }
95 >
96 > interface ICachedClient {
97 > readonly capiClient: CAPIClient;
98 > readonly expiresAt: number;
99 > /** GitHub login returned by `/copilot_internal/user`, when present. */
100 > readonly login?: string;
101 > /** The CAPI `endpoints.telemetry` base URL discovered for this token, if any. */
102 > readonly telemetryEndpoint?: string;
103 > /** The CAPI `endpoints.api` base URL discovered (or overridden) for this token, if any. */
104 > readonly apiEndpoint?: string;
105 > readonly copilotIgnoreEnabled?: boolean;
106 > }
107 >
108 > /**
109 > * Subset of the `RequestType.CopilotToken` mint response we care about.
110 > */
111 > interface ICopilotTokenEnvelope {
112 > readonly token?: unknown;
113 > readonly expires_at?: unknown;
114 > readonly refresh_in?: unknown;
115 > readonly organization_list?: unknown;
116 > }
117 >
118 > /**
119 > * Per-GitHub-token Copilot session token cache entry, plus a per-family
120 > * resolved utility model id. The model id is bound to the same lifetime as
121 > * the Copilot token so the entry can be evicted atomically on 401/403.
122 > */
123 > interface ICachedCopilotToken {
124 > readonly token: string;
125 > readonly expiresAt: number;
126 > readonly modelIdsByFamily: Map<string, string>;
127 > readonly isInternal: boolean;
128 > readonly isVscodeTeamMember: boolean;
129 > }
130 >
131 > /**
132 > * Memoized parts of `CAPIClient` construction that don't depend on the user
133 > * token. Built once and reused by every per-token client.
134 > */
135 > interface ICapiBase {
136 > readonly extensionInfo: IExtensionInformation;
137 > readonly userUrl: string;
138 > }
139 >
140 > // #endregion
141 >
142 > // #region Constants
143 >
144 > /**
145 > * Sentinel {@link CopilotApiError.status} used when the error came from a
146 > * mid-stream SSE `event: error` frame rather than an HTTP non-2xx response.
147 > * The upstream HTTP status was 200 (the stream had already started); the
148 > * real HTTP status is no longer meaningful, so consumers that need an HTTP
149 > * status code (e.g. when re-emitting before headers are sent) should not
150 > * trust this value. Use `envelope.error.type` instead.
151 > */
152 > export const COPILOT_API_ERROR_STATUS_STREAMING = 520;
153 >
154 > /**
155 > * Re-resolve the CAPI endpoint discovery this many seconds before the cache
156 > * entry's notional expiry. The `/copilot_internal/user` response itself
157 > * carries no expiry, so we apply a fixed TTL and refresh ahead of it.
158 > */
159 > const CAPI_CONTEXT_REFRESH_BUFFER_SECONDS = 5 * 60;
160 >
161 > /** Conservative TTL for the `/copilot_internal/user` discovery result. */
162 > const CAPI_CONTEXT_TTL_SECONDS = 30 * 60;
163 >
164 > const USER_API_VERSION = '2025-04-01';
165 >
166 > /**
167 > * Test/debug override for the CAPI base URL. When set to a **loopback** URL,
168 > * {@link CopilotApiService} skips the `api.github.com/copilot_internal/user`
169 > * endpoint-discovery round-trip (which requires a real GitHub token) and routes
170 > * every CAPI request — `models`, `responses`, `messages` — straight at this URL
171 > * instead. Only ever set by the smoke-test harness (see `setupAgentHostSuite`)
172 > * so the agent host's shared CAPI client can talk to the mock LLM server; never
173 > * set in production, so normal per-token discovery is unchanged.
174 > *
175 > * The override is restricted to loopback hosts, plus the reserved
176 > * `vscode-smoke.test` host when the smoke proxy marker is present. Subsequent
177 > * CAPI calls carry the user's GitHub bearer token, so every other non-loopback
178 > * or unparseable value is ignored to prevent token exfiltration.
179 > */
180 > const CAPI_URL_OVERRIDE_ENV = 'VSCODE_AGENT_HOST_CAPI_URL_OVERRIDE';
181 > const CAPI_URL_OVERRIDE_SMOKE_TEST_HOST = 'vscode-smoke.test';
182 > const CAPI_URL_OVERRIDE_SMOKE_TEST_ENV = 'VSCODE_SMOKE_TEST_PROXY_HEADER';
183 >
184 > /** True iff `url` parses and its host is a loopback address (localhost / 127.0.0.0/8 / ::1). */
185 function isLoopbackUrl(url: string): boolean {
186 let hostname: string;
194 return host === 'localhost' || host === '::1' || /^127(?:\.\d{1,3}){3}$/.test(host);
195 }
197 function isAllowedCapiUrlOverride(url: string): boolean {
198 if (isLoopbackUrl(url)) {
208 }
209 }
211 > /**
212 > * Re-mint the Copilot session token this many seconds before its
213 > * server-reported `expires_at`, mirroring the Copilot Chat extension's
214 > * `RefreshableCopilotTokenManager` 5-minute refresh buffer.
215 > */
216 > const COPILOT_TOKEN_REFRESH_BUFFER_SECONDS = 5 * 60;
217 >
218 > /**
219 > * Default CAPI model family for {@link ICopilotApiService.utilityChatCompletion}.
220 > * Matches the Copilot Chat extension's `copilot-utility-small` resolver
221 > * (`CopilotUtilitySmallChatEndpoint.capiFamily === CHAT_MODEL.GPT4OMINI`).
222 > */
223 > const UTILITY_DEFAULT_MODEL_FAMILY = 'gpt-4o-mini';
224 >
225 > /**
226 > * Default `temperature` for utility chat completions. Matches the Copilot
227 > * Chat extension's default `IConversationOptions.temperature`.
228 > */
229 > const UTILITY_DEFAULT_TEMPERATURE = 0.1;
230 >
231 > /**
232 > * Default `top_p` for utility chat completions. Matches the Copilot Chat
233 > * extension's default `IConversationOptions.topP`.
234 > */
235 > const UTILITY_DEFAULT_TOP_P = 1;
236 >
237 > /**
238 > * `OpenAI-Intent` value for utility chat completions. Matches the extension
239 > * vocabulary `'conversation-background'` for non-user-initiated utility
240 > * calls (chat title generation, commit messages, branch names, etc.).
241 > */
242 > const UTILITY_INTENT = 'conversation-background';
243 >
244 > const INTERNAL_COPILOT_ORGANIZATIONS = new Set([
245 > '4535c7beffc844b46bb1ed4aa04d759a',
246 > 'a5db0bcaae94032fe715fb34a5e4bce2',
247 > '7184f66dfcee98cb5f08a1cb936d5225',
248 > '1cb18ac6eedd49b43d74a1c5beb0b955',
249 > 'ea9395b9a9248c05ee6847cbd24355ed',
250 > ]);
251 > const VSCODE_COPILOT_ORGANIZATIONS = new Set(['551cca60ce19654d894e786220822482']);
252 >
253 > // #endregion
254 >
255 > // #region Errors
256 >
257 > /**
258 > * Thrown by {@link ICopilotApiService} when CAPI returns an Anthropic-format
259 > * API error — either as a non-2xx HTTP response or as a mid-stream
260 > * `event: error` SSE frame. Carries enough information for the Phase 2
261 > * Claude proxy to re-emit the error passthrough without re-mapping.
262 > *
263 > * Network/transport failures (connection reset, DNS failure, etc.) are
264 > * **not** wrapped as `CopilotApiError` — they propagate as raw `fetch`
265 > * rejections so consumers can distinguish API errors from transport errors.
266 > */
267 > export class CopilotApiError extends Error {
268 >
269 > /**
270 > * @param status HTTP status from the originating CAPI response, or
271 > * {@link COPILOT_API_ERROR_STATUS_STREAMING} for mid-stream SSE errors.
272 > * @param envelope Anthropic-format error envelope. For HTTP errors with a
273 > * non-conforming body (plain text, malformed JSON, missing fields) this
274 > * is synthesized; for conforming bodies and SSE frames it is the
275 > * server's envelope verbatim.
276 > * @param message Optional override for `Error.message`. Defaults to
277 > * `envelope.error.message`. **Never includes auth tokens.**
278 > */
279 > constructor(
280 readonly status: number,
281 readonly envelope: Anthropic.ErrorResponse,
285 this.name = 'CopilotApiError';
286 }
288 >
289 > /**
290 > * Build a {@link CopilotApiError} from a CAPI HTTP response body. If the
291 > * body parses as a conforming Anthropic envelope, it is used verbatim;
292 > * otherwise a synthetic envelope is constructed with `error.type:
293 > * 'api_error'` and the response body as `error.message` (or status text
294 > * when the body is empty). The returned error's `message` deliberately
295 > * mirrors the original `"<prefix>: <status> <statusText>"` format so
296 > * existing log-line consumers continue to read identifiably. `prefix`
297 > * defaults to `"CAPI request failed"` (the historical wording for
298 > * `messages`); pass `"CAPI models request failed"` for the `models()` path.
299 > */
300 function buildCopilotApiHttpError(status: number, statusText: string, bodyText: string, prefix = 'CAPI request failed'): CopilotApiError {
301 let envelope: Anthropic.ErrorResponse | undefined;
336 );
337 }
339 > // #endregion
340 >
341 > export type FetchFunction = typeof globalThis.fetch;
342 >
343 > export const ICopilotApiService = createDecorator<ICopilotApiService>('copilotApiService');
344 >
345 > /**
346 > * Foundational gateway between the agent host and GitHub Copilot's CAPI proxy
347 > * for Anthropic-style chat completions and model discovery.
348 > *
349 > * ## Goals
350 > *
351 > * 1. **Single source of truth for CAPI auth.** Callers pass a raw GitHub token
352 > * and never deal with endpoint discovery or routing themselves.
353 > * 2. **Stable surface for chat agents.** A small, typed API that abstracts the
354 > * underlying `CAPIClient`, SSE framing, and Anthropic event taxonomy so
355 > * feature code can focus on prompting.
356 > * 3. **Resource-safe streaming.** Async-generator output that fully releases
357 > * the underlying HTTP connection regardless of how the consumer terminates
358 > * iteration (early `break`, thrown error, abort, or natural end-of-stream).
359 > * 4. **Skew- and revocation-tolerant context cache.** Endpoint/sku discovery
360 > * stays cached as long as it's usable and is invalidated immediately on
361 > * `401`/`403` so callers self-heal without restarting the host.
362 > *
363 > * ## Auth strategy
364 > *
365 > * The GitHub user token IS the credential. There is no Copilot session-token
366 > * mint; we send `Authorization: Bearer <github-token>` directly to CAPI's
367 > * `/v1/messages` and `/models` endpoints. This mirrors what the
368 > * `@github/copilot` CLI does (see `fetchCopilotUser` and
369 > * `CopilotAnthropicClient.createWithOAuthToken` in `github/copilot-agent-runtime`).
370 > *
371 > * The `endpoints.api` URL CAPI requests are routed to is discovered per-token
372 > * by calling `GET /copilot_internal/user` once and caching the result. This
373 > * works for both consumer (`api.githubcopilot.com`) and Enterprise
374 > * (`api.enterprise.githubcopilot.com`) accounts without configuration.
375 > *
376 > * {@link utilityChatCompletion} is the one exception to the
377 > * GitHub-token-IS-the-credential rule: CAPI's `/chat/completions` endpoint
378 > * expects a Copilot session token (the same one the Copilot Chat extension
379 > * mints via `RequestType.CopilotToken`). The service mints it internally
380 > * from the supplied GitHub token, caches it per-token alongside the
381 > * resolved utility model id, and refreshes ahead of expiry.
382 > *
383 > * ## Non-goals
384 > *
385 > * - Per-conversation history, retry/backoff, or rate-limit handling. Callers
386 > * own request orchestration.
387 > *
388 > * ## Concurrency model
389 > *
390 > * - Each cached entry is a **distinct {@link CAPIClient} instance** with its
391 > * own discovered domain state. Concurrent in-flight requests for two
392 > * different GitHub tokens cannot trample each other's `endpoints.api` —
393 > * token A's request will always route through the client built for A.
394 > * - Multiple in-flight requests for the **same** GitHub token share a single
395 > * endpoint-discovery call via the per-token cache map (no thundering herd
396 > * on cold start).
397 > * - `AbortSignal` is forwarded to the outgoing API request (messages, models)
398 > * but **not** to the shared discovery call, so cancellation propagates to
399 > * the caller's own request without affecting concurrent callers sharing the
400 > * discovery.
401 > *
402 > * ## Error semantics
403 > *
404 > * - Network/transport errors propagate as raw `fetch` rejections (e.g.
405 > * connection reset, DNS failure). Consumers can distinguish them from
406 > * API errors by `instanceof CopilotApiError`.
407 > * - Non-2xx responses from CAPI's `messages` and `models` endpoints throw
408 > * {@link CopilotApiError} carrying the HTTP `status` and the parsed
409 > * Anthropic error `envelope` (synthesized if the response body isn't a
410 > * conforming envelope). **Tokens are never embedded in error messages.**
411 > * - Streaming `event: error` SSE frames throw {@link CopilotApiError} with
412 > * `status` set to {@link COPILOT_API_ERROR_STATUS_STREAMING} (the upstream
413 > * HTTP status was 200 and is no longer meaningful) and the server-supplied
414 > * error envelope preserved verbatim.
415 > * - Failures of the `/copilot_internal/user` discovery call throw plain
416 > * `Error` (not `CopilotApiError`) with a `"Copilot endpoint discovery
417 > * failed: ..."` prefix — it is an implementation detail of this service
418 > * and is not part of the Anthropic-shaped CAPI surface.
419 > * - Malformed JSON in an SSE `data:` line is logged and skipped, not thrown.
420 > */
421 > /**
422 > * Restricted/enhanced telemetry context derived from a user's minted CAPI Copilot session token,
423 > * mirroring what the Copilot extension reads off its `CopilotToken` (`rt` opt-in, `tid` tracking id)
424 > * plus the CAPI `endpoints.telemetry` host.
425 > */
426 > export interface IRestrictedTelemetryContext {
427 > /** Whether the token opts into enhanced/restricted telemetry (the `rt=1` claim). */
428 > readonly restrictedTelemetryEnabled: boolean;
429 > /** The Copilot user tracking id (`tid` claim), or `undefined` when absent. */
430 > readonly trackingId: string | undefined;
431 > /** The CAPI `endpoints.telemetry` base URL, resolved only when enabled; `undefined` otherwise. */
432 > readonly telemetryEndpoint: string | undefined;
433 > /** Whether the token belongs to a GitHub or Microsoft internal organization. */
434 > readonly isInternal?: boolean;
435 > /** GitHub login returned by `/copilot_internal/user`. */
436 > readonly userName?: string;
437 > /** Whether the token identifies a VS Code team member. */
438 > readonly isVscodeTeamMember?: boolean;
439 > /** Whether content exclusion is enabled; undefined when discovery could not determine it. */
440 > readonly copilotIgnoreEnabled?: boolean;
441 > }
442 >
443 > export interface ICopilotApiService {
444 >
445 > readonly _serviceBrand: undefined;
446 >
447 > /**
448 > * Stream a chat completion as raw Anthropic stream events.
449 > *
450 > * Yields every `Anthropic.MessageStreamEvent` in the order the server
451 > * emits them, **including `message_stop` as the last event** before the
452 > * generator returns. Phase 2 proxy relies on receiving a complete,
453 > * replayable event stream.
454 > *
455 > * @throws on non-2xx status or SSE `error` event.
456 > */
457 > messages(
458 > githubToken: string,
459 > request: Anthropic.MessageCreateParamsStreaming,
460 > options?: ICopilotApiServiceRequestOptions,
461 > ): AsyncGenerator<Anthropic.MessageStreamEvent>;
462 >
463 > /**
464 > * Send a chat completion and return the full aggregated response.
465 > * @throws on non-2xx status.
466 > */
467 > messages(
468 > githubToken: string,
469 > request: Anthropic.MessageCreateParamsNonStreaming,
470 > options?: ICopilotApiServiceRequestOptions,
471 > ): Promise<Anthropic.Message>;
472 >
473 > /**
474 > * Count tokens for a hypothetical request.
475 > *
476 > * @throws always — `countTokens` is not supported by CAPI in Phase 1.5.
477 > * Phase 2 proxy maps this to HTTP 501.
478 > */
479 > countTokens(
480 > githubToken: string,
481 > req: Anthropic.MessageCountTokensParams,
482 > options?: ICopilotApiServiceRequestOptions,
483 > ): Promise<Anthropic.MessageTokensCount>;
484 >
485 > /**
486 > * List models available to the GitHub user.
487 > *
488 > * Each {@link CCAModel} carries a `vendor` (e.g. `'Anthropic'`) and
489 > * `supported_endpoints` (e.g. `['/v1/messages']`). Callers filtering for
490 > * Anthropic-format models should match on both fields.
491 > *
492 > * Known CAPI values as of 2026-04-30:
493 > * - `vendor`: `'Anthropic'` (capitalized)
494 > * - `supported_endpoints`: `'/v1/messages'` for Anthropic chat models
495 > */
496 > models(githubToken: string, options?: ICopilotApiServiceRequestOptions): Promise<CCAModel[]>;
497 >
498 > /**
499 > * Pass-through to CAPI's OpenAI-shaped Responses endpoint
500 > * (`{capiBaseUrl}/responses`). Used by `CodexProxyService` to forward
501 > * `/v1/responses` requests from the Codex CLI without deserializing
502 > * the body. The caller owns the returned `Response` (its body and any
503 > * streaming) and is responsible for consuming or aborting it.
504 > *
505 > * @throws on non-2xx upstream response.
506 > */
507 > responses(
508 > githubToken: string,
509 > body: string,
510 > options?: ICopilotApiServiceRequestOptions,
511 > ): Promise<Response>;
512 >
513 > /**
514 > * Send arbitrary user chat messages through CAPI's `/chat/completions`
515 > * endpoint and return the assistant text.
516 > *
517 > * Internally mints (and caches) a Copilot session token from the
518 > * supplied GitHub token — the same flow the Copilot Chat extension
519 > * uses for its `copilot-utility-small` endpoint (PR title/description,
520 > * commit messages, branch names, chat titles, etc.). Uses the
521 > * `gpt-4o-mini` model family with `top_p = 1` and `temperature = 0.1`
522 > * by default (override via `request.temperature`).
523 > *
524 > * Non-streaming. Callers own prompt construction and any
525 > * domain-specific parsing of the returned text.
526 > *
527 > * @throws {@link CopilotApiError} on non-2xx CAPI response.
528 > * @throws plain `Error` when no model in the requested family is
529 > * available or when the response contains no text content.
530 > */
531 > utilityChatCompletion(
532 > githubToken: string,
533 > request: ICopilotUtilityChatCompletionRequest,
534 > options?: ICopilotApiServiceRequestOptions,
535 > ): Promise<string>;
536 >
537 > /**
538 > * Resolve this user's restricted-telemetry context from the minted CAPI Copilot session token —
539 > * the `rt` opt-in and `tid` tracking id — plus the CAPI `endpoints.telemetry` host. The GitHub
540 > * token itself carries none of these claims; they live in the Copilot session token (minted via
541 > * `RequestType.CopilotToken`), exactly as the Copilot extension reads them off its `CopilotToken`.
542 > * The telemetry endpoint is resolved only when enabled, so public users incur no extra discovery.
543 > */
544 > resolveRestrictedTelemetryContext(githubToken: string): Promise<IRestrictedTelemetryContext>;
545 >
546 > /**
547 > * Resolve the CAPI `endpoints.api` base URL discovered for this GitHub token
548 > * (or the loopback test override), or `undefined` when discovery hasn't run
549 > * or failed. The effective CAPI host varies by account (consumer
550 > * `api.githubcopilot.com` vs. Enterprise / proxy), so callers that need the
551 > * real host — e.g. to resolve the correct proxy — should prefer this over the
552 > * hardcoded default.
553 > */
554 > resolveApiEndpoint(githubToken: string): Promise<string | undefined>;
555 >
556 > /** Resolve the GitHub login cached from `/copilot_internal/user`. */
557 > resolveUserLogin?(githubToken: string): Promise<string | undefined>;
558 > }
559 >
560 > export class CopilotApiService implements ICopilotApiService {
561 >
562 > declare readonly _serviceBrand: undefined;
563 >
564 > private _capiBasePromise: Promise<ICapiBase> | null = null;
565 > private readonly _clientsByToken = new Map<string, Promise<ICachedClient>>();
566 > private readonly _copilotTokensByGithub = new Map<string, Promise<ICachedCopilotToken>>();
567 > private readonly _fetch: FetchFunction;
568 >
569 > constructor(
570 fetchFn: FetchFunction | undefined,
571 @ILogService private readonly _logService: ILogService,
575 this._fetch = fetchFn ?? globalThis.fetch;
576 }
578 > // #region Public API
579 >
580 > messages(
581 > githubToken: string,
582 > request: Anthropic.MessageCreateParamsStreaming,
583 > options?: ICopilotApiServiceRequestOptions,
584 > ): AsyncGenerator<Anthropic.MessageStreamEvent>;
585 > messages(
586 > githubToken: string,
587 > request: Anthropic.MessageCreateParamsNonStreaming,
588 > options?: ICopilotApiServiceRequestOptions,
589 > ): Promise<Anthropic.Message>;
590 > messages(
591 githubToken: string,
592 request: Anthropic.MessageCreateParams,
598 return this._messagesNonStreaming(githubToken, request, options);
599 }
601 > async countTokens(
602 _githubToken: string,
603 _req: Anthropic.MessageCountTokensParams,
606 throw new Error('countTokens not supported by CAPI');
607 }
609 > async models(githubToken: string, options?: ICopilotApiServiceRequestOptions): Promise<CCAModel[]> {
610 const capiClient = await this._getClientForToken(githubToken);
611
638 return json.data ?? [];
639 }
641 > async responses(
642 githubToken: string,
643 body: string,
686 return response;
687 }
689 > async utilityChatCompletion(
690 githubToken: string,
691 request: ICopilotUtilityChatCompletionRequest,
738 return content;
739 }
741 > // #endregion
742 >
743 > // #region Lazy Init
744 >
745 > private _getCapiBase(): Promise<ICapiBase> {
746 if (!this._capiBasePromise) {
747 this._capiBasePromise = this._buildCapiBase().catch(err => {
752 return this._capiBasePromise;
753 }
755 > private async _buildCapiBase(): Promise<ICapiBase> {
756 const [machineId, deviceId] = await Promise.all([
757 getMachineId(err => this._logService.warn('[CopilotApiService] getMachineId failed', err)),
779 return { extensionInfo, userUrl };
780 }
782 > // #endregion
783 >
784 > // #region Streaming
785 >
786 > private async *_messagesStreaming(
787 githubToken: string,
788 request: Anthropic.MessageCreateParams,
797 yield* this._readSSE(response.body);
798 }
800 > // #endregion
801 >
802 > // #region Non-Streaming
803 >
804 > private async _messagesNonStreaming(
805 githubToken: string,
806 request: Anthropic.MessageCreateParams,
810 return response.json() as Promise<Anthropic.Message>;
811 }
813 > // #endregion
814 >
815 > // #region Shared Request
816 >
817 > private async _sendRequest(
818 githubToken: string,
819 request: Anthropic.MessageCreateParams,
872 return response;
873 }
875 > // #endregion
876 >
877 > // #region Per-Token Client
878 >
879 > /**
880 > * Resolve a {@link CAPIClient} that has had its domains updated for the
881 > * supplied user. Concurrent callers for the same token share one
882 > * `/copilot_internal/user` discovery via the cache map; callers with
883 > * different tokens get their **own** `CAPIClient` instance, so the
884 > * `updateDomains` mutation for token A can never affect a request being
885 > * dispatched for token B.
886 > */
887 > private _getClientForToken(githubToken: string): Promise<CAPIClient> {
888 return this._getEntryForToken(githubToken).then(entry => entry.capiClient);
889 }
891 > /**
892 > * Resolve this user's restricted-telemetry context. Reads the `rt`/`tid` claims from the minted
893 > * CAPI Copilot session token (the GitHub token has neither), and resolves the CAPI
894 > * `endpoints.telemetry` host from the cached `/copilot_internal/user` discovery only when the
895 > * user is opted in, so public users pay no extra discovery call.
896 > */
897 > async resolveRestrictedTelemetryContext(githubToken: string): Promise<IRestrictedTelemetryContext> {
898 const token = await this._getCopilotTokenEntry(githubToken);
899 const client = await this._getEntryForToken(githubToken);
914 };
915 }
917 > async resolveApiEndpoint(githubToken: string): Promise<string | undefined> {
918 return (await this._getEntryForToken(githubToken)).apiEndpoint;
919 }
921 > async resolveUserLogin(githubToken: string): Promise<string | undefined> {
922 return (await this._getEntryForToken(githubToken)).login;
923 }
925 > private _getEntryForToken(githubToken: string): Promise<ICachedClient> {
926 const nowSeconds = Date.now() / 1000;
927 const existing = this._clientsByToken.get(githubToken);
951 return pending;
952 }
954 > private _invalidateClientForToken(githubToken: string): void {
955 this._clientsByToken.delete(githubToken);
956 }
958 > private async _buildClientForToken(githubToken: string): Promise<ICachedClient> {
959 const { extensionInfo, userUrl } = await this._getCapiBase();
960 const fetch = this._fetch;
1024 };
1025 }
1027 > // #endregion
1028 >
1029 > // #region Per-Token Copilot Session Token
1030 >
1031 > /**
1032 > * Resolve the Copilot session token for a GitHub token, minting and
1033 > * caching one if needed. Concurrent callers for the same GitHub token
1034 > * share a single in-flight mint; the caller's `AbortSignal` is
1035 > * deliberately NOT forwarded so cancelling one caller does not poison
1036 > * the shared mint for the others.
1037 > */
1038 > private _getCopilotToken(githubToken: string): Promise<string> {
1039 return this._getCopilotTokenEntry(githubToken).then(entry => entry.token);
1040 }
1042 > private _getCopilotTokenEntry(githubToken: string): Promise<ICachedCopilotToken> {
1043 const nowSeconds = Date.now() / 1000;
1044 const existing = this._copilotTokensByGithub.get(githubToken);
1073 return pending;
1074 }
1076 > private _invalidateCopilotTokenForGithub(githubToken: string): void {
1077 this._copilotTokensByGithub.delete(githubToken);
1078 }
1080 > private async _buildCopilotToken(githubToken: string): Promise<ICachedCopilotToken> {
1081 const capiClient = await this._getClientForToken(githubToken);
1082
1128 };
1129 }
1131 > /**
1132 > * Resolve the concrete CAPI model id for the supplied family (e.g.
1133 > * `gpt-4o-mini`). Cached per GitHub token + family alongside the
1134 > * Copilot session token so eviction on 401/403 also clears the cached
1135 > * model id.
1136 > */
1137 > private async _resolveUtilityModelId(githubToken: string, modelFamily: string): Promise<string> {
1138 const pendingEntry = this._copilotTokensByGithub.get(githubToken);
1139 const entry = pendingEntry ? await pendingEntry : undefined;
1152 return match.id;
1153 }
1155 > // #endregion
1156 >
1157 > // #region SSE Parsing
1158 >
1159 > private async *_readSSE(body: ReadableStream<Uint8Array>): AsyncGenerator<Anthropic.MessageStreamEvent> {
1160 const reader = body.getReader();
1161 const decoder = new TextDecoder();
1206 }
1207 }
1209 > /**
1210 > * @returns the parsed stream event, or `undefined` to skip the line.
1211 > * @throws on `error` events from the server.
1212 > */
1213 > private _parseDataLine(line: string): Anthropic.MessageStreamEvent | undefined {
1214 if (!line.startsWith('data: ')) {
1215 return undefined;
1273 return parsed as Anthropic.MessageStreamEvent;
1274 }
1276 > // #endregion
1277 > }
1278 >
1279 > const KNOWN_SSE_EVENT_TYPES = new Set([
1280 > 'message_start', 'message_delta', 'message_stop',
1281 > 'content_block_start', 'content_block_delta', 'content_block_stop',
1282 > ]);
src/vs/platform/endpoint/common/licenseAgreement.ts 12 introduced LOC · 1 range

Open complete file

1 > /*--------------------------------------------------------------------------------------------- licenseAgreement.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > /**
7 > * This file is modified as part of the production build.
8 > *
9 > * WARNING: Do not move or rename this file.
10 > */
11 > export const COPILOT_LICENSE_AGREEMENT: string | undefined = undefined;
12 > export const COPILOT_INTEGRATION_ID: string = 'code-oss';