From d0916b96a46c93d6790e06fa2a6eeea64b934834 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 10 Jun 2026 15:08:17 +0100 Subject: [PATCH 1/5] Add E2E test for session.providerEndpoint.get Validates the new shared API exposed by copilot-agent-runtime (see github/copilot-sdk-internal#133) end-to-end from the Node SDK. Covers both BYOK (returns provider config + headers) and CAPI (returns the resolved CAPI base URL). Regenerates rpc.ts and session-events.ts against the runtime schema that adds the providerEndpoint RPC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/generated/rpc.ts | 345 ++++++++++++++---- nodejs/src/generated/session-events.ts | 28 +- nodejs/test/e2e/provider_endpoint.e2e.test.ts | 86 +++++ 3 files changed, 389 insertions(+), 70 deletions(-) create mode 100644 nodejs/test/e2e/provider_endpoint.e2e.test.ts diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 53aa71fd2..c45c8ccba 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -423,10 +423,10 @@ export type InstalledPluginSource = * Category of instruction source — used for merge logic * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionsSourcesType". + * via the `definition` "InstructionSourceType". */ /** @experimental */ -export type InstructionsSourcesType = +export type InstructionSourceType = /** Instructions loaded from the user's home configuration. */ | "home" /** Instructions loaded from repository-scoped files. */ @@ -445,10 +445,10 @@ export type InstructionsSourcesType = * Where this source lives — used for UI grouping * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionsSourcesLocation". + * via the `definition` "InstructionSourceLocation". */ /** @experimental */ -export type InstructionsSourcesLocation = +export type InstructionSourceLocation = /** Instructions live in user-level configuration. */ | "user" /** Instructions live in repository-level configuration. */ @@ -1011,6 +1011,20 @@ export type ProviderConfigWireApi = | "completions" /** OpenAI Responses API wire format. */ | "responses"; +/** + * Wire protocol the caller must speak to `baseUrl`. Tells the caller which LLM client library to instantiate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderWireProtocol". + */ +/** @experimental */ +export type ProviderWireProtocol = + /** OpenAI Chat Completions wire format (`/chat/completions`). Use the `openai` client library. */ + | "openai-completions" + /** OpenAI Responses wire format (`/responses`). Use the `openai` client library. */ + | "openai-responses" + /** Anthropic Messages wire format (`/v1/messages`). Use the `@anthropic-ai/sdk` client library. */ + | "anthropic"; /** * Schema for the `PushAttachment` type. * @@ -2011,6 +2025,23 @@ export interface AgentReloadResult { */ agents: AgentInfo[]; } +/** + * Optional project paths to include in agent discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AgentsDiscoverRequest". + */ +/** @experimental */ +export interface AgentsDiscoverRequest { + /** + * Optional list of project directory paths to scan for project-scoped agents. When omitted or empty, only user/plugin/remote-independent agents are returned (no project scan). + */ + projectPaths?: string[]; + /** + * When true, omit the host's agents (the `/agents` directory and all plugin agents), leaving only project and remote agents. For multitenant deployments. + */ + excludeHostAgents?: boolean; +} /** * Name of the custom agent to select for subsequent turns. * @@ -3964,6 +3995,23 @@ export interface InstalledPluginInfo { */ enabled: boolean; } +/** + * Optional project paths to include in instruction discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "InstructionsDiscoverRequest". + */ +/** @experimental */ +export interface InstructionsDiscoverRequest { + /** + * Optional list of project directory paths to scan for repository/working-directory instruction sources. When omitted or empty, only user-level and plugin instruction sources are returned (no project scan). + */ + projectPaths?: string[]; + /** + * When true, omit the host's instruction sources (user/home-level files and plugin rules), leaving only repository and working-directory sources. For multitenant deployments. + */ + excludeHostInstructions?: boolean; +} /** * Instruction sources loaded for the session, in merge order. * @@ -3975,16 +4023,16 @@ export interface InstructionsGetSourcesResult { /** * Instruction sources for the session */ - sources: InstructionsSources[]; + sources: InstructionSource[]; } /** - * Schema for the `InstructionsSources` type. + * Schema for the `InstructionSource` type. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "InstructionsSources". + * via the `definition` "InstructionSource". */ /** @experimental */ -export interface InstructionsSources { +export interface InstructionSource { /** * Unique identifier for this source (used for toggling) */ @@ -4001,8 +4049,8 @@ export interface InstructionsSources { * Raw content of the instruction file */ content: string; - type: InstructionsSourcesType; - location: InstructionsSourcesLocation; + type: InstructionSourceType; + location: InstructionSourceLocation; /** * Glob pattern(s) from frontmatter — when set, this instruction applies only to matching files */ @@ -4015,6 +4063,10 @@ export interface InstructionsSources { * When true, this source starts disabled and must be toggled on by the user */ defaultDisabled?: boolean; + /** + * The project path this source was discovered from. Only set by sessionless discovery for repository/working-directory sources, where it disambiguates same-named files (e.g. .github/copilot-instructions.md) across multiple workspace roots. The session-scoped getSources leaves it unset. + */ + projectPath?: string; } /** * Schema for the `LocalSessionMetadataValue` type. @@ -7771,6 +7823,69 @@ export interface ProviderConfigAzure { */ apiVersion?: string; } +/** + * A snapshot of the provider endpoint the session is currently configured to talk to, with enough information for an external caller to make inference calls directly against the same backend using the OpenAI or Anthropic client libraries. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpoint". + */ +/** @experimental */ +export interface ProviderEndpoint { + protocol: ProviderWireProtocol; + /** + * Base URL the caller should pass to the LLM client library (e.g. `new OpenAI({ baseURL })` or `new Anthropic({ baseURL })`). + */ + baseUrl: string; + /** + * Credential for the LLM client constructor (e.g. `new OpenAI({ apiKey })` or `new Anthropic({ apiKey })`). The OpenAI / Anthropic client libraries turn this into the appropriate auth header (typically `Authorization: Bearer …`). Omitted only when the endpoint accepts unauthenticated requests (e.g. a local model server). + */ + apiKey?: string; + /** + * Static HTTP headers the caller must include on every outbound request. Does NOT include the `Authorization` header (the LLM client library adds that from `apiKey`) and does NOT include the `sessionToken` header (sent separately). + */ + headers: { + [k: string]: string | undefined; + }; + sessionToken?: ProviderSessionToken; +} +/** + * Short-lived, rotating credential the caller must send on every request, in addition to `apiKey` if one is present. Omitted when the endpoint does not require one. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderSessionToken". + */ +/** @experimental */ +export interface ProviderSessionToken { + /** + * The short-lived token value. + */ + token: string; + /** + * HTTP header name the token must be sent under. + */ + header: string; + /** + * The model the token is bound to, when applicable. When set, the token is only valid for requests against this model. + */ + model?: string; + /** + * When the token expires. Callers should refresh by calling `get` again before this time, or reactively on any 401/403 response from `baseUrl`. + */ + expiresAt: string; +} +/** + * Optional model identifier to scope the endpoint snapshot to. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointGetRequest". + */ +/** @experimental */ +export interface ProviderEndpointGetRequest { + /** + * Model identifier the caller intends to use against the returned endpoint. Used to pick the wire protocol (Anthropic Messages vs. OpenAI Responses vs. OpenAI Chat Completions) and to scope any returned `sessionToken` to that model. When omitted, the session's currently active model is used. Ignored when the session uses a custom provider that doesn't vary by model. + */ + modelId?: string; +} /** * File attachment * @@ -8553,9 +8668,21 @@ export interface ScheduleEntry { */ id: number; /** - * Interval between scheduled ticks, in milliseconds. + * Interval between scheduled ticks, in milliseconds (relative-interval schedules). + */ + intervalMs?: number; + /** + * 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. + */ + cron?: string; + /** + * IANA timezone the `cron` expression is evaluated in. + */ + tz?: string; + /** + * Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. */ - intervalMs: number; + at?: number; /** * Prompt text that gets enqueued on every tick. */ @@ -8722,6 +8849,32 @@ export interface SendResult { */ messageId: string; } +/** + * Agents discovered across user, project, plugin, and remote sources. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ServerAgentList". + */ +/** @experimental */ +export interface ServerAgentList { + /** + * All discovered agents across all sources + */ + agents: AgentInfo[]; +} +/** + * Instruction sources discovered across user, repository, and plugin sources. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ServerInstructionSourceList". + */ +/** @experimental */ +export interface ServerInstructionSourceList { + /** + * All discovered instruction sources + */ + sources: InstructionSource[]; +} /** * Schema for the `ServerSkill` type. * @@ -11112,9 +11265,13 @@ export interface TaskAgentInfo { */ result?: string; /** - * Model used for the task when specified + * Requested model override for the task when specified */ model?: string; + /** + * Runtime model resolved for the task when available + */ + resolvedModel?: string; executionMode?: TaskExecutionMode; /** * Whether the task is currently in the original sync wait and can be moved to background mode. False once it is already backgrounded, idle, finished, or no longer has a promotable sync waiter. @@ -12838,6 +12995,30 @@ export function createServerRpc(connection: MessageConnection) { discover: async (params: SkillsDiscoverRequest): Promise => connection.sendRequest("skills.discover", params), }, + /** @experimental */ + agents: { + /** + * Discovers custom agents across user, project, plugin, and remote sources. + * + * @param params Optional project paths to include in agent discovery. + * + * @returns Agents discovered across user, project, plugin, and remote sources. + */ + discover: async (params: AgentsDiscoverRequest): Promise => + connection.sendRequest("agents.discover", params), + }, + /** @experimental */ + instructions: { + /** + * Discovers instruction sources across user, repository, and plugin sources. + * + * @param params Optional project paths to include in instruction discovery. + * + * @returns Instruction sources discovered across user, repository, and plugin sources. + */ + discover: async (params: InstructionsDiscoverRequest): Promise => + connection.sendRequest("instructions.discover", params), + }, user: { settings: { /** @@ -13673,15 +13854,6 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ reload: async (): Promise => connection.sendRequest("session.mcp.reload", { sessionId }), - /** - * Reloads MCP server connections for the session with an explicit host-provided configuration. - * - * @param params Opaque MCP reload configuration. - * - * @returns MCP server startup filtering result. - */ - reloadWithConfig: async (params: McpReloadWithConfigRequest): Promise => - connection.sendRequest("session.mcp.reloadWithConfig", { sessionId, ...params }), /** * Runs an MCP sampling inference on behalf of an MCP server. * @@ -13716,29 +13888,6 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ removeGitHub: async (): Promise => connection.sendRequest("session.mcp.removeGitHub", { sessionId }), - /** - * Configures the built-in GitHub MCP server for the session's current auth context. - * - * @param params Opaque auth info used to configure GitHub MCP. - * - * @returns Result of configuring GitHub MCP. - */ - configureGitHub: async (params: McpConfigureGitHubRequest): Promise => - connection.sendRequest("session.mcp.configureGitHub", { sessionId, ...params }), - /** - * Starts an individual MCP server on the session's host. - * - * @param params Server name and opaque configuration for an individual MCP server start. - */ - startServer: async (params: McpStartServerRequest): Promise => - connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), - /** - * Restarts an individual MCP server on the session's host (stops then starts). - * - * @param params Server name and opaque configuration for an individual MCP server restart. - */ - restartServer: async (params: McpRestartServerRequest): Promise => - connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), /** * Stops an individual MCP server on the session's host. * @@ -13746,20 +13895,6 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ stopServer: async (params: McpStopServerRequest): Promise => connection.sendRequest("session.mcp.stopServer", { sessionId, ...params }), - /** - * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. - * - * @param params Registration parameters for an external MCP client. - */ - registerExternalClient: async (params: McpRegisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.registerExternalClient", { sessionId, ...params }), - /** - * Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. - * - * @param params Server name identifying the external client to remove. - */ - unregisterExternalClient: async (params: McpUnregisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.unregisterExternalClient", { sessionId, ...params }), /** * Checks whether a named MCP server is currently running on the session's host. * @@ -13771,15 +13906,6 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.mcp.isServerRunning", { sessionId, ...params }), /** @experimental */ oauth: { - /** - * Responds to a pending MCP OAuth provider request. Marked internal because the `provider` argument is an in-process OAuthClientProvider instance that cannot be carried over the wire; the public OAuth surface will route the response through a wire-clean handshake once the CLI moves on top of the SDK. - * - * @param params MCP OAuth request id and optional provider response. - * - * @returns Empty result after recording the MCP OAuth response. - */ - respond: async (params: McpOauthRespondRequest): Promise => - connection.sendRequest("session.mcp.oauth.respond", { sessionId, ...params }), /** * Starts OAuth authentication for a remote MCP server. * @@ -13862,6 +13988,18 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.plugins.reload", { sessionId, ...params }), }, /** @experimental */ + providerEndpoint: { + /** + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. May throw for sessions whose authentication scheme is not yet supported. + * + * @param params Optional model identifier to scope the endpoint snapshot to. + * + * @returns A snapshot of the provider endpoint the session is currently configured to talk to, with enough information for an external caller to make inference calls directly against the same backend using the OpenAI or Anthropic client libraries. + */ + get: async (params?: ProviderEndpointGetRequest): Promise => + connection.sendRequest("session.providerEndpoint.get", { sessionId, ...params }), + }, + /** @experimental */ options: { /** * Patches the genuinely-mutable subset of session options. @@ -14566,6 +14704,77 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin }; } +/** + * Create typed session-scoped RPC methods that are part of the SDK's internal + * surface. Not exported on the public client API. + * @internal + */ +export function createInternalSessionRpc(connection: MessageConnection, sessionId: string) { + return { + /** @experimental */ + mcp: { + /** + * Reloads MCP server connections for the session with an explicit host-provided configuration. + * + * @param params Opaque MCP reload configuration. + * + * @returns MCP server startup filtering result. + */ + reloadWithConfig: async (params: McpReloadWithConfigRequest): Promise => + connection.sendRequest("session.mcp.reloadWithConfig", { sessionId, ...params }), + /** + * Configures the built-in GitHub MCP server for the session's current auth context. + * + * @param params Opaque auth info used to configure GitHub MCP. + * + * @returns Result of configuring GitHub MCP. + */ + configureGitHub: async (params: McpConfigureGitHubRequest): Promise => + connection.sendRequest("session.mcp.configureGitHub", { sessionId, ...params }), + /** + * Starts an individual MCP server on the session's host. + * + * @param params Server name and opaque configuration for an individual MCP server start. + */ + startServer: async (params: McpStartServerRequest): Promise => + connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), + /** + * Restarts an individual MCP server on the session's host (stops then starts). + * + * @param params Server name and opaque configuration for an individual MCP server restart. + */ + restartServer: async (params: McpRestartServerRequest): Promise => + connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), + /** + * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. + * + * @param params Registration parameters for an external MCP client. + */ + registerExternalClient: async (params: McpRegisterExternalClientRequest): Promise => + connection.sendRequest("session.mcp.registerExternalClient", { sessionId, ...params }), + /** + * Unregisters a previously registered external MCP client by server name. Marked internal as the paired companion of `registerExternalClient`: only in-process callers that registered a client this way can meaningfully unregister it. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. + * + * @param params Server name identifying the external client to remove. + */ + unregisterExternalClient: async (params: McpUnregisterExternalClientRequest): Promise => + connection.sendRequest("session.mcp.unregisterExternalClient", { sessionId, ...params }), + /** @experimental */ + oauth: { + /** + * Responds to a pending MCP OAuth provider request. Marked internal because the `provider` argument is an in-process OAuthClientProvider instance that cannot be carried over the wire; the public OAuth surface will route the response through a wire-clean handshake once the CLI moves on top of the SDK. + * + * @param params MCP OAuth request id and optional provider response. + * + * @returns Empty result after recording the MCP OAuth response. + */ + respond: async (params: McpOauthRespondRequest): Promise => + connection.sendRequest("session.mcp.oauth.respond", { sessionId, ...params }), + }, + }, + }; +} + /** Handler for `sessionFs` client session API methods. */ /** @experimental */ export interface SessionFsHandler { diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 77a354cb6..a4fba8f33 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -715,6 +715,10 @@ export interface ResumeData { * Total number of persisted events in the session at the time of resume */ eventCount: number; + /** + * On-disk byte size of the session's persisted events.jsonl file at resume time; omitted when the file does not exist or cannot be stat'd + */ + eventsFileSizeBytes?: number; /** * Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @@ -959,6 +963,14 @@ export interface ScheduleCreatedEvent { * Scheduled prompt registered via /every or /after */ export interface ScheduleCreatedData { + /** + * Absolute fire time (epoch milliseconds) for a one-shot calendar schedule + */ + at?: number; + /** + * 5-field cron expression for a recurring calendar schedule, evaluated in `tz` + */ + cron?: string; /** * Optional user-facing label shown in the timeline instead of the actual prompt (e.g. `/skill-name args` when the prompt is a skill invocation expansion) */ @@ -968,9 +980,9 @@ export interface ScheduleCreatedData { */ id: number; /** - * Interval between ticks in milliseconds + * Interval between ticks in milliseconds (relative-interval schedules) */ - intervalMs: number; + intervalMs?: number; /** * Prompt text that gets enqueued on every tick */ @@ -979,6 +991,10 @@ export interface ScheduleCreatedData { * Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`) */ recurring?: boolean; + /** + * IANA timezone the `cron` expression is evaluated in + */ + tz?: string; } /** * Session event "session.schedule_cancelled". Scheduled prompt cancelled from the schedule manager dialog @@ -1610,6 +1626,10 @@ export interface ShutdownData { * Error description when shutdownType is "error" */ errorReason?: string; + /** + * On-disk byte size of the session's persisted events.jsonl file at shutdown time; omitted when the file does not exist or cannot be stat'd + */ + eventsFileSizeBytes?: number; /** * Per-model usage breakdown, keyed by model identifier */ @@ -4271,6 +4291,10 @@ export interface HookProgressData { * Human-readable progress message from the hook process */ message: string; + /** + * When true, this status message replaces the previous temporary one instead of accumulating + */ + temporary?: boolean; } /** * Session event "system.message". System/developer instruction content with role and optional template metadata diff --git a/nodejs/test/e2e/provider_endpoint.e2e.test.ts b/nodejs/test/e2e/provider_endpoint.e2e.test.ts new file mode 100644 index 000000000..b21124c83 --- /dev/null +++ b/nodejs/test/e2e/provider_endpoint.e2e.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("session.providerEndpoint.get RPC", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("returns the BYOK provider endpoint when a custom provider is configured", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + provider: { + type: "openai", + wireApi: "completions", + baseUrl: "https://api.example.test/v1", + apiKey: "byok-secret", + headers: { "X-Custom-Header": "byok-yes" }, + }, + }); + + try { + const endpoint = await session.rpc.providerEndpoint.get({}); + + expect(endpoint.protocol).toBe("openai-completions"); + expect(endpoint.baseUrl).toBe("https://api.example.test/v1"); + expect(endpoint.apiKey).toBe("byok-secret"); + expect(endpoint.headers).toMatchObject({ "X-Custom-Header": "byok-yes" }); + // Auth and per-request session-token headers should not appear here. + expect(endpoint.headers).not.toHaveProperty("Authorization"); + expect(endpoint.headers).not.toHaveProperty("Copilot-Session-Token"); + // BYOK sessions never issue a CAPI session token. + expect(endpoint.sessionToken).toBeUndefined(); + } finally { + try { + await session.disconnect(); + } catch { + // disconnect may fail since the BYOK provider URL is fake + } + } + }); + + it("returns the CAPI provider endpoint for an OAuth-authenticated session", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + }); + + try { + const endpoint = await session.rpc.providerEndpoint.get({}); + + // Wire protocol defaults to openai-completions when no model picks + // an alternative endpoint set. + expect(["openai-completions", "openai-responses", "anthropic"]).toContain(endpoint.protocol); + + // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness. + expect(endpoint.baseUrl).toMatch(/^https?:\/\//); + + // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer. + expect(endpoint.apiKey).toBeTypeOf("string"); + expect(endpoint.apiKey!.length).toBeGreaterThan(0); + + // Standard CAPI headers should be present, and the Authorization / + // session-token headers must not be in `headers` (they're carried + // by `apiKey` and `sessionToken` respectively). + expect(endpoint.headers["Copilot-Integration-Id"]).toBeTypeOf("string"); + expect(endpoint.headers["User-Agent"]).toMatch(/Copilot/i); + expect(endpoint.headers["X-GitHub-Api-Version"]).toBeTypeOf("string"); + expect(endpoint.headers["X-Interaction-Id"]).toMatch(/[0-9a-f-]{8,}/); + expect(endpoint.headers).not.toHaveProperty("Authorization"); + expect(endpoint.headers).not.toHaveProperty("Copilot-Session-Token"); + + // If a session token came back, it must use the documented header + // name and an ISO 8601 expiry. The harness proxy may decline to + // issue one — in that case the field is simply omitted. + if (endpoint.sessionToken) { + expect(endpoint.sessionToken.header).toBe("Copilot-Session-Token"); + expect(endpoint.sessionToken.token.length).toBeGreaterThan(0); + expect(Date.parse(endpoint.sessionToken.expiresAt)).not.toBeNaN(); + } + } finally { + await session.disconnect(); + } + }); +}); From 8f658defc3e813c4b6d340e72f51e5305d627e76 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 11 Jun 2026 12:18:45 +0100 Subject: [PATCH 2/5] Update e2e test for renamed provider.getEndpoint API Runtime renamed session.providerEndpoint.get -> session.provider.getEndpoint and split the wire protocol into separate type+wireApi fields. Regenerated rpc.ts/session-events.ts against the runtime schema and updated the test assertions to match. Both BYOK and CAPI cases pass against the real harness CapiProxy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/generated/rpc.ts | 77 +++++++++++++------ nodejs/src/generated/session-events.ts | 28 +++---- nodejs/test/e2e/provider_endpoint.e2e.test.ts | 28 ++++--- 3 files changed, 86 insertions(+), 47 deletions(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index c45c8ccba..c1120c1b7 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -1012,19 +1012,31 @@ export type ProviderConfigWireApi = /** OpenAI Responses API wire format. */ | "responses"; /** - * Wire protocol the caller must speak to `baseUrl`. Tells the caller which LLM client library to instantiate. + * Provider family. Matches the `type` field of a BYOK provider config. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderWireProtocol". + * via the `definition` "ProviderEndpointType". */ /** @experimental */ -export type ProviderWireProtocol = - /** OpenAI Chat Completions wire format (`/chat/completions`). Use the `openai` client library. */ - | "openai-completions" - /** OpenAI Responses wire format (`/responses`). Use the `openai` client library. */ - | "openai-responses" - /** Anthropic Messages wire format (`/v1/messages`). Use the `@anthropic-ai/sdk` client library. */ +export type ProviderEndpointType = + /** OpenAI-compatible endpoint (use the OpenAI client library). */ + | "openai" + /** Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). */ + | "azure" + /** Anthropic endpoint (use the Anthropic client library). */ | "anthropic"; +/** + * Wire API to be used, when required for the provider type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointWireApi". + */ +/** @experimental */ +export type ProviderEndpointWireApi = + /** Classic chat-completions request shape. */ + | "completions" + /** Newer responses request shape. */ + | "responses"; /** * Schema for the `PushAttachment` type. * @@ -2854,6 +2866,10 @@ export interface SlashCommandInfo { * Whether the command is experimental */ experimental?: boolean; + /** + * Whether the command may be the target of `/every` / `/after` schedules. Resolution happens at every tick, so only set this when the command is safe to re-invoke and produces an agent prompt. + */ + schedulable?: boolean; } /** * Optional unstructured input hint @@ -5397,6 +5413,19 @@ export interface McpUnregisterExternalClientRequest { */ serverName: string; } +/** + * Memory configuration for this session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "MemoryConfiguration". + */ +/** @experimental */ +export interface MemoryConfiguration { + /** + * Whether memory is enabled for the session. + */ + enabled: boolean; +} /** * Model identifier and token limits used to compute the context-info breakdown. * @@ -7824,24 +7853,25 @@ export interface ProviderConfigAzure { apiVersion?: string; } /** - * A snapshot of the provider endpoint the session is currently configured to talk to, with enough information for an external caller to make inference calls directly against the same backend using the OpenAI or Anthropic client libraries. + * A snapshot of the provider endpoint the session is currently configured to talk to. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "ProviderEndpoint". */ /** @experimental */ export interface ProviderEndpoint { - protocol: ProviderWireProtocol; + type: ProviderEndpointType; + wireApi?: ProviderEndpointWireApi; /** - * Base URL the caller should pass to the LLM client library (e.g. `new OpenAI({ baseURL })` or `new Anthropic({ baseURL })`). + * Base URL to pass to the LLM client library. */ baseUrl: string; /** - * Credential for the LLM client constructor (e.g. `new OpenAI({ apiKey })` or `new Anthropic({ apiKey })`). The OpenAI / Anthropic client libraries turn this into the appropriate auth header (typically `Authorization: Bearer …`). Omitted only when the endpoint accepts unauthenticated requests (e.g. a local model server). + * Long-lived credential to pass to the LLM client. Omitted only when the endpoint accepts unauthenticated requests. */ apiKey?: string; /** - * Static HTTP headers the caller must include on every outbound request. Does NOT include the `Authorization` header (the LLM client library adds that from `apiKey`) and does NOT include the `sessionToken` header (sent separately). + * HTTP headers the caller must include on every outbound request. Does NOT include the `Authorization` header (the LLM client library adds that from `apiKey`) and does NOT include the `sessionToken` header (sent separately). */ headers: { [k: string]: string | undefined; @@ -7869,20 +7899,20 @@ export interface ProviderSessionToken { */ model?: string; /** - * When the token expires. Callers should refresh by calling `get` again before this time, or reactively on any 401/403 response from `baseUrl`. + * When the token expires, if known. Callers should refresh by calling `getEndpoint` again before this time, or reactively on any 401/403 response from `baseUrl`. */ - expiresAt: string; + expiresAt?: string; } /** * Optional model identifier to scope the endpoint snapshot to. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointGetRequest". + * via the `definition` "ProviderGetEndpointRequest". */ /** @experimental */ -export interface ProviderEndpointGetRequest { +export interface ProviderGetEndpointRequest { /** - * Model identifier the caller intends to use against the returned endpoint. Used to pick the wire protocol (Anthropic Messages vs. OpenAI Responses vs. OpenAI Chat Completions) and to scope any returned `sessionToken` to that model. When omitted, the session's currently active model is used. Ignored when the session uses a custom provider that doesn't vary by model. + * Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using — that path also surfaces an auto-mode `sessionToken` when applicable, preserving auto-mode billing. When `modelId` is supplied, the response never includes a `sessionToken` and the caller must authorize requests with `apiKey` alone. */ modelId?: string; } @@ -9813,6 +9843,7 @@ export interface SessionOpenOptions { * @experimental */ additionalContentExclusionPolicies?: SessionOpenOptionsAdditionalContentExclusionPolicy[]; + memory?: MemoryConfiguration; /** * Capabilities enabled for this session. */ @@ -13988,16 +14019,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.plugins.reload", { sessionId, ...params }), }, /** @experimental */ - providerEndpoint: { + provider: { /** - * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. May throw for sessions whose authentication scheme is not yet supported. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. * * @param params Optional model identifier to scope the endpoint snapshot to. * - * @returns A snapshot of the provider endpoint the session is currently configured to talk to, with enough information for an external caller to make inference calls directly against the same backend using the OpenAI or Anthropic client libraries. + * @returns A snapshot of the provider endpoint the session is currently configured to talk to. */ - get: async (params?: ProviderEndpointGetRequest): Promise => - connection.sendRequest("session.providerEndpoint.get", { sessionId, ...params }), + getEndpoint: async (params?: ProviderGetEndpointRequest): Promise => + connection.sendRequest("session.provider.getEndpoint", { sessionId, ...params }), }, /** @experimental */ options: { diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index a4fba8f33..e219371d3 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -2653,18 +2653,6 @@ export interface AssistantMessageEvent { * Assistant response containing text content, optional tool requests, and interaction metadata */ export interface AssistantMessageData { - /** - * Raw Anthropic content array with advisor blocks (server_tool_use, advisor_tool_result) for verbatim round-tripping - * - * @experimental - */ - anthropicAdvisorBlocks?: unknown[]; - /** - * Anthropic advisor model ID used for this response, for timeline display on replay - * - * @experimental - */ - anthropicAdvisorModel?: string; /** * Provider's completion / response identifier; shared across all chunks of a single API call. Used to group multi-chunk assistant utterances. */ @@ -2714,6 +2702,7 @@ export interface AssistantMessageData { * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ requestId?: string; + serverTools?: AssistantMessageServerTools; /** * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation */ @@ -2727,6 +2716,19 @@ export interface AssistantMessageData { */ turnId?: string; } +/** + * Neutral provider-tagged server-side tool-use payload (tool search, advisor) for verbatim round-tripping + */ +/** @experimental */ +export interface AssistantMessageServerTools { + advisorModel?: string; + functionCallNamespaces?: { + [k: string]: string | undefined; + }; + items?: unknown[]; + provider: string; + rawContentBlocks?: unknown[]; +} /** * A tool invocation request from the assistant */ @@ -3862,7 +3864,7 @@ export interface SkillInvokedData { */ pluginVersion?: string; /** - * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), personal-claude (~/.claude/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) */ source?: string; trigger?: SkillInvokedTrigger; diff --git a/nodejs/test/e2e/provider_endpoint.e2e.test.ts b/nodejs/test/e2e/provider_endpoint.e2e.test.ts index b21124c83..ee74117d4 100644 --- a/nodejs/test/e2e/provider_endpoint.e2e.test.ts +++ b/nodejs/test/e2e/provider_endpoint.e2e.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; -describe("session.providerEndpoint.get RPC", async () => { +describe("session.provider.getEndpoint RPC", async () => { const { copilotClient: client } = await createSdkTestContext(); it("returns the BYOK provider endpoint when a custom provider is configured", async () => { @@ -22,9 +22,10 @@ describe("session.providerEndpoint.get RPC", async () => { }); try { - const endpoint = await session.rpc.providerEndpoint.get({}); + const endpoint = await session.rpc.provider.getEndpoint({}); - expect(endpoint.protocol).toBe("openai-completions"); + expect(endpoint.type).toBe("openai"); + expect(endpoint.wireApi).toBe("completions"); expect(endpoint.baseUrl).toBe("https://api.example.test/v1"); expect(endpoint.apiKey).toBe("byok-secret"); expect(endpoint.headers).toMatchObject({ "X-Custom-Header": "byok-yes" }); @@ -48,11 +49,13 @@ describe("session.providerEndpoint.get RPC", async () => { }); try { - const endpoint = await session.rpc.providerEndpoint.get({}); + const endpoint = await session.rpc.provider.getEndpoint({}); - // Wire protocol defaults to openai-completions when no model picks - // an alternative endpoint set. - expect(["openai-completions", "openai-responses", "anthropic"]).toContain(endpoint.protocol); + expect(["openai", "azure", "anthropic"]).toContain(endpoint.type); + // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes. + if (endpoint.type !== "anthropic") { + expect(["completions", "responses"]).toContain(endpoint.wireApi); + } // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness. expect(endpoint.baseUrl).toMatch(/^https?:\/\//); @@ -71,13 +74,16 @@ describe("session.providerEndpoint.get RPC", async () => { expect(endpoint.headers).not.toHaveProperty("Authorization"); expect(endpoint.headers).not.toHaveProperty("Copilot-Session-Token"); - // If a session token came back, it must use the documented header - // name and an ISO 8601 expiry. The harness proxy may decline to - // issue one — in that case the field is simply omitted. + // When the omit-modelId path returned an auto-mode session token, it + // must use the documented header name and an ISO 8601 expiry. The + // harness may have a non-auto model selected, in which case the + // field is simply omitted. if (endpoint.sessionToken) { expect(endpoint.sessionToken.header).toBe("Copilot-Session-Token"); expect(endpoint.sessionToken.token.length).toBeGreaterThan(0); - expect(Date.parse(endpoint.sessionToken.expiresAt)).not.toBeNaN(); + if (endpoint.sessionToken.expiresAt !== undefined) { + expect(Date.parse(endpoint.sessionToken.expiresAt)).not.toBeNaN(); + } } } finally { await session.disconnect(); From b7579b9c864cae0a788da5603780fd449979e8e9 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 11 Jun 2026 12:24:19 +0100 Subject: [PATCH 3/5] Pass COPILOT_ALLOW_GET_PROVIDER_ENDPOINT through the harness env The API is gated by env var, so set it on the harness env object (which is the same one passed to the CLI subprocess) instead of requiring callers to export it in their own environment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/test/e2e/provider_endpoint.e2e.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nodejs/test/e2e/provider_endpoint.e2e.test.ts b/nodejs/test/e2e/provider_endpoint.e2e.test.ts index ee74117d4..e31984793 100644 --- a/nodejs/test/e2e/provider_endpoint.e2e.test.ts +++ b/nodejs/test/e2e/provider_endpoint.e2e.test.ts @@ -7,7 +7,12 @@ import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("session.provider.getEndpoint RPC", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, env } = await createSdkTestContext(); + + // The provider endpoint API is gated behind an opt-in env var; the harness + // env object is the same one passed to the CLI subprocess, so mutating it + // here enables the API for this test file's client. + env.COPILOT_ALLOW_GET_PROVIDER_ENDPOINT = "true"; it("returns the BYOK provider endpoint when a custom provider is configured", async () => { const session = await client.createSession({ From 16cb006da8e6ea1e79e51a01d3a3488a457b0a03 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 11 Jun 2026 13:18:42 +0100 Subject: [PATCH 4/5] Update e2e for Authorization pass-through in provider endpoint The runtime now surfaces Authorization in the headers map (consistent with BYOK pass-through) rather than stripping it. Update the e2e to assert the new contract and regen the RPC bindings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/generated/rpc.ts | 4 ++-- nodejs/test/e2e/provider_endpoint.e2e.test.ts | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index c1120c1b7..d71e64134 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -7871,7 +7871,7 @@ export interface ProviderEndpoint { */ apiKey?: string; /** - * HTTP headers the caller must include on every outbound request. Does NOT include the `Authorization` header (the LLM client library adds that from `apiKey`) and does NOT include the `sessionToken` header (sent separately). + * HTTP headers the caller must include on every outbound request. */ headers: { [k: string]: string | undefined; @@ -7912,7 +7912,7 @@ export interface ProviderSessionToken { /** @experimental */ export interface ProviderGetEndpointRequest { /** - * Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using — that path also surfaces an auto-mode `sessionToken` when applicable, preserving auto-mode billing. When `modelId` is supplied, the response never includes a `sessionToken` and the caller must authorize requests with `apiKey` alone. + * Model identifier the caller intends to use against the returned endpoint. Used to pick the correct wire shape. Omit to use whichever model the session is currently using. */ modelId?: string; } diff --git a/nodejs/test/e2e/provider_endpoint.e2e.test.ts b/nodejs/test/e2e/provider_endpoint.e2e.test.ts index e31984793..1bac76253 100644 --- a/nodejs/test/e2e/provider_endpoint.e2e.test.ts +++ b/nodejs/test/e2e/provider_endpoint.e2e.test.ts @@ -34,9 +34,6 @@ describe("session.provider.getEndpoint RPC", async () => { expect(endpoint.baseUrl).toBe("https://api.example.test/v1"); expect(endpoint.apiKey).toBe("byok-secret"); expect(endpoint.headers).toMatchObject({ "X-Custom-Header": "byok-yes" }); - // Auth and per-request session-token headers should not appear here. - expect(endpoint.headers).not.toHaveProperty("Authorization"); - expect(endpoint.headers).not.toHaveProperty("Copilot-Session-Token"); // BYOK sessions never issue a CAPI session token. expect(endpoint.sessionToken).toBeUndefined(); } finally { @@ -69,15 +66,13 @@ describe("session.provider.getEndpoint RPC", async () => { expect(endpoint.apiKey).toBeTypeOf("string"); expect(endpoint.apiKey!.length).toBeGreaterThan(0); - // Standard CAPI headers should be present, and the Authorization / - // session-token headers must not be in `headers` (they're carried - // by `apiKey` and `sessionToken` respectively). + // Standard CAPI headers should be present, and Authorization is + // surfaced as the runtime sends it (`Bearer `). expect(endpoint.headers["Copilot-Integration-Id"]).toBeTypeOf("string"); expect(endpoint.headers["User-Agent"]).toMatch(/Copilot/i); expect(endpoint.headers["X-GitHub-Api-Version"]).toBeTypeOf("string"); expect(endpoint.headers["X-Interaction-Id"]).toMatch(/[0-9a-f-]{8,}/); - expect(endpoint.headers).not.toHaveProperty("Authorization"); - expect(endpoint.headers).not.toHaveProperty("Copilot-Session-Token"); + expect(endpoint.headers.Authorization).toBe(`Bearer ${endpoint.apiKey}`); // When the omit-modelId path returned an auto-mode session token, it // must use the documented header name and an ISO 8601 expiry. The From 75820af425e5507c7385816e3621bccdf71404e0 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 11 Jun 2026 18:21:07 +0100 Subject: [PATCH 5/5] Regen SDK bindings for trimmed apiKey description Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/generated/rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index d71e64134..7cca2fa10 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -7867,7 +7867,7 @@ export interface ProviderEndpoint { */ baseUrl: string; /** - * Long-lived credential to pass to the LLM client. Omitted only when the endpoint accepts unauthenticated requests. + * A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests. */ apiKey?: string; /**