privateer-agent 0.12.45 → 0.12.49
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 +28 -1
- package/bin/headless-flags.d.mts +25 -0
- package/bin/headless-flags.mjs +166 -0
- package/bin/privateer-launch.mjs +93 -3
- package/bin/privateer-subagent.mjs +1 -0
- package/extensions/privateer-gate.ts +192 -5
- package/extensions/privateer-vision.ts +39 -0
- package/package.json +1 -1
- package/patches/@earendil-works+pi-coding-agent+0.86.0.patch +162 -10
- package/src/acp/run.ts +3 -1
- package/src/bridge/engineAdapter.ts +1 -1
- package/src/cli/chat.ts +7 -3
- package/src/config/moat.ts +5 -0
- package/src/config/moatManifest.json +1 -0
- package/src/config/privacyDisabled.ts +6 -1
- package/src/config/privacyPolicy.ts +3 -1
- package/src/engine/errors.ts +61 -4
- package/src/ext/permissionGate.ts +23 -4
- package/src/permissions/cliSpend.ts +163 -0
- package/src/permissions/modeGate.ts +9 -3
- package/src/permissions/noQuarter.ts +21 -2
- package/src/providers/account.ts +89 -1
- package/src/providers/visionDelegate.ts +199 -0
- package/src/remote/headlessApproval.ts +146 -0
- package/src/tools/media.ts +151 -12
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// APPROVALS FOR A RUN WITH NO SCREEN, answered from the Privateer app.
|
|
2
|
+
//
|
|
3
|
+
// privateer -p --approve-in-app "make the trailer"
|
|
4
|
+
//
|
|
5
|
+
// A `-p` run's gate has nobody to ask, so it denies. With --approve-in-app it instead
|
|
6
|
+
// brings up the same E2EE relay `/remote-access` uses, waits for the app to attach to
|
|
7
|
+
// this terminal, and puts the question there — "Generate a video (billed…) · about
|
|
8
|
+
// $0.50" — and denies only if nobody answers before the timeout. The relay is started
|
|
9
|
+
// LAZILY, on the first ask: a run that never needs an approval never opens a socket.
|
|
10
|
+
//
|
|
11
|
+
// WHAT THIS IS NOT. The relay reaches an app that is running (foreground, or
|
|
12
|
+
// backgrounded with its socket alive — the app raises its own local notification for an
|
|
13
|
+
// approval then). An app that is fully closed is only reachable by a server-sent push,
|
|
14
|
+
// which does not exist yet (docs/push-on-gate-server.md). So the waiting line says to
|
|
15
|
+
// OPEN the app, rather than promising a notification that may never arrive.
|
|
16
|
+
//
|
|
17
|
+
// POSTURE. Nothing here widens what the gate allows: this is only the asker the gate
|
|
18
|
+
// calls when it has already decided to ask. Fail-closed throughout — not signed in, no
|
|
19
|
+
// relay, timeout, abort: deny. "Allow and remember" from the app counts as allow-once,
|
|
20
|
+
// because a headless run has no later turn for a remembered rule to help and the gate
|
|
21
|
+
// never remembers the always-ask kinds anyway. A PROMPT sent from the app is refused:
|
|
22
|
+
// this terminal takes approvals, not instructions, for the length of the run.
|
|
23
|
+
|
|
24
|
+
import { RemoteBridge, type RelayLike } from "./remoteBridge.ts";
|
|
25
|
+
import type { PermissionRequest } from "../permissions/gate.ts";
|
|
26
|
+
import type { AskOutcome } from "../permissions/modeGate.ts";
|
|
27
|
+
import type { RelayCallbacks } from "./relayClient.ts";
|
|
28
|
+
|
|
29
|
+
/** The relay surface this needs beyond the bridge's: lifecycle and presence. */
|
|
30
|
+
export interface HeadlessRelay extends RelayLike {
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
stop(): void;
|
|
33
|
+
hasController(): boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HeadlessApprovalOptions {
|
|
37
|
+
/** Per ask: how long to wait for the app to attach AND answer. */
|
|
38
|
+
timeoutMs: number;
|
|
39
|
+
/** Where the waiting / outcome lines go (stderr — stdout may be the answer or JSON). */
|
|
40
|
+
log: (line: string) => void;
|
|
41
|
+
signedIn: () => boolean;
|
|
42
|
+
makeRelay: (callbacks: RelayCallbacks) => HeadlessRelay;
|
|
43
|
+
/** Label for this terminal in the app. */
|
|
44
|
+
label?: string;
|
|
45
|
+
/** Presence poll interval; tests shorten it. */
|
|
46
|
+
pollMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mins = (ms: number): string => (ms >= 60_000 ? `${Math.round(ms / 60_000)}m` : `${Math.round(ms / 1000)}s`);
|
|
50
|
+
|
|
51
|
+
export class HeadlessAppApprover {
|
|
52
|
+
private bridge?: RemoteBridge;
|
|
53
|
+
private relay?: HeadlessRelay;
|
|
54
|
+
private starting?: Promise<void>;
|
|
55
|
+
private closed = false;
|
|
56
|
+
// How many times the app has left. A deny that coincides with a departure is the
|
|
57
|
+
// bridge failing the ask closed, not a person pressing Deny — and presence alone
|
|
58
|
+
// can't tell, because the app may already be back by the time we look.
|
|
59
|
+
private departures = 0;
|
|
60
|
+
|
|
61
|
+
constructor(private readonly opts: HeadlessApprovalOptions) {}
|
|
62
|
+
|
|
63
|
+
private ensureRelay(): Promise<void> {
|
|
64
|
+
if (this.starting) return this.starting;
|
|
65
|
+
const bridge = new RemoteBridge({
|
|
66
|
+
onPrompt: () => bridge.sendNotice("This terminal is a headless run waiting for approvals only — it can't take a prompt."),
|
|
67
|
+
onCommand: () => bridge.sendNotice("This terminal is a headless run waiting for approvals only — it can't run commands."),
|
|
68
|
+
onControllerDetached: () => this.departures++,
|
|
69
|
+
});
|
|
70
|
+
this.bridge = bridge;
|
|
71
|
+
this.relay = this.opts.makeRelay(bridge.callbacks);
|
|
72
|
+
bridge.attachRelay(this.relay);
|
|
73
|
+
this.starting = this.relay.start();
|
|
74
|
+
return this.starting;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async waitForController(deadline: number, signal?: AbortSignal): Promise<boolean> {
|
|
78
|
+
const poll = this.opts.pollMs ?? 500;
|
|
79
|
+
while (!this.closed && !signal?.aborted && Date.now() < deadline) {
|
|
80
|
+
if (this.relay?.hasController()) return true;
|
|
81
|
+
await new Promise((r) => setTimeout(r, Math.min(poll, Math.max(0, deadline - Date.now()))));
|
|
82
|
+
}
|
|
83
|
+
return !!this.relay?.hasController() && !signal?.aborted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The gate's asker for this run. `priceNote` (e.g. "about $0.50") rides on the prompt. */
|
|
87
|
+
async ask(req: PermissionRequest, signal?: AbortSignal, priceNote?: string): Promise<AskOutcome> {
|
|
88
|
+
const what = `${req.title}${priceNote ? ` · ${priceNote}` : ""}`;
|
|
89
|
+
if (!this.opts.signedIn()) {
|
|
90
|
+
this.opts.log(`⚓ ${what} — denied: --approve-in-app needs this machine signed in to Privateer (run \`privateer\`, then /login).`);
|
|
91
|
+
return "deny";
|
|
92
|
+
}
|
|
93
|
+
const deadline = Date.now() + this.opts.timeoutMs;
|
|
94
|
+
try {
|
|
95
|
+
await this.ensureRelay();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
this.opts.log(`⚓ ${what} — denied: couldn't reach the Privateer relay (${e instanceof Error ? e.message : String(e)}).`);
|
|
98
|
+
return "deny";
|
|
99
|
+
}
|
|
100
|
+
const asked: PermissionRequest = priceNote ? { ...req, detail: `${req.detail} · ${priceNote}` } : req;
|
|
101
|
+
|
|
102
|
+
this.opts.log(
|
|
103
|
+
`⚓ Approval needed: ${what}\n` +
|
|
104
|
+
` Waiting up to ${mins(this.opts.timeoutMs)} — open the Privateer app and approve it there` +
|
|
105
|
+
(this.opts.label ? ` (terminal "${this.opts.label}")` : "") +
|
|
106
|
+
".",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Loop, because the app coming and going is normal (a phone locks, a user switches
|
|
110
|
+
// apps): a detach fails the pending ask closed, and the next attach gets it again.
|
|
111
|
+
while (Date.now() < deadline && !signal?.aborted && !this.closed) {
|
|
112
|
+
if (!(await this.waitForController(deadline, signal))) break;
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const onAbort = () => controller.abort();
|
|
115
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
116
|
+
const timer = setTimeout(() => controller.abort(), Math.max(0, deadline - Date.now()));
|
|
117
|
+
let outcome: AskOutcome;
|
|
118
|
+
const departuresBefore = this.departures;
|
|
119
|
+
try {
|
|
120
|
+
outcome = await this.bridge!.remoteAsk(asked, controller.signal);
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
signal?.removeEventListener("abort", onAbort);
|
|
124
|
+
}
|
|
125
|
+
if (outcome !== "deny") {
|
|
126
|
+
this.opts.log(`⚓ Approved in the app: ${what}`);
|
|
127
|
+
return "allow";
|
|
128
|
+
}
|
|
129
|
+
// A real "Deny" is the answer. A deny because the app left is not — wait for it
|
|
130
|
+
// to come back and ask again.
|
|
131
|
+
// A dropped socket fails it closed the same way, with no detach to count.
|
|
132
|
+
if (controller.signal.aborted || (this.departures === departuresBefore && this.relay?.isConnected())) break;
|
|
133
|
+
}
|
|
134
|
+
const why = signal?.aborted ? "the run was cancelled" : `no answer from the app in ${mins(this.opts.timeoutMs)}`;
|
|
135
|
+
const deniedInApp = !signal?.aborted && Date.now() < deadline;
|
|
136
|
+
this.opts.log(`⚓ Denied: ${what} — ${deniedInApp ? "denied in the app" : why}.`);
|
|
137
|
+
return "deny";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Drop the relay. Safe to call twice, and before anything started. */
|
|
141
|
+
close(): void {
|
|
142
|
+
this.closed = true;
|
|
143
|
+
this.relay?.stop();
|
|
144
|
+
this.bridge?.dispose();
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/tools/media.ts
CHANGED
|
@@ -947,9 +947,32 @@ export const generateSfxToolDefinition = {
|
|
|
947
947
|
|
|
948
948
|
// ── Capabilities ─────────────────────────────────────────────────────────────
|
|
949
949
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
950
|
+
/** One priced video call shape. `resolution: null` is a call that sends none. */
|
|
951
|
+
export interface VideoPriceRow {
|
|
952
|
+
seconds: number | null;
|
|
953
|
+
resolution: string | null;
|
|
954
|
+
audio: boolean;
|
|
955
|
+
usd: number;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export interface CapabilitiesResponse {
|
|
959
|
+
/** `priceUsdEach` is what one image costs the account; null when the server couldn't price it. */
|
|
960
|
+
image?: { model?: string; blockedByZdr?: boolean; maxPerCall?: number; priceUsdEach?: number | null };
|
|
961
|
+
/** `priceUsd`/`priceTable` are the server's RESERVATION figures — worst-case-biased, so
|
|
962
|
+
* a budget built on them can only come in under. Absent on a server older than them. */
|
|
963
|
+
video?: {
|
|
964
|
+
model?: string;
|
|
965
|
+
blockedByZdr?: boolean;
|
|
966
|
+
durations?: number[] | null;
|
|
967
|
+
aspectRatios?: string[] | null;
|
|
968
|
+
priceUsd?: { min?: number; max?: number } | null;
|
|
969
|
+
priceTable?: VideoPriceRow[] | null;
|
|
970
|
+
priceSource?: "table" | "fallback";
|
|
971
|
+
};
|
|
972
|
+
sprites?: {
|
|
973
|
+
available?: boolean;
|
|
974
|
+
directionSets?: { id: string; billedClips?: number; billedTurnStills?: number }[];
|
|
975
|
+
};
|
|
953
976
|
model3d?: {
|
|
954
977
|
model?: string;
|
|
955
978
|
configured?: boolean;
|
|
@@ -1066,6 +1089,119 @@ function describeAxis(a: MeshAxis): string {
|
|
|
1066
1089
|
return ` ${a.name}: ${(a.values ?? []).join(" | ")} (default ${String(a.default)})${cost}`;
|
|
1067
1090
|
}
|
|
1068
1091
|
|
|
1092
|
+
const usd = (n: number | undefined | null): string => `$${(n ?? 0).toFixed(2)}`;
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* The image and video sections of the capability report.
|
|
1096
|
+
*
|
|
1097
|
+
* Exported and pure for the reason describeModel3d is. The prices are the point: an
|
|
1098
|
+
* agent planning a film used to see what a mesh or an effect cost and nothing at all
|
|
1099
|
+
* for a clip — the dearest call it can make — so it could neither weigh the job nor
|
|
1100
|
+
* set a sensible `--max-spend`. A server too old to report a price gets that said
|
|
1101
|
+
* plainly rather than a $0.00 that reads as free.
|
|
1102
|
+
*/
|
|
1103
|
+
export function describeImageVideo(image: CapabilitiesResponse["image"], video: CapabilitiesResponse["video"]): string[] {
|
|
1104
|
+
const imagePrice =
|
|
1105
|
+
typeof image?.priceUsdEach === "number" ? `, about ${usd(image.priceUsdEach)} each` : ", price not reported";
|
|
1106
|
+
const lines = [
|
|
1107
|
+
`Image model: ${image?.model ?? "unknown"}${image?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : ""}`,
|
|
1108
|
+
` up to ${image?.maxPerCall ?? 1} image(s) per call${imagePrice}`,
|
|
1109
|
+
`Video model: ${video?.model ?? "unknown"}${video?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : ""}`,
|
|
1110
|
+
` clip lengths: ${video?.durations?.length ? `${video.durations.join(", ")}s` : "model default only"}`,
|
|
1111
|
+
` aspect ratios: ${video?.aspectRatios?.length ? video.aspectRatios.join(", ") : "model default only"}`,
|
|
1112
|
+
];
|
|
1113
|
+
const table = video?.priceTable ?? [];
|
|
1114
|
+
if (video?.priceUsd && table.length) {
|
|
1115
|
+
const fallback = video.priceSource === "fallback" ? " (no price row for this model — a conservative estimate)" : "";
|
|
1116
|
+
lines.push(` cost: ${usd(video.priceUsd.min)}-${usd(video.priceUsd.max)} a clip${fallback}, reserved up front and settled at the real cost`);
|
|
1117
|
+
// Per length at the call's default (no resolution sent, no audio): that is the
|
|
1118
|
+
// call an agent makes unless it asks for more, and the line it budgets from.
|
|
1119
|
+
const plain = table.filter((r) => r.resolution === null && !r.audio && r.seconds !== null);
|
|
1120
|
+
if (plain.length) lines.push(` by length (default resolution, no audio): ${plain.map((r) => `${r.seconds}s ${usd(r.usd)}`).join(", ")}`);
|
|
1121
|
+
const resolutions = [...new Set(table.map((r) => r.resolution).filter((r): r is string => !!r))];
|
|
1122
|
+
if (resolutions.length) lines.push(` resolution changes the price: ${resolutions.join(", ")} (pass generate_video's \`resolution\`)`);
|
|
1123
|
+
if (table.some((r) => r.audio)) lines.push(" audio: true costs more on this model");
|
|
1124
|
+
} else {
|
|
1125
|
+
lines.push(" cost: not reported by this server — roughly $0.10-$1 a clip, more for long or high-resolution clips");
|
|
1126
|
+
}
|
|
1127
|
+
return lines;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* What one call of a billed tool is expected to cost, in USD — or null when it can't
|
|
1132
|
+
* be priced from what the server reports. Pure over a capability report, for the
|
|
1133
|
+
* `--max-spend` budget (permissions/cliSpend.ts): it prices the call the model is
|
|
1134
|
+
* ABOUT to make from that call's own arguments, and it errs high everywhere it has
|
|
1135
|
+
* to choose, because a cap that under-counts is not a cap.
|
|
1136
|
+
*/
|
|
1137
|
+
export function quoteMediaCallUsd(tool: string, input: unknown, caps: CapabilitiesResponse): number | null {
|
|
1138
|
+
const args = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
|
|
1139
|
+
const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
1140
|
+
const clip = (seconds: number | undefined, resolution: string | undefined, audio: boolean): number | null => {
|
|
1141
|
+
const table = caps.video?.priceTable ?? [];
|
|
1142
|
+
if (!table.length) return null;
|
|
1143
|
+
const exact = table.find(
|
|
1144
|
+
(r) => r.seconds === (seconds ?? r.seconds) && r.resolution === (resolution ?? null) && r.audio === audio,
|
|
1145
|
+
);
|
|
1146
|
+
// An exact row when the call names a legal length; otherwise the dearest row that
|
|
1147
|
+
// still matches what we do know — never a cheaper guess.
|
|
1148
|
+
if (exact && seconds !== undefined) return exact.usd;
|
|
1149
|
+
// `resolution: null` rows are the server's price for a call that sends none, so
|
|
1150
|
+
// that's the set an unspecified resolution is quoted from.
|
|
1151
|
+
const candidates = table.filter((r) => r.resolution === (resolution ?? null) && (audio || !r.audio));
|
|
1152
|
+
const pool = candidates.length ? candidates : table;
|
|
1153
|
+
return Math.max(...pool.map((r) => r.usd));
|
|
1154
|
+
};
|
|
1155
|
+
switch (tool) {
|
|
1156
|
+
case "generate_image": {
|
|
1157
|
+
// The report prices the ACCOUNT's image model; a call naming another one would
|
|
1158
|
+
// be quoted at the wrong model's rate, so it isn't quoted at all.
|
|
1159
|
+
if (typeof args.model === "string" && args.model) return null;
|
|
1160
|
+
const each = caps.image?.priceUsdEach;
|
|
1161
|
+
if (typeof each !== "number") return null;
|
|
1162
|
+
const n = Math.min(Math.max(Math.trunc(num(args.count) ?? 1), 1), caps.image?.maxPerCall ?? 4);
|
|
1163
|
+
return each * n;
|
|
1164
|
+
}
|
|
1165
|
+
case "generate_video":
|
|
1166
|
+
return clip(num(args.seconds), typeof args.resolution === "string" ? args.resolution : undefined, args.audio === true);
|
|
1167
|
+
case "generate_model":
|
|
1168
|
+
return typeof caps.model3d?.priceUsd?.max === "number" ? caps.model3d.priceUsd.max : null;
|
|
1169
|
+
case "generate_sprite": {
|
|
1170
|
+
const set = typeof args.directions === "string" ? args.directions : "one";
|
|
1171
|
+
const row = caps.sprites?.directionSets?.find((d) => d.id === set);
|
|
1172
|
+
// The sprite pipeline picks each clip's length and resolution itself, so every
|
|
1173
|
+
// clip is quoted at the dearest silent row.
|
|
1174
|
+
const silent = (caps.video?.priceTable ?? []).filter((r) => !r.audio);
|
|
1175
|
+
const clipUsd = silent.length ? Math.max(...silent.map((r) => r.usd)) : null;
|
|
1176
|
+
if (!row || clipUsd === null) return null;
|
|
1177
|
+
const stills = row.billedTurnStills ?? 0;
|
|
1178
|
+
const each = caps.image?.priceUsdEach;
|
|
1179
|
+
if (stills > 0 && (typeof each !== "number" || (typeof args.image_model === "string" && args.image_model))) return null;
|
|
1180
|
+
return clipUsd * (row.billedClips ?? 1) + stills * (each ?? 0);
|
|
1181
|
+
}
|
|
1182
|
+
case "generate_sfx":
|
|
1183
|
+
// No server figure; the tool's own documented ceiling for a single effect.
|
|
1184
|
+
return 0.02;
|
|
1185
|
+
default:
|
|
1186
|
+
// Speech and music: the server reports no price, and a guess would be a cap in
|
|
1187
|
+
// name only.
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** Read the capability report (optionally describing a specific video / 3D model). */
|
|
1193
|
+
export async function readMediaCapabilities(
|
|
1194
|
+
query: { videoModel?: string; model?: string } = {},
|
|
1195
|
+
signal?: AbortSignal,
|
|
1196
|
+
): Promise<{ ok: true; data: CapabilitiesResponse } | { ok: false; message: string }> {
|
|
1197
|
+
const q = new URLSearchParams();
|
|
1198
|
+
if (query.videoModel) q.set("videoModel", query.videoModel);
|
|
1199
|
+
if (query.model) q.set("model", query.model);
|
|
1200
|
+
const qs = q.toString();
|
|
1201
|
+
const r = await callAccount<CapabilitiesResponse>(`/api/agent/media/capabilities${qs ? `?${qs}` : ""}`, { method: "GET", signal });
|
|
1202
|
+
return r.ok ? { ok: true, data: r.data } : { ok: false, message: r.message };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1069
1205
|
/**
|
|
1070
1206
|
* The sound-effect section of the capability report.
|
|
1071
1207
|
*
|
|
@@ -1137,7 +1273,7 @@ export const mediaCapabilitiesToolDefinition = {
|
|
|
1137
1273
|
label: "Media Capabilities",
|
|
1138
1274
|
description:
|
|
1139
1275
|
"Report what this Privateer account can generate right now: which image and video models it " +
|
|
1140
|
-
"resolves to, the clip lengths and aspect ratios that video model accepts, and whether the " +
|
|
1276
|
+
"resolves to, what an image and a clip cost, the clip lengths and aspect ratios that video model accepts, and whether the " +
|
|
1141
1277
|
"account's privacy settings currently block media generation. Free and instant. Call it before " +
|
|
1142
1278
|
"planning a multi-clip video so you pick a legal clip length instead of discovering it through a " +
|
|
1143
1279
|
"rejected — or worse, billed — call.\n" +
|
|
@@ -1157,6 +1293,14 @@ export const mediaCapabilitiesToolDefinition = {
|
|
|
1157
1293
|
"their legal values and the price — is per-model.",
|
|
1158
1294
|
}),
|
|
1159
1295
|
),
|
|
1296
|
+
videoModel: Type.Optional(
|
|
1297
|
+
Type.String({
|
|
1298
|
+
description:
|
|
1299
|
+
"A video model id to describe instead of the account default (e.g. 'bytedance/seedance-2.0'). " +
|
|
1300
|
+
"Clip lengths, aspect ratios and the price per clip are all per-model — check the one you will " +
|
|
1301
|
+
"pass as generate_video's `model` before you spend on it.",
|
|
1302
|
+
}),
|
|
1303
|
+
),
|
|
1160
1304
|
ttsModel: Type.Optional(
|
|
1161
1305
|
Type.String({
|
|
1162
1306
|
description:
|
|
@@ -1166,9 +1310,10 @@ export const mediaCapabilitiesToolDefinition = {
|
|
|
1166
1310
|
}),
|
|
1167
1311
|
),
|
|
1168
1312
|
}),
|
|
1169
|
-
async execute(_toolCallId: string, params: { model?: string; ttsModel?: string }, signal?: AbortSignal) {
|
|
1313
|
+
async execute(_toolCallId: string, params: { model?: string; ttsModel?: string; videoModel?: string }, signal?: AbortSignal) {
|
|
1170
1314
|
const query = new URLSearchParams();
|
|
1171
1315
|
if (params?.model) query.set("model", params.model);
|
|
1316
|
+
if (params?.videoModel) query.set("videoModel", params.videoModel);
|
|
1172
1317
|
if (params?.ttsModel) query.set("ttsModel", params.ttsModel);
|
|
1173
1318
|
const qs = query.toString();
|
|
1174
1319
|
const r = await callAccount<CapabilitiesResponse>(
|
|
@@ -1178,13 +1323,7 @@ export const mediaCapabilitiesToolDefinition = {
|
|
|
1178
1323
|
if (!r.ok) return text(`Could not read media capabilities: ${r.message}`);
|
|
1179
1324
|
|
|
1180
1325
|
const { image, video, model3d, sfx, speech, privacy } = r.data;
|
|
1181
|
-
const lines =
|
|
1182
|
-
`Image model: ${image?.model ?? "unknown"}${image?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : ""}`,
|
|
1183
|
-
` up to ${image?.maxPerCall ?? 1} image(s) per call`,
|
|
1184
|
-
`Video model: ${video?.model ?? "unknown"}${video?.blockedByZdr ? " [BLOCKED by this account's ZDR setting]" : ""}`,
|
|
1185
|
-
` clip lengths: ${video?.durations?.length ? `${video.durations.join(", ")}s` : "model default only"}`,
|
|
1186
|
-
` aspect ratios: ${video?.aspectRatios?.length ? video.aspectRatios.join(", ") : "model default only"}`,
|
|
1187
|
-
];
|
|
1326
|
+
const lines = describeImageVideo(image, video);
|
|
1188
1327
|
|
|
1189
1328
|
lines.push(...describeModel3d(model3d));
|
|
1190
1329
|
lines.push(...describeSfx(sfx));
|