@chrischall/gemini-mcp 1.9.0 → 1.11.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -0
- package/dist/bundle.js +346 -11
- package/dist/client.js +23 -4
- package/dist/jobs.js +4 -0
- package/dist/pricing.js +201 -0
- package/dist/registrars.js +2 -0
- package/dist/session.js +52 -0
- package/dist/tools/generate.js +13 -0
- package/dist/tools/interact.js +8 -2
- package/dist/tools/music.js +4 -0
- package/dist/tools/set.js +17 -2
- package/dist/tools/shared.js +19 -5
- package/dist/tools/usage.js +71 -0
- package/dist/tools/video.js +8 -2
- package/dist/usage.js +156 -0
- package/dist/version.js +1 -1
- package/mint.yaml +57 -0
- package/package.json +4 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.11.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Gemini Images",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Generate and edit images with Google Gemini image models (Nano Banana / Nano Banana Pro) via natural language prompts",
|
|
18
|
-
"version": "1.
|
|
18
|
+
"version": "1.11.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-mcp",
|
|
3
3
|
"displayName": "Gemini Images",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.0",
|
|
5
5
|
"description": "MCP server for Google Gemini image generation and editing — generate and edit images via the Gemini API",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ progress), two guards make re-issuing safe and unnecessary:
|
|
|
83
83
|
| `gemini_video_generate` | Generate a short video (text→video, image→video, or `edit`) via the Gemini omni model (preview); written to disk as MP4 |
|
|
84
84
|
| `gemini_music_generate` | Generate music from a text prompt via a Lyria model — `lyria-3-clip-preview` (~30s, default) or `lyria-3-pro-preview` (longer, WAV-capable); written to disk as MP3/WAV (preview) |
|
|
85
85
|
| `gemini_get_result` | Fetch an async generation started with `async: true` by its `job_id` (status `running` → `done` result). Lets a long generation outlive a host's `tools/call` timeout |
|
|
86
|
+
| `gemini_token_usage` | Token usage and an estimated USD cost for this session so far. Call it before and after a workflow and subtract to attribute that workflow's spend. Priced per call against each call's own model from a dated rate card (`GEMINI_RATE_CARD` overrides it); there is no account-balance endpoint to read, so this is how spend is attributed |
|
|
86
87
|
| `gemini_upload_file` | Upload an image (or video/audio) to the Gemini Files API once — from a `url`, `data_base64`, or a local `path` — and get a reusable `files/<id>` reference |
|
|
87
88
|
| `gemini_list_files` | List the files currently uploaded under this API key, with MIME types and expiry times |
|
|
88
89
|
| `gemini_delete_file` | Delete an uploaded file before its ~48h expiry (confirm-gated) |
|
package/dist/bundle.js
CHANGED
|
@@ -32549,6 +32549,14 @@ var init_parse_lenient = __esm({
|
|
|
32549
32549
|
});
|
|
32550
32550
|
|
|
32551
32551
|
// node_modules/@chrischall/mcp-utils/dist/zod/index.js
|
|
32552
|
+
function toolAnnotations(opts = {}) {
|
|
32553
|
+
return {
|
|
32554
|
+
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
32555
|
+
readOnlyHint: opts.readOnly ?? true,
|
|
32556
|
+
...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
|
|
32557
|
+
...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
|
|
32558
|
+
};
|
|
32559
|
+
}
|
|
32552
32560
|
var PositiveInt, NonNegInt, NonEmptyString, IsoDate, IsoTime, NumericIdString, SafePathSegment, schemaOrigin, schemaConfirm, paginationSchema, pageSchema;
|
|
32553
32561
|
var init_zod2 = __esm({
|
|
32554
32562
|
"node_modules/@chrischall/mcp-utils/dist/zod/index.js"() {
|
|
@@ -32868,7 +32876,7 @@ async function loadStdioDotenv() {
|
|
|
32868
32876
|
}
|
|
32869
32877
|
|
|
32870
32878
|
// src/version.ts
|
|
32871
|
-
var VERSION = "1.
|
|
32879
|
+
var VERSION = "1.11.0";
|
|
32872
32880
|
|
|
32873
32881
|
// src/errors.ts
|
|
32874
32882
|
init_dist();
|
|
@@ -33813,10 +33821,13 @@ function timeoutRiskHint(opts) {
|
|
|
33813
33821
|
return lead + `If the host gives up mid-generation the image is still written to disk (and gemini_interact leaves an <image>.json sidecar with the interaction id) \u2014 no rerun needed, just look in the output dir. For a faster in-band result use a Flash model, drop image_size to 1K/2K, or \u2014 in Claude Code \u2014 set MCP_TOOL_TIMEOUT.`;
|
|
33814
33822
|
}
|
|
33815
33823
|
function buildMeta(model, seed, opts) {
|
|
33816
|
-
const meta3 = { model, seed };
|
|
33824
|
+
const meta3 = reportShape({ model, seed }, opts);
|
|
33825
|
+
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33826
|
+
return meta3;
|
|
33827
|
+
}
|
|
33828
|
+
function reportShape(meta3, opts) {
|
|
33817
33829
|
if (opts.aspect_ratio) meta3.aspect_ratio = opts.aspect_ratio;
|
|
33818
33830
|
if (opts.orientation) meta3.orientation = opts.orientation;
|
|
33819
|
-
if (opts.image_size) meta3.image_size = opts.image_size;
|
|
33820
33831
|
return meta3;
|
|
33821
33832
|
}
|
|
33822
33833
|
async function emitMedia(named, kind, opts, meta3, onWritten) {
|
|
@@ -33969,6 +33980,98 @@ function createBlobJobStore(bucket, opts) {
|
|
|
33969
33980
|
};
|
|
33970
33981
|
}
|
|
33971
33982
|
|
|
33983
|
+
// src/usage.ts
|
|
33984
|
+
var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
33985
|
+
function modalityTokens(list, modality) {
|
|
33986
|
+
if (!Array.isArray(list)) return void 0;
|
|
33987
|
+
for (const entry of list) {
|
|
33988
|
+
if (!entry || typeof entry !== "object") continue;
|
|
33989
|
+
const e = entry;
|
|
33990
|
+
if (typeof e.modality === "string" && e.modality.toLowerCase() === modality) {
|
|
33991
|
+
return num(e.tokenCount) ?? num(e.tokens);
|
|
33992
|
+
}
|
|
33993
|
+
}
|
|
33994
|
+
return void 0;
|
|
33995
|
+
}
|
|
33996
|
+
function readUsage(body) {
|
|
33997
|
+
if (!body || typeof body !== "object") return void 0;
|
|
33998
|
+
const b = body;
|
|
33999
|
+
const meta3 = b.usageMetadata;
|
|
34000
|
+
if (meta3) {
|
|
34001
|
+
const input = num(meta3.promptTokenCount);
|
|
34002
|
+
const output = num(meta3.candidatesTokenCount);
|
|
34003
|
+
const total = num(meta3.totalTokenCount);
|
|
34004
|
+
if (input !== void 0 || output !== void 0 || total !== void 0) {
|
|
34005
|
+
return compact({
|
|
34006
|
+
input_tokens: input ?? 0,
|
|
34007
|
+
output_tokens: output ?? 0,
|
|
34008
|
+
total_tokens: total ?? (input ?? 0) + (output ?? 0),
|
|
34009
|
+
image_tokens: modalityTokens(meta3.candidatesTokensDetails, "image"),
|
|
34010
|
+
video_tokens: modalityTokens(meta3.candidatesTokensDetails, "video"),
|
|
34011
|
+
audio_tokens: modalityTokens(meta3.candidatesTokensDetails, "audio"),
|
|
34012
|
+
cached_tokens: num(meta3.cachedContentTokenCount)
|
|
34013
|
+
});
|
|
34014
|
+
}
|
|
34015
|
+
}
|
|
34016
|
+
const usage = b.usage;
|
|
34017
|
+
if (usage) {
|
|
34018
|
+
const input = num(usage.total_input_tokens);
|
|
34019
|
+
const output = num(usage.total_output_tokens);
|
|
34020
|
+
const total = num(usage.total_tokens);
|
|
34021
|
+
if (input !== void 0 || output !== void 0 || total !== void 0) {
|
|
34022
|
+
return compact({
|
|
34023
|
+
input_tokens: input ?? 0,
|
|
34024
|
+
output_tokens: output ?? 0,
|
|
34025
|
+
total_tokens: total ?? (input ?? 0) + (output ?? 0),
|
|
34026
|
+
image_tokens: modalityTokens(usage.output_tokens_by_modality, "image"),
|
|
34027
|
+
video_tokens: modalityTokens(usage.output_tokens_by_modality, "video"),
|
|
34028
|
+
audio_tokens: modalityTokens(usage.output_tokens_by_modality, "audio"),
|
|
34029
|
+
cached_tokens: num(usage.total_cached_tokens),
|
|
34030
|
+
thought_tokens: num(usage.total_thought_tokens)
|
|
34031
|
+
});
|
|
34032
|
+
}
|
|
34033
|
+
}
|
|
34034
|
+
return void 0;
|
|
34035
|
+
}
|
|
34036
|
+
function compact(u) {
|
|
34037
|
+
const out = {
|
|
34038
|
+
input_tokens: u.input_tokens,
|
|
34039
|
+
output_tokens: u.output_tokens,
|
|
34040
|
+
total_tokens: u.total_tokens
|
|
34041
|
+
};
|
|
34042
|
+
if (u.image_tokens) out.image_tokens = u.image_tokens;
|
|
34043
|
+
if (u.video_tokens) out.video_tokens = u.video_tokens;
|
|
34044
|
+
if (u.audio_tokens) out.audio_tokens = u.audio_tokens;
|
|
34045
|
+
if (u.cached_tokens) out.cached_tokens = u.cached_tokens;
|
|
34046
|
+
if (u.thought_tokens) out.thought_tokens = u.thought_tokens;
|
|
34047
|
+
return out;
|
|
34048
|
+
}
|
|
34049
|
+
function sumUsage(parts) {
|
|
34050
|
+
const known = parts.filter((p) => p !== void 0);
|
|
34051
|
+
if (known.length === 0) return void 0;
|
|
34052
|
+
return compact(
|
|
34053
|
+
known.reduce(
|
|
34054
|
+
(acc, u) => ({
|
|
34055
|
+
input_tokens: acc.input_tokens + u.input_tokens,
|
|
34056
|
+
output_tokens: acc.output_tokens + u.output_tokens,
|
|
34057
|
+
total_tokens: acc.total_tokens + u.total_tokens,
|
|
34058
|
+
image_tokens: (acc.image_tokens ?? 0) + (u.image_tokens ?? 0),
|
|
34059
|
+
video_tokens: (acc.video_tokens ?? 0) + (u.video_tokens ?? 0),
|
|
34060
|
+
audio_tokens: (acc.audio_tokens ?? 0) + (u.audio_tokens ?? 0),
|
|
34061
|
+
cached_tokens: (acc.cached_tokens ?? 0) + (u.cached_tokens ?? 0),
|
|
34062
|
+
thought_tokens: (acc.thought_tokens ?? 0) + (u.thought_tokens ?? 0)
|
|
34063
|
+
}),
|
|
34064
|
+
{ input_tokens: 0, output_tokens: 0, total_tokens: 0 }
|
|
34065
|
+
)
|
|
34066
|
+
);
|
|
34067
|
+
}
|
|
34068
|
+
function annotateReusedUsage(meta3) {
|
|
34069
|
+
if (!meta3.usage) return meta3;
|
|
34070
|
+
meta3.usage_billed = false;
|
|
34071
|
+
if (meta3.cost_estimate) meta3.cost_billed = false;
|
|
34072
|
+
return meta3;
|
|
34073
|
+
}
|
|
34074
|
+
|
|
33972
34075
|
// src/jobs.ts
|
|
33973
34076
|
var JOB_TTL_MS = 6e5;
|
|
33974
34077
|
var DURABLE_JOB_REUSE_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -34049,6 +34152,7 @@ function annotateReused(result, jobId) {
|
|
|
34049
34152
|
}
|
|
34050
34153
|
obj.reused = true;
|
|
34051
34154
|
obj.reused_job_id = jobId;
|
|
34155
|
+
annotateReusedUsage(obj);
|
|
34052
34156
|
return { ...result, content: [{ ...first, text: JSON.stringify(obj, null, 2) }, ...content.slice(1)] };
|
|
34053
34157
|
}
|
|
34054
34158
|
return {
|
|
@@ -34434,6 +34538,113 @@ var JobRegistry = class {
|
|
|
34434
34538
|
};
|
|
34435
34539
|
var JOB_RECORD_RETENTION_LABEL = "30 days";
|
|
34436
34540
|
|
|
34541
|
+
// src/pricing.ts
|
|
34542
|
+
init_dist();
|
|
34543
|
+
var PRICED_AT = "2026-08-28";
|
|
34544
|
+
var RATE_CARD = Object.freeze({
|
|
34545
|
+
"gemini-3-pro-image": { input: 2, text_output: 12, image_output: 120 },
|
|
34546
|
+
"gemini-3.1-flash-image": { input: 0.5, text_output: 3, image_output: 60 },
|
|
34547
|
+
"gemini-3.1-flash-lite-image": { input: 0.25, text_output: 1.5, image_output: 30 },
|
|
34548
|
+
// Google publishes 2.5-flash-image's output as $0.039 PER IMAGE at 1290
|
|
34549
|
+
// tokens, not as a per-1M rate. 0.039 / 1290 * 1e6 = 30.23 — a back-derived
|
|
34550
|
+
// figure, unlike the four above which are published per-1M directly. The
|
|
34551
|
+
// test reconciles it against the $0.039 it came from.
|
|
34552
|
+
"gemini-2.5-flash-image": { input: 0.3, text_output: 3, image_output: 30.2326 },
|
|
34553
|
+
// Omni is token-billed for video: $17.50/1M at ~5,792 tokens per second of
|
|
34554
|
+
// 720p, which is the ~$0.10/second Google quotes. Veo is the per-second
|
|
34555
|
+
// family; this server does not use it.
|
|
34556
|
+
//
|
|
34557
|
+
// No image-output price is published for omni, so there is no image_output
|
|
34558
|
+
// here. An absent rate prices that modality at zero rather than inventing
|
|
34559
|
+
// one — and the first version of this entry DID invent one ($60, copied
|
|
34560
|
+
// from flash-image), along with a $0.50 input and $3.00 text rate that were
|
|
34561
|
+
// both a third of the real figures. Every number here is now quoted.
|
|
34562
|
+
"gemini-omni-flash": { input: 1.5, text_output: 9, video_output: 17.5 },
|
|
34563
|
+
// Lyria bills per song, so tokens describe the work and not the bill.
|
|
34564
|
+
"lyria-3-clip": { input: 0, text_output: 0, per_generation: 0.04 },
|
|
34565
|
+
"lyria-3-pro": { input: 0, text_output: 0, per_generation: 0.08 }
|
|
34566
|
+
});
|
|
34567
|
+
function normalizeModel(model) {
|
|
34568
|
+
return model.replace(/-preview$/, "");
|
|
34569
|
+
}
|
|
34570
|
+
function loadRateCard(env = process.env) {
|
|
34571
|
+
const raw = readEnvVar("GEMINI_RATE_CARD", { env });
|
|
34572
|
+
if (!raw) return { ...RATE_CARD };
|
|
34573
|
+
let parsed;
|
|
34574
|
+
try {
|
|
34575
|
+
parsed = JSON.parse(raw);
|
|
34576
|
+
} catch {
|
|
34577
|
+
return { ...RATE_CARD };
|
|
34578
|
+
}
|
|
34579
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { ...RATE_CARD };
|
|
34580
|
+
const out = { ...RATE_CARD };
|
|
34581
|
+
for (const [model, override] of Object.entries(parsed)) {
|
|
34582
|
+
if (!override || typeof override !== "object" || Array.isArray(override)) continue;
|
|
34583
|
+
const key = normalizeModel(model);
|
|
34584
|
+
const merged = { ...RATE_CARD[key] ?? {}, ...out[key] ?? {} };
|
|
34585
|
+
for (const [field, value] of Object.entries(override)) {
|
|
34586
|
+
if (typeof value === "number" && Number.isFinite(value)) merged[field] = value;
|
|
34587
|
+
}
|
|
34588
|
+
if (typeof merged.input === "number" && typeof merged.text_output === "number") {
|
|
34589
|
+
out[key] = merged;
|
|
34590
|
+
}
|
|
34591
|
+
}
|
|
34592
|
+
return out;
|
|
34593
|
+
}
|
|
34594
|
+
function estimateCost(model, usage, env = process.env) {
|
|
34595
|
+
const card = loadRateCard(env);
|
|
34596
|
+
const key = normalizeModel(model);
|
|
34597
|
+
const rates = card[key];
|
|
34598
|
+
if (!rates) return void 0;
|
|
34599
|
+
const per = (tokens, rate) => (
|
|
34600
|
+
// An absent rate prices as zero, never NaN. A modality with no published
|
|
34601
|
+
// rate is not evidence of spend, and NaN is absorbing — it would poison
|
|
34602
|
+
// every later total in the session.
|
|
34603
|
+
typeof rate === "number" && Number.isFinite(rate) ? tokens * rate / 1e6 : 0
|
|
34604
|
+
);
|
|
34605
|
+
if (typeof rates.per_generation === "number") {
|
|
34606
|
+
return {
|
|
34607
|
+
usd: rates.per_generation,
|
|
34608
|
+
breakdown: {
|
|
34609
|
+
input_usd: 0,
|
|
34610
|
+
text_output_usd: 0,
|
|
34611
|
+
image_usd: 0,
|
|
34612
|
+
video_usd: 0,
|
|
34613
|
+
audio_usd: 0,
|
|
34614
|
+
per_generation_usd: rates.per_generation
|
|
34615
|
+
},
|
|
34616
|
+
priced_at: PRICED_AT,
|
|
34617
|
+
...overriddenFor(key, rates) ? { overridden: true } : {}
|
|
34618
|
+
};
|
|
34619
|
+
}
|
|
34620
|
+
const imageTokens = usage.image_tokens ?? 0;
|
|
34621
|
+
const videoTokens = usage.video_tokens ?? 0;
|
|
34622
|
+
const audioTokens = usage.audio_tokens ?? 0;
|
|
34623
|
+
const textTokens = Math.max(0, usage.output_tokens - imageTokens - videoTokens - audioTokens);
|
|
34624
|
+
const input_usd = per(usage.input_tokens, rates.input);
|
|
34625
|
+
const text_output_usd = per(textTokens, rates.text_output);
|
|
34626
|
+
const image_usd = per(imageTokens, rates.image_output);
|
|
34627
|
+
const video_usd = per(videoTokens, rates.video_output);
|
|
34628
|
+
const audio_usd = per(audioTokens, rates.audio_output);
|
|
34629
|
+
return {
|
|
34630
|
+
usd: input_usd + text_output_usd + image_usd + video_usd + audio_usd,
|
|
34631
|
+
breakdown: { input_usd, text_output_usd, image_usd, video_usd, audio_usd },
|
|
34632
|
+
priced_at: PRICED_AT,
|
|
34633
|
+
...overriddenFor(key, rates) ? { overridden: true } : {}
|
|
34634
|
+
};
|
|
34635
|
+
}
|
|
34636
|
+
function overriddenFor(key, rates) {
|
|
34637
|
+
const shipped = RATE_CARD[key];
|
|
34638
|
+
if (!shipped) return true;
|
|
34639
|
+
return Object.keys(rates).some((f) => rates[f] !== shipped[f]);
|
|
34640
|
+
}
|
|
34641
|
+
function attachCost(meta3, model, usage) {
|
|
34642
|
+
if (!usage) return meta3;
|
|
34643
|
+
const cost = estimateCost(model, usage);
|
|
34644
|
+
if (cost) meta3.cost_estimate = cost;
|
|
34645
|
+
return meta3;
|
|
34646
|
+
}
|
|
34647
|
+
|
|
34437
34648
|
// src/tools/_confirm.ts
|
|
34438
34649
|
init_dist();
|
|
34439
34650
|
init_images();
|
|
@@ -34523,6 +34734,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34523
34734
|
let capturedGrounding;
|
|
34524
34735
|
const failed = [];
|
|
34525
34736
|
let firstError;
|
|
34737
|
+
const usages = [];
|
|
34526
34738
|
await withProgressHeartbeat(extra, `Generating ${count > 1 ? `${count} images` : "image"} (${model})`, async () => {
|
|
34527
34739
|
for (let i = 0; i < count; i++) {
|
|
34528
34740
|
try {
|
|
@@ -34540,6 +34752,7 @@ function registerGenerateTools(server, client2) {
|
|
|
34540
34752
|
timeoutMs: args.timeout_ms
|
|
34541
34753
|
});
|
|
34542
34754
|
const { images: [img], text } = result;
|
|
34755
|
+
usages.push(result.usage);
|
|
34543
34756
|
if (text && !capturedText) capturedText = text;
|
|
34544
34757
|
if (result.grounding && !capturedGrounding) capturedGrounding = result.grounding;
|
|
34545
34758
|
named.push({ image: img, base: count === 1 ? slug : `${slug}-${String(i + 1).padStart(2, "0")}` });
|
|
@@ -34551,6 +34764,9 @@ function registerGenerateTools(server, client2) {
|
|
|
34551
34764
|
});
|
|
34552
34765
|
if (named.length === 0) throw firstError;
|
|
34553
34766
|
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34767
|
+
const usage = sumUsage(usages);
|
|
34768
|
+
if (usage) meta3.usage = usage;
|
|
34769
|
+
attachCost(meta3, model, usage);
|
|
34554
34770
|
if (capturedText) meta3.text = capturedText;
|
|
34555
34771
|
if (capturedGrounding) meta3.grounding = capturedGrounding;
|
|
34556
34772
|
if (report) meta3.image_inputs = report;
|
|
@@ -34629,6 +34845,8 @@ function registerGenerateTools(server, client2) {
|
|
|
34629
34845
|
}));
|
|
34630
34846
|
const { images: [img], text } = result;
|
|
34631
34847
|
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34848
|
+
if (result.usage) meta3.usage = result.usage;
|
|
34849
|
+
attachCost(meta3, model, result.usage);
|
|
34632
34850
|
if (text) meta3.text = text;
|
|
34633
34851
|
if (result.grounding) meta3.grounding = result.grounding;
|
|
34634
34852
|
if (report) meta3.image_inputs = report;
|
|
@@ -34717,6 +34935,7 @@ function registerSetTools(server, client2) {
|
|
|
34717
34935
|
const failed = [];
|
|
34718
34936
|
const report = carried.report || masterOnly.report ? { ...masterOnly.report, ...carried.report } : void 0;
|
|
34719
34937
|
const named = [];
|
|
34938
|
+
const usages = [];
|
|
34720
34939
|
const masterResult = await withProgressHeartbeat(extra, `Generating image set (${model})`, async () => {
|
|
34721
34940
|
const result = await client2.generate({
|
|
34722
34941
|
prompt: masterPrompt,
|
|
@@ -34724,6 +34943,7 @@ function registerSetTools(server, client2) {
|
|
|
34724
34943
|
seed,
|
|
34725
34944
|
...cfg
|
|
34726
34945
|
});
|
|
34946
|
+
usages.push(result.usage);
|
|
34727
34947
|
const master = result.images[0];
|
|
34728
34948
|
named.push({ image: master, base: `${slug}-master` });
|
|
34729
34949
|
const scenePrompts = args.scenes ?? Array.from({ length: args.count ?? 0 }, () => args.master_prompt);
|
|
@@ -34740,7 +34960,9 @@ function registerSetTools(server, client2) {
|
|
|
34740
34960
|
let ref = master;
|
|
34741
34961
|
for (let i = 0; i < scenePrompts.length; i++) {
|
|
34742
34962
|
try {
|
|
34743
|
-
const
|
|
34963
|
+
const scene = await client2.generate({ prompt: composePrompt(scenePrompts[i], styleOnly), images: [ref, ...carriedRefs], seed: seed + i + 1, ...cfg });
|
|
34964
|
+
usages.push(scene.usage);
|
|
34965
|
+
const img = scene.images[0];
|
|
34744
34966
|
named.push({ image: img, base: sceneName(i) });
|
|
34745
34967
|
ref = img;
|
|
34746
34968
|
} catch (err) {
|
|
@@ -34749,7 +34971,10 @@ function registerSetTools(server, client2) {
|
|
|
34749
34971
|
}
|
|
34750
34972
|
} else {
|
|
34751
34973
|
const settled = await Promise.allSettled(
|
|
34752
|
-
scenePrompts.map((p, i) => client2.generate({ prompt: composePrompt(p, styleOnly), images: [master, ...carriedRefs], seed: seed + i + 1, ...cfg }).then((r) =>
|
|
34974
|
+
scenePrompts.map((p, i) => client2.generate({ prompt: composePrompt(p, styleOnly), images: [master, ...carriedRefs], seed: seed + i + 1, ...cfg }).then((r) => {
|
|
34975
|
+
usages.push(r.usage);
|
|
34976
|
+
return r.images[0];
|
|
34977
|
+
}))
|
|
34753
34978
|
);
|
|
34754
34979
|
settled.forEach((outcome, i) => {
|
|
34755
34980
|
if (outcome.status === "fulfilled") named.push({ image: outcome.value, base: sceneName(i) });
|
|
@@ -34760,6 +34985,9 @@ function registerSetTools(server, client2) {
|
|
|
34760
34985
|
});
|
|
34761
34986
|
const masterText = masterResult.text;
|
|
34762
34987
|
const meta3 = buildMeta(model, seed, { ...args, aspect_ratio: aspectRatio });
|
|
34988
|
+
const usage = sumUsage(usages);
|
|
34989
|
+
if (usage) meta3.usage = usage;
|
|
34990
|
+
attachCost(meta3, model, usage);
|
|
34763
34991
|
if (masterText) meta3.text = masterText;
|
|
34764
34992
|
if (masterResult.grounding) meta3.grounding = masterResult.grounding;
|
|
34765
34993
|
if (report) meta3.image_inputs = report;
|
|
@@ -35106,9 +35334,56 @@ var SessionState = class {
|
|
|
35106
35334
|
}
|
|
35107
35335
|
return hit;
|
|
35108
35336
|
}
|
|
35337
|
+
/**
|
|
35338
|
+
* Everything this session has spent, in tokens, across every upstream call.
|
|
35339
|
+
*
|
|
35340
|
+
* Accumulated in the CLIENT rather than in the tools, so a call that was
|
|
35341
|
+
* billed and then failed (a safety filter returning no image, say) still
|
|
35342
|
+
* counts — it cost money regardless of whether a tool could use the result.
|
|
35343
|
+
* Per session, never module scope: one hosted process serves many tenants,
|
|
35344
|
+
* and a shared total would report other people's spend as yours.
|
|
35345
|
+
*/
|
|
35346
|
+
usageTotal;
|
|
35347
|
+
/** How many upstream calls contributed to {@link usageTotal}. */
|
|
35348
|
+
billedCalls = 0;
|
|
35349
|
+
/**
|
|
35350
|
+
* What this session has spent in USD, priced per call.
|
|
35351
|
+
*
|
|
35352
|
+
* Accumulated as calls happen rather than derived from {@link usageTotal} at
|
|
35353
|
+
* the end, because the total spans models: summing a Pro call's tokens with
|
|
35354
|
+
* a Lite call's and pricing the result at one rate would be wrong by up to
|
|
35355
|
+
* 4x. Each call is priced against ITS model and only the money is added up.
|
|
35356
|
+
*
|
|
35357
|
+
* `undefined` while nothing priceable has run — a model absent from the rate
|
|
35358
|
+
* card contributes usage but no cost, so a total of `undefined` beside a
|
|
35359
|
+
* non-zero usage is meaningful rather than a bug. (Video and music DO price:
|
|
35360
|
+
* omni is token-billed and Lyria bills per song. It is an unknown or
|
|
35361
|
+
* newly-released model id that goes unpriced.)
|
|
35362
|
+
*/
|
|
35363
|
+
costUsd;
|
|
35364
|
+
/** How many of {@link billedCalls} could actually be priced. */
|
|
35365
|
+
pricedCalls = 0;
|
|
35366
|
+
/**
|
|
35367
|
+
* Fold one upstream call's usage into the running totals. `model` is what
|
|
35368
|
+
* makes the cost side possible; without it the tokens still count.
|
|
35369
|
+
*/
|
|
35370
|
+
recordUsage(usage, model) {
|
|
35371
|
+
if (!usage) return;
|
|
35372
|
+
this.usageTotal = sumUsage([this.usageTotal, usage]);
|
|
35373
|
+
this.billedCalls += 1;
|
|
35374
|
+
if (!model) return;
|
|
35375
|
+
const cost = estimateCost(model, usage);
|
|
35376
|
+
if (!cost) return;
|
|
35377
|
+
this.costUsd = (this.costUsd ?? 0) + cost.usd;
|
|
35378
|
+
this.pricedCalls += 1;
|
|
35379
|
+
}
|
|
35109
35380
|
/** Forget everything. Tests use this; nothing in `src/` calls it. */
|
|
35110
35381
|
reset() {
|
|
35111
35382
|
this.jobs.reset();
|
|
35383
|
+
this.usageTotal = void 0;
|
|
35384
|
+
this.billedCalls = 0;
|
|
35385
|
+
this.costUsd = void 0;
|
|
35386
|
+
this.pricedCalls = 0;
|
|
35112
35387
|
this.lastInteractionId = void 0;
|
|
35113
35388
|
this.lastMusicInteractionId = void 0;
|
|
35114
35389
|
this.lastVideoInteractionId = void 0;
|
|
@@ -35529,6 +35804,16 @@ var GeminiClient = class {
|
|
|
35529
35804
|
const data = await this.call("GET", "/models?pageSize=200");
|
|
35530
35805
|
return filterImageModels(data.models ?? []);
|
|
35531
35806
|
}
|
|
35807
|
+
/**
|
|
35808
|
+
* Fold one call's usage into the session total and hand it straight back, so
|
|
35809
|
+
* a call site reads `const usage = this.recordUsage(readUsage(data), model)` right
|
|
35810
|
+
* where the response lands — before any "returned no media" throw, so a
|
|
35811
|
+
* billed call that a safety filter emptied is still counted.
|
|
35812
|
+
*/
|
|
35813
|
+
recordUsage(usage, model) {
|
|
35814
|
+
this.session.recordUsage(usage, model);
|
|
35815
|
+
return usage;
|
|
35816
|
+
}
|
|
35532
35817
|
async generate(opts) {
|
|
35533
35818
|
const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
35534
35819
|
const parts = [{ text: opts.prompt }];
|
|
@@ -35553,6 +35838,7 @@ var GeminiClient = class {
|
|
|
35553
35838
|
requestBody,
|
|
35554
35839
|
resolveTimeoutMs(opts.timeoutMs, opts.imageSize)
|
|
35555
35840
|
);
|
|
35841
|
+
const usage = this.recordUsage(readUsage(data), model);
|
|
35556
35842
|
const images = [];
|
|
35557
35843
|
const textParts = [];
|
|
35558
35844
|
let groundingMeta;
|
|
@@ -35583,7 +35869,7 @@ var GeminiClient = class {
|
|
|
35583
35869
|
if (sources.length > 0) grounding.sources = sources;
|
|
35584
35870
|
}
|
|
35585
35871
|
}
|
|
35586
|
-
return { images, text, grounding };
|
|
35872
|
+
return { images, text, grounding, usage };
|
|
35587
35873
|
}
|
|
35588
35874
|
async interact(opts) {
|
|
35589
35875
|
const model = resolveModel(opts.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -35604,13 +35890,14 @@ var GeminiClient = class {
|
|
|
35604
35890
|
body.tools = [searchTool];
|
|
35605
35891
|
}
|
|
35606
35892
|
const data = await this.postInteraction(body, opts);
|
|
35893
|
+
const usage = this.recordUsage(readUsage(data), model);
|
|
35607
35894
|
const { images, text, grounding } = this.extractInteraction(data, opts.searchTypes);
|
|
35608
35895
|
if (images.length === 0) {
|
|
35609
35896
|
throw new McpToolError("Gemini returned no image", {
|
|
35610
35897
|
hint: "The request may have been blocked by safety filters \u2014 try rephrasing the prompt."
|
|
35611
35898
|
});
|
|
35612
35899
|
}
|
|
35613
|
-
return { id: data.id, images: await this.downloadAll(images), text, grounding };
|
|
35900
|
+
return { id: data.id, images: await this.downloadAll(images), text, grounding, usage };
|
|
35614
35901
|
}
|
|
35615
35902
|
/**
|
|
35616
35903
|
* Generate video via the omni model (Interactions API). Same plumbing as
|
|
@@ -35649,13 +35936,14 @@ var GeminiClient = class {
|
|
|
35649
35936
|
started = await this.startInteraction(body, opts, timeoutMs);
|
|
35650
35937
|
}
|
|
35651
35938
|
const data = await this.settleInteraction(started, body, timeoutMs);
|
|
35939
|
+
const usage = this.recordUsage(readUsage(data), model);
|
|
35652
35940
|
const { videos, text } = this.extractInteraction(data);
|
|
35653
35941
|
if (videos.length === 0) {
|
|
35654
35942
|
throw new McpToolError("Gemini returned no video", {
|
|
35655
35943
|
hint: "The request may have been blocked by a safety filter \u2014 try a shorter/simpler prompt or a different aspect ratio."
|
|
35656
35944
|
});
|
|
35657
35945
|
}
|
|
35658
|
-
return { id: data.id, videos: await this.downloadAll(videos), text };
|
|
35946
|
+
return { id: data.id, videos: await this.downloadAll(videos), text, usage };
|
|
35659
35947
|
}
|
|
35660
35948
|
/**
|
|
35661
35949
|
* Generate music via a Lyria model (Interactions API, inline audio). Reuses
|
|
@@ -35671,13 +35959,14 @@ var GeminiClient = class {
|
|
|
35671
35959
|
if (opts.previousInteractionId !== void 0) body.previous_interaction_id = opts.previousInteractionId;
|
|
35672
35960
|
if (opts.background === true) body.background = true;
|
|
35673
35961
|
const data = await this.postInteraction(body, opts);
|
|
35962
|
+
const usage = this.recordUsage(readUsage(data), model);
|
|
35674
35963
|
const { audios, text } = this.extractInteraction(data);
|
|
35675
35964
|
if (audios.length === 0) {
|
|
35676
35965
|
throw new McpToolError("Gemini returned no audio", {
|
|
35677
35966
|
hint: "The request may have been blocked by a safety filter \u2014 try rephrasing the prompt."
|
|
35678
35967
|
});
|
|
35679
35968
|
}
|
|
35680
|
-
return { id: data.id, audios: await this.downloadAll(audios), text };
|
|
35969
|
+
return { id: data.id, audios: await this.downloadAll(audios), text, usage };
|
|
35681
35970
|
}
|
|
35682
35971
|
/**
|
|
35683
35972
|
* POST `/interactions`, wait out the store lag on a chained 404, and — when
|
|
@@ -36194,12 +36483,14 @@ function registerInteractTools(server, client2) {
|
|
|
36194
36483
|
}
|
|
36195
36484
|
});
|
|
36196
36485
|
client2.session.lastInteractionId = r.id;
|
|
36197
|
-
const meta3 = { model, interaction_id: r.id };
|
|
36486
|
+
const meta3 = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
36198
36487
|
if (chainRecovered) meta3.chain_recovered = chainRecovered;
|
|
36199
36488
|
else if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
36200
36489
|
if (continuedFromSidecar) meta3.continued_from_sidecar = true;
|
|
36201
36490
|
if (dropped.length) meta3.dropped_previous_output = dropped;
|
|
36202
36491
|
meta3.hint = `To refine this image, call gemini_interact again with previous_interaction_id: "${r.id}" (or continue_last: true) \u2014 do NOT re-attach the returned image as an input; the interaction already contains it.`;
|
|
36492
|
+
if (r.usage) meta3.usage = r.usage;
|
|
36493
|
+
attachCost(meta3, model, r.usage);
|
|
36203
36494
|
if (r.text) meta3.text = r.text;
|
|
36204
36495
|
if (r.grounding) meta3.grounding = r.grounding;
|
|
36205
36496
|
if (report) meta3.image_inputs = report;
|
|
@@ -36306,8 +36597,10 @@ function registerVideoTools(server, client2) {
|
|
|
36306
36597
|
onInteractionStarted: ctx.reportInteraction
|
|
36307
36598
|
}));
|
|
36308
36599
|
client2.session.lastVideoInteractionId = r.id;
|
|
36309
|
-
const meta3 = { model, interaction_id: r.id };
|
|
36600
|
+
const meta3 = reportShape({ model, interaction_id: r.id }, { ...args, aspect_ratio: aspectRatio });
|
|
36310
36601
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
36602
|
+
if (r.usage) meta3.usage = r.usage;
|
|
36603
|
+
attachCost(meta3, model, r.usage);
|
|
36311
36604
|
if (r.text) meta3.text = r.text;
|
|
36312
36605
|
if (report) meta3.image_inputs = report;
|
|
36313
36606
|
meta3.hint = `To edit this video, call gemini_video_generate again with task: "edit" and previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
@@ -36405,6 +36698,8 @@ function registerMusicTools(server, client2) {
|
|
|
36405
36698
|
client2.session.lastMusicInteractionId = r.id;
|
|
36406
36699
|
const meta3 = { model, interaction_id: r.id };
|
|
36407
36700
|
if (previousInteractionId) meta3.previous_interaction_id = previousInteractionId;
|
|
36701
|
+
if (r.usage) meta3.usage = r.usage;
|
|
36702
|
+
attachCost(meta3, model, r.usage);
|
|
36408
36703
|
if (r.text) meta3.text = r.text;
|
|
36409
36704
|
if (report) meta3.image_inputs = report;
|
|
36410
36705
|
meta3.hint = `To continue this track, call gemini_music_generate again with previous_interaction_id: "${r.id}" (or continue_last: true).`;
|
|
@@ -36500,6 +36795,45 @@ function errorMessage(err) {
|
|
|
36500
36795
|
return err instanceof Error ? err.message : String(err);
|
|
36501
36796
|
}
|
|
36502
36797
|
|
|
36798
|
+
// src/tools/usage.ts
|
|
36799
|
+
init_zod();
|
|
36800
|
+
init_dist();
|
|
36801
|
+
function registerUsageTools(server, client2) {
|
|
36802
|
+
server.registerTool(
|
|
36803
|
+
"gemini_token_usage",
|
|
36804
|
+
{
|
|
36805
|
+
description: "Token usage for this session so far \u2014 what every generation has cost in tokens, added up. Call it before and after a workflow and subtract to get that workflow's cost; call it after a single generation for that call's. Reports tokens AND an estimated USD cost, priced per call against each call's own model and stamped with the date its rates were read (override with GEMINI_RATE_CARD). Note there is no account-balance endpoint to query \u2014 Google Cloud is post-paid and its billing data lags by hours \u2014 so this is the accurate way to attribute spend to a call.",
|
|
36806
|
+
annotations: toolAnnotations({ readOnly: true }),
|
|
36807
|
+
inputSchema: {
|
|
36808
|
+
reset: external_exports.boolean().optional().describe("Zero the running total after reporting it, so the next call measures from here. Use it to bracket a workflow without arithmetic.")
|
|
36809
|
+
}
|
|
36810
|
+
},
|
|
36811
|
+
async (args) => {
|
|
36812
|
+
const usage = client2.session.usageTotal;
|
|
36813
|
+
const calls = client2.session.billedCalls;
|
|
36814
|
+
const costUsd = client2.session.costUsd;
|
|
36815
|
+
const priced = client2.session.pricedCalls;
|
|
36816
|
+
if (args.reset) {
|
|
36817
|
+
client2.session.usageTotal = void 0;
|
|
36818
|
+
client2.session.billedCalls = 0;
|
|
36819
|
+
client2.session.costUsd = void 0;
|
|
36820
|
+
client2.session.pricedCalls = 0;
|
|
36821
|
+
}
|
|
36822
|
+
return textResult({
|
|
36823
|
+
usage: usage ?? null,
|
|
36824
|
+
billed_calls: calls,
|
|
36825
|
+
estimated_cost_usd: costUsd ?? null,
|
|
36826
|
+
priced_calls: priced,
|
|
36827
|
+
rates_published: PRICED_AT,
|
|
36828
|
+
...args.reset ? { reset: true } : {},
|
|
36829
|
+
note: usage === void 0 ? "No generation has run in this session yet, so nothing has been billed through it." : priced < calls ? `Estimate covers ${priced} of ${calls} billed calls \u2014 the rest used a model that is not in the rate card. Add it with GEMINI_RATE_CARD to include it.` : "Estimate priced per call against each call's own model, then added up.",
|
|
36830
|
+
rates: `Published rates read on ${PRICED_AT} (paid tier, USD). Override or extend with GEMINI_RATE_CARD if they have moved \u2014 an estimate is only as current as its card.`,
|
|
36831
|
+
scope: "This session only. A restart starts from zero, and a hosted deployment counts each authenticated session separately \u2014 these are your tokens, not the account's."
|
|
36832
|
+
});
|
|
36833
|
+
}
|
|
36834
|
+
);
|
|
36835
|
+
}
|
|
36836
|
+
|
|
36503
36837
|
// src/tools/files.ts
|
|
36504
36838
|
init_zod();
|
|
36505
36839
|
init_dist();
|
|
@@ -36950,6 +37284,7 @@ var TOOL_REGISTRARS = [
|
|
|
36950
37284
|
registerVideoTools,
|
|
36951
37285
|
registerMusicTools,
|
|
36952
37286
|
registerJobTools,
|
|
37287
|
+
registerUsageTools,
|
|
36953
37288
|
registerFileTools,
|
|
36954
37289
|
registerUploadUrlTools,
|
|
36955
37290
|
registerLibraryTools
|