dsh-gateway-provider 1.0.11 → 1.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.
- package/README.md +5 -3
- package/docs/README.zh.md +1 -1
- package/index.js +29 -13
- package/lib/catalog.js +17 -1
- package/lib/client.js +32 -24
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -84,9 +84,11 @@ Everything lives in **Settings → Gateway Models**:
|
|
|
84
84
|
|
|
85
85
|
## Configuration reference
|
|
86
86
|
|
|
87
|
-
Optional — everything below has a working default.
|
|
88
|
-
`llm-newapi
|
|
89
|
-
the
|
|
87
|
+
Optional — everything below has a working default. With dsh ≥ 0.2 config
|
|
88
|
+
lives in the `llm-newapi` entry of the profile
|
|
89
|
+
(`~/.dsh/profiles/<name>/cordis.patch.yml`; the settings page edits the same
|
|
90
|
+
data). A pre-0.2 `~/.dsh/settings.yaml` section is imported automatically on
|
|
91
|
+
the first start where the plugin loads. The frequently used ones:
|
|
90
92
|
|
|
91
93
|
| Key | Default | Meaning |
|
|
92
94
|
| --- | --- | --- |
|
package/docs/README.zh.md
CHANGED
|
@@ -52,7 +52,7 @@ dsh --profile web
|
|
|
52
52
|
|
|
53
53
|
## 配置参考
|
|
54
54
|
|
|
55
|
-
全部可选 ——
|
|
55
|
+
全部可选 —— 下表每项都有可用默认值。dsh ≥ 0.2 配置存在 profile 的 `llm-newapi` entry(`~/.dsh/profiles/<name>/cordis.patch.yml`,设置页改的就是同一份数据);旧版 `~/.dsh/settings.yaml` 的 `llm-newapi:` 段会在插件可加载后的首次启动自动迁入。常用项:
|
|
56
56
|
|
|
57
57
|
| 键 | 默认值 | 含义 |
|
|
58
58
|
| --- | --- | --- |
|
package/index.js
CHANGED
|
@@ -39,7 +39,11 @@ import { DEFAULT_ENDPOINT_PRIORITY, deriveProtocolURLs } from "./lib/protocols.j
|
|
|
39
39
|
export const name = "llm-newapi";
|
|
40
40
|
export const inject = ["llm"];
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Settings namespace this plugin serves. dsh 0.2 keys settings (and model
|
|
44
|
+
* discovery) by the loader entry id; cordis.patch.yml declares the entry as
|
|
45
|
+
* `llm-newapi`, so the ids coincide and this remains the fallback name.
|
|
46
|
+
*/
|
|
43
47
|
const NS = "llm-newapi";
|
|
44
48
|
/** The legacy single provider route (kept for backwards compatibility). */
|
|
45
49
|
export const PROVIDER = "newapi";
|
|
@@ -60,7 +64,8 @@ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 600_000;
|
|
|
60
64
|
const DEFAULT_CATALOG_TTL_MS = 30 * 60 * 1000;
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
* String field tolerant of hand-edited YAML scalars.
|
|
67
|
+
* String field tolerant of hand-edited YAML scalars. The profile patch
|
|
68
|
+
* (`cordis.patch.yml`, and pre-0.2 `settings.yaml` imports) is a
|
|
64
69
|
* user-editable document parsed with YAML 1.2 core semantics: an unquoted
|
|
65
70
|
* `true` / `false` / `True` / `FALSE` round-trips as a boolean and an
|
|
66
71
|
* unquoted digit run as a number, long before the value reaches a strict
|
|
@@ -183,7 +188,7 @@ export const Config = z.object({
|
|
|
183
188
|
// ---- Additional gateways ----
|
|
184
189
|
/** Additional gateways; each becomes a `gateway:<id>` provider route. */
|
|
185
190
|
gateways: z.array(GatewaySchema).default([]),
|
|
186
|
-
});
|
|
191
|
+
}).volatile();
|
|
187
192
|
|
|
188
193
|
/**
|
|
189
194
|
* Normalize one gateway entry into the connection facts the adapter needs.
|
|
@@ -306,7 +311,12 @@ export function resolveGateways(config, environment) {
|
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
export function apply(ctx, config) {
|
|
309
|
-
|
|
314
|
+
/** Settings namespace this entry serves: the loader entry id, else the legacy name. */
|
|
315
|
+
const settingsNs = ctx.fiber.entry?.options.id ?? NS;
|
|
316
|
+
// dsh 0.2 hands apply() a volatile config reference (a frozen { get } whose
|
|
317
|
+
// snapshot updates after every settings write); plain objects from older
|
|
318
|
+
// harnesses pass through unchanged.
|
|
319
|
+
const current = () => (typeof config?.get === "function" ? config.get() : config);
|
|
310
320
|
let lastRaw;
|
|
311
321
|
let resolved = null;
|
|
312
322
|
/** Resolve (and cache) the gateway list from the current config snapshot. */
|
|
@@ -376,7 +386,7 @@ export function apply(ctx, config) {
|
|
|
376
386
|
const entries = gateways.map((g, i) => ({
|
|
377
387
|
provider: g.provider,
|
|
378
388
|
displayName: g.label,
|
|
379
|
-
settingsNs
|
|
389
|
+
settingsNs,
|
|
380
390
|
settingsPath: i === 0 ? [] : ["gateways", String(g.id)],
|
|
381
391
|
declared: i > 0,
|
|
382
392
|
}));
|
|
@@ -390,7 +400,7 @@ export function apply(ctx, config) {
|
|
|
390
400
|
ensureRegistration();
|
|
391
401
|
|
|
392
402
|
/** Expose model discovery so the web UI can fetch a gateway's model list. */
|
|
393
|
-
ctx.llm.registerModelDiscovery(
|
|
403
|
+
ctx.llm.registerModelDiscovery(settingsNs, async (request) => {
|
|
394
404
|
const env = launchEnvironmentOf(ctx);
|
|
395
405
|
const gateways = resolve();
|
|
396
406
|
const gw = request.provider !== undefined ? gateways.find((g) => g.provider === request.provider) : gateways[0];
|
|
@@ -412,12 +422,18 @@ export function apply(ctx, config) {
|
|
|
412
422
|
}
|
|
413
423
|
});
|
|
414
424
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
425
|
+
// dsh 0.2 settings contract: the volatile-marked Config schema is picked
|
|
426
|
+
// up by the settings service automatically (installSection is gone). Settings
|
|
427
|
+
// writes land in the profile and the loader emits loader/volatile-update;
|
|
428
|
+
// re-resolve and re-register then. resolve() holds the last good snapshot
|
|
429
|
+
// when an update arrives invalid, so the catch only guards registration
|
|
430
|
+
// races (e.g. a provider-route conflict on a mid-flight update).
|
|
431
|
+
ctx.on("loader/volatile-update", () => {
|
|
432
|
+
try {
|
|
433
|
+
ensureRegistration();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
ctx.logger.error("llm-newapi: keeping the last good configuration after a settings update");
|
|
436
|
+
ctx.logger.error(error);
|
|
437
|
+
}
|
|
422
438
|
});
|
|
423
439
|
}
|
package/lib/catalog.js
CHANGED
|
@@ -121,15 +121,31 @@ async function fetchManagementModels(baseURL, apiKey, userId, signal) {
|
|
|
121
121
|
return entries;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Strip trailing slashes and a trailing version segment (`/v1`, `/v2`, …)
|
|
126
|
+
* from a gateway base URL. The catalog always appends its own full paths
|
|
127
|
+
* (`/v1/models`, `/api/user/models`), so a user-supplied base that already
|
|
128
|
+
* carries `/v1` (the OpenAI-SDK convention, which the chat path also accepts)
|
|
129
|
+
* must not leak through as `/v1/v1/models`. Mirrors the derivation the custom
|
|
130
|
+
* (protocol-URL) template performs in `protocols.js`.
|
|
131
|
+
*/
|
|
132
|
+
function stripVersion(baseURL) {
|
|
133
|
+
return String(baseURL ?? "").replace(/\/+$/, "").replace(/\/v\d+$/, "");
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
/**
|
|
125
137
|
* The base URL model discovery fetches `/v1/models` against. Gateways with
|
|
126
138
|
* protocol URL fields (custom template) derive it from their OpenAI-style
|
|
127
139
|
* URL; `null` means discovery is impossible (e.g. only an Anthropic address
|
|
128
140
|
* is configured) and the catalog is exactly the user-declared models.
|
|
141
|
+
* The returned base is version-stripped: the catalog suffixes below always
|
|
142
|
+
* append the version segment themselves.
|
|
129
143
|
*/
|
|
130
144
|
function discoveryBase(connection) {
|
|
131
145
|
const base = connection.catalogBase !== undefined ? connection.catalogBase : connection.baseURL;
|
|
132
|
-
|
|
146
|
+
if (typeof base !== "string" || base.length === 0) return null;
|
|
147
|
+
const stripped = stripVersion(base);
|
|
148
|
+
return stripped.length > 0 ? stripped : null;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
/**
|
package/lib/client.js
CHANGED
|
@@ -77,7 +77,7 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
responsesURLPlaceholder: "https://gw.example.com/v1/responses",
|
|
78
78
|
anthropicURLPlaceholder: "https://gw.example.com/v1/messages",
|
|
79
79
|
catalogHint: "auto:优先 /v1/models,鉴权失败时回退管理 API",
|
|
80
|
-
baseURLClearHint: "留空则回退到环境变量 NEWAPI_BASE_URL
|
|
80
|
+
baseURLClearHint: "留空则回退到环境变量 NEWAPI_BASE_URL 或官方云端地址;结尾带不带 /v1 均可",
|
|
81
81
|
labelClearHint: "留空则回退为 gateway:<id>",
|
|
82
82
|
keyEnvHint: "留空则继承默认网关的 Key(NEWAPI_API_KEY)",
|
|
83
83
|
keyKeepHint: "留空保持不变;填写则覆盖存储的 Key",
|
|
@@ -137,7 +137,7 @@ window.__ModuleLoader__.load({
|
|
|
137
137
|
responsesURLPlaceholder: "https://gw.example.com/v1/responses",
|
|
138
138
|
anthropicURLPlaceholder: "https://gw.example.com/v1/messages",
|
|
139
139
|
catalogHint: "auto: prefer /v1/models, fall back to the management API on auth failure",
|
|
140
|
-
baseURLClearHint: "Empty falls back to NEWAPI_BASE_URL or the public cloud default",
|
|
140
|
+
baseURLClearHint: "Empty falls back to NEWAPI_BASE_URL or the public cloud default; a trailing /v1 is optional",
|
|
141
141
|
labelClearHint: "Empty falls back to gateway:<id>",
|
|
142
142
|
keyEnvHint: "Empty inherits the default gateway's key (NEWAPI_API_KEY)",
|
|
143
143
|
keyKeepHint: "Empty keeps the stored key; type to overwrite",
|
|
@@ -820,8 +820,8 @@ window.__ModuleLoader__.load({
|
|
|
820
820
|
var snapRef = React.useRef(null);
|
|
821
821
|
|
|
822
822
|
var load = React.useCallback(function () {
|
|
823
|
-
api.settings.describe(
|
|
824
|
-
var nsList = res && res.
|
|
823
|
+
api.settings.describe().then(function (res) {
|
|
824
|
+
var nsList = res && res.ok ? (res.value.namespaces || []) : [];
|
|
825
825
|
var desc = nsList.find(function (n) { return n.ns === NS; });
|
|
826
826
|
var snap = desc ? desc.value || {} : {};
|
|
827
827
|
setSnapshot(snap);
|
|
@@ -831,8 +831,8 @@ window.__ModuleLoader__.load({
|
|
|
831
831
|
(snap.gateways || []).forEach(function (gw) {
|
|
832
832
|
if (gw.apiKeyEnv && refs.indexOf(gw.apiKeyEnv) === -1) refs.push(gw.apiKeyEnv);
|
|
833
833
|
});
|
|
834
|
-
return api.credentials.describe(
|
|
835
|
-
var creds = (cres && cres.
|
|
834
|
+
return api.credentials.describe(refs).then(function (cres) {
|
|
835
|
+
var creds = (cres && cres.ok) ? (cres.value || {}) : {};
|
|
836
836
|
setCredentials(creds);
|
|
837
837
|
}).catch(function () { setCredentials({}); });
|
|
838
838
|
}).catch(function () { setSnapshot({}); });
|
|
@@ -849,9 +849,9 @@ window.__ModuleLoader__.load({
|
|
|
849
849
|
/** Mutate one path op into the settings namespace; always reload
|
|
850
850
|
* afterwards (success resyncs the ref, failure rolls it back). */
|
|
851
851
|
var mutate = function (ops) {
|
|
852
|
-
return api.settings.mutate(
|
|
853
|
-
if (!(res && res.
|
|
854
|
-
console.warn("llm-newapi: settings write failed", res && res.
|
|
852
|
+
return api.settings.mutate(NS, ops, undefined).then(function (res) {
|
|
853
|
+
if (!(res && res.ok)) {
|
|
854
|
+
console.warn("llm-newapi: settings write failed", res && res.error);
|
|
855
855
|
load();
|
|
856
856
|
return false;
|
|
857
857
|
}
|
|
@@ -897,13 +897,13 @@ window.__ModuleLoader__.load({
|
|
|
897
897
|
* An empty baseURL (URL-addressed custom gateway) is sent as
|
|
898
898
|
* absent so the host uses its configured discovery base. */
|
|
899
899
|
var discover = function (gwIdx, baseURL, provider) {
|
|
900
|
-
return api.llm.discoverModels(
|
|
901
|
-
if (!(res && res.
|
|
902
|
-
var raw = res && res.
|
|
903
|
-
var msg =
|
|
900
|
+
return api.llm.discoverModels(NS, { provider: provider, baseURL: baseURL || undefined }).then(function (res) {
|
|
901
|
+
if (!(res && res.ok)) {
|
|
902
|
+
var raw = res && res.error;
|
|
903
|
+
var msg = (raw && raw.message) || "";
|
|
904
904
|
throw new Error(msg || T.testFail);
|
|
905
905
|
}
|
|
906
|
-
return (res.
|
|
906
|
+
return (res.value || []);
|
|
907
907
|
});
|
|
908
908
|
};
|
|
909
909
|
|
|
@@ -993,8 +993,8 @@ window.__ModuleLoader__.load({
|
|
|
993
993
|
? { op: "set", path: [k], value: changes[k] }
|
|
994
994
|
: { op: "unset", path: [k] });
|
|
995
995
|
});
|
|
996
|
-
if (ops.length > 0) writes.push(api.settings.mutate(
|
|
997
|
-
if (changes.apiKey !== undefined) writes.push(api.credentials.set(
|
|
996
|
+
if (ops.length > 0) writes.push(api.settings.mutate(NS, ops, undefined));
|
|
997
|
+
if (changes.apiKey !== undefined) writes.push(api.credentials.set(defaultEnv, changes.apiKey));
|
|
998
998
|
// Optimistically sync the ref; load() reconciles after settle.
|
|
999
999
|
var nextSnap = Object.assign({}, snap);
|
|
1000
1000
|
if (changes.baseURL !== undefined) { if (changes.baseURL.length > 0) nextSnap.baseURL = changes.baseURL; else delete nextSnap.baseURL; }
|
|
@@ -1033,16 +1033,16 @@ window.__ModuleLoader__.load({
|
|
|
1033
1033
|
if (touched) {
|
|
1034
1034
|
var nextGateways = gws.slice();
|
|
1035
1035
|
nextGateways[gwIdx] = updated;
|
|
1036
|
-
writes.push(api.settings.mutate(
|
|
1036
|
+
writes.push(api.settings.mutate(NS, [{ op: "set", path: ["gateways"], value: nextGateways }], undefined));
|
|
1037
1037
|
syncGateways(nextGateways);
|
|
1038
1038
|
}
|
|
1039
1039
|
if (changes.apiKey !== undefined) {
|
|
1040
1040
|
var keyRef = changes.apiKeyEnv || gw.apiKeyEnv || defaultEnv;
|
|
1041
|
-
writes.push(api.credentials.set(
|
|
1041
|
+
writes.push(api.credentials.set(keyRef, changes.apiKey));
|
|
1042
1042
|
}
|
|
1043
1043
|
}
|
|
1044
1044
|
return Promise.all(writes).then(function (results) {
|
|
1045
|
-
var ok = results.every(function (r) { return r
|
|
1045
|
+
var ok = results.every(function (r) { return r === undefined || r === null || (r && r.ok) !== false; });
|
|
1046
1046
|
load(); // resync the ref with the persisted truth (success or not)
|
|
1047
1047
|
return { ok: ok, label: ok ? T.configSaved : T.configSaveFail };
|
|
1048
1048
|
}).catch(function () {
|
|
@@ -1068,14 +1068,14 @@ window.__ModuleLoader__.load({
|
|
|
1068
1068
|
var nextGateways = gws.concat([entry]);
|
|
1069
1069
|
syncGateways(nextGateways);
|
|
1070
1070
|
var writes = [
|
|
1071
|
-
api.settings.mutate(
|
|
1071
|
+
api.settings.mutate(NS, [{ op: "set", path: ["gateways"], value: nextGateways }], undefined),
|
|
1072
1072
|
];
|
|
1073
1073
|
if (spec.apiKey !== undefined && spec.apiKey.length > 0) {
|
|
1074
1074
|
var keyEnv = (snapRef.current && snapRef.current.apiKeyEnv) || "NEWAPI_API_KEY";
|
|
1075
|
-
writes.push(api.credentials.set(
|
|
1075
|
+
writes.push(api.credentials.set(spec.apiKeyEnv || keyEnv, spec.apiKey));
|
|
1076
1076
|
}
|
|
1077
1077
|
return Promise.all(writes).then(function (results) {
|
|
1078
|
-
var ok = results.every(function (r) { return r
|
|
1078
|
+
var ok = results.every(function (r) { return r === undefined || r === null || (r && r.ok) !== false; });
|
|
1079
1079
|
load();
|
|
1080
1080
|
return { ok: ok, label: ok ? T.configSaved : T.configSaveFail };
|
|
1081
1081
|
}).catch(function () {
|
|
@@ -1136,7 +1136,7 @@ window.__ModuleLoader__.load({
|
|
|
1136
1136
|
);
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
-
var inject = ["slots", "
|
|
1139
|
+
var inject = ["slots", "remote", "remote.settings", "remote.credentials", "remote.llm"];
|
|
1140
1140
|
|
|
1141
1141
|
function apply(ctx) {
|
|
1142
1142
|
var slots = ctx.get("slots");
|
|
@@ -1147,7 +1147,15 @@ window.__ModuleLoader__.load({
|
|
|
1147
1147
|
id: "newapi-gateways",
|
|
1148
1148
|
order: 20,
|
|
1149
1149
|
label: function () { return T.nav; },
|
|
1150
|
-
inject: function () {
|
|
1150
|
+
inject: function () {
|
|
1151
|
+
// Typed Remote namespaces (dsh ≥ 0.1.2-rc.1): the old
|
|
1152
|
+
// `ctx.get("connection").api` RPC facade was removed.
|
|
1153
|
+
return { api: {
|
|
1154
|
+
settings: ctx.get("remote.settings"),
|
|
1155
|
+
credentials: ctx.get("remote.credentials"),
|
|
1156
|
+
llm: ctx.get("remote.llm"),
|
|
1157
|
+
} };
|
|
1158
|
+
},
|
|
1151
1159
|
}, function (props) {
|
|
1152
1160
|
return React.createElement(GatewayModelsSection, { api: props.api });
|
|
1153
1161
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gateway-provider",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Generic LLM gateway model provider plugin for DeepSeek Harness: mounts newapi / LiteLLM / Higress / any OpenAI-compatible gateway as provider routes, auto-discovers the model list, enriches every model with models.dev parameters, and dispatches each model over its native wire protocol (OpenAI / Anthropic / Gemini) via the pi-ai SDK",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"smoke": "node test/smoke.mjs",
|
|
30
30
|
"test:session": "node test/session-affinity.mjs",
|
|
31
31
|
"test:client": "node test/client-render.mjs",
|
|
32
|
+
"test:client-api": "node test/client-api.mjs",
|
|
32
33
|
"test:urls": "node test/protocol-urls.mjs",
|
|
33
34
|
"test:schema": "node test/schema-coercion.mjs",
|
|
34
35
|
"test:errors": "node test/error-codes.mjs"
|
|
@@ -53,8 +54,10 @@
|
|
|
53
54
|
"dsh": {
|
|
54
55
|
"client": {
|
|
55
56
|
"inject": [
|
|
56
|
-
"@deepseek-ai/dsh-client-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
57
|
+
"@deepseek-ai/dsh-client-connection",
|
|
58
|
+
"@deepseek-ai/dsh-api-gateway",
|
|
59
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
58
61
|
],
|
|
59
62
|
"platform": "web"
|
|
60
63
|
},
|
|
@@ -67,10 +70,10 @@
|
|
|
67
70
|
},
|
|
68
71
|
"peerDependencies": {
|
|
69
72
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
70
|
-
"@deepseek-ai/dsh-credentials": "
|
|
71
|
-
"@deepseek-ai/dsh-launch-environment": "
|
|
72
|
-
"@deepseek-ai/dsh-llm": "
|
|
73
|
-
"@deepseek-ai/dsh-timeout": "
|
|
73
|
+
"@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-timeout": "0.2.0-rc.2",
|
|
74
77
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
75
78
|
}
|
|
76
79
|
}
|