dsh-codex-subscription 3.0.0 → 3.0.1
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.en.md +6 -6
- package/README.md +6 -6
- package/lib/client.js +1507 -47
- package/lib/index.js +1463 -94
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { closeOpenAICodexWebSocketSessions, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats } from "@earendil-works/pi-ai/api/openai-codex-responses";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
|
+
import { appendFileSync, constants, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
7
|
import { request } from "node:https";
|
|
6
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
9
|
import { PassThrough, Readable } from "node:stream";
|
|
@@ -10,8 +12,6 @@ import { HttpsProxyAgent } from "https-proxy-agent";
|
|
|
10
12
|
import WebSocket from "ws";
|
|
11
13
|
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
12
14
|
import { lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
13
|
-
import { constants, readFileSync, readdirSync, statSync } from "node:fs";
|
|
14
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
15
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
16
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
17
17
|
import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
@@ -340,6 +340,51 @@ const OUTPUT_VERBOSITY_DEFAULT = "default";
|
|
|
340
340
|
const OUTPUT_VERBOSITY_MEDIUM = "medium";
|
|
341
341
|
const OUTPUT_VERBOSITY_HIGH = "high";
|
|
342
342
|
const DEFAULT_OUTPUT_VERBOSITY = OUTPUT_VERBOSITY_DEFAULT;
|
|
343
|
+
const VOICE_CALL_FIELD = "voiceCall";
|
|
344
|
+
const VOICE_VOICE_FIELD = "voiceVoice";
|
|
345
|
+
const VOICE_VOICES = Object.freeze([
|
|
346
|
+
"cove",
|
|
347
|
+
"juniper",
|
|
348
|
+
"maple",
|
|
349
|
+
"spruce",
|
|
350
|
+
"ember",
|
|
351
|
+
"vale",
|
|
352
|
+
"breeze",
|
|
353
|
+
"arbor",
|
|
354
|
+
"sol",
|
|
355
|
+
"aube",
|
|
356
|
+
"bubbie",
|
|
357
|
+
"haetsal",
|
|
358
|
+
"hanul",
|
|
359
|
+
"himari",
|
|
360
|
+
"leher",
|
|
361
|
+
"miko",
|
|
362
|
+
"neer",
|
|
363
|
+
"porto",
|
|
364
|
+
"rindo",
|
|
365
|
+
"rio",
|
|
366
|
+
"rivage",
|
|
367
|
+
"selva",
|
|
368
|
+
"sonna",
|
|
369
|
+
"tinta",
|
|
370
|
+
"viola"
|
|
371
|
+
]);
|
|
372
|
+
const VOICE_LANGUAGE_FIELD = "voiceLanguage";
|
|
373
|
+
const VOICE_LANGUAGES = Object.freeze([
|
|
374
|
+
"auto",
|
|
375
|
+
"zh",
|
|
376
|
+
"en",
|
|
377
|
+
"ja",
|
|
378
|
+
"ko",
|
|
379
|
+
"es",
|
|
380
|
+
"fr",
|
|
381
|
+
"de",
|
|
382
|
+
"pt",
|
|
383
|
+
"ru"
|
|
384
|
+
]);
|
|
385
|
+
const normalizeVoiceCall = (value) => value === "off" ? "off" : "on";
|
|
386
|
+
const normalizeVoiceVoice = (value) => VOICE_VOICES.includes(value) ? value : "cove";
|
|
387
|
+
const normalizeVoiceLanguage = (value) => VOICE_LANGUAGES.includes(value) ? value : "auto";
|
|
343
388
|
const REASONING_SUMMARY_FIELD = "reasoningSummary";
|
|
344
389
|
const REASONING_SUMMARIES = Object.freeze([
|
|
345
390
|
"auto",
|
|
@@ -572,6 +617,21 @@ const PREFERENCE_FIELDS = Object.freeze({
|
|
|
572
617
|
default: 10,
|
|
573
618
|
error: "Invalid stream idle timeout preference"
|
|
574
619
|
},
|
|
620
|
+
[VOICE_CALL_FIELD]: {
|
|
621
|
+
choices: ["off", "on"],
|
|
622
|
+
default: "on",
|
|
623
|
+
error: "Invalid voice call setting"
|
|
624
|
+
},
|
|
625
|
+
[VOICE_VOICE_FIELD]: {
|
|
626
|
+
choices: VOICE_VOICES,
|
|
627
|
+
default: "cove",
|
|
628
|
+
error: "Invalid voice preference"
|
|
629
|
+
},
|
|
630
|
+
[VOICE_LANGUAGE_FIELD]: {
|
|
631
|
+
choices: VOICE_LANGUAGES,
|
|
632
|
+
default: "auto",
|
|
633
|
+
error: "Invalid voice language preference"
|
|
634
|
+
},
|
|
575
635
|
[CONTEXT_MODE_FIELD]: {
|
|
576
636
|
choices: [
|
|
577
637
|
CONTEXT_MODE_STANDARD,
|
|
@@ -631,6 +691,108 @@ function createDiagnosticEvents({ limit = 32, now = Date.now } = {}) {
|
|
|
631
691
|
};
|
|
632
692
|
}
|
|
633
693
|
//#endregion
|
|
694
|
+
//#region src/diagnostic-log-format.js
|
|
695
|
+
const MAX_MESSAGE = 300;
|
|
696
|
+
function sanitizeLogText(value) {
|
|
697
|
+
let text = typeof value === "string" ? value : String(value?.message ?? value ?? "");
|
|
698
|
+
text = text.replace(/Bearer\s+[\w.~+/=-]+/giu, "Bearer ***").replace(/\beyJ[\w-]{8,}\.[\w-]{8,}(?:\.[\w-]*)?/gu, "***").replace(/\b(token|access_token|refresh_token|id_token|code|key|secret|password|authorization)=[^\s&"']+/giu, "$1=***").replace(/(["']?(?:access|refresh|id)_?token["']?\s*[:=]\s*)["'][^"']+["']/giu, "$1\"***\"").replace(/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/gu, "***@***").replace(/https?:\/\/[^\s"'<>)]+/giu, (url) => url.split(/[?#]/u)[0]).replace(/[A-Za-z]:[\\/]Users[\\/][^\\/\s"']+/gu, "~").replace(/\/(?:home|Users)\/[^/\s"']+/gu, "~").replace(/\b[0-9a-f]{32,}\b/giu, "***").replace(/\b[A-Za-z0-9_-]{40,}\b/gu, "***").replace(/\s+/gu, " ").trim();
|
|
699
|
+
return text.length > MAX_MESSAGE ? `${text.slice(0, MAX_MESSAGE)}…` : text;
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/diagnostic-log.js
|
|
703
|
+
const LEVELS$1 = [
|
|
704
|
+
"info",
|
|
705
|
+
"warn",
|
|
706
|
+
"error"
|
|
707
|
+
];
|
|
708
|
+
const MAX_ENTRIES$1 = 400;
|
|
709
|
+
const FILE_LIMIT = 256 * 1024;
|
|
710
|
+
function createDiagnosticLog({ now = Date.now, limit = MAX_ENTRIES$1 } = {}) {
|
|
711
|
+
let entries = [];
|
|
712
|
+
let dropped = 0;
|
|
713
|
+
let sequence = 0;
|
|
714
|
+
let file;
|
|
715
|
+
let lastWritten = "";
|
|
716
|
+
const persist = (entry) => {
|
|
717
|
+
if (file === void 0) return;
|
|
718
|
+
try {
|
|
719
|
+
try {
|
|
720
|
+
if (statSync(file).size > FILE_LIMIT) renameSync(file, `${file}.1`);
|
|
721
|
+
} catch {}
|
|
722
|
+
appendFileSync(file, `${JSON.stringify(entry)}\n`);
|
|
723
|
+
} catch {}
|
|
724
|
+
};
|
|
725
|
+
const log = (level, area, message, detail) => {
|
|
726
|
+
try {
|
|
727
|
+
if (!LEVELS$1.includes(level)) return;
|
|
728
|
+
const text = sanitizeLogText(message);
|
|
729
|
+
if (text === "" && detail === void 0) return;
|
|
730
|
+
const entry = {
|
|
731
|
+
sequence: ++sequence,
|
|
732
|
+
time: now(),
|
|
733
|
+
level,
|
|
734
|
+
area: String(area).slice(0, 24),
|
|
735
|
+
message: text
|
|
736
|
+
};
|
|
737
|
+
if (detail !== void 0) entry.detail = sanitizeLogText(detail);
|
|
738
|
+
const signature = `${level}|${entry.area}|${text}|${entry.detail ?? ""}`;
|
|
739
|
+
const previous = entries.at(-1);
|
|
740
|
+
if (previous !== void 0 && signature === lastWritten) {
|
|
741
|
+
previous.count = (previous.count ?? 1) + 1;
|
|
742
|
+
previous.lastTime = entry.time;
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
lastWritten = signature;
|
|
746
|
+
entries.push(entry);
|
|
747
|
+
if (entries.length > limit) {
|
|
748
|
+
entries = entries.slice(-limit);
|
|
749
|
+
dropped++;
|
|
750
|
+
}
|
|
751
|
+
persist(entry);
|
|
752
|
+
} catch {}
|
|
753
|
+
};
|
|
754
|
+
return {
|
|
755
|
+
log,
|
|
756
|
+
info: (area, message, detail) => log("info", area, message, detail),
|
|
757
|
+
warn: (area, message, detail) => log("warn", area, message, detail),
|
|
758
|
+
error: (area, message, detail) => log("error", area, message, detail),
|
|
759
|
+
/** Keep a copy on disk, and start from what the last run left behind. */
|
|
760
|
+
attach(path) {
|
|
761
|
+
try {
|
|
762
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
763
|
+
file = path;
|
|
764
|
+
entries = [...readFileSync(path, "utf8").trim().split("\n").slice(-Math.floor(limit / 2)).flatMap((line) => {
|
|
765
|
+
try {
|
|
766
|
+
const value = JSON.parse(line);
|
|
767
|
+
return LEVELS$1.includes(value?.level) && Number.isSafeInteger(value?.time) && typeof value?.message === "string" ? [{
|
|
768
|
+
...value,
|
|
769
|
+
sequence: ++sequence,
|
|
770
|
+
area: String(value.area ?? "").slice(0, 24),
|
|
771
|
+
message: sanitizeLogText(value.message),
|
|
772
|
+
earlier: true
|
|
773
|
+
}] : [];
|
|
774
|
+
} catch {
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
}), ...entries];
|
|
778
|
+
} catch {
|
|
779
|
+
file = file ?? path;
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
snapshot: (max = 200) => ({
|
|
783
|
+
dropped,
|
|
784
|
+
entries: entries.slice(-max).map((value) => ({ ...value }))
|
|
785
|
+
}),
|
|
786
|
+
clear() {
|
|
787
|
+
entries = [];
|
|
788
|
+
dropped = 0;
|
|
789
|
+
lastWritten = "";
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
/** The log every part of the plugin writes to. */
|
|
794
|
+
const diagnosticLog = createDiagnosticLog();
|
|
795
|
+
//#endregion
|
|
634
796
|
//#region src/oauth-network.js
|
|
635
797
|
const execFileAsync = promisify(execFile);
|
|
636
798
|
const CODEX_AUTH_HOST = "auth.openai.com";
|
|
@@ -876,8 +1038,13 @@ function createCodexNetworkTransport(options = {}) {
|
|
|
876
1038
|
const now = options.now ?? Date.now;
|
|
877
1039
|
const evidence = createDiagnosticEvents({ now });
|
|
878
1040
|
const remember = (area, value) => {
|
|
1041
|
+
const before = attempts.get(area);
|
|
879
1042
|
attempts.set(area, value);
|
|
880
1043
|
evidence.record(area, value);
|
|
1044
|
+
if (value.status === "failed") {
|
|
1045
|
+
const what = value.stage === "http" ? `HTTP ${value.httpStatus}` : `network ${value.code}`;
|
|
1046
|
+
diagnosticLog.log(value.stage === "http" && value.httpStatus < 500 && value.httpStatus !== 401 && value.httpStatus !== 403 ? "warn" : "error", area, `request failed: ${what} (${value.route}, ${value.elapsed})`);
|
|
1047
|
+
} else if (before?.status === "failed") diagnosticLog.info(area, "request works again");
|
|
881
1048
|
};
|
|
882
1049
|
const run = async (area, operation, connection = {}) => {
|
|
883
1050
|
const startedAt = now();
|
|
@@ -1215,6 +1382,11 @@ const RPC_ENDPOINTS = Object.freeze([
|
|
|
1215
1382
|
"runtime/cancel",
|
|
1216
1383
|
"storage/status",
|
|
1217
1384
|
"storage/clear-forecast",
|
|
1385
|
+
"voice/start",
|
|
1386
|
+
"voice/stop",
|
|
1387
|
+
"voice/status",
|
|
1388
|
+
"voice/preview",
|
|
1389
|
+
"voice/catalog",
|
|
1218
1390
|
"remote/status",
|
|
1219
1391
|
"remote/enable",
|
|
1220
1392
|
"remote/disable",
|
|
@@ -2182,10 +2354,14 @@ function inheritedOriginalImageRef(session, assetId) {
|
|
|
2182
2354
|
}
|
|
2183
2355
|
//#endregion
|
|
2184
2356
|
//#region src/version.js
|
|
2185
|
-
const PACKAGE_VERSION = "3.0.
|
|
2357
|
+
const PACKAGE_VERSION = "3.0.1";
|
|
2186
2358
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2187
2359
|
//#endregion
|
|
2188
2360
|
//#region src/http-failure.js
|
|
2361
|
+
let logAuthFailure;
|
|
2362
|
+
const setAuthFailureLogger = (fn) => {
|
|
2363
|
+
logAuthFailure = fn;
|
|
2364
|
+
};
|
|
2189
2365
|
const SIGN_IN_RENEW = "ChatGPT sign-in needs to be renewed";
|
|
2190
2366
|
const REQUEST_BLOCKED = "ChatGPT blocked this request before it reached your account (HTTP 403); your sign-in is fine, try again later or check your network, proxy or VPN";
|
|
2191
2367
|
const CHALLENGE_MARKERS = /just a moment|cf-chl|_cf_chl_opt|attention required|enable javascript and cookies|challenge-platform/iu;
|
|
@@ -2207,9 +2383,16 @@ async function isBlockedByNetwork(response) {
|
|
|
2207
2383
|
}
|
|
2208
2384
|
/** The error message for a 401 or 403 answer, or undefined for any other status. */
|
|
2209
2385
|
async function authFailureMessage(response) {
|
|
2210
|
-
if (response?.status
|
|
2211
|
-
|
|
2212
|
-
|
|
2386
|
+
if (response?.status !== 401 && response?.status !== 403) return void 0;
|
|
2387
|
+
const blocked = response.status === 403 && await isBlockedByNetwork(response);
|
|
2388
|
+
await noteAuthFailure(response, blocked);
|
|
2389
|
+
return blocked ? REQUEST_BLOCKED : SIGN_IN_RENEW;
|
|
2390
|
+
}
|
|
2391
|
+
async function noteAuthFailure(response, blocked) {
|
|
2392
|
+
try {
|
|
2393
|
+
const text = blocked || typeof response.clone !== "function" ? "" : (await response.clone().text()).slice(0, 400);
|
|
2394
|
+
logAuthFailure?.(blocked ? "HTTP 403 from the network layer in front of ChatGPT" : `ChatGPT answered HTTP ${response.status}`, text === "" ? void 0 : text);
|
|
2395
|
+
} catch {}
|
|
2213
2396
|
}
|
|
2214
2397
|
//#endregion
|
|
2215
2398
|
//#region src/codex-images.js
|
|
@@ -2237,7 +2420,7 @@ const FULL_ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
|
|
|
2237
2420
|
const BARE_ATTACHMENT_DIGEST = /^[0-9a-f]{64}$/iu;
|
|
2238
2421
|
const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
|
|
2239
2422
|
const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
|
|
2240
|
-
const record$
|
|
2423
|
+
const record$8 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2241
2424
|
const nonEmpty$2 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
2242
2425
|
const NETWORK_REASONS = {
|
|
2243
2426
|
UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
|
|
@@ -2323,7 +2506,7 @@ function decodeCodexPng(value, maximumBytes) {
|
|
|
2323
2506
|
return new Uint8Array(data);
|
|
2324
2507
|
}
|
|
2325
2508
|
function imageReference(value) {
|
|
2326
|
-
const originalDimensions = record$
|
|
2509
|
+
const originalDimensions = record$8(value.originalDimensions) ? {
|
|
2327
2510
|
width: value.originalDimensions.width,
|
|
2328
2511
|
height: value.originalDimensions.height
|
|
2329
2512
|
} : void 0;
|
|
@@ -2351,7 +2534,7 @@ function invalidAttachmentId(value) {
|
|
|
2351
2534
|
function referenceOf(value, attachments) {
|
|
2352
2535
|
const attachmentId = normalizeAttachmentId(value?.attachmentId);
|
|
2353
2536
|
if (attachmentId === void 0) invalidAttachmentId(value);
|
|
2354
|
-
if (!record$
|
|
2537
|
+
if (!record$8(value) || !attachments.imageLimits.mediaTypes.includes(value.mediaType) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || !Number.isSafeInteger(value.width) || value.width <= 0 || !Number.isSafeInteger(value.height) || value.height <= 0 || value.name !== void 0 && (typeof value.name !== "string" || value.name.length > 256) || value.originalDimensions !== void 0 && (!record$8(value.originalDimensions) || !Number.isSafeInteger(value.originalDimensions.width) || value.originalDimensions.width <= 0 || !Number.isSafeInteger(value.originalDimensions.height) || value.originalDimensions.height <= 0)) throw new Error("referenceImages contains an invalid image reference");
|
|
2355
2538
|
return imageReference({
|
|
2356
2539
|
...value,
|
|
2357
2540
|
attachmentId
|
|
@@ -2361,7 +2544,7 @@ function sessionImageReferences(messages) {
|
|
|
2361
2544
|
const references = /* @__PURE__ */ new Map();
|
|
2362
2545
|
const visit = (content) => {
|
|
2363
2546
|
if (!Array.isArray(content)) return;
|
|
2364
|
-
for (const block of content) if (block?.type === "image" && record$
|
|
2547
|
+
for (const block of content) if (block?.type === "image" && record$8(block.attachment)) {
|
|
2365
2548
|
const id = normalizeAttachmentId(block.attachment.attachmentId);
|
|
2366
2549
|
if (id !== void 0) references.set(id, block.attachment);
|
|
2367
2550
|
} else if (block?.type === "tool-result") visit(block.content);
|
|
@@ -2526,7 +2709,7 @@ async function limitMessage(response, now = Date.now()) {
|
|
|
2526
2709
|
} catch {
|
|
2527
2710
|
return fallback;
|
|
2528
2711
|
}
|
|
2529
|
-
if (!record$
|
|
2712
|
+
if (!record$8(error)) return fallback;
|
|
2530
2713
|
const at = Number.isSafeInteger(error.resets_at) && error.resets_at > 0 ? error.resets_at * 1e3 : Number.isSafeInteger(error.resets_in_seconds) && error.resets_in_seconds > 0 ? now + error.resets_in_seconds * 1e3 : void 0;
|
|
2531
2714
|
if (at === void 0) return fallback;
|
|
2532
2715
|
const minutes = Math.max(1, Math.ceil((at - now) / 6e4));
|
|
@@ -2548,7 +2731,7 @@ async function readTextWithin(response, maximumBytes) {
|
|
|
2548
2731
|
}
|
|
2549
2732
|
function responseMetadata(value) {
|
|
2550
2733
|
const data = Array.isArray(value?.data) ? value.data[0] : void 0;
|
|
2551
|
-
const encoded = record$
|
|
2734
|
+
const encoded = record$8(data) ? data.b64_json : void 0;
|
|
2552
2735
|
if (typeof encoded !== "string") throw new Error("Codex returned no image data");
|
|
2553
2736
|
return {
|
|
2554
2737
|
encoded,
|
|
@@ -2883,7 +3066,8 @@ const METHOD_NOT_FOUND = -32601;
|
|
|
2883
3066
|
const INVALID_PARAMS = -32602;
|
|
2884
3067
|
const INTERNAL_ERROR = -32603;
|
|
2885
3068
|
const MAX_SEEN = 128;
|
|
2886
|
-
const
|
|
3069
|
+
const STREAMING = /delta$|outputDelta|tokenUsage|summaryPartAdded/u;
|
|
3070
|
+
const QUIET$1 = /* @__PURE__ */ new Set([
|
|
2887
3071
|
"command/exec",
|
|
2888
3072
|
"thread/attachment/list",
|
|
2889
3073
|
"thread/list",
|
|
@@ -2903,16 +3087,21 @@ const invalidParams = (message) => new RpcError(INVALID_PARAMS, message);
|
|
|
2903
3087
|
* One host per relay; each remote client gets its own connection.
|
|
2904
3088
|
* @param {{ methods: Record<string, (params: any, context: { client: object, signal: AbortSignal, notify: (method: string, params?: object) => Promise<void> }) => any>, notifications?: Record<string, Function>, onConnection?: (client: object) => void, onClose?: (client: object) => void }} options
|
|
2905
3089
|
*/
|
|
2906
|
-
function createRemoteControlHost({ methods, notifications = {}, onConnection, onActive, onClose, debug = false }) {
|
|
3090
|
+
function createRemoteControlHost({ methods, notifications = {}, onConnection, onActive, onClose, debug = false, debugFile }) {
|
|
2907
3091
|
const seen = /* @__PURE__ */ new Map();
|
|
2908
3092
|
const trace = [];
|
|
2909
3093
|
const record = (entry) => {
|
|
2910
3094
|
if (!debug) return;
|
|
2911
|
-
|
|
3095
|
+
const value = {
|
|
2912
3096
|
at: Date.now(),
|
|
2913
3097
|
...entry
|
|
2914
|
-
}
|
|
3098
|
+
};
|
|
3099
|
+
trace.push(value);
|
|
2915
3100
|
if (trace.length > 60) trace.shift();
|
|
3101
|
+
if (debugFile !== void 0) try {
|
|
3102
|
+
appendFileSync(debugFile, `${JSON.stringify(value)}
|
|
3103
|
+
`);
|
|
3104
|
+
} catch {}
|
|
2916
3105
|
};
|
|
2917
3106
|
const note = (kind, method) => {
|
|
2918
3107
|
const key = `${kind}:${method}`;
|
|
@@ -2932,6 +3121,14 @@ function createRemoteControlHost({ methods, notifications = {}, onConnection, on
|
|
|
2932
3121
|
let initialized = false;
|
|
2933
3122
|
let active = false;
|
|
2934
3123
|
const notify = async (method, params) => {
|
|
3124
|
+
if (debug && !STREAMING.test(method)) record({
|
|
3125
|
+
method: `notify:${method}`,
|
|
3126
|
+
params: JSON.stringify({
|
|
3127
|
+
threadId: params?.threadId,
|
|
3128
|
+
status: params?.status?.type,
|
|
3129
|
+
message: params?.message
|
|
3130
|
+
})
|
|
3131
|
+
});
|
|
2935
3132
|
await client.send(params === void 0 ? { method } : {
|
|
2936
3133
|
method,
|
|
2937
3134
|
params
|
|
@@ -3029,16 +3226,16 @@ function createRemoteControlHost({ methods, notifications = {}, onConnection, on
|
|
|
3029
3226
|
active = true;
|
|
3030
3227
|
onActive?.(client, notify);
|
|
3031
3228
|
}
|
|
3032
|
-
if (debug && !QUIET.has(method)) record({
|
|
3229
|
+
if (debug && !QUIET$1.has(method)) record({
|
|
3033
3230
|
method: `request:${method}`,
|
|
3034
|
-
params: JSON.stringify(params ?? null).slice(0, 700)
|
|
3231
|
+
params: JSON.stringify(params ?? null).slice(0, /realtime/u.test(method) ? 8e3 : 700)
|
|
3035
3232
|
});
|
|
3036
3233
|
const handler = methods[method];
|
|
3037
3234
|
if (!handler) {
|
|
3038
3235
|
record({
|
|
3039
3236
|
method,
|
|
3040
3237
|
missing: true,
|
|
3041
|
-
params: JSON.stringify(params ?? null).slice(0, 600)
|
|
3238
|
+
params: JSON.stringify(params ?? null).slice(0, /realtime/u.test(method) ? 8e3 : 600)
|
|
3042
3239
|
});
|
|
3043
3240
|
fail(id, new RpcError(METHOD_NOT_FOUND, `Method not found: ${method}`)).catch(() => {});
|
|
3044
3241
|
return;
|
|
@@ -3050,7 +3247,7 @@ function createRemoteControlHost({ methods, notifications = {}, onConnection, on
|
|
|
3050
3247
|
...context,
|
|
3051
3248
|
signal: controller.signal
|
|
3052
3249
|
})).then((result) => reply(id, result ?? {}), (error) => {
|
|
3053
|
-
if (!QUIET.has(method)) record({
|
|
3250
|
+
if (!QUIET$1.has(method)) record({
|
|
3054
3251
|
method,
|
|
3055
3252
|
error: String(error?.message ?? error).slice(0, 300),
|
|
3056
3253
|
params: JSON.stringify(params ?? null).slice(0, 600)
|
|
@@ -3081,6 +3278,7 @@ const CONFLICT_RENEW_MS = 15 * 6e4;
|
|
|
3081
3278
|
const MAX_RECONNECT_MS = 3e4;
|
|
3082
3279
|
const REQUEST_TIMEOUT_MS = 3e4;
|
|
3083
3280
|
const REFRESH_MARGIN_MS = 5 * 6e4;
|
|
3281
|
+
const CLIENT_IDLE_MS = 10 * 6e4;
|
|
3084
3282
|
const PING_MS = 1e4;
|
|
3085
3283
|
const PONG_TIMEOUT_MS = 6e4;
|
|
3086
3284
|
const STABLE_MS = 6e4;
|
|
@@ -3167,6 +3365,7 @@ function createRemoteControlRelay(options) {
|
|
|
3167
3365
|
let stop;
|
|
3168
3366
|
let running;
|
|
3169
3367
|
let socket;
|
|
3368
|
+
let subscribeCursor;
|
|
3170
3369
|
const clients = /* @__PURE__ */ new Map();
|
|
3171
3370
|
const assemblies = /* @__PURE__ */ new Map();
|
|
3172
3371
|
const sequences = /* @__PURE__ */ new Map();
|
|
@@ -3213,7 +3412,7 @@ function createRemoteControlRelay(options) {
|
|
|
3213
3412
|
installation_id: installation,
|
|
3214
3413
|
server_id: enrolled.serverId
|
|
3215
3414
|
} : {
|
|
3216
|
-
app_server_version: options.userAgent.split("/").at(-1),
|
|
3415
|
+
app_server_version: options.appServerVersion ?? options.userAgent.split("/").at(-1),
|
|
3217
3416
|
arch: process.arch,
|
|
3218
3417
|
installation_id: installation,
|
|
3219
3418
|
name: options.hostName,
|
|
@@ -3380,7 +3579,17 @@ function createRemoteControlRelay(options) {
|
|
|
3380
3579
|
const deliver = async (envelope) => {
|
|
3381
3580
|
const streamId = envelope.stream_id ?? randomUUID();
|
|
3382
3581
|
const key = `${envelope.client_id}\u0000${streamId}`;
|
|
3582
|
+
const starts = envelope.message?.method === "initialize" && envelope.message?.id !== void 0;
|
|
3383
3583
|
let client = clients.get(key);
|
|
3584
|
+
if (client && starts) {
|
|
3585
|
+
clients.delete(key);
|
|
3586
|
+
lastInbound.delete(key);
|
|
3587
|
+
sequences.delete(key);
|
|
3588
|
+
outbound.delete(key);
|
|
3589
|
+
client.close();
|
|
3590
|
+
client = void 0;
|
|
3591
|
+
}
|
|
3592
|
+
if (!client && !starts) return;
|
|
3384
3593
|
if (!client) {
|
|
3385
3594
|
let closed = false;
|
|
3386
3595
|
const connection = options.serve({
|
|
@@ -3396,6 +3605,7 @@ function createRemoteControlRelay(options) {
|
|
|
3396
3605
|
});
|
|
3397
3606
|
client = {
|
|
3398
3607
|
receive: connection.receive,
|
|
3608
|
+
lastActivity: now(),
|
|
3399
3609
|
close: () => {
|
|
3400
3610
|
closed = true;
|
|
3401
3611
|
connection.close();
|
|
@@ -3403,16 +3613,11 @@ function createRemoteControlRelay(options) {
|
|
|
3403
3613
|
};
|
|
3404
3614
|
clients.set(key, client);
|
|
3405
3615
|
}
|
|
3406
|
-
if (!(Number.isInteger(envelope.seq_id) && (lastInbound.get(key) ?? -1) >= envelope.seq_id)) {
|
|
3616
|
+
if (!(!starts && Number.isInteger(envelope.seq_id) && (lastInbound.get(key) ?? -1) >= envelope.seq_id)) {
|
|
3407
3617
|
if (Number.isInteger(envelope.seq_id)) lastInbound.set(key, envelope.seq_id);
|
|
3408
3618
|
client.receive(envelope.message);
|
|
3409
3619
|
}
|
|
3410
|
-
|
|
3411
|
-
client_id: envelope.client_id,
|
|
3412
|
-
seq_id: envelope.seq_id,
|
|
3413
|
-
stream_id: streamId,
|
|
3414
|
-
type: "ack"
|
|
3415
|
-
});
|
|
3620
|
+
client.lastActivity = now();
|
|
3416
3621
|
};
|
|
3417
3622
|
const receiveChunk = (envelope) => {
|
|
3418
3623
|
const streamId = envelope.stream_id ?? randomUUID();
|
|
@@ -3444,6 +3649,15 @@ function createRemoteControlRelay(options) {
|
|
|
3444
3649
|
type: "client_message"
|
|
3445
3650
|
};
|
|
3446
3651
|
};
|
|
3652
|
+
const sweepIdle = () => {
|
|
3653
|
+
for (const [key, client] of clients) if (now() - client.lastActivity >= CLIENT_IDLE_MS) {
|
|
3654
|
+
clients.delete(key);
|
|
3655
|
+
sequences.delete(key);
|
|
3656
|
+
outbound.delete(key);
|
|
3657
|
+
lastInbound.delete(key);
|
|
3658
|
+
client.close();
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3447
3661
|
const receive = async (raw) => {
|
|
3448
3662
|
const envelope = JSON.parse(raw);
|
|
3449
3663
|
if (typeof envelope?.client_id !== "string" || envelope.client_id.length === 0) return;
|
|
@@ -3454,13 +3668,18 @@ function createRemoteControlRelay(options) {
|
|
|
3454
3668
|
} else if (envelope.type === "ack") {
|
|
3455
3669
|
if (typeof envelope.stream_id === "string" && Number.isInteger(envelope.seq_id)) acknowledge(`${envelope.client_id}\u0000${envelope.stream_id}`, envelope.seq_id, Number.isInteger(envelope.segment_id) ? envelope.segment_id : void 0);
|
|
3456
3670
|
} else if (envelope.type === "client_closed") closeClient(envelope.client_id, envelope.stream_id);
|
|
3457
|
-
else if (envelope.type === "ping")
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3671
|
+
else if (envelope.type === "ping") {
|
|
3672
|
+
const known = typeof envelope.stream_id === "string" ? clients.get(`${envelope.client_id}\u0000${envelope.stream_id}`) : void 0;
|
|
3673
|
+
if (known) known.lastActivity = now();
|
|
3674
|
+
await sendEnvelope({
|
|
3675
|
+
client_id: envelope.client_id,
|
|
3676
|
+
seq_id: envelope.seq_id ?? 0,
|
|
3677
|
+
status: known ? "active" : "unknown",
|
|
3678
|
+
stream_id: envelope.stream_id ?? randomUUID(),
|
|
3679
|
+
type: "pong"
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
if (typeof envelope.cursor === "string" && envelope.cursor !== "") subscribeCursor = envelope.cursor;
|
|
3464
3683
|
};
|
|
3465
3684
|
const connectOnce = async (signal) => {
|
|
3466
3685
|
const current = await ensureEnrollment(signal);
|
|
@@ -3471,7 +3690,8 @@ function createRemoteControlRelay(options) {
|
|
|
3471
3690
|
"x-codex-installation-id": installation,
|
|
3472
3691
|
"x-codex-name": Buffer.from(options.hostName).toString("base64"),
|
|
3473
3692
|
"x-codex-protocol-version": PROTOCOL_VERSION,
|
|
3474
|
-
"x-codex-server-id": current.serverId
|
|
3693
|
+
"x-codex-server-id": current.serverId,
|
|
3694
|
+
...subscribeCursor === void 0 ? {} : { "x-codex-subscribe-cursor": subscribeCursor }
|
|
3475
3695
|
} });
|
|
3476
3696
|
socket = ws;
|
|
3477
3697
|
let queue = Promise.resolve();
|
|
@@ -3496,10 +3716,12 @@ function createRemoteControlRelay(options) {
|
|
|
3496
3716
|
state.status = "connected";
|
|
3497
3717
|
state.connectedAt = now();
|
|
3498
3718
|
state.lastError = void 0;
|
|
3719
|
+
diagnosticLog.info("phone", "connected to the Codex relay");
|
|
3499
3720
|
replay(ws);
|
|
3500
3721
|
if (typeof ws.ping === "function") {
|
|
3501
3722
|
hearFrom();
|
|
3502
3723
|
ping = setInterval(() => {
|
|
3724
|
+
sweepIdle();
|
|
3503
3725
|
try {
|
|
3504
3726
|
ws.ping();
|
|
3505
3727
|
} catch {}
|
|
@@ -3509,7 +3731,8 @@ function createRemoteControlRelay(options) {
|
|
|
3509
3731
|
ws.on("pong", hearFrom);
|
|
3510
3732
|
ws.on("unexpected-response", (request, response) => {
|
|
3511
3733
|
stopBeat();
|
|
3512
|
-
const error = new RemoteControlError("socket", `Unexpected server response: ${response?.statusCode}`);
|
|
3734
|
+
const error = new RemoteControlError(response?.statusCode === 401 || response?.statusCode === 403 ? "unauthorized" : "socket", `Unexpected server response: ${response?.statusCode}`);
|
|
3735
|
+
if (response?.statusCode === 404) enrolled = void 0;
|
|
3513
3736
|
if (response?.statusCode === 429 || response?.statusCode >= 500) error.retryAfterMs = retryAfterMs$1(String(response.headers?.["retry-after"] ?? ""), now());
|
|
3514
3737
|
try {
|
|
3515
3738
|
request?.destroy?.();
|
|
@@ -3556,6 +3779,7 @@ function createRemoteControlRelay(options) {
|
|
|
3556
3779
|
state.status = "waiting";
|
|
3557
3780
|
state.lastError = "conflict";
|
|
3558
3781
|
}
|
|
3782
|
+
diagnosticLog.log(conflicts > 0 ? "warn" : "error", "phone", conflicts > 0 ? "relay refused the connection: another host with the same identity is connected (409)" : `connection to the relay failed (${state.lastError})`, conflicts > 0 ? void 0 : error?.message);
|
|
3559
3783
|
if (error?.code === "unauthorized" || conflicts >= 3) enrolled = void 0;
|
|
3560
3784
|
if (conflicts >= 3 && now() - conflictSince >= (options.conflictRenewMs ?? CONFLICT_RENEW_MS) && options.renewInstallation) {
|
|
3561
3785
|
installation = await options.renewInstallation();
|
|
@@ -3642,7 +3866,8 @@ function createRemoteControl(options) {
|
|
|
3642
3866
|
phones.delete(client.id);
|
|
3643
3867
|
options.onClose?.(client, notify);
|
|
3644
3868
|
},
|
|
3645
|
-
debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1"
|
|
3869
|
+
debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1",
|
|
3870
|
+
debugFile: options.debugFile
|
|
3646
3871
|
});
|
|
3647
3872
|
let relay;
|
|
3648
3873
|
let starting;
|
|
@@ -3656,6 +3881,7 @@ function createRemoteControl(options) {
|
|
|
3656
3881
|
renewInstallation: () => installationId(options.stateFile, true),
|
|
3657
3882
|
hostName: options.hostName,
|
|
3658
3883
|
userAgent: options.userAgent,
|
|
3884
|
+
appServerVersion: options.appServerVersion,
|
|
3659
3885
|
base: options.base,
|
|
3660
3886
|
serve: (client) => host.serve(client)
|
|
3661
3887
|
});
|
|
@@ -3909,6 +4135,447 @@ async function searchFiles({ roots, query, folders, now = Date.now }) {
|
|
|
3909
4135
|
return found.sort((a, b) => b.score - a.score || a.path.length - b.path.length).slice(0, RESULTS);
|
|
3910
4136
|
}
|
|
3911
4137
|
//#endregion
|
|
4138
|
+
//#region src/remote-control-realtime.js
|
|
4139
|
+
const SIDEBAND_BASES = ["wss://api.openai.com/v1/live", "wss://realtime.chatgpt.com/v1/live"];
|
|
4140
|
+
const ACTIVATION_RETRY_MS = [
|
|
4141
|
+
100,
|
|
4142
|
+
300,
|
|
4143
|
+
800
|
|
4144
|
+
];
|
|
4145
|
+
const CONTEXT_APPEND_MAX_BYTES = 500;
|
|
4146
|
+
const CHANNEL_PREFIXES = {
|
|
4147
|
+
final: ["[FINAL]", "[COMPLETE]"],
|
|
4148
|
+
commentary: ["[COMMENTARY]", "[STATUS]"],
|
|
4149
|
+
analysis: ["[ANALYSIS]"]
|
|
4150
|
+
};
|
|
4151
|
+
const DELEGATION_FIELD_MAX = 4 * 1024;
|
|
4152
|
+
const bounded = (text) => text.length > DELEGATION_FIELD_MAX ? `${text.slice(0, DELEGATION_FIELD_MAX - 1)}…` : text;
|
|
4153
|
+
/** A spoken request reaches the agent, and so the conversation, as the plain message it is. */
|
|
4154
|
+
const delegationText = (input) => bounded(input).trim();
|
|
4155
|
+
const dialogue = (transcript) => transcript.map((entry) => `${entry.role === "user" ? "You" : "Voice"}: ${entry.text.replace(/\s+/gu, " ").trim()}`).join("\n");
|
|
4156
|
+
/** What the conversation keeps of a finished call: the dialogue, and a plain request to wrap it up. */
|
|
4157
|
+
function callRecord(transcript) {
|
|
4158
|
+
return `${dialogue(transcript)}\n\nThe voice call has ended. Summarise it in a sentence or two, and carry on with anything that was left unfinished.`;
|
|
4159
|
+
}
|
|
4160
|
+
/** The same dialogue as a note for the conversation's context: kept for the next message, with no turn of its own. */
|
|
4161
|
+
function callNote(transcript) {
|
|
4162
|
+
return `Part of the voice call that was not handed over as a request:\n${dialogue(transcript)}`;
|
|
4163
|
+
}
|
|
4164
|
+
/** Which side channel an agent message belongs on: progress is commentary, an answer is spoken. */
|
|
4165
|
+
function channelOf(text, prefixes = CHANNEL_PREFIXES) {
|
|
4166
|
+
for (const [name, list] of Object.entries(prefixes)) if (Array.isArray(list) && list.some((prefix) => prefix !== "" && text.startsWith(prefix))) return name === "final" ? "speakable" : name === "analysis" ? void 0 : "commentary";
|
|
4167
|
+
return "speakable";
|
|
4168
|
+
}
|
|
4169
|
+
/** Cut a long text on character boundaries into pieces the voice service accepts as one append. */
|
|
4170
|
+
function appendChunks(text, limit = CONTEXT_APPEND_MAX_BYTES) {
|
|
4171
|
+
const chunks = [];
|
|
4172
|
+
let current = "";
|
|
4173
|
+
let bytes = 0;
|
|
4174
|
+
for (const char of text) {
|
|
4175
|
+
const size = Buffer.byteLength(char);
|
|
4176
|
+
if (bytes + size > limit) {
|
|
4177
|
+
chunks.push(current);
|
|
4178
|
+
current = "";
|
|
4179
|
+
bytes = 0;
|
|
4180
|
+
}
|
|
4181
|
+
current += char;
|
|
4182
|
+
bytes += size;
|
|
4183
|
+
}
|
|
4184
|
+
if (current !== "") chunks.push(current);
|
|
4185
|
+
return chunks;
|
|
4186
|
+
}
|
|
4187
|
+
/**
|
|
4188
|
+
* A short utterance that could be a goodbye or a request to hang up, in the languages the voice speaks. It only decides when the agent
|
|
4189
|
+
* is asked; the agent decides whether the person really means to end the call (as in Codex), so a word inside other talk does no harm.
|
|
4190
|
+
*/
|
|
4191
|
+
const isGoodbye = (text) => {
|
|
4192
|
+
const said = String(text).trim().split(" ").filter(Boolean).join(" ");
|
|
4193
|
+
return said.length > 0 && said.length <= 30 && /(拜拜|再见|再會|(?<![a-z])bye(?![a-z])|goodbye|hang up|end (the )?call|挂断|結束通話|结束通话|结束语音|さようなら|バイバイ|またね|じゃあね|안녕히|잘 가|adiós|au revoir|tschüss|tchau|до свидания)/iu.test(said);
|
|
4194
|
+
};
|
|
4195
|
+
const content = (text) => [{
|
|
4196
|
+
type: "input_text",
|
|
4197
|
+
text
|
|
4198
|
+
}];
|
|
4199
|
+
const outbound = {
|
|
4200
|
+
sessionAppend: (text, channel) => ({
|
|
4201
|
+
type: "session.context.append",
|
|
4202
|
+
...channel ? { channel } : {},
|
|
4203
|
+
content: content(text)
|
|
4204
|
+
}),
|
|
4205
|
+
delegationAppend: (delegationItemId, text, channel) => ({
|
|
4206
|
+
type: "delegation.context.append",
|
|
4207
|
+
delegation_item_id: delegationItemId,
|
|
4208
|
+
...channel ? { channel } : {},
|
|
4209
|
+
content: content(text)
|
|
4210
|
+
}),
|
|
4211
|
+
close: () => ({ type: "session.close" })
|
|
4212
|
+
};
|
|
4213
|
+
/** What the voice service sends, as the phone-facing notifications and the work to do. */
|
|
4214
|
+
function parseEvent(payload) {
|
|
4215
|
+
let value;
|
|
4216
|
+
try {
|
|
4217
|
+
value = JSON.parse(payload);
|
|
4218
|
+
} catch {
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
switch (value?.type) {
|
|
4222
|
+
case "session.started":
|
|
4223
|
+
case "session.updated": return { kind: "session" };
|
|
4224
|
+
case "input_transcript.added": return typeof value.item?.text === "string" ? {
|
|
4225
|
+
kind: "delta",
|
|
4226
|
+
role: "user",
|
|
4227
|
+
text: value.item.text
|
|
4228
|
+
} : void 0;
|
|
4229
|
+
case "output_transcript.added": return typeof value.item?.text === "string" ? {
|
|
4230
|
+
kind: "delta",
|
|
4231
|
+
role: "assistant",
|
|
4232
|
+
text: value.item.text
|
|
4233
|
+
} : void 0;
|
|
4234
|
+
case "turn.done": {
|
|
4235
|
+
const role = value.turn?.role;
|
|
4236
|
+
return (role === "user" || role === "assistant") && typeof value.turn?.transcript === "string" ? {
|
|
4237
|
+
kind: "done",
|
|
4238
|
+
role,
|
|
4239
|
+
text: value.turn.transcript
|
|
4240
|
+
} : void 0;
|
|
4241
|
+
}
|
|
4242
|
+
case "delegation.created": {
|
|
4243
|
+
const item = value.item;
|
|
4244
|
+
if (item?.type !== "delegation" || item?.target !== "client" || typeof item.id !== "string") return void 0;
|
|
4245
|
+
const text = (Array.isArray(item.content) ? item.content : []).filter((part) => part?.type === "input_text" && typeof part.text === "string").map((part) => part.text).join("");
|
|
4246
|
+
return {
|
|
4247
|
+
kind: "delegation",
|
|
4248
|
+
id: item.id,
|
|
4249
|
+
text
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
case "error": return {
|
|
4253
|
+
kind: "error",
|
|
4254
|
+
message: String(value.error?.message ?? value.message ?? "The voice service reported an error").slice(0, 300)
|
|
4255
|
+
};
|
|
4256
|
+
default: return;
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* @param {{ WebSocket: Function, credentials: (signal?: AbortSignal) => Promise<{ access: string, accountId: string }>, userAgent: string, bases?: string[], log?: { info: Function, warn: Function, error: Function }, trace?: Function, delegate: (threadId: string, text: string, onMessage: (text: string) => void) => Promise<void> }} options
|
|
4261
|
+
*/
|
|
4262
|
+
/**
|
|
4263
|
+
* Ask the ChatGPT backend for a voice call: the browser's WebRTC offer goes in with the session settings, the answer
|
|
4264
|
+
* comes back as plain SDP and the call id in the Location header. The same request the phone makes for its own call.
|
|
4265
|
+
*/
|
|
4266
|
+
async function createVoiceCall({ fetch, credentials, userAgent, sdp, session, base = "https://chatgpt.com/backend-api", threadId }) {
|
|
4267
|
+
const { access, accountId } = await credentials();
|
|
4268
|
+
const response = await fetch(`${base}/wham/realtime/calls?intent=quicksilver&architecture=avas`, {
|
|
4269
|
+
method: "POST",
|
|
4270
|
+
headers: {
|
|
4271
|
+
authorization: `Bearer ${access}`,
|
|
4272
|
+
"chatgpt-account-id": accountId,
|
|
4273
|
+
"content-type": "application/json",
|
|
4274
|
+
accept: "application/sdp, application/json",
|
|
4275
|
+
"openai-alpha": "quicksilver=v2",
|
|
4276
|
+
"session-id": crypto.randomUUID(),
|
|
4277
|
+
"thread-id": threadId ?? crypto.randomUUID(),
|
|
4278
|
+
"user-agent": userAgent
|
|
4279
|
+
},
|
|
4280
|
+
body: JSON.stringify({
|
|
4281
|
+
sdp,
|
|
4282
|
+
session
|
|
4283
|
+
}),
|
|
4284
|
+
signal: AbortSignal.timeout(2e4)
|
|
4285
|
+
});
|
|
4286
|
+
const answer = await response.text();
|
|
4287
|
+
if (!response.ok) throw new Error(`the voice backend refused the call (HTTP ${response.status}): ${answer.slice(0, 200)}`);
|
|
4288
|
+
const callId = String(response.headers?.get?.("location") ?? "").replace(/[?#].*$/u, "").split("/").filter(Boolean).at(-1);
|
|
4289
|
+
if (!/^rtc_[A-Za-z0-9_-]{8,80}$/u.test(callId ?? "")) throw new Error("the voice backend returned no call id");
|
|
4290
|
+
if (!answer.startsWith("v=0")) throw new Error("the voice backend returned no session description");
|
|
4291
|
+
return {
|
|
4292
|
+
callId,
|
|
4293
|
+
answer
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
const LANGUAGE_NAMES = {
|
|
4297
|
+
zh: "Chinese (Mandarin)",
|
|
4298
|
+
en: "English",
|
|
4299
|
+
ja: "Japanese",
|
|
4300
|
+
ko: "Korean",
|
|
4301
|
+
es: "Spanish",
|
|
4302
|
+
fr: "French",
|
|
4303
|
+
de: "German",
|
|
4304
|
+
pt: "Portuguese",
|
|
4305
|
+
ru: "Russian"
|
|
4306
|
+
};
|
|
4307
|
+
/** What the voice model is told when the call starts from the computer. */
|
|
4308
|
+
function desktopInstructions(language) {
|
|
4309
|
+
const spoken = LANGUAGE_NAMES[language];
|
|
4310
|
+
return [
|
|
4311
|
+
"You are the voice of DeepSeek Harness (DSH), a coding agent that works on the user's computer.",
|
|
4312
|
+
"Talk like a calm, brief colleague: short sentences, no lists, no markup.",
|
|
4313
|
+
"You cannot read files, run commands, search or change anything yourself. Hand every request that needs the computer, files, code, the web or any tool to the agent by delegating it, and say in a few words what you are handing over.",
|
|
4314
|
+
"Chat that needs nothing from the computer (greetings, small talk, questions you can answer) you answer yourself. You cannot hang up yourself. When the user may want to end the call (goodbye, that is all, hang up), do not say that you are hanging up: hand their own words over to the agent and let it decide.",
|
|
4315
|
+
"When the agent reports back, tell the user the result in your own words. Messages that start with [STATUS] are progress; messages that start with [COMPLETE] are final answers.",
|
|
4316
|
+
spoken ? `Speak and answer in ${spoken} unless the user clearly speaks another language.` : "Answer in the language the user speaks."
|
|
4317
|
+
].join(" ");
|
|
4318
|
+
}
|
|
4319
|
+
function createRealtimeVoice(options) {
|
|
4320
|
+
const bases = options.bases ?? SIDEBAND_BASES;
|
|
4321
|
+
const log = options.log ?? {
|
|
4322
|
+
info() {},
|
|
4323
|
+
warn() {},
|
|
4324
|
+
error() {}
|
|
4325
|
+
};
|
|
4326
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4327
|
+
const connect = (base, callId, headers) => new Promise((resolve, reject) => {
|
|
4328
|
+
const url = `${base.replace(/\/$/, "")}/${encodeURIComponent(callId)}`;
|
|
4329
|
+
const ws = new options.WebSocket(url, { headers });
|
|
4330
|
+
let settled = false;
|
|
4331
|
+
const fail = (error) => {
|
|
4332
|
+
if (!settled) {
|
|
4333
|
+
settled = true;
|
|
4334
|
+
reject(error);
|
|
4335
|
+
}
|
|
4336
|
+
};
|
|
4337
|
+
ws.on("open", () => {
|
|
4338
|
+
if (!settled) {
|
|
4339
|
+
settled = true;
|
|
4340
|
+
resolve(ws);
|
|
4341
|
+
}
|
|
4342
|
+
});
|
|
4343
|
+
ws.on("unexpected-response", (request, response) => {
|
|
4344
|
+
try {
|
|
4345
|
+
request?.destroy?.();
|
|
4346
|
+
} catch {}
|
|
4347
|
+
const error = /* @__PURE__ */ new Error(`HTTP ${response?.statusCode}`);
|
|
4348
|
+
error.status = response?.statusCode;
|
|
4349
|
+
fail(error);
|
|
4350
|
+
});
|
|
4351
|
+
ws.on("error", (error) => fail(error));
|
|
4352
|
+
ws.on("close", () => fail(/* @__PURE__ */ new Error("closed before it opened")));
|
|
4353
|
+
});
|
|
4354
|
+
async function join(callId, headers) {
|
|
4355
|
+
let last;
|
|
4356
|
+
for (const base of bases) {
|
|
4357
|
+
const host = new URL(base).host;
|
|
4358
|
+
for (let attempt = 0; attempt <= ACTIVATION_RETRY_MS.length; attempt++) try {
|
|
4359
|
+
const ws = await connect(base, callId, headers);
|
|
4360
|
+
log.info("voice", `joined the voice call on ${host}`);
|
|
4361
|
+
return ws;
|
|
4362
|
+
} catch (error) {
|
|
4363
|
+
last = error;
|
|
4364
|
+
if (error?.status === 404 && attempt < ACTIVATION_RETRY_MS.length) {
|
|
4365
|
+
await new Promise((resolve) => setTimeout(resolve, ACTIVATION_RETRY_MS[attempt]));
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
log.warn("voice", `could not join the voice call on ${host}`, String(error?.message ?? error));
|
|
4369
|
+
break;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
throw last ?? /* @__PURE__ */ new Error("no voice endpoint answered");
|
|
4373
|
+
}
|
|
4374
|
+
const flushTail = (threadId, session) => {
|
|
4375
|
+
if (!session.flushTail || session.tail.length === 0) return;
|
|
4376
|
+
const transcript = session.tail.splice(0);
|
|
4377
|
+
const save = session.silent && options.record ? () => options.record(threadId, callNote(transcript)) : () => options.delegate(threadId, callRecord(transcript), () => {});
|
|
4378
|
+
session.chain = session.chain.then(save).catch((error) => log.warn("voice", "could not save the voice transcript to the conversation", String(error?.message ?? error)));
|
|
4379
|
+
};
|
|
4380
|
+
const stop = (threadId, reason = "requested") => {
|
|
4381
|
+
const session = sessions.get(threadId);
|
|
4382
|
+
if (!session) return false;
|
|
4383
|
+
sessions.delete(threadId);
|
|
4384
|
+
session.ended = true;
|
|
4385
|
+
clearTimeout(session.goodbye);
|
|
4386
|
+
clearTimeout(session.closeTimer);
|
|
4387
|
+
flushTail(threadId, session);
|
|
4388
|
+
try {
|
|
4389
|
+
session.ws.send(JSON.stringify(outbound.close()));
|
|
4390
|
+
} catch {}
|
|
4391
|
+
try {
|
|
4392
|
+
session.ws.close();
|
|
4393
|
+
} catch {}
|
|
4394
|
+
session.notify("thread/realtime/closed", {
|
|
4395
|
+
threadId,
|
|
4396
|
+
reason
|
|
4397
|
+
}).catch(() => {});
|
|
4398
|
+
return true;
|
|
4399
|
+
};
|
|
4400
|
+
async function start(params, notify) {
|
|
4401
|
+
const threadId = typeof params?.threadId === "string" ? params.threadId : "";
|
|
4402
|
+
const callId = params?.transport?.type === "existingCall" ? params.transport.callId : void 0;
|
|
4403
|
+
if (threadId === "") throw new Error("The voice start has no thread");
|
|
4404
|
+
if (typeof callId !== "string" || callId === "") throw new Error("Live voice needs a call the phone has already set up");
|
|
4405
|
+
stop(threadId, "replaced");
|
|
4406
|
+
const { access, accountId } = await options.credentials();
|
|
4407
|
+
const ws = await join(callId, {
|
|
4408
|
+
authorization: `Bearer ${access}`,
|
|
4409
|
+
"chatgpt-account-id": accountId,
|
|
4410
|
+
"openai-alpha": "quicksilver=v2",
|
|
4411
|
+
"user-agent": options.userAgent,
|
|
4412
|
+
...typeof params.realtimeSessionId === "string" ? { "x-session-id": params.realtimeSessionId } : {},
|
|
4413
|
+
...typeof params.voiceSessionId === "string" ? { "voice-session-id": params.voiceSessionId } : {}
|
|
4414
|
+
});
|
|
4415
|
+
const session = {
|
|
4416
|
+
ws,
|
|
4417
|
+
notify,
|
|
4418
|
+
ended: false,
|
|
4419
|
+
chain: Promise.resolve(),
|
|
4420
|
+
tail: [],
|
|
4421
|
+
lines: [],
|
|
4422
|
+
working: 0,
|
|
4423
|
+
flushTail: params.flushTranscriptTailOnSessionEnd !== false,
|
|
4424
|
+
silent: params.silentTailRecord === true,
|
|
4425
|
+
hangUp: params.hangUpOnGoodbye === true
|
|
4426
|
+
};
|
|
4427
|
+
sessions.set(threadId, session);
|
|
4428
|
+
const send = (message) => {
|
|
4429
|
+
try {
|
|
4430
|
+
ws.send(JSON.stringify(message));
|
|
4431
|
+
} catch (error) {
|
|
4432
|
+
log.warn("voice", "could not send to the voice call", String(error?.message ?? error));
|
|
4433
|
+
}
|
|
4434
|
+
};
|
|
4435
|
+
const speak = (delegationId, text) => {
|
|
4436
|
+
const channel = channelOf(text, params.codexResponseHandoffChannelPrefixes ?? CHANNEL_PREFIXES);
|
|
4437
|
+
if (channel === void 0) return;
|
|
4438
|
+
for (const piece of appendChunks(text)) send(delegationId ? outbound.delegationAppend(delegationId, piece, channel) : outbound.sessionAppend(piece, channel));
|
|
4439
|
+
};
|
|
4440
|
+
session.speak = speak;
|
|
4441
|
+
const runRequest = (input, delegationId) => {
|
|
4442
|
+
session.chain = session.chain.then(async () => {
|
|
4443
|
+
if (session.ended) return;
|
|
4444
|
+
try {
|
|
4445
|
+
session.working += 1;
|
|
4446
|
+
try {
|
|
4447
|
+
await options.delegate(threadId, delegationText(input), (text) => {
|
|
4448
|
+
if (!session.ended) speak(delegationId, text);
|
|
4449
|
+
});
|
|
4450
|
+
} finally {
|
|
4451
|
+
session.working -= 1;
|
|
4452
|
+
}
|
|
4453
|
+
} catch (error) {
|
|
4454
|
+
log.warn("voice", "the agent could not take the voice request", String(error?.message ?? error));
|
|
4455
|
+
if (!session.ended) speak(delegationId, `[COMPLETE] I could not do that: ${String(error?.message ?? "the task could not start").slice(0, 160)}`);
|
|
4456
|
+
}
|
|
4457
|
+
});
|
|
4458
|
+
};
|
|
4459
|
+
const seenTypes = /* @__PURE__ */ new Set();
|
|
4460
|
+
ws.on("message", (data) => {
|
|
4461
|
+
const event = parseEvent(String(data));
|
|
4462
|
+
if (!event) try {
|
|
4463
|
+
const type = JSON.parse(String(data))?.type;
|
|
4464
|
+
if (typeof type === "string" && !seenTypes.has(type) && seenTypes.size < 40) {
|
|
4465
|
+
seenTypes.add(type);
|
|
4466
|
+
log.info("voice", `voice service message: ${type}`);
|
|
4467
|
+
}
|
|
4468
|
+
} catch {}
|
|
4469
|
+
if (!event || session.ended) return;
|
|
4470
|
+
if (event.kind === "delta") notify("thread/realtime/transcript/delta", {
|
|
4471
|
+
threadId,
|
|
4472
|
+
role: event.role,
|
|
4473
|
+
delta: event.text
|
|
4474
|
+
}).catch(() => {});
|
|
4475
|
+
else if (event.kind === "done") {
|
|
4476
|
+
if (event.text.trim() !== "") {
|
|
4477
|
+
session.tail.push({
|
|
4478
|
+
role: event.role,
|
|
4479
|
+
text: event.text
|
|
4480
|
+
});
|
|
4481
|
+
if (session.tail.length > 200) session.tail.shift();
|
|
4482
|
+
session.lines.push({
|
|
4483
|
+
role: event.role,
|
|
4484
|
+
text: event.text.slice(0, 400)
|
|
4485
|
+
});
|
|
4486
|
+
if (session.lines.length > 12) session.lines.shift();
|
|
4487
|
+
}
|
|
4488
|
+
notify("thread/realtime/transcript/done", {
|
|
4489
|
+
threadId,
|
|
4490
|
+
role: event.role,
|
|
4491
|
+
text: event.text
|
|
4492
|
+
}).catch(() => {});
|
|
4493
|
+
if (session.hangUp && event.role === "user" && isGoodbye(event.text)) {
|
|
4494
|
+
clearTimeout(session.goodbye);
|
|
4495
|
+
const said = Date.now();
|
|
4496
|
+
session.goodbye = setTimeout(() => {
|
|
4497
|
+
session.goodbye = void 0;
|
|
4498
|
+
if (!session.ended && !(session.delegatedAt > said - 4e3)) runRequest(event.text.trim(), void 0);
|
|
4499
|
+
}, 2500);
|
|
4500
|
+
}
|
|
4501
|
+
} else if (event.kind === "error") {
|
|
4502
|
+
log.error("voice", "the voice service reported an error", event.message);
|
|
4503
|
+
notify("thread/realtime/error", {
|
|
4504
|
+
threadId,
|
|
4505
|
+
message: event.message
|
|
4506
|
+
}).catch(() => {});
|
|
4507
|
+
} else if (event.kind === "delegation") {
|
|
4508
|
+
if (event.text === "") return;
|
|
4509
|
+
clearTimeout(session.goodbye);
|
|
4510
|
+
session.goodbye = void 0;
|
|
4511
|
+
session.delegatedAt = Date.now();
|
|
4512
|
+
runRequest(event.text, event.id);
|
|
4513
|
+
}
|
|
4514
|
+
});
|
|
4515
|
+
ws.on("close", () => {
|
|
4516
|
+
if (session.ended || sessions.get(threadId) !== session) return;
|
|
4517
|
+
sessions.delete(threadId);
|
|
4518
|
+
session.ended = true;
|
|
4519
|
+
flushTail(threadId, session);
|
|
4520
|
+
log.warn("voice", "the voice call closed");
|
|
4521
|
+
notify("thread/realtime/closed", {
|
|
4522
|
+
threadId,
|
|
4523
|
+
reason: "transportClosed"
|
|
4524
|
+
}).catch(() => {});
|
|
4525
|
+
});
|
|
4526
|
+
ws.on("error", (error) => log.warn("voice", "the voice connection failed", String(error?.message ?? error)));
|
|
4527
|
+
notify("thread/realtime/started", {
|
|
4528
|
+
threadId,
|
|
4529
|
+
version: params.version ?? "v3",
|
|
4530
|
+
realtimeSessionId: params.realtimeSessionId ?? null
|
|
4531
|
+
}).catch(() => {});
|
|
4532
|
+
return {};
|
|
4533
|
+
}
|
|
4534
|
+
const append = (threadId, text, channel) => {
|
|
4535
|
+
const session = sessions.get(threadId);
|
|
4536
|
+
if (!session || typeof text !== "string" || text === "") return false;
|
|
4537
|
+
for (const piece of appendChunks(text)) try {
|
|
4538
|
+
session.ws.send(JSON.stringify(outbound.sessionAppend(piece, channel)));
|
|
4539
|
+
} catch {
|
|
4540
|
+
return false;
|
|
4541
|
+
}
|
|
4542
|
+
return true;
|
|
4543
|
+
};
|
|
4544
|
+
return {
|
|
4545
|
+
start,
|
|
4546
|
+
stop,
|
|
4547
|
+
appendText: (threadId, text) => append(threadId, text, void 0),
|
|
4548
|
+
appendSpeech: (threadId, text) => append(threadId, text, "speakable"),
|
|
4549
|
+
active: (threadId) => sessions.has(threadId),
|
|
4550
|
+
/** Ask for the call to end once the voice has finished speaking (the window does it); closes by itself after a while if nobody does. */
|
|
4551
|
+
requestClose(threadId) {
|
|
4552
|
+
const session = sessions.get(threadId);
|
|
4553
|
+
if (!session) return false;
|
|
4554
|
+
session.closing = true;
|
|
4555
|
+
session.closeTimer ??= setTimeout(() => stop(threadId, "requested"), 2e4);
|
|
4556
|
+
return true;
|
|
4557
|
+
},
|
|
4558
|
+
/** What the voice stage shows: the last spoken lines, whether the agent is working on a request, whether the call is about to end. */
|
|
4559
|
+
status(threadId) {
|
|
4560
|
+
const session = sessions.get(threadId);
|
|
4561
|
+
return session ? {
|
|
4562
|
+
active: true,
|
|
4563
|
+
working: session.working > 0,
|
|
4564
|
+
closing: session.closing === true,
|
|
4565
|
+
lines: session.lines.slice(-6)
|
|
4566
|
+
} : {
|
|
4567
|
+
active: false,
|
|
4568
|
+
working: false,
|
|
4569
|
+
closing: false,
|
|
4570
|
+
lines: []
|
|
4571
|
+
};
|
|
4572
|
+
},
|
|
4573
|
+
closeAll: () => {
|
|
4574
|
+
for (const threadId of [...sessions.keys()]) stop(threadId, "shutdown");
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
//#endregion
|
|
3912
4579
|
//#region src/remote-control-dsh.js
|
|
3913
4580
|
/** A folder the phone named, only when it is an existing absolute folder on this machine. */
|
|
3914
4581
|
function localFolder(value) {
|
|
@@ -4259,7 +4926,7 @@ function projectTurns(messages, at = Date.now(), imageUrls = /* @__PURE__ */ new
|
|
|
4259
4926
|
close();
|
|
4260
4927
|
return turns;
|
|
4261
4928
|
}
|
|
4262
|
-
function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [], projectId = null }) {
|
|
4929
|
+
function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [], projectId = null, threadSource = "dsh" }) {
|
|
4263
4930
|
return {
|
|
4264
4931
|
id,
|
|
4265
4932
|
sessionId: id,
|
|
@@ -4279,7 +4946,7 @@ function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns
|
|
|
4279
4946
|
cwd: cwd ?? "",
|
|
4280
4947
|
cliVersion: "dsh",
|
|
4281
4948
|
source: { custom: "dsh" },
|
|
4282
|
-
threadSource
|
|
4949
|
+
threadSource,
|
|
4283
4950
|
agentNickname: null,
|
|
4284
4951
|
agentRole: null,
|
|
4285
4952
|
gitInfo: null,
|
|
@@ -4298,8 +4965,94 @@ function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns
|
|
|
4298
4965
|
*/
|
|
4299
4966
|
const PHONE_RETURN_MS = 12e4;
|
|
4300
4967
|
const ADOPT_WITHIN_MS = 6e5;
|
|
4301
|
-
|
|
4968
|
+
const V1_VOICES = [
|
|
4969
|
+
"juniper",
|
|
4970
|
+
"maple",
|
|
4971
|
+
"spruce",
|
|
4972
|
+
"ember",
|
|
4973
|
+
"vale",
|
|
4974
|
+
"breeze",
|
|
4975
|
+
"arbor",
|
|
4976
|
+
"sol",
|
|
4977
|
+
"cove"
|
|
4978
|
+
];
|
|
4979
|
+
const REALTIME_VOICES = {
|
|
4980
|
+
v1: V1_VOICES,
|
|
4981
|
+
v2: [
|
|
4982
|
+
"alloy",
|
|
4983
|
+
"ash",
|
|
4984
|
+
"ballad",
|
|
4985
|
+
"coral",
|
|
4986
|
+
"echo",
|
|
4987
|
+
"sage",
|
|
4988
|
+
"shimmer",
|
|
4989
|
+
"verse",
|
|
4990
|
+
"marin",
|
|
4991
|
+
"cedar"
|
|
4992
|
+
],
|
|
4993
|
+
v3: [
|
|
4994
|
+
...V1_VOICES,
|
|
4995
|
+
"aube",
|
|
4996
|
+
"bubbie",
|
|
4997
|
+
"haetsal",
|
|
4998
|
+
"hanul",
|
|
4999
|
+
"himari",
|
|
5000
|
+
"leher",
|
|
5001
|
+
"miko",
|
|
5002
|
+
"neer",
|
|
5003
|
+
"porto",
|
|
5004
|
+
"rindo",
|
|
5005
|
+
"rio",
|
|
5006
|
+
"rivage",
|
|
5007
|
+
"selva",
|
|
5008
|
+
"sonna",
|
|
5009
|
+
"tinta",
|
|
5010
|
+
"viola"
|
|
5011
|
+
],
|
|
5012
|
+
defaultV1: "cove",
|
|
5013
|
+
defaultV2: "marin"
|
|
5014
|
+
};
|
|
5015
|
+
/** The phone's saved defaults, nested like a config file and kept across restarts. */
|
|
5016
|
+
function createPhoneConfig(file) {
|
|
5017
|
+
let value = {};
|
|
5018
|
+
try {
|
|
5019
|
+
if (file) value = JSON.parse(readFileSync(file, "utf8"));
|
|
5020
|
+
} catch {}
|
|
5021
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) value = {};
|
|
5022
|
+
const save = () => {
|
|
5023
|
+
if (!file) return;
|
|
5024
|
+
try {
|
|
5025
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
5026
|
+
writeFileSync(file, JSON.stringify(value));
|
|
5027
|
+
} catch {}
|
|
5028
|
+
};
|
|
5029
|
+
const FORBIDDEN = /* @__PURE__ */ new Set([
|
|
5030
|
+
"__proto__",
|
|
5031
|
+
"constructor",
|
|
5032
|
+
"prototype"
|
|
5033
|
+
]);
|
|
5034
|
+
return {
|
|
5035
|
+
read: () => structuredClone(value),
|
|
5036
|
+
write(edits) {
|
|
5037
|
+
if (!Array.isArray(edits)) return;
|
|
5038
|
+
for (const edit of edits.slice(0, 50)) {
|
|
5039
|
+
const path = typeof edit?.keyPath === "string" ? edit.keyPath.split(".").filter(Boolean) : [];
|
|
5040
|
+
if (path.length === 0 || path.length > 6 || path.some((part) => FORBIDDEN.has(part))) continue;
|
|
5041
|
+
let node = value;
|
|
5042
|
+
for (const part of path.slice(0, -1)) {
|
|
5043
|
+
if (node[part] === null || typeof node[part] !== "object") node[part] = {};
|
|
5044
|
+
node = node[part];
|
|
5045
|
+
}
|
|
5046
|
+
if (edit.value === null || edit.value === void 0) delete node[path.at(-1)];
|
|
5047
|
+
else node[path.at(-1)] = edit.value;
|
|
5048
|
+
}
|
|
5049
|
+
save();
|
|
5050
|
+
}
|
|
5051
|
+
};
|
|
5052
|
+
}
|
|
5053
|
+
function createDshRemoteControl({ controller, agents, permissions = () => void 0, workspaces = () => void 0, attachments = () => void 0, usage = () => void 0, usageProfile = () => void 0, projections = () => void 0, skills = () => void 0, commands = () => void 0, llm = () => void 0, speed = void 0, trace = () => {}, phoneReturnMs = PHONE_RETURN_MS, userAgent, appServerUserAgent = userAgent, configFile = void 0, voice = void 0 }) {
|
|
4302
5054
|
const subscribers = /* @__PURE__ */ new Map();
|
|
5055
|
+
const threadSources = /* @__PURE__ */ new Map();
|
|
4303
5056
|
const active = /* @__PURE__ */ new Map();
|
|
4304
5057
|
const queues = /* @__PURE__ */ new Map();
|
|
4305
5058
|
let catalog;
|
|
@@ -4312,6 +5065,9 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4312
5065
|
const openedBy = /* @__PURE__ */ new Map();
|
|
4313
5066
|
const subscribe = (threadId, notify) => {
|
|
4314
5067
|
const set = subscribers.get(threadId) ?? /* @__PURE__ */ new Set();
|
|
5068
|
+
if (notify.clientId) {
|
|
5069
|
+
for (const other of [...set]) if (other !== notify && other.clientId === notify.clientId) set.delete(other);
|
|
5070
|
+
}
|
|
4315
5071
|
set.add(notify);
|
|
4316
5072
|
subscribers.set(threadId, set);
|
|
4317
5073
|
if (notify.clientId) {
|
|
@@ -4404,7 +5160,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4404
5160
|
reasoningEffort: current?.reasoningEffort ?? null,
|
|
4405
5161
|
runtimeWorkspaceRoots: cwd ? [cwd] : [],
|
|
4406
5162
|
sandbox: sandboxFor(preset, cwd),
|
|
4407
|
-
serviceTier:
|
|
5163
|
+
serviceTier: shownTier(),
|
|
4408
5164
|
turnsBackwardsCursor: null
|
|
4409
5165
|
};
|
|
4410
5166
|
};
|
|
@@ -4472,7 +5228,21 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4472
5228
|
return urls;
|
|
4473
5229
|
};
|
|
4474
5230
|
/** The turn DSH is running, as the phone should see it now: what is committed, plus the reply still being written. */
|
|
4475
|
-
const
|
|
5231
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
5232
|
+
const keepShownId = (threadId, committed, shown) => {
|
|
5233
|
+
if (committed === shown) return;
|
|
5234
|
+
aliases.set(`${threadId}\0${committed}`, shown);
|
|
5235
|
+
if (aliases.size > 2e3) aliases.delete(aliases.keys().next().value);
|
|
5236
|
+
};
|
|
5237
|
+
const withShownIds = (id, turns) => aliases.size === 0 ? turns : turns.map((turn) => ({
|
|
5238
|
+
...turn,
|
|
5239
|
+
items: turn.items.map((item) => aliases.has(`${id}\0${item.id}`) ? {
|
|
5240
|
+
...item,
|
|
5241
|
+
id: aliases.get(`${id}\0${item.id}`)
|
|
5242
|
+
} : item)
|
|
5243
|
+
}));
|
|
5244
|
+
const withLiveTurn = (id, shownTurns) => {
|
|
5245
|
+
const turns = withShownIds(id, shownTurns);
|
|
4476
5246
|
const live = active.get(id);
|
|
4477
5247
|
if (!live) return turns;
|
|
4478
5248
|
const typed = (item) => (item?.content ?? []).filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
@@ -4483,7 +5253,11 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4483
5253
|
}] : [];
|
|
4484
5254
|
const last = turns.at(-1);
|
|
4485
5255
|
const ours = last?.items[0]?.type === "userMessage" && typed(last.items[0]) === typed(live.userItem);
|
|
4486
|
-
const items = ours ? [
|
|
5256
|
+
const items = ours ? [
|
|
5257
|
+
live.userItem,
|
|
5258
|
+
...last.items.slice(1),
|
|
5259
|
+
...tail
|
|
5260
|
+
] : [live.userItem, ...tail];
|
|
4487
5261
|
return [...ours ? turns.slice(0, -1) : turns, {
|
|
4488
5262
|
...live.turn,
|
|
4489
5263
|
items,
|
|
@@ -4510,12 +5284,44 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4510
5284
|
createdAt: session.meta?.createdAt,
|
|
4511
5285
|
running: active.has(id),
|
|
4512
5286
|
projectId: projectIdOf(id),
|
|
5287
|
+
threadSource: threadSources.get(id),
|
|
4513
5288
|
preview: first ? visibleText(first.content).slice(0, 200) : "",
|
|
4514
5289
|
turns: withLiveTurn(id, projectTurns(messages, session.meta?.createdAt, imageUrls, cwd))
|
|
4515
5290
|
})
|
|
4516
5291
|
};
|
|
4517
5292
|
};
|
|
4518
5293
|
/** Apply a model or effort the phone picked; DSH validates it against its own catalog. */
|
|
5294
|
+
const TIER_OF = {
|
|
5295
|
+
fast: "priority",
|
|
5296
|
+
ultrafast: "ultrafast"
|
|
5297
|
+
};
|
|
5298
|
+
const MODE_OF = {
|
|
5299
|
+
priority: "fast",
|
|
5300
|
+
fast: "fast",
|
|
5301
|
+
ultrafast: "ultrafast",
|
|
5302
|
+
default: "standard"
|
|
5303
|
+
};
|
|
5304
|
+
const tiersOf = (provider, model) => {
|
|
5305
|
+
try {
|
|
5306
|
+
return speed?.tiers?.(provider, model) ?? [];
|
|
5307
|
+
} catch {
|
|
5308
|
+
return [];
|
|
5309
|
+
}
|
|
5310
|
+
};
|
|
5311
|
+
const shownTier = () => {
|
|
5312
|
+
try {
|
|
5313
|
+
return TIER_OF[speed?.current?.()] ?? null;
|
|
5314
|
+
} catch {
|
|
5315
|
+
return null;
|
|
5316
|
+
}
|
|
5317
|
+
};
|
|
5318
|
+
/** A tier the phone picked: a string id, "default" or null for standard speed; undefined leaves the setting alone. */
|
|
5319
|
+
const applySpeed = async (tier) => {
|
|
5320
|
+
if (tier === void 0 || !speed?.set) return;
|
|
5321
|
+
const mode = tier === null ? "standard" : MODE_OF[tier];
|
|
5322
|
+
if (mode === void 0 || mode === speed.current?.()) return;
|
|
5323
|
+
await speed.set(mode);
|
|
5324
|
+
};
|
|
4519
5325
|
const applySelection = async (agent, sessionId, params) => {
|
|
4520
5326
|
const picked = parseModelKey(params?.model);
|
|
4521
5327
|
const effort = typeof params?.effort === "string" ? params.effort : void 0;
|
|
@@ -4656,6 +5462,9 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4656
5462
|
subscribe(id, notify);
|
|
4657
5463
|
await applySelection(agent, id, params);
|
|
4658
5464
|
applyPermissions(agent, params);
|
|
5465
|
+
const speedBefore = speed?.current?.();
|
|
5466
|
+
const speedForTurn = typeof params?.serviceTierForTurn === "string" ? params.serviceTierForTurn : void 0;
|
|
5467
|
+
await applySpeed(speedForTurn ?? (Object.hasOwn(params ?? {}, "serviceTier") ? params.serviceTier : void 0));
|
|
4659
5468
|
const turn = emptyTurn();
|
|
4660
5469
|
const userItem = {
|
|
4661
5470
|
type: "userMessage",
|
|
@@ -4727,6 +5536,12 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4727
5536
|
});
|
|
4728
5537
|
}
|
|
4729
5538
|
turn.completedAt = seconds(Date.now());
|
|
5539
|
+
if (speedForTurn !== void 0 && speedBefore !== void 0 && speed?.current?.() !== speedBefore) Promise.resolve(speed.set?.(speedBefore)).catch(() => {});
|
|
5540
|
+
const sentText = visibleText(userItem.content.filter((part) => part.type === "text"));
|
|
5541
|
+
try {
|
|
5542
|
+
const committedUser = agent.session.deriveMessages().findLast((message) => typedText(message) !== "" && typedText(message) === sentText);
|
|
5543
|
+
if (committedUser) keepShownId(id, committedUser.id, userItem.id);
|
|
5544
|
+
} catch {}
|
|
4730
5545
|
active.delete(id);
|
|
4731
5546
|
compactions.delete(id);
|
|
4732
5547
|
const usageNow = tokenUsage(agent.session);
|
|
@@ -4791,6 +5606,54 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4791
5606
|
const needle = typeof term === "string" ? term.trim().toLocaleLowerCase() : "";
|
|
4792
5607
|
return needle === "" || snippets.has(entry.id) || `${entry.name ?? ""} ${entry.preview}`.toLocaleLowerCase().includes(needle);
|
|
4793
5608
|
};
|
|
5609
|
+
/** Run a request the voice model handed over as a turn, and pass each message the agent finishes back to it. */
|
|
5610
|
+
const delegateToAgent = async (threadId, text, onMessage) => {
|
|
5611
|
+
for (let waited = 0; active.has(threadId) && waited < 6e5; waited += 500) await new Promise((resolve) => setTimeout(resolve, 500));
|
|
5612
|
+
let finish;
|
|
5613
|
+
const finished = new Promise((resolve) => {
|
|
5614
|
+
finish = resolve;
|
|
5615
|
+
});
|
|
5616
|
+
const observer = async (method, params) => {
|
|
5617
|
+
if (params?.threadId !== threadId) return;
|
|
5618
|
+
if (method === "item/completed" && params.item?.type === "agentMessage" && typeof params.item.text === "string" && params.item.text.trim() !== "") onMessage(params.item.text);
|
|
5619
|
+
else if (method === "turn/completed") finish();
|
|
5620
|
+
};
|
|
5621
|
+
try {
|
|
5622
|
+
await startTurn({
|
|
5623
|
+
threadId,
|
|
5624
|
+
input: [{
|
|
5625
|
+
type: "text",
|
|
5626
|
+
text,
|
|
5627
|
+
text_elements: []
|
|
5628
|
+
}]
|
|
5629
|
+
}, observer);
|
|
5630
|
+
await finished;
|
|
5631
|
+
} finally {
|
|
5632
|
+
subscribers.get(threadId)?.delete(observer);
|
|
5633
|
+
}
|
|
5634
|
+
};
|
|
5635
|
+
/** Leave text in the conversation's context for its next message, without starting a turn. */
|
|
5636
|
+
const recordToAgent = async (threadId, text) => {
|
|
5637
|
+
const agent = await agentFor(threadId);
|
|
5638
|
+
if (typeof agent.inject !== "function") return;
|
|
5639
|
+
agent.inject({
|
|
5640
|
+
id: randomUUID(),
|
|
5641
|
+
role: "user",
|
|
5642
|
+
content: [{
|
|
5643
|
+
type: "text",
|
|
5644
|
+
text
|
|
5645
|
+
}],
|
|
5646
|
+
source: { kind: "user" }
|
|
5647
|
+
});
|
|
5648
|
+
};
|
|
5649
|
+
const realtimeVoice = voice ? createRealtimeVoice({
|
|
5650
|
+
...voice,
|
|
5651
|
+
userAgent,
|
|
5652
|
+
log: diagnosticLog,
|
|
5653
|
+
delegate: delegateToAgent,
|
|
5654
|
+
record: recordToAgent
|
|
5655
|
+
}) : void 0;
|
|
5656
|
+
const phoneConfig = createPhoneConfig(configFile);
|
|
4794
5657
|
const methods = {
|
|
4795
5658
|
initialize: () => ({
|
|
4796
5659
|
codexHome: "",
|
|
@@ -4799,7 +5662,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4799
5662
|
darwin: "macos",
|
|
4800
5663
|
win32: "windows"
|
|
4801
5664
|
}[process.platform] ?? process.platform,
|
|
4802
|
-
userAgent
|
|
5665
|
+
userAgent: appServerUserAgent
|
|
4803
5666
|
}),
|
|
4804
5667
|
"account/read": async () => ({
|
|
4805
5668
|
account: {
|
|
@@ -4847,11 +5710,12 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4847
5710
|
defaultReasoningEffort: model.reasoning?.defaultEffort ?? supported[0].reasoningEffort,
|
|
4848
5711
|
inputModalities: modalities.get(model.id)?.length ? modalities.get(model.id) : ["text"],
|
|
4849
5712
|
supportsPersonality: false,
|
|
4850
|
-
additionalSpeedTiers:
|
|
5713
|
+
additionalSpeedTiers: tiersOf(group.id, model.id).map((tier) => tier.id),
|
|
4851
5714
|
upgrade: null,
|
|
4852
5715
|
upgradeInfo: null,
|
|
4853
5716
|
availabilityNux: null,
|
|
4854
|
-
serviceTiers:
|
|
5717
|
+
serviceTiers: tiersOf(group.id, model.id),
|
|
5718
|
+
defaultServiceTier: null
|
|
4855
5719
|
});
|
|
4856
5720
|
}
|
|
4857
5721
|
}
|
|
@@ -4869,18 +5733,22 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4869
5733
|
const agent = await agentFor(params?.threadId);
|
|
4870
5734
|
await applySelection(agent, params.threadId, params);
|
|
4871
5735
|
applyPermissions(agent, params);
|
|
5736
|
+
if (Object.hasOwn(params ?? {}, "serviceTier")) await applySpeed(params.serviceTier);
|
|
4872
5737
|
return {};
|
|
4873
5738
|
},
|
|
4874
5739
|
"thread/attachment/list": () => ({
|
|
4875
5740
|
data: [],
|
|
4876
5741
|
nextCursor: null
|
|
4877
5742
|
}),
|
|
4878
|
-
"config/batchWrite": () =>
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5743
|
+
"config/batchWrite": (params) => {
|
|
5744
|
+
phoneConfig.write(params?.edits);
|
|
5745
|
+
return {
|
|
5746
|
+
status: "ok",
|
|
5747
|
+
version: "1",
|
|
5748
|
+
filePath: process.platform === "win32" ? "C:dshconfig.toml" : "/dsh/config.toml",
|
|
5749
|
+
overriddenMetadata: null
|
|
5750
|
+
};
|
|
5751
|
+
},
|
|
4884
5752
|
"command/exec": async (params) => {
|
|
4885
5753
|
const folders = await conversationFolders();
|
|
4886
5754
|
try {
|
|
@@ -4908,6 +5776,51 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4908
5776
|
},
|
|
4909
5777
|
"fs/readDirectory": (params) => ({ entries: readFolder(params?.path) }),
|
|
4910
5778
|
"collaborationMode/list": () => ({ data: [] }),
|
|
5779
|
+
"thread/realtime/listVoices": () => {
|
|
5780
|
+
return { voices: REALTIME_VOICES };
|
|
5781
|
+
},
|
|
5782
|
+
"thread/realtime/start": async (params, { notify }) => {
|
|
5783
|
+
const threadId = typeof params?.threadId === "string" ? params.threadId : "";
|
|
5784
|
+
const fail = (message) => {
|
|
5785
|
+
setTimeout(() => {
|
|
5786
|
+
notify("thread/realtime/error", {
|
|
5787
|
+
threadId,
|
|
5788
|
+
message
|
|
5789
|
+
}).catch(() => {});
|
|
5790
|
+
notify("thread/realtime/closed", {
|
|
5791
|
+
threadId,
|
|
5792
|
+
reason: "error"
|
|
5793
|
+
}).catch(() => {});
|
|
5794
|
+
}, 50);
|
|
5795
|
+
return {};
|
|
5796
|
+
};
|
|
5797
|
+
if (!realtimeVoice) return fail("Live voice is not available on this DSH");
|
|
5798
|
+
subscribe(threadId, notify);
|
|
5799
|
+
realtimeVoice.start(params, notify).catch((error) => {
|
|
5800
|
+
diagnosticLog.error("voice", "live voice could not start", String(error?.message ?? error));
|
|
5801
|
+
notify("thread/realtime/error", {
|
|
5802
|
+
threadId,
|
|
5803
|
+
message: "Live voice could not connect to the voice service"
|
|
5804
|
+
}).catch(() => {});
|
|
5805
|
+
notify("thread/realtime/closed", {
|
|
5806
|
+
threadId,
|
|
5807
|
+
reason: "error"
|
|
5808
|
+
}).catch(() => {});
|
|
5809
|
+
});
|
|
5810
|
+
return {};
|
|
5811
|
+
},
|
|
5812
|
+
"thread/realtime/stop": (params) => {
|
|
5813
|
+
realtimeVoice?.stop(params?.threadId);
|
|
5814
|
+
return {};
|
|
5815
|
+
},
|
|
5816
|
+
"thread/realtime/appendText": (params) => {
|
|
5817
|
+
realtimeVoice?.appendText(params?.threadId, params?.text);
|
|
5818
|
+
return {};
|
|
5819
|
+
},
|
|
5820
|
+
"thread/realtime/appendSpeech": (params) => {
|
|
5821
|
+
realtimeVoice?.appendSpeech(params?.threadId, params?.text);
|
|
5822
|
+
return {};
|
|
5823
|
+
},
|
|
4911
5824
|
"permissionProfile/list": () => ({
|
|
4912
5825
|
data: [],
|
|
4913
5826
|
nextCursor: null
|
|
@@ -4922,7 +5835,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
4922
5835
|
nextCursor: null
|
|
4923
5836
|
}),
|
|
4924
5837
|
"config/read": () => ({
|
|
4925
|
-
config:
|
|
5838
|
+
config: phoneConfig.read(),
|
|
4926
5839
|
origins: {},
|
|
4927
5840
|
layers: null
|
|
4928
5841
|
}),
|
|
@@ -5106,8 +6019,14 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
5106
6019
|
const cwd = localFolder(params?.cwd);
|
|
5107
6020
|
const project = typeof params?.projectId === "string" ? workspaceList().find((entry) => String(entry.id) === params.projectId) : void 0;
|
|
5108
6021
|
const created = await service().create(project ? { workspaceId: project.id } : cwd ? { cwd } : {});
|
|
6022
|
+
if (typeof params?.threadSource === "string" && params.threadSource !== "") threadSources.set(created.sessionId, params.threadSource.slice(0, 64));
|
|
5109
6023
|
const agent = await agentFor(created.sessionId);
|
|
5110
|
-
|
|
6024
|
+
const saved = phoneConfig.read();
|
|
6025
|
+
await applySelection(agent, created.sessionId, {
|
|
6026
|
+
...params,
|
|
6027
|
+
model: params?.model ?? saved.model,
|
|
6028
|
+
effort: params?.effort ?? saved.model_reasoning_effort
|
|
6029
|
+
});
|
|
5111
6030
|
const { thread: value } = await openThread(created.sessionId, notify);
|
|
5112
6031
|
emit(created.sessionId, "thread/started", { thread: value });
|
|
5113
6032
|
return {
|
|
@@ -5221,6 +6140,10 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
5221
6140
|
sessionId: params?.threadId,
|
|
5222
6141
|
title: String(params?.name ?? "")
|
|
5223
6142
|
});
|
|
6143
|
+
emit(params?.threadId, "thread/name/updated", {
|
|
6144
|
+
threadId: params?.threadId,
|
|
6145
|
+
threadName: String(params?.name ?? "")
|
|
6146
|
+
});
|
|
5224
6147
|
return {};
|
|
5225
6148
|
},
|
|
5226
6149
|
"thread/queue/list": (params) => ({
|
|
@@ -5569,6 +6492,7 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
5569
6492
|
if (reuse && item.type === "agentMessage") {
|
|
5570
6493
|
const shown = reuse;
|
|
5571
6494
|
reuse = void 0;
|
|
6495
|
+
keepShownId(session.id, item.id, shown.id);
|
|
5572
6496
|
shown.text = item.text;
|
|
5573
6497
|
emit(session.id, "item/completed", {
|
|
5574
6498
|
threadId: session.id,
|
|
@@ -5614,6 +6538,42 @@ function createDshRemoteControl({ controller, agents, permissions = () => void 0
|
|
|
5614
6538
|
};
|
|
5615
6539
|
return {
|
|
5616
6540
|
methods,
|
|
6541
|
+
desktopVoice: realtimeVoice === void 0 ? void 0 : Object.freeze({
|
|
6542
|
+
async start({ sessionId, sdp, voice: voiceName, language }) {
|
|
6543
|
+
if (typeof sdp !== "string" || !sdp.startsWith("v=0") || sdp.length > 64 * 1024) throw new RpcError(-32602, "Invalid voice offer");
|
|
6544
|
+
const agent = await agentFor(sessionId);
|
|
6545
|
+
diagnosticLog.info("voice", "the conversation can take a context note", String(typeof agent.inject === "function"));
|
|
6546
|
+
const session = {
|
|
6547
|
+
audio: { output: { voice: typeof voiceName === "string" && /^[a-z]{2,16}$/u.test(voiceName) ? voiceName : "cove" } },
|
|
6548
|
+
delegation: { type: "client" },
|
|
6549
|
+
instructions: desktopInstructions(language),
|
|
6550
|
+
model: "gpt-live-1.1-codex"
|
|
6551
|
+
};
|
|
6552
|
+
const { callId, answer } = await createVoiceCall({
|
|
6553
|
+
fetch: voice.fetch,
|
|
6554
|
+
credentials: voice.credentials,
|
|
6555
|
+
userAgent,
|
|
6556
|
+
sdp,
|
|
6557
|
+
session,
|
|
6558
|
+
threadId: sessionId
|
|
6559
|
+
});
|
|
6560
|
+
await realtimeVoice.start({
|
|
6561
|
+
threadId: sessionId,
|
|
6562
|
+
transport: {
|
|
6563
|
+
type: "existingCall",
|
|
6564
|
+
callId
|
|
6565
|
+
},
|
|
6566
|
+
flushTranscriptTailOnSessionEnd: true,
|
|
6567
|
+
silentTailRecord: true,
|
|
6568
|
+
hangUpOnGoodbye: true,
|
|
6569
|
+
version: "v3"
|
|
6570
|
+
}, async () => {});
|
|
6571
|
+
return { answer };
|
|
6572
|
+
},
|
|
6573
|
+
stop: (sessionId) => realtimeVoice.stop(sessionId),
|
|
6574
|
+
end: (sessionId) => realtimeVoice.requestClose(sessionId),
|
|
6575
|
+
status: (sessionId) => realtimeVoice.status(sessionId)
|
|
6576
|
+
}),
|
|
5617
6577
|
onSessionEvent,
|
|
5618
6578
|
onStream,
|
|
5619
6579
|
onApproval,
|
|
@@ -6431,6 +7391,7 @@ var CodexLoginCoordinator = class {
|
|
|
6431
7391
|
return;
|
|
6432
7392
|
}
|
|
6433
7393
|
} catch {}
|
|
7394
|
+
diagnosticLog.error("login", `sign-in failed (${classifyLoginFailure(error)})`, error);
|
|
6434
7395
|
session.view = {
|
|
6435
7396
|
id,
|
|
6436
7397
|
provider: "openai-codex",
|
|
@@ -6866,6 +7827,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
6866
7827
|
};
|
|
6867
7828
|
const noteStreamFailure = (stage, error, source) => {
|
|
6868
7829
|
streamFailure.count += 1;
|
|
7830
|
+
diagnosticLog.error("model", `request failed while ${stage === "prepare" ? "preparing" : stage === "request" ? "sending" : stage === "payload" ? "building the payload" : "streaming"}`, error);
|
|
6869
7831
|
streamFailure.stage = stage;
|
|
6870
7832
|
try {
|
|
6871
7833
|
streamFailure.request = source === void 0 ? void 0 : redactRequest(source);
|
|
@@ -7022,11 +7984,11 @@ const LEVELS = [
|
|
|
7022
7984
|
"max"
|
|
7023
7985
|
];
|
|
7024
7986
|
const DEFAULT_REFRESH_TIMEOUT_MS = 1e4;
|
|
7025
|
-
const record$
|
|
7987
|
+
const record$7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7026
7988
|
const nonEmpty$1 = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
7027
7989
|
const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
7028
7990
|
function upgradeModelId(value) {
|
|
7029
|
-
return (typeof value.upgrade === "string" ? nonEmpty$1(value.upgrade) : record$
|
|
7991
|
+
return (typeof value.upgrade === "string" ? nonEmpty$1(value.upgrade) : record$7(value.upgrade) ? nonEmpty$1(value.upgrade.model) : void 0) ?? (record$7(value.upgrade_info) ? nonEmpty$1(value.upgrade_info.model) : void 0) ?? (record$7(value.upgradeInfo) ? nonEmpty$1(value.upgradeInfo.model) : void 0);
|
|
7030
7992
|
}
|
|
7031
7993
|
function retirementMillis(value) {
|
|
7032
7994
|
for (const source of [
|
|
@@ -7035,7 +7997,7 @@ function retirementMillis(value) {
|
|
|
7035
7997
|
value.upgrade_info,
|
|
7036
7998
|
value.upgradeInfo
|
|
7037
7999
|
]) {
|
|
7038
|
-
if (!record$
|
|
8000
|
+
if (!record$7(source)) continue;
|
|
7039
8001
|
for (const raw of [source.retirement_at, source.retirementAt]) {
|
|
7040
8002
|
const seconds = positiveInteger(raw);
|
|
7041
8003
|
if (seconds !== void 0 && Number.isSafeInteger(seconds * 1e3)) return seconds * 1e3;
|
|
@@ -7047,7 +8009,7 @@ function retirementMillis(value) {
|
|
|
7047
8009
|
}
|
|
7048
8010
|
}
|
|
7049
8011
|
function retirementNotice(value, now = Date.now(), targetDisplayName) {
|
|
7050
|
-
if (!record$
|
|
8012
|
+
if (!record$7(value) || !Number.isFinite(now)) return void 0;
|
|
7051
8013
|
const at = retirementMillis(value);
|
|
7052
8014
|
if (at === void 0 || at <= now) return void 0;
|
|
7053
8015
|
const date = new Date(at);
|
|
@@ -7063,7 +8025,7 @@ function retirementNotice(value, now = Date.now(), targetDisplayName) {
|
|
|
7063
8025
|
};
|
|
7064
8026
|
}
|
|
7065
8027
|
function reasoningMap(levels) {
|
|
7066
|
-
const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$1(record$
|
|
8028
|
+
const supported = new Set((Array.isArray(levels) ? levels : []).map((level) => nonEmpty$1(record$7(level) ? level.effort : void 0)).filter(Boolean));
|
|
7067
8029
|
const map = Object.fromEntries(LEVELS.map((level) => [level, null]));
|
|
7068
8030
|
if (supported.has("none")) map.off = "none";
|
|
7069
8031
|
for (const level of LEVELS.slice(1)) if (supported.has(level)) map[level] = level;
|
|
@@ -7071,6 +8033,24 @@ function reasoningMap(levels) {
|
|
|
7071
8033
|
}
|
|
7072
8034
|
const capabilityNames = (values) => [...new Set(values.filter((value) => typeof value === "string" && /^[a-z][a-z0-9_-]{0,31}$/u.test(value)))].sort().slice(0, 16);
|
|
7073
8035
|
const speedTierIds = (value) => [...Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : [], ...Array.isArray(value.service_tiers) ? value.service_tiers.map((tier) => tier?.id) : []];
|
|
8036
|
+
const HONOURED_TIERS = /* @__PURE__ */ new Set([
|
|
8037
|
+
"priority",
|
|
8038
|
+
"fast",
|
|
8039
|
+
"ultrafast"
|
|
8040
|
+
]);
|
|
8041
|
+
function serviceTierEntries(value) {
|
|
8042
|
+
const found = (Array.isArray(value.service_tiers) ? value.service_tiers : []).flatMap((tier) => record$7(tier) && typeof tier.id === "string" && HONOURED_TIERS.has(tier.id) ? [{
|
|
8043
|
+
id: tier.id,
|
|
8044
|
+
name: nonEmpty$1(tier.name) ?? tier.id,
|
|
8045
|
+
description: nonEmpty$1(tier.description) ?? ""
|
|
8046
|
+
}] : []);
|
|
8047
|
+
for (const id of Array.isArray(value.additional_speed_tiers) ? value.additional_speed_tiers : []) if (typeof id === "string" && HONOURED_TIERS.has(id) && !found.some((tier) => tier.id === id)) found.push({
|
|
8048
|
+
id,
|
|
8049
|
+
name: id === "ultrafast" ? "Ultrafast" : "Fast",
|
|
8050
|
+
description: ""
|
|
8051
|
+
});
|
|
8052
|
+
return found;
|
|
8053
|
+
}
|
|
7074
8054
|
function unsupportedCapabilities(value) {
|
|
7075
8055
|
const reasoning = capabilityNames((value.supported_reasoning_levels ?? []).map((item) => item?.effort)).filter((level) => !["none", ...LEVELS.slice(1)].includes(level));
|
|
7076
8056
|
const inputs = capabilityNames(Array.isArray(value.input_modalities) ? value.input_modalities : []).filter((input) => !["text", "image"].includes(input));
|
|
@@ -7089,7 +8069,7 @@ function unsupportedCapabilities(value) {
|
|
|
7089
8069
|
};
|
|
7090
8070
|
}
|
|
7091
8071
|
function visibleModel(value, displayNames, now) {
|
|
7092
|
-
if (!record$
|
|
8072
|
+
if (!record$7(value)) return void 0;
|
|
7093
8073
|
const id = nonEmpty$1(value.slug);
|
|
7094
8074
|
const reserve = id === "gpt-reserve" && ["list", "hide"].includes(value.visibility);
|
|
7095
8075
|
if (id === void 0 || value.visibility !== "list" && !reserve) return void 0;
|
|
@@ -7124,6 +8104,7 @@ function visibleModel(value, displayNames, now) {
|
|
|
7124
8104
|
"medium",
|
|
7125
8105
|
"high"
|
|
7126
8106
|
].includes(value.default_verbosity) ? value.default_verbosity : void 0,
|
|
8107
|
+
serviceTiers: serviceTierEntries(value),
|
|
7127
8108
|
supportsFast: speedTierIds(value).some((tier) => tier === "fast" || tier === "priority"),
|
|
7128
8109
|
supportsUltrafast: speedTierIds(value).includes("ultrafast")
|
|
7129
8110
|
};
|
|
@@ -7134,22 +8115,22 @@ function visibleModel(value, displayNames, now) {
|
|
|
7134
8115
|
* visibility), with low reasoning when the reviewer supports it.
|
|
7135
8116
|
*/
|
|
7136
8117
|
function officialReviewModel(value, parentId) {
|
|
7137
|
-
if (!record$
|
|
7138
|
-
const parent = value.models.find((model) => record$
|
|
8118
|
+
if (!record$7(value) || !Array.isArray(value.models)) return void 0;
|
|
8119
|
+
const parent = value.models.find((model) => record$7(model) && model.slug === parentId);
|
|
7139
8120
|
const id = nonEmpty$1(parent?.auto_review_model_override) ?? "codex-auto-review";
|
|
7140
|
-
const preset = value.models.find((model) => record$
|
|
8121
|
+
const preset = value.models.find((model) => record$7(model) && model.slug === id);
|
|
7141
8122
|
if (preset === void 0) return void 0;
|
|
7142
|
-
const effort = (Array.isArray(preset.supported_reasoning_levels) ? preset.supported_reasoning_levels : []).some((level) => (record$
|
|
8123
|
+
const effort = (Array.isArray(preset.supported_reasoning_levels) ? preset.supported_reasoning_levels : []).some((level) => (record$7(level) ? level.effort : level) === "low") ? "low" : nonEmpty$1(preset.default_reasoning_level);
|
|
7143
8124
|
return {
|
|
7144
8125
|
id,
|
|
7145
8126
|
...effort === void 0 ? {} : { effort }
|
|
7146
8127
|
};
|
|
7147
8128
|
}
|
|
7148
8129
|
function parseOfficialModelCatalog(value, now = Date.now()) {
|
|
7149
|
-
if (!record$
|
|
8130
|
+
if (!record$7(value) || !Array.isArray(value.models)) throw new Error("Codex returned a malformed model catalog");
|
|
7150
8131
|
const displayNames = /* @__PURE__ */ new Map();
|
|
7151
8132
|
for (const model of value.models) {
|
|
7152
|
-
if (!record$
|
|
8133
|
+
if (!record$7(model)) continue;
|
|
7153
8134
|
const id = nonEmpty$1(model.slug);
|
|
7154
8135
|
const name = nonEmpty$1(model.display_name);
|
|
7155
8136
|
if (id !== void 0 && name !== void 0 && !displayNames.has(id)) displayNames.set(id, name);
|
|
@@ -7312,6 +8293,115 @@ function createOfficialModelCatalog(options = {}) {
|
|
|
7312
8293
|
});
|
|
7313
8294
|
}
|
|
7314
8295
|
//#endregion
|
|
8296
|
+
//#region src/deepseek-account-search.js
|
|
8297
|
+
const DSH_ACCOUNT_SEARCH_PROVIDER_ID = "codex-subscription-dsh";
|
|
8298
|
+
const DEFAULT_BASE_URL = "https://api.deepseek.com/v1/tools";
|
|
8299
|
+
const DEFAULT_COUNT = 10;
|
|
8300
|
+
const MAX_QUESTION = 128;
|
|
8301
|
+
const record$6 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8302
|
+
const missing = (message) => new WebError(message, "WEB_PROVIDER_CREDENTIAL_MISSING");
|
|
8303
|
+
function parseSources(value) {
|
|
8304
|
+
if (!record$6(value) || !Array.isArray(value.results)) throw new WebError("DeepSeek returned an invalid search response", "WEB_PROVIDER_ERROR");
|
|
8305
|
+
return {
|
|
8306
|
+
sources: value.results.flatMap((item) => {
|
|
8307
|
+
if (!record$6(item) || typeof item.url !== "string" || item.url.length === 0) return [];
|
|
8308
|
+
return [{
|
|
8309
|
+
url: item.url,
|
|
8310
|
+
...typeof item.title === "string" && item.title.length > 0 ? { title: item.title } : {},
|
|
8311
|
+
...typeof item.snippet === "string" && item.snippet.length > 0 ? { snippet: item.snippet } : {},
|
|
8312
|
+
...typeof item.date === "string" && item.date.length > 0 ? { publishedAt: item.date } : {}
|
|
8313
|
+
}];
|
|
8314
|
+
}),
|
|
8315
|
+
truncated: false
|
|
8316
|
+
};
|
|
8317
|
+
}
|
|
8318
|
+
/**
|
|
8319
|
+
* Search through DeepSeek's Search API with the signed-in DeepSeek account, for conversations whose
|
|
8320
|
+
* model is not a DeepSeek Account one (DSH itself only hands the account token to those).
|
|
8321
|
+
*/
|
|
8322
|
+
async function searchWithDeepSeekAccount(options, request, signal) {
|
|
8323
|
+
signal?.throwIfAborted();
|
|
8324
|
+
const account = options.resolveAccount?.();
|
|
8325
|
+
if (typeof account?.resolveToken !== "function") throw missing("DeepSeek account sign-in is not available");
|
|
8326
|
+
const config = options.resolveConfig?.() ?? {};
|
|
8327
|
+
const baseURL = typeof config.baseURL === "string" && config.baseURL.length > 0 ? config.baseURL : DEFAULT_BASE_URL;
|
|
8328
|
+
const count = Number.isInteger(config.count) && config.count > 0 ? config.count : DEFAULT_COUNT;
|
|
8329
|
+
let endpoint;
|
|
8330
|
+
try {
|
|
8331
|
+
endpoint = `${new URL(baseURL).href.replace(/\/+$/u, "")}/search`;
|
|
8332
|
+
} catch {
|
|
8333
|
+
throw new WebError("DeepSeek search base URL is invalid", "WEB_PROVIDER_ERROR");
|
|
8334
|
+
}
|
|
8335
|
+
let token;
|
|
8336
|
+
try {
|
|
8337
|
+
token = await account.resolveToken(endpoint);
|
|
8338
|
+
} catch {
|
|
8339
|
+
throw missing("DeepSeek account sign-in could not be read");
|
|
8340
|
+
}
|
|
8341
|
+
if (typeof token !== "string" || token.length === 0) throw missing("DeepSeek account is not signed in");
|
|
8342
|
+
const question = config.sendOriginalQuestion === false ? void 0 : Array.from((options.resolveQuestion?.() ?? "").replace(/\s+/gu, " ").trim()).slice(0, MAX_QUESTION).join("");
|
|
8343
|
+
let response;
|
|
8344
|
+
try {
|
|
8345
|
+
response = await (options.fetch ?? fetch)(endpoint, {
|
|
8346
|
+
method: "POST",
|
|
8347
|
+
redirect: "error",
|
|
8348
|
+
headers: {
|
|
8349
|
+
"x-dsh-auth-token": token,
|
|
8350
|
+
"content-type": "application/json",
|
|
8351
|
+
accept: "application/json",
|
|
8352
|
+
"user-agent": USER_AGENT
|
|
8353
|
+
},
|
|
8354
|
+
body: JSON.stringify({
|
|
8355
|
+
query: request.query,
|
|
8356
|
+
count: Math.min(count, request.maxResults ?? count),
|
|
8357
|
+
need_content: false,
|
|
8358
|
+
...question ? { ori_question: question } : {}
|
|
8359
|
+
}),
|
|
8360
|
+
signal
|
|
8361
|
+
});
|
|
8362
|
+
} catch (error) {
|
|
8363
|
+
if (signal?.aborted || error?.name === "AbortError") throw new WebError("DeepSeek web request aborted", "WEB_ABORTED", { cause: error });
|
|
8364
|
+
throw new WebError("DeepSeek web request failed", "WEB_PROVIDER_ERROR", { cause: error });
|
|
8365
|
+
}
|
|
8366
|
+
if (!response.ok) throw response.status === 401 ? new WebError("DeepSeek rejected the account sign-in used for web search; sign in to DeepSeek again", "WEB_PROVIDER_ERROR") : new WebError(`DeepSeek search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
|
|
8367
|
+
let body;
|
|
8368
|
+
try {
|
|
8369
|
+
body = await response.json();
|
|
8370
|
+
} catch (error) {
|
|
8371
|
+
throw new WebError("DeepSeek returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
|
|
8372
|
+
}
|
|
8373
|
+
return parseSources(body);
|
|
8374
|
+
}
|
|
8375
|
+
/**
|
|
8376
|
+
* The DSH search source with the account fallback: DSH's own provider goes first, and only when it
|
|
8377
|
+
* reports a missing credential (no API key, model not a DeepSeek Account one) the account token is used.
|
|
8378
|
+
*/
|
|
8379
|
+
function createDshAccountSearchProvider(options) {
|
|
8380
|
+
return Object.freeze({
|
|
8381
|
+
id: DSH_ACCOUNT_SEARCH_PROVIDER_ID,
|
|
8382
|
+
available: () => true,
|
|
8383
|
+
async search(request, signal) {
|
|
8384
|
+
const provider = options.resolveDshProvider?.();
|
|
8385
|
+
if (provider === void 0 || provider.id === "codex-subscription-dsh" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
|
|
8386
|
+
try {
|
|
8387
|
+
const result = await provider.search(request, signal);
|
|
8388
|
+
options.report?.(`answered by DSH provider ${provider.id}`);
|
|
8389
|
+
return result;
|
|
8390
|
+
} catch (error) {
|
|
8391
|
+
if (error?.code !== "WEB_PROVIDER_CREDENTIAL_MISSING" || signal?.aborted) throw error;
|
|
8392
|
+
try {
|
|
8393
|
+
const result = await searchWithDeepSeekAccount(options, request, signal);
|
|
8394
|
+
options.report?.("answered by DeepSeek account fallback");
|
|
8395
|
+
return result;
|
|
8396
|
+
} catch (fallback) {
|
|
8397
|
+
options.report?.(`DeepSeek account fallback failed: ${fallback?.message}`);
|
|
8398
|
+
throw fallback?.code === "WEB_PROVIDER_CREDENTIAL_MISSING" ? error : fallback;
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
});
|
|
8403
|
+
}
|
|
8404
|
+
//#endregion
|
|
7315
8405
|
//#region src/codex-search.js
|
|
7316
8406
|
const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
|
|
7317
8407
|
const CODEX_AUTO_SEARCH_PROVIDER_ID = "codex-subscription-auto";
|
|
@@ -7437,16 +8527,43 @@ function createCodexSearchProvider(options) {
|
|
|
7437
8527
|
}
|
|
7438
8528
|
});
|
|
7439
8529
|
}
|
|
7440
|
-
|
|
8530
|
+
const UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
8531
|
+
"WEB_PROVIDER_CREDENTIAL_MISSING",
|
|
8532
|
+
"WEB_PROVIDER_UNAVAILABLE",
|
|
8533
|
+
"WEB_PROVIDER_CONFIGURED_MISSING",
|
|
8534
|
+
"WEB_PROVIDER_CONFIGURED_UNAVAILABLE"
|
|
8535
|
+
]);
|
|
8536
|
+
const unavailable = (error) => typeof error?.code === "string" && UNAVAILABLE_CODES.has(error.code);
|
|
8537
|
+
/** Route each request by its initiating model; when that side has no sign-in or key, use the other one. */
|
|
7441
8538
|
function createCodexAutoSearchProvider(options) {
|
|
8539
|
+
const dsh = () => {
|
|
8540
|
+
const provider = options.resolveDshProvider?.();
|
|
8541
|
+
if (provider === void 0 || provider.id === "codex-subscription-auto" || provider.id === "codex-subscription" || provider.available() !== true) throw new WebError("DSH default search is unavailable", "WEB_PROVIDER_UNAVAILABLE");
|
|
8542
|
+
return provider;
|
|
8543
|
+
};
|
|
8544
|
+
const viaDsh = (request, signal) => {
|
|
8545
|
+
try {
|
|
8546
|
+
return Promise.resolve(dsh().search(request, signal));
|
|
8547
|
+
} catch (error) {
|
|
8548
|
+
return Promise.reject(error);
|
|
8549
|
+
}
|
|
8550
|
+
};
|
|
8551
|
+
const viaCodex = (request, signal) => options.codex.search(request, signal);
|
|
7442
8552
|
return Object.freeze({
|
|
7443
8553
|
id: CODEX_AUTO_SEARCH_PROVIDER_ID,
|
|
7444
8554
|
available: () => true,
|
|
7445
8555
|
async search(request, signal) {
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
8556
|
+
const [first, second] = options.resolveModelProvider?.() === "openai-codex" ? [viaCodex, viaDsh] : [viaDsh, viaCodex];
|
|
8557
|
+
try {
|
|
8558
|
+
return await first(request, signal);
|
|
8559
|
+
} catch (error) {
|
|
8560
|
+
if (!unavailable(error) || signal?.aborted) throw error;
|
|
8561
|
+
try {
|
|
8562
|
+
return await second(request, signal);
|
|
8563
|
+
} catch (fallbackError) {
|
|
8564
|
+
throw unavailable(fallbackError) ? error : fallbackError;
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
7450
8567
|
}
|
|
7451
8568
|
});
|
|
7452
8569
|
}
|
|
@@ -7810,6 +8927,11 @@ const actions = Object.freeze({
|
|
|
7810
8927
|
codex_image_generate: "images",
|
|
7811
8928
|
codex_sketch: "sketch"
|
|
7812
8929
|
});
|
|
8930
|
+
const QUIET = /* @__PURE__ */ new Set([
|
|
8931
|
+
"usage",
|
|
8932
|
+
"storage/status",
|
|
8933
|
+
"preferences/models"
|
|
8934
|
+
]);
|
|
7813
8935
|
function safeOperations(raw) {
|
|
7814
8936
|
if (!Array.isArray(raw?.events)) return void 0;
|
|
7815
8937
|
return {
|
|
@@ -7849,6 +8971,8 @@ function createDiagnosticOperations({ now = Date.now } = {}) {
|
|
|
7849
8971
|
status,
|
|
7850
8972
|
observedAt: now()
|
|
7851
8973
|
});
|
|
8974
|
+
if (status === "failed") diagnosticLog.error(actions[action], `${action} failed`);
|
|
8975
|
+
else if (status === "completed" && !QUIET.has(action)) diagnosticLog.info(actions[action], `${action} completed`);
|
|
7852
8976
|
if (events.length > 32) {
|
|
7853
8977
|
events.shift();
|
|
7854
8978
|
dropped++;
|
|
@@ -7863,12 +8987,21 @@ function createDiagnosticOperations({ now = Date.now } = {}) {
|
|
|
7863
8987
|
return result;
|
|
7864
8988
|
} catch (error) {
|
|
7865
8989
|
record(endpoint, signal?.aborted ? "cancelled" : "failed");
|
|
8990
|
+
if (!signal?.aborted) diagnosticLog.error(actions[endpoint] ?? "rpc", `${endpoint} threw`, error);
|
|
7866
8991
|
throw error;
|
|
7867
8992
|
}
|
|
7868
8993
|
},
|
|
7869
8994
|
tool: (exec, result) => {
|
|
7870
8995
|
if (!["codex_image_generate", "codex_sketch"].includes(exec?.name)) return;
|
|
7871
8996
|
record(exec.name, exec.signal?.aborted ? "cancelled" : result?.isError === true ? "failed" : result?.isError === false ? "completed" : "failed", "tool");
|
|
8997
|
+
if (result?.isError === true && !exec.signal?.aborted) {
|
|
8998
|
+
const text = [
|
|
8999
|
+
result.content,
|
|
9000
|
+
result.output,
|
|
9001
|
+
result.error
|
|
9002
|
+
].flat().map((part) => typeof part === "string" ? part : part?.text).find((part) => typeof part === "string" && part !== "");
|
|
9003
|
+
if (text !== void 0) diagnosticLog.error(actions[exec.name], `${exec.name} said`, text);
|
|
9004
|
+
}
|
|
7872
9005
|
},
|
|
7873
9006
|
snapshot: () => ({
|
|
7874
9007
|
scope: "plugin-process",
|
|
@@ -8141,6 +9274,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
8141
9274
|
account,
|
|
8142
9275
|
login,
|
|
8143
9276
|
requests: collected.requests ?? {},
|
|
9277
|
+
log: diagnosticLog.snapshot(300),
|
|
8144
9278
|
...collected.history ? { requestHistory: collected.history } : {},
|
|
8145
9279
|
...collected.operations ? { operations: collected.operations } : {},
|
|
8146
9280
|
...collected.websocket ? { websocket: collected.websocket } : {},
|
|
@@ -9946,7 +11080,7 @@ async function settlesLive(preferences, patch, signal, { attempts = 8, delayMs =
|
|
|
9946
11080
|
}
|
|
9947
11081
|
return live();
|
|
9948
11082
|
}
|
|
9949
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageReader, planHistoryReader, usageProfileReader, resetCreditService, preferences, defaultModel, runtimeManagement, remoteControl, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
|
|
11083
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageReader, planHistoryReader, usageProfileReader, resetCreditService, preferences, defaultModel, runtimeManagement, remoteControl, desktopVoice, voicePreview, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
|
|
9950
11084
|
return async (endpoint, payload, signal) => {
|
|
9951
11085
|
if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
|
|
9952
11086
|
signal.throwIfAborted();
|
|
@@ -9961,6 +11095,59 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageRe
|
|
|
9961
11095
|
if (error?.code === "unavailable") return publicError("unavailable", endpoint === "default-model/status" ? "The default model is unavailable" : "Could not save the default model");
|
|
9962
11096
|
return publicError("internal", endpoint === "default-model/status" ? "Could not read the default model" : "Could not save the default model");
|
|
9963
11097
|
}
|
|
11098
|
+
if (endpoint === "voice/status") try {
|
|
11099
|
+
signal.throwIfAborted();
|
|
11100
|
+
if (!desktopVoice || typeof payload?.sessionId !== "string") return publicError("unavailable", "Voice is unavailable");
|
|
11101
|
+
return {
|
|
11102
|
+
ok: true,
|
|
11103
|
+
value: desktopVoice.status(payload.sessionId)
|
|
11104
|
+
};
|
|
11105
|
+
} catch (error) {
|
|
11106
|
+
if (signal.aborted) throw error;
|
|
11107
|
+
return publicError("operation-error", "Could not read the voice status");
|
|
11108
|
+
}
|
|
11109
|
+
if (endpoint === "voice/start" || endpoint === "voice/stop") try {
|
|
11110
|
+
signal.throwIfAborted();
|
|
11111
|
+
if (!desktopVoice) return publicError("unavailable", "Voice is unavailable");
|
|
11112
|
+
if (typeof payload?.sessionId !== "string" || payload.sessionId.length === 0 || payload.sessionId.length > 512) return publicError("invalid-input", "Invalid voice request");
|
|
11113
|
+
if (endpoint === "voice/stop") {
|
|
11114
|
+
desktopVoice.stop(payload.sessionId);
|
|
11115
|
+
return {
|
|
11116
|
+
ok: true,
|
|
11117
|
+
value: {}
|
|
11118
|
+
};
|
|
11119
|
+
}
|
|
11120
|
+
const current = preferences?.status?.() ?? {};
|
|
11121
|
+
if (current.voiceCall !== "on") return publicError("unavailable", "Voice is turned off");
|
|
11122
|
+
return {
|
|
11123
|
+
ok: true,
|
|
11124
|
+
value: await desktopVoice.start({
|
|
11125
|
+
sessionId: payload.sessionId,
|
|
11126
|
+
sdp: payload.sdp,
|
|
11127
|
+
voice: current.voiceVoice,
|
|
11128
|
+
language: current.voiceLanguage
|
|
11129
|
+
})
|
|
11130
|
+
};
|
|
11131
|
+
} catch (error) {
|
|
11132
|
+
if (signal.aborted) throw error;
|
|
11133
|
+
return publicError("operation-error", "Could not start the voice call");
|
|
11134
|
+
}
|
|
11135
|
+
if (endpoint === "voice/catalog" || endpoint === "voice/preview") try {
|
|
11136
|
+
signal.throwIfAborted();
|
|
11137
|
+
if (!voicePreview) return publicError("unavailable", "Voice preview is unavailable");
|
|
11138
|
+
if (endpoint === "voice/catalog") return {
|
|
11139
|
+
ok: true,
|
|
11140
|
+
value: { voices: await voicePreview.available() }
|
|
11141
|
+
};
|
|
11142
|
+
const value = await voicePreview.preview(payload?.voice);
|
|
11143
|
+
return value === void 0 ? publicError("not-found", "No sample for this voice") : {
|
|
11144
|
+
ok: true,
|
|
11145
|
+
value
|
|
11146
|
+
};
|
|
11147
|
+
} catch (error) {
|
|
11148
|
+
if (signal.aborted) throw error;
|
|
11149
|
+
return publicError("operation-error", "Could not load the voice sample");
|
|
11150
|
+
}
|
|
9964
11151
|
if ([
|
|
9965
11152
|
"remote/status",
|
|
9966
11153
|
"remote/enable",
|
|
@@ -10226,6 +11413,130 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageRe
|
|
|
10226
11413
|
};
|
|
10227
11414
|
}
|
|
10228
11415
|
//#endregion
|
|
11416
|
+
//#region src/voice-preview.js
|
|
11417
|
+
const BASE = "https://chatgpt.com/backend-api";
|
|
11418
|
+
const CATALOG_TTL_MS = 10 * 6e4;
|
|
11419
|
+
const MAX_CLIP_BYTES = 2 * 1024 * 1024;
|
|
11420
|
+
const PREVIEW_HOSTS = /^https:\/\/[a-z0-9.-]*oaistatic\.com\//u;
|
|
11421
|
+
function createVoicePreview({ fetch, credentials, userAgent, now = Date.now, base = BASE }) {
|
|
11422
|
+
let catalog;
|
|
11423
|
+
const clips = /* @__PURE__ */ new Map();
|
|
11424
|
+
const readCatalog = async () => {
|
|
11425
|
+
if (catalog && now() - catalog.at < CATALOG_TTL_MS) return catalog.voices;
|
|
11426
|
+
const { access, accountId } = await credentials();
|
|
11427
|
+
const response = await fetch(`${base}/settings/voices`, {
|
|
11428
|
+
headers: {
|
|
11429
|
+
authorization: `Bearer ${access}`,
|
|
11430
|
+
"chatgpt-account-id": accountId,
|
|
11431
|
+
accept: "application/json",
|
|
11432
|
+
"user-agent": userAgent
|
|
11433
|
+
},
|
|
11434
|
+
signal: AbortSignal.timeout(15e3)
|
|
11435
|
+
});
|
|
11436
|
+
if (!response.ok) throw new Error(`voice catalog HTTP ${response.status}`);
|
|
11437
|
+
const value = await response.json();
|
|
11438
|
+
const voices = (Array.isArray(value?.voices) ? value.voices : []).filter((entry) => typeof entry?.name === "string" && typeof entry?.preview_url === "string" && PREVIEW_HOSTS.test(entry.preview_url));
|
|
11439
|
+
catalog = {
|
|
11440
|
+
at: now(),
|
|
11441
|
+
voices
|
|
11442
|
+
};
|
|
11443
|
+
return voices;
|
|
11444
|
+
};
|
|
11445
|
+
return {
|
|
11446
|
+
/** Which voices ChatGPT's own catalog offers right now (lower-case names), so the settings can mark the others unavailable. */
|
|
11447
|
+
async available() {
|
|
11448
|
+
return (await readCatalog()).map((entry) => entry.name.toLowerCase());
|
|
11449
|
+
},
|
|
11450
|
+
/** The clip of one voice (by its Codex name, e.g. "spruce") as a data URL, with the catalog's description. */
|
|
11451
|
+
async preview(voice) {
|
|
11452
|
+
if (typeof voice !== "string" || !/^[a-z]{2,16}$/u.test(voice)) return void 0;
|
|
11453
|
+
if (clips.has(voice)) return clips.get(voice);
|
|
11454
|
+
const entry = (await readCatalog()).find((item) => item.name.toLowerCase() === voice || item.voice === voice);
|
|
11455
|
+
if (!entry) return void 0;
|
|
11456
|
+
const response = await fetch(entry.preview_url, { signal: AbortSignal.timeout(15e3) });
|
|
11457
|
+
if (!response.ok) throw new Error(`voice sample HTTP ${response.status}`);
|
|
11458
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
11459
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_CLIP_BYTES) throw new Error("voice sample has an unexpected size");
|
|
11460
|
+
const type = /\.mp3(?:$|\?)/u.test(entry.preview_url) ? "audio/mpeg" : "audio/mp4";
|
|
11461
|
+
const result = {
|
|
11462
|
+
name: entry.name,
|
|
11463
|
+
description: typeof entry.description === "string" ? entry.description.slice(0, 120) : "",
|
|
11464
|
+
accent: typeof entry.accent === "string" ? entry.accent.slice(0, 60) : "",
|
|
11465
|
+
audio: `data:${type};base64,${bytes.toString("base64")}`
|
|
11466
|
+
};
|
|
11467
|
+
if (clips.size < 40) clips.set(voice, result);
|
|
11468
|
+
return result;
|
|
11469
|
+
}
|
|
11470
|
+
};
|
|
11471
|
+
}
|
|
11472
|
+
//#endregion
|
|
11473
|
+
//#region src/voice-tools.js
|
|
11474
|
+
/**
|
|
11475
|
+
* The one tool the agent gets while a voice call is running, like Codex's end_realtime_voice_call: the voice model hands
|
|
11476
|
+
* "the user said goodbye" over as a request, and the agent hangs up. The voice model itself has no tools.
|
|
11477
|
+
*/
|
|
11478
|
+
function createEndVoiceCallTool({ end, isActive }) {
|
|
11479
|
+
return defineTool({
|
|
11480
|
+
name: "end_voice_call",
|
|
11481
|
+
description: "End the current voice call. The user is talking to you through a live voice call, so a message that is or ends with a farewell (bye, goodbye, 拜拜, 再见, I am leaving, 先走了) or asks to end the call is a request to end it: call this tool, then say a short goodbye. The words may be rough or have a little more after them. Stopping work, stopping speech or pausing does not end a call. Never call it from a text conversation.",
|
|
11482
|
+
parameters: {},
|
|
11483
|
+
isConcurrencySafe: () => false,
|
|
11484
|
+
async execute(_args, exec) {
|
|
11485
|
+
const sessionId = exec.agent?.id;
|
|
11486
|
+
if (typeof sessionId !== "string" || !isActive(sessionId)) return {
|
|
11487
|
+
ended: false,
|
|
11488
|
+
message: "There is no voice call running in this conversation."
|
|
11489
|
+
};
|
|
11490
|
+
end(sessionId);
|
|
11491
|
+
return {
|
|
11492
|
+
ended: true,
|
|
11493
|
+
message: "The voice call is ending; it closes once the voice has finished speaking. Say a short goodbye."
|
|
11494
|
+
};
|
|
11495
|
+
},
|
|
11496
|
+
output: {
|
|
11497
|
+
schema: {
|
|
11498
|
+
type: "object",
|
|
11499
|
+
additionalProperties: true
|
|
11500
|
+
},
|
|
11501
|
+
render: (_args, value) => [{
|
|
11502
|
+
type: "text",
|
|
11503
|
+
text: value.message
|
|
11504
|
+
}]
|
|
11505
|
+
}
|
|
11506
|
+
});
|
|
11507
|
+
}
|
|
11508
|
+
/** The desktop voice object, with the end-call tool registered for as long as at least one call is open. */
|
|
11509
|
+
function withEndCallTool(desktopVoice, register) {
|
|
11510
|
+
if (!desktopVoice) return desktopVoice;
|
|
11511
|
+
const open = /* @__PURE__ */ new Set();
|
|
11512
|
+
let dispose;
|
|
11513
|
+
const release = (sessionId) => {
|
|
11514
|
+
open.delete(sessionId);
|
|
11515
|
+
if (open.size === 0 && dispose) {
|
|
11516
|
+
dispose();
|
|
11517
|
+
dispose = void 0;
|
|
11518
|
+
}
|
|
11519
|
+
};
|
|
11520
|
+
return Object.freeze({
|
|
11521
|
+
async start(params) {
|
|
11522
|
+
const result = await desktopVoice.start(params);
|
|
11523
|
+
open.add(params.sessionId);
|
|
11524
|
+
dispose ??= register(createEndVoiceCallTool({
|
|
11525
|
+
end: (id) => desktopVoice.end(id),
|
|
11526
|
+
isActive: (id) => desktopVoice.status(id).active
|
|
11527
|
+
}));
|
|
11528
|
+
return result;
|
|
11529
|
+
},
|
|
11530
|
+
stop(sessionId) {
|
|
11531
|
+
const result = desktopVoice.stop(sessionId);
|
|
11532
|
+
release(sessionId);
|
|
11533
|
+
return result;
|
|
11534
|
+
},
|
|
11535
|
+
end: (sessionId) => desktopVoice.end(sessionId),
|
|
11536
|
+
status: (sessionId) => desktopVoice.status(sessionId)
|
|
11537
|
+
});
|
|
11538
|
+
}
|
|
11539
|
+
//#endregion
|
|
10229
11540
|
//#region src/index.js
|
|
10230
11541
|
const z = createRequire(import.meta.resolve("@deepseek-ai/dsh-settings"))("@deepseek-ai/schemastery");
|
|
10231
11542
|
const name = "codex-subscription";
|
|
@@ -10251,25 +11562,28 @@ const REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
|
|
|
10251
11562
|
function createSearchProviderSwitcher(loader) {
|
|
10252
11563
|
const webEntry = () => [...loader.entries()].find((entry) => entry.options?.id === WEB_ENTRY_ID);
|
|
10253
11564
|
const dshProviderId = () => {
|
|
10254
|
-
const
|
|
10255
|
-
return typeof
|
|
10256
|
-
};
|
|
11565
|
+
const configured = webEntry()?.options?.config?.searchProvider;
|
|
11566
|
+
return typeof configured === "string" && configured.length > 0 ? configured : DSH_SEARCH_PROVIDER_FALLBACK;
|
|
11567
|
+
};
|
|
11568
|
+
const current = () => {
|
|
11569
|
+
const value = webEntry()?.fiber?.config?.searchProvider;
|
|
11570
|
+
return typeof value === "string" ? value : value?.get?.();
|
|
11571
|
+
};
|
|
11572
|
+
const writeProvider = async (provider) => {
|
|
11573
|
+
const entry = webEntry();
|
|
11574
|
+
const fiber = entry?.fiber;
|
|
11575
|
+
if (entry === void 0 || fiber === void 0 || typeof fiber.update !== "function") throw new Error("DSH web runtime is unavailable");
|
|
11576
|
+
if (current() === provider) return;
|
|
11577
|
+
await fiber.update({
|
|
11578
|
+
...entry.options?.config ?? {},
|
|
11579
|
+
searchProvider: provider
|
|
11580
|
+
}, true);
|
|
11581
|
+
};
|
|
11582
|
+
const select = (selection) => writeProvider(selection === "codex" ? CODEX_SEARCH_PROVIDER_ID : selection === "auto" ? CODEX_AUTO_SEARCH_PROVIDER_ID : DSH_ACCOUNT_SEARCH_PROVIDER_ID);
|
|
10257
11583
|
return Object.freeze({
|
|
10258
11584
|
dshProviderId,
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
const fiber = entry?.fiber;
|
|
10262
|
-
if (entry === void 0 || fiber === void 0 || typeof fiber.update !== "function") throw new Error("DSH web runtime is unavailable");
|
|
10263
|
-
const baseConfig = entry.options?.config ?? {};
|
|
10264
|
-
const currentConfig = fiber.config ?? baseConfig;
|
|
10265
|
-
const dshProvider = dshProviderId();
|
|
10266
|
-
const provider = selection === "codex" ? CODEX_SEARCH_PROVIDER_ID : selection === "auto" ? CODEX_AUTO_SEARCH_PROVIDER_ID : dshProvider;
|
|
10267
|
-
if (currentConfig.searchProvider === provider) return;
|
|
10268
|
-
await fiber.update({
|
|
10269
|
-
...currentConfig,
|
|
10270
|
-
searchProvider: provider
|
|
10271
|
-
}, true);
|
|
10272
|
-
}
|
|
11585
|
+
current,
|
|
11586
|
+
select
|
|
10273
11587
|
});
|
|
10274
11588
|
}
|
|
10275
11589
|
const settingsFields = {
|
|
@@ -10299,6 +11613,9 @@ const Config = z.object(Object.fromEntries(Object.entries(settingsFields).map(([
|
|
|
10299
11613
|
function apply(ctx, config = {}) {
|
|
10300
11614
|
const settings = createSettingsAdapter(ctx, config);
|
|
10301
11615
|
const searchProvider = createSearchProviderSwitcher(ctx.loader);
|
|
11616
|
+
if (!process.env.NODE_TEST_CONTEXT) diagnosticLog.attach(dshHomePath("state", "codex-subscription", "diagnostic-log.jsonl"));
|
|
11617
|
+
diagnosticLog.info("plugin", `loaded ${PACKAGE_VERSION}`, `DSH ${hostDshVersion() ?? "unknown"}, node ${process.version}, ${process.platform}`);
|
|
11618
|
+
setAuthFailureLogger((message, detail) => diagnosticLog.error("auth", message, detail));
|
|
10302
11619
|
const network = createCodexNetworkTransport();
|
|
10303
11620
|
const diagnosticOperations = createDiagnosticOperations();
|
|
10304
11621
|
ctx.effect(() => ctx.on?.("tools/result", (exec, result) => diagnosticOperations.tool(exec, result)), "codex-subscription: operation evidence");
|
|
@@ -10385,6 +11702,9 @@ function apply(ctx, config = {}) {
|
|
|
10385
11702
|
[OUTPUT_VERBOSITY_FIELD]: normalizeOutputVerbosity(settings.get()[OUTPUT_VERBOSITY_FIELD]),
|
|
10386
11703
|
[INPUT_IMAGE_DETAIL_FIELD]: normalizeInputImageDetail(settings.get()[INPUT_IMAGE_DETAIL_FIELD]),
|
|
10387
11704
|
[REASONING_SUMMARY_FIELD]: normalizeReasoningSummary(settings.get()[REASONING_SUMMARY_FIELD]),
|
|
11705
|
+
[VOICE_CALL_FIELD]: normalizeVoiceCall(settings.get()[VOICE_CALL_FIELD]),
|
|
11706
|
+
[VOICE_VOICE_FIELD]: normalizeVoiceVoice(settings.get()[VOICE_VOICE_FIELD]),
|
|
11707
|
+
[VOICE_LANGUAGE_FIELD]: normalizeVoiceLanguage(settings.get()[VOICE_LANGUAGE_FIELD]),
|
|
10388
11708
|
[RETRY_POLICY_FIELD]: normalizeRetryPolicy(settings.get()[RETRY_POLICY_FIELD]),
|
|
10389
11709
|
[CONNECTORS_FIELD]: normalizeConnectorMode(settings.get()[CONNECTORS_FIELD]),
|
|
10390
11710
|
[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]: normalizeStreamIdleTimeoutMinutes(settings.get()[STREAM_IDLE_TIMEOUT_MINUTES_FIELD]),
|
|
@@ -10580,18 +11900,43 @@ function apply(ctx, config = {}) {
|
|
|
10580
11900
|
resolveSessionId: () => currentAgent()?.session.id,
|
|
10581
11901
|
fetch: (input, init) => network.fetch("search", input, init)
|
|
10582
11902
|
});
|
|
10583
|
-
|
|
11903
|
+
const searchTrace = (route) => diagnosticLog.info("search", `web search: ${route}`);
|
|
11904
|
+
const tracedCodexSearch = Object.freeze({
|
|
11905
|
+
id: codexSearch.id,
|
|
11906
|
+
available: () => codexSearch.available(),
|
|
11907
|
+
async search(request, signal) {
|
|
11908
|
+
const result = await codexSearch.search(request, signal);
|
|
11909
|
+
searchTrace("answered by Codex subscription");
|
|
11910
|
+
return result;
|
|
11911
|
+
}
|
|
11912
|
+
});
|
|
11913
|
+
ctx.web.registerSearchProvider(tracedCodexSearch);
|
|
11914
|
+
const dshSearch = createDshAccountSearchProvider({
|
|
11915
|
+
report: searchTrace,
|
|
11916
|
+
resolveDshProvider: () => ctx.web.searchProviders?.get(searchProvider.dshProviderId()),
|
|
11917
|
+
resolveAccount: () => ctx.get?.("deepseekAccount"),
|
|
11918
|
+
resolveConfig: () => {
|
|
11919
|
+
const config = [...ctx.loader.entries()].find((entry) => entry.options?.id === "web-deepseek-api")?.options?.config;
|
|
11920
|
+
return config !== null && typeof config === "object" ? config : {};
|
|
11921
|
+
},
|
|
11922
|
+
resolveQuestion: () => {
|
|
11923
|
+
return (currentAgent()?.session.deriveMessages?.() ?? []).findLast((message) => message.role === "user" && message.source?.kind === "user")?.content?.flatMap((block) => block.type === "text" ? [block.text] : []).join(" ");
|
|
11924
|
+
}
|
|
11925
|
+
});
|
|
11926
|
+
ctx.web.registerSearchProvider(dshSearch);
|
|
10584
11927
|
ctx.web.registerSearchProvider(createCodexAutoSearchProvider({
|
|
10585
|
-
codex:
|
|
11928
|
+
codex: tracedCodexSearch,
|
|
10586
11929
|
resolveModelProvider: () => currentAgent()?.session.requestContext?.()?.provider,
|
|
10587
|
-
resolveDshProvider: () =>
|
|
11930
|
+
resolveDshProvider: () => dshSearch
|
|
10588
11931
|
}));
|
|
10589
11932
|
ctx.effect(() => {
|
|
10590
11933
|
const select = async (value) => {
|
|
10591
11934
|
try {
|
|
10592
11935
|
await searchProvider.select(value[SEARCH_PROVIDER_FIELD]);
|
|
11936
|
+
diagnosticLog.info("search", `web search source is ${searchProvider.current()}`);
|
|
10593
11937
|
} catch (error) {
|
|
10594
11938
|
ctx.logger?.warn?.("could not select the configured web search provider: %s", error.message);
|
|
11939
|
+
diagnosticLog.warn("search", "could not select the web search provider", error.message);
|
|
10595
11940
|
}
|
|
10596
11941
|
};
|
|
10597
11942
|
select(settings.get());
|
|
@@ -10619,12 +11964,26 @@ function apply(ctx, config = {}) {
|
|
|
10619
11964
|
skills: () => ctx.get?.("skills"),
|
|
10620
11965
|
commands: () => ctx.get?.("commands"),
|
|
10621
11966
|
llm: () => ctx.llm,
|
|
11967
|
+
speed: {
|
|
11968
|
+
tiers: (provider, model) => provider === PROVIDER ? modelCatalog.metadata(model)?.serviceTiers ?? [] : [],
|
|
11969
|
+
current: () => settings.get()[SPEED_MODE_FIELD],
|
|
11970
|
+
set: (mode) => settings.update({ [SPEED_MODE_FIELD]: mode })
|
|
11971
|
+
},
|
|
10622
11972
|
trace: (entry) => {
|
|
10623
11973
|
try {
|
|
10624
11974
|
remoteControl.host.note(entry);
|
|
10625
11975
|
} catch {}
|
|
10626
11976
|
},
|
|
10627
|
-
userAgent: USER_AGENT
|
|
11977
|
+
userAgent: USER_AGENT,
|
|
11978
|
+
configFile: dshHomePath("state", "codex-subscription", "remote-config.json"),
|
|
11979
|
+
voice: {
|
|
11980
|
+
WebSocket,
|
|
11981
|
+
fetch: (input, init) => network.fetch("remote", input, init),
|
|
11982
|
+
credentials: (signal) => readSubscriptionCredentials(resolveAuth, (options) => store.read(PROVIDER, {
|
|
11983
|
+
...options,
|
|
11984
|
+
signal
|
|
11985
|
+
}), signal)
|
|
11986
|
+
}
|
|
10628
11987
|
});
|
|
10629
11988
|
const remoteControl = createRemoteControl({
|
|
10630
11989
|
credentials: (signal) => readSubscriptionCredentials(resolveAuth, (options) => store.read(PROVIDER, {
|
|
@@ -10634,6 +11993,7 @@ function apply(ctx, config = {}) {
|
|
|
10634
11993
|
WebSocket,
|
|
10635
11994
|
fetch: (input, init) => network.fetch("remote", input, init),
|
|
10636
11995
|
stateFile: dshHomePath("state", "codex-subscription", "remote-control.json"),
|
|
11996
|
+
debugFile: dshHomePath("state", "codex-subscription", "remote-debug.jsonl"),
|
|
10637
11997
|
hostName: `DSH (${hostname()})`,
|
|
10638
11998
|
userAgent: USER_AGENT,
|
|
10639
11999
|
base: process.env.DSH_CODEX_REMOTE_BASE || void 0,
|
|
@@ -10792,6 +12152,15 @@ function apply(ctx, config = {}) {
|
|
|
10792
12152
|
defaultModel,
|
|
10793
12153
|
runtimeManagement,
|
|
10794
12154
|
remoteControl,
|
|
12155
|
+
desktopVoice: withEndCallTool(remoteBridge.desktopVoice, (tool) => ctx.tools.register(tool)),
|
|
12156
|
+
voicePreview: createVoicePreview({
|
|
12157
|
+
fetch: (input, init) => network.fetch("remote", input, init),
|
|
12158
|
+
credentials: (signal) => readSubscriptionCredentials(resolveAuth, (options) => store.read(PROVIDER, {
|
|
12159
|
+
...options,
|
|
12160
|
+
signal
|
|
12161
|
+
}), signal),
|
|
12162
|
+
userAgent: USER_AGENT
|
|
12163
|
+
}),
|
|
10795
12164
|
onAccountChanged: () => {
|
|
10796
12165
|
connectorsClient.clear();
|
|
10797
12166
|
usageProfileReader.clear();
|