@genee/omp-opsx-addon 0.7.0 → 0.9.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 +441 -71
- package/index.ts +1030 -208
- package/lib/agent-defs.ts +55 -82
- package/lib/complexity-router.ts +336 -0
- package/lib/concurrency-control.ts +1246 -0
- package/lib/concurrency-multiproc.worker.ts +376 -0
- package/lib/concurrency-state.ts +397 -0
- package/lib/concurrency-tuner.ts +773 -0
- package/lib/concurrency-writer.ts +894 -0
- package/lib/continuity-guard.ts +117 -0
- package/lib/direct-fetchers.ts +18 -3
- package/lib/family-filter.ts +115 -2
- package/lib/model-roles.ts +41 -39
- package/lib/model-selector.ts +300 -36
- package/lib/model-speed.ts +189 -0
- package/lib/pick-model-render.ts +414 -0
- package/lib/pipe-core.ts +325 -98
- package/lib/pipe-push.ts +136 -5
- package/lib/provider-variants.ts +154 -0
- package/lib/selection-filters.ts +42 -1
- package/lib/stall-detector.ts +185 -0
- package/lib/system-prompt.ts +17 -9
- package/lib/tiers-data.ts +40 -5
- package/lib/tiers-updater.ts +51 -8
- package/lib/unified-config.ts +732 -27
- package/lib/usage-poller.ts +44 -1
- package/lib/usage-render.ts +57 -4
- package/lib/usage-widget.ts +18 -4
- package/package.json +1 -1
- package/skills/opsx-orchestration-protocol/SKILL.md +87 -0
package/lib/unified-config.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Unified opsx config schema.
|
|
3
3
|
*
|
|
4
4
|
* `opsx.yml` (per project at `<cwd>/.omp/opsx.yml`, layered on top of
|
|
5
|
-
* `~/.omp/agent/opsx.yml`) declares how each
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `~/.omp/agent/opsx.yml`) declares how each dispatch agent (coder /
|
|
6
|
+
* reviewer / planner / proposal-reviewer) is dispatched. Since `acpx` was
|
|
7
|
+
* removed, every agent is local — values describe the *model* the agent
|
|
8
|
+
* should run on, not an external agent to spawn.
|
|
8
9
|
*
|
|
9
|
-
* Accepted values for each
|
|
10
|
+
* Accepted values for each dispatch agent:
|
|
10
11
|
* - `'auto'` — let the model-selector pick based on tier + usage.
|
|
11
12
|
* - An OMP role alias: `default`, `smol`, `slow`, `vision`, `plan`,
|
|
12
13
|
* `designer`, `commit`, `tiny`, `task`, `advisor` — expanded by the
|
|
@@ -16,15 +17,28 @@
|
|
|
16
17
|
* `openai-codex/gpt-5-codex`.
|
|
17
18
|
*
|
|
18
19
|
* Top-level fields:
|
|
19
|
-
* - `
|
|
20
|
-
*
|
|
21
|
-
* - `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* - `agents`: `{ coder?, reviewer?, planner?, 'proposal-reviewer'? }` map
|
|
21
|
+
* of dispatch agent → value from the grammar above. Canonical form.
|
|
22
|
+
* - `agent_tiers`: dispatch agent → tier_name map overriding the default
|
|
23
|
+
* expected tier (DEFAULT_ROLE_TIER in model-tiers) for the four opsx
|
|
24
|
+
* agents. Canonical form.
|
|
25
|
+
* - Legacy keys `coder` / `reviewer` / `planner` / `proposal-reviewer`
|
|
26
|
+
* (top-level values) and `role_tiers`: same semantics as their
|
|
27
|
+
* replacements, still honored; each layer file using them gets one
|
|
28
|
+
* aggregated warning per config read. Scheduled for removal after one
|
|
29
|
+
* release cycle; within one file the new keys win.
|
|
24
30
|
* - `model_role_tiers`: `omp_role → tier_name | 'skip'` map controlling
|
|
25
31
|
* which selector tier each OMP model role (default/smol/slow/vision/plan/
|
|
26
32
|
* designer/commit/tiny/task/advisor + custom roles) follows after
|
|
27
33
|
* /pick-model; `skip` excludes the role from the session overlay.
|
|
34
|
+
* Disambiguation: those are the OMP host *model roles*, not the four
|
|
35
|
+
* opsx dispatch agents configured via `agents` / `agent_tiers`.
|
|
36
|
+
* - `model_allowlist`: list of `provider/model` glob patterns — restricts
|
|
37
|
+
* which models the auto-selector may pick from.
|
|
38
|
+
* - `tiers`: list of `{ pattern, tier }` overrides for tier scoring.
|
|
39
|
+
* - `concurrency_*`: adaptive per-provider concurrency tuner (see
|
|
40
|
+
* `CONCURRENCY_DEFAULTS` and the `provider-concurrency-control` spec).
|
|
41
|
+
* Flat snake_case keys with per-key fallback + warn — never throws.
|
|
28
42
|
*/
|
|
29
43
|
|
|
30
44
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -32,6 +46,11 @@ import { homedir } from 'os';
|
|
|
32
46
|
import { join } from 'path';
|
|
33
47
|
import { YAML } from 'bun';
|
|
34
48
|
import { TIER_NAMES, type TierName, type TierOverride } from './model-tiers.js';
|
|
49
|
+
import { canonicalizeProvider } from './usage-resolver.js';
|
|
50
|
+
import { DEFAULT_MIN_SAMPLES, SPEED_MODES, type SpeedAwareConfig, type SpeedMode } from './model-speed.js';
|
|
51
|
+
import { QUALITY_PREFERENCES, type QualityPreference, type RoutingConfig } from './complexity-router.js';
|
|
52
|
+
import { DEFAULT_THRESHOLD, DEFAULT_WINDOW, type StallEscalationConfig } from './stall-detector.js';
|
|
53
|
+
import { DEFAULT_WEIGHT, type ContinuityGuardConfig } from './continuity-guard.js';
|
|
35
54
|
|
|
36
55
|
/** OMP-documented role aliases. Plugins MAY pass these to `task(..., model: '@alias')`. */
|
|
37
56
|
export const OMP_ROLE_ALIASES = new Set<string>([
|
|
@@ -54,24 +73,58 @@ export const AUTO_TOKEN = 'auto';
|
|
|
54
73
|
export const OPSX_ROLES = ['coder', 'reviewer', 'planner', 'proposal-reviewer'] as const;
|
|
55
74
|
export type OpsxRole = (typeof OPSX_ROLES)[number];
|
|
56
75
|
|
|
76
|
+
/** Keys superseded by `agents` / `agent_tiers`; still honored, warned on read. */
|
|
77
|
+
const LEGACY_AGENT_KEYS: readonly (OpsxRole | 'role_tiers')[] = [...OPSX_ROLES, 'role_tiers'];
|
|
78
|
+
|
|
57
79
|
/** Raw YAML shape — every key is optional. */
|
|
58
80
|
export interface OpsxYamlShape {
|
|
81
|
+
/** @deprecated Legacy of `agents.coder` — still honored; warned per layer file; removal after one release cycle. */
|
|
59
82
|
coder?: string;
|
|
83
|
+
/** @deprecated Legacy of `agents.reviewer` — still honored; warned per layer file; removal after one release cycle. */
|
|
60
84
|
reviewer?: string;
|
|
85
|
+
/** @deprecated Legacy of `agents.planner` — still honored; warned per layer file; removal after one release cycle. */
|
|
61
86
|
planner?: string;
|
|
87
|
+
/** @deprecated Legacy of `agents['proposal-reviewer']` — still honored; warned per layer file; removal after one release cycle. */
|
|
62
88
|
'proposal-reviewer'?: string;
|
|
89
|
+
/** Dispatch agent → value; canonical replacement for the legacy top-level role keys. */
|
|
90
|
+
agents?: Partial<Record<OpsxRole, string>>;
|
|
63
91
|
primary?: string;
|
|
64
92
|
model_allowlist?: string[];
|
|
65
93
|
tiers?: TierOverride[];
|
|
94
|
+
/** @deprecated Legacy name of `agent_tiers` (same schema) — still honored; warned per layer file; removal after one release cycle. */
|
|
66
95
|
role_tiers?: Partial<Record<OpsxRole, TierName>>;
|
|
96
|
+
/** Dispatch agent → tier override for the four opsx agents. */
|
|
97
|
+
agent_tiers?: Partial<Record<OpsxRole, TierName>>;
|
|
67
98
|
/** OMP model role → tier override; `skip` excludes the role from the overlay. Keys are arbitrary strings (custom roles allowed). */
|
|
68
99
|
model_role_tiers?: Record<string, string>;
|
|
69
100
|
plan_providers?: string[];
|
|
101
|
+
/** Providers removed from the effective plan set: (defaults ∪ plan_providers) − plan_providers_remove. */
|
|
102
|
+
plan_providers_remove?: string[];
|
|
103
|
+
/** false collapses the PAYG-last coverage partition (pay-as-you-go candidates compete normally). Default true. */
|
|
104
|
+
payg_last_resort?: boolean;
|
|
105
|
+
/** china 家族缺省排除 + 同账号区域变体偏好方向(true = 国内语境:排除 gpt/claude/openai/anthropic 家族、区域变体保留国内入口)。缺省 false。 */
|
|
106
|
+
china?: boolean;
|
|
70
107
|
probe_enabled?: boolean;
|
|
71
108
|
probe_timeout_ms?: number;
|
|
72
109
|
probe_http_enabled?: boolean;
|
|
73
110
|
probe_ttl_ms?: number;
|
|
74
111
|
excluded_providers?: string[];
|
|
112
|
+
/** provider → 服务端路由哨兵模型 id(如 `ark-coding-plan: ark-code-latest`)。键经 canonicalizeProvider 规范化。 */
|
|
113
|
+
provider_auto?: Record<string, string>;
|
|
114
|
+
/** provider → 哨兵声明档位(tiny|low|mid|high|top);非法值 warn 丢弃,未声明按 mid。 */
|
|
115
|
+
provider_auto_tiers?: Record<string, string>;
|
|
116
|
+
/** provider → 模式数组:列出的 provider 收窄到命中模型,未列出不受限。 */
|
|
117
|
+
provider_models?: Record<string, string[]>;
|
|
118
|
+
/** 速度感知选择:enabled(缺省 true)、mode(tie-break|aggressive,缺省 tie-break)、min_samples(缺省 20)。 */
|
|
119
|
+
speed_aware?: { enabled?: boolean; mode?: string; min_samples?: number };
|
|
120
|
+
/** 难度路由(W4):prompt 分类 → 写入集 tier 目标偏移。缺省 false。 */
|
|
121
|
+
difficulty_routing?: { enabled?: boolean };
|
|
122
|
+
/** stall 升档(W4):同签名滑窗重复 → default 目标 +1。缺省 false / 6 / 3。 */
|
|
123
|
+
stall_escalation?: { enabled?: boolean; window?: number; threshold?: number };
|
|
124
|
+
/** 连续性守门(W4):在选模型粘性。缺省 false / weight 0.6。 */
|
|
125
|
+
continuity_guard?: { enabled?: boolean; weight?: number };
|
|
126
|
+
/** 质量带旋钮(W4):balanced | cost | quality。缺省 balanced(独立于路由开关)。 */
|
|
127
|
+
quality_preference?: string;
|
|
75
128
|
status_probe_enabled?: boolean;
|
|
76
129
|
redis_enabled?: boolean;
|
|
77
130
|
redis_host?: string;
|
|
@@ -82,6 +135,24 @@ export interface OpsxYamlShape {
|
|
|
82
135
|
redis_probe_interval_ms?: number;
|
|
83
136
|
redis_unhealthy_retry_ms?: number;
|
|
84
137
|
redis_ttl_ms?: number;
|
|
138
|
+
/** 会话启动自动应用一次选择(缺省 true);false 时 session_start 不应用、不写覆盖,行为与本键引入前一致。 */
|
|
139
|
+
autoselect_on_start?: boolean;
|
|
140
|
+
concurrency_enabled?: boolean;
|
|
141
|
+
concurrency_floor?: number;
|
|
142
|
+
concurrency_ceiling?: number;
|
|
143
|
+
concurrency_start?: number;
|
|
144
|
+
concurrency_up_ms?: number;
|
|
145
|
+
concurrency_down_ms?: number;
|
|
146
|
+
concurrency_latency_k?: number;
|
|
147
|
+
concurrency_min_samples?: number;
|
|
148
|
+
concurrency_queue_wait_high_ms?: number;
|
|
149
|
+
concurrency_write_min_interval_ms?: number;
|
|
150
|
+
concurrency_lease_ttl_ms?: number;
|
|
151
|
+
concurrency_decay_after_ms?: number;
|
|
152
|
+
concurrency_eval_interval_ms?: number;
|
|
153
|
+
concurrency_queue_wait_window_ms?: number;
|
|
154
|
+
concurrency_providers?: string[];
|
|
155
|
+
concurrency_ceiling_by_provider?: Record<string, number>;
|
|
85
156
|
}
|
|
86
157
|
|
|
87
158
|
/** A role value as parsed. */
|
|
@@ -100,6 +171,28 @@ export interface ResolvedOpsxConfig {
|
|
|
100
171
|
ompRoleTiers: Record<string, TierName | 'skip'>;
|
|
101
172
|
/** Provider ids that have monthly plan subscriptions. */
|
|
102
173
|
planProviders: string[];
|
|
174
|
+
/** Canonical provider ids removed from the effective plan set. */
|
|
175
|
+
planProvidersRemove: string[];
|
|
176
|
+
/** PAYG candidates sort last (coverage partition). false collapses the partition. Default true. */
|
|
177
|
+
paygLastResort: boolean;
|
|
178
|
+
/** Standing china family exclusion + regional-variant preference direction. Default false (= status quo). */
|
|
179
|
+
china: boolean;
|
|
180
|
+
/** provider → 服务端路由哨兵模型 id(键已 canonicalizeProvider 规范化)。空对象 = 无哨兵。 */
|
|
181
|
+
providerAuto: Record<string, string>;
|
|
182
|
+
/** provider → 哨兵声明档位(值已过 TIER_NAMES 校验)。空对象 = 全部缺省 mid。 */
|
|
183
|
+
providerAutoTiers: Record<string, TierName>;
|
|
184
|
+
/** provider → 模式数组(键已规范化):仅列出的 provider 被收窄,未列出不受限。 */
|
|
185
|
+
providerModels: Record<string, string[]>;
|
|
186
|
+
/** 速度感知选择三键(解析层已校验并补齐缺省;enabled=false 时装配层零读取)。 */
|
|
187
|
+
speedAware: SpeedAwareConfig;
|
|
188
|
+
/** W4 难度路由(缺省 false:结构改造 opt-in,W1-W3 收益不依赖它)。 */
|
|
189
|
+
difficultyRouting: RoutingConfig;
|
|
190
|
+
/** W4 stall 升档(缺省 false / window 6 / threshold 3)。 */
|
|
191
|
+
stallEscalation: StallEscalationConfig;
|
|
192
|
+
/** W4 连续性守门(缺省 false / weight 0.6)。 */
|
|
193
|
+
continuityGuard: ContinuityGuardConfig;
|
|
194
|
+
/** W4 质量带旋钮(缺省 balanced = 既有比较器分支逐字节)。 */
|
|
195
|
+
qualityPreference: QualityPreference;
|
|
103
196
|
warnings: string[];
|
|
104
197
|
/** 是否开启可达性探测,默认 true */
|
|
105
198
|
probe_enabled: boolean;
|
|
@@ -131,6 +224,49 @@ export interface ResolvedOpsxConfig {
|
|
|
131
224
|
redis_unhealthy_retry_ms: number;
|
|
132
225
|
/** Redis key TTL ms(默认 180000)。 */
|
|
133
226
|
redis_ttl_ms: number;
|
|
227
|
+
/** 会话启动自动应用一次选择,默认 true;false 时 session_start handler 直接返回(不 setModel、不写覆盖)。 */
|
|
228
|
+
autoselectOnStart: boolean;
|
|
229
|
+
/** 是否开启自适应并发控制,默认 true;false 时装配层不创建控制器(零写入)。 */
|
|
230
|
+
concurrency_enabled: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* 全局并发上限下界,默认 4(须为 ≥ 1 的整数)。每个 provider 的**有效下限**
|
|
233
|
+
* 为 `min(concurrency_floor, initial)`(见 concurrency-tuner 的
|
|
234
|
+
* `effectiveFloor` / `createProviderState`):该值设定区间,MUST NOT 抬高
|
|
235
|
+
* provider 自身的静态现值(静态 3 的 provider 下限与起点都保持 3)。
|
|
236
|
+
*/
|
|
237
|
+
concurrency_floor: number;
|
|
238
|
+
/**
|
|
239
|
+
* 全局并发上限上界,默认 16。低于**有效 floor** 时:floor 未有效显式配置
|
|
240
|
+
* (缺省或非法回落)则把 floor 压到该 ceiling(区间退化为单点),否则回落
|
|
241
|
+
* 默认(ceiling → 16,必要时 floor 也回落默认);见 `parseOpsxConfig`。
|
|
242
|
+
*/
|
|
243
|
+
concurrency_ceiling: number;
|
|
244
|
+
/** 无现有静态值时的保守启动值,默认 4(区间 clamp 由控制器负责)。 */
|
|
245
|
+
concurrency_start: number;
|
|
246
|
+
/** 加性上调所需干净窗口时长 ms,默认 30000。 */
|
|
247
|
+
concurrency_up_ms: number;
|
|
248
|
+
/** 乘性下调后的冷却时长 ms,默认 60000。 */
|
|
249
|
+
concurrency_down_ms: number;
|
|
250
|
+
/** 延迟膨胀判定比值,默认 2(> 1 的任意正数,允许小数)。 */
|
|
251
|
+
concurrency_latency_k: number;
|
|
252
|
+
/** 延迟判定所需最小样本数,默认 5。 */
|
|
253
|
+
concurrency_min_samples: number;
|
|
254
|
+
/** 排队等待 p50 高企阈值 ms,默认 2000(超阈值禁止上调)。 */
|
|
255
|
+
concurrency_queue_wait_high_ms: number;
|
|
256
|
+
/** 同一 provider 两次成功写入的最小间隔 ms,默认 20000。 */
|
|
257
|
+
concurrency_write_min_interval_ms: number;
|
|
258
|
+
/** 租约 TTL ms,默认 60000。 */
|
|
259
|
+
concurrency_lease_ttl_ms: number;
|
|
260
|
+
/** 无观测衰减周期 ms,默认 86400000(24h)。 */
|
|
261
|
+
concurrency_decay_after_ms: number;
|
|
262
|
+
/** 评估节拍 ms,默认 30000。 */
|
|
263
|
+
concurrency_eval_interval_ms: number;
|
|
264
|
+
/** 排队等待护栏统计窗口 ms,默认 60000(独立于评估节拍)。 */
|
|
265
|
+
concurrency_queue_wait_window_ms: number;
|
|
266
|
+
/** 显式希望由控制器试探上限的 provider(规范化 id,去重、声明顺序)。 */
|
|
267
|
+
concurrency_providers: string[];
|
|
268
|
+
/** per-provider ceiling 覆盖(键已规范化,值为 ≥ floor 的正整数)。 */
|
|
269
|
+
concurrency_ceiling_by_provider: Record<string, number>;
|
|
134
270
|
}
|
|
135
271
|
|
|
136
272
|
/** Default values applied when no config is provided. */
|
|
@@ -202,9 +338,14 @@ export function parseTierOverrides(
|
|
|
202
338
|
return out;
|
|
203
339
|
}
|
|
204
340
|
|
|
205
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Validate tier entries for the four opsx agents. `keyLabel` names the config
|
|
343
|
+
* key being parsed (`agent_tiers`, or legacy `role_tiers`) so warnings point
|
|
344
|
+
* at the key the user actually wrote.
|
|
345
|
+
*/
|
|
206
346
|
export function parseRoleTiers(
|
|
207
347
|
raw: unknown,
|
|
348
|
+
keyLabel: 'agent_tiers' | 'role_tiers',
|
|
208
349
|
warn: (msg: string) => void,
|
|
209
350
|
): Partial<Record<OpsxRole, TierName>> {
|
|
210
351
|
if (!raw || typeof raw !== 'object') return {};
|
|
@@ -213,11 +354,11 @@ export function parseRoleTiers(
|
|
|
213
354
|
const v = (raw as Record<string, unknown>)[role];
|
|
214
355
|
if (v === undefined || v === null) continue;
|
|
215
356
|
if (typeof v !== 'string') {
|
|
216
|
-
warn(`[omp-opsx-addon]
|
|
357
|
+
warn(`[omp-opsx-addon] ${keyLabel}.${role}: expected tier name, got ${typeof v}`);
|
|
217
358
|
continue;
|
|
218
359
|
}
|
|
219
360
|
if (!TIER_NAMES.includes(v as TierName)) {
|
|
220
|
-
warn(`[omp-opsx-addon]
|
|
361
|
+
warn(`[omp-opsx-addon] ${keyLabel}.${role}: unknown tier "${v}" (use one of ${TIER_NAMES.join('/')})`);
|
|
221
362
|
continue;
|
|
222
363
|
}
|
|
223
364
|
out[role] = v as TierName;
|
|
@@ -306,6 +447,375 @@ export function parseStringArray(raw: unknown, key: string, warn: (msg: string)
|
|
|
306
447
|
return out;
|
|
307
448
|
}
|
|
308
449
|
|
|
450
|
+
/**
|
|
451
|
+
* `concurrency_*` 默认值 —— 逐键对应 spec `provider-concurrency-control`
|
|
452
|
+
* 的「配置面与降级」Requirement(不要按 tasks 里的数字出现顺序推断)。
|
|
453
|
+
*/
|
|
454
|
+
export const CONCURRENCY_DEFAULTS = {
|
|
455
|
+
enabled: true,
|
|
456
|
+
floor: 4,
|
|
457
|
+
ceiling: 16,
|
|
458
|
+
start: 4,
|
|
459
|
+
up_ms: 30_000,
|
|
460
|
+
down_ms: 60_000,
|
|
461
|
+
latency_k: 2,
|
|
462
|
+
min_samples: 5,
|
|
463
|
+
queue_wait_high_ms: 2_000,
|
|
464
|
+
write_min_interval_ms: 20_000,
|
|
465
|
+
lease_ttl_ms: 60_000,
|
|
466
|
+
decay_after_ms: 86_400_000,
|
|
467
|
+
eval_interval_ms: 30_000,
|
|
468
|
+
queue_wait_window_ms: 60_000,
|
|
469
|
+
} as const;
|
|
470
|
+
|
|
471
|
+
/** 解析正数(允许小数,用于 `concurrency_latency_k` 这类比值):非法则返回 defaultValue 并 warn。 */
|
|
472
|
+
export function parsePositiveNumber(raw: unknown, key: string, defaultValue: number, warn: (msg: string) => void): number {
|
|
473
|
+
if (raw === undefined || raw === null) return defaultValue;
|
|
474
|
+
if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return raw;
|
|
475
|
+
warn(`[omp-opsx-addon] ${key}: expected positive number, got ${JSON.stringify(raw)}`);
|
|
476
|
+
return defaultValue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** 规范化 provider id 列表:canonicalize + 去重,保持声明顺序;非字符串/空串跳过并 warn。 */
|
|
480
|
+
function canonicalizeProviderList(raw: readonly unknown[], key: string, warn: (msg: string) => void): string[] {
|
|
481
|
+
const out: string[] = [];
|
|
482
|
+
const seen = new Set<string>();
|
|
483
|
+
for (const entry of raw) {
|
|
484
|
+
if (typeof entry !== 'string') {
|
|
485
|
+
warn(`[omp-opsx-addon] ${key}: skipping non-string entry ${JSON.stringify(entry)}`);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const canonical = canonicalizeProvider(entry.trim());
|
|
489
|
+
if (!canonical) {
|
|
490
|
+
warn(`[omp-opsx-addon] ${key}: skipping empty provider id`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (!seen.has(canonical)) {
|
|
494
|
+
seen.add(canonical);
|
|
495
|
+
out.push(canonical);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 解析 `concurrency_providers`:非数组回落 [] 并 warn;条目规范化、去重。 */
|
|
502
|
+
export function parseConcurrencyProviders(raw: unknown, warn: (msg: string) => void): string[] {
|
|
503
|
+
if (raw === undefined || raw === null) return [];
|
|
504
|
+
if (!Array.isArray(raw)) {
|
|
505
|
+
warn(`[omp-opsx-addon] concurrency_providers: expected array of provider ids, got ${JSON.stringify(raw)}`);
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
return canonicalizeProviderList(raw, 'concurrency_providers', warn);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** 解析 `plan_providers` / `plan_providers_remove`:非数组回落 [] 并 warn;条目规范化、去重。 */
|
|
512
|
+
function parsePlanProviderList(raw: unknown, key: string, warn: (msg: string) => void): string[] {
|
|
513
|
+
if (raw === undefined || raw === null) return [];
|
|
514
|
+
if (!Array.isArray(raw)) {
|
|
515
|
+
warn(`[omp-opsx-addon] ${key}: expected array of provider ids, got ${JSON.stringify(raw)}`);
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
return canonicalizeProviderList(raw, key, warn);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 解析 `concurrency_ceiling_by_provider`:键规范化(大小写 / `arkcodingplan` 别名同源),
|
|
523
|
+
* 值须为 ≥ 1 的正整数;非法条目跳过并 warn(回落到全局 `concurrency_ceiling`),
|
|
524
|
+
* 非对象回落 {} 并 warn。
|
|
525
|
+
*
|
|
526
|
+
* 低于全局 `floor` 的覆盖**保留**并 warn:它是用户对单个 provider 的显式收窄,
|
|
527
|
+
* 丢弃它会让该 provider 的 ceiling 回落到全局值(等于抬高用户设定)。控制器的
|
|
528
|
+
* `createProviderState` 会把该 provider 的有效下限压到 `min(floor, 静态现值,
|
|
529
|
+
* 该 ceiling)`,因此 `floor > ceiling` 的覆盖仍自洽(该 provider 固定在它的
|
|
530
|
+
* ceiling 上)。
|
|
531
|
+
*/
|
|
532
|
+
export function parseCeilingByProvider(
|
|
533
|
+
raw: unknown,
|
|
534
|
+
floor: number,
|
|
535
|
+
warn: (msg: string) => void,
|
|
536
|
+
): Record<string, number> {
|
|
537
|
+
if (raw === undefined || raw === null) return {};
|
|
538
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
539
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider: expected object mapping provider → ceiling, got ${JSON.stringify(raw)}`);
|
|
540
|
+
return {};
|
|
541
|
+
}
|
|
542
|
+
const out: Record<string, number> = {};
|
|
543
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
544
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
545
|
+
if (!canonical) {
|
|
546
|
+
warn('[omp-opsx-addon] concurrency_ceiling_by_provider: skipping empty provider id');
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
|
|
550
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: expected positive integer, got ${JSON.stringify(value)}`);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (value < floor) {
|
|
554
|
+
warn(
|
|
555
|
+
`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: ${value} < concurrency_floor ${floor}; keeping the override(有效下限 = min(${value}, provider 静态现值))`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (out[canonical] !== undefined && out[canonical] !== value) {
|
|
559
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: duplicate canonical id "${canonical}" (${out[canonical]} → ${value}); later key wins`);
|
|
560
|
+
}
|
|
561
|
+
out[canonical] = value;
|
|
562
|
+
}
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 解析 `provider_auto`:键规范化(大小写 / 别名同源),键值均须字符串;非法条目跳过并 warn。 */
|
|
567
|
+
export function parseProviderAuto(raw: unknown, warn: (msg: string) => void): Record<string, string> {
|
|
568
|
+
if (raw === undefined || raw === null) return {};
|
|
569
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
570
|
+
warn(`[omp-opsx-addon] provider_auto: expected object mapping provider → auto model id, got ${JSON.stringify(raw)}`);
|
|
571
|
+
return {};
|
|
572
|
+
}
|
|
573
|
+
const out: Record<string, string> = {};
|
|
574
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
575
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
576
|
+
if (!canonical) {
|
|
577
|
+
warn('[omp-opsx-addon] provider_auto: skipping empty provider id');
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
581
|
+
warn(`[omp-opsx-addon] provider_auto.${id}: expected model id string, got ${JSON.stringify(value)}`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
out[canonical] = value.trim();
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** 解析 `provider_auto_tiers`:键规范化,值经 TIER_NAMES 校验;非法值 warn 并丢弃(消费侧缺省 mid)。 */
|
|
590
|
+
export function parseProviderAutoTiers(raw: unknown, warn: (msg: string) => void): Record<string, TierName> {
|
|
591
|
+
if (raw === undefined || raw === null) return {};
|
|
592
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
593
|
+
warn(`[omp-opsx-addon] provider_auto_tiers: expected object mapping provider → tier name, got ${JSON.stringify(raw)}`);
|
|
594
|
+
return {};
|
|
595
|
+
}
|
|
596
|
+
const out: Record<string, TierName> = {};
|
|
597
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
598
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
599
|
+
if (!canonical) {
|
|
600
|
+
warn('[omp-opsx-addon] provider_auto_tiers: skipping empty provider id');
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (typeof value !== 'string' || !(TIER_NAMES as readonly string[]).includes(value)) {
|
|
604
|
+
warn(`[omp-opsx-addon] provider_auto_tiers.${id}: expected one of ${TIER_NAMES.join('|')}, got ${JSON.stringify(value)}`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
out[canonical] = value as TierName;
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** 解析 `provider_models`:键规范化,值须为字符串数组(模式);非数组整键 warn 丢弃,非字符串条目跳过并 warn。 */
|
|
613
|
+
export function parseProviderModels(raw: unknown, warn: (msg: string) => void): Record<string, string[]> {
|
|
614
|
+
if (raw === undefined || raw === null) return {};
|
|
615
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
616
|
+
warn(`[omp-opsx-addon] provider_models: expected object mapping provider → pattern array, got ${JSON.stringify(raw)}`);
|
|
617
|
+
return {};
|
|
618
|
+
}
|
|
619
|
+
const out: Record<string, string[]> = {};
|
|
620
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
621
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
622
|
+
if (!canonical) {
|
|
623
|
+
warn('[omp-opsx-addon] provider_models: skipping empty provider id');
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (!Array.isArray(value)) {
|
|
627
|
+
warn(`[omp-opsx-addon] provider_models.${id}: expected array of patterns, got ${JSON.stringify(value)}`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const patterns: string[] = [];
|
|
631
|
+
for (const entry of value) {
|
|
632
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
633
|
+
warn(`[omp-opsx-addon] provider_models.${id}: skipping non-string/empty pattern ${JSON.stringify(entry)}`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
patterns.push(entry.trim());
|
|
637
|
+
}
|
|
638
|
+
if (patterns.length > 0) out[canonical] = patterns;
|
|
639
|
+
}
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** speed_aware 三键缺省(enabled 缺省开启:T2 类别硬分区 + 缺数据零漂移已消解开启风险)。 */
|
|
644
|
+
export function defaultSpeedAware(): SpeedAwareConfig {
|
|
645
|
+
return { enabled: true, mode: 'tie-break', minSamples: DEFAULT_MIN_SAMPLES };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** 解析 `speed_aware`:非对象整键 warn 回落缺省;mode 非法枚举 / min_samples 非正数 / enabled 非布尔各键 warn 回落各自缺省(parseProviderAutoTiers warn 矩阵同款)。 */
|
|
649
|
+
export function parseSpeedAware(raw: unknown, warn: (msg: string) => void): SpeedAwareConfig {
|
|
650
|
+
if (raw === undefined || raw === null) return defaultSpeedAware();
|
|
651
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
652
|
+
warn(`[omp-opsx-addon] speed_aware: expected object mapping { enabled, mode, min_samples }, got ${JSON.stringify(raw)}`);
|
|
653
|
+
return defaultSpeedAware();
|
|
654
|
+
}
|
|
655
|
+
const obj = raw as { enabled?: unknown; mode?: unknown; min_samples?: unknown };
|
|
656
|
+
let enabled = true;
|
|
657
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
658
|
+
if (typeof obj.enabled === 'boolean') {
|
|
659
|
+
enabled = obj.enabled;
|
|
660
|
+
} else {
|
|
661
|
+
warn(`[omp-opsx-addon] speed_aware.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
let mode: SpeedMode = 'tie-break';
|
|
665
|
+
if (obj.mode !== undefined && obj.mode !== null) {
|
|
666
|
+
if (typeof obj.mode === 'string' && (SPEED_MODES as readonly string[]).includes(obj.mode)) {
|
|
667
|
+
mode = obj.mode as SpeedMode;
|
|
668
|
+
} else {
|
|
669
|
+
warn(`[omp-opsx-addon] speed_aware.mode: expected one of ${SPEED_MODES.join('|')}, got ${JSON.stringify(obj.mode)}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
let minSamples = DEFAULT_MIN_SAMPLES;
|
|
673
|
+
if (obj.min_samples !== undefined && obj.min_samples !== null) {
|
|
674
|
+
if (typeof obj.min_samples === 'number' && Number.isFinite(obj.min_samples) && obj.min_samples > 0) {
|
|
675
|
+
minSamples = obj.min_samples;
|
|
676
|
+
} else {
|
|
677
|
+
warn(`[omp-opsx-addon] speed_aware.min_samples: expected positive number, got ${JSON.stringify(obj.min_samples)}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { enabled, mode, minSamples };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** W4 难度路由缺省(enabled 缺省 false:结构改造 opt-in,与 speed_aware 缺省 true 的差异见 design D7)。 */
|
|
684
|
+
export function defaultDifficultyRouting(): RoutingConfig {
|
|
685
|
+
return { enabled: false };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** 解析 `difficulty_routing`:非对象整键 warn 回落缺省;enabled 非布尔逐键 warn 回落(parseSpeedAware 矩阵同款)。 */
|
|
689
|
+
export function parseDifficultyRouting(raw: unknown, warn: (msg: string) => void): RoutingConfig {
|
|
690
|
+
if (raw === undefined || raw === null) return defaultDifficultyRouting();
|
|
691
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
692
|
+
warn(`[omp-opsx-addon] difficulty_routing: expected object mapping { enabled }, got ${JSON.stringify(raw)}`);
|
|
693
|
+
return defaultDifficultyRouting();
|
|
694
|
+
}
|
|
695
|
+
const obj = raw as { enabled?: unknown };
|
|
696
|
+
let enabled = false;
|
|
697
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
698
|
+
if (typeof obj.enabled === 'boolean') {
|
|
699
|
+
enabled = obj.enabled;
|
|
700
|
+
} else {
|
|
701
|
+
warn(`[omp-opsx-addon] difficulty_routing.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { enabled };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** W4 stall 升档缺省(window=6 / threshold=3,LiteLLM 同参)。 */
|
|
708
|
+
export function defaultStallEscalation(): StallEscalationConfig {
|
|
709
|
+
return { enabled: false, window: DEFAULT_WINDOW, threshold: DEFAULT_THRESHOLD };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** 解析 `stall_escalation`:非对象整键回落;enabled 非布尔、window/threshold 非正整数各键 warn 回落各自缺省。 */
|
|
713
|
+
export function parseStallEscalation(raw: unknown, warn: (msg: string) => void): StallEscalationConfig {
|
|
714
|
+
if (raw === undefined || raw === null) return defaultStallEscalation();
|
|
715
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
716
|
+
warn(`[omp-opsx-addon] stall_escalation: expected object mapping { enabled, window, threshold }, got ${JSON.stringify(raw)}`);
|
|
717
|
+
return defaultStallEscalation();
|
|
718
|
+
}
|
|
719
|
+
const obj = raw as { enabled?: unknown; window?: unknown; threshold?: unknown };
|
|
720
|
+
let enabled = false;
|
|
721
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
722
|
+
if (typeof obj.enabled === 'boolean') {
|
|
723
|
+
enabled = obj.enabled;
|
|
724
|
+
} else {
|
|
725
|
+
warn(`[omp-opsx-addon] stall_escalation.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
let window = DEFAULT_WINDOW;
|
|
729
|
+
if (obj.window !== undefined && obj.window !== null) {
|
|
730
|
+
if (typeof obj.window === 'number' && Number.isInteger(obj.window) && obj.window > 0) {
|
|
731
|
+
window = obj.window;
|
|
732
|
+
} else {
|
|
733
|
+
warn(`[omp-opsx-addon] stall_escalation.window: expected positive integer, got ${JSON.stringify(obj.window)}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
let threshold = DEFAULT_THRESHOLD;
|
|
737
|
+
if (obj.threshold !== undefined && obj.threshold !== null) {
|
|
738
|
+
if (typeof obj.threshold === 'number' && Number.isInteger(obj.threshold) && obj.threshold > 0) {
|
|
739
|
+
threshold = obj.threshold;
|
|
740
|
+
} else {
|
|
741
|
+
warn(`[omp-opsx-addon] stall_escalation.threshold: expected positive integer, got ${JSON.stringify(obj.threshold)}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return { enabled, window, threshold };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** W4 连续性守门缺省(weight 缺省 0.6:越高越保守/粘性越强)。 */
|
|
748
|
+
export function defaultContinuityGuard(): ContinuityGuardConfig {
|
|
749
|
+
return { enabled: false, weight: DEFAULT_WEIGHT };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** 解析 `continuity_guard`:非对象整键回落;enabled 非布尔、weight 越出 (0,1] 开闭区间各键 warn 回落各自缺省。 */
|
|
753
|
+
export function parseContinuityGuard(raw: unknown, warn: (msg: string) => void): ContinuityGuardConfig {
|
|
754
|
+
if (raw === undefined || raw === null) return defaultContinuityGuard();
|
|
755
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
756
|
+
warn(`[omp-opsx-addon] continuity_guard: expected object mapping { enabled, weight }, got ${JSON.stringify(raw)}`);
|
|
757
|
+
return defaultContinuityGuard();
|
|
758
|
+
}
|
|
759
|
+
const obj = raw as { enabled?: unknown; weight?: unknown };
|
|
760
|
+
let enabled = false;
|
|
761
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
762
|
+
if (typeof obj.enabled === 'boolean') {
|
|
763
|
+
enabled = obj.enabled;
|
|
764
|
+
} else {
|
|
765
|
+
warn(`[omp-opsx-addon] continuity_guard.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
let weight = DEFAULT_WEIGHT;
|
|
769
|
+
if (obj.weight !== undefined && obj.weight !== null) {
|
|
770
|
+
if (typeof obj.weight === 'number' && Number.isFinite(obj.weight) && obj.weight > 0 && obj.weight <= 1) {
|
|
771
|
+
weight = obj.weight;
|
|
772
|
+
} else {
|
|
773
|
+
warn(`[omp-opsx-addon] continuity_guard.weight: expected number in (0, 1], got ${JSON.stringify(obj.weight)}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return { enabled, weight };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** 解析 `quality_preference`:非法枚举 warn 回落 balanced(独立于三开关的顶层旋钮)。 */
|
|
780
|
+
export function parseQualityPreference(raw: unknown, warn: (msg: string) => void): QualityPreference {
|
|
781
|
+
if (raw === undefined || raw === null) return 'balanced';
|
|
782
|
+
if (typeof raw === 'string' && (QUALITY_PREFERENCES as readonly string[]).includes(raw)) {
|
|
783
|
+
return raw as QualityPreference;
|
|
784
|
+
}
|
|
785
|
+
warn(`[omp-opsx-addon] quality_preference: expected one of ${QUALITY_PREFERENCES.join('|')}, got ${JSON.stringify(raw)}`);
|
|
786
|
+
return 'balanced';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* 受管 provider 集合(spec: 受管集合 = `concurrency_providers` ∪
|
|
791
|
+
* `providers.maxInFlightRequests` 的键)。纯函数:`providers.maxInFlightRequests`
|
|
792
|
+
* 的读取由装配层负责(config root 的 `config.yml`)。
|
|
793
|
+
*
|
|
794
|
+
* 两侧 id 均经 `canonicalizeProvider` 规范化(大小写与 `ArkCodingPlan` →
|
|
795
|
+
* `ark-coding-plan` 别名收敛为同一列),去重并返回稳定顺序 —— 先
|
|
796
|
+
* `concurrency_providers` 的声明序,再 limits 侧新增键序。
|
|
797
|
+
*/
|
|
798
|
+
export function resolveManagedProviders(
|
|
799
|
+
concurrencyProviders: readonly string[] | undefined,
|
|
800
|
+
providerLimitIds: readonly string[] | Record<string, unknown> | undefined,
|
|
801
|
+
): string[] {
|
|
802
|
+
const limitIds = Array.isArray(providerLimitIds)
|
|
803
|
+
? providerLimitIds
|
|
804
|
+
: providerLimitIds
|
|
805
|
+
? Object.keys(providerLimitIds)
|
|
806
|
+
: [];
|
|
807
|
+
const out: string[] = [];
|
|
808
|
+
const seen = new Set<string>();
|
|
809
|
+
for (const id of [...(concurrencyProviders ?? []), ...limitIds]) {
|
|
810
|
+
if (typeof id !== 'string') continue;
|
|
811
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
812
|
+
if (!canonical || seen.has(canonical)) continue;
|
|
813
|
+
seen.add(canonical);
|
|
814
|
+
out.push(canonical);
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
|
|
309
819
|
/** Parse the full raw opsx config. */
|
|
310
820
|
export function parseOpsxConfig(
|
|
311
821
|
raw: unknown,
|
|
@@ -318,15 +828,21 @@ export function parseOpsxConfig(
|
|
|
318
828
|
warn(m);
|
|
319
829
|
};
|
|
320
830
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
831
|
+
// `agents` mapping (canonical form): the key set is exactly the four
|
|
832
|
+
// managed agent names, so unknown subkeys are reported and dropped
|
|
833
|
+
// instead of vanishing silently.
|
|
834
|
+
const agentsMap = obj.agents as Record<string, unknown> | undefined;
|
|
835
|
+
if (agentsMap && typeof agentsMap === 'object') {
|
|
836
|
+
for (const key of Object.keys(agentsMap)) {
|
|
837
|
+
if (!(OPSX_ROLES as readonly string[]).includes(key)) {
|
|
838
|
+
localWarn(`[omp-opsx-addon] agents: unknown agent "${key}" (valid: ${OPSX_ROLES.join(', ')}); entry ignored`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
325
841
|
}
|
|
326
|
-
|
|
327
842
|
const roles: Record<OpsxRole, RoleValue> = {} as Record<OpsxRole, RoleValue>;
|
|
328
843
|
for (const role of OPSX_ROLES) {
|
|
329
|
-
|
|
844
|
+
// Within one file the `agents` mapping beats the legacy top-level key.
|
|
845
|
+
const raw = agentsMap?.[role] ?? (obj as Record<string, unknown>)[role];
|
|
330
846
|
roles[role] = parseRoleValue(raw as string | undefined, localWarn);
|
|
331
847
|
}
|
|
332
848
|
let redisHost = '127.0.0.1';
|
|
@@ -338,13 +854,72 @@ export function parseOpsxConfig(
|
|
|
338
854
|
}
|
|
339
855
|
}
|
|
340
856
|
|
|
857
|
+
// 并发控制:floor / ceiling 先各自回落,再校验区间(ceiling < floor)。
|
|
858
|
+
// floor 未显式配置时它只是默认值,MUST NOT 反过来抬高用户显式设定的 ceiling
|
|
859
|
+
// ——那等于放开了用户明确设下的上界。这种情况下把 floor 压到 ceiling(区间
|
|
860
|
+
// 退化为单点),保持上界意图;两端都显式给出矛盾值时仍按 spec 回落默认。
|
|
861
|
+
const floorConfigured = typeof obj.concurrency_floor === 'number'
|
|
862
|
+
&& Number.isFinite(obj.concurrency_floor)
|
|
863
|
+
&& Number.isInteger(obj.concurrency_floor)
|
|
864
|
+
&& obj.concurrency_floor > 0;
|
|
865
|
+
let concurrencyFloor = parsePositiveInt(obj.concurrency_floor, 'concurrency_floor', CONCURRENCY_DEFAULTS.floor, localWarn);
|
|
866
|
+
let concurrencyCeiling = parsePositiveInt(obj.concurrency_ceiling, 'concurrency_ceiling', CONCURRENCY_DEFAULTS.ceiling, localWarn);
|
|
867
|
+
if (concurrencyCeiling < concurrencyFloor) {
|
|
868
|
+
if (!floorConfigured) {
|
|
869
|
+
localWarn(
|
|
870
|
+
`[omp-opsx-addon] concurrency_ceiling: ${concurrencyCeiling} < default concurrency_floor ${concurrencyFloor}; using the ceiling as the floor`,
|
|
871
|
+
);
|
|
872
|
+
concurrencyFloor = concurrencyCeiling;
|
|
873
|
+
} else {
|
|
874
|
+
localWarn(
|
|
875
|
+
`[omp-opsx-addon] concurrency_ceiling: ${concurrencyCeiling} < concurrency_floor ${concurrencyFloor}; falling back to ${CONCURRENCY_DEFAULTS.ceiling}`,
|
|
876
|
+
);
|
|
877
|
+
concurrencyCeiling = CONCURRENCY_DEFAULTS.ceiling;
|
|
878
|
+
if (concurrencyCeiling < concurrencyFloor) {
|
|
879
|
+
localWarn(
|
|
880
|
+
`[omp-opsx-addon] concurrency_floor: ${concurrencyFloor} > default ceiling ${CONCURRENCY_DEFAULTS.ceiling}; falling back to ${CONCURRENCY_DEFAULTS.floor}`,
|
|
881
|
+
);
|
|
882
|
+
concurrencyFloor = CONCURRENCY_DEFAULTS.floor;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// 并发控制:`concurrency_up_ms` 是「干净窗口」的长度,`concurrency_eval_interval_ms`
|
|
888
|
+
// 是评估节奏。`evaluateWindow` 每次评估最多记 1 个干净窗口并丢弃余数
|
|
889
|
+
// (`cleanWindowMs = 0`),所以 up_ms ≤ eval_interval 时二者的 limit 轨迹
|
|
890
|
+
// 完全一致;把 up_ms 归一为 eval_interval 只是让配置反映真实生效值,
|
|
891
|
+
// 不改变任何行为(见 concurrency-tuner.ts 的 `max(up_ms, eval_interval_ms)`)。
|
|
892
|
+
let concurrencyUpMs = parsePositiveInt(obj.concurrency_up_ms, 'concurrency_up_ms', CONCURRENCY_DEFAULTS.up_ms, localWarn);
|
|
893
|
+
const concurrencyEvalIntervalMs = parsePositiveInt(obj.concurrency_eval_interval_ms, 'concurrency_eval_interval_ms', CONCURRENCY_DEFAULTS.eval_interval_ms, localWarn);
|
|
894
|
+
if (concurrencyUpMs < concurrencyEvalIntervalMs) {
|
|
895
|
+
localWarn(
|
|
896
|
+
`[omp-opsx-addon] concurrency_up_ms: ${concurrencyUpMs} < concurrency_eval_interval_ms ${concurrencyEvalIntervalMs}; using ${concurrencyEvalIntervalMs} (the effective clean window is max(up_ms, eval_interval_ms))`,
|
|
897
|
+
);
|
|
898
|
+
concurrencyUpMs = concurrencyEvalIntervalMs;
|
|
899
|
+
}
|
|
900
|
+
|
|
341
901
|
return {
|
|
342
902
|
roles,
|
|
343
903
|
modelAllowlist: parseModelAllowlist(obj.model_allowlist, localWarn),
|
|
344
904
|
tierOverrides: parseTierOverrides(obj.tiers, localWarn),
|
|
345
|
-
roleTiers: parseRoleTiers(
|
|
905
|
+
roleTiers: parseRoleTiers(
|
|
906
|
+
obj.agent_tiers ?? obj.role_tiers,
|
|
907
|
+
obj.agent_tiers !== undefined && obj.agent_tiers !== null ? 'agent_tiers' : 'role_tiers',
|
|
908
|
+
localWarn,
|
|
909
|
+
),
|
|
346
910
|
ompRoleTiers: parseOmpRoleTiers(obj.model_role_tiers, localWarn),
|
|
347
|
-
planProviders:
|
|
911
|
+
planProviders: parsePlanProviderList(obj.plan_providers, 'plan_providers', localWarn),
|
|
912
|
+
planProvidersRemove: parsePlanProviderList(obj.plan_providers_remove, 'plan_providers_remove', localWarn),
|
|
913
|
+
paygLastResort: parseProbeBool(obj.payg_last_resort, 'payg_last_resort', true, localWarn),
|
|
914
|
+
china: parseProbeBool(obj.china, 'china', false, localWarn),
|
|
915
|
+
providerAuto: parseProviderAuto(obj.provider_auto, localWarn),
|
|
916
|
+
providerAutoTiers: parseProviderAutoTiers(obj.provider_auto_tiers, localWarn),
|
|
917
|
+
providerModels: parseProviderModels(obj.provider_models, localWarn),
|
|
918
|
+
speedAware: parseSpeedAware(obj.speed_aware, localWarn),
|
|
919
|
+
difficultyRouting: parseDifficultyRouting(obj.difficulty_routing, localWarn),
|
|
920
|
+
stallEscalation: parseStallEscalation(obj.stall_escalation, localWarn),
|
|
921
|
+
continuityGuard: parseContinuityGuard(obj.continuity_guard, localWarn),
|
|
922
|
+
qualityPreference: parseQualityPreference(obj.quality_preference, localWarn),
|
|
348
923
|
warnings,
|
|
349
924
|
probe_enabled: parseProbeBool(obj.probe_enabled, 'probe_enabled', true, localWarn),
|
|
350
925
|
probe_timeout_ms: parsePositiveInt(obj.probe_timeout_ms, 'probe_timeout_ms', 3000, localWarn),
|
|
@@ -361,6 +936,23 @@ export function parseOpsxConfig(
|
|
|
361
936
|
redis_probe_interval_ms: parsePositiveInt(obj.redis_probe_interval_ms, 'redis_probe_interval_ms', 30_000, localWarn),
|
|
362
937
|
redis_unhealthy_retry_ms: parsePositiveInt(obj.redis_unhealthy_retry_ms, 'redis_unhealthy_retry_ms', 5_000, localWarn),
|
|
363
938
|
redis_ttl_ms: parsePositiveInt(obj.redis_ttl_ms, 'redis_ttl_ms', 180_000, localWarn),
|
|
939
|
+
autoselectOnStart: parseProbeBool(obj.autoselect_on_start, 'autoselect_on_start', true, localWarn),
|
|
940
|
+
concurrency_enabled: parseProbeBool(obj.concurrency_enabled, 'concurrency_enabled', CONCURRENCY_DEFAULTS.enabled, localWarn),
|
|
941
|
+
concurrency_floor: concurrencyFloor,
|
|
942
|
+
concurrency_ceiling: concurrencyCeiling,
|
|
943
|
+
concurrency_start: parsePositiveInt(obj.concurrency_start, 'concurrency_start', CONCURRENCY_DEFAULTS.start, localWarn),
|
|
944
|
+
concurrency_up_ms: concurrencyUpMs,
|
|
945
|
+
concurrency_down_ms: parsePositiveInt(obj.concurrency_down_ms, 'concurrency_down_ms', CONCURRENCY_DEFAULTS.down_ms, localWarn),
|
|
946
|
+
concurrency_latency_k: parsePositiveNumber(obj.concurrency_latency_k, 'concurrency_latency_k', CONCURRENCY_DEFAULTS.latency_k, localWarn),
|
|
947
|
+
concurrency_min_samples: parsePositiveInt(obj.concurrency_min_samples, 'concurrency_min_samples', CONCURRENCY_DEFAULTS.min_samples, localWarn),
|
|
948
|
+
concurrency_queue_wait_high_ms: parsePositiveInt(obj.concurrency_queue_wait_high_ms, 'concurrency_queue_wait_high_ms', CONCURRENCY_DEFAULTS.queue_wait_high_ms, localWarn),
|
|
949
|
+
concurrency_write_min_interval_ms: parsePositiveInt(obj.concurrency_write_min_interval_ms, 'concurrency_write_min_interval_ms', CONCURRENCY_DEFAULTS.write_min_interval_ms, localWarn),
|
|
950
|
+
concurrency_lease_ttl_ms: parsePositiveInt(obj.concurrency_lease_ttl_ms, 'concurrency_lease_ttl_ms', CONCURRENCY_DEFAULTS.lease_ttl_ms, localWarn),
|
|
951
|
+
concurrency_decay_after_ms: parsePositiveInt(obj.concurrency_decay_after_ms, 'concurrency_decay_after_ms', CONCURRENCY_DEFAULTS.decay_after_ms, localWarn),
|
|
952
|
+
concurrency_eval_interval_ms: concurrencyEvalIntervalMs,
|
|
953
|
+
concurrency_queue_wait_window_ms: parsePositiveInt(obj.concurrency_queue_wait_window_ms, 'concurrency_queue_wait_window_ms', CONCURRENCY_DEFAULTS.queue_wait_window_ms, localWarn),
|
|
954
|
+
concurrency_providers: parseConcurrencyProviders(obj.concurrency_providers, localWarn),
|
|
955
|
+
concurrency_ceiling_by_provider: parseCeilingByProvider(obj.concurrency_ceiling_by_provider, concurrencyFloor, localWarn),
|
|
364
956
|
};
|
|
365
957
|
}
|
|
366
958
|
|
|
@@ -393,16 +985,29 @@ function pickRoleKeys(raw: OpsxYamlShape): Partial<Record<OpsxRole, string>> {
|
|
|
393
985
|
|
|
394
986
|
function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
|
|
395
987
|
return {
|
|
988
|
+
agents: raw.agents,
|
|
989
|
+
agent_tiers: raw.agent_tiers,
|
|
396
990
|
model_allowlist: raw.model_allowlist,
|
|
397
991
|
tiers: raw.tiers,
|
|
398
992
|
role_tiers: raw.role_tiers,
|
|
399
993
|
model_role_tiers: raw.model_role_tiers,
|
|
400
994
|
plan_providers: raw.plan_providers,
|
|
995
|
+
plan_providers_remove: raw.plan_providers_remove,
|
|
996
|
+
payg_last_resort: raw.payg_last_resort,
|
|
997
|
+
china: raw.china,
|
|
401
998
|
probe_enabled: raw.probe_enabled,
|
|
402
999
|
probe_timeout_ms: raw.probe_timeout_ms,
|
|
403
1000
|
probe_http_enabled: raw.probe_http_enabled,
|
|
404
1001
|
probe_ttl_ms: raw.probe_ttl_ms,
|
|
405
1002
|
excluded_providers: raw.excluded_providers,
|
|
1003
|
+
provider_auto: raw.provider_auto,
|
|
1004
|
+
provider_auto_tiers: raw.provider_auto_tiers,
|
|
1005
|
+
provider_models: raw.provider_models,
|
|
1006
|
+
speed_aware: raw.speed_aware,
|
|
1007
|
+
difficulty_routing: raw.difficulty_routing,
|
|
1008
|
+
stall_escalation: raw.stall_escalation,
|
|
1009
|
+
continuity_guard: raw.continuity_guard,
|
|
1010
|
+
quality_preference: raw.quality_preference,
|
|
406
1011
|
status_probe_enabled: raw.status_probe_enabled,
|
|
407
1012
|
redis_enabled: raw.redis_enabled,
|
|
408
1013
|
redis_host: raw.redis_host,
|
|
@@ -413,6 +1018,23 @@ function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
|
|
|
413
1018
|
redis_probe_interval_ms: raw.redis_probe_interval_ms,
|
|
414
1019
|
redis_unhealthy_retry_ms: raw.redis_unhealthy_retry_ms,
|
|
415
1020
|
redis_ttl_ms: raw.redis_ttl_ms,
|
|
1021
|
+
autoselect_on_start: raw.autoselect_on_start,
|
|
1022
|
+
concurrency_enabled: raw.concurrency_enabled,
|
|
1023
|
+
concurrency_floor: raw.concurrency_floor,
|
|
1024
|
+
concurrency_ceiling: raw.concurrency_ceiling,
|
|
1025
|
+
concurrency_start: raw.concurrency_start,
|
|
1026
|
+
concurrency_up_ms: raw.concurrency_up_ms,
|
|
1027
|
+
concurrency_down_ms: raw.concurrency_down_ms,
|
|
1028
|
+
concurrency_latency_k: raw.concurrency_latency_k,
|
|
1029
|
+
concurrency_min_samples: raw.concurrency_min_samples,
|
|
1030
|
+
concurrency_queue_wait_high_ms: raw.concurrency_queue_wait_high_ms,
|
|
1031
|
+
concurrency_write_min_interval_ms: raw.concurrency_write_min_interval_ms,
|
|
1032
|
+
concurrency_lease_ttl_ms: raw.concurrency_lease_ttl_ms,
|
|
1033
|
+
concurrency_decay_after_ms: raw.concurrency_decay_after_ms,
|
|
1034
|
+
concurrency_eval_interval_ms: raw.concurrency_eval_interval_ms,
|
|
1035
|
+
concurrency_queue_wait_window_ms: raw.concurrency_queue_wait_window_ms,
|
|
1036
|
+
concurrency_providers: raw.concurrency_providers,
|
|
1037
|
+
concurrency_ceiling_by_provider: raw.concurrency_ceiling_by_provider,
|
|
416
1038
|
};
|
|
417
1039
|
}
|
|
418
1040
|
|
|
@@ -441,11 +1063,44 @@ function warnLegacySettingsJson(
|
|
|
441
1063
|
}
|
|
442
1064
|
}
|
|
443
1065
|
|
|
1066
|
+
/**
|
|
1067
|
+
* Per-layer rename diagnostics: one aggregated warning when a layer file
|
|
1068
|
+
* still uses a legacy key (all offending names in a single line), plus one
|
|
1069
|
+
* warning per unknown `agents` subkey (the mapping's key set is exactly the
|
|
1070
|
+
* four managed agents, so a typo would otherwise vanish silently).
|
|
1071
|
+
*/
|
|
1072
|
+
function warnAgentRenameDiagnostics(
|
|
1073
|
+
path: string,
|
|
1074
|
+
raw: OpsxYamlShape,
|
|
1075
|
+
warn: (msg: string) => void,
|
|
1076
|
+
): void {
|
|
1077
|
+
const legacy = LEGACY_AGENT_KEYS.filter((key) => {
|
|
1078
|
+
const v = (raw as Record<string, unknown>)[key];
|
|
1079
|
+
return v !== undefined && v !== null;
|
|
1080
|
+
});
|
|
1081
|
+
if (legacy.length > 0) {
|
|
1082
|
+
warn(
|
|
1083
|
+
`[omp-opsx-addon] ${path}: legacy key(s) ${legacy.map((k) => `'${k}'`).join(', ')} still honored — ` +
|
|
1084
|
+
`move them into the 'agents:' mapping / rename 'role_tiers' to 'agent_tiers' ` +
|
|
1085
|
+
'(within one file the new keys win); legacy keys are scheduled for removal after one release cycle',
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const agents = raw.agents;
|
|
1089
|
+
if (agents && typeof agents === 'object') {
|
|
1090
|
+
for (const key of Object.keys(agents)) {
|
|
1091
|
+
if (!(OPSX_ROLES as readonly string[]).includes(key)) {
|
|
1092
|
+
warn(`[omp-opsx-addon] ${path}: agents.${key}: unknown agent (valid: ${OPSX_ROLES.join(', ')}); entry ignored`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
444
1098
|
/**
|
|
445
1099
|
* Read opsx config from two-layer opsx.yml:
|
|
446
1100
|
* - global: `<homeDir>/.omp/agent/opsx.yml`
|
|
447
|
-
* - project: `<cwd>/.omp/opsx.yml` (per-key override on
|
|
448
|
-
* `model_allowlist` / `tiers` /
|
|
1101
|
+
* - project: `<cwd>/.omp/opsx.yml` (per-key override on `agents` — legacy:
|
|
1102
|
+
* top-level role keys; full overlay on `model_allowlist` / `tiers` /
|
|
1103
|
+
* `agent_tiers` — legacy: `role_tiers`)
|
|
449
1104
|
*
|
|
450
1105
|
* `homeDir` is overridable for tests; defaults to `os.homedir()`.
|
|
451
1106
|
*/
|
|
@@ -471,30 +1126,63 @@ export function readOpsxSettingsFromPaths(
|
|
|
471
1126
|
const homeFromGlobal = globalPath.replace(/\.omp\/agent\/opsx\.yml$/, '');
|
|
472
1127
|
const projectFromPath = projectPath.replace(/\.omp\/opsx\.yml$/, '');
|
|
473
1128
|
warnLegacySettingsJson(projectFromPath, homeFromGlobal, warn);
|
|
1129
|
+
warnAgentRenameDiagnostics(globalPath, globalYaml, warn);
|
|
1130
|
+
warnAgentRenameDiagnostics(projectPath, projectYaml, warn);
|
|
474
1131
|
|
|
475
1132
|
const globalRoles = pickRoleKeys(globalYaml);
|
|
476
1133
|
const projectRoles = pickRoleKeys(projectYaml);
|
|
477
1134
|
|
|
478
|
-
//
|
|
1135
|
+
// Top-level: project fully replaces global when present.
|
|
1136
|
+
const projectTop = pickTopLevelKeys(projectYaml);
|
|
1137
|
+
const globalTop = pickTopLevelKeys(globalYaml);
|
|
1138
|
+
|
|
1139
|
+
// Per-role: project overrides global, fall back to DEFAULT_OPSX. Within
|
|
1140
|
+
// each layer the `agents` mapping beats the legacy top-level key; across
|
|
1141
|
+
// layers it stays a per-key overlay (a project legacy key can override a
|
|
1142
|
+
// global `agents` entry and vice versa).
|
|
479
1143
|
const merged: OpsxYamlShape = { ...globalYaml };
|
|
480
1144
|
for (const role of OPSX_ROLES) {
|
|
481
|
-
const v = projectRoles[role] ?? globalRoles[role] ?? DEFAULT_OPSX[role];
|
|
1145
|
+
const v = projectTop.agents?.[role] ?? projectRoles[role] ?? globalTop.agents?.[role] ?? globalRoles[role] ?? DEFAULT_OPSX[role];
|
|
482
1146
|
(merged as Record<string, unknown>)[role] = v;
|
|
483
1147
|
}
|
|
1148
|
+
// The loop already folded `agents` entries into the resolved legacy keys;
|
|
1149
|
+
// a raw mapping left on `merged` would shadow them inside parseOpsxConfig.
|
|
1150
|
+
merged.agents = undefined;
|
|
484
1151
|
|
|
485
|
-
// Top-level: project fully replaces global when present.
|
|
486
|
-
const projectTop = pickTopLevelKeys(projectYaml);
|
|
487
|
-
const globalTop = pickTopLevelKeys(globalYaml);
|
|
488
1152
|
merged.model_allowlist = projectTop.model_allowlist ?? globalTop.model_allowlist;
|
|
489
1153
|
merged.tiers = projectTop.tiers ?? globalTop.tiers;
|
|
490
1154
|
merged.role_tiers = projectTop.role_tiers ?? globalTop.role_tiers;
|
|
1155
|
+
// Tier map: `agent_tiers` beats `role_tiers` within each layer, then the
|
|
1156
|
+
// legacy whole-key overlay applies (project map replaces global's).
|
|
1157
|
+
const projectTiers = projectTop.agent_tiers ?? projectTop.role_tiers;
|
|
1158
|
+
const globalTiers = globalTop.agent_tiers ?? globalTop.role_tiers;
|
|
1159
|
+
merged.agent_tiers = projectTiers ?? globalTiers;
|
|
491
1160
|
merged.model_role_tiers = projectTop.model_role_tiers ?? globalTop.model_role_tiers;
|
|
492
1161
|
merged.plan_providers = projectTop.plan_providers ?? globalTop.plan_providers;
|
|
1162
|
+
merged.plan_providers_remove = projectTop.plan_providers_remove ?? globalTop.plan_providers_remove;
|
|
1163
|
+
merged.payg_last_resort = projectTop.payg_last_resort ?? globalTop.payg_last_resort;
|
|
1164
|
+
// `??` keeps an explicit project `false` (parseProbeBool only falls back on
|
|
1165
|
+
// null/undefined AFTER the merge, so undefined reveals the other layer).
|
|
1166
|
+
merged.china = projectTop.china ?? globalTop.china;
|
|
493
1167
|
merged.probe_enabled = projectTop.probe_enabled ?? globalTop.probe_enabled;
|
|
494
1168
|
merged.probe_timeout_ms = projectTop.probe_timeout_ms ?? globalTop.probe_timeout_ms;
|
|
495
1169
|
merged.probe_http_enabled = projectTop.probe_http_enabled ?? globalTop.probe_http_enabled;
|
|
496
1170
|
merged.probe_ttl_ms = projectTop.probe_ttl_ms ?? globalTop.probe_ttl_ms;
|
|
497
1171
|
merged.excluded_providers = projectTop.excluded_providers ?? globalTop.excluded_providers;
|
|
1172
|
+
// Provider 键控 map 同为整键覆盖(先例 concurrency_ceiling_by_provider):项目层
|
|
1173
|
+
// 声明任一键即完整替换全局同名键,不做 per-key 叠加——只想加键就把全局条目抄过来。
|
|
1174
|
+
merged.provider_auto = projectTop.provider_auto ?? globalTop.provider_auto;
|
|
1175
|
+
merged.provider_auto_tiers = projectTop.provider_auto_tiers ?? globalTop.provider_auto_tiers;
|
|
1176
|
+
merged.provider_models = projectTop.provider_models ?? globalTop.provider_models;
|
|
1177
|
+
// 对象键一律整键覆盖(同上 provider_* 先例):项目层声明 speed_aware 即
|
|
1178
|
+
// 整份替换全局,不做字段级叠加。
|
|
1179
|
+
merged.speed_aware = projectTop.speed_aware ?? globalTop.speed_aware;
|
|
1180
|
+
// W4 路由四键同款整键覆盖:项目层声明任一键即完整替换全局同名键。
|
|
1181
|
+
merged.difficulty_routing = projectTop.difficulty_routing ?? globalTop.difficulty_routing;
|
|
1182
|
+
merged.stall_escalation = projectTop.stall_escalation ?? globalTop.stall_escalation;
|
|
1183
|
+
merged.continuity_guard = projectTop.continuity_guard ?? globalTop.continuity_guard;
|
|
1184
|
+
// 顶层标量键同 `??` 惯例(项目层显式值优先,undefined 露出另一层)。
|
|
1185
|
+
merged.quality_preference = projectTop.quality_preference ?? globalTop.quality_preference;
|
|
498
1186
|
merged.status_probe_enabled = projectTop.status_probe_enabled ?? globalTop.status_probe_enabled;
|
|
499
1187
|
merged.redis_enabled = projectTop.redis_enabled ?? globalTop.redis_enabled;
|
|
500
1188
|
merged.redis_host = projectTop.redis_host ?? globalTop.redis_host;
|
|
@@ -505,6 +1193,23 @@ export function readOpsxSettingsFromPaths(
|
|
|
505
1193
|
merged.redis_probe_interval_ms = projectTop.redis_probe_interval_ms ?? globalTop.redis_probe_interval_ms;
|
|
506
1194
|
merged.redis_unhealthy_retry_ms = projectTop.redis_unhealthy_retry_ms ?? globalTop.redis_unhealthy_retry_ms;
|
|
507
1195
|
merged.redis_ttl_ms = projectTop.redis_ttl_ms ?? globalTop.redis_ttl_ms;
|
|
1196
|
+
merged.autoselect_on_start = projectTop.autoselect_on_start ?? globalTop.autoselect_on_start;
|
|
1197
|
+
merged.concurrency_enabled = projectTop.concurrency_enabled ?? globalTop.concurrency_enabled;
|
|
1198
|
+
merged.concurrency_floor = projectTop.concurrency_floor ?? globalTop.concurrency_floor;
|
|
1199
|
+
merged.concurrency_ceiling = projectTop.concurrency_ceiling ?? globalTop.concurrency_ceiling;
|
|
1200
|
+
merged.concurrency_start = projectTop.concurrency_start ?? globalTop.concurrency_start;
|
|
1201
|
+
merged.concurrency_up_ms = projectTop.concurrency_up_ms ?? globalTop.concurrency_up_ms;
|
|
1202
|
+
merged.concurrency_down_ms = projectTop.concurrency_down_ms ?? globalTop.concurrency_down_ms;
|
|
1203
|
+
merged.concurrency_latency_k = projectTop.concurrency_latency_k ?? globalTop.concurrency_latency_k;
|
|
1204
|
+
merged.concurrency_min_samples = projectTop.concurrency_min_samples ?? globalTop.concurrency_min_samples;
|
|
1205
|
+
merged.concurrency_queue_wait_high_ms = projectTop.concurrency_queue_wait_high_ms ?? globalTop.concurrency_queue_wait_high_ms;
|
|
1206
|
+
merged.concurrency_write_min_interval_ms = projectTop.concurrency_write_min_interval_ms ?? globalTop.concurrency_write_min_interval_ms;
|
|
1207
|
+
merged.concurrency_lease_ttl_ms = projectTop.concurrency_lease_ttl_ms ?? globalTop.concurrency_lease_ttl_ms;
|
|
1208
|
+
merged.concurrency_decay_after_ms = projectTop.concurrency_decay_after_ms ?? globalTop.concurrency_decay_after_ms;
|
|
1209
|
+
merged.concurrency_eval_interval_ms = projectTop.concurrency_eval_interval_ms ?? globalTop.concurrency_eval_interval_ms;
|
|
1210
|
+
merged.concurrency_queue_wait_window_ms = projectTop.concurrency_queue_wait_window_ms ?? globalTop.concurrency_queue_wait_window_ms;
|
|
1211
|
+
merged.concurrency_providers = projectTop.concurrency_providers ?? globalTop.concurrency_providers;
|
|
1212
|
+
merged.concurrency_ceiling_by_provider = projectTop.concurrency_ceiling_by_provider ?? globalTop.concurrency_ceiling_by_provider;
|
|
508
1213
|
|
|
509
1214
|
return parseOpsxConfig(merged, warn);
|
|
510
1215
|
}
|