glotfile 0.7.0 → 0.7.2

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.
@@ -2216,6 +2216,73 @@ var init_batch = __esm({
2216
2216
  }
2217
2217
  });
2218
2218
 
2219
+ // src/server/ai/pricing.ts
2220
+ function addUsage(into, add) {
2221
+ into.inputTokens += add.inputTokens;
2222
+ into.outputTokens += add.outputTokens;
2223
+ into.cacheCreationInputTokens += add.cacheCreationInputTokens;
2224
+ into.cacheReadInputTokens += add.cacheReadInputTokens;
2225
+ }
2226
+ function usageCostUsd(usage, ai, multiplier = 1) {
2227
+ if (!usage) return void 0;
2228
+ const pricing = resolvePricing(ai);
2229
+ return pricing ? estimateUsageCostUsd(usage, pricing, multiplier) : void 0;
2230
+ }
2231
+ function estimateUsageCostUsd(usage, pricing, multiplier = 1) {
2232
+ const inputCost = (usage.inputTokens + usage.cacheCreationInputTokens * CACHE_WRITE_MULTIPLIER + usage.cacheReadInputTokens * CACHE_READ_MULTIPLIER) * pricing.inputPerMTok;
2233
+ return (inputCost + usage.outputTokens * pricing.outputPerMTok) / 1e6 * multiplier;
2234
+ }
2235
+ function bareModelId(model) {
2236
+ let id = model.trim().toLowerCase();
2237
+ const slash = id.lastIndexOf("/");
2238
+ if (slash !== -1) id = id.slice(slash + 1);
2239
+ const anth = id.lastIndexOf("anthropic.");
2240
+ if (anth !== -1) id = id.slice(anth + "anthropic.".length);
2241
+ return id;
2242
+ }
2243
+ function resolvePricing(ai) {
2244
+ if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
2245
+ return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
2246
+ }
2247
+ if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
2248
+ const id = bareModelId(ai.model);
2249
+ let best;
2250
+ for (const row of PRICE_TABLE) {
2251
+ if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
2252
+ }
2253
+ return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
2254
+ }
2255
+ var BATCH_PRICE_MULTIPLIER, CACHE_WRITE_MULTIPLIER, CACHE_READ_MULTIPLIER, PRICE_TABLE, FREE_PROVIDERS;
2256
+ var init_pricing = __esm({
2257
+ "src/server/ai/pricing.ts"() {
2258
+ "use strict";
2259
+ BATCH_PRICE_MULTIPLIER = 0.5;
2260
+ CACHE_WRITE_MULTIPLIER = 1.25;
2261
+ CACHE_READ_MULTIPLIER = 0.1;
2262
+ PRICE_TABLE = [
2263
+ ["claude-fable-5", 10, 50],
2264
+ ["claude-mythos-5", 10, 50],
2265
+ // Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
2266
+ // the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
2267
+ ["claude-opus-4-1", 15, 75],
2268
+ ["claude-opus-4-0", 15, 75],
2269
+ ["claude-opus-4-2025", 15, 75],
2270
+ ["claude-opus-4", 5, 25],
2271
+ ["claude-sonnet-4", 3, 15],
2272
+ ["claude-haiku-4", 1, 5],
2273
+ ["claude-3-5-haiku", 0.8, 4],
2274
+ ["gpt-5.5-pro", 30, 180],
2275
+ ["gpt-5.5", 5, 30],
2276
+ ["gpt-5.4-pro", 30, 180],
2277
+ ["gpt-5.4-mini", 0.75, 4.5],
2278
+ ["gpt-5.4-nano", 0.2, 1.25],
2279
+ ["gpt-5.4", 2.5, 15],
2280
+ ["gpt-5.3-codex", 1.75, 14]
2281
+ ];
2282
+ FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
2283
+ }
2284
+ });
2285
+
2219
2286
  // src/server/ai/anthropic.ts
2220
2287
  import Anthropic from "@anthropic-ai/sdk";
2221
2288
  var AnthropicProvider;
@@ -2224,6 +2291,7 @@ var init_anthropic = __esm({
2224
2291
  "use strict";
2225
2292
  init_provider();
2226
2293
  init_batch();
2294
+ init_pricing();
2227
2295
  AnthropicProvider = class {
2228
2296
  constructor(config, client) {
2229
2297
  this.config = config;
@@ -2238,9 +2306,25 @@ var init_anthropic = __esm({
2238
2306
  }
2239
2307
  config;
2240
2308
  client;
2309
+ usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
2241
2310
  supportsVision() {
2242
2311
  return true;
2243
2312
  }
2313
+ recordUsage(usage) {
2314
+ if (!usage) return;
2315
+ addUsage(this.usage, {
2316
+ inputTokens: usage.input_tokens ?? 0,
2317
+ outputTokens: usage.output_tokens ?? 0,
2318
+ cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0,
2319
+ cacheReadInputTokens: usage.cache_read_input_tokens ?? 0
2320
+ });
2321
+ }
2322
+ takeUsage() {
2323
+ const taken = this.usage;
2324
+ this.usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
2325
+ const any = taken.inputTokens || taken.outputTokens || taken.cacheCreationInputTokens || taken.cacheReadInputTokens;
2326
+ return any ? taken : void 0;
2327
+ }
2244
2328
  translate(reqs, onBatchComplete, signal, onMalformedReply) {
2245
2329
  return runBatched(reqs, this.config.batchSize, (batch, sig) => this.callBatch(batch, sig), onBatchComplete, signal, onMalformedReply);
2246
2330
  }
@@ -2273,6 +2357,7 @@ var init_anthropic = __esm({
2273
2357
  output_config: { format: { type: "json_schema", schema: req.schema } },
2274
2358
  messages: [{ role: "user", content }]
2275
2359
  });
2360
+ this.recordUsage(res.usage);
2276
2361
  const text = res.content.find((b) => b.type === "text")?.text ?? "{}";
2277
2362
  try {
2278
2363
  return JSON.parse(text);
@@ -2314,6 +2399,7 @@ var init_anthropic = __esm({
2314
2399
  out.set(entry.custom_id, { type: "failed", error: entry.result.error?.message ?? entry.result.type });
2315
2400
  continue;
2316
2401
  }
2402
+ this.recordUsage(entry.result.message?.usage);
2317
2403
  const text = entry.result.message?.content.find((b) => b.type === "text")?.text ?? "";
2318
2404
  try {
2319
2405
  out.set(entry.custom_id, { type: "items", items: parseReplyItems(text) });
@@ -2336,6 +2422,7 @@ var init_anthropic = __esm({
2336
2422
  output_config: { format: { type: "json_schema", schema: BATCH_SCHEMA } },
2337
2423
  messages: [{ role: "user", content }]
2338
2424
  }, { signal });
2425
+ this.recordUsage(res.usage);
2339
2426
  const text = res.content.find((b) => b.type === "text")?.text ?? "";
2340
2427
  return parseReplyItems(text);
2341
2428
  }
@@ -3133,6 +3220,30 @@ var init_pending_batch = __esm({
3133
3220
  }
3134
3221
  });
3135
3222
 
3223
+ // src/server/log.ts
3224
+ import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
3225
+ import { resolve as resolve5 } from "path";
3226
+ function logPath(projectRoot) {
3227
+ return resolve5(projectRoot, ".glotfile", "log.jsonl");
3228
+ }
3229
+ function appendLog(projectRoot, entry) {
3230
+ ensureGlotfileDir(projectRoot);
3231
+ appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
3232
+ }
3233
+ function readLog(projectRoot, limit = 100) {
3234
+ const path = logPath(projectRoot);
3235
+ if (!existsSync7(path)) return [];
3236
+ const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
3237
+ const entries = lines.map((l) => JSON.parse(l));
3238
+ return entries.reverse().slice(0, limit);
3239
+ }
3240
+ var init_log = __esm({
3241
+ "src/server/log.ts"() {
3242
+ "use strict";
3243
+ init_glotfile_dir();
3244
+ }
3245
+ });
3246
+
3136
3247
  // src/server/ai/batch-run.ts
3137
3248
  function buildBatchJobs(reqs, batchSize) {
3138
3249
  const byLocale = /* @__PURE__ */ new Map();
@@ -3147,7 +3258,8 @@ function buildBatchJobs(reqs, batchSize) {
3147
3258
  const jobs = [];
3148
3259
  for (const [locale, group] of byLocale) {
3149
3260
  chunk(group, Math.max(1, batchSize)).forEach((batch, i) => {
3150
- jobs.push({ customId: `${locale}#${i}`, locale, requests: batch });
3261
+ const safeLocale = locale.replace(/[^a-zA-Z0-9-]/g, "-").slice(0, 56);
3262
+ jobs.push({ customId: `${safeLocale}_${i}`, locale, requests: batch });
3151
3263
  });
3152
3264
  }
3153
3265
  return jobs;
@@ -3179,7 +3291,9 @@ async function submitBatchTranslation(state, provider, reqs, batchSize, model, p
3179
3291
  return pending;
3180
3292
  }
3181
3293
  async function applyBatchResults(load, persist, provider, pending, projectRoot, ai) {
3294
+ provider.takeUsage?.();
3182
3295
  const outcomes = await provider.translationBatchResults(pending.batchId);
3296
+ const batchUsage = provider.takeUsage?.();
3183
3297
  const fresh = load();
3184
3298
  const isStale = (r) => {
3185
3299
  const entry = fresh.keys[r.key];
@@ -3188,13 +3302,19 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
3188
3302
  const applied = [];
3189
3303
  const results = [];
3190
3304
  const retryReqs = [];
3191
- let staleSkipped = 0;
3305
+ const stale = [];
3306
+ const jobFailures = [];
3192
3307
  for (const job of pending.jobs) {
3193
3308
  const outcome = outcomes.get(job.customId);
3194
3309
  const itemsById = outcome?.type === "items" ? new Map(outcome.items.map((i) => [i.id, i])) : null;
3310
+ if (!itemsById) {
3311
+ if (!outcome) jobFailures.push({ customId: job.customId, locale: job.locale, type: "missing" });
3312
+ else if (outcome.type === "malformed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "malformed", raw: outcome.raw });
3313
+ else if (outcome.type === "failed") jobFailures.push({ customId: job.customId, locale: job.locale, type: "failed", error: outcome.error });
3314
+ }
3195
3315
  for (const stored of job.requests) {
3196
3316
  if (isStale(stored)) {
3197
- staleSkipped++;
3317
+ stale.push({ key: stored.key, locale: stored.targetLocale });
3198
3318
  continue;
3199
3319
  }
3200
3320
  const { sourceHash: _hash, ...req } = stored;
@@ -3213,7 +3333,20 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
3213
3333
  const retryResults = await runLocaleParallel(
3214
3334
  retryReqs,
3215
3335
  provider,
3216
- {},
3336
+ {
3337
+ // Record the raw reply so an unparseable retry response is diagnosable
3338
+ // from the activity log instead of vanishing into per-item errors.
3339
+ onMalformedReply: (raw, batchSize, locale) => {
3340
+ appendLog(projectRoot, {
3341
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3342
+ kind: "translate",
3343
+ summary: `Malformed model reply (${locale}, batch of ${batchSize})`,
3344
+ model: pending.model,
3345
+ locale,
3346
+ raw
3347
+ });
3348
+ }
3349
+ },
3217
3350
  ai.concurrency,
3218
3351
  void 0,
3219
3352
  ai.batchSize
@@ -3221,10 +3354,34 @@ async function applyBatchResults(load, persist, provider, pending, projectRoot,
3221
3354
  applied.push(...retryReqs);
3222
3355
  results.push(...retryResults);
3223
3356
  }
3357
+ const retryUsage = provider.takeUsage?.();
3358
+ const pricing = resolvePricing({ ...ai, model: pending.model });
3359
+ let estimatedCostUsd;
3360
+ if (pricing && (batchUsage || retryUsage)) {
3361
+ estimatedCostUsd = (batchUsage ? estimateUsageCostUsd(batchUsage, pricing, BATCH_PRICE_MULTIPLIER) : 0) + (retryUsage ? estimateUsageCostUsd(retryUsage, pricing) : 0);
3362
+ }
3363
+ let usage;
3364
+ if (batchUsage || retryUsage) {
3365
+ usage = batchUsage ?? { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 };
3366
+ if (retryUsage) addUsage(usage, retryUsage);
3367
+ }
3224
3368
  const { written, errors } = applyResults(fresh, applied, results);
3225
3369
  persist(fresh);
3226
3370
  clearPendingBatch(projectRoot);
3227
- return { written, errors, staleSkipped, retried: retryReqs.length, screenshotsSkipped };
3371
+ const costSuffix = estimatedCostUsd !== void 0 ? ` (~$${estimatedCostUsd.toFixed(2)})` : "";
3372
+ appendLog(projectRoot, {
3373
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3374
+ kind: "translate",
3375
+ summary: `Applied batch ${pending.batchId}: wrote ${written}, ${errors.length} error(s), ${retryReqs.length} retried, ${stale.length} stale${costSuffix}`,
3376
+ model: pending.model,
3377
+ items: applied.map((r) => ({ id: r.id, key: r.key, source: r.source, targetLocale: r.targetLocale })),
3378
+ results,
3379
+ jobFailures: jobFailures.length ? jobFailures : void 0,
3380
+ stale: stale.length ? stale : void 0,
3381
+ usage,
3382
+ estimatedCostUsd
3383
+ });
3384
+ return { written, errors, staleSkipped: stale.length, retried: retryReqs.length, screenshotsSkipped };
3228
3385
  }
3229
3386
  var init_batch_run = __esm({
3230
3387
  "src/server/ai/batch-run.ts"() {
@@ -3233,55 +3390,8 @@ var init_batch_run = __esm({
3233
3390
  init_batch();
3234
3391
  init_run();
3235
3392
  init_pending_batch();
3236
- }
3237
- });
3238
-
3239
- // src/server/ai/pricing.ts
3240
- function bareModelId(model) {
3241
- let id = model.trim().toLowerCase();
3242
- const slash = id.lastIndexOf("/");
3243
- if (slash !== -1) id = id.slice(slash + 1);
3244
- const anth = id.lastIndexOf("anthropic.");
3245
- if (anth !== -1) id = id.slice(anth + "anthropic.".length);
3246
- return id;
3247
- }
3248
- function resolvePricing(ai) {
3249
- if (ai.inputPricePerMTok !== void 0 && ai.outputPricePerMTok !== void 0) {
3250
- return { source: "profile", inputPerMTok: ai.inputPricePerMTok, outputPerMTok: ai.outputPricePerMTok };
3251
- }
3252
- if (FREE_PROVIDERS.has(ai.provider)) return { source: "builtin", inputPerMTok: 0, outputPerMTok: 0 };
3253
- const id = bareModelId(ai.model);
3254
- let best;
3255
- for (const row of PRICE_TABLE) {
3256
- if (id.startsWith(row[0]) && (!best || row[0].length > best[0].length)) best = row;
3257
- }
3258
- return best ? { source: "builtin", inputPerMTok: best[1], outputPerMTok: best[2] } : null;
3259
- }
3260
- var PRICE_TABLE, FREE_PROVIDERS;
3261
- var init_pricing = __esm({
3262
- "src/server/ai/pricing.ts"() {
3263
- "use strict";
3264
- PRICE_TABLE = [
3265
- ["claude-fable-5", 10, 50],
3266
- ["claude-mythos-5", 10, 50],
3267
- // Deprecated Opus 4.1 / 4.0 cost 3x the 4.5+ generation — they must outrank
3268
- // the shorter "claude-opus-4" prefix (4-2025 covers the dated Opus 4 full IDs).
3269
- ["claude-opus-4-1", 15, 75],
3270
- ["claude-opus-4-0", 15, 75],
3271
- ["claude-opus-4-2025", 15, 75],
3272
- ["claude-opus-4", 5, 25],
3273
- ["claude-sonnet-4", 3, 15],
3274
- ["claude-haiku-4", 1, 5],
3275
- ["claude-3-5-haiku", 0.8, 4],
3276
- ["gpt-5.5-pro", 30, 180],
3277
- ["gpt-5.5", 5, 30],
3278
- ["gpt-5.4-pro", 30, 180],
3279
- ["gpt-5.4-mini", 0.75, 4.5],
3280
- ["gpt-5.4-nano", 0.2, 1.25],
3281
- ["gpt-5.4", 2.5, 15],
3282
- ["gpt-5.3-codex", 1.75, 14]
3283
- ];
3284
- FREE_PROVIDERS = /* @__PURE__ */ new Set(["ollama", "claude-code"]);
3393
+ init_log();
3394
+ init_pricing();
3285
3395
  }
3286
3396
  });
3287
3397
 
@@ -3348,30 +3458,6 @@ var init_estimate = __esm({
3348
3458
  }
3349
3459
  });
3350
3460
 
3351
- // src/server/log.ts
3352
- import { appendFileSync, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
3353
- import { resolve as resolve5 } from "path";
3354
- function logPath(projectRoot) {
3355
- return resolve5(projectRoot, ".glotfile", "log.jsonl");
3356
- }
3357
- function appendLog(projectRoot, entry) {
3358
- ensureGlotfileDir(projectRoot);
3359
- appendFileSync(logPath(projectRoot), JSON.stringify(entry) + "\n", "utf8");
3360
- }
3361
- function readLog(projectRoot, limit = 100) {
3362
- const path = logPath(projectRoot);
3363
- if (!existsSync7(path)) return [];
3364
- const lines = readFileSync7(path, "utf8").split("\n").filter((l) => l.trim() !== "");
3365
- const entries = lines.map((l) => JSON.parse(l));
3366
- return entries.reverse().slice(0, limit);
3367
- }
3368
- var init_log = __esm({
3369
- "src/server/log.ts"() {
3370
- "use strict";
3371
- init_glotfile_dir();
3372
- }
3373
- });
3374
-
3375
3461
  // src/server/scan.ts
3376
3462
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
3377
3463
  import { resolve as resolve6 } from "path";
@@ -6835,6 +6921,7 @@ function createApi(deps) {
6835
6921
  persist(fresh);
6836
6922
  totalWritten += written;
6837
6923
  allErrors.push(...errors);
6924
+ const usage = provider.takeUsage?.();
6838
6925
  appendLog(projectRoot, {
6839
6926
  at: (/* @__PURE__ */ new Date()).toISOString(),
6840
6927
  kind: "translate",
@@ -6845,7 +6932,9 @@ function createApi(deps) {
6845
6932
  const req = reqById.get(r.id);
6846
6933
  return { id: r.id, key: req?.key ?? "", source: req?.source ?? "", targetLocale: req?.targetLocale, context: req?.context, glossary: req?.glossary, screenshot: req ? fresh.keys[req.key]?.screenshot : void 0 };
6847
6934
  }),
6848
- results: batchResults
6935
+ results: batchResults,
6936
+ usage,
6937
+ estimatedCostUsd: usageCostUsd(usage, aiCfg)
6849
6938
  });
6850
6939
  const ld = (localeDone.get(locale) ?? 0) + batchResults.length;
6851
6940
  localeDone.set(locale, ld);
@@ -6917,11 +7006,14 @@ function createApi(deps) {
6917
7006
  }, aiCfg.concurrency, void 0, aiCfg.batchSize);
6918
7007
  const latest = load();
6919
7008
  ({ written, errors } = applyResults(latest, toTranslate, results, void 0, force));
7009
+ const usage = provider.takeUsage?.();
6920
7010
  const entry = {
6921
7011
  at: (/* @__PURE__ */ new Date()).toISOString(),
6922
7012
  kind: "translate",
6923
7013
  summary: `Translated ${toTranslate.length} item(s)`,
6924
7014
  model: aiCfg.model,
7015
+ usage,
7016
+ estimatedCostUsd: usageCostUsd(usage, aiCfg),
6925
7017
  system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
6926
7018
  // Log the screenshot PATH only — never the image bytes.
6927
7019
  items: toTranslate.map((r) => ({
@@ -7019,17 +7111,7 @@ function createApi(deps) {
7019
7111
  if (!supportsBatchTranslate(provider)) {
7020
7112
  return c.json({ error: `Provider "${aiCfg.provider}" does not support batch mode.` }, 400);
7021
7113
  }
7022
- const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, {
7023
- batchSize: aiCfg.batchSize,
7024
- concurrency: aiCfg.concurrency
7025
- });
7026
- appendLog(projectRoot, {
7027
- at: (/* @__PURE__ */ new Date()).toISOString(),
7028
- kind: "translate",
7029
- summary: `Applied batch ${pending.batchId}: wrote ${outcome.written}, ${outcome.retried} retried, ${outcome.staleSkipped} stale`,
7030
- model: aiCfg.model,
7031
- results: []
7032
- });
7114
+ const outcome = await applyBatchResults(load, persist, provider, pending, projectRoot, aiCfg);
7033
7115
  console.log(`[batch] applied ${pending.batchId} \u2014 wrote ${outcome.written}, ${outcome.errors.length} error(s)`);
7034
7116
  return c.json(outcome);
7035
7117
  }));
@@ -7180,6 +7262,7 @@ function createApi(deps) {
7180
7262
  const batch = raw;
7181
7263
  const fresh = load();
7182
7264
  const { written, errors } = applyContext(fresh, chunk2, batch.items ?? [], void 0, body.force === true);
7265
+ const usage = provider.takeUsage?.();
7183
7266
  appendLog(projectRoot, {
7184
7267
  at: (/* @__PURE__ */ new Date()).toISOString(),
7185
7268
  kind: "context",
@@ -7187,7 +7270,9 @@ function createApi(deps) {
7187
7270
  model: aiCfg.model,
7188
7271
  system,
7189
7272
  items: chunk2.map((t) => ({ id: t.id, key: t.key, source: t.source })),
7190
- results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error }))
7273
+ results: (batch.items ?? []).map((r) => ({ id: r.id, value: r.context, error: r.error })),
7274
+ usage,
7275
+ estimatedCostUsd: usageCostUsd(usage, aiCfg)
7191
7276
  });
7192
7277
  persist(fresh);
7193
7278
  totalWritten += written;
@@ -7228,6 +7313,7 @@ var init_api = __esm({
7228
7313
  init_batch_run();
7229
7314
  init_pending_batch();
7230
7315
  init_estimate();
7316
+ init_pricing();
7231
7317
  init_log();
7232
7318
  init_schema();
7233
7319
  init_run3();
@@ -7390,6 +7476,7 @@ init_provider();
7390
7476
  init_batch_run();
7391
7477
  init_pending_batch();
7392
7478
  init_estimate();
7479
+ init_pricing();
7393
7480
  init_log();
7394
7481
  init_scan();
7395
7482
  init_scanner();
@@ -7712,11 +7799,14 @@ async function runTranslate(args) {
7712
7799
  if (!batchCallbackFired) {
7713
7800
  ({ written, errors } = applyResults(state, toTranslate, results));
7714
7801
  }
7802
+ const usage = provider.takeUsage?.();
7715
7803
  appendLog(projectRoot, {
7716
7804
  at: (/* @__PURE__ */ new Date()).toISOString(),
7717
7805
  kind: "translate",
7718
7806
  summary: `Translated ${toTranslate.length} item(s)`,
7719
7807
  model: ai.model,
7808
+ usage,
7809
+ estimatedCostUsd: usageCostUsd(usage, ai),
7720
7810
  system: buildSystemPrompt(toTranslate.some((r) => r.plural !== void 0)),
7721
7811
  items: toTranslate.map((r) => ({
7722
7812
  id: r.id,
@@ -7751,7 +7841,7 @@ async function applyPending(args, provider, pending, ai) {
7751
7841
  provider,
7752
7842
  pending,
7753
7843
  projectRoot,
7754
- { batchSize: ai.batchSize, concurrency: ai.concurrency }
7844
+ ai
7755
7845
  );
7756
7846
  reportApply(outcome);
7757
7847
  }