dsh-copilot-catalog 0.0.0-stage → 0.2.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,33 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — General discovery and Enterprise routing
4
+
5
+ - Remove the model-ID allowlist and sibling templates. Construct any newly
6
+ discovered chat model from its advertised endpoint, limits, vision, reasoning,
7
+ and billing metadata.
8
+ - Support Responses, Chat Completions, and native Anthropic Messages with
9
+ conservative protocol defaults and deterministic endpoint preference.
10
+ - Stop guessing another model's pricing or reasoning capabilities. Preserve
11
+ upstream descriptors for IDs the bundled catalog already knows.
12
+ - Remove discovery's dependency on a particular model's headers.
13
+ - Derive Enterprise endpoints for OAuth grants and API-key access tokens, honor
14
+ explicit API-key URLs/references, and overlay routing defaults in memory for
15
+ new and bundled models even when listing fails.
16
+ - Add actual DSH-adapter/SDK discovery and inference regressions with synthetic
17
+ Enterprise credentials and SSE responses; no live account calls or secret writes.
18
+
19
+ ## 0.1.0 — Initial release preparation
20
+
21
+ - Convert the local Copilot catalog wrapper to strict TypeScript with ESM output
22
+ and generated type declarations.
23
+ - Resolve the original adapter and its own pi-ai catalog using installed npm peers,
24
+ without platform-specific application paths or private OAuth imports.
25
+ - Preserve upstream configuration, credential ownership, and model descriptors.
26
+ - Add two explicit Responses-compatible sibling templates and account filtering.
27
+ - Bound discovery end-to-end and prevent late responses from committing.
28
+ - Harden catalog lookups, HTTP endpoint/redirect handling, and diagnostics.
29
+ - Add DSH bundle metadata, regression tests, peer integration tests, npm package
30
+ allowlist checks, cross-platform CI, and a trusted-publishing release workflow.
31
+
32
+ These entries describe prepared versions; source is available on GitHub, but no
33
+ npm publication is implied.
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,332 @@
1
- # Temporary Holding Version
1
+ # dsh-copilot-catalog
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
+ Account-aware **GitHub Copilot model discovery for DeepSeek Harness (DSH)**.
4
+ Written in strict TypeScript; published as ESM JavaScript with type declarations.
5
+
6
+ This experimental, third-party plugin wraps `@deepseek-ai/dsh-llm-pi-ai`. Before the
7
+ adapter mounts, it reads your existing Copilot OAuth grant, fetches your account's
8
+ `/models` catalog, and updates the **same in-memory pi-ai catalog** the adapter uses.
9
+ No hard-coded application paths, credential-file edits, or signed-app modifications.
10
+
11
+ ## What it does
12
+
13
+ - Shows picker-enabled models whose policy is enabled or absent and which do not
14
+ explicitly reject tool calls.
15
+ - Preserves upstream descriptors for known IDs, including mixed API protocols.
16
+ - Builds descriptors for **any newly discovered model ID**, without a model-name
17
+ allowlist, sibling template, or vendor-name heuristic. Transport selection uses
18
+ the advertised `supported_endpoints`:
19
+
20
+ | Advertised endpoint | pi-ai transport |
21
+ | --- | --- |
22
+ | `/responses` | `openai-responses` |
23
+ | `/v1/messages` | `anthropic-messages` |
24
+ | `/chat/completions` | `openai-completions` |
25
+
26
+ When multiple supported endpoints are offered, preference is Responses, native
27
+ Messages, then Chat Completions, independent of array order or model name.
28
+ - Uses live positive context/output limits, vision support, advertised reasoning
29
+ efforts, and validated pricing for new models. Only unknown protocols, invalid
30
+ request IDs, non-chat/non-streaming models, or insufficient limits are skipped.
31
+ Missing pricing uses zero-valued **unknown rates**, not another model's prices.
32
+ - Keeps **Enterprise routing for discovery and inference**, including known
33
+ bundled models and API-key auth. A validated account endpoint is applied as an
34
+ in-memory provider default even if the model listing later fails. Explicit
35
+ configured API-key endpoints are preserved; no profile setting is rewritten.
36
+ - Leaves the current catalog untouched if discovery fails or yields no supported
37
+ models. On first startup this means all bundled defaults remain available.
38
+ - Bounds the whole attempt to **10 seconds**, including credential reads, refresh,
39
+ auth resolution, fetch, and body parsing. Timed-out work cannot commit later.
40
+ - Makes no credential-store writes. Expiring grants are refreshed only in memory;
41
+ the original adapter owns normal persisted refresh and its cross-process lock.
42
+ - Requires HTTPS Copilot subdomains, refuses redirects and unusual ports, and
43
+ logs controlled error codes instead of potentially sensitive exception messages.
44
+
45
+ ## Compatibility
46
+
47
+ | Component | Supported version |
48
+ | --- | --- |
49
+ | DSH and `@deepseek-ai/dsh-llm-pi-ai` | `0.2.0-rc.2` |
50
+ | `@earendil-works/pi-ai` | `0.87.1` |
51
+ | Node.js | `>=22.19.0` |
52
+ | Runtime format | ESM only; no CommonJS entry |
53
+
54
+ DSH and pi-ai are **peer dependencies**, not vendored copies. Runtime resolution
55
+ starts from the adapter's location, so nested or pnpm layouts do not cause the
56
+ plugin to update a different pi-ai instance. Type-only imports are erased by the
57
+ compiler. The public Copilot provider supplies OAuth hooks; private auth modules
58
+ are not imported. Version ranges are deliberately conservative: verify a new DSH
59
+ or pi-ai version before widening them.
60
+
61
+ ## Enterprise authentication and endpoint routing
62
+
63
+ This plugin also works around pi-ai's API-key path: an explicit API key bypasses
64
+ OAuth `toAuth()`, so upstream otherwise retains the bundled Individual endpoint.
65
+ The wrapper discovers an account endpoint and supplies it to the original adapter
66
+ as a **runtime-only `baseURL` default**, covering new and bundled models.
67
+
68
+ - **Stored Copilot OAuth grant:** use the provider's public `toAuth()` to derive
69
+ the account endpoint. The original adapter still owns persisted refresh and
70
+ derives request auth normally; Enterprise grants do not become Individual keys.
71
+ - **Copilot access token supplied as an API key:** use its `proxy-ep` metadata to
72
+ derive the endpoint through the same read-only OAuth hook. This applies to stored
73
+ API-key records, explicit `apiKeyEnv` references, and ambient `COPILOT_GITHUB_TOKEN`.
74
+ An explicit reference wins over a different stored OAuth account, as in pi-ai.
75
+ - **Opaque token:** no account endpoint can be inferred safely. Set the existing
76
+ provider `baseURL` field explicitly, or use DSH's normal Copilot OAuth sign-in.
77
+ Discovery reports `MISSING_ENDPOINT` rather than assuming the Individual API.
78
+ - **GitHub PAT:** it is not a Copilot access token. The wrapper does not guess the
79
+ Enterprise GitHub domain or implement a second sign-in/exchange flow; use OAuth.
80
+
81
+ For an explicit Enterprise API-key route, use a credential reference, not a literal
82
+ key in your profile:
83
+
84
+ ```yaml
85
+ - id: llm-pi-ai-catalog
86
+ name: dsh-copilot-catalog
87
+ config:
88
+ providers:
89
+ github-copilot:
90
+ apiKeyEnv: COPILOT_ACCESS_TOKEN
91
+ baseURL: https://api.enterprise.githubcopilot.com
92
+ ```
93
+
94
+ Use the endpoint belonging to **your** account; do not assume Enterprise seats all
95
+ share a GitHub Enterprise Server domain. Discovery only trusts HTTPS
96
+ `*.githubcopilot.com` origins. Inferred API-key routing is a startup snapshot;
97
+ restart after changing accounts or moving a key to a different endpoint. If no
98
+ endpoint can be identified, the original adapter remains mounted with its original
99
+ configuration, so an opaque Enterprise key still needs an explicit `baseURL`.
100
+
101
+ ## Installation
102
+
103
+ > **Adapter replacement, not an add-on adapter.** The included DSH bundle disables
104
+ > the standard `llm-pi-ai` entry and inserts `llm-pi-ai-catalog`, configured for
105
+ > `github-copilot`. Do not enable another adapter for the same provider route.
106
+ >
107
+ > **Existing custom routes/settings are not copied automatically.** Before enabling
108
+ > the bundle, back up your profile patch and copy every provider and override you
109
+ > want to retain to the replacement entry. Its settings namespace is
110
+ > `llm-pi-ai-catalog`; your OAuth grant keeps the original `llm-pi-ai/github-copilot`
111
+ > key. A later profile/home patch that re-enables the original adapter must be fixed.
112
+
113
+ ### From npm, after publication
114
+
115
+ Install the bundle through DSH's Plugins page, or use the CLI for your chosen
116
+ profile (for example `desktop` or `web`):
117
+
118
+ ```sh
119
+ dsh plugin --profile <profile> add dsh-copilot-catalog
120
+ ```
121
+
122
+ Restart DSH after installing or changing this host-side plugin. The code has no
123
+ browser component and does not modify or rebuild DSH's web UI.
124
+
125
+ ### From GitHub, after pushing this repository
126
+
127
+ The Git repository includes the TypeScript source and build configuration. Its
128
+ `prepare` script builds the ESM output when the package manager installs from Git.
129
+ Use your actual repository URL in DSH's Plugins page or the CLI:
130
+
131
+ ```sh
132
+ dsh plugin --profile <profile> add 'git+https://github.com/1yefuwang1/dsh-copilot-catalog.git'
133
+ ```
134
+
135
+ ### Local development
136
+
137
+ ```sh
138
+ npm ci --ignore-scripts
139
+ npm run build
140
+ npm run verify
141
+ # Only if you choose to enable it in an actual DSH profile:
142
+ dsh plugin --profile <profile> add -w .
143
+ ```
144
+
145
+ Local linking affects that profile; it is not part of the automated test suite.
146
+ Neither this repository nor its tests automatically edit your active profile.
147
+
148
+ ### Preserve additional provider configuration
149
+
150
+ Add a higher-priority override to your own profile patch before enabling the bundle.
151
+ Copy the **full provider configuration** from your original adapter, including
152
+ custom URLs, models, headers and overrides, as needed:
153
+
154
+ ```yaml
155
+ - id: llm-pi-ai-catalog
156
+ name: dsh-copilot-catalog
157
+ config:
158
+ providers:
159
+ github-copilot: {}
160
+ openai:
161
+ apiKeyEnv: OPENAI_API_KEY
162
+ ```
163
+
164
+ All original adapter configuration fields remain valid. The wrapper exports the
165
+ original `Config` schema unchanged; it adds no secret or custom discovery settings.
166
+ An inferred endpoint is overlaid only while the adapter reads the configuration;
167
+ settings persistence, explicit URLs, and other providers are left untouched.
168
+ If your original adapter has a different entry ID, adjust the disabling override
169
+ in your profile accordingly.
170
+
171
+ ### Check startup
172
+
173
+ Successful discovery logs `Copilot catalog synced` with the supported model count
174
+ and JSON-escaped unsupported IDs. Failures log one of `TIMEOUT`, `HTTP_ERROR`,
175
+ `INVALID_CATALOG`, `UNTRUSTED_ENDPOINT`, `INVALID_AUTH`, `MISSING_ENDPOINT`,
176
+ `IMMUTABLE_CATALOG`, or `DISCOVERY_FAILED`; startup still delegates to the original
177
+ adapter. Missing Copilot credentials keep the defaults. A validated runtime
178
+ endpoint is logged separately without any token or credential value. Sign in through DSH's normal Copilot authorization
179
+ flow, then restart to perform account discovery.
180
+
181
+ ### Rollback
182
+
183
+ Disable or remove this bundle with DSH's Plugins page, remove any profile overrides
184
+ that still enable the replacement, and re-enable the standard `llm-pi-ai` entry if
185
+ your own profile has disabled it. Restore your backed-up provider configuration and
186
+ restart the **whole DSH process** to reset its in-memory catalog. Do not delete the
187
+ OAuth grant. The wrapper does not patch the signed application or store a catalog
188
+ on disk.
189
+
190
+ ## Development and tests
191
+
192
+ ```sh
193
+ npm ci --ignore-scripts
194
+ npm run check:secrets # scan Git candidates; never print suspected secret values
195
+ npm run typecheck # strict TypeScript, no emit
196
+ npm run build # dist/*.js plus dist/*.d.ts
197
+ npm test # synthetic unit/lifecycle/security regression tests
198
+ npm run test:integration # real npm peers; no account or network requests
199
+ npm run test:types # a strict TS consumer checks the published declarations
200
+ npm run test:pack # exact npm tarball allowlist, including declarations
201
+ npm run verify # all of the above
202
+ npm pack --dry-run
203
+ ```
204
+
205
+ The tests use synthetic credentials and response/SSE fixtures. They cover arbitrary
206
+ new IDs, three advertised protocols, prototype-sensitive IDs, reasoning metadata,
207
+ unknown pricing, read-only refresh, endpoint restrictions, deadlines, late responses,
208
+ fail-open delegation, and safe diagnostics. Integration tests mount the **actual DSH
209
+ adapter** and exercise real SDK request construction for OAuth Enterprise grants,
210
+ API-key overrides, stored access tokens, explicit URLs, and failed model listing.
211
+ Every mocked discovery/inference request is asserted to target the Enterprise origin.
212
+ No live Copilot request or authentication flow is run automatically.
213
+
214
+ - [`src/index.ts`](src/index.ts): DSH entry point.
215
+ - [`src/runtime.ts`](src/runtime.ts): shared adapter/pi-ai resolution.
216
+ - [`src/catalog.ts`](src/catalog.ts): pure selection and pricing.
217
+ - [`src/discovery.ts`](src/discovery.ts): bounded read-only discovery.
218
+ - [`src/plugin.ts`](src/plugin.ts): adapter lifecycle and safe logging.
219
+ - [`src/types.ts`](src/types.ts): public interfaces and upstream type integration.
220
+
221
+ For scripts that only need model selection, the `dsh-copilot-catalog/catalog`
222
+ subpath is side-effect-free. Pass an explicit bundled catalog:
223
+
224
+ ```ts
225
+ import { selectAccountModels } from 'dsh-copilot-catalog/catalog';
226
+ import { GITHUB_COPILOT_MODELS } from '@earendil-works/pi-ai/providers/github-copilot.models';
227
+
228
+ const { models, unsupported } = selectAccountModels(response, GITHUB_COPILOT_MODELS);
229
+ ```
230
+
231
+ The package root loads the adapter peers but does not read credentials or perform
232
+ network I/O until `apply()` or `syncCopilotCatalog()` is called. The
233
+ `dsh-copilot-catalog/discovery` helper takes explicit injected dependencies for tests
234
+ and tooling.
235
+
236
+ ## Publish to GitHub and npm
237
+
238
+ The package name is `dsh-copilot-catalog`; availability can change before npm
239
+ publication. The GitHub repository is
240
+ [`1yefuwang1/dsh-copilot-catalog`](https://github.com/1yefuwang1/dsh-copilot-catalog),
241
+ and its repository, homepage, and issue-tracker metadata are configured. Personal
242
+ author details remain intentionally unset.
243
+
244
+ 1. If publishing a fork, update its repository metadata before publishing:
245
+
246
+ ```sh
247
+ npm pkg set 'repository.type=git' \
248
+ 'repository.url=git+https://github.com/OWNER/REPOSITORY.git' \
249
+ 'homepage=https://github.com/OWNER/REPOSITORY#readme' \
250
+ 'bugs.url=https://github.com/OWNER/REPOSITORY/issues'
251
+ # Optional: npm pkg set 'author=Your Name'
252
+ npm install --package-lock-only --ignore-scripts
253
+ ```
254
+
255
+ 2. Review the MIT license, package name, compatibility pins, bundle replacement,
256
+ and tarball contents. Update the changelog and run `npm run verify`.
257
+ 3. Commit and push the source and lockfile. If you have not initialized Git yet:
258
+
259
+ ```sh
260
+ git init -b main
261
+ git add .
262
+ git commit -m 'Initial TypeScript Copilot catalog plugin'
263
+ git remote add origin https://github.com/1yefuwang1/dsh-copilot-catalog.git
264
+ git push -u origin main
265
+ ```
266
+
267
+ 4. The unclaimed package has no npm settings page yet, so the first GitHub Actions
268
+ release needs a one-time granular npm token. Because the package does not yet
269
+ exist, grant the shortest-lived token the minimum package read/write permission
270
+ that can stage a public package. A stage-only token is sufficient; no 2FA bypass
271
+ is needed. Store it as the `NPM_TOKEN` secret in the GitHub `npm` environment.
272
+ Never commit or paste the token into an issue, log, workflow, or configuration.
273
+ 5. Push a matching `vX.Y.Z` tag and publish a GitHub Release for it. The workflow
274
+ validates tag/version and repository metadata, runs the credential audit,
275
+ tests, peer integration, declarations, and tarball allowlist, then runs
276
+ `npm stage publish` from GitHub Actions with provenance. For a new package, npm
277
+ exposes a `0.0.0-stage` placeholder; version contents remain private.
278
+ 6. Review `0.2.0` in npm's **Staged Packages** page and approve it with 2FA. Then
279
+ configure npm **Trusted Publisher** for GitHub user `1yefuwang1`, repository
280
+ `dsh-copilot-catalog`, workflow `publish.yml`, environment `npm`, allowing
281
+ `npm stage publish`. Delete the GitHub `NPM_TOKEN` environment secret. Later
282
+ releases authenticate through short-lived GitHub OIDC and still require staged
283
+ review/approval; npm detects OIDC before token fallback.
284
+
285
+ CI runs on Linux, macOS and Windows, on Node 22 and 24. The publish workflow requires
286
+ Node 24/npm 11.5.1+ for trusted publishing and only runs for a published GitHub
287
+ Release, not for pull requests. The workflow stages rather than directly publishes;
288
+ a maintainer approves each release with 2FA. The `NPM_TOKEN` fallback is only for
289
+ the first staging operation and should be deleted after trusted publishing is configured.
290
+ Generated `dist/` files stay out of Git; only the
291
+ compiled runtime, declarations, DSH bundle patch, README, changelog and license
292
+ enter the npm tarball. Review files, tests, dependencies and credentials do not.
293
+
294
+ ## Limitations
295
+
296
+ - Discovery is a startup/mount snapshot, not continuous sync. Restart after login,
297
+ account changes, upgrades or rollback. Avoid hot-reloading this wrapper: a
298
+ process-wide catalog mutation can outlive its plugin mount.
299
+ - Account discovery deliberately uses strict picker semantics. Individual accounts
300
+ that mark every picker flag false fall back to bundled defaults; the wrapper
301
+ does not broaden access using policy-only heuristics.
302
+ - A successful sync mutates a shared Copilot catalog in memory. Mount only one
303
+ adapter instance. A failed later mount keeps the current process's catalog, not
304
+ necessarily the original defaults; a full restart resets it.
305
+ - Existing upstream model descriptors are intentionally preserved, not refreshed
306
+ field-by-field. Newly discovered IDs use live metadata and conservative per-protocol
307
+ defaults. Unknown pricing is zero-valued metadata, not a claim that access is free.
308
+ - New models require advertised supported endpoints and valid positive context/output
309
+ limits. No endpoint, family, or protocol is guessed from a model name. Optional
310
+ strict tools, developer-role support, long cache retention and advanced features
311
+ are not assumed; correct a model with the original adapter's `modelOverrides` if
312
+ its gateway needs additional compatibility settings.
313
+ - Only recognized advertised reasoning-effort values are selectable (`none` maps to
314
+ `off`). Absent/unrecognized effort metadata does not invent reasoning settings;
315
+ native Anthropic `thinking: true` without an effort list uses standard budget
316
+ levels. Native Messages effort lists use adaptive thinking. Endpoint metadata is
317
+ not a guarantee that every optional feature works identically on every gateway.
318
+ - Original adapter credential filtering, explicit `models` lists, and
319
+ `modelOverrides` still apply. This plugin neither enables account policies nor
320
+ changes stored `availableModelIds`; an upstream credential allowlist can still
321
+ hide a model until normal refresh/sign-in updates it.
322
+ - Enterprise OAuth endpoints outside HTTPS `*.githubcopilot.com` are rejected by
323
+ discovery; normal adapter behavior remains available as fallback.
324
+ - Upstream catalog structure, protocol compatibility and Copilot billing metadata
325
+ are not stable public contracts. Re-test protocol defaults and Enterprise routing
326
+ after updates. This package is not affiliated with DeepSeek, GitHub, or the pi-ai
327
+ maintainers.
328
+
329
+ ## License
330
+
331
+ [MIT](LICENSE). Original DSH and pi-ai packages retain their own licenses; they are
332
+ not redistributed in this tarball.
@@ -0,0 +1,11 @@
1
+ # This bundle replaces the original adapter; never enable both for the same route.
2
+ # Copy any other provider configuration into the replacement before enabling.
3
+ - id: llm-pi-ai
4
+ name: '@deepseek-ai/dsh-llm-pi-ai'
5
+ disabled: true
6
+ - insert:
7
+ - id: llm-pi-ai-catalog
8
+ name: dsh-copilot-catalog
9
+ config:
10
+ providers:
11
+ github-copilot: {}
@@ -0,0 +1,15 @@
1
+ import type { ModelCost } from '@earendil-works/pi-ai';
2
+ import { type AccountSelection, type ModelCatalog } from './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;
@@ -0,0 +1,191 @@
1
+ import { asRecord } from './types.js';
2
+ export const DEFAULT_COPILOT_BASE_URL = 'https://api.individual.githubcopilot.com';
3
+ // Provider identity defaults match the supported pi-ai Copilot OAuth client.
4
+ // These are protocol defaults, not metadata borrowed from a particular model.
5
+ const DEFAULT_HEADERS = Object.freeze({
6
+ 'User-Agent': 'GitHubCopilotChat/0.35.0',
7
+ 'Editor-Version': 'vscode/1.107.0',
8
+ 'Editor-Plugin-Version': 'copilot-chat/0.35.0',
9
+ 'Copilot-Integration-Id': 'vscode-chat',
10
+ });
11
+ const ENDPOINTS = [
12
+ ['/responses', 'openai-responses'],
13
+ ['/v1/messages', 'anthropic-messages'],
14
+ ['/chat/completions', 'openai-completions'],
15
+ ];
16
+ const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
17
+ const PRICE_PARTS = ['input_price', 'output_price', 'cache_price', 'cache_write_price'];
18
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
19
+ const own = (object, key) => Object.hasOwn(object, key) ? object[key] : undefined;
20
+ /** Copy only provider identity headers; never inherit Authorization or API keys. */
21
+ export function copilotHeaders(catalog) {
22
+ const headers = { ...DEFAULT_HEADERS };
23
+ for (const model of Object.values(catalog)) {
24
+ if (model.provider !== 'github-copilot' || !model.headers)
25
+ continue;
26
+ let found = false;
27
+ for (const [name, fallback] of Object.entries(DEFAULT_HEADERS)) {
28
+ const entry = Object.entries(model.headers).find(([key]) => key.toLowerCase() === name.toLowerCase());
29
+ if (entry && entry[1].trim() && !/[\r\n]/.test(entry[1])) {
30
+ headers[name] = entry[1];
31
+ found = true;
32
+ }
33
+ else {
34
+ headers[name] = fallback;
35
+ }
36
+ }
37
+ if (found)
38
+ break;
39
+ }
40
+ return headers;
41
+ }
42
+ function validPrices(value) {
43
+ const record = asRecord(value);
44
+ return record !== undefined && PRICE_PARTS.every((key) => typeof record[key] === 'number' &&
45
+ Number.isFinite(record[key]) && record[key] >= 0);
46
+ }
47
+ /** Convert cents per million tokens only when every required price is valid. */
48
+ export function liveCost(billing, fallback) {
49
+ const prices = asRecord(asRecord(billing)?.token_prices);
50
+ const regular = asRecord(prices?.default);
51
+ if (prices?.batch_size !== 1_000_000 || !validPrices(regular))
52
+ return fallback;
53
+ const convert = (value) => ({
54
+ input: value.input_price / 100,
55
+ output: value.output_price / 100,
56
+ cacheRead: value.cache_price / 100,
57
+ cacheWrite: value.cache_write_price / 100,
58
+ });
59
+ const cost = convert(regular);
60
+ if (positiveInteger(regular.context_max) && validPrices(prices.long_context)) {
61
+ cost.tiers = [{ inputTokensAbove: regular.context_max, ...convert(prices.long_context) }];
62
+ }
63
+ return cost;
64
+ }
65
+ function reasoningMetadata(api, supports) {
66
+ const efforts = supports?.reasoning_effort;
67
+ if (Array.isArray(efforts)) {
68
+ const map = {};
69
+ for (const level of THINKING_LEVELS) {
70
+ const wire = level === 'off' && efforts.includes('none') ? 'none' : level;
71
+ map[level] = efforts.includes(wire) ? wire : null;
72
+ }
73
+ const reasoning = THINKING_LEVELS.some((level) => level !== 'off' && typeof map[level] === 'string');
74
+ // A native Messages effort list describes adaptive output_config.effort.
75
+ return { reasoning, thinkingLevelMap: map, adaptiveThinking: api === 'anthropic-messages' && reasoning };
76
+ }
77
+ if (api === 'anthropic-messages' && supports?.thinking === true) {
78
+ // Legacy native thinking advertises a budget rather than effort spellings.
79
+ // Use pi-ai's standard budget levels, without inventing xhigh/max support.
80
+ return { reasoning: true, thinkingLevelMap: { xhigh: null, max: null }, adaptiveThinking: false };
81
+ }
82
+ // Missing/unknown effort metadata must not invent user-selectable reasoning.
83
+ return { reasoning: false, adaptiveThinking: false };
84
+ }
85
+ function protocolCompat(api, reasoning, adaptiveThinking) {
86
+ switch (api) {
87
+ case 'openai-responses': return {
88
+ supportsDeveloperRole: false,
89
+ supportsStrictMode: false,
90
+ supportsLongCacheRetention: false,
91
+ };
92
+ case 'openai-completions': return {
93
+ supportsDeveloperRole: false,
94
+ supportsStrictMode: false,
95
+ supportsStore: false,
96
+ supportsUsageInStreaming: false,
97
+ supportsLongCacheRetention: false,
98
+ supportsReasoningEffort: reasoning,
99
+ thinkingFormat: 'openai',
100
+ };
101
+ case 'anthropic-messages': return {
102
+ forceAdaptiveThinking: adaptiveThinking,
103
+ supportsStrictTools: false,
104
+ supportsTemperature: false,
105
+ supportsLongCacheRetention: false,
106
+ supportsCacheControlOnTools: false,
107
+ supportsEagerToolInputStreaming: false,
108
+ };
109
+ }
110
+ }
111
+ function createNewModel(item, headers, baseUrl) {
112
+ // IDs are opaque; reject malformed request IDs, never infer a vendor/family.
113
+ const endpoints = item.supported_endpoints;
114
+ if (/[\s\u0000-\u001f\u007f]/u.test(item.id) || !Array.isArray(endpoints))
115
+ return undefined;
116
+ const api = ENDPOINTS.find(([endpoint]) => endpoints.includes(endpoint))?.[1];
117
+ if (!api)
118
+ return undefined;
119
+ const capabilities = asRecord(item.capabilities);
120
+ if (capabilities?.type !== undefined && capabilities.type !== 'chat')
121
+ return undefined;
122
+ const limits = asRecord(capabilities?.limits);
123
+ if (!positiveInteger(limits?.max_context_window_tokens) || !positiveInteger(limits.max_output_tokens) ||
124
+ limits.max_output_tokens > limits.max_context_window_tokens)
125
+ return undefined;
126
+ const supports = asRecord(capabilities?.supports);
127
+ if (supports?.streaming === false)
128
+ return undefined;
129
+ const { reasoning, thinkingLevelMap, adaptiveThinking } = reasoningMetadata(api, supports);
130
+ return {
131
+ id: item.id,
132
+ name: typeof item.name === 'string' && item.name.trim() ? item.name : item.id,
133
+ api,
134
+ provider: 'github-copilot',
135
+ baseUrl,
136
+ contextWindow: limits.max_context_window_tokens,
137
+ maxTokens: limits.max_output_tokens,
138
+ input: supports?.vision === true ? ['text', 'image'] : ['text'],
139
+ // Unknown pricing is represented by zero, never a guessed sibling's rates.
140
+ cost: liveCost(item.billing, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }),
141
+ reasoning,
142
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
143
+ compat: protocolCompat(api, reasoning, adaptiveThinking),
144
+ headers: { ...headers },
145
+ };
146
+ }
147
+ /** Pure selection: preserve known descriptors; build any new ID from endpoint/capability metadata. */
148
+ export function selectAccountModels(response, catalog, { baseUrl = DEFAULT_COPILOT_BASE_URL, } = {}) {
149
+ const data = asRecord(response)?.data;
150
+ if (!Array.isArray(data))
151
+ throw new Error('Invalid Copilot /models response');
152
+ if (!catalog || typeof catalog !== 'object')
153
+ throw new TypeError('A bundled model catalog is required');
154
+ const headers = copilotHeaders(catalog);
155
+ const models = Object.create(null);
156
+ const unsupported = new Set();
157
+ for (const raw of data) {
158
+ const item = asRecord(raw);
159
+ const policy = asRecord(item?.policy);
160
+ const supports = asRecord(asRecord(item?.capabilities)?.supports);
161
+ if (!item || typeof item.id !== 'string' || !item.id.trim() || item.model_picker_enabled !== true ||
162
+ (policy?.state !== undefined && policy.state !== 'enabled') || supports?.tool_calls === false ||
163
+ Object.hasOwn(models, item.id))
164
+ continue;
165
+ // Own-property lookup prevents Object.prototype members becoming descriptors.
166
+ const model = own(catalog, item.id) ?? createNewModel({ ...item, id: item.id }, headers, baseUrl);
167
+ if (model) {
168
+ models[item.id] = model;
169
+ unsupported.delete(item.id);
170
+ }
171
+ else {
172
+ unsupported.add(item.id);
173
+ }
174
+ }
175
+ if (Object.keys(models).length === 0)
176
+ throw new Error('No supported enabled Copilot models found');
177
+ return { models, unsupported: [...unsupported] };
178
+ }
179
+ /** Validate mutability before touching a shared upstream catalog. */
180
+ export function replaceCatalog(catalog, models) {
181
+ const descriptors = Object.getOwnPropertyDescriptors(catalog);
182
+ if (!Object.isExtensible(catalog) || Object.values(descriptors).some((value) => !value.configurable)) {
183
+ throw new Error('The upstream Copilot catalog is not mutable');
184
+ }
185
+ // Plain data properties prevent setters and prototype-sensitive assignment.
186
+ for (const key of Object.keys(descriptors))
187
+ delete catalog[key];
188
+ for (const [key, value] of Object.entries(models)) {
189
+ Object.defineProperty(catalog, key, { value, enumerable: true, configurable: true, writable: true });
190
+ }
191
+ }
@@ -0,0 +1,12 @@
1
+ import { type CredentialReader, type DiscoveryOptions, type SyncResult } from './types.js';
2
+ export declare const VERSION_HEADER = "2026-06-01";
3
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
4
+ export declare class DiscoveryError extends Error {
5
+ readonly code: string;
6
+ readonly name = "DiscoveryError";
7
+ constructor(code: string, message: string);
8
+ }
9
+ /** Do not forward credentials to redirects, unrelated hosts, or unusual ports. */
10
+ export declare function discoveryUrl(baseUrl: unknown): URL;
11
+ /** One read-only, bounded startup discovery. All dependencies are explicit for tests. */
12
+ export declare function syncCopilotCatalog(credentials: CredentialReader | undefined, { catalog, oauth, recordKey, fetcher, timeoutMs, now, baseUrl, resolveApiKey, resolveAmbientApiKey, onEndpoint, }: DiscoveryOptions): Promise<SyncResult>;
@@ -0,0 +1,138 @@
1
+ import { copilotHeaders, replaceCatalog, selectAccountModels } from './catalog.js';
2
+ import { asRecord } from './types.js';
3
+ export const VERSION_HEADER = '2026-06-01';
4
+ export const DEFAULT_TIMEOUT_MS = 10_000;
5
+ export class DiscoveryError extends Error {
6
+ code;
7
+ name = 'DiscoveryError';
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ }
12
+ }
13
+ function oauthGrant(record) {
14
+ const entry = asRecord(record);
15
+ const grant = entry?.kind === 'grant' ? asRecord(entry.payload) : undefined;
16
+ return grant?.type === 'oauth' && typeof grant.refresh === 'string' && grant.refresh.trim() &&
17
+ typeof grant.access === 'string' && grant.access.trim() && typeof grant.expires === 'number' && Number.isFinite(grant.expires)
18
+ ? structuredClone(grant) : undefined;
19
+ }
20
+ /** Do not forward credentials to redirects, unrelated hosts, or unusual ports. */
21
+ export function discoveryUrl(baseUrl) {
22
+ let base;
23
+ try {
24
+ if (typeof baseUrl !== 'string')
25
+ throw new TypeError();
26
+ base = new URL(baseUrl);
27
+ }
28
+ catch {
29
+ throw new DiscoveryError('UNTRUSTED_ENDPOINT', 'Unexpected Copilot API endpoint');
30
+ }
31
+ if (base.protocol !== 'https:' || !base.hostname.endsWith('.githubcopilot.com') ||
32
+ base.username || base.password || (base.port && base.port !== '443')) {
33
+ throw new DiscoveryError('UNTRUSTED_ENDPOINT', 'Unexpected Copilot API endpoint');
34
+ }
35
+ return new URL('/models', base);
36
+ }
37
+ /** One read-only, bounded startup discovery. All dependencies are explicit for tests. */
38
+ export async function syncCopilotCatalog(credentials, { catalog, oauth, recordKey, fetcher = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS, now = Date.now, baseUrl, resolveApiKey, resolveAmbientApiKey, onEndpoint, }) {
39
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60_000) {
40
+ throw new TypeError('timeoutMs must be an integer between 1 and 60000');
41
+ }
42
+ const controller = new AbortController();
43
+ const { signal } = controller;
44
+ // Race the entire operation, including credential reads and OAuth hooks that
45
+ // might not cooperate with abort. A late response may never commit changes.
46
+ let timer;
47
+ const timeout = new Promise((_, reject) => {
48
+ timer = setTimeout(() => {
49
+ const error = new DiscoveryError('TIMEOUT', 'Copilot discovery timed out');
50
+ controller.abort(error);
51
+ reject(error);
52
+ }, timeoutMs);
53
+ });
54
+ const work = async () => {
55
+ const record = resolveApiKey ? undefined : await credentials?.readRecord(recordKey);
56
+ signal.throwIfAborted();
57
+ let credential = resolveApiKey ? undefined : oauthGrant(record);
58
+ let apiKey;
59
+ let url;
60
+ const adoptEndpoint = (value) => {
61
+ signal.throwIfAborted();
62
+ const endpoint = discoveryUrl(value);
63
+ onEndpoint?.(endpoint.origin);
64
+ return endpoint;
65
+ };
66
+ if (credential) {
67
+ // Capture routing before refresh/listing: fallback must not send an
68
+ // Enterprise account to the bundled Individual endpoint.
69
+ let auth = await oauth.toAuth(credential);
70
+ url = adoptEndpoint(auth.baseUrl);
71
+ if (now() + 5 * 60_000 >= credential.expires) {
72
+ // No credential-store writes. The original adapter owns locked refresh.
73
+ credential = await oauth.refresh(credential, signal);
74
+ signal.throwIfAborted();
75
+ auth = await oauth.toAuth(credential);
76
+ url = adoptEndpoint(auth.baseUrl);
77
+ }
78
+ apiKey = auth.apiKey;
79
+ }
80
+ else {
81
+ const entry = asRecord(record);
82
+ const storedKey = entry?.kind === 'api-key' && typeof entry.key === 'string' ? entry.key : undefined;
83
+ apiKey = resolveApiKey ? await resolveApiKey()
84
+ : storedKey ?? (entry?.kind === 'grant' ? undefined : await resolveAmbientApiKey?.());
85
+ signal.throwIfAborted();
86
+ if (typeof apiKey !== 'string' || !apiKey.trim())
87
+ return { status: 'no Copilot credential' };
88
+ if (baseUrl !== undefined) {
89
+ url = adoptEndpoint(baseUrl);
90
+ }
91
+ else if (/(?:^|;)proxy-ep=[^;]+/.test(apiKey)) {
92
+ // toAuth is read-only. It can decode an access token's proxy-ep without
93
+ // requiring a refresh token or synthesizing a persisted OAuth grant.
94
+ const auth = await oauth.toAuth({ type: 'oauth', access: apiKey, refresh: '', expires: 0 });
95
+ url = adoptEndpoint(auth.baseUrl);
96
+ }
97
+ else {
98
+ throw new DiscoveryError('MISSING_ENDPOINT', 'API-key auth requires a Copilot proxy endpoint or explicit baseURL');
99
+ }
100
+ }
101
+ signal.throwIfAborted();
102
+ if (typeof apiKey !== 'string' || !apiKey.trim()) {
103
+ throw new DiscoveryError('INVALID_AUTH', 'No usable Copilot API token');
104
+ }
105
+ const headers = new Headers(copilotHeaders(catalog));
106
+ headers.set('Accept', 'application/json');
107
+ headers.set('Authorization', `Bearer ${apiKey}`);
108
+ headers.set('X-GitHub-Api-Version', VERSION_HEADER);
109
+ const response = await fetcher(url, { headers, signal, redirect: 'error' });
110
+ signal.throwIfAborted();
111
+ if (!response.ok) {
112
+ throw new DiscoveryError('HTTP_ERROR', `Copilot model discovery returned HTTP ${response.status}`);
113
+ }
114
+ const body = await response.json();
115
+ signal.throwIfAborted();
116
+ let selected;
117
+ try {
118
+ selected = selectAccountModels(body, catalog, { baseUrl: url.origin });
119
+ }
120
+ catch {
121
+ throw new DiscoveryError('INVALID_CATALOG', 'No valid supported account catalog');
122
+ }
123
+ signal.throwIfAborted();
124
+ try {
125
+ replaceCatalog(catalog, selected.models);
126
+ }
127
+ catch {
128
+ throw new DiscoveryError('IMMUTABLE_CATALOG', 'The upstream Copilot catalog is not mutable');
129
+ }
130
+ return { status: 'synced', count: Object.keys(selected.models).length, unsupported: selected.unsupported };
131
+ };
132
+ try {
133
+ return await Promise.race([work(), timeout]);
134
+ }
135
+ finally {
136
+ clearTimeout(timer);
137
+ }
138
+ }
@@ -0,0 +1,9 @@
1
+ import type { CopilotAuthOptions, CredentialReader, OriginalAdapter, PluginConfig, PluginContext } from './types.js';
2
+ export declare const name = "llm-pi-ai";
3
+ export declare const Config: OriginalAdapter['Config'];
4
+ export declare const inject: string[];
5
+ export declare const apply: (ctx: PluginContext, config: PluginConfig) => Promise<void>;
6
+ export declare const syncCopilotCatalog: (credentials?: CredentialReader, authOptions?: CopilotAuthOptions) => Promise<import("./types.js").SyncResult>;
7
+ export { selectAccountModels } from './catalog.js';
8
+ export type { AccountModelOptions } from './catalog.js';
9
+ export type { AccountSelection, CopilotAuthOptions, CredentialReader, ModelCatalog, SyncResult } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import { createPlugin } from './plugin.js';
2
+ import { loadRuntime } from './runtime.js';
3
+ const plugin = createPlugin(await loadRuntime());
4
+ export const name = plugin.name;
5
+ export const Config = plugin.Config;
6
+ export const inject = plugin.inject;
7
+ export const apply = (ctx, config) => plugin.apply(ctx, config);
8
+ export const syncCopilotCatalog = (credentials, authOptions) => plugin.sync(credentials, authOptions);
9
+ export { selectAccountModels } from './catalog.js';
@@ -0,0 +1,5 @@
1
+ import type { CatalogPlugin, PluginConfig, PluginDependencies } from './types.js';
2
+ /** Add an account endpoint as a runtime default, never as a persisted setting. */
3
+ export declare function withCopilotEndpoint(config: PluginConfig, endpoint: string | undefined): PluginConfig;
4
+ /** Keep integration separate from peer loading so lifecycle behavior is testable. */
5
+ export declare function createPlugin({ original, catalog, oauth, fetcher, timeoutMs }: PluginDependencies): CatalogPlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,86 @@
1
+ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
2
+ import { DiscoveryError, syncCopilotCatalog } from './discovery.js';
3
+ /** Add an account endpoint as a runtime default, never as a persisted setting. */
4
+ export function withCopilotEndpoint(config, endpoint) {
5
+ if (!endpoint)
6
+ return config;
7
+ const source = config.providers;
8
+ let previous;
9
+ let resolved;
10
+ const get = () => {
11
+ const raw = source.get();
12
+ if (raw === previous && resolved !== undefined)
13
+ return resolved;
14
+ const profile = raw['github-copilot'];
15
+ previous = raw;
16
+ // Explicit user baseURL wins. Do not overwrite other providers or settings.
17
+ resolved = !profile || profile.baseURL !== undefined && profile.baseURL !== null
18
+ ? raw : Object.freeze({ ...raw, 'github-copilot': Object.freeze({ ...profile, baseURL: endpoint }) });
19
+ return resolved;
20
+ };
21
+ return {
22
+ ...config,
23
+ // Volatile references are frozen. Copy their protocol (including symbols)
24
+ // into a detached facade instead of violating frozen-property Proxy rules.
25
+ providers: Object.freeze({ ...source, get }),
26
+ };
27
+ }
28
+ /** Keep integration separate from peer loading so lifecycle behavior is testable. */
29
+ export function createPlugin({ original, catalog, oauth, fetcher, timeoutMs }) {
30
+ if (typeof original?.apply !== 'function' || typeof original.recordKeyFor !== 'function' ||
31
+ !original.Config || !Array.isArray(original.inject) || !catalog ||
32
+ typeof oauth?.refresh !== 'function' || typeof oauth.toAuth !== 'function') {
33
+ throw new Error('Incompatible DSH/pi-ai runtime; see the supported versions in README');
34
+ }
35
+ let accountEndpoint;
36
+ const plugin = {
37
+ name: original.name,
38
+ Config: original.Config,
39
+ inject: [...new Set([...original.inject, 'credentials'])],
40
+ sync: (credentials, authOptions) => {
41
+ accountEndpoint = undefined;
42
+ return syncCopilotCatalog(credentials, {
43
+ catalog, oauth, recordKey: original.recordKeyFor('github-copilot'), fetcher, timeoutMs,
44
+ ...authOptions,
45
+ onEndpoint: (endpoint) => { accountEndpoint = endpoint; },
46
+ });
47
+ },
48
+ async apply(ctx, config) {
49
+ try {
50
+ const credentials = ctx.get('credentials');
51
+ const profile = config.providers.get()['github-copilot'];
52
+ const ref = profile?.apiKeyEnv;
53
+ const readReference = async (value) => {
54
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
55
+ throw new DiscoveryError('INVALID_AUTH', 'Invalid credential reference');
56
+ }
57
+ return (await credentials?.resolve(value))?.value;
58
+ };
59
+ const authOptions = {
60
+ ...(typeof profile?.baseURL === 'string' ? { baseUrl: profile.baseURL } : {}),
61
+ ...(typeof ref === 'string' ? { resolveApiKey: () => readReference(ref) } : {}),
62
+ resolveAmbientApiKey: async () => (await readReference('COPILOT_GITHUB_TOKEN'))
63
+ ?? launchEnvironmentOf(ctx).get('COPILOT_GITHUB_TOKEN')?.value,
64
+ };
65
+ const result = await plugin.sync(credentials, authOptions);
66
+ if (result.status === 'synced') {
67
+ // JSON quoting prevents model IDs from injecting log lines.
68
+ ctx.logger?.info?.('Copilot catalog synced: %d enabled models; unsupported new IDs: %s', result.count, JSON.stringify(result.unsupported));
69
+ }
70
+ else {
71
+ ctx.logger?.info?.('Copilot catalog kept bundled defaults: %s', result.status);
72
+ }
73
+ }
74
+ catch (error) {
75
+ // Arbitrary OAuth/network/credential errors may include tokens or bodies.
76
+ // Only our controlled code is safe to log; never log the raw exception.
77
+ const reason = error instanceof DiscoveryError ? error.code : 'DISCOVERY_FAILED';
78
+ ctx.logger?.warn?.('Copilot model discovery failed; keeping current catalog: %s', reason);
79
+ }
80
+ if (accountEndpoint)
81
+ ctx.logger?.info?.('Copilot runtime endpoint: %s', accountEndpoint);
82
+ return original.apply(ctx, withCopilotEndpoint(config, accountEndpoint));
83
+ },
84
+ };
85
+ return plugin;
86
+ }
@@ -0,0 +1,3 @@
1
+ import type { PluginDependencies } from './types.js';
2
+ /** Resolve pi-ai from the adapter, not the plugin: they must share one catalog. */
3
+ export declare function loadRuntime(): Promise<PluginDependencies>;
@@ -0,0 +1,30 @@
1
+ import { createRequire, findPackageJSON } from 'node:module';
2
+ import { dirname, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ /** Resolve pi-ai from the adapter, not the plugin: they must share one catalog. */
5
+ export async function loadRuntime() {
6
+ const require = createRequire(import.meta.url);
7
+ let adapterEntry;
8
+ try {
9
+ adapterEntry = require.resolve('@deepseek-ai/dsh-llm-pi-ai');
10
+ }
11
+ catch {
12
+ throw new Error('dsh-copilot-catalog requires @deepseek-ai/dsh-llm-pi-ai@0.2.0-rc.2; install it in the same profile');
13
+ }
14
+ const adapterUrl = pathToFileURL(adapterEntry);
15
+ const piManifest = findPackageJSON('@earendil-works/pi-ai', adapterUrl);
16
+ if (!piManifest)
17
+ throw new Error('Cannot resolve the DSH adapter\'s pi-ai dependency');
18
+ const piRoot = dirname(piManifest);
19
+ // These are the public providers/* export targets in supported pi-ai 0.87.1.
20
+ // No private OAuth module or platform-specific application path is imported.
21
+ const [original, { GITHUB_COPILOT_MODELS: catalog }, { githubCopilotProvider }] = await Promise.all([
22
+ import(adapterUrl.href),
23
+ import(pathToFileURL(join(piRoot, 'dist/providers/github-copilot.models.js')).href),
24
+ import(pathToFileURL(join(piRoot, 'dist/providers/github-copilot.js')).href),
25
+ ]);
26
+ const oauth = githubCopilotProvider().auth?.oauth;
27
+ if (!oauth)
28
+ throw new Error('The installed pi-ai Copilot provider has no OAuth hooks');
29
+ return { original, catalog: catalog, oauth };
30
+ }
@@ -0,0 +1,52 @@
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
+ }
45
+ export interface CatalogPlugin {
46
+ name: OriginalAdapter['name'];
47
+ Config: OriginalAdapter['Config'];
48
+ inject: string[];
49
+ sync(credentials?: CredentialReader, authOptions?: CopilotAuthOptions): Promise<SyncResult>;
50
+ apply(ctx: PluginContext, config: PluginConfig): Promise<void>;
51
+ }
52
+ export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ export function asRecord(value) {
2
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ ? value : undefined;
4
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "dsh-copilot-catalog",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Account-aware GitHub Copilot model discovery for DeepSeek Harness, wrapping the existing pi-ai adapter.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
10
+ "./catalog": { "types": "./dist/catalog.d.ts", "import": "./dist/catalog.js" },
11
+ "./discovery": { "types": "./dist/discovery.d.ts", "import": "./dist/discovery.js" },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "npm run build && node --test test/catalog.test.mjs test/discovery.test.mjs test/plugin.test.mjs test/secrets.test.mjs",
25
+ "check": "npm run typecheck && node scripts/check.mjs",
26
+ "verify": "npm run check:secrets && npm run check && npm test && npm run test:integration && npm run test:types && npm run test:pack",
27
+ "check:secrets": "node scripts/check-secrets.mjs",
28
+ "test:types": "npm run build && tsc -p test/types/tsconfig.json",
29
+ "test:integration": "npm run build && node --test test/integration/runtime.test.mjs test/integration/enterprise.test.mjs",
30
+ "test:pack": "npm run build && node scripts/check-pack.mjs",
31
+ "prepare": "npm run build",
32
+ "prepublishOnly": "npm run verify"
33
+ },
34
+ "keywords": [
35
+ "deepseek-harness",
36
+ "dsh",
37
+ "cordis-plugin",
38
+ "github-copilot",
39
+ "model-catalog",
40
+ "oauth"
41
+ ],
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/1yefuwang1/dsh-copilot-catalog.git"
46
+ },
47
+ "homepage": "https://github.com/1yefuwang1/dsh-copilot-catalog#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/1yefuwang1/dsh-copilot-catalog/issues"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.19.0",
53
+ "dsh": "0.2.0-rc.2"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "peerDependencies": {
60
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.2",
61
+ "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
62
+ "@earendil-works/pi-ai": "0.87.1"
63
+ },
64
+ "devDependencies": {
65
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.0-rc.2",
66
+ "@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
67
+ "@earendil-works/pi-ai": "0.87.1",
68
+ "@types/node": "^22.19.0",
69
+ "typescript": "~5.9.3"
70
+ },
71
+ "dsh": {
72
+ "manifestVersion": 1,
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ }
76
+ }
77
+ }