dsh-better-copilot 0.1.0 → 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/CHANGELOG.md +18 -0
- package/README.md +9 -6
- package/dist/guard.d.ts +37 -2
- package/dist/guard.js +82 -1
- package/dist/identity.d.ts +20 -0
- package/dist/identity.js +53 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/plugin.js +18 -20
- package/dist/schema.d.ts +18 -1
- package/dist/schema.js +31 -28
- package/package.json +31 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1 — DSH 0.2.2-rc.1 compatibility
|
|
4
|
+
|
|
5
|
+
- Retarget the manifest, peer ranges and development pins to DSH `0.2.2-rc.1`
|
|
6
|
+
with `cordis ~4.1.0` and `schemastery ~3.19.0`.
|
|
7
|
+
- Move the Gemini tool-declaration guard from the `system-prompt/assemble`
|
|
8
|
+
waterfall to the Copilot wire. `0.2.2-rc.1` assembles tool declarations
|
|
9
|
+
outside the presentation waterfall, so the guard now rewrites the serialized
|
|
10
|
+
request body inside the same refcounted interceptor that carries the wire
|
|
11
|
+
identity. It still adds only a missing root `properties` bucket, still matches
|
|
12
|
+
configured providers and model families, keeps `SKIPPED_*` reason codes and
|
|
13
|
+
`logFixes` summaries, and now runs even when `DSH_COPILOT_WIRE_IDENTITY=off`.
|
|
14
|
+
- Export `installCopilotRequestTransform`, `guardCopilotRequestBody`,
|
|
15
|
+
`GuardedBody` and `MAX_GUARDED_BODY_BYTES`; `./guard` keeps exporting
|
|
16
|
+
`matchesRoute`, `normalizeToolSchemas`, `classifyParameters`,
|
|
17
|
+
`TOOL_SCHEMA_SKIPPED` and their types.
|
|
18
|
+
- Follow the adapter's `1_000_000`/`128_000` profile defaults, its
|
|
19
|
+
`llm-pi-ai/request-headers` waterfall and the branded reasoning-effort id.
|
|
20
|
+
|
|
3
21
|
## 0.1.0 — consolidated Copilot plugin
|
|
4
22
|
|
|
5
23
|
First release of `dsh-better-copilot`, merging three independently installable
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ packages. Their behaviour is consolidated here; see the migration notes below.
|
|
|
19
19
|
|
|
20
20
|
## Compatibility
|
|
21
21
|
|
|
22
|
-
- DSH and DSH peer packages: `0.2.
|
|
22
|
+
- DSH and DSH peer packages: `0.2.2-rc.1`.
|
|
23
23
|
- `@earendil-works/pi-ai`: `^1.1.0` (host-aligned peer range; tested at `1.1.0` and `1.3.0`).
|
|
24
24
|
- Node.js: `>=22.19.0`.
|
|
25
25
|
|
|
@@ -92,11 +92,14 @@ existing Web search settings card is DeepSeek-specific and is not a chooser.
|
|
|
92
92
|
|
|
93
93
|
## Guard
|
|
94
94
|
|
|
95
|
-
The guard is
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
The guard is a Copilot wire-body transform. `0.2.2-rc.1` assembles tool
|
|
96
|
+
declarations outside the presentation waterfall and treats them as
|
|
97
|
+
provider-owned, so the last supported seam is the serialized request the adapter
|
|
98
|
+
dispatches. Inside the same refcounted, trusted-path-checked interceptor that
|
|
99
|
+
carries the wire identity, it adds an empty root `properties` bucket to object
|
|
100
|
+
tool schemas that lack one for a configured Copilot route and model family, and
|
|
101
|
+
leaves every other byte of the request untouched. It never replaces an adapter or
|
|
102
|
+
another plugin's settings, and a guard failure dispatches the original body.
|
|
100
103
|
|
|
101
104
|
## Migration from the three predecessor packages
|
|
102
105
|
|
package/dist/guard.d.ts
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import type { GuardSettings, SkippedTool } from './guard-types.js';
|
|
2
|
+
export { classifyParameters, matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
|
|
3
|
+
export type { NormalizeResult, ParameterDecision } from './schema.js';
|
|
3
4
|
export type { GuardSettings, PromptAssembly, SkippedTool, ToolSchema } from './guard-types.js';
|
|
5
|
+
/**
|
|
6
|
+
* The largest serialized Copilot request this guard will parse. A request that
|
|
7
|
+
* carries tool declarations is far smaller; anything past this bound is left
|
|
8
|
+
* byte-identical rather than copied and re-parsed.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_GUARDED_BODY_BYTES: number;
|
|
11
|
+
/** One rewritten request body plus what the guard changed and refused to change. */
|
|
12
|
+
export interface GuardedBody {
|
|
13
|
+
/** The re-serialized body, or `undefined` when the request stays byte-identical. */
|
|
14
|
+
readonly body: string | undefined;
|
|
15
|
+
/** True only when at least one declaration gained a properties bucket. */
|
|
16
|
+
readonly changed: boolean;
|
|
17
|
+
/** Tool names whose declaration gained an empty properties bucket. */
|
|
18
|
+
readonly fixed: string[];
|
|
19
|
+
/** Tools left byte-identical, with a controlled reason. */
|
|
20
|
+
readonly skipped: readonly SkippedTool[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize tool declarations on one serialized Copilot inference request.
|
|
24
|
+
*
|
|
25
|
+
* The Gemini route measures this at the HTTP boundary: it rejects a request whose
|
|
26
|
+
* tool declaration carries an object parameter schema with no properties bucket.
|
|
27
|
+
* The presentation assembly no longer exposes declarations to listeners, so the
|
|
28
|
+
* guard rewrites the exact serialized body instead — lexically the last point
|
|
29
|
+
* before dispatch, and the same place the failure is measured.
|
|
30
|
+
*
|
|
31
|
+
* Nothing is invented: a body that is not JSON, carries no tool array, exceeds
|
|
32
|
+
* the bound, or targets another route or model family is returned untouched.
|
|
33
|
+
*
|
|
34
|
+
* @param body - the serialized request body as the adapter built it.
|
|
35
|
+
* @param settings - one configuration snapshot for this request.
|
|
36
|
+
* @returns the rewrite result; `body` stays `undefined` unless something changed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function guardCopilotRequestBody(body: string, settings: GuardSettings): GuardedBody;
|
package/dist/guard.js
CHANGED
|
@@ -1 +1,82 @@
|
|
|
1
|
-
|
|
1
|
+
import { classifyParameters, matchesRoute } from './schema.js';
|
|
2
|
+
// The guard subpath keeps publishing its classification primitives: they are the
|
|
3
|
+
// same rules the wire rewrite applies, and consumers may classify a declaration
|
|
4
|
+
// without installing the transform.
|
|
5
|
+
export { classifyParameters, matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* The largest serialized Copilot request this guard will parse. A request that
|
|
8
|
+
* carries tool declarations is far smaller; anything past this bound is left
|
|
9
|
+
* byte-identical rather than copied and re-parsed.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_GUARDED_BODY_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
/** The route this plugin owns; the wire guard runs only on its endpoints. */
|
|
13
|
+
const COPILOT_ROUTE = 'github-copilot';
|
|
14
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
/**
|
|
16
|
+
* Locate one tool declaration's parameter schema on either Copilot inference surface.
|
|
17
|
+
*
|
|
18
|
+
* Chat completions carries `{ type: 'function', function: { name, parameters } }`
|
|
19
|
+
* while Responses carries `{ type: 'function', name, parameters }`. Both are the
|
|
20
|
+
* OpenAI-compatible shape the adapter serializes; anything else is not ours.
|
|
21
|
+
*/
|
|
22
|
+
function declarationOf(tool) {
|
|
23
|
+
if (!isRecord(tool) || tool.type !== 'function')
|
|
24
|
+
return undefined;
|
|
25
|
+
const holder = isRecord(tool.function) ? tool.function : tool;
|
|
26
|
+
return typeof holder.name === 'string' && holder.name.length > 0 ? { holder, name: holder.name } : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Normalize tool declarations on one serialized Copilot inference request.
|
|
30
|
+
*
|
|
31
|
+
* The Gemini route measures this at the HTTP boundary: it rejects a request whose
|
|
32
|
+
* tool declaration carries an object parameter schema with no properties bucket.
|
|
33
|
+
* The presentation assembly no longer exposes declarations to listeners, so the
|
|
34
|
+
* guard rewrites the exact serialized body instead — lexically the last point
|
|
35
|
+
* before dispatch, and the same place the failure is measured.
|
|
36
|
+
*
|
|
37
|
+
* Nothing is invented: a body that is not JSON, carries no tool array, exceeds
|
|
38
|
+
* the bound, or targets another route or model family is returned untouched.
|
|
39
|
+
*
|
|
40
|
+
* @param body - the serialized request body as the adapter built it.
|
|
41
|
+
* @param settings - one configuration snapshot for this request.
|
|
42
|
+
* @returns the rewrite result; `body` stays `undefined` unless something changed.
|
|
43
|
+
*/
|
|
44
|
+
export function guardCopilotRequestBody(body, settings) {
|
|
45
|
+
const unchanged = { body: undefined, changed: false, fixed: [], skipped: [] };
|
|
46
|
+
if (typeof body !== 'string' || body.length === 0 || body.length > MAX_GUARDED_BODY_BYTES)
|
|
47
|
+
return unchanged;
|
|
48
|
+
// A cheap pre-check keeps message-heavy bodies out of the JSON parser entirely.
|
|
49
|
+
if (!body.includes('"tools"'))
|
|
50
|
+
return unchanged;
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return unchanged;
|
|
57
|
+
}
|
|
58
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.tools) || parsed.tools.length === 0)
|
|
59
|
+
return unchanged;
|
|
60
|
+
if (!matchesRoute(COPILOT_ROUTE, parsed.model, settings))
|
|
61
|
+
return unchanged;
|
|
62
|
+
const declarations = parsed.tools.map(declarationOf);
|
|
63
|
+
if (declarations.every(declaration => declaration === undefined))
|
|
64
|
+
return unchanged;
|
|
65
|
+
const fixed = [];
|
|
66
|
+
const skipped = [];
|
|
67
|
+
for (const declaration of declarations) {
|
|
68
|
+
if (declaration === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
const decision = classifyParameters(declaration.holder.parameters);
|
|
71
|
+
if (decision.fix) {
|
|
72
|
+
fixed.push(declaration.name);
|
|
73
|
+
declaration.holder.parameters = { ...declaration.holder.parameters, properties: {} };
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (decision.reason !== undefined)
|
|
77
|
+
skipped.push({ tool: declaration.name, reason: decision.reason });
|
|
78
|
+
}
|
|
79
|
+
if (fixed.length === 0)
|
|
80
|
+
return { body: undefined, changed: false, fixed, skipped };
|
|
81
|
+
return { body: JSON.stringify(parsed), changed: true, fixed, skipped };
|
|
82
|
+
}
|
package/dist/identity.d.ts
CHANGED
|
@@ -49,6 +49,16 @@ export declare function isCopilotRequestPath(pathname: string): boolean;
|
|
|
49
49
|
export declare function rewriteCopilotRequestHeaders(url: URL, headers: Headers, identity: CopilotWireIdentity): boolean;
|
|
50
50
|
/** `full` is the default; `basic` is the minimal fingerprint fix; `0` disables. */
|
|
51
51
|
export declare function wireIdentityMode(env?: NodeJS.ProcessEnv): WireIdentitySetting;
|
|
52
|
+
/**
|
|
53
|
+
* Rewrite one serialized Copilot request body before dispatch.
|
|
54
|
+
*
|
|
55
|
+
* Returning a string replaces the body; `undefined` keeps it byte-identical.
|
|
56
|
+
* A throw is contained like every other pre-dispatch normalization failure.
|
|
57
|
+
*/
|
|
58
|
+
export type CopilotRequestTransform = (request: {
|
|
59
|
+
readonly url: URL;
|
|
60
|
+
readonly body: string;
|
|
61
|
+
}) => string | undefined;
|
|
52
62
|
/**
|
|
53
63
|
* Install a process-wide Copilot header interceptor.
|
|
54
64
|
*
|
|
@@ -59,3 +69,13 @@ export declare function wireIdentityMode(env?: NodeJS.ProcessEnv): WireIdentityS
|
|
|
59
69
|
* current fetch rather than reusing stale installation state.
|
|
60
70
|
*/
|
|
61
71
|
export declare function installCopilotIdentityHeaders(identity?: CopilotWireIdentity): () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Install a process-wide Copilot request-body normalizer.
|
|
74
|
+
*
|
|
75
|
+
* It shares the header interceptor's controller, trust checks and release
|
|
76
|
+
* discipline, so both halves are installed once and released independently. A
|
|
77
|
+
* body is normalized only when the request is a trusted Copilot inference call
|
|
78
|
+
* and the caller passes a serialized string body; every other shape is
|
|
79
|
+
* delegated untouched.
|
|
80
|
+
*/
|
|
81
|
+
export declare function installCopilotRequestTransform(transform: CopilotRequestTransform): () => void;
|
package/dist/identity.js
CHANGED
|
@@ -196,19 +196,29 @@ function createWrapper(state, downstream) {
|
|
|
196
196
|
return downstream(input, init);
|
|
197
197
|
let url;
|
|
198
198
|
let headers;
|
|
199
|
+
let body;
|
|
199
200
|
try {
|
|
200
201
|
url = requestUrl(input);
|
|
201
202
|
if (!isTrustedCopilotUrl(url) || !isCopilotRequestPath(url.pathname))
|
|
202
203
|
return downstream(input, init);
|
|
203
204
|
headers = effectiveHeaders(input, init);
|
|
204
|
-
|
|
205
|
+
if (state.identity !== null)
|
|
206
|
+
rewriteCopilotRequestHeaders(url, headers, state.identity);
|
|
207
|
+
if (state.transform !== null && typeof init?.body === 'string') {
|
|
208
|
+
body = state.transform({ url, body: init.body });
|
|
209
|
+
// A replaced body usually changes length, so its framing cannot be reused.
|
|
210
|
+
if (body !== undefined) {
|
|
211
|
+
headers.delete('content-length');
|
|
212
|
+
headers.delete('transfer-encoding');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
205
215
|
}
|
|
206
216
|
catch {
|
|
207
217
|
// Only pre-dispatch normalization failures are fail-open; the request is
|
|
208
218
|
// delegated once and its eventual rejection is never caught or retried.
|
|
209
219
|
return downstream(input, init);
|
|
210
220
|
}
|
|
211
|
-
return downstream(input, { ...init, headers });
|
|
221
|
+
return downstream(input, body === undefined ? { ...init, headers } : { ...init, headers, body });
|
|
212
222
|
};
|
|
213
223
|
state.wrappers.add(wrapper);
|
|
214
224
|
state.downstreams.set(wrapper, downstream);
|
|
@@ -237,34 +247,70 @@ function attach(state) {
|
|
|
237
247
|
* current fetch rather than reusing stale installation state.
|
|
238
248
|
*/
|
|
239
249
|
export function installCopilotIdentityHeaders(identity = createCopilotWireIdentity()) {
|
|
250
|
+
return install({ identity });
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Install a process-wide Copilot request-body normalizer.
|
|
254
|
+
*
|
|
255
|
+
* It shares the header interceptor's controller, trust checks and release
|
|
256
|
+
* discipline, so both halves are installed once and released independently. A
|
|
257
|
+
* body is normalized only when the request is a trusted Copilot inference call
|
|
258
|
+
* and the caller passes a serialized string body; every other shape is
|
|
259
|
+
* delegated untouched.
|
|
260
|
+
*/
|
|
261
|
+
export function installCopilotRequestTransform(transform) {
|
|
262
|
+
return install({ transform });
|
|
263
|
+
}
|
|
264
|
+
function install(options) {
|
|
240
265
|
if (typeof globalThis.fetch !== 'function')
|
|
241
266
|
return () => { };
|
|
242
267
|
if (controller === undefined) {
|
|
243
268
|
controller = {
|
|
244
269
|
active: true,
|
|
245
|
-
identity,
|
|
270
|
+
identity: null,
|
|
271
|
+
transform: null,
|
|
246
272
|
installs: 0,
|
|
273
|
+
transforms: 0,
|
|
247
274
|
wrappers: new WeakSet(),
|
|
248
275
|
downstreams: new WeakMap(),
|
|
249
276
|
};
|
|
250
277
|
}
|
|
251
278
|
const state = controller;
|
|
279
|
+
const ownsIdentity = options.identity !== undefined, ownsTransform = options.transform !== undefined;
|
|
280
|
+
// The first live identity install owns it; later concurrent installs share it.
|
|
281
|
+
if (ownsIdentity && state.installs === 0)
|
|
282
|
+
state.identity = options.identity;
|
|
283
|
+
// A body transform is per installer, and the latest live one wins.
|
|
284
|
+
if (ownsTransform)
|
|
285
|
+
state.transform = options.transform;
|
|
252
286
|
try {
|
|
253
287
|
attach(state);
|
|
254
288
|
}
|
|
255
289
|
catch (error) {
|
|
256
|
-
if (state.installs === 0 && controller === state)
|
|
290
|
+
if (state.installs === 0 && state.transforms === 0 && controller === state)
|
|
257
291
|
controller = undefined;
|
|
258
292
|
throw error;
|
|
259
293
|
}
|
|
260
|
-
|
|
294
|
+
if (ownsIdentity)
|
|
295
|
+
state.installs += 1;
|
|
296
|
+
if (ownsTransform)
|
|
297
|
+
state.transforms += 1;
|
|
261
298
|
let released = false;
|
|
262
299
|
return () => {
|
|
263
300
|
if (released)
|
|
264
301
|
return;
|
|
265
302
|
released = true;
|
|
266
|
-
|
|
267
|
-
|
|
303
|
+
if (ownsIdentity) {
|
|
304
|
+
state.installs -= 1;
|
|
305
|
+
if (state.installs === 0)
|
|
306
|
+
state.identity = null;
|
|
307
|
+
}
|
|
308
|
+
if (ownsTransform) {
|
|
309
|
+
state.transforms -= 1;
|
|
310
|
+
if (state.transforms === 0)
|
|
311
|
+
state.transform = null;
|
|
312
|
+
}
|
|
313
|
+
if (state.installs > 0 || state.transforms > 0)
|
|
268
314
|
return;
|
|
269
315
|
state.active = false;
|
|
270
316
|
if (state.current !== undefined && globalThis.fetch === state.current.wrapper) {
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export declare const syncCopilotCatalog: (credentials?: CredentialReader, authOp
|
|
|
9
9
|
export { DEFAULT_SEARCH_MODEL } from './config.js';
|
|
10
10
|
export { liveCost, selectAccountModels } from './catalog.js';
|
|
11
11
|
export { DiscoveryError } from './discovery.js';
|
|
12
|
-
export { createCopilotWireIdentity, installCopilotIdentityHeaders, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
|
|
12
|
+
export { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilotRequestTransform, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
|
|
13
|
+
export { guardCopilotRequestBody, MAX_GUARDED_BODY_BYTES } from './guard.js';
|
|
14
|
+
export type { GuardedBody } from './guard.js';
|
|
13
15
|
export { COPILOT_SEARCH_PROVIDER_ID, CopilotSearchProvider } from './provider.js';
|
|
14
16
|
export { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
|
|
15
17
|
export { loadRuntime } from './runtime.js';
|
|
@@ -19,6 +21,6 @@ export type { BetterCopilotDependencies, BetterCopilotPlugin } from './plugin.js
|
|
|
19
21
|
export type { AccountModelOptions } from './catalog.js';
|
|
20
22
|
export type { NormalizeResult } from './schema.js';
|
|
21
23
|
export type { AccountSelection, CopilotAuthOptions as CatalogAuthOptions, CredentialReader, ModelCatalog, SyncResult, } from './catalog-types.js';
|
|
22
|
-
export type { CopilotWireIdentity, CopilotWireIdentityOverrides, WireIdentityMode, WireIdentitySetting, } from './identity.js';
|
|
24
|
+
export type { CopilotRequestTransform, CopilotWireIdentity, CopilotWireIdentityOverrides, WireIdentityMode, WireIdentitySetting, } from './identity.js';
|
|
23
25
|
export type { CopilotAuthOptions as SearchAuthOptions, CopilotSearchOptions, ResolvedCopilotAuth, SearchMode, } from './search-types.js';
|
|
24
26
|
export type { GuardSettings, PromptAssembly, SkippedTool, ToolSchema } from './guard-types.js';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ export const syncCopilotCatalog = (credentials, authOptions) => plugin.sync(cred
|
|
|
11
11
|
export { DEFAULT_SEARCH_MODEL } from './config.js';
|
|
12
12
|
export { liveCost, selectAccountModels } from './catalog.js';
|
|
13
13
|
export { DiscoveryError } from './discovery.js';
|
|
14
|
-
export { createCopilotWireIdentity, installCopilotIdentityHeaders, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
|
|
14
|
+
export { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilotRequestTransform, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
|
|
15
|
+
export { guardCopilotRequestBody, MAX_GUARDED_BODY_BYTES } from './guard.js';
|
|
15
16
|
export { COPILOT_SEARCH_PROVIDER_ID, CopilotSearchProvider } from './provider.js';
|
|
16
17
|
export { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
|
|
17
18
|
export { loadRuntime } from './runtime.js';
|
package/dist/plugin.js
CHANGED
|
@@ -2,9 +2,10 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
|
|
|
2
2
|
import { createCopilotAuthResolver } from './auth.js';
|
|
3
3
|
import { createConfig } from './config.js';
|
|
4
4
|
import { DiscoveryError, syncCopilotCatalog } from './discovery.js';
|
|
5
|
-
import { createCopilotWireIdentity, installCopilotIdentityHeaders, wireIdentityMode } from './identity.js';
|
|
5
|
+
import { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilotRequestTransform, wireIdentityMode } from './identity.js';
|
|
6
6
|
import { CopilotSearchProvider } from './provider.js';
|
|
7
|
-
import {
|
|
7
|
+
import { TOOL_SCHEMA_SKIPPED } from './schema.js';
|
|
8
|
+
import { guardCopilotRequestBody } from './guard.js';
|
|
8
9
|
export const name = 'better-copilot';
|
|
9
10
|
/** Search and guard are optional capabilities; the adapter requires these two. */
|
|
10
11
|
export const inject = ['llm', 'credentials'];
|
|
@@ -89,33 +90,30 @@ export function createPlugin(dependencies) {
|
|
|
89
90
|
ctx.effect(() => install(), 'copilot: wire identity headers');
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
// 2. Gemini tool-declaration guard:
|
|
93
|
+
// 2. Gemini tool-declaration guard: one contained wire transform. The
|
|
94
|
+
// assembly waterfall no longer sees provider-owned declarations, so the
|
|
95
|
+
// guard rewrites the serialized request at the boundary that measures
|
|
96
|
+
// the defect. It is installed even when wire identity is opted out.
|
|
93
97
|
contain('tool-schema guard', 'Gemini tool-schema normalization', () => {
|
|
94
|
-
|
|
95
|
-
// An upstream failure belongs to the downstream chain and propagates unchanged.
|
|
96
|
-
const downstream = await next();
|
|
98
|
+
const transform = ({ body }) => {
|
|
97
99
|
try {
|
|
98
100
|
const settings = guardSettings(config.toolSchema);
|
|
99
|
-
|
|
100
|
-
return downstream;
|
|
101
|
-
const result = normalizeToolSchemas(downstream.tools);
|
|
101
|
+
const result = guardCopilotRequestBody(body, settings);
|
|
102
102
|
if (settings.logFixes) {
|
|
103
|
-
if (result.fixed.length > 0)
|
|
104
|
-
log('info', 'better-copilot: added a properties bucket to %d tool schema(s)
|
|
105
|
-
|
|
106
|
-
if (result.skipped.length > 0) {
|
|
103
|
+
if (result.fixed.length > 0)
|
|
104
|
+
log('info', 'better-copilot: added a properties bucket to %d tool schema(s) on the Copilot wire', result.fixed.length);
|
|
105
|
+
if (result.skipped.length > 0)
|
|
107
106
|
log('warn', 'better-copilot: %s left %d tool schema(s) unchanged: %s', TOOL_SCHEMA_SKIPPED, result.skipped.length, JSON.stringify(result.skipped));
|
|
108
|
-
}
|
|
109
107
|
}
|
|
110
|
-
|
|
111
|
-
return downstream;
|
|
112
|
-
return { ...downstream, tools: result.tools };
|
|
108
|
+
return result.body;
|
|
113
109
|
}
|
|
114
110
|
catch {
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
// The request is delegated byte-identical; a guard failure never fails a call.
|
|
112
|
+
log('warn', 'better-copilot: skipped one request after an internal error');
|
|
113
|
+
return undefined;
|
|
117
114
|
}
|
|
118
|
-
}
|
|
115
|
+
};
|
|
116
|
+
ctx.effect(() => installCopilotRequestTransform(transform), 'copilot: tool-schema wire normalization');
|
|
119
117
|
});
|
|
120
118
|
// 3. Search provider: mounts only when the web service exists.
|
|
121
119
|
contain('search provider', 'Copilot web search', () => {
|
package/dist/schema.d.ts
CHANGED
|
@@ -11,14 +11,31 @@ export interface NormalizeResult {
|
|
|
11
11
|
/** Tools left byte-identical, with a controlled reason. */
|
|
12
12
|
readonly skipped: readonly SkippedTool[];
|
|
13
13
|
}
|
|
14
|
+
/** The verdict for one declaration's parameter schema. */
|
|
15
|
+
export type ParameterDecision = {
|
|
16
|
+
readonly fix: true;
|
|
17
|
+
} | {
|
|
18
|
+
readonly fix: false;
|
|
19
|
+
readonly reason?: string;
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
22
|
+
* Decide whether one root parameter schema needs an empty properties bucket.
|
|
16
23
|
*
|
|
17
24
|
* The GitHub Copilot Gemini route rejects a request whose tool declaration carries
|
|
18
25
|
* an object parameter schema with no properties bucket (measured: HTTP 400 with a
|
|
19
26
|
* plain-text body). Adding the bucket is JSON-Schema neutral and does not change
|
|
20
27
|
* unions, required fields, additionalProperties or descriptions. Nothing else is
|
|
21
28
|
* rewritten: references, non-object schemas and every nested node stay untouched.
|
|
29
|
+
*
|
|
30
|
+
* @param parameters - the root schema exactly as the declaration carries it.
|
|
31
|
+
* @returns whether to add the bucket, or the controlled reason it stays untouched.
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyParameters(parameters: unknown): ParameterDecision;
|
|
34
|
+
/**
|
|
35
|
+
* Add an empty root properties bucket to object parameter schemas that lack one.
|
|
36
|
+
*
|
|
37
|
+
* @param tools - the declarations one assembly or request carries.
|
|
38
|
+
* @returns the rewritten declarations plus every fixed and skipped tool name.
|
|
22
39
|
*/
|
|
23
40
|
export declare function normalizeToolSchemas(tools: readonly ToolSchema[]): NormalizeResult;
|
|
24
41
|
/**
|
package/dist/schema.js
CHANGED
|
@@ -7,48 +7,51 @@ const isPlainObject = (value) => {
|
|
|
7
7
|
return prototype === Object.prototype || prototype === null;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Decide whether one root parameter schema needs an empty properties bucket.
|
|
11
11
|
*
|
|
12
12
|
* The GitHub Copilot Gemini route rejects a request whose tool declaration carries
|
|
13
13
|
* an object parameter schema with no properties bucket (measured: HTTP 400 with a
|
|
14
14
|
* plain-text body). Adding the bucket is JSON-Schema neutral and does not change
|
|
15
15
|
* unions, required fields, additionalProperties or descriptions. Nothing else is
|
|
16
16
|
* rewritten: references, non-object schemas and every nested node stay untouched.
|
|
17
|
+
*
|
|
18
|
+
* @param parameters - the root schema exactly as the declaration carries it.
|
|
19
|
+
* @returns whether to add the bucket, or the controlled reason it stays untouched.
|
|
20
|
+
*/
|
|
21
|
+
export function classifyParameters(parameters) {
|
|
22
|
+
if (parameters === undefined)
|
|
23
|
+
return { fix: false, reason: 'SKIPPED_MISSING_PARAMETERS' };
|
|
24
|
+
if (!isPlainObject(parameters))
|
|
25
|
+
return { fix: false, reason: 'SKIPPED_NOT_OBJECT' };
|
|
26
|
+
if (Object.hasOwn(parameters, '$ref') || Object.hasOwn(parameters, '$dynamicRef'))
|
|
27
|
+
return { fix: false, reason: 'SKIPPED_REFERENCE' };
|
|
28
|
+
if (parameters.type !== 'object')
|
|
29
|
+
return { fix: false, reason: 'SKIPPED_NOT_OBJECT_TYPE' };
|
|
30
|
+
if (parameters.properties !== undefined) {
|
|
31
|
+
return isPlainObject(parameters.properties) ? { fix: false } : { fix: false, reason: 'SKIPPED_INVALID_PROPERTIES' };
|
|
32
|
+
}
|
|
33
|
+
return { fix: true };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Add an empty root properties bucket to object parameter schemas that lack one.
|
|
37
|
+
*
|
|
38
|
+
* @param tools - the declarations one assembly or request carries.
|
|
39
|
+
* @returns the rewritten declarations plus every fixed and skipped tool name.
|
|
17
40
|
*/
|
|
18
41
|
export function normalizeToolSchemas(tools) {
|
|
19
42
|
const fixed = [];
|
|
20
43
|
const skipped = [];
|
|
21
44
|
const output = [];
|
|
22
45
|
for (const tool of tools) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
output.push(tool);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (!isPlainObject(parameters)) {
|
|
30
|
-
skipped.push({ tool: tool.name, reason: 'SKIPPED_NOT_OBJECT' });
|
|
31
|
-
output.push(tool);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (Object.hasOwn(parameters, '$ref') || Object.hasOwn(parameters, '$dynamicRef')) {
|
|
35
|
-
skipped.push({ tool: tool.name, reason: 'SKIPPED_REFERENCE' });
|
|
36
|
-
output.push(tool);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (parameters.type !== 'object') {
|
|
40
|
-
skipped.push({ tool: tool.name, reason: 'SKIPPED_NOT_OBJECT_TYPE' });
|
|
41
|
-
output.push(tool);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (parameters.properties !== undefined) {
|
|
45
|
-
if (!isPlainObject(parameters.properties))
|
|
46
|
-
skipped.push({ tool: tool.name, reason: 'SKIPPED_INVALID_PROPERTIES' });
|
|
47
|
-
output.push(tool);
|
|
46
|
+
const decision = classifyParameters(tool.parameters);
|
|
47
|
+
if (decision.fix) {
|
|
48
|
+
fixed.push(tool.name);
|
|
49
|
+
output.push({ ...tool, parameters: { ...tool.parameters, properties: {} } });
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
if (decision.reason !== undefined)
|
|
53
|
+
skipped.push({ tool: tool.name, reason: decision.reason });
|
|
54
|
+
output.push(tool);
|
|
52
55
|
}
|
|
53
56
|
return { tools: output, changed: fixed.length > 0, fixed, skipped };
|
|
54
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-copilot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Account-aware GitHub Copilot model discovery, Copilot-compatible wire identity, native Responses web search and a Copilot Gemini tool-declaration guard in one DeepSeek Harness plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,19 +39,6 @@
|
|
|
39
39
|
"CHANGELOG.md",
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "tsc -p tsconfig.json",
|
|
44
|
-
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json",
|
|
45
|
-
"test": "pnpm run build && node --import tsx --test test/catalog.test.ts test/discovery.test.ts test/identity.test.ts test/secrets.test.ts test/catalog-plugin.test.ts test/config.test.ts test/composition.test.ts test/auth.test.ts test/refresh.test.ts test/provider.test.ts test/responses.test.ts test/search-plugin.test.ts test/schema.test.ts test/guard-plugin.test.ts test/patch.test.ts",
|
|
46
|
-
"test:integration": "pnpm run build && node --import tsx --test test/integration/nested-layout.test.ts test/integration/catalog-runtime.test.ts test/integration/enterprise.test.ts test/integration/identity-wire.test.ts test/integration/search-runtime.test.ts test/integration/guard-runtime.test.ts test/integration/scoped.test.ts test/integration/wire.test.ts",
|
|
47
|
-
"test:types": "pnpm run build && tsc -p test/types/tsconfig.json",
|
|
48
|
-
"test:pack": "pnpm run build && tsx ../../scripts/check-pack.ts .",
|
|
49
|
-
"check": "pnpm run typecheck && tsx ../../scripts/check.ts",
|
|
50
|
-
"check:secrets": "tsx ../../scripts/check-secrets.ts",
|
|
51
|
-
"prepare": "pnpm run build",
|
|
52
|
-
"prepublishOnly": "pnpm run verify",
|
|
53
|
-
"verify": "pnpm run check:secrets && pnpm run check && pnpm run test && pnpm run test:integration && pnpm run test:types && pnpm run test:pack"
|
|
54
|
-
},
|
|
55
42
|
"keywords": [
|
|
56
43
|
"deepseek-harness",
|
|
57
44
|
"dsh",
|
|
@@ -75,33 +62,33 @@
|
|
|
75
62
|
},
|
|
76
63
|
"engines": {
|
|
77
64
|
"node": ">=22.19.0",
|
|
78
|
-
"dsh": "0.2.
|
|
65
|
+
"dsh": "0.2.2-rc.1"
|
|
79
66
|
},
|
|
80
67
|
"publishConfig": {
|
|
81
68
|
"access": "public",
|
|
82
69
|
"registry": "https://registry.npmjs.org/"
|
|
83
70
|
},
|
|
84
71
|
"peerDependencies": {
|
|
85
|
-
"@deepseek-ai/cordis": "~4.0
|
|
86
|
-
"@deepseek-ai/dsh-credentials": "0.2.
|
|
87
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.
|
|
88
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.2.
|
|
89
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.
|
|
90
|
-
"@deepseek-ai/dsh-web": "0.2.
|
|
91
|
-
"@deepseek-ai/schemastery": "~3.
|
|
72
|
+
"@deepseek-ai/cordis": "~4.1.0",
|
|
73
|
+
"@deepseek-ai/dsh-credentials": "0.2.2-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-web": "0.2.2-rc.1",
|
|
78
|
+
"@deepseek-ai/schemastery": "~3.19.0",
|
|
92
79
|
"@earendil-works/pi-ai": "^1.1.0"
|
|
93
80
|
},
|
|
94
81
|
"devDependencies": {
|
|
95
|
-
"@deepseek-ai/cordis": "4.0
|
|
96
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
97
|
-
"@deepseek-ai/dsh-credentials": "0.2.
|
|
98
|
-
"@deepseek-ai/dsh-launch-environment": "0.2.
|
|
99
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.2.
|
|
100
|
-
"@deepseek-ai/dsh-scope": "0.2.
|
|
101
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
102
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.
|
|
103
|
-
"@deepseek-ai/dsh-web": "0.2.
|
|
104
|
-
"@deepseek-ai/schemastery": "3.
|
|
82
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
83
|
+
"@deepseek-ai/dsh-agent": "0.2.2-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-credentials": "0.2.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-scope": "0.2.2-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-web": "0.2.2-rc.1",
|
|
91
|
+
"@deepseek-ai/schemastery": "3.19.0",
|
|
105
92
|
"@earendil-works/pi-ai": "1.3.0"
|
|
106
93
|
},
|
|
107
94
|
"dsh": {
|
|
@@ -109,5 +96,16 @@
|
|
|
109
96
|
"bundle": {
|
|
110
97
|
"patch": "./cordis.patch.yml"
|
|
111
98
|
}
|
|
99
|
+
},
|
|
100
|
+
"scripts": {
|
|
101
|
+
"build": "tsc -p tsconfig.json",
|
|
102
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json",
|
|
103
|
+
"test": "pnpm run build && node --import tsx --test test/catalog.test.ts test/discovery.test.ts test/identity.test.ts test/secrets.test.ts test/catalog-plugin.test.ts test/config.test.ts test/composition.test.ts test/auth.test.ts test/refresh.test.ts test/provider.test.ts test/responses.test.ts test/search-plugin.test.ts test/schema.test.ts test/guard.test.ts test/guard-plugin.test.ts test/patch.test.ts",
|
|
104
|
+
"test:integration": "pnpm run build && node --import tsx --test test/integration/nested-layout.test.ts test/integration/catalog-runtime.test.ts test/integration/enterprise.test.ts test/integration/identity-wire.test.ts test/integration/search-runtime.test.ts test/integration/guard-runtime.test.ts test/integration/wire.test.ts",
|
|
105
|
+
"test:types": "pnpm run build && tsc -p test/types/tsconfig.json",
|
|
106
|
+
"test:pack": "pnpm run build && tsx ../../scripts/check-pack.ts .",
|
|
107
|
+
"check": "pnpm run typecheck && tsx ../../scripts/check.ts",
|
|
108
|
+
"check:secrets": "tsx ../../scripts/check-secrets.ts",
|
|
109
|
+
"verify": "pnpm run check:secrets && pnpm run check && pnpm run test && pnpm run test:integration && pnpm run test:types && pnpm run test:pack"
|
|
112
110
|
}
|
|
113
|
-
}
|
|
111
|
+
}
|