@genee/omp-opsx-addon 0.9.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 +10 -9
- package/index.ts +123 -65
- package/lib/agent-defs.ts +26 -8
- 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 +43 -14
- package/lib/pipe-push.ts +83 -8
- package/lib/reachability-refresh.ts +10 -2
- package/lib/system-prompt.ts +15 -6
- package/lib/tiers-data.ts +8 -1
- package/lib/unified-config.ts +53 -91
- package/package.json +1 -1
- package/types/host-settings-modules.d.ts +48 -0
|
@@ -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,14 +17,17 @@
|
|
|
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';
|
|
27
|
-
import {
|
|
29
|
+
import { applyAgentModelOverrides, clearModelRolesRuntimeOverride } from './host-settings-adapter.js';
|
|
30
|
+
import { OPSX_AGENTS } from './unified-config.js';
|
|
28
31
|
|
|
29
32
|
/** Per-agent names written into `task.agentModelOverrides` neutralization. */
|
|
30
33
|
const AGENT_MODEL_KEYS: Record<string, string> = {
|
|
@@ -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 {
|
|
@@ -222,15 +225,30 @@ export function dispatchModelHint(role: string, selection: SelectionContext): st
|
|
|
222
225
|
*/
|
|
223
226
|
export function agentOverrideNeutralization(): Record<string, string> {
|
|
224
227
|
const neutral: Record<string, string> = {};
|
|
225
|
-
for (const role of
|
|
228
|
+
for (const role of OPSX_AGENTS) neutral[AGENT_MODEL_KEYS[role]] = '';
|
|
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)`);
|