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 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.1-rc.5`.
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 an additive `system-prompt/assemble` listener. For a route whose
96
- provider is listed and whose model id contains one of the configured families it
97
- adds an empty root `properties` bucket to object tool schemas that lack one, and
98
- leaves every other schema byte-identical. It never replaces an adapter or
99
- another plugin's settings.
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
- export { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
2
- export type { NormalizeResult } from './schema.js';
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
- export { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
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
+ }
@@ -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
- rewriteCopilotRequestHeaders(url, headers, state.identity);
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
- state.installs += 1;
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
- state.installs -= 1;
267
- if (state.installs > 0)
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 { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
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: an additive, contained global listener.
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
- ctx.on('system-prompt/assemble', async (_assembly, _context, next) => {
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
- if (!matchesRoute(downstream.variables.provider, downstream.variables.model, settings))
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) for %s/%s', result.fixed.length, String(downstream.variables.provider), String(downstream.variables.model));
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
- if (!result.changed)
111
- return downstream;
112
- return { ...downstream, tools: result.tools };
108
+ return result.body;
113
109
  }
114
110
  catch {
115
- log('warn', 'better-copilot: skipped one assembly after an internal error');
116
- return downstream;
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
- }, { global: true, prepend: true });
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
- * Add an empty root properties bucket to object parameter schemas that lack one.
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
- * Add an empty root properties bucket to object parameter schemas that lack one.
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 parameters = tool.parameters;
24
- if (parameters === undefined) {
25
- skipped.push({ tool: tool.name, reason: 'SKIPPED_MISSING_PARAMETERS' });
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
- fixed.push(tool.name);
51
- output.push({ ...tool, parameters: { ...parameters, properties: {} } });
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.0",
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.1-rc.5"
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.5",
86
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.5",
87
- "@deepseek-ai/dsh-launch-environment": "0.2.1-rc.5",
88
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.5",
89
- "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.5",
90
- "@deepseek-ai/dsh-web": "0.2.1-rc.5",
91
- "@deepseek-ai/schemastery": "~3.18.5",
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.5",
96
- "@deepseek-ai/dsh-agent": "0.2.1-rc.5",
97
- "@deepseek-ai/dsh-credentials": "0.2.1-rc.5",
98
- "@deepseek-ai/dsh-launch-environment": "0.2.1-rc.5",
99
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.5",
100
- "@deepseek-ai/dsh-scope": "0.2.1-rc.5",
101
- "@deepseek-ai/dsh-session": "0.2.1-rc.5",
102
- "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.5",
103
- "@deepseek-ai/dsh-web": "0.2.1-rc.5",
104
- "@deepseek-ai/schemastery": "3.18.5",
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
+ }