dsh-better-copilot 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — consolidated Copilot plugin
4
+
5
+ First release of `dsh-better-copilot`, merging three independently installable
6
+ plugins into one package with one bundle row (`better-copilot`) and one combined
7
+ configuration object:
8
+
9
+ - `dsh-copilot-catalog@0.2.9` — account-aware model discovery, Copilot wire
10
+ identity and the pi-ai adapter wrapper.
11
+ - `dsh-copilot-search@0.1.5` — the `github-copilot-search` Responses web-search
12
+ provider.
13
+ - `dsh-copilot-tool-schema@0.1.3` — the Copilot Gemini tool-declaration guard.
14
+
15
+ Behaviour is preserved:
16
+
17
+ - the wrapper still disables and replaces `llm-pi-ai`, still preserves the
18
+ adapter's own provider schema, and still installs the wire-identity
19
+ interceptor best-effort;
20
+ - search still resolves credentials through the adapter's public pi-ai surface,
21
+ still keeps a search-owned guarded refresh clone, and still refuses untrusted
22
+ origins and redirects;
23
+ - the guard still only adds a missing root `properties` bucket for configured
24
+ provider/model families and leaves every other declaration untouched.
25
+
26
+ Changed:
27
+
28
+ - one plugin row and one config object: `providers` (unchanged adapter schema),
29
+ `search` (was the search plugin's row config) and `toolSchema` (was the guard's
30
+ row config);
31
+ - subpath exports are now `.`, `./catalog`, `./discovery`, `./identity`,
32
+ `./search` and `./guard`; the predecessor subpaths are removed;
33
+ - the colliding `CopilotAuthOptions` type is exported as `CatalogAuthOptions`
34
+ and `SearchAuthOptions`;
35
+ - a missing `web` service, a throwing guard registration, a throwing search
36
+ registration and a throwing identity installer are contained: they degrade
37
+ only their own capability instead of failing the mount.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-copilot-catalog contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # dsh-better-copilot
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One DeepSeek Harness plugin for GitHub Copilot:
4
+
5
+ - **Account-aware model discovery** — reads the stored Copilot OAuth grant (or an
6
+ explicit API-key reference), lists the account's models, and keeps the bundled
7
+ catalog only as a fallback.
8
+ - **Copilot-compatible wire identity** — installs a refcounted `fetch` header
9
+ interceptor while mounted so the pi-ai adapter presents Copilot client
10
+ identity headers.
11
+ - **Native Responses web search** — registers the `github-copilot-search`
12
+ provider for the ordinary `web_search` tool.
13
+ - **Gemini tool-declaration guard** — adds a missing root `properties` bucket to
14
+ tool parameter schemas for configured Copilot routes.
15
+
16
+ The plugin is a replacement for, and superset of, the earlier separate
17
+ `dsh-copilot-catalog`, `dsh-copilot-search` and `dsh-copilot-tool-schema`
18
+ packages. Their behaviour is consolidated here; see the migration notes below.
19
+
20
+ ## Compatibility
21
+
22
+ - DSH and DSH peer packages: `0.2.1-rc.5`.
23
+ - `@earendil-works/pi-ai`: `^1.1.0` (host-aligned peer range; tested at `1.1.0` and `1.3.0`).
24
+ - Node.js: `>=22.19.0`.
25
+
26
+ The plugin resolves the same public pi-ai runtime the DSH adapter uses, including
27
+ nested pnpm layouts. It vendors neither DSH nor pi-ai and does not modify
28
+ signed applications. This is an experimental third-party integration, not
29
+ affiliated with GitHub, DeepSeek or the pi-ai maintainers.
30
+
31
+ ## Installation
32
+
33
+ Install or link this leaf package, not the monorepo root. The package is
34
+ published as `dsh-better-copilot`; until a registry release exists, install the
35
+ packed tarball or link the workspace directory. Restart the DSH host after
36
+ installing or removing a host plugin.
37
+
38
+ The bundle patch:
39
+
40
+ - disables the stock `llm-pi-ai` row and mounts this plugin in its place (the
41
+ merged plugin is the adapter wrapper), so never enable two adapters for the
42
+ same route;
43
+ - selects `github-copilot-search` for the `web` row and keeps the HTTP fetch
44
+ provider;
45
+ - inserts one `better-copilot` row whose config names a `github-copilot` provider
46
+ profile (an empty profile, `{}`) plus default `search`/`toolSchema` settings.
47
+
48
+ ## Configuration
49
+
50
+ ```yaml
51
+ - id: better-copilot
52
+ name: dsh-better-copilot
53
+ config:
54
+ providers:
55
+ # The pi-ai adapter's own provider profile: baseURL, apiKeyEnv, models, ...
56
+ github-copilot: {}
57
+ search:
58
+ model: gpt-6.1-sol
59
+ maxTokens: 4096
60
+ timeoutMs: 60000
61
+ maxResponseBytes: 2097152
62
+ includeSources: true
63
+ searchMode: default
64
+ forceSearch: false
65
+ # apiKeyEnv / baseURL are optional credential and endpoint overrides
66
+ toolSchema:
67
+ providers: [github-copilot]
68
+ families: [gemini]
69
+ logFixes: false
70
+ ```
71
+
72
+ `providers` is the pi-ai adapter's own schema, reused unchanged, so every
73
+ provider setting behaves exactly as it does with the stock adapter. `search`
74
+ configures the web-search provider; `toolSchema` configures the Gemini guard.
75
+
76
+ Loader patches replace a row's `config` wholesale, not key by key: an override
77
+ must restate `providers`, `search` and `toolSchema` together.
78
+
79
+ ## Wire identity
80
+
81
+ While mounted, the plugin installs a refcounted `fetch` header interceptor that
82
+ rewrites Copilot request headers to a VS Code/`copilot-chat` identity. Set
83
+ `DSH_COPILOT_WIRE_IDENTITY=basic` to avoid local VS Code IDs, or `0` to disable
84
+ the interceptor entirely.
85
+
86
+ ## Search
87
+
88
+ Search uses the stored `llm-pi-ai/github-copilot` credential — inference may use
89
+ either the stock adapter or this wrapper. Sign in through DSH's normal Copilot
90
+ flow. `web_search` is an ordinary tool independent of the conversation model; the
91
+ existing Web search settings card is DeepSeek-specific and is not a chooser.
92
+
93
+ ## Guard
94
+
95
+ The guard is an additive `system-prompt/assemble` listener. For a route whose
96
+ provider is listed and whose model id contains one of the configured families it
97
+ adds an empty root `properties` bucket to object tool schemas that lack one, and
98
+ leaves every other schema byte-identical. It never replaces an adapter or
99
+ another plugin's settings.
100
+
101
+ ## Migration from the three predecessor packages
102
+
103
+ | Was | Now |
104
+ | --- | --- |
105
+ | package `dsh-copilot-catalog`, row `llm-pi-ai-catalog` | package `dsh-better-copilot`, row `better-copilot` |
106
+ | package `dsh-copilot-search`, row `web-search-copilot` | same row `better-copilot`; `model` became `search.model` |
107
+ | package `dsh-copilot-tool-schema`, row `copilot-tool-schema` | same row `better-copilot`; guard fields moved under `toolSchema.*` |
108
+ | `@deepseek-ai/dsh-llm-pi-ai` disabled by the catalog bundle | still disabled by this bundle |
109
+ | `web.searchProvider: github-copilot-search` | still selected by this bundle |
110
+
111
+ Subpath exports also changed: the predecessor specifiers `…/provider`,
112
+ `…/responses`, `…/auth`, `…/plugin`, `…/schema` and `…/types` are gone. Use the
113
+ merged package root, `dsh-better-copilot/catalog`, `/discovery`, `/identity`,
114
+ `/search` and `/guard` instead. The colliding `CopilotAuthOptions` type is
115
+ exported as `CatalogAuthOptions` and `SearchAuthOptions`.
116
+
117
+ The predecessor npm versions remain published; this repository no longer builds
118
+ them and treats them as superseded.
119
+
120
+ ## Limits
121
+
122
+ - Discovery is one read-only, bounded startup request; failures keep the bundled
123
+ catalog and log only a controlled reason code.
124
+ - Search requests never forward credentials to redirects or untrusted origins.
125
+ - The guard only supplies a missing `properties` bucket; it does not rewrite
126
+ references, unions or nested schemas.
127
+
128
+ The full field mapping is: the search plugin's row-level `apiKeyEnv`, `baseURL`,
129
+ `model`, `maxTokens`, `timeoutMs`, `maxResponseBytes`, `includeSources`,
130
+ `searchMode` and `forceSearch` are now `search.*`; the guard's `providers`,
131
+ `families` and `logFixes` are now `toolSchema.*`; and every adapter provider
132
+ setting stays under `providers.*` unchanged.
@@ -0,0 +1,21 @@
1
+ # This bundle replaces the pi-ai adapter and adds the Copilot search and guard
2
+ # behaviours: never enable another adapter for the same route.
3
+ # Copy any other provider configuration into the replacement before enabling.
4
+ - id: llm-pi-ai
5
+ name: '@deepseek-ai/dsh-llm-pi-ai'
6
+ disabled: true
7
+ # Select the search provider explicitly: the base profile pins DeepSeek search.
8
+ # This does not replace an LLM adapter or change its model/provider configuration.
9
+ - id: web
10
+ name: '@deepseek-ai/dsh-web'
11
+ config:
12
+ searchProvider: github-copilot-search
13
+ fetchProvider: http
14
+ - insert:
15
+ - id: better-copilot
16
+ name: dsh-better-copilot
17
+ config:
18
+ providers:
19
+ github-copilot: {}
20
+ search: {}
21
+ toolSchema: {}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { type CredentialKey, type CredentialProvider } from '@deepseek-ai/dsh-credentials';
2
+ import { WebError } from '@deepseek-ai/dsh-web';
3
+ import type { CredentialStore } from '@earendil-works/pi-ai';
4
+ import type { CopilotAuthOptions, ResolvedCopilotAuth, SearchContext, SearchRuntime } from './search-types.js';
5
+ /** Caller cancellation of the refresh in progress on this async chain, if any. */
6
+ export declare function callerSignal(): AbortSignal | undefined;
7
+ export type CopilotCredentialService = Pick<CredentialProvider, 'readRecord' | 'modifyRecord' | 'resolve'>;
8
+ declare const AUTH_FAILURES: {
9
+ readonly aborted: readonly ["Copilot search authentication aborted", "WEB_ABORTED"];
10
+ readonly invalid: readonly ["The stored Copilot credential is invalid; sign in again", "WEB_PROVIDER_AUTH_INVALID"];
11
+ readonly writeRefused: readonly ["Copilot search only persists refreshes of existing OAuth grants", "WEB_PROVIDER_AUTH_WRITE_REFUSED"];
12
+ readonly badReference: readonly ["Invalid Copilot credential reference", "WEB_PROVIDER_AUTH_INVALID"];
13
+ readonly missingReference: readonly ["The configured Copilot credential reference is unset", "WEB_PROVIDER_CREDENTIAL_MISSING"];
14
+ readonly badToken: readonly ["Copilot authentication returned an unusable token", "WEB_PROVIDER_AUTH_INVALID"];
15
+ readonly missingEndpoint: readonly ["An opaque Copilot API key requires an explicit trusted baseURL", "WEB_PROVIDER_ENDPOINT_MISSING"];
16
+ readonly failed: readonly ["Copilot search authentication failed; check credentials or sign in again", "WEB_PROVIDER_AUTH_ERROR"];
17
+ };
18
+ type AuthFailure = keyof typeof AUTH_FAILURES;
19
+ /** Closed messages, even if an injected dependency constructs or mutates an owned error. */
20
+ export declare class CopilotAuthError extends WebError {
21
+ readonly kind: AuthFailure;
22
+ constructor(kind: AuthFailure);
23
+ }
24
+ /**
25
+ * Share the adapter's serialized, cross-process record lock for refresh only.
26
+ * Search has no login/logout capability and cannot create or replace API-key records.
27
+ */
28
+ export declare function createCopilotCredentialStore(credentials: Pick<CredentialProvider, 'readRecord' | 'modifyRecord'> | undefined, key: CredentialKey, caller?: AbortSignal): CredentialStore;
29
+ /** Public pi-ai auth resolution on the same DSH credential record used for inference. */
30
+ export declare function createCopilotAuthResolver(ctx: SearchContext, runtime: SearchRuntime): (options: CopilotAuthOptions, signal: AbortSignal) => Promise<ResolvedCopilotAuth | undefined>;
31
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,232 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { credentialRef, isCredentialRefName } from '@deepseek-ai/dsh-credentials';
3
+ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
4
+ import { WebError } from '@deepseek-ai/dsh-web';
5
+ import { CopilotEndpointError, trustedCopilotOrigin } from './endpoint.js';
6
+ const PROVIDER = 'github-copilot';
7
+ /** The installed SDK's refresh hook receives its own signal, never the caller's.
8
+ * Carry the caller's cancellation across the public getAuth() call so a cancelled
9
+ * operation cannot dispatch later requests or commit a refreshed grant.
10
+ */
11
+ const callerSignals = new AsyncLocalStorage();
12
+ /** Caller cancellation of the refresh in progress on this async chain, if any. */
13
+ export function callerSignal() { return callerSignals.getStore(); }
14
+ const AUTH_FAILURES = {
15
+ aborted: ['Copilot search authentication aborted', 'WEB_ABORTED'],
16
+ invalid: ['The stored Copilot credential is invalid; sign in again', 'WEB_PROVIDER_AUTH_INVALID'],
17
+ writeRefused: ['Copilot search only persists refreshes of existing OAuth grants', 'WEB_PROVIDER_AUTH_WRITE_REFUSED'],
18
+ badReference: ['Invalid Copilot credential reference', 'WEB_PROVIDER_AUTH_INVALID'],
19
+ missingReference: ['The configured Copilot credential reference is unset', 'WEB_PROVIDER_CREDENTIAL_MISSING'],
20
+ badToken: ['Copilot authentication returned an unusable token', 'WEB_PROVIDER_AUTH_INVALID'],
21
+ missingEndpoint: ['An opaque Copilot API key requires an explicit trusted baseURL', 'WEB_PROVIDER_ENDPOINT_MISSING'],
22
+ failed: ['Copilot search authentication failed; check credentials or sign in again', 'WEB_PROVIDER_AUTH_ERROR'],
23
+ };
24
+ /** Closed messages, even if an injected dependency constructs or mutates an owned error. */
25
+ export class CopilotAuthError extends WebError {
26
+ kind;
27
+ constructor(kind) {
28
+ const safeKind = typeof kind === 'string' && Object.hasOwn(AUTH_FAILURES, kind) ? kind : 'failed';
29
+ const [message, code] = AUTH_FAILURES[safeKind];
30
+ super(message, code);
31
+ this.kind = safeKind;
32
+ }
33
+ }
34
+ function aborted(signal) {
35
+ if (signal?.aborted)
36
+ throw new CopilotAuthError('aborted');
37
+ }
38
+ function invalidCredential() {
39
+ throw new CopilotAuthError('invalid');
40
+ }
41
+ function oauthCredential(value) {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
43
+ return invalidCredential();
44
+ const grant = value;
45
+ if (grant.type !== 'oauth' || typeof grant.access !== 'string' || !grant.access.trim() || grant.access.length > 16_384 ||
46
+ typeof grant.refresh !== 'string' || !grant.refresh.trim() || grant.refresh.length > 16_384 ||
47
+ /[\u0000-\u0020\u007f-\u009f]/u.test(grant.access) || /[\u0000-\u0020\u007f-\u009f]/u.test(grant.refresh) ||
48
+ typeof grant.expires !== 'number' || !Number.isFinite(grant.expires) || grant.expires < 0)
49
+ return invalidCredential();
50
+ return structuredClone(grant);
51
+ }
52
+ /** Read only the credential family owned by the original pi-ai adapter. */
53
+ function fromRecord(record) {
54
+ if (record === undefined)
55
+ return undefined;
56
+ if (record.kind === 'grant')
57
+ return oauthCredential(record.payload);
58
+ if (record.kind !== 'api-key')
59
+ return invalidCredential();
60
+ if (record.key !== undefined && (typeof record.key !== 'string' || !record.key.trim()))
61
+ return invalidCredential();
62
+ if (record.env !== undefined && (typeof record.env !== 'object' || record.env === null ||
63
+ Array.isArray(record.env) || Object.entries(record.env).some(([key, value]) => !isCredentialRefName(key) || typeof value !== 'string'))) {
64
+ return invalidCredential();
65
+ }
66
+ return {
67
+ type: 'api_key',
68
+ ...(record.key === undefined ? {} : { key: record.key }),
69
+ ...(record.env === undefined ? {} : { env: { ...record.env } }),
70
+ };
71
+ }
72
+ /** Match the adapter's JSON image without laundering nonfinite values or foreign prototypes. */
73
+ function jsonImage(value) {
74
+ if (Array.isArray(value))
75
+ return value.map((entry) => entry === undefined ? null : jsonImage(entry));
76
+ if (value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
77
+ return Object.fromEntries(Object.entries(value).filter(([, member]) => member !== undefined)
78
+ .map(([key, member]) => [key, jsonImage(member)]));
79
+ }
80
+ return value;
81
+ }
82
+ /**
83
+ * Share the adapter's serialized, cross-process record lock for refresh only.
84
+ * Search has no login/logout capability and cannot create or replace API-key records.
85
+ */
86
+ export function createCopilotCredentialStore(credentials, key, caller) {
87
+ // Checked before the transaction and again before it commits; the SDK's refresh hook
88
+ // no longer receives the caller's signal, so cancellation must be honoured here.
89
+ const cancelled = () => caller?.aborted === true || callerSignal()?.aborted === true;
90
+ const read = async (providerId, options) => {
91
+ aborted(options?.signal);
92
+ if (providerId !== PROVIDER)
93
+ return undefined;
94
+ const record = await credentials?.readRecord(key);
95
+ aborted(options?.signal);
96
+ return fromRecord(record);
97
+ };
98
+ return {
99
+ read,
100
+ async list(options) {
101
+ const current = await read(PROVIDER, options);
102
+ return current === undefined ? [] : [{ providerId: PROVIDER, type: current.type }];
103
+ },
104
+ async modify(providerId, mutate, options) {
105
+ aborted(options?.signal);
106
+ if (providerId !== PROVIDER || credentials === undefined) {
107
+ throw new CopilotAuthError('writeRefused');
108
+ }
109
+ const record = await credentials.modifyRecord(key, async (current) => {
110
+ // DSH's lock wait is not cancellable: check again once it actually acquired the lock.
111
+ aborted(options?.signal);
112
+ // The SDK's refresh hook no longer receives the caller's signal, so a refresh that
113
+ // finished after cancellation must not commit through this managed record write.
114
+ if (cancelled())
115
+ throw new CopilotAuthError('aborted');
116
+ const previous = fromRecord(current);
117
+ // Capture eligibility and account authority before passing a mutable clone to the SDK.
118
+ const wasOAuth = current?.kind === 'grant' && previous?.type === 'oauth';
119
+ const refreshOwner = previous?.type === 'oauth' ? previous.refresh : undefined;
120
+ const enterpriseOwner = previous?.type === 'oauth' ? previous.enterpriseUrl : undefined;
121
+ const next = await mutate(previous);
122
+ aborted(options?.signal);
123
+ // The SDK calls this mutator with the refresh inside it, so gate the commit
124
+ // itself: a refresh that finished after caller cancellation must not persist.
125
+ if (cancelled())
126
+ throw new CopilotAuthError('aborted');
127
+ if (next === undefined)
128
+ return undefined;
129
+ if (!wasOAuth || next.type !== 'oauth')
130
+ throw new CopilotAuthError('writeRefused');
131
+ const refreshed = oauthCredential(next);
132
+ if (refreshed.refresh !== refreshOwner || refreshed.enterpriseUrl !== enterpriseOwner) {
133
+ throw new CopilotAuthError('writeRefused');
134
+ }
135
+ const payload = jsonImage(refreshed);
136
+ aborted(options?.signal);
137
+ return { kind: 'grant', payload };
138
+ });
139
+ aborted(options?.signal);
140
+ return fromRecord(record);
141
+ },
142
+ async delete() {
143
+ throw new CopilotAuthError('writeRefused');
144
+ },
145
+ };
146
+ }
147
+ /** Observe late settlement while caller cancellation releases the request promptly. */
148
+ function abortable(operation, signal) {
149
+ return new Promise((resolve, reject) => {
150
+ const onAbort = () => reject(new CopilotAuthError('aborted'));
151
+ signal.addEventListener('abort', onAbort, { once: true });
152
+ operation.then((value) => {
153
+ signal.removeEventListener('abort', onAbort);
154
+ resolve(value);
155
+ }, (error) => {
156
+ signal.removeEventListener('abort', onAbort);
157
+ reject(error);
158
+ });
159
+ if (signal.aborted)
160
+ onAbort();
161
+ });
162
+ }
163
+ /** Public pi-ai auth resolution on the same DSH credential record used for inference. */
164
+ export function createCopilotAuthResolver(ctx, runtime) {
165
+ return async (options, signal) => {
166
+ aborted(signal);
167
+ const work = async () => {
168
+ const explicitOrigin = options.baseURL === undefined ? undefined : trustedCopilotOrigin(options.baseURL);
169
+ if (options.apiKeyEnv !== undefined && !isCredentialRefName(options.apiKeyEnv)) {
170
+ throw new CopilotAuthError('badReference');
171
+ }
172
+ // Snapshot both storage and launch environment for this operation, not startup.
173
+ const credentials = ctx.get('credentials');
174
+ const environment = launchEnvironmentOf(ctx);
175
+ const readReference = async (name) => {
176
+ aborted(signal);
177
+ if (!isCredentialRefName(name))
178
+ return undefined;
179
+ const hit = await credentials?.resolve(credentialRef(name));
180
+ aborted(signal);
181
+ return hit?.value ?? environment.get(name)?.value;
182
+ };
183
+ const apiKey = options.apiKeyEnv === undefined ? undefined : await readReference(options.apiKeyEnv);
184
+ aborted(signal);
185
+ if (options.apiKeyEnv !== undefined && (typeof apiKey !== 'string' || !apiKey.trim())) {
186
+ throw new CopilotAuthError('missingReference');
187
+ }
188
+ const models = runtime.createModels({
189
+ credentials: createCopilotCredentialStore(credentials, runtime.recordKeyFor(PROVIDER), signal),
190
+ authContext: { env: readReference, fileExists: async () => false },
191
+ });
192
+ models.setProvider(runtime.provider);
193
+ const resolved = await callerSignals.run(signal, () => models.getAuth(PROVIDER, { signal, ...(apiKey === undefined ? {} : { apiKey }) }));
194
+ aborted(signal);
195
+ if (resolved === undefined)
196
+ return undefined;
197
+ const token = resolved.auth.apiKey;
198
+ if (typeof token !== 'string' || !token.trim() || token.length > 16_384 || /[\u0000-\u0020\u007f-\u009f]/u.test(token)) {
199
+ throw new CopilotAuthError('badToken');
200
+ }
201
+ // OAuth's account endpoint owns routing; an explicit origin is an API-key setting only.
202
+ let baseUrl = resolved.auth.baseUrl ?? explicitOrigin;
203
+ if (baseUrl === undefined && /(?:^|;)proxy-ep=[^;]+/u.test(token)) {
204
+ const derived = await runtime.oauth.toAuth({ type: 'oauth', access: token, refresh: '', expires: 0 });
205
+ aborted(signal);
206
+ baseUrl = derived.baseUrl;
207
+ }
208
+ if (baseUrl === undefined) {
209
+ throw new CopilotAuthError('missingEndpoint');
210
+ }
211
+ const origin = trustedCopilotOrigin(baseUrl);
212
+ aborted(signal);
213
+ return { apiKey: token, baseUrl: origin, headers: runtime.headers };
214
+ };
215
+ try {
216
+ return await abortable(work(), signal);
217
+ }
218
+ catch (error) {
219
+ aborted(signal);
220
+ // Error classification and getters can themselves throw; never expose that exception.
221
+ let controlled;
222
+ try {
223
+ if (error instanceof CopilotAuthError)
224
+ controlled = new CopilotAuthError(error.kind);
225
+ else if (error instanceof CopilotEndpointError)
226
+ controlled = new CopilotEndpointError();
227
+ }
228
+ catch { /* malformed host dependency errors degrade to the fixed auth failure */ }
229
+ throw controlled ?? new CopilotAuthError('failed');
230
+ }
231
+ };
232
+ }
@@ -0,0 +1,54 @@
1
+ import type { Api, Model, OAuthAuth, OAuthCredential } from '@earendil-works/pi-ai';
2
+ export type OriginalAdapter = Pick<typeof import('@deepseek-ai/dsh-llm-pi-ai'), 'name' | 'Config' | 'inject' | 'apply' | 'recordKeyFor'>;
3
+ export type PluginContext = Parameters<OriginalAdapter['apply']>[0];
4
+ export type PluginConfig = Parameters<OriginalAdapter['apply']>[1];
5
+ export type CredentialKey = ReturnType<OriginalAdapter['recordKeyFor']>;
6
+ export type ModelCatalog = Record<string, Model<Api>>;
7
+ export type CopilotOAuth = Pick<OAuthAuth, 'refresh' | 'toAuth'>;
8
+ export type CopilotGrant = OAuthCredential;
9
+ /** Only the read seam is accepted; discovery has no credential write capability. */
10
+ export interface CredentialReader {
11
+ readRecord(key: CredentialKey): Promise<unknown>;
12
+ }
13
+ export interface AccountSelection {
14
+ models: ModelCatalog;
15
+ unsupported: string[];
16
+ }
17
+ export type SyncResult = {
18
+ status: 'no Copilot credential';
19
+ } | {
20
+ status: 'synced';
21
+ count: number;
22
+ unsupported: string[];
23
+ };
24
+ export interface CopilotAuthOptions {
25
+ /** Explicit API-key route endpoint; OAuth grants still own their derived endpoint. */
26
+ baseUrl?: string;
27
+ /** An explicit apiKeyEnv reference wins over the stored account grant, as in pi-ai. */
28
+ resolveApiKey?: () => Promise<string | undefined>;
29
+ /** Read-only ambient lookup, consulted only when no OAuth grant owns the route. */
30
+ resolveAmbientApiKey?: () => Promise<string | undefined>;
31
+ }
32
+ export interface DiscoveryOptions extends CopilotAuthOptions {
33
+ catalog: ModelCatalog;
34
+ oauth: CopilotOAuth;
35
+ recordKey: CredentialKey;
36
+ fetcher?: typeof globalThis.fetch;
37
+ timeoutMs?: number;
38
+ now?: () => number;
39
+ /** Routing may adopt a validated account endpoint even if model listing later fails. */
40
+ onEndpoint?: (baseUrl: string) => void;
41
+ }
42
+ export interface PluginDependencies extends Omit<DiscoveryOptions, 'recordKey'> {
43
+ original: OriginalAdapter;
44
+ /** Test seam for the process-wide wire-identity installer; defaults to the real interceptor. */
45
+ installIdentity?: () => () => void;
46
+ }
47
+ export interface CatalogPlugin {
48
+ name: OriginalAdapter['name'];
49
+ Config: OriginalAdapter['Config'];
50
+ inject: string[];
51
+ sync(credentials?: CredentialReader, authOptions?: CopilotAuthOptions): Promise<SyncResult>;
52
+ apply(ctx: PluginContext, config: PluginConfig): Promise<void>;
53
+ }
54
+ export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
@@ -0,0 +1,4 @@
1
+ export function asRecord(value) {
2
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ ? value : undefined;
4
+ }
@@ -0,0 +1,15 @@
1
+ import type { ModelCost } from '@earendil-works/pi-ai';
2
+ import { type AccountSelection, type ModelCatalog } from './catalog-types.js';
3
+ export declare const DEFAULT_COPILOT_BASE_URL = "https://api.individual.githubcopilot.com";
4
+ export interface AccountModelOptions {
5
+ /** Trusted account API root. Startup discovery supplies the validated OAuth origin. */
6
+ baseUrl?: string;
7
+ }
8
+ /** Copy only provider identity headers; never inherit Authorization or API keys. */
9
+ export declare function copilotHeaders(catalog: ModelCatalog): Record<string, string>;
10
+ /** Convert cents per million tokens only when every required price is valid. */
11
+ export declare function liveCost(billing: unknown, fallback: ModelCost): ModelCost;
12
+ /** Pure selection: preserve known descriptors; build any new ID from endpoint/capability metadata. */
13
+ export declare function selectAccountModels(response: unknown, catalog: ModelCatalog, { baseUrl, }?: AccountModelOptions): AccountSelection;
14
+ /** Validate mutability before touching a shared upstream catalog. */
15
+ export declare function replaceCatalog(catalog: ModelCatalog, models: ModelCatalog): void;