dsh-plugin-subscriptions 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/lib/auth/rpc.d.ts +14 -0
- package/lib/auth/rpc.js +18 -0
- package/lib/client/VideoGenerateToolview.d.ts +42 -0
- package/lib/client/VideoGenerateToolview.js +178 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +10 -0
- package/lib/client/locales.d.ts +6 -0
- package/lib/client/locales.js +6 -0
- package/lib/client.js +282 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +552 -43
- package/lib/providers/catalog-store.d.ts +37 -0
- package/lib/providers/catalog-store.js +167 -0
- package/lib/providers/codex.d.ts +18 -3
- package/lib/providers/codex.js +60 -14
- package/lib/providers/common.d.ts +47 -5
- package/lib/providers/common.js +74 -11
- package/lib/providers/grok.d.ts +14 -1
- package/lib/providers/grok.js +25 -8
- package/lib/tools/video-generate.d.ts +89 -0
- package/lib/tools/video-generate.js +255 -0
- package/package.json +8 -6
package/lib/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import z from "@deepseek-ai/schemastery";
|
|
|
2
2
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, createUserMessage, errorChain, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
-
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
6
5
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
6
|
import { basename, dirname, join } from "node:path";
|
|
7
|
+
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
8
8
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
10
|
|
|
@@ -371,6 +371,8 @@ const IMAGE_MEDIA_TYPES = [
|
|
|
371
371
|
"image/webp",
|
|
372
372
|
"image/gif"
|
|
373
373
|
];
|
|
374
|
+
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
375
|
+
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
374
376
|
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
375
377
|
var BadRequest = class extends Error {};
|
|
376
378
|
function ok(value) {
|
|
@@ -436,6 +438,17 @@ function readImageRef(payload) {
|
|
|
436
438
|
...name$1 === void 0 ? {} : { name: name$1 }
|
|
437
439
|
};
|
|
438
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Validate the `video` endpoint's payload into a bare file name. Rejecting
|
|
443
|
+
* anything with a path separator (the pattern allows none) pins every read
|
|
444
|
+
* inside the plugin's videos directory.
|
|
445
|
+
*/
|
|
446
|
+
function readVideoName(payload) {
|
|
447
|
+
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
448
|
+
const name$1 = payload.name;
|
|
449
|
+
if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
|
|
450
|
+
return name$1;
|
|
451
|
+
}
|
|
439
452
|
async function dispatch(controller, endpoint, payload, signal) {
|
|
440
453
|
switch (endpoint) {
|
|
441
454
|
case "status": {
|
|
@@ -456,6 +469,7 @@ async function dispatch(controller, endpoint, payload, signal) {
|
|
|
456
469
|
return ok({ ok: true });
|
|
457
470
|
case "usage": return ok(await controller.usage(readProvider(payload), signal));
|
|
458
471
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
472
|
+
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
459
473
|
default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
460
474
|
}
|
|
461
475
|
}
|
|
@@ -678,14 +692,26 @@ var TokenManager = class {
|
|
|
678
692
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
679
693
|
const DISCOVERY_TTL_MS = 5 * 6e4;
|
|
680
694
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
695
|
+
* Cache for one provider's discovered model catalog. The TTL only decides
|
|
696
|
+
* when to REFRESH; it never makes the cache forget: capability metadata
|
|
697
|
+
* (reasoning efforts) must stay stable for a session that selected an effort,
|
|
698
|
+
* or mid-conversation calls fail UNSUPPORTED_REASONING_EFFORT the moment the
|
|
699
|
+
* cache goes stale. `listModels` awaits freshness via {@link get};
|
|
700
|
+
* `resolveModel` uses {@link resolve}, which serves the last-known catalog
|
|
701
|
+
* while a stale entry refreshes in the background, and only awaits the fetch
|
|
702
|
+
* when nothing is known yet. An optional {@link CatalogPersistence} seeds the
|
|
703
|
+
* last-known state across restarts and receives every successful fetch. A 401
|
|
704
|
+
* during a fetch must call {@link invalidate}.
|
|
685
705
|
*/
|
|
686
706
|
var ModelCatalogCache = class {
|
|
687
707
|
entry;
|
|
688
|
-
|
|
708
|
+
inflight;
|
|
709
|
+
/** Settles once the persisted snapshot (when any) has been considered. */
|
|
710
|
+
seeded;
|
|
711
|
+
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
712
|
+
seedDisabled = false;
|
|
713
|
+
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
714
|
+
this.persistence = persistence;
|
|
689
715
|
this.ttlMs = ttlMs;
|
|
690
716
|
}
|
|
691
717
|
/**
|
|
@@ -696,27 +722,200 @@ var ModelCatalogCache = class {
|
|
|
696
722
|
if (this.entry === void 0 || Date.now() - this.entry.at >= this.ttlMs) return void 0;
|
|
697
723
|
return this.entry.models;
|
|
698
724
|
}
|
|
725
|
+
/** Load the persisted snapshot once; a fetch or invalidate that landed first wins. */
|
|
726
|
+
ensureSeeded() {
|
|
727
|
+
if (this.persistence === void 0) return Promise.resolve();
|
|
728
|
+
this.seeded ??= this.persistence.load().then((snapshot) => {
|
|
729
|
+
if (snapshot !== void 0 && this.entry === void 0 && !this.seedDisabled) this.entry = snapshot;
|
|
730
|
+
}, () => void 0);
|
|
731
|
+
return this.seeded;
|
|
732
|
+
}
|
|
733
|
+
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
734
|
+
refresh(fetcher) {
|
|
735
|
+
this.inflight ??= fetcher().then((models) => {
|
|
736
|
+
const snapshot = {
|
|
737
|
+
at: Date.now(),
|
|
738
|
+
models
|
|
739
|
+
};
|
|
740
|
+
this.entry = snapshot;
|
|
741
|
+
this.persistence?.save(snapshot).catch(() => void 0);
|
|
742
|
+
return models;
|
|
743
|
+
}).finally(() => {
|
|
744
|
+
this.inflight = void 0;
|
|
745
|
+
});
|
|
746
|
+
return this.inflight;
|
|
747
|
+
}
|
|
699
748
|
/**
|
|
700
749
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
701
750
|
* @param fetcher - performs the provider's model-list request.
|
|
702
751
|
* @returns the discovered models.
|
|
752
|
+
* @throws the fetcher's failure (the `listModels` caller warns and falls back).
|
|
703
753
|
*/
|
|
704
754
|
async get(fetcher) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
755
|
+
await this.ensureSeeded();
|
|
756
|
+
return this.cached() ?? this.refresh(fetcher);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The models for capability resolution. A fresh cache answers directly; a
|
|
760
|
+
* stale one answers immediately from the last-known catalog while a
|
|
761
|
+
* background refresh runs (a mid-conversation `resolveModel` must neither
|
|
762
|
+
* block on nor fail with the network); a cold cache awaits one fetch.
|
|
763
|
+
* @param fetcher - performs the provider's model-list request.
|
|
764
|
+
* @returns the models, or `undefined` when nothing is known (the caller
|
|
765
|
+
* falls back to its static metadata). Never throws.
|
|
766
|
+
*/
|
|
767
|
+
async resolve(fetcher) {
|
|
768
|
+
await this.ensureSeeded();
|
|
769
|
+
const fresh = this.cached();
|
|
770
|
+
if (fresh !== void 0) return fresh;
|
|
771
|
+
const known = this.entry?.models;
|
|
772
|
+
if (known !== void 0) {
|
|
773
|
+
this.refresh(fetcher).catch(() => void 0);
|
|
774
|
+
return known;
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
return await this.refresh(fetcher);
|
|
778
|
+
} catch {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
713
781
|
}
|
|
714
782
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
715
783
|
invalidate() {
|
|
716
784
|
this.entry = void 0;
|
|
785
|
+
this.seedDisabled = true;
|
|
786
|
+
this.persistence?.clear().catch(() => void 0);
|
|
717
787
|
}
|
|
718
788
|
};
|
|
719
789
|
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/providers/catalog-store.ts
|
|
792
|
+
/**
|
|
793
|
+
* Absolute path of the catalog store file.
|
|
794
|
+
* @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
|
|
795
|
+
*/
|
|
796
|
+
function modelsFilePath() {
|
|
797
|
+
return dshHomePath("plugins", "subscriptions", "models.json");
|
|
798
|
+
}
|
|
799
|
+
/** Validate one persisted reasoning block, or undefined when malformed. */
|
|
800
|
+
function sanitizeReasoning(value) {
|
|
801
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
802
|
+
const raw = value;
|
|
803
|
+
if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
|
|
804
|
+
const seen = /* @__PURE__ */ new Set();
|
|
805
|
+
const efforts = [];
|
|
806
|
+
for (const entry of raw.efforts) {
|
|
807
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
808
|
+
const effort = entry;
|
|
809
|
+
if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) return void 0;
|
|
810
|
+
seen.add(effort.id);
|
|
811
|
+
efforts.push({
|
|
812
|
+
id: ReasoningEffortId(effort.id),
|
|
813
|
+
name: effort.name,
|
|
814
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
|
|
818
|
+
return {
|
|
819
|
+
efforts,
|
|
820
|
+
...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/** Validate one persisted model, or undefined when malformed. */
|
|
824
|
+
function sanitizeModel(value) {
|
|
825
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
826
|
+
const raw = value;
|
|
827
|
+
if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
|
|
828
|
+
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
829
|
+
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
830
|
+
return {
|
|
831
|
+
id: raw.id,
|
|
832
|
+
name: raw.name,
|
|
833
|
+
...raw.description === void 0 ? {} : { description: raw.description },
|
|
834
|
+
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
835
|
+
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
836
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Validate one persisted snapshot. Strict: any malformed field drops the
|
|
841
|
+
* whole snapshot rather than repairing it — the next successful discovery
|
|
842
|
+
* rewrites the entry anyway.
|
|
843
|
+
* @param value - the raw per-provider file entry.
|
|
844
|
+
* @returns the validated snapshot, or undefined when unusable.
|
|
845
|
+
*/
|
|
846
|
+
function sanitizeSnapshot(value) {
|
|
847
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
848
|
+
const raw = value;
|
|
849
|
+
if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
|
|
850
|
+
if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
|
|
851
|
+
const seen = /* @__PURE__ */ new Set();
|
|
852
|
+
const models = [];
|
|
853
|
+
for (const entry of raw.models) {
|
|
854
|
+
const model = sanitizeModel(entry);
|
|
855
|
+
if (model === void 0 || seen.has(model.id)) return void 0;
|
|
856
|
+
seen.add(model.id);
|
|
857
|
+
models.push(model);
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
at: raw.at,
|
|
861
|
+
models
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
/** Read the whole file; missing or unparsable reads as an empty cache. */
|
|
865
|
+
async function readCatalogFile(path) {
|
|
866
|
+
let text;
|
|
867
|
+
try {
|
|
868
|
+
text = await readFile(path, "utf8");
|
|
869
|
+
} catch {
|
|
870
|
+
return {};
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(text);
|
|
874
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
875
|
+
return parsed;
|
|
876
|
+
} catch {
|
|
877
|
+
return {};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/** Persist the whole file atomically (tmp file + rename). */
|
|
881
|
+
async function writeCatalogFile(store, path) {
|
|
882
|
+
await mkdir(dirname(path), { recursive: true });
|
|
883
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
884
|
+
try {
|
|
885
|
+
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
886
|
+
await rename(tmp, path);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
await rm(tmp, { force: true });
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Build the durable half of one provider's catalog cache over the shared
|
|
894
|
+
* models.json file (concurrent writers are last-writer-wins, acceptable for
|
|
895
|
+
* a cache).
|
|
896
|
+
* @param provider - the provider route keying the file entry.
|
|
897
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
898
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
899
|
+
*/
|
|
900
|
+
function catalogStore(provider, path = modelsFilePath()) {
|
|
901
|
+
return {
|
|
902
|
+
async load() {
|
|
903
|
+
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
904
|
+
},
|
|
905
|
+
async save(snapshot) {
|
|
906
|
+
const store = await readCatalogFile(path);
|
|
907
|
+
store[provider] = snapshot;
|
|
908
|
+
await writeCatalogFile(store, path);
|
|
909
|
+
},
|
|
910
|
+
async clear() {
|
|
911
|
+
const store = await readCatalogFile(path);
|
|
912
|
+
if (store[provider] === void 0) return;
|
|
913
|
+
delete store[provider];
|
|
914
|
+
await writeCatalogFile(store, path);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
720
919
|
//#endregion
|
|
721
920
|
//#region src/auth/jwt.ts
|
|
722
921
|
/** Minimal JWT payload decoding for claims extraction (no signature verification). */
|
|
@@ -1325,8 +1524,28 @@ function isCodexPermanentRefreshError(error) {
|
|
|
1325
1524
|
return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
1326
1525
|
}
|
|
1327
1526
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
1527
|
+
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
1528
|
+
const SESSION_WINDOW_SECONDS = 300 * 60;
|
|
1529
|
+
const WEEKLY_WINDOW_SECONDS = 10080 * 60;
|
|
1530
|
+
/** Whether a reported duration approximately matches the expected window length. */
|
|
1531
|
+
function matchesWindow(seconds, expected) {
|
|
1532
|
+
return seconds >= expected * .95 && seconds <= expected * 1.05;
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Classify a wham/usage window by its reported duration. The backend has been
|
|
1536
|
+
* observed to place the weekly lane in `primary_window` with no secondary
|
|
1537
|
+
* window, so slot position alone is unreliable; the caller's positional
|
|
1538
|
+
* fallback applies only when the duration is absent.
|
|
1539
|
+
*/
|
|
1540
|
+
function codexWindowKind(window, fallback) {
|
|
1541
|
+
const seconds = window.limit_window_seconds;
|
|
1542
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) return fallback;
|
|
1543
|
+
if (matchesWindow(seconds, SESSION_WINDOW_SECONDS)) return "session";
|
|
1544
|
+
if (matchesWindow(seconds, WEEKLY_WINDOW_SECONDS)) return "weekly";
|
|
1545
|
+
return "other";
|
|
1546
|
+
}
|
|
1328
1547
|
/** Map one wham/usage window into a {@link UsageWindow}; undefined when unusable. */
|
|
1329
|
-
function codexUsageWindow(value,
|
|
1548
|
+
function codexUsageWindow(value, fallbackKind) {
|
|
1330
1549
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1331
1550
|
const window = value;
|
|
1332
1551
|
if (typeof window.used_percent !== "number" || !Number.isFinite(window.used_percent)) return void 0;
|
|
@@ -1334,7 +1553,7 @@ function codexUsageWindow(value, kind) {
|
|
|
1334
1553
|
if (typeof window.reset_at === "number" && window.reset_at > 0) resetsAt = window.reset_at * 1e3;
|
|
1335
1554
|
else if (typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0) resetsAt = Date.now() + window.reset_after_seconds * 1e3;
|
|
1336
1555
|
return {
|
|
1337
|
-
kind,
|
|
1556
|
+
kind: codexWindowKind(window, fallbackKind),
|
|
1338
1557
|
usedPercent: window.used_percent,
|
|
1339
1558
|
...resetsAt === void 0 ? {} : { resetsAt }
|
|
1340
1559
|
};
|
|
@@ -1342,8 +1561,11 @@ function codexUsageWindow(value, kind) {
|
|
|
1342
1561
|
/**
|
|
1343
1562
|
* Fetch the codex subscription usage from the ChatGPT backend wham/usage
|
|
1344
1563
|
* endpoint (the source of the codex CLI `/status` rate-limit lines). The
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1564
|
+
* windows are classified by their reported duration (`limit_window_seconds`)
|
|
1565
|
+
* rather than by slot, since the backend has been observed to report the
|
|
1566
|
+
* weekly lane as `primary_window` without a secondary window; slot order is
|
|
1567
|
+
* kept only as a fallback when the duration is absent. The lookup itself
|
|
1568
|
+
* consumes no rate-limit budget.
|
|
1347
1569
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
1348
1570
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
1349
1571
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -1430,10 +1652,15 @@ async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
1430
1652
|
}
|
|
1431
1653
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
1432
1654
|
var CodexAdapter = class extends LlmAdapter {
|
|
1433
|
-
catalog
|
|
1655
|
+
catalog;
|
|
1434
1656
|
constructor(options) {
|
|
1435
1657
|
super();
|
|
1436
1658
|
this.options = options;
|
|
1659
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
1660
|
+
}
|
|
1661
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
1662
|
+
async fetchCatalog() {
|
|
1663
|
+
return fetchCodexModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
1437
1664
|
}
|
|
1438
1665
|
providerInfo(provider) {
|
|
1439
1666
|
return {
|
|
@@ -1453,7 +1680,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1453
1680
|
if (await this.options.tokens.peek() === void 0) return [];
|
|
1454
1681
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
1455
1682
|
try {
|
|
1456
|
-
return (await this.catalog.get(
|
|
1683
|
+
return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
|
|
1457
1684
|
provider,
|
|
1458
1685
|
id: model.id,
|
|
1459
1686
|
name: model.name,
|
|
@@ -1467,10 +1694,21 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1467
1694
|
return this.staticModels(provider);
|
|
1468
1695
|
}
|
|
1469
1696
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1697
|
+
/**
|
|
1698
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
1699
|
+
* stale-while-revalidate path so capability metadata stays stable across a
|
|
1700
|
+
* long conversation: a discovered-only effort (one missing from the static
|
|
1701
|
+
* CODEX_EFFORTS list) selected by the user must not vanish — and fail the
|
|
1702
|
+
* call — just because the TTL lapsed mid-turn.
|
|
1703
|
+
*/
|
|
1704
|
+
async discovered(model) {
|
|
1705
|
+
if (!this.options.discovery) return void 0;
|
|
1706
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
1707
|
+
}
|
|
1708
|
+
async resolveModel(provider, model) {
|
|
1709
|
+
const discovered = await this.discovered(model);
|
|
1472
1710
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
1473
|
-
return
|
|
1711
|
+
return {
|
|
1474
1712
|
provider,
|
|
1475
1713
|
id: model,
|
|
1476
1714
|
name: discovered?.name ?? configured?.name ?? model,
|
|
@@ -1482,7 +1720,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
1482
1720
|
efforts: CODEX_EFFORTS,
|
|
1483
1721
|
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
1484
1722
|
}
|
|
1485
|
-
}
|
|
1723
|
+
};
|
|
1486
1724
|
}
|
|
1487
1725
|
async *stream(options) {
|
|
1488
1726
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -2538,10 +2776,15 @@ async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
|
2538
2776
|
}
|
|
2539
2777
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
2540
2778
|
var GrokAdapter = class extends LlmAdapter {
|
|
2541
|
-
catalog
|
|
2779
|
+
catalog;
|
|
2542
2780
|
constructor(options) {
|
|
2543
2781
|
super();
|
|
2544
2782
|
this.options = options;
|
|
2783
|
+
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
2784
|
+
}
|
|
2785
|
+
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
2786
|
+
async fetchCatalog() {
|
|
2787
|
+
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
|
|
2545
2788
|
}
|
|
2546
2789
|
providerInfo(provider) {
|
|
2547
2790
|
return {
|
|
@@ -2561,7 +2804,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2561
2804
|
if (await this.options.tokens.peek() === void 0) return [];
|
|
2562
2805
|
if (!this.options.discovery) return this.staticModels(provider);
|
|
2563
2806
|
try {
|
|
2564
|
-
return (await this.catalog.get(
|
|
2807
|
+
return (await this.catalog.get(() => this.fetchCatalog())).map((model) => ({
|
|
2565
2808
|
provider,
|
|
2566
2809
|
id: model.id,
|
|
2567
2810
|
name: model.name,
|
|
@@ -2575,10 +2818,22 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2575
2818
|
return this.staticModels(provider);
|
|
2576
2819
|
}
|
|
2577
2820
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2821
|
+
/**
|
|
2822
|
+
* The discovered entry for one model. Resolved through the cache's
|
|
2823
|
+
* stale-while-revalidate path: capability metadata must stay stable across
|
|
2824
|
+
* a long conversation — a session that selected a reasoning effort calls
|
|
2825
|
+
* this on EVERY step, and forgetting the efforts just because the TTL
|
|
2826
|
+
* lapsed mid-turn would fail the call with UNSUPPORTED_REASONING_EFFORT
|
|
2827
|
+
* before provider I/O.
|
|
2828
|
+
*/
|
|
2829
|
+
async discovered(model) {
|
|
2830
|
+
if (!this.options.discovery) return void 0;
|
|
2831
|
+
return (await this.catalog.resolve(() => this.fetchCatalog()))?.find((entry) => entry.id === model);
|
|
2832
|
+
}
|
|
2833
|
+
async resolveModel(provider, model) {
|
|
2834
|
+
const discovered = await this.discovered(model);
|
|
2580
2835
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
2581
|
-
return
|
|
2836
|
+
return {
|
|
2582
2837
|
provider,
|
|
2583
2838
|
id: model,
|
|
2584
2839
|
name: discovered?.name ?? configured?.name ?? model,
|
|
@@ -2587,7 +2842,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
2587
2842
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
2588
2843
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
2589
2844
|
...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
|
|
2590
|
-
}
|
|
2845
|
+
};
|
|
2591
2846
|
}
|
|
2592
2847
|
async *stream(options) {
|
|
2593
2848
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
@@ -2675,7 +2930,7 @@ function normalizeHandles(value, field) {
|
|
|
2675
2930
|
if (handles.length > MAX_HANDLES) throw new Error(`x_search: ${field} supports at most ${MAX_HANDLES} handles`);
|
|
2676
2931
|
return handles;
|
|
2677
2932
|
}
|
|
2678
|
-
function isRecord(value) {
|
|
2933
|
+
function isRecord$1(value) {
|
|
2679
2934
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2680
2935
|
}
|
|
2681
2936
|
/**
|
|
@@ -2684,7 +2939,7 @@ function isRecord(value) {
|
|
|
2684
2939
|
* top-level `citations` and inline `url_citation` annotations for sources.
|
|
2685
2940
|
*/
|
|
2686
2941
|
function parseXSearchResponse(payload) {
|
|
2687
|
-
const body = isRecord(payload) ? payload : {};
|
|
2942
|
+
const body = isRecord$1(payload) ? payload : {};
|
|
2688
2943
|
let answer = typeof body.output_text === "string" ? body.output_text.trim() : "";
|
|
2689
2944
|
const citations = [];
|
|
2690
2945
|
const push = (url) => {
|
|
@@ -2693,12 +2948,12 @@ function parseXSearchResponse(payload) {
|
|
|
2693
2948
|
if (Array.isArray(body.citations)) for (const citation of body.citations) push(citation);
|
|
2694
2949
|
const parts = [];
|
|
2695
2950
|
if (Array.isArray(body.output)) for (const item of body.output) {
|
|
2696
|
-
if (!isRecord(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
2951
|
+
if (!isRecord$1(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
|
|
2697
2952
|
for (const part of item.content) {
|
|
2698
|
-
if (!isRecord(part)) continue;
|
|
2953
|
+
if (!isRecord$1(part)) continue;
|
|
2699
2954
|
if ((part.type === "output_text" || part.type === "text") && typeof part.text === "string" && part.text.trim().length > 0) parts.push(part.text.trim());
|
|
2700
2955
|
if (Array.isArray(part.annotations)) {
|
|
2701
|
-
for (const annotation of part.annotations) if (isRecord(annotation) && annotation.type === "url_citation") push(annotation.url);
|
|
2956
|
+
for (const annotation of part.annotations) if (isRecord$1(annotation) && annotation.type === "url_citation") push(annotation.url);
|
|
2702
2957
|
}
|
|
2703
2958
|
}
|
|
2704
2959
|
}
|
|
@@ -2709,7 +2964,7 @@ function parseXSearchResponse(payload) {
|
|
|
2709
2964
|
};
|
|
2710
2965
|
}
|
|
2711
2966
|
/** Bound a call-card title's query. */
|
|
2712
|
-
function truncate$
|
|
2967
|
+
function truncate$2(text, max = 60) {
|
|
2713
2968
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
2714
2969
|
}
|
|
2715
2970
|
/**
|
|
@@ -2781,11 +3036,11 @@ function createXSearchTool(options) {
|
|
|
2781
3036
|
},
|
|
2782
3037
|
presentCall: (args) => ({
|
|
2783
3038
|
card: "generic",
|
|
2784
|
-
title: `x_search: ${truncate$
|
|
3039
|
+
title: `x_search: ${truncate$2(args.query)}`,
|
|
2785
3040
|
kind: "search"
|
|
2786
3041
|
}),
|
|
2787
3042
|
presentResult: (_args, result) => {
|
|
2788
|
-
if (result.isError || !isRecord(result.meta)) return void 0;
|
|
3043
|
+
if (result.isError || !isRecord$1(result.meta)) return void 0;
|
|
2789
3044
|
return {
|
|
2790
3045
|
card: "web",
|
|
2791
3046
|
kind: "search",
|
|
@@ -2870,7 +3125,7 @@ function imageFileName(index) {
|
|
|
2870
3125
|
return `image-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}-${index}.png`;
|
|
2871
3126
|
}
|
|
2872
3127
|
/** Bound a call-card title's prompt. */
|
|
2873
|
-
function truncate(text, max = 60) {
|
|
3128
|
+
function truncate$1(text, max = 60) {
|
|
2874
3129
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
2875
3130
|
}
|
|
2876
3131
|
/**
|
|
@@ -3006,7 +3261,7 @@ function createImageGenerateTool(options) {
|
|
|
3006
3261
|
},
|
|
3007
3262
|
presentCall: (args) => ({
|
|
3008
3263
|
card: "generic",
|
|
3009
|
-
title: `image_generate: ${truncate(args.prompt)}`
|
|
3264
|
+
title: `image_generate: ${truncate$1(args.prompt)}`
|
|
3010
3265
|
}),
|
|
3011
3266
|
presentResult: (_args, result) => ({
|
|
3012
3267
|
card: "generic",
|
|
@@ -3073,6 +3328,249 @@ function createImageGenerateTool(options) {
|
|
|
3073
3328
|
});
|
|
3074
3329
|
}
|
|
3075
3330
|
|
|
3331
|
+
//#endregion
|
|
3332
|
+
//#region src/tools/video-generate.ts
|
|
3333
|
+
/** Endpoint the generation request is posted to. */
|
|
3334
|
+
const VIDEO_GENERATE_URL = "https://api.x.ai/v1/videos/generations";
|
|
3335
|
+
/** The video model the grok subscription endpoint serves. */
|
|
3336
|
+
const VIDEO_GENERATE_MODEL = "grok-imagine-video-1.5";
|
|
3337
|
+
/** Polling endpoint for one generation request. */
|
|
3338
|
+
function videoStatusUrl(requestId) {
|
|
3339
|
+
return `https://api.x.ai/v1/videos/${encodeURIComponent(requestId)}`;
|
|
3340
|
+
}
|
|
3341
|
+
/** Default delay between two status polls. */
|
|
3342
|
+
const DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
3343
|
+
/** Default overall deadline for one generation (submit → done). */
|
|
3344
|
+
const DEFAULT_MAX_WAIT_MS = 10 * 6e4;
|
|
3345
|
+
/** xAI's supported clip length range in seconds. */
|
|
3346
|
+
const DURATION_RANGE = {
|
|
3347
|
+
min: 1,
|
|
3348
|
+
max: 15
|
|
3349
|
+
};
|
|
3350
|
+
/**
|
|
3351
|
+
* Assemble the request body from tool arguments (hand-checks the non-empty
|
|
3352
|
+
* prompt and the duration range the schema DSL cannot express).
|
|
3353
|
+
*/
|
|
3354
|
+
function buildVideoGenerateBody(args) {
|
|
3355
|
+
const prompt = args.prompt.trim();
|
|
3356
|
+
if (prompt.length === 0) throw new Error("video_generate: prompt must be a non-empty string");
|
|
3357
|
+
if (args.duration !== void 0 && (!Number.isInteger(args.duration) || args.duration < DURATION_RANGE.min || args.duration > DURATION_RANGE.max)) throw new Error(`video_generate: duration must be an integer between ${String(DURATION_RANGE.min)} and ${String(DURATION_RANGE.max)} seconds`);
|
|
3358
|
+
const imageUrl = args.image_url?.trim();
|
|
3359
|
+
return {
|
|
3360
|
+
prompt,
|
|
3361
|
+
model: VIDEO_GENERATE_MODEL,
|
|
3362
|
+
...args.duration === void 0 ? {} : { duration: args.duration },
|
|
3363
|
+
...args.aspect_ratio === void 0 ? {} : { aspect_ratio: args.aspect_ratio },
|
|
3364
|
+
...args.resolution === void 0 ? {} : { resolution: args.resolution },
|
|
3365
|
+
...imageUrl === void 0 || imageUrl.length === 0 ? {} : { image: { url: imageUrl } }
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
function isRecord(value) {
|
|
3369
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Extract the request id from the submit response. Throws when the payload
|
|
3373
|
+
* carries none.
|
|
3374
|
+
*/
|
|
3375
|
+
function parseVideoStartResponse(payload) {
|
|
3376
|
+
const body = isRecord(payload) ? payload : {};
|
|
3377
|
+
if (typeof body.request_id !== "string" || body.request_id.length === 0) throw new Error("video_generate: the response carried no request_id");
|
|
3378
|
+
return body.request_id;
|
|
3379
|
+
}
|
|
3380
|
+
/**
|
|
3381
|
+
* Decode one poll response. A `done` payload without a video URL and an
|
|
3382
|
+
* unrecognized status both throw (the poll loop cannot make progress on
|
|
3383
|
+
* either).
|
|
3384
|
+
*/
|
|
3385
|
+
function parseVideoStatusResponse(payload) {
|
|
3386
|
+
const body = isRecord(payload) ? payload : {};
|
|
3387
|
+
switch (body.status) {
|
|
3388
|
+
case "pending": return { status: "pending" };
|
|
3389
|
+
case "done": {
|
|
3390
|
+
const video = isRecord(body.video) ? body.video : {};
|
|
3391
|
+
if (typeof video.url !== "string" || video.url.length === 0) throw new Error("video_generate: the completed response carried no video URL");
|
|
3392
|
+
return {
|
|
3393
|
+
status: "done",
|
|
3394
|
+
url: video.url,
|
|
3395
|
+
...typeof video.duration === "number" ? { duration: video.duration } : {}
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
case "failed":
|
|
3399
|
+
case "expired": {
|
|
3400
|
+
const error = isRecord(body.error) ? body.error : {};
|
|
3401
|
+
const detail = typeof error.message === "string" && error.message.length > 0 ? error.message : typeof body.error === "string" && body.error.length > 0 ? body.error : void 0;
|
|
3402
|
+
return {
|
|
3403
|
+
status: body.status,
|
|
3404
|
+
...detail === void 0 ? {} : { detail }
|
|
3405
|
+
};
|
|
3406
|
+
}
|
|
3407
|
+
default: throw new Error(`video_generate: unexpected status ${JSON.stringify(body.status)}`);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
/** Directory the downloaded MP4 files are written to. */
|
|
3411
|
+
function videosDirectory() {
|
|
3412
|
+
return dshHomePath("plugins", "subscriptions", "videos");
|
|
3413
|
+
}
|
|
3414
|
+
/** Timestamped, collision-safe file name for one generated video. */
|
|
3415
|
+
function videoFileName() {
|
|
3416
|
+
return `video-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}.mp4`;
|
|
3417
|
+
}
|
|
3418
|
+
/** Bound a call-card title's prompt. */
|
|
3419
|
+
function truncate(text, max = 60) {
|
|
3420
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
3421
|
+
}
|
|
3422
|
+
/** Abort-aware sleep between two polls. */
|
|
3423
|
+
function sleep(ms, signal) {
|
|
3424
|
+
if (ms <= 0) return Promise.resolve();
|
|
3425
|
+
return new Promise((resolve, reject) => {
|
|
3426
|
+
const onAbort = () => {
|
|
3427
|
+
clearTimeout(timer);
|
|
3428
|
+
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("video_generate: aborted"));
|
|
3429
|
+
};
|
|
3430
|
+
const timer = setTimeout(() => {
|
|
3431
|
+
signal.removeEventListener("abort", onAbort);
|
|
3432
|
+
resolve();
|
|
3433
|
+
}, ms);
|
|
3434
|
+
if (signal.aborted) {
|
|
3435
|
+
onAbort();
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Build the `video_generate` tool definition.
|
|
3443
|
+
* @param options - grok session source, fetch implementation, and video directory.
|
|
3444
|
+
* @returns the tool to register on `ctx.tools`.
|
|
3445
|
+
*/
|
|
3446
|
+
function createVideoGenerateTool(options) {
|
|
3447
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3448
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
3449
|
+
return defineTool({
|
|
3450
|
+
name: "video_generate",
|
|
3451
|
+
description: `Generate a short video (1-15 seconds) with the grok subscription (${VIDEO_GENERATE_MODEL}) and save it as an MP4 file. Generation is asynchronous and may take a minute or more; the tool waits for completion and returns the saved file path. Optionally animate a still image by passing image_url (image-to-video).`,
|
|
3452
|
+
parameters: {
|
|
3453
|
+
prompt: {
|
|
3454
|
+
type: "string",
|
|
3455
|
+
required: true,
|
|
3456
|
+
description: "What the video should show."
|
|
3457
|
+
},
|
|
3458
|
+
duration: {
|
|
3459
|
+
type: "integer",
|
|
3460
|
+
description: "Clip length in seconds (1-15); omit for the provider default."
|
|
3461
|
+
},
|
|
3462
|
+
aspect_ratio: {
|
|
3463
|
+
type: "string",
|
|
3464
|
+
enum: [
|
|
3465
|
+
"16:9",
|
|
3466
|
+
"9:16",
|
|
3467
|
+
"1:1",
|
|
3468
|
+
"4:3",
|
|
3469
|
+
"3:4",
|
|
3470
|
+
"3:2",
|
|
3471
|
+
"2:3"
|
|
3472
|
+
],
|
|
3473
|
+
description: "Output aspect ratio; omit for the provider default (16:9)."
|
|
3474
|
+
},
|
|
3475
|
+
resolution: {
|
|
3476
|
+
type: "string",
|
|
3477
|
+
enum: [
|
|
3478
|
+
"480p",
|
|
3479
|
+
"720p",
|
|
3480
|
+
"1080p"
|
|
3481
|
+
],
|
|
3482
|
+
description: "Output resolution; omit for the provider default (480p). Higher is slower."
|
|
3483
|
+
},
|
|
3484
|
+
image_url: {
|
|
3485
|
+
type: "string",
|
|
3486
|
+
description: "Optional public URL or base64 data URL of a JPEG/PNG/WebP image to animate (image-to-video); the image becomes the starting frame."
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3489
|
+
output: {
|
|
3490
|
+
schema: {
|
|
3491
|
+
type: "object",
|
|
3492
|
+
properties: {
|
|
3493
|
+
path: {
|
|
3494
|
+
type: "string",
|
|
3495
|
+
required: true
|
|
3496
|
+
},
|
|
3497
|
+
url: {
|
|
3498
|
+
type: "string",
|
|
3499
|
+
required: true
|
|
3500
|
+
},
|
|
3501
|
+
duration: { type: "number" }
|
|
3502
|
+
},
|
|
3503
|
+
additionalProperties: false
|
|
3504
|
+
},
|
|
3505
|
+
render: (_args, value) => [{
|
|
3506
|
+
type: "text",
|
|
3507
|
+
text: `Saved video to ${value.path}` + (value.duration === void 0 ? "" : ` (${String(value.duration)}s)`) + `\nTemporary provider URL (expires soon): ${value.url}`
|
|
3508
|
+
}],
|
|
3509
|
+
presentationMeta: (_args, value) => ({
|
|
3510
|
+
fileName: basename(value.path),
|
|
3511
|
+
...value.duration === void 0 ? {} : { duration: value.duration }
|
|
3512
|
+
})
|
|
3513
|
+
},
|
|
3514
|
+
presentCall: (args) => ({
|
|
3515
|
+
card: "generic",
|
|
3516
|
+
title: `video_generate: ${truncate(args.prompt)}`
|
|
3517
|
+
}),
|
|
3518
|
+
async execute(args, exec) {
|
|
3519
|
+
const body = buildVideoGenerateBody(args);
|
|
3520
|
+
const session = await options.tokens.session();
|
|
3521
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
3522
|
+
const headers = {
|
|
3523
|
+
"authorization": `Bearer ${session.accessToken}`,
|
|
3524
|
+
"accept": "application/json"
|
|
3525
|
+
};
|
|
3526
|
+
const submit = await fetchFn(VIDEO_GENERATE_URL, {
|
|
3527
|
+
method: "POST",
|
|
3528
|
+
headers: {
|
|
3529
|
+
...headers,
|
|
3530
|
+
"content-type": "application/json"
|
|
3531
|
+
},
|
|
3532
|
+
body: JSON.stringify(body),
|
|
3533
|
+
signal: exec.signal
|
|
3534
|
+
});
|
|
3535
|
+
if (!submit.ok) throw await httpLlmError(submit, "video_generate");
|
|
3536
|
+
const requestId = parseVideoStartResponse(await submit.json());
|
|
3537
|
+
const deadline = Date.now() + maxWaitMs;
|
|
3538
|
+
let done;
|
|
3539
|
+
for (;;) {
|
|
3540
|
+
await sleep(pollIntervalMs, exec.signal);
|
|
3541
|
+
const poll = await fetchFn(videoStatusUrl(requestId), {
|
|
3542
|
+
method: "GET",
|
|
3543
|
+
headers,
|
|
3544
|
+
signal: exec.signal
|
|
3545
|
+
});
|
|
3546
|
+
if (!poll.ok) throw await httpLlmError(poll, "video_generate");
|
|
3547
|
+
const status = parseVideoStatusResponse(await poll.json());
|
|
3548
|
+
if (status.status === "done") {
|
|
3549
|
+
done = status;
|
|
3550
|
+
break;
|
|
3551
|
+
}
|
|
3552
|
+
if (status.status === "failed" || status.status === "expired") throw new Error(`video_generate: generation ${status.status} (request ${requestId})` + (status.detail === void 0 ? "" : `: ${status.detail}`));
|
|
3553
|
+
if (Date.now() >= deadline) throw new Error(`video_generate: timed out after ${String(maxWaitMs)}ms waiting for request ${requestId}`);
|
|
3554
|
+
}
|
|
3555
|
+
const download = await fetchFn(done.url, {
|
|
3556
|
+
method: "GET",
|
|
3557
|
+
signal: exec.signal
|
|
3558
|
+
});
|
|
3559
|
+
if (!download.ok) throw await httpLlmError(download, "video_generate download");
|
|
3560
|
+
const data = Buffer.from(await download.arrayBuffer());
|
|
3561
|
+
const directory = options.videosDir ?? videosDirectory();
|
|
3562
|
+
await mkdir(directory, { recursive: true });
|
|
3563
|
+
const path = join(directory, videoFileName());
|
|
3564
|
+
await writeFile(path, data);
|
|
3565
|
+
return {
|
|
3566
|
+
path,
|
|
3567
|
+
url: done.url,
|
|
3568
|
+
...done.duration === void 0 ? {} : { duration: done.duration }
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3076
3574
|
//#endregion
|
|
3077
3575
|
//#region src/index.ts
|
|
3078
3576
|
const name = "dsh-plugin-subscriptions";
|
|
@@ -3202,6 +3700,12 @@ var SubscriptionsAuthController = class {
|
|
|
3202
3700
|
dataBase64: Buffer.from(stored.data).toString("base64")
|
|
3203
3701
|
};
|
|
3204
3702
|
}
|
|
3703
|
+
async readVideo(name$1, signal) {
|
|
3704
|
+
return {
|
|
3705
|
+
mediaType: "video/mp4",
|
|
3706
|
+
dataBase64: (await readFile(join(videosDirectory(), name$1), { signal })).toString("base64")
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3205
3709
|
async status(provider) {
|
|
3206
3710
|
const session = await getSession(provider);
|
|
3207
3711
|
const account = accountOf(provider, session);
|
|
@@ -3303,7 +3807,8 @@ function apply(ctx, config) {
|
|
|
3303
3807
|
tokens,
|
|
3304
3808
|
discovery: !overridden.has("codex"),
|
|
3305
3809
|
onWarn,
|
|
3306
|
-
resolveAttachments
|
|
3810
|
+
resolveAttachments,
|
|
3811
|
+
catalogStore: catalogStore("codex")
|
|
3307
3812
|
})));
|
|
3308
3813
|
break;
|
|
3309
3814
|
}
|
|
@@ -3350,14 +3855,18 @@ function apply(ctx, config) {
|
|
|
3350
3855
|
tokens,
|
|
3351
3856
|
discovery: !overridden.has("grok"),
|
|
3352
3857
|
onWarn,
|
|
3353
|
-
resolveAttachments
|
|
3858
|
+
resolveAttachments,
|
|
3859
|
+
catalogStore: catalogStore("grok")
|
|
3354
3860
|
})));
|
|
3355
3861
|
break;
|
|
3356
3862
|
}
|
|
3357
3863
|
}
|
|
3358
3864
|
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers));
|
|
3359
3865
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
3360
|
-
if (grokTokens !== void 0)
|
|
3866
|
+
if (grokTokens !== void 0) {
|
|
3867
|
+
toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));
|
|
3868
|
+
toolsCtx.tools.register(createVideoGenerateTool({ tokens: grokTokens }));
|
|
3869
|
+
}
|
|
3361
3870
|
if (codexTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
|
|
3362
3871
|
tokens: codexTokens,
|
|
3363
3872
|
resolveAttachments,
|