@apinggithub/mydsh-auto-model-router 0.2.2 → 0.2.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/auto-adapter.mjs CHANGED
@@ -107,6 +107,49 @@ function reasoningEfforts(info) {
107
107
  return FALLBACK_EFFORTS.map((effort) => ({ ...effort }))
108
108
  }
109
109
 
110
+ /**
111
+ * Pick the reasoning effort to send to the routed model.
112
+ *
113
+ * The session's model selection keeps its own `reasoningEffort` and the host's
114
+ * selection listener re-applies it after every waterfall rewrite, so the value
115
+ * that reaches this adapter belongs to the *virtual* model, not to the model it
116
+ * forwards to. Passing it through unchanged sends a thinking parameter to a
117
+ * model with no thinking mode, which the provider rejects outright
118
+ * (`does not support reasoning effort "low"`).
119
+ *
120
+ * @param {object} llm - ctx.llm
121
+ * @param {object} route - the routed provider/model
122
+ * @param {unknown} requested - effort the caller asked for (usually the UI choice)
123
+ * @returns {Promise<string | undefined>} an effort the target advertises, or undefined
124
+ */
125
+ async function supportedEffort(llm, route, requested) {
126
+ const info = await underlyingInfo(llm, route)
127
+ const efforts = Array.isArray(info?.reasoning?.efforts) ? info.reasoning.efforts : []
128
+ if (efforts.length === 0) return undefined
129
+ const ids = new Set(efforts.map((effort) => effort.id))
130
+ if (typeof route.reasoningEffort === 'string' && ids.has(route.reasoningEffort)) return route.reasoningEffort
131
+ if (typeof requested === 'string' && ids.has(requested)) return requested
132
+ const fallback = info?.reasoning?.defaultEffort
133
+ return typeof fallback === 'string' && ids.has(fallback) ? fallback : undefined
134
+ }
135
+
136
+ /**
137
+ * Request options for the routed model: same call, different provider/model,
138
+ * and only the reasoning effort that model actually supports.
139
+ *
140
+ * @param {object} llm - ctx.llm
141
+ * @param {object} route - the routed provider/model
142
+ * @param {object} options - options the runtime handed to `stream()`
143
+ * @returns {Promise<object>}
144
+ */
145
+ async function forwardOptions(llm, route, options) {
146
+ const forwarded = { ...options, provider: route.provider, model: route.model }
147
+ delete forwarded.reasoningEffort
148
+ const effort = await supportedEffort(llm, route, options?.reasoningEffort)
149
+ if (effort !== undefined) forwarded.reasoningEffort = effort
150
+ return forwarded
151
+ }
152
+
110
153
  /**
111
154
  * Build the virtual adapter for one plugin mount.
112
155
  *
@@ -194,13 +237,9 @@ export function createAutoAdapter(resolve = {}) {
194
237
  // runtime; the adapter contract requires we stream chunks back.
195
238
  const llm = readLlm()
196
239
  if (typeof llm?.stream === 'function') {
240
+ const forwarded = await forwardOptions(llm, route, options)
197
241
  // ctx.llm.stream returns the AsyncIterable directly (not { stream }).
198
- const stream = llm.stream({
199
- ...options,
200
- provider: route.provider,
201
- model: route.model,
202
- ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort },
203
- })
242
+ const stream = llm.stream(forwarded)
204
243
  for await (const chunk of stream) yield chunk
205
244
  return
206
245
  }
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.2.2'
25
+ export const PLUGIN_VERSION = '0.2.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'])
@@ -297,9 +297,13 @@ function normalizeLevels(levels) {
297
297
  }
298
298
 
299
299
  function normalizeLevelRoute(route, where) {
300
+ // `reasoningEffort: null` means "this route has no thinking mode at all":
301
+ // the field is omitted so a provider whose model declares no reasoning
302
+ // metadata (Zhipu/Bailian flash models) is not rejected with
303
+ // UNSUPPORTED_REASONING_EFFORT.
304
+ const omitEffort = route.reasoningEffort === null
300
305
  const normalized = normalizeRoute(route, where)
301
- // Levels always carry an explicit reasoning effort; absent → 'off' (no
302
- // thinking mode) so every level spells out its thinking intensity.
306
+ // Every other level carries an explicit effort; absent → 'off' (no thinking).
303
307
  const contextWindow = route.contextWindow === undefined || route.contextWindow === null
304
308
  ? undefined
305
309
  : Number(route.contextWindow)
@@ -308,7 +312,7 @@ function normalizeLevelRoute(route, where) {
308
312
  }
309
313
  return {
310
314
  ...normalized,
311
- reasoningEffort: normalized.reasoningEffort ?? 'off',
315
+ ...omitEffort ? {} : { reasoningEffort: normalized.reasoningEffort ?? 'off' },
312
316
  // A level may declare that its route is a free-quota route. The flag rides
313
317
  // along with the route so the router can prefer/demote it.
314
318
  ...route.freeQuota === true ? { freeQuota: true } : {},
@@ -328,7 +332,7 @@ function normalizeRoute(route, where) {
328
332
  `mydsh-auto-model-router: "${where}" needs both provider and model, got ${JSON.stringify(route)}`,
329
333
  )
330
334
  }
331
- const reasoningEffort = route.reasoningEffort === undefined
335
+ const reasoningEffort = route.reasoningEffort === undefined || route.reasoningEffort === null
332
336
  ? undefined
333
337
  : String(route.reasoningEffort).trim() || undefined
334
338
  return {
package/index.mjs CHANGED
@@ -341,9 +341,10 @@ export function apply(ctx, input = {}) {
341
341
  ...proposed,
342
342
  provider: route.provider,
343
343
  model: route.model,
344
- // Levels always carry an explicit reasoningEffort (default 'off'), so
345
- // the routed effort is always stated — including 'off' for no thinking.
346
- reasoningEffort: route.reasoningEffort,
344
+ // A level that omits `reasoningEffort` (configured as `null`) has no
345
+ // thinking mode: sending an effort to a model without reasoning
346
+ // metadata would be rejected with UNSUPPORTED_REASONING_EFFORT.
347
+ ...route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort },
347
348
  }
348
349
  } catch (error) {
349
350
  // The router must never be able to fail a user request: hand the host's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apinggithub/mydsh-auto-model-router",
3
- "version": "0.2.2",
3
+ "version": "0.2.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",