dsh-gateway-provider 1.1.1 → 1.1.3
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 +46 -3
- package/docs/README.zh.md +44 -3
- package/index.js +13 -2
- package/lib/catalog.js +7 -6
- package/lib/client.js +13 -7
- package/lib/pi-provider.js +17 -4
- package/lib/protocols.js +22 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -66,6 +66,48 @@ it should answer `✓ Connected — N models`. Not using the public newapi
|
|
|
66
66
|
cloud? Set **Base URL** on the card (or `baseURL` in config) to your own
|
|
67
67
|
gateway address first.
|
|
68
68
|
|
|
69
|
+
## Terminal / headless
|
|
70
|
+
|
|
71
|
+
The host half of this plugin (the `llm-newapi` provider route) works in any
|
|
72
|
+
profile — including terminal / TUI setups such as
|
|
73
|
+
[dsh-tui](https://github.com/ccch1mneyyy/dsh-TUI). **Settings → Gateway
|
|
74
|
+
Models is web-only**; without the web UI you configure the gateway by
|
|
75
|
+
overriding the bundle's `llm-newapi` entry in the profile patch. A patch
|
|
76
|
+
layer replaces `config` wholesale, so write the whole block out:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# ~/.dsh/profiles/<name>/cordis.patch.yml
|
|
80
|
+
# Override by id — do not insert a second llm-newapi row (duplicate = loader error).
|
|
81
|
+
- id: llm-newapi
|
|
82
|
+
config:
|
|
83
|
+
label: My Gateway
|
|
84
|
+
baseURL: https://gw.example.com
|
|
85
|
+
apiKeyEnv: GW_API_KEY
|
|
86
|
+
models:
|
|
87
|
+
- id: some-model
|
|
88
|
+
protocol: gemini
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Store the key under the name in `apiKeyEnv` (same rules as Install):
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
echo "GW_API_KEY: sk-REPLACE_WITH_YOUR_KEY" >> ~/.dsh/.credentials.yaml
|
|
95
|
+
# or: export GW_API_KEY=sk-REPLACE_WITH_YOUR_KEY
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Install into a non-web profile the same way — swap the profile name:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
dsh plugin --profile dsh-tui add dsh-gateway-provider
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
After a restart the gateway models should appear in the TUI model picker.
|
|
105
|
+
Connection Test / Sync / per-model editing still need the web settings page
|
|
106
|
+
(or hand-edits of the YAML above). A future in-terminal settings surface
|
|
107
|
+
(`tuiSettingsSections`, see [#5](https://github.com/Luck9Star/dsh-gateway-provider/issues/5))
|
|
108
|
+
may cover the scalar fields; until then the YAML override is the supported
|
|
109
|
+
path.
|
|
110
|
+
|
|
69
111
|
## Daily use
|
|
70
112
|
|
|
71
113
|
Everything lives in **Settings → Gateway Models**:
|
|
@@ -92,7 +134,7 @@ the first start where the plugin loads. The frequently used ones:
|
|
|
92
134
|
|
|
93
135
|
| Key | Default | Meaning |
|
|
94
136
|
| --- | --- | --- |
|
|
95
|
-
| `baseURL` | `https://api.newapi.ai` | Your gateway's base URL. Env fallbacks: `NEWAPI_BASE_URL`, `NEWAPI_API_URL`. |
|
|
137
|
+
| `baseURL` | `https://api.newapi.ai` | Your gateway's base URL. Env fallbacks: `NEWAPI_BASE_URL`, `NEWAPI_API_URL`. A trailing `/v1` or `/v1beta` is tolerated (stripped for discovery); Gemini requests always go to `{base}/v1beta/models/…`. |
|
|
96
138
|
| `apiKeyEnv` | `NEWAPI_API_KEY` | Which env/credential variable holds the key. |
|
|
97
139
|
| `label` | `NewAPI` | Route label shown in the picker. |
|
|
98
140
|
| `flavor` | `newapi` | Template label only (`newapi` / `litellm` / `higress` / `openai-compatible` / `custom`). |
|
|
@@ -103,7 +145,7 @@ the first start where the plugin loads. The frequently used ones:
|
|
|
103
145
|
| `sortModelsByRelease` | `true` | Newest models first. |
|
|
104
146
|
| `catalogMode` | `auto` | `v1` (`/v1/models` only) / `management` (newapi user API) / `auto`. |
|
|
105
147
|
| `endpointPriority` | responses → anthropic → openai → gemini | Which protocol to prefer when a model supports several. |
|
|
106
|
-
| `openaiURL` / `responsesURL` / `anthropicURL` | — | Fully-custom gateways only: per-protocol endpoint URLs; unset = that protocol off. |
|
|
148
|
+
| `openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL` | — | Fully-custom gateways only: per-protocol endpoint URLs; unset = that protocol off. `geminiURL` may be a bare host, `…/v1beta`, or `…/v1beta/models` — the `/v1beta` segment is ensured (pi-ai expects it in the base). |
|
|
107
149
|
| `maxTokens` / `defaultContextWindow` | `32768` / `128000` | Fallbacks when models.dev has no data. |
|
|
108
150
|
| `streamIdleTimeoutMs` | `600000` | Idle timeout while streaming. |
|
|
109
151
|
| `headers` | — | Extra HTTP headers sent to the gateway. |
|
|
@@ -116,7 +158,8 @@ the first start where the plugin loads. The frequently used ones:
|
|
|
116
158
|
| `401` / auth errors on every request | Key missing or wrong: check the badge in Settings → Gateway Models, or `NEWAPI_API_KEY` in `~/.dsh/.credentials.yaml`. |
|
|
117
159
|
| A model's context window looks wrong | models.dev had no match. Edit the model on the settings page (or a `models:` override). |
|
|
118
160
|
| Wrong format answers / tool calls flaky for one model | That model is routed over a protocol it handles poorly. Pin `protocol` on the model (`openai`, `openai-response`, `anthropic`, `gemini`). |
|
|
119
|
-
| Custom gateway with separate endpoints | Use `flavor: "custom"` and set `openaiURL` / `responsesURL` / `anthropicURL` explicitly. |
|
|
161
|
+
| Custom gateway with separate endpoints | Use `flavor: "custom"` and set `openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL` explicitly. |
|
|
162
|
+
| Gemini model returns HTML / `Unexpected token '<'` (issue #4) | The gateway serves Gemini under `/v1beta`; fixed in 1.1.2 — a plain `baseURL` now sends Gemini to `{base}/v1beta/models/…` (a `baseURL` ending in `/v1beta` also works). No older-version workaround exists; update the plugin. |
|
|
120
163
|
|
|
121
164
|
## How it works (one minute version)
|
|
122
165
|
|
package/docs/README.zh.md
CHANGED
|
@@ -42,6 +42,46 @@ dsh --profile web
|
|
|
42
42
|
|
|
43
43
|
**预期结果:** 模型选择器多出一条 "NewAPI" 路由,列出网关的对话模型,最新在前。在网关卡片上点 **Test** —— 应显示 `✓ Connected — N models`。用的不是 newapi 公有云?先在卡片上改 **Base URL**(或配置里的 `baseURL`)指向你自己的网关地址。
|
|
44
44
|
|
|
45
|
+
## 终端 / 无界面(headless)
|
|
46
|
+
|
|
47
|
+
本插件的 host 半边(`llm-newapi` provider 路由)在任意 profile 都能用 ——
|
|
48
|
+
包括终端 / TUI(例如 [dsh-tui](https://github.com/ccch1mneyyy/dsh-TUI))。
|
|
49
|
+
**Settings → Gateway Models 仅限 web**;没有 web UI 时,通过在 profile
|
|
50
|
+
patch 里覆盖 bundle 插入的 `llm-newapi` 条目来配置网关。patch 层会整段
|
|
51
|
+
替换 `config`,所以要把整个块写全:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
# ~/.dsh/profiles/<name>/cordis.patch.yml
|
|
55
|
+
# 按 id 覆盖 —— 不要再 insert 第二条 llm-newapi(重复行 = 加载器报错)。
|
|
56
|
+
- id: llm-newapi
|
|
57
|
+
config:
|
|
58
|
+
label: My Gateway
|
|
59
|
+
baseURL: https://gw.example.com
|
|
60
|
+
apiKeyEnv: GW_API_KEY
|
|
61
|
+
models:
|
|
62
|
+
- id: some-model
|
|
63
|
+
protocol: gemini
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
密钥按 `apiKeyEnv` 里的名字存放(规则与「安装」一节相同):
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
echo "GW_API_KEY: sk-REPLACE_WITH_YOUR_KEY" >> ~/.dsh/.credentials.yaml
|
|
70
|
+
# 或: export GW_API_KEY=sk-REPLACE_WITH_YOUR_KEY
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
装到非 web profile 时同样操作,换成对应 profile 名即可:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
dsh plugin --profile dsh-tui add dsh-gateway-provider
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
重启后,网关模型会出现在 TUI 的模型选择器里。连接测试 / 同步 /
|
|
80
|
+
按模型编辑仍需要 web 设置页(或手工改上面的 YAML)。之后可能会加终端内
|
|
81
|
+
设置面(`tuiSettingsSections`,见
|
|
82
|
+
[#5](https://github.com/Luck9Star/dsh-gateway-provider/issues/5))覆盖标量
|
|
83
|
+
字段;在那之前,YAML 覆盖就是受支持的用法。
|
|
84
|
+
|
|
45
85
|
## 日常使用
|
|
46
86
|
|
|
47
87
|
一切都在 **Settings → Gateway Models**:
|
|
@@ -56,7 +96,7 @@ dsh --profile web
|
|
|
56
96
|
|
|
57
97
|
| 键 | 默认值 | 含义 |
|
|
58
98
|
| --- | --- | --- |
|
|
59
|
-
| `baseURL` | `https://api.newapi.ai` | 网关 base URL。环境变量兜底:`NEWAPI_BASE_URL`、`NEWAPI_API_URL
|
|
99
|
+
| `baseURL` | `https://api.newapi.ai` | 网关 base URL。环境变量兜底:`NEWAPI_BASE_URL`、`NEWAPI_API_URL`。结尾带 `/v1` 或 `/v1beta` 均可(发现时会剥掉);Gemini 请求固定发往 `{base}/v1beta/models/…`。 |
|
|
60
100
|
| `apiKeyEnv` | `NEWAPI_API_KEY` | key 存在哪个环境/凭据变量里。 |
|
|
61
101
|
| `label` | `NewAPI` | 选择器里显示的路由名。 |
|
|
62
102
|
| `flavor` | `newapi` | 仅作模板标签(`newapi` / `litellm` / `higress` / `openai-compatible` / `custom`)。 |
|
|
@@ -67,7 +107,7 @@ dsh --profile web
|
|
|
67
107
|
| `sortModelsByRelease` | `true` | 最新模型排前面。 |
|
|
68
108
|
| `catalogMode` | `auto` | `v1`(只用 `/v1/models`)/ `management`(newapi 用户 API)/ `auto`。 |
|
|
69
109
|
| `endpointPriority` | responses → anthropic → openai → gemini | 模型支持多种协议时的优先序。 |
|
|
70
|
-
| `openaiURL` / `responsesURL` / `anthropicURL` | — | 仅完全自定义网关:按协议分别填端点 URL;不填 =
|
|
110
|
+
| `openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL` | — | 仅完全自定义网关:按协议分别填端点 URL;不填 = 该协议关闭。`geminiURL` 可填裸主机名、`…/v1beta` 或 `…/v1beta/models` —— `/v1beta` 段会自动确保(pi-ai 要求它含在 base 里)。 |
|
|
71
111
|
| `maxTokens` / `defaultContextWindow` | `32768` / `128000` | models.dev 无数据时的兜底。 |
|
|
72
112
|
| `streamIdleTimeoutMs` | `600000` | 流式空闲超时。 |
|
|
73
113
|
| `headers` | — | 发往网关的额外 HTTP 头。 |
|
|
@@ -80,7 +120,8 @@ dsh --profile web
|
|
|
80
120
|
| 每次请求都 `401` / 鉴权错误 | key 缺失或不对:看 Settings → Gateway Models 的徽标,或 `~/.dsh/.credentials.yaml` 里的 `NEWAPI_API_KEY`。 |
|
|
81
121
|
| 某模型的上下文窗口看着不对 | models.dev 没匹配上。在设置页编辑该模型(或写 `models:` 覆盖)。 |
|
|
82
122
|
| 单个模型工具调用不稳、格式怪 | 该模型被路由到了它处理不好的协议。在模型上钉死 `protocol`(`openai`、`openai-response`、`anthropic`、`gemini`)。 |
|
|
83
|
-
| 自定义网关各协议端点分开 | 用 `flavor: "custom"`,显式填 `openaiURL` / `responsesURL` / `anthropicURL`。 |
|
|
123
|
+
| 自定义网关各协议端点分开 | 用 `flavor: "custom"`,显式填 `openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL`。 |
|
|
124
|
+
| Gemini 模型返回 HTML / `Unexpected token '<'`(issue #4) | 网关把 Gemini 挂在 `/v1beta` 下;1.1.2 已修复 —— 普通 `baseURL` 也会把 Gemini 请求发到 `{base}/v1beta/models/…`(`baseURL` 以 `/v1beta` 结尾同样可用)。旧版本没有任何绕过手段,请升级插件。 |
|
|
84
125
|
|
|
85
126
|
## 工作原理(一分钟版)
|
|
86
127
|
|
package/index.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Registers one or more `gateway:*` provider routes on `ctx.llm`, each backed
|
|
5
5
|
* by an OpenAI-compatible `/v1/models` gateway (newapi, LiteLLM, Higress, or
|
|
6
6
|
* any generic gateway), or by a fully-custom gateway whose per-protocol
|
|
7
|
-
* endpoint URLs (`openaiURL` / `responsesURL` / `anthropicURL`
|
|
7
|
+
* endpoint URLs (`openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL`)
|
|
8
|
+
* are written
|
|
8
9
|
* out in full. Backwards-compatible with the original single-connection
|
|
9
10
|
* `newapi` route: the legacy flat fields (`baseURL`, `apiKeyEnv`, …) build a
|
|
10
11
|
* default gateway, while the `gateways` array adds more.
|
|
@@ -121,6 +122,12 @@ const GatewaySchema = z.object({
|
|
|
121
122
|
responsesURL: coercedString(),
|
|
122
123
|
/** Full Anthropic messages endpoint URL (custom template). */
|
|
123
124
|
anthropicURL: coercedString(),
|
|
125
|
+
/**
|
|
126
|
+
* Gemini native endpoint base (custom template): a bare host, a base
|
|
127
|
+
* ending in `/v1beta`, or one with a trailing `/models`. The `/v1beta`
|
|
128
|
+
* segment is ensured — pi-ai's Gemini client expects it in the base.
|
|
129
|
+
*/
|
|
130
|
+
geminiURL: coercedString(),
|
|
124
131
|
/** Environment-variable name (credential ref) holding the API key. */
|
|
125
132
|
apiKeyEnv: coercedString().role("credential-ref"),
|
|
126
133
|
/** Gateway type label (newapi/litellm/higress/openai-compatible/custom). */
|
|
@@ -149,6 +156,8 @@ export const Config = z.object({
|
|
|
149
156
|
responsesURL: coercedString(),
|
|
150
157
|
/** Full Anthropic messages endpoint URL (custom template for the default gateway). */
|
|
151
158
|
anthropicURL: coercedString(),
|
|
159
|
+
/** Gemini native endpoint base (custom template for the default gateway). */
|
|
160
|
+
geminiURL: coercedString(),
|
|
152
161
|
/** Gateway type label shown in the UI (newapi/litellm/higress/openai-compatible/custom). */
|
|
153
162
|
flavor: z.union(FLAVORS).default("newapi"),
|
|
154
163
|
/** models.dev catalog URL (any fetch-able URL; file: works for offline mirrors). */
|
|
@@ -193,7 +202,8 @@ export const Config = z.object({
|
|
|
193
202
|
/**
|
|
194
203
|
* Normalize one gateway entry into the connection facts the adapter needs.
|
|
195
204
|
* Per-gateway fields fall back to the shared defaults. Protocol URL fields
|
|
196
|
-
* (`openaiURL` / `responsesURL` / `anthropicURL`, the "custom"
|
|
205
|
+
* (`openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL`, the "custom"
|
|
206
|
+
* template)
|
|
197
207
|
* redefine the gateway's protocol surface: each configured URL becomes the
|
|
198
208
|
* exact SDK base for its wire protocol, unconfigured protocols are
|
|
199
209
|
* unavailable, and the plain `baseURL` is no longer inherited from the
|
|
@@ -290,6 +300,7 @@ export function resolveGateways(config, environment) {
|
|
|
290
300
|
openaiURL: config.openaiURL,
|
|
291
301
|
responsesURL: config.responsesURL,
|
|
292
302
|
anthropicURL: config.anthropicURL,
|
|
303
|
+
geminiURL: config.geminiURL,
|
|
293
304
|
}, defaults, PROVIDER, defaultLabel) },
|
|
294
305
|
];
|
|
295
306
|
// Additional gateways from the array.
|
package/lib/catalog.js
CHANGED
|
@@ -122,15 +122,16 @@ async function fetchManagementModels(baseURL, apiKey, userId, signal) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Strip trailing slashes and a trailing version segment (`/v1`, `/v2`,
|
|
126
|
-
* from a gateway base URL. The catalog always
|
|
127
|
-
* (`/v1/models`, `/api/user/models`), so a
|
|
128
|
-
* carries
|
|
129
|
-
*
|
|
125
|
+
* Strip trailing slashes and a trailing version segment (`/v1`, `/v2`, and
|
|
126
|
+
* the Gemini-style `/v1beta`) from a gateway base URL. The catalog always
|
|
127
|
+
* appends its own full paths (`/v1/models`, `/api/user/models`), so a
|
|
128
|
+
* user-supplied base that already carries a version (the SDK conventions,
|
|
129
|
+
* which the chat path also accepts) must not leak through as
|
|
130
|
+
* `/v1/v1/models` or `/v1beta/v1/models`. Mirrors the derivation the custom
|
|
130
131
|
* (protocol-URL) template performs in `protocols.js`.
|
|
131
132
|
*/
|
|
132
133
|
function stripVersion(baseURL) {
|
|
133
|
-
return String(baseURL ?? "").replace(/\/+$/, "").replace(/\/v\d
|
|
134
|
+
return String(baseURL ?? "").replace(/\/+$/, "").replace(/\/v\d+(?:beta)?$/i, "");
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/**
|
package/lib/client.js
CHANGED
|
@@ -26,7 +26,7 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
var CATALOG_MODES = ["auto", "v1", "management"];
|
|
27
27
|
var PROTOCOLS = ["openai", "openai-response", "anthropic", "gemini"];
|
|
28
28
|
/** Gateway template fields (suffix "URL") → card protocol tag. */
|
|
29
|
-
var PROTOCOL_URL_KEYS = ["openaiURL", "responsesURL", "anthropicURL"];
|
|
29
|
+
var PROTOCOL_URL_KEYS = ["openaiURL", "responsesURL", "anthropicURL", "geminiURL"];
|
|
30
30
|
|
|
31
31
|
// ---- i18n (manual zh/en switch; no locale registration needed) ----
|
|
32
32
|
var ZH = (typeof navigator !== "undefined" && navigator.language && navigator.language.startsWith("zh"));
|
|
@@ -65,19 +65,21 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
tplHintLitellm: "LiteLLM Proxy:/v1/chat/completions 与 /v1/responses,经 /v1/models 自动发现",
|
|
66
66
|
tplHintHigress: "Higress AI 网关:OpenAI 格式为主,新版本同端口支持 Anthropic /v1/messages",
|
|
67
67
|
tplHintOpenaiCompatible: "任意 OpenAI 兼容网关:填一个基础地址,各协议路径自动派生",
|
|
68
|
-
tplHintCustom: "
|
|
68
|
+
tplHintCustom: "各协议地址分别填写(Gemini 可只填主机名,自动补 /v1beta);留空的协议不启用,模型仅以已启用协议请求",
|
|
69
69
|
tplNewapi: "NewAPI", tplLitellm: "LiteLLM", tplHigress: "Higress",
|
|
70
70
|
tplOpenaiCompatible: "OpenAI 兼容", tplCustom: "完全自定义",
|
|
71
|
-
openaiURL: "OpenAI 兼容地址", responsesURL: "Responses 地址", anthropicURL: "Anthropic 地址",
|
|
71
|
+
openaiURL: "OpenAI 兼容地址", responsesURL: "Responses 地址", anthropicURL: "Anthropic 地址", geminiURL: "Gemini 地址",
|
|
72
72
|
openaiURLHint: "完整地址,以 /chat/completions 结尾(也可只填到 /v1)",
|
|
73
73
|
responsesURLHint: "完整地址,以 /responses 结尾(也可只填到 /v1)",
|
|
74
74
|
anthropicURLHint: "完整地址,以 /v1/messages 结尾",
|
|
75
|
+
geminiURLHint: "以 /v1beta 结尾(也可只填主机名,自动补 /v1beta)",
|
|
75
76
|
customNeedOne: "至少填写一个协议地址",
|
|
76
77
|
customURLPlaceholder: "https://gw.example.com/v1/chat/completions",
|
|
77
78
|
responsesURLPlaceholder: "https://gw.example.com/v1/responses",
|
|
78
79
|
anthropicURLPlaceholder: "https://gw.example.com/v1/messages",
|
|
80
|
+
geminiURLPlaceholder: "https://gw.example.com/v1beta",
|
|
79
81
|
catalogHint: "auto:优先 /v1/models,鉴权失败时回退管理 API",
|
|
80
|
-
baseURLClearHint: "留空则回退到环境变量 NEWAPI_BASE_URL 或官方云端地址;结尾带不带 /v1 均可",
|
|
82
|
+
baseURLClearHint: "留空则回退到环境变量 NEWAPI_BASE_URL 或官方云端地址;结尾带不带 /v1 或 /v1beta 均可",
|
|
81
83
|
labelClearHint: "留空则回退为 gateway:<id>",
|
|
82
84
|
keyEnvHint: "留空则继承默认网关的 Key(NEWAPI_API_KEY)",
|
|
83
85
|
keyKeepHint: "留空保持不变;填写则覆盖存储的 Key",
|
|
@@ -125,19 +127,21 @@ window.__ModuleLoader__.load({
|
|
|
125
127
|
tplHintLitellm: "LiteLLM proxy: /v1/chat/completions and /v1/responses; discovery via /v1/models",
|
|
126
128
|
tplHintHigress: "Higress AI gateway: OpenAI format; newer versions also serve Anthropic /v1/messages",
|
|
127
129
|
tplHintOpenaiCompatible: "Any OpenAI-compatible gateway: one base address, protocol paths derived automatically",
|
|
128
|
-
tplHintCustom: "Enter each protocol's
|
|
130
|
+
tplHintCustom: "Enter each protocol's URL (Gemini accepts a bare host; /v1beta is added automatically); empty protocols stay disabled and models use enabled protocols only",
|
|
129
131
|
tplNewapi: "NewAPI", tplLitellm: "LiteLLM", tplHigress: "Higress",
|
|
130
132
|
tplOpenaiCompatible: "OpenAI-compatible", tplCustom: "Fully custom",
|
|
131
|
-
openaiURL: "OpenAI-compatible URL", responsesURL: "Responses URL", anthropicURL: "Anthropic URL",
|
|
133
|
+
openaiURL: "OpenAI-compatible URL", responsesURL: "Responses URL", anthropicURL: "Anthropic URL", geminiURL: "Gemini URL",
|
|
132
134
|
openaiURLHint: "Full address ending in /chat/completions (a /v1 base also works)",
|
|
133
135
|
responsesURLHint: "Full address ending in /responses (a /v1 base also works)",
|
|
134
136
|
anthropicURLHint: "Full address ending in /v1/messages",
|
|
137
|
+
geminiURLHint: "Ending in /v1beta (a bare host also works; /v1beta is added)",
|
|
135
138
|
customNeedOne: "Fill at least one protocol URL",
|
|
136
139
|
customURLPlaceholder: "https://gw.example.com/v1/chat/completions",
|
|
137
140
|
responsesURLPlaceholder: "https://gw.example.com/v1/responses",
|
|
138
141
|
anthropicURLPlaceholder: "https://gw.example.com/v1/messages",
|
|
142
|
+
geminiURLPlaceholder: "https://gw.example.com/v1beta",
|
|
139
143
|
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; a trailing /v1 is optional",
|
|
144
|
+
baseURLClearHint: "Empty falls back to NEWAPI_BASE_URL or the public cloud default; a trailing /v1 or /v1beta is optional",
|
|
141
145
|
labelClearHint: "Empty falls back to gateway:<id>",
|
|
142
146
|
keyEnvHint: "Empty inherits the default gateway's key (NEWAPI_API_KEY)",
|
|
143
147
|
keyKeepHint: "Empty keeps the stored key; type to overwrite",
|
|
@@ -166,6 +170,7 @@ window.__ModuleLoader__.load({
|
|
|
166
170
|
openaiURL: { label: T.openaiURL, hint: T.openaiURLHint, ph: T.customURLPlaceholder, tag: "openai" },
|
|
167
171
|
responsesURL: { label: T.responsesURL, hint: T.responsesURLHint, ph: T.responsesURLPlaceholder, tag: "responses" },
|
|
168
172
|
anthropicURL: { label: T.anthropicURL, hint: T.anthropicURLHint, ph: T.anthropicURLPlaceholder, tag: "anthropic" },
|
|
173
|
+
geminiURL: { label: T.geminiURL, hint: T.geminiURLHint, ph: T.geminiURLPlaceholder, tag: "gemini" },
|
|
169
174
|
};
|
|
170
175
|
|
|
171
176
|
/** Protocol tags a gateway card shows (configured URL fields only). */
|
|
@@ -346,6 +351,7 @@ window.__ModuleLoader__.load({
|
|
|
346
351
|
openaiURL: protoOf(isDefault ? props.snapshot : gw, "openaiURL"),
|
|
347
352
|
responsesURL: protoOf(isDefault ? props.snapshot : gw, "responsesURL"),
|
|
348
353
|
anthropicURL: protoOf(isDefault ? props.snapshot : gw, "anthropicURL"),
|
|
354
|
+
geminiURL: protoOf(isDefault ? props.snapshot : gw, "geminiURL"),
|
|
349
355
|
apiKey: "",
|
|
350
356
|
};
|
|
351
357
|
var _v = React.useState(initial); var values = _v[0]; var setValues = _v[1];
|
package/lib/pi-provider.js
CHANGED
|
@@ -52,17 +52,30 @@ const PROTOCOLS = {
|
|
|
52
52
|
* `/responses`. A gateway baseURL without `/v1` makes the SDK request
|
|
53
53
|
* `{base}/responses`, which the gateway answers with its HTML UI (HTTP 200,
|
|
54
54
|
* content-type text/html) instead of an SSE stream, so the SDK receives zero
|
|
55
|
-
* events.
|
|
56
|
-
*
|
|
55
|
+
* events.
|
|
56
|
+
*
|
|
57
|
+
* pi-ai's Gemini client follows the same convention with `/v1beta`: whenever
|
|
58
|
+
* a model carries a baseUrl it sets `httpOptions.apiVersion = ""` (its own
|
|
59
|
+
* Google provider ships `…googleapis.com/v1beta` as the base), so the version
|
|
60
|
+
* segment must come from the base itself. Without it the SDK requests
|
|
61
|
+
* `{base}/models/{model}:generateContent`, which gateways serving Gemini
|
|
62
|
+
* under `/v1beta` (e.g. new-api) answer with their HTML UI fallback.
|
|
63
|
+
* The Anthropic SDK builds its own full path, so it needs no version appended.
|
|
57
64
|
* @param baseURL - the gateway base URL as configured.
|
|
58
65
|
* @param api - the pi-ai API identifier the model will use.
|
|
59
|
-
* @returns the baseURL the
|
|
66
|
+
* @returns the baseURL the SDK should point at.
|
|
60
67
|
*/
|
|
61
|
-
function sdkBaseURL(baseURL, api) {
|
|
68
|
+
export function sdkBaseURL(baseURL, api) {
|
|
62
69
|
const trimmed = String(baseURL ?? "").replace(/\/+$/, "");
|
|
63
70
|
if (api === "openai-completions" || api === "openai-responses") {
|
|
64
71
|
if (!/\/v\d+$/.test(trimmed)) return `${trimmed}/v1`;
|
|
65
72
|
}
|
|
73
|
+
// pi-ai pins apiVersion = "" when a baseUrl is set, so the version segment
|
|
74
|
+
// must already live in the base (issue #4). Case-insensitive, matching the
|
|
75
|
+
// anthropic branch's tolerance in protocols.js.
|
|
76
|
+
if (api === "google-generative-ai" && !/\/v\d+beta$/i.test(trimmed)) {
|
|
77
|
+
return `${trimmed}/v1beta`;
|
|
78
|
+
}
|
|
66
79
|
return trimmed;
|
|
67
80
|
}
|
|
68
81
|
|
package/lib/protocols.js
CHANGED
|
@@ -24,6 +24,10 @@ const ENDPOINT_SUFFIXES = {
|
|
|
24
24
|
"openai-completions": "/chat/completions",
|
|
25
25
|
"openai-responses": "/responses",
|
|
26
26
|
"anthropic-messages": "/v1/messages",
|
|
27
|
+
// Gemini has no fixed endpoint suffix (the path embeds the model id:
|
|
28
|
+
// `{base}/v1beta/models/{model}:generateContent`), so only a trailing
|
|
29
|
+
// `/models` — copied from the docs' list URL — is tolerated.
|
|
30
|
+
"google-generative-ai": "/models",
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
/** Trim a configured URL and strip one known endpoint suffix if present. */
|
|
@@ -36,14 +40,16 @@ function stripSuffix(url, suffix) {
|
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
42
|
* Derive the per-protocol facts from fully-qualified endpoint URLs
|
|
39
|
-
* (`openaiURL` / `responsesURL` / `anthropicURL`). Unset URLs
|
|
40
|
-
* protocol is unavailable on this gateway ("没填的就是没有"): the
|
|
41
|
-
* `availableTypes` is exactly the configured set.
|
|
43
|
+
* (`openaiURL` / `responsesURL` / `anthropicURL` / `geminiURL`). Unset URLs
|
|
44
|
+
* mean the protocol is unavailable on this gateway ("没填的就是没有"): the
|
|
45
|
+
* returned `availableTypes` is exactly the configured set.
|
|
42
46
|
*
|
|
43
47
|
* Each URL may be the complete endpoint address (`…/v1/chat/completions`),
|
|
44
48
|
* an SDK base (`…/v1`), or — for anthropic — a bare host; the derivation
|
|
45
49
|
* strips the known suffix and re-adds the version segment where the SDK
|
|
46
|
-
* expects it.
|
|
50
|
+
* expects it. Gemini URLs accept a bare host, a base ending in `/v1beta`,
|
|
51
|
+
* or one with a trailing `/models`; the `/v1beta` segment is always ensured
|
|
52
|
+
* (pi-ai's Gemini client disables its own apiVersion when baseUrl is set).
|
|
47
53
|
* @param urls - the gateway's protocol URL fields (any may be absent).
|
|
48
54
|
* @returns `{ apiBases, availableTypes, catalogBase }` where apiBases maps
|
|
49
55
|
* pi-ai API id → SDK base URL, availableTypes lists gateway endpoint
|
|
@@ -80,7 +86,19 @@ export function deriveProtocolURLs(urls) {
|
|
|
80
86
|
availableTypes.push("anthropic");
|
|
81
87
|
}
|
|
82
88
|
}
|
|
89
|
+
// Gemini: pi-ai's client pins `apiVersion = ""` whenever the model carries
|
|
90
|
+
// a baseUrl (mirroring its builtin Google provider, whose baseUrl is
|
|
91
|
+
// `https://generativelanguage.googleapis.com/v1beta`), so the version
|
|
92
|
+
// segment must live in the base. Accept a bare host (append `/v1beta`), a
|
|
93
|
+
// base already ending in `/v1beta`, or one with a trailing `/models`.
|
|
94
|
+
const gm = stripSuffix(urls.geminiURL, ENDPOINT_SUFFIXES["google-generative-ai"]);
|
|
95
|
+
if (gm !== undefined) {
|
|
96
|
+
apiBases["google-generative-ai"] = /\/v\d+beta$/i.test(gm) ? gm : `${gm}/v1beta`;
|
|
97
|
+
availableTypes.push("gemini");
|
|
98
|
+
}
|
|
83
99
|
if (availableTypes.length === 0) return undefined;
|
|
100
|
+
// Gemini has no OpenAI-shaped `/v1/models` list endpoint, so only the
|
|
101
|
+
// OpenAI-style URLs contribute the discovery base.
|
|
84
102
|
for (const api of ["openai-completions", "openai-responses"]) {
|
|
85
103
|
if (apiBases[api] !== undefined) { catalogBase = apiBases[api].replace(/\/v\d+$/, ""); break; }
|
|
86
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gateway-provider",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
73
|
-
"@deepseek-ai/dsh-credentials": "0.2.1-rc.2",
|
|
74
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.1-rc.2",
|
|
75
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
76
|
-
"@deepseek-ai/dsh-timeout": "0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-credentials": ">=0.2.1-rc.2 <0.2.2-0",
|
|
74
|
+
"@deepseek-ai/dsh-launch-environment": ">=0.2.1-rc.2 <0.2.2-0",
|
|
75
|
+
"@deepseek-ai/dsh-llm": ">=0.2.1-rc.2 <0.2.2-0",
|
|
76
|
+
"@deepseek-ai/dsh-timeout": ">=0.2.1-rc.2 <0.2.2-0",
|
|
77
77
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
78
78
|
}
|
|
79
79
|
}
|