dsh-plugin-model-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/cordis.patch.yml +15 -0
  4. package/lib/client/ModelProxyCard.d.ts +14 -0
  5. package/lib/client/ModelProxyCard.d.ts.map +1 -0
  6. package/lib/client/catalog.d.ts +140 -0
  7. package/lib/client/catalog.d.ts.map +1 -0
  8. package/lib/client/controller.d.ts +107 -0
  9. package/lib/client/controller.d.ts.map +1 -0
  10. package/lib/client/index.d.ts +8 -0
  11. package/lib/client/index.d.ts.map +1 -0
  12. package/lib/client/locales.d.ts +106 -0
  13. package/lib/client/locales.d.ts.map +1 -0
  14. package/lib/client.js +1129 -0
  15. package/lib/client.js.map +7 -0
  16. package/lib/host/config.d.ts +47 -0
  17. package/lib/host/config.d.ts.map +1 -0
  18. package/lib/host/config.js +153 -0
  19. package/lib/host/config.js.map +1 -0
  20. package/lib/host/credentials.d.ts +24 -0
  21. package/lib/host/credentials.d.ts.map +1 -0
  22. package/lib/host/credentials.js +45 -0
  23. package/lib/host/credentials.js.map +1 -0
  24. package/lib/host/dispatcher.d.ts +30 -0
  25. package/lib/host/dispatcher.d.ts.map +1 -0
  26. package/lib/host/dispatcher.js +167 -0
  27. package/lib/host/dispatcher.js.map +1 -0
  28. package/lib/host/fetch-wrap.d.ts +45 -0
  29. package/lib/host/fetch-wrap.d.ts.map +1 -0
  30. package/lib/host/fetch-wrap.js +100 -0
  31. package/lib/host/fetch-wrap.js.map +1 -0
  32. package/lib/host/index.d.ts +18 -0
  33. package/lib/host/index.d.ts.map +1 -0
  34. package/lib/host/index.js +262 -0
  35. package/lib/host/index.js.map +1 -0
  36. package/lib/host/probe.d.ts +30 -0
  37. package/lib/host/probe.d.ts.map +1 -0
  38. package/lib/host/probe.js +43 -0
  39. package/lib/host/probe.js.map +1 -0
  40. package/lib/invariant.d.ts +5 -0
  41. package/lib/invariant.d.ts.map +1 -0
  42. package/lib/invariant.js +7 -0
  43. package/lib/invariant.js.map +1 -0
  44. package/package.json +95 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 dsh-plugin-model-proxy contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # dsh-plugin-model-proxy
2
+
3
+ Community DSH plugin: route **specific provider/model pairs** through specific proxies (`http://`, `https://`, `socks5://`, `socks5h://`) — with a **Settings UI**.
4
+
5
+ ## Why
6
+
7
+ Some model endpoints are only reachable through a proxy while their siblings
8
+ connect fine — for instance `opencode/muse-spark-1.2-contributor` may answer
9
+ `403 RegionError` to direct connections that other models from the same
10
+ provider never see. The coarse fixes don't help: process-wide proxy
11
+ environment variables drag every request through the tunnel, and swapping a
12
+ whole provider's `baseURL` hides the real upstream. This plugin routes
13
+ **specific provider/model pairs** at the transport layer instead — everything
14
+ else stays direct, and the configured endpoint is never rewritten.
15
+
16
+ ## Features
17
+
18
+ - **Per-rule routing**: `{provider, model, proxyUrl, enabled}` — specificity: exact model > prefix `muse-*` > `*`.
19
+ - **Purpose filter**: optional per-rule `purpose` (e.g. `compaction`) so chat goes through the proxy while background calls stay direct.
20
+ - **credentialRef**: keep proxy passwords in the DSH credentials service instead of settings.yaml; rules reference them by name (`user:password` entries). Soft dependency — installs without it keep working.
21
+ - **Auto probe**: newly configured proxies are connectivity-tested once (CONNECT/socks handshake, no model quota); results go to the host log.
22
+ - **Protocols**: `http://`, `https://` (CONNECT via `undici.ProxyAgent`), `socks5://` / `socks5h://` (via optional `socks`, tunnelled by undici `Agent` + custom connect).
23
+ - **Zero baseURL mutation** — keep the real upstream.
24
+ - **Live**: change rules → next `llm/stream` uses them; in-flight streams unaffected.
25
+ - **UI**: `Settings → Plugins → Model Proxy` card (also works via `~/.dsh/settings.yaml`).
26
+ - **Provider picker**: dropdown groups user-configured providers first (derived from `llm.providers` × settings mirror, same semantics as the built-in Models page); bare directory routes follow, and "Custom…" accepts anything — hand-written yaml rules, wildcards, gateways the catalog doesn't know. Provider and model fields are dropdowns fed by the live host catalog (`llm.providers` / `llm.models`, refreshed on `llm/adapters-updated`); a "Custom…" entry keeps free text for wildcards (`muse-*`, `*`) or not-yet-installed providers.
27
+ - **Batch & grouped management**: adding rules checks multiple models for one provider at once — one rule each, sharing proxy/purpose/credential; the list groups cards by provider with group-level apply-proxy, enable/disable-all, and delete (cross-provider grouping never affects match order).
28
+
29
+ ## Install
30
+
31
+ ### Recommended: profile-managed (bundle)
32
+
33
+ The package declares a `dsh.bundle` layer, so the DSH CLI links the dependency
34
+ **and** appends the activation row in one step:
35
+
36
+ ```bash
37
+ dsh plugin --profile <name> add dsh-plugin-model-proxy
38
+ # optional SOCKS support:
39
+ dsh plugin --profile <name> add socks
40
+ ```
41
+
42
+ Verify the composed layer without booting, then run:
43
+
44
+ ```bash
45
+ dsh --profile <name> --dump-config # shows a "# == dsh-plugin-model-proxy" layer
46
+ dsh --profile <name>
47
+ ```
48
+
49
+ Uninstall removes both the dependency and the layer:
50
+
51
+ ```bash
52
+ dsh plugin --profile <name> remove dsh-plugin-model-proxy
53
+ ```
54
+
55
+ Installing from GitHub (`dsh plugin --profile <name> add github:<user>/dsh-plugin-model-proxy#<sha>`)
56
+ fetches sources; pnpm ≥10 asks you to allowlist the `prepare` build — copy the
57
+ package key it prints into the profile's `pnpm-workspace.yaml` under
58
+ `allowBuilds:`, then re-run the `add`. Pin a commit so later pushes cannot
59
+ change what runs on your machine.
60
+
61
+ ### Manual: hand-written patch
62
+
63
+ If you manage compositions by hand, install the dependency into the **profile
64
+ directory** first (`dsh plugin --profile <name> add ./` from a checkout works;
65
+ module resolution anchors at the profile). Then insert exactly ONE loader row:
66
+
67
+ ```yaml
68
+ # ~/.dsh/cordis.patch.yml (or a --patch overlay)
69
+ - insert:
70
+ - id: model-proxy/host
71
+ name: dsh-plugin-model-proxy
72
+ config: {}
73
+ ```
74
+
75
+ Patch-file rules that trip people up:
76
+
77
+ - The file must parse as a **top-level YAML array** of patch entries. Comments
78
+ are fine; a file of only comments (no `- …` rows and no `[]`) fails to parse
79
+ and breaks every boot/dump that reads it.
80
+ - The loader-entry specifier field is **`name:`** (the package name), not `module:`.
81
+ - New rows must be nested under **`insert:`** — a bare `- id:` row means "patch an existing entry".
82
+ - Only the **host** half is a loader entry. The browser half is discovered automatically
83
+ from the package's `dsh.client` declaration and served at
84
+ `/plugins/dsh-plugin-model-proxy/client.js`. Do NOT add a `model-proxy/client`
85
+ row — it would run browser code inside the Node process.
86
+ - **Never mix this path with the profile-managed install above.** Once the
87
+ dependency declares `dsh.bundle.patch`, `dsh plugin add` injects the host row
88
+ automatically; keeping a hand-written copy in `~/.dsh/cordis.patch.yml`
89
+ inserts the same loader id twice and the composition collides at boot.
90
+
91
+ Uninstall = remove the inserted rows + `pnpm remove dsh-plugin-model-proxy`
92
+ from the profile.
93
+
94
+ ## Configure
95
+
96
+ ### Via UI
97
+
98
+ `Settings → Plugins → Model Proxy`
99
+
100
+ ### Via file
101
+
102
+ `~/.dsh/settings.yaml`:
103
+
104
+ ```yaml
105
+ model-proxy:
106
+ enabled: true
107
+ debug: false
108
+ defaultProxy: "" # fallback when no rule matches
109
+ rules:
110
+ - provider: opencode
111
+ model: muse-spark-1.2-contributor
112
+ proxyUrl: socks5://127.0.0.1:1080
113
+ enabled: true
114
+ - provider: opencode
115
+ model: "*"
116
+ proxyUrl: "" # direct for the rest of this provider
117
+ ```
118
+
119
+ `proxyUrl: ""` means **direct (exempt)**. `socks5h://` resolves DNS at the proxy.
120
+
121
+ ## How it works (non-invasive)
122
+
123
+ 1. Host registers `model-proxy` settings namespace (`installSettingsSection`, live).
124
+ 2. Wraps `globalThis.fetch` reversibly.
125
+ 3. Listens on `llm/stream` waterfall, resolves `proxyUrl` for `(provider, model)` via `AsyncLocalStorage`, then injects a `dispatcher` into the adapter's `fetch` — for proxied requests it routes through `undici.fetch` (Node's native global fetch ignores custom dispatchers), with `undici.ProxyAgent` for `http(s)` and an undici `Agent` + socks `connect` (with TLS) for `socks5/h`.
126
+ 4. Browser registers `settings.plugin.item` with key `model-proxy` — automatically paired by the Plugins tab (`served ∩ registered`).
127
+
128
+ No Adapter fork, no `baseURL` rewrite. See `DESIGN.md` for full design and invasiveness analysis.
129
+
130
+ ## Invasiveness
131
+
132
+ | Check | Result |
133
+ |---|---|
134
+ | Modifies `packages/*` | No |
135
+ | Requires fork of `dsh-llm` | No |
136
+ | Global side effect reversible | Yes (`ctx.effect` dispose restores `fetch`) |
137
+ | Client bundle purity gate | Passes (only type-only slot import) |
138
+
139
+ ## Building
140
+
141
+ ```bash
142
+ pnpm install
143
+ pnpm run build
144
+ ```
145
+
146
+ ## Testing
147
+
148
+ Fully offline (local origin + CONNECT-capable toy proxy; no real network or proxy needed):
149
+
150
+ ```bash
151
+ pnpm test # builds host + client, then runs node --test tests/
152
+ pnpm run typecheck
153
+ ```
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,15 @@
1
+ # dsh-plugin-model-proxy — bundle layer
2
+ #
3
+ # Declared by package.json#dsh.bundle.patch and applied automatically by
4
+ # `dsh plugin --profile <name> add dsh-plugin-model-proxy`. Manual installs can
5
+ # copy these rows into ~/.dsh/cordis.patch.yml or a --patch overlay.
6
+ #
7
+ # Only the HOST row is a loader entry (the specifier field is `name:`). The
8
+ # browser half is discovered automatically from package.json#dsh.client and
9
+ # served at /plugins/dsh-plugin-model-proxy/client.js — never add a second
10
+ # `model-proxy/client` loader row, it would run browser code in the host.
11
+
12
+ - insert:
13
+ - id: model-proxy/host
14
+ name: dsh-plugin-model-proxy
15
+ config: {}
@@ -0,0 +1,14 @@
1
+ import { ProviderCatalogStore } from './catalog.js';
2
+ import type { ModelProxyController } from './controller.js';
3
+ import { en } from './locales.js';
4
+ type Props = {
5
+ controller: ModelProxyController;
6
+ /** Live provider/model catalog; absent or empty falls back to free text. */
7
+ catalog?: ProviderCatalogStore;
8
+ t?: (k: keyof typeof en) => string;
9
+ };
10
+ /** Mirrors host assertServiceable for defaultProxy so bad values never reach the wire. */
11
+ export declare function validateDefaultProxy(defaultProxy: string): string | undefined;
12
+ export declare function ModelProxyCard({ controller, catalog, t: tProp }: Props): import("react").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=ModelProxyCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelProxyCard.d.ts","sourceRoot":"","sources":["../../src/client/ModelProxyCard.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAA+B,MAAM,iBAAiB,CAAA;AAExF,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAEjC,KAAK,KAAK,GAAG;IACX,UAAU,EAAE,oBAAoB,CAAA;IAChC,4EAA4E;IAC5E,OAAO,CAAC,EAAE,oBAAoB,CAAA;IAC9B,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,OAAO,EAAE,KAAK,MAAM,CAAA;CACnC,CAAA;AAoKD,0FAA0F;AAC1F,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG7E;AAqFD,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,+BAmetE"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Provider/model catalog feeding the settings card dropdowns.
3
+ *
4
+ * Data sources mirror the built-in Models page and the model-selection
5
+ * surface:
6
+ * - `llm.providers` — configurable provider directory merged with live routes
7
+ * (`{ providers: [{ provider, displayName, settingsNs, settingsPath,
8
+ * active, declared? }] }`)
9
+ * - `llm.models` — advisory per-provider model catalog
10
+ * (`{ groups: [{ id, name, models: [{ id, name?, … }] }], failures }`)
11
+ * - `settings.describe` — optional enrichment: cross-references each row's
12
+ * settings address to derive `configured`, so the card can group
13
+ * user-configured providers ahead of the bare directory (same semantics as
14
+ * the built-in Models page).
15
+ *
16
+ * Refresh triggers mirror ui-model-selection's convergence sources:
17
+ * `llm/adapters-updated` (registry topology commits) and
18
+ * `settings/document-updated` (configuration-owned catalogs).
19
+ *
20
+ * Everything here is advisory. The card always keeps a free-text escape
21
+ * hatch, because proxy rules legitimately target things no catalog lists:
22
+ * wildcard patterns (`vendor-*`, `*`) and providers whose adapter is not
23
+ * installed (yet).
24
+ */
25
+ /** One selectable provider row (shape of `llm.providers.providers`). */
26
+ export type CatalogProvider = {
27
+ provider: string;
28
+ displayName: string;
29
+ active: boolean;
30
+ /** Settings namespace whose section configures this provider (absent = no settings address). */
31
+ settingsNs?: string;
32
+ /** Path from that section's root to this provider's profile object. */
33
+ settingsPath?: string[];
34
+ /** Adapter knows this route only because configuration declared it. */
35
+ declared?: boolean;
36
+ /**
37
+ * Mirrors ui-settings-models' derivation: the provider's settings namespace
38
+ * exists AND its profile object is present in the merged value. Undefined
39
+ * when unknown (no settings address, or settings.describe unavailable).
40
+ */
41
+ configured?: boolean;
42
+ };
43
+ /** One selectable model row (subset of an `llm.models` group entry). */
44
+ export type CatalogModel = {
45
+ id: string;
46
+ name?: string;
47
+ };
48
+ export type CatalogSnapshot = {
49
+ status: 'idle' | 'loading' | 'ready' | 'unavailable';
50
+ providers: CatalogProvider[];
51
+ /** Provider id → catalog models; absent/empty means no known models. */
52
+ modelsByProvider: Record<string, CatalogModel[]>;
53
+ };
54
+ /** Structural wire face so tests can pass fakes without a transport. */
55
+ export type CatalogApiFace = {
56
+ llm: {
57
+ providers(payload: object): Promise<unknown>;
58
+ models(payload: object): Promise<unknown>;
59
+ };
60
+ /**
61
+ * Optional settings mirror used to derive each provider's `configured`
62
+ * flag. Absent or failing keeps configured undefined — grouping degrades
63
+ * to the flat directory list.
64
+ */
65
+ settings?: {
66
+ describe(payload: object): Promise<unknown>;
67
+ };
68
+ };
69
+ /** Structural face of `ctx.remote` — `$on` returns its own disposer. */
70
+ export type CatalogRemoteFace = {
71
+ $on(event: string, listener: (...args: never[]) => void): () => void;
72
+ };
73
+ type Listener = () => void;
74
+ /**
75
+ * Unwrap one RPC envelope (`{ result: { ok, value | error } }`), throwing on
76
+ * a business rejection with its wire message.
77
+ */
78
+ export declare function unwrapRpc(response: unknown): unknown;
79
+ /**
80
+ * Extract selectable provider rows from an unwrapped `llm.providers` value.
81
+ * Malformed rows are skipped, never fatal — the dropdown degrades to whatever
82
+ * is parseable.
83
+ */
84
+ export declare function pickProviders(value: unknown): CatalogProvider[];
85
+ /** Deep-get along a path; undefined as soon as any hop is missing. */
86
+ export declare function getPath(value: unknown, path: readonly string[]): unknown;
87
+ /**
88
+ * Attach `configured` to provider rows, mirroring ui-settings-models'
89
+ * derivation: the settings namespace exists in the mirror AND either the
90
+ * provider configures via the whole section (`settingsPath` empty) or its
91
+ * profile object is present in the merged value. Rows without a settings
92
+ * address keep `configured` undefined (unknown, not false).
93
+ */
94
+ export declare function attachConfigured(providers: CatalogProvider[], namespaces: ReadonlyArray<{
95
+ ns?: unknown;
96
+ value?: unknown;
97
+ }>): CatalogProvider[];
98
+ /**
99
+ * Group an unwrapped `llm.models` value into provider id → model rows.
100
+ * Failures entries are ignored (the dropdown simply offers nothing for a
101
+ * provider whose catalog failed host-side).
102
+ */
103
+ export declare function pickModelsByProvider(value: unknown): Record<string, CatalogModel[]>;
104
+ /**
105
+ * uSES-compatible store over the two catalog RPCs. Loads once on bind(),
106
+ * reloads on forwarded topology/settings events, and keeps the last good
107
+ * rows across a failed background refresh so an outage never blanks open
108
+ * dropdowns.
109
+ */
110
+ export declare class ProviderCatalogStore {
111
+ private readonly api;
112
+ private readonly remote?;
113
+ private readonly listeners;
114
+ private snapshot;
115
+ private generation;
116
+ private unsubs;
117
+ private bound;
118
+ constructor(api: CatalogApiFace, remote?: CatalogRemoteFace | undefined);
119
+ /** Idempotent: start loading and subscribe to refresh events. */
120
+ bind(): void;
121
+ /** Stop listening; in-flight loads become no-ops via the generation guard. */
122
+ dispose(): void;
123
+ subscribe(listener: Listener): () => void;
124
+ getSnapshot(): CatalogSnapshot;
125
+ /**
126
+ * Fetch both catalogs. Each call settles independently (`allSettled`
127
+ * semantics via mapped thunks): a provider-catalog failure fails the whole
128
+ * load, while a model-catalog failure degrades to an empty model map —
129
+ * models are an enrichment, providers drive the primary dropdown.
130
+ */
131
+ load(): Promise<void>;
132
+ /**
133
+ * Keep last good rows across a failed refresh; the card falls back to
134
+ * free-text inputs only when the snapshot holds no providers at all.
135
+ */
136
+ private markUnavailable;
137
+ private set;
138
+ }
139
+ export {};
140
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/client/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Client controller for model-proxy settings card.
3
+ * Binds to `model-proxy` namespace via ctx.settingsScope and exposes
4
+ * a small observable store for the card.
5
+ */
6
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client';
7
+ export type ProxyRule = {
8
+ /** Card key; may be absent on hand-written yaml rules until edited here. */
9
+ id?: string;
10
+ provider: string;
11
+ model: string;
12
+ proxyUrl: string;
13
+ enabled: boolean;
14
+ /** Only apply to calls with this GenerateOptions.purpose; empty = all. */
15
+ purpose?: string;
16
+ /** Credentials-service entry ("user:password") overriding proxyUrl userinfo. */
17
+ credentialRef?: string;
18
+ };
19
+ export type ModelProxyConfig = {
20
+ enabled: boolean;
21
+ rules: ProxyRule[];
22
+ defaultProxy: string;
23
+ debug: boolean;
24
+ };
25
+ export type ModelProxySnapshot = {
26
+ status: 'loading' | 'ready' | 'unavailable';
27
+ value?: ModelProxyConfig;
28
+ revision?: number;
29
+ writable: boolean;
30
+ error?: string;
31
+ };
32
+ type Listener = () => void;
33
+ /**
34
+ * Strip unknown keys from drafted rules before persisting. The wire layer
35
+ * tolerates extra keys and passes them through materialization, so without
36
+ * this a legacy field (e.g. the removed `note`) read from the stored document
37
+ * would ride every save forever. Writing only known fields converges stored
38
+ * documents to the schema shape.
39
+ *
40
+ * Dropping is loud, not silent: every stripped key is named on the console,
41
+ * so a hand-written key vanishing on save is never a mystery.
42
+ */
43
+ export declare function sanitizeRules(rules: ProxyRule[]): ProxyRule[];
44
+ /** Stable-enough client-side id for rule cards (React keys / list diffs). */
45
+ export declare function makeRuleId(): string;
46
+ /** One "batch add" spec: shared fields + the model patterns to fan out over. */
47
+ export type CreatorSpec = {
48
+ provider: string;
49
+ /** Model ids / wildcard patterns; one rule each. Duplicates collapse. */
50
+ models: string[];
51
+ proxyUrl: string;
52
+ purpose?: string;
53
+ credentialRef?: string;
54
+ enabled?: boolean;
55
+ };
56
+ /**
57
+ * Fan one creator spec out into N prefilled rules sharing provider and
58
+ * connection fields. Returns [] when provider or models are effectively
59
+ * empty — callers gate the button on the same predicate for inline counts.
60
+ */
61
+ export declare function buildCreatorRules(spec: CreatorSpec): ProxyRule[];
62
+ /**
63
+ * Group rules by provider for the card's grouped view: group order is first
64
+ * appearance, intra-group order preserved. Matching semantics only care about
65
+ * order WITHIN a provider (same-pass first-wins), so regrouping across
66
+ * providers is display-safe by construction.
67
+ */
68
+ export declare function groupByProvider(rules: ProxyRule[]): Array<{
69
+ provider: string;
70
+ rules: ProxyRule[];
71
+ }>;
72
+ /**
73
+ * Fill missing rule ids so every card has a stable React key. Host-side code
74
+ * never relies on `id` (the schema leaves it optional), so this is purely a
75
+ * UI concern and does not dirty the saved document.
76
+ */
77
+ export declare function ensureRuleIds(cfg: ModelProxyConfig): ModelProxyConfig;
78
+ export declare class ModelProxyController {
79
+ private readonly scope;
80
+ private listeners;
81
+ private snapshot;
82
+ private unsub?;
83
+ constructor(scope: SettingsScope<ModelProxyConfig>);
84
+ bind(): void;
85
+ dispose(): void;
86
+ subscribe(l: Listener): () => void;
87
+ getSnapshot(): ModelProxySnapshot;
88
+ private pull;
89
+ private emit;
90
+ /**
91
+ * Persist the config as four fenced field writes.
92
+ *
93
+ * The scope contract exposes per-field writes only, so full atomicity would
94
+ * require collapsing the namespace to a single field (a breaking settings
95
+ * shape change). Until then we order the writes so the field most likely to
96
+ * be REJECTED by host validation (`rules`) goes FIRST: if it fails, nothing
97
+ * else has been committed and the failure aborts cleanly instead of leaving
98
+ * mixed old/new state. The remaining fields are booleans plus a URL that the
99
+ * card pre-validates with the same checks the host applies, making their
100
+ * rejection practically unreachable.
101
+ */
102
+ save(next: ModelProxyConfig): Promise<void>;
103
+ setEnabled(v: boolean): Promise<void>;
104
+ setDebug(v: boolean): Promise<void>;
105
+ }
106
+ export {};
107
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/client/controller.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE3E,MAAM,MAAM,SAAS,GAAG;IACtB,4EAA4E;IAC5E,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,aAAa,CAAA;IAC3C,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAA;AAK1B;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAa7D;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,yEAAyE;IACzE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,CAgBhE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC,CAanG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,gBAAgB,CAErE;AAED,qBAAa,oBAAoB;IAKnB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAJlC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,QAAQ,CAA6D;IAC7E,OAAO,CAAC,KAAK,CAAC,CAAY;gBAEG,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC;IAEnE,IAAI,IAAI,IAAI;IAKZ,OAAO,IAAI,IAAI;IAKf,SAAS,CAAC,CAAC,EAAE,QAAQ,GAAG,MAAM,IAAI;IAKlC,WAAW,IAAI,kBAAkB;IAIjC,OAAO,CAAC,IAAI;IAiBZ,OAAO,CAAC,IAAI;IAIZ;;;;;;;;;;;OAWG;IACG,IAAI,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrC,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAG1C"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Browser half: registers `settings.plugin.item` card for `model-proxy`.
3
+ * Non-invasive: only depends on the declared slot via type-only import.
4
+ */
5
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
6
+ export declare const inject: string[];
7
+ export declare function apply(ctx: ClientContext): void;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAO3E,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CA8C9C"}
@@ -0,0 +1,106 @@
1
+ export declare const en: {
2
+ readonly title: "Model Proxy";
3
+ readonly desc: "Route specific provider/model pairs through HTTP/HTTPS/SOCKS5 proxies. Specificity: exact model > prefix \"vendor-*\" > \"*\". Optional purpose filter targets compaction/session-title calls.";
4
+ readonly enabled: "Enable proxy routing";
5
+ readonly debug: "Debug logging (host log)";
6
+ readonly rules: "Rules (first match wins — exact before prefix before \"*\")";
7
+ readonly addRule: "Add rule";
8
+ readonly provider: "Provider";
9
+ readonly providerPlaceholder: "e.g. openai";
10
+ readonly model: "Model";
11
+ readonly modelPlaceholder: "e.g. gpt-4o-mini, vendor-* or *";
12
+ readonly purpose: "Purpose filter";
13
+ readonly purposePlaceholder: "empty = all purposes (e.g. compaction, session-title)";
14
+ readonly credRef: "Credential ref";
15
+ readonly credRefPlaceholder: "credentials entry holding user:password (optional)";
16
+ readonly proxyUrl: "Proxy URL";
17
+ readonly proxyPlaceholder: "socks5://127.0.0.1:1080 or http://127.0.0.1:7890 (empty=direct)";
18
+ readonly enabledShort: "On";
19
+ readonly delete: "Delete";
20
+ readonly defaultProxy: "Fallback proxy (when no rule matches)";
21
+ readonly defaultPlaceholder: "optional, e.g. http://127.0.0.1:7890";
22
+ readonly saving: "Saving…";
23
+ readonly save: "Save";
24
+ readonly saved: "Saved";
25
+ readonly unavailable: "Model proxy settings unavailable.";
26
+ readonly empty: "No rules yet — add one above. Tip: use model \"*\" for all models of a provider.";
27
+ readonly invalid: "Please fix highlighted fields.";
28
+ readonly duplicate: "Duplicate provider+model";
29
+ readonly reading: "Loading…";
30
+ readonly readOnly: "Read-only (settings provider not writable).";
31
+ readonly choose: "Select…";
32
+ readonly customOption: "Custom…";
33
+ readonly backToList: "Pick from the installed catalog";
34
+ readonly dormant: "not configured";
35
+ readonly groupConfigured: "Configured providers";
36
+ readonly groupDirectory: "Other directory routes";
37
+ readonly wildcardAll: "any model";
38
+ readonly catalogUnavailable: "Provider catalog unavailable — type values manually.";
39
+ readonly creatorTitle: "New rules";
40
+ readonly creatorModels: "Models";
41
+ readonly creatorPatternHint: "Or a pattern (vendor-* or *)";
42
+ readonly createN: "Create {n} rule(s)";
43
+ readonly cancel: "Cancel";
44
+ readonly alreadyRuled: "rule exists";
45
+ readonly noneCatalogued: "No catalog models — use * or a custom pattern.";
46
+ readonly groupApplyProxy: "proxy for all…";
47
+ readonly groupApply: "Apply";
48
+ readonly groupEnableAll: "Enable all";
49
+ readonly groupDisableAll: "Disable all";
50
+ readonly groupDelete: "Delete group";
51
+ readonly groupConfirmDelete: "Sure?";
52
+ };
53
+ export declare const zh: {
54
+ readonly title: "模型代理";
55
+ readonly desc: "按 provider/model 粒度走代理(HTTP/HTTPS/SOCKS5)。优先级:精确 model > 前缀 \"vendor-*\" > \"*\"。可选用途过滤可单独控制 compaction/session-title 等调用。";
56
+ readonly enabled: "启用代理路由";
57
+ readonly debug: "调试日志(Host 日志)";
58
+ readonly rules: "规则(首条命中即用 — 精确 > 前缀 > \"*\")";
59
+ readonly addRule: "添加规则";
60
+ readonly provider: "Provider";
61
+ readonly providerPlaceholder: "例如 openai";
62
+ readonly model: "Model";
63
+ readonly modelPlaceholder: "例如 gpt-4o-mini、vendor-* 或 *";
64
+ readonly purpose: "用途过滤";
65
+ readonly purposePlaceholder: "留空=所有用途(如 compaction、session-title)";
66
+ readonly credRef: "凭据引用";
67
+ readonly credRefPlaceholder: "credentials 服务条目(user:password),可选";
68
+ readonly proxyUrl: "代理地址";
69
+ readonly proxyPlaceholder: "socks5://127.0.0.1:1080 或 http://127.0.0.1:7890(空=直连)";
70
+ readonly enabledShort: "启用";
71
+ readonly delete: "删除";
72
+ readonly defaultProxy: "兜底代理(无规则命中时)";
73
+ readonly defaultPlaceholder: "可选,例如 http://127.0.0.1:7890";
74
+ readonly saving: "保存中…";
75
+ readonly save: "保存";
76
+ readonly saved: "已保存";
77
+ readonly unavailable: "模型代理设置不可用。";
78
+ readonly empty: "暂无规则 — 点击上方添加。提示:model 用 \"*\" 表示该 provider 的所有模型。";
79
+ readonly invalid: "请修正标红字段。";
80
+ readonly duplicate: "provider+model 重复";
81
+ readonly reading: "加载中…";
82
+ readonly readOnly: "只读(settings provider 不可写)。";
83
+ readonly choose: "请选择…";
84
+ readonly customOption: "自定义…";
85
+ readonly backToList: "从已安装目录选择";
86
+ readonly dormant: "未配置";
87
+ readonly groupConfigured: "已配置的 Provider";
88
+ readonly groupDirectory: "目录中的其他路由";
89
+ readonly wildcardAll: "所有模型";
90
+ readonly catalogUnavailable: "提供方目录不可用 — 请手动输入。";
91
+ readonly creatorTitle: "新建规则";
92
+ readonly creatorModels: "勾选模型";
93
+ readonly creatorPatternHint: "或自定义模式(vendor-*、*)";
94
+ readonly createN: "创建 {n} 条规则";
95
+ readonly cancel: "取消";
96
+ readonly alreadyRuled: "已有规则";
97
+ readonly noneCatalogued: "该 provider 无目录模型——可用 * 或自定义模式。";
98
+ readonly groupApplyProxy: "整组代理地址…";
99
+ readonly groupApply: "应用";
100
+ readonly groupEnableAll: "全部启用";
101
+ readonly groupDisableAll: "全部停用";
102
+ readonly groupDelete: "删除本组";
103
+ readonly groupConfirmDelete: "确认?";
104
+ };
105
+ export type LocaleKey = keyof typeof en;
106
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../src/client/locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDL,CAAA;AAEV,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDL,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA"}