@oar-sh/oar 0.9.23 → 0.9.24
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/CHANGELOG.md +165 -0
- package/README.md +9 -9
- package/bin/oar.js +54 -4
- package/package.json +9 -7
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +91 -17
- package/server/routes/sessions-routes.mjs +235 -109
- package/server/server-runtime.mjs +395 -81
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +4 -2
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +2 -2
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
|
@@ -5025,15 +5025,18 @@ export function createCopilotSdkSessionRunner({
|
|
|
5025
5025
|
dbg('image turn', message.id, result.via, result.outcome);
|
|
5026
5026
|
} catch (error) {
|
|
5027
5027
|
if (isOpenAIImageTerminalError(error)) {
|
|
5028
|
-
|
|
5028
|
+
// A refusal by OpenAI, or a reply the relay could not process after
|
|
5029
|
+
// OpenAI answered 2xx; the codes are the ones the relay's image
|
|
5030
|
+
// operation uses for the same two cases.
|
|
5031
|
+
const code = error.relayProcessing === true ? 'image-reply-unprocessed' : 'image-provider-rejected';
|
|
5032
|
+
dbg(error.relayProcessing === true ? 'image reply not processed' : 'image turn refused by OpenAI', message.id, error.message);
|
|
5029
5033
|
await publishResponse(message, {
|
|
5030
5034
|
text: error.message,
|
|
5031
5035
|
model: null,
|
|
5032
5036
|
terminalError: {
|
|
5033
5037
|
kind: 'openai-image-failed',
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
stableCode: 'image-provider-rejected',
|
|
5038
|
+
code,
|
|
5039
|
+
stableCode: code,
|
|
5037
5040
|
message: error.message,
|
|
5038
5041
|
...(error.detail ? { detail: String(error.detail) } : {}),
|
|
5039
5042
|
failedAt: new Date().toISOString(),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
|
|
4
|
+
import { loadAgentSdk } from '../agent-sdk-loader.mjs';
|
|
5
|
+
|
|
4
6
|
import {
|
|
5
7
|
createTurnLiveness,
|
|
6
8
|
createTurnStalledError,
|
|
@@ -18,7 +20,7 @@ import { WORKING_DIRECTORY_MISSING_CODE, isWorkingDirectoryMissingError } from '
|
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
async function defaultAgentFactory() {
|
|
21
|
-
const { Agent } = await
|
|
23
|
+
const { Agent } = await loadAgentSdk('cursor');
|
|
22
24
|
return {
|
|
23
25
|
create: (options) => Agent.create(options),
|
|
24
26
|
resume: (agentId, options) => Agent.resume(agentId, options),
|
|
@@ -30,7 +32,7 @@ async function defaultAgentFactory() {
|
|
|
30
32
|
// `storeFactoryImpl(storePath, { cwd })` with the real store rooted in the
|
|
31
33
|
// storePath's directory.
|
|
32
34
|
async function defaultStoreFactory() {
|
|
33
|
-
const { SqliteLocalAgentStore } = await
|
|
35
|
+
const { SqliteLocalAgentStore } = await loadAgentSdk('cursor', { entry: 'sqlite' });
|
|
34
36
|
return (storePath, { cwd } = {}) =>
|
|
35
37
|
SqliteLocalAgentStore.open({
|
|
36
38
|
workspaceRef: cwd || path.dirname(storePath),
|
|
@@ -497,7 +499,7 @@ async function defaultModelsList({ apiKey }) {
|
|
|
497
499
|
// The namespace form is the only working call shape: `Cursor` has a private
|
|
498
500
|
// constructor and `models` is a static member, so the old client-instance
|
|
499
501
|
// fallback could only ever throw a TypeError that masked the real error.
|
|
500
|
-
const sdk = await
|
|
502
|
+
const sdk = await loadAgentSdk('cursor');
|
|
501
503
|
return sdk.Cursor.models.list({ apiKey });
|
|
502
504
|
}
|
|
503
505
|
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Agent SDK rows inside Settings: the Claude Agent SDK in the Claude, Claude
|
|
2
|
+
// API and DeepSeek tabs, the Cursor SDK in the Cursor tab, and the Windows
|
|
3
|
+
// helper (koffi, the relay's own lock check) under General.
|
|
4
|
+
//
|
|
5
|
+
// The relay installs these on demand from pins it ships (agent-sdk-store.mjs),
|
|
6
|
+
// so a row is mostly a window on the store: its state, the pinned and the
|
|
7
|
+
// installed version, the live npm log, the last error and the next automatic
|
|
8
|
+
// retry, and the one button that state calls for. Nothing here composes a
|
|
9
|
+
// command; the request carries an SDK id and the relay looks it up.
|
|
10
|
+
//
|
|
11
|
+
// Every visible state comes from the `agent_sdk_state` broadcast (the same
|
|
12
|
+
// payload as GET /api/agent-sdks/status), so an install survives closing the
|
|
13
|
+
// modal or being started by a first chat or another device. Rendering reads
|
|
14
|
+
// the latest payload and never assumes a local step sequence; the log is
|
|
15
|
+
// redrawn from the whole buffer whenever `logSeq` moves.
|
|
16
|
+
//
|
|
17
|
+
// The markup holds only empty mounts (`data-agent-sdk="<id>"`); this module
|
|
18
|
+
// draws the row into each. One SDK can have several mounts (Claude's), so the
|
|
19
|
+
// parts are found by `data-part`, not by id. The log is host process output:
|
|
20
|
+
// it goes in with textContent, and the little that goes through innerHTML is
|
|
21
|
+
// static and escaped with escHtml anyway.
|
|
22
|
+
|
|
23
|
+
import { escHtml } from './store.js';
|
|
24
|
+
import { cancelAgentSdkInstall, getAgentSdkStatus, startAgentSdkInstall } from './api-client.js';
|
|
25
|
+
import { updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
26
|
+
|
|
27
|
+
export const AGENT_SDK_IDS = Object.freeze(['claude', 'cursor', 'koffi']);
|
|
28
|
+
const STATES = Object.freeze(['missing', 'installing', 'ready', 'error', 'unsupported']);
|
|
29
|
+
const ACTIONS = Object.freeze(['install', 'retry', 'reinstall', 'cancel']);
|
|
30
|
+
const ACTION_LABELS = Object.freeze({ install: 'Install', retry: 'Retry', reinstall: 'Reinstall', cancel: 'Cancel' });
|
|
31
|
+
// Until the first payload names them.
|
|
32
|
+
const DEFAULT_LABELS = Object.freeze({ claude: 'Claude Agent SDK', cursor: 'Cursor SDK', koffi: 'Windows helper' });
|
|
33
|
+
const STATE_WORDS = Object.freeze({
|
|
34
|
+
missing: 'Not installed',
|
|
35
|
+
installing: 'Installing…',
|
|
36
|
+
ready: 'Installed',
|
|
37
|
+
error: 'Install failed',
|
|
38
|
+
unsupported: 'Not available on this platform',
|
|
39
|
+
});
|
|
40
|
+
const LOG_WORDS = Object.freeze({ installing: 'Running', ready: 'Finished', error: 'Failed' });
|
|
41
|
+
|
|
42
|
+
let sdkState = { root: null, host: null, sdks: {} };
|
|
43
|
+
let loadFailed = false;
|
|
44
|
+
// A request the relay refused before any broadcast, on the row that sent it;
|
|
45
|
+
// the SDK's next transition clears it.
|
|
46
|
+
let localErrors = {};
|
|
47
|
+
const requestsInFlight = new Set();
|
|
48
|
+
// One-shot per failed install: its log opens once, and a later redraw of the
|
|
49
|
+
// same payload does not fight the user closing it.
|
|
50
|
+
const expandedFailures = new Map();
|
|
51
|
+
let statusRefresh = null;
|
|
52
|
+
|
|
53
|
+
function normalizeId(raw) {
|
|
54
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
55
|
+
return AGENT_SDK_IDS.includes(value) ? value : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeState(raw) {
|
|
59
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
60
|
+
return STATES.includes(value) ? value : 'missing';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function labelOf(id, status) {
|
|
64
|
+
return String(status?.label || '').trim() || DEFAULT_LABELS[id] || id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The Windows helper only where it can exist; the SDK rows always. */
|
|
68
|
+
export function isAgentSdkRowShown(id, snapshot) {
|
|
69
|
+
if (normalizeId(id) !== 'koffi') return true;
|
|
70
|
+
const status = snapshot?.sdks?.koffi;
|
|
71
|
+
if (!status || typeof status !== 'object') return false;
|
|
72
|
+
if (status.support) return status.support !== 'no';
|
|
73
|
+
return snapshot?.host?.platform === 'win32';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** "Installed · pinned 0.3.1 · installed 0.3.1", with whose install it waits for. */
|
|
77
|
+
export function agentSdkStatusText(status) {
|
|
78
|
+
if (!status || typeof status !== 'object') return 'Checking…';
|
|
79
|
+
const state = normalizeState(status.state);
|
|
80
|
+
const heldBy = Number(status.heldBy) || 0;
|
|
81
|
+
const parts = [state === 'installing' && heldBy
|
|
82
|
+
? `Installing by another OAR process (pid ${heldBy})…`
|
|
83
|
+
: STATE_WORDS[state]];
|
|
84
|
+
const pinned = String(status.pinned || '').trim();
|
|
85
|
+
const installed = String(status.installed || '').trim();
|
|
86
|
+
if (pinned) parts.push(`pinned ${pinned}`);
|
|
87
|
+
if (installed) parts.push(`installed ${installed}`);
|
|
88
|
+
else if (state !== 'unsupported') parts.push('none installed');
|
|
89
|
+
return parts.join(' · ');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function agentSdkStatusState(status) {
|
|
93
|
+
if (!status || typeof status !== 'object') return 'pending';
|
|
94
|
+
const state = normalizeState(status.state);
|
|
95
|
+
if (state === 'ready') return 'active';
|
|
96
|
+
if (state === 'error' && status.kind !== 'cancelled') return 'error';
|
|
97
|
+
if (state === 'installing') return 'pending';
|
|
98
|
+
return 'unconfigured';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatRetryTime(iso) {
|
|
102
|
+
const at = new Date(iso);
|
|
103
|
+
if (Number.isNaN(at.getTime())) return '';
|
|
104
|
+
try {
|
|
105
|
+
return at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
106
|
+
} catch {
|
|
107
|
+
return at.toISOString().slice(11, 16);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The line under the row: the error, the next automatic retry. */
|
|
112
|
+
export function agentSdkNoteText(status) {
|
|
113
|
+
if (!status || typeof status !== 'object') return '';
|
|
114
|
+
const state = normalizeState(status.state);
|
|
115
|
+
const parts = [];
|
|
116
|
+
// "Not available" is the whole story already in the status line.
|
|
117
|
+
const error = String(status.error || '').trim();
|
|
118
|
+
if (error && state === 'error') parts.push(error);
|
|
119
|
+
if (status.retryAt && state === 'error') {
|
|
120
|
+
const time = formatRetryTime(status.retryAt);
|
|
121
|
+
if (time) parts.push(`Next automatic retry at ${time}.`);
|
|
122
|
+
}
|
|
123
|
+
return parts.join(' ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The buttons a row offers, in order. */
|
|
127
|
+
export function agentSdkActions(status, { reinstall = true } = {}) {
|
|
128
|
+
if (!status || typeof status !== 'object') return [];
|
|
129
|
+
switch (normalizeState(status.state)) {
|
|
130
|
+
case 'missing': return ['install'];
|
|
131
|
+
case 'installing': return ['cancel'];
|
|
132
|
+
case 'error': return ['retry'];
|
|
133
|
+
case 'ready': return reinstall ? ['reinstall'] : [];
|
|
134
|
+
default: return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function agentSdkLogSummary(status) {
|
|
139
|
+
const word = status?.kind === 'cancelled' ? 'Cancelled' : (LOG_WORDS[normalizeState(status?.state)] || 'Install');
|
|
140
|
+
return `${word} · install log`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function part(mount, name) {
|
|
144
|
+
return mount.querySelector(`[data-part="${name}"]`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function setHidden(element, hidden) {
|
|
148
|
+
if (element) element.hidden = !!hidden;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function mountSkeleton(mount, id) {
|
|
152
|
+
if (mount.dataset.agentSdkMounted === '1') return;
|
|
153
|
+
mount.dataset.agentSdkMounted = '1';
|
|
154
|
+
const buttons = ACTIONS.map((action) => (
|
|
155
|
+
`<button type="button" class="summary-btn" data-action="${escHtml(action)}" hidden>${escHtml(ACTION_LABELS[action])}</button>`
|
|
156
|
+
)).join('');
|
|
157
|
+
mount.innerHTML = `
|
|
158
|
+
<div class="settings-row" style="margin-bottom: 0;">
|
|
159
|
+
<span class="cli-install-label" data-part="label">${escHtml(DEFAULT_LABELS[id])}</span>
|
|
160
|
+
<div class="settings-help settings-provider-status" data-part="status" style="flex: 1 1 auto;">Checking…</div>
|
|
161
|
+
<div class="settings-inline-actions">${buttons}</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="settings-help settings-provider-status settings-gap-xs" data-part="note" hidden></div>
|
|
164
|
+
<details class="cli-install-log" data-part="log-details" hidden>
|
|
165
|
+
<summary data-part="log-summary">Install log</summary>
|
|
166
|
+
<pre data-part="log"></pre>
|
|
167
|
+
</details>
|
|
168
|
+
`;
|
|
169
|
+
for (const button of mount.querySelectorAll('button[data-action]')) {
|
|
170
|
+
button.addEventListener('click', () => { void runAgentSdkAction(id, button.dataset.action); });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderMount(mount, { expandFailure = false } = {}) {
|
|
175
|
+
const id = normalizeId(mount.dataset.agentSdk);
|
|
176
|
+
if (!id) return;
|
|
177
|
+
mountSkeleton(mount, id);
|
|
178
|
+
const status = sdkState.sdks?.[id] || null;
|
|
179
|
+
const shown = isAgentSdkRowShown(id, sdkState);
|
|
180
|
+
setHidden(mount, !shown);
|
|
181
|
+
if (!shown) return;
|
|
182
|
+
|
|
183
|
+
const label = part(mount, 'label');
|
|
184
|
+
if (label) label.textContent = labelOf(id, status);
|
|
185
|
+
|
|
186
|
+
const statusLine = part(mount, 'status');
|
|
187
|
+
if (statusLine) {
|
|
188
|
+
// The same words as the store's error, which the note shows already.
|
|
189
|
+
const localError = localErrors[id] && localErrors[id] !== String(status?.error || '').trim() ? localErrors[id] : '';
|
|
190
|
+
statusLine.textContent = localError
|
|
191
|
+
|| (!status && loadFailed ? 'Unable to load the SDK status.' : agentSdkStatusText(status));
|
|
192
|
+
statusLine.dataset.state = localError || (!status && loadFailed) ? 'error' : agentSdkStatusState(status);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The Windows helper has nothing to reinstall: its state and a Retry.
|
|
196
|
+
const offered = agentSdkActions(status, { reinstall: id !== 'koffi' });
|
|
197
|
+
const busy = requestsInFlight.has(id);
|
|
198
|
+
for (const button of mount.querySelectorAll('button[data-action]')) {
|
|
199
|
+
const action = button.dataset.action;
|
|
200
|
+
setHidden(button, !offered.includes(action));
|
|
201
|
+
// canInstall is false while spawns are off or the folder is unusable;
|
|
202
|
+
// Cancel needs nothing but the install it stops.
|
|
203
|
+
button.disabled = busy || (action !== 'cancel' && status?.canInstall === false);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const note = part(mount, 'note');
|
|
207
|
+
const noteText = agentSdkNoteText(status);
|
|
208
|
+
if (note) {
|
|
209
|
+
note.textContent = noteText;
|
|
210
|
+
note.dataset.state = noteText && status?.kind !== 'cancelled' ? 'error' : '';
|
|
211
|
+
}
|
|
212
|
+
setHidden(note, !noteText);
|
|
213
|
+
|
|
214
|
+
const details = part(mount, 'log-details');
|
|
215
|
+
const log = String(status?.log || '');
|
|
216
|
+
const showLog = !!status && (normalizeState(status.state) === 'installing' || log.length > 0);
|
|
217
|
+
setHidden(details, !showLog);
|
|
218
|
+
const summary = part(mount, 'log-summary');
|
|
219
|
+
if (summary && status) summary.textContent = agentSdkLogSummary(status);
|
|
220
|
+
const logBox = part(mount, 'log');
|
|
221
|
+
// Keyed on the install, not on logSeq alone: a new install starts with an
|
|
222
|
+
// empty buffer.
|
|
223
|
+
const logKey = `${status?.startedAt || ''}|${status?.logSeq || 0}`;
|
|
224
|
+
if (logBox && showLog && mount.dataset.logKey !== logKey) {
|
|
225
|
+
logBox.textContent = log;
|
|
226
|
+
logBox.scrollTop = logBox.scrollHeight;
|
|
227
|
+
mount.dataset.logKey = logKey;
|
|
228
|
+
}
|
|
229
|
+
if (details && showLog && expandFailure) details.open = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function renderAgentSdkRows() {
|
|
233
|
+
if (typeof document === 'undefined') return;
|
|
234
|
+
const expand = new Set();
|
|
235
|
+
for (const id of AGENT_SDK_IDS) {
|
|
236
|
+
const status = sdkState.sdks?.[id];
|
|
237
|
+
if (!status || normalizeState(status.state) !== 'error' || !status.log) continue;
|
|
238
|
+
const signature = `${status.startedAt || ''}|${status.finishedAt || ''}`;
|
|
239
|
+
if (expandedFailures.get(id) === signature) continue;
|
|
240
|
+
expandedFailures.set(id, signature);
|
|
241
|
+
if (status.kind !== 'cancelled') expand.add(id);
|
|
242
|
+
}
|
|
243
|
+
for (const mount of document.querySelectorAll('[data-agent-sdk]')) {
|
|
244
|
+
renderMount(mount, { expandFailure: expand.has(normalizeId(mount.dataset.agentSdk)) });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// A broadcast can outrun the HTTP response that started it. Within one
|
|
249
|
+
// install (same startedAt) logSeq only grows, so an older snapshot never
|
|
250
|
+
// regresses the log or the state.
|
|
251
|
+
function isStaleStatus(current, incoming) {
|
|
252
|
+
if (!current || !incoming) return false;
|
|
253
|
+
if (current.startedAt && incoming.startedAt && incoming.startedAt < current.startedAt) return true;
|
|
254
|
+
if (!current.startedAt || current.startedAt !== incoming.startedAt) return false;
|
|
255
|
+
const currentSeq = Number(current.logSeq || 0);
|
|
256
|
+
const incomingSeq = Number(incoming.logSeq || 0);
|
|
257
|
+
if (incomingSeq < currentSeq) return true;
|
|
258
|
+
// Equal: a settled install is not reopened by the slower answer that
|
|
259
|
+
// snapshotted its start.
|
|
260
|
+
return incomingSeq === currentSeq
|
|
261
|
+
&& normalizeState(current.state) !== 'installing'
|
|
262
|
+
&& normalizeState(incoming.state) === 'installing';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Single entry point for every payload (GET, POST answers, the broadcast).
|
|
267
|
+
* It also feeds provider readiness, so a failed SDK shows up wherever that is
|
|
268
|
+
* asked.
|
|
269
|
+
*/
|
|
270
|
+
export function applyAgentSdkState(payload) {
|
|
271
|
+
if (!payload || typeof payload !== 'object' || !payload.sdks || typeof payload.sdks !== 'object') {
|
|
272
|
+
renderAgentSdkRows();
|
|
273
|
+
return sdkState;
|
|
274
|
+
}
|
|
275
|
+
const sdks = { ...sdkState.sdks };
|
|
276
|
+
for (const id of AGENT_SDK_IDS) {
|
|
277
|
+
const incoming = payload.sdks[id];
|
|
278
|
+
if (!incoming || typeof incoming !== 'object' || isStaleStatus(sdks[id], incoming)) continue;
|
|
279
|
+
const before = sdks[id];
|
|
280
|
+
if (!before || before.state !== incoming.state || before.startedAt !== incoming.startedAt) delete localErrors[id];
|
|
281
|
+
sdks[id] = incoming;
|
|
282
|
+
}
|
|
283
|
+
sdkState = {
|
|
284
|
+
root: typeof payload.root === 'string' ? payload.root : sdkState.root,
|
|
285
|
+
host: payload.host && typeof payload.host === 'object' ? payload.host : sdkState.host,
|
|
286
|
+
sdks,
|
|
287
|
+
};
|
|
288
|
+
loadFailed = false;
|
|
289
|
+
updateProviderReadinessInputs({ agentSdks: sdkState });
|
|
290
|
+
renderAgentSdkRows();
|
|
291
|
+
return sdkState;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function getAgentSdkState() {
|
|
295
|
+
return sdkState;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Reads the rows; a second caller joins the read in flight. */
|
|
299
|
+
export function refreshAgentSdkRows() {
|
|
300
|
+
if (statusRefresh) return statusRefresh;
|
|
301
|
+
const promise = (async () => {
|
|
302
|
+
try {
|
|
303
|
+
const payload = await getAgentSdkStatus();
|
|
304
|
+
if (!payload) {
|
|
305
|
+
loadFailed = true;
|
|
306
|
+
renderAgentSdkRows();
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return applyAgentSdkState(payload);
|
|
310
|
+
} finally {
|
|
311
|
+
if (statusRefresh === promise) statusRefresh = null;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
statusRefresh = promise;
|
|
315
|
+
return promise;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Install, Retry, Reinstall or Cancel, from a row's button. */
|
|
319
|
+
export async function runAgentSdkAction(sdk, action) {
|
|
320
|
+
const id = normalizeId(sdk);
|
|
321
|
+
const wanted = ACTIONS.includes(action) ? action : '';
|
|
322
|
+
if (!id || !wanted || requestsInFlight.has(id)) return;
|
|
323
|
+
requestsInFlight.add(id);
|
|
324
|
+
delete localErrors[id];
|
|
325
|
+
renderAgentSdkRows();
|
|
326
|
+
try {
|
|
327
|
+
const payload = wanted === 'cancel'
|
|
328
|
+
? await cancelAgentSdkInstall(id)
|
|
329
|
+
: await startAgentSdkInstall(id, { reinstall: wanted === 'reinstall' });
|
|
330
|
+
applyAgentSdkState(payload);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
localErrors = { ...localErrors, [id]: String(error?.message || 'The SDK request failed.') };
|
|
333
|
+
// The refusal carried the rows, but as a throw: read them again.
|
|
334
|
+
await refreshAgentSdkRows().catch(() => {});
|
|
335
|
+
} finally {
|
|
336
|
+
requestsInFlight.delete(id);
|
|
337
|
+
renderAgentSdkRows();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Tests only. */
|
|
342
|
+
export function resetAgentSdkUiForTests() {
|
|
343
|
+
sdkState = { root: null, host: null, sdks: {} };
|
|
344
|
+
loadFailed = false;
|
|
345
|
+
localErrors = {};
|
|
346
|
+
requestsInFlight.clear();
|
|
347
|
+
expandedFailures.clear();
|
|
348
|
+
statusRefresh = null;
|
|
349
|
+
}
|
|
@@ -331,6 +331,8 @@ export async function updateClaudeSettings({
|
|
|
331
331
|
enabled = undefined,
|
|
332
332
|
model = undefined,
|
|
333
333
|
enabledModels = undefined,
|
|
334
|
+
shownModels = undefined,
|
|
335
|
+
autoEnableNew = undefined,
|
|
334
336
|
attributionMode = undefined,
|
|
335
337
|
backgroundCommandLimitMinutes = undefined,
|
|
336
338
|
} = {}) {
|
|
@@ -338,6 +340,8 @@ export async function updateClaudeSettings({
|
|
|
338
340
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
339
341
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
340
342
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
343
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
344
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
341
345
|
if (typeof attributionMode === 'string' && attributionMode.trim()) payload.attributionMode = attributionMode.trim();
|
|
342
346
|
if (Number.isFinite(backgroundCommandLimitMinutes)) payload.backgroundCommandLimitMinutes = backgroundCommandLimitMinutes;
|
|
343
347
|
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
@@ -393,6 +397,25 @@ export async function cancelCliInstall() {
|
|
|
393
397
|
return settingsRequest('/api/cli/install/cancel', null, 'Failed to cancel the CLI install');
|
|
394
398
|
}
|
|
395
399
|
|
|
400
|
+
// Agent SDKs the relay installs on demand. As for the CLIs, the body carries
|
|
401
|
+
// only an SDK id, which the relay looks up in its own table.
|
|
402
|
+
export async function getAgentSdkStatus() {
|
|
403
|
+
return apiFetch('/api/agent-sdks/status');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function startAgentSdkInstall(sdk, { reinstall = false } = {}) {
|
|
407
|
+
return settingsRequest(
|
|
408
|
+
'/api/agent-sdks/install',
|
|
409
|
+
{ sdk: String(sdk || ''), reinstall: reinstall === true },
|
|
410
|
+
'Failed to start the SDK install',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Stops a running install, and dismisses a failure otherwise.
|
|
415
|
+
export async function cancelAgentSdkInstall(sdk) {
|
|
416
|
+
return settingsRequest('/api/agent-sdks/install/cancel', { sdk: String(sdk || '') }, 'Failed to cancel the SDK install');
|
|
417
|
+
}
|
|
418
|
+
|
|
396
419
|
// Grok account auth (Sign in / Sign out). One route shorter than Claude's: the
|
|
397
420
|
// device code rides in the authorize URL and the CLI polls x.ai itself, so
|
|
398
421
|
// nothing is ever pasted back through the relay.
|
|
@@ -424,11 +447,15 @@ export async function updateGrokSettings({
|
|
|
424
447
|
enabled = undefined,
|
|
425
448
|
model = undefined,
|
|
426
449
|
enabledModels = undefined,
|
|
450
|
+
shownModels = undefined,
|
|
451
|
+
autoEnableNew = undefined,
|
|
427
452
|
} = {}) {
|
|
428
453
|
const payload = {};
|
|
429
454
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
430
455
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
431
456
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
457
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
458
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
432
459
|
return settingsRequest('/api/settings/grok', payload, 'Failed to update Grok settings');
|
|
433
460
|
}
|
|
434
461
|
|
|
@@ -493,18 +520,26 @@ export async function loadClaudeCloudBranches(repo) {
|
|
|
493
520
|
|
|
494
521
|
export async function updateOpenAISettings({
|
|
495
522
|
apiKey = '',
|
|
496
|
-
model =
|
|
523
|
+
model = undefined,
|
|
497
524
|
baseUrl = undefined,
|
|
498
525
|
enabled = undefined,
|
|
526
|
+
enabledModels = undefined,
|
|
527
|
+
shownModels = undefined,
|
|
528
|
+
autoEnableNew = undefined,
|
|
499
529
|
remove = false,
|
|
500
530
|
} = {}) {
|
|
501
531
|
const payload = {
|
|
502
532
|
apiKey: String(apiKey || '').trim(),
|
|
503
|
-
model: String(model || '').trim() || 'gpt-4o',
|
|
504
533
|
remove: remove === true,
|
|
505
534
|
};
|
|
535
|
+
// Every Settings call names the model; the Select Models save does not, and
|
|
536
|
+
// must not reset the default model to gpt-4o.
|
|
537
|
+
if (model !== undefined) payload.model = String(model || '').trim() || 'gpt-4o';
|
|
506
538
|
if (typeof baseUrl === 'string') payload.baseUrl = String(baseUrl).trim();
|
|
507
539
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
540
|
+
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
541
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
542
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
508
543
|
return settingsRequest('/api/settings/openai', payload, 'Failed to update OpenAI settings');
|
|
509
544
|
}
|
|
510
545
|
|
|
@@ -517,6 +552,8 @@ export async function updateCursorSettings({
|
|
|
517
552
|
model = undefined,
|
|
518
553
|
enabled = undefined,
|
|
519
554
|
enabledModels = undefined,
|
|
555
|
+
shownModels = undefined,
|
|
556
|
+
autoEnableNew = undefined,
|
|
520
557
|
remove = false,
|
|
521
558
|
} = {}) {
|
|
522
559
|
const payload = {
|
|
@@ -528,6 +565,8 @@ export async function updateCursorSettings({
|
|
|
528
565
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
529
566
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
530
567
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
568
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
569
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
531
570
|
return settingsRequest('/api/settings/cursor', payload, 'Failed to update Cursor settings');
|
|
532
571
|
}
|
|
533
572
|
|
|
@@ -546,6 +585,8 @@ export async function updateDeepSeekSettings({
|
|
|
546
585
|
model = undefined,
|
|
547
586
|
enabled = undefined,
|
|
548
587
|
enabledModels = undefined,
|
|
588
|
+
shownModels = undefined,
|
|
589
|
+
autoEnableNew = undefined,
|
|
549
590
|
baseUrl = undefined,
|
|
550
591
|
attribution = undefined,
|
|
551
592
|
monthlyBudget = undefined,
|
|
@@ -563,6 +604,8 @@ export async function updateDeepSeekSettings({
|
|
|
563
604
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
564
605
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
565
606
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
607
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
608
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
566
609
|
if (baseUrl === null || typeof baseUrl === 'string') payload.baseUrl = baseUrl === null ? null : String(baseUrl).trim();
|
|
567
610
|
if (typeof attribution === 'string' && attribution.trim()) payload.attribution = attribution.trim();
|
|
568
611
|
if (monthlyBudget === null || (typeof monthlyBudget === 'number' && Number.isFinite(monthlyBudget))) payload.monthlyBudget = monthlyBudget;
|
|
@@ -590,6 +633,8 @@ export async function updateClaudeApiSettings({
|
|
|
590
633
|
model = undefined,
|
|
591
634
|
enabled = undefined,
|
|
592
635
|
enabledModels = undefined,
|
|
636
|
+
shownModels = undefined,
|
|
637
|
+
autoEnableNew = undefined,
|
|
593
638
|
monthlyCapUsd = undefined,
|
|
594
639
|
remove = false,
|
|
595
640
|
} = {}) {
|
|
@@ -602,6 +647,8 @@ export async function updateClaudeApiSettings({
|
|
|
602
647
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
603
648
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
604
649
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
650
|
+
if (Array.isArray(shownModels)) payload.shownModels = shownModels;
|
|
651
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
605
652
|
if (monthlyCapUsd === null || (typeof monthlyCapUsd === 'number' && Number.isFinite(monthlyCapUsd))) payload.monthlyCapUsd = monthlyCapUsd;
|
|
606
653
|
}
|
|
607
654
|
return settingsRequest('/api/settings/claude-api', payload, 'Failed to update Claude API settings');
|
|
@@ -940,14 +987,50 @@ export async function refreshModelVariantCatalog() {
|
|
|
940
987
|
return apiFetch('/api/model-variants/refresh', { method: 'POST' });
|
|
941
988
|
}
|
|
942
989
|
|
|
943
|
-
|
|
990
|
+
/**
|
|
991
|
+
* Copilot's selection and its "Turn new models on automatically". A null
|
|
992
|
+
* list leaves the selection alone (only the switch changes): an empty one is
|
|
993
|
+
* refused, at least one variant stays on. `shownVariantIds` names the rows
|
|
994
|
+
* the save could see: the relay turns only those on or off.
|
|
995
|
+
*/
|
|
996
|
+
export async function saveEnabledModelVariants(enabledVariantIds = [], { autoEnableNew = undefined, shownVariantIds = undefined } = {}) {
|
|
997
|
+
const payload = {};
|
|
998
|
+
if (enabledVariantIds !== null) {
|
|
999
|
+
payload.enabledVariantIds = Array.isArray(enabledVariantIds)
|
|
1000
|
+
? enabledVariantIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
1001
|
+
: [];
|
|
1002
|
+
}
|
|
1003
|
+
if (Array.isArray(shownVariantIds)) {
|
|
1004
|
+
payload.shownVariantIds = shownVariantIds.map((value) => String(value || '').trim()).filter(Boolean);
|
|
1005
|
+
}
|
|
1006
|
+
if (typeof autoEnableNew === 'boolean') payload.autoEnableNew = autoEnableNew;
|
|
944
1007
|
return apiFetch('/api/model-variants', {
|
|
945
1008
|
method: 'PATCH',
|
|
946
|
-
body: JSON.stringify(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1009
|
+
body: JSON.stringify(payload),
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Opening Select Models clears every provider's "New" tags and the menu dot;
|
|
1014
|
+
// answers `{ ok, unseenNewModelCount }`.
|
|
1015
|
+
export async function markModelsSeen() {
|
|
1016
|
+
return apiFetch('/api/model-variants/seen', { method: 'POST' });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Settings › General › Models: the background check for new models. A failed
|
|
1020
|
+
// read throws with the relay's reason, which the section shows.
|
|
1021
|
+
export async function loadModelRefreshSettings() {
|
|
1022
|
+
return apiFetchOrThrow('/api/model-refresh-settings');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Any of `{ auto, intervalHours, excluded }`; a refused value throws with the relay's reason. */
|
|
1026
|
+
export async function updateModelRefreshSettings(patch = {}) {
|
|
1027
|
+
const payload = {};
|
|
1028
|
+
if (typeof patch.auto === 'boolean') payload.auto = patch.auto;
|
|
1029
|
+
if (patch.intervalHours !== undefined) payload.intervalHours = Number(patch.intervalHours);
|
|
1030
|
+
if (Array.isArray(patch.excluded)) payload.excluded = patch.excluded.map((value) => String(value || '').trim()).filter(Boolean);
|
|
1031
|
+
return apiFetchOrThrow('/api/model-refresh-settings', {
|
|
1032
|
+
method: 'POST',
|
|
1033
|
+
body: JSON.stringify(payload),
|
|
951
1034
|
});
|
|
952
1035
|
}
|
|
953
1036
|
|