373d10595b
Captures roughly forty version bumps (v0.2.6 → v0.2.47) of work that
accumulated without commits.
- Pluggable provider system under server/providers/: gemini, anthropic,
openai, openai-compatible, ollama, whisper-compatible, relay. Mix and
match transcription + analysis per request via the picker UI.
- Relay backend integration. Hardcoded relay URL in server/relay-default.js
(operator-controlled at build time, not user-configurable). New
/api/relay/{status,policy} endpoints proxy to the relay; balance pings
populate a cached credit display.
- Per-install identity in server/install-id.js for relay credit accounting.
Sent to the relay as X-Recap-Install-Id; persists across upgrades, lost
on a full uninstall + reinstall. Not surfaced in the UI.
- Admin login gate (server/admin-auth.js + setAdminPassword action). Scrypt
password hash + HMAC-signed session cookie.
- Entitlement scheme rename: pro / max (each paired with subscriptions and
relay_pro / relay_max), replacing the misleading "core" entitlement
that conflicted with the user-facing "Core" tier name.
- Activation screen: dynamic credit count pulled from /api/relay/policy,
"Skip — use free mode" button, accurate paid-feature list.
- Top toolbar: inline credit-balance pill (or "BYO configured" fallback),
Upgrade + "I have a key" buttons.
- Picker UI: per-provider sections with Save/Test/Delete buttons, sections
collapsible by chevron, default-collapsed unless currently selected,
"Use comped credits (reset to relay)" link when the user has strayed,
green hint under inputs whose values are server-configured.
- Activity log: chevron-collapsible groups per video, refresh-survival via
localStorage + a 500-entry server-side buffer, explicit Clear button.
- YouTube captions fast-path with user toggle (skips audio download + AI
transcription when captions are available — uncheck for speaker labels).
- Cancel button: AbortController plumbed through every provider SDK call;
retryAPI short-circuits on AbortError; cancellation events surface in
the activity log instead of silent retries.
- Long-video analysis: auto-coalesce transcript entries before building the
analysis prompt so local-model context windows (32k-ish) don't overflow.
Original entries preserved for transcript display via an index map; the
analyzer sees a coarser view but click-to-seek timestamps stay precise.
- StartOS action grouping (Setup / AI Providers) so the actions list is
navigable.
- Manifest description rewritten to reflect multi-provider support and
free-tier relay credits.
- Smaller fixes: summarize-button enablement no longer requires a Gemini
key when other providers are configured; analysis fallback chain handles
context-length and 503 capacity errors; single-segment expansion for
providers that don't return per-segment timestamps (Parakeet et al.);
many other UX polish items.
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
// Shared cost-calculation helper for the provider abstraction.
|
|
//
|
|
// Each provider knows two things:
|
|
// 1. Its pricing table (per-1M-token rates per model).
|
|
// 2. How to map its native usage shape into the normalized
|
|
// { inputTokens, outputTokens, thinkingTokens, totalTokens } shape.
|
|
//
|
|
// This module then turns (rates, normalized usage) → the cost record
|
|
// the rest of the app already understands. Same shape gemini-helpers
|
|
// `calcCost` produces, so dashboards / logs don't care which provider
|
|
// was used.
|
|
|
|
// Format a normalized usage object against a per-model rate table into
|
|
// the shared cost record. `rates` is { input, output, thinking? } in
|
|
// USD per 1M tokens; `usage` is { inputTokens, outputTokens,
|
|
// thinkingTokens, totalTokens } counts.
|
|
export function formatCost(rates, usage) {
|
|
const inputTokens = usage.inputTokens || 0;
|
|
const outputTokens = usage.outputTokens || 0;
|
|
const thinkingTokens = usage.thinkingTokens || 0;
|
|
const thinkingRate = rates.thinking != null ? rates.thinking : rates.output;
|
|
|
|
const inputCost = (inputTokens / 1_000_000) * rates.input;
|
|
const outputCost = (outputTokens / 1_000_000) * rates.output;
|
|
const thinkingCost = (thinkingTokens / 1_000_000) * thinkingRate;
|
|
const totalCost = inputCost + outputCost + thinkingCost;
|
|
|
|
return {
|
|
inputTokens,
|
|
outputTokens,
|
|
thinkingTokens,
|
|
totalTokens: usage.totalTokens || (inputTokens + outputTokens + thinkingTokens),
|
|
inputCost: inputCost.toFixed(6),
|
|
outputCost: outputCost.toFixed(6),
|
|
thinkingCost: thinkingCost.toFixed(6),
|
|
totalCost: totalCost.toFixed(6),
|
|
totalCostDisplay: totalCost < 0.01
|
|
? `$${(totalCost * 100).toFixed(3)}¢`
|
|
: `$${totalCost.toFixed(4)}`,
|
|
};
|
|
}
|
|
|
|
// Look up rates for a model in a provider's pricing table, falling back
|
|
// to the table's "default" row. Each provider defines its own table.
|
|
export function ratesFor(pricingTable, model) {
|
|
return pricingTable[model] || pricingTable["default"] || { input: 0, output: 0 };
|
|
}
|
|
|
|
// Zero-cost record — used by providers that don't charge (Ollama,
|
|
// local, openai-compatible without a known pricing table).
|
|
export function zeroCost(usage = {}) {
|
|
const inputTokens = usage.inputTokens || 0;
|
|
const outputTokens = usage.outputTokens || 0;
|
|
const thinkingTokens = usage.thinkingTokens || 0;
|
|
return {
|
|
inputTokens,
|
|
outputTokens,
|
|
thinkingTokens,
|
|
totalTokens: usage.totalTokens || (inputTokens + outputTokens + thinkingTokens),
|
|
inputCost: "0.000000",
|
|
outputCost: "0.000000",
|
|
thinkingCost: "0.000000",
|
|
totalCost: "0.000000",
|
|
totalCostDisplay: "$0.0000",
|
|
};
|
|
}
|