Files
aiturk-hermes-ide/apps/mobile/src/voice-usage.mjs
T

88 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Paid-list-price estimate, not a quota lookup or an invoice. Sources checked
// 2026-09-08: https://ai.google.dev/gemini-api/docs/pricing and /api/live.
// Live prompt counts include history billed again on each generation request.
export const VOICE_PRICING = Object.freeze({
model: 'gemini-3.1-flash-live-preview', checked: '2026-09-08',
input: Object.freeze({ TEXT: 0.75, AUDIO: 3, IMAGE: 1, VIDEO: 1 }),
output: Object.freeze({ TEXT: 4.5, AUDIO: 12 }),
})
export const DEFAULT_VOICE_LIMITS = Object.freeze({ minutes: 10, usd: 0.25 })
export function voiceLimits(value = DEFAULT_VOICE_LIMITS) {
const minutes = Number(value.minutes), usd = Number(value.usd)
// Ephemeral credentials currently expire after ten minutes; don't offer a
// duration the connection cannot actually support.
if (!Number.isInteger(minutes) || minutes < 1 || minutes > 10 ||
!Number.isFinite(usd) || usd < 0.01 || usd > 10)
throw new Error('Ses sınırı 1–10 dakika ve 0,01–10 USD arasında olmalı.')
return { minutes, usd }
}
function count(value) {
if (value === undefined) return 0 // protobuf omits zero-valued fields
if (!Number.isSafeInteger(value) || value < 0) throw new Error('Geçersiz kullanım sayacı.')
return value
}
function modalityCost(total, details, rates) {
if (details !== undefined && !Array.isArray(details)) throw new Error('Geçersiz kullanım ayrıntısı.')
let tokens = 0, low = 0, high = 0, uncertain = false
const minimum = Math.min(...Object.values(rates)), maximum = Math.max(...Object.values(rates))
for (const detail of details || []) {
const n = count(detail?.tokenCount), rate = Object.hasOwn(rates, detail?.modality) ? rates[detail.modality] : undefined
tokens += n; low += n * (rate ?? minimum); high += n * (rate ?? maximum)
if (rate === undefined && n) uncertain = true
}
if (tokens > total) throw new Error('Kullanım toplamı ayrıntılarla uyuşmuyor.')
const remainder = total - tokens
return { low: low + remainder * minimum, high: high + remainder * maximum,
uncertain: uncertain || remainder > 0 }
}
export function estimateUsage(metadata, model) {
if (model !== VOICE_PRICING.model) throw new Error('Bu ses modelinin fiyatı doğrulanmadı.')
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata) ||
!['promptTokenCount', 'responseTokenCount', 'totalTokenCount'].some(k => k in metadata))
throw new Error('Google kullanım bilgisi eksik.')
const prompt = count(metadata.promptTokenCount), response = count(metadata.responseTokenCount)
const thoughts = count(metadata.thoughtsTokenCount), tools = count(metadata.toolUsePromptTokenCount)
const total = count(metadata.totalTokenCount)
if (metadata.totalTokenCount !== undefined && total < prompt + response)
throw new Error('Kullanım toplamı sayaçlarla uyuşmuyor.')
const input = modalityCost(prompt, metadata.promptTokensDetails, VOICE_PRICING.input)
const output = modalityCost(response, metadata.responseTokensDetails, VOICE_PRICING.output)
const tool = modalityCost(tools, metadata.toolUsePromptTokensDetails, VOICE_PRICING.input)
// Thinking/tool counters may overlap aggregate counts. Keep a range instead
// of promising a falsely precise bill or silently treating them as free.
const residual = Math.max(0, total - prompt - response)
const extraUpper = Math.max(residual * 12, thoughts * 4.5 + tool.high)
return { lowUsd: (input.low + output.low) / 1e6,
highUsd: (input.high + output.high + extraUpper) / 1e6,
uncertain: input.uncertain || output.uncertain || extraUpper > 0 || count(metadata.cachedContentTokenCount) > 0 }
}
export class VoiceUsage {
constructor(model, limits = DEFAULT_VOICE_LIMITS) {
this.model = model; this.limits = voiceLimits(limits)
this.lowUsd = 0; this.highUsd = 0; this.reports = 0; this.uncertain = false
}
add(metadata) {
// Do not de-duplicate equal reports: distinct requests can have equal
// token counts, and subtracting prior prompt counts loses history cost.
const cost = estimateUsage(metadata, this.model)
this.lowUsd += cost.lowUsd; this.highUsd += cost.highUsd
this.uncertain ||= cost.uncertain; this.reports++
return this.snapshot()
}
snapshot() {
return { lowUsd: this.lowUsd, highUsd: this.highUsd, reports: this.reports,
uncertain: this.uncertain, reached: this.highUsd >= this.limits.usd }
}
}
export function usageLabel(usage, seconds) {
const cost = !usage.reports ? 'Maliyet henüz bilinmiyor; Google kullanım bildirimi bekleniyor.' :
`Bildirilen kullanımın ücretli tarife tahmini: ${usage.lowUsd.toFixed(4)}–${usage.highUsd.toFixed(4)} USD${usage.uncertain ? ' (eksik ayrıntılar için aralık)' : ''}.`
return `Yerel süre: ${seconds} sn. ${cost} Gerçek fatura veya kalan ücretsiz kota değildir; IDE modeli dahil değildir.`
}