@genee/omp-opsx-addon 0.10.0 → 0.11.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/README.md +4 -2
- package/index.ts +24 -15
- package/lib/concurrency-control.ts +6 -6
- package/lib/concurrency-writer.ts +25 -13
- package/lib/edit-variant-pin.ts +34 -26
- package/lib/host-settings-adapter.ts +227 -0
- package/lib/model-roles.ts +41 -12
- package/package.json +1 -1
- package/types/host-settings-modules.d.ts +48 -0
package/README.md
CHANGED
|
@@ -458,8 +458,10 @@ quality_preference: balanced # balanced(缺省,逐字节现状)| cost |
|
|
|
458
458
|
- top 补选没有可用模型(候选耗尽/allowlist 全过滤)时,slow 不写覆盖、warn 汇总
|
|
459
459
|
(`no-pick`),命令不中断;无任何模型时计划为空,与既有「模型注册表不可用」分支一致。
|
|
460
460
|
- 覆盖为 **session 作用域、不写盘**:`overrideModelRoles` 只写 runtime overlay,
|
|
461
|
-
session 结束即还原;不会修改你的 `~/.omp/agent/config.yml
|
|
462
|
-
|
|
461
|
+
session 结束即还原;不会修改你的 `~/.omp/agent/config.yml`。每次应用前先清除本 session
|
|
462
|
+
的 modelRoles 运行时覆盖(宿主 18.3+ 走注册表句柄 `clearOverrideValue(cfgModelRoles)`,
|
|
463
|
+
≤18.1 走路径式 `clearOverride('modelRoles')`,两者由 `lib/host-settings-adapter.ts`
|
|
464
|
+
统一选取),避免上一 selector 的旧 role 值残留;`reset` 重算无约束
|
|
463
465
|
默认并覆盖(**不清空**,否则回落 config 硬钉)。
|
|
464
466
|
|
|
465
467
|
> **手动清理建议**:若全局 `~/.omp/agent/config.yml` 里有 `modelRoles.smol` 钉在会耗尽的
|
package/index.ts
CHANGED
|
@@ -843,7 +843,7 @@ export interface ConcurrencyAssemblyDeps {
|
|
|
843
843
|
configPath: string;
|
|
844
844
|
/** Agent dir the log reader / `model_perf` path derive from. */
|
|
845
845
|
agentDir?: string;
|
|
846
|
-
/** Host settings
|
|
846
|
+
/** Host settings singleton for the hot-apply (API generation resolved inside the adapter). */
|
|
847
847
|
settings?: SettingsPort | null;
|
|
848
848
|
/** Cross-process ① aggregation channel (`null` → local-only + warn). */
|
|
849
849
|
redis?: ConcurrencyRedisPort | null;
|
|
@@ -910,11 +910,15 @@ export default (pi: ExtensionAPI) => {
|
|
|
910
910
|
// but the model repeatedly breaks sloppy control lines too — retry loops
|
|
911
911
|
// burn tokens and abort agents. Point it at the simplest old_string/
|
|
912
912
|
// new_string "replace" variant instead.
|
|
913
|
-
// Preferred path:
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
913
|
+
// Preferred path: the runtime override (registry handle on omp >= 18.3,
|
|
914
|
+
// `settings.override` before that), in-memory and per-session. omp 18.0.11
|
|
915
|
+
// dropped `edit.modelVariants` from the path-based settings schema, so on
|
|
916
|
+
// hosts where neither runtime API reaches it the pin is persisted into the
|
|
917
|
+
// global config.yml once — the host reads the merged view directly, making
|
|
918
|
+
// the file equivalent. Fire-and-forget: the pin applies from the first
|
|
919
|
+
// edit turn, nothing here depends on its completion, and the function
|
|
920
|
+
// never rejects (it warns through the host logger).
|
|
921
|
+
void applyEditVariantPin(pi, { 'deepseek-v4-flash': 'replace' });
|
|
918
922
|
const directory = process.cwd();
|
|
919
923
|
const warn = (msg: string) => pi.logger.warn(msg);
|
|
920
924
|
const opsxConfig = readOpsxSettings(directory, warn, opsxGlobalHomeForTests ?? homedir());
|
|
@@ -1155,8 +1159,8 @@ export default (pi: ExtensionAPI) => {
|
|
|
1155
1159
|
// copy in node_modules is a separate, uninitialized instance — writing to
|
|
1156
1160
|
// it was a no-op). This thin wrapper is the single call site the
|
|
1157
1161
|
// constrained-period tick-reselect path (auto-model-family) also uses.
|
|
1158
|
-
const applyRoleModel = (sel: SelectionContext): void => {
|
|
1159
|
-
applyRoleModelOverrides(pi.pi?.settings, sel, warn);
|
|
1162
|
+
const applyRoleModel = async (sel: SelectionContext): Promise<void> => {
|
|
1163
|
+
await applyRoleModelOverrides(pi.pi?.settings, sel, warn);
|
|
1160
1164
|
};
|
|
1161
1165
|
|
|
1162
1166
|
// Instance-level usage-record claim: true only while this instance's
|
|
@@ -1170,10 +1174,15 @@ export default (pi: ExtensionAPI) => {
|
|
|
1170
1174
|
let recordsOwnUsage = false;
|
|
1171
1175
|
|
|
1172
1176
|
// ── adaptive provider concurrency: host seams ─────────────────
|
|
1173
|
-
/**
|
|
1177
|
+
/**
|
|
1178
|
+
* Host settings port for the control loop's hot-apply. The API shape is the
|
|
1179
|
+
* adapter's business (`lib/host-settings-adapter.ts`: registry handles on
|
|
1180
|
+
* omp >= 18.3, path-based `override` before that), so any object — i.e.
|
|
1181
|
+
* every host generation — qualifies here.
|
|
1182
|
+
*/
|
|
1174
1183
|
const hostSettingsPort = (): SettingsPort | null => {
|
|
1175
|
-
const settings = pi.pi?.settings as unknown
|
|
1176
|
-
return settings && typeof settings
|
|
1184
|
+
const settings = pi.pi?.settings as unknown;
|
|
1185
|
+
return settings && typeof settings === 'object' ? (settings as SettingsPort) : null;
|
|
1177
1186
|
};
|
|
1178
1187
|
/** Config root + agent dir for the control loop (host settings, else `~/.omp/agent`). */
|
|
1179
1188
|
const resolveConcurrencyPaths = (): { agentDir: string; configPath: string } => {
|
|
@@ -1749,7 +1758,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1749
1758
|
false,
|
|
1750
1759
|
reprobeUnreachable ? { reprobeUnreachable: true } : undefined,
|
|
1751
1760
|
);
|
|
1752
|
-
applyRoleModel(sel);
|
|
1761
|
+
await applyRoleModel(sel);
|
|
1753
1762
|
const picked = sel.selectorResults.reviewer?.picked;
|
|
1754
1763
|
if (picked && ctx.model && `${ctx.model.provider}/${ctx.model.id}` !== `${picked.provider}/${picked.id}`) {
|
|
1755
1764
|
await pi.setModel(picked);
|
|
@@ -1995,7 +2004,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1995
2004
|
}
|
|
1996
2005
|
}
|
|
1997
2006
|
selection = freshSel;
|
|
1998
|
-
applyRoleModel(freshSel);
|
|
2007
|
+
await applyRoleModel(freshSel);
|
|
1999
2008
|
const content = changes.length > 0
|
|
2000
2009
|
? renderPickModelReport('refresh', { action: '已刷新', constraint: formatFamilyConstraint(familyConstraint) || undefined, detail: [changes.join('\n')] }, symbols)
|
|
2001
2010
|
: renderPickModelReport('refresh', { action: '已刷新 · 所有模型状态正常,无需切换' }, symbols);
|
|
@@ -2062,7 +2071,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
2062
2071
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
2063
2072
|
selection = sel;
|
|
2064
2073
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint, collapsed);
|
|
2065
|
-
applyRoleModel(sel);
|
|
2074
|
+
await applyRoleModel(sel);
|
|
2066
2075
|
pi.sendMessage({
|
|
2067
2076
|
customType: 'pick-model',
|
|
2068
2077
|
content: renderPickModelReport('reset', {
|
|
@@ -2235,7 +2244,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
2235
2244
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
2236
2245
|
selection = sel;
|
|
2237
2246
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint, collapsed);
|
|
2238
|
-
applyRoleModel(sel);
|
|
2247
|
+
await applyRoleModel(sel);
|
|
2239
2248
|
const content = renderPickModelReport('switch', {
|
|
2240
2249
|
action: '已切换 selector 约束',
|
|
2241
2250
|
constraint: formatFamilyConstraint(familyConstraint) || undefined,
|
|
@@ -471,7 +471,7 @@ export interface ConcurrencyControlOptions {
|
|
|
471
471
|
dbPath?: string;
|
|
472
472
|
/** Cross-process ① aggregation channel; `null`/omitted disables it (warn + local only). */
|
|
473
473
|
redis?: ConcurrencyRedisPort | null;
|
|
474
|
-
/**
|
|
474
|
+
/** Host settings singleton for the hot-apply seam; omitted → file write only. */
|
|
475
475
|
settings?: SettingsPort | null;
|
|
476
476
|
/** Per-provider ceiling overrides (`concurrency_ceiling_by_provider`). */
|
|
477
477
|
ceilingByProvider?: Record<string, number>;
|
|
@@ -868,7 +868,7 @@ export class ConcurrencyController {
|
|
|
868
868
|
const shared = sharedMap[provider];
|
|
869
869
|
const divergedFromShared = typeof shared === 'number' && shared !== next.limit;
|
|
870
870
|
if ((evaluation.increased || evaluation.decreased || decay.moved || divergedFromShared) && isWriter) {
|
|
871
|
-
view.written = this.persistLimit(provider, next.limit, sharedMap, now);
|
|
871
|
+
view.written = await this.persistLimit(provider, next.limit, sharedMap, now);
|
|
872
872
|
}
|
|
873
873
|
result.providers[provider] = view;
|
|
874
874
|
}
|
|
@@ -1145,12 +1145,12 @@ export class ConcurrencyController {
|
|
|
1145
1145
|
return Math.max(1, Math.ceil((interval * AGGREGATION_TTL_INTERVALS) / 1000));
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
|
-
private persistLimit(
|
|
1148
|
+
private async persistLimit(
|
|
1149
1149
|
provider: string,
|
|
1150
1150
|
limit: number,
|
|
1151
1151
|
sharedMap: Record<string, number>,
|
|
1152
1152
|
now: number,
|
|
1153
|
-
): boolean {
|
|
1153
|
+
): Promise<boolean> {
|
|
1154
1154
|
// Write gate (task 4.2): |Δ| >= 1 AND more than
|
|
1155
1155
|
// concurrency_write_min_interval_ms since this provider's last write.
|
|
1156
1156
|
const gate = shouldWriteLimit({
|
|
@@ -1179,7 +1179,7 @@ export class ConcurrencyController {
|
|
|
1179
1179
|
return false;
|
|
1180
1180
|
}
|
|
1181
1181
|
// Adopt the in-lock read: it is the freshest view of every provider, so
|
|
1182
|
-
// the rest of this tick and the whole-key
|
|
1182
|
+
// the rest of this tick and the whole-key hot-apply override below never
|
|
1183
1183
|
// carry a snapshot taken before another process's write landed.
|
|
1184
1184
|
if (outcome.limits) {
|
|
1185
1185
|
for (const [id, value] of Object.entries(outcome.limits)) sharedMap[id] = value;
|
|
@@ -1188,7 +1188,7 @@ export class ConcurrencyController {
|
|
|
1188
1188
|
this.lastWriteAt.set(provider, now);
|
|
1189
1189
|
if (this.settings) {
|
|
1190
1190
|
try {
|
|
1191
|
-
const applied = applyLimit({
|
|
1191
|
+
const applied = await applyLimit({
|
|
1192
1192
|
settings: this.settings,
|
|
1193
1193
|
merged: outcome.limits ?? merged,
|
|
1194
1194
|
provider,
|
|
@@ -38,6 +38,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statS
|
|
|
38
38
|
import { dirname } from 'path';
|
|
39
39
|
import { YAML } from 'bun';
|
|
40
40
|
import { clampLimit } from './concurrency-tuner.js';
|
|
41
|
+
import { overrideProviderMaxInFlight } from './host-settings-adapter.js';
|
|
41
42
|
import { canonicalizeProvider } from './usage-resolver.js';
|
|
42
43
|
|
|
43
44
|
/** Sink for degradation notices; defaults to a no-op. */
|
|
@@ -218,8 +219,8 @@ export function readManagedLimits(
|
|
|
218
219
|
/**
|
|
219
220
|
* Merge one provider's new limit into the **complete** per-provider map.
|
|
220
221
|
* `providers.maxInFlightRequests` is a whole-key override on the host side, so
|
|
221
|
-
* every caller MUST pass the full map (this is the map that goes to
|
|
222
|
-
*
|
|
222
|
+
* every caller MUST pass the full map (this is the map that goes to the
|
|
223
|
+
* hot-apply override — {@link applyLimit} — and to {@link writeManagedLimit}).
|
|
223
224
|
*/
|
|
224
225
|
export function buildMergedMap(
|
|
225
226
|
current: Record<string, number>,
|
|
@@ -582,10 +583,15 @@ export function writeManagedLimit(args: WriteManagedLimitArgs): WriteManagedLimi
|
|
|
582
583
|
}
|
|
583
584
|
|
|
584
585
|
/**
|
|
585
|
-
* Injected host settings port (`pi.pi.settings`)
|
|
586
|
+
* Injected host settings port (`pi.pi.settings`).
|
|
587
|
+
*
|
|
588
|
+
* Only the identity of the host singleton matters here: the hot-apply goes
|
|
589
|
+
* through `lib/host-settings-adapter.ts`, which resolves the API generation
|
|
590
|
+
* (registry handle on omp >= 18.3, path-based `override` before that). Hence
|
|
591
|
+
* `override` is optional — a host that dropped it is still a usable port.
|
|
586
592
|
*/
|
|
587
593
|
export interface SettingsPort {
|
|
588
|
-
override(path: string, value: unknown): void;
|
|
594
|
+
override?(path: string, value: unknown): void;
|
|
589
595
|
}
|
|
590
596
|
|
|
591
597
|
export interface ApplyLimitArgs {
|
|
@@ -604,8 +610,10 @@ export interface ApplyLimitResult {
|
|
|
604
610
|
}
|
|
605
611
|
|
|
606
612
|
/**
|
|
607
|
-
* Hot-apply the shared map in this process
|
|
608
|
-
*
|
|
613
|
+
* Hot-apply the shared map in this process through the host settings runtime
|
|
614
|
+
* override (`providers.maxInFlightRequests`: registry handle on omp >= 18.3,
|
|
615
|
+
* path-based `settings.override` before that — both via
|
|
616
|
+
* `lib/host-settings-adapter.ts`).
|
|
609
617
|
*
|
|
610
618
|
* `providers.maxInFlightRequests` is a **whole-key override**, so the second
|
|
611
619
|
* argument MUST be the complete per-provider map — passing only the target
|
|
@@ -614,13 +622,14 @@ export interface ApplyLimitResult {
|
|
|
614
622
|
* (the caller keeps the previous value), while a partial map would silently
|
|
615
623
|
* make the omitted providers unlimited. When the API throws (host schema moved
|
|
616
624
|
* on), the persisted config is untouched — the host config watcher picks the
|
|
617
|
-
* value up — and the failure is warned, never fatal.
|
|
625
|
+
* value up — and the failure is warned, never fatal. Async because the adapter
|
|
626
|
+
* loads host handles by dynamic import.
|
|
618
627
|
*/
|
|
619
|
-
export function applyLimit(args: ApplyLimitArgs): ApplyLimitResult {
|
|
628
|
+
export async function applyLimit(args: ApplyLimitArgs): Promise<ApplyLimitResult> {
|
|
620
629
|
const warn = args.warn ?? NO_WARN;
|
|
621
630
|
const settings = args.settings;
|
|
622
|
-
if (!settings
|
|
623
|
-
warn('[omp-opsx-addon] concurrency: settings
|
|
631
|
+
if (!settings) {
|
|
632
|
+
warn('[omp-opsx-addon] concurrency: settings unavailable; skipping hot apply (config file value still applies)');
|
|
624
633
|
return { applied: false, reason: 'settings-unavailable' };
|
|
625
634
|
}
|
|
626
635
|
const provider = canonicalizeProvider(args.provider.trim());
|
|
@@ -631,15 +640,18 @@ export function applyLimit(args: ApplyLimitArgs): ApplyLimitResult {
|
|
|
631
640
|
const payload = buildMergedMap(args.merged ?? {}, provider, args.value);
|
|
632
641
|
for (const [id, value] of Object.entries(payload)) {
|
|
633
642
|
if (!id || typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
|
|
634
|
-
warn(`[omp-opsx-addon] concurrency: refusing
|
|
643
|
+
warn(`[omp-opsx-addon] concurrency: refusing the hot-apply override — invalid map entry ${JSON.stringify(id)}: ${JSON.stringify(value)}`);
|
|
635
644
|
return { applied: false, reason: 'invalid-merged' };
|
|
636
645
|
}
|
|
637
646
|
}
|
|
638
647
|
try {
|
|
639
|
-
settings
|
|
648
|
+
if (!(await overrideProviderMaxInFlight(settings, payload))) {
|
|
649
|
+
warn('[omp-opsx-addon] concurrency: no settings runtime-override API available (host handle / settings.override); skipping hot apply (config file value still applies)');
|
|
650
|
+
return { applied: false, reason: 'settings-unavailable' };
|
|
651
|
+
}
|
|
640
652
|
return { applied: true };
|
|
641
653
|
} catch (err) {
|
|
642
|
-
warn(`[omp-opsx-addon] concurrency: settings
|
|
654
|
+
warn(`[omp-opsx-addon] concurrency: settings hot-apply failed (${message(err)}); the host config watcher will apply the persisted value`);
|
|
643
655
|
return { applied: false, reason: 'override-failed' };
|
|
644
656
|
}
|
|
645
657
|
}
|
package/lib/edit-variant-pin.ts
CHANGED
|
@@ -2,9 +2,11 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import type { ExtensionAPI } from '@oh-my-pi/pi-coding-agent';
|
|
5
|
+
import { overrideEditModelVariants } from './host-settings-adapter.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Runtime edit-variant pin fallback for
|
|
8
|
+
* Runtime edit-variant pin, with a persisted fallback for hosts that no longer
|
|
9
|
+
* expose `edit.modelVariants` to the runtime settings API.
|
|
8
10
|
*
|
|
9
11
|
* The host hard-maps deepseek-v4-flash to the sloppy §/» marker grammar (it
|
|
10
12
|
* misreads hashline ranges), but the model repeatedly breaks sloppy control
|
|
@@ -12,11 +14,14 @@ import type { ExtensionAPI } from '@oh-my-pi/pi-coding-agent';
|
|
|
12
14
|
* plain old_string/new_string "replace" variant instead.
|
|
13
15
|
*
|
|
14
16
|
* Up to 17.x this extension did that via
|
|
15
|
-
* `settings.override("edit.modelVariants", …)
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `settings.override("edit.modelVariants", …)`; omp 18.3 replaced that
|
|
18
|
+
* path-based API with registry handles (`edit.modelVariants` handle →
|
|
19
|
+
* `handle.override(settings, …)`, see `lib/host-settings-adapter.ts`), so the
|
|
20
|
+
* runtime path lives on. On 18.0.11–18.x hosts whose path-based schema
|
|
21
|
+
* dropped `edit.modelVariants`, `override()` throws inside `get()`: the
|
|
22
|
+
* path-segment table has no entry for it and `getByPath` iterates
|
|
23
|
+
* `undefined`. Those hosts still honor the key when it arrives through the
|
|
24
|
+
* global config file — `#getEditVariantEntries` reads the merged view
|
|
20
25
|
* directly and `resolveEditMode` prefers the per-model variant over
|
|
21
26
|
* `edit.mode` — so persisting once is functionally equivalent to the runtime
|
|
22
27
|
* pin.
|
|
@@ -102,38 +107,41 @@ export function persistEditVariantPin(
|
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
/**
|
|
105
|
-
* Pin the edit variant for the target models.
|
|
110
|
+
* Pin the edit variant for the target models. Never rejects: every failure is
|
|
111
|
+
* warned through the host logger (the caller fires this and forgets it).
|
|
106
112
|
*
|
|
107
|
-
* Preferred path: in-memory
|
|
108
|
-
*
|
|
109
|
-
*
|
|
113
|
+
* Preferred path: the in-memory runtime override — the `edit.modelVariants`
|
|
114
|
+
* registry handle on omp >= 18.3, the path-based `settings.override` before
|
|
115
|
+
* that (both resolved inside `lib/host-settings-adapter.ts`). When this host
|
|
116
|
+
* offers no runtime path that reaches the setting (18.0.11+ dropped it from
|
|
117
|
+
* the path-based schema), persist into the global config.yml once —
|
|
118
|
+
* idempotent, effective from the next session.
|
|
110
119
|
*/
|
|
111
|
-
export function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string>): void {
|
|
120
|
+
export async function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string>): Promise<void> {
|
|
112
121
|
// `pi.pi` is the whole SDK module namespace; its `settings` field has no
|
|
113
|
-
// declared runtime instance type on the extension-facing surface, so
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
| { override(path: string, value: unknown): void; getAgentDir?: () => string }
|
|
118
|
-
| undefined;
|
|
119
|
-
if (!settings) {
|
|
122
|
+
// declared runtime instance type on the extension-facing surface, so the
|
|
123
|
+
// `in`/`typeof` checks below are the whole contract.
|
|
124
|
+
const settings: unknown = pi.pi?.settings;
|
|
125
|
+
if (!settings || typeof settings !== 'object') {
|
|
120
126
|
pi.logger?.warn('[omp-opsx-addon] pi.pi.settings unavailable; edit variant pin skipped');
|
|
121
127
|
return;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
try {
|
|
125
|
-
settings
|
|
126
|
-
return;
|
|
131
|
+
if (await overrideEditModelVariants(settings, pin)) return;
|
|
127
132
|
} catch {
|
|
128
|
-
/*
|
|
133
|
+
/* host schema no longer knows the path — persist instead */
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
try {
|
|
132
|
-
let agentDir
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
let agentDir = '';
|
|
138
|
+
if ('getAgentDir' in settings && typeof settings.getAgentDir === 'function') {
|
|
139
|
+
try {
|
|
140
|
+
const dir = settings.getAgentDir();
|
|
141
|
+
if (typeof dir === 'string') agentDir = dir;
|
|
142
|
+
} catch {
|
|
143
|
+
agentDir = '';
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
if (!agentDir) agentDir = path.join(os.homedir(), '.omp', 'agent');
|
|
139
147
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Settings API compatibility adapter (change: omp-18.3-settings-api-compat).
|
|
3
|
+
*
|
|
4
|
+
* omp 18.3.2 removed the path-based Settings API (`settings.override(path, value)`
|
|
5
|
+
* / `settings.clearOverride(path)`) in favour of registry handles: a handle
|
|
6
|
+
* carries the setting's identity — `handle.override(settings, value)` writes the
|
|
7
|
+
* runtime overlay and clearing goes through
|
|
8
|
+
* `settings.clearOverrideValue(handle)` (equivalent to
|
|
9
|
+
* `handle.clearOverride(settings)`). Every path-based call this plugin made
|
|
10
|
+
* (`clearOverride('modelRoles')`, `override('task.agentModelOverrides', …)`,
|
|
11
|
+
* `override('providers.maxInFlightRequests', …)`,
|
|
12
|
+
* `override('edit.modelVariants', …)`) now throws
|
|
13
|
+
* (`settings.clearOverride is not a function`), which took down the whole
|
|
14
|
+
* `/pick-model` apply path and silently disabled the concurrency hot-apply.
|
|
15
|
+
*
|
|
16
|
+
* Handles live on deep host subpaths and are loaded LAZILY via dynamic
|
|
17
|
+
* `import()` — never a static import:
|
|
18
|
+
* - this package's own dev dependency is older (18.1.2) and ships no such
|
|
19
|
+
* subpaths, so a static import would break typecheck/build;
|
|
20
|
+
* - the host's legacy-pi-compat redirect rewrites these specifiers to the
|
|
21
|
+
* host's own module instances, which is the singleton the session reads
|
|
22
|
+
* (a separately `require()`d copy would be a no-op target for writes).
|
|
23
|
+
*
|
|
24
|
+
* The specifiers MUST BE STRING LITERALS — `import('@oh-my-pi/pi-coding-agent/
|
|
25
|
+
* config/model-settings')` — never a computed/concatenated string. The host
|
|
26
|
+
* rewrites extension sources textually (it collects string-literal import
|
|
27
|
+
* references and swaps them for absolute host paths), so a runtime-built
|
|
28
|
+
* specifier is not seen by the rewriter and resolves natively against this
|
|
29
|
+
* package's own 18.1.2 copy, i.e. the handle silently stays `undefined` on
|
|
30
|
+
* exactly the hosts this adapter exists for. `types/host-settings-modules.d.ts`
|
|
31
|
+
* supplies the build-time declarations those literal specifiers need.
|
|
32
|
+
* Each specifier is attempted independently of the others and the result (hit
|
|
33
|
+
* or miss) is cached for the process lifetime, so a host without these
|
|
34
|
+
* subpaths pays each failed import exactly once.
|
|
35
|
+
*
|
|
36
|
+
* Every helper falls back to the path-based API when the handle is missing, so
|
|
37
|
+
* hosts <= 18.1 keep their previous behaviour byte-for-byte.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Minimal host registry-handle surface (omp >= 18.3 settings API). */
|
|
41
|
+
export interface HostSettingHandle {
|
|
42
|
+
/** Write this setting's runtime override (never persisted). */
|
|
43
|
+
override(settings: unknown, value: unknown): void;
|
|
44
|
+
/**
|
|
45
|
+
* Clear this setting's runtime override. Present on handles, but the
|
|
46
|
+
* documented clear path is `settings.clearOverrideValue(handle)`, so the
|
|
47
|
+
* adapter tries that first (see {@link clearModelRolesRuntimeOverride}).
|
|
48
|
+
*/
|
|
49
|
+
clearOverride?(settings: unknown): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Resolved handle per setting this plugin touches; a miss stays `undefined`. */
|
|
53
|
+
export interface HostSettingsHandles {
|
|
54
|
+
/** `cfgModelRoles` — `config/model-settings`. */
|
|
55
|
+
modelRoles?: HostSettingHandle;
|
|
56
|
+
/** `cfgTaskAgentModelOverrides` — `task/settings`. */
|
|
57
|
+
taskAgentModelOverrides?: HostSettingHandle;
|
|
58
|
+
/** `cfgProvidersMaxInFlightRequests` — `session/settings`. */
|
|
59
|
+
providersMaxInFlight?: HostSettingHandle;
|
|
60
|
+
/** `cfgEditModelVariants` — `edit/settings`. */
|
|
61
|
+
editModelVariants?: HostSettingHandle;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface HandleSpec {
|
|
65
|
+
key: keyof HostSettingsHandles;
|
|
66
|
+
exportName: string;
|
|
67
|
+
/**
|
|
68
|
+
* Literal-specifier loader (see the header: a computed specifier would not
|
|
69
|
+
* be rewritten by the host loader). Dynamic, so an older host's missing
|
|
70
|
+
* subpath rejects instead of breaking module evaluation.
|
|
71
|
+
*/
|
|
72
|
+
load: () => Promise<unknown>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const HANDLE_SPECS: readonly HandleSpec[] = [
|
|
76
|
+
{
|
|
77
|
+
key: 'modelRoles',
|
|
78
|
+
exportName: 'cfgModelRoles',
|
|
79
|
+
load: () => import('@oh-my-pi/pi-coding-agent/config/model-settings'),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'taskAgentModelOverrides',
|
|
83
|
+
exportName: 'cfgTaskAgentModelOverrides',
|
|
84
|
+
load: () => import('@oh-my-pi/pi-coding-agent/task/settings'),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'providersMaxInFlight',
|
|
88
|
+
exportName: 'cfgProvidersMaxInFlightRequests',
|
|
89
|
+
load: () => import('@oh-my-pi/pi-coding-agent/session/settings'),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'editModelVariants',
|
|
93
|
+
exportName: 'cfgEditModelVariants',
|
|
94
|
+
load: () => import('@oh-my-pi/pi-coding-agent/edit/settings'),
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
async function loadHandle(spec: HandleSpec): Promise<HostSettingHandle | undefined> {
|
|
99
|
+
try {
|
|
100
|
+
const mod = (await spec.load()) as Record<string, unknown> | null | undefined;
|
|
101
|
+
const candidate = mod?.[spec.exportName] as Partial<HostSettingHandle> | undefined;
|
|
102
|
+
if (candidate && typeof candidate.override === 'function') return candidate as HostSettingHandle;
|
|
103
|
+
} catch {
|
|
104
|
+
/* subpath absent on this host (<= 18.1) — the path-based fallback applies */
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let handlesPromise: Promise<HostSettingsHandles> | null = null;
|
|
110
|
+
let handlesForTest: HostSettingsHandles | null | undefined;
|
|
111
|
+
|
|
112
|
+
/** Test-only seam: inject fake handles, bypassing the dynamic imports. */
|
|
113
|
+
export function _setHandlesForTest(handles: HostSettingsHandles | null | undefined): void {
|
|
114
|
+
handlesForTest = handles;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Test-only seam: drop injected handles and any cached load. */
|
|
118
|
+
export function _resetForTest(): void {
|
|
119
|
+
handlesForTest = undefined;
|
|
120
|
+
handlesPromise = null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handles of the running host, loaded once per process. A host that lacks the
|
|
125
|
+
* subpaths yields `{}` — cached, so the failed imports run exactly once.
|
|
126
|
+
*/
|
|
127
|
+
export function hostSettingsHandles(): Promise<HostSettingsHandles> {
|
|
128
|
+
if (handlesForTest !== undefined) return Promise.resolve(handlesForTest ?? {});
|
|
129
|
+
handlesPromise ??= (async () => {
|
|
130
|
+
const out: HostSettingsHandles = {};
|
|
131
|
+
await Promise.all(
|
|
132
|
+
HANDLE_SPECS.map(async (spec) => {
|
|
133
|
+
const handle = await loadHandle(spec);
|
|
134
|
+
if (handle) out[spec.key] = handle;
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
return out;
|
|
138
|
+
})();
|
|
139
|
+
return handlesPromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Path-based Settings API on hosts <= 18.1. */
|
|
143
|
+
interface PathSettings {
|
|
144
|
+
override?(path: string, value: unknown): void;
|
|
145
|
+
clearOverride?(path: string): void;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Handle-clearing Settings API on hosts >= 18.3. */
|
|
149
|
+
interface HandleClearingSettings {
|
|
150
|
+
clearOverrideValue?(handle: unknown): void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Shared write path: registry handle first, path-based `override` second. */
|
|
154
|
+
async function writeOverride(
|
|
155
|
+
handleKey: keyof HostSettingsHandles,
|
|
156
|
+
legacyPath: string,
|
|
157
|
+
settings: unknown,
|
|
158
|
+
value: unknown,
|
|
159
|
+
): Promise<boolean> {
|
|
160
|
+
const handle = (await hostSettingsHandles())[handleKey];
|
|
161
|
+
if (handle) {
|
|
162
|
+
handle.override(settings, value);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
const legacy = typeof settings === 'object' && settings !== null ? (settings as PathSettings) : null;
|
|
166
|
+
if (legacy && typeof legacy.override === 'function') {
|
|
167
|
+
legacy.override(legacyPath, value);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear the session-scoped `modelRoles` overlay.
|
|
175
|
+
*
|
|
176
|
+
* omp >= 18.3: `settings.clearOverrideValue(cfgModelRoles)` (falling back to
|
|
177
|
+
* `handle.clearOverride(settings)`). <= 18.1: `settings.clearOverride('modelRoles')`.
|
|
178
|
+
*
|
|
179
|
+
* Returns `false` when neither API is present — callers warn and keep writing
|
|
180
|
+
* (an absent *clear* API does not mean settings are unusable).
|
|
181
|
+
*/
|
|
182
|
+
export async function clearModelRolesRuntimeOverride(settings: unknown): Promise<boolean> {
|
|
183
|
+
const handle = (await hostSettingsHandles()).modelRoles;
|
|
184
|
+
if (handle) {
|
|
185
|
+
const clearing = typeof settings === 'object' && settings !== null
|
|
186
|
+
? (settings as HandleClearingSettings)
|
|
187
|
+
: null;
|
|
188
|
+
if (clearing && typeof clearing.clearOverrideValue === 'function') {
|
|
189
|
+
clearing.clearOverrideValue(handle);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
if (typeof handle.clearOverride === 'function') {
|
|
193
|
+
handle.clearOverride(settings);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const legacy = typeof settings === 'object' && settings !== null ? (settings as PathSettings) : null;
|
|
198
|
+
if (legacy && typeof legacy.clearOverride === 'function') {
|
|
199
|
+
legacy.clearOverride('modelRoles');
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Write the `task.agentModelOverrides` neutralization (handle API, else path API). */
|
|
206
|
+
export function applyAgentModelOverrides(
|
|
207
|
+
settings: unknown,
|
|
208
|
+
value: Record<string, string>,
|
|
209
|
+
): Promise<boolean> {
|
|
210
|
+
return writeOverride('taskAgentModelOverrides', 'task.agentModelOverrides', settings, value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Hot-apply the complete `providers.maxInFlightRequests` map (handle API, else path API). */
|
|
214
|
+
export function overrideProviderMaxInFlight(
|
|
215
|
+
settings: unknown,
|
|
216
|
+
value: Record<string, number>,
|
|
217
|
+
): Promise<boolean> {
|
|
218
|
+
return writeOverride('providersMaxInFlight', 'providers.maxInFlightRequests', settings, value);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Pin `edit.modelVariants` at runtime (handle API, else path API). */
|
|
222
|
+
export function overrideEditModelVariants(
|
|
223
|
+
settings: unknown,
|
|
224
|
+
value: Record<string, string>,
|
|
225
|
+
): Promise<boolean> {
|
|
226
|
+
return writeOverride('editModelVariants', 'edit.modelVariants', settings, value);
|
|
227
|
+
}
|
package/lib/model-roles.ts
CHANGED
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
* commit/tiny via enumeration fallbacks terminating at smol, task via
|
|
18
18
|
* primary-session inheritance, plan as a graceful no-op, advisor via the
|
|
19
19
|
* static slow chain, custom roles via the config layer. The landing sequence
|
|
20
|
-
* is unchanged:
|
|
21
|
-
* empty-string neutralization of persisted per-agent pins
|
|
20
|
+
* is unchanged: clear the modelRoles session overlay → overrideModelRoles(payload)
|
|
21
|
+
* → empty-string neutralization of persisted per-agent pins (the clear and the
|
|
22
|
+
* neutralization go through `lib/host-settings-adapter.ts`: registry handles on
|
|
23
|
+
* omp >= 18.3, path-based settings calls before that).
|
|
22
24
|
*/
|
|
23
25
|
import { type ModelRole } from '@oh-my-pi/pi-coding-agent/config/model-roles';
|
|
24
26
|
import type { Model, Api } from '@oh-my-pi/pi-catalog/types';
|
|
25
27
|
import type { SelectionContext } from '../index.js';
|
|
26
28
|
import type { TierName } from './model-tiers.js';
|
|
29
|
+
import { applyAgentModelOverrides, clearModelRolesRuntimeOverride } from './host-settings-adapter.js';
|
|
27
30
|
import { OPSX_AGENTS } from './unified-config.js';
|
|
28
31
|
|
|
29
32
|
/** Per-agent names written into `task.agentModelOverrides` neutralization. */
|
|
@@ -138,8 +141,8 @@ export function resolveOmpRoleTier(
|
|
|
138
141
|
* Build the minimal write-set overlay plan (design D5): for each role in
|
|
139
142
|
* `OMP_WRITE_SET_ROLES`, resolve its tier (or `skip`), apply capability
|
|
140
143
|
* gating, then assign the single picked model for the tier. Roles without a
|
|
141
|
-
* pick are recorded with a reason and omitted from the payload — after
|
|
142
|
-
*
|
|
144
|
+
* pick are recorded with a reason and omitted from the payload — after the
|
|
145
|
+
* pre-clear they fall back to the user's config layer (or stay
|
|
143
146
|
* unconfigured). Roles outside the write set are never iterated.
|
|
144
147
|
*/
|
|
145
148
|
export function buildRoleOverridePlan(input: BuildRoleOverridePlanInput): RoleOverridePlan {
|
|
@@ -226,11 +229,26 @@ export function agentOverrideNeutralization(): Record<string, string> {
|
|
|
226
229
|
return neutral;
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Minimal Settings surface this module needs (host-process `pi.pi.settings`).
|
|
234
|
+
*
|
|
235
|
+
* Deliberately a union of both host generations: `overrideModelRoles` (the
|
|
236
|
+
* session overlay write) exists in every version, while clearing and the
|
|
237
|
+
* path-based writes moved from `clearOverride(path)` / `override(path, value)`
|
|
238
|
+
* (<= 18.1) to registry handles (`clearOverrideValue(handle)`, see
|
|
239
|
+
* `lib/host-settings-adapter.ts`) in 18.3. Optional members keep every
|
|
240
|
+
* generation assignable; `applyRoleModelOverrides` never assumes a method
|
|
241
|
+
* exists.
|
|
242
|
+
*/
|
|
230
243
|
export interface RoleSettings {
|
|
231
|
-
|
|
244
|
+
/** Session overlay write (limited to `{smol, default, slow, vision}` by the plan). */
|
|
232
245
|
overrideModelRoles(roles: Record<string, string>): void;
|
|
233
|
-
|
|
246
|
+
/** omp <= 18.1: clear the `modelRoles` runtime overlay by path. */
|
|
247
|
+
clearOverride?(key: string): void;
|
|
248
|
+
/** omp >= 18.3: clear a runtime overlay through its registry handle. */
|
|
249
|
+
clearOverrideValue?(handle: unknown): void;
|
|
250
|
+
/** omp <= 18.1: path-based runtime override. */
|
|
251
|
+
override?(key: string, value: unknown): void;
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
/**
|
|
@@ -243,25 +261,36 @@ export interface RoleSettings {
|
|
|
243
261
|
* picked model are omitted and fall back to the config layer after the
|
|
244
262
|
* clear) → neutralize persisted per-agent pins with empty strings. Never writes
|
|
245
263
|
* disk; the runtime overlay is released with the session.
|
|
264
|
+
*
|
|
265
|
+
* Async because clearing/writing goes through `lib/host-settings-adapter.ts`,
|
|
266
|
+
* whose host handles are loaded by dynamic import; every call site awaits.
|
|
267
|
+
* Clearing and the neutralization write degrade to a warn when this host
|
|
268
|
+
* offers neither API generation — the missing *clear* is not a reason to skip
|
|
269
|
+
* the overlay write itself (the plan payload still applies).
|
|
246
270
|
*/
|
|
247
|
-
export function applyRoleModelOverrides(
|
|
271
|
+
export async function applyRoleModelOverrides(
|
|
248
272
|
settings: RoleSettings | null | undefined,
|
|
249
273
|
sel: SelectionContext,
|
|
250
274
|
warn: (msg: string) => void = () => {},
|
|
251
|
-
): void {
|
|
275
|
+
): Promise<void> {
|
|
252
276
|
if (!settings) {
|
|
253
277
|
warn('[omp-opsx-addon] pi.pi.settings unavailable; model role overrides skipped');
|
|
254
278
|
return;
|
|
255
279
|
}
|
|
256
|
-
settings
|
|
280
|
+
if (!(await clearModelRolesRuntimeOverride(settings))) {
|
|
281
|
+
warn('[omp-opsx-addon] no settings API to clear the modelRoles session overlay (host handles and path-based clearOverride both unavailable); writing the overlay without a pre-clear — stale roles from a previous selection may linger');
|
|
282
|
+
}
|
|
257
283
|
settings.overrideModelRoles(sel.roleOverrides);
|
|
258
|
-
settings
|
|
284
|
+
const neutralized = await applyAgentModelOverrides(settings, agentOverrideNeutralization());
|
|
285
|
+
if (!neutralized) {
|
|
286
|
+
warn('[omp-opsx-addon] no settings API for task.agentModelOverrides (host handles and path-based override both unavailable)');
|
|
287
|
+
}
|
|
259
288
|
const applied = Object.keys(sel.roleOverrides);
|
|
260
289
|
const skippedText = sel.skippedRoles.length > 0
|
|
261
290
|
? `; skipped: ${sel.skippedRoles.map((s) => `${s.role}(${s.reason})`).join(', ')}`
|
|
262
291
|
: '';
|
|
263
292
|
const skipWarns = sel.skippedRoles.filter((s) => s.reason !== 'skip');
|
|
264
|
-
warn(`[omp-opsx-addon] modelRoles applied (${applied.length} roles: ${applied.join(', ') || 'none'})${skippedText}; per-agent pins neutralized`);
|
|
293
|
+
warn(`[omp-opsx-addon] modelRoles applied (${applied.length} roles: ${applied.join(', ') || 'none'})${skippedText}; per-agent pins ${neutralized ? 'neutralized' : 'NOT neutralized'}`);
|
|
265
294
|
for (const s of skipWarns) {
|
|
266
295
|
if (s.reason === 'capability') {
|
|
267
296
|
warn(`[omp-opsx-addon] model role "${s.role}": no capable model in selection; skipped (role keeps configured/default value; change selector/allowlist or set model_role_tiers.${s.role}: skip)`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient declarations for the host Settings registry-handle subpaths
|
|
3
|
+
* (change: omp-18.3-settings-api-compat).
|
|
4
|
+
*
|
|
5
|
+
* `lib/host-settings-adapter.ts` must import these four modules with LITERAL
|
|
6
|
+
* specifiers: the host's legacy-pi-compat loader rewrites extension sources
|
|
7
|
+
* textually (it collects string-literal import/import() references and replaces
|
|
8
|
+
* them with absolute host paths), so a computed specifier would silently resolve
|
|
9
|
+
* against this package's own `@oh-my-pi/pi-coding-agent` dev dependency
|
|
10
|
+
* (18.1.2), which ships none of these subpaths.
|
|
11
|
+
*
|
|
12
|
+
* Literal specifiers, however, are resolved by `tsc` at build time — hence these
|
|
13
|
+
* declarations. Their shape mirrors the host's `Setting` handle: `override` is
|
|
14
|
+
* the write path, `clearOverride` is the (optional) clear path that
|
|
15
|
+
* `Settings.clearOverrideValue(handle)` performs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
declare module '@oh-my-pi/pi-coding-agent/config/model-settings' {
|
|
19
|
+
/** Registered setting handle `modelRoles`. */
|
|
20
|
+
export const cfgModelRoles: {
|
|
21
|
+
override(settings: unknown, value: unknown): void;
|
|
22
|
+
clearOverride?(settings: unknown): void;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module '@oh-my-pi/pi-coding-agent/task/settings' {
|
|
27
|
+
/** Registered setting handle `task.agentModelOverrides`. */
|
|
28
|
+
export const cfgTaskAgentModelOverrides: {
|
|
29
|
+
override(settings: unknown, value: unknown): void;
|
|
30
|
+
clearOverride?(settings: unknown): void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '@oh-my-pi/pi-coding-agent/session/settings' {
|
|
35
|
+
/** Registered setting handle `providers.maxInFlightRequests`. */
|
|
36
|
+
export const cfgProvidersMaxInFlightRequests: {
|
|
37
|
+
override(settings: unknown, value: unknown): void;
|
|
38
|
+
clearOverride?(settings: unknown): void;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module '@oh-my-pi/pi-coding-agent/edit/settings' {
|
|
43
|
+
/** Registered setting handle `edit.modelVariants`. */
|
|
44
|
+
export const cfgEditModelVariants: {
|
|
45
|
+
override(settings: unknown, value: unknown): void;
|
|
46
|
+
clearOverride?(settings: unknown): void;
|
|
47
|
+
};
|
|
48
|
+
}
|