dsh-model-extension 1.2.3-alpha.2 → 1.2.3
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/README.md +4 -2
- package/lib/client.js +7 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +17 -47
- package/lib/index.js.map +1 -1
- package/package.json +1 -2
package/lib/index.js
CHANGED
|
@@ -366,22 +366,22 @@ function piAiUnavailableReason() {
|
|
|
366
366
|
/**
|
|
367
367
|
* dsh-model-extension — host-side entry (v1.0.0).
|
|
368
368
|
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
369
|
+
* The host half is the metadata service: same-origin routes backing the
|
|
370
|
+
* Models+ page's quick-load. models.dev is fetched ONLY on an explicit user
|
|
371
|
+
* click (the title-row button); the raw file caches next to settings.yaml
|
|
372
|
+
* under DSH home. Only the flat models.dev shape (top-level keys = full model
|
|
373
|
+
* ids) is accepted; anything else is a refusal, per plan. The pi-ai catalog is
|
|
374
|
+
* the other, richer source: it is read from the installed package the host
|
|
375
|
+
* adapter itself loads, with no network call at all (./pi-ai-catalog.ts).
|
|
376
|
+
*
|
|
377
|
+
* No version gate lives here. The `dsh.adapter` anchor is gone: the host (as
|
|
378
|
+
* of 0.1.7-rc.1) admits or denies this package from its own
|
|
379
|
+
* `@deepseek-ai/dsh*` peerDependencies, so a second, softer copy of that
|
|
380
|
+
* decision inside the plugin body would only emit a warning nobody can act
|
|
381
|
+
* on. The UI is fully plugin-owned, and the host's settings schema validation
|
|
382
|
+
* plus revision fencing reject — never corrupt — anything a mismatched wire
|
|
383
|
+
* contract might produce.
|
|
383
384
|
*/
|
|
384
|
-
const ADAPTER_VERSION = "0.1.7-alpha.1";
|
|
385
385
|
/** Cordis service name (distinct from the npm package name). */
|
|
386
386
|
const name = "model-extension";
|
|
387
387
|
/** Required services — the route registry lives on the host web server. */
|
|
@@ -391,33 +391,6 @@ const METADATA_URL = "https://models.dev/models.json";
|
|
|
391
391
|
/** Download timeout in milliseconds. */
|
|
392
392
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
393
393
|
/**
|
|
394
|
-
* Read the running host's version by resolving the installed CLI package's
|
|
395
|
-
* manifest.
|
|
396
|
-
*
|
|
397
|
-
* Node resolves symlinks by default, so the plugin module's real path is the
|
|
398
|
-
* project checkout — walking up from there never reaches the global tree.
|
|
399
|
-
* The resolution therefore starts at the *link* path under the profile
|
|
400
|
-
* node_modules (symlink not resolved), then falls back to this module's own
|
|
401
|
-
* real path.
|
|
402
|
-
* @returns the version string, or undefined when unresolvable.
|
|
403
|
-
*/
|
|
404
|
-
function readHostVersion() {
|
|
405
|
-
const candidates = [];
|
|
406
|
-
const dshHome = process.env.DSH_HOME;
|
|
407
|
-
if (dshHome !== void 0 && dshHome.length > 0) {
|
|
408
|
-
candidates.push(`${dshHome}/profiles/web/node_modules/dsh-model-extension/lib/index.js`);
|
|
409
|
-
candidates.push(`${dshHome}/profiles/node/node_modules/dsh-model-extension/lib/index.js`);
|
|
410
|
-
}
|
|
411
|
-
try {
|
|
412
|
-
candidates.push(fileURLToPath(import.meta.url));
|
|
413
|
-
} catch {}
|
|
414
|
-
for (const specifier of ["@deepseek-ai/dsh/package.json", "@deepseek-ai/dsh-base/package.json"]) for (const base of candidates) try {
|
|
415
|
-
const manifestPath = createRequire(base).resolve(specifier);
|
|
416
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
417
|
-
if (typeof manifest.version === "string") return manifest.version;
|
|
418
|
-
} catch {}
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
394
|
* Resolve the directory the metadata file lives in: next to the first
|
|
422
395
|
* settings.yaml found (plan: "DSH home, same level as settings.yaml"),
|
|
423
396
|
* falling back to DSH home itself and then the process cwd.
|
|
@@ -499,14 +472,11 @@ function json(res, status, body) {
|
|
|
499
472
|
res.end(JSON.stringify(body));
|
|
500
473
|
}
|
|
501
474
|
/**
|
|
502
|
-
* Host plugin body:
|
|
503
|
-
*
|
|
504
|
-
* usage of the same registry).
|
|
475
|
+
* Host plugin body: mount the metadata routes (an explicit webServer register,
|
|
476
|
+
* mirroring the hmr plugin's usage of the same registry).
|
|
505
477
|
* @param ctx - cordis context.
|
|
506
478
|
*/
|
|
507
479
|
function apply(ctx) {
|
|
508
|
-
const hostVersion = readHostVersion();
|
|
509
|
-
if (hostVersion === void 0 || hostVersion !== ADAPTER_VERSION) ctx.logger?.warn(`[dsh-model-extension] host version ${hostVersion ?? "<unreadable>"} differs from the validated anchor ${ADAPTER_VERSION}; registering anyway — if settings reads or writes misbehave, please report the host version`);
|
|
510
480
|
const webServer = ctx.webServer;
|
|
511
481
|
if (webServer === void 0) {
|
|
512
482
|
ctx.logger?.warn("[dsh-model-extension] webServer service absent; metadata routes not mounted");
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/pi-ai-catalog.ts","../src/index.ts"],"sourcesContent":["/**\n * pi-ai catalog reader.\n *\n * The values a Models+ row can meaningfully carry are exactly the fields of\n * pi-ai's `Model` — `thinkingLevelMap` (our `reasoningEfforts`), `compat`,\n * capacities and modalities — so this module reads the installed catalog and\n * projects it into the prefill index. models.dev knows none of the reasoning\n * or compat surface.\n *\n * ## Why the catalog is read through pi-ai's own API\n *\n * `@earendil-works/pi-ai/providers/all` is a *declared* subpath export of the\n * installed package, and the host's own adapter reads the catalog through it\n * (`dsh-llm-pi-ai` calls `getBuiltinProviders()` / `getBuiltinModels()`). Doing\n * the same buys three things:\n *\n * - **The data-format responsibility stays with pi-ai.** The `data/*.json`\n * layout, its `schemaVersion`, and how groups flatten into models are pi-ai's\n * business. Calling its API means a future layout change is absorbed by the\n * package that made it, instead of by this plugin.\n * - **The data is the host's own.** Same package, same module instance, same\n * values the adapter resolves — there is no second opinion to drift from.\n * - **No version gate is needed, or wanted.** A host upgrade that changes the\n * catalog simply yields the new catalog. Gating on a version we recognize\n * would turn every host upgrade into a silent feature outage; what this\n * module guards instead is the *shape of what it received*.\n *\n * ## What is actually guarded\n *\n * The failure modes are answered as concrete findings, never as a version\n * guess:\n *\n * | Drift | Answer |\n * |---|---|\n * | pi-ai renamed or dropped from the adapter's deps | resolution fails, and the refusal carries every copy that was visible |\n * | a second, stale copy wins resolution | resolution starts at the adapter the host loads, and every visible copy is reported |\n * | the catalog API stops exporting what we call | the source is disabled, naming the missing export |\n * | a projected field stops arriving | a finding is reported, and the affected rows simply inherit from the host catalog |\n *\n * Resolution is hand-rolled for one reason: pi-ai is ESM-only with no\n * `require`/`default` export condition, so `createRequire().resolve()` refuses\n * every subpath. Anchors start inside the profile because Node resolves a\n * symlink to its real path before resolving imports, and the home root is a\n * last resort — a DSH home can hold a stale copy there.\n *\n * The one thing this module cannot detect by itself is a change to the *host's*\n * acceptance rules: its `COMPAT_GATES` offer/withhold split is not exported,\n * and a switch it withdrew would be refused at save time. That is covered by\n * `scripts/audit-pi-ai-contract.mjs`.\n *\n * Nothing here touches the network.\n *\n * @module dsh-model-extension/pi-ai-catalog\n */\n\nimport { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/** The pi-ai package whose catalog backs a `dsh-llm-pi-ai` route. */\nconst PI_AI_PACKAGE = '@earendil-works/pi-ai'\n\n/** The host adapter that owns the catalog routes; its location seeds resolution. */\nconst PI_AI_ADAPTER_PACKAGE = '@deepseek-ai/dsh-llm-pi-ai'\n\n/** The declared subpath this module reads the catalog through. */\nconst PI_AI_ALL_SUBPATH = 'dist/providers/all.js'\n\n/** How many parent directories the hand-rolled `node_modules` walk visits. */\nconst RESOLUTION_DEPTH = 12\n\n/** One catalog model projected to the fields the prefill consumes. */\nexport interface PiAiModelEntry {\n /** pi-ai provider id — the route key a catalog route is named after. */\n readonly provider: string\n /** Wire protocol this model speaks, which picks its `compat` field set. */\n readonly api: string\n /** Model id passed to the provider. */\n readonly id: string\n /** Display name, when the catalog carries one. */\n readonly name?: string\n /** Maximum combined request and response context in tokens. */\n readonly contextWindow?: number\n /** Maximum output tokens. */\n readonly maxTokens?: number\n /** Accepted request modalities. */\n readonly input?: readonly string[]\n /** Whether the model reasons at all; `false` makes every effort moot. */\n readonly reasoning?: boolean\n /**\n * pi-ai's own level→wire-spelling map. Semantically our `reasoningEfforts`,\n * except that `null` means *unsupported* here while our dict reads it as\n * *supported, send nothing* — see the client-side mapping before copying.\n */\n readonly thinkingLevelMap?: Readonly<Record<string, string | null>>\n /** pi-ai's wire-compatibility switches for this model. */\n readonly compat?: Readonly<Record<string, unknown>>\n}\n\n/** One pi-ai copy the resolution anchors could see. */\nexport interface PiAiCopy {\n /** Package directory. */\n readonly dir: string\n /** Its version, when readable. */\n readonly version?: string\n}\n\n/**\n * Provenance and drift findings attached to every served catalog.\n *\n * These exist so that no failure stays silent. A catalog that is empty, stale,\n * or read from a build the host does not actually load all look identical from\n * the outside — the difference surfaces much later, as wrong values written\n * into settings, which is the outcome this structure is meant to prevent.\n */\nexport interface PiAiCatalogDiagnostics {\n /** Directory the served data was read from. */\n readonly packageDir: string\n /**\n * How the package was found. `adapter` means the resolution followed the\n * same path the host's own import does, so the data describes the running\n * host; `home-fallback` means it did not, and callers should treat the\n * values as advisory.\n */\n readonly resolvedVia: 'adapter' | 'home-fallback'\n /** Every copy the anchors could see — more than one is itself a finding. */\n readonly copies: readonly PiAiCopy[]\n /** Non-fatal findings worth surfacing: missing fields, odd shapes, etc. */\n readonly warnings: readonly string[]\n}\n\n/** The projected catalog, plus the provenance a UI can show. */\nexport interface PiAiCatalog {\n /** Installed pi-ai version, when its manifest is readable. */\n readonly piAiVersion?: string\n /** Generation timestamp of the shipped catalog snapshot. */\n readonly generatedAt?: string\n /** Every catalog model, across every provider. */\n readonly models: readonly PiAiModelEntry[]\n /** Provenance and drift findings, so a silent failure is impossible. */\n readonly diagnostics: PiAiCatalogDiagnostics\n}\n\n/**\n * Counters for what a projection could not carry over.\n *\n * A nonzero count is how a structural change announces itself. Without these\n * it would be invisible: dropping a field looks exactly like a field that was\n * never there, and the row just ends up prefilled with less than it should\n * have been.\n */\ninterface ProjectionLoss {\n /** Models the catalog returned that were not objects. */\n records: number\n /** Entries whose `api` was missing — without it, compat cannot be filtered. */\n missingApi: number\n /** Level-map values dropped for not being `string | null`. */\n droppedLevels: number\n}\n\n/** The shape we require of the catalog module, checked before it is trusted. */\ninterface PiAiCatalogModule {\n getBuiltinProviders?: () => unknown\n getBuiltinModels?: (provider: string) => unknown\n getBuiltinModelDataGeneratedAt?: () => unknown\n}\n\n/**\n * Whether a directory holds the pi-ai package.\n * @param dir - candidate package directory.\n * @returns true when it carries a package manifest.\n */\nfunction isPiAiPackage(dir: string): boolean {\n return existsSync(join(dir, 'package.json'))\n}\n\n/**\n * Walk up from a resolved module entry looking for pi-ai's package directory,\n * mirroring the ancestor `node_modules` lookup Node itself performs.\n *\n * The walk exists because pi-ai cannot be resolved by specifier: its `exports`\n * has no `require`/`default` condition, so a CJS resolution reports the\n * subpath as not exported rather than following it.\n * @param entryPath - a resolved file inside the host adapter package.\n * @returns the pi-ai package directory, or undefined when the chain misses it.\n */\nfunction piAiPackageDirFrom(entryPath: string): string | undefined {\n let dir = dirname(entryPath)\n for (let depth = 0; depth < RESOLUTION_DEPTH; depth += 1) {\n const candidate = join(dir, 'node_modules', PI_AI_PACKAGE)\n if (isPiAiPackage(candidate)) return candidate\n const parent = dirname(dir)\n if (parent === dir || parent.length === 0) break\n dir = parent\n }\n return undefined\n}\n\n/**\n * Profile directory names under a DSH home.\n * @param dshHome - the harness home.\n * @returns the directory names; empty when the home is unreadable.\n */\nfunction profileNames(dshHome: string): string[] {\n try {\n return readdirSync(join(dshHome, 'profiles'), { withFileTypes: true })\n .filter(entry => entry.isDirectory())\n .map(entry => entry.name)\n .filter(name => name.length > 0 && !name.startsWith('.'))\n } catch {\n return []\n }\n}\n\n/**\n * Module-resolution anchors, most authoritative first.\n *\n * The profile-scoped link paths lead because Node resolves a symlink to its\n * real path before resolving imports: a plugin installed as a link would\n * otherwise search its own checkout and never reach the host's tree. Each\n * anchor is a *path*, not a loaded module — `createRequire` resolves from the\n * string given to it without following the link itself.\n * @returns candidate paths to resolve the host adapter from.\n */\nfunction resolutionAnchors(): string[] {\n const anchors: string[] = []\n const dshHome = process.env.DSH_HOME\n if (dshHome !== undefined && dshHome.length > 0) {\n for (const profile of profileNames(dshHome)) {\n anchors.push(join(dshHome, 'profiles', profile, 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n }\n // The shared layer, then the home root — the latter can hold a stale copy,\n // so it only ever answers when nothing closer did.\n anchors.push(join(dshHome, 'profiles', 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n anchors.push(join(dshHome, 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n }\n try {\n anchors.push(fileURLToPath(import.meta.url))\n } catch { /* a non-file URL anchor would only break resolution anyway */ }\n return anchors\n}\n\n/**\n * Direct package paths probed only when the host adapter cannot be resolved at\n * all. Kept separate from {@link resolutionAnchors} because these are guesses\n * about layout, not resolution — anything they find is labelled a fallback.\n * @returns candidate pi-ai package directories, most-likely first.\n */\nfunction directProbes(): string[] {\n const dshHome = process.env.DSH_HOME\n if (dshHome === undefined || dshHome.length === 0) return []\n return [\n join(dshHome, 'profiles', 'node_modules', PI_AI_PACKAGE),\n join(dshHome, 'node_modules', PI_AI_PACKAGE),\n ]\n}\n\n/**\n * Read a package's version without going through module resolution (pi-ai's\n * `exports` refuses every subpath, including its own manifest).\n * @param packageDir - the package directory.\n * @returns the version, or undefined when unreadable.\n */\nfunction packageVersion(packageDir: string): string | undefined {\n try {\n const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { version?: unknown }\n return typeof pkg.version === 'string' ? pkg.version : undefined\n } catch {\n return undefined\n }\n}\n\n/** A located package, with how it was found. */\ninterface ResolvedPackage {\n readonly dir: string\n readonly via: 'adapter' | 'home-fallback'\n}\n\n/**\n * Locate the pi-ai package the running host loads.\n *\n * Resolution follows the host adapter on purpose: that is the copy the host's\n * own `import` reaches, so its data describes the running host. Only when no\n * anchor reaches the adapter does the search fall back to guessing at layouts —\n * and that result is labelled, because it may be a copy the host never loads.\n * @returns the package directory with its provenance, or undefined.\n */\nfunction resolvePiAiPackageDir(): ResolvedPackage | undefined {\n for (const anchor of resolutionAnchors()) {\n try {\n const require = createRequire(anchor)\n const adapterEntry = require.resolve(PI_AI_ADAPTER_PACKAGE)\n const found = piAiPackageDirFrom(adapterEntry)\n if (found !== undefined) return { dir: found, via: 'adapter' }\n } catch { /* this anchor cannot see the adapter; try the next */ }\n }\n for (const dir of directProbes()) {\n if (isPiAiPackage(dir)) return { dir, via: 'home-fallback' }\n }\n return undefined\n}\n\n/**\n * Every pi-ai copy the anchors can see, for the diagnostics block.\n *\n * A second copy is not an error by itself — a DSH home legitimately carries\n * one — but \"which copy answered\" is the first question any wrong-value report\n * raises, and it is unanswerable after the fact unless it is recorded here.\n * @returns the copies, each with its version where readable.\n */\nfunction collectCopies(): PiAiCopy[] {\n const dirs = new Set<string>(directProbes())\n for (const anchor of resolutionAnchors()) {\n try {\n const require = createRequire(anchor)\n const found = piAiPackageDirFrom(require.resolve(PI_AI_ADAPTER_PACKAGE))\n if (found !== undefined) dirs.add(found)\n } catch { /* this anchor cannot see the adapter */ }\n }\n const copies: PiAiCopy[] = []\n const seenReal = new Set<string>()\n for (const dir of dirs) {\n if (!isPiAiPackage(dir)) continue\n // A symlinked copy shows up under both its link path and its real path,\n // and both describe the same files — counting them separately would report\n // a phantom extra copy on every linked install.\n let real = dir\n try {\n real = realpathSync(dir)\n } catch { /* keep the link path if it cannot be resolved */ }\n if (seenReal.has(real)) continue\n seenReal.add(real)\n const copy: { dir: string; version?: string } = { dir }\n const version = packageVersion(dir)\n if (version !== undefined) copy.version = version\n copies.push(copy)\n }\n return copies\n}\n\n/** Read one object-valued field off a raw model. */\nfunction objectField(source: Record<string, unknown>, key: string): Record<string, unknown> | undefined {\n const value = source[key]\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n}\n\n/**\n * Project one catalog model onto the prefill fields, dropping anything absent\n * so the served payload stays small.\n * @param raw - the model object the catalog API returned.\n * @param loss - accumulator for values the projection could not carry.\n * @returns the projected entry.\n */\nfunction projectModel(raw: Record<string, unknown>, loss: ProjectionLoss): PiAiModelEntry {\n const entry: {\n provider: string\n api: string\n id: string\n name?: string\n contextWindow?: number\n maxTokens?: number\n input?: string[]\n reasoning?: boolean\n thinkingLevelMap?: Record<string, string | null>\n compat?: Record<string, unknown>\n } = {\n provider: typeof raw['provider'] === 'string' ? raw['provider'] : '',\n api: typeof raw['api'] === 'string' ? raw['api'] : '',\n id: typeof raw['id'] === 'string' ? raw['id'] : '',\n }\n if (entry.api.length === 0) loss.missingApi += 1\n\n if (typeof raw['name'] === 'string' && raw['name'].length > 0) entry.name = raw['name']\n if (typeof raw['contextWindow'] === 'number') entry.contextWindow = raw['contextWindow']\n if (typeof raw['maxTokens'] === 'number') entry.maxTokens = raw['maxTokens']\n if (typeof raw['reasoning'] === 'boolean') entry.reasoning = raw['reasoning']\n if (Array.isArray(raw['input'])) {\n entry.input = raw['input'].filter((m): m is string => typeof m === 'string')\n }\n\n const levelMap = objectField(raw, 'thinkingLevelMap')\n if (levelMap !== undefined) {\n const projected: Record<string, string | null> = {}\n for (const [level, wire] of Object.entries(levelMap)) {\n if (wire === null || typeof wire === 'string') projected[level] = wire\n else loss.droppedLevels += 1\n }\n if (Object.keys(projected).length > 0) entry.thinkingLevelMap = projected\n }\n\n const compat = objectField(raw, 'compat')\n if (compat !== undefined && Object.keys(compat).length > 0) entry.compat = compat\n\n return entry\n}\n\n/**\n * Load the catalog through pi-ai's own API.\n *\n * The API's presence is what is checked, not a version: a package that still\n * exports `getBuiltinProviders`/`getBuiltinModels` is usable no matter what\n * version it calls itself, and one that stopped exporting them is unusable no\n * matter how familiar its version looks.\n * @param resolved - the located package, and how it was found.\n * @returns the catalog, or undefined when the API is not there to call.\n */\nasync function loadCatalog(resolved: ResolvedPackage): Promise<PiAiCatalog | undefined> {\n const entry = join(resolved.dir, PI_AI_ALL_SUBPATH)\n let module: PiAiCatalogModule\n try {\n module = await import(pathToFileURL(entry).href) as PiAiCatalogModule\n } catch {\n return undefined\n }\n const { getBuiltinProviders, getBuiltinModels } = module\n if (typeof getBuiltinProviders !== 'function' || typeof getBuiltinModels !== 'function') {\n return undefined\n }\n\n let providerIds: unknown\n try {\n providerIds = getBuiltinProviders()\n } catch {\n return undefined\n }\n if (!Array.isArray(providerIds)) return undefined\n\n const loss: ProjectionLoss = { records: 0, missingApi: 0, droppedLevels: 0 }\n const models: PiAiModelEntry[] = []\n for (const providerId of providerIds) {\n if (typeof providerId !== 'string' || providerId.length === 0) continue\n let provided: unknown\n try {\n provided = getBuiltinModels.call(module, providerId)\n } catch {\n continue\n }\n if (!Array.isArray(provided)) continue\n for (const raw of provided) {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n loss.records += 1\n continue\n }\n models.push(projectModel(raw as Record<string, unknown>, loss))\n }\n }\n if (models.length === 0) return undefined\n\n const warnings: string[] = []\n if (resolved.via === 'home-fallback') {\n warnings.push(\n 'the host adapter could not be resolved, so the catalog came from a guessed path; '\n + 'these values may not be the ones the running host uses',\n )\n }\n if (loss.records > 0) {\n warnings.push(`${String(loss.records)} catalog entr(ies) were not model objects and were skipped`)\n }\n if (loss.missingApi > 0) {\n warnings.push(\n `${String(loss.missingApi)} model(s) carry no \\`api\\`, so their compat switches cannot be filtered `\n + 'by protocol and are left to the host catalog',\n )\n }\n if (loss.droppedLevels > 0) {\n warnings.push(\n `${String(loss.droppedLevels)} thinking-level value(s) were not \\`string | null\\` and were dropped`,\n )\n }\n // A field that silently stops arriving would otherwise look exactly like a\n // catalog that never had it. These fire only on total absence, which is the\n // signature of a rename rather than of ordinary data churn.\n if (!models.some(model => model.compat !== undefined)) {\n warnings.push('no model carried a `compat` block; the field may have been renamed')\n }\n if (!models.some(model => model.thinkingLevelMap !== undefined)\n && models.some(model => model.reasoning === true)) {\n warnings.push('no model carried a `thinkingLevelMap`; the field may have been renamed')\n }\n\n let generatedAt: string | undefined\n try {\n const timestamp = module.getBuiltinModelDataGeneratedAt?.()\n if (typeof timestamp === 'number') generatedAt = new Date(timestamp).toISOString()\n } catch { /* the timestamp is a nicety, never a gate */ }\n\n const piAiVersion = packageVersion(resolved.dir)\n return {\n models,\n diagnostics: {\n packageDir: resolved.dir,\n resolvedVia: resolved.via,\n copies: collectCopies(),\n warnings,\n },\n ...(generatedAt !== undefined ? { generatedAt } : {}),\n ...(piAiVersion !== undefined ? { piAiVersion } : {}),\n }\n}\n\n/** The in-flight (or settled) load; a promise so concurrent callers share one. */\nlet load: Promise<PiAiCatalog | undefined> | undefined\n\n/** Why the last load produced nothing, for the refusal the route returns. */\nlet failure: string | undefined\n\n/**\n * The installed pi-ai catalog, or undefined when the host offers none.\n *\n * Loaded once and memoized: the catalog is a snapshot built into the installed\n * package, so it cannot change while this process runs. The API module is\n * imported lazily — the host adapter imports the same module, so in a running\n * host this is a module-cache hit rather than a load.\n * @returns the catalog, or undefined for the models.dev-only fallback.\n */\nexport function piAiCatalog(): Promise<PiAiCatalog | undefined> {\n load ??= (async (): Promise<PiAiCatalog | undefined> => {\n const resolved = resolvePiAiPackageDir()\n if (resolved === undefined) {\n failure = 'no @earendil-works/pi-ai package could be located from the host adapter'\n return undefined\n }\n const catalog = await loadCatalog(resolved)\n if (catalog === undefined) {\n failure = `the catalog API (${PI_AI_ALL_SUBPATH}) could not be called at ${resolved.dir}`\n }\n return catalog\n })()\n return load\n}\n\n/**\n * Every pi-ai copy visible on this host.\n *\n * Exported for the failure path: when the catalog is unavailable, \"which\n * copies exist, where, and what version\" is the only fact that turns a refusal\n * into something actionable.\n * @returns the copies, with versions where readable.\n */\nexport function piAiCopies(): readonly PiAiCopy[] {\n return collectCopies()\n}\n\n/**\n * Why the catalog could not be served, once a load has been attempted.\n * @returns the reason, or undefined when nothing has failed.\n */\nexport function piAiUnavailableReason(): string | undefined {\n return failure\n}\n","/**\r\n * dsh-model-extension — host-side entry (v1.0.0).\r\n *\r\n * Two halves:\r\n * 1. The adapter anchor: ADVISORY at runtime — on a host-version mismatch the\r\n * plugin logs a warning and registers anyway. The UI is fully plugin-owned,\r\n * and the host's own settings schema validation plus revision fencing keep\r\n * a mismatched wire write from corrupting anything (worst case: a refused\r\n * write with a diagnostic), so a hard gate would only make compatible\r\n * upgrades silently drop the plugin.\r\n * 2. The metadata service: three same-origin routes backing the Models+ page's\r\n * quick-load. models.dev is fetched ONLY on an explicit user click (the\r\n * title-row button); the raw file caches next to settings.yaml under DSH\r\n * home. Only the flat models.dev shape (top-level keys = full model ids)\r\n * is accepted; anything else is a refusal, per plan. The pi-ai catalog is\r\n * the other, richer source: it is read from the installed package the host\r\n * adapter itself loads, with no network call at all (./pi-ai-catalog.ts).\r\n */\r\nimport { createRequire } from 'node:module'\r\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\r\nimport { dirname, join } from 'node:path'\r\nimport { fileURLToPath } from 'node:url'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { piAiCatalog, piAiCopies, piAiUnavailableReason } from './pi-ai-catalog'\r\n\r\n/** Adapter anchor, injected at build time from package.json `dsh.adapter`. */\r\ndeclare const __DSH_ADAPTER_VERSION__: string\r\nconst ADAPTER_VERSION: string = typeof __DSH_ADAPTER_VERSION__ === 'string'\r\n ? __DSH_ADAPTER_VERSION__\r\n : '0.0.0-unknown'\r\n\r\n/** Cordis service name (distinct from the npm package name). */\r\nexport const name = 'model-extension'\r\n\r\n/** Required services — the route registry lives on the host web server. */\r\nexport const inject = ['webServer']\r\n\r\n/** The one metadata source this plugin accepts (plan: fixed, never configurable). */\r\nconst METADATA_URL = 'https://models.dev/models.json'\r\n\r\n/** Download timeout in milliseconds. */\r\nconst DOWNLOAD_TIMEOUT_MS = 30_000\r\n\r\n/** Structured face of the host web server this plugin needs. */\r\ninterface WebServerLike {\r\n register(route: {\r\n kind: 'exact'\r\n path: string\r\n handler: (req: IncomingMessage, res: ServerResponse) => void\r\n }): (() => void) | Promise<() => void>\r\n}\r\n\r\n/** Minimal cordis context face this plugin uses. */\r\ninterface PluginContext {\r\n logger?: {\r\n warn(msg: string): void\r\n info(msg: string): void\r\n }\r\n effect?: (fn: () => (() => void) | void, name?: string) => void\r\n webServer?: WebServerLike\r\n}\r\n\r\n/** One precise model entry in the served index. */\r\ninterface MetadataEntry {\r\n id: string\r\n context?: number\r\n output?: number\r\n input: string[]\r\n reasoning: boolean\r\n}\r\n\r\n/**\r\n * Read the running host's version by resolving the installed CLI package's\r\n * manifest.\r\n *\r\n * Node resolves symlinks by default, so the plugin module's real path is the\r\n * project checkout — walking up from there never reaches the global tree.\r\n * The resolution therefore starts at the *link* path under the profile\r\n * node_modules (symlink not resolved), then falls back to this module's own\r\n * real path.\r\n * @returns the version string, or undefined when unresolvable.\r\n */\r\nfunction readHostVersion(): string | undefined {\r\n const candidates: string[] = []\r\n const dshHome = process.env.DSH_HOME\r\n if (dshHome !== undefined && dshHome.length > 0) {\r\n candidates.push(`${dshHome}/profiles/web/node_modules/dsh-model-extension/lib/index.js`)\r\n candidates.push(`${dshHome}/profiles/node/node_modules/dsh-model-extension/lib/index.js`)\r\n }\r\n try {\r\n candidates.push(fileURLToPath(import.meta.url))\r\n } catch { /* ignore */ }\r\n\r\n for (const specifier of ['@deepseek-ai/dsh/package.json', '@deepseek-ai/dsh-base/package.json']) {\r\n for (const base of candidates) {\r\n try {\r\n const require = createRequire(base)\r\n const manifestPath = require.resolve(specifier)\r\n const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: unknown }\r\n if (typeof manifest.version === 'string') return manifest.version\r\n } catch { /* next candidate */ }\r\n }\r\n }\r\n return undefined\r\n}\r\n\r\n/**\r\n * Resolve the directory the metadata file lives in: next to the first\r\n * settings.yaml found (plan: \"DSH home, same level as settings.yaml\"),\r\n * falling back to DSH home itself and then the process cwd.\r\n * @returns the directory path.\r\n */\r\nfunction metadataDir(): string {\r\n const dshHome = process.env.DSH_HOME\r\n const probes: string[] = []\r\n if (dshHome !== undefined && dshHome.length > 0) {\r\n probes.push(join(dshHome, 'settings.yaml'))\r\n probes.push(join(dshHome, 'profiles', 'web', 'settings.yaml'))\r\n probes.push(dshHome)\r\n }\r\n probes.push(process.cwd())\r\n for (const probe of probes) {\r\n if (probe.endsWith('.yaml')) {\r\n if (existsSync(probe)) return dirname(probe)\r\n continue\r\n }\r\n return probe\r\n }\r\n return process.cwd()\r\n}\r\n\r\n/** The metadata file's absolute path. */\r\nfunction metadataPath(): string {\r\n return join(metadataDir(), 'models.json')\r\n}\r\n\r\n/**\r\n * Validate the raw models.dev payload: ONLY the flat shape (top-level keys\r\n * are full model ids) is accepted, per plan.\r\n * @param data - the parsed payload.\r\n * @returns the precise index.\r\n * @throws when the shape is not the flat models.dev format.\r\n */\r\nfunction toIndex(data: unknown): MetadataEntry[] {\r\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\r\n throw new Error('unexpected metadata shape (not an object)')\r\n }\r\n const entries = Object.entries(data as Record<string, unknown>)\r\n if (entries.length === 0) throw new Error('metadata file is empty')\r\n const first = entries[0]![1]\r\n if (typeof first !== 'object' || first === null || 'models' in (first as object)) {\r\n throw new Error('unexpected metadata shape (provider-grouped or foreign format)')\r\n }\r\n const index: MetadataEntry[] = []\r\n for (const [key, value] of entries) {\r\n if (typeof value !== 'object' || value === null) continue\r\n const row = value as Record<string, unknown>\r\n const limit = typeof row['limit'] === 'object' && row['limit'] !== null\r\n ? row['limit'] as Record<string, unknown>\r\n : {}\r\n const modalities = typeof row['modalities'] === 'object' && row['modalities'] !== null\r\n ? row['modalities'] as Record<string, unknown>\r\n : {}\r\n const rawInput = Array.isArray(modalities['input'])\r\n ? modalities['input'].filter((m): m is string => typeof m === 'string')\r\n : []\r\n index.push({\r\n id: typeof row['id'] === 'string' ? row['id'] : key,\r\n context: typeof limit['context'] === 'number' ? limit['context'] : undefined,\r\n output: typeof limit['output'] === 'number' ? limit['output'] : undefined,\r\n // Only the modalities this UI offers; anything else is dropped.\r\n input: rawInput.filter(m => m === 'text' || m === 'image'),\r\n reasoning: row['reasoning'] === true,\r\n })\r\n }\r\n return index\r\n}\r\n\r\n/** Read the cached metadata file, or undefined when absent/unreadable. */\r\nfunction readCachedIndex(): MetadataEntry[] | undefined {\r\n try {\r\n const raw = readFileSync(metadataPath(), 'utf8')\r\n return toIndex(JSON.parse(raw) as unknown)\r\n } catch {\r\n return undefined\r\n }\r\n}\r\n\r\n/** Whether the raw payload is accepted for caching. */\r\nfunction isIndexable(data: unknown): boolean {\r\n try {\r\n toIndex(data)\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Write one JSON response. */\r\nfunction json(res: ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/**\r\n * Host plugin body: gate on the exact host version, then mount the two\r\n * metadata routes (an explicit webServer register, mirroring the hmr plugin's\r\n * usage of the same registry).\r\n * @param ctx - cordis context.\r\n */\r\nexport function apply(ctx: PluginContext): void {\r\n const hostVersion = readHostVersion()\r\n if (hostVersion === undefined || hostVersion !== ADAPTER_VERSION) {\r\n // Advisory only: the plugin-owned UI does not fork host components, and\r\n // the host's settings schema validation + revision fence reject (never\r\n // corrupt) anything a mismatched wire contract might produce.\r\n ctx.logger?.warn(\r\n `[dsh-model-extension] host version ${hostVersion ?? '<unreadable>'} differs from the validated anchor ${ADAPTER_VERSION}; registering anyway — if settings reads or writes misbehave, please report the host version`,\r\n )\r\n }\r\n\r\n const webServer = ctx.webServer\r\n if (webServer === undefined) {\r\n ctx.logger?.warn('[dsh-model-extension] webServer service absent; metadata routes not mounted')\r\n return\r\n }\r\n\r\n ctx.effect?.(() => {\r\n // GET: serve the cached index; 404 (with guidance) when not ready yet.\r\n const disposeIndex = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/models-index',\r\n handler: (req, res) => {\r\n if (req.method !== 'GET' && req.method !== 'HEAD') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n const index = readCachedIndex()\r\n if (index === undefined) {\r\n json(res, 404, { ok: false, message: '元数据未就绪:请点击「下载/更新元数据」,或手动将 models.json 放入 DSH home。' })\r\n return\r\n }\r\n json(res, 200, index)\r\n },\r\n })\r\n\r\n // POST: the explicit download/update. Fetches models.dev, validates the\r\n // flat shape, caches next to settings.yaml, and answers with the index.\r\n const disposeDownload = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/models-download',\r\n handler: (req, res) => {\r\n if (req.method !== 'POST' && req.method !== 'GET') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n void (async () => {\r\n try {\r\n const response = await fetch(METADATA_URL, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })\r\n if (!response.ok) {\r\n json(res, 502, { ok: false, message: `models.dev 返回 HTTP ${String(response.status)}` })\r\n return\r\n }\r\n const data: unknown = await response.json()\r\n if (!isIndexable(data)) {\r\n json(res, 422, { ok: false, message: 'models.dev 数据格式不被识别(仅支持 models.dev/models.json 的扁平格式)' })\r\n return\r\n }\r\n writeFileSync(metadataPath(), JSON.stringify(data), 'utf8')\r\n ctx.logger?.info(`[dsh-model-extension] metadata cached at ${metadataPath()}`)\r\n json(res, 200, { ok: true, index: toIndex(data) })\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error)\r\n json(res, 502, { ok: false, message })\r\n }\r\n })()\r\n },\r\n })\r\n\r\n // GET: the installed pi-ai catalog. Served straight from the package the\r\n // host adapter loads — no network, no cache file. A refusal (404) is the\r\n // client's signal to fall back to the models.dev index alone.\r\n const disposeCatalog = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/pi-ai-catalog',\r\n handler: (req, res) => {\r\n if (req.method !== 'GET' && req.method !== 'HEAD') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n void (async () => {\r\n try {\r\n const catalog = await piAiCatalog()\r\n if (catalog === undefined) {\r\n // The refusal carries the reason AND the copies that are visible:\r\n // \"the host ships no pi-ai\" and \"it ships one this reader cannot\r\n // call\" are otherwise indistinguishable, and only the second is\r\n // worth acting on.\r\n json(res, 404, {\r\n ok: false,\r\n message: `pi-ai 内置目录不可用:${piAiUnavailableReason() ?? '原因未知'}`,\r\n copies: piAiCopies(),\r\n })\r\n return\r\n }\r\n json(res, 200, { ok: true, ...catalog })\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error)\r\n json(res, 500, { ok: false, message })\r\n }\r\n })()\r\n },\r\n })\r\n\r\n return () => {\r\n const a = disposeIndex\r\n if (typeof a === 'function') a()\r\n const b = disposeDownload\r\n if (typeof b === 'function') b()\r\n const c = disposeCatalog\r\n if (typeof c === 'function') c()\r\n }\r\n }, 'dsh-model-extension: metadata routes')\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/pi-ai-catalog.ts","../src/index.ts"],"sourcesContent":["/**\n * pi-ai catalog reader.\n *\n * The values a Models+ row can meaningfully carry are exactly the fields of\n * pi-ai's `Model` — `thinkingLevelMap` (our `reasoningEfforts`), `compat`,\n * capacities and modalities — so this module reads the installed catalog and\n * projects it into the prefill index. models.dev knows none of the reasoning\n * or compat surface.\n *\n * ## Why the catalog is read through pi-ai's own API\n *\n * `@earendil-works/pi-ai/providers/all` is a *declared* subpath export of the\n * installed package, and the host's own adapter reads the catalog through it\n * (`dsh-llm-pi-ai` calls `getBuiltinProviders()` / `getBuiltinModels()`). Doing\n * the same buys three things:\n *\n * - **The data-format responsibility stays with pi-ai.** The `data/*.json`\n * layout, its `schemaVersion`, and how groups flatten into models are pi-ai's\n * business. Calling its API means a future layout change is absorbed by the\n * package that made it, instead of by this plugin.\n * - **The data is the host's own.** Same package, same module instance, same\n * values the adapter resolves — there is no second opinion to drift from.\n * - **No version gate is needed, or wanted.** A host upgrade that changes the\n * catalog simply yields the new catalog. Gating on a version we recognize\n * would turn every host upgrade into a silent feature outage; what this\n * module guards instead is the *shape of what it received*.\n *\n * ## What is actually guarded\n *\n * The failure modes are answered as concrete findings, never as a version\n * guess:\n *\n * | Drift | Answer |\n * |---|---|\n * | pi-ai renamed or dropped from the adapter's deps | resolution fails, and the refusal carries every copy that was visible |\n * | a second, stale copy wins resolution | resolution starts at the adapter the host loads, and every visible copy is reported |\n * | the catalog API stops exporting what we call | the source is disabled, naming the missing export |\n * | a projected field stops arriving | a finding is reported, and the affected rows simply inherit from the host catalog |\n *\n * Resolution is hand-rolled for one reason: pi-ai is ESM-only with no\n * `require`/`default` export condition, so `createRequire().resolve()` refuses\n * every subpath. Anchors start inside the profile because Node resolves a\n * symlink to its real path before resolving imports, and the home root is a\n * last resort — a DSH home can hold a stale copy there.\n *\n * The one thing this module cannot detect by itself is a change to the *host's*\n * acceptance rules: its `COMPAT_GATES` offer/withhold split is not exported,\n * and a switch it withdrew would be refused at save time. That is covered by\n * `scripts/audit-pi-ai-contract.mjs`.\n *\n * Nothing here touches the network.\n *\n * @module dsh-model-extension/pi-ai-catalog\n */\n\nimport { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/** The pi-ai package whose catalog backs a `dsh-llm-pi-ai` route. */\nconst PI_AI_PACKAGE = '@earendil-works/pi-ai'\n\n/** The host adapter that owns the catalog routes; its location seeds resolution. */\nconst PI_AI_ADAPTER_PACKAGE = '@deepseek-ai/dsh-llm-pi-ai'\n\n/** The declared subpath this module reads the catalog through. */\nconst PI_AI_ALL_SUBPATH = 'dist/providers/all.js'\n\n/** How many parent directories the hand-rolled `node_modules` walk visits. */\nconst RESOLUTION_DEPTH = 12\n\n/** One catalog model projected to the fields the prefill consumes. */\nexport interface PiAiModelEntry {\n /** pi-ai provider id — the route key a catalog route is named after. */\n readonly provider: string\n /** Wire protocol this model speaks, which picks its `compat` field set. */\n readonly api: string\n /** Model id passed to the provider. */\n readonly id: string\n /** Display name, when the catalog carries one. */\n readonly name?: string\n /** Maximum combined request and response context in tokens. */\n readonly contextWindow?: number\n /** Maximum output tokens. */\n readonly maxTokens?: number\n /** Accepted request modalities. */\n readonly input?: readonly string[]\n /** Whether the model reasons at all; `false` makes every effort moot. */\n readonly reasoning?: boolean\n /**\n * pi-ai's own level→wire-spelling map. Semantically our `reasoningEfforts`,\n * except that `null` means *unsupported* here while our dict reads it as\n * *supported, send nothing* — see the client-side mapping before copying.\n */\n readonly thinkingLevelMap?: Readonly<Record<string, string | null>>\n /** pi-ai's wire-compatibility switches for this model. */\n readonly compat?: Readonly<Record<string, unknown>>\n}\n\n/** One pi-ai copy the resolution anchors could see. */\nexport interface PiAiCopy {\n /** Package directory. */\n readonly dir: string\n /** Its version, when readable. */\n readonly version?: string\n}\n\n/**\n * Provenance and drift findings attached to every served catalog.\n *\n * These exist so that no failure stays silent. A catalog that is empty, stale,\n * or read from a build the host does not actually load all look identical from\n * the outside — the difference surfaces much later, as wrong values written\n * into settings, which is the outcome this structure is meant to prevent.\n */\nexport interface PiAiCatalogDiagnostics {\n /** Directory the served data was read from. */\n readonly packageDir: string\n /**\n * How the package was found. `adapter` means the resolution followed the\n * same path the host's own import does, so the data describes the running\n * host; `home-fallback` means it did not, and callers should treat the\n * values as advisory.\n */\n readonly resolvedVia: 'adapter' | 'home-fallback'\n /** Every copy the anchors could see — more than one is itself a finding. */\n readonly copies: readonly PiAiCopy[]\n /** Non-fatal findings worth surfacing: missing fields, odd shapes, etc. */\n readonly warnings: readonly string[]\n}\n\n/** The projected catalog, plus the provenance a UI can show. */\nexport interface PiAiCatalog {\n /** Installed pi-ai version, when its manifest is readable. */\n readonly piAiVersion?: string\n /** Generation timestamp of the shipped catalog snapshot. */\n readonly generatedAt?: string\n /** Every catalog model, across every provider. */\n readonly models: readonly PiAiModelEntry[]\n /** Provenance and drift findings, so a silent failure is impossible. */\n readonly diagnostics: PiAiCatalogDiagnostics\n}\n\n/**\n * Counters for what a projection could not carry over.\n *\n * A nonzero count is how a structural change announces itself. Without these\n * it would be invisible: dropping a field looks exactly like a field that was\n * never there, and the row just ends up prefilled with less than it should\n * have been.\n */\ninterface ProjectionLoss {\n /** Models the catalog returned that were not objects. */\n records: number\n /** Entries whose `api` was missing — without it, compat cannot be filtered. */\n missingApi: number\n /** Level-map values dropped for not being `string | null`. */\n droppedLevels: number\n}\n\n/** The shape we require of the catalog module, checked before it is trusted. */\ninterface PiAiCatalogModule {\n getBuiltinProviders?: () => unknown\n getBuiltinModels?: (provider: string) => unknown\n getBuiltinModelDataGeneratedAt?: () => unknown\n}\n\n/**\n * Whether a directory holds the pi-ai package.\n * @param dir - candidate package directory.\n * @returns true when it carries a package manifest.\n */\nfunction isPiAiPackage(dir: string): boolean {\n return existsSync(join(dir, 'package.json'))\n}\n\n/**\n * Walk up from a resolved module entry looking for pi-ai's package directory,\n * mirroring the ancestor `node_modules` lookup Node itself performs.\n *\n * The walk exists because pi-ai cannot be resolved by specifier: its `exports`\n * has no `require`/`default` condition, so a CJS resolution reports the\n * subpath as not exported rather than following it.\n * @param entryPath - a resolved file inside the host adapter package.\n * @returns the pi-ai package directory, or undefined when the chain misses it.\n */\nfunction piAiPackageDirFrom(entryPath: string): string | undefined {\n let dir = dirname(entryPath)\n for (let depth = 0; depth < RESOLUTION_DEPTH; depth += 1) {\n const candidate = join(dir, 'node_modules', PI_AI_PACKAGE)\n if (isPiAiPackage(candidate)) return candidate\n const parent = dirname(dir)\n if (parent === dir || parent.length === 0) break\n dir = parent\n }\n return undefined\n}\n\n/**\n * Profile directory names under a DSH home.\n * @param dshHome - the harness home.\n * @returns the directory names; empty when the home is unreadable.\n */\nfunction profileNames(dshHome: string): string[] {\n try {\n return readdirSync(join(dshHome, 'profiles'), { withFileTypes: true })\n .filter(entry => entry.isDirectory())\n .map(entry => entry.name)\n .filter(name => name.length > 0 && !name.startsWith('.'))\n } catch {\n return []\n }\n}\n\n/**\n * Module-resolution anchors, most authoritative first.\n *\n * The profile-scoped link paths lead because Node resolves a symlink to its\n * real path before resolving imports: a plugin installed as a link would\n * otherwise search its own checkout and never reach the host's tree. Each\n * anchor is a *path*, not a loaded module — `createRequire` resolves from the\n * string given to it without following the link itself.\n * @returns candidate paths to resolve the host adapter from.\n */\nfunction resolutionAnchors(): string[] {\n const anchors: string[] = []\n const dshHome = process.env.DSH_HOME\n if (dshHome !== undefined && dshHome.length > 0) {\n for (const profile of profileNames(dshHome)) {\n anchors.push(join(dshHome, 'profiles', profile, 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n }\n // The shared layer, then the home root — the latter can hold a stale copy,\n // so it only ever answers when nothing closer did.\n anchors.push(join(dshHome, 'profiles', 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n anchors.push(join(dshHome, 'node_modules', 'dsh-model-extension', 'lib', 'index.js'))\n }\n try {\n anchors.push(fileURLToPath(import.meta.url))\n } catch { /* a non-file URL anchor would only break resolution anyway */ }\n return anchors\n}\n\n/**\n * Direct package paths probed only when the host adapter cannot be resolved at\n * all. Kept separate from {@link resolutionAnchors} because these are guesses\n * about layout, not resolution — anything they find is labelled a fallback.\n * @returns candidate pi-ai package directories, most-likely first.\n */\nfunction directProbes(): string[] {\n const dshHome = process.env.DSH_HOME\n if (dshHome === undefined || dshHome.length === 0) return []\n return [\n join(dshHome, 'profiles', 'node_modules', PI_AI_PACKAGE),\n join(dshHome, 'node_modules', PI_AI_PACKAGE),\n ]\n}\n\n/**\n * Read a package's version without going through module resolution (pi-ai's\n * `exports` refuses every subpath, including its own manifest).\n * @param packageDir - the package directory.\n * @returns the version, or undefined when unreadable.\n */\nfunction packageVersion(packageDir: string): string | undefined {\n try {\n const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as { version?: unknown }\n return typeof pkg.version === 'string' ? pkg.version : undefined\n } catch {\n return undefined\n }\n}\n\n/** A located package, with how it was found. */\ninterface ResolvedPackage {\n readonly dir: string\n readonly via: 'adapter' | 'home-fallback'\n}\n\n/**\n * Locate the pi-ai package the running host loads.\n *\n * Resolution follows the host adapter on purpose: that is the copy the host's\n * own `import` reaches, so its data describes the running host. Only when no\n * anchor reaches the adapter does the search fall back to guessing at layouts —\n * and that result is labelled, because it may be a copy the host never loads.\n * @returns the package directory with its provenance, or undefined.\n */\nfunction resolvePiAiPackageDir(): ResolvedPackage | undefined {\n for (const anchor of resolutionAnchors()) {\n try {\n const require = createRequire(anchor)\n const adapterEntry = require.resolve(PI_AI_ADAPTER_PACKAGE)\n const found = piAiPackageDirFrom(adapterEntry)\n if (found !== undefined) return { dir: found, via: 'adapter' }\n } catch { /* this anchor cannot see the adapter; try the next */ }\n }\n for (const dir of directProbes()) {\n if (isPiAiPackage(dir)) return { dir, via: 'home-fallback' }\n }\n return undefined\n}\n\n/**\n * Every pi-ai copy the anchors can see, for the diagnostics block.\n *\n * A second copy is not an error by itself — a DSH home legitimately carries\n * one — but \"which copy answered\" is the first question any wrong-value report\n * raises, and it is unanswerable after the fact unless it is recorded here.\n * @returns the copies, each with its version where readable.\n */\nfunction collectCopies(): PiAiCopy[] {\n const dirs = new Set<string>(directProbes())\n for (const anchor of resolutionAnchors()) {\n try {\n const require = createRequire(anchor)\n const found = piAiPackageDirFrom(require.resolve(PI_AI_ADAPTER_PACKAGE))\n if (found !== undefined) dirs.add(found)\n } catch { /* this anchor cannot see the adapter */ }\n }\n const copies: PiAiCopy[] = []\n const seenReal = new Set<string>()\n for (const dir of dirs) {\n if (!isPiAiPackage(dir)) continue\n // A symlinked copy shows up under both its link path and its real path,\n // and both describe the same files — counting them separately would report\n // a phantom extra copy on every linked install.\n let real = dir\n try {\n real = realpathSync(dir)\n } catch { /* keep the link path if it cannot be resolved */ }\n if (seenReal.has(real)) continue\n seenReal.add(real)\n const copy: { dir: string; version?: string } = { dir }\n const version = packageVersion(dir)\n if (version !== undefined) copy.version = version\n copies.push(copy)\n }\n return copies\n}\n\n/** Read one object-valued field off a raw model. */\nfunction objectField(source: Record<string, unknown>, key: string): Record<string, unknown> | undefined {\n const value = source[key]\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n}\n\n/**\n * Project one catalog model onto the prefill fields, dropping anything absent\n * so the served payload stays small.\n * @param raw - the model object the catalog API returned.\n * @param loss - accumulator for values the projection could not carry.\n * @returns the projected entry.\n */\nfunction projectModel(raw: Record<string, unknown>, loss: ProjectionLoss): PiAiModelEntry {\n const entry: {\n provider: string\n api: string\n id: string\n name?: string\n contextWindow?: number\n maxTokens?: number\n input?: string[]\n reasoning?: boolean\n thinkingLevelMap?: Record<string, string | null>\n compat?: Record<string, unknown>\n } = {\n provider: typeof raw['provider'] === 'string' ? raw['provider'] : '',\n api: typeof raw['api'] === 'string' ? raw['api'] : '',\n id: typeof raw['id'] === 'string' ? raw['id'] : '',\n }\n if (entry.api.length === 0) loss.missingApi += 1\n\n if (typeof raw['name'] === 'string' && raw['name'].length > 0) entry.name = raw['name']\n if (typeof raw['contextWindow'] === 'number') entry.contextWindow = raw['contextWindow']\n if (typeof raw['maxTokens'] === 'number') entry.maxTokens = raw['maxTokens']\n if (typeof raw['reasoning'] === 'boolean') entry.reasoning = raw['reasoning']\n if (Array.isArray(raw['input'])) {\n entry.input = raw['input'].filter((m): m is string => typeof m === 'string')\n }\n\n const levelMap = objectField(raw, 'thinkingLevelMap')\n if (levelMap !== undefined) {\n const projected: Record<string, string | null> = {}\n for (const [level, wire] of Object.entries(levelMap)) {\n if (wire === null || typeof wire === 'string') projected[level] = wire\n else loss.droppedLevels += 1\n }\n if (Object.keys(projected).length > 0) entry.thinkingLevelMap = projected\n }\n\n const compat = objectField(raw, 'compat')\n if (compat !== undefined && Object.keys(compat).length > 0) entry.compat = compat\n\n return entry\n}\n\n/**\n * Load the catalog through pi-ai's own API.\n *\n * The API's presence is what is checked, not a version: a package that still\n * exports `getBuiltinProviders`/`getBuiltinModels` is usable no matter what\n * version it calls itself, and one that stopped exporting them is unusable no\n * matter how familiar its version looks.\n * @param resolved - the located package, and how it was found.\n * @returns the catalog, or undefined when the API is not there to call.\n */\nasync function loadCatalog(resolved: ResolvedPackage): Promise<PiAiCatalog | undefined> {\n const entry = join(resolved.dir, PI_AI_ALL_SUBPATH)\n let module: PiAiCatalogModule\n try {\n module = await import(pathToFileURL(entry).href) as PiAiCatalogModule\n } catch {\n return undefined\n }\n const { getBuiltinProviders, getBuiltinModels } = module\n if (typeof getBuiltinProviders !== 'function' || typeof getBuiltinModels !== 'function') {\n return undefined\n }\n\n let providerIds: unknown\n try {\n providerIds = getBuiltinProviders()\n } catch {\n return undefined\n }\n if (!Array.isArray(providerIds)) return undefined\n\n const loss: ProjectionLoss = { records: 0, missingApi: 0, droppedLevels: 0 }\n const models: PiAiModelEntry[] = []\n for (const providerId of providerIds) {\n if (typeof providerId !== 'string' || providerId.length === 0) continue\n let provided: unknown\n try {\n provided = getBuiltinModels.call(module, providerId)\n } catch {\n continue\n }\n if (!Array.isArray(provided)) continue\n for (const raw of provided) {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n loss.records += 1\n continue\n }\n models.push(projectModel(raw as Record<string, unknown>, loss))\n }\n }\n if (models.length === 0) return undefined\n\n const warnings: string[] = []\n if (resolved.via === 'home-fallback') {\n warnings.push(\n 'the host adapter could not be resolved, so the catalog came from a guessed path; '\n + 'these values may not be the ones the running host uses',\n )\n }\n if (loss.records > 0) {\n warnings.push(`${String(loss.records)} catalog entr(ies) were not model objects and were skipped`)\n }\n if (loss.missingApi > 0) {\n warnings.push(\n `${String(loss.missingApi)} model(s) carry no \\`api\\`, so their compat switches cannot be filtered `\n + 'by protocol and are left to the host catalog',\n )\n }\n if (loss.droppedLevels > 0) {\n warnings.push(\n `${String(loss.droppedLevels)} thinking-level value(s) were not \\`string | null\\` and were dropped`,\n )\n }\n // A field that silently stops arriving would otherwise look exactly like a\n // catalog that never had it. These fire only on total absence, which is the\n // signature of a rename rather than of ordinary data churn.\n if (!models.some(model => model.compat !== undefined)) {\n warnings.push('no model carried a `compat` block; the field may have been renamed')\n }\n if (!models.some(model => model.thinkingLevelMap !== undefined)\n && models.some(model => model.reasoning === true)) {\n warnings.push('no model carried a `thinkingLevelMap`; the field may have been renamed')\n }\n\n let generatedAt: string | undefined\n try {\n const timestamp = module.getBuiltinModelDataGeneratedAt?.()\n if (typeof timestamp === 'number') generatedAt = new Date(timestamp).toISOString()\n } catch { /* the timestamp is a nicety, never a gate */ }\n\n const piAiVersion = packageVersion(resolved.dir)\n return {\n models,\n diagnostics: {\n packageDir: resolved.dir,\n resolvedVia: resolved.via,\n copies: collectCopies(),\n warnings,\n },\n ...(generatedAt !== undefined ? { generatedAt } : {}),\n ...(piAiVersion !== undefined ? { piAiVersion } : {}),\n }\n}\n\n/** The in-flight (or settled) load; a promise so concurrent callers share one. */\nlet load: Promise<PiAiCatalog | undefined> | undefined\n\n/** Why the last load produced nothing, for the refusal the route returns. */\nlet failure: string | undefined\n\n/**\n * The installed pi-ai catalog, or undefined when the host offers none.\n *\n * Loaded once and memoized: the catalog is a snapshot built into the installed\n * package, so it cannot change while this process runs. The API module is\n * imported lazily — the host adapter imports the same module, so in a running\n * host this is a module-cache hit rather than a load.\n * @returns the catalog, or undefined for the models.dev-only fallback.\n */\nexport function piAiCatalog(): Promise<PiAiCatalog | undefined> {\n load ??= (async (): Promise<PiAiCatalog | undefined> => {\n const resolved = resolvePiAiPackageDir()\n if (resolved === undefined) {\n failure = 'no @earendil-works/pi-ai package could be located from the host adapter'\n return undefined\n }\n const catalog = await loadCatalog(resolved)\n if (catalog === undefined) {\n failure = `the catalog API (${PI_AI_ALL_SUBPATH}) could not be called at ${resolved.dir}`\n }\n return catalog\n })()\n return load\n}\n\n/**\n * Every pi-ai copy visible on this host.\n *\n * Exported for the failure path: when the catalog is unavailable, \"which\n * copies exist, where, and what version\" is the only fact that turns a refusal\n * into something actionable.\n * @returns the copies, with versions where readable.\n */\nexport function piAiCopies(): readonly PiAiCopy[] {\n return collectCopies()\n}\n\n/**\n * Why the catalog could not be served, once a load has been attempted.\n * @returns the reason, or undefined when nothing has failed.\n */\nexport function piAiUnavailableReason(): string | undefined {\n return failure\n}\n","/**\r\n * dsh-model-extension — host-side entry (v1.0.0).\r\n *\r\n * The host half is the metadata service: same-origin routes backing the\r\n * Models+ page's quick-load. models.dev is fetched ONLY on an explicit user\r\n * click (the title-row button); the raw file caches next to settings.yaml\r\n * under DSH home. Only the flat models.dev shape (top-level keys = full model\r\n * ids) is accepted; anything else is a refusal, per plan. The pi-ai catalog is\r\n * the other, richer source: it is read from the installed package the host\r\n * adapter itself loads, with no network call at all (./pi-ai-catalog.ts).\r\n *\r\n * No version gate lives here. The `dsh.adapter` anchor is gone: the host (as\r\n * of 0.1.7-rc.1) admits or denies this package from its own\r\n * `@deepseek-ai/dsh*` peerDependencies, so a second, softer copy of that\r\n * decision inside the plugin body would only emit a warning nobody can act\r\n * on. The UI is fully plugin-owned, and the host's settings schema validation\r\n * plus revision fencing reject — never corrupt — anything a mismatched wire\r\n * contract might produce.\r\n */\r\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\r\nimport { dirname, join } from 'node:path'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { piAiCatalog, piAiCopies, piAiUnavailableReason } from './pi-ai-catalog'\r\n\r\n/** Cordis service name (distinct from the npm package name). */\r\nexport const name = 'model-extension'\r\n\r\n/** Required services — the route registry lives on the host web server. */\r\nexport const inject = ['webServer']\r\n\r\n/** The one metadata source this plugin accepts (plan: fixed, never configurable). */\r\nconst METADATA_URL = 'https://models.dev/models.json'\r\n\r\n/** Download timeout in milliseconds. */\r\nconst DOWNLOAD_TIMEOUT_MS = 30_000\r\n\r\n/** Structured face of the host web server this plugin needs. */\r\ninterface WebServerLike {\r\n register(route: {\r\n kind: 'exact'\r\n path: string\r\n handler: (req: IncomingMessage, res: ServerResponse) => void\r\n }): (() => void) | Promise<() => void>\r\n}\r\n\r\n/** Minimal cordis context face this plugin uses. */\r\ninterface PluginContext {\r\n logger?: {\r\n warn(msg: string): void\r\n info(msg: string): void\r\n }\r\n effect?: (fn: () => (() => void) | void, name?: string) => void\r\n webServer?: WebServerLike\r\n}\r\n\r\n/** One precise model entry in the served index. */\r\ninterface MetadataEntry {\r\n id: string\r\n context?: number\r\n output?: number\r\n input: string[]\r\n reasoning: boolean\r\n}\r\n\r\n/**\r\n * Resolve the directory the metadata file lives in: next to the first\r\n * settings.yaml found (plan: \"DSH home, same level as settings.yaml\"),\r\n * falling back to DSH home itself and then the process cwd.\r\n * @returns the directory path.\r\n */\r\nfunction metadataDir(): string {\r\n const dshHome = process.env.DSH_HOME\r\n const probes: string[] = []\r\n if (dshHome !== undefined && dshHome.length > 0) {\r\n probes.push(join(dshHome, 'settings.yaml'))\r\n probes.push(join(dshHome, 'profiles', 'web', 'settings.yaml'))\r\n probes.push(dshHome)\r\n }\r\n probes.push(process.cwd())\r\n for (const probe of probes) {\r\n if (probe.endsWith('.yaml')) {\r\n if (existsSync(probe)) return dirname(probe)\r\n continue\r\n }\r\n return probe\r\n }\r\n return process.cwd()\r\n}\r\n\r\n/** The metadata file's absolute path. */\r\nfunction metadataPath(): string {\r\n return join(metadataDir(), 'models.json')\r\n}\r\n\r\n/**\r\n * Validate the raw models.dev payload: ONLY the flat shape (top-level keys\r\n * are full model ids) is accepted, per plan.\r\n * @param data - the parsed payload.\r\n * @returns the precise index.\r\n * @throws when the shape is not the flat models.dev format.\r\n */\r\nfunction toIndex(data: unknown): MetadataEntry[] {\r\n if (typeof data !== 'object' || data === null || Array.isArray(data)) {\r\n throw new Error('unexpected metadata shape (not an object)')\r\n }\r\n const entries = Object.entries(data as Record<string, unknown>)\r\n if (entries.length === 0) throw new Error('metadata file is empty')\r\n const first = entries[0]![1]\r\n if (typeof first !== 'object' || first === null || 'models' in (first as object)) {\r\n throw new Error('unexpected metadata shape (provider-grouped or foreign format)')\r\n }\r\n const index: MetadataEntry[] = []\r\n for (const [key, value] of entries) {\r\n if (typeof value !== 'object' || value === null) continue\r\n const row = value as Record<string, unknown>\r\n const limit = typeof row['limit'] === 'object' && row['limit'] !== null\r\n ? row['limit'] as Record<string, unknown>\r\n : {}\r\n const modalities = typeof row['modalities'] === 'object' && row['modalities'] !== null\r\n ? row['modalities'] as Record<string, unknown>\r\n : {}\r\n const rawInput = Array.isArray(modalities['input'])\r\n ? modalities['input'].filter((m): m is string => typeof m === 'string')\r\n : []\r\n index.push({\r\n id: typeof row['id'] === 'string' ? row['id'] : key,\r\n context: typeof limit['context'] === 'number' ? limit['context'] : undefined,\r\n output: typeof limit['output'] === 'number' ? limit['output'] : undefined,\r\n // Only the modalities this UI offers; anything else is dropped.\r\n input: rawInput.filter(m => m === 'text' || m === 'image'),\r\n reasoning: row['reasoning'] === true,\r\n })\r\n }\r\n return index\r\n}\r\n\r\n/** Read the cached metadata file, or undefined when absent/unreadable. */\r\nfunction readCachedIndex(): MetadataEntry[] | undefined {\r\n try {\r\n const raw = readFileSync(metadataPath(), 'utf8')\r\n return toIndex(JSON.parse(raw) as unknown)\r\n } catch {\r\n return undefined\r\n }\r\n}\r\n\r\n/** Whether the raw payload is accepted for caching. */\r\nfunction isIndexable(data: unknown): boolean {\r\n try {\r\n toIndex(data)\r\n return true\r\n } catch {\r\n return false\r\n }\r\n}\r\n\r\n/** Write one JSON response. */\r\nfunction json(res: ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/**\r\n * Host plugin body: mount the metadata routes (an explicit webServer register,\r\n * mirroring the hmr plugin's usage of the same registry).\r\n * @param ctx - cordis context.\r\n */\r\nexport function apply(ctx: PluginContext): void {\r\n const webServer = ctx.webServer\r\n if (webServer === undefined) {\r\n ctx.logger?.warn('[dsh-model-extension] webServer service absent; metadata routes not mounted')\r\n return\r\n }\r\n\r\n ctx.effect?.(() => {\r\n // GET: serve the cached index; 404 (with guidance) when not ready yet.\r\n const disposeIndex = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/models-index',\r\n handler: (req, res) => {\r\n if (req.method !== 'GET' && req.method !== 'HEAD') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n const index = readCachedIndex()\r\n if (index === undefined) {\r\n json(res, 404, { ok: false, message: '元数据未就绪:请点击「下载/更新元数据」,或手动将 models.json 放入 DSH home。' })\r\n return\r\n }\r\n json(res, 200, index)\r\n },\r\n })\r\n\r\n // POST: the explicit download/update. Fetches models.dev, validates the\r\n // flat shape, caches next to settings.yaml, and answers with the index.\r\n const disposeDownload = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/models-download',\r\n handler: (req, res) => {\r\n if (req.method !== 'POST' && req.method !== 'GET') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n void (async () => {\r\n try {\r\n const response = await fetch(METADATA_URL, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })\r\n if (!response.ok) {\r\n json(res, 502, { ok: false, message: `models.dev 返回 HTTP ${String(response.status)}` })\r\n return\r\n }\r\n const data: unknown = await response.json()\r\n if (!isIndexable(data)) {\r\n json(res, 422, { ok: false, message: 'models.dev 数据格式不被识别(仅支持 models.dev/models.json 的扁平格式)' })\r\n return\r\n }\r\n writeFileSync(metadataPath(), JSON.stringify(data), 'utf8')\r\n ctx.logger?.info(`[dsh-model-extension] metadata cached at ${metadataPath()}`)\r\n json(res, 200, { ok: true, index: toIndex(data) })\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error)\r\n json(res, 502, { ok: false, message })\r\n }\r\n })()\r\n },\r\n })\r\n\r\n // GET: the installed pi-ai catalog. Served straight from the package the\r\n // host adapter loads — no network, no cache file. A refusal (404) is the\r\n // client's signal to fall back to the models.dev index alone.\r\n const disposeCatalog = webServer.register({\r\n kind: 'exact',\r\n path: '/plugins/dsh-model-extension/pi-ai-catalog',\r\n handler: (req, res) => {\r\n if (req.method !== 'GET' && req.method !== 'HEAD') {\r\n res.writeHead(405)\r\n res.end()\r\n return\r\n }\r\n void (async () => {\r\n try {\r\n const catalog = await piAiCatalog()\r\n if (catalog === undefined) {\r\n // The refusal carries the reason AND the copies that are visible:\r\n // \"the host ships no pi-ai\" and \"it ships one this reader cannot\r\n // call\" are otherwise indistinguishable, and only the second is\r\n // worth acting on.\r\n json(res, 404, {\r\n ok: false,\r\n message: `pi-ai 内置目录不可用:${piAiUnavailableReason() ?? '原因未知'}`,\r\n copies: piAiCopies(),\r\n })\r\n return\r\n }\r\n json(res, 200, { ok: true, ...catalog })\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error)\r\n json(res, 500, { ok: false, message })\r\n }\r\n })()\r\n },\r\n })\r\n\r\n return () => {\r\n const a = disposeIndex\r\n if (typeof a === 'function') a()\r\n const b = disposeDownload\r\n if (typeof b === 'function') b()\r\n const c = disposeCatalog\r\n if (typeof c === 'function') c()\r\n }\r\n }, 'dsh-model-extension: metadata routes')\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-extension",
|
|
3
|
-
"version": "1.2.3
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "DeepSeek Harness plugin: the Models+ settings page — a fully plugin-owned model/provider UI with per-model extension fields (reasoning efforts, input modalities, compat flags) and models.dev metadata prefill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"registry": "https://registry.npmjs.org/"
|
|
29
29
|
},
|
|
30
30
|
"dsh": {
|
|
31
|
-
"adapter": "0.1.7-alpha.1",
|
|
32
31
|
"bundle": {
|
|
33
32
|
"patch": "./cordis.patch.yml"
|
|
34
33
|
},
|