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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }