dsh-plugin-subscription-oauth 0.12.8 → 0.13.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/lib/providers/workbuddy.d.ts +23 -0
- package/lib/providers/workbuddy.js +237 -1
- package/package.json +1 -1
|
@@ -63,6 +63,29 @@ export declare class WorkBuddyAdapter extends LlmAdapter {
|
|
|
63
63
|
/** The stored session's fields as the token the request signs with. */
|
|
64
64
|
private tokenOf;
|
|
65
65
|
private requestBody;
|
|
66
|
+
/**
|
|
67
|
+
* Mend the tool schemas the way WorkBuddy's model gateway accepts.
|
|
68
|
+
*
|
|
69
|
+
* A tool name may hold only letters, digits, `_`, `-`; another character is refused, and
|
|
70
|
+
* a schema with a union at the parameter root or a `\0` escape is turned away by the
|
|
71
|
+
* model. Each unsafe name is made safe and the safe→original map kept on the adapter so
|
|
72
|
+
* the model's calls can be renamed back in the answer. Adapted from magpie's fitTools.
|
|
73
|
+
*
|
|
74
|
+
* @param tools - the declared tool schemas, when any.
|
|
75
|
+
* @returns the mended wire tools, or undefined when there are none.
|
|
76
|
+
*/
|
|
77
|
+
private fittedTools;
|
|
78
|
+
/** The safe→original tool names to restore in the answer. */
|
|
79
|
+
private safeToolNames;
|
|
80
|
+
/** The assistant/tool history of the request being built, for renaming past calls. */
|
|
81
|
+
private historyMessages;
|
|
82
|
+
/**
|
|
83
|
+
* Restore the original tool names the model was given, in one parsed chunk.
|
|
84
|
+
*
|
|
85
|
+
* @param value - a decoded SSE payload.
|
|
86
|
+
* @returns whether any name changed.
|
|
87
|
+
*/
|
|
88
|
+
private namedBack;
|
|
66
89
|
/** The system prompt: the caller's, else WorkBuddy's default. */
|
|
67
90
|
private systemPrompt;
|
|
68
91
|
private toChatMessage;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module dsh-plugin-subscription-oauth/providers/workbuddy
|
|
10
10
|
*/
|
|
11
|
-
import { LlmAdapter, errorChain, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
import { LlmAdapter, errorChain, ReasoningEffortId, ToolCallId, } from '@deepseek-ai/dsh-llm';
|
|
12
12
|
import { idleWatchdog, mapFetchFailure } from './common.js';
|
|
13
13
|
import { proxiedFetch } from '../http.js';
|
|
14
14
|
import { findEntry } from './catalog.js';
|
|
@@ -85,6 +85,121 @@ function randomHex() {
|
|
|
85
85
|
crypto.getRandomValues(bytes);
|
|
86
86
|
return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
87
87
|
}
|
|
88
|
+
/** A tool's name may hold letters, digits, `_`, `-`; another character is refused. */
|
|
89
|
+
const NAME_OK = /^[A-Za-z0-9_-]+$/;
|
|
90
|
+
/** A name with each character WorkBuddy refuses as `_`, kept apart from `taken`. */
|
|
91
|
+
function safeName(name, taken) {
|
|
92
|
+
const base = name.replace(/[^A-Za-z0-9_-]/g, '_') || 'tool';
|
|
93
|
+
let candidate = base;
|
|
94
|
+
for (let index = 2; taken.has(candidate); index++)
|
|
95
|
+
candidate = `${base}_${index}`;
|
|
96
|
+
taken.add(candidate);
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
/** True for a plain object usable as a JSON-schema node. */
|
|
100
|
+
function isSchemaRecord(value) {
|
|
101
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Mend a schema where the model gateway refuses one: `\0` escapes in a pattern, and an
|
|
105
|
+
* `items` list (deep models take a single shape or an anyOf, not a bare array).
|
|
106
|
+
*/
|
|
107
|
+
function fitSchema(schema, depth = 0) {
|
|
108
|
+
if (depth > 64)
|
|
109
|
+
return false;
|
|
110
|
+
let changed = false;
|
|
111
|
+
if (Array.isArray(schema)) {
|
|
112
|
+
for (const item of schema)
|
|
113
|
+
if (isSchemaRecord(item) && fitSchema(item, depth + 1))
|
|
114
|
+
changed = true;
|
|
115
|
+
return changed;
|
|
116
|
+
}
|
|
117
|
+
if (typeof schema.pattern === 'string' && schema.pattern.includes('\\0')) {
|
|
118
|
+
schema.pattern = schema.pattern.replace(/\\0/g, '\\u0000');
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(schema.items)) {
|
|
122
|
+
const list = schema.items.filter(isSchemaRecord);
|
|
123
|
+
schema.items = list.length === 0 ? {} : list.length === 1 ? list[0] : { anyOf: list };
|
|
124
|
+
delete schema.additionalItems;
|
|
125
|
+
changed = true;
|
|
126
|
+
}
|
|
127
|
+
for (const value of Object.values(schema)) {
|
|
128
|
+
if (isSchemaRecord(value) && fitSchema(value, depth + 1))
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
return changed;
|
|
132
|
+
}
|
|
133
|
+
/** The schema a local `$ref` names, or `node` itself. */
|
|
134
|
+
function localRef(root, node) {
|
|
135
|
+
const ref = node.$ref;
|
|
136
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/'))
|
|
137
|
+
return node;
|
|
138
|
+
let at = root;
|
|
139
|
+
for (const key of ref.slice(2).split('/')) {
|
|
140
|
+
at = isSchemaRecord(at) ? at[key.replaceAll('~1', '/').replaceAll('~0', '~')] : undefined;
|
|
141
|
+
}
|
|
142
|
+
return isSchemaRecord(at) ? at : node;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Make a tool's parameters an object at the root, the shape every model takes: `allOf`'s
|
|
146
|
+
* branches are merged whole; of `anyOf`/`oneOf`'s object branches the properties are
|
|
147
|
+
* merged and a field each requires stays required.
|
|
148
|
+
*/
|
|
149
|
+
function objectRoot(node, root = node, depth = 0) {
|
|
150
|
+
const unions = ['allOf', 'anyOf', 'oneOf'].filter((key) => Array.isArray(node[key]));
|
|
151
|
+
if (node.type === 'object' && unions.length === 0)
|
|
152
|
+
return false;
|
|
153
|
+
const properties = isSchemaRecord(node.properties) ? { ...node.properties } : {};
|
|
154
|
+
const required = Array.isArray(node.required) ? [...node.required] : [];
|
|
155
|
+
for (const branch0 of Array.isArray(node.allOf) ? node.allOf : []) {
|
|
156
|
+
let branch = localRef(root, isSchemaRecord(branch0) ? branch0 : {});
|
|
157
|
+
if (unions.length > 0 && depth < 8)
|
|
158
|
+
objectRoot((branch = structuredClone(branch)), root, depth + 1);
|
|
159
|
+
if (isSchemaRecord(branch) && isSchemaRecord(branch.properties)) {
|
|
160
|
+
for (const [key, value] of Object.entries(branch.properties))
|
|
161
|
+
if (!(key in properties))
|
|
162
|
+
properties[key] = value;
|
|
163
|
+
}
|
|
164
|
+
if (isSchemaRecord(branch) && Array.isArray(branch.required))
|
|
165
|
+
required.push(...branch.required);
|
|
166
|
+
}
|
|
167
|
+
const branches = [];
|
|
168
|
+
for (const key of ['anyOf', 'oneOf']) {
|
|
169
|
+
for (const branch0 of Array.isArray(node[key]) ? node[key] : []) {
|
|
170
|
+
let branch = localRef(root, isSchemaRecord(branch0) ? branch0 : {});
|
|
171
|
+
if (unions.length > 0 && depth < 8)
|
|
172
|
+
objectRoot((branch = structuredClone(branch)), root, depth + 1);
|
|
173
|
+
if (isSchemaRecord(branch) && (branch.type === 'object' || isSchemaRecord(branch.properties)))
|
|
174
|
+
branches.push(branch);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const from = new Map();
|
|
178
|
+
for (const branch of branches) {
|
|
179
|
+
if (!isSchemaRecord(branch.properties))
|
|
180
|
+
continue;
|
|
181
|
+
for (const [key, value] of Object.entries(branch.properties)) {
|
|
182
|
+
if (key in properties)
|
|
183
|
+
continue;
|
|
184
|
+
const seen = from.get(key) ?? [];
|
|
185
|
+
if (!seen.some((entry) => JSON.stringify(entry) === JSON.stringify(value)))
|
|
186
|
+
seen.push(value);
|
|
187
|
+
from.set(key, seen);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const [key, list] of from)
|
|
191
|
+
properties[key] = list.length === 1 ? list[0] : { anyOf: list };
|
|
192
|
+
delete node.allOf;
|
|
193
|
+
delete node.anyOf;
|
|
194
|
+
delete node.oneOf;
|
|
195
|
+
node.type = 'object';
|
|
196
|
+
node.properties = properties;
|
|
197
|
+
if (required.length > 0)
|
|
198
|
+
node.required = [...new Set(required)];
|
|
199
|
+
else
|
|
200
|
+
delete node.required;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
88
203
|
/** The WorkBuddy China wire adapter serving the `workbuddy` route. */
|
|
89
204
|
export class WorkBuddyAdapter extends LlmAdapter {
|
|
90
205
|
options;
|
|
@@ -159,6 +274,9 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
159
274
|
throw new Error('WorkBuddy is not signed in');
|
|
160
275
|
const token = this.tokenOf(held.session);
|
|
161
276
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
277
|
+
// Capture the tool-call history so unsafe tool names in past turns can be renamed to
|
|
278
|
+
// the safe ones the model now sees, keeping the conversation consistent.
|
|
279
|
+
this.historyMessages = options.messages.filter((message) => message.role === 'assistant' || message.role === 'tool');
|
|
162
280
|
try {
|
|
163
281
|
const body = this.requestBody(options);
|
|
164
282
|
const response = await this.fetchFn(WORKBUDDY_API, {
|
|
@@ -215,13 +333,108 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
215
333
|
// Claude Code's own) out of every message — WorkBuddy reads them all, whatever role.
|
|
216
334
|
const system = this.systemPrompt(options);
|
|
217
335
|
messages.unshift({ role: 'system', content: system });
|
|
336
|
+
const tools = this.fittedTools(options.tools);
|
|
218
337
|
return {
|
|
219
338
|
model: options.model,
|
|
220
339
|
messages: unflagMessages(messages),
|
|
340
|
+
...tools === undefined ? {} : { tools: tools.tools },
|
|
221
341
|
...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },
|
|
222
342
|
stream: true,
|
|
223
343
|
};
|
|
224
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Mend the tool schemas the way WorkBuddy's model gateway accepts.
|
|
347
|
+
*
|
|
348
|
+
* A tool name may hold only letters, digits, `_`, `-`; another character is refused, and
|
|
349
|
+
* a schema with a union at the parameter root or a `\0` escape is turned away by the
|
|
350
|
+
* model. Each unsafe name is made safe and the safe→original map kept on the adapter so
|
|
351
|
+
* the model's calls can be renamed back in the answer. Adapted from magpie's fitTools.
|
|
352
|
+
*
|
|
353
|
+
* @param tools - the declared tool schemas, when any.
|
|
354
|
+
* @returns the mended wire tools, or undefined when there are none.
|
|
355
|
+
*/
|
|
356
|
+
fittedTools(tools) {
|
|
357
|
+
if (tools === undefined || tools.length === 0)
|
|
358
|
+
return undefined;
|
|
359
|
+
const wire = tools.map((tool) => ({
|
|
360
|
+
type: 'function',
|
|
361
|
+
function: {
|
|
362
|
+
name: tool.name,
|
|
363
|
+
...tool.description === '' ? {} : { description: tool.description },
|
|
364
|
+
...tool.parameters === undefined ? {} : { parameters: structuredClone(tool.parameters) },
|
|
365
|
+
},
|
|
366
|
+
}));
|
|
367
|
+
const taken = new Set();
|
|
368
|
+
for (const tool of wire) {
|
|
369
|
+
const name = tool.function.name;
|
|
370
|
+
if (typeof name === 'string' && NAME_OK.test(name))
|
|
371
|
+
taken.add(name);
|
|
372
|
+
}
|
|
373
|
+
const names = new Map();
|
|
374
|
+
const theirs = new Map();
|
|
375
|
+
for (const tool of wire) {
|
|
376
|
+
const fn = tool.function;
|
|
377
|
+
if (typeof fn.name === 'string' && !NAME_OK.test(fn.name)) {
|
|
378
|
+
const safe = safeName(fn.name, taken);
|
|
379
|
+
names.set(safe, fn.name);
|
|
380
|
+
theirs.set(fn.name, safe);
|
|
381
|
+
fn.name = safe;
|
|
382
|
+
}
|
|
383
|
+
if (fn.parameters !== undefined) {
|
|
384
|
+
if (!isSchemaRecord(fn.parameters))
|
|
385
|
+
fn.parameters = { type: 'object', properties: {} };
|
|
386
|
+
else {
|
|
387
|
+
fitSchema(fn.parameters);
|
|
388
|
+
objectRoot(fn.parameters);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Rename the calls in the conversation history to the safe names, so a turn that
|
|
393
|
+
// already called a tool matches the name the model now sees.
|
|
394
|
+
for (const message of this.historyMessages ?? []) {
|
|
395
|
+
for (const call of Array.isArray(message.tool_calls) ? message.tool_calls : []) {
|
|
396
|
+
const fn = call?.function;
|
|
397
|
+
if (fn && typeof fn.name === 'string' && theirs.has(fn.name))
|
|
398
|
+
fn.name = theirs.get(fn.name);
|
|
399
|
+
}
|
|
400
|
+
if (message.role === 'tool' && typeof message.name === 'string' && theirs.has(message.name)) {
|
|
401
|
+
message.name = theirs.get(message.name);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
this.safeToolNames = names;
|
|
405
|
+
return { tools: wire, names };
|
|
406
|
+
}
|
|
407
|
+
/** The safe→original tool names to restore in the answer. */
|
|
408
|
+
safeToolNames = new Map();
|
|
409
|
+
/** The assistant/tool history of the request being built, for renaming past calls. */
|
|
410
|
+
historyMessages = [];
|
|
411
|
+
/**
|
|
412
|
+
* Restore the original tool names the model was given, in one parsed chunk.
|
|
413
|
+
*
|
|
414
|
+
* @param value - a decoded SSE payload.
|
|
415
|
+
* @returns whether any name changed.
|
|
416
|
+
*/
|
|
417
|
+
namedBack(value) {
|
|
418
|
+
let changed = false;
|
|
419
|
+
for (const choice of Array.isArray(value.choices) ? value.choices : []) {
|
|
420
|
+
if (!isSchemaRecord(choice))
|
|
421
|
+
continue;
|
|
422
|
+
for (const at of [choice.message, choice.delta]) {
|
|
423
|
+
if (!isSchemaRecord(at))
|
|
424
|
+
continue;
|
|
425
|
+
for (const call of Array.isArray(at.tool_calls) ? at.tool_calls : []) {
|
|
426
|
+
if (!isSchemaRecord(call) || !isSchemaRecord(call.function))
|
|
427
|
+
continue;
|
|
428
|
+
const original = this.safeToolNames.get(String(call.function.name));
|
|
429
|
+
if (original !== undefined) {
|
|
430
|
+
call.function.name = original;
|
|
431
|
+
changed = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return changed;
|
|
437
|
+
}
|
|
225
438
|
/** The system prompt: the caller's, else WorkBuddy's default. */
|
|
226
439
|
systemPrompt(options) {
|
|
227
440
|
if (options.system !== undefined && options.system.trim() !== '')
|
|
@@ -316,6 +529,29 @@ export class WorkBuddyAdapter extends LlmAdapter {
|
|
|
316
529
|
const msg = chunk.msg;
|
|
317
530
|
throw new Error(`WorkBuddy: ${typeof msg === 'string' ? msg : `error ${wireCode}`}`);
|
|
318
531
|
}
|
|
532
|
+
// Restore the tool names the model called to the ones the client declared, before
|
|
533
|
+
// the chunk is read as a stream event.
|
|
534
|
+
this.namedBack(chunk);
|
|
535
|
+
// A tool-call delta: emit it so the harness can run the tool. The id and name ride
|
|
536
|
+
// the first delta of each call; later deltas carry argument fragments.
|
|
537
|
+
const toolCalls = chunk.choices?.[0]?.delta?.tool_calls;
|
|
538
|
+
if (Array.isArray(toolCalls)) {
|
|
539
|
+
for (const call of toolCalls) {
|
|
540
|
+
const fn = call?.function;
|
|
541
|
+
if (fn === undefined || fn === null)
|
|
542
|
+
continue;
|
|
543
|
+
const index = call.index ?? 0;
|
|
544
|
+
const id = ToolCallId(call.id ?? `wb-tool-${index}`);
|
|
545
|
+
const name = fn.name;
|
|
546
|
+
const args = fn.arguments;
|
|
547
|
+
if (typeof name === 'string' && name !== '') {
|
|
548
|
+
yield { type: 'tool-call-delta', index, id, name, argumentsDelta: '' };
|
|
549
|
+
}
|
|
550
|
+
if (typeof args === 'string' && args !== '') {
|
|
551
|
+
yield { type: 'tool-call-delta', index, id, argumentsDelta: args };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
319
555
|
const delta = chunk.choices?.[0]?.delta?.content;
|
|
320
556
|
if (typeof delta === 'string' && delta !== '') {
|
|
321
557
|
if (!started) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|