feat(mobile): bind device identities and bound voice usage in 0.1.4 dev

This commit is contained in:
yilsem committed 2026-09-12 12:11:18 +03:00
1 parent ac6687181e
commit 5f75dcc99a
24 files changed
+909 -47

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// 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.`
}