Files
ClaudeMeter/wear/src/main/java/com/eps/claudemeter/ApiPoller.kt
T
wenil 4083609b12 feat: ClaudeMeter — Wear OS rate-limit tracker for Claude Code
- wear: Compose for Wear UI, polls api.anthropic.com, shows 5h/7d utilization
- mobile: companion app — token input + Data Layer API + WiFi ADB install
- ADB pairing via libadb-android (SPAKE2 + TLS), pm install on watch
- Build: AGP 8.7.3, Gradle 8.13, Kotlin 1.9.22, compileSdk 35
2026-06-19 11:43:24 +03:00

62 lines
2.7 KiB
Kotlin
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.
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
)