dsh-github-copilot 0.4.0-alpha.18

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.
Files changed (75) hide show
  1. package/AGENTS.md +233 -0
  2. package/CONTRIBUTING.md +46 -0
  3. package/LICENSE +21 -0
  4. package/README.md +365 -0
  5. package/README.zh.md +376 -0
  6. package/SECURITY.md +29 -0
  7. package/cordis.patch.yml +16 -0
  8. package/deployment-baseline.json +752 -0
  9. package/docs/agent-readiness.md +95 -0
  10. package/docs/images/copilot-auth-card-signed-in.png +0 -0
  11. package/docs/images/copilot-device-code-copy.png +0 -0
  12. package/docs/images/copilot-model-freshness.png +0 -0
  13. package/docs/images/copilot-model-refreshing.png +0 -0
  14. package/docs/images/copilot-provider-authorization.png +0 -0
  15. package/docs/images/copilot-provider-entry.png +0 -0
  16. package/docs/images/github-copilot-auth-flow.gif +0 -0
  17. package/docs/model-compatibility-acceptance.md +46 -0
  18. package/docs/npm-distribution.md +134 -0
  19. package/docs/session-search-routing.md +53 -0
  20. package/docs/single-route-migration.md +115 -0
  21. package/lib/client.js +7153 -0
  22. package/lib/client.js.map +1 -0
  23. package/lib/index.js +5384 -0
  24. package/lib/remote.js +162 -0
  25. package/lib/routed-web.js +80 -0
  26. package/lib/search-routing-pFLux0W7.js +763 -0
  27. package/lib/types/account-model-auth.d.ts +15 -0
  28. package/lib/types/account-model-catalog.d.ts +67 -0
  29. package/lib/types/account-model-source.d.ts +85 -0
  30. package/lib/types/authorization-controller.d.ts +100 -0
  31. package/lib/types/client.d.ts +106 -0
  32. package/lib/types/compact-account.d.ts +42 -0
  33. package/lib/types/compatibility.d.ts +17 -0
  34. package/lib/types/config.d.ts +42 -0
  35. package/lib/types/content-file.d.ts +10 -0
  36. package/lib/types/copilot-auth.d.ts +23 -0
  37. package/lib/types/copilot-grant.d.ts +11 -0
  38. package/lib/types/copilot-identity.d.ts +6 -0
  39. package/lib/types/copilot-request.d.ts +11 -0
  40. package/lib/types/current-provider.d.ts +59 -0
  41. package/lib/types/deepseek-search-fallback.d.ts +11 -0
  42. package/lib/types/failure.d.ts +38 -0
  43. package/lib/types/http.d.ts +48 -0
  44. package/lib/types/index.d.ts +39 -0
  45. package/lib/types/migration-status.d.ts +50 -0
  46. package/lib/types/model-protocol.d.ts +31 -0
  47. package/lib/types/pi-provider-bridge.d.ts +22 -0
  48. package/lib/types/plan.d.ts +148 -0
  49. package/lib/types/preview-provider.d.ts +35 -0
  50. package/lib/types/preview-route.d.ts +63 -0
  51. package/lib/types/probe.d.ts +39 -0
  52. package/lib/types/reasoning-presentation.d.ts +55 -0
  53. package/lib/types/remote.d.ts +151 -0
  54. package/lib/types/responses-reasoning-text.d.ts +24 -0
  55. package/lib/types/responses-reasoning.d.ts +7 -0
  56. package/lib/types/route-ownership.d.ts +70 -0
  57. package/lib/types/routed-web.d.ts +38 -0
  58. package/lib/types/search-backend.d.ts +16 -0
  59. package/lib/types/search-routing.d.ts +43 -0
  60. package/lib/types/serialize.d.ts +109 -0
  61. package/lib/types/sse.d.ts +28 -0
  62. package/lib/types/temporary-models.d.ts +25 -0
  63. package/lib/types/tool-schema-compat.d.ts +17 -0
  64. package/lib/types/traditional-search.d.ts +17 -0
  65. package/lib/types/types.d.ts +69 -0
  66. package/lib/types/usage.d.ts +26 -0
  67. package/lib/types/watchdog.d.ts +22 -0
  68. package/lib/types/web-delegate.d.ts +12 -0
  69. package/lib/types/web-search-routing-card.d.ts +10 -0
  70. package/lib/types/web-search-routing-config.d.ts +16 -0
  71. package/lib/types/wire-anthropic.d.ts +37 -0
  72. package/lib/types/wire.d.ts +56 -0
  73. package/lib/web-delegate.js +9 -0
  74. package/package.json +200 -0
  75. package/scripts/check-search-composition.mjs +191 -0
@@ -0,0 +1,763 @@
1
+ import { attributionHeaders } from "@deepseek-ai/dsh-llm";
2
+ import { WebError } from "@deepseek-ai/dsh-web";
3
+ import { builtinProviders, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
4
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
5
+ //#region lib/types/copilot-identity.js
6
+ /** Native OAuth ownership and the sole plugin-owned, account-discovered route. */
7
+ const GITHUB_COPILOT_CREDENTIAL_KEY$1 = "llm-pi-ai/github-copilot";
8
+ const GITHUB_COPILOT_PROVIDER_ID = "github-copilot";
9
+ const GITHUB_COPILOT_PREVIEW_PROVIDER_ID = "github-copilot-preview";
10
+ //#endregion
11
+ //#region lib/types/model-protocol.js
12
+ /** Project only public leaves and the same pi library's supported wire efforts. */
13
+ function projectModelFacts(model) {
14
+ const supported = getSupportedThinkingLevels(model);
15
+ return {
16
+ id: model.id,
17
+ provider: model.provider,
18
+ api: model.api,
19
+ name: model.name,
20
+ baseUrl: model.baseUrl,
21
+ contextWindow: model.contextWindow,
22
+ maxTokens: model.maxTokens,
23
+ input: [...model.input],
24
+ reasoning: model.reasoning,
25
+ supportsReasoningOff: supported.includes("off"),
26
+ reasoningEfforts: Object.fromEntries(supported.filter((level) => level !== "off").map((level) => [level, model.thinkingLevelMap?.[level] ?? level])),
27
+ ...model.headers === void 0 ? {} : { headers: { ...model.headers } }
28
+ };
29
+ }
30
+ /** Stock Core owns its own catalog; local metadata must not retire corrections on its behalf. */
31
+ function readCopilotCatalog(_ctx) {
32
+ return {
33
+ models: getBuiltinModels("github-copilot").map(projectModelFacts),
34
+ authoritative: false
35
+ };
36
+ }
37
+ /** Scope optional preview contributions to this plugin's successfully mounted route service. */
38
+ function isPluginPreviewProvider(ctx, provider) {
39
+ if (provider !== "github-copilot-preview") return false;
40
+ const service = ctx.get("githubCopilotPreview");
41
+ if (typeof service !== "object" || service === null) return false;
42
+ const getView = Reflect.get(service, "getView");
43
+ if (typeof getView !== "function") return false;
44
+ const view = getView.call(service);
45
+ return typeof view === "object" && view !== null && Reflect.get(view, "provider") === provider;
46
+ }
47
+ //#endregion
48
+ //#region lib/types/current-provider.js
49
+ /**
50
+ * Detection of an operation's provider route: the initiating Agent or explicit
51
+ * request names a route and model, the `llm-pi-ai` settings
52
+ * section carries the route's profile, and the pi-ai catalog supplies the
53
+ * defaults (wire protocol, base URL) a profile that overrides nothing inherits.
54
+ * The probe and plan modules read this to decide which search-capable
55
+ * protocol to use without any configuration.
56
+ * @module dsh-github-copilot/current-provider
57
+ */
58
+ /** Settings namespace of the harness's pi-ai LLM adapter (its `providers` dict). */
59
+ const LLM_PI_AI_NAMESPACE = "llm-pi-ai";
60
+ /** Catalog providers indexed by route id. The catalog is static for a given
61
+ * pi-ai version; constructing it per call would rebuild ~30 provider objects
62
+ * on every route read (several per request). */
63
+ let catalogCache;
64
+ function catalogById() {
65
+ if (catalogCache === void 0) {
66
+ const map = /* @__PURE__ */ new Map();
67
+ for (const provider of builtinProviders()) map.set(provider.id, provider);
68
+ catalogCache = map;
69
+ }
70
+ return catalogCache;
71
+ }
72
+ /** Per-provider model tables, cached the same way. */
73
+ const modelTableCache = /* @__PURE__ */ new Map();
74
+ function modelTableOf(provider) {
75
+ const cached = modelTableCache.get(provider);
76
+ if (cached !== void 0) return cached;
77
+ try {
78
+ const models = getBuiltinModels(provider);
79
+ modelTableCache.set(provider, models);
80
+ return models;
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+ /**
86
+ * The catalog facts for one route+model: the model's own wire protocol and
87
+ * base URL. A route pi-ai does not ship, or a model it does not list, yields
88
+ * no facts — the profile is then the whole answer.
89
+ * @param provider - provider route key.
90
+ * @param model - model id.
91
+ * @returns the catalog protocol and base URL, when known.
92
+ */
93
+ function catalogModelFacts(ctx, provider, model) {
94
+ if (provider === "github-copilot-preview") {
95
+ if (!isPluginPreviewProvider(ctx, provider)) return {};
96
+ const owner = ctx.get("githubCopilotPreview");
97
+ if (typeof owner !== "object" || owner === null) return {};
98
+ const method = Reflect.get(owner, "routeFacts");
99
+ if (typeof method !== "function") return {};
100
+ const facts = method.call(owner, model);
101
+ if (typeof facts !== "object" || facts === null) return {};
102
+ const api = Reflect.get(facts, "api"), baseURL = Reflect.get(facts, "baseURL");
103
+ if (typeof api !== "string" || typeof baseURL !== "string") return {};
104
+ return {
105
+ api,
106
+ baseUrl: baseURL
107
+ };
108
+ }
109
+ if (provider === "github-copilot") {
110
+ const found = readCopilotCatalog(ctx).models.find((candidate) => candidate.id === model);
111
+ return found === void 0 ? {} : {
112
+ api: found.api,
113
+ baseUrl: found.baseUrl,
114
+ headers: found.headers
115
+ };
116
+ }
117
+ if (catalogById().get(provider) === void 0) return {};
118
+ const found = modelTableOf(provider).find((candidate) => candidate.id === model);
119
+ return found === void 0 ? {} : {
120
+ api: found.api,
121
+ baseUrl: found.baseUrl,
122
+ headers: found.headers
123
+ };
124
+ }
125
+ /** Read one route profile from the llm-pi-ai settings section, defensively narrowed. */
126
+ function profileFacts(section, provider, model) {
127
+ if (typeof section !== "object" || section === null) return void 0;
128
+ const providers = section["providers"];
129
+ if (typeof providers !== "object" || providers === null) return void 0;
130
+ const profile = providers[provider];
131
+ if (typeof profile !== "object" || profile === null) return void 0;
132
+ const record = profile;
133
+ const stringField = (key) => typeof record[key] === "string" && record[key].length > 0 ? record[key] : void 0;
134
+ const selected = (Array.isArray(record["models"]) ? record["models"] : []).find((candidate) => typeof candidate === "object" && candidate !== null && candidate["id"] === model);
135
+ const declaredApis = Array.isArray(selected?.["apis"]) ? selected.apis.filter((api) => typeof api === "string") : [];
136
+ const selectedApi = typeof selected?.["api"] === "string" ? selected.api : void 0;
137
+ const routeApi = stringField("api");
138
+ const effectiveApi = provider === "github-copilot" ? routeApi : selectedApi ?? routeApi;
139
+ const supportedApis = provider === "github-copilot" ? void 0 : declaredApis.length > 0 ? declaredApis : selectedApi === void 0 ? void 0 : [selectedApi];
140
+ return {
141
+ ...effectiveApi === void 0 ? {} : { api: effectiveApi },
142
+ ...stringField("baseURL") === void 0 ? {} : { baseURL: stringField("baseURL") },
143
+ ...stringField("apiKeyEnv") === void 0 ? {} : { apiKeyEnv: stringField("apiKeyEnv") },
144
+ ...supportedApis === void 0 ? {} : { supportedApis }
145
+ };
146
+ }
147
+ /**
148
+ * Read the public asynchronous initiator seam when supported by Core.
149
+ * Older Core and agentless callers have no session selection; never substitute
150
+ * the default-model service or an arbitrary registered Agent.
151
+ * @param ctx - Host context with the optional Agent registry.
152
+ * @returns the initiating Agent, or undefined when the capability/caller is absent.
153
+ */
154
+ function currentSearchInitiator(ctx) {
155
+ const agents = ctx.get("agents");
156
+ return typeof agents?.currentInitiator === "function" ? agents.currentInitiator() : void 0;
157
+ }
158
+ /**
159
+ * Capture the initiating Session's effective request route. Agent.options is
160
+ * only an activation seed; Core model-selection waterfalls need not update it.
161
+ * @param agent - initiating Agent recovered at the operation entry.
162
+ * @returns provider/model leaves from the last committed request header, or none.
163
+ */
164
+ function currentSearchSelection(agent) {
165
+ const session = agent?.session;
166
+ const config = typeof session?.requestHeader === "function" ? session.requestHeader()?.config : void 0;
167
+ return config?.provider && config.model ? {
168
+ provider: config.provider,
169
+ model: config.model
170
+ } : void 0;
171
+ }
172
+ /**
173
+ * Resolve one operation's route. Explicit GenerateOptions fields are already
174
+ * resolved by Core and take precedence; otherwise use the initiating Session's
175
+ * effective request header. Missing request evidence fails closed.
176
+ * @param ctx - context used for public initiator, settings and catalog reads.
177
+ * @param explicit - explicit request selection, captured at operation entry.
178
+ * @returns owned route facts, or undefined without a usable selection.
179
+ */
180
+ function currentChatRoute(ctx, explicit) {
181
+ const selection = explicit ?? currentSearchSelection(currentSearchInitiator(ctx));
182
+ if (!selection?.provider || !selection.model) return void 0;
183
+ if (selection.provider === "github-copilot-preview" && !isPluginPreviewProvider(ctx, selection.provider)) return void 0;
184
+ const profile = selection.provider === "github-copilot-preview" ? void 0 : profileFacts(ctx.get("settings")?.get(LLM_PI_AI_NAMESPACE), selection.provider, selection.model);
185
+ const catalog = catalogModelFacts(ctx, selection.provider, selection.model);
186
+ return {
187
+ provider: selection.provider,
188
+ model: selection.model,
189
+ ...profile?.api === void 0 ? {} : { api: profile.api },
190
+ ...profile?.api === void 0 && catalog.api !== void 0 ? { api: catalog.api } : {},
191
+ ...profile?.baseURL === void 0 ? {} : { baseURL: profile.baseURL },
192
+ ...profile?.baseURL === void 0 && catalog.baseUrl !== void 0 ? { baseURL: catalog.baseUrl } : {},
193
+ ...profile?.apiKeyEnv === void 0 ? {} : { apiKeyEnv: profile.apiKeyEnv },
194
+ ...profile?.supportedApis === void 0 ? {} : { supportedApis: profile.supportedApis },
195
+ ...catalog.headers === void 0 ? {} : { headers: catalog.headers }
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region lib/types/plan.js
200
+ /**
201
+ * Search-plan resolution: which protocol and endpoint native search runs
202
+ * through. The initiating Agent's captured chat route (selection plus the
203
+ * `llm-pi-ai` settings section) is detected and probed — its own protocol
204
+ * when that can search. The {@link SearchPlan} class owns the probe lifecycle so
205
+ * `available()` stays synchronous while the verdict lands in the background.
206
+ * @module dsh-github-copilot/plan
207
+ */
208
+ /** Credential record that owns GitHub Copilot OAuth state and refresh. */
209
+ const GITHUB_COPILOT_CREDENTIAL_KEY = "llm-pi-ai/github-copilot";
210
+ /** Default `anthropic-version` header value. */
211
+ const DEFAULT_API_VERSION = "2023-06-01";
212
+ /** Standard server-side web search tool type (OpenAI/DeepSeek official). */
213
+ const WEB_SEARCH_TOOL_TYPE = "web_search";
214
+ /**
215
+ * Versioned Responses spelling retained as a capability-probe fallback.
216
+ */
217
+ const RESPONSES_WEB_SEARCH_TOOL_TYPE = "web_search_2025_08_26";
218
+ /** Anthropic server-side web search tool type (versioned). */
219
+ const ANTHROPIC_WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
220
+ /** Whether an anthropic base already carries the `/v1` version segment. */
221
+ function hasV1Segment(baseURL) {
222
+ return /\/v1\/?$/u.test(baseURL);
223
+ }
224
+ /**
225
+ * Normalize an anthropic-messages base so appending `/messages` reaches the
226
+ * Messages endpoint under either base convention: the SDK-style root
227
+ * (`https://api.anthropic.com` → `/v1/messages`) and the v1-included base
228
+ * (`https://api.deepseek.com/anthropic/v1` → `/messages`).
229
+ * @param baseURL - the configured or catalog base.
230
+ * @returns the base `/messages` is appended to.
231
+ */
232
+ function ensureV1Base(baseURL) {
233
+ const trimmed = baseURL.replace(/\/+$/, "");
234
+ return hasV1Segment(trimmed) ? trimmed : `${trimmed}/v1`;
235
+ }
236
+ /**
237
+ * Return the selected Copilot model's native search protocol. No endpoint or
238
+ * cross-provider sibling is guessed.
239
+ * @param route - the current chat route facts.
240
+ * @returns the single native candidate, or none for an unsupported route.
241
+ */
242
+ function siblingCandidates(route) {
243
+ if (route.provider !== "github-copilot" && route.provider !== "github-copilot-preview") return [];
244
+ const base = route.baseURL?.replace(/\/+$/, "") ?? "";
245
+ if (base.length === 0) return [];
246
+ if (route.api === "openai-responses" || route.api === "anthropic-messages") return [{
247
+ protocol: route.api,
248
+ baseURL: base
249
+ }];
250
+ if (route.supportedApis?.includes("openai-responses") === true) return [{
251
+ protocol: "openai-responses",
252
+ baseURL: base
253
+ }];
254
+ return [];
255
+ }
256
+ /** Build one fully defaulted candidate from a protocol and its base. */
257
+ function buildCandidate(protocol, baseURL, route) {
258
+ return {
259
+ protocol,
260
+ baseURL: protocol === "anthropic-messages" ? ensureV1Base(baseURL) : baseURL,
261
+ model: route.model,
262
+ apiKeyEnv: GITHUB_COPILOT_CREDENTIAL_KEY,
263
+ apiVersion: DEFAULT_API_VERSION,
264
+ ...route.headers === void 0 ? {} : { headers: route.headers },
265
+ ...protocol === "openai-responses" ? { webSearchToolType: WEB_SEARCH_TOOL_TYPE } : {}
266
+ };
267
+ }
268
+ /**
269
+ * Build only from the route captured by the caller, without rereading selection.
270
+ * @param route - operation-local route facts.
271
+ * @returns native Copilot candidates, or none without supported facts.
272
+ */
273
+ function candidatesForRoute(route) {
274
+ if (route === void 0 || route.provider !== "github-copilot" && route.provider !== "github-copilot-preview") return [];
275
+ if (route.api === void 0 || route.api === "openai-completions" || route.api === "openai-responses" || route.api === "anthropic-messages") return siblingCandidates(route).map(({ protocol, baseURL }) => buildCandidate(protocol, baseURL, route));
276
+ return [];
277
+ }
278
+ /**
279
+ * One candidate set plus its probe lifecycle. The probe runs in the
280
+ * background so plugin load never blocks on a provider round-trip;
281
+ * `available()` stays synchronous and `settle()` awaits the verdict. A
282
+ * failed plan keeps its reason so the first search (or tool call) surfaces
283
+ * the auto-disable diagnostic instead of a bare "no provider" error.
284
+ */
285
+ var SearchPlan = class {
286
+ probe;
287
+ signal;
288
+ candidates;
289
+ /** Resolves when the probe verdict (or the immediate decision) lands. */
290
+ settled;
291
+ status;
292
+ chosen;
293
+ reason;
294
+ /**
295
+ * @param candidates - the candidates in probe order; empty disables the plan.
296
+ * @param probe - verifies one candidate; called sequentially until one passes.
297
+ * @param probeEnabled - when false the first candidate is trusted outright.
298
+ * @param signal - captured owner/proof cancellation, retained through final HTTP.
299
+ */
300
+ constructor(candidates, probe, probeEnabled, signal) {
301
+ this.probe = probe;
302
+ this.signal = signal;
303
+ this.candidates = candidates;
304
+ if (candidates.length === 0) {
305
+ this.status = "failed";
306
+ this.reason = "GitHub Copilot hosted search is unavailable: select an account-available github-copilot model whose pi-ai catalog protocol is openai-responses or anthropic-messages";
307
+ this.settled = Promise.resolve();
308
+ return;
309
+ }
310
+ if (!probeEnabled) {
311
+ this.status = "ready";
312
+ this.chosen = candidates[0];
313
+ this.settled = Promise.resolve();
314
+ return;
315
+ }
316
+ this.status = "probing";
317
+ this.settled = this.runProbe();
318
+ }
319
+ /** Probe candidates in order; the first supported verdict wins. */
320
+ async runProbe() {
321
+ for (const candidate of this.candidates) {
322
+ let outcome;
323
+ try {
324
+ outcome = await this.probe(candidate);
325
+ } catch (error) {
326
+ outcome = {
327
+ supported: false,
328
+ detail: `probe crashed: ${String(error)}`
329
+ };
330
+ }
331
+ if (outcome.supported) {
332
+ this.status = "ready";
333
+ this.chosen = outcome.webSearchToolType !== void 0 && outcome.webSearchToolType !== candidate.webSearchToolType ? {
334
+ ...candidate,
335
+ webSearchToolType: outcome.webSearchToolType
336
+ } : candidate;
337
+ return;
338
+ }
339
+ this.reason = outcome.detail;
340
+ }
341
+ this.status = "failed";
342
+ if (this.reason === void 0) this.reason = "native web search is disabled: every candidate failed the capability probe";
343
+ else this.reason = `native web search is disabled: ${this.reason}`;
344
+ }
345
+ /**
346
+ * Cheap synchronous usability check for the listener gate. While probing
347
+ * the plan is PROVISIONALLY available — the first gated request may enter
348
+ * and await {@link settle} — so the probe verdict and the stream start in
349
+ * parallel; only a failed (or candidate-less) plan is unavailable and keeps
350
+ * the request on the normal adapter path.
351
+ * @returns whether a search can currently be served.
352
+ */
353
+ available() {
354
+ return this.candidates.length > 0 && this.status !== "failed";
355
+ }
356
+ /**
357
+ * Await the probe verdict and return the winning candidate.
358
+ * @returns the candidate whose probe passed (or the first, when probing is off).
359
+ * @throws WebError `WEB_PROVIDER_UNAVAILABLE` naming the auto-disable reason.
360
+ */
361
+ async settle() {
362
+ await this.settled;
363
+ if (this.status === "ready" && this.chosen !== void 0) return this.chosen;
364
+ throw new WebError(this.reason ?? "native web search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
365
+ }
366
+ /** The candidate currently serving searches, once the plan settled on one. */
367
+ chosenCandidate() {
368
+ return this.chosen;
369
+ }
370
+ /** The auto-disable reason, once a probe failed (or no candidate existed). */
371
+ failureReason() {
372
+ return this.reason;
373
+ }
374
+ };
375
+ /** Whether two candidate sets are identical (skips re-probing on no-op settings changes). */
376
+ function sameCandidates(left, right) {
377
+ if (left.length !== right.length) return false;
378
+ return left.every((candidate, index) => {
379
+ const other = right[index];
380
+ return other !== void 0 && candidate.protocol === other.protocol && candidate.baseURL === other.baseURL && candidate.model === other.model && candidate.apiKeyEnv === other.apiKeyEnv && candidate.apiVersion === other.apiVersion && JSON.stringify(candidate.headers) === JSON.stringify(other.headers) && candidate.webSearchToolType === other.webSearchToolType;
381
+ });
382
+ }
383
+ //#endregion
384
+ //#region lib/types/copilot-request.js
385
+ function normalizeRequestAuth(auth) {
386
+ return typeof auth === "string" ? { apiKey: auth } : auth;
387
+ }
388
+ function applyRequestAuth(candidate, auth) {
389
+ return {
390
+ ...candidate,
391
+ ...auth.baseURL === void 0 ? {} : { baseURL: candidate.protocol === "anthropic-messages" ? `${auth.baseURL.replace(/\/+$/, "").replace(/\/v1$/u, "")}/v1` : auth.baseURL },
392
+ ...auth.headers === void 0 ? {} : { headers: {
393
+ ...candidate.headers,
394
+ ...auth.headers
395
+ } }
396
+ };
397
+ }
398
+ /** Materialize pi-ai catalog headers and Copilot's per-request metadata. */
399
+ function providerRequestHeaders(candidate, initiator) {
400
+ const headers = {};
401
+ for (const [name, value] of Object.entries(candidate.headers ?? {})) if (value !== null) headers[name] = value;
402
+ try {
403
+ const hostname = new URL(candidate.baseURL).hostname.toLowerCase();
404
+ if (hostname.endsWith(".githubcopilot.com") || hostname.startsWith("copilot-api.")) {
405
+ headers["X-Initiator"] = initiator;
406
+ headers["Openai-Intent"] = "conversation-edits";
407
+ }
408
+ } catch {}
409
+ return headers;
410
+ }
411
+ //#endregion
412
+ //#region lib/types/http.js
413
+ /**
414
+ * Shared HTTP plumbing for the capability probe and the inline wires:
415
+ * bounded response reading, caller-cancellation races for preflight steps,
416
+ * and the stable {@link WebError} translations. The patterns mirror the
417
+ * harness `dsh-web` package family (web-search-deepseek, model discovery) so
418
+ * every credentialed request rejects redirects before a `Location` target can
419
+ * be contacted.
420
+ * @module dsh-github-copilot/http
421
+ */
422
+ /**
423
+ * Endpoint replies larger than this are refused. The endpoint is whatever
424
+ * base URL the configuration named, so the ceiling holds on the bytes
425
+ * actually read rather than on the length the server claims.
426
+ */
427
+ const MAX_RESPONSE_BYTES = 4194304;
428
+ /**
429
+ * Read a response body with a hard byte cap, refusing one that outgrows it.
430
+ * A declared length is checked first so an honest server is turned away
431
+ * without transferring anything; the accumulated total is what actually
432
+ * enforces the bound, because a server that under-declares (or streams) tells
433
+ * us nothing up front.
434
+ * @param response - the HTTP response whose body is read to completion.
435
+ * @param url - the requested URL, named in the refusal message.
436
+ * @returns the decoded body text.
437
+ * @throws WebError `WEB_PROVIDER_ERROR` when the body exceeds the cap.
438
+ */
439
+ async function readBounded(response, url) {
440
+ const oversized = () => new WebError(`${url} answered with more than ${MAX_RESPONSE_BYTES} bytes`, "WEB_PROVIDER_ERROR");
441
+ const declared = Number(response.headers.get("content-length") ?? NaN);
442
+ if (Number.isFinite(declared) && declared > 4194304) {
443
+ await response.body?.cancel();
444
+ throw oversized();
445
+ }
446
+ if (response.body === null) return "";
447
+ const reader = response.body.getReader();
448
+ const chunks = [];
449
+ let total = 0;
450
+ try {
451
+ for (;;) {
452
+ const { done, value } = await reader.read();
453
+ if (done) break;
454
+ total += value.byteLength;
455
+ if (total > 4194304) throw oversized();
456
+ chunks.push(value);
457
+ }
458
+ } finally {
459
+ await reader.cancel().catch(() => void 0);
460
+ }
461
+ const body = new Uint8Array(total);
462
+ let offset = 0;
463
+ for (const chunk of chunks) {
464
+ body.set(chunk, offset);
465
+ offset += chunk.byteLength;
466
+ }
467
+ return new TextDecoder("utf-8", { fatal: true }).decode(body);
468
+ }
469
+ /** Whether an error is the fetch/`AbortSignal` abort signal. */
470
+ function isAbortError(error) {
471
+ return error instanceof DOMException && error.name === "AbortError";
472
+ }
473
+ /**
474
+ * Race a same-process asynchronous preflight (credential resolution) against
475
+ * a cancellation signal. The attached settlement handlers keep observing an
476
+ * uncooperative operation after the race is lost, so a later rejection cannot
477
+ * become unhandled.
478
+ * @param operation - the preflight promise.
479
+ * @param signal - the cancellation signal (request abort or idle watchdog).
480
+ * @returns the operation's value, or a rejection once the signal fires.
481
+ */
482
+ function abortable(operation, signal) {
483
+ if (signal === void 0) return operation;
484
+ if (signal.aborted) {
485
+ operation.then(() => void 0, () => void 0);
486
+ return Promise.reject(new DOMException("aborted", "AbortError"));
487
+ }
488
+ return new Promise((resolve, reject) => {
489
+ const onAbort = () => reject(new DOMException("aborted", "AbortError"));
490
+ signal.addEventListener("abort", onAbort, { once: true });
491
+ operation.then((value) => {
492
+ signal.removeEventListener("abort", onAbort);
493
+ resolve(value);
494
+ }, (error) => {
495
+ signal.removeEventListener("abort", onAbort);
496
+ reject(error);
497
+ });
498
+ });
499
+ }
500
+ /**
501
+ * Extract a provider error message from an error envelope whose shape varies
502
+ * by protocol and vendor: `{ error: { message } }`, `{ error: "text" }`, or
503
+ * `{ message }`. Returns `undefined` when nothing usable is present.
504
+ * @param body - the parsed non-2xx response body.
505
+ * @returns the provider message, or `undefined`.
506
+ */
507
+ function providerErrorMessage(body) {
508
+ if (typeof body !== "object" || body === null) return void 0;
509
+ const record = body;
510
+ const error = record["error"];
511
+ if (typeof error === "string" && error.length > 0) return error;
512
+ if (typeof error === "object" && error !== null) {
513
+ const message = error["message"];
514
+ if (typeof message === "string" && message.length > 0) return message;
515
+ }
516
+ const message = record["message"];
517
+ return typeof message === "string" && message.length > 0 ? message : void 0;
518
+ }
519
+ //#endregion
520
+ //#region lib/types/traditional-search.js
521
+ /**
522
+ * Traditional `ctx.web` search provider backed by a verified OpenAI Responses
523
+ * candidate. This is intentionally search-only: URL fetching remains owned by
524
+ * dedicated anonymous fetch providers.
525
+ * @module dsh-github-copilot/traditional-search
526
+ */
527
+ /** Stable provider id selected through `web.searchProvider`. */
528
+ const GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID = "github-copilot-hosted";
529
+ /** @deprecated Use {@link GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID}. */
530
+ const COPILOT_HOSTED_SEARCH_PROVIDER_ID = GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID;
531
+ /** Build the search-only provider registered with `ctx.web`. */
532
+ function createTraditionalSearchProvider(available, plan, hooks, config, unavailableMessage = "the github-copilot-hosted search provider requires an initiating Agent via agents.currentInitiator() and an eligible Session.requestHeader().config route") {
533
+ return {
534
+ id: GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID,
535
+ available,
536
+ search: (request, signal) => {
537
+ if (signal?.aborted === true) return Promise.reject(aborted());
538
+ if (!available()) return Promise.reject(new WebError(unavailableMessage, "WEB_PROVIDER_UNAVAILABLE"));
539
+ return searchResponses(request, signal, plan, hooks, config());
540
+ }
541
+ };
542
+ }
543
+ /** Execute one bounded native-search Responses request and normalize its result. */
544
+ async function searchResponses(request, signal, plan, hooks, config) {
545
+ if (isAborted(signal)) throw aborted();
546
+ const timeout = AbortSignal.timeout(config.idleTimeoutMs);
547
+ let combined = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
548
+ let ownership;
549
+ let candidate;
550
+ try {
551
+ const resolved = await abortable(Promise.resolve(plan(combined)), combined);
552
+ ownership = resolved.signal;
553
+ if (ownership !== void 0) combined = AbortSignal.any([combined, ownership]);
554
+ candidate = await abortable(resolved.settle(), combined);
555
+ } catch (error) {
556
+ if (signal?.aborted === true) throw aborted();
557
+ if (isAborted(ownership)) throw invalidated();
558
+ if (timeout.aborted || isAbortError(error)) throw new WebError("web search timed out", "WEB_PROVIDER_ERROR");
559
+ throw new WebError("the native search capability probe did not find a usable Responses provider", "WEB_PROVIDER_UNAVAILABLE");
560
+ }
561
+ if (candidate.protocol !== "openai-responses") throw new WebError("the verified search candidate does not support the Responses bridge", "WEB_PROVIDER_UNAVAILABLE");
562
+ let auth;
563
+ try {
564
+ auth = normalizeRequestAuth(await abortable(hooks.resolveApiKey(candidate), combined));
565
+ } catch (error) {
566
+ throw translateError(error, signal, timeout, ownership);
567
+ }
568
+ if (auth === void 0 || auth.apiKey.length === 0) throw new WebError(`no API key for "${candidate.apiKeyEnv}"`, "WEB_PROVIDER_UNAVAILABLE");
569
+ candidate = applyRequestAuth(candidate, auth);
570
+ const apiKey = auth.apiKey;
571
+ const endpoint = `${candidate.baseURL.replace(/\/+$/, "")}/responses`;
572
+ let response;
573
+ try {
574
+ combined.throwIfAborted();
575
+ response = await fetch(endpoint, {
576
+ method: "POST",
577
+ redirect: "error",
578
+ signal: combined,
579
+ headers: {
580
+ authorization: `Bearer ${apiKey}`,
581
+ "content-type": "application/json",
582
+ accept: "application/json",
583
+ ...attributionHeaders(),
584
+ ...providerRequestHeaders(candidate, "user")
585
+ },
586
+ body: JSON.stringify({
587
+ model: candidate.model,
588
+ input: [{
589
+ role: "user",
590
+ content: [{
591
+ type: "input_text",
592
+ text: request.query
593
+ }]
594
+ }],
595
+ tools: [{ type: candidate.webSearchToolType ?? "web_search" }],
596
+ tool_choice: candidate.webSearchToolType === "web_search" || candidate.webSearchToolType === void 0 ? "required" : { type: candidate.webSearchToolType },
597
+ include: ["web_search_call.action.sources"],
598
+ stream: false
599
+ })
600
+ });
601
+ } catch (error) {
602
+ throw translateError(error, signal, timeout, ownership);
603
+ }
604
+ if (!response.ok) {
605
+ await response.body?.cancel().catch(() => void 0);
606
+ throw new WebError(`Responses API error (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
607
+ }
608
+ let body;
609
+ try {
610
+ body = JSON.parse(await abortable(readBounded(response, endpoint), combined));
611
+ combined.throwIfAborted();
612
+ } catch (error) {
613
+ if (combined.aborted || isAbortError(error)) throw translateError(error, signal, timeout, ownership);
614
+ throw new WebError("Responses API returned an invalid search result", "WEB_PROVIDER_ERROR");
615
+ }
616
+ const content = [];
617
+ const sources = /* @__PURE__ */ new Map();
618
+ for (const item of body.output ?? []) {
619
+ if (item.type === "web_search_call") {
620
+ for (const source of item.action?.sources ?? []) addSource(sources, source);
621
+ continue;
622
+ }
623
+ if (item.type !== "message") continue;
624
+ for (const part of item.content ?? []) {
625
+ if (typeof part.text === "string" && part.text.length > 0) content.push(part.text);
626
+ for (const annotation of part.annotations ?? []) addSource(sources, annotation);
627
+ }
628
+ }
629
+ return {
630
+ ...content.length > 0 ? { content: content.join("\n") } : {},
631
+ sources: [...sources.values()],
632
+ truncated: false
633
+ };
634
+ }
635
+ /** Add one valid source, merging richer duplicate metadata without inventing it. */
636
+ function addSource(sources, source) {
637
+ if (typeof source.url !== "string" || source.url.length === 0) return;
638
+ const existing = sources.get(source.url);
639
+ sources.set(source.url, {
640
+ url: source.url,
641
+ ...existing?.title !== void 0 ? { title: existing.title } : typeof source.title === "string" ? { title: source.title } : {},
642
+ ...existing?.snippet !== void 0 ? { snippet: existing.snippet } : typeof source.snippet === "string" ? { snippet: source.snippet } : {},
643
+ ...existing?.publishedAt !== void 0 ? { publishedAt: existing.publishedAt } : typeof source.published_at === "string" ? { publishedAt: source.published_at } : {}
644
+ });
645
+ }
646
+ /** Translate cancellation/timeout/transport failures into the web seam taxonomy. */
647
+ function translateError(error, caller, timeout, ownership) {
648
+ if (isAborted(caller)) return aborted();
649
+ if (isAborted(ownership)) return invalidated();
650
+ if (timeout.aborted || isAbortError(error)) return new WebError("web search timed out", "WEB_PROVIDER_ERROR", { cause: error });
651
+ if (error instanceof WebError) return error;
652
+ return new WebError("web search provider request failed", "WEB_PROVIDER_ERROR", { cause: error });
653
+ }
654
+ function invalidated() {
655
+ return new WebError("web search owner or credential proof was invalidated", "WEB_PROVIDER_UNAVAILABLE");
656
+ }
657
+ function aborted() {
658
+ return new WebError("web search aborted", "WEB_ABORTED");
659
+ }
660
+ /** Read the signal live after awaits without preserving an earlier narrowing. */
661
+ function isAborted(signal) {
662
+ return signal?.aborted === true;
663
+ }
664
+ //#endregion
665
+ //#region lib/types/search-backend.js
666
+ /** Bounded metadata included in the same query's successful or failed fallback notice. */
667
+ function describeSearchBackend(backend) {
668
+ const origin = backend.origin.slice(0, 512) + (backend.origin.length > 512 ? " (truncated)" : "");
669
+ const model = JSON.stringify(backend.model.slice(0, 256)) + (backend.model.length > 256 ? " (truncated)" : "");
670
+ return `Fallback backend: adapter=${backend.provider}; origin=${origin}; model=${model}; endpoint=${backend.customEndpoint ? "custom" : "default"}.`;
671
+ }
672
+ /** Known sanitized failure, distinct from arbitrary native provider exception messages. */
673
+ var DescribedSearchFallbackError = class extends WebError {
674
+ backend;
675
+ constructor(backend, code = "WEB_PROVIDER_ERROR") {
676
+ super("the configured DeepSeek fallback failed", code);
677
+ this.backend = backend;
678
+ }
679
+ };
680
+ //#endregion
681
+ //#region lib/types/search-routing.js
682
+ /** Match only the canonical route or a verified plugin-owned managed route. */
683
+ function isCopilotSearchSelection(selection, managedOwned) {
684
+ return selection?.provider === "github-copilot" || selection?.provider === "github-copilot-preview" && managedOwned;
685
+ }
686
+ function assertContinuable(signal, canContinue) {
687
+ if (signal?.aborted === true) throw new WebError("web search aborted", "WEB_ABORTED");
688
+ if (!canContinue()) throw new WebError("web search owner or account proof invalidated", "WEB_PROVIDER_UNAVAILABLE");
689
+ }
690
+ /** Public reason codes only: never copy upstream bodies, headers or arbitrary exception messages. */
691
+ function publicFailureReason(error) {
692
+ if (error instanceof WebError && [
693
+ "WEB_PROVIDER_UNAVAILABLE",
694
+ "WEB_PROVIDER_ERROR",
695
+ "WEB_PROVIDER_CONFIGURED_UNAVAILABLE",
696
+ "WEB_PROVIDER_CONFIGURED_MISSING"
697
+ ].includes(error.code)) return error.code;
698
+ return "COPILOT_SEARCH_FAILED";
699
+ }
700
+ /**
701
+ * Route one already-validated search using captured request identity.
702
+ * Non-Copilot calls delegate untouched. Copilot failures reach a known DeepSeek
703
+ * provider only under the explicit spending policy; cancellation and invalidated
704
+ * ownership never trigger fallback. Consumer validation, batching and deadlines
705
+ * remain the caller's responsibility, not this provider-selection function.
706
+ */
707
+ async function routeSessionSearch(request, signal, deps) {
708
+ if (!isCopilotSearchSelection(deps.selection, deps.managedOwned)) return { result: await deps.delegate(request, signal) };
709
+ const model = deps.selection.model;
710
+ const allowFallback = deps.fallback === "deepseek";
711
+ assertContinuable(signal, deps.canContinue);
712
+ if (deps.copilot.id !== "github-copilot-hosted") throw new WebError("plugin-owned Copilot search provider is required", "WEB_PROVIDER_UNAVAILABLE");
713
+ let failure;
714
+ try {
715
+ if (!deps.copilot.available()) throw new WebError("Copilot search is unavailable for this initiating model", "WEB_PROVIDER_UNAVAILABLE");
716
+ const result = await deps.copilot.search(request, signal);
717
+ assertContinuable(signal, deps.canContinue);
718
+ return {
719
+ result,
720
+ routing: {
721
+ requestedProvider: GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID,
722
+ actualProvider: GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID,
723
+ model,
724
+ fallback: false
725
+ }
726
+ };
727
+ } catch (error) {
728
+ assertContinuable(signal, deps.canContinue);
729
+ if (error instanceof WebError && error.code === "WEB_ABORTED") throw error;
730
+ if (!allowFallback) throw error;
731
+ failure = error;
732
+ }
733
+ const fallback = await deps.resolveDeepSeek();
734
+ assertContinuable(signal, deps.canContinue);
735
+ if (fallback.id !== "deepseek-official" || !fallback.available()) throw new WebError("the explicitly requested DeepSeek fallback is unavailable", "WEB_PROVIDER_UNAVAILABLE");
736
+ const reason = publicFailureReason(failure);
737
+ let result;
738
+ try {
739
+ result = await fallback.search(request, signal);
740
+ } catch (error) {
741
+ assertContinuable(signal, deps.canContinue);
742
+ if (error instanceof WebError && error.code === "WEB_ABORTED") throw error;
743
+ const backend = error instanceof DescribedSearchFallbackError && error.backend !== void 0 ? ` ${describeSearchBackend(error.backend)}` : "";
744
+ throw new WebError(`Copilot search failed (${reason}); DeepSeek fallback also failed and may have incurred DeepSeek API charges (or configured backend charges).${backend}`, "WEB_PROVIDER_ERROR");
745
+ }
746
+ assertContinuable(signal, deps.canContinue);
747
+ const notice = `Search provider: deepseek-official (explicit fallback from github-copilot-hosted; reason: ${reason}). This fallback may incur DeepSeek API charges (or charges from a configured custom backend). These are not Copilot search results.`;
748
+ return {
749
+ result: {
750
+ ...result,
751
+ content: `${notice}\n\n${result.content ?? ""}`.trimEnd()
752
+ },
753
+ routing: {
754
+ requestedProvider: GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID,
755
+ actualProvider: "deepseek-official",
756
+ model,
757
+ fallback: true,
758
+ reason
759
+ }
760
+ };
761
+ }
762
+ //#endregion
763
+ export { currentSearchInitiator as C, GITHUB_COPILOT_CREDENTIAL_KEY$1 as D, readCopilotCatalog as E, GITHUB_COPILOT_PREVIEW_PROVIDER_ID as O, currentChatRoute as S, isPluginPreviewProvider as T, RESPONSES_WEB_SEARCH_TOOL_TYPE as _, COPILOT_HOSTED_SEARCH_PROVIDER_ID as a, candidatesForRoute as b, abortable as c, readBounded as d, applyRequestAuth as f, GITHUB_COPILOT_CREDENTIAL_KEY as g, ANTHROPIC_WEB_SEARCH_TOOL_TYPE as h, describeSearchBackend as i, GITHUB_COPILOT_PROVIDER_ID as k, isAbortError as l, providerRequestHeaders as m, routeSessionSearch as n, GITHUB_COPILOT_HOSTED_SEARCH_PROVIDER_ID as o, normalizeRequestAuth as p, DescribedSearchFallbackError as r, createTraditionalSearchProvider as s, isCopilotSearchSelection as t, providerErrorMessage as u, SearchPlan as v, currentSearchSelection as w, sameCandidates as x, WEB_SEARCH_TOOL_TYPE as y };