dsh-llm-xai-oauth 0.1.1
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 +145 -0
- package/README.zh.md +115 -0
- package/cordis.patch.yml +6 -0
- package/lib/adapter.js +379 -0
- package/lib/adapter.js.map +1 -0
- package/lib/bootstrap.js +64 -0
- package/lib/bootstrap.js.map +1 -0
- package/lib/browser.js +25 -0
- package/lib/browser.js.map +1 -0
- package/lib/discover.js +141 -0
- package/lib/discover.js.map +1 -0
- package/lib/dsh-defaults.js +145 -0
- package/lib/dsh-defaults.js.map +1 -0
- package/lib/index.js +207 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.js +6 -0
- package/lib/invariant.js.map +1 -0
- package/lib/login-flow.js +40 -0
- package/lib/login-flow.js.map +1 -0
- package/lib/login.js +29 -0
- package/lib/login.js.map +1 -0
- package/lib/oauth.js +230 -0
- package/lib/oauth.js.map +1 -0
- package/lib/serialize.js +132 -0
- package/lib/serialize.js.map +1 -0
- package/lib/sse.js +15 -0
- package/lib/sse.js.map +1 -0
- package/lib/tokens.js +37 -0
- package/lib/tokens.js.map +1 -0
- package/lib/translate.js +125 -0
- package/lib/translate.js.map +1 -0
- package/lib/types/adapter.d.ts +58 -0
- package/lib/types/bootstrap.d.ts +21 -0
- package/lib/types/browser.d.ts +5 -0
- package/lib/types/discover.d.ts +9 -0
- package/lib/types/dsh-defaults.d.ts +18 -0
- package/lib/types/index.d.ts +156 -0
- package/lib/types/invariant.d.ts +4 -0
- package/lib/types/login-flow.d.ts +13 -0
- package/lib/types/login.d.ts +2 -0
- package/lib/types/oauth.d.ts +39 -0
- package/lib/types/serialize.d.ts +10 -0
- package/lib/types/sse.d.ts +2 -0
- package/lib/types/tokens.d.ts +19 -0
- package/lib/types/translate.d.ts +5 -0
- package/lib/types/types.d.ts +101 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +99 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cyjyyd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# dsh-llm-xai-oauth
|
|
2
|
+
|
|
3
|
+
Use a SuperGrok / X Premium subscription as a **native DeepSeek Harness LLM provider**. No xAI API key. Reuses the OAuth tokens already on the machine.
|
|
4
|
+
|
|
5
|
+
中文:[README.zh.md](README.zh.md)。Companion TUI: [dsh-ssh-tui](https://github.com/cyjyyd/dsh-ssh-tui).
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
DeepSeek Harness already has an official DeepSeek route and a generic `llm-pi-ai` catalog. The catalog can *advertise* `xai`, but it cannot refresh a SuperGrok OAuth token or talk to the Grok subscription proxy. This plugin fills that gap:
|
|
10
|
+
|
|
11
|
+
- Registers the live `xai` adapter route (`grok-4.6` / `grok-4.5`(订阅实时目录;静态回退仍含 grok-4.3))
|
|
12
|
+
- Searches `~/.grok-bridge/auth.json`, `~/.grok/auth.json`, and a small set of `~/.config` / `~/.local/share` locations
|
|
13
|
+
- If nothing reusable is found, starts the xAI device-code login on a TTY and writes `~/.grok-bridge/auth.json`
|
|
14
|
+
- Once a usable token exists, sets `agent-default-model` to `xai` / `grok-4.6` unless that section is already on `xai`
|
|
15
|
+
- Refreshes the access token five minutes before expiry
|
|
16
|
+
- Calls `https://cli-chat-proxy.grok.com/v1/chat/completions` with the same Bearer + `x-grok-client-*` headers grok-bridge uses
|
|
17
|
+
- Streams text, reasoning, tool calls, and usage through the same harness seam as DeepSeek official
|
|
18
|
+
|
|
19
|
+
`/model`, the Models page, and `agent-default-model` then treat Grok like any other provider.
|
|
20
|
+
|
|
21
|
+
On start the plugin first searches the machine for a reusable token. If none is found and the process has a TTY, it runs the xAI device-code login, then writes `agent-default-model` so dsh uses SuperGrok.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Requires Node.js ≥ 22.19 and a DeepSeek Harness CLI (`dsh`) that can load profile bundles.
|
|
26
|
+
|
|
27
|
+
On a remote SSH box, install this next to [dsh-ssh-tui](https://github.com/cyjyyd/dsh-ssh-tui) so `/model` can switch onto SuperGrok without a browser.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# any profile: tui / web / headless. The CLI pulls npm.
|
|
31
|
+
dsh plugin --profile tui add dsh-llm-xai-oauth
|
|
32
|
+
dsh plugin --profile web add dsh-llm-xai-oauth
|
|
33
|
+
dsh plugin --profile headless add dsh-llm-xai-oauth
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Local checkout:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/cyjyyd/dsh-llm-xai-oauth.git
|
|
40
|
+
cd dsh-llm-xai-oauth
|
|
41
|
+
bash scripts/install.sh tui
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
GitHub still works if you want a checkout instead of the registry:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile tui add github:cyjyyd/dsh-llm-xai-oauth
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The bundle patch inserts `id: llm-xai-oauth`. Confirm with:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
dsh --profile tui --dump-config | grep llm-xai-oauth
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Login
|
|
57
|
+
|
|
58
|
+
The plugin does this itself when dsh starts:
|
|
59
|
+
|
|
60
|
+
1. Search the machine for a reusable SuperGrok token (grok-bridge, Grok CLI, then common config dirs).
|
|
61
|
+
2. If none is found and the process has a TTY, print the device-code URL / user code and wait for `auth.x.ai`.
|
|
62
|
+
3. Write `~/.grok-bridge/auth.json` (`0600`) and set `$DSH_HOME/settings.yaml` `agent-default-model` to `xai` / `grok-4.6` if that section is not already on `xai`. A later login always writes the SuperGrok default.
|
|
63
|
+
|
|
64
|
+
If grok-bridge or the official Grok CLI already logged in, step 1 succeeds and **no browser dance runs**. Headless / CI hosts without a TTY skip the prompt; set `DSH_XAI_OAUTH_NO_LOGIN=1` to skip it even on a TTY.
|
|
65
|
+
|
|
66
|
+
To log in ahead of time, from a machine that can open `auth.x.ai`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cd dsh-llm-xai-oauth
|
|
70
|
+
npm install
|
|
71
|
+
npm run login
|
|
72
|
+
# force a new device-code even when a token already exists:
|
|
73
|
+
node lib/login.js --force
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
That starts the xAI device-code flow, writes `~/.grok-bridge/auth.json` (`0600`), and later requests refresh through `https://auth.x.ai/oauth2/token`.
|
|
77
|
+
|
|
78
|
+
Override client / endpoints only when xAI changes them:
|
|
79
|
+
|
|
80
|
+
| Env | Default |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `GROK_OAUTH_CLIENT_ID` | `b1a00492-073a-47ea-816f-4c329264a828` |
|
|
83
|
+
| `GROK_OAUTH_SCOPE` | `openid profile email offline_access grok-cli:access api:access` |
|
|
84
|
+
| `GROK_OAUTH_BASE` | `https://auth.x.ai` |
|
|
85
|
+
| `GROK_UPSTREAM_BASE` | `https://cli-chat-proxy.grok.com/v1` |
|
|
86
|
+
| `HTTPS_PROXY` | honored for both refresh and chat |
|
|
87
|
+
| `DSH_XAI_OAUTH_NO_LOGIN` | skip the startup device-code prompt |
|
|
88
|
+
| `GROK_BRIDGE_NO_BROWSER` | print the URL instead of opening a browser |
|
|
89
|
+
|
|
90
|
+
## Use it
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
# $DSH_HOME/settings.yaml
|
|
94
|
+
agent-default-model:
|
|
95
|
+
provider: xai
|
|
96
|
+
model: grok-4.6
|
|
97
|
+
reasoningEffort: high
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or pick the route at launch / in TUI:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
dsh --profile tui --provider xai --model grok-4.6
|
|
104
|
+
dsh --profile headless "Reply with exactly: xai-harness-ok. Do not use tools."
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The plugin also queries `GET {baseURL}/models` with the SuperGrok OAuth token at startup. The live listing currently returns `grok-4.6` (off / low / medium / high / **xhigh**) and `grok-4.5` (off / low / medium / high). `/model` in dsh-ssh-tui reads that adapter catalog.
|
|
108
|
+
|
|
109
|
+
Default catalog (advisory; the adapter still accepts a `grok-*` id the proxy serves):
|
|
110
|
+
|
|
111
|
+
| Model | Context | Output cap | Reasoning efforts |
|
|
112
|
+
| --- | --- | --- | --- |
|
|
113
|
+
| `grok-4.6` | 500K | 64K | off / low / medium / high / xhigh |
|
|
114
|
+
| `grok-4.5` | 500K | 64K | off / low / medium / high |
|
|
115
|
+
| `grok-4.3` | 1M | 30K | off / low / medium / high |
|
|
116
|
+
|
|
117
|
+
`max` in a DeepSeek-style picker maps to `xhigh`.
|
|
118
|
+
|
|
119
|
+
Optional plugin settings live under `llm-xai-oauth:` (`baseURL`, `reasoningEffort`, `models`, retry / idle timeout). Change them without restarting; the next request re-resolves.
|
|
120
|
+
|
|
121
|
+
## What it is not
|
|
122
|
+
|
|
123
|
+
- Not an xAI API-key provider. Metered `api.x.ai` stays on the generic catalog if you ever add an API key there.
|
|
124
|
+
- Not a Codex / Claude / Copilot aggregator. Those belong in other plugins.
|
|
125
|
+
- Not a Web “one-click login” settings card. Device login is CLI / TTY; token reuse is the point for SSH / headless machines.
|
|
126
|
+
- Does not register `registerConfigurableProviders('xai')`, because `llm-pi-ai` already advertises that catalog key. This package owns the **live adapter route**.
|
|
127
|
+
|
|
128
|
+
## Layout
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
src/index.ts Cordis plugin: search / login / register the xai route
|
|
132
|
+
src/bootstrap.ts local token search, device login, dsh default model
|
|
133
|
+
src/discover.ts bounded home/config token search
|
|
134
|
+
src/login-flow.ts xAI device-code prompt
|
|
135
|
+
src/dsh-defaults.ts write agent-default-model
|
|
136
|
+
src/adapter.ts fetch + SSE chat-completions adapter
|
|
137
|
+
src/oauth.ts load / refresh / device-code
|
|
138
|
+
src/serialize.ts harness messages → Grok wire body
|
|
139
|
+
src/translate.ts SSE → harness StreamChunks
|
|
140
|
+
cordis.patch.yml profile bundle insert
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# dsh-llm-xai-oauth
|
|
2
|
+
|
|
3
|
+
把 SuperGrok / X Premium 订阅接到 DeepSeek Harness,当成**原生 LLM 提供商**用。不需要 xAI API Key。复用本机已经登录过的 OAuth token。
|
|
4
|
+
|
|
5
|
+
English: [README.md](README.md)。配套 TUI:[dsh-ssh-tui](https://github.com/cyjyyd/dsh-ssh-tui)。
|
|
6
|
+
|
|
7
|
+
## 它解决什么
|
|
8
|
+
|
|
9
|
+
Harness 自带 DeepSeek 官方路由,以及通用的 `llm-pi-ai` 目录。目录可以*展示* `xai`,但不能刷新 SuperGrok 的 OAuth token,也不能打 Grok 订阅代理。这个插件补上这一段:
|
|
10
|
+
|
|
11
|
+
- 注册真正可调用的 `xai` 路由(`grok-4.6` / `grok-4.5`(订阅实时目录;静态回退仍含 grok-4.3))
|
|
12
|
+
- 启动时先搜本机 token:`~/.grok-bridge/auth.json`、`~/.grok/auth.json`,以及 `~/.config` / `~/.local/share` 下常见位置
|
|
13
|
+
- 找不到可用 token 且当前是 TTY 时,走 xAI device-code 登录,写回 `~/.grok-bridge/auth.json`
|
|
14
|
+
- 拿到可用 token 后自动把 `agent-default-model` 设成 `xai` / `grok-4.6`(已经是 `xai` 时不改用户选的 grok 模型和思考强度)
|
|
15
|
+
- 过期前 5 分钟自动 refresh
|
|
16
|
+
- 用和 grok-bridge 相同的 Bearer + `x-grok-client-*` 头调用 `https://cli-chat-proxy.grok.com/v1/chat/completions`
|
|
17
|
+
- 文本、思考、工具调用、usage 走和 DeepSeek 官方一样的 harness 流式协议
|
|
18
|
+
|
|
19
|
+
之后 `/model`、Models 页、`agent-default-model` 都会把 Grok 当成普通提供商。
|
|
20
|
+
|
|
21
|
+
刻意做窄:不替换内置 xAI catalog 卡片,不做全订阅登录台,不另建一份 token 仓库。
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
需要 Node.js ≥ 22.19,以及能加载 profile bundle 的 `dsh` CLI。
|
|
26
|
+
|
|
27
|
+
远程 SSH 机器上请和 [dsh-ssh-tui](https://github.com/cyjyyd/dsh-ssh-tui) 一起装,这样 `/model` 能切到 SuperGrok,不必再走浏览器登录。
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile tui add dsh-llm-xai-oauth
|
|
31
|
+
dsh plugin --profile web add dsh-llm-xai-oauth
|
|
32
|
+
dsh plugin --profile headless add dsh-llm-xai-oauth
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
本地仓库:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/cyjyyd/dsh-llm-xai-oauth.git
|
|
39
|
+
cd dsh-llm-xai-oauth
|
|
40
|
+
bash scripts/install.sh tui
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
仍可用 GitHub 源,不必走 npm:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
dsh plugin --profile tui add github:cyjyyd/dsh-llm-xai-oauth
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
bundle patch 会插入 `id: llm-xai-oauth`。确认:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dsh --profile tui --dump-config | grep llm-xai-oauth
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 登录
|
|
56
|
+
|
|
57
|
+
插件启动时自己做这三步:
|
|
58
|
+
|
|
59
|
+
1. 在本机搜可复用的 SuperGrok token(grok-bridge、Grok CLI,再扫常见 config 目录)。
|
|
60
|
+
2. 找不到、且 stdin/stdout 是 TTY,就打印 device-code 链接和用户码,等 `auth.x.ai` 授权。
|
|
61
|
+
3. 把 token 写到 `~/.grok-bridge/auth.json`(`0600`),并在 `agent-default-model` 还不是 `xai` 时写成 `xai` / `grok-4.6`。新登录会强制写成 SuperGrok 默认。
|
|
62
|
+
|
|
63
|
+
本机已经用 grok-bridge 或官方 Grok CLI 登录过,第 1 步就会命中,**不会再弹浏览器**。没有 TTY 的 headless / CI 不会提示登录;TTY 上也可以设 `DSH_XAI_OAUTH_NO_LOGIN=1` 跳过。
|
|
64
|
+
|
|
65
|
+
提前登录(能打开 `auth.x.ai` 的机器上):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd dsh-llm-xai-oauth
|
|
69
|
+
npm install
|
|
70
|
+
npm run login
|
|
71
|
+
# 即使已有 token 也强制再走一遍 device-code:
|
|
72
|
+
node lib/login.js --force
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
这会走 xAI device-code,把 token 写到 `~/.grok-bridge/auth.json`(权限 `0600`),之后的请求通过 `https://auth.x.ai/oauth2/token` refresh。
|
|
76
|
+
|
|
77
|
+
## 使用
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# $DSH_HOME/settings.yaml
|
|
81
|
+
agent-default-model:
|
|
82
|
+
provider: xai
|
|
83
|
+
model: grok-4.6
|
|
84
|
+
reasoningEffort: high
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
或启动时指定:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
dsh --profile tui --provider xai --model grok-4.6
|
|
91
|
+
dsh --profile headless "Reply with exactly: xai-harness-ok. Do not use tools."
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
默认目录(仅作选择器展示;代理实际能提供的 `grok-*` 仍可请求):
|
|
95
|
+
|
|
96
|
+
| 模型 | 上下文 | 输出上限 | 思考强度 |
|
|
97
|
+
| --- | --- | --- | --- |
|
|
98
|
+
| `grok-4.6` | 500K | 64K | off / low / medium / high / xhigh |
|
|
99
|
+
| `grok-4.5` | 500K | 64K | off / low / medium / high |
|
|
100
|
+
| `grok-4.3` | 1M | 30K | off / low / medium / high |
|
|
101
|
+
|
|
102
|
+
DeepSeek 风格选择器里的 `max` 会映射成 `xhigh`。
|
|
103
|
+
|
|
104
|
+
可选配置在 `llm-xai-oauth:`(`baseURL`、`reasoningEffort`、`models`、重试 / 空闲超时)。改完不用重启,下一次请求再生效。
|
|
105
|
+
|
|
106
|
+
## 它不是什么
|
|
107
|
+
|
|
108
|
+
- 不是 xAI API Key 提供商。计量接口 `api.x.ai` 仍走通用 catalog。
|
|
109
|
+
- 不是 Codex / Claude / Copilot 聚合器。
|
|
110
|
+
- 不是 Web「一键登录」设置页。TTY device login + 复用本机 token,是给 SSH / headless 用的。
|
|
111
|
+
- 不会 `registerConfigurableProviders('xai')`,因为 `llm-pi-ai` 已经占用了这个目录名。本包只拥有**实际调用路由**。
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* Direct-fetch xAI/Grok adapter. Connection facts and the OAuth access token
|
|
55
|
+
* resolve once per request, matching dsh-llm-deepseek so configuration and
|
|
56
|
+
* token refresh reach the next call without a restart.
|
|
57
|
+
*/
|
|
58
|
+
import { attributionHeaders, contentHasImage, CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
59
|
+
import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout';
|
|
60
|
+
import { fetch as undiciFetch } from 'undici';
|
|
61
|
+
import { serializeRequest } from './serialize.js';
|
|
62
|
+
import { parseSse } from './sse.js';
|
|
63
|
+
import { translate } from './translate.js';
|
|
64
|
+
import { proxyDispatcher } from './oauth.js';
|
|
65
|
+
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
66
|
+
export const DEFAULT_CONTEXT_WINDOW = 500_000;
|
|
67
|
+
export const DEFAULT_MAX_TOKENS = 64_000;
|
|
68
|
+
const STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT';
|
|
69
|
+
const EFFORT_NAMES = {
|
|
70
|
+
off: 'Off',
|
|
71
|
+
low: 'Low',
|
|
72
|
+
medium: 'Medium',
|
|
73
|
+
high: 'High',
|
|
74
|
+
xhigh: 'Extra high',
|
|
75
|
+
};
|
|
76
|
+
function modelInfo(provider, model) {
|
|
77
|
+
return {
|
|
78
|
+
provider,
|
|
79
|
+
id: model.id,
|
|
80
|
+
name: model.name ?? model.id,
|
|
81
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
82
|
+
inputModalities: [...(model.inputModalities ?? ['text'])],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const KNOWN_EFFORTS = new Set(['off', 'low', 'medium', 'high', 'xhigh']);
|
|
86
|
+
function asEffort(value) {
|
|
87
|
+
return typeof value === 'string' && KNOWN_EFFORTS.has(value)
|
|
88
|
+
? value
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
function numberField(value) {
|
|
92
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
93
|
+
}
|
|
94
|
+
function stringField(value) {
|
|
95
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
96
|
+
}
|
|
97
|
+
/** Parse one SuperGrok / OpenAI-compatible model listing into a catalog entry. */
|
|
98
|
+
export function catalogModelFromListing(value) {
|
|
99
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
100
|
+
return undefined;
|
|
101
|
+
const record = value;
|
|
102
|
+
const id = stringField(record.id) ?? stringField(record.model);
|
|
103
|
+
if (id === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
const effortsRaw = Array.isArray(record.reasoning_efforts) ? record.reasoning_efforts : [];
|
|
106
|
+
const efforts = [];
|
|
107
|
+
let listedDefault;
|
|
108
|
+
for (const item of effortsRaw) {
|
|
109
|
+
if (typeof item === 'string') {
|
|
110
|
+
const effort = asEffort(item);
|
|
111
|
+
if (effort !== undefined && !efforts.includes(effort))
|
|
112
|
+
efforts.push(effort);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (item === null || typeof item !== 'object')
|
|
116
|
+
continue;
|
|
117
|
+
const entry = item;
|
|
118
|
+
const effort = asEffort(entry.id) ?? asEffort(entry.value);
|
|
119
|
+
if (effort === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
if (!efforts.includes(effort))
|
|
122
|
+
efforts.push(effort);
|
|
123
|
+
if (entry.default === true)
|
|
124
|
+
listedDefault = effort;
|
|
125
|
+
}
|
|
126
|
+
if (efforts.length > 0 && !efforts.includes('off'))
|
|
127
|
+
efforts.unshift('off');
|
|
128
|
+
const defaultEffort = listedDefault
|
|
129
|
+
?? asEffort(record.reasoning_effort)
|
|
130
|
+
?? (efforts.includes('high') ? 'high' : efforts.find(effort => effort !== 'off'));
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
...stringField(record.name) === undefined ? {} : { name: stringField(record.name) },
|
|
134
|
+
...stringField(record.description) === undefined ? {} : { description: stringField(record.description) },
|
|
135
|
+
...numberField(record.context_window) === undefined ? {} : { contextWindow: numberField(record.context_window) },
|
|
136
|
+
...numberField(record.max_tokens) === undefined && numberField(record.max_output_tokens) === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: { maxTokens: numberField(record.max_tokens) ?? numberField(record.max_output_tokens) },
|
|
139
|
+
inputModalities: ['text', 'image'],
|
|
140
|
+
...efforts.length === 0 ? {} : { reasoningEfforts: efforts },
|
|
141
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Merge a live `/models` listing over the static fallback catalog. */
|
|
145
|
+
export function mergeXaiCatalog(fallback, listed) {
|
|
146
|
+
if (listed.length === 0)
|
|
147
|
+
return [...fallback];
|
|
148
|
+
const byId = new Map();
|
|
149
|
+
for (const model of listed)
|
|
150
|
+
byId.set(model.id, model);
|
|
151
|
+
for (const model of fallback) {
|
|
152
|
+
const live = byId.get(model.id);
|
|
153
|
+
if (live === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
byId.set(model.id, {
|
|
156
|
+
...model,
|
|
157
|
+
...live,
|
|
158
|
+
reasoningEfforts: live.reasoningEfforts ?? model.reasoningEfforts,
|
|
159
|
+
defaultEffort: live.defaultEffort ?? model.defaultEffort,
|
|
160
|
+
contextWindow: live.contextWindow ?? model.contextWindow,
|
|
161
|
+
maxTokens: live.maxTokens ?? model.maxTokens,
|
|
162
|
+
name: live.name ?? model.name,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return [...byId.values()];
|
|
166
|
+
}
|
|
167
|
+
function collectImageRefs(blocks, refs) {
|
|
168
|
+
for (const block of blocks) {
|
|
169
|
+
if (block.type === 'image')
|
|
170
|
+
refs.set(block.attachment.attachmentId, block.attachment);
|
|
171
|
+
else if (block.type === 'tool-result')
|
|
172
|
+
collectImageRefs(block.content, refs);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function providerRetryAfterMs(value) {
|
|
176
|
+
if (value === null)
|
|
177
|
+
return undefined;
|
|
178
|
+
if (/^\d+$/.test(value)) {
|
|
179
|
+
const delay = Number(value) * 1_000;
|
|
180
|
+
return Number.isFinite(delay) && delay > 0 ? delay : undefined;
|
|
181
|
+
}
|
|
182
|
+
const delay = Date.parse(value) - Date.now();
|
|
183
|
+
return Number.isFinite(delay) && delay > 0 ? delay : undefined;
|
|
184
|
+
}
|
|
185
|
+
function requestId(headers) {
|
|
186
|
+
const value = headers.get('x-request-id') ?? headers.get('x-grok-request-id');
|
|
187
|
+
return value === null || value.length === 0 ? undefined : ProviderRequestId(value);
|
|
188
|
+
}
|
|
189
|
+
export function httpErrorCode(status, error) {
|
|
190
|
+
if (status === 401 || status === 403)
|
|
191
|
+
return 'AUTH';
|
|
192
|
+
const detail = [error?.code, error?.type, error?.message].filter(Boolean).join(' ');
|
|
193
|
+
if (isQuotaExceededError(detail))
|
|
194
|
+
return QUOTA_EXCEEDED_CODE;
|
|
195
|
+
if (status === 429)
|
|
196
|
+
return 'RATE_LIMIT';
|
|
197
|
+
if (status === 400) {
|
|
198
|
+
if (isContextWindowExceededError(detail))
|
|
199
|
+
return CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
200
|
+
return 'INVALID_REQUEST';
|
|
201
|
+
}
|
|
202
|
+
if (status >= 500)
|
|
203
|
+
return 'SERVER';
|
|
204
|
+
return `HTTP_${status}`;
|
|
205
|
+
}
|
|
206
|
+
export class XaiAdapter extends LlmAdapter {
|
|
207
|
+
config;
|
|
208
|
+
constructor(config) {
|
|
209
|
+
super();
|
|
210
|
+
this.config = config;
|
|
211
|
+
}
|
|
212
|
+
providerInfo(provider) {
|
|
213
|
+
return { id: provider, name: 'xAI (Grok OAuth)' };
|
|
214
|
+
}
|
|
215
|
+
providerRetryPolicy(_provider) {
|
|
216
|
+
return this.config.options().retryPolicy;
|
|
217
|
+
}
|
|
218
|
+
catalog() {
|
|
219
|
+
const live = this.config.liveCatalog?.();
|
|
220
|
+
const fallback = this.config.options().models;
|
|
221
|
+
return live === undefined || live.length === 0 ? fallback : live;
|
|
222
|
+
}
|
|
223
|
+
listModels(provider) {
|
|
224
|
+
return Promise.resolve(this.catalog().map(model => modelInfo(provider, model)));
|
|
225
|
+
}
|
|
226
|
+
resolveModel(provider, model, _signal) {
|
|
227
|
+
const connection = this.config.options();
|
|
228
|
+
const configured = this.catalog().find(entry => entry.id === model);
|
|
229
|
+
const contextWindow = configured?.contextWindow ?? connection.defaultContextWindow;
|
|
230
|
+
const efforts = configured?.reasoningEfforts ?? ['off', 'low', 'medium', 'high', 'xhigh'];
|
|
231
|
+
const defaultEffort = configured?.defaultEffort
|
|
232
|
+
?? connection.defaults.reasoningEffort
|
|
233
|
+
?? 'high';
|
|
234
|
+
const selected = efforts.includes(defaultEffort) ? defaultEffort : efforts[0];
|
|
235
|
+
return Promise.resolve({
|
|
236
|
+
...configured === undefined
|
|
237
|
+
? { provider, id: model, name: model, inputModalities: ['text', 'image'] }
|
|
238
|
+
: modelInfo(provider, configured),
|
|
239
|
+
context: { contextWindow },
|
|
240
|
+
defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
|
|
241
|
+
reasoning: {
|
|
242
|
+
efforts: efforts.map(id => ({ id: ReasoningEffortId(id), name: EFFORT_NAMES[id] ?? id })),
|
|
243
|
+
...selected === undefined ? {} : { defaultEffort: ReasoningEffortId(selected) },
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async *stream(options) {
|
|
248
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
249
|
+
try {
|
|
250
|
+
const connection = this.config.options();
|
|
251
|
+
const accessToken = await this.config.resolveAccessToken(connection);
|
|
252
|
+
const consumer = new AbortController();
|
|
253
|
+
const upstream = options.signal === undefined
|
|
254
|
+
? consumer.signal
|
|
255
|
+
: AbortSignal.any([options.signal, consumer.signal]);
|
|
256
|
+
const watchdog = __addDisposableResource(env_1, idleWatchdog(upstream, connection.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE), false);
|
|
257
|
+
const images = await this.resolveImages(options, connection);
|
|
258
|
+
const iterator = this.request(options, watchdog.signal, connection, accessToken, images, () => { watchdog.pulse(); })[Symbol.asyncIterator]();
|
|
259
|
+
let exhausted = false;
|
|
260
|
+
try {
|
|
261
|
+
while (true) {
|
|
262
|
+
const result = await watchdog.next(iterator);
|
|
263
|
+
if (result.done) {
|
|
264
|
+
exhausted = true;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
yield result.value;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (timeoutOf(watchdog.signal, STREAM_IDLE_TIMEOUT_CODE) !== undefined) {
|
|
272
|
+
throw new LlmError(`xAI stream idle timeout after ${connection.streamIdleTimeoutMs}ms`, 'TIMEOUT', { cause: error });
|
|
273
|
+
}
|
|
274
|
+
if (options.signal?.aborted) {
|
|
275
|
+
throw new LlmError('xAI request aborted by caller', 'ABORTED', { cause: error });
|
|
276
|
+
}
|
|
277
|
+
if (error instanceof LlmError)
|
|
278
|
+
throw error;
|
|
279
|
+
throw new LlmError(`xAI API stream from ${connection.baseURL} failed`, 'TRANSPORT', { cause: error });
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
consumer.abort('xAI stream consumer stopped');
|
|
283
|
+
if (!exhausted && iterator.return !== undefined) {
|
|
284
|
+
try {
|
|
285
|
+
await iterator.return();
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// The consumer controller already owns termination.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch (e_1) {
|
|
294
|
+
env_1.error = e_1;
|
|
295
|
+
env_1.hasError = true;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
__disposeResources(env_1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async resolveImages(options, connection) {
|
|
302
|
+
const hasImage = options.messages.some(message => contentHasImage(message.content));
|
|
303
|
+
if (!hasImage)
|
|
304
|
+
return new Map();
|
|
305
|
+
const configured = connection.models.find(entry => entry.id === options.model);
|
|
306
|
+
const modalities = configured?.inputModalities ?? ['text', 'image'];
|
|
307
|
+
if (!modalities.includes('image')) {
|
|
308
|
+
throw new LlmError(`xAI model "${options.model}" does not support image input`, 'UNSUPPORTED_CONTENT');
|
|
309
|
+
}
|
|
310
|
+
const attachments = this.config.resolveAttachments?.();
|
|
311
|
+
if (attachments === undefined) {
|
|
312
|
+
throw new LlmError('xAI image input requires the durable attachment service', 'UNSUPPORTED_CONTENT');
|
|
313
|
+
}
|
|
314
|
+
const refs = new Map();
|
|
315
|
+
for (const message of options.messages)
|
|
316
|
+
collectImageRefs(message.content, refs);
|
|
317
|
+
const images = new Map();
|
|
318
|
+
for (const ref of refs.values()) {
|
|
319
|
+
const stored = await attachments.readImage(ref, options.signal);
|
|
320
|
+
images.set(ref.attachmentId, Buffer.from(stored.data).toString('base64'));
|
|
321
|
+
}
|
|
322
|
+
return images;
|
|
323
|
+
}
|
|
324
|
+
async *request(options, signal, connection, accessToken, images, onComment) {
|
|
325
|
+
const body = serializeRequest(options, connection.defaults, images);
|
|
326
|
+
const payload = JSON.stringify(body);
|
|
327
|
+
const headers = {
|
|
328
|
+
authorization: `Bearer ${accessToken}`,
|
|
329
|
+
'content-type': 'application/json',
|
|
330
|
+
accept: 'text/event-stream',
|
|
331
|
+
'x-grok-client-version': connection.clientVersion,
|
|
332
|
+
'x-grok-client-mode': connection.clientMode,
|
|
333
|
+
...attributionHeaders(),
|
|
334
|
+
...options.sessionId !== undefined
|
|
335
|
+
? { 'x-deepseek-harness-session-id': String(options.sessionId) }
|
|
336
|
+
: {},
|
|
337
|
+
};
|
|
338
|
+
const dispatcher = proxyDispatcher();
|
|
339
|
+
let response;
|
|
340
|
+
try {
|
|
341
|
+
response = await undiciFetch(`${connection.baseURL}/chat/completions`, {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers,
|
|
344
|
+
body: payload,
|
|
345
|
+
signal,
|
|
346
|
+
...dispatcher === undefined ? {} : { dispatcher },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (signal.aborted)
|
|
351
|
+
throw error;
|
|
352
|
+
throw new LlmError(`xAI API request to ${connection.baseURL} failed`, 'TRANSPORT', { cause: error });
|
|
353
|
+
}
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
let message = `xAI API error (HTTP ${response.status})`;
|
|
356
|
+
let providerError;
|
|
357
|
+
try {
|
|
358
|
+
const parsed = await response.json();
|
|
359
|
+
providerError = parsed.error;
|
|
360
|
+
if (providerError?.message)
|
|
361
|
+
message = providerError.message;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Keep the HTTP status as the failure identity.
|
|
365
|
+
}
|
|
366
|
+
const delay = providerRetryAfterMs(response.headers.get('retry-after'));
|
|
367
|
+
const id = requestId(response.headers);
|
|
368
|
+
throw new LlmError(message, httpErrorCode(response.status, providerError), {
|
|
369
|
+
status: response.status,
|
|
370
|
+
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
371
|
+
...id === undefined ? {} : { requestId: id },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (!response.body)
|
|
375
|
+
throw new LlmError('xAI API returned no response body', 'EMPTY_RESPONSE');
|
|
376
|
+
yield* translate(parseSse(response.body, onComment));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=adapter.js.map
|