@tzyc/dsh-model-visibility 0.0.0-stage → 1.9.1

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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Curated corrections for facts the bundled catalog gets wrong.
3
+ *
4
+ * The pi-ai / models.dev snapshot is a snapshot: vendor model names collide
5
+ * (`glm-5.3` is a text-only flagship while `glm-5.3-flash` is the multimodal
6
+ * one), and an entry can be attributed to the wrong provider family. A wrong
7
+ * row that says "multimodal" is worse than an unknown one: the composer would
8
+ * offer image input for a model that rejects it, and the request would fail
9
+ * upstream.
10
+ *
11
+ * Corrections are deliberately tiny, keyed by normalized model id and labelled
12
+ * `official` in the UI, with the documentation URL kept for the tooltip. They
13
+ * only replace `catalog` / `inferred` / `unknown` evidence — an explicit
14
+ * provider declaration or a relay listing still wins, because that is the
15
+ * user's own routing configuration.
16
+ *
17
+ * Add an entry only when a primary source (the vendor's model documentation)
18
+ * contradicts the snapshot. Never guess from a model name here.
19
+ *
20
+ * @module lib/capability-corrections
21
+ */
22
+
23
+ import { modelIdVariants } from './capability-catalog.js'
24
+
25
+ /**
26
+ * Corrected facts keyed by the normalized catalog id.
27
+ *
28
+ * Shape:
29
+ * `input` — exact input modalities the vendor documents.
30
+ * `code` — dictionary key under `correction.*` for the localized note.
31
+ * `note` — English fallback when the dictionary has no key (diagnostics).
32
+ * `source` — canonical documentation URL, shown in the tooltip.
33
+ */
34
+ export const CORRECTIONS = Object.freeze({
35
+ 'glm-5.3': Object.freeze({
36
+ input: Object.freeze(['text']),
37
+ code: 'glm-5.3-text-only',
38
+ note: 'GLM-5.3 accepts text only; the multimodal variant is GLM-5.3-Flash / FlashX.',
39
+ source: 'https://docs.bigmodel.cn/cn/guide/models/text/glm-5.3',
40
+ }),
41
+ })
42
+
43
+ /** Normalized-id index over {@link CORRECTIONS}. */
44
+ const INDEX = (() => {
45
+ const index = new Map()
46
+ for (const [id, correction] of Object.entries(CORRECTIONS)) {
47
+ for (const variant of modelIdVariants(id)) {
48
+ if (!index.has(variant)) index.set(variant, { id, ...correction })
49
+ }
50
+ }
51
+ return index
52
+ })()
53
+
54
+ /**
55
+ * Find the curated correction for one model id.
56
+ * @param value - model id as any provider spells it.
57
+ * @returns `{ id, input, code, note, source }` or undefined.
58
+ */
59
+ export function lookupCorrection(value) {
60
+ for (const variant of modelIdVariants(value)) {
61
+ const found = INDEX.get(variant)
62
+ if (found !== undefined) return found
63
+ }
64
+ return undefined
65
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Request-side image-input gate for relay adapters.
3
+ *
4
+ * The settings page can prove that a relay model accepts images (relay
5
+ * listing, built-in catalog, vendor correction), but the `llm-pi-ai` adapter
6
+ * builds an internal model object from the route configuration. For a relay
7
+ * that leaves `input` empty, that object falls back to the provider's
8
+ * `defaultInput` (`["text"]`), so both the session admission check and the
9
+ * adapter's own serializer reject an image that the relay really accepts.
10
+ *
11
+ * This module patches only the adapter method that materializes that model
12
+ * object (`modelOf`), which is used by `resolveModel`, `stream` and
13
+ * `imageRequestTarget` alike. The patch is read-only: it never writes settings,
14
+ * never changes a route, and returns the original object untouched when the
15
+ * capability merge says the model is text-only. It is scoped to the pi-ai
16
+ * adapter shape, so built-in adapters and the DeepSeek Messages adapter keep
17
+ * their own declarations.
18
+ *
19
+ * @module lib/image-input-gate
20
+ */
21
+
22
+ /**
23
+ * True when an adapter is the pi-ai route adapter whose models come from
24
+ * `modelOf(snapshot, provider, model)`.
25
+ * @param adapter - registered adapter instance.
26
+ * @returns whether the adapter has the patched surface.
27
+ */
28
+ function isPiAiAdapter(adapter) {
29
+ return (
30
+ adapter !== null &&
31
+ typeof adapter === 'object' &&
32
+ typeof adapter.modelOf === 'function' &&
33
+ typeof adapter.profileOf === 'function' &&
34
+ typeof adapter.current === 'function'
35
+ )
36
+ }
37
+
38
+ /**
39
+ * Patch every registered pi-ai adapter so image-capable relay models advertise
40
+ * `image` to the admission check and to the request serializer.
41
+ *
42
+ * @param llm - the Host `ctx.llm` service (a traceable service proxy is fine).
43
+ * @param options - `{ allow(provider, model), warn(error) }`.
44
+ * @returns `{ refresh, dispose }`; call `refresh` after adapters change.
45
+ */
46
+ export function installImageInputGate(llm, options = {}) {
47
+ const allow = typeof options.allow === 'function' ? options.allow : () => false
48
+ const warn = typeof options.warn === 'function' ? options.warn : () => {}
49
+ /** adapter -> { original, wrapper } */
50
+ const patched = new Map()
51
+
52
+ const shouldAddImage = (provider, model) => {
53
+ try {
54
+ return allow(provider, model) === true
55
+ } catch (error) {
56
+ warn(error)
57
+ return false
58
+ }
59
+ }
60
+
61
+ const patchAdapter = (adapter) => {
62
+ if (!isPiAiAdapter(adapter) || patched.has(adapter)) return
63
+ const original = adapter.modelOf
64
+ const wrapper = function modelOf(snapshot, provider, model) {
65
+ const resolved = original.call(this, snapshot, provider, model)
66
+ if (resolved === null || typeof resolved !== 'object') return resolved
67
+ if (Array.isArray(resolved.input) && resolved.input.includes('image')) return resolved
68
+ if (!shouldAddImage(provider, model)) return resolved
69
+ const input = Array.isArray(resolved.input) ? resolved.input : ['text']
70
+ return { ...resolved, input: [...input, 'image'] }
71
+ }
72
+ try {
73
+ Object.defineProperty(adapter, 'modelOf', {
74
+ value: wrapper,
75
+ configurable: true,
76
+ writable: true,
77
+ })
78
+ patched.set(adapter, { original, wrapper })
79
+ } catch (error) {
80
+ warn(error)
81
+ }
82
+ }
83
+
84
+ /** Re-scan the registry; registrations are replaced on route/config changes. */
85
+ const refresh = () => {
86
+ let adapters
87
+ try {
88
+ adapters = llm?.adapters
89
+ } catch (_error) {
90
+ return
91
+ }
92
+ if (!(adapters instanceof Map)) return
93
+ for (const registration of adapters.values()) patchAdapter(registration?.adapter)
94
+ }
95
+
96
+ refresh()
97
+
98
+ return {
99
+ refresh,
100
+ dispose() {
101
+ for (const [adapter, record] of patched) {
102
+ try {
103
+ if (Object.getOwnPropertyDescriptor(adapter, 'modelOf')?.value === record.wrapper) {
104
+ delete adapter.modelOf
105
+ }
106
+ } catch (_error) {
107
+ // A disposed adapter simply keeps the wrapper; it no longer serves.
108
+ }
109
+ }
110
+ patched.clear()
111
+ },
112
+ }
113
+ }