mycomesh-consumer 0.1.33 → 0.1.35
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 +10 -0
- package/package.json +1 -1
- package/src/consumer-runtime.mjs +202 -6
- package/src/consumer.mjs +2 -1
package/README.md
CHANGED
|
@@ -65,6 +65,16 @@ mycomesh responses --input 'hello' --model mycomesh-codex-standard-v1
|
|
|
65
65
|
|
|
66
66
|
The browser's export block is the canonical way to obtain both values.
|
|
67
67
|
|
|
68
|
+
## Temporary sharing
|
|
69
|
+
|
|
70
|
+
The local page can create one time-limited HTTPS API URL and an independent
|
|
71
|
+
`myco_share_...` key for testing an OpenAI-compatible client outside the
|
|
72
|
+
Consumer machine. Install `cloudflared`, choose a lifetime, and click **Start
|
|
73
|
+
sharing**. The public endpoint exposes only models, Responses, Chat
|
|
74
|
+
Completions, and Codex search routes; it does not expose the dashboard or the
|
|
75
|
+
payment key. Stopping the share, reaching its expiry, or restarting the
|
|
76
|
+
Consumer invalidates the temporary key.
|
|
77
|
+
|
|
68
78
|
## Relay scheduling and V8 payments
|
|
69
79
|
|
|
70
80
|
The Consumer checks each configured Relay's V8 health and automatically tries
|
package/package.json
CHANGED
package/src/consumer-runtime.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join, dirname } from "node:path";
|
|
@@ -81,6 +82,9 @@ const RESPONSES_REQUEST_OPTION_FIELDS = new Set([
|
|
|
81
82
|
"user",
|
|
82
83
|
]);
|
|
83
84
|
const RESPONSES_LOCAL_OPTION_FIELDS = new Set(["stream", "stream_options"]);
|
|
85
|
+
const MAX_SHARE_MINUTES = 24 * 60;
|
|
86
|
+
const TUNNEL_START_TIMEOUT_MS = 30_000;
|
|
87
|
+
const TUNNEL_READY_TIMEOUT_MS = 90_000;
|
|
84
88
|
|
|
85
89
|
const DEFAULT_NETWORK = Object.freeze({
|
|
86
90
|
chain_id: DEFAULT_CHAIN_ID,
|
|
@@ -587,6 +591,10 @@ export class NativeConsumerState {
|
|
|
587
591
|
this.paymentKeyFromEnv = Boolean(String(env.MYCOMESH_V8_PAYMENT_KEY || "").trim());
|
|
588
592
|
this.paymentKey = this.loadPaymentKey(env.MYCOMESH_V8_PAYMENT_KEY);
|
|
589
593
|
this.paymentAddress = paymentKeyAddress(this.paymentKey);
|
|
594
|
+
this.tunnelCommand = options.tunnelCommand || env.MYCOMESH_CONSUMER_TUNNEL_COMMAND || "cloudflared";
|
|
595
|
+
this.tunnelSpawn = options.tunnelSpawn || defaultSpawn;
|
|
596
|
+
this.tunnelProbe = options.tunnelProbe || waitForTunnelReady;
|
|
597
|
+
this.share = null;
|
|
590
598
|
}
|
|
591
599
|
|
|
592
600
|
loadPaymentKey(configured) {
|
|
@@ -717,6 +725,7 @@ export class NativeConsumerState {
|
|
|
717
725
|
input_tokens: allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0),
|
|
718
726
|
output_tokens: allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0),
|
|
719
727
|
},
|
|
728
|
+
share: this.sharePayload(),
|
|
720
729
|
};
|
|
721
730
|
try {
|
|
722
731
|
const grant = await this.keyGrant(this.paymentAddress);
|
|
@@ -740,6 +749,80 @@ export class NativeConsumerState {
|
|
|
740
749
|
return payload;
|
|
741
750
|
}
|
|
742
751
|
|
|
752
|
+
sharePayload() {
|
|
753
|
+
const share = this.activeShare();
|
|
754
|
+
return share ? {
|
|
755
|
+
active: true,
|
|
756
|
+
base_url: share.baseUrl,
|
|
757
|
+
api_key: share.apiKey,
|
|
758
|
+
expires_at: share.expiresAt,
|
|
759
|
+
} : { active: false };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
activeShare() {
|
|
763
|
+
if (this.share && this.share.expiresAt <= Math.floor(Date.now() / 1000)) {
|
|
764
|
+
void this.stopShare();
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
return this.share;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
authorizeBearer(authorization, { shareOnly = false } = {}) {
|
|
771
|
+
if (!shareOnly && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
|
|
772
|
+
const share = this.activeShare();
|
|
773
|
+
return Boolean(share && sameSecret(authorization, `Bearer ${share.apiKey}`));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async startShare(minutesValue) {
|
|
777
|
+
const minutes = Number(minutesValue);
|
|
778
|
+
if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_SHARE_MINUTES) {
|
|
779
|
+
throw new Error(`share duration must be between 1 and ${MAX_SHARE_MINUTES} minutes`);
|
|
780
|
+
}
|
|
781
|
+
await this.stopShare();
|
|
782
|
+
const runtime = createConsumerServer(this, { host: "127.0.0.1", port: 0, publicOnly: true });
|
|
783
|
+
const address = await runtime.listen();
|
|
784
|
+
const apiKey = `myco_share_${randomBytes(24).toString("base64url")}`;
|
|
785
|
+
const share = {
|
|
786
|
+
apiKey,
|
|
787
|
+
baseUrl: "",
|
|
788
|
+
expiresAt: Math.floor(Date.now() / 1000) + minutes * 60,
|
|
789
|
+
process: null,
|
|
790
|
+
runtime,
|
|
791
|
+
timer: null,
|
|
792
|
+
};
|
|
793
|
+
this.share = share;
|
|
794
|
+
try {
|
|
795
|
+
const child = this.tunnelSpawn(
|
|
796
|
+
this.tunnelCommand,
|
|
797
|
+
["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${address.port}`],
|
|
798
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
799
|
+
);
|
|
800
|
+
share.process = child;
|
|
801
|
+
const publicUrl = await waitForTunnelUrl(child);
|
|
802
|
+
if (this.share !== share) throw new Error("temporary share was cancelled");
|
|
803
|
+
await this.tunnelProbe(publicUrl, apiKey, this.dispatcher);
|
|
804
|
+
if (this.share !== share) throw new Error("temporary share was cancelled");
|
|
805
|
+
share.baseUrl = `${publicUrl.replace(/\/+$/, "")}/v1`;
|
|
806
|
+
share.timer = setTimeout(() => { void this.stopShare(); }, minutes * 60 * 1000);
|
|
807
|
+
share.timer.unref?.();
|
|
808
|
+
child.once("exit", () => { if (this.share === share) void this.stopShare(); });
|
|
809
|
+
return this.sharePayload();
|
|
810
|
+
} catch (error) {
|
|
811
|
+
await this.stopShare();
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async stopShare() {
|
|
817
|
+
const share = this.share;
|
|
818
|
+
this.share = null;
|
|
819
|
+
if (!share) return { active: false };
|
|
820
|
+
if (share.timer) clearTimeout(share.timer);
|
|
821
|
+
if (share.process && share.process.exitCode === null) share.process.kill("SIGTERM");
|
|
822
|
+
await share.runtime?.close();
|
|
823
|
+
return { active: false };
|
|
824
|
+
}
|
|
825
|
+
|
|
743
826
|
pendingPaymentKey() {
|
|
744
827
|
if (!existsSync(this.pendingKeyPath)) return null;
|
|
745
828
|
const value = readFileSync(this.pendingKeyPath, "utf8").trim();
|
|
@@ -949,10 +1032,59 @@ function writeJson(response, status, value, headers = {}) {
|
|
|
949
1032
|
response.end(body);
|
|
950
1033
|
}
|
|
951
1034
|
|
|
952
|
-
|
|
1035
|
+
function waitForTunnelUrl(child) {
|
|
1036
|
+
return new Promise((resolve, reject) => {
|
|
1037
|
+
let output = "";
|
|
1038
|
+
const finish = (error, url) => {
|
|
1039
|
+
clearTimeout(timer);
|
|
1040
|
+
child.removeListener("error", onError);
|
|
1041
|
+
child.removeListener("exit", onExit);
|
|
1042
|
+
child.stdout?.removeListener("data", onData);
|
|
1043
|
+
child.stderr?.removeListener("data", onData);
|
|
1044
|
+
if (error) reject(error); else resolve(url);
|
|
1045
|
+
};
|
|
1046
|
+
const onData = (chunk) => {
|
|
1047
|
+
output = `${output}${String(chunk)}`.slice(-16_384);
|
|
1048
|
+
const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
1049
|
+
if (match) finish(null, match[0]);
|
|
1050
|
+
};
|
|
1051
|
+
const onError = (error) => finish(new Error(`could not start cloudflared: ${error.message}`));
|
|
1052
|
+
const onExit = (code) => finish(new Error(`cloudflared exited before publishing a URL (${code ?? "signal"})`));
|
|
1053
|
+
const timer = setTimeout(
|
|
1054
|
+
() => finish(new Error("timed out waiting for the temporary HTTPS URL")),
|
|
1055
|
+
TUNNEL_START_TIMEOUT_MS,
|
|
1056
|
+
);
|
|
1057
|
+
child.once("error", onError);
|
|
1058
|
+
child.once("exit", onExit);
|
|
1059
|
+
child.stdout?.on("data", onData);
|
|
1060
|
+
child.stderr?.on("data", onData);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function waitForTunnelReady(publicUrl, apiKey, dispatcher) {
|
|
1065
|
+
const deadline = Date.now() + TUNNEL_READY_TIMEOUT_MS;
|
|
1066
|
+
let lastError;
|
|
1067
|
+
while (Date.now() < deadline) {
|
|
1068
|
+
try {
|
|
1069
|
+
const response = await fetchWithTimeout(`${publicUrl.replace(/\/+$/, "")}/v1/health`, {
|
|
1070
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
1071
|
+
dispatcher,
|
|
1072
|
+
}, Math.min(3000, deadline - Date.now()));
|
|
1073
|
+
const payload = await readJsonResponse(response);
|
|
1074
|
+
if (response.ok && payload.protocol === "mycomesh-temporary-share/v1") return;
|
|
1075
|
+
lastError = new Error(`HTTP ${response.status}`);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
lastError = error;
|
|
1078
|
+
}
|
|
1079
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error(`temporary HTTPS URL was not reachable: ${lastError?.message || "timed out"}`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async function handleInference(state, request, response, path, alphaSearchQuery, shareOnly = false) {
|
|
953
1085
|
const authorization = String(request.headers.authorization || "");
|
|
954
|
-
if (!
|
|
955
|
-
writeJson(response, 401, openaiError("invalid MycoMesh
|
|
1086
|
+
if (!state.authorizeBearer(authorization, { shareOnly })) {
|
|
1087
|
+
writeJson(response, 401, openaiError("invalid MycoMesh access key", "invalid_api_key"));
|
|
956
1088
|
return;
|
|
957
1089
|
}
|
|
958
1090
|
let body;
|
|
@@ -960,6 +1092,15 @@ async function handleInference(state, request, response, path) {
|
|
|
960
1092
|
writeJson(response, 400, openaiError(error.message, "invalid_request_error"));
|
|
961
1093
|
return;
|
|
962
1094
|
}
|
|
1095
|
+
const alphaSearch = alphaSearchQuery !== undefined;
|
|
1096
|
+
if (alphaSearch) {
|
|
1097
|
+
body = {
|
|
1098
|
+
model: body.model,
|
|
1099
|
+
input: [{ type: "mycomesh_alpha_search_request", request: body, query: alphaSearchQuery }],
|
|
1100
|
+
max_output_tokens: body.max_output_tokens ?? 2000,
|
|
1101
|
+
};
|
|
1102
|
+
path = "/v1/responses";
|
|
1103
|
+
}
|
|
963
1104
|
if (path.endsWith("/responses/compact") && !hasCompactionTrigger(body.input)) {
|
|
964
1105
|
const items = Array.isArray(body.input) ? [...body.input] : [];
|
|
965
1106
|
if (body.input !== undefined && body.input !== "" && !Array.isArray(body.input)) {
|
|
@@ -975,6 +1116,15 @@ async function handleInference(state, request, response, path) {
|
|
|
975
1116
|
return;
|
|
976
1117
|
}
|
|
977
1118
|
for (const [name, value] of Object.entries(result.headers || {})) response.setHeader(name, value);
|
|
1119
|
+
if (alphaSearch) {
|
|
1120
|
+
const payload = result.payload?.mycomesh_alpha_search_response;
|
|
1121
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1122
|
+
writeJson(response, 502, openaiError("Provider returned an invalid alpha/search response", "mycomesh_provider_error"));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
writeJson(response, 200, payload, result.headers);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
978
1128
|
if (body.stream === true) {
|
|
979
1129
|
response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache", connection: "keep-alive", "x-mycomesh-streaming-mode": "buffered" });
|
|
980
1130
|
const events = path.endsWith("/chat/completions")
|
|
@@ -990,11 +1140,38 @@ async function handleInference(state, request, response, path) {
|
|
|
990
1140
|
}
|
|
991
1141
|
}
|
|
992
1142
|
|
|
993
|
-
export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 } = {}) {
|
|
1143
|
+
export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, publicOnly = false } = {}) {
|
|
994
1144
|
const server = createServer(async (request, response) => {
|
|
995
1145
|
try {
|
|
996
1146
|
const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
|
|
997
1147
|
const path = url.pathname;
|
|
1148
|
+
if (publicOnly) {
|
|
1149
|
+
response.setHeader("Access-Control-Allow-Origin", "*");
|
|
1150
|
+
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
|
1151
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1152
|
+
response.setHeader("Access-Control-Expose-Headers", "PAYMENT-RESPONSE, Retry-After");
|
|
1153
|
+
if (request.method === "OPTIONS") { response.writeHead(204); response.end(); return; }
|
|
1154
|
+
if (request.method === "GET" && (path === "/health" || path === "/v1/health")) {
|
|
1155
|
+
if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
|
|
1156
|
+
writeJson(response, 200, { ok: true, protocol: "mycomesh-temporary-share/v1", expires_at: state.activeShare()?.expiresAt });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
|
|
1160
|
+
if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
|
|
1161
|
+
try { const selected = await state.chooseRelay(); writeJson(response, 200, { object: "list", data: [{ id: selected.health.v8.model || DEFAULT_MODEL, object: "model", owned_by: "mycomesh", relay: selected.relayUrl }] }); }
|
|
1162
|
+
catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
|
|
1166
|
+
const relayPath = path.endsWith("/chat/completions") ? "/v1/chat/completions" : path.endsWith("/responses/compact") ? "/v1/responses/compact" : "/v1/responses";
|
|
1167
|
+
await handleInference(state, request, response, relayPath, undefined, true); return;
|
|
1168
|
+
}
|
|
1169
|
+
if (request.method === "POST" && ["/alpha/search", "/v1/alpha/search", "/backend-api/codex/alpha/search"].includes(path)) {
|
|
1170
|
+
await handleInference(state, request, response, "/v1/responses", Object.fromEntries(url.searchParams), true); return;
|
|
1171
|
+
}
|
|
1172
|
+
writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
998
1175
|
if (request.method === "GET" && path === "/") {
|
|
999
1176
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1000
1177
|
response.end(consumerHtml());
|
|
@@ -1031,6 +1208,17 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 }
|
|
|
1031
1208
|
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1032
1209
|
return;
|
|
1033
1210
|
}
|
|
1211
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/share/start") {
|
|
1212
|
+
if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
|
|
1213
|
+
try { const value = await decodeRequestBody(request); writeJson(response, 200, { ok: true, share: await state.startShare(value.minutes) }); }
|
|
1214
|
+
catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (request.method === "POST" && path === "/v1/mycomesh/local/share/stop") {
|
|
1218
|
+
if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
|
|
1219
|
+
writeJson(response, 200, { ok: true, share: await state.stopShare() });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1034
1222
|
if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
|
|
1035
1223
|
const relayPath = path.endsWith("/chat/completions")
|
|
1036
1224
|
? "/v1/chat/completions"
|
|
@@ -1039,6 +1227,9 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 }
|
|
|
1039
1227
|
: "/v1/responses";
|
|
1040
1228
|
await handleInference(state, request, response, relayPath); return;
|
|
1041
1229
|
}
|
|
1230
|
+
if (request.method === "POST" && ["/alpha/search", "/v1/alpha/search", "/backend-api/codex/alpha/search"].includes(path)) {
|
|
1231
|
+
await handleInference(state, request, response, "/v1/responses", Object.fromEntries(url.searchParams)); return;
|
|
1232
|
+
}
|
|
1042
1233
|
writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
|
|
1043
1234
|
} catch (error) {
|
|
1044
1235
|
if (!response.headersSent) writeJson(response, 500, openaiError(error.message)); else response.destroy();
|
|
@@ -1147,21 +1338,23 @@ function consumerHtml() {
|
|
|
1147
1338
|
body{font:15px system-ui;max-width:960px;margin:32px auto;padding:0 18px;color:#17211d;background:#f5f7f5}
|
|
1148
1339
|
h1{margin-bottom:4px}.muted{color:#68736e}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
|
|
1149
1340
|
.panel{background:white;border:1px solid #d9e0dc;border-radius:6px;padding:18px;margin:14px 0}.value{word-break:break-all;background:#f1f4f2;padding:10px;border-radius:4px;font:12px monospace;white-space:pre-wrap}
|
|
1150
|
-
button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input{padding:9px;border:1px solid #c7d0ca;border-radius:4px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
1341
|
+
button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input,select{padding:9px;border:1px solid #c7d0ca;border-radius:4px;background:#fff}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
1151
1342
|
table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #e1e7e3;text-align:left;font-size:13px}
|
|
1152
1343
|
</style></head><body>
|
|
1153
1344
|
<h1>MycoMesh Consumer V8</h1><p class="muted">Native Node runtime. This page manages credentials, prepaid balance, payment keys, and local consumption history.</p>
|
|
1154
1345
|
<div class="grid"><section class="panel"><h2>Credentials</h2><p>API URL</p><div id="url" class="value">Loading...</div><p>Payment key</p><div id="key" class="value"></div><p>Shell export</p><div id="export" class="value"></div><button id="copy" type="button">Copy export</button></section>
|
|
1155
1346
|
<section class="panel"><h2>Usage</h2><p id="usage">Loading...</p><p id="balance"></p><p>Key address</p><div id="address" class="value"></div><div class="row"><button id="register" type="button">Register key</button><button id="rotate" type="button">Rotate key</button></div></section></div>
|
|
1156
1347
|
<section class="panel"><h2>Prepaid top-up</h2><div class="row"><button id="connect" type="button">Connect wallet</button><input id="wallet" placeholder="Wallet address"><input id="amount" placeholder="10.00"><button id="topup" type="button">Top up</button></div><pre id="plan" class="value"></pre></section>
|
|
1348
|
+
<section class="panel"><h2>Temporary share</h2><div class="row"><select id="shareMinutes"><option value="10">10 minutes</option><option value="30" selected>30 minutes</option><option value="60">1 hour</option><option value="360">6 hours</option></select><button id="shareStart" type="button">Start sharing</button><button id="shareStop" type="button">Stop</button></div><p>API URL</p><div id="shareUrl" class="value">Inactive</div><p>Temporary key</p><div id="shareKey" class="value"></div><p id="shareExpiry" class="muted"></p><button id="shareCopy" type="button">Copy export</button></section>
|
|
1157
1349
|
<section class="panel"><h2>Consumption history</h2><table><thead><tr><th>Time</th><th>Model</th><th>Tokens</th><th>Fee</th><th>Relay</th></tr></thead><tbody id="history"></tbody></table></section>
|
|
1158
1350
|
<script>
|
|
1159
1351
|
let state;let wallet;const $=id=>document.getElementById(id);
|
|
1160
|
-
async function api(path,options={}){const r=await fetch(path,options);const v=await r.json();if(!r.ok)throw Error(v.error||'request failed');return v}
|
|
1352
|
+
async function api(path,options={}){const r=await fetch(path,options);const v=await r.json();if(!r.ok)throw Error(typeof v.error==='string'?v.error:(v.error?.message||'request failed'));return v}
|
|
1161
1353
|
function render(){
|
|
1162
1354
|
$('url').textContent=state.credentials.base_url;$('key').textContent=state.credentials.api_key;$('export').textContent=state.credentials.export;$('address').textContent=state.key.address;
|
|
1163
1355
|
$('usage').textContent=state.usage.request_count+' requests, '+state.usage.total_spent_units+' units';$('balance').textContent='Available balance: '+(state.account?.available_balance_units||'unknown');
|
|
1164
1356
|
$('history').innerHTML=(state.history||[]).map(x=>'<tr><td>'+new Date(x.timestamp*1000).toLocaleString()+'</td><td>'+x.model+'</td><td>'+x.input_tokens+' / '+x.output_tokens+'</td><td>'+x.actual_fee_units+'</td><td>'+x.relay_url+'</td></tr>').join('')||'<tr><td colspan="5">No consumption recorded.</td></tr>';
|
|
1357
|
+
$('shareUrl').textContent=state.share?.base_url||'Inactive';$('shareKey').textContent=state.share?.api_key||'';$('shareExpiry').textContent=state.share?.expires_at?'Expires '+new Date(state.share.expires_at*1000).toLocaleString():'';
|
|
1165
1358
|
}
|
|
1166
1359
|
async function load(){state=await api('/v1/mycomesh/local/dashboard');render()}
|
|
1167
1360
|
async function connectWallet(){if(!window.ethereum)throw Error('No injected wallet found');const accounts=await window.ethereum.request({method:'eth_requestAccounts'});wallet=accounts[0];if(!wallet)throw Error('Wallet was not connected');$('wallet').value=wallet;$('connect').textContent=wallet.slice(0,6)+'...'+wallet.slice(-4)}
|
|
@@ -1173,6 +1366,9 @@ $('copy').onclick=()=>navigator.clipboard?.writeText($('export').textContent);
|
|
|
1173
1366
|
$('connect').onclick=()=>connectWallet().catch(error=>$('plan').textContent=error.message);
|
|
1174
1367
|
$('register').onclick=()=>showPlan('register_key',true);$('topup').onclick=()=>showPlan('top_up',true);
|
|
1175
1368
|
$('rotate').onclick=async()=>{try{await requireWallet();const oldAddress=state.key.address;const pending=await api('/v1/mycomesh/local/key/prepare',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key}});const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(plan);let activated;for(let i=0;i<8&&!activated;i++){try{activated=await api('/v1/mycomesh/local/key/activate',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key,'content-type':'application/json'},body:JSON.stringify({wallet})})}catch(error){if(i===7)throw error;await new Promise(resolve=>setTimeout(resolve,1800))}}const revoke=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'revoke_key',wallet,key_address:oldAddress})});await sendPlan(revoke);$('plan').textContent=JSON.stringify({pending,activated,revoke},null,2);await load()}catch(error){$('plan').textContent=error.message}};
|
|
1369
|
+
$('shareStart').onclick=async()=>{const button=$('shareStart');button.disabled=true;button.textContent='Starting...';try{const v=await api('/v1/mycomesh/local/share/start',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key,'content-type':'application/json'},body:JSON.stringify({minutes:Number($('shareMinutes').value)})});state.share=v.share;render()}catch(error){$('shareExpiry').textContent=error.message}finally{button.disabled=false;button.textContent='Start sharing'}};
|
|
1370
|
+
$('shareStop').onclick=async()=>{try{const v=await api('/v1/mycomesh/local/share/stop',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key}});state.share=v.share;render()}catch(error){$('shareExpiry').textContent=error.message}};
|
|
1371
|
+
$('shareCopy').onclick=()=>navigator.clipboard?.writeText("export OPENAI_BASE_URL='"+$('shareUrl').textContent+"'\nexport OPENAI_API_KEY='"+$('shareKey').textContent+"'");
|
|
1176
1372
|
load().catch(error=>$('plan').textContent=error.message);
|
|
1177
1373
|
</script></body></html>`;
|
|
1178
1374
|
}
|
package/src/consumer.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createConsumerServer,
|
|
12
12
|
} from "./consumer-runtime.mjs";
|
|
13
13
|
|
|
14
|
-
export const CONSUMER_RELEASE_VERSION = "0.1.
|
|
14
|
+
export const CONSUMER_RELEASE_VERSION = "0.1.35";
|
|
15
15
|
export const API_COMMANDS = new Set(["health", "models", "responses", "chat"]);
|
|
16
16
|
const API_VALUE_OPTIONS = new Set([
|
|
17
17
|
"--base-url",
|
|
@@ -111,6 +111,7 @@ export async function main(argv, dependencies = {}) {
|
|
|
111
111
|
|
|
112
112
|
const shutdown = async () => {
|
|
113
113
|
clearPid(parsed.dataDir);
|
|
114
|
+
await state.stopShare?.();
|
|
114
115
|
await runtime.close();
|
|
115
116
|
};
|
|
116
117
|
if (parsed.noCodex) {
|