@genee/omp-opsx-addon 0.8.0 → 0.10.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 +440 -71
- package/index.ts +1106 -235
- package/lib/agent-defs.ts +80 -89
- 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/family-filter.ts +115 -2
- package/lib/model-roles.ts +43 -41
- 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 +303 -93
- package/lib/pipe-push.ts +212 -26
- package/lib/provider-variants.ts +154 -0
- package/lib/reachability-refresh.ts +10 -2
- package/lib/selection-filters.ts +42 -1
- package/lib/stall-detector.ts +185 -0
- package/lib/system-prompt.ts +28 -11
- package/lib/tiers-data.ts +47 -5
- package/lib/tiers-updater.ts +51 -8
- package/lib/unified-config.ts +735 -68
- 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` are no longer read (ignored). Each
|
|
27
|
+
* layer file that still contains them gets one aggregated migration
|
|
28
|
+
* warning per config read pointing at the move into `agents:` /
|
|
29
|
+
* rename to `agent_tiers:`. Affected agents fall back to `auto`.
|
|
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>([
|
|
@@ -50,28 +69,51 @@ export const OMP_ROLE_ALIASES = new Set<string>([
|
|
|
50
69
|
/** Special "let the plugin decide" marker. */
|
|
51
70
|
export const AUTO_TOKEN = 'auto';
|
|
52
71
|
|
|
53
|
-
/**
|
|
54
|
-
export const
|
|
55
|
-
export type
|
|
72
|
+
/** Dispatch agents the plugin manages. */
|
|
73
|
+
export const OPSX_AGENTS = ['coder', 'reviewer', 'planner', 'proposal-reviewer'] as const;
|
|
74
|
+
export type OpsxAgent = (typeof OPSX_AGENTS)[number];
|
|
75
|
+
|
|
76
|
+
/** Keys superseded by `agents` / `agent_tiers`; no longer read — warn-only migration detect. */
|
|
77
|
+
const LEGACY_AGENT_KEYS: readonly (OpsxAgent | 'role_tiers')[] = [...OPSX_AGENTS, 'role_tiers'];
|
|
56
78
|
|
|
57
79
|
/** Raw YAML shape — every key is optional. */
|
|
58
80
|
export interface OpsxYamlShape {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
planner?: string;
|
|
62
|
-
'proposal-reviewer'?: string;
|
|
63
|
-
primary?: string;
|
|
81
|
+
/** Dispatch agent → value; canonical form. */
|
|
82
|
+
agents?: Partial<Record<OpsxAgent, string>>;
|
|
64
83
|
model_allowlist?: string[];
|
|
65
84
|
tiers?: TierOverride[];
|
|
66
|
-
|
|
85
|
+
/** Dispatch agent → tier override for the four opsx agents. */
|
|
86
|
+
agent_tiers?: Partial<Record<OpsxAgent, TierName>>;
|
|
67
87
|
/** OMP model role → tier override; `skip` excludes the role from the overlay. Keys are arbitrary strings (custom roles allowed). */
|
|
68
88
|
model_role_tiers?: Record<string, string>;
|
|
69
89
|
plan_providers?: string[];
|
|
90
|
+
/** Providers removed from the effective plan set: (defaults ∪ plan_providers) − plan_providers_remove. */
|
|
91
|
+
plan_providers_remove?: string[];
|
|
92
|
+
/** false collapses the PAYG-last coverage partition (pay-as-you-go candidates compete normally). Default true. */
|
|
93
|
+
payg_last_resort?: boolean;
|
|
94
|
+
/** china 家族缺省排除 + 同账号区域变体偏好方向(true = 国内语境:排除 gpt/claude/openai/anthropic 家族、区域变体保留国内入口)。缺省 false。 */
|
|
95
|
+
china?: boolean;
|
|
70
96
|
probe_enabled?: boolean;
|
|
71
97
|
probe_timeout_ms?: number;
|
|
72
98
|
probe_http_enabled?: boolean;
|
|
73
99
|
probe_ttl_ms?: number;
|
|
74
100
|
excluded_providers?: string[];
|
|
101
|
+
/** provider → 服务端路由哨兵模型 id(如 `ark-coding-plan: ark-code-latest`)。键经 canonicalizeProvider 规范化。 */
|
|
102
|
+
provider_auto?: Record<string, string>;
|
|
103
|
+
/** provider → 哨兵声明档位(tiny|low|mid|high|top);非法值 warn 丢弃,未声明按 mid。 */
|
|
104
|
+
provider_auto_tiers?: Record<string, string>;
|
|
105
|
+
/** provider → 模式数组:列出的 provider 收窄到命中模型,未列出不受限。 */
|
|
106
|
+
provider_models?: Record<string, string[]>;
|
|
107
|
+
/** 速度感知选择:enabled(缺省 true)、mode(tie-break|aggressive,缺省 tie-break)、min_samples(缺省 20)。 */
|
|
108
|
+
speed_aware?: { enabled?: boolean; mode?: string; min_samples?: number };
|
|
109
|
+
/** 难度路由(W4):prompt 分类 → 写入集 tier 目标偏移。缺省 false。 */
|
|
110
|
+
difficulty_routing?: { enabled?: boolean };
|
|
111
|
+
/** stall 升档(W4):同签名滑窗重复 → default 目标 +1。缺省 false / 6 / 3。 */
|
|
112
|
+
stall_escalation?: { enabled?: boolean; window?: number; threshold?: number };
|
|
113
|
+
/** 连续性守门(W4):在选模型粘性。缺省 false / weight 0.6。 */
|
|
114
|
+
continuity_guard?: { enabled?: boolean; weight?: number };
|
|
115
|
+
/** 质量带旋钮(W4):balanced | cost | quality。缺省 balanced(独立于路由开关)。 */
|
|
116
|
+
quality_preference?: string;
|
|
75
117
|
status_probe_enabled?: boolean;
|
|
76
118
|
redis_enabled?: boolean;
|
|
77
119
|
redis_host?: string;
|
|
@@ -82,24 +124,64 @@ export interface OpsxYamlShape {
|
|
|
82
124
|
redis_probe_interval_ms?: number;
|
|
83
125
|
redis_unhealthy_retry_ms?: number;
|
|
84
126
|
redis_ttl_ms?: number;
|
|
127
|
+
/** 会话启动自动应用一次选择(缺省 true);false 时 session_start 不应用、不写覆盖,行为与本键引入前一致。 */
|
|
128
|
+
autoselect_on_start?: boolean;
|
|
129
|
+
concurrency_enabled?: boolean;
|
|
130
|
+
concurrency_floor?: number;
|
|
131
|
+
concurrency_ceiling?: number;
|
|
132
|
+
concurrency_start?: number;
|
|
133
|
+
concurrency_up_ms?: number;
|
|
134
|
+
concurrency_down_ms?: number;
|
|
135
|
+
concurrency_latency_k?: number;
|
|
136
|
+
concurrency_min_samples?: number;
|
|
137
|
+
concurrency_queue_wait_high_ms?: number;
|
|
138
|
+
concurrency_write_min_interval_ms?: number;
|
|
139
|
+
concurrency_lease_ttl_ms?: number;
|
|
140
|
+
concurrency_decay_after_ms?: number;
|
|
141
|
+
concurrency_eval_interval_ms?: number;
|
|
142
|
+
concurrency_queue_wait_window_ms?: number;
|
|
143
|
+
concurrency_providers?: string[];
|
|
144
|
+
concurrency_ceiling_by_provider?: Record<string, number>;
|
|
85
145
|
}
|
|
86
146
|
|
|
87
|
-
/**
|
|
88
|
-
export type
|
|
147
|
+
/** An agent value as parsed. */
|
|
148
|
+
export type AgentValue =
|
|
89
149
|
| { kind: 'auto' }
|
|
90
150
|
| { kind: 'omp-alias'; alias: string }
|
|
91
151
|
| { kind: 'model'; modelId: string }
|
|
92
152
|
| { kind: 'provider-model'; provider: string; modelId: string };
|
|
93
153
|
|
|
94
154
|
export interface ResolvedOpsxConfig {
|
|
95
|
-
|
|
155
|
+
agents: Record<OpsxAgent, AgentValue>;
|
|
96
156
|
modelAllowlist: string[];
|
|
97
157
|
tierOverrides: TierOverride[];
|
|
98
|
-
|
|
158
|
+
agentTiers: Partial<Record<OpsxAgent, TierName>>;
|
|
99
159
|
/** OMP model role → tier (or `'skip'`); drives the pick-model role overlay. */
|
|
100
160
|
ompRoleTiers: Record<string, TierName | 'skip'>;
|
|
101
161
|
/** Provider ids that have monthly plan subscriptions. */
|
|
102
162
|
planProviders: string[];
|
|
163
|
+
/** Canonical provider ids removed from the effective plan set. */
|
|
164
|
+
planProvidersRemove: string[];
|
|
165
|
+
/** PAYG candidates sort last (coverage partition). false collapses the partition. Default true. */
|
|
166
|
+
paygLastResort: boolean;
|
|
167
|
+
/** Standing china family exclusion + regional-variant preference direction. Default false (= status quo). */
|
|
168
|
+
china: boolean;
|
|
169
|
+
/** provider → 服务端路由哨兵模型 id(键已 canonicalizeProvider 规范化)。空对象 = 无哨兵。 */
|
|
170
|
+
providerAuto: Record<string, string>;
|
|
171
|
+
/** provider → 哨兵声明档位(值已过 TIER_NAMES 校验)。空对象 = 全部缺省 mid。 */
|
|
172
|
+
providerAutoTiers: Record<string, TierName>;
|
|
173
|
+
/** provider → 模式数组(键已规范化):仅列出的 provider 被收窄,未列出不受限。 */
|
|
174
|
+
providerModels: Record<string, string[]>;
|
|
175
|
+
/** 速度感知选择三键(解析层已校验并补齐缺省;enabled=false 时装配层零读取)。 */
|
|
176
|
+
speedAware: SpeedAwareConfig;
|
|
177
|
+
/** W4 难度路由(缺省 false:结构改造 opt-in,W1-W3 收益不依赖它)。 */
|
|
178
|
+
difficultyRouting: RoutingConfig;
|
|
179
|
+
/** W4 stall 升档(缺省 false / window 6 / threshold 3)。 */
|
|
180
|
+
stallEscalation: StallEscalationConfig;
|
|
181
|
+
/** W4 连续性守门(缺省 false / weight 0.6)。 */
|
|
182
|
+
continuityGuard: ContinuityGuardConfig;
|
|
183
|
+
/** W4 质量带旋钮(缺省 balanced = 既有比较器分支逐字节)。 */
|
|
184
|
+
qualityPreference: QualityPreference;
|
|
103
185
|
warnings: string[];
|
|
104
186
|
/** 是否开启可达性探测,默认 true */
|
|
105
187
|
probe_enabled: boolean;
|
|
@@ -131,10 +213,53 @@ export interface ResolvedOpsxConfig {
|
|
|
131
213
|
redis_unhealthy_retry_ms: number;
|
|
132
214
|
/** Redis key TTL ms(默认 180000)。 */
|
|
133
215
|
redis_ttl_ms: number;
|
|
216
|
+
/** 会话启动自动应用一次选择,默认 true;false 时 session_start handler 直接返回(不 setModel、不写覆盖)。 */
|
|
217
|
+
autoselectOnStart: boolean;
|
|
218
|
+
/** 是否开启自适应并发控制,默认 true;false 时装配层不创建控制器(零写入)。 */
|
|
219
|
+
concurrency_enabled: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* 全局并发上限下界,默认 4(须为 ≥ 1 的整数)。每个 provider 的**有效下限**
|
|
222
|
+
* 为 `min(concurrency_floor, initial)`(见 concurrency-tuner 的
|
|
223
|
+
* `effectiveFloor` / `createProviderState`):该值设定区间,MUST NOT 抬高
|
|
224
|
+
* provider 自身的静态现值(静态 3 的 provider 下限与起点都保持 3)。
|
|
225
|
+
*/
|
|
226
|
+
concurrency_floor: number;
|
|
227
|
+
/**
|
|
228
|
+
* 全局并发上限上界,默认 16。低于**有效 floor** 时:floor 未有效显式配置
|
|
229
|
+
* (缺省或非法回落)则把 floor 压到该 ceiling(区间退化为单点),否则回落
|
|
230
|
+
* 默认(ceiling → 16,必要时 floor 也回落默认);见 `parseOpsxConfig`。
|
|
231
|
+
*/
|
|
232
|
+
concurrency_ceiling: number;
|
|
233
|
+
/** 无现有静态值时的保守启动值,默认 4(区间 clamp 由控制器负责)。 */
|
|
234
|
+
concurrency_start: number;
|
|
235
|
+
/** 加性上调所需干净窗口时长 ms,默认 30000。 */
|
|
236
|
+
concurrency_up_ms: number;
|
|
237
|
+
/** 乘性下调后的冷却时长 ms,默认 60000。 */
|
|
238
|
+
concurrency_down_ms: number;
|
|
239
|
+
/** 延迟膨胀判定比值,默认 2(> 1 的任意正数,允许小数)。 */
|
|
240
|
+
concurrency_latency_k: number;
|
|
241
|
+
/** 延迟判定所需最小样本数,默认 5。 */
|
|
242
|
+
concurrency_min_samples: number;
|
|
243
|
+
/** 排队等待 p50 高企阈值 ms,默认 2000(超阈值禁止上调)。 */
|
|
244
|
+
concurrency_queue_wait_high_ms: number;
|
|
245
|
+
/** 同一 provider 两次成功写入的最小间隔 ms,默认 20000。 */
|
|
246
|
+
concurrency_write_min_interval_ms: number;
|
|
247
|
+
/** 租约 TTL ms,默认 60000。 */
|
|
248
|
+
concurrency_lease_ttl_ms: number;
|
|
249
|
+
/** 无观测衰减周期 ms,默认 86400000(24h)。 */
|
|
250
|
+
concurrency_decay_after_ms: number;
|
|
251
|
+
/** 评估节拍 ms,默认 30000。 */
|
|
252
|
+
concurrency_eval_interval_ms: number;
|
|
253
|
+
/** 排队等待护栏统计窗口 ms,默认 60000(独立于评估节拍)。 */
|
|
254
|
+
concurrency_queue_wait_window_ms: number;
|
|
255
|
+
/** 显式希望由控制器试探上限的 provider(规范化 id,去重、声明顺序)。 */
|
|
256
|
+
concurrency_providers: string[];
|
|
257
|
+
/** per-provider ceiling 覆盖(键已规范化,值为 ≥ floor 的正整数)。 */
|
|
258
|
+
concurrency_ceiling_by_provider: Record<string, number>;
|
|
134
259
|
}
|
|
135
260
|
|
|
136
261
|
/** Default values applied when no config is provided. */
|
|
137
|
-
export const
|
|
262
|
+
export const DEFAULT_OPSX_AGENTS: Record<OpsxAgent, string> = {
|
|
138
263
|
coder: 'auto',
|
|
139
264
|
reviewer: 'auto',
|
|
140
265
|
planner: 'auto',
|
|
@@ -149,8 +274,8 @@ function splitProviderModel(raw: string): { provider?: string; modelId: string }
|
|
|
149
274
|
return { provider: provider || undefined, modelId };
|
|
150
275
|
}
|
|
151
276
|
|
|
152
|
-
/** Parse a single
|
|
153
|
-
export function
|
|
277
|
+
/** Parse a single agent value string into an AgentValue. */
|
|
278
|
+
export function parseAgentValue(raw: string | undefined, _warn: (msg: string) => void): AgentValue {
|
|
154
279
|
if (raw === undefined || raw === null) {
|
|
155
280
|
return { kind: 'auto' };
|
|
156
281
|
}
|
|
@@ -162,7 +287,7 @@ export function parseRoleValue(raw: string | undefined, _warn: (msg: string) =>
|
|
|
162
287
|
const legacyStripped = trimmed.replace(/^(pi|local):/i, '').trim();
|
|
163
288
|
if (legacyStripped !== trimmed) {
|
|
164
289
|
_warn(`[omp-opsx-addon] legacy "${trimmed}" → "${legacyStripped}" (pi:/local: prefix no longer needed)`);
|
|
165
|
-
return
|
|
290
|
+
return parseAgentValue(legacyStripped, _warn);
|
|
166
291
|
}
|
|
167
292
|
if (OMP_ROLE_ALIASES.has(trimmed.toLowerCase())) {
|
|
168
293
|
return { kind: 'omp-alias', alias: trimmed.toLowerCase() };
|
|
@@ -202,22 +327,26 @@ export function parseTierOverrides(
|
|
|
202
327
|
return out;
|
|
203
328
|
}
|
|
204
329
|
|
|
205
|
-
/**
|
|
206
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Validate tier entries for the four opsx agents. `keyLabel` names the config
|
|
332
|
+
* key being parsed (always `'agent_tiers'` after legacy `role_tiers` removal).
|
|
333
|
+
*/
|
|
334
|
+
export function parseAgentTiers(
|
|
207
335
|
raw: unknown,
|
|
336
|
+
keyLabel: 'agent_tiers',
|
|
208
337
|
warn: (msg: string) => void,
|
|
209
|
-
): Partial<Record<
|
|
338
|
+
): Partial<Record<OpsxAgent, TierName>> {
|
|
210
339
|
if (!raw || typeof raw !== 'object') return {};
|
|
211
|
-
const out: Partial<Record<
|
|
212
|
-
for (const role of
|
|
340
|
+
const out: Partial<Record<OpsxAgent, TierName>> = {};
|
|
341
|
+
for (const role of OPSX_AGENTS) {
|
|
213
342
|
const v = (raw as Record<string, unknown>)[role];
|
|
214
343
|
if (v === undefined || v === null) continue;
|
|
215
344
|
if (typeof v !== 'string') {
|
|
216
|
-
warn(`[omp-opsx-addon]
|
|
345
|
+
warn(`[omp-opsx-addon] ${keyLabel}.${role}: expected tier name, got ${typeof v}`);
|
|
217
346
|
continue;
|
|
218
347
|
}
|
|
219
348
|
if (!TIER_NAMES.includes(v as TierName)) {
|
|
220
|
-
warn(`[omp-opsx-addon]
|
|
349
|
+
warn(`[omp-opsx-addon] ${keyLabel}.${role}: unknown tier "${v}" (use one of ${TIER_NAMES.join('/')})`);
|
|
221
350
|
continue;
|
|
222
351
|
}
|
|
223
352
|
out[role] = v as TierName;
|
|
@@ -306,6 +435,375 @@ export function parseStringArray(raw: unknown, key: string, warn: (msg: string)
|
|
|
306
435
|
return out;
|
|
307
436
|
}
|
|
308
437
|
|
|
438
|
+
/**
|
|
439
|
+
* `concurrency_*` 默认值 —— 逐键对应 spec `provider-concurrency-control`
|
|
440
|
+
* 的「配置面与降级」Requirement(不要按 tasks 里的数字出现顺序推断)。
|
|
441
|
+
*/
|
|
442
|
+
export const CONCURRENCY_DEFAULTS = {
|
|
443
|
+
enabled: true,
|
|
444
|
+
floor: 4,
|
|
445
|
+
ceiling: 16,
|
|
446
|
+
start: 4,
|
|
447
|
+
up_ms: 30_000,
|
|
448
|
+
down_ms: 60_000,
|
|
449
|
+
latency_k: 2,
|
|
450
|
+
min_samples: 5,
|
|
451
|
+
queue_wait_high_ms: 2_000,
|
|
452
|
+
write_min_interval_ms: 20_000,
|
|
453
|
+
lease_ttl_ms: 60_000,
|
|
454
|
+
decay_after_ms: 86_400_000,
|
|
455
|
+
eval_interval_ms: 30_000,
|
|
456
|
+
queue_wait_window_ms: 60_000,
|
|
457
|
+
} as const;
|
|
458
|
+
|
|
459
|
+
/** 解析正数(允许小数,用于 `concurrency_latency_k` 这类比值):非法则返回 defaultValue 并 warn。 */
|
|
460
|
+
export function parsePositiveNumber(raw: unknown, key: string, defaultValue: number, warn: (msg: string) => void): number {
|
|
461
|
+
if (raw === undefined || raw === null) return defaultValue;
|
|
462
|
+
if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return raw;
|
|
463
|
+
warn(`[omp-opsx-addon] ${key}: expected positive number, got ${JSON.stringify(raw)}`);
|
|
464
|
+
return defaultValue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** 规范化 provider id 列表:canonicalize + 去重,保持声明顺序;非字符串/空串跳过并 warn。 */
|
|
468
|
+
function canonicalizeProviderList(raw: readonly unknown[], key: string, warn: (msg: string) => void): string[] {
|
|
469
|
+
const out: string[] = [];
|
|
470
|
+
const seen = new Set<string>();
|
|
471
|
+
for (const entry of raw) {
|
|
472
|
+
if (typeof entry !== 'string') {
|
|
473
|
+
warn(`[omp-opsx-addon] ${key}: skipping non-string entry ${JSON.stringify(entry)}`);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const canonical = canonicalizeProvider(entry.trim());
|
|
477
|
+
if (!canonical) {
|
|
478
|
+
warn(`[omp-opsx-addon] ${key}: skipping empty provider id`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (!seen.has(canonical)) {
|
|
482
|
+
seen.add(canonical);
|
|
483
|
+
out.push(canonical);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** 解析 `concurrency_providers`:非数组回落 [] 并 warn;条目规范化、去重。 */
|
|
490
|
+
export function parseConcurrencyProviders(raw: unknown, warn: (msg: string) => void): string[] {
|
|
491
|
+
if (raw === undefined || raw === null) return [];
|
|
492
|
+
if (!Array.isArray(raw)) {
|
|
493
|
+
warn(`[omp-opsx-addon] concurrency_providers: expected array of provider ids, got ${JSON.stringify(raw)}`);
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
return canonicalizeProviderList(raw, 'concurrency_providers', warn);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 解析 `plan_providers` / `plan_providers_remove`:非数组回落 [] 并 warn;条目规范化、去重。 */
|
|
500
|
+
function parsePlanProviderList(raw: unknown, key: string, warn: (msg: string) => void): string[] {
|
|
501
|
+
if (raw === undefined || raw === null) return [];
|
|
502
|
+
if (!Array.isArray(raw)) {
|
|
503
|
+
warn(`[omp-opsx-addon] ${key}: expected array of provider ids, got ${JSON.stringify(raw)}`);
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
return canonicalizeProviderList(raw, key, warn);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 解析 `concurrency_ceiling_by_provider`:键规范化(大小写 / `arkcodingplan` 别名同源),
|
|
511
|
+
* 值须为 ≥ 1 的正整数;非法条目跳过并 warn(回落到全局 `concurrency_ceiling`),
|
|
512
|
+
* 非对象回落 {} 并 warn。
|
|
513
|
+
*
|
|
514
|
+
* 低于全局 `floor` 的覆盖**保留**并 warn:它是用户对单个 provider 的显式收窄,
|
|
515
|
+
* 丢弃它会让该 provider 的 ceiling 回落到全局值(等于抬高用户设定)。控制器的
|
|
516
|
+
* `createProviderState` 会把该 provider 的有效下限压到 `min(floor, 静态现值,
|
|
517
|
+
* 该 ceiling)`,因此 `floor > ceiling` 的覆盖仍自洽(该 provider 固定在它的
|
|
518
|
+
* ceiling 上)。
|
|
519
|
+
*/
|
|
520
|
+
export function parseCeilingByProvider(
|
|
521
|
+
raw: unknown,
|
|
522
|
+
floor: number,
|
|
523
|
+
warn: (msg: string) => void,
|
|
524
|
+
): Record<string, number> {
|
|
525
|
+
if (raw === undefined || raw === null) return {};
|
|
526
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
527
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider: expected object mapping provider → ceiling, got ${JSON.stringify(raw)}`);
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
const out: Record<string, number> = {};
|
|
531
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
532
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
533
|
+
if (!canonical) {
|
|
534
|
+
warn('[omp-opsx-addon] concurrency_ceiling_by_provider: skipping empty provider id');
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
|
|
538
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: expected positive integer, got ${JSON.stringify(value)}`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (value < floor) {
|
|
542
|
+
warn(
|
|
543
|
+
`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: ${value} < concurrency_floor ${floor}; keeping the override(有效下限 = min(${value}, provider 静态现值))`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
if (out[canonical] !== undefined && out[canonical] !== value) {
|
|
547
|
+
warn(`[omp-opsx-addon] concurrency_ceiling_by_provider.${id}: duplicate canonical id "${canonical}" (${out[canonical]} → ${value}); later key wins`);
|
|
548
|
+
}
|
|
549
|
+
out[canonical] = value;
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** 解析 `provider_auto`:键规范化(大小写 / 别名同源),键值均须字符串;非法条目跳过并 warn。 */
|
|
555
|
+
export function parseProviderAuto(raw: unknown, warn: (msg: string) => void): Record<string, string> {
|
|
556
|
+
if (raw === undefined || raw === null) return {};
|
|
557
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
558
|
+
warn(`[omp-opsx-addon] provider_auto: expected object mapping provider → auto model id, got ${JSON.stringify(raw)}`);
|
|
559
|
+
return {};
|
|
560
|
+
}
|
|
561
|
+
const out: Record<string, string> = {};
|
|
562
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
563
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
564
|
+
if (!canonical) {
|
|
565
|
+
warn('[omp-opsx-addon] provider_auto: skipping empty provider id');
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
569
|
+
warn(`[omp-opsx-addon] provider_auto.${id}: expected model id string, got ${JSON.stringify(value)}`);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
out[canonical] = value.trim();
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** 解析 `provider_auto_tiers`:键规范化,值经 TIER_NAMES 校验;非法值 warn 并丢弃(消费侧缺省 mid)。 */
|
|
578
|
+
export function parseProviderAutoTiers(raw: unknown, warn: (msg: string) => void): Record<string, TierName> {
|
|
579
|
+
if (raw === undefined || raw === null) return {};
|
|
580
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
581
|
+
warn(`[omp-opsx-addon] provider_auto_tiers: expected object mapping provider → tier name, got ${JSON.stringify(raw)}`);
|
|
582
|
+
return {};
|
|
583
|
+
}
|
|
584
|
+
const out: Record<string, TierName> = {};
|
|
585
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
586
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
587
|
+
if (!canonical) {
|
|
588
|
+
warn('[omp-opsx-addon] provider_auto_tiers: skipping empty provider id');
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (typeof value !== 'string' || !(TIER_NAMES as readonly string[]).includes(value)) {
|
|
592
|
+
warn(`[omp-opsx-addon] provider_auto_tiers.${id}: expected one of ${TIER_NAMES.join('|')}, got ${JSON.stringify(value)}`);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
out[canonical] = value as TierName;
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** 解析 `provider_models`:键规范化,值须为字符串数组(模式);非数组整键 warn 丢弃,非字符串条目跳过并 warn。 */
|
|
601
|
+
export function parseProviderModels(raw: unknown, warn: (msg: string) => void): Record<string, string[]> {
|
|
602
|
+
if (raw === undefined || raw === null) return {};
|
|
603
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
604
|
+
warn(`[omp-opsx-addon] provider_models: expected object mapping provider → pattern array, got ${JSON.stringify(raw)}`);
|
|
605
|
+
return {};
|
|
606
|
+
}
|
|
607
|
+
const out: Record<string, string[]> = {};
|
|
608
|
+
for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
609
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
610
|
+
if (!canonical) {
|
|
611
|
+
warn('[omp-opsx-addon] provider_models: skipping empty provider id');
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (!Array.isArray(value)) {
|
|
615
|
+
warn(`[omp-opsx-addon] provider_models.${id}: expected array of patterns, got ${JSON.stringify(value)}`);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const patterns: string[] = [];
|
|
619
|
+
for (const entry of value) {
|
|
620
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
621
|
+
warn(`[omp-opsx-addon] provider_models.${id}: skipping non-string/empty pattern ${JSON.stringify(entry)}`);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
patterns.push(entry.trim());
|
|
625
|
+
}
|
|
626
|
+
if (patterns.length > 0) out[canonical] = patterns;
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** speed_aware 三键缺省(enabled 缺省开启:T2 类别硬分区 + 缺数据零漂移已消解开启风险)。 */
|
|
632
|
+
export function defaultSpeedAware(): SpeedAwareConfig {
|
|
633
|
+
return { enabled: true, mode: 'tie-break', minSamples: DEFAULT_MIN_SAMPLES };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** 解析 `speed_aware`:非对象整键 warn 回落缺省;mode 非法枚举 / min_samples 非正数 / enabled 非布尔各键 warn 回落各自缺省(parseProviderAutoTiers warn 矩阵同款)。 */
|
|
637
|
+
export function parseSpeedAware(raw: unknown, warn: (msg: string) => void): SpeedAwareConfig {
|
|
638
|
+
if (raw === undefined || raw === null) return defaultSpeedAware();
|
|
639
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
640
|
+
warn(`[omp-opsx-addon] speed_aware: expected object mapping { enabled, mode, min_samples }, got ${JSON.stringify(raw)}`);
|
|
641
|
+
return defaultSpeedAware();
|
|
642
|
+
}
|
|
643
|
+
const obj = raw as { enabled?: unknown; mode?: unknown; min_samples?: unknown };
|
|
644
|
+
let enabled = true;
|
|
645
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
646
|
+
if (typeof obj.enabled === 'boolean') {
|
|
647
|
+
enabled = obj.enabled;
|
|
648
|
+
} else {
|
|
649
|
+
warn(`[omp-opsx-addon] speed_aware.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
let mode: SpeedMode = 'tie-break';
|
|
653
|
+
if (obj.mode !== undefined && obj.mode !== null) {
|
|
654
|
+
if (typeof obj.mode === 'string' && (SPEED_MODES as readonly string[]).includes(obj.mode)) {
|
|
655
|
+
mode = obj.mode as SpeedMode;
|
|
656
|
+
} else {
|
|
657
|
+
warn(`[omp-opsx-addon] speed_aware.mode: expected one of ${SPEED_MODES.join('|')}, got ${JSON.stringify(obj.mode)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
let minSamples = DEFAULT_MIN_SAMPLES;
|
|
661
|
+
if (obj.min_samples !== undefined && obj.min_samples !== null) {
|
|
662
|
+
if (typeof obj.min_samples === 'number' && Number.isFinite(obj.min_samples) && obj.min_samples > 0) {
|
|
663
|
+
minSamples = obj.min_samples;
|
|
664
|
+
} else {
|
|
665
|
+
warn(`[omp-opsx-addon] speed_aware.min_samples: expected positive number, got ${JSON.stringify(obj.min_samples)}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { enabled, mode, minSamples };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** W4 难度路由缺省(enabled 缺省 false:结构改造 opt-in,与 speed_aware 缺省 true 的差异见 design D7)。 */
|
|
672
|
+
export function defaultDifficultyRouting(): RoutingConfig {
|
|
673
|
+
return { enabled: false };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** 解析 `difficulty_routing`:非对象整键 warn 回落缺省;enabled 非布尔逐键 warn 回落(parseSpeedAware 矩阵同款)。 */
|
|
677
|
+
export function parseDifficultyRouting(raw: unknown, warn: (msg: string) => void): RoutingConfig {
|
|
678
|
+
if (raw === undefined || raw === null) return defaultDifficultyRouting();
|
|
679
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
680
|
+
warn(`[omp-opsx-addon] difficulty_routing: expected object mapping { enabled }, got ${JSON.stringify(raw)}`);
|
|
681
|
+
return defaultDifficultyRouting();
|
|
682
|
+
}
|
|
683
|
+
const obj = raw as { enabled?: unknown };
|
|
684
|
+
let enabled = false;
|
|
685
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
686
|
+
if (typeof obj.enabled === 'boolean') {
|
|
687
|
+
enabled = obj.enabled;
|
|
688
|
+
} else {
|
|
689
|
+
warn(`[omp-opsx-addon] difficulty_routing.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return { enabled };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** W4 stall 升档缺省(window=6 / threshold=3,LiteLLM 同参)。 */
|
|
696
|
+
export function defaultStallEscalation(): StallEscalationConfig {
|
|
697
|
+
return { enabled: false, window: DEFAULT_WINDOW, threshold: DEFAULT_THRESHOLD };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** 解析 `stall_escalation`:非对象整键回落;enabled 非布尔、window/threshold 非正整数各键 warn 回落各自缺省。 */
|
|
701
|
+
export function parseStallEscalation(raw: unknown, warn: (msg: string) => void): StallEscalationConfig {
|
|
702
|
+
if (raw === undefined || raw === null) return defaultStallEscalation();
|
|
703
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
704
|
+
warn(`[omp-opsx-addon] stall_escalation: expected object mapping { enabled, window, threshold }, got ${JSON.stringify(raw)}`);
|
|
705
|
+
return defaultStallEscalation();
|
|
706
|
+
}
|
|
707
|
+
const obj = raw as { enabled?: unknown; window?: unknown; threshold?: unknown };
|
|
708
|
+
let enabled = false;
|
|
709
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
710
|
+
if (typeof obj.enabled === 'boolean') {
|
|
711
|
+
enabled = obj.enabled;
|
|
712
|
+
} else {
|
|
713
|
+
warn(`[omp-opsx-addon] stall_escalation.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
let window = DEFAULT_WINDOW;
|
|
717
|
+
if (obj.window !== undefined && obj.window !== null) {
|
|
718
|
+
if (typeof obj.window === 'number' && Number.isInteger(obj.window) && obj.window > 0) {
|
|
719
|
+
window = obj.window;
|
|
720
|
+
} else {
|
|
721
|
+
warn(`[omp-opsx-addon] stall_escalation.window: expected positive integer, got ${JSON.stringify(obj.window)}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
let threshold = DEFAULT_THRESHOLD;
|
|
725
|
+
if (obj.threshold !== undefined && obj.threshold !== null) {
|
|
726
|
+
if (typeof obj.threshold === 'number' && Number.isInteger(obj.threshold) && obj.threshold > 0) {
|
|
727
|
+
threshold = obj.threshold;
|
|
728
|
+
} else {
|
|
729
|
+
warn(`[omp-opsx-addon] stall_escalation.threshold: expected positive integer, got ${JSON.stringify(obj.threshold)}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return { enabled, window, threshold };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** W4 连续性守门缺省(weight 缺省 0.6:越高越保守/粘性越强)。 */
|
|
736
|
+
export function defaultContinuityGuard(): ContinuityGuardConfig {
|
|
737
|
+
return { enabled: false, weight: DEFAULT_WEIGHT };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** 解析 `continuity_guard`:非对象整键回落;enabled 非布尔、weight 越出 (0,1] 开闭区间各键 warn 回落各自缺省。 */
|
|
741
|
+
export function parseContinuityGuard(raw: unknown, warn: (msg: string) => void): ContinuityGuardConfig {
|
|
742
|
+
if (raw === undefined || raw === null) return defaultContinuityGuard();
|
|
743
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
744
|
+
warn(`[omp-opsx-addon] continuity_guard: expected object mapping { enabled, weight }, got ${JSON.stringify(raw)}`);
|
|
745
|
+
return defaultContinuityGuard();
|
|
746
|
+
}
|
|
747
|
+
const obj = raw as { enabled?: unknown; weight?: unknown };
|
|
748
|
+
let enabled = false;
|
|
749
|
+
if (obj.enabled !== undefined && obj.enabled !== null) {
|
|
750
|
+
if (typeof obj.enabled === 'boolean') {
|
|
751
|
+
enabled = obj.enabled;
|
|
752
|
+
} else {
|
|
753
|
+
warn(`[omp-opsx-addon] continuity_guard.enabled: expected boolean, got ${JSON.stringify(obj.enabled)}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
let weight = DEFAULT_WEIGHT;
|
|
757
|
+
if (obj.weight !== undefined && obj.weight !== null) {
|
|
758
|
+
if (typeof obj.weight === 'number' && Number.isFinite(obj.weight) && obj.weight > 0 && obj.weight <= 1) {
|
|
759
|
+
weight = obj.weight;
|
|
760
|
+
} else {
|
|
761
|
+
warn(`[omp-opsx-addon] continuity_guard.weight: expected number in (0, 1], got ${JSON.stringify(obj.weight)}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { enabled, weight };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** 解析 `quality_preference`:非法枚举 warn 回落 balanced(独立于三开关的顶层旋钮)。 */
|
|
768
|
+
export function parseQualityPreference(raw: unknown, warn: (msg: string) => void): QualityPreference {
|
|
769
|
+
if (raw === undefined || raw === null) return 'balanced';
|
|
770
|
+
if (typeof raw === 'string' && (QUALITY_PREFERENCES as readonly string[]).includes(raw)) {
|
|
771
|
+
return raw as QualityPreference;
|
|
772
|
+
}
|
|
773
|
+
warn(`[omp-opsx-addon] quality_preference: expected one of ${QUALITY_PREFERENCES.join('|')}, got ${JSON.stringify(raw)}`);
|
|
774
|
+
return 'balanced';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* 受管 provider 集合(spec: 受管集合 = `concurrency_providers` ∪
|
|
779
|
+
* `providers.maxInFlightRequests` 的键)。纯函数:`providers.maxInFlightRequests`
|
|
780
|
+
* 的读取由装配层负责(config root 的 `config.yml`)。
|
|
781
|
+
*
|
|
782
|
+
* 两侧 id 均经 `canonicalizeProvider` 规范化(大小写与 `ArkCodingPlan` →
|
|
783
|
+
* `ark-coding-plan` 别名收敛为同一列),去重并返回稳定顺序 —— 先
|
|
784
|
+
* `concurrency_providers` 的声明序,再 limits 侧新增键序。
|
|
785
|
+
*/
|
|
786
|
+
export function resolveManagedProviders(
|
|
787
|
+
concurrencyProviders: readonly string[] | undefined,
|
|
788
|
+
providerLimitIds: readonly string[] | Record<string, unknown> | undefined,
|
|
789
|
+
): string[] {
|
|
790
|
+
const limitIds = Array.isArray(providerLimitIds)
|
|
791
|
+
? providerLimitIds
|
|
792
|
+
: providerLimitIds
|
|
793
|
+
? Object.keys(providerLimitIds)
|
|
794
|
+
: [];
|
|
795
|
+
const out: string[] = [];
|
|
796
|
+
const seen = new Set<string>();
|
|
797
|
+
for (const id of [...(concurrencyProviders ?? []), ...limitIds]) {
|
|
798
|
+
if (typeof id !== 'string') continue;
|
|
799
|
+
const canonical = canonicalizeProvider(id.trim());
|
|
800
|
+
if (!canonical || seen.has(canonical)) continue;
|
|
801
|
+
seen.add(canonical);
|
|
802
|
+
out.push(canonical);
|
|
803
|
+
}
|
|
804
|
+
return out;
|
|
805
|
+
}
|
|
806
|
+
|
|
309
807
|
/** Parse the full raw opsx config. */
|
|
310
808
|
export function parseOpsxConfig(
|
|
311
809
|
raw: unknown,
|
|
@@ -318,16 +816,21 @@ export function parseOpsxConfig(
|
|
|
318
816
|
warn(m);
|
|
319
817
|
};
|
|
320
818
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
819
|
+
// `agents` mapping (canonical form): the key set is exactly the four
|
|
820
|
+
// managed agent names, so unknown subkeys are reported and dropped
|
|
821
|
+
// instead of vanishing silently.
|
|
822
|
+
const agentsMap = obj.agents as Record<string, unknown> | undefined;
|
|
823
|
+
if (agentsMap && typeof agentsMap === 'object') {
|
|
824
|
+
for (const key of Object.keys(agentsMap)) {
|
|
825
|
+
if (!(OPSX_AGENTS as readonly string[]).includes(key)) {
|
|
826
|
+
localWarn(`[omp-opsx-addon] agents: unknown agent "${key}" (valid: ${OPSX_AGENTS.join(', ')}); entry ignored`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
325
829
|
}
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
roles[role] = parseRoleValue(raw as string | undefined, localWarn);
|
|
830
|
+
const agents: Record<OpsxAgent, AgentValue> = {} as Record<OpsxAgent, AgentValue>;
|
|
831
|
+
for (const role of OPSX_AGENTS) {
|
|
832
|
+
const raw = agentsMap?.[role];
|
|
833
|
+
agents[role] = parseAgentValue(raw as string | undefined, localWarn);
|
|
331
834
|
}
|
|
332
835
|
let redisHost = '127.0.0.1';
|
|
333
836
|
if (obj.redis_host !== undefined) {
|
|
@@ -338,13 +841,72 @@ export function parseOpsxConfig(
|
|
|
338
841
|
}
|
|
339
842
|
}
|
|
340
843
|
|
|
844
|
+
// 并发控制:floor / ceiling 先各自回落,再校验区间(ceiling < floor)。
|
|
845
|
+
// floor 未显式配置时它只是默认值,MUST NOT 反过来抬高用户显式设定的 ceiling
|
|
846
|
+
// ——那等于放开了用户明确设下的上界。这种情况下把 floor 压到 ceiling(区间
|
|
847
|
+
// 退化为单点),保持上界意图;两端都显式给出矛盾值时仍按 spec 回落默认。
|
|
848
|
+
const floorConfigured = typeof obj.concurrency_floor === 'number'
|
|
849
|
+
&& Number.isFinite(obj.concurrency_floor)
|
|
850
|
+
&& Number.isInteger(obj.concurrency_floor)
|
|
851
|
+
&& obj.concurrency_floor > 0;
|
|
852
|
+
let concurrencyFloor = parsePositiveInt(obj.concurrency_floor, 'concurrency_floor', CONCURRENCY_DEFAULTS.floor, localWarn);
|
|
853
|
+
let concurrencyCeiling = parsePositiveInt(obj.concurrency_ceiling, 'concurrency_ceiling', CONCURRENCY_DEFAULTS.ceiling, localWarn);
|
|
854
|
+
if (concurrencyCeiling < concurrencyFloor) {
|
|
855
|
+
if (!floorConfigured) {
|
|
856
|
+
localWarn(
|
|
857
|
+
`[omp-opsx-addon] concurrency_ceiling: ${concurrencyCeiling} < default concurrency_floor ${concurrencyFloor}; using the ceiling as the floor`,
|
|
858
|
+
);
|
|
859
|
+
concurrencyFloor = concurrencyCeiling;
|
|
860
|
+
} else {
|
|
861
|
+
localWarn(
|
|
862
|
+
`[omp-opsx-addon] concurrency_ceiling: ${concurrencyCeiling} < concurrency_floor ${concurrencyFloor}; falling back to ${CONCURRENCY_DEFAULTS.ceiling}`,
|
|
863
|
+
);
|
|
864
|
+
concurrencyCeiling = CONCURRENCY_DEFAULTS.ceiling;
|
|
865
|
+
if (concurrencyCeiling < concurrencyFloor) {
|
|
866
|
+
localWarn(
|
|
867
|
+
`[omp-opsx-addon] concurrency_floor: ${concurrencyFloor} > default ceiling ${CONCURRENCY_DEFAULTS.ceiling}; falling back to ${CONCURRENCY_DEFAULTS.floor}`,
|
|
868
|
+
);
|
|
869
|
+
concurrencyFloor = CONCURRENCY_DEFAULTS.floor;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// 并发控制:`concurrency_up_ms` 是「干净窗口」的长度,`concurrency_eval_interval_ms`
|
|
875
|
+
// 是评估节奏。`evaluateWindow` 每次评估最多记 1 个干净窗口并丢弃余数
|
|
876
|
+
// (`cleanWindowMs = 0`),所以 up_ms ≤ eval_interval 时二者的 limit 轨迹
|
|
877
|
+
// 完全一致;把 up_ms 归一为 eval_interval 只是让配置反映真实生效值,
|
|
878
|
+
// 不改变任何行为(见 concurrency-tuner.ts 的 `max(up_ms, eval_interval_ms)`)。
|
|
879
|
+
let concurrencyUpMs = parsePositiveInt(obj.concurrency_up_ms, 'concurrency_up_ms', CONCURRENCY_DEFAULTS.up_ms, localWarn);
|
|
880
|
+
const concurrencyEvalIntervalMs = parsePositiveInt(obj.concurrency_eval_interval_ms, 'concurrency_eval_interval_ms', CONCURRENCY_DEFAULTS.eval_interval_ms, localWarn);
|
|
881
|
+
if (concurrencyUpMs < concurrencyEvalIntervalMs) {
|
|
882
|
+
localWarn(
|
|
883
|
+
`[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))`,
|
|
884
|
+
);
|
|
885
|
+
concurrencyUpMs = concurrencyEvalIntervalMs;
|
|
886
|
+
}
|
|
887
|
+
|
|
341
888
|
return {
|
|
342
|
-
|
|
889
|
+
agents,
|
|
343
890
|
modelAllowlist: parseModelAllowlist(obj.model_allowlist, localWarn),
|
|
344
891
|
tierOverrides: parseTierOverrides(obj.tiers, localWarn),
|
|
345
|
-
|
|
892
|
+
agentTiers: parseAgentTiers(
|
|
893
|
+
obj.agent_tiers,
|
|
894
|
+
'agent_tiers',
|
|
895
|
+
localWarn,
|
|
896
|
+
),
|
|
346
897
|
ompRoleTiers: parseOmpRoleTiers(obj.model_role_tiers, localWarn),
|
|
347
|
-
planProviders:
|
|
898
|
+
planProviders: parsePlanProviderList(obj.plan_providers, 'plan_providers', localWarn),
|
|
899
|
+
planProvidersRemove: parsePlanProviderList(obj.plan_providers_remove, 'plan_providers_remove', localWarn),
|
|
900
|
+
paygLastResort: parseProbeBool(obj.payg_last_resort, 'payg_last_resort', true, localWarn),
|
|
901
|
+
china: parseProbeBool(obj.china, 'china', false, localWarn),
|
|
902
|
+
providerAuto: parseProviderAuto(obj.provider_auto, localWarn),
|
|
903
|
+
providerAutoTiers: parseProviderAutoTiers(obj.provider_auto_tiers, localWarn),
|
|
904
|
+
providerModels: parseProviderModels(obj.provider_models, localWarn),
|
|
905
|
+
speedAware: parseSpeedAware(obj.speed_aware, localWarn),
|
|
906
|
+
difficultyRouting: parseDifficultyRouting(obj.difficulty_routing, localWarn),
|
|
907
|
+
stallEscalation: parseStallEscalation(obj.stall_escalation, localWarn),
|
|
908
|
+
continuityGuard: parseContinuityGuard(obj.continuity_guard, localWarn),
|
|
909
|
+
qualityPreference: parseQualityPreference(obj.quality_preference, localWarn),
|
|
348
910
|
warnings,
|
|
349
911
|
probe_enabled: parseProbeBool(obj.probe_enabled, 'probe_enabled', true, localWarn),
|
|
350
912
|
probe_timeout_ms: parsePositiveInt(obj.probe_timeout_ms, 'probe_timeout_ms', 3000, localWarn),
|
|
@@ -361,6 +923,23 @@ export function parseOpsxConfig(
|
|
|
361
923
|
redis_probe_interval_ms: parsePositiveInt(obj.redis_probe_interval_ms, 'redis_probe_interval_ms', 30_000, localWarn),
|
|
362
924
|
redis_unhealthy_retry_ms: parsePositiveInt(obj.redis_unhealthy_retry_ms, 'redis_unhealthy_retry_ms', 5_000, localWarn),
|
|
363
925
|
redis_ttl_ms: parsePositiveInt(obj.redis_ttl_ms, 'redis_ttl_ms', 180_000, localWarn),
|
|
926
|
+
autoselectOnStart: parseProbeBool(obj.autoselect_on_start, 'autoselect_on_start', true, localWarn),
|
|
927
|
+
concurrency_enabled: parseProbeBool(obj.concurrency_enabled, 'concurrency_enabled', CONCURRENCY_DEFAULTS.enabled, localWarn),
|
|
928
|
+
concurrency_floor: concurrencyFloor,
|
|
929
|
+
concurrency_ceiling: concurrencyCeiling,
|
|
930
|
+
concurrency_start: parsePositiveInt(obj.concurrency_start, 'concurrency_start', CONCURRENCY_DEFAULTS.start, localWarn),
|
|
931
|
+
concurrency_up_ms: concurrencyUpMs,
|
|
932
|
+
concurrency_down_ms: parsePositiveInt(obj.concurrency_down_ms, 'concurrency_down_ms', CONCURRENCY_DEFAULTS.down_ms, localWarn),
|
|
933
|
+
concurrency_latency_k: parsePositiveNumber(obj.concurrency_latency_k, 'concurrency_latency_k', CONCURRENCY_DEFAULTS.latency_k, localWarn),
|
|
934
|
+
concurrency_min_samples: parsePositiveInt(obj.concurrency_min_samples, 'concurrency_min_samples', CONCURRENCY_DEFAULTS.min_samples, localWarn),
|
|
935
|
+
concurrency_queue_wait_high_ms: parsePositiveInt(obj.concurrency_queue_wait_high_ms, 'concurrency_queue_wait_high_ms', CONCURRENCY_DEFAULTS.queue_wait_high_ms, localWarn),
|
|
936
|
+
concurrency_write_min_interval_ms: parsePositiveInt(obj.concurrency_write_min_interval_ms, 'concurrency_write_min_interval_ms', CONCURRENCY_DEFAULTS.write_min_interval_ms, localWarn),
|
|
937
|
+
concurrency_lease_ttl_ms: parsePositiveInt(obj.concurrency_lease_ttl_ms, 'concurrency_lease_ttl_ms', CONCURRENCY_DEFAULTS.lease_ttl_ms, localWarn),
|
|
938
|
+
concurrency_decay_after_ms: parsePositiveInt(obj.concurrency_decay_after_ms, 'concurrency_decay_after_ms', CONCURRENCY_DEFAULTS.decay_after_ms, localWarn),
|
|
939
|
+
concurrency_eval_interval_ms: concurrencyEvalIntervalMs,
|
|
940
|
+
concurrency_queue_wait_window_ms: parsePositiveInt(obj.concurrency_queue_wait_window_ms, 'concurrency_queue_wait_window_ms', CONCURRENCY_DEFAULTS.queue_wait_window_ms, localWarn),
|
|
941
|
+
concurrency_providers: parseConcurrencyProviders(obj.concurrency_providers, localWarn),
|
|
942
|
+
concurrency_ceiling_by_provider: parseCeilingByProvider(obj.concurrency_ceiling_by_provider, concurrencyFloor, localWarn),
|
|
364
943
|
};
|
|
365
944
|
}
|
|
366
945
|
|
|
@@ -381,28 +960,30 @@ function readOpsxYamlFile(path: string): OpsxYamlShape {
|
|
|
381
960
|
return {};
|
|
382
961
|
}
|
|
383
962
|
}
|
|
384
|
-
function pickRoleKeys(raw: OpsxYamlShape): Partial<Record<OpsxRole, string>> {
|
|
385
|
-
const out: Partial<Record<OpsxRole, string>> = {};
|
|
386
|
-
for (const role of OPSX_ROLES) {
|
|
387
|
-
const v = raw[role];
|
|
388
|
-
if (v === undefined || v === null) continue;
|
|
389
|
-
out[role] = String(v);
|
|
390
|
-
}
|
|
391
|
-
return out;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
963
|
function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
|
|
395
964
|
return {
|
|
965
|
+
agents: raw.agents,
|
|
966
|
+
agent_tiers: raw.agent_tiers,
|
|
396
967
|
model_allowlist: raw.model_allowlist,
|
|
397
968
|
tiers: raw.tiers,
|
|
398
|
-
role_tiers: raw.role_tiers,
|
|
399
969
|
model_role_tiers: raw.model_role_tiers,
|
|
400
970
|
plan_providers: raw.plan_providers,
|
|
971
|
+
plan_providers_remove: raw.plan_providers_remove,
|
|
972
|
+
payg_last_resort: raw.payg_last_resort,
|
|
973
|
+
china: raw.china,
|
|
401
974
|
probe_enabled: raw.probe_enabled,
|
|
402
975
|
probe_timeout_ms: raw.probe_timeout_ms,
|
|
403
976
|
probe_http_enabled: raw.probe_http_enabled,
|
|
404
977
|
probe_ttl_ms: raw.probe_ttl_ms,
|
|
405
978
|
excluded_providers: raw.excluded_providers,
|
|
979
|
+
provider_auto: raw.provider_auto,
|
|
980
|
+
provider_auto_tiers: raw.provider_auto_tiers,
|
|
981
|
+
provider_models: raw.provider_models,
|
|
982
|
+
speed_aware: raw.speed_aware,
|
|
983
|
+
difficulty_routing: raw.difficulty_routing,
|
|
984
|
+
stall_escalation: raw.stall_escalation,
|
|
985
|
+
continuity_guard: raw.continuity_guard,
|
|
986
|
+
quality_preference: raw.quality_preference,
|
|
406
987
|
status_probe_enabled: raw.status_probe_enabled,
|
|
407
988
|
redis_enabled: raw.redis_enabled,
|
|
408
989
|
redis_host: raw.redis_host,
|
|
@@ -413,6 +994,23 @@ function pickTopLevelKeys(raw: OpsxYamlShape): OpsxYamlShape {
|
|
|
413
994
|
redis_probe_interval_ms: raw.redis_probe_interval_ms,
|
|
414
995
|
redis_unhealthy_retry_ms: raw.redis_unhealthy_retry_ms,
|
|
415
996
|
redis_ttl_ms: raw.redis_ttl_ms,
|
|
997
|
+
autoselect_on_start: raw.autoselect_on_start,
|
|
998
|
+
concurrency_enabled: raw.concurrency_enabled,
|
|
999
|
+
concurrency_floor: raw.concurrency_floor,
|
|
1000
|
+
concurrency_ceiling: raw.concurrency_ceiling,
|
|
1001
|
+
concurrency_start: raw.concurrency_start,
|
|
1002
|
+
concurrency_up_ms: raw.concurrency_up_ms,
|
|
1003
|
+
concurrency_down_ms: raw.concurrency_down_ms,
|
|
1004
|
+
concurrency_latency_k: raw.concurrency_latency_k,
|
|
1005
|
+
concurrency_min_samples: raw.concurrency_min_samples,
|
|
1006
|
+
concurrency_queue_wait_high_ms: raw.concurrency_queue_wait_high_ms,
|
|
1007
|
+
concurrency_write_min_interval_ms: raw.concurrency_write_min_interval_ms,
|
|
1008
|
+
concurrency_lease_ttl_ms: raw.concurrency_lease_ttl_ms,
|
|
1009
|
+
concurrency_decay_after_ms: raw.concurrency_decay_after_ms,
|
|
1010
|
+
concurrency_eval_interval_ms: raw.concurrency_eval_interval_ms,
|
|
1011
|
+
concurrency_queue_wait_window_ms: raw.concurrency_queue_wait_window_ms,
|
|
1012
|
+
concurrency_providers: raw.concurrency_providers,
|
|
1013
|
+
concurrency_ceiling_by_provider: raw.concurrency_ceiling_by_provider,
|
|
416
1014
|
};
|
|
417
1015
|
}
|
|
418
1016
|
|
|
@@ -441,11 +1039,42 @@ function warnLegacySettingsJson(
|
|
|
441
1039
|
}
|
|
442
1040
|
}
|
|
443
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Per-layer migration diagnostics: one aggregated warning when a layer file
|
|
1044
|
+
* still uses a removed legacy key (all offending names in a single line),
|
|
1045
|
+
* plus one warning per unknown `agents` subkey (the mapping's key set is
|
|
1046
|
+
* exactly the four managed agents, so a typo would otherwise vanish silently).
|
|
1047
|
+
*/
|
|
1048
|
+
function warnAgentRenameDiagnostics(
|
|
1049
|
+
path: string,
|
|
1050
|
+
raw: OpsxYamlShape,
|
|
1051
|
+
warn: (msg: string) => void,
|
|
1052
|
+
): void {
|
|
1053
|
+
const legacy = LEGACY_AGENT_KEYS.filter((key) => {
|
|
1054
|
+
const v = (raw as Record<string, unknown>)[key];
|
|
1055
|
+
return v !== undefined && v !== null;
|
|
1056
|
+
});
|
|
1057
|
+
if (legacy.length > 0) {
|
|
1058
|
+
warn(
|
|
1059
|
+
`[omp-opsx-addon] ${path}: legacy key(s) ${legacy.map((k) => `'${k}'`).join(', ')} are no longer read (ignored); ` +
|
|
1060
|
+
`migrate top-level role keys into the 'agents:' mapping / rename 'role_tiers' to 'agent_tiers'.`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const agents = raw.agents;
|
|
1064
|
+
if (agents && typeof agents === 'object') {
|
|
1065
|
+
for (const key of Object.keys(agents)) {
|
|
1066
|
+
if (!(OPSX_AGENTS as readonly string[]).includes(key)) {
|
|
1067
|
+
warn(`[omp-opsx-addon] ${path}: agents.${key}: unknown agent (valid: ${OPSX_AGENTS.join(', ')}); entry ignored`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
444
1073
|
/**
|
|
445
1074
|
* Read opsx config from two-layer opsx.yml:
|
|
446
1075
|
* - global: `<homeDir>/.omp/agent/opsx.yml`
|
|
447
|
-
* - project: `<cwd>/.omp/opsx.yml` (per-key override on
|
|
448
|
-
* `model_allowlist` / `tiers` / `
|
|
1076
|
+
* - project: `<cwd>/.omp/opsx.yml` (per-key override on `agents`; full
|
|
1077
|
+
* overlay on `model_allowlist` / `tiers` / `agent_tiers`)
|
|
449
1078
|
*
|
|
450
1079
|
* `homeDir` is overridable for tests; defaults to `os.homedir()`.
|
|
451
1080
|
*/
|
|
@@ -471,30 +1100,51 @@ export function readOpsxSettingsFromPaths(
|
|
|
471
1100
|
const homeFromGlobal = globalPath.replace(/\.omp\/agent\/opsx\.yml$/, '');
|
|
472
1101
|
const projectFromPath = projectPath.replace(/\.omp\/opsx\.yml$/, '');
|
|
473
1102
|
warnLegacySettingsJson(projectFromPath, homeFromGlobal, warn);
|
|
1103
|
+
warnAgentRenameDiagnostics(globalPath, globalYaml, warn);
|
|
1104
|
+
warnAgentRenameDiagnostics(projectPath, projectYaml, warn);
|
|
474
1105
|
|
|
475
|
-
|
|
476
|
-
const
|
|
1106
|
+
// Top-level: project fully replaces global when present.
|
|
1107
|
+
const projectTop = pickTopLevelKeys(projectYaml);
|
|
1108
|
+
const globalTop = pickTopLevelKeys(globalYaml);
|
|
477
1109
|
|
|
478
|
-
// Per-
|
|
1110
|
+
// Per-agent: project overrides global, fall back to DEFAULT_OPSX_AGENTS.
|
|
479
1111
|
const merged: OpsxYamlShape = { ...globalYaml };
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1112
|
+
const foldedAgents: Partial<Record<OpsxAgent, string>> = {};
|
|
1113
|
+
for (const role of OPSX_AGENTS) {
|
|
1114
|
+
foldedAgents[role] = projectTop.agents?.[role] ?? globalTop.agents?.[role] ?? DEFAULT_OPSX_AGENTS[role];
|
|
483
1115
|
}
|
|
1116
|
+
merged.agents = foldedAgents;
|
|
484
1117
|
|
|
485
|
-
// Top-level: project fully replaces global when present.
|
|
486
|
-
const projectTop = pickTopLevelKeys(projectYaml);
|
|
487
|
-
const globalTop = pickTopLevelKeys(globalYaml);
|
|
488
1118
|
merged.model_allowlist = projectTop.model_allowlist ?? globalTop.model_allowlist;
|
|
489
1119
|
merged.tiers = projectTop.tiers ?? globalTop.tiers;
|
|
490
|
-
|
|
1120
|
+
// Tier map: whole-key overlay (project map replaces global's).
|
|
1121
|
+
merged.agent_tiers = projectTop.agent_tiers ?? globalTop.agent_tiers;
|
|
491
1122
|
merged.model_role_tiers = projectTop.model_role_tiers ?? globalTop.model_role_tiers;
|
|
492
1123
|
merged.plan_providers = projectTop.plan_providers ?? globalTop.plan_providers;
|
|
1124
|
+
merged.plan_providers_remove = projectTop.plan_providers_remove ?? globalTop.plan_providers_remove;
|
|
1125
|
+
merged.payg_last_resort = projectTop.payg_last_resort ?? globalTop.payg_last_resort;
|
|
1126
|
+
// `??` keeps an explicit project `false` (parseProbeBool only falls back on
|
|
1127
|
+
// null/undefined AFTER the merge, so undefined reveals the other layer).
|
|
1128
|
+
merged.china = projectTop.china ?? globalTop.china;
|
|
493
1129
|
merged.probe_enabled = projectTop.probe_enabled ?? globalTop.probe_enabled;
|
|
494
1130
|
merged.probe_timeout_ms = projectTop.probe_timeout_ms ?? globalTop.probe_timeout_ms;
|
|
495
1131
|
merged.probe_http_enabled = projectTop.probe_http_enabled ?? globalTop.probe_http_enabled;
|
|
496
1132
|
merged.probe_ttl_ms = projectTop.probe_ttl_ms ?? globalTop.probe_ttl_ms;
|
|
497
1133
|
merged.excluded_providers = projectTop.excluded_providers ?? globalTop.excluded_providers;
|
|
1134
|
+
// Provider 键控 map 同为整键覆盖(先例 concurrency_ceiling_by_provider):项目层
|
|
1135
|
+
// 声明任一键即完整替换全局同名键,不做 per-key 叠加——只想加键就把全局条目抄过来。
|
|
1136
|
+
merged.provider_auto = projectTop.provider_auto ?? globalTop.provider_auto;
|
|
1137
|
+
merged.provider_auto_tiers = projectTop.provider_auto_tiers ?? globalTop.provider_auto_tiers;
|
|
1138
|
+
merged.provider_models = projectTop.provider_models ?? globalTop.provider_models;
|
|
1139
|
+
// 对象键一律整键覆盖(同上 provider_* 先例):项目层声明 speed_aware 即
|
|
1140
|
+
// 整份替换全局,不做字段级叠加。
|
|
1141
|
+
merged.speed_aware = projectTop.speed_aware ?? globalTop.speed_aware;
|
|
1142
|
+
// W4 路由四键同款整键覆盖:项目层声明任一键即完整替换全局同名键。
|
|
1143
|
+
merged.difficulty_routing = projectTop.difficulty_routing ?? globalTop.difficulty_routing;
|
|
1144
|
+
merged.stall_escalation = projectTop.stall_escalation ?? globalTop.stall_escalation;
|
|
1145
|
+
merged.continuity_guard = projectTop.continuity_guard ?? globalTop.continuity_guard;
|
|
1146
|
+
// 顶层标量键同 `??` 惯例(项目层显式值优先,undefined 露出另一层)。
|
|
1147
|
+
merged.quality_preference = projectTop.quality_preference ?? globalTop.quality_preference;
|
|
498
1148
|
merged.status_probe_enabled = projectTop.status_probe_enabled ?? globalTop.status_probe_enabled;
|
|
499
1149
|
merged.redis_enabled = projectTop.redis_enabled ?? globalTop.redis_enabled;
|
|
500
1150
|
merged.redis_host = projectTop.redis_host ?? globalTop.redis_host;
|
|
@@ -505,6 +1155,23 @@ export function readOpsxSettingsFromPaths(
|
|
|
505
1155
|
merged.redis_probe_interval_ms = projectTop.redis_probe_interval_ms ?? globalTop.redis_probe_interval_ms;
|
|
506
1156
|
merged.redis_unhealthy_retry_ms = projectTop.redis_unhealthy_retry_ms ?? globalTop.redis_unhealthy_retry_ms;
|
|
507
1157
|
merged.redis_ttl_ms = projectTop.redis_ttl_ms ?? globalTop.redis_ttl_ms;
|
|
1158
|
+
merged.autoselect_on_start = projectTop.autoselect_on_start ?? globalTop.autoselect_on_start;
|
|
1159
|
+
merged.concurrency_enabled = projectTop.concurrency_enabled ?? globalTop.concurrency_enabled;
|
|
1160
|
+
merged.concurrency_floor = projectTop.concurrency_floor ?? globalTop.concurrency_floor;
|
|
1161
|
+
merged.concurrency_ceiling = projectTop.concurrency_ceiling ?? globalTop.concurrency_ceiling;
|
|
1162
|
+
merged.concurrency_start = projectTop.concurrency_start ?? globalTop.concurrency_start;
|
|
1163
|
+
merged.concurrency_up_ms = projectTop.concurrency_up_ms ?? globalTop.concurrency_up_ms;
|
|
1164
|
+
merged.concurrency_down_ms = projectTop.concurrency_down_ms ?? globalTop.concurrency_down_ms;
|
|
1165
|
+
merged.concurrency_latency_k = projectTop.concurrency_latency_k ?? globalTop.concurrency_latency_k;
|
|
1166
|
+
merged.concurrency_min_samples = projectTop.concurrency_min_samples ?? globalTop.concurrency_min_samples;
|
|
1167
|
+
merged.concurrency_queue_wait_high_ms = projectTop.concurrency_queue_wait_high_ms ?? globalTop.concurrency_queue_wait_high_ms;
|
|
1168
|
+
merged.concurrency_write_min_interval_ms = projectTop.concurrency_write_min_interval_ms ?? globalTop.concurrency_write_min_interval_ms;
|
|
1169
|
+
merged.concurrency_lease_ttl_ms = projectTop.concurrency_lease_ttl_ms ?? globalTop.concurrency_lease_ttl_ms;
|
|
1170
|
+
merged.concurrency_decay_after_ms = projectTop.concurrency_decay_after_ms ?? globalTop.concurrency_decay_after_ms;
|
|
1171
|
+
merged.concurrency_eval_interval_ms = projectTop.concurrency_eval_interval_ms ?? globalTop.concurrency_eval_interval_ms;
|
|
1172
|
+
merged.concurrency_queue_wait_window_ms = projectTop.concurrency_queue_wait_window_ms ?? globalTop.concurrency_queue_wait_window_ms;
|
|
1173
|
+
merged.concurrency_providers = projectTop.concurrency_providers ?? globalTop.concurrency_providers;
|
|
1174
|
+
merged.concurrency_ceiling_by_provider = projectTop.concurrency_ceiling_by_provider ?? globalTop.concurrency_ceiling_by_provider;
|
|
508
1175
|
|
|
509
1176
|
return parseOpsxConfig(merged, warn);
|
|
510
1177
|
}
|