@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
|
@@ -1526,14 +1526,61 @@ function resolveGeneratedImageExtension(mimeType) {
|
|
|
1526
1526
|
return GENERATED_IMAGE_EXTENSIONS[normalized] || 'png';
|
|
1527
1527
|
}
|
|
1528
1528
|
|
|
1529
|
+
// Base64 payloads are checked with a character loop, never with a regular
|
|
1530
|
+
// expression over the whole string. V8 compiles a regexp without
|
|
1531
|
+
// optimisation once the process has generated more than 1 MB of regexp code
|
|
1532
|
+
// and holds more than 16 MB of executable memory (TooMuchRegExpCode in
|
|
1533
|
+
// regexp.cc), which a relay reaches after some uptime. An unoptimised
|
|
1534
|
+
// `[...]+` then pushes one backtrack frame per character and overflows the
|
|
1535
|
+
// 64 MiB regexp stack at 4,194,304 characters, a PNG of about 3 MB, with
|
|
1536
|
+
// "Maximum call stack size exceeded": every gpt-image reply above that size
|
|
1537
|
+
// was dropped after the provider had billed it, and the row was retried four
|
|
1538
|
+
// more times (2026-11-07). A fresh process optimises the same regexp and
|
|
1539
|
+
// never shows it.
|
|
1540
|
+
// An empty string is not base64 here: every caller checks for a missing
|
|
1541
|
+
// payload first and answers it with its own message.
|
|
1542
|
+
export function isBase64Text(value) {
|
|
1543
|
+
const text = String(value || '');
|
|
1544
|
+
if (!text) return false;
|
|
1545
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1546
|
+
const c = text.charCodeAt(i);
|
|
1547
|
+
if (
|
|
1548
|
+
(c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
|
|
1549
|
+
|| c === 43 || c === 47 || c === 61 // + / =
|
|
1550
|
+
|| c === 32 || c === 9 || c === 10 || c === 13 || c === 11 || c === 12 // ASCII whitespace
|
|
1551
|
+
) continue;
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* `data:<mime>;base64,<payload>` taken apart without a regexp over the
|
|
1559
|
+
* payload: `{ mimeType, payload }`, or null when the string is not that
|
|
1560
|
+
* shape (no comma, no `;base64` right before it, a parameter in the type,
|
|
1561
|
+
* an empty payload).
|
|
1562
|
+
*/
|
|
1563
|
+
export function splitBase64DataUrl(dataUrl) {
|
|
1564
|
+
const text = String(dataUrl || '');
|
|
1565
|
+
if (!/^data:/i.test(text)) return null;
|
|
1566
|
+
const comma = text.indexOf(',');
|
|
1567
|
+
if (comma < 0) return null;
|
|
1568
|
+
const header = text.slice('data:'.length, comma);
|
|
1569
|
+
const payload = text.slice(comma + 1);
|
|
1570
|
+
if (!/;base64$/i.test(header) || !payload) return null;
|
|
1571
|
+
const mimeType = header.slice(0, -';base64'.length);
|
|
1572
|
+
if (!mimeType || mimeType.includes(';') || mimeType.includes(',')) return null;
|
|
1573
|
+
return { mimeType, payload };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1529
1576
|
function parseGeneratedImageData(raw) {
|
|
1530
1577
|
const dataUrl = String(raw?.dataUrl || raw?.data_url || '').trim();
|
|
1531
1578
|
if (dataUrl.startsWith('data:')) {
|
|
1532
|
-
const
|
|
1533
|
-
if (!
|
|
1579
|
+
const parts = splitBase64DataUrl(dataUrl);
|
|
1580
|
+
if (!parts) throw new Error('Invalid generated image data URL');
|
|
1534
1581
|
return {
|
|
1535
|
-
mimeType: String(raw?.mimeType || raw?.mime_type ||
|
|
1536
|
-
dataBase64:
|
|
1582
|
+
mimeType: String(raw?.mimeType || raw?.mime_type || parts.mimeType || '').trim().toLowerCase(),
|
|
1583
|
+
dataBase64: parts.payload.trim(),
|
|
1537
1584
|
};
|
|
1538
1585
|
}
|
|
1539
1586
|
return {
|
|
@@ -1552,7 +1599,7 @@ export function normalizeGeneratedImageResponses(rawGeneratedImages, { maxImages
|
|
|
1552
1599
|
const mimeType = parsed.mimeType || 'image/png';
|
|
1553
1600
|
if (!mimeType.startsWith('image/')) throw new Error('Generated image type must be image/*');
|
|
1554
1601
|
if (!parsed.dataBase64) throw new Error('Generated image payload is missing base64 data');
|
|
1555
|
-
if (
|
|
1602
|
+
if (!isBase64Text(parsed.dataBase64)) {
|
|
1556
1603
|
throw new Error('Generated image payload is not valid base64');
|
|
1557
1604
|
}
|
|
1558
1605
|
const buffer = Buffer.from(parsed.dataBase64, 'base64');
|
|
@@ -1578,7 +1625,7 @@ export function shouldAcceptAssistantResponsePayload({ text = '', terminalFailur
|
|
|
1578
1625
|
function parseOpenAIImageBase64Value(value) {
|
|
1579
1626
|
const raw = String(value || '').trim();
|
|
1580
1627
|
if (!raw) return '';
|
|
1581
|
-
if (
|
|
1628
|
+
if (!isBase64Text(raw)) {
|
|
1582
1629
|
throw new Error('OpenAI image payload contains invalid base64 data');
|
|
1583
1630
|
}
|
|
1584
1631
|
return Buffer.from(raw, 'base64').toString('base64');
|
|
@@ -1669,10 +1716,10 @@ export function resolveOpenAIImageEditAttachment(rawAttachments, {
|
|
|
1669
1716
|
let bytes = null;
|
|
1670
1717
|
|
|
1671
1718
|
if (dataUrl.startsWith('data:')) {
|
|
1672
|
-
const
|
|
1673
|
-
if (!
|
|
1674
|
-
mimeType = mimeType || String(
|
|
1675
|
-
const payload = parseOpenAIImageBase64Value(
|
|
1719
|
+
const parts = splitBase64DataUrl(dataUrl);
|
|
1720
|
+
if (!parts) throw new Error('Invalid image attachment data URL');
|
|
1721
|
+
mimeType = mimeType || String(parts.mimeType || '').trim().toLowerCase();
|
|
1722
|
+
const payload = parseOpenAIImageBase64Value(parts.payload);
|
|
1676
1723
|
bytes = Buffer.from(payload, 'base64');
|
|
1677
1724
|
} else {
|
|
1678
1725
|
const filePath = String(firstImage.path || '').trim();
|
|
@@ -7806,9 +7853,16 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7806
7853
|
});
|
|
7807
7854
|
} catch (error) {
|
|
7808
7855
|
const status = Number(error?.status || 0);
|
|
7809
|
-
|
|
7856
|
+
// `relayProcessing`: the provider answered 2xx and the relay could not
|
|
7857
|
+
// process the reply. The generation is billed, a retry pays again and
|
|
7858
|
+
// fails the same way, so the row fails like a refusal.
|
|
7859
|
+
const relayProcessing = generatedPayload?.relayProcessing === true;
|
|
7860
|
+
const terminal = (status >= 400 && status < 500) || relayProcessing;
|
|
7810
7861
|
const uncertain = !!generatedPayload?.generatedImages;
|
|
7811
7862
|
const outcome = terminal ? 'terminal' : (uncertain ? 'uncertain' : 'retryable');
|
|
7863
|
+
const errorMessage = relayProcessing
|
|
7864
|
+
? `The relay could not process the image OpenAI returned (${error?.message || 'unknown error'}). The generation was billed; the message was not retried.`
|
|
7865
|
+
: String(error?.message || 'Image operation failed');
|
|
7812
7866
|
stmts.finishAttempt.run({
|
|
7813
7867
|
id: acquired.attempt.id,
|
|
7814
7868
|
status: terminal ? 'failed_terminal' : (uncertain ? 'uncertain' : 'failed_retryable'),
|
|
@@ -7816,8 +7870,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7816
7870
|
providerConversationId: null,
|
|
7817
7871
|
providerResponseId: null,
|
|
7818
7872
|
httpStatus: status || null,
|
|
7819
|
-
errorCode: terminal
|
|
7820
|
-
|
|
7873
|
+
errorCode: terminal
|
|
7874
|
+
? (relayProcessing ? 'IMAGE_REPLY_UNPROCESSED' : 'IMAGE_PROVIDER_REJECTED')
|
|
7875
|
+
: (uncertain ? 'IMAGE_FINALIZATION_UNCERTAIN' : 'IMAGE_PROVIDER_UNAVAILABLE'),
|
|
7876
|
+
errorMessage: errorMessage.slice(0, 500),
|
|
7821
7877
|
completedAt: new Date().toISOString(),
|
|
7822
7878
|
});
|
|
7823
7879
|
if (outcome === 'retryable') {
|
|
@@ -7831,8 +7887,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7831
7887
|
}
|
|
7832
7888
|
} else {
|
|
7833
7889
|
const failure = {
|
|
7834
|
-
code: outcome === 'uncertain'
|
|
7835
|
-
|
|
7890
|
+
code: outcome === 'uncertain'
|
|
7891
|
+
? 'IMAGE_FINALIZATION_UNCERTAIN'
|
|
7892
|
+
: (relayProcessing ? 'IMAGE_REPLY_UNPROCESSED' : 'IMAGE_PROVIDER_REJECTED'),
|
|
7893
|
+
message: errorMessage.slice(0, 500),
|
|
7836
7894
|
retryable: false,
|
|
7837
7895
|
};
|
|
7838
7896
|
const failResponse = await fetch(`${selfBaseUrl}${remotePath}/api/response`, {
|
|
@@ -7947,7 +8005,13 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7947
8005
|
try {
|
|
7948
8006
|
payload = JSON.parse(rawText);
|
|
7949
8007
|
} catch {
|
|
7950
|
-
|
|
8008
|
+
// After a 2xx the generation is billed; the callers fail the row
|
|
8009
|
+
// instead of paying for it again. An error status with a body that
|
|
8010
|
+
// is not JSON stays a provider failure.
|
|
8011
|
+
return res.status(502).json({
|
|
8012
|
+
error: 'OpenAI image API returned invalid JSON',
|
|
8013
|
+
...(response.ok ? { relayProcessing: true } : {}),
|
|
8014
|
+
});
|
|
7951
8015
|
}
|
|
7952
8016
|
}
|
|
7953
8017
|
|
|
@@ -7969,7 +8033,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7969
8033
|
maxImages: MAX_UPLOAD_ATTACHMENTS,
|
|
7970
8034
|
});
|
|
7971
8035
|
} catch (error) {
|
|
7972
|
-
|
|
8036
|
+
// The provider answered 2xx, so the generation is billed; what failed
|
|
8037
|
+
// is the relay's handling of the reply. Logged with the stack, and
|
|
8038
|
+
// flagged so the callers fail the row instead of requeueing it: a
|
|
8039
|
+
// retry pays again and fails the same way.
|
|
8040
|
+
console.error(
|
|
8041
|
+
`[${ts()}] IMAGE reply of ${requestBody.model} not processed: ${error?.message || error} (body ${rawText.length} chars)\n${error?.stack || ''}`,
|
|
8042
|
+
);
|
|
8043
|
+
return res.status(502).json({
|
|
8044
|
+
error: error?.message || 'OpenAI image API returned no images',
|
|
8045
|
+
relayProcessing: true,
|
|
8046
|
+
});
|
|
7973
8047
|
}
|
|
7974
8048
|
|
|
7975
8049
|
return res.json({
|