opencode-lobster-auth 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,175 +1,255 @@
1
- # opencode-lobster-auth
2
-
3
- Signs in to a [LobsterAI](https://lobsterai.youdao.com) (有道龙虾, NetEase
4
- Youdao's desktop agent) account and serves its models as the provider
5
- `lobster` in [magpie](https://usemagpie.ai). About 30 models: DeepSeek, GLM,
6
- Kimi, Qwen, MiniMax and Doubao.
7
-
8
- **English** · [中文](README.zh-CN.md)
9
-
10
- ## Install
11
-
12
- A folder plugin — it is not published to npm:
13
-
14
- ```sh
15
- magpie plugin add /path/to/magpie-lobster
16
- magpie plugin login lobster
17
- ```
18
-
19
- ## Signing in
20
-
21
- Two ways, either one is enough. Both name the account by the Youdao account
22
- id (`yid`), so signing in the second way to an account already signed in the
23
- first way replaces it in place.
24
-
25
- - **Browser (a Youdao account).** The plugin serves a callback on
26
- `127.0.0.1` and magpie opens Youdao's sign-in page. The browser comes back
27
- with a code, which is traded for tokens at `/api/auth/exchange`. Kept as
28
- an `oauth` sign-in holding the access and refresh tokens and their expiry,
29
- so magpie renews it.
30
- - **The desktop's sign-in.** Uses the account LobsterAI's own desktop app is
31
- signed in to. **No page is opened and nothing is typed.** The app's SQLite
32
- database is read, never written:
33
- - Windows: `%APPDATA%\LobsterAI\lobsterai.sqlite`
34
- - macOS: `~/Library/Application Support/LobsterAI/lobsterai.sqlite`
35
- - Linux: `~/.config/LobsterAI/lobsterai.sqlite`
36
-
37
- A database that isn't there, or one the app keeps encrypted, fails this
38
- way with a message saying so; use the browser way instead.
39
-
40
- Both are kept in magpie's `plugin-auth.json`.
41
-
42
- ### Why the import keeps no token
43
-
44
- The desktop way stores **no credential at all** — `access` and `refresh` are
45
- empty and `expires` is 0 — and reads the token from the app's database on
46
- every request:
47
-
48
- - A copied `refreshToken` would be shared with the app: whichever side
49
- refreshes first signs the other out. Empty means neither disturbs the
50
- other.
51
- - `expires` of 0 means magpie never renews it — magpie renews only an
52
- `oauth` sign-in whose expiry is set — so the app's session is never
53
- touched.
54
- - Reading it fresh means the plugin follows a token the app renewed, with no
55
- need to sign in again.
56
-
57
- The cost: if the app is signed out, uninstalled, or its database becomes
58
- unreadable, the account stops working at once and magpie is told to ask for
59
- a new sign-in. That is more honest than a token that quietly went stale.
60
-
61
- ### Renewal
62
-
63
- A browser sign-in is renewed by magpie through the plugin's `auth.refresh`,
64
- six hours before it ends. The desktop's sign-in is structurally never
65
- renewed, as above.
66
-
67
- The plugin never calls LobsterAI's own refresh endpoint to check or renew
68
- anything: `/api/auth/refresh` may spend the account's real refresh token, so
69
- it is deliberately left alone.
70
-
71
- ## Requests
72
-
73
- Chats are chat completions at
74
- `https://lobsterai-server.youdao.com/api/proxy/v1`. Three of LobsterAI's
75
- habits are handled:
76
-
77
- - **Every reply is SSE**, even one asked for with `stream: false`. A
78
- non-streaming caller gets one `chat.completion` reassembled from it; a
79
- streaming caller gets the events as they are.
80
- - **Failures ride inside a 200.** LobsterAI puts an error in the stream
81
- (`event: error`) and still answers 200, which would leave a caller with
82
- nothing to fail over on. The plugin reads the first record and answers the
83
- status it means: `40100` → 401, `40300` → 403, `42900` or rate-limit
84
- wording → 429, anything else → 400. A sign-in LobsterAI turns down is a
85
- genuine 401, passed through with magpie's lapse mark so the account is
86
- flagged.
87
- - **Requests carry the desktop client's headers**
88
- (`X-LobsterAI-Client-Version`, `X-LobsterAI-Client-Capabilities`).
89
- LobsterAI doesn't insist on them, but they name the feature set the plugin
90
- speaks.
91
-
92
- ### Thinking levels
93
-
94
- A model's `thinkingConfig` gives it levels — `off`, `high`, `max` in the
95
- current catalogue — and they reach the wire in LobsterAI's own field:
96
- `{"lobsterai_options":{"version":1,"thinking":{"level":"off"}}}`.
97
-
98
- Two things are worth knowing:
99
-
100
- - **`lobsterai_options` is only for models that have a thinking profile.**
101
- 21 of the 29 models have none, and sending the field to one of those is a
102
- hard error (`code 4000`, *model does not have a valid thinkingConfig*), so
103
- the whole chat fails. A level is passed on only where there is a profile
104
- to map it through.
105
- - **The lowest level is offered as `none`, not `off`.** magpie doesn't use
106
- what a variant holds; it maps `reasoning_effort` onto a level name the
107
- model declared, from a ladder of its own
108
- (`none/minimal/low/medium/high/xhigh/max`). `off` is not on that ladder,
109
- so a level declared as `off` can never be chosen. The plugin declares
110
- `none` and turns it back into `off` for LobsterAI.
111
-
112
- Measured on `deepseek-flash`: `none` leaves `reasoning_content` at exactly
113
- 0 bytes, while `high`, `max` and no level at all don't.
114
-
115
- ### Kimi K3
116
-
117
- LobsterAI's `kimi-k3` takes its sampling parameters from the server, so
118
- `temperature`, `top_p`, `n` and the penalties are dropped, `reasoning_effort`
119
- is fixed to `max`, and an assistant tool call missing `reasoning_content` is
120
- given an empty one.
121
-
122
- `kimi-k3` is one of the models LobsterAI serves but does not list:
123
- `/api/models/available` returns 29 models and none of them is K3, yet a chat
124
- against `kimi-k3` answers normally. It is kept in the list this package
125
- declares for a signed-out account, so `lobster/kimi-k3` is offered — and the
126
- handling above is live, not a precaution.
127
-
128
- ## Models
129
-
130
- Signed in, the list is the account's own (`/api/models/available`): 29
131
- models, each with its context window, whether it takes images, and its
132
- thinking levels. Until then a static list of five is used, so the provider
133
- is visible before signing in.
134
-
135
- `costMultiplier` becomes magpie's `rate`. It is **time-of-day pricing**:
136
- `deepseek-flash` is 0.05 in LobsterAI's off-peak hours and 0.1 in the peak
137
- ones (09:00–12:00 and 14:00–18:00, Beijing time). The rate is read when the
138
- list is, so it can be a while out of date.
139
-
140
- ## Usage
141
-
142
- `magpie quota` shows the free allowance, when it ends and the credits left,
143
- from `/api/user/quota` and `/api/user/profile-summary`. The plugin reports
144
- `kept`: it renews nothing itself, so it must not claim a renewal it didn't
145
- do.
146
-
147
- ## Not here
148
-
149
- - **Company accounts.** LobsterAI sends them through a different page
150
- (`EnterpriseIdentitySelect`). Only personal accounts are handled.
151
- - **A verified `/api/auth/refresh`.** Calling it may spend the account's
152
- real refresh token, so it is not exercised. The renewal path is written
153
- from the app's own code and has not been confirmed against the service.
154
- - **An exact output limit.** The catalogue has no such field. The plugin
155
- declares a conservative 32,000; measured, LobsterAI takes 262,144 and
156
- answers 500 at 524,288, so the real ceiling is somewhere between.
157
- - **OpenCode.** The plugin is written in OpenCode's provider-plugin format —
158
- that is the format magpie loads — but it has only been exercised in magpie.
159
- `auth.refresh`, `auth.refreshLead` and the `magpie` field in `package.json`
160
- are magpie's own, which OpenCode ignores.
161
- - **Several accounts at once.** magpie keeps one sign-in per provider.
162
-
163
- ## Development
164
-
165
- ```sh
166
- bun test
167
- ```
168
-
169
- The entry point only assembles the hooks. The logic lives in `lib/` as plain
170
- functions and is reached through `_internal`, because magpie and OpenCode
171
- both treat every exported function as a plugin of its own.
172
-
173
- ## License
174
-
175
- MIT
1
+ # opencode-lobster-auth
2
+
3
+ Signs in to a [LobsterAI](https://lobsterai.youdao.com) (有道龙虾, NetEase
4
+ Youdao's desktop agent) account and serves its models as the provider
5
+ `lobster` in [magpie](https://usemagpie.ai). About 30 models: DeepSeek, GLM,
6
+ Kimi, Qwen, MiniMax and Doubao.
7
+
8
+ **English** · [中文](README.zh-CN.md)
9
+
10
+ ## Install
11
+
12
+ From npm:
13
+
14
+ ```sh
15
+ magpie plugin add opencode-lobster-auth
16
+ magpie plugin login lobster
17
+ ```
18
+
19
+ `magpie plugin update` picks up later versions.
20
+
21
+ To run a checkout instead of the published package, point at the folder:
22
+
23
+ ```sh
24
+ magpie plugin add /path/to/magpie-lobster
25
+ ```
26
+
27
+ ## Signing in
28
+
29
+ Two ways, either one is enough. Both name the account by the Youdao account
30
+ id (`yid`), so signing in the second way to an account already signed in the
31
+ first way replaces it in place.
32
+
33
+ - **Browser (a Youdao account).** The plugin serves a callback on
34
+ `127.0.0.1` and magpie opens Youdao's sign-in page. The browser comes back
35
+ with a code, which is traded for tokens at `/api/auth/exchange`. Kept as
36
+ an `oauth` sign-in holding the access and refresh tokens and their expiry,
37
+ so magpie renews it.
38
+ - **Remote browser.** For magpie on a server, in Docker, or anywhere the
39
+ browser is on another machine: the callback lands on the browser's own
40
+ `127.0.0.1` and never reaches magpie. The sign-in page's plain query names
41
+ the callback port, so magpie offers a **Callback URL** field: sign in, copy
42
+ the whole address the browser ends on, paste it there, and magpie sends it
43
+ to the port the plugin is listening on. The rest is the browser way.
44
+ - **The desktop's sign-in.** Uses the account LobsterAI's own desktop app is
45
+ signed in to. **No page is opened and nothing is typed.** The app's SQLite
46
+ database is read, never written:
47
+ - Windows: `%APPDATA%\LobsterAI\lobsterai.sqlite`
48
+ - macOS: `~/Library/Application Support/LobsterAI/lobsterai.sqlite`
49
+ - Linux: `~/.config/LobsterAI/lobsterai.sqlite`
50
+
51
+ A database that isn't there, or one the app keeps encrypted, fails this
52
+ way with a message saying so; use the browser way instead. The imported
53
+ identity is checked against the current token, and an incomplete or
54
+ conflicting identity fails closed rather than using another account's token.
55
+
56
+ Both are kept in magpie's `plugin-auth.json`.
57
+
58
+ ### Why the import keeps no token
59
+
60
+ The desktop way stores **no credential at all** — `access` and `refresh` are
61
+ empty and `expires` is 0 — and reads the token from the app's database on
62
+ every request:
63
+
64
+ - A copied `refreshToken` would be shared with the app: whichever side
65
+ refreshes first signs the other out. Empty means neither disturbs the
66
+ other.
67
+ - `expires` of 0 means magpie never renews it — magpie renews only an
68
+ `oauth` sign-in whose expiry is set — so the app's session is never
69
+ touched.
70
+ - Reading it fresh means the plugin follows a token the app renewed, with no
71
+ need to sign in again.
72
+
73
+ The cost: if the app is signed out, uninstalled, or its database becomes
74
+ unreadable, the account stops working at once and magpie is told to ask for
75
+ a new sign-in. That is more honest than a token that quietly went stale.
76
+
77
+ ### Renewal
78
+
79
+ A browser sign-in is renewed by magpie through the plugin's `auth.refresh`,
80
+ six hours before it ends. The desktop's sign-in is structurally never
81
+ renewed, as above.
82
+
83
+ The plugin never calls LobsterAI's own refresh endpoint to check or renew
84
+ anything: `/api/auth/refresh` may spend the account's real refresh token, so
85
+ it is deliberately left alone.
86
+
87
+ ## Requests
88
+
89
+ Chats are chat completions at
90
+ `https://lobsterai-server.youdao.com/api/proxy/v1`. Three of LobsterAI's
91
+ habits are handled:
92
+
93
+ - **Every reply is SSE**, even one asked for with `stream: false`. A
94
+ non-streaming caller gets one `chat.completion` reassembled from it; a
95
+ streaming caller gets the events as they are.
96
+ - **Failures ride inside a 200.** LobsterAI puts errors in the stream
97
+ (`event: error`, or an error JSON record) and still answers 200, which
98
+ would leave a caller with nothing to fail over on. The plugin inspects the
99
+ early records, preserves plain-text error messages, and answers the status
100
+ it means: `40100` → 401, `40300` → 403, `42900` or rate-limit wording →
101
+ 429, anything else → 400. Non-streaming requests also catch an error that
102
+ appears after an initial response chunk; streaming requests preserve such
103
+ an event after content has started. A sign-in LobsterAI turns down is a
104
+ genuine 401, passed through with magpie's lapse mark so the account is
105
+ flagged.
106
+ - **Requests carry the desktop client's headers**
107
+ (`X-LobsterAI-Client-Version`, `X-LobsterAI-Client-Capabilities`).
108
+ LobsterAI doesn't insist on them, but they name the feature set the plugin
109
+ speaks.
110
+
111
+ ### Thinking levels
112
+
113
+ There are two kinds of model in LobsterAI's catalogue, and the plugin
114
+ speaks to each in its own way.
115
+
116
+ **8 models carry a `thinkingConfig`** (`deepseek-*`, `glm-*`), with the
117
+ levels `off`/`high`/`max`. Those go out in LobsterAI's own field:
118
+ `{"lobsterai_options":{"version":1,"thinking":{"level":"off"}}}`. Sending
119
+ that field to a model **without** a profile is a hard error (`code 4000`,
120
+ *model does not have a valid thinkingConfig*), so it is written only where
121
+ there is a profile to map it through. With a profile, only its declared
122
+ levels are offered; separately measured `reasoning_effort` levels apply only
123
+ while a model has no profile.
124
+
125
+ **10 models carry no profile** but still report `supportsThinking: true`.
126
+ For those the plugin declares levels and lets magpie's `reasoning_effort`
127
+ pass straight through, which LobsterAI honours.
128
+
129
+ Two things are worth knowing:
130
+
131
+ - **The lowest level is offered as `none`, not `off`.** magpie doesn't use
132
+ what a variant holds; it maps `reasoning_effort` onto a level name the
133
+ model declared, from a ladder of its own
134
+ (`none/minimal/low/medium/high/xhigh/max`). `off` is not on that ladder,
135
+ so a level declared as `off` can never be chosen. With a profile the
136
+ plugin declares `none` and turns it back into `off`; without one it sends
137
+ `none` as it is, since LobsterAI accepts it.
138
+ - **Levels differ per model and were measured, not assumed.** LobsterAI
139
+ refuses some of them outright: `MiniMax-M3.1-Flash` rejects `none`
140
+ (*requires adaptive thinking*), `qwen3.7-max` stops at `xhigh`, and
141
+ `qwen3.7-plus` answers `max` with an occasional 500. `MiniMax-M3` writes
142
+ its thinking into `<think>` tags inside the content and `reasoning_content`
143
+ stays empty; its effort isn't graded at all, so it offers only `none` and
144
+ `max`. A model whose levels were not measured declares none, and magpie
145
+ shows it as *thinks, no levels*.
146
+
147
+ Measured on `deepseek-flash`: `none` leaves `reasoning_content` at exactly
148
+ 0 bytes, while `high`, `max` and no level at all don't — and on
149
+ `kimi-k3.1-pro`, `none` leaves it at 0 bytes while `xhigh` produces some.
150
+
151
+ ### Kimi
152
+
153
+ Every Kimi model takes its sampling parameters from the server:
154
+ `temperature` must be 1, `top_p` 0.95 and the penalties 0, so the plugin
155
+ drops `temperature`, `top_p`, `n` and both penalties for the whole family —
156
+ it measured that for `kimi-k3`, `kimi-k3.1-pro` and `kimi-k2.8-preview`
157
+ alike. A client that always sends `temperature` (most agents do) would
158
+ otherwise be answered `50204 invalid temperature: only 1 is allowed`.
159
+
160
+ Only `kimi-k3` has `reasoning_effort` fixed to `max`. That forcing is
161
+ not extended to `kimi-k3.1-pro`, which honours the full ladder and would
162
+ otherwise have every level silently overridden. Separately, tool-call
163
+ history repair stays enabled for the `kimi-k3` family, including
164
+ `kimi-k3.1-pro`: an assistant tool call missing `reasoning_content` is given
165
+ an empty string without overwriting existing reasoning or the chosen effort.
166
+
167
+ `kimi-k3` is one of the models LobsterAI serves without listing it in every
168
+ catalogue revision. It is kept in the list this package declares for a
169
+ signed-out account, so `lobster/kimi-k3` stays offered.
170
+
171
+ ## Models
172
+
173
+ Signed in, the list is the account's own (`/api/models/available`) — 18 or
174
+ 19 usable models, each with its context window, whether it takes images,
175
+ and its thinking levels. Until then a static list of five is used, so the
176
+ provider is visible before signing in.
177
+
178
+ `costMultiplier` becomes magpie's `rate`. It is **time-of-day pricing**:
179
+ `deepseek-flash` is 0.05 in LobsterAI's off-peak hours and 0.1 in the peak
180
+ ones (09:00–12:00 and 14:00–18:00, Beijing time). The rate is read when the
181
+ list is, so it can be a while out of date.
182
+
183
+ The reply limit is 131,072. The catalogue's `maxTokens` field is always
184
+ null, so this was measured: all 18 models answered `max_tokens=131072`,
185
+ `glm-5.3` names `[1, 131072]` as its own range, and 524,288 is where
186
+ LobsterAI starts answering 500.
187
+
188
+ ## Usage
189
+
190
+ `magpie quota` shows the credits left and when the soonest batch expires,
191
+ from `/api/user/quota` and `/api/user/profile-summary`. The credits are
192
+ reported as a **wallet window** (`余额·点数` with `runsOut`/`runsOutAt`)
193
+ rather than a plain string, so magpie can show the lapse and routing can
194
+ spend the account that runs out first. Every day's check-in adds 100 points
195
+ that lapse 30 days later, first in first out, so knowing the date matters.
196
+ The plugin reports `kept`: it renews nothing itself, so it must not claim a
197
+ renewal it didn't do.
198
+
199
+ Expired free allowances are not reported as a live window — the free period
200
+ ended in August 2026, and showing "6% used" looked like a usable allowance.
201
+
202
+ ## Daily check-in
203
+
204
+ The plugin implements `auth.checkin`, so magpie can claim LobsterAI's daily
205
+ gift (100 points) for you. **It is off by default**: turn it on under
206
+ Settings → Usage → Plugins. Once on, every signed-in LobsterAI account is
207
+ checked in once a day (Beijing time).
208
+
209
+ This is the only way to collect those points without opening LobsterAI's own
210
+ desktop app, whose sidebar holds the check-in. The claim is idempotent — a
211
+ day already claimed answers `51104`, and the plugin reports `done` rather
212
+ than claiming again — and every claim is confirmed by re-reading the state
213
+ before it is reported as `claimed`. HTTP failures, malformed business
214
+ responses and invalid state are reported as `failed`, not as an inactive
215
+ activity or an ineligible account, so magpie can retry later that day.
216
+
217
+ ## Not here
218
+
219
+ - **Company accounts.** LobsterAI sends them through a different page
220
+ (`EnterpriseIdentitySelect`). Only personal accounts are handled.
221
+ - **A verified `/api/auth/refresh`.** Calling it may spend the account's
222
+ real refresh token, so it is not exercised. The renewal path is written
223
+ from the app's own code and has not been confirmed against the service.
224
+ - **OpenCode.** The plugin is written in OpenCode's provider-plugin format —
225
+ that is the format magpie loads — but it has only been exercised in magpie.
226
+ `auth.refresh`, `auth.refreshLead`, `auth.checkin` and the `magpie` field
227
+ in `package.json` are magpie's own, which OpenCode ignores.
228
+
229
+ ## Several accounts
230
+
231
+ magpie can hold more than one sign-in per provider, and this plugin supports
232
+ that: sign in to a second Youdao account through the browser and both are
233
+ listed.
234
+
235
+ The desktop import is the exception to watch. LobsterAI's desktop app has one
236
+ signed-in account at a time, and the plugin reads that app's token on every
237
+ request. So an account imported from the desktop is tied to the account that
238
+ was signed in when it was imported: switch the desktop to another account,
239
+ and the imported one is marked *signed out · sign in again* rather than
240
+ quietly spending the other account's credits. Sign it in again, or use the
241
+ browser sign-in for accounts you want to keep independent.
242
+
243
+ ## Development
244
+
245
+ ```sh
246
+ bun test
247
+ ```
248
+
249
+ The entry point only assembles the hooks. The logic lives in `lib/` as plain
250
+ functions and is reached through `_internal`, because magpie and OpenCode
251
+ both treat every exported function as a plugin of its own.
252
+
253
+ ## License
254
+
255
+ MIT