@mytegroupinc/myte-core 0.0.37 → 0.0.38

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.
@@ -0,0 +1,431 @@
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
+ };
package/mytecody-cli.js CHANGED
@@ -12,6 +12,9 @@ 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");
15
18
 
16
19
  const PACKAGE_NAME = "myte";
17
20
  const PACKAGE_VERSION = require("./package.json").version;
@@ -249,6 +252,7 @@ function commonStatus(args, envPath) {
249
252
  gateway: {
250
253
  base_url: gatewayBase(args),
251
254
  inference_base_url: codyInferenceBase(args),
255
+ responses_transport: "async-job-bridge",
252
256
  network_required_for_coding: true,
253
257
  },
254
258
  instruction_pack: {
@@ -765,7 +769,7 @@ function writeCodexModelCatalog() {
765
769
  return catalogPath;
766
770
  }
767
771
 
768
- function writeCodexConfig(args = {}) {
772
+ function writeCodexConfig(args = {}, providerBaseUrl = codyInferenceBase(args)) {
769
773
  fs.mkdirSync(codexHome(), { recursive: true });
770
774
  const catalogPath = writeCodexModelCatalog();
771
775
  const config = `model = ${tomlString(DEFAULT_MODEL_ALIAS)}
@@ -786,7 +790,7 @@ terminal_title = ["project"]
786
790
 
787
791
  [model_providers.myte_ai]
788
792
  name = "Myte AI"
789
- base_url = ${tomlString(codyInferenceBase(args))}
793
+ base_url = ${tomlString(providerBaseUrl)}
790
794
  env_key = "MYTE_CODY_AUTH_TOKEN"
791
795
  wire_api = "responses"
792
796
  requires_openai_auth = false
@@ -826,14 +830,14 @@ function resolveCodexCommand() {
826
830
  return null;
827
831
  }
828
832
 
829
- function codexProviderArgs(args = {}) {
833
+ function codexProviderArgs(args = {}, providerBaseUrl = codyInferenceBase(args)) {
830
834
  return [
831
835
  "-c",
832
836
  'model_provider="myte_ai"',
833
837
  "-c",
834
838
  'model_providers.myte_ai.name="Myte AI"',
835
839
  "-c",
836
- `model_providers.myte_ai.base_url="${codyInferenceBase(args)}"`,
840
+ `model_providers.myte_ai.base_url="${providerBaseUrl}"`,
837
841
  "-c",
838
842
  'model_providers.myte_ai.env_key="MYTE_CODY_AUTH_TOKEN"',
839
843
  "-c",
@@ -863,8 +867,8 @@ function codexProviderArgs(args = {}) {
863
867
  ];
864
868
  }
865
869
 
866
- function codexLaunchArgs(rawArgs, args = {}) {
867
- const providerArgs = codexProviderArgs(args);
870
+ function codexLaunchArgs(rawArgs, args = {}, providerBaseUrl = codyInferenceBase(args)) {
871
+ const providerArgs = codexProviderArgs(args, providerBaseUrl);
868
872
  if (!rawArgs.length) return providerArgs;
869
873
  if (rawArgs[0] === "exec") return [...providerArgs, "exec", "--skip-git-repo-check", ...rawArgs.slice(1)];
870
874
  return [...providerArgs, ...rawArgs];
@@ -880,7 +884,6 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
880
884
  const progress = setupProgress(splash);
881
885
  splash.start("preparing trusted workspace");
882
886
  progress("preparing trusted workspace");
883
- writeCodexConfig(args);
884
887
  try {
885
888
  const install = await ensureBrandedEngineInstalled(args, envPath, { progress });
886
889
  if (install.ok && install.installed) {
@@ -914,12 +917,27 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
914
917
  console.error("Run `mytecody update` with access to the Myte release manifest.");
915
918
  return 1;
916
919
  }
917
- const launchArgs = [...command.args, ...codexLaunchArgs(rawArgs, args)];
920
+ let bridge = null;
921
+ try {
922
+ progress("opening Myte inference bridge");
923
+ bridge = await startMyteCodyAsyncResponsesBridge({
924
+ gatewayRoot: gatewayRoot(args),
925
+ token,
926
+ });
927
+ writeCodexConfig(args, bridge.baseUrl);
928
+ } catch (error) {
929
+ await splash.stop();
930
+ console.error(`MyteCody inference bridge failed to start: ${error && error.message ? error.message : error}`);
931
+ return 1;
932
+ }
933
+
934
+ const launchArgs = [...command.args, ...codexLaunchArgs(rawArgs, args, bridge.baseUrl)];
918
935
  const env = {
919
936
  ...process.env,
920
937
  CODEX_HOME: codexHome(),
921
938
  MYTE_CODY_AUTH_TOKEN: token,
922
939
  MYTE_CODY_BRAND: "1",
940
+ MYTE_CODY_BRIDGE_BASE_URL: bridge.baseUrl,
923
941
  };
924
942
  progress("opening MyteCody workspace");
925
943
  await splash.stop();
@@ -933,9 +951,11 @@ async function runCodex(rawArgs, args = {}, envPath = null) {
933
951
  });
934
952
  child.on("error", (error) => {
935
953
  console.error(`Unable to launch MyteCody engine: ${error.message || error}`);
936
- resolve(1);
954
+ bridge.close().finally(() => resolve(1));
955
+ });
956
+ child.on("close", (code) => {
957
+ bridge.close().finally(() => resolve(Number.isInteger(code) ? code : 1));
937
958
  });
938
- child.on("close", (code) => resolve(Number.isInteger(code) ? code : 1));
939
959
  });
940
960
  }
941
961
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mytegroupinc/myte-core",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Myte CLI core implementation.",
5
5
  "type": "commonjs",
6
6
  "main": "cli.js",