@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 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(installRoot(), "current", "manifest.json");
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(installRoot(), "current", "bin", executable);
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("reading local MyteCody engine artifact");
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 MyteCody engine (${formatBytes(expectedSize)})`);
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 MyteCody engine (${formatBytes(bytes.length)})`);
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 MyteCody engine ${pct}% (${formatBytes(received)} / ${formatBytes(total)})`);
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 MyteCody engine (${formatBytes(bytes.length)})`);
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
- bridge = await startMyteCodyAsyncResponsesBridge({
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 = payload.auth.present && payload.release.client_installed;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",
@@ -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
- };