@fonlan/dsh-model-parameters 0.1.7 → 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.
@@ -1,4 +1,5 @@
1
1
  import { type ModelParametersConfig } from '../shared/config.js';
2
+ import { type CatalogFetch, type CatalogTransport } from './system-proxy.js';
2
3
  /** Structural logger subset satisfied by cordis' LoggerService. */
3
4
  export interface CatalogLogger {
4
5
  info(format: any, ...param: any[]): void;
@@ -30,16 +31,23 @@ export interface CatalogSnapshot {
30
31
  providers: number;
31
32
  /** Epoch ms the cache was last fetched (0 = never fetched). */
32
33
  fetchedAt: number;
34
+ /** Route the last download attempt took; undefined until an attempt runs. */
35
+ transport?: CatalogTransport;
36
+ /** Message of the last failed download; undefined when the last attempt succeeded. */
37
+ error?: string;
33
38
  }
34
39
  export declare class CatalogManager {
35
40
  private readonly logger;
36
41
  private snapshot;
37
42
  private loaded;
38
43
  private inflight;
44
+ private transport;
45
+ private fetchError;
39
46
  private readonly dir;
40
47
  private readonly cachePath;
41
- constructor(logger: CatalogLogger, dataDir?: string);
42
- /** The current snapshot (empty until the first load/fetch completes). */
48
+ private readonly fetchJson;
49
+ constructor(logger: CatalogLogger, dataDir?: string, fetchJson?: CatalogFetch);
50
+ /** The current snapshot, including the route and error of the last download attempt. */
43
51
  snapshotOf(): CatalogSnapshot;
44
52
  /**
45
53
  * Load the disk cache once, then fetch when stale (or `force`). A failed
@@ -0,0 +1,112 @@
1
+ /** Structural logger subset satisfied by cordis' LoggerService. */
2
+ export interface ProxyLogger {
3
+ info(format: any, ...param: any[]): void;
4
+ warn(format: any, ...param: any[]): void;
5
+ }
6
+ /** Where a detected proxy configuration came from. */
7
+ export type ProxySource = 'env' | 'windows' | 'macos' | 'none';
8
+ /** One proxy configuration read from the operating system. */
9
+ export interface SystemProxyConfig {
10
+ /** Proxy URL for `http:` targets. */
11
+ http?: string;
12
+ /** Proxy URL for `https:` targets. */
13
+ https?: string;
14
+ /** The OS bypass/exceptions list, comma separated. */
15
+ noProxy?: string;
16
+ source: 'windows' | 'macos';
17
+ /** Notes about settings this module cannot honor (PAC, SOCKS, malformed entries). */
18
+ diagnostics: string[];
19
+ }
20
+ /** What the catalog download will do for one URL. */
21
+ export interface CatalogRoute {
22
+ /** True when this plugin must tunnel the request itself. */
23
+ tunnel: boolean;
24
+ /** Proxy URL, present only when {@link CatalogRoute.tunnel} is true. */
25
+ proxy?: string;
26
+ source: ProxySource;
27
+ /** Effective bypass list (OS exceptions; loopback is always bypassed). */
28
+ noProxy: string;
29
+ diagnostics: string[];
30
+ }
31
+ /** Structured routing outcome, safe to cross the settings API (credentials stripped). */
32
+ export interface CatalogTransport {
33
+ kind: 'direct' | 'env' | 'macos' | 'windows';
34
+ /** Masked proxy URL (no credentials), when a proxy is actually used. */
35
+ proxy?: string;
36
+ }
37
+ /** A downloaded and parsed catalog document plus the route it took. */
38
+ export interface CatalogFetchResult {
39
+ body: unknown;
40
+ transport: CatalogTransport;
41
+ }
42
+ /** Download one JSON document; {@link CatalogManager} takes one for tests. */
43
+ export type CatalogFetch = (url: string, timeoutMs: number) => Promise<CatalogFetchResult>;
44
+ /** Runs one detection command; injectable so the OS parsers stay testable. */
45
+ export type CommandRunner = (command: string, args: string[], timeoutMs: number) => Promise<string>;
46
+ /** Detection inputs; every field exists so tests can pin a platform and its probes. */
47
+ export interface DetectOptions {
48
+ platform?: NodeJS.Platform;
49
+ env?: NodeJS.ProcessEnv;
50
+ run?: CommandRunner;
51
+ timeoutMs?: number;
52
+ }
53
+ /**
54
+ * Parse the value table `reg query <key>` prints. Value names and the
55
+ * `REG_*` type tokens are not localized, so the same table parses under every
56
+ * Windows display language. Binary values (the `DefaultConnectionSettings`
57
+ * blob) are read as empty — nothing in this module consumes them.
58
+ */
59
+ export declare function parseRegistryQuery(output: string): Map<string, string>;
60
+ /**
61
+ * Turn the Internet Settings values into a proxy configuration.
62
+ * `ProxyServer` is either `host:port` (both schemes) or a
63
+ * `http=host:port;https=host:port` list.
64
+ * @returns the configuration, or undefined when the OS names no proxy at all.
65
+ */
66
+ export declare function windowsProxyFromRegistry(values: Map<string, string>): SystemProxyConfig | undefined;
67
+ /** macOS `scutil --proxy` prints a flat key/value dictionary with nested arrays. */
68
+ export declare function parseScutilProxyOutput(output: string): SystemProxyConfig | undefined;
69
+ /**
70
+ * Read the operating system's proxy configuration (never the environment).
71
+ * Linux has no portable API for this; its users configure the environment,
72
+ * which the harness routes.
73
+ * @param options - platform, probe runner and timeout (tests pin all three).
74
+ * @returns the detected configuration, or undefined when the OS names none.
75
+ */
76
+ export declare function detectSystemProxy(options?: DetectOptions): Promise<SystemProxyConfig | undefined>;
77
+ /** A host this machine owns: a proxy cannot reach it, so it always goes direct. */
78
+ export declare function isLoopbackHost(hostname: string): boolean;
79
+ /**
80
+ * Decide whether a bypass list exempts one URL. Semantics follow the harness's
81
+ * own matcher — an entry covers its subdomains, a leading `.`/`*.` means the
82
+ * same, an entry may carry a `:port`, `*` bypasses everything — plus the two
83
+ * forms OS lists add: `host*` wildcards and `<local>` (a dotless hostname).
84
+ * Loopback is always bypassed. CIDR notation is not matched.
85
+ */
86
+ export declare function shouldBypassProxy(noProxy: string, url: URL): boolean;
87
+ /**
88
+ * Decide how the catalog download for `target` is routed: defer to the harness
89
+ * for environment proxies, tunnel a detected OS proxy, or go direct. The OS is
90
+ * consulted only when the environment names no proxy, so an exported variable
91
+ * keeps winning exactly as it does for the rest of the harness.
92
+ */
93
+ export declare function resolveCatalogRoute(target: URL | string, options?: DetectOptions): Promise<CatalogRoute>;
94
+ /** Strip any proxy credentials before the route crosses the settings API or logs. */
95
+ export declare function maskProxyUrl(proxy: string): string | undefined;
96
+ /** Describe a route for the settings page. */
97
+ export declare function transportOf(route: CatalogRoute): CatalogTransport;
98
+ /**
99
+ * Download and parse one HTTPS JSON document through an `http(s)://` proxy,
100
+ * following redirects. Throws on a non-2xx answer, a malformed body, or a
101
+ * transport timeout so callers keep the previously cached catalog.
102
+ */
103
+ export declare function fetchJsonViaProxy(url: string, proxy: string, timeoutMs: number): Promise<unknown>;
104
+ /**
105
+ * Download the catalog for the settings page: defer to the harness when the
106
+ * environment names a proxy, otherwise tunnel a detected OS proxy, otherwise
107
+ * fetch directly. Never throws for routing reasons — transport failures do
108
+ * throw, and the caller keeps its cached copy.
109
+ * @param options - detection overrides; production passes nothing, tests pin
110
+ * the platform and its OS probes.
111
+ */
112
+ export declare function fetchJsonWithSystemProxy(url: string, timeoutMs: number, logger?: ProxyLogger, options?: DetectOptions): Promise<CatalogFetchResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonlan/dsh-model-parameters",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "DSH plugin: when a provider's model sync returns only ids, automatically fill display name, context window, max output tokens, reasoning efforts, and input modalities from the models.dev catalog (cached, TTL-refreshed), and surface a settings page with toggles, refresh, and a fill report.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,6 +49,7 @@
49
49
  "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
50
50
  "prepare": "npm run build",
51
51
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json",
52
+ "test": "pnpm build && node scripts/verify-proxy.mjs",
52
53
  "watch": "tsdown --watch",
53
54
  "release:patch": "npm version patch && git push && git push --tags",
54
55
  "release:minor": "npm version minor && git push && git push --tags",
@@ -70,12 +71,12 @@
70
71
  "peerDependencies": {
71
72
  "@deepseek-ai/cordis": "^4.0.1",
72
73
  "@deepseek-ai/schemastery": "^3.18.4",
73
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
74
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
75
- "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
76
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
77
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
78
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
74
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
75
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
76
+ "@deepseek-ai/dsh-llm-pi-ai": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
77
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
78
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
79
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-rc.2 || ^0.2.1-rc.3",
79
80
  "react": "^18.2.0",
80
81
  "react-dom": "^18.2.0"
81
82
  },
@@ -85,14 +86,14 @@
85
86
  "@types/react-dom": "~18.3.0",
86
87
  "tsdown": "^0.22.14",
87
88
  "typescript": "^5.9.0",
88
- "@deepseek-ai/cordis": "4.0.5-alpha.1",
89
- "@deepseek-ai/schemastery": "3.18.5-alpha.1",
90
- "@deepseek-ai/dsh-settings": "0.2.1-alpha.1",
91
- "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.1",
92
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-alpha.1",
93
- "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.1",
94
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.1",
95
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.1"
89
+ "@deepseek-ai/cordis": "4.0.5",
90
+ "@deepseek-ai/schemastery": "3.18.5",
91
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.3",
92
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.3",
93
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.3",
94
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
95
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
96
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3"
96
97
  },
97
98
  "packageManager": "pnpm@11.21.0"
98
99
  }
package/src/client/api.ts CHANGED
@@ -1,12 +1,22 @@
1
1
  /** Client transport for the model-parameters settings API. */
2
2
  import type { ModelParametersConfig } from '../shared/config.js'
3
3
 
4
+ export interface CatalogTransportView {
5
+ kind: 'direct' | 'env' | 'macos' | 'windows'
6
+ /** Masked proxy URL (no credentials), present when a proxy is actually used. */
7
+ proxy?: string
8
+ }
9
+
4
10
  export interface CatalogView {
5
11
  entries: number
6
12
  providers: number
7
13
  fetchedAt: number
8
14
  fresh: boolean
9
15
  ttlDays: number
16
+ /** Route the last download attempt took (null until one runs). */
17
+ transport: CatalogTransportView | null
18
+ /** Message of the last failed download (cleared on success). */
19
+ error: string | null
10
20
  }
11
21
 
12
22
  export interface FillReportView {
@@ -33,6 +33,12 @@ export const zh = {
33
33
  catalogFresh: '新鲜',
34
34
  catalogStale: '已过期',
35
35
  catalogNever: '尚未获取',
36
+ catalogRoute: '下载经由',
37
+ routeDirect: '直连',
38
+ routeEnv: '环境变量代理',
39
+ routeMacos: 'macOS 系统代理',
40
+ routeWindows: 'Windows 系统代理',
41
+ catalogDownloadFailed: '上次下载失败',
36
42
  refresh: '立即更新目录并补全',
37
43
  refreshBusy: '更新中…',
38
44
  reconcileNow: '立即补全一次',
@@ -78,6 +84,12 @@ export const en: Record<keyof typeof zh, string> = {
78
84
  catalogFresh: 'fresh',
79
85
  catalogStale: 'stale',
80
86
  catalogNever: 'not fetched yet',
87
+ catalogRoute: 'Download route',
88
+ routeDirect: 'direct',
89
+ routeEnv: 'environment proxy',
90
+ routeMacos: 'macOS system proxy',
91
+ routeWindows: 'Windows system proxy',
92
+ catalogDownloadFailed: 'Last download failed',
81
93
  refresh: 'Update catalog & fill now',
82
94
  refreshBusy: 'Updating…',
83
95
  reconcileNow: 'Fill now',
@@ -16,7 +16,7 @@ import type { Context } from '@deepseek-ai/cordis'
16
16
  type ClientContext = Context
17
17
  import type { Translate } from '@deepseek-ai/dsh-client-ui-slots'
18
18
  import { LOCALE_NS } from './locales'
19
- import { api, type PluginState } from './api'
19
+ import { api, type CatalogTransportView, type PluginState } from './api'
20
20
  import type { ModelParametersConfig } from '../shared/config.js'
21
21
  import './settings-section.css'
22
22
 
@@ -49,6 +49,20 @@ function formatTime(epochMs: number): string {
49
49
  }
50
50
  }
51
51
 
52
+ /** Localized label for the route the last catalog download took. */
53
+ function routeLabel(t: Translate, transport: CatalogTransportView): string {
54
+ switch (transport.kind) {
55
+ case 'env':
56
+ return t('routeEnv')
57
+ case 'macos':
58
+ return t('routeMacos')
59
+ case 'windows':
60
+ return t('routeWindows')
61
+ default:
62
+ return t('routeDirect')
63
+ }
64
+ }
65
+
52
66
  export function makeSettingsSection(ctx: ClientContext): () => JSX.Element {
53
67
  const t: Translate = (() => {
54
68
  try {
@@ -267,7 +281,17 @@ function ModelParametersBody(props: {
267
281
  }}
268
282
  />
269
283
  </label>
284
+ {catalog.transport != null && (
285
+ <span>
286
+ {t('catalogRoute')}:{' '}
287
+ <strong>{routeLabel(t, catalog.transport)}</strong>
288
+ {catalog.transport.proxy !== undefined && <> <code className="mp-code">{catalog.transport.proxy}</code></>}
289
+ </span>
290
+ )}
270
291
  </div>
292
+ {catalog.error != null && (
293
+ <p className="mp-error">{t('catalogDownloadFailed')}: {catalog.error}</p>
294
+ )}
271
295
  <div className="mp-actions">
272
296
  <button className="mp-button" disabled={busy} onClick={() => void onRefresh()}>
273
297
  {busy ? t('refreshBusy') : t('refresh')}
package/src/index.ts CHANGED
@@ -20,6 +20,26 @@ import {
20
20
  type CatalogEntry,
21
21
  type CatalogSnapshot,
22
22
  } from './server/catalog.js'
23
+ import {
24
+ detectSystemProxy,
25
+ fetchJsonViaProxy,
26
+ fetchJsonWithSystemProxy,
27
+ isLoopbackHost,
28
+ maskProxyUrl,
29
+ parseRegistryQuery,
30
+ parseScutilProxyOutput,
31
+ resolveCatalogRoute,
32
+ shouldBypassProxy,
33
+ transportOf,
34
+ windowsProxyFromRegistry,
35
+ type CatalogFetch,
36
+ type CatalogFetchResult,
37
+ type CatalogRoute,
38
+ type CatalogTransport,
39
+ type CommandRunner,
40
+ type DetectOptions,
41
+ type SystemProxyConfig,
42
+ } from './server/system-proxy.js'
23
43
 
24
44
  export const name = '@fonlan/dsh-model-parameters'
25
45
 
@@ -31,6 +51,28 @@ export const inject: string[] = []
31
51
  export { Config }
32
52
  export { CatalogManager, acceptedInputModalities, identityReasoningEfforts }
33
53
  export type { CatalogEntry, CatalogSnapshot }
54
+ export {
55
+ detectSystemProxy,
56
+ fetchJsonViaProxy,
57
+ fetchJsonWithSystemProxy,
58
+ isLoopbackHost,
59
+ maskProxyUrl,
60
+ parseRegistryQuery,
61
+ parseScutilProxyOutput,
62
+ resolveCatalogRoute,
63
+ shouldBypassProxy,
64
+ transportOf,
65
+ windowsProxyFromRegistry,
66
+ }
67
+ export type {
68
+ CatalogFetch,
69
+ CatalogFetchResult,
70
+ CatalogRoute,
71
+ CatalogTransport,
72
+ CommandRunner,
73
+ DetectOptions,
74
+ SystemProxyConfig,
75
+ }
34
76
 
35
77
  export function apply(ctx: Context, config: ModelParametersLiveConfig = {}): void {
36
78
  const service = new ModelParametersService(ctx, config)
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * models.dev catalog: fetch, disk cache, lazy TTL refresh, and id matching.
3
3
  *
4
+ * The download goes through {@link fetchJsonWithSystemProxy}, so it follows the
5
+ * proxy environment the harness already routes (unchanged) and, when no proxy
6
+ * variable is set, the operating system's proxy — see system-proxy.ts.
7
+ *
4
8
  * The catalog is flattened into a list of entries keyed by provider, then
5
9
  * indexed by the last path segment of the model id (case-insensitive) so a
6
10
  * synced bare id like `qwen3.7-max` can resolve `Qwen/Qwen3.7-Max` wherever
@@ -24,6 +28,11 @@ import {
24
28
  REASONING_LEVELS,
25
29
  type ModelParametersConfig,
26
30
  } from '../shared/config.js'
31
+ import {
32
+ fetchJsonWithSystemProxy,
33
+ type CatalogFetch,
34
+ type CatalogTransport,
35
+ } from './system-proxy.js'
27
36
 
28
37
  /** Structural logger subset satisfied by cordis' LoggerService. */
29
38
  export interface CatalogLogger {
@@ -58,6 +67,10 @@ export interface CatalogSnapshot {
58
67
  providers: number
59
68
  /** Epoch ms the cache was last fetched (0 = never fetched). */
60
69
  fetchedAt: number
70
+ /** Route the last download attempt took; undefined until an attempt runs. */
71
+ transport?: CatalogTransport
72
+ /** Message of the last failed download; undefined when the last attempt succeeded. */
73
+ error?: string
61
74
  }
62
75
 
63
76
  const DAY_MS = 24 * 60 * 60 * 1000
@@ -87,20 +100,25 @@ export class CatalogManager {
87
100
  private snapshot: CatalogSnapshot = { entries: [], providers: 0, fetchedAt: 0 }
88
101
  private loaded = false
89
102
  private inflight: Promise<void> | null = null
103
+ private transport: CatalogTransport | undefined
104
+ private fetchError: string | undefined
90
105
  private readonly dir: string
91
106
  private readonly cachePath: string
107
+ private readonly fetchJson: CatalogFetch
92
108
 
93
109
  constructor(
94
110
  private readonly logger: CatalogLogger,
95
111
  dataDir?: string,
112
+ fetchJson?: CatalogFetch,
96
113
  ) {
97
114
  this.dir = dataDir ?? join(homedir(), '.dsh', 'model-parameters')
98
115
  this.cachePath = join(this.dir, 'catalog.json')
116
+ this.fetchJson = fetchJson ?? ((url, timeoutMs) => fetchJsonWithSystemProxy(url, timeoutMs, logger))
99
117
  }
100
118
 
101
- /** The current snapshot (empty until the first load/fetch completes). */
119
+ /** The current snapshot, including the route and error of the last download attempt. */
102
120
  snapshotOf(): CatalogSnapshot {
103
- return this.snapshot
121
+ return { ...this.snapshot, transport: this.transport, error: this.fetchError }
104
122
  }
105
123
 
106
124
  /**
@@ -122,10 +140,14 @@ export class CatalogManager {
122
140
  return true
123
141
  }
124
142
  this.inflight = this.fetchAndPersist()
143
+ .then(() => {
144
+ this.fetchError = undefined
145
+ })
125
146
  .catch((error) => {
126
- this.logger.warn(
127
- `model-parameters: catalog fetch failed (${error instanceof Error ? error.message : String(error)}); using cached copy`,
128
- )
147
+ this.fetchError = error instanceof Error ? error.message : String(error)
148
+ const attempted = (error as { transport?: CatalogTransport }).transport
149
+ if (attempted !== undefined) this.transport = attempted
150
+ this.logger.warn(`model-parameters: catalog fetch failed (${this.fetchError}); using cached copy`)
129
151
  })
130
152
  .finally(() => {
131
153
  this.inflight = null
@@ -208,23 +230,17 @@ export class CatalogManager {
208
230
  }
209
231
 
210
232
  private async fetchAndPersist(): Promise<void> {
211
- const controller = new AbortController()
212
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
213
- let response: Response
214
- try {
215
- response = await fetch(CATALOG_URL, { signal: controller.signal, headers: { accept: 'application/json' } })
216
- } finally {
217
- clearTimeout(timer)
218
- }
219
- if (!response.ok) {
220
- throw new Error(`HTTP ${response.status} ${response.statusText}`)
233
+ const { body, transport } = await this.fetchJson(CATALOG_URL, FETCH_TIMEOUT_MS)
234
+ if (typeof body !== 'object' || body === null) {
235
+ throw new Error('catalog response is not a JSON object')
221
236
  }
222
- const body = (await response.json()) as Record<string, unknown>
237
+ const document = body as Record<string, unknown>
223
238
  const fetchedAt = Date.now()
224
- this.snapshot = this.buildSnapshot(body, fetchedAt)
239
+ this.snapshot = this.buildSnapshot(document, fetchedAt)
225
240
  await mkdir(this.dir, { recursive: true })
226
- const doc: CacheDocument = { fetchedAt, body }
241
+ const doc: CacheDocument = { fetchedAt, body: document }
227
242
  await writeFile(this.cachePath, JSON.stringify(doc), 'utf8')
243
+ this.transport = transport
228
244
  this.logger.info(
229
245
  `model-parameters: catalog refreshed from models.dev (${this.snapshot.providers} providers, ${this.snapshot.entries.length} models)`,
230
246
  )
package/src/server/rpc.ts CHANGED
@@ -156,6 +156,8 @@ function stateOf(service: ModelParametersService): unknown {
156
156
  fresh: snapshot.fetchedAt > 0
157
157
  && Date.now() - snapshot.fetchedAt < config.ttlDays * 24 * 60 * 60 * 1000,
158
158
  ttlDays: config.ttlDays,
159
+ transport: snapshot.transport ?? null,
160
+ error: snapshot.error ?? null,
159
161
  },
160
162
  report: service.report(),
161
163
  }