@openchambery/web 1.19.19 → 1.19.20-beta.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.
Files changed (86) hide show
  1. package/dist/assets/{AgentAvatar-BRfqdWCk.js → AgentAvatar-B6-RdwKY.js} +1 -1
  2. package/dist/assets/{AssistantView-DaQygS2g.js → AssistantView-DMTsVKM3.js} +3 -3
  3. package/dist/assets/{DiagramView-DU13N_4L.js → DiagramView-BBL9oI1k.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-C6tR_fyF.js → FadeInOnReveal-BbBpeOth.js} +1 -1
  5. package/dist/assets/{Icon-B5uh1TBO.js → Icon-CCkKTz-u.js} +1 -1
  6. package/dist/assets/MarkdownRenderer-DgBpy0ML.js +2 -0
  7. package/dist/assets/{MarkdownRendererImpl-CfPlhzKB.js → MarkdownRendererImpl-oe15Jjl0.js} +1 -1
  8. package/dist/assets/{MarkstreamRendererImpl-BZu0kqan.js → MarkstreamRendererImpl-C8d0NDM_.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-DeLsBXQl.js → MobileDetailNavigation-DaOH9vG9.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-6jsuuuL-.js → MobileShareBridge-B5EEs8Dl.js} +2 -2
  11. package/dist/assets/MobileSurface-DN_xSpOw.js +2 -0
  12. package/dist/assets/{MultiRunWindow-EN15uCln.js → MultiRunWindow-DQkpVpO5.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-_lWjW-Z6.js → OnboardingScreen-DMXidDTr.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-DfKTImP7.js → OverlayScrollbar-CrlChx-Y.js} +1 -1
  15. package/dist/assets/{SettingsView-Cm-fwFNM.js → SettingsView-BFSKcf2Z.js} +2 -2
  16. package/dist/assets/{SettingsWindow-CSBXDRrg.js → SettingsWindow-DjZQR7lN.js} +1 -1
  17. package/dist/assets/{TerminalView-CWRwefBO.js → TerminalView-CK50XlAX.js} +1 -1
  18. package/dist/assets/{ToolOutputDialog-CJFSAiw6.js → ToolOutputDialog-BUQeVPtR.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-BCBPxOn0.js → appThemeRegistry-BqqyByZY.js} +1 -1
  20. package/dist/assets/{button-B1A-UMjO.js → button-BS1ba7R-.js} +1 -1
  21. package/dist/assets/{collapsible-CMDtiFan.js → collapsible-DfCfMPYL.js} +1 -1
  22. package/dist/assets/{context-menu-DhqbwEHq.js → context-menu-CtSnP8-Q.js} +1 -1
  23. package/dist/assets/{desktopHosts-B052a4UK.js → desktopHosts-B1ixEylr.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-OjKNXr10.js → desktopRelayRestore-4JmFUw8r.js} +1 -1
  25. package/dist/assets/{diagram-Cp2-31s3.js → diagram-CTd-5KxX.js} +1 -1
  26. package/dist/assets/{dialog-Ck2GVXYI.js → dialog-CnoVLRpn.js} +1 -1
  27. package/dist/assets/{es-0Yrlq6Oy.js → es-rqMiRP7-.js} +1 -1
  28. package/dist/assets/{fileMentionTouchSelection-LxAU-jJG.js → fileMentionTouchSelection-EP-6eAzk.js} +1 -1
  29. package/dist/assets/{fileQueries-B_s_B-fH.js → fileQueries-DQc8EJUf.js} +1 -1
  30. package/dist/assets/{fr-AXS_d4Ac.js → fr-DsBZeN8Y.js} +1 -1
  31. package/dist/assets/{gitApi-BSN7ZfSp.js → gitApi-BYPtCCvV.js} +1 -1
  32. package/dist/assets/{input-DVaUSQqw.js → input-CPygKZwH.js} +1 -1
  33. package/dist/assets/{insertionBoundaries-DiTLBzlO.js → insertionBoundaries-BLQTgGzH.js} +1 -1
  34. package/dist/assets/{ja-iISfz4W8.js → ja-Bgr8-dOg.js} +1 -1
  35. package/dist/assets/{ko-DeN9jk1E.js → ko-B_Gxh-0l.js} +1 -1
  36. package/dist/assets/{main-BbJQ9Vtx.js → main-CFAEmyuW.js} +2 -2
  37. package/dist/assets/main-DDg5K0wL.js +169 -0
  38. package/dist/assets/{markdownHeightCache-BWYOoF-W.js → markdownHeightCache-i3SmzGmZ.js} +1 -1
  39. package/dist/assets/{miniChat-DaZ0MUes.js → miniChat-CcpkS8Vo.js} +2 -2
  40. package/dist/assets/{mobile-ctmwtKIg.js → mobile-DiFR05ZK.js} +2 -2
  41. package/dist/assets/{multirun-9Z45iiC7.js → multirun-YRfDzmpg.js} +1 -1
  42. package/dist/assets/{persistence-DwRBC4zl.js → persistence-Ce3AnpEr.js} +2 -2
  43. package/dist/assets/{pl-C32gRuHl.js → pl-DW2XJGC4.js} +1 -1
  44. package/dist/assets/{platform-DQ5_LNKO.js → platform-0w_Q9_ji.js} +1 -1
  45. package/dist/assets/{projectMeta-CIGtyoau.js → projectMeta-DCyCHsMx.js} +1 -1
  46. package/dist/assets/{pt-BR-CodLmzOe.js → pt-BR-CIsanf4T.js} +1 -1
  47. package/dist/assets/{renderElectronMiniChatApp-Bpmux7pj.js → renderElectronMiniChatApp-B3Mk5Lrm.js} +2 -2
  48. package/dist/assets/{renderMobileApp-DYlwZgHu.js → renderMobileApp-1kjmaG5P.js} +4 -4
  49. package/dist/assets/{runtimeConfig-B0raYq9L.js → runtimeConfig-C4CpRLk-.js} +1 -1
  50. package/dist/assets/{runtimeEndpointReset-D09YtB1G.js → runtimeEndpointReset-CAycS8QL.js} +1 -1
  51. package/dist/assets/{sessionChangeSummary-B2KglTnI.js → sessionChangeSummary-BIBjNW1j.js} +1 -1
  52. package/dist/assets/{sessionLookup-CBlhAA0q.js → sessionLookup-BMfwaIF2.js} +1 -1
  53. package/dist/assets/{sortable-tabs-strip-CSQUAwi0.js → sortable-tabs-strip-D3ZwNg1m.js} +1 -1
  54. package/dist/assets/{store-BBmfPqaV.js → store-CIwMPVXR.js} +2 -2
  55. package/dist/assets/{terminalApi-CfgOfnZk.js → terminalApi-xjqssKke.js} +1 -1
  56. package/dist/assets/{textarea-C0GHF5Of.js → textarea-KQwYSuvU.js} +1 -1
  57. package/dist/assets/{tooltip-C5FMIBsd.js → tooltip-BkfF78IS.js} +1 -1
  58. package/dist/assets/{uk-L_pdbOtS.js → uk-BPmPEUYJ.js} +1 -1
  59. package/dist/assets/{url-CMQwHq4k.js → url-_z7evzdK.js} +1 -1
  60. package/dist/assets/{useAppFontEffects-CDbz3983.js → useAppFontEffects-B25qIF_R.js} +5 -5
  61. package/dist/assets/{useAssistantUIStore-CLPCNoj_.js → useAssistantUIStore-CMutVh9B.js} +1 -1
  62. package/dist/assets/{useDesktopWindowControlsLayout-DNqYfyg5.js → useDesktopWindowControlsLayout-DzC1-UAo.js} +1 -1
  63. package/dist/assets/{useEffectiveDirectory-B_8RoAcb.js → useEffectiveDirectory-CGHtjqdu.js} +1 -1
  64. package/dist/assets/{useFontPreferences-DCaY8NbU.js → useFontPreferences-B1JYyzpK.js} +1 -1
  65. package/dist/assets/{useMobileNavigationStore-Btsf88wS.js → useMobileNavigationStore-CgkuSW74.js} +4 -4
  66. package/dist/assets/{useSessionAutoCleanup-B7uBJZxU.js → useSessionAutoCleanup-v1zEP2Id.js} +1 -1
  67. package/dist/assets/useUIStore-Bxv3f23A.js +1 -0
  68. package/dist/assets/{useWorkerHighlightedLines-CPzF9843.js → useWorkerHighlightedLines-C_zH7AcW.js} +1 -1
  69. package/dist/assets/{zh-CN-CwyG5bxD.js → zh-CN-IWKl_PjK.js} +1 -1
  70. package/dist/assets/{zh-TW-Ba7atZ7i.js → zh-TW-Doq-iQ7V.js} +1 -1
  71. package/dist/index.html +8 -8
  72. package/dist/mini-chat.html +8 -8
  73. package/dist/mobile.html +8 -8
  74. package/package.json +1 -1
  75. package/server/lib/opencode/settings-helpers.js +6 -0
  76. package/server/lib/opencode/settings-helpers.test.js +3 -0
  77. package/server/lib/small-model/DOCUMENTATION.md +33 -14
  78. package/server/lib/small-model/custom-api.js +141 -0
  79. package/server/lib/small-model/custom-api.test.js +101 -0
  80. package/server/lib/small-model/index.js +91 -27
  81. package/server/lib/small-model/routes.js +22 -0
  82. package/server/lib/small-model/summary-settings.test.js +85 -1
  83. package/dist/assets/MarkdownRenderer-CQHRhqB1.js +0 -2
  84. package/dist/assets/MobileSurface-DCP4BC35.js +0 -2
  85. package/dist/assets/main-DFRHWaDr.js +0 -169
  86. package/dist/assets/useUIStore-CxOoCv7B.js +0 -1
package/dist/index.html CHANGED
@@ -552,27 +552,27 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-BbJQ9Vtx.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-CFAEmyuW.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
559
559
  <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-zIalUnbb.js">
560
560
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
561
561
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
562
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-CxOoCv7B.js">
562
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Bxv3f23A.js">
563
563
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
564
564
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
565
565
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
566
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwRBC4zl.js">
567
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B052a4UK.js">
568
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-OjKNXr10.js">
566
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Ce3AnpEr.js">
567
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B1ixEylr.js">
568
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-4JmFUw8r.js">
569
569
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
570
570
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
571
- <link rel="modulepreload" crossorigin href="/assets/platform-DQ5_LNKO.js">
572
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
571
+ <link rel="modulepreload" crossorigin href="/assets/platform-0w_Q9_ji.js">
572
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-xjqssKke.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B0raYq9L.js">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-C4CpRLk-.js">
576
576
  <link rel="stylesheet" crossorigin href="/assets/src-DZ60kHBP.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
@@ -74,27 +74,27 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-DaZ0MUes.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-CcpkS8Vo.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-zIalUnbb.js">
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
84
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-CxOoCv7B.js">
84
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Bxv3f23A.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
86
86
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
87
87
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
88
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwRBC4zl.js">
89
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B052a4UK.js">
90
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-OjKNXr10.js">
88
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Ce3AnpEr.js">
89
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B1ixEylr.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-4JmFUw8r.js">
91
91
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
93
- <link rel="modulepreload" crossorigin href="/assets/platform-DQ5_LNKO.js">
94
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
93
+ <link rel="modulepreload" crossorigin href="/assets/platform-0w_Q9_ji.js">
94
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-xjqssKke.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B0raYq9L.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-C4CpRLk-.js">
98
98
  <link rel="stylesheet" crossorigin href="/assets/src-DZ60kHBP.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
package/dist/mobile.html CHANGED
@@ -63,27 +63,27 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-ctmwtKIg.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-DiFR05ZK.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
70
70
  <link rel="modulepreload" crossorigin href="/assets/runtime-fetch-zIalUnbb.js">
71
71
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
72
72
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
73
- <link rel="modulepreload" crossorigin href="/assets/useUIStore-CxOoCv7B.js">
73
+ <link rel="modulepreload" crossorigin href="/assets/useUIStore-Bxv3f23A.js">
74
74
  <link rel="modulepreload" crossorigin href="/assets/vendor-pathe-vBmvQE8f.js">
75
75
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DLPElpoQ.js">
76
76
  <link rel="modulepreload" crossorigin href="/assets/vendor-tanstack-query-core-BWz0ose6.js">
77
- <link rel="modulepreload" crossorigin href="/assets/persistence-DwRBC4zl.js">
78
- <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B052a4UK.js">
79
- <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-OjKNXr10.js">
77
+ <link rel="modulepreload" crossorigin href="/assets/persistence-Ce3AnpEr.js">
78
+ <link rel="modulepreload" crossorigin href="/assets/desktopHosts-B1ixEylr.js">
79
+ <link rel="modulepreload" crossorigin href="/assets/desktopRelayRestore-4JmFUw8r.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-opencode-sdk-C07HQypa.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/vendor-aparajita-capacitor-secure-storage-CjQdwfjS.js">
82
- <link rel="modulepreload" crossorigin href="/assets/platform-DQ5_LNKO.js">
83
- <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
82
+ <link rel="modulepreload" crossorigin href="/assets/platform-0w_Q9_ji.js">
83
+ <link rel="modulepreload" crossorigin href="/assets/terminalApi-xjqssKke.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-B0raYq9L.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-C4CpRLk-.js">
87
87
  <link rel="stylesheet" crossorigin href="/assets/src-DZ60kHBP.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "1.19.19",
3
+ "version": "1.19.20-beta.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -452,6 +452,12 @@ export const createSettingsHelpers = (dependencies) => {
452
452
  ? trimmed.slice(0, SUMMARY_MODEL_ID_MAX_LENGTH)
453
453
  : undefined;
454
454
  }
455
+ if (typeof candidate.summaryCustomModelID === 'string') {
456
+ const trimmed = candidate.summaryCustomModelID.trim();
457
+ result.summaryCustomModelID = trimmed.length > 0
458
+ ? trimmed.slice(0, SUMMARY_MODEL_ID_MAX_LENGTH)
459
+ : undefined;
460
+ }
455
461
  if (typeof candidate.summaryCustomBaseURL === 'string') {
456
462
  const trimmed = candidate.summaryCustomBaseURL.trim();
457
463
  if (!trimmed) {
@@ -45,6 +45,7 @@ describe('settings helpers', () => {
45
45
  summaryModelMode: 'custom',
46
46
  summaryCustomBaseURL: 'https://summary.example.test/v1/',
47
47
  summaryModelID: 'summary-model',
48
+ summaryCustomModelID: 'custom-model',
48
49
  summaryCustomAPIToken: 'summary-token',
49
50
  summaryCommitPrompt: 'Return commit JSON.',
50
51
  summarySessionTitlePrompt: 'Return a short title.',
@@ -54,6 +55,7 @@ describe('settings helpers', () => {
54
55
  summaryModelMode: 'custom',
55
56
  summaryCustomBaseURL: 'https://summary.example.test/v1',
56
57
  summaryModelID: 'summary-model',
58
+ summaryCustomModelID: 'custom-model',
57
59
  summaryCustomAPIToken: 'summary-token',
58
60
  summaryCommitPrompt: 'Return commit JSON.',
59
61
  summarySessionTitlePrompt: 'Return a short title.',
@@ -63,6 +65,7 @@ describe('settings helpers', () => {
63
65
  summaryModelMode: 'custom',
64
66
  summaryCustomBaseURL: 'https://summary.example.test/v1',
65
67
  summaryModelID: 'summary-model',
68
+ summaryCustomModelID: 'custom-model',
66
69
  hasSummaryCustomAPIToken: true,
67
70
  });
68
71
  expect(response).not.toHaveProperty('summaryCustomAPIToken');
@@ -1,7 +1,10 @@
1
1
  # Small Model
2
2
 
3
- Server-side LLM calls that reuse the user's existing OpenCode provider logins
4
- (`~/.local/share/opencode/auth.json`). OpenCode uses a "small model" internally
3
+ Server-side LLM calls that reuse the user's existing OpenCode provider logins.
4
+ Dedicated adapters still read `~/.local/share/opencode/auth.json`. Plugin and
5
+ other non-dedicated providers are listed from OpenCode's connected catalog
6
+ (`provider.list()` ∩ `config.providers`, same as `loadConnectedCatalog`).
7
+ OpenCode uses a "small model" internally
5
8
  (titles, summaries) but does not expose it through the SDK or plugins — this
6
9
  module replicates that mechanism as an OpenChamber runtime API.
7
10
 
@@ -83,12 +86,18 @@ every other runtime API.
83
86
  `cost.input` / `cost.output`, `model.api.url`, provider `name`. Client-safe
84
87
  provider catalog projection remains in `opencode/provider-catalog.js` for
85
88
  `/api/config/catalog/providers`.
86
- - `routes.js` — `GET /api/small-model` (resolution preview) and
89
+ - `routes.js` — `GET /api/small-model` (resolution preview),
87
90
  `POST /api/small-model/generate` (`{ prompt, system?, maxOutputTokens?,
88
- model?, directory? }` → `{ text, providerID, modelID, source }`).
89
- The preview response includes `callableModels`, the Provider/model allowlist
90
- consumed by Settings → Summary AI. Callable lists receive the same
91
- `directory` as describe/generate so they share the directory catalog.
91
+ model?, directory? }` → `{ text, providerID, modelID, source }`),
92
+ `POST /api/small-model/test` (custom OpenAI-compatible probe → `{ ok, code? }`),
93
+ and `POST /api/small-model/custom-models` (best-effort `{baseURL}/models`
94
+ suggestions → `{ models }`). The preview response includes `callableModels`,
95
+ the Provider/model allowlist consumed by Settings → Summary AI. Callable
96
+ lists receive the same `directory` as describe/generate so they share the
97
+ directory catalog.
98
+ - `custom-api.js` — custom Summary AI probe and model listing. Failure codes
99
+ are `incomplete`, `token` (401/403), `model` (404 / `model_not_found`), or
100
+ `baseURL` (network / non-OpenAI-compatible). Tokens are never logged.
92
101
 
93
102
  ## Dispatch
94
103
 
@@ -98,10 +107,16 @@ every other runtime API.
98
107
  | `openai` / `anthropic` / `google` / `github-copilot` with usable dedicated auth | Direct dedicated adapter (`call.js`) |
99
108
  | Any other usable auth + catalog model (plugin providers, …) | Temporary OpenCode session (`opencode-session.js`) |
100
109
 
101
- Callable Settings lists include every `isUsableAuthEntry` provider that has at
102
- least one catalog model id (no `api.url` gate). OpenAI OAuth still surfaces
103
- only `gpt-5.4-mini`; Copilot only `gpt-5.4-nano`; Copilot auth aliases merge
104
- to `github-copilot`.
110
+ Callable Settings lists:
111
+ - Dedicated adapters (`openai`, `anthropic`, `google`, `github-copilot`) stay
112
+ gated by usable `auth.json` entries and the existing small-model whitelist
113
+ (OpenAI OAuth → `gpt-5.4-mini`; Copilot → `gpt-5.4-nano`).
114
+ - Every other provider is taken from the connected catalog (plugin / env
115
+ credentials that OpenCode reports as connected), intersected with catalog
116
+ model ids. No `api.url` gate. If the connected catalog request fails, the
117
+ list falls back to usable `auth.json` plugin entries rather than pretending
118
+ none exist.
119
+ Copilot auth aliases merge to `github-copilot`.
105
120
 
106
121
  ## OpenCode session lifecycle
107
122
 
@@ -149,9 +164,13 @@ directly.
149
164
  Commit-message generation and session-title refresh pass `purpose: 'commit'`
150
165
  or `purpose: 'session-title'` to `generateSmallModelText`. Settings → Summary
151
166
  AI can select an authenticated OpenCode provider/model or a custom
152
- OpenAI-compatible `baseURL`, model ID, and API token. A custom token stays in
153
- the server settings file; settings read responses expose only
154
- `hasSummaryCustomAPIToken`.
167
+ OpenAI-compatible `baseURL`, model ID, and API token. Custom mode stores the
168
+ model id in `summaryCustomModelID` (legacy `summaryModelID` is still read when
169
+ that field is absent). Custom mode is enabled only when base URL, model ID,
170
+ and API token are all present. A custom token stays in the server settings
171
+ file; settings read responses expose only `hasSummaryCustomAPIToken`. Settings
172
+ can probe the custom API (`POST /api/small-model/test`) and load model
173
+ suggestions from `{baseURL}/models` without blocking free-form entry.
155
174
 
156
175
  `summaryCommitPrompt` and `summarySessionTitlePrompt` replace the respective
157
176
  call's system prompt when non-empty. With no persisted provider choice, summary
@@ -0,0 +1,141 @@
1
+ const TEST_TIMEOUT_MS = 15_000;
2
+ const MODELS_TIMEOUT_MS = 8_000;
3
+
4
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
5
+
6
+ export function parseCustomApiBaseURL(value) {
7
+ if (typeof value !== 'string') return null;
8
+ const trimmed = value.trim();
9
+ if (!trimmed) return null;
10
+ try {
11
+ const url = new URL(trimmed);
12
+ if ((url.protocol !== 'https:' && url.protocol !== 'http:') || url.username || url.password) {
13
+ return null;
14
+ }
15
+ return url.toString().replace(/\/+$/, '');
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function classifyCustomSummaryApiFailure({ status, bodyText, cause } = {}) {
22
+ const text = typeof bodyText === 'string' ? bodyText : '';
23
+ if (status === 401 || status === 403) return 'token';
24
+ if (status === 404) return 'model';
25
+ if (/model_not_found|invalid_model|model[_ ]?not[_ ]found/i.test(text)) return 'model';
26
+ if (status === 400 && /model/i.test(text)) return 'model';
27
+
28
+ const code = typeof cause?.code === 'string' ? cause.code : '';
29
+ const name = typeof cause?.name === 'string' ? cause.name : '';
30
+ if (
31
+ name === 'AbortError'
32
+ || name === 'TimeoutError'
33
+ || code === 'ENOTFOUND'
34
+ || code === 'ECONNREFUSED'
35
+ || code === 'ECONNRESET'
36
+ || code === 'ERR_INVALID_URL'
37
+ || code === 'ENETUNREACH'
38
+ ) {
39
+ return 'baseURL';
40
+ }
41
+ if (!status) return 'baseURL';
42
+ return 'baseURL';
43
+ }
44
+
45
+ const parseJson = (bodyText) => {
46
+ try {
47
+ return JSON.parse(bodyText);
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ export async function testCustomSummaryApi({
54
+ baseURL,
55
+ modelID,
56
+ apiToken,
57
+ fetchImpl = globalThis.fetch.bind(globalThis),
58
+ } = {}) {
59
+ const parsedBaseURL = parseCustomApiBaseURL(baseURL);
60
+ const trimmedModelID = typeof modelID === 'string' ? modelID.trim() : '';
61
+ const trimmedToken = typeof apiToken === 'string' ? apiToken.trim() : '';
62
+ if (!parsedBaseURL || !trimmedModelID || !trimmedToken) {
63
+ return { ok: false, code: 'incomplete' };
64
+ }
65
+
66
+ try {
67
+ const response = await fetchImpl(`${parsedBaseURL}/chat/completions`, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ Accept: 'application/json',
72
+ Authorization: `Bearer ${trimmedToken}`,
73
+ },
74
+ body: JSON.stringify({
75
+ model: trimmedModelID,
76
+ messages: [{ role: 'user', content: 'ping' }],
77
+ max_tokens: 1,
78
+ stream: false,
79
+ }),
80
+ signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
81
+ });
82
+ const bodyText = await response.text().catch(() => '');
83
+ if (!response.ok) {
84
+ return {
85
+ ok: false,
86
+ code: classifyCustomSummaryApiFailure({ status: response.status, bodyText }),
87
+ };
88
+ }
89
+ const payload = parseJson(bodyText);
90
+ if (!isRecord(payload) || !Array.isArray(payload.choices)) {
91
+ return { ok: false, code: 'baseURL' };
92
+ }
93
+ return { ok: true };
94
+ } catch (error) {
95
+ return { ok: false, code: classifyCustomSummaryApiFailure({ cause: error }) };
96
+ }
97
+ }
98
+
99
+ export async function listCustomSummaryModels({
100
+ baseURL,
101
+ apiToken,
102
+ fetchImpl = globalThis.fetch.bind(globalThis),
103
+ } = {}) {
104
+ const parsedBaseURL = parseCustomApiBaseURL(baseURL);
105
+ const trimmedToken = typeof apiToken === 'string' ? apiToken.trim() : '';
106
+ if (!parsedBaseURL || !trimmedToken) {
107
+ return [];
108
+ }
109
+
110
+ try {
111
+ const response = await fetchImpl(`${parsedBaseURL}/models`, {
112
+ method: 'GET',
113
+ headers: {
114
+ Accept: 'application/json',
115
+ Authorization: `Bearer ${trimmedToken}`,
116
+ },
117
+ signal: AbortSignal.timeout(MODELS_TIMEOUT_MS),
118
+ });
119
+ if (!response.ok) return [];
120
+ const payload = parseJson(await response.text().catch(() => ''));
121
+ const rows = Array.isArray(payload?.data)
122
+ ? payload.data
123
+ : Array.isArray(payload?.models)
124
+ ? payload.models
125
+ : Array.isArray(payload)
126
+ ? payload
127
+ : [];
128
+ const ids = [];
129
+ for (const row of rows) {
130
+ const id = typeof row === 'string'
131
+ ? row.trim()
132
+ : typeof row?.id === 'string'
133
+ ? row.id.trim()
134
+ : '';
135
+ if (id) ids.push(id);
136
+ }
137
+ return [...new Set(ids)];
138
+ } catch {
139
+ return [];
140
+ }
141
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ classifyCustomSummaryApiFailure,
4
+ listCustomSummaryModels,
5
+ parseCustomApiBaseURL,
6
+ testCustomSummaryApi,
7
+ } from './custom-api.js';
8
+
9
+ describe('parseCustomApiBaseURL', () => {
10
+ it('accepts http(s) URLs and strips trailing slashes', () => {
11
+ expect(parseCustomApiBaseURL('https://api.example.test/v1/')).toBe('https://api.example.test/v1');
12
+ expect(parseCustomApiBaseURL('http://127.0.0.1:4000')).toBe('http://127.0.0.1:4000');
13
+ });
14
+
15
+ it('rejects non-http URLs and embedded credentials', () => {
16
+ expect(parseCustomApiBaseURL('ftp://api.example.test')).toBeNull();
17
+ expect(parseCustomApiBaseURL('https://user:pass@api.example.test/v1')).toBeNull();
18
+ expect(parseCustomApiBaseURL('not a url')).toBeNull();
19
+ });
20
+ });
21
+
22
+ describe('classifyCustomSummaryApiFailure', () => {
23
+ it('maps 401/403 to token and 404/model_not_found to model', () => {
24
+ expect(classifyCustomSummaryApiFailure({ status: 401 })).toBe('token');
25
+ expect(classifyCustomSummaryApiFailure({ status: 403 })).toBe('token');
26
+ expect(classifyCustomSummaryApiFailure({ status: 404 })).toBe('model');
27
+ expect(classifyCustomSummaryApiFailure({
28
+ status: 400,
29
+ bodyText: '{"error":{"code":"model_not_found"}}',
30
+ })).toBe('model');
31
+ });
32
+
33
+ it('maps network and non-OpenAI-compatible failures to baseURL', () => {
34
+ expect(classifyCustomSummaryApiFailure({ cause: { code: 'ENOTFOUND' } })).toBe('baseURL');
35
+ expect(classifyCustomSummaryApiFailure({ cause: { name: 'AbortError' } })).toBe('baseURL');
36
+ expect(classifyCustomSummaryApiFailure({ status: 502, bodyText: '<html>bad gateway</html>' })).toBe('baseURL');
37
+ });
38
+ });
39
+
40
+ describe('testCustomSummaryApi', () => {
41
+ it('returns incomplete when any of the custom triple is missing', async () => {
42
+ expect(await testCustomSummaryApi({
43
+ baseURL: 'https://api.example.test/v1',
44
+ modelID: 'gpt-4.1-mini',
45
+ })).toEqual({ ok: false, code: 'incomplete' });
46
+ });
47
+
48
+ it('classifies HTTP failures from the probe request', async () => {
49
+ const fetchImpl = vi.fn(async () => new Response('unauthorized', { status: 401 }));
50
+ expect(await testCustomSummaryApi({
51
+ baseURL: 'https://api.example.test/v1',
52
+ modelID: 'gpt-4.1-mini',
53
+ apiToken: 'sk-test',
54
+ fetchImpl,
55
+ })).toEqual({ ok: false, code: 'token' });
56
+ });
57
+
58
+ it('accepts an OpenAI-compatible chat completion payload', async () => {
59
+ const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
60
+ choices: [{ message: { content: 'ok' } }],
61
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
62
+ expect(await testCustomSummaryApi({
63
+ baseURL: 'https://api.example.test/v1',
64
+ modelID: 'gpt-4.1-mini',
65
+ apiToken: 'sk-test',
66
+ fetchImpl,
67
+ })).toEqual({ ok: true });
68
+ });
69
+
70
+ it('treats a 200 HTML body as a baseURL failure', async () => {
71
+ const fetchImpl = vi.fn(async () => new Response('<html>not openai</html>', { status: 200 }));
72
+ expect(await testCustomSummaryApi({
73
+ baseURL: 'https://api.example.test/v1',
74
+ modelID: 'gpt-4.1-mini',
75
+ apiToken: 'sk-test',
76
+ fetchImpl,
77
+ })).toEqual({ ok: false, code: 'baseURL' });
78
+ });
79
+ });
80
+
81
+ describe('listCustomSummaryModels', () => {
82
+ it('returns unique model ids from an OpenAI-compatible /models payload', async () => {
83
+ const fetchImpl = vi.fn(async () => new Response(JSON.stringify({
84
+ data: [{ id: 'gpt-4.1-mini' }, { id: 'gpt-4.1-nano' }, { id: 'gpt-4.1-mini' }],
85
+ }), { status: 200 }));
86
+ expect(await listCustomSummaryModels({
87
+ baseURL: 'https://api.example.test/v1',
88
+ apiToken: 'sk-test',
89
+ fetchImpl,
90
+ })).toEqual(['gpt-4.1-mini', 'gpt-4.1-nano']);
91
+ });
92
+
93
+ it('degrades to an empty list when /models is unavailable', async () => {
94
+ const fetchImpl = vi.fn(async () => { throw Object.assign(new Error('offline'), { code: 'ENOTFOUND' }); });
95
+ expect(await listCustomSummaryModels({
96
+ baseURL: 'https://api.example.test/v1',
97
+ apiToken: 'sk-test',
98
+ fetchImpl,
99
+ })).toEqual([]);
100
+ });
101
+ });