@circuitwall/jarela 2.2.0 → 2.2.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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +2 -2
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +61 -20
- package/.next/standalone/.next/server/app/api/v1/models/route.js +5 -3
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +27 -24
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page.js +313 -251
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +2 -2
- package/.next/standalone/.next/server/chunks/2471.js +17 -17
- package/.next/standalone/.next/server/chunks/2563.js +24 -50
- package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
- package/.next/standalone/.next/server/chunks/3524.js +5 -33
- package/.next/standalone/.next/server/chunks/424.js +10 -5
- package/.next/standalone/.next/server/chunks/4508.js +2 -2
- package/.next/standalone/.next/server/chunks/46.js +111 -46
- package/.next/standalone/.next/server/chunks/466.js +88 -13
- package/.next/standalone/.next/server/chunks/6649.js +1 -1
- package/.next/standalone/.next/server/chunks/9590.js +108 -46
- package/.next/standalone/.next/server/chunks/9640.js +3 -6
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +104 -45
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-f201e3e2e1f57715.js → 1395-a340fb0a345382a4.js} +89 -14
- package/.next/standalone/.next/static/chunks/{4576-13f7546a145a3624.js → 7045-8bc2f14e430ca661.js} +58 -2
- package/.next/standalone/.next/static/chunks/app/{page-c6e972a7fd13dae8.js → page-7017f07f8a7a083c.js} +170 -151
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +8 -0
- package/api/client.models.test.ts +31 -0
- package/api/client.ts +16 -1
- package/api/types.ts +8 -0
- package/app/api/v1/models/[name]/route.test.ts +89 -0
- package/app/api/v1/models/[name]/route.ts +54 -3
- package/app/api/v1/models/route.ts +4 -2
- package/app/api/v1/providers/[provider]/models/route.test.ts +53 -0
- package/app/api/v1/providers/[provider]/models/route.ts +27 -16
- package/components/agents/agent-editor/AdvancedSection.tsx +8 -2
- package/components/agents/agent-editor/ModelSection.tsx +6 -3
- package/components/models/ModelEditor.tsx +1 -1
- package/components/models/ModelsPanel.tsx +34 -8
- package/components/models/model-editor/GitHubCopilotAuth.tsx +1 -1
- package/components/models/model-editor/save-helpers.ts +2 -2
- package/components/models/model-editor/useModelEditorForm.ts +1 -1
- package/components/models/model-editor/useModelSaveHandlers.ts +1 -1
- package/components/setup/wizard/StepCredentials.tsx +1 -1
- package/components/setup/wizard/StepModel.tsx +3 -8
- package/hooks/useModels.ts +17 -1
- package/lib/agents/effective-model.test.ts +45 -0
- package/lib/agents/effective-model.ts +11 -5
- package/lib/agents/llm.ts +2 -2
- package/lib/agents/run-thread.ts +10 -5
- package/lib/agents/warm-summary-background.test.ts +1 -2
- package/lib/agents/warm-summary-background.ts +4 -7
- package/lib/db/migrations.ts +10 -0
- package/lib/health/probes.test.ts +8 -11
- package/lib/health/probes.ts +5 -31
- package/lib/models/editor-payload.ts +1 -1
- package/lib/providers/github-copilot-auth.ts +2 -3
- package/lib/providers/github-copilot.test.ts +27 -0
- package/lib/providers/github-copilot.ts +21 -44
- package/lib/stores/integrations.test.ts +7 -0
- package/lib/stores/integrations.ts +4 -4
- package/lib/stores/model-config.test.ts +88 -0
- package/lib/stores/model-config.ts +90 -8
- package/package.json +1 -1
- /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_ssgManifest.js +0 -0
|
@@ -1,105 +1,6 @@
|
|
|
1
1
|
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([[8974],{
|
|
2
2
|
|
|
3
|
-
/***/
|
|
4
|
-
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
5
|
-
|
|
6
|
-
"use strict";
|
|
7
|
-
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
8
|
-
/* harmony export */ D: () => (/* binding */ useTheme),
|
|
9
|
-
/* harmony export */ ThemeProvider: () => (/* binding */ ThemeProvider)
|
|
10
|
-
/* harmony export */ });
|
|
11
|
-
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(95155);
|
|
12
|
-
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12115);
|
|
13
|
-
/* __next_internal_client_entry_do_not_use__ ThemeProvider,useTheme auto */
|
|
14
|
-
|
|
15
|
-
const STORAGE_KEY = "jarela-theme";
|
|
16
|
-
function isTheme(v) {
|
|
17
|
-
return v === "light" || v === "dark" || v === "system";
|
|
18
|
-
}
|
|
19
|
-
function readStored() {
|
|
20
|
-
if (false) {}
|
|
21
|
-
try {
|
|
22
|
-
const v = window.localStorage.getItem(STORAGE_KEY);
|
|
23
|
-
return isTheme(v) ? v : "system";
|
|
24
|
-
} catch {
|
|
25
|
-
return "system";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const LIGHT_CHROME = "#ffffff";
|
|
29
|
-
const DARK_CHROME = "#09090b";
|
|
30
|
-
function resolveChrome(theme) {
|
|
31
|
-
if (theme === "light") return LIGHT_CHROME;
|
|
32
|
-
if (theme === "dark") return DARK_CHROME;
|
|
33
|
-
if (false) {}
|
|
34
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches ? DARK_CHROME : LIGHT_CHROME;
|
|
35
|
-
}
|
|
36
|
-
// Keep the single <meta name="theme-color"> tag (installed by the pre-paint
|
|
37
|
-
// script in app/layout.tsx) aligned with the active surface so the PWA's
|
|
38
|
-
// desktop title bar and mobile address bar match the theme.
|
|
39
|
-
function syncChrome(theme) {
|
|
40
|
-
if (typeof document === "undefined") return;
|
|
41
|
-
const meta = document.querySelector('meta[name="theme-color"]');
|
|
42
|
-
if (meta) meta.setAttribute("content", resolveChrome(theme));
|
|
43
|
-
}
|
|
44
|
-
function apply(theme) {
|
|
45
|
-
if (typeof document === "undefined") return;
|
|
46
|
-
document.documentElement.setAttribute("data-theme", theme);
|
|
47
|
-
syncChrome(theme);
|
|
48
|
-
}
|
|
49
|
-
const ThemeContext = /*#__PURE__*/ (0,react__WEBPACK_IMPORTED_MODULE_1__.createContext)(null);
|
|
50
|
-
function ThemeProvider({ children }) {
|
|
51
|
-
// Initial state stays "system" on the server to match the pre-paint script,
|
|
52
|
-
// which writes data-theme before React hydrates. The effect below syncs the
|
|
53
|
-
// React state to whatever the script (or localStorage) decided.
|
|
54
|
-
const [theme, setThemeState] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("system");
|
|
55
|
-
(0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
|
|
56
|
-
const stored = readStored();
|
|
57
|
-
setThemeState(stored);
|
|
58
|
-
// When in "system" mode, mirror OS-level changes into the PWA chrome.
|
|
59
|
-
if (false) {}
|
|
60
|
-
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
61
|
-
const onChange = ()=>{
|
|
62
|
-
if (readStored() === "system") syncChrome("system");
|
|
63
|
-
};
|
|
64
|
-
mq.addEventListener?.("change", onChange);
|
|
65
|
-
return ()=>mq.removeEventListener?.("change", onChange);
|
|
66
|
-
}, []);
|
|
67
|
-
const setTheme = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((t)=>{
|
|
68
|
-
setThemeState(t);
|
|
69
|
-
apply(t);
|
|
70
|
-
try {
|
|
71
|
-
window.localStorage.setItem(STORAGE_KEY, t);
|
|
72
|
-
} catch {
|
|
73
|
-
/* ignore quota / private-mode errors */ }
|
|
74
|
-
}, []);
|
|
75
|
-
return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ThemeContext.Provider, {
|
|
76
|
-
value: {
|
|
77
|
-
theme,
|
|
78
|
-
setTheme
|
|
79
|
-
},
|
|
80
|
-
children: children
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
function useTheme() {
|
|
84
|
-
const ctx = (0,react__WEBPACK_IMPORTED_MODULE_1__.useContext)(ThemeContext);
|
|
85
|
-
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
|
86
|
-
return ctx;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/***/ }),
|
|
91
|
-
|
|
92
|
-
/***/ 39852:
|
|
93
|
-
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
|
|
94
|
-
|
|
95
|
-
Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 96513));
|
|
96
|
-
;
|
|
97
|
-
Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 12283));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/***/ }),
|
|
101
|
-
|
|
102
|
-
/***/ 96513:
|
|
3
|
+
/***/ 9024:
|
|
103
4
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
104
5
|
|
|
105
6
|
"use strict";
|
|
@@ -826,40 +727,8 @@ function useUrlSync() {
|
|
|
826
727
|
};
|
|
827
728
|
}
|
|
828
729
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
* Resolve which `ModelConfig` will actually be used for an agent at runtime.
|
|
832
|
-
*
|
|
833
|
-
* Mirrors what `lib/agents/llm.ts` does server-side: when an agent has
|
|
834
|
-
* `model_config_name` set, that wins; otherwise the workspace-wide default
|
|
835
|
-
* model is used. Both client (BridgeEditor, AgentEditor capability hints)
|
|
836
|
-
* and server need the same logic — this is the single source of truth.
|
|
837
|
-
*
|
|
838
|
-
* Returns `null` only when neither the requested config nor the default
|
|
839
|
-
* exists in the provided list (typically: no models configured yet).
|
|
840
|
-
*/ function resolveAgentModel(agent, models) {
|
|
841
|
-
const defaultModel = models.find((m)=>m.is_default) ?? models[0] ?? null;
|
|
842
|
-
if (!agent?.model_config_name) return defaultModel;
|
|
843
|
-
return models.find((m)=>m.name === agent.model_config_name) ?? defaultModel;
|
|
844
|
-
}
|
|
845
|
-
/**
|
|
846
|
-
* Diagnose whether an agent has a runnable model RIGHT NOW. Used by the
|
|
847
|
-
* Scheduled Tasks and Watchers panels to surface model-availability issues
|
|
848
|
-
* before an unattended run silently no-ops with `no_model`.
|
|
849
|
-
*/ function agentModelStatus(agent, models) {
|
|
850
|
-
if (!agent) return {
|
|
851
|
-
state: "no-agent"
|
|
852
|
-
};
|
|
853
|
-
const effectiveModel = resolveAgentModel(agent, models);
|
|
854
|
-
if (effectiveModel) return {
|
|
855
|
-
state: "ok",
|
|
856
|
-
model: effectiveModel
|
|
857
|
-
};
|
|
858
|
-
return {
|
|
859
|
-
state: "no-model"
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
|
|
730
|
+
// EXTERNAL MODULE: ./lib/agents/effective-model.ts
|
|
731
|
+
var effective_model = __webpack_require__(53036);
|
|
863
732
|
;// ./hooks/useConfigurationIssues.ts
|
|
864
733
|
/* __next_internal_client_entry_do_not_use__ useConfigurationIssues auto */
|
|
865
734
|
|
|
@@ -915,7 +784,7 @@ function useUrlSync() {
|
|
|
915
784
|
pushIssue("no-default-model", "info", "No default model set", "Agents without an explicit model will fail. Pick one default.", "?tab=models", "Open Models →");
|
|
916
785
|
}
|
|
917
786
|
for (const a of agents){
|
|
918
|
-
const status = agentModelStatus(a, models);
|
|
787
|
+
const status = (0,effective_model/* agentModelStatus */.xu)(a, models);
|
|
919
788
|
if (status.state === "no-model") {
|
|
920
789
|
pushIssue(`agent-no-model:${a.id}`, "error", `Agent "${a.name}" has no model`, "Pick a model or set a workspace default.", `?tab=agents&item=${encodeURIComponent(a.id)}`, "Fix in Agents →");
|
|
921
790
|
}
|
|
@@ -11315,6 +11184,8 @@ var plus = __webpack_require__(2125);
|
|
|
11315
11184
|
var arrow_up_right = __webpack_require__(9751);
|
|
11316
11185
|
// EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/star.mjs
|
|
11317
11186
|
var star = __webpack_require__(95697);
|
|
11187
|
+
// EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/power-off.mjs
|
|
11188
|
+
var power_off = __webpack_require__(18402);
|
|
11318
11189
|
// EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/trash.mjs
|
|
11319
11190
|
var trash = __webpack_require__(25879);
|
|
11320
11191
|
;// ./components/ui/PanelHeader.tsx
|
|
@@ -11396,6 +11267,24 @@ function useModels() {
|
|
|
11396
11267
|
}, [
|
|
11397
11268
|
refreshModels
|
|
11398
11269
|
]);
|
|
11270
|
+
const setDefault = (0,react.useCallback)(async (name)=>{
|
|
11271
|
+
const m = await client/* api */.FH.models.patch(name, {
|
|
11272
|
+
is_default: true
|
|
11273
|
+
});
|
|
11274
|
+
await refreshModels();
|
|
11275
|
+
return m;
|
|
11276
|
+
}, [
|
|
11277
|
+
refreshModels
|
|
11278
|
+
]);
|
|
11279
|
+
const setEnabled = (0,react.useCallback)(async (name, enabled)=>{
|
|
11280
|
+
const m = await client/* api */.FH.models.patch(name, {
|
|
11281
|
+
enabled
|
|
11282
|
+
});
|
|
11283
|
+
await refreshModels();
|
|
11284
|
+
return m;
|
|
11285
|
+
}, [
|
|
11286
|
+
refreshModels
|
|
11287
|
+
]);
|
|
11399
11288
|
const remove = (0,react.useCallback)(async (name)=>{
|
|
11400
11289
|
await client/* api */.FH.models.delete(name);
|
|
11401
11290
|
await refreshModels();
|
|
@@ -11433,6 +11322,8 @@ function useModels() {
|
|
|
11433
11322
|
refresh,
|
|
11434
11323
|
create,
|
|
11435
11324
|
update,
|
|
11325
|
+
setDefault,
|
|
11326
|
+
setEnabled,
|
|
11436
11327
|
remove,
|
|
11437
11328
|
assign,
|
|
11438
11329
|
unassign
|
|
@@ -11447,6 +11338,8 @@ function useModels() {
|
|
|
11447
11338
|
refresh,
|
|
11448
11339
|
create,
|
|
11449
11340
|
update,
|
|
11341
|
+
setDefault,
|
|
11342
|
+
setEnabled,
|
|
11450
11343
|
remove,
|
|
11451
11344
|
assign,
|
|
11452
11345
|
unassign
|
|
@@ -11957,7 +11850,7 @@ const PROVIDER_COLORS = {
|
|
|
11957
11850
|
"github-copilot": "bg-purple-900/40 text-purple-300 border-purple-700"
|
|
11958
11851
|
};
|
|
11959
11852
|
function ModelsPanel() {
|
|
11960
|
-
const { models, assignments, loading, create, update, remove, refresh } = useModels();
|
|
11853
|
+
const { models, assignments, loading, create, update, setDefault, setEnabled, remove, refresh } = useModels();
|
|
11961
11854
|
const { agents, loading: agentsLoading } = useAgents();
|
|
11962
11855
|
const strategyOverrides = agents.filter((agent)=>agent.usage_strategy != null);
|
|
11963
11856
|
const [editing, setEditing] = (0,react.useState)(null);
|
|
@@ -12005,12 +11898,20 @@ function ModelsPanel() {
|
|
|
12005
11898
|
refresh();
|
|
12006
11899
|
}
|
|
12007
11900
|
async function handleSetDefault(m) {
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
}
|
|
11901
|
+
setDeleteError(null);
|
|
11902
|
+
try {
|
|
11903
|
+
await setDefault(m.name);
|
|
11904
|
+
} catch (e) {
|
|
11905
|
+
setDeleteError(`Could not set "${m.name}" as default: ${(0,utils_error/* errorMessage */.g)(e)}`);
|
|
11906
|
+
}
|
|
11907
|
+
}
|
|
11908
|
+
async function handleToggleEnabled(m) {
|
|
11909
|
+
setDeleteError(null);
|
|
11910
|
+
try {
|
|
11911
|
+
await setEnabled(m.name, !m.enabled);
|
|
11912
|
+
} catch (e) {
|
|
11913
|
+
setDeleteError(`Could not ${m.enabled ? "disable" : "enable"} "${m.name}": ${(0,utils_error/* errorMessage */.g)(e)}`);
|
|
11914
|
+
}
|
|
12014
11915
|
}
|
|
12015
11916
|
async function handleRemove(name) {
|
|
12016
11917
|
const affectedAgents = assignments.filter((assignment)=>assignment.model_config_name === name).map((assignment)=>agents.find((agent)=>agent.id === assignment.agent_id)?.name ?? assignment.agent_id);
|
|
@@ -12286,7 +12187,7 @@ function ModelsPanel() {
|
|
|
12286
12187
|
return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
|
|
12287
12188
|
"data-deep-link-id": m.name,
|
|
12288
12189
|
onClick: ()=>setEditing(m),
|
|
12289
|
-
className:
|
|
12190
|
+
className: `flex items-center gap-3 py-2.5 border-b border-border/60 group cursor-pointer hover:bg-surface-3/30 transition-colors ${m.enabled ? "" : "opacity-60"}`,
|
|
12290
12191
|
children: [
|
|
12291
12192
|
/*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
|
|
12292
12193
|
className: "shrink-0 text-fg-subtle",
|
|
@@ -12302,13 +12203,17 @@ function ModelsPanel() {
|
|
|
12302
12203
|
className: "flex items-center gap-2 mb-0.5",
|
|
12303
12204
|
children: [
|
|
12304
12205
|
/*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
|
|
12305
|
-
className:
|
|
12206
|
+
className: `text-sm font-medium ${m.enabled ? "text-fg" : "text-fg-subtle line-through"}`,
|
|
12306
12207
|
children: m.name
|
|
12307
12208
|
}),
|
|
12308
12209
|
m.is_default && /*#__PURE__*/ (0,jsx_runtime.jsx)(star/* default */.A, {
|
|
12309
12210
|
size: 11,
|
|
12310
12211
|
className: "text-yellow-700 dark:text-yellow-400 fill-yellow-400 shrink-0"
|
|
12311
12212
|
}),
|
|
12213
|
+
!m.enabled && /*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
|
|
12214
|
+
className: "text-xs px-1.5 py-0.5 rounded border bg-surface-2 text-fg-muted border-border",
|
|
12215
|
+
children: "disabled"
|
|
12216
|
+
}),
|
|
12312
12217
|
/*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
|
|
12313
12218
|
className: `text-xs px-1.5 py-0.5 rounded border ${PROVIDER_COLORS[m.provider] ?? "bg-surface-2 text-fg-muted border-border"}`,
|
|
12314
12219
|
children: m.provider
|
|
@@ -12331,10 +12236,10 @@ function ModelsPanel() {
|
|
|
12331
12236
|
/*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
|
|
12332
12237
|
className: "flex gap-1 opacity-40 group-hover:opacity-100 pointer-coarse:opacity-100 transition-opacity shrink-0",
|
|
12333
12238
|
children: [
|
|
12334
|
-
!m.is_default && /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
|
|
12239
|
+
!m.is_default && m.enabled && /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
|
|
12335
12240
|
onClick: (e)=>{
|
|
12336
12241
|
e.stopPropagation();
|
|
12337
|
-
handleSetDefault(m);
|
|
12242
|
+
void handleSetDefault(m);
|
|
12338
12243
|
},
|
|
12339
12244
|
className: "p-1 text-fg-subtle hover:text-yellow-700 dark:hover:text-yellow-400 transition-colors",
|
|
12340
12245
|
title: "Set as default",
|
|
@@ -12342,6 +12247,21 @@ function ModelsPanel() {
|
|
|
12342
12247
|
size: 13
|
|
12343
12248
|
})
|
|
12344
12249
|
}),
|
|
12250
|
+
/*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
|
|
12251
|
+
onClick: (e)=>{
|
|
12252
|
+
e.stopPropagation();
|
|
12253
|
+
void handleToggleEnabled(m);
|
|
12254
|
+
},
|
|
12255
|
+
className: "p-1 text-fg-subtle hover:text-fg transition-colors",
|
|
12256
|
+
title: m.enabled ? "Disable; hide from model selection" : "Enable; show in model selection",
|
|
12257
|
+
"aria-label": m.enabled ? `Disable ${m.name}` : `Enable ${m.name}`,
|
|
12258
|
+
"aria-pressed": !m.enabled,
|
|
12259
|
+
children: m.enabled ? /*#__PURE__*/ (0,jsx_runtime.jsx)(power/* default */.A, {
|
|
12260
|
+
size: 13
|
|
12261
|
+
}) : /*#__PURE__*/ (0,jsx_runtime.jsx)(power_off/* default */.A, {
|
|
12262
|
+
size: 13
|
|
12263
|
+
})
|
|
12264
|
+
}),
|
|
12345
12265
|
/*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
|
|
12346
12266
|
onClick: (e)=>{
|
|
12347
12267
|
e.stopPropagation();
|
|
@@ -19679,7 +19599,7 @@ function RouteTable({ bridge_id, module }) {
|
|
|
19679
19599
|
client/* api */.FH.models.list().then(setModels).catch(()=>{});
|
|
19680
19600
|
}, []);
|
|
19681
19601
|
/** Vision capability of an agent (resolves via its model_config_name, with default fallback). */ function agentVisionState(a) {
|
|
19682
|
-
const cfg = resolveAgentModel(a, models);
|
|
19602
|
+
const cfg = (0,effective_model/* resolveAgentModel */.Fu)(a, models);
|
|
19683
19603
|
if (!cfg) return {
|
|
19684
19604
|
supported: false,
|
|
19685
19605
|
classified: false,
|
|
@@ -24780,7 +24700,7 @@ function WatcherCard({ watcher, agent, agents, models, onCancel, onRunNow, onCha
|
|
|
24780
24700
|
const overdue = !watcher.last_error && new Date(watcher.next_run_at).getTime() < Date.now() - 60000;
|
|
24781
24701
|
// Pre-flight model availability for the assigned agent; only relevant
|
|
24782
24702
|
// when the watcher reacts via an agent prompt (not a pure script).
|
|
24783
|
-
const modelStatus = watcher.reaction_kind === "agent_prompt" ? agentModelStatus(agent ?? null, models) : null;
|
|
24703
|
+
const modelStatus = watcher.reaction_kind === "agent_prompt" ? (0,effective_model/* agentModelStatus */.xu)(agent ?? null, models) : null;
|
|
24784
24704
|
return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
|
|
24785
24705
|
"data-deep-link-id": watcher.id,
|
|
24786
24706
|
className: "mb-2 rounded-lg border border-border bg-surface-2 overflow-hidden",
|
|
@@ -25512,7 +25432,7 @@ function TaskCard({ task, agent, agents, models, onCancel, onRunNow, onChanged }
|
|
|
25512
25432
|
collapseSeconds: true
|
|
25513
25433
|
});
|
|
25514
25434
|
const overdue = !task.last_error && new Date(task.next_run_at).getTime() < Date.now() - 60000;
|
|
25515
|
-
const modelStatus = task.reaction_kind === "agent_prompt" ? agentModelStatus(agent ?? null, models) : null;
|
|
25435
|
+
const modelStatus = task.reaction_kind === "agent_prompt" ? (0,effective_model/* agentModelStatus */.xu)(agent ?? null, models) : null;
|
|
25516
25436
|
return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
|
|
25517
25437
|
"data-deep-link-id": task.id,
|
|
25518
25438
|
className: "mb-2 rounded-lg border border-border bg-surface-2 overflow-hidden group",
|
|
@@ -33478,14 +33398,113 @@ function AppShell() {
|
|
|
33478
33398
|
}
|
|
33479
33399
|
|
|
33480
33400
|
|
|
33401
|
+
/***/ }),
|
|
33402
|
+
|
|
33403
|
+
/***/ 30737:
|
|
33404
|
+
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
33405
|
+
|
|
33406
|
+
"use strict";
|
|
33407
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
33408
|
+
/* harmony export */ D: () => (/* binding */ useTheme),
|
|
33409
|
+
/* harmony export */ ThemeProvider: () => (/* binding */ ThemeProvider)
|
|
33410
|
+
/* harmony export */ });
|
|
33411
|
+
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(95155);
|
|
33412
|
+
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12115);
|
|
33413
|
+
/* __next_internal_client_entry_do_not_use__ ThemeProvider,useTheme auto */
|
|
33414
|
+
|
|
33415
|
+
const STORAGE_KEY = "jarela-theme";
|
|
33416
|
+
function isTheme(v) {
|
|
33417
|
+
return v === "light" || v === "dark" || v === "system";
|
|
33418
|
+
}
|
|
33419
|
+
function readStored() {
|
|
33420
|
+
if (false) {}
|
|
33421
|
+
try {
|
|
33422
|
+
const v = window.localStorage.getItem(STORAGE_KEY);
|
|
33423
|
+
return isTheme(v) ? v : "system";
|
|
33424
|
+
} catch {
|
|
33425
|
+
return "system";
|
|
33426
|
+
}
|
|
33427
|
+
}
|
|
33428
|
+
const LIGHT_CHROME = "#ffffff";
|
|
33429
|
+
const DARK_CHROME = "#09090b";
|
|
33430
|
+
function resolveChrome(theme) {
|
|
33431
|
+
if (theme === "light") return LIGHT_CHROME;
|
|
33432
|
+
if (theme === "dark") return DARK_CHROME;
|
|
33433
|
+
if (false) {}
|
|
33434
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? DARK_CHROME : LIGHT_CHROME;
|
|
33435
|
+
}
|
|
33436
|
+
// Keep the single <meta name="theme-color"> tag (installed by the pre-paint
|
|
33437
|
+
// script in app/layout.tsx) aligned with the active surface so the PWA's
|
|
33438
|
+
// desktop title bar and mobile address bar match the theme.
|
|
33439
|
+
function syncChrome(theme) {
|
|
33440
|
+
if (typeof document === "undefined") return;
|
|
33441
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
33442
|
+
if (meta) meta.setAttribute("content", resolveChrome(theme));
|
|
33443
|
+
}
|
|
33444
|
+
function apply(theme) {
|
|
33445
|
+
if (typeof document === "undefined") return;
|
|
33446
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
33447
|
+
syncChrome(theme);
|
|
33448
|
+
}
|
|
33449
|
+
const ThemeContext = /*#__PURE__*/ (0,react__WEBPACK_IMPORTED_MODULE_1__.createContext)(null);
|
|
33450
|
+
function ThemeProvider({ children }) {
|
|
33451
|
+
// Initial state stays "system" on the server to match the pre-paint script,
|
|
33452
|
+
// which writes data-theme before React hydrates. The effect below syncs the
|
|
33453
|
+
// React state to whatever the script (or localStorage) decided.
|
|
33454
|
+
const [theme, setThemeState] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("system");
|
|
33455
|
+
(0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
|
|
33456
|
+
const stored = readStored();
|
|
33457
|
+
setThemeState(stored);
|
|
33458
|
+
// When in "system" mode, mirror OS-level changes into the PWA chrome.
|
|
33459
|
+
if (false) {}
|
|
33460
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
33461
|
+
const onChange = ()=>{
|
|
33462
|
+
if (readStored() === "system") syncChrome("system");
|
|
33463
|
+
};
|
|
33464
|
+
mq.addEventListener?.("change", onChange);
|
|
33465
|
+
return ()=>mq.removeEventListener?.("change", onChange);
|
|
33466
|
+
}, []);
|
|
33467
|
+
const setTheme = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((t)=>{
|
|
33468
|
+
setThemeState(t);
|
|
33469
|
+
apply(t);
|
|
33470
|
+
try {
|
|
33471
|
+
window.localStorage.setItem(STORAGE_KEY, t);
|
|
33472
|
+
} catch {
|
|
33473
|
+
/* ignore quota / private-mode errors */ }
|
|
33474
|
+
}, []);
|
|
33475
|
+
return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ThemeContext.Provider, {
|
|
33476
|
+
value: {
|
|
33477
|
+
theme,
|
|
33478
|
+
setTheme
|
|
33479
|
+
},
|
|
33480
|
+
children: children
|
|
33481
|
+
});
|
|
33482
|
+
}
|
|
33483
|
+
function useTheme() {
|
|
33484
|
+
const ctx = (0,react__WEBPACK_IMPORTED_MODULE_1__.useContext)(ThemeContext);
|
|
33485
|
+
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
|
33486
|
+
return ctx;
|
|
33487
|
+
}
|
|
33488
|
+
|
|
33489
|
+
|
|
33490
|
+
/***/ }),
|
|
33491
|
+
|
|
33492
|
+
/***/ 39852:
|
|
33493
|
+
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
|
|
33494
|
+
|
|
33495
|
+
Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 9024));
|
|
33496
|
+
;
|
|
33497
|
+
Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 12283));
|
|
33498
|
+
|
|
33499
|
+
|
|
33481
33500
|
/***/ })
|
|
33482
33501
|
|
|
33483
33502
|
},
|
|
33484
33503
|
/******/ __webpack_require__ => { // webpackRuntimeModules
|
|
33485
33504
|
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
|
|
33486
|
-
/******/ __webpack_require__.O(0, [1973,8463,8095,
|
|
33505
|
+
/******/ __webpack_require__.O(0, [1973,8463,8095,7045,1395,8441,5254,7358], () => (__webpack_exec__(39852)));
|
|
33487
33506
|
/******/ var __webpack_exports__ = __webpack_require__.O();
|
|
33488
33507
|
/******/ _N_E = __webpack_exports__;
|
|
33489
33508
|
/******/ }
|
|
33490
33509
|
]);
|
|
33491
|
-
//# sourceMappingURL=page-
|
|
33510
|
+
//# sourceMappingURL=page-7017f07f8a7a083c.js.map
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.1] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Let models be disabled temporarily so they drop out of model selection and default/router resolution without being deleted, and set the default model in one atomic step that no longer rewrites model params or silently clears the default on edits.**
|
|
15
|
+
- **Show fresh agents, tasks, credentials, models, and other lists right after a save instead of a briefly stale browser-cached copy.**
|
|
16
|
+
- **Stop treating a `GITHUB_TOKEN` environment PAT as a GitHub Copilot credential and report Copilot catalog configuration errors as 400 instead of 500.**
|
|
17
|
+
|
|
10
18
|
## [2.2.0] - 2026-10-15
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -17,6 +17,7 @@ function model(modelId: string): ModelConfig {
|
|
|
17
17
|
model_id: modelId,
|
|
18
18
|
params: {},
|
|
19
19
|
is_default: false,
|
|
20
|
+
enabled: true,
|
|
20
21
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
21
22
|
updated_at: "2026-01-01T00:00:00.000Z",
|
|
22
23
|
};
|
|
@@ -46,4 +47,34 @@ describe("api.models list cache", () => {
|
|
|
46
47
|
await expect(Promise.all([oldList, latestList])).resolves.toEqual([[updated], [updated]]);
|
|
47
48
|
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
48
49
|
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("api request HTTP cache mode", () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.unstubAllGlobals();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("bypasses the browser HTTP cache for mutable lists so reads after writes are fresh", async () => {
|
|
58
|
+
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse([])));
|
|
59
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
60
|
+
|
|
61
|
+
await api.models.list({ force: true });
|
|
62
|
+
await api.agents.list({ force: true });
|
|
63
|
+
await api.tasks.list({ force: true });
|
|
64
|
+
await api.credentials.list();
|
|
65
|
+
|
|
66
|
+
expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
67
|
+
for (const [, init] of fetchMock.mock.calls) {
|
|
68
|
+
expect((init as RequestInit).cache).toBe("no-store");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("lets read-only dashboard metrics keep the server's HTTP cache", async () => {
|
|
73
|
+
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({}));
|
|
74
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
75
|
+
|
|
76
|
+
await api.dashboard.metrics(7);
|
|
77
|
+
|
|
78
|
+
expect((fetchMock.mock.calls[0][1] as RequestInit).cache).toBe("default");
|
|
79
|
+
});
|
|
49
80
|
});
|
package/api/client.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
MemorySearchHit,
|
|
52
52
|
ModelConfig,
|
|
53
53
|
ModelConfigIn,
|
|
54
|
+
ModelConfigPatch,
|
|
54
55
|
Credential,
|
|
55
56
|
CredentialIn,
|
|
56
57
|
StreamOptions,
|
|
@@ -388,6 +389,11 @@ async function request<T>(path: string, init?: RequestInit & { timeoutMs?: numbe
|
|
|
388
389
|
try {
|
|
389
390
|
const res = await fetch(`${BASE}${path}`, {
|
|
390
391
|
headers: { "Content-Type": "application/json", ...fetchInit.headers },
|
|
392
|
+
// The in-memory list/value caches above are the client cache layer
|
|
393
|
+
// and are invalidated on writes. The browser HTTP cache would honor
|
|
394
|
+
// the server's short max-age and serve a stale list right after a
|
|
395
|
+
// save, so bypass it unless a caller opts in.
|
|
396
|
+
cache: "no-store",
|
|
391
397
|
...fetchInit,
|
|
392
398
|
signal: timeoutCtrl.signal,
|
|
393
399
|
});
|
|
@@ -811,6 +817,14 @@ export const api = {
|
|
|
811
817
|
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("jarela:models-changed"));
|
|
812
818
|
return updated;
|
|
813
819
|
},
|
|
820
|
+
patch: async (name: string, data: ModelConfigPatch) => {
|
|
821
|
+
const updated = await request<ModelConfig>(`/models/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(data) });
|
|
822
|
+
// Enabling/disabling or setting the default can move the default flag
|
|
823
|
+
// on other rows, so drop the cached list instead of patching one row.
|
|
824
|
+
invalidateModelListCache();
|
|
825
|
+
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("jarela:models-changed"));
|
|
826
|
+
return updated;
|
|
827
|
+
},
|
|
814
828
|
delete: async (name: string) => {
|
|
815
829
|
const res = await request<{ deleted: boolean }>(`/models/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
816
830
|
if (res.deleted) {
|
|
@@ -918,7 +932,8 @@ export const api = {
|
|
|
918
932
|
},
|
|
919
933
|
|
|
920
934
|
dashboard: {
|
|
921
|
-
|
|
935
|
+
// Read-only aggregates: the server's 30s HTTP cache is intended here.
|
|
936
|
+
metrics: (days = 30) => request<DashboardMetrics>(`/dashboard/metrics?days=${encodeURIComponent(String(days))}`, { cache: "default" }),
|
|
922
937
|
pricingTask: () => request<DashboardPricingTaskStatus>("/dashboard/pricing-task"),
|
|
923
938
|
setupPricingTask: () => request<DashboardPricingTaskStatus>("/dashboard/pricing-task", {
|
|
924
939
|
method: "POST",
|
package/api/types.ts
CHANGED
|
@@ -482,10 +482,18 @@ export interface ModelConfig {
|
|
|
482
482
|
// this for legacy rows that had inline secrets.
|
|
483
483
|
credential_id?: string | null;
|
|
484
484
|
is_default: boolean;
|
|
485
|
+
// Disabled configs are hidden from model pickers and skipped by runtime
|
|
486
|
+
// default/router resolution; the row and its credentials are kept.
|
|
487
|
+
enabled: boolean;
|
|
485
488
|
created_at: string;
|
|
486
489
|
updated_at: string;
|
|
487
490
|
}
|
|
488
491
|
|
|
492
|
+
export interface ModelConfigPatch {
|
|
493
|
+
enabled?: boolean;
|
|
494
|
+
is_default?: true;
|
|
495
|
+
}
|
|
496
|
+
|
|
489
497
|
export interface ModelConfigIn {
|
|
490
498
|
provider: string;
|
|
491
499
|
model_id: string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { NextRequest } from "next/server";
|
|
6
|
+
|
|
7
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-model-route-"));
|
|
8
|
+
process.env.JARELA_DB_DIR = tmpRoot;
|
|
9
|
+
|
|
10
|
+
const { upsertModelConfig, deleteModelConfig, listModelConfigs, getModelConfig } = await import("@/lib/stores/model-config");
|
|
11
|
+
const { PATCH, PUT } = await import("./route");
|
|
12
|
+
|
|
13
|
+
const ctx = (name: string) => ({ params: Promise.resolve({ name }) });
|
|
14
|
+
const json = (method: string, name: string, body: unknown) => new NextRequest(
|
|
15
|
+
`http://localhost/api/v1/models/${name}`,
|
|
16
|
+
{ method, body: JSON.stringify(body), headers: { "Content-Type": "application/json" } },
|
|
17
|
+
);
|
|
18
|
+
const defaults = () => listModelConfigs().filter((r) => r.is_default === 1).map((r) => r.name);
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
for (const r of listModelConfigs()) deleteModelConfig(r.name);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* */ }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("PATCH /api/v1/models/[name]", () => {
|
|
29
|
+
it("switches the default without touching params", async () => {
|
|
30
|
+
upsertModelConfig("alpha", "anthropic", "claude-x", { temperature: 0.1 }, true);
|
|
31
|
+
upsertModelConfig("beta", "anthropic", "claude-y", { temperature: 0.7 }, false);
|
|
32
|
+
const before = getModelConfig("beta")!.params;
|
|
33
|
+
|
|
34
|
+
const res = await PATCH(json("PATCH", "beta", { is_default: true }), ctx("beta"));
|
|
35
|
+
|
|
36
|
+
expect(res.status).toBe(200);
|
|
37
|
+
await expect(res.json()).resolves.toMatchObject({ name: "beta", is_default: true, enabled: true });
|
|
38
|
+
expect(defaults()).toEqual(["beta"]);
|
|
39
|
+
expect(getModelConfig("beta")!.params).toBe(before);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("disables a model and promotes another default atomically", async () => {
|
|
43
|
+
upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
|
|
44
|
+
upsertModelConfig("beta", "anthropic", "claude-y", {}, false);
|
|
45
|
+
|
|
46
|
+
const res = await PATCH(json("PATCH", "alpha", { enabled: false }), ctx("alpha"));
|
|
47
|
+
|
|
48
|
+
expect(res.status).toBe(200);
|
|
49
|
+
await expect(res.json()).resolves.toMatchObject({ enabled: false, is_default: false });
|
|
50
|
+
expect(defaults()).toEqual(["beta"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("re-enables and sets default in one request", async () => {
|
|
54
|
+
upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
|
|
55
|
+
upsertModelConfig("beta", "anthropic", "claude-y", {}, false);
|
|
56
|
+
await PATCH(json("PATCH", "beta", { enabled: false }), ctx("beta"));
|
|
57
|
+
|
|
58
|
+
const res = await PATCH(json("PATCH", "beta", { enabled: true, is_default: true }), ctx("beta"));
|
|
59
|
+
|
|
60
|
+
expect(res.status).toBe(200);
|
|
61
|
+
expect(defaults()).toEqual(["beta"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns 409 when disabling the last enabled model and 404 for unknown models", async () => {
|
|
65
|
+
upsertModelConfig("solo", "anthropic", "claude-x", {}, true);
|
|
66
|
+
|
|
67
|
+
expect((await PATCH(json("PATCH", "solo", { enabled: false }), ctx("solo"))).status).toBe(409);
|
|
68
|
+
expect(getModelConfig("solo")!.enabled).toBe(1);
|
|
69
|
+
expect((await PATCH(json("PATCH", "ghost", { is_default: true }), ctx("ghost"))).status).toBe(404);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects empty, contradictory, and unknown-field bodies", async () => {
|
|
73
|
+
upsertModelConfig("solo", "anthropic", "claude-x", {}, true);
|
|
74
|
+
for (const body of [{}, { enabled: false, is_default: true }, { is_default: false }, { params: {} }]) {
|
|
75
|
+
expect((await PATCH(json("PATCH", "solo", body), ctx("solo"))).status).toBe(400);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("PUT /api/v1/models/[name]", () => {
|
|
81
|
+
it("keeps the default flag when is_default is omitted", async () => {
|
|
82
|
+
upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
|
|
83
|
+
|
|
84
|
+
const res = await PUT(json("PUT", "alpha", { provider: "anthropic", model_id: "claude-x", params: { temperature: 0.3 } }), ctx("alpha"));
|
|
85
|
+
|
|
86
|
+
expect(res.status).toBe(200);
|
|
87
|
+
expect(defaults()).toEqual(["alpha"]);
|
|
88
|
+
});
|
|
89
|
+
});
|