dsh-better-copilot 0.1.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — builtin review routing and temperature compatibility
4
+
5
+ - Add volatile `autoReview.enabled`, `autoReview.model` and
6
+ `autoReview.reasoningEffort` settings. Builtin GitHub Copilot reviews default to
7
+ `gpt-6.1-terra`; DeepSeek Flash and non-Copilot routes retain their current
8
+ model. Omitted effort uses the selected model's adapter default.
9
+ - Redispatch detached frozen auxiliary requests through the public LLM
10
+ waterfall, leaving conversation requests, review policy, permission state,
11
+ Session metadata and cancellation unchanged; dispose the listener on unload.
12
+ - Remove the builtin Copilot reviewer's sampling parameter unless its model
13
+ positively advertises support. Map boolean account temperature capability
14
+ metadata into known/new catalog descriptors and strip explicitly unsupported
15
+ temperature on ordinary Copilot inference requests without model-name guesses.
16
+ - Compose temperature and Gemini normalization in the existing wire transform;
17
+ a Gemini guard failure cannot discard a temperature fix. Add routing, effort
18
+ validation, metadata, protocol, lifecycle and publication regressions.
19
+
20
+ ## 0.2.0 — read-only subagent route badge
21
+
22
+ - Add a browser client half that shows an addressed subagent's effective model
23
+ and reasoning effort in the Web session header, beside the title. The label is
24
+ text only: no selector, no click target, no Host write, because the child's
25
+ route is fixed at creation and reused on resume.
26
+ - Read the route from the existing `modelSelection` session projection and show
27
+ an adapter-defaulted effort as `default`. Provider/model/effort ids render
28
+ verbatim; catalog display names are not available to client plugins.
29
+ - Keep ordinary conversations and the DSH-rendered subagent catalog untouched.
30
+ Ship the bundle through `./client` and a `dsh.client` declaration, with render
31
+ tests and the publication allowlist updated.
32
+
3
33
  ## 0.1.1 — DSH 0.2.2-rc.1 compatibility
4
34
 
5
35
  - Retarget the manifest, peer ranges and development pins to DSH `0.2.2-rc.1`
package/README.md CHANGED
@@ -12,6 +12,10 @@ One DeepSeek Harness plugin for GitHub Copilot:
12
12
  provider for the ordinary `web_search` tool.
13
13
  - **Gemini tool-declaration guard** — adds a missing root `properties` bucket to
14
14
  tool parameter schemas for configured Copilot routes.
15
+ - **Builtin Auto review overrides** — selects the review model and effort without
16
+ changing the conversation model or the builtin authorization policy.
17
+ - **Temperature compatibility** — omits unsupported sampling controls on the
18
+ Copilot wire, including the builtin reviewer's hard-coded zero.
15
19
 
16
20
  The plugin is a replacement for, and superset of, the earlier separate
17
21
  `dsh-copilot-catalog`, `dsh-copilot-search` and `dsh-copilot-tool-schema`
@@ -67,14 +71,55 @@ The bundle patch:
67
71
  providers: [github-copilot]
68
72
  families: [gemini]
69
73
  logFixes: false
74
+ autoReview:
75
+ enabled: true
76
+ # Optional: leave unset for Terra on Copilot, current model otherwise
77
+ # model: gpt-6.1-terra
78
+ # reasoningEffort: high
70
79
  ```
71
80
 
72
81
  `providers` is the pi-ai adapter's own schema, reused unchanged, so every
73
82
  provider setting behaves exactly as it does with the stock adapter. `search`
74
- configures the web-search provider; `toolSchema` configures the Gemini guard.
83
+ configures the web-search provider; `toolSchema` configures the Gemini guard;
84
+ `autoReview` configures the builtin review request only.
75
85
 
76
86
  Loader patches replace a row's `config` wholesale, not key by key: an override
77
- must restate `providers`, `search` and `toolSchema` together.
87
+ must restate every non-default group it needs together. Omitted `search`,
88
+ `toolSchema` and `autoReview` groups resolve their defaults.
89
+
90
+ ## Builtin Auto review
91
+
92
+ `autoReview` applies only to requests marked `purpose: auto-review` by the builtin
93
+ experimental Auto review plugin. It does not enable Auto review, select a
94
+ permission preset, replace its policy or change normal conversation/subagent
95
+ requests.
96
+
97
+ - With no `autoReview.model`, GitHub Copilot reviews use `gpt-6.1-terra`.
98
+ DeepSeek Flash (`deepseek-v4-flash`) stays on Flash; other non-Copilot providers
99
+ retain their current model.
100
+ - `autoReview.model` selects an exact model id on the current provider;
101
+ `autoReview.reasoningEffort` selects that model's exact effort id. Explicit
102
+ settings win over defaults, including on DeepSeek. Omitted effort uses the
103
+ selected model's adapter default, not the main agent's effort. A model change
104
+ drops any prior model's effort. Unsupported explicit efforts fail normally
105
+ before provider I/O, without clamping or silently switching models.
106
+ - Settings are volatile: edits apply to the next review, not an in-flight review.
107
+ `autoReview.enabled: false` restores the builtin request's model and effort.
108
+
109
+ The builtin reviewer hard-codes `temperature: 0`. Copilot review requests omit
110
+ this parameter unless the selected catalog model positively advertises
111
+ `compat.supportsTemperature: true`, so older GPT metadata lacking a temperature
112
+ flag does not cause an unsupported-parameter error. This conservative rule is
113
+ limited to the reviewer-owned control; it does not discard explicit sampling
114
+ settings on ordinary requests with unknown support.
115
+
116
+ For ordinary Copilot inference, account discovery maps boolean
117
+ `capabilities.supports.temperature` into `compat.supportsTemperature`. The
118
+ existing trusted wire transform strips `temperature` only for exact catalog
119
+ models declaring `false`; supported and unknown metadata preserve the original
120
+ body. No model-name or reasoning-capability heuristics are used. Temperature
121
+ normalization remains active with wire identity disabled, and a Gemini guard
122
+ failure cannot undo a successful temperature fix.
78
123
 
79
124
  ## Wire identity
80
125
 
@@ -99,7 +144,27 @@ dispatches. Inside the same refcounted, trusted-path-checked interceptor that
99
144
  carries the wire identity, it adds an empty root `properties` bucket to object
100
145
  tool schemas that lack one for a configured Copilot route and model family, and
101
146
  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.
147
+ another plugin's settings, and a guard failure preserves the body after any
148
+ successful temperature normalization.
149
+
150
+ ## Subagent model badge (Web)
151
+
152
+ The package now carries a small browser client half. When a subagent
153
+ conversation is open in the Web GUI, the session header shows the child's
154
+ effective route as muted text next to the title — for example
155
+ `gpt-6.1-sol · high`. The label is read-only: the plugin renders no selector and
156
+ writes nothing back, because the Host fixes a child's provider, model and
157
+ reasoning effort when the subagent is created and reuses the recorded route on
158
+ resume. An adapter-defaulted effort appears as `default`, and the tooltip and
159
+ accessible name say that the route is fixed.
160
+
161
+ The badge reads the child Session's `modelSelection` projection; it adds no Host
162
+ service, configuration field or RPC. Ids are shown exactly as the Host records
163
+ them, because catalog display names are not exposed to third-party client
164
+ plugins. Ordinary conversations are unchanged, and the subagent catalog
165
+ dropdown is untouched: DSH renders those rows in its own client package rather
166
+ than through an extension slot. The browser half is published as `./client`
167
+ and declared through `dsh.client`; no additional wiring is needed.
103
168
 
104
169
  ## Migration from the three predecessor packages
105
170
 
@@ -0,0 +1,24 @@
1
+ import { type GenerateOptions } from '@deepseek-ai/dsh-llm';
2
+ import { type ModelCatalog, type PluginContext } from './catalog-types.js';
3
+ import type { AutoReviewSettings } from './config.js';
4
+ /** GitHub Copilot's default builtin authorization reviewer. */
5
+ export declare const DEFAULT_REVIEW_MODEL = "gpt-6.1-terra";
6
+ /** One live configuration snapshot for a review request. */
7
+ export interface AutoReviewOverrides {
8
+ enabled: boolean;
9
+ model?: string;
10
+ reasoningEffort?: string;
11
+ }
12
+ /**
13
+ * Select only an auxiliary builtin review route. Never modify loop requests or
14
+ * caller-owned inputs, including the frozen request the builtin reviewer sends.
15
+ * Changing model drops a previous model's effort unless explicitly overridden.
16
+ */
17
+ export declare function configureAutoReview(options: GenerateOptions<string>, settings: AutoReviewOverrides, catalog?: ModelCatalog): GenerateOptions<string>;
18
+ /**
19
+ * The waterfall's next() accepts no replacement request. Redispatch a detached
20
+ * auxiliary request through the public LLM service instead, marking that exact
21
+ * object so the nested waterfall delegates once without rereading live settings.
22
+ * Cordis owns listener disposal; no review policy, permission or Session changes.
23
+ */
24
+ export declare function installAutoReviewRouting(ctx: PluginContext, config: AutoReviewSettings, catalog: ModelCatalog): void;
@@ -0,0 +1,57 @@
1
+ import { isAgentLoopRequest } from '@deepseek-ai/dsh-llm';
2
+ import { asRecord } from './catalog-types.js';
3
+ /** GitHub Copilot's default builtin authorization reviewer. */
4
+ export const DEFAULT_REVIEW_MODEL = 'gpt-6.1-terra';
5
+ /**
6
+ * Select only an auxiliary builtin review route. Never modify loop requests or
7
+ * caller-owned inputs, including the frozen request the builtin reviewer sends.
8
+ * Changing model drops a previous model's effort unless explicitly overridden.
9
+ */
10
+ export function configureAutoReview(options, settings, catalog = {}) {
11
+ if (!settings.enabled || options.purpose !== 'auto-review' || isAgentLoopRequest(options))
12
+ return options;
13
+ const model = settings.model ?? (options.provider === 'github-copilot' && options.model !== 'deepseek-v4-flash'
14
+ ? DEFAULT_REVIEW_MODEL : options.model);
15
+ const reasoningEffort = settings.reasoningEffort ?? (model === options.model ? options.reasoningEffort : undefined);
16
+ const descriptor = Object.hasOwn(catalog, model) ? catalog[model] : undefined;
17
+ // The builtin's hard-coded zero is not a user inference setting. Require
18
+ // positive support for reviewer sampling; old SDK GPT catalogs omit this flag.
19
+ const omitTemperature = options.provider === 'github-copilot' && options.temperature !== undefined
20
+ && !(descriptor?.provider === 'github-copilot' && asRecord(descriptor.compat)?.supportsTemperature === true);
21
+ if (model === options.model && reasoningEffort === options.reasoningEffort && !omitTemperature)
22
+ return options;
23
+ const { reasoningEffort: _previous, temperature, ...rest } = options;
24
+ return Object.freeze({
25
+ ...rest, model,
26
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort: reasoningEffort }),
27
+ ...(omitTemperature || temperature === undefined ? {} : { temperature }),
28
+ });
29
+ }
30
+ /**
31
+ * The waterfall's next() accepts no replacement request. Redispatch a detached
32
+ * auxiliary request through the public LLM service instead, marking that exact
33
+ * object so the nested waterfall delegates once without rereading live settings.
34
+ * Cordis owns listener disposal; no review policy, permission or Session changes.
35
+ */
36
+ export function installAutoReviewRouting(ctx, config, catalog) {
37
+ const routed = new WeakSet();
38
+ ctx.on('llm/stream', function (options, next) {
39
+ if (routed.has(options) || options.purpose !== 'auto-review' || isAgentLoopRequest(options))
40
+ return next();
41
+ const enabled = config.enabled.get();
42
+ if (!enabled)
43
+ return next();
44
+ const model = config.model.get();
45
+ const reasoningEffort = config.reasoningEffort.get();
46
+ const selected = configureAutoReview(options, {
47
+ enabled,
48
+ ...(model === undefined ? {} : { model }),
49
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
50
+ }, catalog);
51
+ if (selected === options)
52
+ return next();
53
+ routed.add(selected);
54
+ // Purpose typing is owned by the optional builtin package, not a hard peer.
55
+ return this.stream(selected);
56
+ });
57
+ }
package/dist/catalog.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { asRecord } from './catalog-types.js';
2
+ import { withCopilotTemperatureSupport } from './temperature.js';
2
3
  export const DEFAULT_COPILOT_BASE_URL = 'https://api.individual.githubcopilot.com';
3
4
  // Provider identity defaults match the supported pi-ai Copilot OAuth client.
4
5
  // These are protocol defaults, not metadata borrowed from a particular model.
@@ -165,7 +166,7 @@ export function selectAccountModels(response, catalog, { baseUrl = DEFAULT_COPIL
165
166
  // Own-property lookup prevents Object.prototype members becoming descriptors.
166
167
  const model = own(catalog, item.id) ?? createNewModel({ ...item, id: item.id }, headers, baseUrl);
167
168
  if (model) {
168
- models[item.id] = model;
169
+ models[item.id] = withCopilotTemperatureSupport(model, supports?.temperature);
169
170
  unsupported.delete(item.id);
170
171
  }
171
172
  else {
package/dist/client.js ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /// <reference path="./client.types.d.ts" />
3
+ /* Read-only subagent route badge for the Web conversation header.
4
+ * The Host fixes a subagent's provider, model and reasoning effort when the
5
+ * child is created, so this client half only presents them. It never renders a
6
+ * selection control and writes nothing back to the Session.
7
+ */
8
+ window.__ModuleLoader__.load({
9
+ id: 'dsh-better-copilot',
10
+ factory(require) {
11
+ const React = require('react');
12
+ const h = React.createElement;
13
+ const NS = 'better-copilot.subagent';
14
+ const SLOT = 'conversation.session.header.actions';
15
+ const ID = 'better-copilot-subagent-model';
16
+ const en = {
17
+ 'badge.defaultEffort': 'default',
18
+ 'badge.fixed': "Model and effort are fixed for this subagent's lifetime.",
19
+ 'badge.aria': 'Subagent model {model} from {provider}, reasoning effort {effort}. {fixed}',
20
+ };
21
+ const zh = {
22
+ 'badge.defaultEffort': '默认',
23
+ 'badge.fixed': '该子智能体的模型与推理强度在创建时已固定。',
24
+ 'badge.aria': '子智能体模型 {model}({provider}),推理强度 {effort}。{fixed}',
25
+ };
26
+ /** Validate one projected selection; anything malformed is not rendered. */
27
+ function readSelection(value) {
28
+ if (typeof value !== 'object' || value === null)
29
+ return null;
30
+ const candidate = value;
31
+ const provider = typeof candidate.provider === 'string' ? candidate.provider : '';
32
+ const model = typeof candidate.model === 'string' ? candidate.model : '';
33
+ if (provider === '' || model === '')
34
+ return null;
35
+ const reasoningEffort = typeof candidate.reasoningEffort === 'string' && candidate.reasoningEffort !== ''
36
+ ? candidate.reasoningEffort
37
+ : undefined;
38
+ return reasoningEffort === undefined ? { provider, model } : { provider, model, reasoningEffort };
39
+ }
40
+ /** Read the effective selection from the modelSelection wire view. */
41
+ function selectedOf(view) {
42
+ if (typeof view !== 'object' || view === null)
43
+ return null;
44
+ const projected = view;
45
+ return readSelection(projected.next) ?? readSelection(projected.lastUsed);
46
+ }
47
+ /** Compose the visible label, tooltip and accessible name for one route. */
48
+ function subagentBadge(selection, t) {
49
+ const explicit = selection.reasoningEffort;
50
+ const effort = explicit === undefined ? t('badge.defaultEffort') : explicit;
51
+ const route = explicit === undefined
52
+ ? `${selection.provider}/${selection.model}`
53
+ : `${selection.provider}/${selection.model} · ${explicit}`;
54
+ return {
55
+ label: `${selection.model} · ${effort}`,
56
+ title: `${route} — ${t('badge.fixed')}`,
57
+ aria: t('badge.aria', {
58
+ provider: selection.provider,
59
+ model: selection.model,
60
+ effort,
61
+ fixed: t('badge.fixed'),
62
+ }),
63
+ };
64
+ }
65
+ const badgeStyle = {
66
+ display: 'inline-block',
67
+ maxWidth: '26ch',
68
+ overflow: 'hidden',
69
+ color: 'var(--dsw-alias-label-tertiary)',
70
+ fontSize: '12px',
71
+ lineHeight: '18px',
72
+ whiteSpace: 'nowrap',
73
+ textOverflow: 'ellipsis',
74
+ verticalAlign: 'middle',
75
+ cursor: 'default',
76
+ };
77
+ /** Read-only header cell: visible only for an addressed subagent's own route. */
78
+ function SubagentModelBadge({ sessionId, useSessions, useProjection, t }) {
79
+ const subagent = useSessions((state) => state.byId[sessionId]?.origin === 'subagent');
80
+ const view = useProjection('modelSelection');
81
+ if (!subagent)
82
+ return null;
83
+ const selection = selectedOf(view);
84
+ if (selection === null)
85
+ return null;
86
+ const badge = subagentBadge(selection, t);
87
+ return h('span', { style: badgeStyle, title: badge.title, 'aria-label': badge.aria }, badge.label);
88
+ }
89
+ function apply(ctx) {
90
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }));
91
+ ctx.slots.inject(SLOT, () => ctx.slots.register({ name: SLOT, id: ID, order: 15, locale: NS }, SubagentModelBadge));
92
+ }
93
+ return {
94
+ inject: ['slots', 'locale'],
95
+ apply,
96
+ };
97
+ },
98
+ });
package/dist/config.d.ts CHANGED
@@ -30,11 +30,18 @@ export interface GuardConfigValue {
30
30
  families: Volatile<string[]>;
31
31
  logFixes: Volatile<boolean>;
32
32
  }
33
+ /** Builtin Auto review routing; omitted leaves retain the model's adapter defaults. */
34
+ export interface AutoReviewSettings {
35
+ enabled: Volatile<boolean>;
36
+ model: Volatile<string | undefined>;
37
+ reasoningEffort: Volatile<string | undefined>;
38
+ }
33
39
  /** The one configuration object every capability reads from. */
34
40
  export interface BetterCopilotConfig {
35
41
  providers: ProvidersValue;
36
42
  search: SearchSettings;
37
43
  toolSchema: GuardConfigValue;
44
+ autoReview: AutoReviewSettings;
38
45
  }
39
46
  /** User-facing configuration: plain values, before volatile resolution. */
40
47
  export interface BetterCopilotConfigInput {
@@ -58,6 +65,11 @@ export interface BetterCopilotConfigInput {
58
65
  families?: string[];
59
66
  logFixes?: boolean;
60
67
  };
68
+ autoReview?: {
69
+ enabled?: boolean;
70
+ model?: string;
71
+ reasoningEffort?: string;
72
+ };
61
73
  }
62
74
  /**
63
75
  * The merged Config schema as the plugin and the Loader see it: callable, with a
@@ -115,6 +127,16 @@ export declare const GuardConfig: z<Schemastery.ObjectS<NoInfer<{
115
127
  families: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
116
128
  logFixes: z<boolean, boolean, "volatile-defined">;
117
129
  }>>, "plain">;
130
+ /** Only the builtin reviewer is affected; exact model/effort ids are validated by LLM dispatch. */
131
+ export declare const AutoReviewConfig: z<Schemastery.ObjectS<NoInfer<{
132
+ enabled: z<boolean, boolean, "volatile-defined">;
133
+ model: z<string, string, "volatile">;
134
+ reasoningEffort: z<string, string, "volatile">;
135
+ }>>, Schemastery.ObjectT<NoInfer<{
136
+ enabled: z<boolean, boolean, "volatile-defined">;
137
+ model: z<string, string, "volatile">;
138
+ reasoningEffort: z<string, string, "volatile">;
139
+ }>>, "plain">;
118
140
  /**
119
141
  * The merged plugin's single configuration object: the adapter's provider map
120
142
  * plus one group per additional capability.
package/dist/config.js CHANGED
@@ -23,6 +23,12 @@ export const GuardConfig = z.object({
23
23
  families: z.array(z.string()).default(['gemini']).volatile(),
24
24
  logFixes: z.boolean().default(false).volatile(),
25
25
  });
26
+ /** Only the builtin reviewer is affected; exact model/effort ids are validated by LLM dispatch. */
27
+ export const AutoReviewConfig = z.object({
28
+ enabled: z.boolean().default(true).volatile(),
29
+ model: z.string().min(1).volatile(),
30
+ reasoningEffort: z.string().min(1).volatile(),
31
+ });
26
32
  /**
27
33
  * Reuse the pi-ai adapter's own provider profile schema instead of restating it.
28
34
  * A restated schema would silently drift from the adapter's defaults, validation
@@ -47,5 +53,6 @@ export function createConfig(originalConfig) {
47
53
  providers: providerProfileSchema(originalConfig),
48
54
  search: SearchConfig,
49
55
  toolSchema: GuardConfig,
56
+ autoReview: AutoReviewConfig,
50
57
  });
51
58
  }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,9 @@ export declare const apply: (ctx: PluginContext, config: BetterCopilotConfig) =>
7
7
  /** Preserved predecessor entry point: one read-only discovery with the merged runtime. */
8
8
  export declare const syncCopilotCatalog: (credentials?: CredentialReader, authOptions?: CopilotAuthOptions) => Promise<SyncResult>;
9
9
  export { DEFAULT_SEARCH_MODEL } from './config.js';
10
+ export { configureAutoReview, DEFAULT_REVIEW_MODEL } from './auto-review.js';
11
+ export type { AutoReviewOverrides } from './auto-review.js';
12
+ export { normalizeCopilotTemperature } from './temperature.js';
10
13
  export { liveCost, selectAccountModels } from './catalog.js';
11
14
  export { DiscoveryError } from './discovery.js';
12
15
  export { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilotRequestTransform, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
@@ -16,7 +19,7 @@ export { COPILOT_SEARCH_PROVIDER_ID, CopilotSearchProvider } from './provider.js
16
19
  export { matchesRoute, normalizeToolSchemas, TOOL_SCHEMA_SKIPPED } from './schema.js';
17
20
  export { loadRuntime } from './runtime.js';
18
21
  export type { BetterCopilotRuntime, RuntimeOptions } from './runtime.js';
19
- export type { BetterCopilotConfig, BetterCopilotConfigSchema } from './config.js';
22
+ export type { AutoReviewSettings, BetterCopilotConfig, BetterCopilotConfigSchema } from './config.js';
20
23
  export type { BetterCopilotDependencies, BetterCopilotPlugin } from './plugin.js';
21
24
  export type { AccountModelOptions } from './catalog.js';
22
25
  export type { NormalizeResult } from './schema.js';
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ export const apply = (ctx, config) => plugin.apply(ctx, config);
9
9
  /** Preserved predecessor entry point: one read-only discovery with the merged runtime. */
10
10
  export const syncCopilotCatalog = (credentials, authOptions) => plugin.sync(credentials, authOptions);
11
11
  export { DEFAULT_SEARCH_MODEL } from './config.js';
12
+ export { configureAutoReview, DEFAULT_REVIEW_MODEL } from './auto-review.js';
13
+ export { normalizeCopilotTemperature } from './temperature.js';
12
14
  export { liveCost, selectAccountModels } from './catalog.js';
13
15
  export { DiscoveryError } from './discovery.js';
14
16
  export { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilotRequestTransform, isCopilotRequestPath, isTrustedCopilotUrl, rewriteCopilotRequestHeaders, wireIdentityMode, } from './identity.js';
package/dist/plugin.js CHANGED
@@ -6,6 +6,8 @@ import { createCopilotWireIdentity, installCopilotIdentityHeaders, installCopilo
6
6
  import { CopilotSearchProvider } from './provider.js';
7
7
  import { TOOL_SCHEMA_SKIPPED } from './schema.js';
8
8
  import { guardCopilotRequestBody } from './guard.js';
9
+ import { installAutoReviewRouting } from './auto-review.js';
10
+ import { normalizeCopilotTemperature } from './temperature.js';
9
11
  export const name = 'better-copilot';
10
12
  /** Search and guard are optional capabilities; the adapter requires these two. */
11
13
  export const inject = ['llm', 'credentials'];
@@ -96,21 +98,28 @@ export function createPlugin(dependencies) {
96
98
  // the defect. It is installed even when wire identity is opted out.
97
99
  contain('tool-schema guard', 'Gemini tool-schema normalization', () => {
98
100
  const transform = ({ body }) => {
101
+ let normalized;
102
+ try {
103
+ normalized = normalizeCopilotTemperature(body, dependencies.catalog);
104
+ }
105
+ catch {
106
+ log('warn', 'better-copilot: skipped temperature normalization after an internal error');
107
+ }
99
108
  try {
100
109
  const settings = guardSettings(config.toolSchema);
101
- const result = guardCopilotRequestBody(body, settings);
110
+ const result = guardCopilotRequestBody(normalized ?? body, settings);
102
111
  if (settings.logFixes) {
103
112
  if (result.fixed.length > 0)
104
113
  log('info', 'better-copilot: added a properties bucket to %d tool schema(s) on the Copilot wire', result.fixed.length);
105
114
  if (result.skipped.length > 0)
106
115
  log('warn', 'better-copilot: %s left %d tool schema(s) unchanged: %s', TOOL_SCHEMA_SKIPPED, result.skipped.length, JSON.stringify(result.skipped));
107
116
  }
108
- return result.body;
117
+ return result.body ?? normalized;
109
118
  }
110
119
  catch {
111
- // The request is delegated byte-identical; a guard failure never fails a call.
120
+ // Guard failure never fails a call or discards a temperature fix.
112
121
  log('warn', 'better-copilot: skipped one request after an internal error');
113
- return undefined;
122
+ return normalized;
114
123
  }
115
124
  };
116
125
  ctx.effect(() => installCopilotRequestTransform(transform), 'copilot: tool-schema wire normalization');
@@ -144,7 +153,11 @@ export function createPlugin(dependencies) {
144
153
  });
145
154
  });
146
155
  });
147
- // 4. Catalog discovery, then the adapter itself. Only this step is fatal:
156
+ // 4. Builtin authorization reviewer routing, independently contained.
157
+ contain('auto-review routing', 'builtin review model overrides', () => {
158
+ installAutoReviewRouting(ctx, config.autoReview, dependencies.catalog);
159
+ });
160
+ // 5. Catalog discovery, then the adapter itself. Only this step is fatal:
148
161
  // without the adapter there is no Copilot inference route at all.
149
162
  try {
150
163
  const credentials = ctx.get('credentials');
@@ -0,0 +1,19 @@
1
+ import type { Api, Model } from '@earendil-works/pi-ai';
2
+ import { type ModelCatalog } from './catalog-types.js';
3
+ /**
4
+ * Apply only an explicit boolean from capabilities.supports.temperature.
5
+ * OpenAI compat schemas do not yet declare this field, but the wire boundary
6
+ * reads the same metadata used natively by pi-ai's Anthropic serializer.
7
+ * Preserve descriptor identity when discovery has nothing new to say.
8
+ */
9
+ export declare function withCopilotTemperatureSupport<T extends Model<Api>>(model: T, support: unknown): T;
10
+ /**
11
+ * Omit an unsupported temperature after SDK serialization, for every request
12
+ * purpose (including auxiliary review). IDs are opaque and reasoning capability
13
+ * says nothing about temperature: only an exact catalog descriptor with explicit
14
+ * false compatibility metadata is sufficient to remove the field.
15
+ *
16
+ * Install through the existing trusted Copilot request transform, not as a
17
+ * separate interceptor. Undefined means the original body stays byte-identical.
18
+ */
19
+ export declare function normalizeCopilotTemperature(body: string, catalog: ModelCatalog): string | undefined;
@@ -0,0 +1,39 @@
1
+ import { asRecord } from './catalog-types.js';
2
+ /**
3
+ * Apply only an explicit boolean from capabilities.supports.temperature.
4
+ * OpenAI compat schemas do not yet declare this field, but the wire boundary
5
+ * reads the same metadata used natively by pi-ai's Anthropic serializer.
6
+ * Preserve descriptor identity when discovery has nothing new to say.
7
+ */
8
+ export function withCopilotTemperatureSupport(model, support) {
9
+ if (typeof support !== 'boolean' || asRecord(model.compat)?.supportsTemperature === support)
10
+ return model;
11
+ return { ...model, compat: { ...model.compat, supportsTemperature: support } };
12
+ }
13
+ /**
14
+ * Omit an unsupported temperature after SDK serialization, for every request
15
+ * purpose (including auxiliary review). IDs are opaque and reasoning capability
16
+ * says nothing about temperature: only an exact catalog descriptor with explicit
17
+ * false compatibility metadata is sufficient to remove the field.
18
+ *
19
+ * Install through the existing trusted Copilot request transform, not as a
20
+ * separate interceptor. Undefined means the original body stays byte-identical.
21
+ */
22
+ export function normalizeCopilotTemperature(body, catalog) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(body);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ const request = asRecord(parsed);
31
+ if (!request || typeof request.model !== 'string' || !Object.hasOwn(request, 'temperature') ||
32
+ !Object.hasOwn(catalog, request.model))
33
+ return undefined;
34
+ const model = catalog[request.model];
35
+ if (model?.provider !== 'github-copilot' || asRecord(model.compat)?.supportsTemperature !== false)
36
+ return undefined;
37
+ delete request.temperature;
38
+ return JSON.stringify(request);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-better-copilot",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
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",
@@ -30,6 +30,7 @@
30
30
  "types": "./dist/guard.d.ts",
31
31
  "import": "./dist/guard.js"
32
32
  },
33
+ "./client": "./dist/client.js",
33
34
  "./package.json": "./package.json"
34
35
  },
35
36
  "files": [
@@ -72,6 +73,7 @@
72
73
  "@deepseek-ai/cordis": "~4.1.0",
73
74
  "@deepseek-ai/dsh-credentials": "0.2.2-rc.1",
74
75
  "@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
76
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
75
77
  "@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
76
78
  "@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
77
79
  "@deepseek-ai/dsh-web": "0.2.2-rc.1",
@@ -83,24 +85,33 @@
83
85
  "@deepseek-ai/dsh-agent": "0.2.2-rc.1",
84
86
  "@deepseek-ai/dsh-credentials": "0.2.2-rc.1",
85
87
  "@deepseek-ai/dsh-launch-environment": "0.2.2-rc.1",
88
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
86
89
  "@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
87
90
  "@deepseek-ai/dsh-scope": "0.2.2-rc.1",
88
91
  "@deepseek-ai/dsh-session": "0.2.2-rc.1",
89
92
  "@deepseek-ai/dsh-system-prompt": "0.2.2-rc.1",
90
93
  "@deepseek-ai/dsh-web": "0.2.2-rc.1",
91
94
  "@deepseek-ai/schemastery": "3.19.0",
92
- "@earendil-works/pi-ai": "1.3.0"
95
+ "@earendil-works/pi-ai": "1.3.0",
96
+ "@types/react": "^19.3.0",
97
+ "@types/react-dom": "^19.3.0",
98
+ "react": "^19.3.0",
99
+ "react-dom": "^19.3.0"
93
100
  },
94
101
  "dsh": {
95
102
  "manifestVersion": 1,
96
103
  "bundle": {
97
104
  "patch": "./cordis.patch.yml"
105
+ },
106
+ "client": {
107
+ "platform": "web",
108
+ "immediately": true
98
109
  }
99
110
  },
100
111
  "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",
112
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
113
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json && tsc -p tsconfig.client.json --noEmit",
114
+ "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 test/client.render.test.ts test/temperature.test.ts test/auto-review.test.ts",
104
115
  "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
116
  "test:types": "pnpm run build && tsc -p test/types/tsconfig.json",
106
117
  "test:pack": "pnpm run build && tsx ../../scripts/check-pack.ts .",