@mytegroupinc/myte-core 0.0.38 → 0.0.39
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/mytecody-cli.js +158 -13
- package/package.json +1 -1
- package/lib/mytecody-async-responses-bridge.js +0 -431
package/mytecody-cli.js
CHANGED
|
@@ -12,9 +12,6 @@ const {
|
|
|
12
12
|
normalizeMyteAiBase,
|
|
13
13
|
} = require("./lib/ai-gateway");
|
|
14
14
|
const { createMyteSplash } = require("./lib/mytecody-splash");
|
|
15
|
-
const {
|
|
16
|
-
startMyteCodyAsyncResponsesBridge,
|
|
17
|
-
} = require("./lib/mytecody-async-responses-bridge");
|
|
18
15
|
|
|
19
16
|
const PACKAGE_NAME = "myte";
|
|
20
17
|
const PACKAGE_VERSION = require("./package.json").version;
|
|
@@ -154,13 +151,21 @@ function installRoot() {
|
|
|
154
151
|
return path.join(os.homedir(), ".myte", "cody");
|
|
155
152
|
}
|
|
156
153
|
|
|
154
|
+
function currentInstallRoot() {
|
|
155
|
+
return path.join(installRoot(), "current");
|
|
156
|
+
}
|
|
157
|
+
|
|
157
158
|
function currentClientManifestPath() {
|
|
158
|
-
return path.join(
|
|
159
|
+
return path.join(currentInstallRoot(), "manifest.json");
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
function currentEnginePath() {
|
|
162
163
|
const executable = process.platform === "win32" ? "mytecody-engine.exe" : "mytecody-engine";
|
|
163
|
-
return path.join(
|
|
164
|
+
return path.join(currentInstallRoot(), "bin", executable);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function currentBridgePath() {
|
|
168
|
+
return path.join(currentInstallRoot(), "lib", "mytecody-async-responses-bridge.js");
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
function codexHome() {
|
|
@@ -187,6 +192,96 @@ function installedClientCommand() {
|
|
|
187
192
|
return { cmd: enginePath, args: [], source: "myte-installed-engine" };
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
function loadSignedBridge() {
|
|
196
|
+
const bridgePath = currentBridgePath();
|
|
197
|
+
if (!fs.existsSync(bridgePath)) {
|
|
198
|
+
throw new Error("signed MyteCody inference bridge asset is missing; run `mytecody update`.");
|
|
199
|
+
}
|
|
200
|
+
const bridge = require(bridgePath);
|
|
201
|
+
if (!bridge || typeof bridge.startMyteCodyAsyncResponsesBridge !== "function") {
|
|
202
|
+
throw new Error("signed MyteCody inference bridge asset is invalid.");
|
|
203
|
+
}
|
|
204
|
+
return bridge;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function releaseAssetsForPlatform(manifest, artifact) {
|
|
208
|
+
const assets = [];
|
|
209
|
+
const collect = (value, source) => {
|
|
210
|
+
if (!value) return;
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
value.forEach((item, index) => {
|
|
213
|
+
if (item && typeof item === "object") {
|
|
214
|
+
assets.push({ ...item, source, index });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === "object") {
|
|
220
|
+
Object.entries(value).forEach(([name, item], index) => {
|
|
221
|
+
if (item && typeof item === "object") {
|
|
222
|
+
assets.push({ name: item.name || name, ...item, source, index });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
collect(manifest && manifest.client_assets, "manifest.client_assets");
|
|
229
|
+
collect(artifact && artifact.assets, "artifact.assets");
|
|
230
|
+
|
|
231
|
+
return assets
|
|
232
|
+
.map((asset) => ({
|
|
233
|
+
...asset,
|
|
234
|
+
install_path: asset.install_path || asset.path || "",
|
|
235
|
+
}))
|
|
236
|
+
.filter((asset) => asset && asset.url && asset.sha256 && asset.install_path);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function assertSafeReleaseAssetInstallPath(installPath) {
|
|
240
|
+
const raw = String(installPath || "").replace(/\\/g, "/").trim();
|
|
241
|
+
if (!raw) throw new Error("release asset install_path is required");
|
|
242
|
+
if (path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw)) {
|
|
243
|
+
throw new Error(`release asset install_path must be relative: ${installPath}`);
|
|
244
|
+
}
|
|
245
|
+
const parts = raw.split("/").filter(Boolean);
|
|
246
|
+
if (!parts.length || parts.includes("..")) {
|
|
247
|
+
throw new Error(`release asset install_path is unsafe: ${installPath}`);
|
|
248
|
+
}
|
|
249
|
+
return path.join(currentInstallRoot(), ...parts);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function currentReleaseAssetRecordByPath(current) {
|
|
253
|
+
const map = new Map();
|
|
254
|
+
for (const asset of Array.isArray(current && current.assets) ? current.assets : []) {
|
|
255
|
+
if (asset && asset.install_path) {
|
|
256
|
+
map.set(String(asset.install_path).replace(/\\/g, "/"), asset);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return map;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function installedReleaseAssetsMatchManifest(manifest, artifact) {
|
|
263
|
+
const assets = releaseAssetsForPlatform(manifest, artifact);
|
|
264
|
+
if (!assets.length) return true;
|
|
265
|
+
const current = readCurrentClientManifest();
|
|
266
|
+
const currentAssets = currentReleaseAssetRecordByPath(current);
|
|
267
|
+
|
|
268
|
+
for (const asset of assets) {
|
|
269
|
+
const installPath = String(asset.install_path || "").replace(/\\/g, "/");
|
|
270
|
+
const currentAsset = currentAssets.get(installPath);
|
|
271
|
+
if (!currentAsset) return false;
|
|
272
|
+
if (String(currentAsset.sha256 || "").toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
|
|
276
|
+
if (!fs.existsSync(targetPath)) return false;
|
|
277
|
+
const installedSha = sha256Hex(fs.readFileSync(targetPath));
|
|
278
|
+
if (installedSha.toLowerCase() !== String(asset.installed_sha256 || asset.sha256).toLowerCase()) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
190
285
|
function installedClientMatchesManifest(manifest, artifact) {
|
|
191
286
|
const current = readCurrentClientManifest();
|
|
192
287
|
if (!current || !installedClientCommand()) return false;
|
|
@@ -207,6 +302,8 @@ function installedClientMatchesManifest(manifest, artifact) {
|
|
|
207
302
|
}
|
|
208
303
|
}
|
|
209
304
|
|
|
305
|
+
if (!installedReleaseAssetsMatchManifest(manifest, artifact)) return false;
|
|
306
|
+
|
|
210
307
|
return true;
|
|
211
308
|
}
|
|
212
309
|
|
|
@@ -271,8 +368,10 @@ function commonStatus(args, envPath) {
|
|
|
271
368
|
platform: platformKey(),
|
|
272
369
|
install_root: installRoot(),
|
|
273
370
|
engine_path: currentEnginePath(),
|
|
371
|
+
bridge_path: currentBridgePath(),
|
|
274
372
|
client_manifest: currentClientManifestPath(),
|
|
275
373
|
client_installed: Boolean(current && installedClientCommand()),
|
|
374
|
+
bridge_installed: fs.existsSync(currentBridgePath()),
|
|
276
375
|
client_version: current && current.version ? current.version : null,
|
|
277
376
|
},
|
|
278
377
|
};
|
|
@@ -517,11 +616,11 @@ function localPathFromArtifactUrl(urlValue) {
|
|
|
517
616
|
return null;
|
|
518
617
|
}
|
|
519
618
|
|
|
520
|
-
async function readArtifactBytes(artifact, { progress } = {}) {
|
|
619
|
+
async function readArtifactBytes(artifact, { progress, label = "MyteCody engine" } = {}) {
|
|
521
620
|
const urlValue = artifact && artifact.url ? String(artifact.url) : "";
|
|
522
621
|
const localPath = localPathFromArtifactUrl(urlValue);
|
|
523
622
|
if (localPath) {
|
|
524
|
-
if (progress) progress(
|
|
623
|
+
if (progress) progress(`reading local ${label} artifact`);
|
|
525
624
|
return fs.readFileSync(localPath);
|
|
526
625
|
}
|
|
527
626
|
const headers = {};
|
|
@@ -529,7 +628,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
529
628
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
530
629
|
if (progress) {
|
|
531
630
|
const expectedSize = Number(artifact && artifact.size_bytes ? artifact.size_bytes : 0);
|
|
532
|
-
progress(`downloading
|
|
631
|
+
progress(`downloading ${label} (${formatBytes(expectedSize)})`);
|
|
533
632
|
}
|
|
534
633
|
const response = await fetch(urlValue, { method: "GET", headers });
|
|
535
634
|
if (!response.ok) {
|
|
@@ -538,7 +637,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
538
637
|
}
|
|
539
638
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
540
639
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
541
|
-
if (progress) progress(`downloaded
|
|
640
|
+
if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
|
|
542
641
|
return bytes;
|
|
543
642
|
}
|
|
544
643
|
|
|
@@ -556,13 +655,13 @@ async function readArtifactBytes(artifact, { progress } = {}) {
|
|
|
556
655
|
if (progress && total > 0) {
|
|
557
656
|
const pct = Math.min(100, Math.floor((received / total) * 100));
|
|
558
657
|
if (pct >= lastPct + 10 || pct === 100) {
|
|
559
|
-
progress(`downloading
|
|
658
|
+
progress(`downloading ${label} ${pct}% (${formatBytes(received)} / ${formatBytes(total)})`);
|
|
560
659
|
lastPct = pct;
|
|
561
660
|
}
|
|
562
661
|
}
|
|
563
662
|
}
|
|
564
663
|
const bytes = Buffer.concat(chunks);
|
|
565
|
-
if (progress) progress(`downloaded
|
|
664
|
+
if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
|
|
566
665
|
return bytes;
|
|
567
666
|
}
|
|
568
667
|
|
|
@@ -621,6 +720,34 @@ function installArtifactBytes(bytes, manifest, artifact) {
|
|
|
621
720
|
return installedManifest;
|
|
622
721
|
}
|
|
623
722
|
|
|
723
|
+
async function installReleaseAssets(manifest, artifact, { progress } = {}) {
|
|
724
|
+
const assets = releaseAssetsForPlatform(manifest, artifact);
|
|
725
|
+
const installed = [];
|
|
726
|
+
for (const asset of assets) {
|
|
727
|
+
const name = String(asset.name || path.basename(String(asset.install_path || "")) || "client asset");
|
|
728
|
+
const bytes = await readArtifactBytes(asset, { progress, label: `MyteCody ${name}` });
|
|
729
|
+
const digest = sha256Hex(bytes);
|
|
730
|
+
if (digest.toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
|
|
731
|
+
throw new Error(`Release asset SHA-256 mismatch for ${name}: expected ${asset.sha256}, got ${digest}`);
|
|
732
|
+
}
|
|
733
|
+
const installBytes = artifactBytesForInstall(bytes, asset);
|
|
734
|
+
const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
|
|
735
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
736
|
+
fs.writeFileSync(targetPath, installBytes);
|
|
737
|
+
installed.push({
|
|
738
|
+
name,
|
|
739
|
+
install_path: String(asset.install_path || "").replace(/\\/g, "/"),
|
|
740
|
+
url: asset.url,
|
|
741
|
+
sha256: asset.sha256,
|
|
742
|
+
format: artifactFormat(asset),
|
|
743
|
+
size_bytes: bytes.length,
|
|
744
|
+
installed_sha256: sha256Hex(installBytes),
|
|
745
|
+
installed_size_bytes: installBytes.length,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return installed;
|
|
749
|
+
}
|
|
750
|
+
|
|
624
751
|
async function fetchJson(url, { headers = {}, timeoutMs = 8000 } = {}) {
|
|
625
752
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
|
|
626
753
|
const timeoutId =
|
|
@@ -920,7 +1047,8 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
|
|
|
920
1047
|
let bridge = null;
|
|
921
1048
|
try {
|
|
922
1049
|
progress("opening Myte inference bridge");
|
|
923
|
-
|
|
1050
|
+
const signedBridge = loadSignedBridge();
|
|
1051
|
+
bridge = await signedBridge.startMyteCodyAsyncResponsesBridge({
|
|
924
1052
|
gatewayRoot: gatewayRoot(args),
|
|
925
1053
|
token,
|
|
926
1054
|
});
|
|
@@ -969,7 +1097,8 @@ async function runDoctor(args, envPath) {
|
|
|
969
1097
|
if (args["probe-gateway"]) {
|
|
970
1098
|
payload.gateway.probe = await probeGateway(args);
|
|
971
1099
|
}
|
|
972
|
-
payload.ready_for_coding =
|
|
1100
|
+
payload.ready_for_coding =
|
|
1101
|
+
payload.auth.present && payload.release.client_installed && payload.release.bridge_installed;
|
|
973
1102
|
if (payload.gateway.probe) {
|
|
974
1103
|
payload.ready_for_coding = Boolean(payload.ready_for_coding && payload.gateway.probe.ok);
|
|
975
1104
|
}
|
|
@@ -998,6 +1127,7 @@ async function runDoctor(args, envPath) {
|
|
|
998
1127
|
console.log(`package update: ${payload.package.check_status}`);
|
|
999
1128
|
}
|
|
1000
1129
|
console.log(`client: ${payload.release.client_installed ? payload.release.client_version : "not installed"}`);
|
|
1130
|
+
console.log(`bridge: ${payload.release.bridge_installed ? "installed" : "not installed"}`);
|
|
1001
1131
|
console.log(`install: ${payload.release.install_root}`);
|
|
1002
1132
|
console.log("");
|
|
1003
1133
|
console.log("Coding requires the Myte AI gateway and a Myte AI key.");
|
|
@@ -1015,6 +1145,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1015
1145
|
const artifact = manifest ? artifactForPlatform(manifest) : null;
|
|
1016
1146
|
const signature = manifest ? signatureAccepted(manifest, args) : { ok: false, signature: { status: "not-checked", verified: false } };
|
|
1017
1147
|
const artifactMetadata = manifest ? validateArtifactMetadata(artifact) : { status: "not-checked", ok: false };
|
|
1148
|
+
const releaseAssets = manifest ? releaseAssetsForPlatform(manifest, artifact) : [];
|
|
1018
1149
|
const payload = {
|
|
1019
1150
|
ok: true,
|
|
1020
1151
|
dry_run: isDryRun,
|
|
@@ -1028,6 +1159,13 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1028
1159
|
trusted_unsigned: Boolean(signature.trusted_unsigned),
|
|
1029
1160
|
},
|
|
1030
1161
|
artifact: artifactMetadata,
|
|
1162
|
+
release_assets: releaseAssets.map((asset) => ({
|
|
1163
|
+
name: asset.name || null,
|
|
1164
|
+
install_path: asset.install_path,
|
|
1165
|
+
url: asset.url || null,
|
|
1166
|
+
format: asset.format || null,
|
|
1167
|
+
sha256_present: Boolean(asset.sha256),
|
|
1168
|
+
})),
|
|
1031
1169
|
};
|
|
1032
1170
|
|
|
1033
1171
|
if (!isDryRun) {
|
|
@@ -1046,6 +1184,11 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1046
1184
|
throw new Error(`Artifact SHA-256 mismatch: expected ${artifact.sha256}, got ${digest}`);
|
|
1047
1185
|
}
|
|
1048
1186
|
const installed = installArtifactBytes(bytes, manifest, artifact);
|
|
1187
|
+
const installedAssets = await installReleaseAssets(manifest, artifact, { progress });
|
|
1188
|
+
if (installedAssets.length) {
|
|
1189
|
+
installed.assets = installedAssets;
|
|
1190
|
+
fs.writeFileSync(currentClientManifestPath(), JSON.stringify(installed, null, 2), "utf8");
|
|
1191
|
+
}
|
|
1049
1192
|
payload.installed = {
|
|
1050
1193
|
ok: true,
|
|
1051
1194
|
version: installed.version,
|
|
@@ -1055,6 +1198,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
|
|
|
1055
1198
|
installed_sha256: installed.artifact.installed_sha256,
|
|
1056
1199
|
installed_size_bytes: installed.artifact.installed_size_bytes,
|
|
1057
1200
|
format: installed.artifact.format,
|
|
1201
|
+
assets: installedAssets,
|
|
1058
1202
|
};
|
|
1059
1203
|
payload.release = {
|
|
1060
1204
|
...payload.release,
|
|
@@ -1185,6 +1329,7 @@ module.exports = {
|
|
|
1185
1329
|
codexProviderArgs,
|
|
1186
1330
|
codyInferenceBase,
|
|
1187
1331
|
codyGatewayUrl,
|
|
1332
|
+
currentBridgePath,
|
|
1188
1333
|
currentClientManifestPath,
|
|
1189
1334
|
currentEnginePath,
|
|
1190
1335
|
ensureBrandedEngineInstalled,
|
package/package.json
CHANGED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const http = require("node:http");
|
|
4
|
-
const crypto = require("node:crypto");
|
|
5
|
-
|
|
6
|
-
const HOST = "127.0.0.1";
|
|
7
|
-
const PATCH_TOOL_NAME = "apply_patch";
|
|
8
|
-
|
|
9
|
-
function sendJson(res, status, payload, headers = {}) {
|
|
10
|
-
res.writeHead(status, {
|
|
11
|
-
"content-type": "application/json",
|
|
12
|
-
"cache-control": "no-store",
|
|
13
|
-
...headers,
|
|
14
|
-
});
|
|
15
|
-
res.end(JSON.stringify(payload));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readJsonBody(req) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const chunks = [];
|
|
21
|
-
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
22
|
-
req.on("error", reject);
|
|
23
|
-
req.on("end", () => {
|
|
24
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
25
|
-
if (!text.trim()) return resolve(null);
|
|
26
|
-
try {
|
|
27
|
-
resolve(JSON.parse(text));
|
|
28
|
-
} catch (error) {
|
|
29
|
-
reject(new Error(`invalid JSON body: ${error.message || error}`));
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stableJson(value) {
|
|
36
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
37
|
-
if (value && typeof value === "object") {
|
|
38
|
-
return `{${Object.keys(value)
|
|
39
|
-
.sort()
|
|
40
|
-
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
41
|
-
.join(",")}}`;
|
|
42
|
-
}
|
|
43
|
-
return JSON.stringify(value);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function requestHash(value) {
|
|
47
|
-
return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseJsonMaybe(value) {
|
|
51
|
-
if (value && typeof value === "object") return value;
|
|
52
|
-
if (typeof value !== "string") return null;
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(value);
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function extractPatchArgument(argumentsValue) {
|
|
61
|
-
const parsed = parseJsonMaybe(argumentsValue);
|
|
62
|
-
if (parsed && typeof parsed.patch === "string") return parsed.patch;
|
|
63
|
-
if (parsed && typeof parsed.input === "string") return parsed.input;
|
|
64
|
-
if (
|
|
65
|
-
typeof argumentsValue === "string" &&
|
|
66
|
-
argumentsValue.includes("*** Begin Patch") &&
|
|
67
|
-
argumentsValue.includes("*** End Patch")
|
|
68
|
-
) {
|
|
69
|
-
return argumentsValue;
|
|
70
|
-
}
|
|
71
|
-
return "";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function normalizeOutputItem(item) {
|
|
75
|
-
if (!item || typeof item !== "object") return null;
|
|
76
|
-
const normalized = { ...item };
|
|
77
|
-
if (
|
|
78
|
-
normalized.type === "function_call" &&
|
|
79
|
-
normalized.arguments &&
|
|
80
|
-
typeof normalized.arguments !== "string"
|
|
81
|
-
) {
|
|
82
|
-
normalized.arguments = JSON.stringify(normalized.arguments);
|
|
83
|
-
}
|
|
84
|
-
return normalized;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function functionCallToCustomPatchCall(item) {
|
|
88
|
-
if (!item || item.type !== "function_call" || item.name !== PATCH_TOOL_NAME) return null;
|
|
89
|
-
const patch = extractPatchArgument(item.arguments);
|
|
90
|
-
if (!patch) return null;
|
|
91
|
-
const callId = item.call_id || item.id || `call_myte_patch_${Date.now()}`;
|
|
92
|
-
return {
|
|
93
|
-
id: item.id || callId,
|
|
94
|
-
type: "custom_tool_call",
|
|
95
|
-
status: item.status || "completed",
|
|
96
|
-
call_id: callId,
|
|
97
|
-
name: PATCH_TOOL_NAME,
|
|
98
|
-
input: patch,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function adaptOutputItemForCodex(rawItem) {
|
|
103
|
-
const item = normalizeOutputItem(rawItem);
|
|
104
|
-
if (!item) return null;
|
|
105
|
-
return functionCallToCustomPatchCall(item) || item;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function adaptResponsePayloadForCodex(payload) {
|
|
109
|
-
if (!payload || typeof payload !== "object") return payload;
|
|
110
|
-
const output = Array.isArray(payload.output)
|
|
111
|
-
? payload.output.map(adaptOutputItemForCodex).filter(Boolean)
|
|
112
|
-
: payload.output;
|
|
113
|
-
return {
|
|
114
|
-
...payload,
|
|
115
|
-
model: "myte",
|
|
116
|
-
output,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function sse(res, event) {
|
|
121
|
-
res.write(`event: ${event.type}\n`);
|
|
122
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function chunkText(text, size = 4096) {
|
|
126
|
-
const value = String(text || "");
|
|
127
|
-
const chunks = [];
|
|
128
|
-
for (let index = 0; index < value.length; index += size) {
|
|
129
|
-
chunks.push(value.slice(index, index + size));
|
|
130
|
-
}
|
|
131
|
-
return chunks.length ? chunks : [""];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function emitOutputItem(res, item, outputIndex) {
|
|
135
|
-
if (item && item.type === "custom_tool_call" && item.name === PATCH_TOOL_NAME) {
|
|
136
|
-
const itemId = item.id || item.call_id || `call_myte_patch_${Date.now()}`;
|
|
137
|
-
const callId = item.call_id || itemId;
|
|
138
|
-
const input = item.input || "";
|
|
139
|
-
sse(res, {
|
|
140
|
-
type: "response.output_item.added",
|
|
141
|
-
output_index: outputIndex,
|
|
142
|
-
item: {
|
|
143
|
-
...item,
|
|
144
|
-
id: itemId,
|
|
145
|
-
call_id: callId,
|
|
146
|
-
input: "",
|
|
147
|
-
status: "in_progress",
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
for (const delta of chunkText(input)) {
|
|
151
|
-
sse(res, {
|
|
152
|
-
type: "response.custom_tool_call_input.delta",
|
|
153
|
-
output_index: outputIndex,
|
|
154
|
-
item_id: itemId,
|
|
155
|
-
call_id: callId,
|
|
156
|
-
delta,
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
sse(res, {
|
|
160
|
-
type: "response.custom_tool_call_input.done",
|
|
161
|
-
output_index: outputIndex,
|
|
162
|
-
item_id: itemId,
|
|
163
|
-
call_id: callId,
|
|
164
|
-
input,
|
|
165
|
-
});
|
|
166
|
-
sse(res, {
|
|
167
|
-
type: "response.output_item.done",
|
|
168
|
-
output_index: outputIndex,
|
|
169
|
-
item: {
|
|
170
|
-
...item,
|
|
171
|
-
id: itemId,
|
|
172
|
-
call_id: callId,
|
|
173
|
-
input,
|
|
174
|
-
status: "completed",
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
sse(res, {
|
|
181
|
-
type: "response.output_item.done",
|
|
182
|
-
output_index: outputIndex,
|
|
183
|
-
item,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function retryAfterMs(headers, fallbackMs) {
|
|
188
|
-
const headerValue =
|
|
189
|
-
headers && typeof headers.get === "function" ? Number(headers.get("retry-after")) : 0;
|
|
190
|
-
if (Number.isFinite(headerValue) && headerValue > 0) {
|
|
191
|
-
return Math.max(250, Math.min(15000, headerValue * 1000));
|
|
192
|
-
}
|
|
193
|
-
return fallbackMs;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function sleep(ms) {
|
|
197
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function fetchJson(url, options = {}) {
|
|
201
|
-
const response = await fetch(url, options);
|
|
202
|
-
const text = await response.text();
|
|
203
|
-
let body = {};
|
|
204
|
-
if (text.trim()) {
|
|
205
|
-
try {
|
|
206
|
-
body = JSON.parse(text);
|
|
207
|
-
} catch {
|
|
208
|
-
body = { raw_text: text };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return { response, body, text };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function absoluteGatewayUrl(root, pathOrUrl) {
|
|
215
|
-
const value = String(pathOrUrl || "");
|
|
216
|
-
if (/^https?:\/\//i.test(value)) return value;
|
|
217
|
-
const base = String(root || "").replace(/\/+$/, "");
|
|
218
|
-
const tail = value.startsWith("/") ? value : `/${value}`;
|
|
219
|
-
return `${base}${tail}`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function resultPayloadFromJobResult(body) {
|
|
223
|
-
if (body && typeof body === "object" && body.object === "response") return body;
|
|
224
|
-
if (body && typeof body === "object" && body.payload && typeof body.payload === "object") {
|
|
225
|
-
return body.payload;
|
|
226
|
-
}
|
|
227
|
-
return body;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function submitAndDrainResponseJob(body, options) {
|
|
231
|
-
const gatewayRoot = String(options.gatewayRoot || "").replace(/\/+$/, "");
|
|
232
|
-
const token = String(options.token || "").trim();
|
|
233
|
-
if (!gatewayRoot) throw new Error("missing Myte Cody gateway root");
|
|
234
|
-
if (!token) throw new Error("missing MYTEAI_API_KEY");
|
|
235
|
-
|
|
236
|
-
const bridgeRequestId = `codybridge_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
237
|
-
const requestBody = {
|
|
238
|
-
...body,
|
|
239
|
-
request_id: body.request_id || bridgeRequestId,
|
|
240
|
-
stream: false,
|
|
241
|
-
};
|
|
242
|
-
const timeoutMs = Number(options.timeoutMs || process.env.MYTE_CODY_ASYNC_TIMEOUT_MS || 900000);
|
|
243
|
-
const startedAt = Date.now();
|
|
244
|
-
const submitUrl = `${gatewayRoot}/cody/v1/jobs/responses`;
|
|
245
|
-
const authHeaders = {
|
|
246
|
-
authorization: `Bearer ${token}`,
|
|
247
|
-
accept: "application/json",
|
|
248
|
-
};
|
|
249
|
-
const idempotencyKey = `mytecody-${bridgeRequestId}-${requestHash(requestBody).slice(0, 16)}`;
|
|
250
|
-
const submitted = await fetchJson(submitUrl, {
|
|
251
|
-
method: "POST",
|
|
252
|
-
headers: {
|
|
253
|
-
...authHeaders,
|
|
254
|
-
"content-type": "application/json",
|
|
255
|
-
"idempotency-key": idempotencyKey,
|
|
256
|
-
},
|
|
257
|
-
body: JSON.stringify(requestBody),
|
|
258
|
-
});
|
|
259
|
-
if (!submitted.response.ok && submitted.response.status !== 202) {
|
|
260
|
-
throw new Error(
|
|
261
|
-
`MyteCody async submit failed (${submitted.response.status}): ${submitted.text.slice(0, 500)}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const submitBody = submitted.body || {};
|
|
266
|
-
const jobId = submitBody.job_id || submitBody.id;
|
|
267
|
-
const poll = submitBody.poll || {};
|
|
268
|
-
let resultUrl = absoluteGatewayUrl(
|
|
269
|
-
gatewayRoot,
|
|
270
|
-
poll.result_url || (jobId ? `/cody/v1/jobs/${jobId}/result` : ""),
|
|
271
|
-
);
|
|
272
|
-
const statusUrl = absoluteGatewayUrl(
|
|
273
|
-
gatewayRoot,
|
|
274
|
-
poll.status_url || (jobId ? `/cody/v1/jobs/${jobId}` : ""),
|
|
275
|
-
);
|
|
276
|
-
if (!jobId || !resultUrl) {
|
|
277
|
-
return resultPayloadFromJobResult(submitBody);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
let waitMs = retryAfterMs(submitted.response.headers, 500);
|
|
281
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
282
|
-
const separator = resultUrl.includes("?") ? "&" : "?";
|
|
283
|
-
const result = await fetchJson(`${resultUrl}${separator}consume=1`, {
|
|
284
|
-
method: "GET",
|
|
285
|
-
headers: authHeaders,
|
|
286
|
-
});
|
|
287
|
-
if (result.response.status === 200) return resultPayloadFromJobResult(result.body);
|
|
288
|
-
if (result.response.status !== 202) {
|
|
289
|
-
throw new Error(
|
|
290
|
-
`MyteCody async result failed (${result.response.status}): ${result.text.slice(0, 500)}`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
const status = result.body || {};
|
|
294
|
-
if (status.status === "failed" || status.status === "cancelled" || status.status === "expired") {
|
|
295
|
-
throw new Error(`MyteCody async job ${jobId} ended with status ${status.status}`);
|
|
296
|
-
}
|
|
297
|
-
resultUrl = absoluteGatewayUrl(gatewayRoot, status.poll?.result_url || resultUrl);
|
|
298
|
-
waitMs = retryAfterMs(result.response.headers, waitMs);
|
|
299
|
-
if (typeof options.onPoll === "function") {
|
|
300
|
-
options.onPoll({
|
|
301
|
-
job_id: jobId,
|
|
302
|
-
status: status.status || "queued",
|
|
303
|
-
status_url: statusUrl,
|
|
304
|
-
result_url: resultUrl,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
await sleep(waitMs);
|
|
308
|
-
}
|
|
309
|
-
throw new Error(`MyteCody async job ${jobId} timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async function handleResponses(req, res, body, options) {
|
|
313
|
-
const responseId = `resp_myte_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
314
|
-
const wantsStream = body && body.stream !== false;
|
|
315
|
-
if (!wantsStream) {
|
|
316
|
-
const payload = adaptResponsePayloadForCodex(await submitAndDrainResponseJob(body, options));
|
|
317
|
-
return sendJson(res, 200, payload);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
res.writeHead(200, {
|
|
321
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
322
|
-
"cache-control": "no-cache, no-transform",
|
|
323
|
-
connection: "keep-alive",
|
|
324
|
-
"x-myte-cody-async-bridge": "1",
|
|
325
|
-
});
|
|
326
|
-
sse(res, {
|
|
327
|
-
type: "response.created",
|
|
328
|
-
response: { id: responseId, object: "response", status: "in_progress" },
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
const keepalive = setInterval(() => {
|
|
332
|
-
res.write(": keepalive\n\n");
|
|
333
|
-
}, 10000);
|
|
334
|
-
let closed = false;
|
|
335
|
-
res.on("close", () => {
|
|
336
|
-
closed = true;
|
|
337
|
-
clearInterval(keepalive);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const payload = adaptResponsePayloadForCodex(await submitAndDrainResponseJob(body, options));
|
|
342
|
-
if (closed) return;
|
|
343
|
-
const output = Array.isArray(payload && payload.output) ? payload.output : [];
|
|
344
|
-
output.forEach((item, index) => emitOutputItem(res, item, index));
|
|
345
|
-
sse(res, {
|
|
346
|
-
type: "response.completed",
|
|
347
|
-
response: {
|
|
348
|
-
...payload,
|
|
349
|
-
id: responseId,
|
|
350
|
-
object: payload && payload.object ? payload.object : "response",
|
|
351
|
-
status: "completed",
|
|
352
|
-
},
|
|
353
|
-
});
|
|
354
|
-
clearInterval(keepalive);
|
|
355
|
-
res.end();
|
|
356
|
-
} catch (error) {
|
|
357
|
-
clearInterval(keepalive);
|
|
358
|
-
if (!closed) {
|
|
359
|
-
sse(res, {
|
|
360
|
-
type: "response.failed",
|
|
361
|
-
response: {
|
|
362
|
-
id: responseId,
|
|
363
|
-
error: {
|
|
364
|
-
code: "myte_cody_async_bridge_failed",
|
|
365
|
-
message: error && error.message ? error.message : String(error),
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
res.end();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function handle(req, res, options) {
|
|
375
|
-
try {
|
|
376
|
-
const url = new URL(req.url, `http://${HOST}`);
|
|
377
|
-
if (req.method === "GET" && (url.pathname === "/health" || url.pathname === "/v1/health")) {
|
|
378
|
-
return sendJson(res, 200, { ok: true, service: "myte-cody-async-responses-bridge" });
|
|
379
|
-
}
|
|
380
|
-
if (req.method === "GET" && url.pathname === "/v1/models") {
|
|
381
|
-
return sendJson(res, 200, {
|
|
382
|
-
object: "list",
|
|
383
|
-
data: [{ id: "myte", object: "model", owned_by: "myte" }],
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (req.method === "POST" && (url.pathname === "/v1/responses" || url.pathname === "/responses")) {
|
|
387
|
-
const body = await readJsonBody(req);
|
|
388
|
-
if (!body || typeof body !== "object") {
|
|
389
|
-
return sendJson(res, 400, { error: { message: "JSON object body required" } });
|
|
390
|
-
}
|
|
391
|
-
return handleResponses(req, res, body, options);
|
|
392
|
-
}
|
|
393
|
-
return sendJson(res, 404, { error: { message: "not found" } });
|
|
394
|
-
} catch (error) {
|
|
395
|
-
return sendJson(res, 500, {
|
|
396
|
-
error: { message: error && error.message ? error.message : String(error) },
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async function startMyteCodyAsyncResponsesBridge(options = {}) {
|
|
402
|
-
const server = http.createServer((req, res) => {
|
|
403
|
-
handle(req, res, options);
|
|
404
|
-
});
|
|
405
|
-
await new Promise((resolve, reject) => {
|
|
406
|
-
server.once("error", reject);
|
|
407
|
-
server.listen(0, HOST, resolve);
|
|
408
|
-
});
|
|
409
|
-
const address = server.address();
|
|
410
|
-
const port = address && typeof address === "object" ? address.port : 0;
|
|
411
|
-
let closed = false;
|
|
412
|
-
return {
|
|
413
|
-
host: HOST,
|
|
414
|
-
port,
|
|
415
|
-
baseUrl: `http://${HOST}:${port}/v1`,
|
|
416
|
-
close: () => {
|
|
417
|
-
if (closed) return Promise.resolve();
|
|
418
|
-
closed = true;
|
|
419
|
-
return new Promise((resolve) => {
|
|
420
|
-
server.close(() => resolve());
|
|
421
|
-
});
|
|
422
|
-
},
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
module.exports = {
|
|
427
|
-
adaptResponsePayloadForCodex,
|
|
428
|
-
extractPatchArgument,
|
|
429
|
-
startMyteCodyAsyncResponsesBridge,
|
|
430
|
-
submitAndDrainResponseJob,
|
|
431
|
-
};
|