Skip to content

Latest commit

 

History

History
226 lines (184 loc) · 16.6 KB

File metadata and controls

226 lines (184 loc) · 16.6 KB

Официальный источник: /api/oauth/usage

Тот же процент, что показывает /usage внутри Claude Code.

Проверено: 2026-08-04, Claude Code 2.1.221, тариф Max. Эндпоинт не документирован публично. Схема снята с живого ответа и с клиента (функция fetchUtilization в бинаре). Она может измениться без предупреждения — декодер написан так, чтобы пропажа поля давала честную ошибку, а не молчаливый ноль, и чтобы приложение уходило на локальную оценку.

Запрос

GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer <accessToken>
Content-Type: application/json

Beta-заголовки не нужны — проверены три варианта, 200 даёт уже голый Bearer. Клиент Claude Code ходит сюда с таймаутом 5 с и флагом refreshOAuth: true (на 401 обновляет токен и повторяет).

Токен

OAuth-креды Claude Code, запись Keychain Claude Code-credentials (generic password). Читается она /usr/bin/security find-generic-password -w, а не своим запросом к Keychain: у утилиты доступ к записи не отбирается при обновлении токена, потому что ею же этот токен и пишется — разбор в USAGE.md. Запасной путь — SecItemCopyMatching по kSecAttrService, без права показывать диалог. Своей авторизации у ClaudeWeek нет и не будет: сервер узнаёт аккаунт по этому токену, поэтому виджет всегда показывает те же цифры, что /usage.

Другого токена этот эндпоинт не принимает — проверено 2026-08-05. Годовой токен от claude setup-token (sk-ant-oat01-…, тот же префикс, что у токена сеанса) получает 401 Invalid bearer token; тот же токен на /api/oauth/profile401 Invalid OAuth token. The provided token was not found or is malformed; заголовки anthropic-beta: oauth-2025-04-20 и anthropic-version ничего не меняют. Документация Claude Code это подтверждает: такой токен умеет «only model requests». API-ключ (sk-ant-api…) не подходит тем более — эндпоинт oauth-only.

Отсюда следствие для интерфейса: поля «свой токен» в приложении нет. Оно было и вырезано вместе с authSource — вставлять туда нечего, кроме чужого токена сеанса, который живёт около часа и не обновляется.

Своего OAuth-флоу тоже не будет. Для него нужен client_id, зарегистрированный у Anthropic; сторонним программам его не выдают, а единственный подходящий принадлежит Claude Code — тогда человек в окне согласия разрешал бы доступ не тому, кто просит. Чтение Keychain честнее: разрешение спрашивает macOS системным диалогом, и его можно не дать.

Форма записи:

{
  "claudeAiOauth": {
    "accessToken": "sk-ant-oat01-…",
    "expiresAt": 1786000000000,
    "refreshToken": "",
    "refreshTokenExpiresAt": 1786000000000,
    "rateLimitTier": "",
    "scopes": [""],
    "subscriptionType": "max"
  },
  "organizationUuid": ""
}

expiresAt — миллисекунды (значения меньше 1e11 трактуем как секунды). Токен живёт около часа.

organizationUuid читается тоже — из него и subscriptionType складывается метка аккаунта (OAuthCredentials.accountMark, вида 7f3a1b2c·max). Токен меняется раз в час, а она держится, пока не вошли другим аккаунтом, — и это единственное, чем смена аккаунта замечается вообще: в транскриптах ~/.claude/projects маркера аккаунта нет, а сервер отвечает по токену и о смене не сообщает. По расхождению метки счёт начинается заново (USAGE.md). Метка в state.json хранится целиком, сам UUID — нет: сравнивать хватает её.

Обновлением токена ClaudeWeek не занимается. Refresh-цикл — дело Claude Code; два процесса, наперегонки меняющие одну запись Keychain, теряют токен. Мы перечитываем запись перед каждым запросом и на 401 уходим на локальную оценку, а не пытаемся чинить авторизацию сами.

Ответ

Значимые поля (остальные — null или не про нас):

Поле Тип Смысл
seven_day.utilization число 0…100 расход недельного лимита — то, что показывает виджет
seven_day.resets_at ISO 8601 момент сброса недели — источник правды для окна
five_hour.utilization число 0…100 расход пятичасовой сессии — первая полоса панели
five_hour.resets_at ISO 8601 когда отпустит сессия; без него процент не показываем
limits[] массив те же лимиты списком: kind, percent, resets_at, is_active
seven_day_opus, seven_day_sonnet, seven_day_oauth_apps объект или null лимиты по моделям; на Max приходят null
extra_usage, spend объект докупленные кредиты; к недельному лимиту отношения не имеют

resets_at приходит с микросекундами и смещением вместо Z: 2026-08-07T12:00:00.357993+00:00. ISO8601DateFormatter разбирает такую строку только с .withFractionalSeconds, а строку без долей — только без этого флага, поэтому в ISO8601.parse держатся оба форматтера. Разбор доли секунды сохраняет, а округляет их уже провайдер — ниже.

Живой ответ (2026-08-04, сокращён):

{
  "five_hour": {"utilization": 41, "resets_at": "2026-08-04T20:19:59.357955+00:00",
                "limit_dollars": null, "remaining_dollars": null, "used_dollars": null},
  "seven_day": {"utilization": 50, "resets_at": "2026-08-07T12:00:00.357993+00:00",
                "limit_dollars": null, "remaining_dollars": null, "used_dollars": null},
  "seven_day_opus": null,
  "seven_day_sonnet": null,
  "limits": [
    {"group": "session", "kind": "session", "percent": 41, "is_active": false,
     "resets_at": "2026-08-04T20:19:59.357955+00:00", "severity": "normal"},
    {"group": "weekly", "kind": "weekly_all", "percent": 50, "is_active": true,
     "resets_at": "2026-08-07T12:00:00.357993+00:00", "severity": "normal"}
  ],
  "member_dashboard_available": false
}

Запасной путь

Недельная строка читается сначала из seven_day, а если он null — из limits[] по kind == "weekly_all". Оба представления в наблюдавшемся ответе совпадают; дубль держим на случай, если одно из них пропадёт.

Если не нашлось ни то ни другое — UsageError.decoding, и приложение уходит на локальную оценку. Ноль вместо процента не показываем никогда: «потрачено 0 %» — опаснее честного «не смог».

Дисциплина запросов

Ситуация Поведение
подряд идущие обновления не чаще раза в 60 с, между ними отдаётся значение из памяти
401, 403 unauthorized → локальная оценка; пауза, затем перечитываем Keychain
429, 5xx, сеть пауза 1 → 2 → 4 → 8 → 15 минут
во время паузы отдаётся последнее удачное число, но не дольше 15 минут с момента, когда оно получено; дальше — честная ошибка и уход на локальную оценку
Mac спит таймер гасится на willSleepNotification, будится на didWakeNotification

Пауза после отказа не «до перезапуска», как предполагал первоначальный план: Claude Code обновляет токен сам, и следующая попытка через минуту обычно проходит. Перезапуск приложения ради этого — лишний.

Возраст числа виден всегда. Снимок помечается моментом, когда сервер ответил, а не моментом, когда его спросили: внутри минутного троттлинга и во время паузы после отказа наружу идёт одно и то же число, и выдавать его за свежее нельзя. Старше двух минут — кружок источника желтеет и подписывается «данные offline» (PanelModel.freshFor). Старше пятнадцати — число отбрасывается совсем (OfficialProvider.staleLimit): локальная оценка по сегодняшним транскриптам честнее позавчерашнего официального процента, выданного за сегодняшний.

Единственный сетевой адресат — api.anthropic.com. Токен не логируется, не пишется на диск и не выводится в UI. В кеш (~/.config/claude-week/cache.json) кладутся только проценты, границы окна и метка времени.

Что мы делаем с ответом сверх показа

resets_at округляется до минуты — у обоих лимитов. Сервер отдаёт его с секундным дрейфом — в одном ответе 12:00:00.357993, в следующем 11:59:59.9…, — и без округления подпись прыгает между «сброс ПТ 16:00» и «сброс ПТ 15:59», а обратный отсчёт сессии дёргается на минуту туда-сюда. Сброс имеет минутное разрешение; полминуты на недельном окне не значат ничего.

Пятичасовая сессия кешируется вместе со своим resets_at. Локальный источник её не считает вовсе — веса моделей приближают недельную формулу, а не сессионную, и второе число по тем же данным было бы выдумкой. Поэтому офлайн показывается последнее официальное значение, но только пока resets_at в будущем: после сброса процент не «слегка устарел», а обнулился, так что из кеша он стирается, а строка сессии из панели исчезает.

Официальный процент калибрует локальную оценку. На каждом успешном ответе локальная стоимость недели делится на официальный процент, и полученный бюджет (weeklyBudget) вместе с моментом сброса (officialWindowEnd) сохраняется в cache.json. Когда сеть пропадает, офлайн-оценка опирается на них, а не на resetHour из конфига. Замер: 52 % официальных против 52 % офлайн-оценки.

Калибровка идёт не ниже 5 % (ResolvingProvider.minimumCalibrationPercent). Процент приходит целым числом: на 2 % ошибка округления — четверть значения, и бюджет из неё вышел бы перекошенным вдвое. В начале недели держим прежний бюджет и ждём, пока расход накопится.

Что в ответе есть, а мы не читаем

seven_day_opus, seven_day_sonnet, seven_day_oauth_apps, extra_usage, spend — лимиты по отдельным моделям и докупленные кредиты. На проверявшемся тарифе Max все приходили null, поэтому разбор не написан. На других тарифах там числа, и похожие программы их показывают: отдельной строкой под недельный лимит модели и суммой в долларах под докупленное. Пункт 5 роудмапа; перед работой обязательно снять живой ответ зондом — угадывать форму по одному пустому полю нельзя.

Чего в ответе нет

Разбивки по моделям. «Сколько ушло на Opus, а сколько на Sonnet» сервер не сообщает: seven_day_opus и seven_day_sonnet — это отдельные лимиты по моделям, а не расход, и на проверявшемся Max они приходят null; токенов и долларов по моделям в ответе нет ни в каком виде. Поэтому разбивка в панели (клик по цифрам процента ставит её на место строк дней) считается по локальным транскриптам и взвешивается по ценам моделей — итог недели при этом остаётся официальным, а доли помечены знаком у каждого числа и подписью под строками: «примерный локальный подсчёт».

Разбивки по суткам. Приходит одно число на всю неделю. Форма недели восстанавливается из локальных транскриптов и масштабируется к официальному итогу (решение §2.3 плана): доля каждых суток в локальной стоимости умножается на официальный процент. Итог поэтому точный, а полосы — правдоподобные. В панели этой оговорки нет: кружок источника говорит только «данные online» или «данные offline», а различие итога и формы живёт в --json (isEstimate) и здесь.

Как перепроверить схему

swift scripts/probe-usage.swift

Зонд читает токен из Keychain, делает один запрос и печатает ответ целиком. Токен не выводит — только префикс и длину.