@alvin0/ai-agent-sdk-provider-copilot 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/index.d.ts +1297 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2680 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
import { AgentSdkError, ModelInvocationContext, RetryPolicyConfig, SafeErrorRecord } from "@alvin0/ai-agent-sdk-core";
|
|
2
|
+
import { ComposableModelProviderPlugin, CredentialOperationOptions, CredentialStore, ModelTarget } from "@alvin0/ai-agent-sdk-core/provider";
|
|
3
|
+
import { HttpModelAdapter, ProtocolRequest, ProtocolSseEvent, ProtocolStreamChunk, ProviderCatalogModel, ProviderRequestLogger, RuntimeModelDiscoveryContext, RuntimeWireProtocol } from "@alvin0/ai-agent-sdk-provider-http";
|
|
4
|
+
import { ChatCompletionsDialect } from "@alvin0/ai-agent-sdk-protocol-openai-chat-completions";
|
|
5
|
+
import { ResponsesDialect } from "@alvin0/ai-agent-sdk-protocol-responses";
|
|
6
|
+
//#region src/catalog.d.ts
|
|
7
|
+
/** Path of the catalog surface, relative to the pinned Copilot base URL. */
|
|
8
|
+
declare const COPILOT_CATALOG_PATH = "/models";
|
|
9
|
+
/** Maximum raw catalog bytes when the caller configures none. */
|
|
10
|
+
declare const COPILOT_DEFAULT_MAX_CATALOG_BYTES: number;
|
|
11
|
+
/** Maximum catalog entries accepted when the caller configures none. */
|
|
12
|
+
declare const COPILOT_DEFAULT_MAX_CATALOG_MODELS = 2048;
|
|
13
|
+
/** Maximum catalog response chunks accepted when the caller configures none. */
|
|
14
|
+
declare const COPILOT_DEFAULT_MAX_CATALOG_CHUNKS = 10000;
|
|
15
|
+
/** Catalog request deadline when the caller configures none. */
|
|
16
|
+
declare const COPILOT_DEFAULT_CATALOG_TIMEOUT_MS = 30000;
|
|
17
|
+
/**
|
|
18
|
+
* Which endpoint a generation model is dispatched to.
|
|
19
|
+
*
|
|
20
|
+
* Declared HERE rather than in `./router.ts`, where the router's own types live,
|
|
21
|
+
* for one structural reason: `./router.ts` imports {@link CopilotGenerationModel}
|
|
22
|
+
* from this module, so the dependency edge already runs router → catalog. Putting
|
|
23
|
+
* the endpoint union in the router would make it run both ways, which the repo's
|
|
24
|
+
* source-ownership check forbids and which nothing here needs. `./router.ts`
|
|
25
|
+
* re-exports this type, so the router remains the module a reader goes to for
|
|
26
|
+
* endpoint selection.
|
|
27
|
+
*/
|
|
28
|
+
type CopilotEndpoint = 'responses' | 'chat-completions';
|
|
29
|
+
/** Why an entry was left out of both catalogs. */
|
|
30
|
+
type CopilotOmitReason =
|
|
31
|
+
/** capabilities.type is not one of the recognized values. */
|
|
32
|
+
'capability-type-unrecognized' |
|
|
33
|
+
/** No usable id. */
|
|
34
|
+
'model-id-missing';
|
|
35
|
+
/** One entry that was dropped, with the reason an operator needs to see it. */
|
|
36
|
+
interface CopilotOmittedModel {
|
|
37
|
+
/** The entry's id, or `''` when it had none — the reason says which. */
|
|
38
|
+
readonly id: string;
|
|
39
|
+
/** Why it was dropped. */
|
|
40
|
+
readonly reason: CopilotOmitReason;
|
|
41
|
+
}
|
|
42
|
+
/** A generation model, plus whatever the catalog disclosed about its endpoint. */
|
|
43
|
+
interface CopilotGenerationModel {
|
|
44
|
+
/** The SDK catalog model, handed to `provider-http` unchanged. */
|
|
45
|
+
readonly model: ProviderCatalogModel;
|
|
46
|
+
/**
|
|
47
|
+
* The endpoint the catalog disclosed, when it disclosed one.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` means UNKNOWN, not "not supported". The router handles those two
|
|
50
|
+
* states differently (Requirement 8.6).
|
|
51
|
+
*/
|
|
52
|
+
readonly declaredEndpoint: CopilotEndpoint | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* An embedding model, carrying only the facts the catalog stated.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately NOT a {@link ProviderCatalogModel}: an embedding model has no
|
|
58
|
+
* context window or output cap to report, and `Copilot_Embedding_Adapter` needs
|
|
59
|
+
* different facts (batch ceiling, whether a requested dimension count is
|
|
60
|
+
* honoured). Every field but `id` is optional because every one of them is absent
|
|
61
|
+
* from some real entry.
|
|
62
|
+
*/
|
|
63
|
+
interface CopilotEmbeddingModel {
|
|
64
|
+
/** Wire model id, passed to the endpoint verbatim. */
|
|
65
|
+
readonly id: string;
|
|
66
|
+
/** Display label, when the catalog supplied one. */
|
|
67
|
+
readonly name?: string;
|
|
68
|
+
/** Model family, when disclosed; embedding compatibility identity is derived from it. */
|
|
69
|
+
readonly family?: string;
|
|
70
|
+
/** Token ceiling for one input, from `limits.max_context_window_tokens`. */
|
|
71
|
+
readonly maxInputTokens?: number;
|
|
72
|
+
/** Ceiling on inputs per request, from `limits.max_inputs`. */
|
|
73
|
+
readonly maxInputs?: number;
|
|
74
|
+
/** Whether `supports.dimensions` was stated, and what it said. */
|
|
75
|
+
readonly supportsDimensions?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** The result of one discovery, partitioned. */
|
|
78
|
+
interface CopilotCatalogSnapshot {
|
|
79
|
+
/** Models usable for generation, each with its preliminary endpoint disclosure. */
|
|
80
|
+
readonly generation: readonly CopilotGenerationModel[];
|
|
81
|
+
/** Models usable for embedding. */
|
|
82
|
+
readonly embedding: readonly CopilotEmbeddingModel[];
|
|
83
|
+
/** Dropped entries with their reasons — these go to observation, not to a catalog. */
|
|
84
|
+
readonly omitted: readonly CopilotOmittedModel[];
|
|
85
|
+
}
|
|
86
|
+
/** Resolved bounds for one catalog read. Every field is a bound, never "unlimited". */
|
|
87
|
+
interface CopilotCatalogLimits {
|
|
88
|
+
/** Maximum raw response bytes. */
|
|
89
|
+
readonly maxBytes: number;
|
|
90
|
+
/** Maximum entries accepted before the response is called malformed. */
|
|
91
|
+
readonly maxModels: number;
|
|
92
|
+
/** Maximum response chunks. */
|
|
93
|
+
readonly maxChunks: number;
|
|
94
|
+
/** Deadline for the catalog request AND its body read. */
|
|
95
|
+
readonly timeoutMs: number;
|
|
96
|
+
/** Permit an `http:` base URL for a trusted local test endpoint. */
|
|
97
|
+
readonly allowInsecureHttp?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The caller-facing catalog options, in the spelling `CopilotProviderOptions` uses.
|
|
101
|
+
*
|
|
102
|
+
* Split into two groups on purpose. The four `max*`/`timeout` values bound ONE
|
|
103
|
+
* read and are resolved here by {@link resolveCopilotCatalogLimits}. The three
|
|
104
|
+
* cache values (TTL, stale TTL, failure backoff) bound how often reads happen at
|
|
105
|
+
* all, and `provider-http` already owns that policy — {@link
|
|
106
|
+
* copilotCatalogCacheOptions} forwards them without a default, so an unset option
|
|
107
|
+
* keeps the runtime's own default instead of this package pinning a second one
|
|
108
|
+
* (Requirement 8.7).
|
|
109
|
+
*/
|
|
110
|
+
interface CopilotCatalogOptions {
|
|
111
|
+
/** Maximum raw catalog bytes. Defaults to {@link COPILOT_DEFAULT_MAX_CATALOG_BYTES}. */
|
|
112
|
+
readonly maxCatalogBytes?: number;
|
|
113
|
+
/** Maximum catalog entries. Defaults to {@link COPILOT_DEFAULT_MAX_CATALOG_MODELS}. */
|
|
114
|
+
readonly maxCatalogModels?: number;
|
|
115
|
+
/** Maximum catalog response chunks. Defaults to {@link COPILOT_DEFAULT_MAX_CATALOG_CHUNKS}. */
|
|
116
|
+
readonly maxCatalogChunks?: number;
|
|
117
|
+
/** Catalog request deadline. Defaults to {@link COPILOT_DEFAULT_CATALOG_TIMEOUT_MS}. */
|
|
118
|
+
readonly catalogTimeoutMs?: number;
|
|
119
|
+
/** How long a discovered catalog stays fresh. */
|
|
120
|
+
readonly catalogTtlMs?: number;
|
|
121
|
+
/** How long a stale catalog may still be served while a refresh is attempted. */
|
|
122
|
+
readonly catalogStaleTtlMs?: number;
|
|
123
|
+
/** How long to wait before retrying discovery after it failed. */
|
|
124
|
+
readonly catalogFailureBackoffMs?: number;
|
|
125
|
+
/** Permit an `http:` base URL for a trusted local test endpoint. */
|
|
126
|
+
readonly allowInsecureHttp?: boolean;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the per-read bounds, rejecting a value that cannot bound anything.
|
|
130
|
+
*
|
|
131
|
+
* Validation happens here rather than at the read, so a `0` or a `NaN` in the
|
|
132
|
+
* configuration is a construction-time error instead of a silently disabled limit
|
|
133
|
+
* discovered under load (Requirement 8.2).
|
|
134
|
+
* @param options - the caller's catalog options.
|
|
135
|
+
* @returns the four resolved bounds plus the insecure-HTTP opt-in.
|
|
136
|
+
* @throws RangeError when a configured bound is not a positive safe integer.
|
|
137
|
+
*/
|
|
138
|
+
declare function resolveCopilotCatalogLimits(options?: CopilotCatalogOptions): CopilotCatalogLimits;
|
|
139
|
+
/**
|
|
140
|
+
* Forward the three cache-policy options, and only the ones that were set.
|
|
141
|
+
*
|
|
142
|
+
* A conditional spread rather than defaults: `provider-http` owns catalog caching,
|
|
143
|
+
* and a default written here would override the runtime's own without anyone
|
|
144
|
+
* asking for it (Requirement 8.7).
|
|
145
|
+
* @param options - the caller's catalog options.
|
|
146
|
+
* @returns an object carrying only the cache options the caller supplied.
|
|
147
|
+
*/
|
|
148
|
+
declare function copilotCatalogCacheOptions(options?: CopilotCatalogOptions): {
|
|
149
|
+
readonly catalogTtlMs?: number;
|
|
150
|
+
readonly catalogStaleTtlMs?: number;
|
|
151
|
+
readonly catalogFailureBackoffMs?: number;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Read `GET {baseUrl}/models` and partition it.
|
|
155
|
+
*
|
|
156
|
+
* The base URL is re-pinned here from `context.baseUrl` rather than trusted as a
|
|
157
|
+
* string: the catalog is the first Copilot call an adapter makes, and a pin
|
|
158
|
+
* compared before dispatch is the only check that runs before the resolved
|
|
159
|
+
* `Authorization` header leaves the process.
|
|
160
|
+
* @param context - the discovery context `provider-http` supplies: base URL,
|
|
161
|
+
* already-resolved headers, and the operation's signal.
|
|
162
|
+
* @param limits - bounds from {@link resolveCopilotCatalogLimits}.
|
|
163
|
+
* @param fetchImpl - HTTP implementation, injected for tests and non-browser runtimes.
|
|
164
|
+
* @returns the partitioned snapshot; an empty one when the endpoint answered a
|
|
165
|
+
* non-2xx status, because a catalog that could not be fetched is advisory too.
|
|
166
|
+
* @throws AgentSdkError with `COPILOT_REDIRECT_REJECTED` on any redirect shape, or
|
|
167
|
+
* `COPILOT_CATALOG_MALFORMED` when the response is the wrong shape structurally.
|
|
168
|
+
* @throws RangeError when the response exceeds a configured bound.
|
|
169
|
+
*/
|
|
170
|
+
declare function discoverCopilotModels(context: RuntimeModelDiscoveryContext, limits: CopilotCatalogLimits, fetchImpl: typeof globalThis.fetch): Promise<CopilotCatalogSnapshot>;
|
|
171
|
+
/**
|
|
172
|
+
* Partition an already-read catalog body.
|
|
173
|
+
*
|
|
174
|
+
* Exported separately from the fetch so the partition is testable — and readable —
|
|
175
|
+
* as what it is: a pure function from a parsed body to three lists.
|
|
176
|
+
* @param body - the parsed root object of the catalog response.
|
|
177
|
+
* @param maxModels - entry-count ceiling; exceeding it is structural, not per-entry.
|
|
178
|
+
* @returns the partitioned snapshot.
|
|
179
|
+
* @throws AgentSdkError with `COPILOT_CATALOG_MALFORMED` when `data` is not an
|
|
180
|
+
* array or holds more than `maxModels` entries.
|
|
181
|
+
*/
|
|
182
|
+
declare function partitionCopilotCatalog(body: Record<string, unknown>, maxModels: number): CopilotCatalogSnapshot;
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/common/identity.d.ts
|
|
185
|
+
/**
|
|
186
|
+
* The two Copilot endpoint constants and the editor-header override type, in the
|
|
187
|
+
* leaf layer so every module that has to speak to the Copilot surface can reach
|
|
188
|
+
* them.
|
|
189
|
+
*
|
|
190
|
+
* They BELONG to `../adapter.ts` — that module is the public door, documents the
|
|
191
|
+
* client-identity tradeoff, and re-exports everything here. The values live one
|
|
192
|
+
* layer down for the same structural reason `COPILOT_ERROR_CODES` does:
|
|
193
|
+
* `../catalog.ts` needs {@link COPILOT_BASE_URL} and `../exchange.ts` needs the
|
|
194
|
+
* two editor headers, while `../adapter.ts` builds the catalog reader and the
|
|
195
|
+
* token cache. Declaring the constants in `../adapter.ts` would make that edge
|
|
196
|
+
* run both ways, which the repo's circular-dependency check forbids. Copying the
|
|
197
|
+
* strings instead would be worse: a client identity that exists in two places is
|
|
198
|
+
* a client identity that can disagree with itself.
|
|
199
|
+
*
|
|
200
|
+
* Read `../adapter.ts` for what these values mean, why they are overridable
|
|
201
|
+
* options rather than hidden constants, and what is still outstanding on each.
|
|
202
|
+
*
|
|
203
|
+
* @module ai-agent-sdk/providers/copilot/identity
|
|
204
|
+
*/
|
|
205
|
+
/** The Copilot API base. Documented on the re-export in `../adapter.ts`. */
|
|
206
|
+
declare const COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
207
|
+
/** Default `Editor-Version`. Documented on the re-export in `../adapter.ts`. */
|
|
208
|
+
declare const COPILOT_EDITOR_VERSION = "vscode/1.99.0";
|
|
209
|
+
/** Default `Editor-Plugin-Version`. Documented on the re-export in `../adapter.ts`. */
|
|
210
|
+
declare const COPILOT_EDITOR_PLUGIN_VERSION = "copilot-chat/0.26.0";
|
|
211
|
+
/**
|
|
212
|
+
* Overrides for the two editor headers.
|
|
213
|
+
*
|
|
214
|
+
* Each field is independent: leaving one undefined keeps that header's exported
|
|
215
|
+
* default rather than dropping the header, because a dropped header is an HTTP
|
|
216
|
+
* 400 rather than a lenient request.
|
|
217
|
+
*/
|
|
218
|
+
interface CopilotEditorHeaders {
|
|
219
|
+
/** Overrides {@link COPILOT_EDITOR_VERSION}. */
|
|
220
|
+
readonly editorVersion?: string;
|
|
221
|
+
/** Overrides {@link COPILOT_EDITOR_PLUGIN_VERSION}. */
|
|
222
|
+
readonly editorPluginVersion?: string;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/common/store-types.d.ts
|
|
226
|
+
/**
|
|
227
|
+
* The long-lived GitHub user token, prefixed `ghu_`.
|
|
228
|
+
*
|
|
229
|
+
* It DOES NOT rotate when it is exchanged for a `Copilot_Api_Token`. That is the
|
|
230
|
+
* structural difference from Codex's single-use refresh token, and it is why the
|
|
231
|
+
* credential file is written exactly once at login and only read from then on.
|
|
232
|
+
*/
|
|
233
|
+
interface CopilotGitHubToken {
|
|
234
|
+
/** The token value. Never put this in an error message or a trace. */
|
|
235
|
+
readonly token: string;
|
|
236
|
+
/** Token type as declared by the endpoint, when present. Diagnostics only. */
|
|
237
|
+
readonly tokenType?: string;
|
|
238
|
+
/** Granted scope, when the endpoint discloses it. Diagnostics only. */
|
|
239
|
+
readonly scope?: string;
|
|
240
|
+
}
|
|
241
|
+
/** Session identity, limited to what the endpoint actually discloses. */
|
|
242
|
+
interface CopilotAccountIdentity {
|
|
243
|
+
readonly login?: string;
|
|
244
|
+
readonly id?: number;
|
|
245
|
+
readonly name?: string;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The persisted credential document.
|
|
249
|
+
*
|
|
250
|
+
* Four things are deliberately absent, each one a decision:
|
|
251
|
+
*
|
|
252
|
+
* - **No `Copilot_Api_Token`.** The short-lived token lives ~25 minutes;
|
|
253
|
+
* persisting it adds a write path and a second secret on disk while saving
|
|
254
|
+
* nothing, because the next process run almost always has to exchange again.
|
|
255
|
+
* - **No refresh-token field.** There is no refresh token in this model.
|
|
256
|
+
* `Copilot_Token_Exchange` does not consume the credential, so there is
|
|
257
|
+
* nothing to rotate.
|
|
258
|
+
* - **No `last_refresh`.** Codex needs it as a fallback when `exp` is
|
|
259
|
+
* unreadable. Copilot reads `expires_at` straight from the exchange response,
|
|
260
|
+
* and that response lives in memory rather than in this file, so a
|
|
261
|
+
* file-age fallback would have nothing to say.
|
|
262
|
+
* - **No API-key-equivalent config.** This surface rejects personal access
|
|
263
|
+
* tokens, so a "use an API key instead of OAuth" configuration has no
|
|
264
|
+
* situation to represent.
|
|
265
|
+
*/
|
|
266
|
+
interface CopilotAuthFile {
|
|
267
|
+
/**
|
|
268
|
+
* Structure version. Reading a file with an unknown version is an error, not
|
|
269
|
+
* an implicit migration.
|
|
270
|
+
*/
|
|
271
|
+
readonly version: 1;
|
|
272
|
+
readonly github: CopilotGitHubToken;
|
|
273
|
+
readonly account?: CopilotAccountIdentity;
|
|
274
|
+
/** The OAuth client id that minted this token; used to diagnose a 403. */
|
|
275
|
+
readonly clientId?: string;
|
|
276
|
+
/** Login time, ISO-8601. */
|
|
277
|
+
readonly obtainedAt?: string;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* @deprecated Read/write variant, kept only for symmetry with Codex. Normal
|
|
281
|
+
* runtime composition uses {@link CopilotCredentialStore}.
|
|
282
|
+
*/
|
|
283
|
+
interface CopilotAuthStore {
|
|
284
|
+
readonly location: string;
|
|
285
|
+
read(): Promise<CopilotAuthFile | undefined>;
|
|
286
|
+
write(file: CopilotAuthFile): Promise<void>;
|
|
287
|
+
}
|
|
288
|
+
/** Compare-and-swap variant used by normal runtime composition. */
|
|
289
|
+
type CopilotCredentialStore = CredentialStore<CopilotAuthFile>;
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/router.d.ts
|
|
292
|
+
/**
|
|
293
|
+
* Model id prefixes dispatched to `/responses` when the catalog says nothing.
|
|
294
|
+
*
|
|
295
|
+
* Exported and overridable for the same reason `COPILOT_EDITOR_VERSION` is: this
|
|
296
|
+
* is a fact about a remote endpoint that WILL go stale, and a user has to be able
|
|
297
|
+
* to correct it without waiting for a release.
|
|
298
|
+
* `CopilotProviderOptions.responsesModelPrefixes` ADDS to this list rather than
|
|
299
|
+
* replacing it, so an override cannot silently drop the prefixes shipped here.
|
|
300
|
+
*
|
|
301
|
+
* Kept deliberately short. A prefix that matches too much pushes models toward
|
|
302
|
+
* the endpoint where guessing wrong costs the request (see the module note), so
|
|
303
|
+
* an absent prefix is the cheaper error.
|
|
304
|
+
*/
|
|
305
|
+
declare const COPILOT_RESPONSES_MODEL_PREFIXES: readonly string[];
|
|
306
|
+
/** The endpoint chosen for one model id, and who chose it. */
|
|
307
|
+
interface CopilotEndpointDecision {
|
|
308
|
+
/** The model id the decision is keyed by, verbatim as the caller spelled it. */
|
|
309
|
+
readonly model: string;
|
|
310
|
+
/** The endpoint the request goes to. */
|
|
311
|
+
readonly endpoint: CopilotEndpoint;
|
|
312
|
+
/** Wire protocol id of the sub-protocol that serves {@link endpoint}. */
|
|
313
|
+
readonly protocolId: string;
|
|
314
|
+
/**
|
|
315
|
+
* Which step of the decision order produced this.
|
|
316
|
+
*
|
|
317
|
+
* Reported to observation (Requirement 9.8) so that when a model runs against
|
|
318
|
+
* the wrong endpoint, the log says who decided rather than leaving an operator
|
|
319
|
+
* to reconstruct it.
|
|
320
|
+
*/
|
|
321
|
+
readonly source: 'override' | 'catalog' | 'allowlist' | 'default';
|
|
322
|
+
}
|
|
323
|
+
/** Memoized, append-only endpoint selection for one adapter instance. */
|
|
324
|
+
interface CopilotEndpointRouter {
|
|
325
|
+
/**
|
|
326
|
+
* Decide the endpoint for a model id.
|
|
327
|
+
*
|
|
328
|
+
* MEMOIZED AND APPEND-ONLY: a key that already has a decision is returned
|
|
329
|
+
* unchanged and never recomputed. This is the mechanism that makes
|
|
330
|
+
* Requirement 9.7 hold — no code path can change its mind between two retries.
|
|
331
|
+
* @param modelId - the wire model id of the request being dispatched.
|
|
332
|
+
* @returns the decision for that model, recording it on first sight.
|
|
333
|
+
*/
|
|
334
|
+
decide(modelId: string): CopilotEndpointDecision;
|
|
335
|
+
/**
|
|
336
|
+
* Feed in discovered catalog metadata.
|
|
337
|
+
*
|
|
338
|
+
* Adds ONLY keys that have no decision yet; an id already decided is skipped
|
|
339
|
+
* even when the metadata now disagrees with the recorded decision.
|
|
340
|
+
* @param models - the generation half of a {@link CopilotGenerationModel} list.
|
|
341
|
+
*/
|
|
342
|
+
learn(models: readonly CopilotGenerationModel[]): void;
|
|
343
|
+
/**
|
|
344
|
+
* Every decision recorded so far, in the order it was recorded.
|
|
345
|
+
* @returns a frozen snapshot, for `--models` and for the conformance harness.
|
|
346
|
+
*/
|
|
347
|
+
snapshot(): readonly CopilotEndpointDecision[];
|
|
348
|
+
}
|
|
349
|
+
/** Construction options for {@link createCopilotEndpointRouter}. */
|
|
350
|
+
interface CopilotEndpointRouterOptions {
|
|
351
|
+
/**
|
|
352
|
+
* Endpoints pinned by the application, keyed by model id. Wins over every
|
|
353
|
+
* other source, including a catalog disclosure that contradicts it.
|
|
354
|
+
*/
|
|
355
|
+
readonly overrides?: Readonly<Record<string, CopilotEndpoint>>;
|
|
356
|
+
/**
|
|
357
|
+
* The full responses-prefix allowlist to use.
|
|
358
|
+
*
|
|
359
|
+
* The caller passes the already-merged list — `copilotAdapter` spreads
|
|
360
|
+
* {@link COPILOT_RESPONSES_MODEL_PREFIXES} first and the user's additions
|
|
361
|
+
* after — so the "adds, never replaces" rule is visible at the call site
|
|
362
|
+
* instead of hidden in here. Defaults to the shipped list when omitted.
|
|
363
|
+
*/
|
|
364
|
+
readonly prefixes?: readonly string[];
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Build a router for one adapter instance.
|
|
368
|
+
*
|
|
369
|
+
* Overrides are validated HERE, not at dispatch: a typo in
|
|
370
|
+
* `endpointOverrides` is a configuration mistake, and a configuration mistake
|
|
371
|
+
* that surfaces while building the provider is cheaper than one that surfaces on
|
|
372
|
+
* the first request to one particular model.
|
|
373
|
+
* @param options - overrides and the merged prefix allowlist.
|
|
374
|
+
* @returns a router whose `decisions` map only ever grows.
|
|
375
|
+
* @throws AgentSdkError with `COPILOT_ENDPOINT_OVERRIDE_INVALID` when an
|
|
376
|
+
* override pins an endpoint that does not exist.
|
|
377
|
+
*/
|
|
378
|
+
declare function createCopilotEndpointRouter(options?: CopilotEndpointRouterOptions): CopilotEndpointRouter;
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/dual-protocol.d.ts
|
|
381
|
+
/** Protocol id the HTTP layer reports for every Copilot request. */
|
|
382
|
+
declare const COPILOT_DUAL_PROTOCOL_ID = "copilot-dual";
|
|
383
|
+
/**
|
|
384
|
+
* The Copilot dialect, FLAT on purpose.
|
|
385
|
+
*
|
|
386
|
+
* Declared in this module rather than in `./adapter.ts` — where the design's file
|
|
387
|
+
* map lists it — for one structural reason: `copilotAdapter` builds the composite,
|
|
388
|
+
* so the source edge already runs adapter → dual-protocol, and the two projection
|
|
389
|
+
* functions are runtime values. Declaring them in `./adapter.ts` would make that
|
|
390
|
+
* edge bidirectional, which the repo's circular-dependency check forbids. The
|
|
391
|
+
* public placement is preserved by re-export: `./adapter.ts` re-exports this type
|
|
392
|
+
* and both projections, the same way `./router.ts` re-exports `CopilotEndpoint`
|
|
393
|
+
* from `./catalog.ts`. DD-2 also puts ownership here —
|
|
394
|
+
* "the composite owns the two pure projection functions".
|
|
395
|
+
*
|
|
396
|
+
* Every field is a primitive or a string array. See rule 2 in the module note for
|
|
397
|
+
* why nesting the two sub-dialects instead would be a silent-data-loss bug.
|
|
398
|
+
*/
|
|
399
|
+
interface CopilotDialect {
|
|
400
|
+
/** Send temperature/top_p. Both endpoints accept them; some models refuse. */
|
|
401
|
+
readonly sampling: boolean;
|
|
402
|
+
/** Send the output-token limit. */
|
|
403
|
+
readonly maxOutputTokens: boolean;
|
|
404
|
+
/** Send JSON-schema structured output. */
|
|
405
|
+
readonly structuredOutputs: boolean;
|
|
406
|
+
/** Declare tools in the request. */
|
|
407
|
+
readonly tools: boolean;
|
|
408
|
+
/** Responses only: `store`. */
|
|
409
|
+
readonly store: boolean;
|
|
410
|
+
/** Responses only: `include`. */
|
|
411
|
+
readonly include: readonly string[];
|
|
412
|
+
/** Responses only: `reasoning.summary`. `'none'` asks for no summary at all. */
|
|
413
|
+
readonly reasoningSummary: 'auto' | 'concise' | 'detailed' | 'none';
|
|
414
|
+
/** Chat Completions only: `stream_options.include_usage`. */
|
|
415
|
+
readonly streamUsage: boolean;
|
|
416
|
+
/** Chat Completions only: the role the system prompt travels under. */
|
|
417
|
+
readonly systemRole: 'system' | 'developer';
|
|
418
|
+
/** Chat Completions only: `parallel_tool_calls`. */
|
|
419
|
+
readonly parallelToolCalls: boolean;
|
|
420
|
+
/** Prompt/session cache key, used by BOTH branches. */
|
|
421
|
+
readonly promptCacheKey?: string;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Conservative defaults, matching each sub-protocol's own defaults where the two
|
|
425
|
+
* agree.
|
|
426
|
+
*
|
|
427
|
+
* `store: false` because retaining prompts on someone else's server is an explicit
|
|
428
|
+
* decision, `include` carries `reasoning.encrypted_content` because without it a
|
|
429
|
+
* reasoning model loses its chain of thought across a tool call, and
|
|
430
|
+
* `parallelToolCalls: false` because older gateways reject the field outright.
|
|
431
|
+
*/
|
|
432
|
+
declare const COPILOT_DEFAULT_DIALECT: CopilotDialect;
|
|
433
|
+
/**
|
|
434
|
+
* Project the Copilot dialect onto the Responses dialect.
|
|
435
|
+
*
|
|
436
|
+
* PURE and TOTAL: every {@link CopilotDialect} flag has exactly one destination
|
|
437
|
+
* here or none at all. `tools`, `streamUsage`, `systemRole` and
|
|
438
|
+
* `parallelToolCalls` have no Responses destination and are DROPPED rather than
|
|
439
|
+
* bent into a nearby flag — Responses declares tools from the request itself and
|
|
440
|
+
* has no `reasoning_effort`-style neighbour worth guessing at.
|
|
441
|
+
*
|
|
442
|
+
* `reasoningSummary: 'none'` is expressed by ABSENCE, because that is how the
|
|
443
|
+
* Responses serializer spells "ask for no summary" (`summary` is only sent when
|
|
444
|
+
* the knob is defined). {@link resolvedResponsesDialect} therefore drops the
|
|
445
|
+
* sub-protocol's own `reasoningSummary` default before merging, so this branch of
|
|
446
|
+
* the projection is not overwritten by it.
|
|
447
|
+
* @param dialect - the resolved Copilot dialect for this request.
|
|
448
|
+
* @returns the Responses knobs this dialect determines, and only those.
|
|
449
|
+
*/
|
|
450
|
+
declare function toResponsesDialect(dialect: CopilotDialect): Partial<ResponsesDialect>;
|
|
451
|
+
/**
|
|
452
|
+
* Project the Copilot dialect onto the Chat Completions dialect.
|
|
453
|
+
*
|
|
454
|
+
* PURE and TOTAL, same rule as {@link toResponsesDialect}: `store`, `include` and
|
|
455
|
+
* `reasoningSummary` have no Chat Completions destination and are dropped —
|
|
456
|
+
* `reasoningEffort` is a different knob (how hard to think, not whether to report
|
|
457
|
+
* a summary), so mapping onto it would be a guess dressed as a translation.
|
|
458
|
+
*
|
|
459
|
+
* Two flags change type on the way across, and each mapping is total:
|
|
460
|
+
*
|
|
461
|
+
* | Copilot | Chat Completions |
|
|
462
|
+
* | --- | --- |
|
|
463
|
+
* | `maxOutputTokens: true` | `maxTokensField: 'max_tokens'` |
|
|
464
|
+
* | `maxOutputTokens: false` | `maxTokensField: false` |
|
|
465
|
+
* | `structuredOutputs: true` | `structuredOutputs: 'json-schema'` |
|
|
466
|
+
* | `structuredOutputs: false` | `structuredOutputs: false` |
|
|
467
|
+
*
|
|
468
|
+
* The accepted cost of the first row: a caller cannot reach
|
|
469
|
+
* `'max_completion_tokens'` through {@link CopilotDialect}. Copilot's
|
|
470
|
+
* `/chat/completions` takes `max_tokens`, and the models that demand the newer
|
|
471
|
+
* spelling are the ones the router sends to `/responses` anyway, so the boolean
|
|
472
|
+
* buys a flag a caller can reason about and costs a spelling no Copilot model has
|
|
473
|
+
* been observed to need.
|
|
474
|
+
* @param dialect - the resolved Copilot dialect for this request.
|
|
475
|
+
* @returns the Chat Completions knobs this dialect determines, and only those.
|
|
476
|
+
*/
|
|
477
|
+
declare function toChatCompletionsDialect(dialect: CopilotDialect): Partial<ChatCompletionsDialect>;
|
|
478
|
+
/**
|
|
479
|
+
* The parts of a sub-protocol the composite uses.
|
|
480
|
+
*
|
|
481
|
+
* Structural rather than an import of either package's own definition type, so
|
|
482
|
+
* that a test can hand in a stub and so that neither sub-protocol's marker fields
|
|
483
|
+
* become part of this contract.
|
|
484
|
+
*/
|
|
485
|
+
interface CopilotSubProtocol<Dialect extends object> {
|
|
486
|
+
/** Reported on the decision as `protocolId`. */
|
|
487
|
+
readonly id: string;
|
|
488
|
+
/** Merged UNDER the projection by the composite; never read by the runtime. */
|
|
489
|
+
readonly defaultDialect: Dialect;
|
|
490
|
+
readonly endpointPath: (request: ProtocolRequest, dialect: Dialect) => string;
|
|
491
|
+
readonly protocolHeaders?: (dialect: Dialect) => Readonly<Record<string, string>>;
|
|
492
|
+
readonly serialize: (request: ProtocolRequest, dialect: Dialect) => Readonly<Record<string, unknown>>;
|
|
493
|
+
readonly translate: (events: AsyncIterable<ProtocolSseEvent>, request: ProtocolRequest, displayName: string) => AsyncGenerator<ProtocolStreamChunk>;
|
|
494
|
+
}
|
|
495
|
+
/** The `/responses` half. `openAiResponsesProtocol` satisfies this. */
|
|
496
|
+
type ResponsesProtocolLike = CopilotSubProtocol<ResponsesDialect>;
|
|
497
|
+
/** The `/chat/completions` half. `openAiChatCompletionsProtocol` satisfies this. */
|
|
498
|
+
type ChatCompletionsProtocolLike = CopilotSubProtocol<ChatCompletionsDialect>;
|
|
499
|
+
/** Construction options for {@link copilotDualProtocol}. */
|
|
500
|
+
interface CopilotDualProtocolOptions {
|
|
501
|
+
/** Decides, once per model id, which branch a request takes. */
|
|
502
|
+
readonly router: CopilotEndpointRouter;
|
|
503
|
+
/** The protocol serving `/responses`. */
|
|
504
|
+
readonly responses: ResponsesProtocolLike;
|
|
505
|
+
/** The protocol serving `/chat/completions`. */
|
|
506
|
+
readonly chat: ChatCompletionsProtocolLike;
|
|
507
|
+
/**
|
|
508
|
+
* Synchronous, best-effort observer of every endpoint decision (Requirement
|
|
509
|
+
* 9.8).
|
|
510
|
+
*
|
|
511
|
+
* Carries `{ model, endpoint, protocolId, source }` — no prompt and no
|
|
512
|
+
* credential, because an observer is a diagnostic channel and neither of those
|
|
513
|
+
* is diagnostic. Throwing in here does NOT affect the request: the error is
|
|
514
|
+
* trapped, since a broken log sink must not decide whether a generation runs.
|
|
515
|
+
*/
|
|
516
|
+
readonly onDecision?: (decision: CopilotEndpointDecision) => void;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Build the composite protocol.
|
|
520
|
+
*
|
|
521
|
+
* @param options - the router, the two sub-protocols, and the optional observer.
|
|
522
|
+
* @returns a `RuntimeWireProtocol<CopilotDialect>` with id `'copilot-dual'`.
|
|
523
|
+
*/
|
|
524
|
+
declare function copilotDualProtocol(options: CopilotDualProtocolOptions): RuntimeWireProtocol<CopilotDialect>;
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/auth.d.ts
|
|
527
|
+
/**
|
|
528
|
+
* The command that produces a credential, named in every message that tells a
|
|
529
|
+
* caller how to fix a credential problem.
|
|
530
|
+
*
|
|
531
|
+
* Exported so the token-exchange path names the SAME command: a 401 and a 403
|
|
532
|
+
* there both end in "sign in again", and two copies of that string are two
|
|
533
|
+
* strings that can drift apart.
|
|
534
|
+
*/
|
|
535
|
+
declare const COPILOT_LOGIN_COMMAND = "npm run provider:copilot:login-device";
|
|
536
|
+
/**
|
|
537
|
+
* An in-memory {@link CopilotAuthStore} — the read/write variant (Requirement 6.4).
|
|
538
|
+
*
|
|
539
|
+
* The read/write variant exists for symmetry with Codex; normal runtime
|
|
540
|
+
* composition uses {@link memoryCopilotCredentialStore}, the compare-and-swap
|
|
541
|
+
* variant.
|
|
542
|
+
* @param initial - the file the store starts with, or nothing for an empty store.
|
|
543
|
+
* @returns a store backed by a single mutable slot.
|
|
544
|
+
*/
|
|
545
|
+
declare function memoryCopilotAuthStore(initial?: CopilotAuthFile): CopilotAuthStore;
|
|
546
|
+
/**
|
|
547
|
+
* An in-memory compare-and-swap store, for deterministic runtime and tests
|
|
548
|
+
* (Requirements 6.2, 6.4).
|
|
549
|
+
*
|
|
550
|
+
* Values are `structuredClone`d in BOTH directions, which is the point of this
|
|
551
|
+
* double: a caller that mutates the object it wrote, or the object it read, must
|
|
552
|
+
* not be able to change what the store holds. Without the clone a test could pass
|
|
553
|
+
* for the wrong reason — the store and the caller sharing one object rather than
|
|
554
|
+
* the store having committed anything.
|
|
555
|
+
*
|
|
556
|
+
* A commit whose `expectedRevision` disagrees with the current revision raises
|
|
557
|
+
* {@link COPILOT_ERROR_CODES.CREDENTIAL_REVISION_CONFLICT} (Requirement 6.3), the
|
|
558
|
+
* Copilot-owned code, so exactly one of two concurrent commits wins and the loser
|
|
559
|
+
* can tell why it lost.
|
|
560
|
+
* @param initial - the file the store starts with, or nothing for an empty store.
|
|
561
|
+
* @returns a compare-and-swap store over a single mutable slot.
|
|
562
|
+
*/
|
|
563
|
+
declare function memoryCopilotCredentialStore(initial?: CopilotAuthFile): CopilotCredentialStore;
|
|
564
|
+
/**
|
|
565
|
+
* One read of the credential store, carried through a single operation.
|
|
566
|
+
*
|
|
567
|
+
* `revision` travels with `file` rather than being re-read later, because the
|
|
568
|
+
* token cache keys on the pair: a file that changed under us has a different
|
|
569
|
+
* revision even when the token value happens to be identical.
|
|
570
|
+
*/
|
|
571
|
+
interface CopilotCredentialSnapshot {
|
|
572
|
+
readonly file: CopilotAuthFile;
|
|
573
|
+
/** The revision at read time, or `null` for a store with no revisions. */
|
|
574
|
+
readonly revision: string | null;
|
|
575
|
+
/** The store's human-readable location, named in diagnostics. */
|
|
576
|
+
readonly label: string;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Require a usable `GitHub_User_Token`, with a message that says how to get one.
|
|
580
|
+
*
|
|
581
|
+
* Three shapes of "there is no credential" — an empty store, a file with no
|
|
582
|
+
* `github` field, and a `github.token` that is the empty string — collapse into
|
|
583
|
+
* the SAME code, because to the person reading the error they are one problem
|
|
584
|
+
* with one fix. That code is the SDK's own {@link MISSING_CREDENTIAL_CODE} rather
|
|
585
|
+
* than a Copilot-specific one, so a consumer does not have to write a second
|
|
586
|
+
* branch for a situation it already handles, and the message carries the command
|
|
587
|
+
* to run `Copilot_Login_Cli` (Requirement 13.4).
|
|
588
|
+
*
|
|
589
|
+
* The token value is never interpolated into the message; only the store's label
|
|
590
|
+
* is (Requirement 13.7).
|
|
591
|
+
* @param file - the credential file, or `undefined` when the store was empty.
|
|
592
|
+
* @param label - the store location named in the diagnostic.
|
|
593
|
+
* @returns the long-lived GitHub token.
|
|
594
|
+
*/
|
|
595
|
+
declare function requireGitHubToken(file: CopilotAuthFile | undefined, label: string): CopilotGitHubToken;
|
|
596
|
+
/** Exchange this long before the `Copilot_Api_Token` actually expires. */
|
|
597
|
+
declare const COPILOT_TOKEN_EXCHANGE_MARGIN_MS: number;
|
|
598
|
+
/**
|
|
599
|
+
* The part of a `Copilot_Api_Token` that {@link shouldExchange} reads.
|
|
600
|
+
*
|
|
601
|
+
* Declared here rather than imported so this module owns no dependency on the
|
|
602
|
+
* exchange module: expiry arithmetic is the whole of what the decision needs, and
|
|
603
|
+
* `CopilotApiToken` — which carries the token value and the declared endpoint
|
|
604
|
+
* besides — satisfies this shape structurally, so `shouldExchange` accepts one
|
|
605
|
+
* with no conversion and there is only ever one declaration of the full type.
|
|
606
|
+
*/
|
|
607
|
+
interface CopilotTokenExpiry {
|
|
608
|
+
/** Expiry instant in epoch MILLISECONDS, derived from the endpoint's `expires_at`. */
|
|
609
|
+
readonly expiresAtMs: number;
|
|
610
|
+
/** The endpoint's `refresh_in` hint, in seconds, when it sent one. Advisory. */
|
|
611
|
+
readonly refreshInSeconds?: number;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Whether a token exchange has to happen before the next request.
|
|
615
|
+
*
|
|
616
|
+
* A pure function of three values that reads no global clock, so a property test
|
|
617
|
+
* can place `now` at every boundary without a fake timer (Requirements 5.2, 5.3).
|
|
618
|
+
* `undefined` — no token yet — is always `true`.
|
|
619
|
+
*
|
|
620
|
+
* `expires_at` is the authority and `refresh_in` is advisory: the hint may only
|
|
621
|
+
* SHORTEN the refresh moment, never lengthen it. The endpoint is allowed to ask
|
|
622
|
+
* for an earlier exchange; it is not allowed to ask this SDK to hold a token past
|
|
623
|
+
* the expiry it announced itself.
|
|
624
|
+
*
|
|
625
|
+
* There is deliberately no fallback branch. `shouldRefresh` in
|
|
626
|
+
* `provider-codex/src/auth.ts` decodes a JWT for `exp` and falls back to a
|
|
627
|
+
* `last_refresh` age when it cannot; a `Copilot_Api_Token` is not a JWT this SDK
|
|
628
|
+
* has any business reading, and the expiry is stated outright in the exchange
|
|
629
|
+
* response body. With no second source, a fallback would have to invent a
|
|
630
|
+
* lifetime, and an invented lifetime violates the no-inference principle.
|
|
631
|
+
*
|
|
632
|
+
* Because the decision is made BEFORE dispatch, a 401 from the Copilot base URL
|
|
633
|
+
* always means the credential is genuinely dead rather than "the token expired
|
|
634
|
+
* mid-flight" — which is what lets auth failures stay non-retryable
|
|
635
|
+
* (Requirement 5.8).
|
|
636
|
+
* @param api - the cached token, or `undefined` when there is none.
|
|
637
|
+
* @param now - current time in epoch milliseconds.
|
|
638
|
+
* @param marginMs - exchange this long before expiry.
|
|
639
|
+
* @returns true when an exchange is due.
|
|
640
|
+
*/
|
|
641
|
+
declare function shouldExchange(api: CopilotTokenExpiry | undefined, now: number, marginMs?: number): boolean;
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/common/http.d.ts
|
|
644
|
+
/** Shared HTTP settings. Every field is optional and every default is a bound, not "unlimited". */
|
|
645
|
+
interface CopilotHttpOptions {
|
|
646
|
+
/** Cancellation for the request and for the body read. */
|
|
647
|
+
readonly signal?: AbortSignal;
|
|
648
|
+
/** HTTP implementation, for tests and non-browser runtimes. */
|
|
649
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
650
|
+
/** Deadline for one request. Defaults to {@link COPILOT_DEFAULT_REQUEST_TIMEOUT_MS}. */
|
|
651
|
+
readonly requestTimeoutMs?: number;
|
|
652
|
+
/** Maximum response bytes. Defaults to {@link COPILOT_DEFAULT_MAX_RESPONSE_BYTES}. */
|
|
653
|
+
readonly maxResponseBytes?: number;
|
|
654
|
+
/** Maximum response chunks. Defaults to {@link COPILOT_DEFAULT_MAX_RESPONSE_CHUNKS}. */
|
|
655
|
+
readonly maxResponseChunks?: number;
|
|
656
|
+
/**
|
|
657
|
+
* Permit an `http:` origin for a trusted local test endpoint. Defaults to false.
|
|
658
|
+
*
|
|
659
|
+
* A separate, explicitly enabled option rather than a lenient default, because
|
|
660
|
+
* cleartext HTTP here carries a bearer token (Requirement 2.2).
|
|
661
|
+
*/
|
|
662
|
+
readonly allowInsecureIssuer?: boolean;
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/exchange.d.ts
|
|
666
|
+
/** GitHub's API base, where the token-exchange surface lives. */
|
|
667
|
+
declare const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
668
|
+
/** Path of the token-exchange surface. */
|
|
669
|
+
declare const COPILOT_TOKEN_EXCHANGE_PATH = "/copilot_internal/v2/token";
|
|
670
|
+
/** Settings for one token exchange. Every field is optional; every default is a bound. */
|
|
671
|
+
interface CopilotExchangeOptions extends CopilotHttpOptions {
|
|
672
|
+
/** Overrides {@link DEFAULT_GITHUB_API_BASE_URL}; pinned as its own origin. */
|
|
673
|
+
readonly githubApiBaseUrl?: string;
|
|
674
|
+
/** Overrides for the two mandatory editor headers. */
|
|
675
|
+
readonly editorHeaders?: CopilotEditorHeaders;
|
|
676
|
+
/**
|
|
677
|
+
* Further secret values to strike out of any body this exchange retains,
|
|
678
|
+
* beyond the credential it sends itself.
|
|
679
|
+
*
|
|
680
|
+
* Requirement 13.7 is stated over BOTH tokens, not just the one a given request
|
|
681
|
+
* carries, and an exchange knows only its own. The remaining value — the
|
|
682
|
+
* `Copilot_Api_Token` currently held — reaches this path from
|
|
683
|
+
* {@link createCopilotTokenCache}, which is the one component holding both at
|
|
684
|
+
* once. Without it, a body echoing the live API token back would travel into
|
|
685
|
+
* `cause` intact, because the redaction here would be looking for the wrong
|
|
686
|
+
* string.
|
|
687
|
+
*/
|
|
688
|
+
readonly additionalSecrets?: readonly string[];
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* The result of one `Copilot_Token_Exchange`, held in process memory only.
|
|
692
|
+
*
|
|
693
|
+
* Structurally satisfies `CopilotTokenExpiry` from `./auth.ts`, so `shouldExchange`
|
|
694
|
+
* accepts one of these with no conversion.
|
|
695
|
+
*/
|
|
696
|
+
interface CopilotApiToken {
|
|
697
|
+
/** Bearer token for the Copilot API base. Short-lived, ~25 minutes. */
|
|
698
|
+
readonly token: string;
|
|
699
|
+
/** Expiry instant in epoch MILLISECONDS, derived from `expires_at` (seconds). */
|
|
700
|
+
readonly expiresAtMs: number;
|
|
701
|
+
/** The endpoint's `refresh_in` hint in seconds, when it sent a usable one. ADVISORY. */
|
|
702
|
+
readonly refreshInSeconds?: number;
|
|
703
|
+
/**
|
|
704
|
+
* The endpoint's declared `endpoints.api`, when present.
|
|
705
|
+
*
|
|
706
|
+
* MUST NOT be used as a base URL. A server-designated base URL is a redirect
|
|
707
|
+
* under another name, and Requirements 3.8/7.8 settled that this SDK does not
|
|
708
|
+
* follow provider-controlled redirection. This field exists so `--status` can
|
|
709
|
+
* print it and so a configuration drift is visible. See DD-6.
|
|
710
|
+
*/
|
|
711
|
+
readonly declaredApiEndpoint?: string;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Exchange a `GitHub_User_Token` for a `Copilot_Api_Token`.
|
|
715
|
+
*
|
|
716
|
+
* The long-lived credential is NOT consumed: nothing here writes to a store, and
|
|
717
|
+
* the persisted value is left exactly as it was (Requirement 3.4).
|
|
718
|
+
* @param github - the long-lived GitHub user token. Its value never reaches an
|
|
719
|
+
* error message, and any occurrence of it in a response body is redacted before
|
|
720
|
+
* the body is retained as a cause (Requirement 13.7).
|
|
721
|
+
* @param options - base URL override, injected fetch, signal, and the read bounds.
|
|
722
|
+
* @returns the short-lived token plus its expiry and the advisory fields.
|
|
723
|
+
* @throws AgentSdkError with `COPILOT_ENDPOINT_ORIGIN_INVALID` or
|
|
724
|
+
* `COPILOT_REDIRECT_REJECTED`, or {@link CopilotTokenExchangeError} with
|
|
725
|
+
* `COPILOT_TENANT_UNSUPPORTED`, `COPILOT_CREDENTIAL_REJECTED`,
|
|
726
|
+
* `COPILOT_TOKEN_EXCHANGE_FAILED` or `COPILOT_TOKEN_MALFORMED`, per the
|
|
727
|
+
* classification order in the module note.
|
|
728
|
+
*/
|
|
729
|
+
declare function exchangeCopilotToken(github: CopilotGitHubToken, options?: CopilotExchangeOptions): Promise<CopilotApiToken>;
|
|
730
|
+
/**
|
|
731
|
+
* A cache entry, bound to exactly the credential that produced it.
|
|
732
|
+
*
|
|
733
|
+
* The pair `(sourceToken, sourceRevision)` is the whole key: signing in as a
|
|
734
|
+
* different account invalidates the entry the moment the store returns a
|
|
735
|
+
* different token value, with no TTL involved and no clock consulted.
|
|
736
|
+
*/
|
|
737
|
+
interface CopilotTokenCacheEntry {
|
|
738
|
+
/** The token this credential produced. */
|
|
739
|
+
readonly api: CopilotApiToken;
|
|
740
|
+
/** The `GitHub_User_Token` value used. Compared with `===`. */
|
|
741
|
+
readonly sourceToken: string;
|
|
742
|
+
/** The store revision at read time, or `null` for a store without revisions. */
|
|
743
|
+
readonly sourceRevision: string | null;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* The process-memory cache in front of {@link exchangeCopilotToken}.
|
|
747
|
+
*
|
|
748
|
+
* Owned by an adapter instance, and injectable through the adapter's `tokenCache`
|
|
749
|
+
* option so several routes sharing one unchanged credential exchange once rather
|
|
750
|
+
* than once per route.
|
|
751
|
+
*/
|
|
752
|
+
interface CopilotTokenCache {
|
|
753
|
+
/**
|
|
754
|
+
* Return a live `Copilot_Api_Token` for this credential, exchanging when due.
|
|
755
|
+
*
|
|
756
|
+
* Concurrent calls that all need an exchange are COALESCED into exactly one
|
|
757
|
+
* in-flight exchange (Requirement 5.4).
|
|
758
|
+
* @param source - one read of the credential store, revision included.
|
|
759
|
+
* @param operation - the calling operation; only its signal is read, and it
|
|
760
|
+
* bounds THIS caller's wait, never the shared exchange.
|
|
761
|
+
* @param context - invocation context for the observation record, when there is one.
|
|
762
|
+
* @returns a token that is live as of the decision moment.
|
|
763
|
+
*/
|
|
764
|
+
acquire(source: CopilotCredentialSnapshot, operation: CredentialOperationOptions, context?: ModelInvocationContext): Promise<CopilotApiToken>;
|
|
765
|
+
/** Drop the current entry; used when the API surface rejects a token before its expiry. */
|
|
766
|
+
invalidate(): void;
|
|
767
|
+
}
|
|
768
|
+
/** Provider name recorded on the credential-operation observation by default. */
|
|
769
|
+
declare const COPILOT_PROVIDER_ID = "copilot";
|
|
770
|
+
/** Settings for {@link createCopilotTokenCache}: the exchange settings, plus a clock. */
|
|
771
|
+
interface CopilotTokenCacheOptions extends CopilotExchangeOptions {
|
|
772
|
+
/**
|
|
773
|
+
* Provider name on the observation record. Defaults to {@link COPILOT_PROVIDER_ID}.
|
|
774
|
+
*
|
|
775
|
+
* An adapter with a custom `id` passes it here so the record names the provider
|
|
776
|
+
* the caller configured rather than the family.
|
|
777
|
+
*/
|
|
778
|
+
readonly providerId?: string;
|
|
779
|
+
/** Exchange this long before expiry. Defaults to `COPILOT_TOKEN_EXCHANGE_MARGIN_MS`. */
|
|
780
|
+
readonly marginMs?: number;
|
|
781
|
+
/**
|
|
782
|
+
* The clock the exchange decision reads, injectable so a test places `now`
|
|
783
|
+
* exactly on a boundary instead of waiting for one.
|
|
784
|
+
*/
|
|
785
|
+
readonly now?: () => number;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Build a token cache over one set of exchange settings.
|
|
789
|
+
*
|
|
790
|
+
* ## The mistake this is written to avoid
|
|
791
|
+
*
|
|
792
|
+
* The shared exchange gets its OWN `AbortController` plus its own deadline, and
|
|
793
|
+
* NEVER any single caller's signal. Were the caller's signal handed to it, the
|
|
794
|
+
* first caller to abort would cancel the exchange every other caller is waiting
|
|
795
|
+
* on, and those callers would fail for a reason that has nothing to do with them.
|
|
796
|
+
* Instead each caller — the one that started the exchange included — races the
|
|
797
|
+
* shared promise against its OWN signal: an aborted caller leaves, and the
|
|
798
|
+
* exchange still completes for everyone else (Property 18).
|
|
799
|
+
*
|
|
800
|
+
* The observation therefore counts exchanges actually DISPATCHED rather than
|
|
801
|
+
* callers served, which is what makes the coalescing observable instead of merely
|
|
802
|
+
* claimed (Property 52). It is recorded with the `'refresh'` operation name: that
|
|
803
|
+
* parameter's union is closed at `'resolve' | 'refresh' | 'login'`, widening it
|
|
804
|
+
* would change a public type of `provider-http`, and Requirement 18.4 forbids
|
|
805
|
+
* that — see DD-7.
|
|
806
|
+
*
|
|
807
|
+
* ## Two paths deliberately absent
|
|
808
|
+
*
|
|
809
|
+
* There is no revision-conflict recovery, unlike `provider-codex`. That path
|
|
810
|
+
* exists there because a Codex refresh token rotates and is single-use, so a lost
|
|
811
|
+
* race destroys a credential. A `GitHub_User_Token` does not rotate and an
|
|
812
|
+
* exchange does not consume it, so two racing processes simply exchange twice —
|
|
813
|
+
* and a branch no situation reaches is a branch nothing verifies (DD-8).
|
|
814
|
+
*
|
|
815
|
+
* Nothing here writes to a store. The `Copilot_Api_Token` is never persisted: it
|
|
816
|
+
* lives ~25 minutes, so persisting it would add a second secret on disk, a second
|
|
817
|
+
* write path, and a new state to reason about, to save one request inside a
|
|
818
|
+
* 25-minute window (DD-9, Requirement 3.3).
|
|
819
|
+
*
|
|
820
|
+
* A failure is returned to every waiting caller as-is and never retried here — an
|
|
821
|
+
* endpoint that rejected the credential will reject it again, and this layer has
|
|
822
|
+
* no way to change that (Requirement 5.8, Property 19).
|
|
823
|
+
* @param options - exchange settings, the observation provider name, the margin
|
|
824
|
+
* and the clock. `options.signal` is deliberately IGNORED for the exchange
|
|
825
|
+
* itself; per-caller cancellation travels through `operation.signal`.
|
|
826
|
+
* @returns a cache over a single credential slot.
|
|
827
|
+
*/
|
|
828
|
+
declare function createCopilotTokenCache(options?: CopilotTokenCacheOptions): CopilotTokenCache;
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/adapter.d.ts
|
|
831
|
+
/** Registry id, provider family, and observation label when the caller sets none. */
|
|
832
|
+
declare const COPILOT_ROUTE_ID = "copilot";
|
|
833
|
+
/** Display name reported by the adapter and the plugin. */
|
|
834
|
+
declare const COPILOT_DISPLAY_NAME = "GitHub Copilot";
|
|
835
|
+
/**
|
|
836
|
+
* Everything a Copilot route can be configured with.
|
|
837
|
+
*
|
|
838
|
+
* The `authStore` is REQUIRED and injected: paths, the filesystem and the
|
|
839
|
+
* environment belong to `Copilot_Node_Auth`, so a Universal package cannot supply
|
|
840
|
+
* a default here (Requirement 6.1). Every other field is optional, and an absent
|
|
841
|
+
* one is spread away rather than passed as `undefined` — see
|
|
842
|
+
* {@link copilotAdapter}.
|
|
843
|
+
*/
|
|
844
|
+
interface CopilotProviderOptions {
|
|
845
|
+
/**
|
|
846
|
+
* Where the credentials live: the compare-and-swap variant.
|
|
847
|
+
*
|
|
848
|
+
* This is the main path. {@link copilotAdapter} also accepts the read/write
|
|
849
|
+
* variant through an overload; `copilotPlugin` does not, because transactional
|
|
850
|
+
* registration and a store with no revisions are a poor pair.
|
|
851
|
+
*/
|
|
852
|
+
readonly authStore: CopilotCredentialStore;
|
|
853
|
+
/** Endpoint base; defaults to `COPILOT_BASE_URL`. */
|
|
854
|
+
readonly baseUrl?: string;
|
|
855
|
+
/**
|
|
856
|
+
* Permit a cleartext `http:` base URL.
|
|
857
|
+
*
|
|
858
|
+
* Explicit opt-in rather than a lenient default, because every request to this
|
|
859
|
+
* surface carries a bearer token (Requirement 2.2).
|
|
860
|
+
*/
|
|
861
|
+
readonly allowInsecureHttp?: boolean;
|
|
862
|
+
/** Overrides for the two mandatory editor headers (Requirement 2.4). */
|
|
863
|
+
readonly editorHeaders?: CopilotEditorHeaders;
|
|
864
|
+
/** Pin an endpoint for specific model ids, overriding the router (Requirement 9.6). */
|
|
865
|
+
readonly endpointOverrides?: Readonly<Record<string, CopilotEndpoint>>;
|
|
866
|
+
/**
|
|
867
|
+
* Extra model-id prefixes treated as `/responses`-capable when the catalog says
|
|
868
|
+
* nothing.
|
|
869
|
+
*
|
|
870
|
+
* ADDS to `COPILOT_RESPONSES_MODEL_PREFIXES`; it cannot replace it, so an
|
|
871
|
+
* override never silently drops a prefix this package ships.
|
|
872
|
+
*/
|
|
873
|
+
readonly responsesModelPrefixes?: readonly string[];
|
|
874
|
+
/** Synchronous, best-effort observer of every endpoint decision (Requirement 9.8). */
|
|
875
|
+
readonly onEndpointDecision?: (decision: CopilotEndpointDecision) => void;
|
|
876
|
+
/**
|
|
877
|
+
* A token cache shared with other routes.
|
|
878
|
+
*
|
|
879
|
+
* Pass one cache to several routes backed by the SAME credential and they
|
|
880
|
+
* exchange once between them instead of once each.
|
|
881
|
+
*/
|
|
882
|
+
readonly tokenCache?: CopilotTokenCache;
|
|
883
|
+
/** Exchange this long before the API token expires. */
|
|
884
|
+
readonly exchangeMarginMs?: number;
|
|
885
|
+
/** GitHub API base, where the token exchange lives; pinned as its own origin. */
|
|
886
|
+
readonly githubApiBaseUrl?: string;
|
|
887
|
+
/**
|
|
888
|
+
* The model catalog.
|
|
889
|
+
*
|
|
890
|
+
* Left undefined, the adapter DISCOVERS it: which models an account may call
|
|
891
|
+
* depends on its plan, its organisation policy and the editor identity the
|
|
892
|
+
* request presents, so no hardcoded list is right for two accounts at once
|
|
893
|
+
* (Requirement 8.1).
|
|
894
|
+
*/
|
|
895
|
+
readonly models?: readonly ProviderCatalogModel[];
|
|
896
|
+
/** Maximum raw catalog bytes. */
|
|
897
|
+
readonly maxCatalogBytes?: number;
|
|
898
|
+
/** Maximum catalog entries; more than this is a malformed catalog, not a truncated one. */
|
|
899
|
+
readonly maxCatalogModels?: number;
|
|
900
|
+
/** Maximum catalog response chunks. */
|
|
901
|
+
readonly maxCatalogChunks?: number;
|
|
902
|
+
/** Catalog request deadline. */
|
|
903
|
+
readonly catalogTimeoutMs?: number;
|
|
904
|
+
/** How long a discovered catalog stays fresh. */
|
|
905
|
+
readonly catalogTtlMs?: number;
|
|
906
|
+
/** How long a stale catalog may still be served while a refresh runs. */
|
|
907
|
+
readonly catalogStaleTtlMs?: number;
|
|
908
|
+
/** How long to wait before retrying discovery after it failed. */
|
|
909
|
+
readonly catalogFailureBackoffMs?: number;
|
|
910
|
+
/** Dialect overrides, merged shallowly over `COPILOT_DEFAULT_DIALECT`. */
|
|
911
|
+
readonly dialect?: Partial<CopilotDialect>;
|
|
912
|
+
/** Output cap when neither caller nor catalog names one. */
|
|
913
|
+
readonly defaultMaxTokens?: number;
|
|
914
|
+
/** Context capacity assumed for an uncatalogued model. */
|
|
915
|
+
readonly defaultContextWindow?: number;
|
|
916
|
+
/** Idle bound while a stream read is outstanding. */
|
|
917
|
+
readonly streamIdleTimeoutMs?: number;
|
|
918
|
+
/** Deadline for one request. */
|
|
919
|
+
readonly requestTimeoutMs?: number;
|
|
920
|
+
/** Maximum serialized request bytes. */
|
|
921
|
+
readonly maxRequestBytes?: number;
|
|
922
|
+
/** Maximum response bytes. */
|
|
923
|
+
readonly maxResponseBytes?: number;
|
|
924
|
+
/** Maximum response chunks. */
|
|
925
|
+
readonly maxResponseChunks?: number;
|
|
926
|
+
/** Maximum SSE events in one stream. */
|
|
927
|
+
readonly maxSseEvents?: number;
|
|
928
|
+
/** Maximum characters in one SSE event. */
|
|
929
|
+
readonly maxSseEventChars?: number;
|
|
930
|
+
/** Maximum bytes read from a non-success response (Requirement 13.6). */
|
|
931
|
+
readonly maxErrorBodyBytes?: number;
|
|
932
|
+
/** Deadline granted to {@link requestLogger} before the request proceeds anyway. */
|
|
933
|
+
readonly requestLoggerTimeoutMs?: number;
|
|
934
|
+
/** Retry policy this route owns (Requirement 7.7). */
|
|
935
|
+
readonly retryPolicy?: RetryPolicyConfig;
|
|
936
|
+
/**
|
|
937
|
+
* Exact wire-request observer.
|
|
938
|
+
*
|
|
939
|
+
* BEST-EFFORT: credentials are redacted by the transport, the logger's deadline
|
|
940
|
+
* is `requestLoggerTimeoutMs`, and a logger that overruns or throws does not
|
|
941
|
+
* stop the request (Requirement 14.5).
|
|
942
|
+
*/
|
|
943
|
+
readonly requestLogger?: ProviderRequestLogger;
|
|
944
|
+
/** HTTP implementation, for tests and non-browser runtimes. */
|
|
945
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
946
|
+
/** Registry id; defaults to {@link COPILOT_ROUTE_ID}. */
|
|
947
|
+
readonly id?: string;
|
|
948
|
+
/** Routes the plugin installs; defaults to `[id]`. */
|
|
949
|
+
readonly routes?: readonly string[];
|
|
950
|
+
/** Default model; a string form requires exactly one route (Requirement 7.5). */
|
|
951
|
+
readonly defaultModel?: string | ModelTarget;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* The same options against the read/write store variant.
|
|
955
|
+
*
|
|
956
|
+
* Kept for symmetry with `provider-codex` and with the two store contracts
|
|
957
|
+
* (Requirement 7.3). It has no revisions, so a commit cannot be
|
|
958
|
+
* compare-and-swapped — which costs nothing here, since nothing on the Copilot
|
|
959
|
+
* credential path writes.
|
|
960
|
+
*/
|
|
961
|
+
interface CopilotLegacyProviderOptions extends Omit<CopilotProviderOptions, 'authStore'> {
|
|
962
|
+
/** Where the credentials live: the read/write variant. */
|
|
963
|
+
readonly authStore: CopilotAuthStore;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Create a Copilot adapter.
|
|
967
|
+
*
|
|
968
|
+
* Both store variants are accepted, and the variant is chosen by INSPECTING THE
|
|
969
|
+
* MARKER through `captureCopilotStore` — which reads data properties only and
|
|
970
|
+
* performs no storage I/O, so building a provider cannot run a line of the
|
|
971
|
+
* caller's code (Requirement 7.3).
|
|
972
|
+
* @param options - credential store, endpoint, catalog, dialect and transport settings.
|
|
973
|
+
* @returns the adapter, ready to register.
|
|
974
|
+
* @throws AgentSdkError with `CREDENTIAL_STORE_INVALID` when `authStore` is
|
|
975
|
+
* neither store variant, or `COPILOT_ENDPOINT_OVERRIDE_INVALID` when
|
|
976
|
+
* `endpointOverrides` pins an endpoint that does not exist.
|
|
977
|
+
*/
|
|
978
|
+
declare function copilotAdapter(options: CopilotProviderOptions): HttpModelAdapter;
|
|
979
|
+
declare function copilotAdapter(options: CopilotLegacyProviderOptions): HttpModelAdapter;
|
|
980
|
+
/** Plugin options; the CAS store variant only. */
|
|
981
|
+
type CopilotPluginOptions = CopilotProviderOptions;
|
|
982
|
+
/**
|
|
983
|
+
* The transactional plugin for installing the Copilot provider.
|
|
984
|
+
*
|
|
985
|
+
* Composition follows `codexPlugin`: `id` defaults to {@link COPILOT_ROUTE_ID},
|
|
986
|
+
* `family` is `'copilot'`, `routes` defaults to `[id]`, and a string
|
|
987
|
+
* `defaultModel` requires exactly one route so the model target's provider can be
|
|
988
|
+
* inferred (Requirements 7.4, 7.5).
|
|
989
|
+
*
|
|
990
|
+
* One difference from Codex: there is no overload per store variant. The
|
|
991
|
+
* compare-and-swap store is the main path here, the read/write variant exists for
|
|
992
|
+
* symmetry, and {@link copilotAdapter} is where it is accepted (Requirement 6.2).
|
|
993
|
+
* The marker is checked at construction rather than at setup so a wrong store is
|
|
994
|
+
* reported while the runtime is being composed, not on the first generation.
|
|
995
|
+
* @param options - the same options {@link copilotAdapter} takes, CAS store only.
|
|
996
|
+
* @returns a composable plugin registering one Copilot adapter.
|
|
997
|
+
* @throws TypeError when `authStore` is not the compare-and-swap variant, or when
|
|
998
|
+
* a string `defaultModel` is paired with anything but exactly one route.
|
|
999
|
+
*/
|
|
1000
|
+
declare function copilotPlugin(options: CopilotPluginOptions): ComposableModelProviderPlugin & {
|
|
1001
|
+
readonly family: 'copilot';
|
|
1002
|
+
};
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/common/error-codes.d.ts
|
|
1005
|
+
/**
|
|
1006
|
+
* The Copilot error codes, in the leaf layer so the shared HTTP modules can reach
|
|
1007
|
+
* them.
|
|
1008
|
+
*
|
|
1009
|
+
* The taxonomy BELONGS to `../errors.ts` — that module is the public door, and it
|
|
1010
|
+
* re-exports everything here. The definition lives one layer down for a structural
|
|
1011
|
+
* reason: `common/` is a leaf, and `common/http.ts` needs
|
|
1012
|
+
* `ENDPOINT_ORIGIN_INVALID` and `COPILOT_REDIRECT_REJECTED` to throw. Importing
|
|
1013
|
+
* them from a root module would make `common/` depend on the root while the root
|
|
1014
|
+
* already depends on `common/`, which is the source-ownership cycle the repo's
|
|
1015
|
+
* package-graph check forbids. Duplicating the two strings instead would be worse:
|
|
1016
|
+
* a code that exists in two places is a code that can disagree with itself.
|
|
1017
|
+
*
|
|
1018
|
+
* Read `../errors.ts` for the taxonomy's rationale, including the three situations
|
|
1019
|
+
* that deliberately get an EXISTING SDK code rather than a Copilot one.
|
|
1020
|
+
*
|
|
1021
|
+
* @module ai-agent-sdk/providers/copilot/error-codes
|
|
1022
|
+
*/
|
|
1023
|
+
/**
|
|
1024
|
+
* Stable codes for the failures that are specific to Copilot.
|
|
1025
|
+
*
|
|
1026
|
+
* Frozen, and flat strings rather than a TS enum, for the same reason the core
|
|
1027
|
+
* taxonomy is: a consumer routes on the value, and the value has to survive
|
|
1028
|
+
* serialization into a log line.
|
|
1029
|
+
*/
|
|
1030
|
+
declare const COPILOT_ERROR_CODES: Readonly<{
|
|
1031
|
+
/** The token-exchange surface rejected the credential: a PAT, or a non-allowlisted OAuth App. */
|
|
1032
|
+
readonly CREDENTIAL_REJECTED: 'COPILOT_CREDENTIAL_REJECTED';
|
|
1033
|
+
/** Token exchange failed for a reason that is not the credential. */
|
|
1034
|
+
readonly TOKEN_EXCHANGE_FAILED: 'COPILOT_TOKEN_EXCHANGE_FAILED';
|
|
1035
|
+
/** The token-exchange response carried no readable `expires_at`, or was not JSON. */
|
|
1036
|
+
readonly TOKEN_MALFORMED: 'COPILOT_TOKEN_MALFORMED';
|
|
1037
|
+
/** A `*.ghe.com` data-residency tenant has no token-exchange surface. */
|
|
1038
|
+
readonly TENANT_UNSUPPORTED: 'COPILOT_TENANT_UNSUPPORTED';
|
|
1039
|
+
/** The endpoint rejected the request for missing `Editor_Headers`. */
|
|
1040
|
+
readonly EDITOR_HEADERS_MISSING: 'COPILOT_EDITOR_HEADERS_MISSING';
|
|
1041
|
+
/** The target URL is not on the same origin as the configured issuer/base URL. */
|
|
1042
|
+
readonly ENDPOINT_ORIGIN_INVALID: 'COPILOT_ENDPOINT_ORIGIN_INVALID';
|
|
1043
|
+
/** The response was a redirect; this SDK does not follow it. */
|
|
1044
|
+
readonly REDIRECT_REJECTED: 'COPILOT_REDIRECT_REJECTED';
|
|
1045
|
+
/** Device flow: the user denied the request. */
|
|
1046
|
+
readonly DEVICE_LOGIN_DENIED: 'COPILOT_DEVICE_LOGIN_DENIED';
|
|
1047
|
+
/** Device flow: the code expired server-side. */
|
|
1048
|
+
readonly DEVICE_LOGIN_EXPIRED: 'COPILOT_DEVICE_LOGIN_EXPIRED';
|
|
1049
|
+
/** Device flow: the absolute 15-minute bound passed without approval. */
|
|
1050
|
+
readonly DEVICE_LOGIN_TIMEOUT: 'COPILOT_DEVICE_LOGIN_TIMEOUT';
|
|
1051
|
+
/** Device flow: failed for any other reason. */
|
|
1052
|
+
readonly DEVICE_LOGIN_FAILED: 'COPILOT_DEVICE_LOGIN_FAILED';
|
|
1053
|
+
/** A credential commit found a revision other than the expected one. */
|
|
1054
|
+
readonly CREDENTIAL_REVISION_CONFLICT: 'COPILOT_CREDENTIAL_REVISION_CONFLICT';
|
|
1055
|
+
/** The `/models` response was the wrong shape at the structural level. */
|
|
1056
|
+
readonly CATALOG_MALFORMED: 'COPILOT_CATALOG_MALFORMED';
|
|
1057
|
+
/** `endpointOverrides` pinned a model to an endpoint that does not exist. */
|
|
1058
|
+
readonly ENDPOINT_OVERRIDE_INVALID: 'COPILOT_ENDPOINT_OVERRIDE_INVALID';
|
|
1059
|
+
}>;
|
|
1060
|
+
/** One of the codes {@link COPILOT_ERROR_CODES} owns. */
|
|
1061
|
+
type CopilotErrorCode = (typeof COPILOT_ERROR_CODES)[keyof typeof COPILOT_ERROR_CODES];
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/errors.d.ts
|
|
1064
|
+
/** A sanitized cause plus the message it is allowed to travel with. */
|
|
1065
|
+
interface CopilotCredentialFailure {
|
|
1066
|
+
/** SDK-authored text. Never carries a token value, because nothing interpolates one in. */
|
|
1067
|
+
readonly message: string;
|
|
1068
|
+
/** The cause, reduced to serializable facts; `undefined` when there was none. */
|
|
1069
|
+
readonly cause: SafeErrorRecord | undefined;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Build the inputs for an error on the Copilot credential path.
|
|
1073
|
+
*
|
|
1074
|
+
* This is the ONLY door: {@link CopilotTokenExchangeError} and
|
|
1075
|
+
* {@link CopilotDeviceLoginError} take a {@link CopilotCredentialFailure} rather
|
|
1076
|
+
* than a raw `cause`, so there is no code path that can attach an unfiltered
|
|
1077
|
+
* value to a credential-path error. `message` is SDK-authored text; a
|
|
1078
|
+
* `GitHub_User_Token` or a `Copilot_Api_Token` is never interpolated into it, and
|
|
1079
|
+
* response bodies reach `cause` only after the bounded read has replaced every
|
|
1080
|
+
* occurrence of the tokens held in memory with `[REDACTED]` (Requirement 13.7).
|
|
1081
|
+
* @param message - SDK-authored, actionable text. No token values.
|
|
1082
|
+
* @param cause - the caught value, if any; filtered before it is retained.
|
|
1083
|
+
* @returns the sanitized pair an error class accepts.
|
|
1084
|
+
*/
|
|
1085
|
+
declare function credentialFailure(message: string, cause?: unknown): CopilotCredentialFailure;
|
|
1086
|
+
/** Whether a token-exchange failure can ever succeed on a retry. */
|
|
1087
|
+
type CopilotTokenExchangeFailureKind = 'permanent' | 'transient';
|
|
1088
|
+
/**
|
|
1089
|
+
* A `Copilot_Token_Exchange` that did not produce a token.
|
|
1090
|
+
*
|
|
1091
|
+
* `kind` exists because `code` alone does not answer the only question a caller
|
|
1092
|
+
* has to answer next: `TOKEN_EXCHANGE_FAILED` covers both a 5xx worth waiting out
|
|
1093
|
+
* and a 4xx that will fail identically forever.
|
|
1094
|
+
*/
|
|
1095
|
+
declare class CopilotTokenExchangeError extends AgentSdkError {
|
|
1096
|
+
/** Retry classification for this failure. */
|
|
1097
|
+
readonly kind: CopilotTokenExchangeFailureKind;
|
|
1098
|
+
/**
|
|
1099
|
+
* @param failure - message and filtered cause from {@link credentialFailure}.
|
|
1100
|
+
* @param code - the Copilot code for this row of the classification table.
|
|
1101
|
+
* @param kind - whether a retry could ever succeed.
|
|
1102
|
+
*/
|
|
1103
|
+
constructor(failure: CopilotCredentialFailure, code: CopilotErrorCode, kind: CopilotTokenExchangeFailureKind);
|
|
1104
|
+
}
|
|
1105
|
+
/** Why a device login ended without a token. */
|
|
1106
|
+
type CopilotDeviceLoginReason = 'denied' | 'expired' | 'timeout' | 'aborted' | 'failed';
|
|
1107
|
+
/**
|
|
1108
|
+
* A device login that ended without a `GitHub_User_Token`.
|
|
1109
|
+
*
|
|
1110
|
+
* The `code` is derived from `reason` rather than passed in, so the two can never
|
|
1111
|
+
* disagree — a caller reading `code` and a caller reading `reason` always see the
|
|
1112
|
+
* same outcome.
|
|
1113
|
+
*/
|
|
1114
|
+
declare class CopilotDeviceLoginError extends AgentSdkError {
|
|
1115
|
+
/** The distinguishable reason the flow ended. */
|
|
1116
|
+
readonly reason: CopilotDeviceLoginReason;
|
|
1117
|
+
/**
|
|
1118
|
+
* @param failure - message and filtered cause from {@link credentialFailure}.
|
|
1119
|
+
* @param reason - the outcome; decides the `code`, with `aborted` mapping to
|
|
1120
|
+
* {@link MODEL_ERROR_CODES.ABORTED} rather than a Copilot-specific code.
|
|
1121
|
+
*/
|
|
1122
|
+
constructor(failure: CopilotCredentialFailure, reason: CopilotDeviceLoginReason);
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/oauth.d.ts
|
|
1126
|
+
/** GitHub's OAuth issuer. */
|
|
1127
|
+
declare const DEFAULT_COPILOT_OAUTH_ISSUER = "https://github.com";
|
|
1128
|
+
/**
|
|
1129
|
+
* Default OAuth client id. Public, not a secret.
|
|
1130
|
+
*
|
|
1131
|
+
* This is the client id published in GitHub's own editor-plugin sources (the
|
|
1132
|
+
* value `copilot.vim` and the other Copilot editor integrations ship in the
|
|
1133
|
+
* clear), which is why it is on the allowlist that
|
|
1134
|
+
* `copilot_internal/v2/token` checks. Sending it means this SDK signs in AS that
|
|
1135
|
+
* editor client. See the module note for why that makes it a named option
|
|
1136
|
+
* instead of a hidden constant.
|
|
1137
|
+
*
|
|
1138
|
+
* ⚠ UNVERIFIED against a live account. Recorded 2026-09-10 from public editor
|
|
1139
|
+
* integration sources only; no sign-in against a real Copilot account has
|
|
1140
|
+
* confirmed THIS client id.
|
|
1141
|
+
*
|
|
1142
|
+
* The 2026-09-10 live run that confirmed the two editor headers did NOT confirm
|
|
1143
|
+
* this value, and could not: it was handed an existing `ghu_` user-to-server token
|
|
1144
|
+
* out of band, so it exercised the EXCHANGE (which answered 200 for that token)
|
|
1145
|
+
* while never running the device flow that would put this `client_id` on the wire.
|
|
1146
|
+
* What that run does establish is the shape of the claim still outstanding — the
|
|
1147
|
+
* exchange endpoint and the allowlist check are live and reachable, and the only
|
|
1148
|
+
* untested link is whether they accept a token minted by this particular app.
|
|
1149
|
+
*
|
|
1150
|
+
* TODO(copilot-identity): confirm on a real Copilot account, then replace this
|
|
1151
|
+
* warning with the confirmation date. To confirm: run the device flow against
|
|
1152
|
+
* `https://github.com/login/device/code` with this `client_id` and
|
|
1153
|
+
* `scope=read:user`, approve it on a Copilot-enabled account, then exchange the
|
|
1154
|
+
* resulting user token at `GET https://api.github.com/copilot_internal/v2/token`.
|
|
1155
|
+
* The client id is confirmed when that exchange returns a Copilot token rather
|
|
1156
|
+
* than 401/403. A non-allowlisted client id fails at the exchange, not at
|
|
1157
|
+
* sign-in, so the device flow succeeding on its own proves nothing — and equally,
|
|
1158
|
+
* an exchange that succeeds for a token this flow did not mint proves nothing
|
|
1159
|
+
* about this constant.
|
|
1160
|
+
*/
|
|
1161
|
+
declare const COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
1162
|
+
/** Requested scope; enough to exchange a token and read identity, no more. */
|
|
1163
|
+
declare const COPILOT_OAUTH_SCOPE = "read:user";
|
|
1164
|
+
/**
|
|
1165
|
+
* The absolute ceiling on one device login, INDEPENDENT of the server's
|
|
1166
|
+
* `expires_in`.
|
|
1167
|
+
*
|
|
1168
|
+
* `expires_in` is honoured when it is shorter — there is no point polling a code
|
|
1169
|
+
* the server has already retired. It is not honoured when it is longer: a server
|
|
1170
|
+
* that answers `expires_in: 86400` would otherwise hang a CLI for a day, and this
|
|
1171
|
+
* SDK is not the right place to hold that terminal hostage (Requirement 4.3).
|
|
1172
|
+
*/
|
|
1173
|
+
declare const COPILOT_DEVICE_CODE_MAX_WAIT_MS: number;
|
|
1174
|
+
/** Poll interval used when the device-code response states none. */
|
|
1175
|
+
declare const COPILOT_DEFAULT_POLL_INTERVAL_SECONDS = 5;
|
|
1176
|
+
/**
|
|
1177
|
+
* Seconds added on every `slow_down`, per RFC 8628 §3.5.
|
|
1178
|
+
*
|
|
1179
|
+
* The increment is what makes the wait STRICTLY increase even when the server
|
|
1180
|
+
* repeats `slow_down` without a new `interval`. Without it, a server that only
|
|
1181
|
+
* ever says "slow down" would be polled at exactly the rate it just objected to.
|
|
1182
|
+
*/
|
|
1183
|
+
declare const COPILOT_SLOW_DOWN_INCREMENT_SECONDS = 5;
|
|
1184
|
+
/**
|
|
1185
|
+
* The warning shown beside the user code, worded exactly as the Codex device
|
|
1186
|
+
* prompt words it.
|
|
1187
|
+
*
|
|
1188
|
+
* A device code is a bearer of authorization that the user types into a page they
|
|
1189
|
+
* navigated to themselves. The one attack that works is getting somebody to type
|
|
1190
|
+
* an attacker's code, so the prompt has to say so; and it lives here rather than
|
|
1191
|
+
* in the CLI so every front end that renders a Copilot prompt renders the same
|
|
1192
|
+
* sentence.
|
|
1193
|
+
*/
|
|
1194
|
+
declare const COPILOT_DEVICE_LOGIN_WARNING = "Only continue if YOU started this login. If someone sent you this code, stop.";
|
|
1195
|
+
/**
|
|
1196
|
+
* The scheduler the poll loop waits on, injectable so no test waits real time.
|
|
1197
|
+
*
|
|
1198
|
+
* `now` travels with the timer rather than sitting in a second option, because
|
|
1199
|
+
* the two are read together on every iteration: a fake timer that advances
|
|
1200
|
+
* pending callbacks while `Date.now()` stands still would let a test satisfy the
|
|
1201
|
+
* 15-minute bound by accident, in either direction. Handing both through one
|
|
1202
|
+
* object makes "virtual clock" a single substitution (Properties 11, 12, 14).
|
|
1203
|
+
*/
|
|
1204
|
+
interface CopilotTimer {
|
|
1205
|
+
/** Schedule `handler` after `ms`; returns whatever handle `clear` accepts. */
|
|
1206
|
+
readonly setTimeout: (handler: () => void, ms: number) => unknown;
|
|
1207
|
+
/** Cancel a handle from {@link CopilotTimer.setTimeout}. */
|
|
1208
|
+
readonly clearTimeout: (handle: unknown) => void;
|
|
1209
|
+
/** Current time in epoch milliseconds. */
|
|
1210
|
+
readonly now: () => number;
|
|
1211
|
+
}
|
|
1212
|
+
/** The real scheduler: `setTimeout`, `clearTimeout` and `Date.now`. */
|
|
1213
|
+
declare const DEFAULT_COPILOT_TIMER: CopilotTimer;
|
|
1214
|
+
/**
|
|
1215
|
+
* Settings for both device-flow legs.
|
|
1216
|
+
*
|
|
1217
|
+
* Extends {@link CopilotHttpOptions}, so the issuer pin, the deadline and the two
|
|
1218
|
+
* read bounds are the same ones every other Copilot call site uses
|
|
1219
|
+
* (Requirement 4.7). `oauthIssuer` is named rather than called `issuer` because
|
|
1220
|
+
* this package pins THREE origins independently and the field name is what says
|
|
1221
|
+
* which one is being set.
|
|
1222
|
+
*/
|
|
1223
|
+
interface CopilotOAuthOptions extends CopilotHttpOptions {
|
|
1224
|
+
/** OAuth issuer base URL; defaults to {@link DEFAULT_COPILOT_OAUTH_ISSUER}. */
|
|
1225
|
+
readonly oauthIssuer?: string;
|
|
1226
|
+
/** OAuth client id; defaults to {@link COPILOT_OAUTH_CLIENT_ID}. */
|
|
1227
|
+
readonly clientId?: string;
|
|
1228
|
+
/** Requested scope; defaults to {@link COPILOT_OAUTH_SCOPE}. */
|
|
1229
|
+
readonly scope?: string;
|
|
1230
|
+
/** Scheduler for the poll wait; defaults to {@link DEFAULT_COPILOT_TIMER}. */
|
|
1231
|
+
readonly timer?: CopilotTimer;
|
|
1232
|
+
}
|
|
1233
|
+
/** A pending device authorization the user has to approve. */
|
|
1234
|
+
interface CopilotDeviceCode {
|
|
1235
|
+
/** URL to open in a browser. Displayed to the user; never fetched by this SDK. */
|
|
1236
|
+
readonly verificationUrl: string;
|
|
1237
|
+
/** One-time code the user types there. */
|
|
1238
|
+
readonly userCode: string;
|
|
1239
|
+
/** Opaque handle this SDK polls with. Never shown to the user. */
|
|
1240
|
+
readonly deviceCode: string;
|
|
1241
|
+
/** Seconds to wait between polls, as the server asked. */
|
|
1242
|
+
readonly intervalSeconds: number;
|
|
1243
|
+
/** Seconds until the server retires the code. */
|
|
1244
|
+
readonly expiresInSeconds: number;
|
|
1245
|
+
}
|
|
1246
|
+
/** Progress reported while a device login runs. */
|
|
1247
|
+
interface CopilotLoginProgress {
|
|
1248
|
+
/** The code is ready; show it, with {@link COPILOT_DEVICE_LOGIN_WARNING}. */
|
|
1249
|
+
readonly onPrompt?: (code: CopilotDeviceCode) => void;
|
|
1250
|
+
/** Called before each poll, with the interval currently in effect. */
|
|
1251
|
+
readonly onPoll?: (elapsedMs: number, intervalSeconds: number) => void;
|
|
1252
|
+
}
|
|
1253
|
+
/** Result of a completed device login. */
|
|
1254
|
+
interface CopilotLoginResult {
|
|
1255
|
+
/** Where the credential was written. Always present. */
|
|
1256
|
+
readonly location: string;
|
|
1257
|
+
/** GitHub login, when the endpoint discloses one. */
|
|
1258
|
+
readonly login: string | undefined;
|
|
1259
|
+
/** Numeric account id, when the endpoint discloses one. */
|
|
1260
|
+
readonly accountId: number | undefined;
|
|
1261
|
+
/** Granted scope, when the endpoint discloses it. */
|
|
1262
|
+
readonly scope: string | undefined;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Start a device authorization.
|
|
1266
|
+
*
|
|
1267
|
+
* `Accept: application/json` is set here as well as on the token leg. It is
|
|
1268
|
+
* load-bearing on the token leg (see {@link pollForCopilotToken}) and harmless
|
|
1269
|
+
* here, and setting it on both keeps the pair from drifting into "one of the two
|
|
1270
|
+
* legs parses JSON".
|
|
1271
|
+
* @param options - issuer, client id, scope, cancellation and read bounds.
|
|
1272
|
+
* @returns the code, the URL and the timings to show the user.
|
|
1273
|
+
* @throws CopilotDeviceLoginError with `reason: 'aborted'` when the caller's
|
|
1274
|
+
* signal aborts, or `reason: 'failed'` when the endpoint answers with anything
|
|
1275
|
+
* other than a usable device authorization.
|
|
1276
|
+
*/
|
|
1277
|
+
declare function requestCopilotDeviceCode(options?: CopilotOAuthOptions): Promise<CopilotDeviceCode>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Run a full device login and persist the resulting `GitHub_User_Token`.
|
|
1280
|
+
*
|
|
1281
|
+
* The store is read BEFORE the flow starts, so the commit carries the revision
|
|
1282
|
+
* that was current when the login began and a concurrent login loses the race
|
|
1283
|
+
* loudly instead of silently overwriting. Nothing else is written: the
|
|
1284
|
+
* `GitHub_User_Token` does not rotate, so this is the only write in the whole
|
|
1285
|
+
* Copilot credential path.
|
|
1286
|
+
* @param store - the credential store to write, in either variant.
|
|
1287
|
+
* @param options - issuer, client id, scope, cancellation, bounds and timer.
|
|
1288
|
+
* @param progress - prompt and poll notifications for a CLI to render.
|
|
1289
|
+
* @returns the store location plus whatever identity the endpoint disclosed.
|
|
1290
|
+
* @throws CopilotDeviceLoginError with `reason` distinguishing `denied`,
|
|
1291
|
+
* `expired`, `timeout`, `aborted` and `failed`.
|
|
1292
|
+
*/
|
|
1293
|
+
declare function runCopilotDeviceLogin(store: CopilotCredentialStore, options?: CopilotOAuthOptions, progress?: CopilotLoginProgress): Promise<CopilotLoginResult>;
|
|
1294
|
+
declare function runCopilotDeviceLogin(store: CopilotAuthStore, options?: CopilotOAuthOptions, progress?: CopilotLoginProgress): Promise<CopilotLoginResult>;
|
|
1295
|
+
//#endregion
|
|
1296
|
+
export { COPILOT_BASE_URL, COPILOT_CATALOG_PATH, COPILOT_DEFAULT_CATALOG_TIMEOUT_MS, COPILOT_DEFAULT_DIALECT, COPILOT_DEFAULT_MAX_CATALOG_BYTES, COPILOT_DEFAULT_MAX_CATALOG_CHUNKS, COPILOT_DEFAULT_MAX_CATALOG_MODELS, COPILOT_DEFAULT_POLL_INTERVAL_SECONDS, COPILOT_DEVICE_CODE_MAX_WAIT_MS, COPILOT_DEVICE_LOGIN_WARNING, COPILOT_DISPLAY_NAME, COPILOT_DUAL_PROTOCOL_ID, COPILOT_EDITOR_PLUGIN_VERSION, COPILOT_EDITOR_VERSION, COPILOT_ERROR_CODES, COPILOT_LOGIN_COMMAND, COPILOT_OAUTH_CLIENT_ID, COPILOT_OAUTH_SCOPE, COPILOT_PROVIDER_ID, COPILOT_RESPONSES_MODEL_PREFIXES, COPILOT_ROUTE_ID, COPILOT_SLOW_DOWN_INCREMENT_SECONDS, COPILOT_TOKEN_EXCHANGE_MARGIN_MS, COPILOT_TOKEN_EXCHANGE_PATH, type ChatCompletionsProtocolLike, type CopilotAccountIdentity, type CopilotApiToken, type CopilotAuthFile, type CopilotAuthStore, type CopilotCatalogLimits, type CopilotCatalogOptions, type CopilotCatalogSnapshot, type CopilotCredentialFailure, type CopilotCredentialSnapshot, type CopilotCredentialStore, type CopilotDeviceCode, CopilotDeviceLoginError, type CopilotDeviceLoginReason, type CopilotDialect, type CopilotDualProtocolOptions, type CopilotEditorHeaders, type CopilotEmbeddingModel, type CopilotEndpoint, type CopilotEndpointDecision, type CopilotEndpointRouter, type CopilotEndpointRouterOptions, type CopilotErrorCode, type CopilotExchangeOptions, type CopilotGenerationModel, type CopilotGitHubToken, type CopilotLegacyProviderOptions, type CopilotLoginProgress, type CopilotLoginResult, type CopilotOAuthOptions, type CopilotOmitReason, type CopilotOmittedModel, type CopilotPluginOptions, type CopilotProviderOptions, type CopilotSubProtocol, type CopilotTimer, type CopilotTokenCache, type CopilotTokenCacheEntry, type CopilotTokenCacheOptions, CopilotTokenExchangeError, type CopilotTokenExchangeFailureKind, type CopilotTokenExpiry, DEFAULT_COPILOT_OAUTH_ISSUER, DEFAULT_COPILOT_TIMER, DEFAULT_GITHUB_API_BASE_URL, type ResponsesProtocolLike, copilotAdapter, copilotCatalogCacheOptions, copilotDualProtocol, copilotPlugin, createCopilotEndpointRouter, createCopilotTokenCache, credentialFailure, discoverCopilotModels, exchangeCopilotToken, memoryCopilotAuthStore, memoryCopilotCredentialStore, partitionCopilotCatalog, requestCopilotDeviceCode, requireGitHubToken, resolveCopilotCatalogLimits, runCopilotDeviceLogin, shouldExchange, toChatCompletionsDialect, toResponsesDialect };
|
|
1297
|
+
//# sourceMappingURL=index.d.ts.map
|