@goodandready/dsh-image-gen 0.11.0 → 0.11.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.
package/lib/client.js CHANGED
@@ -1751,27 +1751,25 @@ window.__ModuleLoader__.load({
1751
1751
  onLoad: () => setLoaded(true),
1752
1752
  }) : null
1753
1753
  )
1754
- }// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285).
1754
+ }// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301).
1755
1755
 
1756
- function InpaintCanvasOverlay({ react, t, parsed, onClose }) {
1756
+ function InpaintCanvas(props) {
1757
+ const { imgUrl, targetRef, onClose, t } = props
1757
1758
  const canvasRef = react.useRef(null)
1758
1759
  const isDrawingRef = react.useRef(false)
1760
+ const undoStackRef = react.useRef([])
1759
1761
  const [brushSize, setBrushSize] = react.useState(24)
1760
- const [toolMode, setToolMode] = react.useState('brush')
1761
- const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1762
- const [copied, setCopied] = react.useState(false)
1762
+ const [toolMode, setToolMode] = react.useState('brush') // 'brush' | 'eraser'
1763
1763
  const [hasStrokes, setHasStrokes] = react.useState(false)
1764
- const undoStackRef = react.useRef([])
1765
-
1766
- const imgUrl = (parsed && (parsed.url || (parsed.attachment && attachmentImageUrl(parsed.attachment)))) || ''
1767
- const targetRef = (parsed && (parsed.attachment?.attachmentId || parsed.path || parsed.url)) || 'current image'
1764
+ const [copied, setCopied] = react.useState(false)
1765
+ const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1768
1766
 
1769
1767
  const initCanvas = react.useCallback(() => {
1770
1768
  const canvas = canvasRef.current
1771
1769
  if (!canvas) return
1772
1770
  const ctx = canvas.getContext('2d')
1773
1771
  if (!ctx) return
1774
- ctx.fillStyle = '#000000'
1772
+ ctx.fillStyle = 'black'
1775
1773
  ctx.fillRect(0, 0, canvas.width, canvas.height)
1776
1774
  undoStackRef.current = [ctx.getImageData(0, 0, canvas.width, canvas.height)]
1777
1775
  }, [])
@@ -1780,9 +1778,7 @@ window.__ModuleLoader__.load({
1780
1778
  initCanvas()
1781
1779
  }, [initCanvas])
1782
1780
 
1783
- function getPos(e) {
1784
- const canvas = canvasRef.current
1785
- if (!canvas) return { x: 0, y: 0 }
1781
+ function getPos(e, canvas) {
1786
1782
  const rect = canvas.getBoundingClientRect()
1787
1783
  const clientX = e.touches ? e.touches[0].clientX : e.clientX
1788
1784
  const clientY = e.touches ? e.touches[0].clientY : e.clientY
@@ -1796,42 +1792,40 @@ window.__ModuleLoader__.load({
1796
1792
 
1797
1793
  function startDraw(e) {
1798
1794
  if (e.touches && e.touches.length > 1) return
1799
- e.preventDefault()
1800
1795
  const canvas = canvasRef.current
1801
1796
  if (!canvas) return
1802
1797
  const ctx = canvas.getContext('2d')
1803
1798
  if (!ctx) return
1804
-
1805
1799
  isDrawingRef.current = true
1806
- const pos = getPos(e)
1800
+ const pos = getPos(e, canvas)
1801
+ ctx.beginPath()
1802
+ ctx.moveTo(pos.x, pos.y)
1807
1803
  ctx.lineWidth = brushSize
1808
1804
  ctx.lineCap = 'round'
1809
1805
  ctx.lineJoin = 'round'
1810
- const color = toolMode === 'eraser' ? '#000000' : '#ffffff'
1806
+ const color = toolMode === 'eraser' ? 'black' : 'white'
1811
1807
  ctx.strokeStyle = color
1812
1808
  ctx.fillStyle = color
1813
1809
 
1814
- ctx.beginPath()
1815
1810
  ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2)
1816
1811
  ctx.fill()
1817
1812
  ctx.beginPath()
1818
1813
  ctx.moveTo(pos.x, pos.y)
1814
+ setHasStrokes(true)
1819
1815
  }
1820
1816
 
1821
1817
  function draw(e) {
1822
1818
  if (!isDrawingRef.current) return
1823
1819
  if (e.touches && e.touches.length > 1) return
1824
- e.preventDefault()
1825
1820
  const canvas = canvasRef.current
1826
1821
  if (!canvas) return
1827
1822
  const ctx = canvas.getContext('2d')
1828
1823
  if (!ctx) return
1829
-
1830
- const pos = getPos(e)
1824
+ const pos = getPos(e, canvas)
1831
1825
  ctx.lineWidth = brushSize
1832
1826
  ctx.lineCap = 'round'
1833
1827
  ctx.lineJoin = 'round'
1834
- const color = toolMode === 'eraser' ? '#000000' : '#ffffff'
1828
+ const color = toolMode === 'eraser' ? 'black' : 'white'
1835
1829
  ctx.strokeStyle = color
1836
1830
  ctx.lineTo(pos.x, pos.y)
1837
1831
  ctx.stroke()
@@ -2005,7 +1999,7 @@ window.__ModuleLoader__.load({
2005
1999
  borderRadius: '8px',
2006
2000
  overflow: 'hidden',
2007
2001
  border: '1px solid var(--dsw-alias-border-l2)',
2008
- background: '#000000',
2002
+ background: 'black',
2009
2003
  margin: '4px 0',
2010
2004
  userSelect: 'none',
2011
2005
  touchAction: 'none',
@@ -2071,9 +2065,11 @@ window.__ModuleLoader__.load({
2071
2065
  )
2072
2066
  )
2073
2067
  }
2074
- // 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286).
2068
+ var InpaintCanvasOverlay = InpaintCanvas
2069
+ // 106-style-matrix-view.js — 2×2 Style Matrix toolview with Blind A/B Compare (#286, #297, #301).
2075
2070
 
2076
2071
  function StyleMatrixCard(props) {
2072
+ const t = props.t || ((k) => k)
2077
2073
  const block = props.block || {}
2078
2074
  const parsed = react.useMemo(() => {
2079
2075
  const raw = block.output || block.text || ''
@@ -2166,7 +2162,7 @@ window.__ModuleLoader__.load({
2166
2162
  left: 0,
2167
2163
  right: 0,
2168
2164
  bottom: 0,
2169
- background: 'rgba(0,0,0,0.85)',
2165
+ background: 'var(--dsw-alias-bg-overlay, rgb(0 0 0 / 85%))',
2170
2166
  display: 'flex',
2171
2167
  alignItems: 'center',
2172
2168
  justifyContent: 'center',
@@ -2183,8 +2179,8 @@ window.__ModuleLoader__.load({
2183
2179
  ) : null
2184
2180
  )
2185
2181
  }
2186
- // -------------------------------------------------------------- Theme Pair Toolview (#189)
2187
- function ThemePairToolView(props) {
2182
+ // -------------------------------------------------------------- Theme Pair Toolview (#189, #296)
2183
+ function ThemePairView(props) {
2188
2184
  const block = props.block
2189
2185
  const t = props.t || ((k) => k)
2190
2186
  const [copiedHtml, setCopiedHtml] = react.useState(false)
@@ -2285,11 +2281,14 @@ window.__ModuleLoader__.load({
2285
2281
  ) : null
2286
2282
  ) : null
2287
2283
  )
2288
- } const en = {
2284
+ }
2285
+ var ThemePairToolView = ThemePairView
2286
+ const en = {
2289
2287
  'settings.navLabel': 'Image Studio',
2290
2288
  'settings.title': 'Image Studio',
2291
2289
  'settings.description': 'AI Image generation, inpainting, variations, upscale & vectorization.',
2292
- 'settings.unavailable': 'Plugin settings are initializing. They will appear automatically in a few seconds.',
2290
+ 'settings.unavailable': 'Plugin settings are unavailable: the host settings namespace is not ready.',
2291
+ 'settings.loading': 'Loading plugin settings…',
2293
2292
  'settings.unsaved': 'Unsaved changes',
2294
2293
  'settings.saving': 'Saving…',
2295
2294
  'settings.save': 'Save settings',
@@ -2602,6 +2601,7 @@ window.__ModuleLoader__.load({
2602
2601
  'settings.reset': '重置',
2603
2602
  'settings.invalidNumber': '请输入有效数值',
2604
2603
  'settings.unavailable': '设置命名空间当前不可用。',
2604
+ 'settings.loading': '正在加载插件设置…',
2605
2605
 
2606
2606
  'f.enabled': '启用图像生成',
2607
2607
  'f.enabledHint': '关闭后将禁用所有图像生成及处理工具。',
@@ -2766,46 +2766,19 @@ window.__ModuleLoader__.load({
2766
2766
  }
2767
2767
 
2768
2768
  // -------------------------------------------------------------- Registration & Apply
2769
- // Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291)
2769
+ // Dual compatibility with DSH 0.1.5-rc.3 (settingsScope) and 0.1.6+ (configForms) (#291, #295)
2770
2770
  const inject = ['slots', 'locale', 'sessions']
2771
2771
 
2772
2772
  function resolveActiveScope(ctx) {
2773
2773
  if (!ctx) return null
2774
- if (ctx.get && typeof ctx.get === 'function') {
2775
- try {
2776
- const lan = ctx.get('lanSettings')
2777
- if (lan && typeof lan.get === 'function') {
2778
- const s = lan.get(SETTINGS_NS)
2779
- if (s) return s
2780
- }
2781
- } catch (_) { /* scope unavailable */ }
2782
- try {
2783
- const cf = ctx.get('configForms')
2784
- if (cf && typeof cf.get === 'function') {
2785
- const s = cf.get(SETTINGS_NS)
2786
- if (s) return s
2787
- }
2788
- } catch (_) { /* scope unavailable */ }
2789
- try {
2790
- const ss = ctx.get('settingsScope')
2791
- if (ss && typeof ss.bind === 'function') {
2792
- const s = ss.bind({ namespace: SETTINGS_NS })
2793
- if (s) return s
2794
- }
2795
- } catch (_) { /* scope unavailable */ }
2796
- }
2797
- if (ctx.configForms && typeof ctx.configForms.get === 'function') {
2798
- try {
2799
- const s = ctx.configForms.get(SETTINGS_NS)
2800
- if (s) return s
2801
- } catch (_) { /* scope unavailable */ }
2802
- }
2803
- if (ctx.settingsScope && typeof ctx.settingsScope.bind === 'function') {
2804
- try {
2805
- const s = ctx.settingsScope.bind({ namespace: SETTINGS_NS })
2806
- if (s) return s
2807
- } catch (_) { /* scope unavailable */ }
2808
- }
2774
+ try {
2775
+ const lan = (ctx.get && ctx.get('lanSettings')) || ctx.lanSettings
2776
+ if (lan?.get) return lan.get(SETTINGS_NS)
2777
+ const cf = (ctx.get && ctx.get('configForms')) || ctx.configForms
2778
+ if (cf?.get) return cf.get(SETTINGS_NS)
2779
+ const ss = (ctx.get && ctx.get('settingsScope')) || ctx.settingsScope
2780
+ if (ss?.bind) return ss.bind({ namespace: SETTINGS_NS })
2781
+ } catch (_) { /* scope unavailable */ }
2809
2782
  return null
2810
2783
  }
2811
2784
 
@@ -2813,70 +2786,93 @@ window.__ModuleLoader__.load({
2813
2786
  const listeners = new Set()
2814
2787
  let activeTarget = resolveActiveScope(ctx)
2815
2788
  let activeUnsub = null
2789
+ let httpSnapshot = { status: 'loading', writable: true, value: {} }
2790
+
2791
+ const notify = () => { for (const fn of listeners) fn() }
2792
+
2793
+ const syncHttpConfig = () => {
2794
+ if (typeof fetch !== 'function') return
2795
+ fetch('/dsh-image-gen/config', { cache: 'no-store' })
2796
+ .then((r) => r.json())
2797
+ .then((data) => {
2798
+ if (data && data.ok && data.config) {
2799
+ httpSnapshot = { status: 'ready', writable: true, value: data.config }
2800
+ notify()
2801
+ }
2802
+ })
2803
+ .catch(() => {})
2804
+ }
2805
+ syncHttpConfig()
2816
2806
 
2817
2807
  const bindTarget = (target) => {
2818
2808
  if (!target || target === activeTarget) return
2819
2809
  if (typeof activeUnsub === 'function') {
2820
- try { activeUnsub() } catch (_) { /* scope unavailable */ }
2810
+ try { activeUnsub() } catch { /* ignore */ }
2821
2811
  activeUnsub = null
2822
2812
  }
2823
2813
  activeTarget = target
2824
2814
  if (typeof activeTarget.subscribe === 'function') {
2825
2815
  try {
2826
- activeUnsub = activeTarget.subscribe(() => {
2827
- for (const fn of listeners) fn()
2828
- })
2829
- } catch (_) { /* scope unavailable */ }
2816
+ activeUnsub = activeTarget.subscribe(notify)
2817
+ } catch { /* ignore */ }
2830
2818
  }
2831
- for (const fn of listeners) fn()
2819
+ notify()
2832
2820
  }
2833
2821
 
2834
- if (activeTarget && typeof activeTarget.subscribe === 'function') {
2835
- try {
2836
- activeUnsub = activeTarget.subscribe(() => {
2837
- for (const fn of listeners) fn()
2838
- })
2839
- } catch (_) { /* scope unavailable */ }
2822
+ if (activeTarget?.subscribe) {
2823
+ try { activeUnsub = activeTarget.subscribe(notify) } catch { /* ignore */ }
2840
2824
  }
2841
2825
 
2842
2826
  if (typeof ctx?.inject === 'function') {
2843
2827
  try {
2844
2828
  ctx.inject(['configForms'], (sctx) => {
2845
- try {
2846
- const s = sctx?.configForms?.get?.(SETTINGS_NS)
2847
- if (s) bindTarget(s)
2848
- } catch (_) { /* scope unavailable */ }
2829
+ const s = sctx?.configForms?.get?.(SETTINGS_NS)
2830
+ if (s) bindTarget(s)
2849
2831
  })
2850
- } catch (_) { /* scope unavailable */ }
2851
- try {
2852
2832
  ctx.inject(['settingsScope'], (sctx) => {
2853
- try {
2854
- const s = sctx?.settingsScope?.bind?.({ namespace: SETTINGS_NS })
2855
- if (s) bindTarget(s)
2856
- } catch (_) { /* scope unavailable */ }
2833
+ const s = sctx?.settingsScope?.bind?.({ namespace: SETTINGS_NS })
2834
+ if (s) bindTarget(s)
2857
2835
  })
2858
- } catch (_) { /* scope unavailable */ }
2836
+ } catch { /* ignore */ }
2859
2837
  }
2860
2838
 
2861
2839
  return {
2862
2840
  getSnapshot() {
2863
2841
  const target = activeTarget || resolveActiveScope(ctx)
2864
- if (target && typeof target.getSnapshot === 'function') {
2865
- return target.getSnapshot()
2866
- }
2867
- return { status: 'unavailable', writable: false, value: {} }
2842
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2843
+ if (snap?.status === 'ready' && snap.writable) return snap
2844
+ if (httpSnapshot.status === 'ready') return httpSnapshot
2845
+ return snap || httpSnapshot
2868
2846
  },
2869
2847
  async set(key, val) {
2870
2848
  const target = activeTarget || resolveActiveScope(ctx)
2871
- if (target && typeof target.set === 'function') {
2872
- return target.set(key, val)
2873
- }
2849
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2850
+ if (snap?.status === 'ready' && snap.writable && target.set) return target.set(key, val)
2851
+ httpSnapshot = { ...httpSnapshot, value: { ...httpSnapshot.value, [key]: val } }
2852
+ notify()
2853
+ try {
2854
+ await fetch('/dsh-image-gen/config', {
2855
+ method: 'PUT',
2856
+ headers: { 'Content-Type': 'application/json' },
2857
+ body: JSON.stringify({ [key]: val }),
2858
+ })
2859
+ } catch { /* ignore */ }
2874
2860
  },
2875
2861
  async delete(key) {
2876
2862
  const target = activeTarget || resolveActiveScope(ctx)
2877
- if (target && typeof target.delete === 'function') {
2878
- return target.delete(key)
2879
- }
2863
+ const snap = target?.getSnapshot ? target.getSnapshot() : null
2864
+ if (snap?.status === 'ready' && snap.writable && target.delete) return target.delete(key)
2865
+ const nextVal = { ...httpSnapshot.value }
2866
+ delete nextVal[key]
2867
+ httpSnapshot = { ...httpSnapshot, value: nextVal }
2868
+ notify()
2869
+ try {
2870
+ await fetch('/dsh-image-gen/config', {
2871
+ method: 'PUT',
2872
+ headers: { 'Content-Type': 'application/json' },
2873
+ body: JSON.stringify({ [key]: undefined }),
2874
+ })
2875
+ } catch { /* ignore */ }
2880
2876
  },
2881
2877
  subscribe(fn) {
2882
2878
  listeners.add(fn)
@@ -2905,27 +2901,17 @@ window.__ModuleLoader__.load({
2905
2901
  if (typeof ctx.slots.inject === 'function') {
2906
2902
  try {
2907
2903
  ctx.slots.inject(slotName, () => {
2908
- try {
2909
- return registerFn()
2910
- } catch (err) {
2911
- console.warn('[dsh-image-gen] Error registering slot ' + slotName + ':', err)
2912
- }
2904
+ try { return registerFn() } catch (err) { console.warn('[dsh-image-gen] Slot err ' + slotName + ':', err) }
2913
2905
  })
2914
2906
  return
2915
- } catch (err) {
2916
- console.warn('[dsh-image-gen] Failed to inject slot ' + slotName + ':', err)
2917
- }
2907
+ } catch (err) { console.warn('[dsh-image-gen] Failed inject ' + slotName + ':', err) }
2918
2908
  }
2919
2909
  if (typeof ctx.slots.register === 'function') {
2920
- try {
2921
- registerFn()
2922
- } catch (err) {
2923
- console.warn('[dsh-image-gen] Failed direct registration for ' + slotName + ':', err)
2924
- }
2910
+ try { registerFn() } catch (err) { console.warn('[dsh-image-gen] Direct reg err ' + slotName + ':', err) }
2925
2911
  }
2926
2912
  }
2927
2913
 
2928
- // Register toolviews for all 8 visual tools
2914
+ // Register toolviews for all visual tools
2929
2915
  const toolviewTools = [
2930
2916
  'generate_image',
2931
2917
  'edit_image',
@@ -2981,7 +2967,6 @@ window.__ModuleLoader__.load({
2981
2967
  )
2982
2968
  )
2983
2969
 
2984
-
2985
2970
  // Native Sidebar Right Pane Tab & BetterSidebar
2986
2971
  if (typeof ctx.inject === 'function') {
2987
2972
  try {
@@ -3035,7 +3020,7 @@ window.__ModuleLoader__.load({
3035
3020
  console.warn('[dsh-image-gen] native sidebar registration failed', e)
3036
3021
  }
3037
3022
  })
3038
- } catch (e) { /* native sidebar unavailable */ }
3023
+ } catch (_) { /* native sidebar unavailable */ }
3039
3024
 
3040
3025
  try {
3041
3026
  ctx.inject(['betterSidebar'], (sctx) => {
@@ -3053,7 +3038,7 @@ window.__ModuleLoader__.load({
3053
3038
  console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
3054
3039
  }
3055
3040
  })
3056
- } catch (e) { /* betterSidebar unavailable */ }
3041
+ } catch (_) { /* betterSidebar unavailable */ }
3057
3042
  }
3058
3043
 
3059
3044
  // Conversation header utilities chip (quick-access gallery button in chat header)
@@ -3070,22 +3055,7 @@ window.__ModuleLoader__.load({
3070
3055
  )
3071
3056
  )
3072
3057
 
3073
- // List seat (plugins.item)
3074
- registerSlotWhenReady('plugins.item', () =>
3075
- ctx.slots.register(
3076
- {
3077
- name: 'plugins.item',
3078
- id: 'dsh-image-gen',
3079
- order: 60,
3080
- label: () => 'Image Studio',
3081
- locale: NS,
3082
- inject: () => cardOnce().inject(),
3083
- },
3084
- (props) => react.createElement(ErrorBoundary, null, react.createElement(FalSettingsCard, props))
3085
- )
3086
- )
3087
-
3088
- // Settings card item (legacy seat, kept as a fallback)
3058
+ // Settings card item — sole slot registration for settings card (#208, #300)
3089
3059
  registerSlotWhenReady('settings.plugin.item', () =>
3090
3060
  ctx.slots.register(
3091
3061
  {
@@ -3098,7 +3068,6 @@ window.__ModuleLoader__.load({
3098
3068
  )
3099
3069
  )
3100
3070
  }
3101
-
3102
3071
  // -------------------------------------------------------------- Asset Vault View (#159)
3103
3072
  function AssetVaultView(props) {
3104
3073
  const { ctx, onSelectAsset } = props
@@ -3146,7 +3115,7 @@ window.__ModuleLoader__.load({
3146
3115
  }, [q, provider, aspect, sort])
3147
3116
 
3148
3117
  const handleDelete = (id) => {
3149
- if (!confirm('Are you sure you want to delete this asset?')) return
3118
+ if (!window.confirm('Are you sure you want to delete this asset?')) return
3150
3119
  fetch('/dsh-image-gen/vault?id=' + encodeURIComponent(id), { method: 'DELETE' })
3151
3120
  .then((r) => r.json())
3152
3121
  .then((res) => {
@@ -3342,7 +3311,7 @@ window.__ModuleLoader__.load({
3342
3311
  const [isFullscreen, setIsFullscreen] = react.useState(false)
3343
3312
  const [layoutGrid, setLayoutGrid] = react.useState('1x1') // '1x1' | '2x2' | '1x4'
3344
3313
  const [prompt, setPrompt] = react.useState(() => {
3345
- try { return localStorage.getItem('dsh_studio_prompt') || '' } catch (_) { return '' }
3314
+ try { return window.localStorage.getItem('dsh_studio_prompt') || '' } catch (_) { return '' }
3346
3315
  })
3347
3316
  const [stylePreset, setStylePreset] = react.useState('none')
3348
3317
  const [aspectRatio, setAspectRatio] = react.useState('1:1')
@@ -3381,7 +3350,7 @@ window.__ModuleLoader__.load({
3381
3350
 
3382
3351
  const updatePrompt = (val) => {
3383
3352
  setPrompt(val)
3384
- try { localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage unavailable */ }
3353
+ try { window.localStorage.setItem('dsh_studio_prompt', val) } catch (_) { /* storage unavailable */ }
3385
3354
  }
3386
3355
 
3387
3356
  const handleSelectFromVault = (asset) => {
@@ -35,6 +35,25 @@ export function isFatalPromptError(error) {
35
35
  export function isRetryableProviderError(error) {
36
36
  if (isFatalPromptError(error)) return false
37
37
  const msg = (error?.message || String(error || '')).toLowerCase()
38
+
39
+ // Non-retryable: authentication, authorization, or invalid request / client configuration errors
40
+ if (
41
+ msg.includes('401') ||
42
+ msg.includes('403') ||
43
+ msg.includes('unauthorized') ||
44
+ msg.includes('forbidden') ||
45
+ msg.includes('invalid api key') ||
46
+ msg.includes('invalid key') ||
47
+ msg.includes('api key missing') ||
48
+ msg.includes('api_key_invalid') ||
49
+ msg.includes('bad request') ||
50
+ msg.includes('invalid argument') ||
51
+ msg.includes('invalid parameter') ||
52
+ msg.includes('400')
53
+ ) {
54
+ return false
55
+ }
56
+
38
57
  return (
39
58
  msg.includes('429') ||
40
59
  msg.includes('rate limit') ||
@@ -57,10 +76,9 @@ export function isRetryableProviderError(error) {
57
76
  msg.includes('balance is insufficient') ||
58
77
  msg.includes('quota') ||
59
78
  msg.includes('credit') ||
60
- msg.includes('unauthorized') ||
61
- msg.includes('401') ||
62
- msg.includes('invalid api key') ||
63
79
  msg.includes('econnrefused') ||
80
+ msg.includes('econnreset') ||
81
+ msg.includes('etimedout') ||
64
82
  msg.includes('enotfound') ||
65
83
  msg.includes('fetch failed') ||
66
84
  msg.includes('network')
@@ -150,6 +168,10 @@ export async function executeWithFallback(generators, chain, seed, prompt, optio
150
168
  throw new Error(`Content or policy error on ${providerKey} (not cascading): ${formatted}`)
151
169
  }
152
170
 
171
+ if (!isRetryableProviderError(err)) {
172
+ throw new Error(`Non-retryable provider error on ${providerKey} (not cascading): ${formatted}`)
173
+ }
174
+
153
175
  if (logger && typeof logger.warn === 'function') {
154
176
  logger.warn(`[dsh-image-gen] Provider ${providerKey} failed: ${formatted}. Trying next candidate...`)
155
177
  }