@genee/omp-opsx-addon 0.3.0 → 0.5.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 +82 -0
- package/index.ts +248 -154
- package/lib/agent-defs.ts +4 -4
- package/lib/edit-variant-pin.ts +4 -4
- package/lib/logger.ts +33 -0
- package/lib/model-roles.ts +270 -0
- package/lib/pipe-core.ts +784 -0
- package/lib/pipe-push.ts +467 -0
- package/lib/reachability-cache.ts +2 -1
- package/lib/reachability-probe.ts +3 -2
- package/lib/status-probe.ts +4 -3
- package/lib/unified-config.ts +42 -1
- package/lib/usage-poller.ts +208 -7
- package/lib/usage-redis-multiproc.worker.ts +5 -1
- package/lib/usage-render.ts +151 -97
- package/lib/usage-resolver.ts +10 -4
- package/package.json +1 -1
package/lib/agent-defs.ts
CHANGED
|
@@ -5,7 +5,7 @@ const MARKER = '<!-- @genee/omp-opsx-addon -->';
|
|
|
5
5
|
const CODE_REVIEWER_MD = (model?: string) => `---
|
|
6
6
|
name: code-reviewer
|
|
7
7
|
description: OpenSpec 代码审查 agent,审查代码实现并执行全局验证
|
|
8
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
8
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
你是一位资深代码审查员。你根据 OpenSpec 提案和设计文档审查代码实现,并在审阅的同时执行最后一次全局验证。
|
|
@@ -71,7 +71,7 @@ ${MARKER}
|
|
|
71
71
|
const PROPOSAL_REVIEWER_MD = (model?: string) => `---
|
|
72
72
|
name: proposal-reviewer
|
|
73
73
|
description: OpenSpec 提案审查 agent,审查提案质量
|
|
74
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
74
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
75
75
|
---
|
|
76
76
|
|
|
77
77
|
你是一位资深需求审查员。你审查 OpenSpec 提案的完整性和质量。
|
|
@@ -123,7 +123,7 @@ ${MARKER}
|
|
|
123
123
|
const CODER_MD = (model?: string) => `---
|
|
124
124
|
name: coder
|
|
125
125
|
description: OpenSpec 变更实现 agent,同步 tasks.md 进度
|
|
126
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}
|
|
126
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}
|
|
127
127
|
skill: openspec-apply-change
|
|
128
128
|
---
|
|
129
129
|
|
|
@@ -184,7 +184,7 @@ ${MARKER}
|
|
|
184
184
|
const PLANNER_MD = (model?: string) => `---
|
|
185
185
|
name: planner
|
|
186
186
|
description: OpenSpec 提案规划 agent
|
|
187
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}\nskill: openspec-propose
|
|
187
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}\nskill: openspec-propose
|
|
188
188
|
---
|
|
189
189
|
|
|
190
190
|
你是一个 OpenSpec 提案规划 agent。负责创建和更新 OpenSpec 变更提案文档。
|
package/lib/edit-variant-pin.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string
|
|
|
117
117
|
| { override(path: string, value: unknown): void; getAgentDir?: () => string }
|
|
118
118
|
| undefined;
|
|
119
119
|
if (!settings) {
|
|
120
|
-
|
|
120
|
+
pi.logger?.warn('[omp-opsx-addon] pi.pi.settings unavailable; edit variant pin skipped');
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -139,18 +139,18 @@ export function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string
|
|
|
139
139
|
|
|
140
140
|
const configPath = resolveGlobalConfigPath(agentDir);
|
|
141
141
|
if (!configPath) {
|
|
142
|
-
|
|
142
|
+
pi.logger?.warn(
|
|
143
143
|
`[omp-opsx-addon] edit variant pin failed: no global config.yml found under ${agentDir}`,
|
|
144
144
|
);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
const outcome = persistEditVariantPin(configPath, pin);
|
|
148
148
|
if (outcome === 'written') {
|
|
149
|
-
|
|
149
|
+
pi.logger?.warn(
|
|
150
150
|
`[omp-opsx-addon] edit variant pin persisted to ${configPath} (effective from the next session)`,
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
} catch (err) {
|
|
154
|
-
|
|
154
|
+
pi.logger?.warn('[omp-opsx-addon] edit variant pin failed', { err });
|
|
155
155
|
}
|
|
156
156
|
}
|
package/lib/logger.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件日志通道(模块级注入)。
|
|
3
|
+
*
|
|
4
|
+
* 主进程日志不再走 console.*(会在 omp TUI 里打爆界面),改走宿主
|
|
5
|
+
* pi.logger(滚动文件日志)。lib 模块拿不到 pi 对象,故提供模块级 holder:
|
|
6
|
+
* 入口 setOpsxLogger(pi.logger) 注入,lib 侧 getLogger() 读取。
|
|
7
|
+
* 未注入(测试场景直接调用叶子函数)时默认 console,输出与迁移前一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 窄化日志接口:宿主 pi.logger 与 console 的结构交集,仅本插件使用的四级方法。 */
|
|
11
|
+
export interface OpsxLogger {
|
|
12
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
14
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
15
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 模块级当前 logger,默认 console(未注入/测试场景保持旧行为)。 */
|
|
19
|
+
let current: OpsxLogger = console;
|
|
20
|
+
|
|
21
|
+
/** 插件入口注入宿主 pi.logger(进程级单例,多实例重复注入幂等)。
|
|
22
|
+
* 空值注入忽略:保持 getLogger() 恒可用(console 兜底)——测试 mock 的
|
|
23
|
+
* pi 对象可能不含 logger,此时视同未注入,输出与迁移前一致。 */
|
|
24
|
+
export function setOpsxLogger(logger: OpsxLogger): void {
|
|
25
|
+
if (logger) {
|
|
26
|
+
current = logger;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 读取当前 logger。 */
|
|
31
|
+
export function getLogger(): OpsxLogger {
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pick-model → OMP modelRoles application.
|
|
3
|
+
*
|
|
4
|
+
* change: pick-model-role-targeting — selector results are applied by TIER
|
|
5
|
+
* via session-scoped `settings.overrideModelRoles` instead of per-agent
|
|
6
|
+
* `task.agentModelOverrides` writes.
|
|
7
|
+
*
|
|
8
|
+
* change: pick-model-batch-all-roles — the overlay covers EVERY role known to
|
|
9
|
+
* the host (`getKnownRoleIds(settings)`: the 10 built-in roles plus custom
|
|
10
|
+
* roles from cycleOrder/modelRoles/modelTags). `buildRoleOverridePlan` maps
|
|
11
|
+
* each known role to a tier (default table, overridable via
|
|
12
|
+
* `model_role_tiers`), feeds it the single picked model for that tier, and
|
|
13
|
+
* records skip reasons (`skip` config sentinel, capability gate such as
|
|
14
|
+
* vision/image, or no picked model for the tier). The landing sequence is
|
|
15
|
+
* unchanged: clearOverride('modelRoles') → overrideModelRoles(payload) →
|
|
16
|
+
* empty-string neutralization of persisted per-agent pins.
|
|
17
|
+
*/
|
|
18
|
+
import { type ModelRole } from '@oh-my-pi/pi-coding-agent/config/model-roles';
|
|
19
|
+
import type { Model, Api } from '@oh-my-pi/pi-catalog/types';
|
|
20
|
+
import type { SelectionContext } from '../index.js';
|
|
21
|
+
import type { TierName } from './model-tiers.js';
|
|
22
|
+
import { OPSX_ROLES } from './unified-config.js';
|
|
23
|
+
|
|
24
|
+
/** Per-agent names written into `task.agentModelOverrides` neutralization. */
|
|
25
|
+
const AGENT_MODEL_KEYS: Record<string, string> = {
|
|
26
|
+
coder: 'coder',
|
|
27
|
+
reviewer: 'code-reviewer',
|
|
28
|
+
planner: 'planner',
|
|
29
|
+
'proposal-reviewer': 'proposal-reviewer',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Opsx role → OMP tier role alias (without `@`). */
|
|
33
|
+
export const ROLE_TO_OMP_ALIAS: Record<string, string> = {
|
|
34
|
+
coder: 'smol',
|
|
35
|
+
reviewer: 'default',
|
|
36
|
+
planner: 'default',
|
|
37
|
+
'proposal-reviewer': 'default',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Built-in OMP model role → default tier (change: pick-model-batch-all-roles
|
|
42
|
+
* D2). Key source is `MODEL_ROLE_IDS`; the `satisfies` clause forces the
|
|
43
|
+
* table to cover every built-in role — no hardcoded role list is enumerated
|
|
44
|
+
* in code.
|
|
45
|
+
*/
|
|
46
|
+
export const OMP_ROLE_TO_TIER = {
|
|
47
|
+
tiny: 'tiny',
|
|
48
|
+
smol: 'mid',
|
|
49
|
+
task: 'mid',
|
|
50
|
+
commit: 'low',
|
|
51
|
+
default: 'high',
|
|
52
|
+
designer: 'high',
|
|
53
|
+
vision: 'high',
|
|
54
|
+
slow: 'top',
|
|
55
|
+
plan: 'top',
|
|
56
|
+
advisor: 'top',
|
|
57
|
+
} satisfies Record<ModelRole, TierName>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Capability requirements per role (D3). A role only receives a model that
|
|
61
|
+
* passes its predicate; when no candidate qualifies the role is skipped
|
|
62
|
+
* (reason `capability`) rather than written an incapable model. The table
|
|
63
|
+
* is extensible; vision is the only gated role today.
|
|
64
|
+
*
|
|
65
|
+
* `Model.input` is a required `("text"|"image")[]` in the catalog; a
|
|
66
|
+
* non-array value is treated conservatively as text-only.
|
|
67
|
+
*/
|
|
68
|
+
export const ROLE_CAPABILITY: Record<string, (model: Model<Api>) => boolean> = {
|
|
69
|
+
vision: (model) => Array.isArray(model.input) && model.input.includes('image'),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Why a known role was left out of the overlay payload. */
|
|
73
|
+
export type RoleSkipReason = 'skip' | 'capability' | 'no-pick';
|
|
74
|
+
|
|
75
|
+
/** A role omitted from the overlay payload, with the reason. */
|
|
76
|
+
export interface SkippedRole {
|
|
77
|
+
role: string;
|
|
78
|
+
reason: RoleSkipReason;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The full-role overlay plan consumed by `applyRoleModelOverrides`. */
|
|
82
|
+
export interface RoleOverridePlan {
|
|
83
|
+
/** role → 'provider/id' written to `overrideModelRoles`. */
|
|
84
|
+
overrides: Record<string, string>;
|
|
85
|
+
/** Roles not written (config skip / capability gate / no picked model). */
|
|
86
|
+
skipped: SkippedRole[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Inputs for `buildRoleOverridePlan`. */
|
|
90
|
+
export interface BuildRoleOverridePlanInput {
|
|
91
|
+
/** All roles the host knows (built-in + custom), from `getKnownRoleIds`. */
|
|
92
|
+
knownRoleIds: readonly string[];
|
|
93
|
+
/** Parsed `model_role_tiers` config (role → tier override or 'skip'). */
|
|
94
|
+
ompRoleTiers: Record<string, TierName | 'skip'>;
|
|
95
|
+
/**
|
|
96
|
+
* Tier → picked `provider/id` for that tier. mid/high reuse the opsx
|
|
97
|
+
* selector picks (coder/reviewer); tiny/low/top are supplemental picks
|
|
98
|
+
* over the same candidate set. `undefined` means the tier has no pick
|
|
99
|
+
* (every role mapped to it is skipped with reason `no-pick`).
|
|
100
|
+
*/
|
|
101
|
+
tierPicks: Partial<Record<TierName, string | undefined>>;
|
|
102
|
+
/**
|
|
103
|
+
* Capability-gated picks keyed by role (e.g. vision picked from
|
|
104
|
+
* image-capable candidates only). `undefined` means no qualifying model.
|
|
105
|
+
*/
|
|
106
|
+
capabilityPicks?: Record<string, string | undefined>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Roles whose fallback tier was already warned about (one warning per role). */
|
|
110
|
+
const warnedCustomRoles = new Set<string>();
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the tier an OMP model role should follow: explicit
|
|
114
|
+
* `model_role_tiers` value (including the `skip` sentinel) → built-in
|
|
115
|
+
* default table → `high` fallback for custom roles (warned once per role;
|
|
116
|
+
* D7: custom roles are treated as primary roles unless configured).
|
|
117
|
+
*/
|
|
118
|
+
export function resolveOmpRoleTier(
|
|
119
|
+
role: string,
|
|
120
|
+
ompRoleTiers: Record<string, TierName | 'skip'>,
|
|
121
|
+
warn: (msg: string) => void = () => {},
|
|
122
|
+
): TierName | 'skip' {
|
|
123
|
+
const configured = ompRoleTiers[role];
|
|
124
|
+
if (configured !== undefined) return configured;
|
|
125
|
+
if (role in OMP_ROLE_TO_TIER) return OMP_ROLE_TO_TIER[role as ModelRole];
|
|
126
|
+
if (!warnedCustomRoles.has(role)) {
|
|
127
|
+
warnedCustomRoles.add(role);
|
|
128
|
+
warn(`[omp-opsx-addon] custom model role "${role}" has no tier mapping; using high (set model_role_tiers.${role} to tune or 'skip' to exclude)`);
|
|
129
|
+
}
|
|
130
|
+
return 'high';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the full-role overlay plan (D2/D3/D4/D7): for every known role,
|
|
135
|
+
* resolve its tier (or `skip`), apply capability gating, then assign the
|
|
136
|
+
* single picked model for the tier. Roles without a pick are recorded with
|
|
137
|
+
* a reason and omitted from the payload — after `clearOverride` they fall
|
|
138
|
+
* back to the user's config layer (or stay unconfigured).
|
|
139
|
+
*/
|
|
140
|
+
export function buildRoleOverridePlan(
|
|
141
|
+
input: BuildRoleOverridePlanInput,
|
|
142
|
+
warn: (msg: string) => void = () => {},
|
|
143
|
+
): RoleOverridePlan {
|
|
144
|
+
const overrides: Record<string, string> = {};
|
|
145
|
+
const skipped: SkippedRole[] = [];
|
|
146
|
+
for (const role of input.knownRoleIds) {
|
|
147
|
+
const tier = resolveOmpRoleTier(role, input.ompRoleTiers, warn);
|
|
148
|
+
if (tier === 'skip') {
|
|
149
|
+
skipped.push({ role, reason: 'skip' });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (role in ROLE_CAPABILITY) {
|
|
153
|
+
// Capability-gated roles (vision) use their own candidate set;
|
|
154
|
+
// a missing pick means no qualifying model OR no pick at all —
|
|
155
|
+
// both surface as `capability` (D3: never write an incapable model).
|
|
156
|
+
const picked = input.capabilityPicks?.[role];
|
|
157
|
+
if (!picked) {
|
|
158
|
+
skipped.push({ role, reason: 'capability' });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
overrides[role] = picked;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const picked = input.tierPicks[tier];
|
|
165
|
+
if (!picked) {
|
|
166
|
+
skipped.push({ role, reason: 'no-pick' });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
overrides[role] = picked;
|
|
170
|
+
}
|
|
171
|
+
return { overrides, skipped };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Model string for the agent frontmatter `model:` field.
|
|
176
|
+
*
|
|
177
|
+
* - auto role → the tier role alias (`@smol` for coder, `@default` for the
|
|
178
|
+
* high-tier roles) so resolution binds to the tier at the frontmatter layer;
|
|
179
|
+
* - omp-alias → `@<alias>` (e.g. `@slow`);
|
|
180
|
+
* - pinned → the concrete `provider/model` (or bare model id);
|
|
181
|
+
* - no value → undefined (template omits the `model:` line).
|
|
182
|
+
*/
|
|
183
|
+
export function frontmatterModel(role: string, value: { kind: string } & Record<string, unknown>): string | undefined {
|
|
184
|
+
if (value.kind === 'auto') return `@${ROLE_TO_OMP_ALIAS[role] ?? 'default'}`;
|
|
185
|
+
if (value.kind === 'omp-alias' && typeof value.alias === 'string') return `@${value.alias}`;
|
|
186
|
+
if (value.kind === 'provider-model' && typeof value.provider === 'string' && typeof value.modelId === 'string') {
|
|
187
|
+
return `${value.provider}/${value.modelId}`;
|
|
188
|
+
}
|
|
189
|
+
if (value.kind === 'model' && typeof value.modelId === 'string') return value.modelId;
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Explicit model string for the dispatch `task(..., model:"...")` call, or
|
|
195
|
+
* undefined when resolution must flow through the tier role.
|
|
196
|
+
*
|
|
197
|
+
* auto roles never pass a model — requestModel is the top-priority layer and
|
|
198
|
+
* a concrete model would shadow the frontmatter `@alias`; omp-alias roles
|
|
199
|
+
* (e.g. @slow, roleModels value without `/`) likewise resolve through
|
|
200
|
+
* frontmatter. Only a concrete provider/model pin stays explicit.
|
|
201
|
+
*/
|
|
202
|
+
export function dispatchModelHint(role: string, selection: SelectionContext): string | undefined {
|
|
203
|
+
if (selection.roleKind?.[role as keyof SelectionContext['roleKind']] === 'auto') return undefined;
|
|
204
|
+
const picked = selection.selectorResults[role as keyof SelectionContext['selectorResults']]?.picked;
|
|
205
|
+
if (picked) return `${picked.provider}/${picked.id}`;
|
|
206
|
+
const hint = selection.roleModels[role as keyof SelectionContext['roleModels']];
|
|
207
|
+
return hint && hint.includes('/') ? hint : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Runtime neutralization for persisted per-agent pins.
|
|
212
|
+
*
|
|
213
|
+
* Empty STRING values (not 'off'/'false'): the OMP model-resolution chain
|
|
214
|
+
* expands `''` to zero patterns (Rdt@12103716 returns on empty trim;
|
|
215
|
+
* VT@12104171 flattens to []), so the settingsOverride layer (LNs@12104228)
|
|
216
|
+
* falls through to the frontmatter `@alias` layer; while cross-layer merge
|
|
217
|
+
* (#X/#Se@11863299 — scalar leaf replace, only `undefined` skipped) lets the
|
|
218
|
+
* runtime `''` shadow the global config pin. 'off'/'false' are NOT empty:
|
|
219
|
+
* they come back as literal patterns and short-circuit the frontmatter layer.
|
|
220
|
+
*/
|
|
221
|
+
export function agentOverrideNeutralization(): Record<string, string> {
|
|
222
|
+
const neutral: Record<string, string> = {};
|
|
223
|
+
for (const role of OPSX_ROLES) neutral[AGENT_MODEL_KEYS[role]] = '';
|
|
224
|
+
return neutral;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Minimal Settings surface this module needs (host-process `pi.pi.settings`). */
|
|
228
|
+
export interface RoleSettings {
|
|
229
|
+
clearOverride(key: string): void;
|
|
230
|
+
overrideModelRoles(roles: Record<string, string>): void;
|
|
231
|
+
override(key: string, value: unknown): void;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Apply the selector result to OMP model roles (session-scoped).
|
|
236
|
+
*
|
|
237
|
+
* Sequence (design D1/D5/D7): clear the session modelRoles overlay first
|
|
238
|
+
* (overrideModelRoles only adds and skips falsy values, so stale roles from
|
|
239
|
+
* a previous constraint would otherwise linger) → write the full-role plan
|
|
240
|
+
* payload (roles skipped for config `skip`, capability gating, or no picked
|
|
241
|
+
* model are omitted and fall back to the config layer after the clear) →
|
|
242
|
+
* neutralize persisted per-agent pins with empty strings. Never writes
|
|
243
|
+
* disk; the runtime overlay is released with the session.
|
|
244
|
+
*/
|
|
245
|
+
export function applyRoleModelOverrides(
|
|
246
|
+
settings: RoleSettings | null | undefined,
|
|
247
|
+
sel: SelectionContext,
|
|
248
|
+
warn: (msg: string) => void = () => {},
|
|
249
|
+
): void {
|
|
250
|
+
if (!settings) {
|
|
251
|
+
warn('[omp-opsx-addon] pi.pi.settings unavailable; model role overrides skipped');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
settings.clearOverride('modelRoles');
|
|
255
|
+
settings.overrideModelRoles(sel.roleOverrides);
|
|
256
|
+
settings.override('task.agentModelOverrides', agentOverrideNeutralization());
|
|
257
|
+
const applied = Object.keys(sel.roleOverrides);
|
|
258
|
+
const skippedText = sel.skippedRoles.length > 0
|
|
259
|
+
? `; skipped: ${sel.skippedRoles.map((s) => `${s.role}(${s.reason})`).join(', ')}`
|
|
260
|
+
: '';
|
|
261
|
+
const skipWarns = sel.skippedRoles.filter((s) => s.reason !== 'skip');
|
|
262
|
+
warn(`[omp-opsx-addon] modelRoles applied (${applied.length} roles: ${applied.join(', ') || 'none'})${skippedText}; per-agent pins neutralized`);
|
|
263
|
+
for (const s of skipWarns) {
|
|
264
|
+
if (s.reason === 'capability') {
|
|
265
|
+
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)`);
|
|
266
|
+
} else {
|
|
267
|
+
warn(`[omp-opsx-addon] model role "${s.role}": no model picked for its tier; skipped (role keeps configured/default value)`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|