dsh-plugin-subscription-oauth 0.8.18 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The WorkBuddy China wire adapter serving the `workbuddy` route.
3
+ *
4
+ * WorkBuddy chats are chat completions at `https://copilot.tencent.com/v2/chat/completions`,
5
+ * streamed only, and the API refuses a request whose User-Agent is not
6
+ * `WorkBuddy/<version>`. The route ships the China build's catalog (Hy, GLM, Kimi,
7
+ * DeepSeek, MiniMax). Adapted from magpie's `workbuddy` for this plugin's contracts.
8
+ *
9
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy
10
+ */
11
+ import { LlmAdapter, errorChain, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
12
+ import { idleWatchdog, mapFetchFailure } from './common.js';
13
+ import { proxiedFetch } from '../http.js';
14
+ import { findEntry } from './catalog.js';
15
+ import { WORKBUDDY_ENDPOINT, WORKBUDDY_UA_VERSION, workbuddyHeaders } from '../auth/workbuddy-flow.js';
16
+ /** The `workbuddy` provider route key (China build). */
17
+ export const WORKBUDDY_PROVIDER_ID = 'workbuddy';
18
+ /** Human name for the route and its cards. */
19
+ export const WORKBUDDY_PROVIDER_NAME = 'WorkBuddy';
20
+ /** Where the Settings page sends someone to get an account. */
21
+ export const WORKBUDDY_CODE_URL = 'https://copilot.tencent.com';
22
+ /** The chat completions endpoint. */
23
+ const WORKBUDDY_API = `${WORKBUDDY_ENDPOINT}/v2/chat/completions`;
24
+ /** The model the picker preselects. */
25
+ export const WORKBUDDY_DEFAULT_MODEL = 'hy4-preview-f';
26
+ /** The China build's catalog, [id, name, context, efforts?]. */
27
+ const CN_MODELS = [
28
+ ['auto', 'Auto', 168_000],
29
+ ['hy4-preview-f', 'Hy4 preview', 1_000_000, ['high']],
30
+ ['hy3', 'Hy3', 192_000, ['low', 'high']],
31
+ ['hy3-x', 'Hy3-X', 192_000, ['low', 'high']],
32
+ ['deepseek-v4.1-flash', 'Deepseek-V4.1-Flash', 1_000_000],
33
+ ['glm-5.3', 'GLM-5.3', 1_000_000, ['low', 'high', 'max']],
34
+ ['glm-5.3-flash', 'GLM-5.3-Flash', 1_000_000, ['low', 'high', 'max']],
35
+ ['glm-5.2', 'GLM-5.2', 1_000_000, ['high', 'xhigh']],
36
+ ['glm-5.1', 'GLM-5.1', 200_000],
37
+ ['glm-5v-turbo', 'GLM-5v-Turbo', 200_000],
38
+ ['minimax-m3', 'MiniMax-M3', 512_000],
39
+ ['kimi-k3-1', 'Kimi-K3', 1_000_000, ['low', 'high', 'xhigh']],
40
+ ['kimi-k2.7', 'Kimi-K2.7-Code', 256_000],
41
+ ['kimi-k2.6', 'Kimi-K2.6', 256_000],
42
+ ['deepseek-v4-pro', 'Deepseek-V4-Pro', 1_000_000, ['none', 'high', 'xhigh']],
43
+ ];
44
+ /** The catalog the route serves. */
45
+ export const WORKBUDDY_CATALOG = CN_MODELS.map((entry) => {
46
+ const [id, name, contextWindow, efforts] = entry;
47
+ return {
48
+ id,
49
+ name,
50
+ contextWindow,
51
+ ...efforts === undefined ? {} : { reasoningEfforts: efforts },
52
+ };
53
+ });
54
+ /** The WorkBuddy China wire adapter serving the `workbuddy` route. */
55
+ export class WorkBuddyAdapter extends LlmAdapter {
56
+ options;
57
+ fetchFn;
58
+ constructor(options) {
59
+ super();
60
+ this.options = options;
61
+ this.fetchFn = options.fetchFn ?? proxiedFetch;
62
+ }
63
+ providerInfo(provider) {
64
+ return { id: provider, name: WORKBUDDY_PROVIDER_NAME };
65
+ }
66
+ async listModels(_provider) {
67
+ if (await this.options.tokens.session() === undefined)
68
+ return [];
69
+ const hidden = await this.hidden();
70
+ return WORKBUDDY_CATALOG.filter((entry) => !hidden.has(entry.id)).map((entry) => ({
71
+ provider: WORKBUDDY_PROVIDER_ID,
72
+ id: entry.id,
73
+ name: entry.name,
74
+ ...entry.description === undefined ? {} : { description: entry.description },
75
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
76
+ }));
77
+ }
78
+ async hidden() {
79
+ if (this.options.hiddenModels === undefined)
80
+ return new Set();
81
+ try {
82
+ return await this.options.hiddenModels();
83
+ }
84
+ catch {
85
+ return new Set();
86
+ }
87
+ }
88
+ /** The catalog the Settings list reads. */
89
+ catalogEntries() {
90
+ return WORKBUDDY_CATALOG;
91
+ }
92
+ /** The entries the route itself serves, without hand-added extras. */
93
+ ownCatalogEntries() {
94
+ return WORKBUDDY_CATALOG;
95
+ }
96
+ async resolveModel(_provider, model) {
97
+ const entry = findEntry(WORKBUDDY_CATALOG, model);
98
+ if (entry === undefined) {
99
+ return {
100
+ provider: WORKBUDDY_PROVIDER_ID,
101
+ id: model,
102
+ name: model,
103
+ context: { contextWindow: 0 },
104
+ inputModalities: ['text', 'image'],
105
+ };
106
+ }
107
+ return {
108
+ provider: WORKBUDDY_PROVIDER_ID,
109
+ id: entry.id,
110
+ name: entry.name,
111
+ ...entry.description === undefined ? {} : { description: entry.description },
112
+ ...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
113
+ context: { contextWindow: entry.contextWindow },
114
+ ...entry.reasoningEfforts === undefined || entry.reasoningEfforts.length === 0
115
+ ? {}
116
+ : { reasoning: { efforts: entry.reasoningEfforts.map((id) => ({ id: ReasoningEffortId(id), name: id })) } },
117
+ };
118
+ }
119
+ providerRetryPolicy() {
120
+ return undefined;
121
+ }
122
+ async *stream(options) {
123
+ const held = await this.options.tokens.session();
124
+ if (held === undefined)
125
+ throw new Error('WorkBuddy is not signed in');
126
+ const token = this.tokenOf(held.session);
127
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
128
+ try {
129
+ const response = await this.fetchFn(WORKBUDDY_API, {
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ 'Accept': 'text/event-stream',
134
+ ...workbuddyHeaders(token),
135
+ },
136
+ body: JSON.stringify(this.requestBody(options)),
137
+ signal: watchdog.signal,
138
+ });
139
+ if (!response.ok || response.body === null) {
140
+ throw await this.errorOf(response);
141
+ }
142
+ yield* this.readStream(response, watchdog);
143
+ }
144
+ catch (error) {
145
+ throw mapFetchFailure(WORKBUDDY_PROVIDER_NAME, error, watchdog, options.signal);
146
+ }
147
+ finally {
148
+ watchdog.stop();
149
+ }
150
+ }
151
+ /** The stored session's fields as the token the request signs with. */
152
+ tokenOf(session) {
153
+ return {
154
+ accessToken: session.accessToken,
155
+ expiresAt: session.expiresAt,
156
+ ...session.refreshToken === undefined ? {} : { refreshToken: session.refreshToken },
157
+ ...session.uid === undefined ? {} : { uid: session.uid },
158
+ ...session.enterpriseId === undefined ? {} : { enterpriseId: session.enterpriseId },
159
+ ...session.domain === undefined ? {} : { domain: session.domain },
160
+ ...session.tokenType === undefined ? {} : { tokenType: session.tokenType },
161
+ ...session.refreshExpiresAt === undefined ? {} : { refreshExpiresAt: session.refreshExpiresAt },
162
+ };
163
+ }
164
+ requestBody(options) {
165
+ const messages = [];
166
+ if (options.system !== undefined && options.system.trim() !== '') {
167
+ messages.push({ role: 'system', content: options.system });
168
+ }
169
+ for (const message of options.messages) {
170
+ messages.push(this.toChatMessage(message));
171
+ }
172
+ return {
173
+ model: options.model,
174
+ messages,
175
+ ...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },
176
+ stream: true,
177
+ };
178
+ }
179
+ toChatMessage(message) {
180
+ const role = message.role === 'assistant' ? 'assistant' : 'user';
181
+ const textParts = [];
182
+ for (const block of message.content) {
183
+ if (block.type === 'text')
184
+ textParts.push(block.text);
185
+ }
186
+ return { role, content: textParts.join('\n') };
187
+ }
188
+ /** Turn a non-2xx into an LlmError, naming WorkBuddy's specific refusals. */
189
+ async errorOf(response) {
190
+ const text = await response.text().catch(() => '');
191
+ let body = {};
192
+ try {
193
+ body = JSON.parse(text);
194
+ }
195
+ catch {
196
+ body = {};
197
+ }
198
+ const code = Number(body.code ?? body.data?.code ?? 0);
199
+ const message = String(body.msg ?? body.error?.message ?? body.message ?? text.slice(0, 200) ?? `HTTP ${response.status}`);
200
+ if (/unapproved channel|illegal api invocation/i.test(text)) {
201
+ return new Error(`${message} — WorkBuddy refuses chats from Codex and Claude Code; use another client or add another provider`);
202
+ }
203
+ if (code === 11140 || /account has been restricted due to unusual activity/i.test(text)) {
204
+ return new Error(`${message} — WorkBuddy restricted this account; it stays out until WorkBuddy lifts that`);
205
+ }
206
+ if (code === 14018 || /credits exhausted|额度已用尽/i.test(text)) {
207
+ return new Error(`${message} — WorkBuddy requires an active credit package even for 0-rate free models`);
208
+ }
209
+ const { LlmError } = await import('@deepseek-ai/dsh-llm');
210
+ return new LlmError(`${WORKBUDDY_PROVIDER_NAME} request failed (HTTP ${response.status}): ${message}`, response.status === 401 || response.status === 403 ? 'INVALID_CREDENTIAL'
211
+ : response.status === 429 ? 'RATE_LIMIT'
212
+ : response.status >= 500 ? 'SERVER' : 'HTTP_ERROR', { status: response.status });
213
+ }
214
+ async *readStream(response, watchdog) {
215
+ const reader = response.body.getReader();
216
+ const decoder = new TextDecoder();
217
+ let buffer = '';
218
+ let started = false;
219
+ try {
220
+ while (true) {
221
+ const { done, value } = await reader.read();
222
+ if (done)
223
+ break;
224
+ watchdog.pulse();
225
+ buffer += decoder.decode(value, { stream: true });
226
+ const frames = buffer.split('\n\n');
227
+ buffer = frames.pop() ?? '';
228
+ for (const frame of frames) {
229
+ const data = frame
230
+ .split('\n')
231
+ .filter((line) => line.startsWith('data:'))
232
+ .map((line) => line.slice(5).trim())
233
+ .join('\n');
234
+ if (data === '' || data === '[DONE]')
235
+ continue;
236
+ let chunk;
237
+ try {
238
+ chunk = JSON.parse(data);
239
+ }
240
+ catch {
241
+ continue;
242
+ }
243
+ const delta = chunk.choices?.[0]?.delta?.content;
244
+ if (typeof delta === 'string' && delta !== '') {
245
+ if (!started) {
246
+ yield { type: 'block-start', index: 0, blockType: 'text' };
247
+ started = true;
248
+ }
249
+ yield { type: 'text-delta', index: 0, text: delta };
250
+ }
251
+ if (chunk.usage !== undefined) {
252
+ const input = chunk.usage.prompt_tokens ?? 0;
253
+ const output = chunk.usage.completion_tokens ?? 0;
254
+ yield {
255
+ type: 'usage',
256
+ usage: { inputTokens: input, outputTokens: output, totalTokens: chunk.usage.total_tokens ?? input + output },
257
+ };
258
+ }
259
+ }
260
+ }
261
+ if (started)
262
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: '' } };
263
+ yield { type: 'finish', reason: { kind: 'stop' } };
264
+ }
265
+ finally {
266
+ reader.cancel().catch(() => { });
267
+ }
268
+ }
269
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.8.18",
3
+ "version": "0.10.0",
4
4
  "description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
5
5
  "license": "MIT",
6
6
  "author": "EricFL1998",