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 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 || !provider) return all
62
- const has = all.filter((t) => tierTarget(t, provider, modelTiers))
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
- let prices = (policy.prices && Object.keys(policy.prices).length) ? policy.prices : (config.prices || DEFAULT_PRICES)
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
- const resolved = state.forced
369
- ? { target: state.forced, used: 'forced', note: 'forced' }
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 ? ' (tools: ' + td.stepRec.lastTools.join(',') + ')' : ''))
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 模式生效);clear=true 清除强制,恢复规则路由。',
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) !== args.model) {
774
- const ok = appendRouterHeader(session, args.model)
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
- // 档位 → 模型 id 的解析;mid 档无候选时向上取 strong(保质量),再取 fast
25
- export function resolveTier(tier, provider, modelTiers) {
26
- if (!modelTiers) return { target: null, used: tier, note: 'tiers-not-ready' }
27
- const target = tierTarget(tier, provider, modelTiers)
28
- if (target) return { target, used: tier, note: '' }
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 = tierTarget('strong', provider, modelTiers)
31
- if (strong) return { target: strong, used: 'strong', note: 'mid->strong fallback' }
32
- const fast = tierTarget('fast', provider, modelTiers)
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.5.0",
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",