@apinggithub/mydsh-auto-model-router 0.3.3 → 0.3.4

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
@@ -63,10 +63,12 @@ LLM dispatch
63
63
  | `L2` | writing code, review, refactoring | strong |
64
64
  | `L3` | complex bug fixes, multi-step tasks, reports | most powerful |
65
65
 
66
- Each level is a user-assigned provider/model route with a **required**
67
- `reasoningEffort` (defaults to `off` when omitted): `off` = no thinking mode,
68
- `high`/`max` = thinking intensity. Levels may be left unconfigured; the router
69
- then falls to the nearest configured level (direction depends on the cost
66
+ Each level is a user-assigned provider/model route with an **optional**
67
+ `reasoningEffort`: omitting it (or writing `null`) sends no effort at all —
68
+ required for models without a thinking mode (GLM/Qwen flash), which the host
69
+ otherwise rejects; `off` explicitly disables thinking; `high`/`max` set its
70
+ intensity. Levels may be left unconfigured; the router then falls to the nearest
71
+ configured level (direction depends on the cost
70
72
  mode).
71
73
 
72
74
  ## Auto mode in the model selector
package/README.zh.md CHANGED
@@ -52,9 +52,10 @@ LLM 分发
52
52
  | `L2` | 写代码、审查、重构 | 较强 |
53
53
  | `L3` | 复杂 BUG 修复、多步任务、汇报 | 最强 |
54
54
 
55
- 每个层级是一个用户自行指定的 provider/model 路由,**必须**附带
56
- `reasoningEffort`(省略时默认 `off`):`off` = 无思考模式,`high`/`max` =
57
- 思考强度。层级可以留空不配置;路由会落到最近的已配置层级(方向由成本模式决定)。
55
+ 每个层级是一个用户自行指定的 provider/model 路由,**可选**附带
56
+ `reasoningEffort`:不写(或写 `null`)= 不发送该字段——GLM/Qwen flash 这类没有
57
+ 思考模式的模型**必须**这样,否则宿主会拒绝请求;`off` = 显式关闭思考;`high`/`max`
58
+ = 思考强度。层级可以留空不配置;路由会落到最近的已配置层级(方向由成本模式决定)。
58
59
 
59
60
  ## 模型选择框中的 Auto 模式
60
61
 
package/config.mjs CHANGED
@@ -22,7 +22,7 @@
22
22
  // fallbackChain: [route, ...] (model-level, appended with the default)
23
23
  // maxRetries: per-route retry budget on request-error
24
24
 
25
- export const PLUGIN_VERSION = '0.3.3'
25
+ export const PLUGIN_VERSION = '0.3.4'
26
26
 
27
27
  export const LEVEL_IDS = Object.freeze(['L0', 'L1', 'L2', 'L3'])
28
28
  export const COST_MODES = Object.freeze(['cost-first', 'quality-first', 'balanced'])
@@ -382,13 +382,14 @@ function normalizeLevels(levels) {
382
382
  }
383
383
 
384
384
  function normalizeLevelRoute(route, where) {
385
- // `reasoningEffort: null` means "this route has no thinking mode at all":
386
- // the field is omitted so a provider whose model declares no reasoning
387
- // metadata (Zhipu/Bailian flash models) is not rejected with
388
- // UNSUPPORTED_REASONING_EFFORT.
389
- const omitEffort = route.reasoningEffort === null
385
+ // A level states a reasoning effort only when the config states one.
386
+ //
387
+ // This used to default to 'off', which broke twice over: providers whose
388
+ // models have no thinking mode (GLM/Qwen flash) reject *any* effort with
389
+ // UNSUPPORTED_REASONING_EFFORT, and `apply()` normalises the config a second
390
+ // time, so an omitted field came back as 'off' even when the config had said
391
+ // `null`. Absent now stays absent, which makes normalisation idempotent.
390
392
  const normalized = normalizeRoute(route, where)
391
- // Every other level carries an explicit effort; absent → 'off' (no thinking).
392
393
  const contextWindow = route.contextWindow === undefined || route.contextWindow === null
393
394
  ? undefined
394
395
  : Number(route.contextWindow)
@@ -397,7 +398,6 @@ function normalizeLevelRoute(route, where) {
397
398
  }
398
399
  return {
399
400
  ...normalized,
400
- ...omitEffort ? {} : { reasoningEffort: normalized.reasoningEffort ?? 'off' },
401
401
  // A level may declare that its route is a free-quota route. The flag rides
402
402
  // along with the route so the router can prefer/demote it.
403
403
  ...route.freeQuota === true ? { freeQuota: true } : {},
package/index.mjs CHANGED
@@ -294,8 +294,29 @@ export function apply(ctx, input = {}) {
294
294
  }
295
295
  })
296
296
 
297
- // ── ★ agent/request: reuse the per-input decision ──────────────────────
298
- // Only a NEW user input triggers a fresh decision (and only when the
297
+ /**
298
+ * Rewrite a host proposal onto one routed model.
299
+ *
300
+ * The incoming proposal still carries the *session's* reasoning effort — the
301
+ * host's model-selection listener re-applies it on every step — and that
302
+ * value belongs to the virtual `auto` model, not to the model we route to.
303
+ * Spreading it through sends a thinking parameter to a model that has no
304
+ * thinking mode, and the host rejects the request with
305
+ * `provider "zhipu" model "glm-4.7-flash" does not support reasoning effort "off"`.
306
+ * So the inherited effort is dropped unless the level itself declares one.
307
+ *
308
+ * @param {object} proposed - config from the host's own waterfall
309
+ * @param {object} route - the level's route
310
+ * @returns {object} the rewritten config
311
+ */
312
+ const withRoute = (proposed, route) => {
313
+ const next = { ...proposed, provider: route.provider, model: route.model }
314
+ delete next.reasoningEffort
315
+ if (route.reasoningEffort !== undefined) next.reasoningEffort = route.reasoningEffort
316
+ return next
317
+ }
318
+
319
+ // ── ★ agent/request: reuse the per-input decision ────────────────────── // Only a NEW user input triggers a fresh decision (and only when the
299
320
  // session picked the virtual auto provider). Steps within the same input
300
321
  // reuse state.current — no per-step re-judging, no per-step classifier.
301
322
  ctx.on('agent/request', async (payload, next) => {
@@ -333,15 +354,13 @@ export function apply(ctx, input = {}) {
333
354
  }
334
355
  // fallback: user dismissed → stay in heuristic mode.
335
356
  const route = router.current(sessionId)
336
- return { ...proposed, provider: route.provider, model: route.model,
337
- ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort } }
357
+ return withRoute(proposed, route)
338
358
  }
339
359
  if (decision.needsAsk === 'fixed-reelect') {
340
360
  const picked = await askFixedLevel(ctx, agentOf(payload), router, sessionId)
341
361
  if (picked) router.setMode(sessionId, 'fixed', { fixedLevel: picked })
342
362
  const route = router.current(sessionId)
343
- return { ...proposed, provider: route.provider, model: route.model,
344
- ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort } }
363
+ return withRoute(proposed, route)
345
364
  }
346
365
  }
347
366
 
@@ -363,15 +382,7 @@ export function apply(ctx, input = {}) {
363
382
  if (demotion && config.freeQuota.notify) {
364
383
  await notifyFreeQuotaFallback(agentOf(payload), sessionId, demotion, resolveLocale(ctx))
365
384
  }
366
- return {
367
- ...proposed,
368
- provider: route.provider,
369
- model: route.model,
370
- // A level that omits `reasoningEffort` (configured as `null`) has no
371
- // thinking mode: sending an effort to a model without reasoning
372
- // metadata would be rejected with UNSUPPORTED_REASONING_EFFORT.
373
- ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort },
374
- }
385
+ return withRoute(proposed, route)
375
386
  } catch (error) {
376
387
  // The router must never be able to fail a user request: hand the host's
377
388
  // own proposal back untouched and surface the reason once.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apinggithub/mydsh-auto-model-router",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Hybrid model router for DeepSeek Harness: declarative rules, user-configurable cost control (cost-first / quality-first / balanced), optional LLM task classifier, and failure fallback chains.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",