dsh-agentic-router 1.5.0 → 1.6.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 +6 -0
- package/docs/DESIGN.md +6 -0
- package/dsh/index.js +79 -26
- package/dsh/strategies.js +62 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,12 @@ DeepSeek Harness(DSH)插件:**学习型智能路由(agentic router)+
|
|
|
20
20
|
(步骤切换对下一步生效,一阶延迟)。步骤级决策落盘 `steps.jsonl`,`agentic_router_stats`
|
|
21
21
|
可见 `stepSwitches` 与 `stepLog`。
|
|
22
22
|
|
|
23
|
+
**v1.6.0 跨 provider 与修复**:`resolveTier` 支持跨 provider 解析——优先当前 provider 同档候选,
|
|
24
|
+
无候选时按 providers 顺序取其它 provider 同档模型(mid 档再回退 strong/fast),`request/header`
|
|
25
|
+
同时写入目标 `provider` 与 `model`;每个会话首回合(尚无 header/provider)延迟到首个请求拿到真实
|
|
26
|
+
provider 后再决策应用;`agentic_router_force` 钉死模型、步骤级路由不再覆盖;内置价表补齐
|
|
27
|
+
`deepseek-account`,并在加载时与旧 `policy.json` 价格合并。可用配置 `crossProvider: false` 关闭跨 provider。
|
|
28
|
+
|
|
23
29
|
## 架构
|
|
24
30
|
|
|
25
31
|
```
|
package/docs/DESIGN.md
CHANGED
|
@@ -263,3 +263,9 @@ loop 每步顺序:`inbox.claim()`(发 `agent/inbox/claimed`)→ `systemPro
|
|
|
263
263
|
- 奖励飞轮仍按回合结算(回合基线的策略选择),步骤级是确定性规则;未来可把步骤级升降档纳入学习(如按工具调用频率学阈值)
|
|
264
264
|
- 重型集合 `HEAVY_TOOLS` 是静态清单,可配置化
|
|
265
265
|
- 降级方向:重型步骤后的轻量步骤会回到基线(简单任务场景:写代码阶段 pro、前后问答 flash)
|
|
266
|
+
|
|
267
|
+
### 13.6 v1.6.0:跨 provider 解析、首回合延迟与 force 钉死
|
|
268
|
+
|
|
269
|
+
- `resolveTier` 解析顺序:同 provider 同档 → 其它 provider 同档(`crossProvider`,默认开)→ mid 回退(同 provider strong/fast → 跨 provider strong/fast);`appendRouterHeader` 同步写 `config.provider` 与 `config.model`
|
|
270
|
+
- 首回合(`currentProvider` 为空)在 inbox 只挂起,等首个 `agent/request` 拿到真实 provider 后解析并落盘,再于 `llm/stream` 延迟应用
|
|
271
|
+
- `agentic_router_force` 期间跳过步骤级升降档(force 钉死模型);force 支持 `model` 或 `provider/model`
|
package/dsh/index.js
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
import { classify, featuresOf } from './classify.js'
|
|
24
24
|
import {
|
|
25
|
-
tierOf, tierTarget, resolveTier,
|
|
25
|
+
tierOf, tierTarget, tierHasAny, resolveTier,
|
|
26
26
|
recommendRule, recommendUcb, recommendLin, recommendKnn,
|
|
27
27
|
rewardOf, applyReward, correctReward,
|
|
28
|
-
DEFAULT_PRICES, turnCostYuan, resolvePrice,
|
|
28
|
+
DEFAULT_PRICES, turnCostYuan, resolvePrice, mergePrices,
|
|
29
29
|
} from './strategies.js'
|
|
30
30
|
import { metaPick, DEFAULT_WEIGHTS } from './meta.js'
|
|
31
31
|
import { Store, defaultHome } from './store.js'
|
|
@@ -56,10 +56,15 @@ function fallbackTiers() {
|
|
|
56
56
|
|
|
57
57
|
// 当前 provider 有候选模型的档位列表(UCB/LinUCB/k-NN 只在这些档位间探索)。
|
|
58
58
|
// provider 缺失或档位表不可用时回退到全部档位(保持专家函数原有行为)。
|
|
59
|
-
function availableTiersFor(provider, modelTiers) {
|
|
59
|
+
function availableTiersFor(provider, modelTiers, crossProvider) {
|
|
60
60
|
const all = ['fast', 'mid', 'strong']
|
|
61
|
-
if (!modelTiers
|
|
62
|
-
|
|
61
|
+
if (!modelTiers) return all
|
|
62
|
+
if (provider && !crossProvider) {
|
|
63
|
+
const has = all.filter((t) => tierTarget(t, provider, modelTiers))
|
|
64
|
+
return has.length ? has : all
|
|
65
|
+
}
|
|
66
|
+
// 跨 provider(或无 provider):只看档位是否有候选(跨所有 provider),避免探索"幽灵档"
|
|
67
|
+
const has = all.filter((t) => tierHasAny(t, modelTiers))
|
|
63
68
|
return has.length ? has : all
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -69,6 +74,7 @@ function freshState() {
|
|
|
69
74
|
seq: 0,
|
|
70
75
|
decisions: [],
|
|
71
76
|
forced: null,
|
|
77
|
+
crossProvider: true,
|
|
72
78
|
pending: new Map(),
|
|
73
79
|
modelTiers: fallbackTiers(),
|
|
74
80
|
turnDecision: {},
|
|
@@ -193,7 +199,7 @@ function currentProvider(session) {
|
|
|
193
199
|
// 核心切换:向会话日志追加 request/header(reason: 'router' / 'router-step'),
|
|
194
200
|
// 下一轮 prompt 组装即读到新模型。fast/mid 档去掉 reasoningEffort,
|
|
195
201
|
// 让目标模型走自身默认 effort(避免 flash 不支持 max 而整步失败)。
|
|
196
|
-
function appendRouterHeader(session, targetModel, reason = 'router') {
|
|
202
|
+
function appendRouterHeader(session, targetModel, reason = 'router', targetProvider = null) {
|
|
197
203
|
try {
|
|
198
204
|
const last = session.requestHeader()
|
|
199
205
|
const header = {}
|
|
@@ -204,6 +210,7 @@ function appendRouterHeader(session, targetModel, reason = 'router') {
|
|
|
204
210
|
const config = {}
|
|
205
211
|
for (const k of Object.keys(prevConfig)) config[k] = prevConfig[k]
|
|
206
212
|
config.model = targetModel
|
|
213
|
+
if (targetProvider && config.provider !== targetProvider) config.provider = targetProvider
|
|
207
214
|
if (tierOf(targetModel) !== 'strong') delete config.reasoningEffort
|
|
208
215
|
header.config = config
|
|
209
216
|
session.append('request/header', { header, reason })
|
|
@@ -213,6 +220,14 @@ function appendRouterHeader(session, targetModel, reason = 'router') {
|
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
|
|
223
|
+
// 解析强制模型:支持 'model' 或 'provider/model'(跨厂商强制时需指定 provider)
|
|
224
|
+
function parseForced(forced) {
|
|
225
|
+
if (typeof forced !== 'string' || !forced) return { provider: null, model: forced || null }
|
|
226
|
+
const i = forced.indexOf('/')
|
|
227
|
+
if (i > 0) return { provider: forced.slice(0, i), model: forced.slice(i + 1) }
|
|
228
|
+
return { provider: null, model: forced }
|
|
229
|
+
}
|
|
230
|
+
|
|
216
231
|
// 步骤级路由(v1.5.0):视为"重型工作"的工具调用——上一步出现它们时,
|
|
217
232
|
// 下一步升级到 strong(pro)处理其结果/继续执行;轻量工具保持回合基线。
|
|
218
233
|
const HEAVY_TOOLS = new Set([
|
|
@@ -264,12 +279,15 @@ export function apply(ctx, config = {}) {
|
|
|
264
279
|
const store = new Store(config.home || defaultHome())
|
|
265
280
|
const settleDelayMs = typeof config.settleDelayMs === 'number' ? config.settleDelayMs : SETTLE_DELAY_MS
|
|
266
281
|
const policy = store.loadPolicy()
|
|
267
|
-
|
|
282
|
+
const overlay = (policy.prices && Object.keys(policy.prices).length) ? policy.prices : (config.prices || {})
|
|
283
|
+
let prices = mergePrices(DEFAULT_PRICES, overlay)
|
|
268
284
|
const costWeight = typeof config.costWeight === 'number' ? config.costWeight : 20
|
|
269
285
|
const costCap = typeof config.costCap === 'number' ? config.costCap : 0.5
|
|
286
|
+
const crossProvider = config.crossProvider !== false
|
|
270
287
|
|
|
271
288
|
const state = freshState()
|
|
272
289
|
state.mode = policy.mode || config.mode || 'shadow'
|
|
290
|
+
state.crossProvider = crossProvider
|
|
273
291
|
if (typeof policy.forced === 'string') state.forced = policy.forced
|
|
274
292
|
|
|
275
293
|
// 飞轮恢复:重放 rewards.jsonl 重建 UCB/LinUCB/k-NN/EXP3;
|
|
@@ -350,7 +368,7 @@ export function apply(ctx, config = {}) {
|
|
|
350
368
|
const f = featuresOf(cls, text)
|
|
351
369
|
// 只探索当前 provider 有候选模型的档位(跳过无候选的"幽灵档",
|
|
352
370
|
// 如 mid 无模型时探索 mid 只会白费并回退到最贵的 strong)
|
|
353
|
-
const available = availableTiersFor(provider, state.modelTiers)
|
|
371
|
+
const available = availableTiersFor(provider, state.modelTiers, state.crossProvider)
|
|
354
372
|
const recs = {
|
|
355
373
|
rule: recommendRule(cls),
|
|
356
374
|
ucb: recommendUcb(cls, state.ucb, available),
|
|
@@ -365,17 +383,22 @@ export function apply(ctx, config = {}) {
|
|
|
365
383
|
}
|
|
366
384
|
const meta = metaPick(recs, exp3)
|
|
367
385
|
const chosenRec = meta.byName[meta.chosen] || recs.rule
|
|
368
|
-
|
|
369
|
-
? { target:
|
|
370
|
-
: resolveTier(chosenRec.tier, provider, state.modelTiers)
|
|
386
|
+
let resolved = state.forced
|
|
387
|
+
? (() => { const pf = parseForced(state.forced); return { target: pf.model, provider: pf.provider, used: 'forced', note: 'forced' } })()
|
|
388
|
+
: resolveTier(chosenRec.tier, provider, state.modelTiers, { crossProvider: state.crossProvider })
|
|
389
|
+
// 首回合/无 provider:active 模式下等首个 request 拿到真实 provider 后再解析、应用与落盘
|
|
390
|
+
const defer = state.mode === 'active' && !provider && !state.forced
|
|
391
|
+
if (defer) resolved = { target: null, provider: null, used: 'pending', note: 'provider-unknown' }
|
|
371
392
|
const target = resolved.target
|
|
393
|
+
const targetProvider = resolved.provider || null
|
|
372
394
|
state.seq += 1
|
|
373
395
|
const td = {
|
|
374
396
|
clsType: cls.type, f, recs, meta, chosen: meta.chosen,
|
|
375
397
|
finalized: false, t0: now(), toolErrors: 0, retries: 0,
|
|
376
398
|
currentModel: current, provider, costWeight, costCap,
|
|
377
399
|
estInputTokens: Math.ceil(text.length / 1.5),
|
|
378
|
-
target, applied: false, needsApply: false, actualModel: current,
|
|
400
|
+
target, targetProvider, applied: false, needsApply: false, actualModel: current,
|
|
401
|
+
awaitProvider: defer, recordCommitted: false,
|
|
379
402
|
// 回合级基线(供步骤级路由 v1.5.0 参考:无重型工具时保持该档)
|
|
380
403
|
turnTier: target ? tierOf(target) : null, turnTarget: target,
|
|
381
404
|
}
|
|
@@ -398,6 +421,8 @@ export function apply(ctx, config = {}) {
|
|
|
398
421
|
metaPool: meta.pool,
|
|
399
422
|
resolvedTier: resolved.used + (resolved.note ? ' [' + resolved.note + ']' : ''),
|
|
400
423
|
targetModel: state.mode === 'active' ? target : null,
|
|
424
|
+
targetProvider: state.mode === 'active' ? targetProvider : null,
|
|
425
|
+
deferred: defer,
|
|
401
426
|
applied: false,
|
|
402
427
|
appliedAt: source,
|
|
403
428
|
swapped: false,
|
|
@@ -409,10 +434,11 @@ export function apply(ctx, config = {}) {
|
|
|
409
434
|
}
|
|
410
435
|
|
|
411
436
|
// 决策记录落盘(在 applyDecision 之后调用,保证 applied/swapped 已是最终值)
|
|
412
|
-
function commitRecord(record) {
|
|
437
|
+
function commitRecord(record, td) {
|
|
413
438
|
store.appendJsonl('decisions.jsonl', record)
|
|
414
439
|
state.decisions.push(record)
|
|
415
440
|
if (state.decisions.length > 200) state.decisions.shift()
|
|
441
|
+
if (td) td.recordCommitted = true
|
|
416
442
|
}
|
|
417
443
|
|
|
418
444
|
// 应用切换:active 模式且目标与当前不同 → 追加 request/header。
|
|
@@ -421,7 +447,7 @@ export function apply(ctx, config = {}) {
|
|
|
421
447
|
if (state.mode !== 'active' || !target) return false
|
|
422
448
|
const base = td.actualModel || td.currentModel
|
|
423
449
|
if (base && target === base) return false
|
|
424
|
-
const ok = session ? appendRouterHeader(session, target) : false
|
|
450
|
+
const ok = session ? appendRouterHeader(session, target, 'router', td.targetProvider || null) : false
|
|
425
451
|
td.applied = ok
|
|
426
452
|
if (record) { record.applied = ok; record.swapped = ok }
|
|
427
453
|
if (ok) {
|
|
@@ -444,7 +470,7 @@ export function apply(ctx, config = {}) {
|
|
|
444
470
|
const r = decideAndRecord(payload, text, current, provider, 'inbox')
|
|
445
471
|
if (!r) return
|
|
446
472
|
applyDecision(session, r.key, r.td, r.record, r.target)
|
|
447
|
-
commitRecord(r.record)
|
|
473
|
+
if (!r.td.awaitProvider) commitRecord(r.record, r.td)
|
|
448
474
|
log('decision #' + state.seq + ' type=' + r.td.clsType + ' c=' + r.td.f[1] +
|
|
449
475
|
' meta=' + r.td.chosen + ' tier=' + r.record.resolvedTier +
|
|
450
476
|
' applied=' + r.td.applied + ' target=' + (r.target || '-'))
|
|
@@ -470,21 +496,23 @@ export function apply(ctx, config = {}) {
|
|
|
470
496
|
const key = fullKey(payload)
|
|
471
497
|
const td = state.turnDecision[key]
|
|
472
498
|
if (!td || td.finalized) return next()
|
|
499
|
+
if (state.forced) return next() // 强制模型时跳过步骤级升降档(force 钉死模型)
|
|
473
500
|
const tools = lastToolCalls(session)
|
|
474
501
|
if (!tools.length) return next() // 上一步是纯文本(通常回合收尾),保持现状
|
|
475
502
|
const heavy = tools.some((n) => HEAVY_TOOLS.has(n))
|
|
476
503
|
const stepTier = heavy ? 'strong' : (td.turnTier || null)
|
|
477
504
|
if (!stepTier) return next()
|
|
478
505
|
const provider = td.provider || currentProvider(session) || ''
|
|
479
|
-
const resolved = resolveTier(stepTier, provider, state.modelTiers)
|
|
506
|
+
const resolved = resolveTier(stepTier, provider, state.modelTiers, { crossProvider: state.crossProvider })
|
|
480
507
|
const target = resolved.target
|
|
508
|
+
const targetProvider = resolved.provider || null
|
|
481
509
|
const cur = currentModel(session)
|
|
482
510
|
const changed = target && target !== cur
|
|
483
511
|
// 注意:pre-step 在组装之后、buildRequest(loop 追加自己的 header)之前触发。
|
|
484
512
|
// 这里直接追加会被本步 buildRequest 的 change(旧模型) 覆盖(与 v1.4.0 冷启动同款竞态)。
|
|
485
513
|
// 因此只决策并挂起,真正追加推迟到 llm/stream 收尾(此时 loop 的 header 已落盘)。
|
|
486
|
-
if (changed) td.stepTarget = target
|
|
487
|
-
const stepRec = { turn: key, step: payload.step, lastTools: tools.slice(0, 5), stepTier, target, current: cur, applied: false, pending: changed, ts: Date.now() }
|
|
514
|
+
if (changed) { td.stepTarget = target; td.stepTargetProvider = targetProvider }
|
|
515
|
+
const stepRec = { turn: key, step: payload.step, lastTools: tools.slice(0, 5), stepTier, target, provider: targetProvider, current: cur, applied: false, pending: changed, ts: Date.now() }
|
|
488
516
|
if (changed) td.stepRec = stepRec
|
|
489
517
|
state.stepLog.push(stepRec)
|
|
490
518
|
if (state.stepLog.length > 30) state.stepLog.shift()
|
|
@@ -505,22 +533,43 @@ export function apply(ctx, config = {}) {
|
|
|
505
533
|
if (!timer || typeof timer.interval !== 'function') settleFeedback().catch(() => {})
|
|
506
534
|
const key = fullKey(payload)
|
|
507
535
|
const model = typeof cfg.model === 'string' ? cfg.model : null
|
|
536
|
+
const provider = typeof cfg.provider === 'string' ? cfg.provider : ''
|
|
508
537
|
const td = state.turnDecision[key]
|
|
509
538
|
if (td) {
|
|
510
539
|
if (model) {
|
|
511
540
|
td.actualModel = model
|
|
512
541
|
state.lastActual = { model, turn: payload.turn, step: payload.step, key }
|
|
513
542
|
}
|
|
543
|
+
if (provider) td.provider = provider
|
|
544
|
+
if (td.awaitProvider && !td.finalized) {
|
|
545
|
+
const chosenRec = (td.recs && td.recs[td.chosen]) || td.recs.rule
|
|
546
|
+
let resolved = state.forced
|
|
547
|
+
? (() => { const pf = parseForced(state.forced); return { target: pf.model, provider: pf.provider, used: 'forced', note: 'forced' } })()
|
|
548
|
+
: resolveTier(chosenRec.tier, provider, state.modelTiers, { crossProvider: state.crossProvider })
|
|
549
|
+
td.target = resolved.target
|
|
550
|
+
td.targetProvider = resolved.provider || null
|
|
551
|
+
td.turnTier = td.target ? tierOf(td.target) : null
|
|
552
|
+
td.turnTarget = td.target
|
|
553
|
+
td.awaitProvider = false
|
|
554
|
+
if (td.rec) {
|
|
555
|
+
td.rec.resolvedTier = resolved.used + (resolved.note ? ' [' + resolved.note + ']' : '')
|
|
556
|
+
td.rec.targetModel = state.mode === 'active' ? td.target : null
|
|
557
|
+
td.rec.targetProvider = state.mode === 'active' ? td.targetProvider : null
|
|
558
|
+
td.rec.currentModel = model
|
|
559
|
+
}
|
|
560
|
+
if (!td.recordCommitted) commitRecord(td.rec, td)
|
|
561
|
+
td.needsApply = state.mode === 'active' && td.target && td.target !== model
|
|
562
|
+
log('decision #' + state.seq + ' resolved@request tier=' + td.rec.resolvedTier + ' target=' + (td.target || '-') + ' needsApply=' + td.needsApply)
|
|
563
|
+
}
|
|
514
564
|
return cfg
|
|
515
565
|
}
|
|
516
566
|
const pkey = agentKey(payload) + ':' + payload.turn
|
|
517
567
|
const text = state.pending.get(pkey) || ''
|
|
518
568
|
state.pending.delete(pkey)
|
|
519
|
-
const provider = typeof cfg.provider === 'string' ? cfg.provider : ''
|
|
520
569
|
const r = decideAndRecord(payload, text, model, provider, 'request')
|
|
521
570
|
if (r) {
|
|
522
571
|
r.td.needsApply = state.mode === 'active' && r.target && r.target !== r.td.currentModel
|
|
523
|
-
commitRecord(r.record)
|
|
572
|
+
if (!r.td.awaitProvider) commitRecord(r.record, r.td)
|
|
524
573
|
if (r.td.needsApply) log('decision #' + state.seq + ' -> apply deferred to llm/stream')
|
|
525
574
|
else log('decision #' + state.seq + ' type=' + r.td.clsType + ' meta=' + r.td.chosen + ' tier=' + r.record.resolvedTier)
|
|
526
575
|
}
|
|
@@ -571,15 +620,16 @@ export function apply(ctx, config = {}) {
|
|
|
571
620
|
if (sessions && typeof sessions.get === 'function') session = sessions.get(sid) || null
|
|
572
621
|
} catch (e) { session = null }
|
|
573
622
|
if (session) {
|
|
574
|
-
if (td.stepTarget && td.stepTarget !== currentModel(session)) {
|
|
575
|
-
const ok = appendRouterHeader(session, td.stepTarget, 'router-step')
|
|
623
|
+
if (td.stepTarget && !state.forced && (td.stepTarget !== currentModel(session) || (td.stepTargetProvider && td.stepTargetProvider !== currentProvider(session)))) {
|
|
624
|
+
const ok = appendRouterHeader(session, td.stepTarget, 'router-step', td.stepTargetProvider || null)
|
|
576
625
|
if (ok) {
|
|
577
626
|
state.counters.stepSwitches++
|
|
578
627
|
if (td.stepRec) { td.stepRec.applied = true }
|
|
579
|
-
log('STEP-SWAP -> ' + td.stepTarget + (td.stepRec ? '
|
|
628
|
+
log('STEP-SWAP -> ' + td.stepTarget + (td.stepTargetProvider ? ' (' + td.stepTargetProvider + ')' : '') + (td.stepRec ? ' [tools: ' + td.stepRec.lastTools.join(',') + ']' : ''))
|
|
580
629
|
}
|
|
581
630
|
}
|
|
582
631
|
td.stepTarget = null
|
|
632
|
+
td.stepTargetProvider = null
|
|
583
633
|
if (td.needsApply && !td.applied) applyDecision(session, key, td, td.rec, td.target)
|
|
584
634
|
}
|
|
585
635
|
const u = td.usage || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, reasoningTokens: 0, estimated: false }
|
|
@@ -633,6 +683,7 @@ export function apply(ctx, config = {}) {
|
|
|
633
683
|
const td = state.turnDecision[key]
|
|
634
684
|
if (!td || td.finalized) return
|
|
635
685
|
td.finalized = true
|
|
686
|
+
if (!td.recordCommitted && td.rec) commitRecord(td.rec, td)
|
|
636
687
|
const t1 = now()
|
|
637
688
|
const dtMs = t1 != null && td.t0 != null ? t1 - td.t0 : null
|
|
638
689
|
const r = rewardOf(td, tag, dtMs)
|
|
@@ -703,6 +754,7 @@ export function apply(ctx, config = {}) {
|
|
|
703
754
|
return {
|
|
704
755
|
mode: state.mode,
|
|
705
756
|
forcedModel: state.forced,
|
|
757
|
+
crossProvider: state.crossProvider,
|
|
706
758
|
totalDecisions: state.seq,
|
|
707
759
|
modelTiers: state.modelTiers,
|
|
708
760
|
prices,
|
|
@@ -743,7 +795,7 @@ export function apply(ctx, config = {}) {
|
|
|
743
795
|
})
|
|
744
796
|
register({
|
|
745
797
|
name: 'agentic_router_force',
|
|
746
|
-
description: '强制后续请求使用指定模型 id(仅 active
|
|
798
|
+
description: '强制后续请求使用指定模型 id(仅 active 模式生效,支持 model 或 provider/model);clear=true 清除强制,恢复规则路由。',
|
|
747
799
|
parameters: {
|
|
748
800
|
type: 'object',
|
|
749
801
|
properties: {
|
|
@@ -764,14 +816,15 @@ export function apply(ctx, config = {}) {
|
|
|
764
816
|
store.savePolicy({ mode: state.mode, forced: args.model, prices })
|
|
765
817
|
// 立即对存活会话应用(下一请求生效)
|
|
766
818
|
if (state.mode === 'active') {
|
|
819
|
+
const pf = parseForced(args.model)
|
|
767
820
|
try {
|
|
768
821
|
const agents = ctx.get('agents')
|
|
769
822
|
if (agents && typeof agents.list === 'function') {
|
|
770
823
|
for (const agent of agents.list()) {
|
|
771
824
|
try {
|
|
772
825
|
const session = sessionOf({ agent })
|
|
773
|
-
if (session && currentModel(session) !==
|
|
774
|
-
const ok = appendRouterHeader(session,
|
|
826
|
+
if (session && currentModel(session) !== pf.model) {
|
|
827
|
+
const ok = appendRouterHeader(session, pf.model, 'router', pf.provider)
|
|
775
828
|
if (ok) state.headerAppends++
|
|
776
829
|
}
|
|
777
830
|
} catch (e) { /* per-agent best effort */ }
|
package/dsh/strategies.js
CHANGED
|
@@ -21,18 +21,45 @@ export function tierTarget(tier, provider, modelTiers) {
|
|
|
21
21
|
return null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// 档位是否有候选(跨所有 provider)——供 availableTiersFor 判断该档是否值得探索
|
|
25
|
+
export function tierHasAny(tier, modelTiers) {
|
|
26
|
+
const list = modelTiers && modelTiers.tiers && modelTiers.tiers[tier]
|
|
27
|
+
return Array.isArray(list) && list.length > 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 档位 → 模型 id 的解析(v1.6.0 支持跨 provider):
|
|
31
|
+
// 1) 优先当前 provider 的候选(保持会话 provider 稳定,避免无谓跨厂商)
|
|
32
|
+
// 2) 无候选时,若 crossProvider(默认开),按 providers 顺序取其它 provider 的同档候选
|
|
33
|
+
// 3) mid 档仍无候选时:先同 provider 的 strong/fast,再跨 provider 的 strong/fast
|
|
34
|
+
// 返回 { target, provider, used, note };provider 为最终选定的 provider id。
|
|
35
|
+
export function resolveTier(tier, provider, modelTiers, opts = {}) {
|
|
36
|
+
if (!modelTiers) return { target: null, provider: null, used: tier, note: 'tiers-not-ready' }
|
|
37
|
+
const cross = opts.crossProvider !== false
|
|
38
|
+
const same = (t) => {
|
|
39
|
+
const m = tierTarget(t, provider, modelTiers)
|
|
40
|
+
return m ? { provider, model: m } : null
|
|
41
|
+
}
|
|
42
|
+
const crossPick = (t) => {
|
|
43
|
+
if (!cross) return null
|
|
44
|
+
for (const p of modelTiers.providers || []) {
|
|
45
|
+
const pid = p && (p.provider || p.id || p.name)
|
|
46
|
+
if (!pid || pid === provider) continue
|
|
47
|
+
const m = tierTarget(t, pid, modelTiers)
|
|
48
|
+
if (m) return { provider: pid, model: m }
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
const isCross = (r) => !!(r && provider && r.provider !== provider)
|
|
53
|
+
|
|
54
|
+
const direct = same(tier) || crossPick(tier)
|
|
55
|
+
if (direct) return { target: direct.model, provider: direct.provider, used: tier, note: isCross(direct) ? 'cross-provider' : '' }
|
|
29
56
|
if (tier === 'mid') {
|
|
30
|
-
const strong =
|
|
31
|
-
if (strong) return { target: strong, used: 'strong', note: 'mid->strong fallback' }
|
|
32
|
-
const fast =
|
|
33
|
-
if (fast) return { target: fast, used: 'fast', note: 'mid->fast fallback' }
|
|
57
|
+
const strong = same('strong') || crossPick('strong')
|
|
58
|
+
if (strong) return { target: strong.model, provider: strong.provider, used: 'strong', note: 'mid->strong fallback' + (isCross(strong) ? ' [cross-provider]' : '') }
|
|
59
|
+
const fast = same('fast') || crossPick('fast')
|
|
60
|
+
if (fast) return { target: fast.model, provider: fast.provider, used: 'fast', note: 'mid->fast fallback' + (isCross(fast) ? ' [cross-provider]' : '') }
|
|
34
61
|
}
|
|
35
|
-
return { target: null, used: tier, note: 'no-candidate' }
|
|
62
|
+
return { target: null, provider: null, used: tier, note: 'no-candidate' }
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
export function recommendRule(cls) {
|
|
@@ -117,6 +144,12 @@ export function recommendKnn(cls, f, knnState, availableTiers = ['fast', 'mid',
|
|
|
117
144
|
// 4) CONSERVATIVE 保守值 + 标记 unknown,提醒用户补价格
|
|
118
145
|
export const DEFAULT_PRICES = {
|
|
119
146
|
'deepseek-official': {
|
|
147
|
+
'deepseek-flash': { in: 0.2, out: 2 },
|
|
148
|
+
'deepseek-v4-flash': { in: 0.2, out: 2 },
|
|
149
|
+
'deepseek-v4-pro': { in: 1, out: 12 },
|
|
150
|
+
},
|
|
151
|
+
'deepseek-account': {
|
|
152
|
+
'deepseek-flash': { in: 0.2, out: 2 },
|
|
120
153
|
'deepseek-v4-flash': { in: 0.2, out: 2 },
|
|
121
154
|
'deepseek-v4-pro': { in: 1, out: 12 },
|
|
122
155
|
},
|
|
@@ -127,6 +160,25 @@ export const DEFAULT_PRICES = {
|
|
|
127
160
|
},
|
|
128
161
|
}
|
|
129
162
|
|
|
163
|
+
// 价格表合并:以 base(内置默认价)为底,over(policy/config 的用户覆盖)为准。
|
|
164
|
+
// 这样新增内置价(如 deepseek-account)能对既有安装生效,同时不覆盖用户自定义价格。
|
|
165
|
+
export function mergePrices(base, over) {
|
|
166
|
+
const out = {}
|
|
167
|
+
for (const [k, v] of Object.entries(base || {})) {
|
|
168
|
+
out[k] = {}
|
|
169
|
+
for (const [k2, v2] of Object.entries(v || {})) out[k][k2] = { ...v2 }
|
|
170
|
+
}
|
|
171
|
+
for (const [k, v] of Object.entries(over || {})) {
|
|
172
|
+
if (!out[k]) out[k] = {}
|
|
173
|
+
for (const [k2, v2] of Object.entries(v || {})) {
|
|
174
|
+
if (v2 && typeof v2 === 'object' && typeof v2.in === 'number' && typeof v2.out === 'number') {
|
|
175
|
+
out[k][k2] = { in: v2.in, out: v2.out }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
130
182
|
const CONSERVATIVE = { in: 3, out: 16 }
|
|
131
183
|
|
|
132
184
|
// 解析价格并返回来源(透明标注:价格到底从哪来的)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentic-router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin: learning agentic router — rule + UCB + LinUCB + k-NN experts under an EXP3 meta-selector, with a durable quality-reward flywheel, REAL model switching via the session request-header precedence chain, and STEP-LEVEL routing (v1.5.0)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|