@apinggithub/mydsh-auto-model-router 0.2.1 → 0.2.3
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 +103 -1
- package/config.mjs +9 -5
- package/index.mjs +4 -3
- package/package.json +1 -1
package/auto-adapter.mjs
CHANGED
|
@@ -26,6 +26,25 @@ import { routesEqual } from './router.mjs'
|
|
|
26
26
|
export const AUTO_PROVIDER = 'auto'
|
|
27
27
|
export const AUTO_MODEL = 'auto'
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Reasoning efforts advertised when the routed model's own metadata cannot be
|
|
31
|
+
* read: the four values the DeepSeek catalog accepts.
|
|
32
|
+
*/
|
|
33
|
+
const FALLBACK_EFFORTS = Object.freeze([
|
|
34
|
+
{ id: 'off', name: 'Off' },
|
|
35
|
+
{ id: 'low', name: 'Low' },
|
|
36
|
+
{ id: 'high', name: 'High' },
|
|
37
|
+
{ id: 'max', name: 'Max' },
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Context window advertised when the routed model does not report one.
|
|
42
|
+
* DeepSeek's 0.2.x catalog tops out at 1M tokens; assuming the largest window
|
|
43
|
+
* merely keeps compaction from firing early — the routed provider still
|
|
44
|
+
* rejects an oversized request and the fallback chain takes over.
|
|
45
|
+
*/
|
|
46
|
+
const FALLBACK_CONTEXT_WINDOW = 1_000_000
|
|
47
|
+
|
|
29
48
|
/** Provider route metadata shown in the selector. */
|
|
30
49
|
export function autoProviderInfo(provider) {
|
|
31
50
|
return { id: provider, name: 'Auto Router' }
|
|
@@ -41,6 +60,53 @@ export function autoResolveModel(provider, model) {
|
|
|
41
60
|
return { provider, id: model, name: model }
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The route the virtual model should borrow its capabilities from.
|
|
65
|
+
*
|
|
66
|
+
* The host asks `resolveModel("auto", "auto")` before it assembles a request,
|
|
67
|
+
* long before any session decision exists, so this picks the configured
|
|
68
|
+
* default level (falling back to the first configured one).
|
|
69
|
+
*
|
|
70
|
+
* @param {object | undefined} router
|
|
71
|
+
* @returns {{ provider: string, model: string, reasoningEffort?: string } | undefined}
|
|
72
|
+
*/
|
|
73
|
+
function representativeRoute(router) {
|
|
74
|
+
try {
|
|
75
|
+
const level = router?.config?.costControl?.defaultLevel
|
|
76
|
+
const route = (level === undefined ? undefined : router?.routeForLevel?.(level))
|
|
77
|
+
?? router?.firstConfiguredRoute?.()
|
|
78
|
+
if (route === undefined || route === null) return undefined
|
|
79
|
+
if (typeof route.provider !== 'string' || route.provider === '') return undefined
|
|
80
|
+
if (typeof route.model !== 'string' || route.model === '') return undefined
|
|
81
|
+
return route
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Read the routed model's own metadata, tolerating a host without it. */
|
|
88
|
+
async function underlyingInfo(llm, route) {
|
|
89
|
+
if (route === undefined || typeof llm?.resolveModelInfo !== 'function') return undefined
|
|
90
|
+
try {
|
|
91
|
+
return await llm.resolveModelInfo(route.provider, route.model)
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Copy the routed model's efforts, or the DeepSeek defaults. */
|
|
98
|
+
function reasoningEfforts(info) {
|
|
99
|
+
const efforts = info?.reasoning?.efforts
|
|
100
|
+
if (Array.isArray(efforts) && efforts.length > 0) {
|
|
101
|
+
return efforts.map((effort) => ({
|
|
102
|
+
id: effort.id,
|
|
103
|
+
name: effort.name,
|
|
104
|
+
...effort.description === undefined ? {} : { description: effort.description },
|
|
105
|
+
}))
|
|
106
|
+
}
|
|
107
|
+
return FALLBACK_EFFORTS.map((effort) => ({ ...effort }))
|
|
108
|
+
}
|
|
109
|
+
|
|
44
110
|
/**
|
|
45
111
|
* Build the virtual adapter for one plugin mount.
|
|
46
112
|
*
|
|
@@ -64,7 +130,43 @@ export function createAutoAdapter(resolve = {}) {
|
|
|
64
130
|
return undefined
|
|
65
131
|
},
|
|
66
132
|
listModels: async () => autoListModels(),
|
|
67
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Capability metadata for the virtual model.
|
|
135
|
+
*
|
|
136
|
+
* The host validates a request against the exact model *before* any
|
|
137
|
+
* provider I/O and assembles the conversation with what it learns here:
|
|
138
|
+
* without a context window it cannot budget compaction, and without
|
|
139
|
+
* reasoning efforts it treats the route as non-thinking and stops echoing
|
|
140
|
+
* the `thinking` blocks a thinking-mode provider requires on every turn.
|
|
141
|
+
* Borrowing the routed model's own metadata keeps the virtual provider
|
|
142
|
+
* indistinguishable from the model it forwards to.
|
|
143
|
+
*/
|
|
144
|
+
async resolveModel(provider, model) {
|
|
145
|
+
const route = representativeRoute(readRouter())
|
|
146
|
+
const info = await underlyingInfo(readLlm(), route)
|
|
147
|
+
const efforts = reasoningEfforts(info)
|
|
148
|
+
const requested = route?.reasoningEffort
|
|
149
|
+
const wanted = efforts.some((effort) => effort.id === requested)
|
|
150
|
+
? requested
|
|
151
|
+
: info?.reasoning?.defaultEffort
|
|
152
|
+
const defaultEffort = efforts.some((effort) => effort.id === wanted) ? wanted : efforts[0]?.id
|
|
153
|
+
const contextWindow = Number.isInteger(info?.context?.contextWindow) && info.context.contextWindow > 0
|
|
154
|
+
? info.context.contextWindow
|
|
155
|
+
: FALLBACK_CONTEXT_WINDOW
|
|
156
|
+
return {
|
|
157
|
+
provider,
|
|
158
|
+
id: model,
|
|
159
|
+
name: 'Auto',
|
|
160
|
+
context: { contextWindow },
|
|
161
|
+
systemPromptUpdate: info?.systemPromptUpdate ?? 'in-history',
|
|
162
|
+
...info?.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: info.defaultMaxTokens },
|
|
163
|
+
...info?.inputModalities === undefined ? {} : { inputModalities: info.inputModalities },
|
|
164
|
+
reasoning: {
|
|
165
|
+
efforts,
|
|
166
|
+
...defaultEffort === undefined ? {} : { defaultEffort },
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
},
|
|
68
170
|
/**
|
|
69
171
|
* The 0.2.x adapter contract: the runtime validates a request against the
|
|
70
172
|
* model's capabilities and binds it to one concrete adapter generation
|
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.
|
|
25
|
+
export const PLUGIN_VERSION = '0.2.3'
|
|
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
|
-
//
|
|
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
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|