package com.eps.claudemeter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.HttpURLConnection import java.net.URL import kotlin.math.roundToInt // ponytail: тот же запрос, что у Clawdmeter — POST с max_tokens=1 ради rate-limit заголовков. // HttpURLConnection (stdlib), без OkHttp. Один запрос = один Usage. object ApiPoller { private const val URL_STR = "https://api.anthropic.com/v1/messages" // ponytail: haiku — самый дешёвый, нужен только заголовок private const val BODY = """{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}""" suspend fun poll(token: String): Usage = withContext(Dispatchers.IO) { val conn = (URL(URL_STR).openConnection() as HttpURLConnection).apply { requestMethod = "POST" setRequestProperty("Authorization", "Bearer $token") setRequestProperty("anthropic-version", "2023-06-01") setRequestProperty("Content-Type", "application/json") setRequestProperty("User-Agent", "claude-code/2.1.5") doOutput = true connectTimeout = 20000 readTimeout = 20000 } try { conn.outputStream.use { it.write(BODY.toByteArray()) } val code = conn.responseCode if (code >= 400) throw Exception("HTTP $code: ${conn.errorStream?.bufferedReader()?.use { it.readText() }?.take(200)}") val h: (String) -> String? = { name -> conn.getHeaderField(name) } Usage( short = pct(h("anthropic-ratelimit-unified-5h-utilization")), shortReset = resetMin(h("anthropic-ratelimit-unified-5h-reset")), weekly = pct(h("anthropic-ratelimit-unified-7d-utilization")), weeklyReset = resetMin(h("anthropic-ratelimit-unified-7d-reset")), status = h("anthropic-ratelimit-unified-5h-status") ?: "unknown", ) } finally { conn.disconnect() } } private fun pct(s: String?): Int = s?.toFloatOrNull()?.let { (it * 100).roundToInt() } ?: 0 private fun resetMin(s: String?): Int { val ts = s?.toFloatOrNull()?.toLong() ?: return 0 val mins = (ts - System.currentTimeMillis() / 1000) / 60 return if (mins > 0) mins.toInt() else 0 } } data class Usage( val short: Int, // % загрузки 5-часового окна val shortReset: Int, // минуты до сброса val weekly: Int, // % недельного окна val weeklyReset: Int, // минуты до сброса val status: String, // normal / banned / unknown )