88 lines
4.8 KiB
JavaScript
88 lines
4.8 KiB
JavaScript
// 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.`
|
||
}
|