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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/README.zh.md +115 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/adapter.js +379 -0
  6. package/lib/adapter.js.map +1 -0
  7. package/lib/bootstrap.js +64 -0
  8. package/lib/bootstrap.js.map +1 -0
  9. package/lib/browser.js +25 -0
  10. package/lib/browser.js.map +1 -0
  11. package/lib/discover.js +141 -0
  12. package/lib/discover.js.map +1 -0
  13. package/lib/dsh-defaults.js +145 -0
  14. package/lib/dsh-defaults.js.map +1 -0
  15. package/lib/index.js +207 -0
  16. package/lib/index.js.map +1 -0
  17. package/lib/invariant.js +6 -0
  18. package/lib/invariant.js.map +1 -0
  19. package/lib/login-flow.js +40 -0
  20. package/lib/login-flow.js.map +1 -0
  21. package/lib/login.js +29 -0
  22. package/lib/login.js.map +1 -0
  23. package/lib/oauth.js +230 -0
  24. package/lib/oauth.js.map +1 -0
  25. package/lib/serialize.js +132 -0
  26. package/lib/serialize.js.map +1 -0
  27. package/lib/sse.js +15 -0
  28. package/lib/sse.js.map +1 -0
  29. package/lib/tokens.js +37 -0
  30. package/lib/tokens.js.map +1 -0
  31. package/lib/translate.js +125 -0
  32. package/lib/translate.js.map +1 -0
  33. package/lib/types/adapter.d.ts +58 -0
  34. package/lib/types/bootstrap.d.ts +21 -0
  35. package/lib/types/browser.d.ts +5 -0
  36. package/lib/types/discover.d.ts +9 -0
  37. package/lib/types/dsh-defaults.d.ts +18 -0
  38. package/lib/types/index.d.ts +156 -0
  39. package/lib/types/invariant.d.ts +4 -0
  40. package/lib/types/login-flow.d.ts +13 -0
  41. package/lib/types/login.d.ts +2 -0
  42. package/lib/types/oauth.d.ts +39 -0
  43. package/lib/types/serialize.d.ts +10 -0
  44. package/lib/types/sse.d.ts +2 -0
  45. package/lib/types/tokens.d.ts +19 -0
  46. package/lib/types/translate.d.ts +5 -0
  47. package/lib/types/types.d.ts +101 -0
  48. package/lib/types.js +2 -0
  49. package/lib/types.js.map +1 -0
  50. 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
@@ -0,0 +1,6 @@
1
+ # First-class xAI/Grok OAuth provider. Registers the `xai` LLM route so
2
+ # `/model`, the Models page, and agent-default-model can select Grok the
3
+ # same way they select DeepSeek official.
4
+ - insert:
5
+ - id: llm-xai-oauth
6
+ name: dsh-llm-xai-oauth
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