@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.
- package/README.en.md +21 -0
- package/README.md +11 -0
- package/lib/client.js +40 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +586 -22
- package/lib/types/index.d.ts +3 -0
- package/lib/types/server/catalog.d.ts +10 -2
- package/lib/types/server/system-proxy.d.ts +112 -0
- package/package.json +16 -15
- package/src/client/api.ts +10 -0
- package/src/client/locales.ts +12 -0
- package/src/client/settings-section.tsx +25 -1
- package/src/index.ts +42 -0
- package/src/server/catalog.ts +34 -18
- package/src/server/rpc.ts +2 -0
- package/src/server/system-proxy.ts +683 -0
|
@@ -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
|
-
|
|
42
|
-
|
|
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.
|
|
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
|
|
89
|
-
"@deepseek-ai/schemastery": "3.18.5
|
|
90
|
-
"@deepseek-ai/dsh-settings": "0.2.1-
|
|
91
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.1-
|
|
92
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.2.1-
|
|
93
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.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 {
|
package/src/client/locales.ts
CHANGED
|
@@ -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)
|
package/src/server/catalog.ts
CHANGED
|
@@ -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
|
|
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.
|
|
127
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
237
|
+
const document = body as Record<string, unknown>
|
|
223
238
|
const fetchedAt = Date.now()
|
|
224
|
-
this.snapshot = this.buildSnapshot(
|
|
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
|
}
|