@oar-sh/oar 0.9.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.
- package/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Grok / xAI plan usage.
|
|
5
|
+
*
|
|
6
|
+
* There is no ACP plan-quota RPC (probed 2026-08-08: session/usage, x.ai/usage,
|
|
7
|
+
* … all Method not found). Authoritative remaining credits live only in the
|
|
8
|
+
* product `/usage` TUI. What the agent *does* return is rich per-prompt usage
|
|
9
|
+
* on the session/prompt result `_meta.usage` (tokens + costUsdTicks).
|
|
10
|
+
*
|
|
11
|
+
* So the Grok card is reconstructed like Cursor's estimated path:
|
|
12
|
+
* - always show the latest turn's token/cost detail when a snapshot exists;
|
|
13
|
+
* - optionally accumulate turn cost into a monthly cycle when the user enters
|
|
14
|
+
* a monthly USD allowance + reset day;
|
|
15
|
+
* - badge meters as estimated and link to the console for real billing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
SOURCE_CACHE,
|
|
20
|
+
SOURCE_LIVE,
|
|
21
|
+
SOURCE_MANUAL,
|
|
22
|
+
SOURCE_WORKER,
|
|
23
|
+
STATUS_NOT_CONFIGURED,
|
|
24
|
+
STATUS_OK,
|
|
25
|
+
STATUS_PARTIAL,
|
|
26
|
+
STATUS_UNAVAILABLE,
|
|
27
|
+
buildDetailSection,
|
|
28
|
+
buildMeter,
|
|
29
|
+
buildProviderCard,
|
|
30
|
+
buildUnavailableCard,
|
|
31
|
+
pickNonNegative,
|
|
32
|
+
pickNumber,
|
|
33
|
+
roundCurrency,
|
|
34
|
+
toFiniteNumber,
|
|
35
|
+
toIsoTimestamp,
|
|
36
|
+
toTrimmedString,
|
|
37
|
+
} from './plan-usage-contract.mjs';
|
|
38
|
+
// Reuse Cursor's cycle calendar (same monthly reset-day semantics).
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_CURSOR_RESET_DAY,
|
|
41
|
+
normalizeCursorResetDay,
|
|
42
|
+
resolveCursorBillingCycle,
|
|
43
|
+
} from './plan-usage-cursor.mjs';
|
|
44
|
+
|
|
45
|
+
export const GROK_PROVIDER_ID = 'grok';
|
|
46
|
+
export const GROK_LABEL = 'Grok';
|
|
47
|
+
|
|
48
|
+
/** Console landing for SuperGrok / API billing (card footer link). */
|
|
49
|
+
export const GROK_BILLING_URL = 'https://console.x.ai';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ACP reports cost as integer ticks. Working assumption: 1 USD = 1e9 ticks
|
|
53
|
+
* (a live grok-4.5 turn reported costUsdTicks=148864000 for ~14k tokens,
|
|
54
|
+
* which reads as $0.149 at 1e9 ticks/USD).
|
|
55
|
+
*
|
|
56
|
+
* Calibration attempt 2026-08-08: exact USD verification is impossible for
|
|
57
|
+
* OIDC subscription logins — the account has no dollar billing at all, only
|
|
58
|
+
* a weekly SuperGrok percentage quota on grok.com (console.x.ai shows
|
|
59
|
+
* nothing). Magnitude cross-check: ~$2.45 of tick-USD booked locally lined
|
|
60
|
+
* up with 25% of a weekly plan quota, implying a weekly allowance in the
|
|
61
|
+
* $10-40-equivalent range — plausible at 1e9, implausible (<$1/week) at
|
|
62
|
+
* 1e10. So 1e9 is very likely right but unprovable to the cent; everything
|
|
63
|
+
* derived from it stays labeled "estimated".
|
|
64
|
+
*/
|
|
65
|
+
export const COST_USD_TICKS_PER_USD = 1_000_000_000;
|
|
66
|
+
|
|
67
|
+
export const DEFAULT_GROK_RESET_DAY = DEFAULT_CURSOR_RESET_DAY;
|
|
68
|
+
|
|
69
|
+
export function costUsdTicksToUsd(ticks) {
|
|
70
|
+
const numeric = toFiniteNumber(ticks);
|
|
71
|
+
if (numeric === null || numeric < 0) return null;
|
|
72
|
+
// Full precision here: rounding per turn before the cycle ledger sums the
|
|
73
|
+
// values would book every sub-cent turn as $0.00 forever. Display formatting
|
|
74
|
+
// (formatUsd / roundCurrency) rounds at render time.
|
|
75
|
+
return numeric / COST_USD_TICKS_PER_USD;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeGrokAllowanceSettings(raw = {}) {
|
|
79
|
+
const monthlyUsd = (() => {
|
|
80
|
+
const value = toFiniteNumber(raw?.monthlyUsd);
|
|
81
|
+
// A $0 allowance cannot meter anything ("$X of $0.00 used") — treat it
|
|
82
|
+
// as unset, same as negative or missing.
|
|
83
|
+
if (value === null || value <= 0) return null;
|
|
84
|
+
return Math.round(value * 100) / 100;
|
|
85
|
+
})();
|
|
86
|
+
return {
|
|
87
|
+
monthlyUsd,
|
|
88
|
+
resetDay: normalizeCursorResetDay(
|
|
89
|
+
raw?.resetDay === null || raw?.resetDay === undefined || raw?.resetDay === ''
|
|
90
|
+
? DEFAULT_GROK_RESET_DAY
|
|
91
|
+
: raw.resetDay,
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveGrokBillingCycle(options = {}) {
|
|
97
|
+
return resolveCursorBillingCycle({
|
|
98
|
+
resetDay: options.resetDay ?? DEFAULT_GROK_RESET_DAY,
|
|
99
|
+
now: options.now,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a single turn's usage from ACP prompt-result `_meta` (or a
|
|
105
|
+
* usage_update payload). Returns null when nothing usable is present.
|
|
106
|
+
*/
|
|
107
|
+
export function normalizeGrokTurnUsage(raw = {}) {
|
|
108
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
109
|
+
|
|
110
|
+
// Accept both the nested `_meta.usage` shape and a flattened worker body.
|
|
111
|
+
const usage = raw.usage && typeof raw.usage === 'object' ? raw.usage : raw;
|
|
112
|
+
const meta = raw._meta && typeof raw._meta === 'object' ? raw._meta : raw;
|
|
113
|
+
|
|
114
|
+
const inputTokens = pickNonNegative(
|
|
115
|
+
usage.inputTokens,
|
|
116
|
+
usage.input_tokens,
|
|
117
|
+
meta.inputTokens,
|
|
118
|
+
meta.input_tokens,
|
|
119
|
+
);
|
|
120
|
+
const outputTokens = pickNonNegative(
|
|
121
|
+
usage.outputTokens,
|
|
122
|
+
usage.output_tokens,
|
|
123
|
+
meta.outputTokens,
|
|
124
|
+
meta.output_tokens,
|
|
125
|
+
);
|
|
126
|
+
const totalTokens = pickNonNegative(
|
|
127
|
+
usage.totalTokens,
|
|
128
|
+
usage.total_tokens,
|
|
129
|
+
meta.totalTokens,
|
|
130
|
+
meta.total_tokens,
|
|
131
|
+
);
|
|
132
|
+
const cachedReadTokens = pickNonNegative(
|
|
133
|
+
usage.cachedReadTokens,
|
|
134
|
+
usage.cacheReadTokens,
|
|
135
|
+
usage.cache_read_tokens,
|
|
136
|
+
meta.cachedReadTokens,
|
|
137
|
+
);
|
|
138
|
+
const cacheCreationTokens = pickNonNegative(
|
|
139
|
+
usage.cacheCreationTokens,
|
|
140
|
+
usage.cacheWriteTokens,
|
|
141
|
+
usage.cache_creation_tokens,
|
|
142
|
+
meta.cacheCreationTokens,
|
|
143
|
+
);
|
|
144
|
+
const reasoningTokens = pickNonNegative(
|
|
145
|
+
usage.reasoningTokens,
|
|
146
|
+
usage.reasoning_tokens,
|
|
147
|
+
meta.reasoningTokens,
|
|
148
|
+
);
|
|
149
|
+
const costUsd = pickNonNegative(
|
|
150
|
+
usage.costUsd,
|
|
151
|
+
usage.cost_usd,
|
|
152
|
+
meta.costUsd,
|
|
153
|
+
) ?? costUsdTicksToUsd(
|
|
154
|
+
pickNumber(usage.costUsdTicks, usage.cost_usd_ticks, meta.costUsdTicks),
|
|
155
|
+
);
|
|
156
|
+
const apiDurationMs = pickNonNegative(usage.apiDurationMs, usage.api_duration_ms, meta.apiDurationMs);
|
|
157
|
+
const modelCalls = pickNonNegative(usage.modelCalls, usage.model_calls);
|
|
158
|
+
const numTurns = pickNonNegative(usage.numTurns, usage.num_turns, meta.numTurns);
|
|
159
|
+
const modelId = toTrimmedString(
|
|
160
|
+
usage.modelId
|
|
161
|
+
|| usage.model
|
|
162
|
+
|| meta.modelId
|
|
163
|
+
|| meta.model
|
|
164
|
+
|| raw.modelId
|
|
165
|
+
|| raw.model,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const modelUsageRaw = usage.modelUsage || usage.model_usage || meta.modelUsage || null;
|
|
169
|
+
const modelUsage = [];
|
|
170
|
+
if (modelUsageRaw && typeof modelUsageRaw === 'object') {
|
|
171
|
+
for (const [model, entry] of Object.entries(modelUsageRaw)) {
|
|
172
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
173
|
+
// ACP `_meta` ships a { modelName: entry } map, but a worker re-posting
|
|
174
|
+
// an already-normalized blob ships an array — whose Object.entries keys
|
|
175
|
+
// are indices ("0"), so the entry's own model field must win there.
|
|
176
|
+
const modelName = toTrimmedString(Array.isArray(modelUsageRaw) ? entry.model : model);
|
|
177
|
+
if (!modelName) continue;
|
|
178
|
+
modelUsage.push({
|
|
179
|
+
model: modelName,
|
|
180
|
+
inputTokens: pickNonNegative(entry.inputTokens, entry.input_tokens),
|
|
181
|
+
outputTokens: pickNonNegative(entry.outputTokens, entry.output_tokens),
|
|
182
|
+
totalTokens: pickNonNegative(entry.totalTokens, entry.total_tokens),
|
|
183
|
+
cachedReadTokens: pickNonNegative(entry.cachedReadTokens, entry.cacheReadTokens),
|
|
184
|
+
cacheCreationTokens: pickNonNegative(entry.cacheCreationTokens, entry.cacheWriteTokens),
|
|
185
|
+
reasoningTokens: pickNonNegative(entry.reasoningTokens),
|
|
186
|
+
costUsd: pickNonNegative(entry.costUsd)
|
|
187
|
+
?? costUsdTicksToUsd(pickNumber(entry.costUsdTicks, entry.cost_usd_ticks)),
|
|
188
|
+
modelCalls: pickNonNegative(entry.modelCalls),
|
|
189
|
+
apiDurationMs: pickNonNegative(entry.apiDurationMs),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// apiDurationMs is deliberately not a signal: a duration-only payload has
|
|
195
|
+
// no tokens or cost and would only inflate the cycle's turn count.
|
|
196
|
+
const hasSignal = [
|
|
197
|
+
inputTokens, outputTokens, totalTokens, cachedReadTokens, cacheCreationTokens,
|
|
198
|
+
reasoningTokens, costUsd,
|
|
199
|
+
].some((value) => value !== null) || modelUsage.length > 0;
|
|
200
|
+
|
|
201
|
+
if (!hasSignal) return null;
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
modelId,
|
|
205
|
+
inputTokens,
|
|
206
|
+
outputTokens,
|
|
207
|
+
totalTokens,
|
|
208
|
+
cachedReadTokens,
|
|
209
|
+
cacheCreationTokens,
|
|
210
|
+
reasoningTokens,
|
|
211
|
+
costUsd,
|
|
212
|
+
apiDurationMs,
|
|
213
|
+
modelCalls,
|
|
214
|
+
numTurns,
|
|
215
|
+
modelUsage,
|
|
216
|
+
capturedAt: toIsoTimestamp(raw.capturedAt) || new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Extract usage from a live ACP session/prompt result object.
|
|
222
|
+
*/
|
|
223
|
+
export function extractGrokUsageFromPromptResult(promptResult = null) {
|
|
224
|
+
if (!promptResult || typeof promptResult !== 'object') return null;
|
|
225
|
+
const meta = promptResult._meta && typeof promptResult._meta === 'object'
|
|
226
|
+
? promptResult._meta
|
|
227
|
+
: null;
|
|
228
|
+
if (!meta) return null;
|
|
229
|
+
return normalizeGrokTurnUsage({
|
|
230
|
+
...meta,
|
|
231
|
+
usage: meta.usage,
|
|
232
|
+
modelId: meta.modelId,
|
|
233
|
+
inputTokens: meta.inputTokens,
|
|
234
|
+
outputTokens: meta.outputTokens,
|
|
235
|
+
totalTokens: meta.totalTokens,
|
|
236
|
+
cachedReadTokens: meta.cachedReadTokens,
|
|
237
|
+
reasoningTokens: meta.reasoningTokens,
|
|
238
|
+
costUsdTicks: meta.usage?.costUsdTicks ?? meta.costUsdTicks,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatUsd(value) {
|
|
243
|
+
const numeric = roundCurrency(value);
|
|
244
|
+
return numeric === null ? null : `$${numeric.toFixed(2)}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatTokens(value) {
|
|
248
|
+
const numeric = toFiniteNumber(value);
|
|
249
|
+
if (numeric === null || numeric < 0) return null;
|
|
250
|
+
if (numeric < 1000) return String(Math.round(numeric));
|
|
251
|
+
if (numeric < 1_000_000) return `${(numeric / 1000).toFixed(1)}k`;
|
|
252
|
+
return `${(numeric / 1_000_000).toFixed(1)}M`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatDuration(ms) {
|
|
256
|
+
const value = toFiniteNumber(ms);
|
|
257
|
+
if (value === null || value < 0) return null;
|
|
258
|
+
const totalSeconds = Math.round(value / 1000);
|
|
259
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
260
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
261
|
+
const seconds = totalSeconds % 60;
|
|
262
|
+
return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {object} options
|
|
267
|
+
* @param {object|null} options.usage last-turn snapshot (normalizeGrokTurnUsage)
|
|
268
|
+
* @param {object|null} options.cycleTotals { costUsd, inputTokens, outputTokens, totalTokens, turnCount }
|
|
269
|
+
* @param {object|null} options.allowances normalizeGrokAllowanceSettings()
|
|
270
|
+
* @param {object|null} options.cycle resolveGrokBillingCycle()
|
|
271
|
+
* @param {object|null} options.billing normalizeGrokBillingCredits() — live plan quota
|
|
272
|
+
* @param {string|null} options.capturedAt
|
|
273
|
+
* @param {boolean} options.configured Grok provider enabled
|
|
274
|
+
* @param {string|null} options.message
|
|
275
|
+
*/
|
|
276
|
+
export function buildGrokPlanCard({
|
|
277
|
+
usage = null,
|
|
278
|
+
cycleTotals = null,
|
|
279
|
+
allowances = null,
|
|
280
|
+
cycle = null,
|
|
281
|
+
billing = null,
|
|
282
|
+
capturedAt = null,
|
|
283
|
+
configured = true,
|
|
284
|
+
message = null,
|
|
285
|
+
stale = false,
|
|
286
|
+
} = {}) {
|
|
287
|
+
if (!configured) {
|
|
288
|
+
// Caller usually filters Grok out when disabled; this is a defensive path.
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const settings = normalizeGrokAllowanceSettings(allowances || {});
|
|
293
|
+
const resolvedCycle = cycle || resolveGrokBillingCycle({ resetDay: settings.resetDay });
|
|
294
|
+
const totals = cycleTotals && typeof cycleTotals === 'object'
|
|
295
|
+
? {
|
|
296
|
+
costUsd: toFiniteNumber(cycleTotals.costUsd) || 0,
|
|
297
|
+
inputTokens: toFiniteNumber(cycleTotals.inputTokens) || 0,
|
|
298
|
+
outputTokens: toFiniteNumber(cycleTotals.outputTokens) || 0,
|
|
299
|
+
totalTokens: toFiniteNumber(cycleTotals.totalTokens) || 0,
|
|
300
|
+
turnCount: toFiniteNumber(cycleTotals.turnCount) || 0,
|
|
301
|
+
}
|
|
302
|
+
: { costUsd: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, turnCount: 0 };
|
|
303
|
+
|
|
304
|
+
const liveQuotaPercent = toFiniteNumber(billing?.usagePercent);
|
|
305
|
+
const hasLiveQuota = liveQuotaPercent !== null;
|
|
306
|
+
|
|
307
|
+
if (!hasLiveQuota && !usage && totals.turnCount <= 0 && totals.costUsd <= 0) {
|
|
308
|
+
return buildUnavailableCard({
|
|
309
|
+
provider: GROK_PROVIDER_ID,
|
|
310
|
+
label: GROK_LABEL,
|
|
311
|
+
status: STATUS_UNAVAILABLE,
|
|
312
|
+
message: toTrimmedString(message)
|
|
313
|
+
|| 'No Grok usage captured yet. Run a Grok turn — per-prompt tokens and cost come from the agent result (there is no live plan-quota API).',
|
|
314
|
+
links: [{ label: 'xAI console / billing', url: GROK_BILLING_URL }],
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const meters = [];
|
|
319
|
+
if (hasLiveQuota) {
|
|
320
|
+
const periodLabel = billing.periodType === 'weekly'
|
|
321
|
+
? 'Weekly plan usage'
|
|
322
|
+
: `Plan usage${billing.periodType ? ` (${billing.periodType})` : ''}`;
|
|
323
|
+
const productBits = (billing.products || [])
|
|
324
|
+
.map((entry) => `${entry.product} ${entry.usagePercent}%`)
|
|
325
|
+
.join(' · ');
|
|
326
|
+
meters.push(buildMeter({
|
|
327
|
+
id: 'grok-plan-quota',
|
|
328
|
+
label: periodLabel,
|
|
329
|
+
unit: 'percent',
|
|
330
|
+
utilization: liveQuotaPercent,
|
|
331
|
+
resetAt: billing.periodEnd || null,
|
|
332
|
+
estimated: false,
|
|
333
|
+
emphasis: 'primary',
|
|
334
|
+
note: productBits
|
|
335
|
+
? `Live from the Grok subscription (${productBits}).`
|
|
336
|
+
: 'Live from the Grok subscription.',
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
if (settings.monthlyUsd !== null) {
|
|
340
|
+
meters.push(buildMeter({
|
|
341
|
+
id: 'grok-monthly-spend',
|
|
342
|
+
label: 'Estimated monthly spend',
|
|
343
|
+
unit: 'usd',
|
|
344
|
+
used: totals.costUsd,
|
|
345
|
+
allowance: settings.monthlyUsd,
|
|
346
|
+
resetAt: resolvedCycle.endsAt,
|
|
347
|
+
estimated: true,
|
|
348
|
+
emphasis: hasLiveQuota ? 'secondary' : 'primary',
|
|
349
|
+
note: 'Local estimate from relay turns. xAI does not expose remaining plan credits over ACP.',
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const details = [];
|
|
354
|
+
if (usage) {
|
|
355
|
+
const rows = [];
|
|
356
|
+
const cost = formatUsd(usage.costUsd);
|
|
357
|
+
if (cost) rows.push({ label: 'Last turn cost (estimate)', value: cost });
|
|
358
|
+
if (usage.modelId) rows.push({ label: 'Model', value: usage.modelId });
|
|
359
|
+
const tokenBits = [
|
|
360
|
+
formatTokens(usage.inputTokens) ? `in ${formatTokens(usage.inputTokens)}` : null,
|
|
361
|
+
formatTokens(usage.outputTokens) ? `out ${formatTokens(usage.outputTokens)}` : null,
|
|
362
|
+
formatTokens(usage.cachedReadTokens) ? `cache r ${formatTokens(usage.cachedReadTokens)}` : null,
|
|
363
|
+
formatTokens(usage.reasoningTokens) ? `reason ${formatTokens(usage.reasoningTokens)}` : null,
|
|
364
|
+
formatTokens(usage.totalTokens) ? `total ${formatTokens(usage.totalTokens)}` : null,
|
|
365
|
+
].filter(Boolean);
|
|
366
|
+
if (tokenBits.length) rows.push({ label: 'Tokens', value: tokenBits.join(' · ') });
|
|
367
|
+
const apiDuration = formatDuration(usage.apiDurationMs);
|
|
368
|
+
if (apiDuration) rows.push({ label: 'API time', value: apiDuration });
|
|
369
|
+
if (usage.numTurns !== null) rows.push({ label: 'Agent turns', value: String(usage.numTurns) });
|
|
370
|
+
const lastSection = buildDetailSection({
|
|
371
|
+
id: 'grok-last-turn',
|
|
372
|
+
label: 'Latest turn',
|
|
373
|
+
rows,
|
|
374
|
+
note: 'From the last Grok agent prompt result on this relay host.',
|
|
375
|
+
});
|
|
376
|
+
if (lastSection) details.push(lastSection);
|
|
377
|
+
|
|
378
|
+
const modelRows = (usage.modelUsage || []).map((entry) => ({
|
|
379
|
+
label: entry.model,
|
|
380
|
+
value: formatUsd(entry.costUsd) ?? '—',
|
|
381
|
+
hint: [
|
|
382
|
+
formatTokens(entry.inputTokens) ? `in ${formatTokens(entry.inputTokens)}` : null,
|
|
383
|
+
formatTokens(entry.outputTokens) ? `out ${formatTokens(entry.outputTokens)}` : null,
|
|
384
|
+
formatTokens(entry.cachedReadTokens) ? `cache r ${formatTokens(entry.cachedReadTokens)}` : null,
|
|
385
|
+
].filter(Boolean).join(' · ') || null,
|
|
386
|
+
}));
|
|
387
|
+
const modelSection = buildDetailSection({
|
|
388
|
+
id: 'grok-models',
|
|
389
|
+
label: 'By model (last turn)',
|
|
390
|
+
rows: modelRows,
|
|
391
|
+
});
|
|
392
|
+
if (modelSection) details.push(modelSection);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (totals.turnCount > 0 || totals.costUsd > 0) {
|
|
396
|
+
const cycleRows = [];
|
|
397
|
+
const cycleCost = formatUsd(totals.costUsd);
|
|
398
|
+
if (cycleCost) cycleRows.push({ label: 'Cycle spend (estimate)', value: cycleCost });
|
|
399
|
+
if (totals.turnCount > 0) cycleRows.push({ label: 'Turns tracked', value: String(Math.round(totals.turnCount)) });
|
|
400
|
+
const cycleTokens = formatTokens(totals.totalTokens);
|
|
401
|
+
if (cycleTokens) cycleRows.push({ label: 'Tokens (sum)', value: cycleTokens });
|
|
402
|
+
if (resolvedCycle.startsAt && resolvedCycle.endsAt) {
|
|
403
|
+
cycleRows.push({
|
|
404
|
+
label: 'Billing cycle',
|
|
405
|
+
value: `${resolvedCycle.startsAt.slice(0, 10)} → ${resolvedCycle.endsAt.slice(0, 10)}`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const cycleSection = buildDetailSection({
|
|
409
|
+
id: 'grok-cycle',
|
|
410
|
+
label: 'Current cycle (local)',
|
|
411
|
+
rows: cycleRows,
|
|
412
|
+
note: settings.monthlyUsd === null
|
|
413
|
+
? 'Set a monthly allowance in Settings to show a remaining-budget meter.'
|
|
414
|
+
: 'Accumulated from Grok turns on this relay. Not an xAI statement.',
|
|
415
|
+
});
|
|
416
|
+
if (cycleSection) details.push(cycleSection);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const hasMeter = meters.filter(Boolean).length > 0;
|
|
420
|
+
return buildProviderCard({
|
|
421
|
+
provider: GROK_PROVIDER_ID,
|
|
422
|
+
label: GROK_LABEL,
|
|
423
|
+
status: hasMeter ? STATUS_OK : STATUS_PARTIAL,
|
|
424
|
+
message: hasMeter
|
|
425
|
+
? null
|
|
426
|
+
: 'No live plan-quota data for Grok right now. Showing last-turn cost/tokens; set a monthly allowance in Settings for an estimated budget meter.',
|
|
427
|
+
source: hasLiveQuota
|
|
428
|
+
? SOURCE_LIVE
|
|
429
|
+
: (settings.monthlyUsd !== null ? SOURCE_MANUAL : (stale ? SOURCE_CACHE : SOURCE_WORKER)),
|
|
430
|
+
stale: stale === true,
|
|
431
|
+
capturedAt: capturedAt || usage?.capturedAt || null,
|
|
432
|
+
meters: meters.filter(Boolean),
|
|
433
|
+
details,
|
|
434
|
+
links: [{ label: 'xAI console / billing', url: GROK_BILLING_URL }],
|
|
435
|
+
});
|
|
436
|
+
}
|