@mytegroupinc/myte-core 0.0.38 → 0.0.40

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/README.md CHANGED
@@ -28,16 +28,17 @@ This package exists so the public wrapper can stay small and versioned cleanly.
28
28
  - Public package documentation is intentionally minimal. Internal rollout and design notes are not part of the npm package contract.
29
29
  - `mytecody` is a model-agnostic MyteCody team launcher. It supports `doctor`,
30
30
  `doctor --probe-gateway`, `update --dry-run`, and `update`. On first coding
31
- run, it installs the branded MyteCody engine from the Myte release manifest
32
- into a user-local Myte cache after signature/hash checks. Coding execution
33
- requires that engine, a reachable Myte AI Cody gateway, and `MYTEAI_API_KEY`.
31
+ run, it installs the branded MyteCody engine and signed client assets from
32
+ the Myte release manifest into a user-local Myte cache after signature/hash
33
+ checks. Coding execution requires those assets, a reachable Myte AI Cody
34
+ gateway, and `MYTEAI_API_KEY`.
34
35
  - `mytecody doctor` reports both the signed MyteCody engine state and this npm
35
36
  package version. `mytecody update` updates the engine only. Use
36
37
  `npm install -g myte@latest` to update the launcher and the Myte API CLI
37
38
  commands shipped by npm.
38
- - The package does not bundle the large MyteCody engine binary. See
39
- `THIRD_PARTY_NOTICES.md` and `TRADEMARKS.md` for Codex lineage and Myte brand
40
- notices.
39
+ - The package does not bundle the large MyteCody engine binary or the async
40
+ Responses bridge implementation. See `THIRD_PARTY_NOTICES.md` and
41
+ `TRADEMARKS.md` for Codex lineage and Myte brand notices.
41
42
 
42
43
  ## Agent Usage Contract
43
44
 
@@ -13,9 +13,9 @@ Source lineage tracked for this slice:
13
13
  - License: Apache-2.0
14
14
  - Upstream: https://github.com/openai/codex
15
15
 
16
- The MyteCody engine artifact is distributed separately from this npm package
17
- through a Myte release manifest. This package verifies the release artifact
18
- before installing it into the user's Myte-owned local cache.
16
+ The MyteCody engine and signed client assets are distributed separately from
17
+ this npm package through a Myte release manifest. This package verifies release
18
+ artifacts before installing them into the user's Myte-owned local cache.
19
19
 
20
20
  Myte names, marks, logos, terminal branding, and product design are not part of
21
21
  the OpenAI Codex project and remain Myte-owned brand assets.
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,100 @@ function installedClientCommand() {
187
192
  return { cmd: enginePath, args: [], source: "myte-installed-engine" };
188
193
  }
189
194
 
195
+ function installedClientUsable() {
196
+ return Boolean(installedClientCommand() && fs.existsSync(currentBridgePath()) && readCurrentClientManifest());
197
+ }
198
+
199
+ function loadSignedBridge() {
200
+ const bridgePath = currentBridgePath();
201
+ if (!fs.existsSync(bridgePath)) {
202
+ throw new Error("signed MyteCody inference bridge asset is missing; run `mytecody update`.");
203
+ }
204
+ const bridge = require(bridgePath);
205
+ if (!bridge || typeof bridge.startMyteCodyAsyncResponsesBridge !== "function") {
206
+ throw new Error("signed MyteCody inference bridge asset is invalid.");
207
+ }
208
+ return bridge;
209
+ }
210
+
211
+ function releaseAssetsForPlatform(manifest, artifact) {
212
+ const assets = [];
213
+ const collect = (value, source) => {
214
+ if (!value) return;
215
+ if (Array.isArray(value)) {
216
+ value.forEach((item, index) => {
217
+ if (item && typeof item === "object") {
218
+ assets.push({ ...item, source, index });
219
+ }
220
+ });
221
+ return;
222
+ }
223
+ if (typeof value === "object") {
224
+ Object.entries(value).forEach(([name, item], index) => {
225
+ if (item && typeof item === "object") {
226
+ assets.push({ name: item.name || name, ...item, source, index });
227
+ }
228
+ });
229
+ }
230
+ };
231
+
232
+ collect(manifest && manifest.client_assets, "manifest.client_assets");
233
+ collect(artifact && artifact.assets, "artifact.assets");
234
+
235
+ return assets
236
+ .map((asset) => ({
237
+ ...asset,
238
+ install_path: asset.install_path || asset.path || "",
239
+ }))
240
+ .filter((asset) => asset && asset.url && asset.sha256 && asset.install_path);
241
+ }
242
+
243
+ function assertSafeReleaseAssetInstallPath(installPath) {
244
+ const raw = String(installPath || "").replace(/\\/g, "/").trim();
245
+ if (!raw) throw new Error("release asset install_path is required");
246
+ if (path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw)) {
247
+ throw new Error(`release asset install_path must be relative: ${installPath}`);
248
+ }
249
+ const parts = raw.split("/").filter(Boolean);
250
+ if (!parts.length || parts.includes("..")) {
251
+ throw new Error(`release asset install_path is unsafe: ${installPath}`);
252
+ }
253
+ return path.join(currentInstallRoot(), ...parts);
254
+ }
255
+
256
+ function currentReleaseAssetRecordByPath(current) {
257
+ const map = new Map();
258
+ for (const asset of Array.isArray(current && current.assets) ? current.assets : []) {
259
+ if (asset && asset.install_path) {
260
+ map.set(String(asset.install_path).replace(/\\/g, "/"), asset);
261
+ }
262
+ }
263
+ return map;
264
+ }
265
+
266
+ function installedReleaseAssetsMatchManifest(manifest, artifact) {
267
+ const assets = releaseAssetsForPlatform(manifest, artifact);
268
+ if (!assets.length) return true;
269
+ const current = readCurrentClientManifest();
270
+ const currentAssets = currentReleaseAssetRecordByPath(current);
271
+
272
+ for (const asset of assets) {
273
+ const installPath = String(asset.install_path || "").replace(/\\/g, "/");
274
+ const currentAsset = currentAssets.get(installPath);
275
+ if (!currentAsset) return false;
276
+ if (String(currentAsset.sha256 || "").toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
277
+ return false;
278
+ }
279
+ const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
280
+ if (!fs.existsSync(targetPath)) return false;
281
+ const installedSha = sha256Hex(fs.readFileSync(targetPath));
282
+ if (installedSha.toLowerCase() !== String(asset.installed_sha256 || asset.sha256).toLowerCase()) {
283
+ return false;
284
+ }
285
+ }
286
+ return true;
287
+ }
288
+
190
289
  function installedClientMatchesManifest(manifest, artifact) {
191
290
  const current = readCurrentClientManifest();
192
291
  if (!current || !installedClientCommand()) return false;
@@ -207,6 +306,8 @@ function installedClientMatchesManifest(manifest, artifact) {
207
306
  }
208
307
  }
209
308
 
309
+ if (!installedReleaseAssetsMatchManifest(manifest, artifact)) return false;
310
+
210
311
  return true;
211
312
  }
212
313
 
@@ -271,8 +372,10 @@ function commonStatus(args, envPath) {
271
372
  platform: platformKey(),
272
373
  install_root: installRoot(),
273
374
  engine_path: currentEnginePath(),
375
+ bridge_path: currentBridgePath(),
274
376
  client_manifest: currentClientManifestPath(),
275
377
  client_installed: Boolean(current && installedClientCommand()),
378
+ bridge_installed: fs.existsSync(currentBridgePath()),
276
379
  client_version: current && current.version ? current.version : null,
277
380
  },
278
381
  };
@@ -517,11 +620,11 @@ function localPathFromArtifactUrl(urlValue) {
517
620
  return null;
518
621
  }
519
622
 
520
- async function readArtifactBytes(artifact, { progress } = {}) {
623
+ async function readArtifactBytes(artifact, { progress, label = "MyteCody engine" } = {}) {
521
624
  const urlValue = artifact && artifact.url ? String(artifact.url) : "";
522
625
  const localPath = localPathFromArtifactUrl(urlValue);
523
626
  if (localPath) {
524
- if (progress) progress("reading local MyteCody engine artifact");
627
+ if (progress) progress(`reading local ${label} artifact`);
525
628
  return fs.readFileSync(localPath);
526
629
  }
527
630
  const headers = {};
@@ -529,7 +632,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
529
632
  if (token) headers.Authorization = `Bearer ${token}`;
530
633
  if (progress) {
531
634
  const expectedSize = Number(artifact && artifact.size_bytes ? artifact.size_bytes : 0);
532
- progress(`downloading MyteCody engine (${formatBytes(expectedSize)})`);
635
+ progress(`downloading ${label} (${formatBytes(expectedSize)})`);
533
636
  }
534
637
  const response = await fetch(urlValue, { method: "GET", headers });
535
638
  if (!response.ok) {
@@ -538,7 +641,7 @@ async function readArtifactBytes(artifact, { progress } = {}) {
538
641
  }
539
642
  if (!response.body || typeof response.body.getReader !== "function") {
540
643
  const bytes = Buffer.from(await response.arrayBuffer());
541
- if (progress) progress(`downloaded MyteCody engine (${formatBytes(bytes.length)})`);
644
+ if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
542
645
  return bytes;
543
646
  }
544
647
 
@@ -556,13 +659,13 @@ async function readArtifactBytes(artifact, { progress } = {}) {
556
659
  if (progress && total > 0) {
557
660
  const pct = Math.min(100, Math.floor((received / total) * 100));
558
661
  if (pct >= lastPct + 10 || pct === 100) {
559
- progress(`downloading MyteCody engine ${pct}% (${formatBytes(received)} / ${formatBytes(total)})`);
662
+ progress(`downloading ${label} ${pct}% (${formatBytes(received)} / ${formatBytes(total)})`);
560
663
  lastPct = pct;
561
664
  }
562
665
  }
563
666
  }
564
667
  const bytes = Buffer.concat(chunks);
565
- if (progress) progress(`downloaded MyteCody engine (${formatBytes(bytes.length)})`);
668
+ if (progress) progress(`downloaded ${label} (${formatBytes(bytes.length)})`);
566
669
  return bytes;
567
670
  }
568
671
 
@@ -621,6 +724,34 @@ function installArtifactBytes(bytes, manifest, artifact) {
621
724
  return installedManifest;
622
725
  }
623
726
 
727
+ async function installReleaseAssets(manifest, artifact, { progress } = {}) {
728
+ const assets = releaseAssetsForPlatform(manifest, artifact);
729
+ const installed = [];
730
+ for (const asset of assets) {
731
+ const name = String(asset.name || path.basename(String(asset.install_path || "")) || "client asset");
732
+ const bytes = await readArtifactBytes(asset, { progress, label: `MyteCody ${name}` });
733
+ const digest = sha256Hex(bytes);
734
+ if (digest.toLowerCase() !== String(asset.sha256 || "").toLowerCase()) {
735
+ throw new Error(`Release asset SHA-256 mismatch for ${name}: expected ${asset.sha256}, got ${digest}`);
736
+ }
737
+ const installBytes = artifactBytesForInstall(bytes, asset);
738
+ const targetPath = assertSafeReleaseAssetInstallPath(asset.install_path);
739
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
740
+ fs.writeFileSync(targetPath, installBytes);
741
+ installed.push({
742
+ name,
743
+ install_path: String(asset.install_path || "").replace(/\\/g, "/"),
744
+ url: asset.url,
745
+ sha256: asset.sha256,
746
+ format: artifactFormat(asset),
747
+ size_bytes: bytes.length,
748
+ installed_sha256: sha256Hex(installBytes),
749
+ installed_size_bytes: installBytes.length,
750
+ });
751
+ }
752
+ return installed;
753
+ }
754
+
624
755
  async function fetchJson(url, { headers = {}, timeoutMs = 8000 } = {}) {
625
756
  const controller = typeof AbortController !== "undefined" ? new AbortController() : undefined;
626
757
  const timeoutId =
@@ -920,7 +1051,8 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
920
1051
  let bridge = null;
921
1052
  try {
922
1053
  progress("opening Myte inference bridge");
923
- bridge = await startMyteCodyAsyncResponsesBridge({
1054
+ const signedBridge = loadSignedBridge();
1055
+ bridge = await signedBridge.startMyteCodyAsyncResponsesBridge({
924
1056
  gatewayRoot: gatewayRoot(args),
925
1057
  token,
926
1058
  });
@@ -969,7 +1101,8 @@ async function runDoctor(args, envPath) {
969
1101
  if (args["probe-gateway"]) {
970
1102
  payload.gateway.probe = await probeGateway(args);
971
1103
  }
972
- payload.ready_for_coding = payload.auth.present && payload.release.client_installed;
1104
+ payload.ready_for_coding =
1105
+ payload.auth.present && payload.release.client_installed && payload.release.bridge_installed;
973
1106
  if (payload.gateway.probe) {
974
1107
  payload.ready_for_coding = Boolean(payload.ready_for_coding && payload.gateway.probe.ok);
975
1108
  }
@@ -998,6 +1131,7 @@ async function runDoctor(args, envPath) {
998
1131
  console.log(`package update: ${payload.package.check_status}`);
999
1132
  }
1000
1133
  console.log(`client: ${payload.release.client_installed ? payload.release.client_version : "not installed"}`);
1134
+ console.log(`bridge: ${payload.release.bridge_installed ? "installed" : "not installed"}`);
1001
1135
  console.log(`install: ${payload.release.install_root}`);
1002
1136
  console.log("");
1003
1137
  console.log("Coding requires the Myte AI gateway and a Myte AI key.");
@@ -1015,6 +1149,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
1015
1149
  const artifact = manifest ? artifactForPlatform(manifest) : null;
1016
1150
  const signature = manifest ? signatureAccepted(manifest, args) : { ok: false, signature: { status: "not-checked", verified: false } };
1017
1151
  const artifactMetadata = manifest ? validateArtifactMetadata(artifact) : { status: "not-checked", ok: false };
1152
+ const releaseAssets = manifest ? releaseAssetsForPlatform(manifest, artifact) : [];
1018
1153
  const payload = {
1019
1154
  ok: true,
1020
1155
  dry_run: isDryRun,
@@ -1028,6 +1163,13 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
1028
1163
  trusted_unsigned: Boolean(signature.trusted_unsigned),
1029
1164
  },
1030
1165
  artifact: artifactMetadata,
1166
+ release_assets: releaseAssets.map((asset) => ({
1167
+ name: asset.name || null,
1168
+ install_path: asset.install_path,
1169
+ url: asset.url || null,
1170
+ format: asset.format || null,
1171
+ sha256_present: Boolean(asset.sha256),
1172
+ })),
1031
1173
  };
1032
1174
 
1033
1175
  if (!isDryRun) {
@@ -1046,6 +1188,11 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
1046
1188
  throw new Error(`Artifact SHA-256 mismatch: expected ${artifact.sha256}, got ${digest}`);
1047
1189
  }
1048
1190
  const installed = installArtifactBytes(bytes, manifest, artifact);
1191
+ const installedAssets = await installReleaseAssets(manifest, artifact, { progress });
1192
+ if (installedAssets.length) {
1193
+ installed.assets = installedAssets;
1194
+ fs.writeFileSync(currentClientManifestPath(), JSON.stringify(installed, null, 2), "utf8");
1195
+ }
1049
1196
  payload.installed = {
1050
1197
  ok: true,
1051
1198
  version: installed.version,
@@ -1055,6 +1202,7 @@ async function buildUpdatePayload(args, envPath, { dryRun = false, progress = nu
1055
1202
  installed_sha256: installed.artifact.installed_sha256,
1056
1203
  installed_size_bytes: installed.artifact.installed_size_bytes,
1057
1204
  format: installed.artifact.format,
1205
+ assets: installedAssets,
1058
1206
  };
1059
1207
  payload.release = {
1060
1208
  ...payload.release,
@@ -1082,9 +1230,31 @@ async function ensureBrandedEngineInstalled(args = {}, envPath = null, { progres
1082
1230
  delete updateArgs.json;
1083
1231
 
1084
1232
  const source = manifestUrl(updateArgs);
1085
- const manifestResult = await readManifest(source, { fetchManifest: true, progress });
1233
+ let manifestResult;
1234
+ try {
1235
+ manifestResult = await readManifest(source, { fetchManifest: true, progress });
1236
+ } catch (error) {
1237
+ if (isUrl(source) && installedClientUsable()) {
1238
+ return {
1239
+ ok: true,
1240
+ installed: false,
1241
+ reason: "cached-engine-manifest-unavailable",
1242
+ manifest_status: "fetch-failed",
1243
+ error: error && error.message ? error.message : String(error),
1244
+ };
1245
+ }
1246
+ throw error;
1247
+ }
1086
1248
  const manifest = manifestResult.manifest;
1087
1249
  if (!manifest) {
1250
+ if (isUrl(source) && installedClientUsable()) {
1251
+ return {
1252
+ ok: true,
1253
+ installed: false,
1254
+ reason: "cached-engine-manifest-unavailable",
1255
+ manifest_status: manifestResult.status,
1256
+ };
1257
+ }
1088
1258
  return { ok: false, installed: false, reason: "manifest-unavailable", manifest_status: manifestResult.status };
1089
1259
  }
1090
1260
  const signature = signatureAccepted(manifest, updateArgs);
@@ -1185,6 +1355,7 @@ module.exports = {
1185
1355
  codexProviderArgs,
1186
1356
  codyInferenceBase,
1187
1357
  codyGatewayUrl,
1358
+ currentBridgePath,
1188
1359
  currentClientManifestPath,
1189
1360
  currentEnginePath,
1190
1361
  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.40",
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
- };