iobroker.ai-usage 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +73 -20
  2. package/admin/ai-usage.svg +12 -17
  3. package/admin/custom/assets/Components-52mNLgTp.js +1 -0
  4. package/admin/custom/assets/ConfigPanel-Balb6rXz.js +1 -0
  5. package/admin/custom/assets/index-CJLGptkU.js +1 -0
  6. package/admin/custom/assets/{mf-entry-bootstrap-0-eec08a33.js → mf-entry-bootstrap-0-fadc1b6e.js} +1 -1
  7. package/admin/custom/assets/virtualExposes-B7ft9DTx.js +1 -0
  8. package/admin/custom/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__AiUsageComponentSet__customComponents_js-CP4YA3u3.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__AiUsageComponentSet__customComponents_js-DblFyTyg.js} +1 -1
  9. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__AiUsageComponentSet__customComponents_js-DpYtjEm8.js +2 -0
  10. package/admin/custom/assets/{virtual_mf-exposes___mfe_internal__AiUsageComponentSet__customComponents_js-CEfffc0D.js → virtual_mf-exposes___mfe_internal__AiUsageComponentSet__customComponents_js-B9kw23ao.js} +2 -2
  11. package/admin/custom/customComponents.js +1 -1
  12. package/admin/custom/i18n/de.json +4 -1
  13. package/admin/custom/i18n/en.json +4 -1
  14. package/admin/custom/i18n/es.json +4 -1
  15. package/admin/custom/i18n/fr.json +4 -1
  16. package/admin/custom/i18n/it.json +4 -1
  17. package/admin/custom/i18n/nl.json +4 -1
  18. package/admin/custom/i18n/pl.json +4 -1
  19. package/admin/custom/i18n/pt.json +4 -1
  20. package/admin/custom/i18n/ru.json +4 -1
  21. package/admin/custom/i18n/uk.json +4 -1
  22. package/admin/custom/i18n/zh-cn.json +4 -1
  23. package/admin/custom/mf-manifest.json +1 -1
  24. package/build/lib/http.js +6 -6
  25. package/build/lib/http.js.map +2 -2
  26. package/build/lib/poll-engine.js +107 -16
  27. package/build/lib/poll-engine.js.map +2 -2
  28. package/build/lib/provider.js.map +2 -2
  29. package/build/lib/providers/chatgpt-sub.js +1 -0
  30. package/build/lib/providers/chatgpt-sub.js.map +2 -2
  31. package/build/lib/providers/claude-sub.js +9 -5
  32. package/build/lib/providers/claude-sub.js.map +2 -2
  33. package/build/lib/providers/gemini-sub.js.map +2 -2
  34. package/build/lib/snapshot-tree.js +21 -9
  35. package/build/lib/snapshot-tree.js.map +2 -2
  36. package/build/lib/totals.js.map +1 -1
  37. package/build/main.js +35 -34
  38. package/build/main.js.map +2 -2
  39. package/io-package.json +32 -1
  40. package/package.json +1 -1
  41. package/admin/custom/assets/Components-Bq1fGAd7.js +0 -1
  42. package/admin/custom/assets/ConfigPanel-DGMF4_qT.js +0 -1
  43. package/admin/custom/assets/index-CKngiVu3.js +0 -1
  44. package/admin/custom/assets/virtualExposes-Dc9myzsk.js +0 -1
  45. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__AiUsageComponentSet__customComponents_js-DlNU_Z38.js +0 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/providers/claude-sub.ts"],
4
- "sourcesContent": ["import { getJson, type JsonFetch } from \"../http\";\nimport {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport { sanitizeId } from \"../pure-helpers\";\nimport { CLAUDE_OAUTH, refreshTokens, type JsonPost } from \"./claude-auth\";\n\n/**\n * Parse a Claude subscription `GET /api/oauth/usage` response into a snapshot.\n *\n * Source-verified against the HA reference integration (trickv/hass-claude-usage):\n * the LIVE per-scope meters are the top-level `limits[]` array (kind session /\n * weekly_all / weekly_scoped + scope.model/surface, percent, resets_at); the flat\n * `five_hour`/`seven_day*` keys are placeholders on current accounts and serve only\n * as a fallback. Extra usage arrives as `extra_usage` (credits \u00D7 decimal_places) or\n * the newer `spend` schema (money objects with amount_minor/exponent).\n *\n * @param body the usage response\n * @returns the snapshot\n */\nexport function parseClaudeUsage(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected usage response\");\n }\n const raw = body as Record<string, unknown>;\n const limits: LimitWindow[] = [];\n const seen = new Set<string>();\n const push = (name: string, label: string, percent: unknown, resetsAt: unknown): void => {\n const id = sanitizeId(name);\n const value = Number(percent);\n if (!id || seen.has(id) || !Number.isFinite(value)) {\n return;\n }\n seen.add(id);\n const window: LimitWindow = { name: id, label, percent: value };\n if (typeof resetsAt === \"string\" && resetsAt) {\n window.resetAt = resetsAt;\n }\n limits.push(window);\n };\n\n // The live meters: limits[] \u2014 buckets keyed by kind + model + surface.\n if (Array.isArray(raw.limits)) {\n for (const entry of raw.limits) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const limit = entry as Record<string, unknown>;\n const kind = typeof limit.kind === \"string\" ? limit.kind : \"\";\n if (!kind) {\n continue;\n }\n const scope = (limit.scope ?? {}) as Record<string, unknown>;\n const model = ((scope.model ?? {}) as Record<string, unknown>).display_name;\n const surface = scope.surface;\n const nameParts = [kind === \"session\" ? \"session\" : kind === \"weekly_all\" ? \"week\" : kind];\n const labelParts = [\n kind === \"session\" ? \"Session (5 h)\" : kind === \"weekly_all\" ? \"Week (all models)\" : kind.replace(/_/g, \" \"),\n ];\n if (typeof model === \"string\" && model) {\n nameParts.push(model);\n labelParts.push(model);\n }\n if (typeof surface === \"string\" && surface) {\n nameParts.push(surface);\n labelParts.push(`(${surface})`);\n }\n push(nameParts.join(\"-\"), labelParts.join(\" \"), limit.percent, limit.resets_at);\n }\n }\n\n // Fallback for older payloads without the limits[] array.\n if (limits.length === 0) {\n const flat = (key: string, name: string, label: string): void => {\n const block = raw[key];\n if (typeof block === \"object\" && block !== null) {\n const data = block as Record<string, unknown>;\n push(name, label, data.utilization, data.resets_at);\n }\n };\n flat(\"five_hour\", \"session\", \"Session (5 h)\");\n flat(\"seven_day\", \"week\", \"Week (all models)\");\n flat(\"seven_day_sonnet\", \"week-sonnet\", \"Week Sonnet\");\n }\n\n const snapshot: UsageSnapshot = {};\n if (limits.length > 0) {\n snapshot.limits = limits;\n }\n applyExtraUsage(raw, snapshot);\n return snapshot;\n}\n\n/**\n * Map the extra-usage block (either schema) onto credits + monthly costs.\n *\n * @param raw the usage response\n * @param snapshot the snapshot to extend\n */\nfunction applyExtraUsage(raw: Record<string, unknown>, snapshot: UsageSnapshot): void {\n const extra = raw.extra_usage as Record<string, unknown> | undefined | null;\n const spend = raw.spend as Record<string, unknown> | undefined | null;\n if (extra && extra.is_enabled === true) {\n const divisor = 10 ** (Number.isFinite(Number(extra.decimal_places)) ? Number(extra.decimal_places) : 2);\n const used = Number(extra.used_credits);\n const limit = Number(extra.monthly_limit);\n const percent = Number(extra.utilization);\n snapshot.credits = {\n used: Number.isFinite(used) ? used / divisor : undefined,\n limit: Number.isFinite(limit) ? limit / divisor : undefined,\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (snapshot.credits.used !== undefined) {\n snapshot.costs = { month: snapshot.credits.used, currency: \"USD\" };\n }\n return;\n }\n if (spend && spend.enabled === true) {\n const money = (value: unknown): number | undefined => {\n const obj = value as Record<string, unknown> | null | undefined;\n const amount = Number(obj?.amount_minor);\n const exponent = Number(obj?.exponent);\n return Number.isFinite(amount) ? amount / 10 ** (Number.isFinite(exponent) ? exponent : 2) : undefined;\n };\n const used = money(spend.used);\n const percent = Number(spend.percent);\n snapshot.credits = {\n used,\n limit: money(spend.limit),\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (used !== undefined) {\n snapshot.costs = { month: used, currency: \"USD\" };\n }\n }\n}\n\n/**\n * The Claude subscription provider: keeps the access token fresh (refresh 60 s\n * before expiry, persisted via the store) and reads the usage meters.\n *\n * @param store the token storage\n * @param fetchJson the JSON-GET seam\n * @param postJson the JSON-POST seam (token refresh)\n * @param now clock (ms) \u2014 injected for tests\n * @returns the provider\n */\nexport function claudeSubProvider(\n store: TokenStore,\n fetchJson: JsonFetch = getJson,\n postJson: JsonPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"claude-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 run the Claude sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshTokens(cached, postJson, now());\n await store.save(cached);\n }\n const body = await fetchJson(CLAUDE_OAUTH.usageUrl, {\n Authorization: `Bearer ${cached.accessToken}`,\n \"anthropic-beta\": CLAUDE_OAUTH.betaHeader,\n // Identify ourselves \u2014 an unset/odd user agent lands in a harder-throttled\n // bucket of this endpoint (community-measured; sources in the concept doc).\n \"User-Agent\": \"ioBroker.ai-usage\",\n });\n return parseClaudeUsage(body);\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { getJson, type JsonFetch } from \"../http\";\nimport {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport { sanitizeId } from \"../pure-helpers\";\nimport { CLAUDE_OAUTH, refreshTokens, type JsonPost } from \"./claude-auth\";\n\n/**\n * Parse a Claude subscription `GET /api/oauth/usage` response into a snapshot.\n *\n * Source-verified against the HA reference integration (trickv/hass-claude-usage):\n * the LIVE per-scope meters are the top-level `limits[]` array (kind session /\n * weekly_all / weekly_scoped + scope.model/surface, percent, resets_at); the flat\n * `five_hour`/`seven_day*` keys are placeholders on current accounts and serve only\n * as a fallback. Extra usage arrives as `extra_usage` (credits \u00D7 decimal_places) or\n * the newer `spend` schema (money objects with amount_minor/exponent).\n *\n * @param body the usage response\n * @returns the snapshot\n */\nexport function parseClaudeUsage(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected usage response\");\n }\n const raw = body as Record<string, unknown>;\n const limits: LimitWindow[] = [];\n const seen = new Set<string>();\n const push = (name: string, label: string, percent: unknown, resetsAt: unknown, scoped = false): void => {\n const id = sanitizeId(name);\n const value = Number(percent);\n if (!id || seen.has(id) || !Number.isFinite(value)) {\n return;\n }\n seen.add(id);\n const window: LimitWindow = { name: id, label, percent: value };\n if (typeof resetsAt === \"string\" && resetsAt) {\n window.resetAt = resetsAt;\n }\n if (scoped) {\n window.scoped = true;\n }\n limits.push(window);\n };\n\n // The live meters: limits[] \u2014 buckets keyed by kind + model + surface.\n if (Array.isArray(raw.limits)) {\n for (const entry of raw.limits) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const limit = entry as Record<string, unknown>;\n const kind = typeof limit.kind === \"string\" ? limit.kind : \"\";\n if (!kind) {\n continue;\n }\n const scope = (limit.scope ?? {}) as Record<string, unknown>;\n const model = ((scope.model ?? {}) as Record<string, unknown>).display_name;\n const surface = scope.surface;\n const nameParts = [kind === \"session\" ? \"session\" : kind === \"weekly_all\" ? \"week\" : kind];\n const labelParts = [\n kind === \"session\" ? \"Session (5 h)\" : kind === \"weekly_all\" ? \"Week (all models)\" : kind.replace(/_/g, \" \"),\n ];\n if (typeof model === \"string\" && model) {\n nameParts.push(model);\n labelParts.push(model);\n }\n if (typeof surface === \"string\" && surface) {\n nameParts.push(surface);\n labelParts.push(`(${surface})`);\n }\n // `session` and `weekly_all` are the plan-wide meters; every other kind\n // (weekly_scoped and friends) covers ONE model and must not drive the\n // account's warning \u2014 see LimitWindow.scoped.\n const scoped = kind !== \"session\" && kind !== \"weekly_all\";\n push(nameParts.join(\"-\"), labelParts.join(\" \"), limit.percent, limit.resets_at, scoped);\n }\n }\n\n // Fallback for older payloads without the limits[] array.\n if (limits.length === 0) {\n const flat = (key: string, name: string, label: string, scoped = false): void => {\n const block = raw[key];\n if (typeof block === \"object\" && block !== null) {\n const data = block as Record<string, unknown>;\n push(name, label, data.utilization, data.resets_at, scoped);\n }\n };\n flat(\"five_hour\", \"session\", \"Session (5 h)\");\n flat(\"seven_day\", \"week\", \"Week (all models)\");\n flat(\"seven_day_sonnet\", \"week-sonnet\", \"Week Sonnet\", true);\n }\n\n const snapshot: UsageSnapshot = {};\n if (limits.length > 0) {\n snapshot.limits = limits;\n }\n applyExtraUsage(raw, snapshot);\n return snapshot;\n}\n\n/**\n * Map the extra-usage block (either schema) onto credits + monthly costs.\n *\n * @param raw the usage response\n * @param snapshot the snapshot to extend\n */\nfunction applyExtraUsage(raw: Record<string, unknown>, snapshot: UsageSnapshot): void {\n const extra = raw.extra_usage as Record<string, unknown> | undefined | null;\n const spend = raw.spend as Record<string, unknown> | undefined | null;\n if (extra && extra.is_enabled === true) {\n const divisor = 10 ** (Number.isFinite(Number(extra.decimal_places)) ? Number(extra.decimal_places) : 2);\n const used = Number(extra.used_credits);\n const limit = Number(extra.monthly_limit);\n const percent = Number(extra.utilization);\n snapshot.credits = {\n used: Number.isFinite(used) ? used / divisor : undefined,\n limit: Number.isFinite(limit) ? limit / divisor : undefined,\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (snapshot.credits.used !== undefined) {\n snapshot.costs = { month: snapshot.credits.used, currency: \"USD\" };\n }\n return;\n }\n if (spend && spend.enabled === true) {\n const money = (value: unknown): number | undefined => {\n const obj = value as Record<string, unknown> | null | undefined;\n const amount = Number(obj?.amount_minor);\n const exponent = Number(obj?.exponent);\n return Number.isFinite(amount) ? amount / 10 ** (Number.isFinite(exponent) ? exponent : 2) : undefined;\n };\n const used = money(spend.used);\n const percent = Number(spend.percent);\n snapshot.credits = {\n used,\n limit: money(spend.limit),\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (used !== undefined) {\n snapshot.costs = { month: used, currency: \"USD\" };\n }\n }\n}\n\n/**\n * The Claude subscription provider: keeps the access token fresh (refresh 60 s\n * before expiry, persisted via the store) and reads the usage meters.\n *\n * @param store the token storage\n * @param fetchJson the JSON-GET seam\n * @param postJson the JSON-POST seam (token refresh)\n * @param now clock (ms) \u2014 injected for tests\n * @returns the provider\n */\nexport function claudeSubProvider(\n store: TokenStore,\n fetchJson: JsonFetch = getJson,\n postJson: JsonPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"claude-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 run the Claude sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshTokens(cached, postJson, now());\n await store.save(cached);\n }\n const body = await fetchJson(CLAUDE_OAUTH.usageUrl, {\n Authorization: `Bearer ${cached.accessToken}`,\n \"anthropic-beta\": CLAUDE_OAUTH.betaHeader,\n // Identify ourselves \u2014 an unset/odd user agent lands in a harder-throttled\n // bucket of this endpoint (community-measured; sources in the concept doc).\n \"User-Agent\": \"ioBroker.ai-usage\",\n });\n return parseClaudeUsage(body);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/providers/gemini-sub.ts"],
4
- "sourcesContent": ["import {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport type { FormPost } from \"./chatgpt-auth\";\nimport { refreshGeminiTokens } from \"./gemini-auth\";\nimport { sanitizeId } from \"../pure-helpers\";\n\n/**\n * Google's internal Code-Assist endpoints. The `daily-` host is tried first:\n * two independent measurements report 429 on the plain host and 200 on `daily-`,\n * so the plain one stays as a fallback rather than as the default.\n */\nexport const GEMINI_HOSTS = [\n \"https://daily-cloudcode-pa.googleapis.com/v1internal\",\n \"https://cloudcode-pa.googleapis.com/v1internal\",\n] as const;\n\n/**\n * The identity that decides WHICH quota buckets Google returns. Sending the\n * Gemini-CLI identity to a paid account yields the retired free-tier set, where\n * every bucket sits at 100 % forever \u2014 a counter that never moves is worse than\n * an error, so both calls must carry this one identity.\n */\nexport const GEMINI_IDENTITY = {\n ideType: \"ANTIGRAVITY\",\n userAgent: \"antigravity/1.11.3\",\n} as const;\n\n/** A JSON POST seam that also reports which host answered. */\nexport type GeminiPost = (\n url: string,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n) => Promise<unknown>;\n\n/**\n * Call a Code-Assist method, falling back to the second host on failure.\n *\n * @param method the method name (loadCodeAssist, retrieveUserQuota)\n * @param body the request body\n * @param accessToken the bearer token\n * @param post the POST seam\n * @returns the parsed answer\n */\nasync function callCodeAssist(\n method: string,\n body: Record<string, unknown>,\n accessToken: string,\n post: GeminiPost,\n): Promise<unknown> {\n const headers = {\n Authorization: `Bearer ${accessToken}`,\n \"User-Agent\": GEMINI_IDENTITY.userAgent,\n };\n let lastError: unknown;\n for (const host of GEMINI_HOSTS) {\n try {\n return await post(`${host}:${method}`, body, headers);\n } catch (e) {\n // An auth failure is the same on every host \u2014 only retry transport trouble.\n if (e instanceof FetchError && e.kind === \"auth\") {\n throw e;\n }\n lastError = e;\n }\n }\n throw lastError instanceof Error ? lastError : new FetchError(\"network\", \"no Code-Assist host answered\");\n}\n\n/**\n * Read the project id and the tier out of a `loadCodeAssist` answer.\n *\n * @param body the parsed answer\n * @returns project id and tier label\n */\nexport function parseCodeAssist(body: unknown): { project: string; tier: string } {\n const raw = (typeof body === \"object\" && body !== null ? body : {}) as Record<string, unknown>;\n const project = typeof raw.cloudaicompanionProject === \"string\" ? raw.cloudaicompanionProject : \"\";\n const paid = (raw.paidTier ?? {}) as Record<string, unknown>;\n const current = (raw.currentTier ?? {}) as Record<string, unknown>;\n const tier =\n (typeof paid.name === \"string\" && paid.name) ||\n (typeof current.name === \"string\" && current.name) ||\n (typeof paid.id === \"string\" && paid.id) ||\n (typeof current.id === \"string\" && current.id) ||\n \"\";\n return { project, tier };\n}\n\n/**\n * Turn a `retrieveUserQuota` answer into limit windows.\n *\n * Google reports what is LEFT (`remainingFraction` 0\u20261); the tree shows utilisation,\n * so it is inverted here. A bucket without a usable fraction is skipped rather than\n * guessed \u2014 an invented 0 % would read as \"nothing used yet\".\n *\n * @param body the parsed answer\n * @returns the snapshot\n */\nexport function parseGeminiQuota(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected quota response\");\n }\n const raw = body as Record<string, unknown>;\n const buckets = Array.isArray(raw.buckets) ? raw.buckets : [];\n const limits: LimitWindow[] = [];\n const seen = new Set<string>();\n for (const entry of buckets) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const bucket = entry as Record<string, unknown>;\n const fraction = Number(bucket.remainingFraction);\n if (!Number.isFinite(fraction)) {\n continue;\n }\n const model = typeof bucket.modelId === \"string\" ? bucket.modelId : \"\";\n const kind = typeof bucket.tokenType === \"string\" ? bucket.tokenType.toLowerCase() : \"\";\n const name = sanitizeId(model || kind || \"quota\");\n if (!name || seen.has(name)) {\n continue;\n }\n seen.add(name);\n const window: LimitWindow = {\n name,\n label: model || kind || \"Quota\",\n percent: Math.round((1 - Math.min(Math.max(fraction, 0), 1)) * 1000) / 10,\n };\n if (typeof bucket.resetTime === \"string\" && bucket.resetTime) {\n window.resetAt = bucket.resetTime;\n }\n limits.push(window);\n }\n return limits.length ? { limits } : {};\n}\n\n/**\n * The Google/Gemini subscription provider.\n *\n * @param store where the tokens live\n * @param post the JSON POST seam\n * @param postForm the form POST seam (token refresh)\n * @param now clock (ms)\n * @returns the provider\n */\nexport function geminiSubProvider(\n store: TokenStore,\n post: GeminiPost,\n postForm: FormPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"gemini-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 start the Google sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshGeminiTokens(cached, postForm, now());\n await store.save(cached);\n }\n // The project id is stable per account \u2014 look it up once, then reuse it.\n if (!cached.accountRef) {\n const info = parseCodeAssist(\n await callCodeAssist(\n \"loadCodeAssist\",\n { metadata: { ideType: GEMINI_IDENTITY.ideType } },\n cached.accessToken,\n post,\n ),\n );\n if (!info.project) {\n throw new FetchError(\n \"auth\",\n \"Google returned no project for this account \u2014 a Google AI subscription (Pro/Ultra) is required\",\n );\n }\n cached = { ...cached, accountRef: info.project };\n await store.save(cached);\n }\n return parseGeminiQuota(\n await callCodeAssist(\"retrieveUserQuota\", { project: cached.accountRef }, cached.accessToken, post),\n );\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport type { FormPost } from \"./chatgpt-auth\";\nimport { refreshGeminiTokens } from \"./gemini-auth\";\nimport { sanitizeId } from \"../pure-helpers\";\n\n/**\n * Google's internal Code-Assist endpoints. The `daily-` host is tried first:\n * two independent measurements report 429 on the plain host and 200 on `daily-`,\n * so the plain one stays as a fallback rather than as the default.\n */\nexport const GEMINI_HOSTS = [\n \"https://daily-cloudcode-pa.googleapis.com/v1internal\",\n \"https://cloudcode-pa.googleapis.com/v1internal\",\n] as const;\n\n/**\n * The identity that decides WHICH quota buckets Google returns. Sending the\n * Gemini-CLI identity to a paid account yields the retired free-tier set, where\n * every bucket sits at 100 % forever \u2014 a counter that never moves is worse than\n * an error, so both calls must carry this one identity.\n */\nexport const GEMINI_IDENTITY = {\n ideType: \"ANTIGRAVITY\",\n userAgent: \"antigravity/1.11.3\",\n} as const;\n\n/** A JSON POST seam that also reports which host answered. */\nexport type GeminiPost = (\n url: string,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n) => Promise<unknown>;\n\n/**\n * Call a Code-Assist method, falling back to the second host on failure.\n *\n * @param method the method name (loadCodeAssist, retrieveUserQuota)\n * @param body the request body\n * @param accessToken the bearer token\n * @param post the POST seam\n * @returns the parsed answer\n */\nasync function callCodeAssist(\n method: string,\n body: Record<string, unknown>,\n accessToken: string,\n post: GeminiPost,\n): Promise<unknown> {\n const headers = {\n Authorization: `Bearer ${accessToken}`,\n \"User-Agent\": GEMINI_IDENTITY.userAgent,\n };\n let lastError: unknown;\n for (const host of GEMINI_HOSTS) {\n try {\n return await post(`${host}:${method}`, body, headers);\n } catch (e) {\n // An auth failure is the same on every host \u2014 only retry transport trouble.\n if (e instanceof FetchError && e.kind === \"auth\") {\n throw e;\n }\n lastError = e;\n }\n }\n throw lastError instanceof Error ? lastError : new FetchError(\"network\", \"no Code-Assist host answered\");\n}\n\n/**\n * Read the project id and the tier out of a `loadCodeAssist` answer.\n *\n * @param body the parsed answer\n * @returns project id and tier label\n */\nexport function parseCodeAssist(body: unknown): { project: string; tier: string } {\n const raw = (typeof body === \"object\" && body !== null ? body : {}) as Record<string, unknown>;\n const project = typeof raw.cloudaicompanionProject === \"string\" ? raw.cloudaicompanionProject : \"\";\n const paid = (raw.paidTier ?? {}) as Record<string, unknown>;\n const current = (raw.currentTier ?? {}) as Record<string, unknown>;\n const tier =\n (typeof paid.name === \"string\" && paid.name) ||\n (typeof current.name === \"string\" && current.name) ||\n (typeof paid.id === \"string\" && paid.id) ||\n (typeof current.id === \"string\" && current.id) ||\n \"\";\n return { project, tier };\n}\n\n/**\n * Turn a `retrieveUserQuota` answer into limit windows.\n *\n * Google reports what is LEFT (`remainingFraction` 0\u20261); the tree shows utilisation,\n * so it is inverted here. A bucket without a usable fraction is skipped rather than\n * guessed \u2014 an invented 0 % would read as \"nothing used yet\".\n *\n * @param body the parsed answer\n * @returns the snapshot\n */\nexport function parseGeminiQuota(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected quota response\");\n }\n const raw = body as Record<string, unknown>;\n const buckets = Array.isArray(raw.buckets) ? raw.buckets : [];\n const limits: LimitWindow[] = [];\n const seen = new Set<string>();\n for (const entry of buckets) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const bucket = entry as Record<string, unknown>;\n const fraction = Number(bucket.remainingFraction);\n if (!Number.isFinite(fraction)) {\n continue;\n }\n const model = typeof bucket.modelId === \"string\" ? bucket.modelId : \"\";\n const kind = typeof bucket.tokenType === \"string\" ? bucket.tokenType.toLowerCase() : \"\";\n const name = sanitizeId(model || kind || \"quota\");\n if (!name || seen.has(name)) {\n continue;\n }\n seen.add(name);\n // NOT marked as `scoped`: Google reports no plan-wide bucket at all, so these\n // per-model buckets ARE the user's quota. Marking them would leave the account\n // without any window that can raise a warning.\n const window: LimitWindow = {\n name,\n label: model || kind || \"Quota\",\n percent: Math.round((1 - Math.min(Math.max(fraction, 0), 1)) * 1000) / 10,\n };\n if (typeof bucket.resetTime === \"string\" && bucket.resetTime) {\n window.resetAt = bucket.resetTime;\n }\n limits.push(window);\n }\n return limits.length ? { limits } : {};\n}\n\n/**\n * The Google/Gemini subscription provider.\n *\n * @param store where the tokens live\n * @param post the JSON POST seam\n * @param postForm the form POST seam (token refresh)\n * @param now clock (ms)\n * @returns the provider\n */\nexport function geminiSubProvider(\n store: TokenStore,\n post: GeminiPost,\n postForm: FormPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"gemini-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 start the Google sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshGeminiTokens(cached, postForm, now());\n await store.save(cached);\n }\n // The project id is stable per account \u2014 look it up once, then reuse it.\n if (!cached.accountRef) {\n const info = parseCodeAssist(\n await callCodeAssist(\n \"loadCodeAssist\",\n { metadata: { ideType: GEMINI_IDENTITY.ideType } },\n cached.accessToken,\n post,\n ),\n );\n if (!info.project) {\n throw new FetchError(\n \"auth\",\n \"Google returned no project for this account \u2014 a Google AI subscription (Pro/Ultra) is required\",\n );\n }\n cached = { ...cached, accountRef: info.project };\n await store.save(cached);\n }\n return parseGeminiQuota(\n await callCodeAssist(\"retrieveUserQuota\", { project: cached.accountRef }, cached.accessToken, post),\n );\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var snapshot_tree_exports = {};
20
20
  __export(snapshot_tree_exports, {
21
+ limitingWindow: () => limitingWindow,
21
22
  mapSnapshot: () => mapSnapshot,
22
23
  maxLimitPercent: () => maxLimitPercent
23
24
  });
@@ -156,19 +157,30 @@ function mapSnapshot(accountId, accountName, provider, snapshot) {
156
157
  }
157
158
  return { objects, writes };
158
159
  }
159
- function maxLimitPercent(snapshot) {
160
- var _a, _b, _c;
161
- const percents = [
162
- ...(_b = (_a = snapshot.limits) == null ? void 0 : _a.map((limit) => limit.percent)) != null ? _b : [],
163
- ...((_c = snapshot.credits) == null ? void 0 : _c.percent) !== void 0 ? [snapshot.credits.percent] : []
164
- ];
165
- if (percents.length === 0) {
166
- return void 0;
160
+ function limitingWindow(snapshot) {
161
+ var _a, _b;
162
+ let best;
163
+ for (const limit of (_a = snapshot.limits) != null ? _a : []) {
164
+ if (limit.scoped) {
165
+ continue;
166
+ }
167
+ if (!best || limit.percent > best.percent) {
168
+ best = { percent: limit.percent, label: limit.label };
169
+ }
167
170
  }
168
- return Math.max(...percents);
171
+ const credits = (_b = snapshot.credits) == null ? void 0 : _b.percent;
172
+ if (credits !== void 0 && (!best || credits > best.percent)) {
173
+ best = { percent: credits, label: "Credits" };
174
+ }
175
+ return best;
176
+ }
177
+ function maxLimitPercent(snapshot) {
178
+ var _a;
179
+ return (_a = limitingWindow(snapshot)) == null ? void 0 : _a.percent;
169
180
  }
170
181
  // Annotate the CommonJS export names for ESM import in node:
171
182
  0 && (module.exports = {
183
+ limitingWindow,
172
184
  mapSnapshot,
173
185
  maxLimitPercent
174
186
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/snapshot-tree.ts"],
4
- "sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: string | Record<string, string>;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: string,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created,\n * so the tree mirrors exactly what the provider's source delivers.\n *\n * @param accountId the id-safe account id (device node)\n * @param accountName the display name for the device object\n * @param provider the provider kind (device object name suffix)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(\n accountId: string,\n accountName: string,\n provider: string,\n snapshot: UsageSnapshot,\n): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: string): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n\n objects.push({ id: accountId, type: \"device\", common: { name: `${accountName} (${provider})` } });\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, \"Limit windows\");\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n channel(`${accountId}.limits.${windowId}`, limit.label);\n add(\n state(`${accountId}.limits.${windowId}.percent`, `${limit.label} used`, \"number\", \"value\", limit.percent, \"%\"),\n );\n if (limit.resetAt !== undefined) {\n add(\n state(`${accountId}.limits.${windowId}.resetAt`, `${limit.label} resets at`, \"string\", \"date\", limit.resetAt),\n );\n }\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, \"Credits\");\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, \"Credits used\", \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, \"Credits limit\", \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(state(`${accountId}.credits.remaining`, \"Credits remaining\", \"number\", \"value\", credits.remaining, unit));\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, \"Credits used (percent)\", \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(state(`${accountId}.credits.granted`, \"Granted balance\", \"number\", \"value\", credits.granted, unit));\n }\n if (credits.toppedUp !== undefined) {\n add(state(`${accountId}.credits.toppedUp`, \"Topped-up balance\", \"number\", \"value\", credits.toppedUp, unit));\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, \"Costs\");\n if (costs.today !== undefined) {\n add(state(`${accountId}.costs.today`, \"Costs today\", \"number\", \"value\", costs.today, costs.currency));\n }\n if (costs.month !== undefined) {\n add(state(`${accountId}.costs.month`, \"Costs this month\", \"number\", \"value\", costs.month, costs.currency));\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, \"Costs total (lifetime)\", \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n \"Costs projected month-end (computed)\",\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, \"Tokens\");\n if (tokens.inputToday !== undefined) {\n add(state(`${accountId}.tokens.inputToday`, \"Input tokens today\", \"number\", \"value\", tokens.inputToday));\n }\n if (tokens.outputToday !== undefined) {\n add(state(`${accountId}.tokens.outputToday`, \"Output tokens today\", \"number\", \"value\", tokens.outputToday));\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, \"Per-model usage\");\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n channel(`${accountId}.models.${modelId}`, model.model);\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n `${model.model} tokens today`,\n \"number\",\n \"value\",\n model.tokens,\n ),\n );\n }\n if (model.cost !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.costToday`,\n `${model.model} costs today`,\n \"number\",\n \"value\",\n model.cost,\n snapshot.costs?.currency ?? \"USD\",\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n add(state(`${accountId}.available`, \"Balance sufficient for calls\", \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * The highest limit percent in a snapshot, or undefined when it has no windows.\n *\n * @param snapshot the snapshot\n * @returns the maximum percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n const percents = [\n ...(snapshot.limits?.map(limit => limit.percent) ?? []),\n ...(snapshot.credits?.percent !== undefined ? [snapshot.credits.percent] : []),\n ];\n if (percents.length === 0) {\n return undefined;\n }\n return Math.max(...percents);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { sanitizeId } from \"./pure-helpers\";\n\n/** An object to create: id (relative to the instance), type and common. */\nexport interface ObjectDef {\n /** Object id relative to the instance root. */\n id: string;\n /** ioBroker object type. */\n type: \"device\" | \"channel\" | \"state\";\n /** The object's common. */\n common: {\n name: string | Record<string, string>;\n type?: \"boolean\" | \"number\" | \"string\";\n role?: string;\n read?: boolean;\n write?: boolean;\n unit?: string;\n min?: number;\n max?: number;\n };\n}\n\n/** A state value to write (ack). */\nexport interface StateWrite {\n /** State id relative to the instance root. */\n id: string;\n /** The value. */\n value: boolean | number | string;\n}\n\n/**\n * A read-only state definition plus its value \u2014 the tree builder's working unit.\n *\n * @param id the state id\n * @param name the object name\n * @param type the value type\n * @param role the state role\n * @param value the current value\n * @param unit optional unit\n * @returns the def/value pair\n */\nfunction state(\n id: string,\n name: string,\n type: \"boolean\" | \"number\" | \"string\",\n role: string,\n value: boolean | number | string,\n unit?: string,\n): { def: ObjectDef; write: StateWrite } {\n const common: ObjectDef[\"common\"] = { name, type, role, read: true, write: false };\n if (unit !== undefined) {\n common.unit = unit;\n }\n return { def: { id, type: \"state\", common }, write: { id, value } };\n}\n\n/** The result of mapping one snapshot: objects to upsert (parents first) and values to write. */\nexport interface TreeResult {\n /** Objects in creation order (device, channels, states). */\n objects: ObjectDef[];\n /** State values to write with ack. */\n writes: StateWrite[];\n}\n\n/**\n * Map one account snapshot onto the harmonised object tree: `limits.*`, `credits.*`,\n * `costs.*`, `tokens.*` (+ `models.*`) \u2014 only what the snapshot carries is created,\n * so the tree mirrors exactly what the provider's source delivers.\n *\n * @param accountId the id-safe account id (device node)\n * @param accountName the display name for the device object\n * @param provider the provider kind (device object name suffix)\n * @param snapshot the fetched snapshot\n * @returns objects (parents first) and state writes\n */\nexport function mapSnapshot(\n accountId: string,\n accountName: string,\n provider: string,\n snapshot: UsageSnapshot,\n): TreeResult {\n const objects: ObjectDef[] = [];\n const writes: StateWrite[] = [];\n const add = (pair: { def: ObjectDef; write: StateWrite }): void => {\n objects.push(pair.def);\n writes.push(pair.write);\n };\n const channel = (id: string, name: string): void => {\n objects.push({ id, type: \"channel\", common: { name } });\n };\n\n objects.push({ id: accountId, type: \"device\", common: { name: `${accountName} (${provider})` } });\n\n if (snapshot.limits && snapshot.limits.length > 0) {\n channel(`${accountId}.limits`, \"Limit windows\");\n for (const limit of snapshot.limits) {\n const windowId = sanitizeId(limit.name);\n if (!windowId) {\n continue;\n }\n channel(`${accountId}.limits.${windowId}`, limit.label);\n add(\n state(`${accountId}.limits.${windowId}.percent`, `${limit.label} used`, \"number\", \"value\", limit.percent, \"%\"),\n );\n if (limit.resetAt !== undefined) {\n add(\n state(`${accountId}.limits.${windowId}.resetAt`, `${limit.label} resets at`, \"string\", \"date\", limit.resetAt),\n );\n }\n }\n }\n\n const credits = snapshot.credits;\n if (credits) {\n channel(`${accountId}.credits`, \"Credits\");\n const unit = credits.pieces ? \"\" : credits.currency;\n if (credits.used !== undefined) {\n add(state(`${accountId}.credits.used`, \"Credits used\", \"number\", \"value\", credits.used, unit));\n }\n if (credits.limit !== undefined) {\n add(state(`${accountId}.credits.limit`, \"Credits limit\", \"number\", \"value\", credits.limit, unit));\n }\n if (credits.remaining !== undefined) {\n add(state(`${accountId}.credits.remaining`, \"Credits remaining\", \"number\", \"value\", credits.remaining, unit));\n }\n if (credits.percent !== undefined) {\n add(state(`${accountId}.credits.percent`, \"Credits used (percent)\", \"number\", \"value\", credits.percent, \"%\"));\n }\n if (credits.granted !== undefined) {\n add(state(`${accountId}.credits.granted`, \"Granted balance\", \"number\", \"value\", credits.granted, unit));\n }\n if (credits.toppedUp !== undefined) {\n add(state(`${accountId}.credits.toppedUp`, \"Topped-up balance\", \"number\", \"value\", credits.toppedUp, unit));\n }\n }\n\n const costs = snapshot.costs;\n if (costs) {\n channel(`${accountId}.costs`, \"Costs\");\n if (costs.today !== undefined) {\n add(state(`${accountId}.costs.today`, \"Costs today\", \"number\", \"value\", costs.today, costs.currency));\n }\n if (costs.month !== undefined) {\n add(state(`${accountId}.costs.month`, \"Costs this month\", \"number\", \"value\", costs.month, costs.currency));\n }\n if (costs.total !== undefined) {\n add(state(`${accountId}.costs.total`, \"Costs total (lifetime)\", \"number\", \"value\", costs.total, costs.currency));\n }\n if (costs.projectedMonth !== undefined) {\n add(\n state(\n `${accountId}.costs.projectedMonth`,\n \"Costs projected month-end (computed)\",\n \"number\",\n \"value\",\n costs.projectedMonth,\n costs.currency,\n ),\n );\n }\n }\n\n const tokens = snapshot.tokens;\n if (tokens) {\n channel(`${accountId}.tokens`, \"Tokens\");\n if (tokens.inputToday !== undefined) {\n add(state(`${accountId}.tokens.inputToday`, \"Input tokens today\", \"number\", \"value\", tokens.inputToday));\n }\n if (tokens.outputToday !== undefined) {\n add(state(`${accountId}.tokens.outputToday`, \"Output tokens today\", \"number\", \"value\", tokens.outputToday));\n }\n if (tokens.perModel && tokens.perModel.length > 0) {\n channel(`${accountId}.models`, \"Per-model usage\");\n for (const model of tokens.perModel) {\n const modelId = sanitizeId(model.model);\n if (!modelId) {\n continue;\n }\n channel(`${accountId}.models.${modelId}`, model.model);\n if (model.tokens !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.tokensToday`,\n `${model.model} tokens today`,\n \"number\",\n \"value\",\n model.tokens,\n ),\n );\n }\n if (model.cost !== undefined) {\n add(\n state(\n `${accountId}.models.${modelId}.costToday`,\n `${model.model} costs today`,\n \"number\",\n \"value\",\n model.cost,\n snapshot.costs?.currency ?? \"USD\",\n ),\n );\n }\n }\n }\n }\n\n if (snapshot.available !== undefined) {\n add(state(`${accountId}.available`, \"Balance sufficient for calls\", \"boolean\", \"indicator\", snapshot.available));\n }\n\n return { objects, writes };\n}\n\n/**\n * The window that decides how full an account is: the highest PLAN-WIDE limit, or\n * the granted budget when the account has no windows at all.\n *\n * Windows marked `scoped` are left out on purpose \u2014 they cover a single model and\n * may sit at 100 % permanently for a model the user never touches, which would\n * pin the account's warning on forever. Their datapoints still exist; they just\n * do not speak for the account.\n *\n * @param snapshot the snapshot\n * @returns percent plus the label that produced it, or undefined when nothing applies\n */\nexport function limitingWindow(snapshot: UsageSnapshot): { percent: number; label: string } | undefined {\n let best: { percent: number; label: string } | undefined;\n for (const limit of snapshot.limits ?? []) {\n if (limit.scoped) {\n continue;\n }\n if (!best || limit.percent > best.percent) {\n best = { percent: limit.percent, label: limit.label };\n }\n }\n const credits = snapshot.credits?.percent;\n if (credits !== undefined && (!best || credits > best.percent)) {\n best = { percent: credits, label: \"Credits\" };\n }\n return best;\n}\n\n/**\n * The highest plan-wide limit percent in a snapshot, or undefined when it has none.\n *\n * @param snapshot the snapshot\n * @returns the maximum percent\n */\nexport function maxLimitPercent(snapshot: UsageSnapshot): number | undefined {\n return limitingWindow(snapshot)?.percent;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/totals.ts"],
4
- "sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { maxLimitPercent } from \"./snapshot-tree\";\n\n/** One account's contribution to the totals. */\nexport interface AccountStatus {\n /** The last successful snapshot, if any. */\n snapshot?: UsageSnapshot;\n /** Whether the account is currently reachable. */\n reachable: boolean;\n /** Whether the account is above its warn threshold. */\n warning: boolean;\n}\n\n/** The adapter-wide totals. */\nexport interface Totals {\n /** Summed real money spent today (same-currency accounts only). */\n costsToday: number;\n /** Summed real money spent this month. */\n costsMonth: number;\n /** Summed projected month-end spend. */\n costsProjectedMonth: number;\n /** The currency the sums are in. */\n currency: string;\n /** The highest limit utilisation of any account (percent). */\n maxLimitPercent: number;\n /** Number of accounts above their warn threshold. */\n warningsActive: number;\n /** True when any limit window is full (>= 100 %). */\n limitReached: boolean;\n /** Reachable accounts. */\n accountsReachable: number;\n /** Configured (enabled) accounts. */\n accounts: number;\n}\n\n/** The currency the totals are summed in. Non-matching and piece-counters stay out. */\nconst TOTAL_CURRENCY = \"USD\";\n\n/**\n * Compute the adapter-wide totals from the in-memory account statuses. Money sums\n * include only real-money costs in {@link TOTAL_CURRENCY}; piece-counters and\n * foreign currencies are excluded by design.\n *\n * @param statuses each account's status\n * @returns the totals\n */\nexport function computeTotals(statuses: readonly AccountStatus[]): Totals {\n let costsToday = 0;\n let costsMonth = 0;\n let costsProjectedMonth = 0;\n let maxPercent = 0;\n let warningsActive = 0;\n let limitReached = false;\n let reachable = 0;\n for (const status of statuses) {\n if (status.reachable) {\n reachable++;\n }\n if (status.warning) {\n warningsActive++;\n }\n const snapshot = status.snapshot;\n if (!snapshot) {\n continue;\n }\n const costs = snapshot.costs;\n if (costs && costs.currency === TOTAL_CURRENCY) {\n costsToday += costs.today ?? 0;\n costsMonth += costs.month ?? 0;\n costsProjectedMonth += costs.projectedMonth ?? costs.month ?? 0;\n }\n const percent = maxLimitPercent(snapshot);\n if (percent !== undefined) {\n maxPercent = Math.max(maxPercent, percent);\n if (percent >= 100) {\n limitReached = true;\n }\n }\n }\n return {\n costsToday: round2(costsToday),\n costsMonth: round2(costsMonth),\n costsProjectedMonth: round2(costsProjectedMonth),\n currency: TOTAL_CURRENCY,\n maxLimitPercent: round2(maxPercent),\n warningsActive,\n limitReached,\n accountsReachable: reachable,\n accounts: statuses.length,\n };\n}\n\n/**\n * Round to two decimals (money/percent display).\n *\n * @param value the value\n * @returns the rounded value\n */\nfunction round2(value: number): number {\n return Math.round(value * 100) / 100;\n}\n"],
4
+ "sourcesContent": ["import type { UsageSnapshot } from \"./provider\";\nimport { maxLimitPercent } from \"./snapshot-tree\";\n\n/** One account's contribution to the totals. */\nexport interface AccountStatus {\n /** The last successful snapshot, if any. */\n snapshot?: UsageSnapshot;\n /** Whether the account is currently reachable. */\n reachable: boolean;\n /** Whether the account is above its warn threshold. */\n warning: boolean;\n}\n\n/** The adapter-wide totals. */\nexport interface Totals {\n /** Summed real money spent today (same-currency accounts only). */\n costsToday: number;\n /** Summed real money spent this month. */\n costsMonth: number;\n /** Summed projected month-end spend. */\n costsProjectedMonth: number;\n /** The currency the sums are in. */\n currency: string;\n /** The highest PLAN-WIDE limit utilisation of any account (percent); model-scoped windows stay out. */\n maxLimitPercent: number;\n /** Number of accounts above their warn threshold. */\n warningsActive: number;\n /** True when any plan-wide limit window is full (>= 100 %). */\n limitReached: boolean;\n /** Reachable accounts. */\n accountsReachable: number;\n /** Configured (enabled) accounts. */\n accounts: number;\n}\n\n/** The currency the totals are summed in. Non-matching and piece-counters stay out. */\nconst TOTAL_CURRENCY = \"USD\";\n\n/**\n * Compute the adapter-wide totals from the in-memory account statuses. Money sums\n * include only real-money costs in {@link TOTAL_CURRENCY}; piece-counters and\n * foreign currencies are excluded by design.\n *\n * @param statuses each account's status\n * @returns the totals\n */\nexport function computeTotals(statuses: readonly AccountStatus[]): Totals {\n let costsToday = 0;\n let costsMonth = 0;\n let costsProjectedMonth = 0;\n let maxPercent = 0;\n let warningsActive = 0;\n let limitReached = false;\n let reachable = 0;\n for (const status of statuses) {\n if (status.reachable) {\n reachable++;\n }\n if (status.warning) {\n warningsActive++;\n }\n const snapshot = status.snapshot;\n if (!snapshot) {\n continue;\n }\n const costs = snapshot.costs;\n if (costs && costs.currency === TOTAL_CURRENCY) {\n costsToday += costs.today ?? 0;\n costsMonth += costs.month ?? 0;\n costsProjectedMonth += costs.projectedMonth ?? costs.month ?? 0;\n }\n const percent = maxLimitPercent(snapshot);\n if (percent !== undefined) {\n maxPercent = Math.max(maxPercent, percent);\n if (percent >= 100) {\n limitReached = true;\n }\n }\n }\n return {\n costsToday: round2(costsToday),\n costsMonth: round2(costsMonth),\n costsProjectedMonth: round2(costsProjectedMonth),\n currency: TOTAL_CURRENCY,\n maxLimitPercent: round2(maxPercent),\n warningsActive,\n limitReached,\n accountsReachable: reachable,\n accounts: statuses.length,\n };\n}\n\n/**\n * Round to two decimals (money/percent display).\n *\n * @param value the value\n * @returns the rounded value\n */\nfunction round2(value: number): number {\n return Math.round(value * 100) / 100;\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,2BAAgC;AAmChC,MAAM,iBAAiB;AAUhB,SAAS,cAAc,UAA4C;AA9C1E;AA+CE,MAAI,aAAa;AACjB,MAAI,aAAa;AACjB,MAAI,sBAAsB;AAC1B,MAAI,aAAa;AACjB,MAAI,iBAAiB;AACrB,MAAI,eAAe;AACnB,MAAI,YAAY;AAChB,aAAW,UAAU,UAAU;AAC7B,QAAI,OAAO,WAAW;AACpB;AAAA,IACF;AACA,QAAI,OAAO,SAAS;AAClB;AAAA,IACF;AACA,UAAM,WAAW,OAAO;AACxB,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,QAAQ,SAAS;AACvB,QAAI,SAAS,MAAM,aAAa,gBAAgB;AAC9C,qBAAc,WAAM,UAAN,YAAe;AAC7B,qBAAc,WAAM,UAAN,YAAe;AAC7B,8BAAuB,iBAAM,mBAAN,YAAwB,MAAM,UAA9B,YAAuC;AAAA,IAChE;AACA,UAAM,cAAU,sCAAgB,QAAQ;AACxC,QAAI,YAAY,QAAW;AACzB,mBAAa,KAAK,IAAI,YAAY,OAAO;AACzC,UAAI,WAAW,KAAK;AAClB,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,YAAY,OAAO,UAAU;AAAA,IAC7B,YAAY,OAAO,UAAU;AAAA,IAC7B,qBAAqB,OAAO,mBAAmB;AAAA,IAC/C,UAAU;AAAA,IACV,iBAAiB,OAAO,UAAU;AAAA,IAClC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,UAAU,SAAS;AAAA,EACrB;AACF;AAQA,SAAS,OAAO,OAAuB;AACrC,SAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;AACnC;",
6
6
  "names": []
7
7
  }
package/build/main.js CHANGED
@@ -198,7 +198,6 @@ class AiUsageAdapter extends utils.Adapter {
198
198
  this.stopDevicePoller(provider);
199
199
  const id = import_pure_helpers.SUBSCRIPTION_IDS[provider];
200
200
  if (id) {
201
- await this.setSignedInState(id, true);
202
201
  await ((_a = this.engine) == null ? void 0 : _a.pollNow(id));
203
202
  }
204
203
  this.log.info(`${provider}: signed in`);
@@ -285,10 +284,6 @@ class AiUsageAdapter extends utils.Adapter {
285
284
  this.attempts.delete(provider);
286
285
  this.signInErrors.delete(provider);
287
286
  this.stopDevicePoller(provider);
288
- const id = import_pure_helpers.SUBSCRIPTION_IDS[provider];
289
- if (id) {
290
- await this.setSignedInState(id, false);
291
- }
292
287
  return { status: "signed-out" };
293
288
  }
294
289
  // ------------------------------------------------------------ token files
@@ -402,7 +397,10 @@ class AiUsageAdapter extends utils.Adapter {
402
397
  ) : void 0
403
398
  });
404
399
  await this.engine.start();
405
- await this.publishSignedInStates(accounts);
400
+ const removed = await this.removeRetiredStates(accounts);
401
+ if (removed > 0) {
402
+ this.log.info(`Object tree updated: removed ${removed} obsolete status datapoint(s)`);
403
+ }
406
404
  this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);
407
405
  } catch (e) {
408
406
  this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -469,40 +467,43 @@ class AiUsageAdapter extends utils.Adapter {
469
467
  }
470
468
  }
471
469
  /**
472
- * Create and write `<account>.info.signedIn` for every subscription in the table.
470
+ * Status states retired in 0.5.0 — `provider` (visible in the node name anyway),
471
+ * `reachable`/`serviceOnline`/`state` (three spellings of one fact) and `signedIn`
472
+ * (the settings page asks the adapter directly).
473
+ *
474
+ * ioBroker never garbage-collects an object whose id the adapter stopped writing:
475
+ * it would sit there frozen on its last value and keep lying. So the old ids are
476
+ * deleted from a fixed list on every start — no state reading, no heuristics.
477
+ */
478
+ static RETIRED_INFO_STATES = [
479
+ "info.provider",
480
+ "info.reachable",
481
+ "info.serviceOnline",
482
+ "info.state",
483
+ "info.signedIn"
484
+ ];
485
+ /**
486
+ * Remove the retired status states of every configured account.
473
487
  *
474
488
  * @param accounts the configured accounts
489
+ * @returns how many objects were actually deleted
475
490
  */
476
- async publishSignedInStates(accounts) {
491
+ async removeRetiredStates(accounts) {
492
+ let removed = 0;
477
493
  for (const account of accounts) {
478
- if (!import_sign_in.SIGN_IN_FLOWS[account.provider]) {
479
- continue;
494
+ for (const suffix of AiUsageAdapter.RETIRED_INFO_STATES) {
495
+ const id = `${account.id}.${suffix}`;
496
+ try {
497
+ if (await this.getObjectAsync(id)) {
498
+ await this.delObjectAsync(id);
499
+ removed++;
500
+ }
501
+ } catch (e) {
502
+ this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);
503
+ }
480
504
  }
481
- const tokens = await this.tokenStore(account.provider).load();
482
- await this.setSignedInState(account.id, !!tokens);
483
505
  }
484
- }
485
- /**
486
- * Write the sign-in indicator INSIDE the account node — the pre-0.3.0 layout kept
487
- * a second `auth.<name>` branch, which showed every subscription twice in the tree.
488
- *
489
- * @param accountId the account's object id
490
- * @param signedIn whether a usable sign-in exists
491
- */
492
- async setSignedInState(accountId, signedIn) {
493
- await this.extendObject(`${accountId}.info.signedIn`, {
494
- type: "state",
495
- common: {
496
- name: { en: "Signed in", de: "Angemeldet" },
497
- type: "boolean",
498
- role: "indicator",
499
- read: true,
500
- write: false,
501
- def: false
502
- },
503
- native: {}
504
- });
505
- await this.setState(`${accountId}.info.signedIn`, { val: signedIn, ack: true });
506
+ return removed;
506
507
  }
507
508
  /**
508
509
  * Delete object trees that no longer belong to a configured account, plus the
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { Credentials } from \"@iobroker/adapter-core\";\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { postForm, postJson } from \"./lib/http\";\nimport { PollEngine } from \"./lib/poll-engine\";\nimport {\n clampPollInterval,\n parseAccounts,\n SUBSCRIPTION_IDS,\n validAccountIds,\n type AccountConfig,\n} from \"./lib/pure-helpers\";\nimport type { TokenSet, TokenStore, UsageProvider } from \"./lib/provider\";\nimport { SIGN_IN_FLOWS, attemptExpired, type SignInState } from \"./lib/sign-in\";\nimport { buildAuthorizeUrl, exchangeCode, generatePkce, type PkcePair } from \"./lib/providers/claude-auth\";\nimport { claudeSubProvider } from \"./lib/providers/claude-sub\";\nimport {\n exchangeDeviceCode,\n pollDeviceCode,\n startDeviceCode,\n type DeviceCodeStart,\n} from \"./lib/providers/chatgpt-auth\";\nimport { chatgptSubProvider } from \"./lib/providers/chatgpt-sub\";\nimport {\n buildGeminiAuthorizeUrl,\n exchangeGeminiCode,\n extractGeminiCode,\n generateGeminiPkce,\n type GeminiPkce,\n} from \"./lib/providers/gemini-auth\";\nimport { geminiSubProvider } from \"./lib/providers/gemini-sub\";\nimport { anthropicApiProvider } from \"./lib/providers/anthropic-api\";\nimport { deepSeekProvider } from \"./lib/providers/deepseek\";\nimport { openAiProvider } from \"./lib/providers/openai\";\nimport { openRouterProvider } from \"./lib/providers/openrouter\";\n\n/** A cancellable handle: interval or timeout \u2014 the engine treats them uniformly. */\ntype TimerHandle =\n | { kind: \"interval\"; handle: ioBroker.Interval | undefined }\n | { kind: \"timeout\"; handle: ioBroker.Timeout | undefined };\n\n/** One running sign-in attempt (secrets live in memory only, never on disk). */\ntype Attempt =\n | { flow: \"paste-code\"; pkce: PkcePair; url: string; expiresAt: number }\n | { flow: \"paste-url\"; pkce: GeminiPkce; url: string; expiresAt: number }\n | { flow: \"device-code\"; start: DeviceCodeStart };\n\n/**\n * AI Usage adapter \u2014 reads usage windows, credits and costs of AI accounts into\n * read-only states. Three subscriptions sign in with the user's own account\n * (Claude, ChatGPT, Google), the other accounts use a key from the admin's central\n * credential storage. Orchestration lives in the unit-tested {@link PollEngine};\n * this class wires ioBroker IO, the sign-in flows and the token files to it.\n */\nexport class AiUsageAdapter extends utils.Adapter {\n private engine: PollEngine | null = null;\n /** Running sign-in attempts, keyed by provider kind. */\n private readonly attempts = new Map<string, Attempt>();\n /** Last failure reason per provider, shown in the admin row. */\n private readonly signInErrors = new Map<string, string>();\n /** Device-code pollers, so they can be stopped on unload. */\n private readonly devicePollers = new Map<string, TimerHandle>();\n\n /**\n * @param options the adapter options\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"ai-usage\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n }\n\n // ---------------------------------------------------------------- sign-in\n\n /**\n * Handle admin messages: the three sign-in flows plus their status.\n *\n * @param obj the message\n */\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n const provider = this.providerFrom(obj.message);\n switch (obj.command) {\n case \"signInStart\":\n this.respond(obj, provider ? await this.startSignIn(provider) : { error: \"unknown provider\" });\n return;\n case \"signInSubmit\":\n this.respond(obj, provider ? await this.submitSignIn(provider, obj.message) : { error: \"unknown provider\" });\n return;\n case \"signInStatus\":\n this.respond(obj, provider ? await this.signInState(provider) : { error: \"unknown provider\" });\n return;\n case \"signOut\":\n this.respond(obj, provider ? await this.signOut(provider) : { error: \"unknown provider\" });\n return;\n default:\n // Always answer, or the caller's callback dangles until timeout.\n this.respond(obj, { error: `Unknown command: ${obj.command}` });\n }\n } catch (e) {\n this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);\n this.respond(obj, { error: \"internal error \u2014 see log\" });\n }\n }\n\n /**\n * Send a message response, when the caller expects one.\n *\n * @param obj the request message\n * @param response the response payload\n */\n private respond(obj: ioBroker.Message, response: unknown): void {\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, response, obj.callback);\n }\n }\n\n /**\n * The provider kind named in a message, if it is a subscription we know.\n *\n * @param message the message payload ({ provider })\n * @returns the provider kind, or undefined\n */\n private providerFrom(message: unknown): string | undefined {\n const value =\n typeof (message as { provider?: unknown })?.provider === \"string\"\n ? (message as { provider: string }).provider\n : \"\";\n return SIGN_IN_FLOWS[value] ? value : undefined;\n }\n\n /**\n * Begin a sign-in: build the link (Claude/Google) or fetch a device code (ChatGPT).\n *\n * @param provider the subscription kind\n * @returns what the admin panel has to show\n */\n private async startSignIn(provider: string): Promise<SignInState | { error: string }> {\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const flow = SIGN_IN_FLOWS[provider];\n const now = Date.now();\n try {\n if (flow === \"paste-code\") {\n const pkce = generatePkce();\n const url = buildAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n if (flow === \"paste-url\") {\n const pkce = generateGeminiPkce();\n const url = buildGeminiAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n const start = await startDeviceCode(postJson, now);\n this.attempts.set(provider, { flow: \"device-code\", start });\n this.armDevicePoller(provider, start);\n return {\n status: \"awaiting-device\",\n userCode: start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: start.expiresAt,\n };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Finish a paste-based sign-in (Claude code, Google address).\n *\n * @param provider the subscription kind\n * @param message the message payload ({ value })\n * @returns the resulting state\n */\n private async submitSignIn(provider: string, message: unknown): Promise<SignInState> {\n const attempt = this.attempts.get(provider);\n const value =\n typeof (message as { value?: unknown })?.value === \"string\" ? (message as { value: string }).value.trim() : \"\";\n if (!attempt || attempt.flow === \"device-code\") {\n return { status: \"failed\", reason: \"start the sign-in first\" };\n }\n if (!value) {\n return { status: \"failed\", reason: \"nothing pasted\" };\n }\n try {\n const now = Date.now();\n const tokens =\n attempt.flow === \"paste-code\"\n ? await exchangeCode(value, attempt.pkce, postJson, now)\n : await exchangeGeminiCode(extractGeminiCode(value, attempt.pkce.state), attempt.pkce, postForm, now);\n await this.finishSignIn(provider, tokens);\n return { status: \"signed-in\" };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Store fresh tokens, mark the account signed in and poll it right away \u2014 waiting\n * up to a full interval after a successful sign-in reads as \"it did not work\".\n *\n * @param provider the subscription kind\n * @param tokens the token set\n */\n private async finishSignIn(provider: string, tokens: TokenSet): Promise<void> {\n await this.tokenStore(provider).save(tokens);\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const id = SUBSCRIPTION_IDS[provider];\n if (id) {\n await this.setSignedInState(id, true);\n await this.engine?.pollNow(id);\n }\n this.log.info(`${provider}: signed in`);\n }\n\n /**\n * Poll the device-code endpoint until the user confirmed, the window closed or\n * the adapter stops. The handle lives in memory only \u2014 a restart mid-flow just\n * means the user starts again, which is cheaper than persisting a 15-minute secret.\n *\n * @param provider the subscription kind\n * @param start the device-code handle\n */\n private armDevicePoller(provider: string, start: DeviceCodeStart): void {\n const tick = async (): Promise<void> => {\n try {\n if (attemptExpired(start.expiresAt, Date.now())) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, \"the code expired \u2014 start the sign-in again\");\n return;\n }\n const result = await pollDeviceCode(start, postJson);\n if (result.status === \"ready\") {\n this.stopDevicePoller(provider);\n const tokens = await exchangeDeviceCode(result.code, result.codeVerifier, postForm, Date.now());\n await this.finishSignIn(provider, tokens);\n }\n } catch (e) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, e instanceof Error ? e.message : String(e));\n }\n };\n this.devicePollers.set(provider, {\n kind: \"interval\",\n handle: this.setInterval(() => void tick(), Math.max(start.intervalSec, 1) * 1000),\n });\n }\n\n /**\n * Stop a running device-code poller.\n *\n * @param provider the subscription kind\n */\n private stopDevicePoller(provider: string): void {\n const handle = this.devicePollers.get(provider);\n if (handle?.kind === \"interval\") {\n this.clearInterval(handle.handle);\n }\n this.devicePollers.delete(provider);\n }\n\n /**\n * The current sign-in state of one subscription, for the admin row.\n *\n * @param provider the subscription kind\n * @returns the state\n */\n private async signInState(provider: string): Promise<SignInState> {\n const failure = this.signInErrors.get(provider);\n const attempt = this.attempts.get(provider);\n if (attempt?.flow === \"device-code\") {\n return {\n status: \"awaiting-device\",\n userCode: attempt.start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: attempt.start.expiresAt,\n };\n }\n if (attempt) {\n return { status: \"awaiting-paste\", url: attempt.url, flow: attempt.flow };\n }\n if (failure) {\n return { status: \"failed\", reason: failure };\n }\n return (await this.tokenStore(provider).load()) ? { status: \"signed-in\" } : { status: \"signed-out\" };\n }\n\n /**\n * Forget the tokens of one subscription.\n *\n * @param provider the subscription kind\n * @returns the resulting state\n */\n private async signOut(provider: string): Promise<SignInState> {\n await this.tokenStore(provider).clear();\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const id = SUBSCRIPTION_IDS[provider];\n if (id) {\n await this.setSignedInState(id, false);\n }\n return { status: \"signed-out\" };\n }\n\n // ------------------------------------------------------------ token files\n\n /**\n * Where one subscription's tokens live: an encrypted file in the instance data\n * directory, named after the PROVIDER. Keying by provider (not by account name)\n * keeps a sign-in alive when an account is renamed \u2014 the previous scheme lost it.\n *\n * @param provider the subscription kind\n * @returns the store\n */\n private tokenStore(provider: string): TokenStore {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const file = join(dir, `tokens-${provider}.json`);\n return {\n load: async (): Promise<TokenSet | null> => {\n try {\n const parsed = JSON.parse(this.decrypt(await readFile(file, \"utf8\"))) as Partial<TokenSet>;\n if (typeof parsed.accessToken !== \"string\" || typeof parsed.refreshToken !== \"string\") {\n return null;\n }\n return {\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken,\n expiresAt: Number(parsed.expiresAt) || 0,\n accountRef: typeof parsed.accountRef === \"string\" ? parsed.accountRef : undefined,\n };\n } catch {\n return null; // never signed in (or unreadable) \u2014 the provider reports auth-required\n }\n },\n save: async (tokens: TokenSet): Promise<void> => {\n await mkdir(dir, { recursive: true });\n await writeFile(file, this.encrypt(JSON.stringify(tokens)), \"utf8\");\n },\n clear: async (): Promise<void> => {\n await unlink(file).catch(() => {\n /* already gone */\n });\n },\n };\n }\n\n /**\n * Carry a sign-in from the pre-0.3.0 layout over: tokens used to be stored per\n * ACCOUNT NAME (`claude-tokens-<name>.json`). Without this the rename to fixed\n * account ids would silently sign the user out.\n */\n private async migrateTokenFiles(): Promise<void> {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const target = join(dir, \"tokens-claude-sub.json\");\n try {\n await readFile(target, \"utf8\");\n return; // already migrated\n } catch {\n // not there yet \u2014 look for an old file\n }\n for (const legacy of [\"claude-tokens-Claude.json\", \"claude-tokens-claude.json\"]) {\n try {\n await rename(join(dir, legacy), target);\n this.log.info(\"Carried the existing Claude sign-in over to the new layout\");\n return;\n } catch {\n // try the next candidate\n }\n }\n }\n\n // ------------------------------------------------------------- life cycle\n\n /** Validate the configuration, clean up stale objects and start the engine. */\n private async onReady(): Promise<void> {\n try {\n const accounts = parseAccounts(this.config.accounts);\n const interval = clampPollInterval(this.config.pollInterval);\n await this.migrateTokenFiles();\n await this.cleanupStaleObjects();\n if (accounts.length === 0) {\n this.log.info(\"No AI accounts configured \u2014 add accounts in the instance settings\");\n await this.setState(\"info.connection\", { val: false, ack: true });\n return;\n }\n const providers = new Map<string, UsageProvider>();\n for (const account of accounts) {\n const provider = await this.makeProvider(account);\n if (provider) {\n providers.set(account.id, provider);\n }\n }\n this.engine = new PollEngine(accounts, providers, interval, {\n upsertObject: async def => {\n await this.extendObject(def.id, { type: def.type, common: def.common as ioBroker.ObjectCommon, native: {} });\n },\n setState: (id, value) => {\n void this.setState(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n schedule: (cb, ms): TimerHandle => ({ kind: \"interval\", handle: this.setInterval(cb, ms) }),\n scheduleOnce: (cb, ms): TimerHandle => ({ kind: \"timeout\", handle: this.setTimeout(cb, ms) }),\n cancel: handle => {\n const timer = handle as TimerHandle;\n if (timer.kind === \"interval\") {\n this.clearInterval(timer.handle);\n } else {\n this.clearTimeout(timer.handle);\n }\n },\n now: () => Date.now(),\n log: {\n debug: m => this.log.debug(m),\n info: m => this.log.info(m),\n warn: m => this.log.warn(m),\n error: m => this.log.error(m),\n },\n notify: this.config.notifications\n ? (_account, message) =>\n void this.registerNotification(\"ai-usage\", \"userActionRequired\", message).catch(e =>\n this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`),\n )\n : undefined,\n });\n await this.engine.start();\n await this.publishSignedInStates(accounts);\n this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);\n } catch (e) {\n this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Build the provider for one account.\n *\n * @param account the validated account config\n * @returns the provider, or undefined to skip the account\n */\n private async makeProvider(account: AccountConfig): Promise<UsageProvider | undefined> {\n switch (account.provider) {\n case \"claude-sub\":\n return claudeSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"chatgpt-sub\":\n return chatgptSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"gemini-sub\":\n return geminiSubProvider(this.tokenStore(account.provider), postJson, postForm);\n case \"openrouter\": {\n const key = await this.resolveKey(account);\n return key ? openRouterProvider(key) : undefined;\n }\n case \"deepseek\": {\n const key = await this.resolveKey(account);\n return key ? deepSeekProvider(key) : undefined;\n }\n case \"openai\": {\n const key = await this.resolveKey(account);\n return key ? openAiProvider(key) : undefined;\n }\n case \"anthropic-api\": {\n const key = await this.resolveKey(account);\n return key ? anthropicApiProvider(key) : undefined;\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Read and decrypt a key-form credential from the central credential storage.\n *\n * @param account the account whose credential to resolve\n * @returns the key, or undefined (with a log line) when it cannot be read\n */\n private async resolveKey(account: AccountConfig): Promise<string | undefined> {\n if (!account.credentialId) {\n this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);\n return undefined;\n }\n try {\n const credential = await Credentials.getCredentials(this, account.credentialId);\n const values = credential.values as { key?: unknown };\n const key = typeof values.key === \"string\" && values.key ? values.key : undefined;\n if (!key) {\n this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);\n }\n return key;\n } catch (e) {\n this.log.warn(\n `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`,\n );\n return undefined;\n }\n }\n\n /**\n * Create and write `<account>.info.signedIn` for every subscription in the table.\n *\n * @param accounts the configured accounts\n */\n private async publishSignedInStates(accounts: readonly AccountConfig[]): Promise<void> {\n for (const account of accounts) {\n if (!SIGN_IN_FLOWS[account.provider]) {\n continue;\n }\n const tokens = await this.tokenStore(account.provider).load();\n await this.setSignedInState(account.id, !!tokens);\n }\n }\n\n /**\n * Write the sign-in indicator INSIDE the account node \u2014 the pre-0.3.0 layout kept\n * a second `auth.<name>` branch, which showed every subscription twice in the tree.\n *\n * @param accountId the account's object id\n * @param signedIn whether a usable sign-in exists\n */\n private async setSignedInState(accountId: string, signedIn: boolean): Promise<void> {\n await this.extendObject(`${accountId}.info.signedIn`, {\n type: \"state\",\n common: {\n name: { en: \"Signed in\", de: \"Angemeldet\" },\n type: \"boolean\",\n role: \"indicator\",\n read: true,\n write: false,\n def: false,\n },\n native: {},\n });\n await this.setState(`${accountId}.info.signedIn`, { val: signedIn, ack: true });\n }\n\n /**\n * Delete object trees that no longer belong to a configured account, plus the\n * `auth` branch of the pre-0.3.0 layout. An EMPTY table deletes nothing \u2014 the\n * guard against wiping everything through an accidental clear.\n */\n private async cleanupStaleObjects(): Promise<void> {\n const keepIds = validAccountIds(this.config.accounts);\n if (keepIds.length === 0) {\n return;\n }\n const keep = new Set([...keepIds, \"info\", \"total\"]);\n try {\n const objects = await this.getAdapterObjectsAsync();\n const roots = new Set<string>();\n for (const id of Object.keys(objects)) {\n const root = id.substring(this.namespace.length + 1).split(\".\")[0];\n if (root && !keep.has(root)) {\n roots.add(root);\n }\n }\n for (const root of roots) {\n this.log.info(\n root === \"auth\"\n ? \"Removing the old sign-in branch \u2014 the sign-in state now lives inside each account\"\n : `Removing objects of no longer configured account \"${root}\"`,\n );\n await this.delObjectAsync(root, { recursive: true });\n }\n } catch (e) {\n this.log.warn(`Cleanup of stale objects failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Tear down synchronously \u2014 no async/await here, else the controller kills the\n * process before cleanup finishes.\n *\n * @param callback invoked when cleanup is done\n */\n private onUnload(callback: () => void): void {\n try {\n for (const provider of [...this.devicePollers.keys()]) {\n this.stopDevicePoller(provider);\n }\n this.engine?.stop();\n this.engine = null;\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // never block shutdown\n }\n callback();\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AiUsageAdapter(options);\n} else {\n (() => new AiUsageAdapter())();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { Credentials } from \"@iobroker/adapter-core\";\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { postForm, postJson } from \"./lib/http\";\nimport { PollEngine } from \"./lib/poll-engine\";\nimport {\n clampPollInterval,\n parseAccounts,\n SUBSCRIPTION_IDS,\n validAccountIds,\n type AccountConfig,\n} from \"./lib/pure-helpers\";\nimport type { TokenSet, TokenStore, UsageProvider } from \"./lib/provider\";\nimport { SIGN_IN_FLOWS, attemptExpired, type SignInState } from \"./lib/sign-in\";\nimport { buildAuthorizeUrl, exchangeCode, generatePkce, type PkcePair } from \"./lib/providers/claude-auth\";\nimport { claudeSubProvider } from \"./lib/providers/claude-sub\";\nimport {\n exchangeDeviceCode,\n pollDeviceCode,\n startDeviceCode,\n type DeviceCodeStart,\n} from \"./lib/providers/chatgpt-auth\";\nimport { chatgptSubProvider } from \"./lib/providers/chatgpt-sub\";\nimport {\n buildGeminiAuthorizeUrl,\n exchangeGeminiCode,\n extractGeminiCode,\n generateGeminiPkce,\n type GeminiPkce,\n} from \"./lib/providers/gemini-auth\";\nimport { geminiSubProvider } from \"./lib/providers/gemini-sub\";\nimport { anthropicApiProvider } from \"./lib/providers/anthropic-api\";\nimport { deepSeekProvider } from \"./lib/providers/deepseek\";\nimport { openAiProvider } from \"./lib/providers/openai\";\nimport { openRouterProvider } from \"./lib/providers/openrouter\";\n\n/** A cancellable handle: interval or timeout \u2014 the engine treats them uniformly. */\ntype TimerHandle =\n | { kind: \"interval\"; handle: ioBroker.Interval | undefined }\n | { kind: \"timeout\"; handle: ioBroker.Timeout | undefined };\n\n/** One running sign-in attempt (secrets live in memory only, never on disk). */\ntype Attempt =\n | { flow: \"paste-code\"; pkce: PkcePair; url: string; expiresAt: number }\n | { flow: \"paste-url\"; pkce: GeminiPkce; url: string; expiresAt: number }\n | { flow: \"device-code\"; start: DeviceCodeStart };\n\n/**\n * AI Usage adapter \u2014 reads usage windows, credits and costs of AI accounts into\n * read-only states. Three subscriptions sign in with the user's own account\n * (Claude, ChatGPT, Google), the other accounts use a key from the admin's central\n * credential storage. Orchestration lives in the unit-tested {@link PollEngine};\n * this class wires ioBroker IO, the sign-in flows and the token files to it.\n */\nexport class AiUsageAdapter extends utils.Adapter {\n private engine: PollEngine | null = null;\n /** Running sign-in attempts, keyed by provider kind. */\n private readonly attempts = new Map<string, Attempt>();\n /** Last failure reason per provider, shown in the admin row. */\n private readonly signInErrors = new Map<string, string>();\n /** Device-code pollers, so they can be stopped on unload. */\n private readonly devicePollers = new Map<string, TimerHandle>();\n\n /**\n * @param options the adapter options\n */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({ ...options, name: \"ai-usage\" });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n }\n\n // ---------------------------------------------------------------- sign-in\n\n /**\n * Handle admin messages: the three sign-in flows plus their status.\n *\n * @param obj the message\n */\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n const provider = this.providerFrom(obj.message);\n switch (obj.command) {\n case \"signInStart\":\n this.respond(obj, provider ? await this.startSignIn(provider) : { error: \"unknown provider\" });\n return;\n case \"signInSubmit\":\n this.respond(obj, provider ? await this.submitSignIn(provider, obj.message) : { error: \"unknown provider\" });\n return;\n case \"signInStatus\":\n this.respond(obj, provider ? await this.signInState(provider) : { error: \"unknown provider\" });\n return;\n case \"signOut\":\n this.respond(obj, provider ? await this.signOut(provider) : { error: \"unknown provider\" });\n return;\n default:\n // Always answer, or the caller's callback dangles until timeout.\n this.respond(obj, { error: `Unknown command: ${obj.command}` });\n }\n } catch (e) {\n this.log.error(`onMessage failed: ${e instanceof Error ? e.message : String(e)}`);\n this.respond(obj, { error: \"internal error \u2014 see log\" });\n }\n }\n\n /**\n * Send a message response, when the caller expects one.\n *\n * @param obj the request message\n * @param response the response payload\n */\n private respond(obj: ioBroker.Message, response: unknown): void {\n if (obj.callback) {\n this.sendTo(obj.from, obj.command, response, obj.callback);\n }\n }\n\n /**\n * The provider kind named in a message, if it is a subscription we know.\n *\n * @param message the message payload ({ provider })\n * @returns the provider kind, or undefined\n */\n private providerFrom(message: unknown): string | undefined {\n const value =\n typeof (message as { provider?: unknown })?.provider === \"string\"\n ? (message as { provider: string }).provider\n : \"\";\n return SIGN_IN_FLOWS[value] ? value : undefined;\n }\n\n /**\n * Begin a sign-in: build the link (Claude/Google) or fetch a device code (ChatGPT).\n *\n * @param provider the subscription kind\n * @returns what the admin panel has to show\n */\n private async startSignIn(provider: string): Promise<SignInState | { error: string }> {\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const flow = SIGN_IN_FLOWS[provider];\n const now = Date.now();\n try {\n if (flow === \"paste-code\") {\n const pkce = generatePkce();\n const url = buildAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n if (flow === \"paste-url\") {\n const pkce = generateGeminiPkce();\n const url = buildGeminiAuthorizeUrl(pkce);\n this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 60_000 });\n return { status: \"awaiting-paste\", url, flow };\n }\n const start = await startDeviceCode(postJson, now);\n this.attempts.set(provider, { flow: \"device-code\", start });\n this.armDevicePoller(provider, start);\n return {\n status: \"awaiting-device\",\n userCode: start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: start.expiresAt,\n };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Finish a paste-based sign-in (Claude code, Google address).\n *\n * @param provider the subscription kind\n * @param message the message payload ({ value })\n * @returns the resulting state\n */\n private async submitSignIn(provider: string, message: unknown): Promise<SignInState> {\n const attempt = this.attempts.get(provider);\n const value =\n typeof (message as { value?: unknown })?.value === \"string\" ? (message as { value: string }).value.trim() : \"\";\n if (!attempt || attempt.flow === \"device-code\") {\n return { status: \"failed\", reason: \"start the sign-in first\" };\n }\n if (!value) {\n return { status: \"failed\", reason: \"nothing pasted\" };\n }\n try {\n const now = Date.now();\n const tokens =\n attempt.flow === \"paste-code\"\n ? await exchangeCode(value, attempt.pkce, postJson, now)\n : await exchangeGeminiCode(extractGeminiCode(value, attempt.pkce.state), attempt.pkce, postForm, now);\n await this.finishSignIn(provider, tokens);\n return { status: \"signed-in\" };\n } catch (e) {\n const reason = e instanceof Error ? e.message : String(e);\n this.signInErrors.set(provider, reason);\n return { status: \"failed\", reason };\n }\n }\n\n /**\n * Store fresh tokens, mark the account signed in and poll it right away \u2014 waiting\n * up to a full interval after a successful sign-in reads as \"it did not work\".\n *\n * @param provider the subscription kind\n * @param tokens the token set\n */\n private async finishSignIn(provider: string, tokens: TokenSet): Promise<void> {\n await this.tokenStore(provider).save(tokens);\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n const id = SUBSCRIPTION_IDS[provider];\n if (id) {\n await this.engine?.pollNow(id);\n }\n this.log.info(`${provider}: signed in`);\n }\n\n /**\n * Poll the device-code endpoint until the user confirmed, the window closed or\n * the adapter stops. The handle lives in memory only \u2014 a restart mid-flow just\n * means the user starts again, which is cheaper than persisting a 15-minute secret.\n *\n * @param provider the subscription kind\n * @param start the device-code handle\n */\n private armDevicePoller(provider: string, start: DeviceCodeStart): void {\n const tick = async (): Promise<void> => {\n try {\n if (attemptExpired(start.expiresAt, Date.now())) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, \"the code expired \u2014 start the sign-in again\");\n return;\n }\n const result = await pollDeviceCode(start, postJson);\n if (result.status === \"ready\") {\n this.stopDevicePoller(provider);\n const tokens = await exchangeDeviceCode(result.code, result.codeVerifier, postForm, Date.now());\n await this.finishSignIn(provider, tokens);\n }\n } catch (e) {\n this.stopDevicePoller(provider);\n this.attempts.delete(provider);\n this.signInErrors.set(provider, e instanceof Error ? e.message : String(e));\n }\n };\n this.devicePollers.set(provider, {\n kind: \"interval\",\n handle: this.setInterval(() => void tick(), Math.max(start.intervalSec, 1) * 1000),\n });\n }\n\n /**\n * Stop a running device-code poller.\n *\n * @param provider the subscription kind\n */\n private stopDevicePoller(provider: string): void {\n const handle = this.devicePollers.get(provider);\n if (handle?.kind === \"interval\") {\n this.clearInterval(handle.handle);\n }\n this.devicePollers.delete(provider);\n }\n\n /**\n * The current sign-in state of one subscription, for the admin row.\n *\n * @param provider the subscription kind\n * @returns the state\n */\n private async signInState(provider: string): Promise<SignInState> {\n const failure = this.signInErrors.get(provider);\n const attempt = this.attempts.get(provider);\n if (attempt?.flow === \"device-code\") {\n return {\n status: \"awaiting-device\",\n userCode: attempt.start.userCode,\n verificationUrl: \"https://auth.openai.com/codex/device\",\n expiresAt: attempt.start.expiresAt,\n };\n }\n if (attempt) {\n return { status: \"awaiting-paste\", url: attempt.url, flow: attempt.flow };\n }\n if (failure) {\n return { status: \"failed\", reason: failure };\n }\n return (await this.tokenStore(provider).load()) ? { status: \"signed-in\" } : { status: \"signed-out\" };\n }\n\n /**\n * Forget the tokens of one subscription.\n *\n * @param provider the subscription kind\n * @returns the resulting state\n */\n private async signOut(provider: string): Promise<SignInState> {\n await this.tokenStore(provider).clear();\n this.attempts.delete(provider);\n this.signInErrors.delete(provider);\n this.stopDevicePoller(provider);\n return { status: \"signed-out\" };\n }\n\n // ------------------------------------------------------------ token files\n\n /**\n * Where one subscription's tokens live: an encrypted file in the instance data\n * directory, named after the PROVIDER. Keying by provider (not by account name)\n * keeps a sign-in alive when an account is renamed \u2014 the previous scheme lost it.\n *\n * @param provider the subscription kind\n * @returns the store\n */\n private tokenStore(provider: string): TokenStore {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const file = join(dir, `tokens-${provider}.json`);\n return {\n load: async (): Promise<TokenSet | null> => {\n try {\n const parsed = JSON.parse(this.decrypt(await readFile(file, \"utf8\"))) as Partial<TokenSet>;\n if (typeof parsed.accessToken !== \"string\" || typeof parsed.refreshToken !== \"string\") {\n return null;\n }\n return {\n accessToken: parsed.accessToken,\n refreshToken: parsed.refreshToken,\n expiresAt: Number(parsed.expiresAt) || 0,\n accountRef: typeof parsed.accountRef === \"string\" ? parsed.accountRef : undefined,\n };\n } catch {\n return null; // never signed in (or unreadable) \u2014 the provider reports auth-required\n }\n },\n save: async (tokens: TokenSet): Promise<void> => {\n await mkdir(dir, { recursive: true });\n await writeFile(file, this.encrypt(JSON.stringify(tokens)), \"utf8\");\n },\n clear: async (): Promise<void> => {\n await unlink(file).catch(() => {\n /* already gone */\n });\n },\n };\n }\n\n /**\n * Carry a sign-in from the pre-0.3.0 layout over: tokens used to be stored per\n * ACCOUNT NAME (`claude-tokens-<name>.json`). Without this the rename to fixed\n * account ids would silently sign the user out.\n */\n private async migrateTokenFiles(): Promise<void> {\n const dir = utils.getAbsoluteInstanceDataDir(this);\n const target = join(dir, \"tokens-claude-sub.json\");\n try {\n await readFile(target, \"utf8\");\n return; // already migrated\n } catch {\n // not there yet \u2014 look for an old file\n }\n for (const legacy of [\"claude-tokens-Claude.json\", \"claude-tokens-claude.json\"]) {\n try {\n await rename(join(dir, legacy), target);\n this.log.info(\"Carried the existing Claude sign-in over to the new layout\");\n return;\n } catch {\n // try the next candidate\n }\n }\n }\n\n // ------------------------------------------------------------- life cycle\n\n /** Validate the configuration, clean up stale objects and start the engine. */\n private async onReady(): Promise<void> {\n try {\n const accounts = parseAccounts(this.config.accounts);\n const interval = clampPollInterval(this.config.pollInterval);\n await this.migrateTokenFiles();\n await this.cleanupStaleObjects();\n if (accounts.length === 0) {\n this.log.info(\"No AI accounts configured \u2014 add accounts in the instance settings\");\n await this.setState(\"info.connection\", { val: false, ack: true });\n return;\n }\n const providers = new Map<string, UsageProvider>();\n for (const account of accounts) {\n const provider = await this.makeProvider(account);\n if (provider) {\n providers.set(account.id, provider);\n }\n }\n this.engine = new PollEngine(accounts, providers, interval, {\n upsertObject: async def => {\n await this.extendObject(def.id, { type: def.type, common: def.common as ioBroker.ObjectCommon, native: {} });\n },\n setState: (id, value) => {\n void this.setState(id, { val: value, ack: true }).catch(() => {\n /* states DB going down \u2014 never crash the poll loop */\n });\n },\n schedule: (cb, ms): TimerHandle => ({ kind: \"interval\", handle: this.setInterval(cb, ms) }),\n scheduleOnce: (cb, ms): TimerHandle => ({ kind: \"timeout\", handle: this.setTimeout(cb, ms) }),\n cancel: handle => {\n const timer = handle as TimerHandle;\n if (timer.kind === \"interval\") {\n this.clearInterval(timer.handle);\n } else {\n this.clearTimeout(timer.handle);\n }\n },\n now: () => Date.now(),\n log: {\n debug: m => this.log.debug(m),\n info: m => this.log.info(m),\n warn: m => this.log.warn(m),\n error: m => this.log.error(m),\n },\n notify: this.config.notifications\n ? (_account, message) =>\n void this.registerNotification(\"ai-usage\", \"userActionRequired\", message).catch(e =>\n this.log.debug(`Could not raise notification: ${e instanceof Error ? e.message : String(e)}`),\n )\n : undefined,\n });\n await this.engine.start();\n const removed = await this.removeRetiredStates(accounts);\n if (removed > 0) {\n this.log.info(`Object tree updated: removed ${removed} obsolete status datapoint(s)`);\n }\n this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);\n } catch (e) {\n this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Build the provider for one account.\n *\n * @param account the validated account config\n * @returns the provider, or undefined to skip the account\n */\n private async makeProvider(account: AccountConfig): Promise<UsageProvider | undefined> {\n switch (account.provider) {\n case \"claude-sub\":\n return claudeSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"chatgpt-sub\":\n return chatgptSubProvider(this.tokenStore(account.provider), undefined, postJson);\n case \"gemini-sub\":\n return geminiSubProvider(this.tokenStore(account.provider), postJson, postForm);\n case \"openrouter\": {\n const key = await this.resolveKey(account);\n return key ? openRouterProvider(key) : undefined;\n }\n case \"deepseek\": {\n const key = await this.resolveKey(account);\n return key ? deepSeekProvider(key) : undefined;\n }\n case \"openai\": {\n const key = await this.resolveKey(account);\n return key ? openAiProvider(key) : undefined;\n }\n case \"anthropic-api\": {\n const key = await this.resolveKey(account);\n return key ? anthropicApiProvider(key) : undefined;\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Read and decrypt a key-form credential from the central credential storage.\n *\n * @param account the account whose credential to resolve\n * @returns the key, or undefined (with a log line) when it cannot be read\n */\n private async resolveKey(account: AccountConfig): Promise<string | undefined> {\n if (!account.credentialId) {\n this.log.warn(`${account.name}: no credential selected \u2014 pick one in the instance settings`);\n return undefined;\n }\n try {\n const credential = await Credentials.getCredentials(this, account.credentialId);\n const values = credential.values as { key?: unknown };\n const key = typeof values.key === \"string\" && values.key ? values.key : undefined;\n if (!key) {\n this.log.warn(`${account.name}: credential ${account.credentialId} carries no API key`);\n }\n return key;\n } catch (e) {\n this.log.warn(\n `${account.name}: cannot read credential ${account.credentialId} (${e instanceof Error ? e.message : String(e)})`,\n );\n return undefined;\n }\n }\n\n /**\n * Status states retired in 0.5.0 \u2014 `provider` (visible in the node name anyway),\n * `reachable`/`serviceOnline`/`state` (three spellings of one fact) and `signedIn`\n * (the settings page asks the adapter directly).\n *\n * ioBroker never garbage-collects an object whose id the adapter stopped writing:\n * it would sit there frozen on its last value and keep lying. So the old ids are\n * deleted from a fixed list on every start \u2014 no state reading, no heuristics.\n */\n private static readonly RETIRED_INFO_STATES = [\n \"info.provider\",\n \"info.reachable\",\n \"info.serviceOnline\",\n \"info.state\",\n \"info.signedIn\",\n ];\n\n /**\n * Remove the retired status states of every configured account.\n *\n * @param accounts the configured accounts\n * @returns how many objects were actually deleted\n */\n private async removeRetiredStates(accounts: readonly AccountConfig[]): Promise<number> {\n let removed = 0;\n for (const account of accounts) {\n for (const suffix of AiUsageAdapter.RETIRED_INFO_STATES) {\n const id = `${account.id}.${suffix}`;\n try {\n // Existence-checked so the log line can report a real number; these are\n // stable dead orphans, never touched concurrently.\n if (await this.getObjectAsync(id)) {\n await this.delObjectAsync(id);\n removed++;\n }\n } catch (e) {\n this.log.debug(`Could not remove ${id}: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n }\n return removed;\n }\n\n /**\n * Delete object trees that no longer belong to a configured account, plus the\n * `auth` branch of the pre-0.3.0 layout. An EMPTY table deletes nothing \u2014 the\n * guard against wiping everything through an accidental clear.\n */\n private async cleanupStaleObjects(): Promise<void> {\n const keepIds = validAccountIds(this.config.accounts);\n if (keepIds.length === 0) {\n return;\n }\n const keep = new Set([...keepIds, \"info\", \"total\"]);\n try {\n const objects = await this.getAdapterObjectsAsync();\n const roots = new Set<string>();\n for (const id of Object.keys(objects)) {\n const root = id.substring(this.namespace.length + 1).split(\".\")[0];\n if (root && !keep.has(root)) {\n roots.add(root);\n }\n }\n for (const root of roots) {\n this.log.info(\n root === \"auth\"\n ? \"Removing the old sign-in branch \u2014 the sign-in state now lives inside each account\"\n : `Removing objects of no longer configured account \"${root}\"`,\n );\n await this.delObjectAsync(root, { recursive: true });\n }\n } catch (e) {\n this.log.warn(`Cleanup of stale objects failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n }\n\n /**\n * Tear down synchronously \u2014 no async/await here, else the controller kills the\n * process before cleanup finishes.\n *\n * @param callback invoked when cleanup is done\n */\n private onUnload(callback: () => void): void {\n try {\n for (const provider of [...this.devicePollers.keys()]) {\n this.stopDevicePoller(provider);\n }\n this.engine?.stop();\n this.engine = null;\n void this.setState(\"info.connection\", { val: false, ack: true });\n } catch {\n // never block shutdown\n }\n callback();\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new AiUsageAdapter(options);\n} else {\n (() => new AiUsageAdapter())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,8 +1,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "ai-usage",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "news": {
6
+ "0.5.0": {
7
+ "en": "Two status datapoints per account instead of six: an offline marker ioBroker actually shows plus the reason in plain text; the retired ones are removed automatically",
8
+ "de": "Zwei Status-Datenpunkte je Konto statt sechs: ein Offline-Kennzeichen, das ioBroker wirklich anzeigt, plus den Grund im Klartext; die alten werden automatisch entfernt",
9
+ "ru": "Два статусных состояния на аккаунт вместо шести: признак «не в сети», который ioBroker действительно показывает, и причина текстом; старые удаляются автоматически",
10
+ "pt": "Dois pontos de estado por conta em vez de seis: um marcador offline que o ioBroker mostra mesmo, mais o motivo em texto; os antigos são removidos automaticamente",
11
+ "nl": "Twee statusdatapunten per account in plaats van zes: een offline-markering die ioBroker echt toont, plus de reden in gewone taal; de oude worden automatisch verwijderd",
12
+ "fr": "Deux points d'état par compte au lieu de six : un marqueur hors ligne qu'ioBroker affiche vraiment, plus la raison en clair ; les anciens sont supprimés automatiquement",
13
+ "it": "Due punti di stato per account invece di sei: un contrassegno offline che ioBroker mostra davvero e il motivo in chiaro; quelli superati vengono rimossi automaticamente",
14
+ "es": "Dos puntos de estado por cuenta en lugar de seis: un marcador de desconexión que ioBroker sí muestra y el motivo en texto claro; los antiguos se eliminan automáticamente",
15
+ "pl": "Dwa punkty stanu na konto zamiast sześciu: znacznik offline, który ioBroker naprawdę pokazuje, oraz powód tekstem; stare są usuwane automatycznie",
16
+ "uk": "Дві точки стану на акаунт замість шести: позначка «не в мережі», яку ioBroker справді показує, і причина текстом; застарілі видаляються автоматично",
17
+ "zh-cn": "每个账户从六个状态数据点减到两个:ioBroker 真正会显示的离线标记,加上纯文本的原因;旧的会被自动删除"
18
+ },
19
+ "0.4.0": {
20
+ "en": "Limits of a single model no longer report the whole account as full, every account shows whether the AI service itself is online, plus Sentry error reporting and a new icon",
21
+ "de": "Limits eines einzelnen Modells melden nicht mehr das ganze Konto als voll, jedes Konto zeigt ob der KI-Dienst selbst online ist, dazu Fehlerberichte über Sentry und ein neues Symbol",
22
+ "ru": "Лимиты отдельной модели больше не отмечают весь аккаунт как исчерпанный, каждый аккаунт показывает, онлайн ли сам сервис ИИ, плюс отчёты об ошибках Sentry и новый значок",
23
+ "pt": "Limites de um único modelo já não marcam a conta inteira como cheia, cada conta mostra se o próprio serviço de IA está online, mais relatórios de erros Sentry e um novo ícone",
24
+ "nl": "Limieten van één model melden niet langer het hele account als vol, elk account toont of de AI-dienst zelf online is, plus foutrapportage via Sentry en een nieuw pictogram",
25
+ "fr": "Les limites d'un seul modèle ne marquent plus tout le compte comme plein, chaque compte indique si le service IA est en ligne, plus les rapports d'erreurs Sentry et une nouvelle icône",
26
+ "it": "I limiti di un singolo modello non segnalano più l'intero account come pieno, ogni account mostra se il servizio IA è online, più i rapporti di errore Sentry e una nuova icona",
27
+ "es": "Los límites de un solo modelo ya no marcan toda la cuenta como llena, cada cuenta muestra si el servicio de IA está en línea, además de informes de errores Sentry y un icono nuevo",
28
+ "pl": "Limity pojedynczego modelu nie oznaczają już całego konta jako pełnego, każde konto pokazuje, czy usługa AI jest online, do tego raporty błędów Sentry i nowa ikona",
29
+ "uk": "Ліміти окремої моделі більше не позначають весь акаунт як заповнений, кожен акаунт показує, чи сервіс ШІ онлайн, а також звіти про помилки Sentry і нова піктограма",
30
+ "zh-cn": "单个模型的限额不再把整个账户标记为已满,每个账户都会显示 AI 服务本身是否在线,另有 Sentry 错误报告和新图标"
31
+ },
6
32
  "0.3.0": {
7
33
  "en": "Three subscriptions instead of one: Claude, ChatGPT and Google/Gemini each with their own guided sign-in; ChatGPT and Gemini could not be tested on a live account yet",
8
34
  "de": "Drei Abos statt einem: Claude, ChatGPT und Google/Gemini mit je eigener geführter Anmeldung; ChatGPT und Gemini konnten noch an keinem echten Konto getestet werden",
@@ -43,6 +69,11 @@
43
69
  "zh-cn": "首个版本:将 Claude、OpenAI、Anthropic、OpenRouter 和 DeepSeek 账户的使用限额、余额与费用作为数据点,并在达到所选阈值时发出警告"
44
70
  }
45
71
  },
72
+ "plugins": {
73
+ "sentry": {
74
+ "dsn": "https://66f27ceff38c1ce1e6cdcc3095ba06d3@o4511524771266560.ingest.de.sentry.io/4511524773560400"
75
+ }
76
+ },
46
77
  "titleLang": {
47
78
  "en": "AI Usage",
48
79
  "de": "AI Usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.ai-usage",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "ioBroker adapter monitoring usage, limits and costs of AI accounts (Claude, OpenAI, OpenRouter, DeepSeek)",
5
5
  "author": {
6
6
  "name": "krobi",