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,