@deepblend/dsh-blender-host 0.2.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/lib/index.js +100 -7
  2. package/package.json +2 -2
package/lib/index.js CHANGED
@@ -301,17 +301,20 @@ export const StudioConfig = z.object({
301
301
  /** Provider route used by the built-in vision reviewer. */
302
302
  visualReviewProvider: z.string().default('deepseek-official'),
303
303
  /** Model used by the built-in vision reviewer. */
304
- visualReviewModel: z.string().default('deepseek-flash'),
304
+ visualReviewModel: z.string().default('deepseek-v4.1-flash'),
305
305
  /**
306
306
  * Token budget for one reviewer call.
307
307
  *
308
308
  * MEASURED, and the reason this is a config field rather than a constant: with the
309
- * full reviewer prompt and a 2x2 sheet, `deepseek-flash` spent ~11 000 REASONING
310
- * tokens before emitting any text. At 2048 — a budget that looks generous for a
311
- * 600-character JSON answer — the stream ended with `max-tokens` and NO text, which
312
- * downstream is indistinguishable from "the model reviewed the sheet and found
313
- * nothing wrong". That silent approval is the failure mode this number exists to
314
- * prevent, and the size of it is a property of the model, not of this code.
309
+ * full reviewer prompt and a 2x2 sheet, the model spends most of its output budget on
310
+ * REASONING before emitting any text. MEASURED twice, with the model the deployment
311
+ * actually serves (`deepseek-v4.1-flash`, via `visual-review-live-probe.mjs`): one
312
+ * review of a 640x360 sheet answered in 1049 tokens — 345 in, 704 out, of which 686
313
+ * were reasoning — and at the probe's own 400-token budget the same call ended with
314
+ * `max-tokens` and NO text at all. That empty answer is indistinguishable downstream
315
+ * from "the model reviewed the sheet and found nothing wrong", and that silent
316
+ * approval is the failure mode this number exists to prevent. The size of it is a
317
+ * property of the model, so this budget is sized for the reasoning, not the answer.
315
318
  */
316
319
  visualReviewMaxTokens: z.number().default(24_000),
317
320
  /** Views in the standard plan, in reading order (SPEC §12.3). */
@@ -1830,6 +1833,75 @@ export default class BlenderStudio extends Service {
1830
1833
  *
1831
1834
  * @returns {(request: object) => Promise<{ findings: object[], operations: object[], note: string|null, model: string|null, provider: string|null, raw: string|null }>}
1832
1835
  */
1836
+ /**
1837
+ * Whether the configured vision route can actually review an image.
1838
+ *
1839
+ * Answers `{ problem: null }` when the route is usable, or a sentence naming what is wrong and
1840
+ * what the catalog offers instead. Never throws: an absent catalog is "cannot check", not "bad
1841
+ * route", and the model call downstream is still the authority on whether the call works.
1842
+ *
1843
+ * @param {string} provider
1844
+ * @param {string} model
1845
+ * @returns {Promise<{ problem: string|null, providers: string[], models: string[], imageCapable: string[] }>}
1846
+ */
1847
+ async _visionRoute(provider, model) {
1848
+ const empty = { problem: null, providers: [], models: [], imageCapable: [] }
1849
+ const llm = this.ctx.get('llm')
1850
+ if (llm === undefined || typeof llm.listModels !== 'function') return empty
1851
+
1852
+ let providers = []
1853
+ try {
1854
+ providers = (llm.listProviders?.() ?? []).map(entry => entry.id).filter(id => typeof id === 'string')
1855
+ } catch {
1856
+ return empty
1857
+ }
1858
+ if (providers.length > 0 && !providers.includes(provider)) {
1859
+ return {
1860
+ problem: `The vision reviewer is configured for provider "${provider}", which this deployment does not offer. ` +
1861
+ `Set \`visualReviewProvider\` to one of: ${providers.join(', ')}.`,
1862
+ providers,
1863
+ models: [],
1864
+ imageCapable: [],
1865
+ }
1866
+ }
1867
+
1868
+ let catalog = []
1869
+ try {
1870
+ catalog = await llm.listModels(provider)
1871
+ } catch {
1872
+ return { ...empty, providers }
1873
+ }
1874
+ const models = catalog.map(entry => entry.id).filter(id => typeof id === 'string')
1875
+ const imageCapable = catalog
1876
+ .filter(entry => (entry.inputModalities ?? []).includes('image'))
1877
+ .map(entry => entry.id)
1878
+
1879
+ const entry = catalog.find(candidate => candidate.id === model)
1880
+ if (entry === undefined) {
1881
+ return {
1882
+ problem: `The vision reviewer is configured for model "${model}", which provider "${provider}" does not serve. ` +
1883
+ `Set \`visualReviewModel\` to one of: ${models.join(', ') || '(the catalog is empty)'}.`,
1884
+ providers,
1885
+ models,
1886
+ imageCapable,
1887
+ }
1888
+ }
1889
+ if (!(entry.inputModalities ?? []).includes('image')) {
1890
+ // THE QUIET HALF OF THIS FAILURE. A text-only model answers happily and never sees the sheet,
1891
+ // so the review comes back with zero findings — which downstream is indistinguishable from
1892
+ // "looked at it, found nothing wrong".
1893
+ return {
1894
+ problem: `The vision reviewer is configured for model "${model}", which does not accept images ` +
1895
+ `(inputModalities: ${JSON.stringify(entry.inputModalities ?? [])}). A review would silently omit the ` +
1896
+ `contact sheet. Set \`visualReviewModel\` to one of: ${imageCapable.join(', ') || '(none of the catalog accepts images)'}.`,
1897
+ providers,
1898
+ models,
1899
+ imageCapable,
1900
+ }
1901
+ }
1902
+ return { problem: null, providers, models, imageCapable }
1903
+ }
1904
+
1833
1905
  createVisualReviewer() {
1834
1906
  const provider = this.config.visualReviewProvider
1835
1907
  const model = this.config.visualReviewModel
@@ -1844,6 +1916,27 @@ export default class BlenderStudio extends Service {
1844
1916
  )
1845
1917
  }
1846
1918
 
1919
+ // THE ROUTE IS CHECKED BEFORE ANYTHING IS SPENT. MEASURED: the shipped default named a model
1920
+ // the provider had stopped serving, so every review ended in an HTTP 404 from inside the
1921
+ // stream — after the sheet had been uploaded — and the loop recorded "no second opinion" in a
1922
+ // note nobody read. A day of reviews ran that way with every suite green.
1923
+ //
1924
+ // Best-effort by design: a harness that does not expose a catalog (`listProviders`/
1925
+ // `listModels` are optional) still gets the model call and its own error handling. What this
1926
+ // adds is a refusal that NAMES the fix, at the one moment the caller can still do something.
1927
+ const catalogue = await this._visionRoute(provider, model)
1928
+ if (catalogue.problem !== null) {
1929
+ throw new BlenderError(BlenderErrorCode.VISUAL_REVIEW_MODEL_UNAVAILABLE, catalogue.problem, {
1930
+ detail: {
1931
+ provider,
1932
+ model,
1933
+ providers: catalogue.providers,
1934
+ models: catalogue.models,
1935
+ imageCapable: catalogue.imageCapable,
1936
+ },
1937
+ })
1938
+ }
1939
+
1847
1940
  const ref = await attachments.saveImage({
1848
1941
  data: request.sheetPng,
1849
1942
  mediaType: 'image/png',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-host",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "DeepBlend Host service: the blenderStudio business facade over the BlenderRuntime seam.",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "dependencies": {
23
- "@deepblend/dsh-blender-contracts": "0.2.1"
23
+ "@deepblend/dsh-blender-contracts": "0.2.2"
24
24
  },
25
25
  "license": "MIT",
26
26
  "repository": {