dsh-gateway-provider 1.0.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/LICENSE +21 -0
- package/README.md +234 -0
- package/README.zh.md +218 -0
- package/cordis.patch.yml +10 -0
- package/index.js +398 -0
- package/lib/adapter.js +222 -0
- package/lib/catalog.js +367 -0
- package/lib/client.js +1164 -0
- package/lib/modelsdev.js +113 -0
- package/lib/pi-bridge.js +481 -0
- package/lib/pi-provider.js +148 -0
- package/lib/protocols.js +150 -0
- package/lib/thinking.js +219 -0
- package/package.json +64 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct a pi-ai `Provider` for one newapi gateway.
|
|
3
|
+
*
|
|
4
|
+
* The gateway advertises models over `GET /v1/models` (with per-model
|
|
5
|
+
* `supported_endpoint_types`). This module maps each discovered model into a
|
|
6
|
+
* pi-ai `Model` whose `api` field names the wire protocol pi-ai's protocol
|
|
7
|
+
* layer should speak for it, then builds the provider with
|
|
8
|
+
* `createProvider({ id, baseUrl, auth, models, api })`.
|
|
9
|
+
*
|
|
10
|
+
* The gateway's endpoint-type vocabulary (openai / openai-response /
|
|
11
|
+
* anthropic / gemini) maps onto pi-ai's API identifiers:
|
|
12
|
+
*
|
|
13
|
+
* | gateway endpoint type | pi-ai api |
|
|
14
|
+
* |----------------------|------------------------|
|
|
15
|
+
* | openai | openai-completions |
|
|
16
|
+
* | openai-response | openai-responses |
|
|
17
|
+
* | anthropic | anthropic-messages |
|
|
18
|
+
* | gemini | google-generative-ai |
|
|
19
|
+
*
|
|
20
|
+
* A model advertising several types picks the first that matches the gateway's
|
|
21
|
+
* configured priority (default openai-responses → anthropic → openai → gemini).
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-gateway-provider/pi-provider
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createProvider } from "@earendil-works/pi-ai";
|
|
27
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
28
|
+
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
29
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
30
|
+
import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
|
|
31
|
+
import { DEFAULT_MAX_TOKENS, NO_COST } from "./catalog.js";
|
|
32
|
+
import { findPiModel } from "./thinking.js";
|
|
33
|
+
import { apiOfEndpointType, pickModelApi, DEFAULT_ENDPOINT_PRIORITY } from "./protocols.js";
|
|
34
|
+
|
|
35
|
+
// Re-exported for existing importers; the mapping itself lives in protocols.js.
|
|
36
|
+
export { apiOfEndpointType, pickModelApi, DEFAULT_ENDPOINT_PRIORITY };
|
|
37
|
+
|
|
38
|
+
/** pi-ai protocol factories keyed by their API identifier. */
|
|
39
|
+
const PROTOCOLS = {
|
|
40
|
+
"openai-completions": openAICompletionsApi,
|
|
41
|
+
"openai-responses": openAIResponsesApi,
|
|
42
|
+
"anthropic-messages": anthropicMessagesApi,
|
|
43
|
+
"google-generative-ai": googleGenerativeAIApi,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize the gateway base URL for one pi-ai API.
|
|
48
|
+
*
|
|
49
|
+
* The OpenAI Node SDK (used by pi-ai for openai-completions / openai-responses)
|
|
50
|
+
* expects the baseURL to already include `/v1` — its default is
|
|
51
|
+
* `https://api.openai.com/v1`, and `responses.create()` only appends
|
|
52
|
+
* `/responses`. A gateway baseURL without `/v1` makes the SDK request
|
|
53
|
+
* `{base}/responses`, which the gateway answers with its HTML UI (HTTP 200,
|
|
54
|
+
* content-type text/html) instead of an SSE stream, so the SDK receives zero
|
|
55
|
+
* events. Anthropic and Google protocols build their own full paths, so they
|
|
56
|
+
* need no `/v1` appended.
|
|
57
|
+
* @param baseURL - the gateway base URL as configured.
|
|
58
|
+
* @param api - the pi-ai API identifier the model will use.
|
|
59
|
+
* @returns the baseURL the OpenAI SDK should point at.
|
|
60
|
+
*/
|
|
61
|
+
function sdkBaseURL(baseURL, api) {
|
|
62
|
+
const trimmed = String(baseURL ?? "").replace(/\/+$/, "");
|
|
63
|
+
if (api === "openai-completions" || api === "openai-responses") {
|
|
64
|
+
if (!/\/v\d+$/.test(trimmed)) return `${trimmed}/v1`;
|
|
65
|
+
}
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build one pi-ai `Model` from a discovered gateway entry, enriched with
|
|
71
|
+
* models.dev parameters and the user's per-model overrides.
|
|
72
|
+
* @param entry - the enriched catalog entry (id, name, contextWindow, …).
|
|
73
|
+
* @param providerId - the provider route key (becomes `model.provider`).
|
|
74
|
+
* @param baseURL - the gateway base URL (becomes `model.baseUrl`).
|
|
75
|
+
* @param priority - endpoint-type preference order.
|
|
76
|
+
* @param defaults - fallback capacities (contextWindow, maxTokens); when it
|
|
77
|
+
* carries `apiBases` (protocol URL fields configured), that entry wins for
|
|
78
|
+
* the model's API instead of deriving from `baseURL`.
|
|
79
|
+
* @param override - per-model override object from settings (may be undefined).
|
|
80
|
+
* @returns the pi-ai Model object.
|
|
81
|
+
*/
|
|
82
|
+
export function buildModel(entry, providerId, baseURL, priority, defaults, override) {
|
|
83
|
+
const api = pickModelApi(entry.endpointTypes, priority);
|
|
84
|
+
// Cost comes from the pi-ai builtin catalog when the model resolves there;
|
|
85
|
+
// models without a pi-ai match (custom gateway entries, internal-test ids)
|
|
86
|
+
// carry NO_COST — the harness only reads cost for usage accounting and
|
|
87
|
+
// tolerates zero values gracefully.
|
|
88
|
+
const piModel = findPiModel(entry.id);
|
|
89
|
+
// A gateway with protocol URL fields (custom template) carries exact SDK
|
|
90
|
+
// bases per API; otherwise derive from the single base URL.
|
|
91
|
+
const apiBase = defaults?.apiBases?.[api];
|
|
92
|
+
return {
|
|
93
|
+
id: entry.id,
|
|
94
|
+
name: entry.name ?? entry.id,
|
|
95
|
+
api,
|
|
96
|
+
provider: providerId,
|
|
97
|
+
baseUrl: apiBase !== undefined ? apiBase : sdkBaseURL(baseURL, api),
|
|
98
|
+
reasoning: entry.reasoning ?? false,
|
|
99
|
+
input: entry.input ?? ["text"],
|
|
100
|
+
cost: piModel?.cost ?? NO_COST,
|
|
101
|
+
contextWindow: override?.contextWindow ?? entry.contextWindow ?? defaults?.defaultContextWindow ?? defaults?.contextWindow,
|
|
102
|
+
maxTokens: override?.maxTokens ?? entry.maxTokens ?? defaults?.maxTokens,
|
|
103
|
+
...entry.thinkingLevelMap !== undefined ? { thinkingLevelMap: entry.thinkingLevelMap } : {},
|
|
104
|
+
...piModel?.thinkingLevelMap !== undefined ? { thinkingLevelMap: piModel.thinkingLevelMap } : {},
|
|
105
|
+
...piModel?.compat !== undefined ? { compat: piModel.compat } : {},
|
|
106
|
+
...entry.compat !== undefined ? { compat: entry.compat } : {},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Construct the pi-ai Provider for one gateway connection.
|
|
112
|
+
*
|
|
113
|
+
* The provider carries every discovered model. `api` is passed as a **map**
|
|
114
|
+
* keyed by pi-ai API identifier (openai-completions / openai-responses /
|
|
115
|
+
* anthropic-messages / google-generative-ai): pi-ai's `createProvider` then
|
|
116
|
+
* dispatches each model to the protocol its `model.api` field names. Passing a
|
|
117
|
+
* single ProviderStreams instead would force every model through one protocol
|
|
118
|
+
* regardless of its `api` field.
|
|
119
|
+
*
|
|
120
|
+
* @param spec - the resolved gateway facts.
|
|
121
|
+
* @returns the pi-ai Provider registered into the adapter's Models collection.
|
|
122
|
+
*/
|
|
123
|
+
export function buildProvider(spec) {
|
|
124
|
+
return createProvider({
|
|
125
|
+
id: spec.providerId,
|
|
126
|
+
name: spec.displayName,
|
|
127
|
+
baseUrl: spec.baseURL,
|
|
128
|
+
auth: { apiKey: gatewayAuth(spec.displayName) },
|
|
129
|
+
models: spec.models,
|
|
130
|
+
api: {
|
|
131
|
+
"openai-completions": openAICompletionsApi(),
|
|
132
|
+
"openai-responses": openAIResponsesApi(),
|
|
133
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
134
|
+
"google-generative-ai": googleGenerativeAIApi(),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Api-key auth resolved from the harness credential the adapter holds. */
|
|
140
|
+
function gatewayAuth(name) {
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
resolve: ({ credential }) => Promise.resolve({
|
|
144
|
+
auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
|
|
145
|
+
source: name,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
package/lib/protocols.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared endpoint-type ↔ pi-ai protocol vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* The gateway advertises per-model endpoint types (openai / openai-response /
|
|
5
|
+
* anthropic / gemini); pi-ai identifies wire protocols by API id
|
|
6
|
+
* (openai-completions / openai-responses / anthropic-messages /
|
|
7
|
+
* google-generative-ai). This module owns the mapping and the priority-based
|
|
8
|
+
* selection so both the catalog (discovery preview) and the pi-provider
|
|
9
|
+
* (request dispatch) share one source of truth without importing each other.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-gateway-provider/protocols
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Default endpoint-type preference: prefer the modern Responses API. */
|
|
15
|
+
export const DEFAULT_ENDPOINT_PRIORITY = ["openai-response", "anthropic", "openai", "gemini"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Known endpoint path suffixes each wire protocol is served under, used to
|
|
19
|
+
* reduce a fully-qualified endpoint URL to the SDK base URL. The OpenAI SDK
|
|
20
|
+
* (chat + responses) expects a base ending in `/v1` and appends the suffix
|
|
21
|
+
* itself; the Anthropic SDK appends `/v1/messages`.
|
|
22
|
+
*/
|
|
23
|
+
const ENDPOINT_SUFFIXES = {
|
|
24
|
+
"openai-completions": "/chat/completions",
|
|
25
|
+
"openai-responses": "/responses",
|
|
26
|
+
"anthropic-messages": "/v1/messages",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Trim a configured URL and strip one known endpoint suffix if present. */
|
|
30
|
+
function stripSuffix(url, suffix) {
|
|
31
|
+
const s = String(url ?? "").trim().replace(/\/+$/, "");
|
|
32
|
+
if (s.length === 0) return undefined;
|
|
33
|
+
if (s.toLowerCase().endsWith(suffix)) return s.slice(0, -suffix.length).replace(/\/+$/, "") || undefined;
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derive the per-protocol facts from fully-qualified endpoint URLs
|
|
39
|
+
* (`openaiURL` / `responsesURL` / `anthropicURL`). Unset URLs mean the
|
|
40
|
+
* protocol is unavailable on this gateway ("没填的就是没有"): the returned
|
|
41
|
+
* `availableTypes` is exactly the configured set.
|
|
42
|
+
*
|
|
43
|
+
* Each URL may be the complete endpoint address (`…/v1/chat/completions`),
|
|
44
|
+
* an SDK base (`…/v1`), or — for anthropic — a bare host; the derivation
|
|
45
|
+
* strips the known suffix and re-adds the version segment where the SDK
|
|
46
|
+
* expects it.
|
|
47
|
+
* @param urls - the gateway's protocol URL fields (any may be absent).
|
|
48
|
+
* @returns `{ apiBases, availableTypes, catalogBase }` where apiBases maps
|
|
49
|
+
* pi-ai API id → SDK base URL, availableTypes lists gateway endpoint
|
|
50
|
+
* types, and catalogBase is the bare base for `/v1/models` discovery
|
|
51
|
+
* (null when no OpenAI-style URL is set); undefined when no URL is set.
|
|
52
|
+
*/
|
|
53
|
+
export function deriveProtocolURLs(urls) {
|
|
54
|
+
if (urls === null || typeof urls !== "object") return undefined;
|
|
55
|
+
const apiBases = {};
|
|
56
|
+
const availableTypes = [];
|
|
57
|
+
let catalogBase;
|
|
58
|
+
|
|
59
|
+
const oc = stripSuffix(urls.openaiURL, ENDPOINT_SUFFIXES["openai-completions"]);
|
|
60
|
+
if (oc !== undefined) {
|
|
61
|
+
apiBases["openai-completions"] = /\/v\d+$/.test(oc) ? oc : `${oc}/v1`;
|
|
62
|
+
availableTypes.push("openai");
|
|
63
|
+
}
|
|
64
|
+
const rs = stripSuffix(urls.responsesURL, ENDPOINT_SUFFIXES["openai-responses"]);
|
|
65
|
+
if (rs !== undefined) {
|
|
66
|
+
apiBases["openai-responses"] = /\/v\d+$/.test(rs) ? rs : `${rs}/v1`;
|
|
67
|
+
availableTypes.push("openai-response");
|
|
68
|
+
}
|
|
69
|
+
// Anthropic: the SDK appends `/v1/messages` itself, so accept the full
|
|
70
|
+
// endpoint URL, a base ending in `/v1`, or a bare host.
|
|
71
|
+
const anRaw = String(urls.anthropicURL ?? "").trim().replace(/\/+$/, "");
|
|
72
|
+
if (anRaw.length > 0) {
|
|
73
|
+
const lower = anRaw.toLowerCase();
|
|
74
|
+
const stripped = lower.endsWith(ENDPOINT_SUFFIXES["anthropic-messages"])
|
|
75
|
+
? anRaw.slice(0, -ENDPOINT_SUFFIXES["anthropic-messages"].length)
|
|
76
|
+
: lower.endsWith("/v1") ? anRaw.slice(0, -"/v1".length) : anRaw;
|
|
77
|
+
const an = stripped.replace(/\/+$/, "");
|
|
78
|
+
if (an.length > 0) {
|
|
79
|
+
apiBases["anthropic-messages"] = an;
|
|
80
|
+
availableTypes.push("anthropic");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (availableTypes.length === 0) return undefined;
|
|
84
|
+
for (const api of ["openai-completions", "openai-responses"]) {
|
|
85
|
+
if (apiBases[api] !== undefined) { catalogBase = apiBases[api].replace(/\/v\d+$/, ""); break; }
|
|
86
|
+
}
|
|
87
|
+
return { apiBases, availableTypes, catalogBase: catalogBase ?? null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Intersect a model's advertised endpoint types with the protocols the
|
|
92
|
+
* gateway actually has addresses for. Models that advertise nothing inherit
|
|
93
|
+
* the full available set (the gateway's protocols define their fallback).
|
|
94
|
+
* @param endpointTypes - protocols the model advertises (may be undefined).
|
|
95
|
+
* @param availableTypes - configured protocols, or undefined = all allowed.
|
|
96
|
+
* @returns the effective endpoint-type list, or null when the model cannot
|
|
97
|
+
* be served on any configured protocol (caller drops it).
|
|
98
|
+
*/
|
|
99
|
+
export function effectiveEndpointTypes(endpointTypes, availableTypes) {
|
|
100
|
+
if (availableTypes === undefined) return endpointTypes;
|
|
101
|
+
if (!Array.isArray(endpointTypes) || endpointTypes.length === 0) return availableTypes;
|
|
102
|
+
const kept = endpointTypes.filter((t) => availableTypes.includes(t));
|
|
103
|
+
return kept.length > 0 ? kept : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Gateway endpoint type → pi-ai API identifier. */
|
|
107
|
+
export function apiOfEndpointType(type) {
|
|
108
|
+
switch (type) {
|
|
109
|
+
case "openai": return "openai-completions";
|
|
110
|
+
case "openai-response": return "openai-responses";
|
|
111
|
+
case "anthropic": return "anthropic-messages";
|
|
112
|
+
case "gemini": return "google-generative-ai";
|
|
113
|
+
default: return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** pi-ai API identifier → gateway endpoint type (inverse mapping). */
|
|
118
|
+
export function endpointTypeOfApi(api) {
|
|
119
|
+
switch (api) {
|
|
120
|
+
case "openai-completions": return "openai";
|
|
121
|
+
case "openai-responses": return "openai-response";
|
|
122
|
+
case "anthropic-messages": return "anthropic";
|
|
123
|
+
case "google-generative-ai": return "gemini";
|
|
124
|
+
default: return api;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Choose the pi-ai API for one model from its advertised endpoint types,
|
|
130
|
+
* honoring the priority order (most-preferred first).
|
|
131
|
+
* @param {string[]} endpointTypes - protocols the model advertises (gateway vocabulary).
|
|
132
|
+
* @param {string[]} priority - ordered gateway endpoint types to try.
|
|
133
|
+
* @returns {string} the pi-ai API identifier, or "openai-completions" as fallback.
|
|
134
|
+
*/
|
|
135
|
+
export function pickModelApi(endpointTypes, priority) {
|
|
136
|
+
const advertised = Array.isArray(endpointTypes) ? endpointTypes : [];
|
|
137
|
+
if (advertised.length === 0) return "openai-completions";
|
|
138
|
+
for (const preferred of priority ?? DEFAULT_ENDPOINT_PRIORITY) {
|
|
139
|
+
if (advertised.includes(preferred)) {
|
|
140
|
+
const api = apiOfEndpointType(preferred);
|
|
141
|
+
if (api !== undefined) return api;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Fall back to any advertised type we recognize.
|
|
145
|
+
for (const type of advertised) {
|
|
146
|
+
const api = apiOfEndpointType(type);
|
|
147
|
+
if (api !== undefined) return api;
|
|
148
|
+
}
|
|
149
|
+
return "openai-completions";
|
|
150
|
+
}
|
package/lib/thinking.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model thinking-level management, reusing the DeepSeek Harness's own model
|
|
3
|
+
* directory and reasoning machinery from `@earendil-works/pi-ai` (the library
|
|
4
|
+
* behind the official `llm-pi-ai` adapter, always installed with the harness).
|
|
5
|
+
*
|
|
6
|
+
* Instead of heuristically deriving reasoning levels, this module:
|
|
7
|
+
*
|
|
8
|
+
* 1. looks the model up in pi-ai's builtin catalog (1100+ models across 37
|
|
9
|
+
* providers) by id — every entry carries `reasoning` plus a
|
|
10
|
+
* `thinkingLevelMap` that spells the provider-native wire value for each
|
|
11
|
+
* thinking level (`null` = unsupported, absent = supported with the level
|
|
12
|
+
* string itself as the wire value);
|
|
13
|
+
* 2. derives the selectable levels with pi-ai's own
|
|
14
|
+
* {@link getSupportedThinkingLevels} (levels `off` / `minimal` / `low` /
|
|
15
|
+
* `medium` / `high` / `xhigh` / `max` with pi-ai's asymmetric defaulting:
|
|
16
|
+
* an absent key supports the base levels, while `xhigh`/`max` need an
|
|
17
|
+
* explicit value);
|
|
18
|
+
* 3. serializes the thinking parameters following pi-ai's openai-completions
|
|
19
|
+
* branches: `compat.thinkingFormat === "deepseek"` models get
|
|
20
|
+
* `thinking: {type: enabled|disabled}` (+ `reasoning_effort` unless
|
|
21
|
+
* `supportsReasoningEffort` is false), everything else gets OpenAI-style
|
|
22
|
+
* `reasoning_effort` mapped through `thinkingLevelMap`.
|
|
23
|
+
*
|
|
24
|
+
* Gateway model ids are first normalized to their base form (namespace prefix
|
|
25
|
+
* and channel suffixes like `-highspeed` stripped) so variants such as
|
|
26
|
+
* `glm-5.2-highspeed` resolve to `glm-5.2`; the same model advertised under
|
|
27
|
+
* several providers is resolved to its first-party entry (native wire
|
|
28
|
+
* semantics) rather than an arbitrary aggregator copy.
|
|
29
|
+
*
|
|
30
|
+
* Only when the pi-ai catalog misses a model (custom gateway entries) does the
|
|
31
|
+
* module fall back to models.dev inference (`reasoning: true` + family), with
|
|
32
|
+
* an optional `extended` mode that widens the fallback to the full normalized
|
|
33
|
+
* level set for the user to choose explicitly.
|
|
34
|
+
*
|
|
35
|
+
* @module dsh-gateway-provider/thinking
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
39
|
+
import { getBuiltinModels, getBuiltinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
40
|
+
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
41
|
+
|
|
42
|
+
/** Gateway channel-suffix markers stripped before a catalog lookup. */
|
|
43
|
+
const MODEL_VARIANT_SUFFIXES = ["highspeed", "high-speed", "lowspeed", "low-speed"];
|
|
44
|
+
const VARIANT_SUFFIX_RE = new RegExp(`-(${MODEL_VARIANT_SUFFIXES.join("|")})$`, "i");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a gateway model id to its base form: lowercase, strip any
|
|
48
|
+
* `provider/` namespace, and drop a trailing gateway channel suffix
|
|
49
|
+
* (`-highspeed`, `-lowspeed`, ...). Variants like `glm-5.2-highspeed`
|
|
50
|
+
* resolve to their catalog model `glm-5.2`. Variant identity is preserved
|
|
51
|
+
* separately by {@link variantLabel} so pickers can tell them apart.
|
|
52
|
+
* @param id - the gateway-advertised model id.
|
|
53
|
+
* @returns the normalized base id, or "" when there is nothing usable.
|
|
54
|
+
*/
|
|
55
|
+
export function baseModelId(id) {
|
|
56
|
+
const s = String(id ?? "").trim().toLowerCase();
|
|
57
|
+
if (s.length === 0) return "";
|
|
58
|
+
const bare = s.includes("/") ? s.slice(s.lastIndexOf("/") + 1) : s;
|
|
59
|
+
return bare.replace(VARIANT_SUFFIX_RE, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The human-readable variant tag stripped by {@link baseModelId}, Title-Cased
|
|
64
|
+
* (e.g. `glm-5.2-highspeed` → `Highspeed`, `gpt-5.5-lowspeed` → `Lowspeed`).
|
|
65
|
+
* Returns "" for non-variant ids so the base model name is left untouched.
|
|
66
|
+
* @param id - the gateway-advertised model id.
|
|
67
|
+
*/
|
|
68
|
+
export function variantLabel(id) {
|
|
69
|
+
const m = VARIANT_SUFFIX_RE.exec(String(id ?? ""));
|
|
70
|
+
if (m === null) return "";
|
|
71
|
+
const tag = m[1].replace(/-/g, "").toLowerCase();
|
|
72
|
+
return tag.charAt(0).toUpperCase() + tag.slice(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Providers whose entries describe the model's native wire format. */
|
|
76
|
+
const FIRST_PARTY = new Set([
|
|
77
|
+
"deepseek", "minimax", "minimax-cn", "zai", "zai-coding-cn",
|
|
78
|
+
"moonshotai", "moonshotai-cn", "kimi-coding",
|
|
79
|
+
"anthropic", "openai", "openai-codex", "google", "google-vertex", "xai",
|
|
80
|
+
"mistral", "groq", "cerebras", "nvidia", "together", "xiaomi", "ant-ling",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** Preferred first-party provider per model family (the lab's native wire). */
|
|
84
|
+
const FAMILY_PROVIDER = [
|
|
85
|
+
[/deepseek/, "deepseek"],
|
|
86
|
+
[/glm|zai/, "zai"],
|
|
87
|
+
[/kimi/, "moonshotai"],
|
|
88
|
+
[/minimax/, "minimax"],
|
|
89
|
+
[/gpt|chatgpt|\bo[1-9]/, "openai"],
|
|
90
|
+
[/claude|anthropic/, "anthropic"],
|
|
91
|
+
[/gemini|google/, "google"],
|
|
92
|
+
[/grok/, "xai"],
|
|
93
|
+
[/mistral|codestral/, "mistral"],
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pick the most authoritative entry among several provider copies of one
|
|
98
|
+
* model: the family's first-party provider wins, then any first-party entry,
|
|
99
|
+
* then the original (stable) order.
|
|
100
|
+
*/
|
|
101
|
+
function preferEntry(entries, id) {
|
|
102
|
+
if (entries.length === 1) return entries[0];
|
|
103
|
+
const hint = FAMILY_PROVIDER.find(([re]) => re.test(id))?.[1];
|
|
104
|
+
if (hint !== undefined) {
|
|
105
|
+
const hit = entries.find((e) => e.provider === hint);
|
|
106
|
+
if (hit !== undefined) return hit;
|
|
107
|
+
}
|
|
108
|
+
const firstParty = entries.find((e) => FIRST_PARTY.has(e.provider));
|
|
109
|
+
return firstParty ?? entries[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One shared base-id → pi-ai model entry index over all built-in providers. */
|
|
113
|
+
let piIndex;
|
|
114
|
+
function indexPiModels() {
|
|
115
|
+
if (piIndex !== undefined) return piIndex;
|
|
116
|
+
const map = new Map();
|
|
117
|
+
for (const provider of getBuiltinProviders()) {
|
|
118
|
+
for (const model of getBuiltinModels(provider)) {
|
|
119
|
+
const key = baseModelId(model.id);
|
|
120
|
+
if (key.length === 0) continue;
|
|
121
|
+
if (!map.has(key)) map.set(key, []);
|
|
122
|
+
map.get(key).push(model);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
piIndex = map;
|
|
126
|
+
return map;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Look up a gateway model id in the harness's builtin pi-ai catalog, resolving
|
|
131
|
+
* namespace/suffix variants and provider copies to the authoritative entry.
|
|
132
|
+
*/
|
|
133
|
+
export function findPiModel(id) {
|
|
134
|
+
const key = baseModelId(id);
|
|
135
|
+
if (key.length === 0) return undefined;
|
|
136
|
+
const entries = indexPiModels().get(key);
|
|
137
|
+
return entries === undefined ? undefined : preferEntry(entries, key);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The reasoning family of one model, used for fallback and MiniMax handling.
|
|
142
|
+
* The family comes from models.dev metadata when present, else from the model
|
|
143
|
+
* id prefix, else from the pi-ai provider.
|
|
144
|
+
*/
|
|
145
|
+
export function reasoningFamilyOf(entry, model, piModel) {
|
|
146
|
+
const id = String(model ?? piModel?.id ?? entry?.id ?? "").toLowerCase();
|
|
147
|
+
const family = typeof entry?.family === "string" ? entry.family.toLowerCase() : "";
|
|
148
|
+
const provider = typeof piModel?.provider === "string" ? piModel.provider.toLowerCase() : "";
|
|
149
|
+
if (family.startsWith("deepseek") || id.startsWith("deepseek") || provider === "deepseek") return "deepseek";
|
|
150
|
+
if (family.startsWith("minimax") || id.startsWith("minimax") || provider.startsWith("minimax")) return "minimax";
|
|
151
|
+
return "generic";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function capitalize(level) {
|
|
155
|
+
return level.length === 0 ? level : `${level.charAt(0).toUpperCase()}${level.slice(1)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const effort = (level) => ({ id: ReasoningEffortId(level), name: capitalize(level) });
|
|
159
|
+
|
|
160
|
+
/** The normalized thinking-level vocabulary ReasoningEffortId accepts. */
|
|
161
|
+
const KNOWN_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build a selectable effort set from an explicit user override (the per-model
|
|
165
|
+
* `reasoningLevels` array). Unknown / duplicate entries are dropped; an empty
|
|
166
|
+
* result means the caller should fall back to catalog inference.
|
|
167
|
+
*/
|
|
168
|
+
export function effortsFromLevels(levels) {
|
|
169
|
+
if (!Array.isArray(levels)) return [];
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
const clean = [];
|
|
172
|
+
for (const raw of levels) {
|
|
173
|
+
const level = String(raw ?? "").toLowerCase();
|
|
174
|
+
if (!KNOWN_LEVELS.has(level) || seen.has(level)) continue;
|
|
175
|
+
seen.add(level);
|
|
176
|
+
clean.push(effort(level));
|
|
177
|
+
}
|
|
178
|
+
return clean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Native DeepSeek reasoning set (off / high / max). */
|
|
182
|
+
const DEEPSEEK_EFFORTS = [effort("off"), effort("high"), effort("max")];
|
|
183
|
+
/** MiniMax has a two-state wire (`adaptive|disabled`): off + one "on" level. */
|
|
184
|
+
const MINIMAX_EFFORTS = [effort("off"), effort("high")];
|
|
185
|
+
/** OpenAI-standard generic set, safe for unknown reasoning models. */
|
|
186
|
+
const GENERIC_EFFORTS = [effort("off"), effort("low"), effort("medium"), effort("high")];
|
|
187
|
+
/** Full normalized set, offered only under the explicit `extended` opt-in. */
|
|
188
|
+
const EXTENDED_EFFORTS = [effort("off"), effort("minimal"), effort("low"), effort("medium"), effort("high"), effort("xhigh"), effort("max")];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The selectable reasoning efforts for one model.
|
|
192
|
+
*
|
|
193
|
+
* With a pi-ai catalog hit the level set is pi-ai's own answer for that exact
|
|
194
|
+
* model (including its per-level support matrix), except MiniMax, which is
|
|
195
|
+
* collapsed to its real two-state wire. Without a hit, models.dev inference
|
|
196
|
+
* falls back to family heuristics; `extended` widens the generic fallback to
|
|
197
|
+
* the full normalized set for explicit user choice.
|
|
198
|
+
*/
|
|
199
|
+
export function thinkingEffortsOf(entry, piModel, opts = {}) {
|
|
200
|
+
const extended = opts.extended === true;
|
|
201
|
+
if (piModel !== undefined) {
|
|
202
|
+
if (piModel.reasoning !== true) return undefined;
|
|
203
|
+
if (reasoningFamilyOf(entry, undefined, piModel) === "minimax") return { efforts: MINIMAX_EFFORTS };
|
|
204
|
+
const levels = getSupportedThinkingLevels(piModel);
|
|
205
|
+
if (levels.length === 0 || levels.length === 1 && levels[0] === "off") return undefined;
|
|
206
|
+
return { efforts: levels.map((level) => ({ id: ReasoningEffortId(level), name: capitalize(level) })) };
|
|
207
|
+
}
|
|
208
|
+
if (entry === undefined || entry.reasoning !== true) return undefined;
|
|
209
|
+
switch (reasoningFamilyOf(entry)) {
|
|
210
|
+
case "deepseek":
|
|
211
|
+
return { efforts: DEEPSEEK_EFFORTS };
|
|
212
|
+
case "minimax":
|
|
213
|
+
return { efforts: MINIMAX_EFFORTS };
|
|
214
|
+
default:
|
|
215
|
+
return { efforts: extended ? EXTENDED_EFFORTS : GENERIC_EFFORTS };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-gateway-provider",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"lib",
|
|
15
|
+
"cordis.patch.yml"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"link": "bash scripts/link.sh",
|
|
19
|
+
"smoke": "node test/smoke.mjs",
|
|
20
|
+
"test:client": "node test/client-render.mjs",
|
|
21
|
+
"test:urls": "node test/protocol-urls.mjs"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"deepseek-harness",
|
|
25
|
+
"dsh",
|
|
26
|
+
"cordis",
|
|
27
|
+
"llm",
|
|
28
|
+
"llm-provider",
|
|
29
|
+
"provider",
|
|
30
|
+
"newapi",
|
|
31
|
+
"new-api",
|
|
32
|
+
"openai",
|
|
33
|
+
"anthropic",
|
|
34
|
+
"gemini",
|
|
35
|
+
"models.dev",
|
|
36
|
+
"dsh-plugin",
|
|
37
|
+
"pi-ai"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dsh": {
|
|
41
|
+
"client": {
|
|
42
|
+
"inject": [
|
|
43
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
44
|
+
"@deepseek-ai/dsh-client-web-react"
|
|
45
|
+
],
|
|
46
|
+
"platform": "web"
|
|
47
|
+
},
|
|
48
|
+
"bundle": {
|
|
49
|
+
"patch": "./cordis.patch.yml"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
55
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
62
|
+
"@earendil-works/pi-ai": "^0.82.1"
|
|
63
|
+
}
|
|
64
|
+
}
|