Files
OmniRoute/open-sse/services/usage/hyperagent.ts
NOXX - Commiter 1b010f6c40 feat(sse): add HyperAgent (hyperagent.com) unofficial web provider (#7994)
* feat(sse): add HyperAgent (hyperagent.com) unofficial web provider

Reverse-engineered from live SPA captures (hyperagent/*.txt):

Chat:
- Cookie session auth (full Cookie header)
- New thread via GET /threads/new (or POST /api/threads)
- POST /api/threads/{id}/chat with SPA feature flags + content
- SSE parse of text/session_start/session_end/done events
- Multi-turn sticky threadId + sessionId cache (history prefix + last assistant)

Models:
- Hardcoded catalog from SPA pricing map
- Pretty display names (Claude Fable 5) while wire modelId stays fable etc.
- /v1/models exposes pretty name; chat uses modelId

Limits:
- GET /api/settings/billing/usage → creditBlocks initialUsd/remainingUsd/usedUsd
- USD Credits quota for Limits page

Tests: 15/15 unit/executor-hyperagent

* fix(sse): HyperAgent execution mode + fable-latest wire model (no plan mode)

* fix(sse): document HyperAgent env vars + regenerate golden snapshot

Addresses pre-merge review feedback on #7994:
- Documents HYPERAGENT_USAGE_URL in .env.example and ENVIRONMENT.md
  (OMNIROUTE_DATA_DIR was already documented via the sibling PromptQL
  provider) so check-env-doc-sync.test.ts passes.
- Regenerates the provider-translate-path golden snapshot to include
  the new hyperagent/ha registry entries.
- Swaps the local toNumber() helper in usage/hyperagent.ts for the
  canonical @/shared/utils/numeric import (#7879 no-restricted-syntax
  rule landed on the release branch after this PR was opened).
- Freezes file-size baseline entries for the new
  open-sse/executors/hyperagent.ts (937 LOC, new-file cap 800) and the
  +3 LOC growth in src/lib/usage/providerLimits.ts (1000->1003), both
  irreducible to this PR's own provider-registration wiring.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-22 08:08:46 -03:00

164 lines
5.0 KiB
TypeScript

/**
* HyperAgent billing usage → UsageQuota for Limits page.
*
* Live capture (hyperagent/get_balance.txt):
* GET https://hyperagent.com/api/settings/billing/usage
* credentials: include (session Cookie)
*
* creditData.creditBlocks[]:
* initialUsd → total ($500)
* remainingUsd → remaining ($499.56)
* usedUsd → used
* expiryDate → optional resetAt
*
* plan.name → "Pay As You Go" etc.
*/
import { type UsageQuota } from "./quota.ts";
import { toNumber } from "@/shared/utils/numeric";
const USAGE_URL =
process.env.HYPERAGENT_USAGE_URL || "https://hyperagent.com/api/settings/billing/usage";
function readStr(v: unknown): string {
if (typeof v !== "string") return "";
const t = v.trim();
return t.length ? t : "";
}
function readPs(data: unknown, keys: string[]): string {
if (!data || typeof data !== "object" || Array.isArray(data)) return "";
const rec = data as Record<string, unknown>;
for (const k of keys) {
const v = readStr(rec[k]);
if (v) return v;
}
return "";
}
/** Round money to 2 decimals for UI. */
export function roundUsd(n: number): number {
if (!Number.isFinite(n)) return 0;
return Math.round(n * 100) / 100;
}
export function buildHyperAgentCreditsQuota(block: {
initialUsd?: number | null;
remainingUsd?: number | null;
usedUsd?: number | null;
expiryDate?: string | null;
}): UsageQuota {
const total = roundUsd(toNumber(block.initialUsd));
const remaining = roundUsd(toNumber(block.remainingUsd));
let used = roundUsd(toNumber(block.usedUsd));
if (used <= 0 && total > 0) used = roundUsd(Math.max(0, total - remaining));
const rem = remaining > 0 || total <= 0 ? remaining : Math.max(0, total - used);
const remainingPercentage = total > 0 ? Math.round((rem / total) * 1000) / 10 : rem > 0 ? 100 : 0;
const expiry = readStr(block.expiryDate) || null;
return {
used,
total: total > 0 ? total : used + rem,
remaining: rem,
remainingPercentage,
resetAt: expiry,
unlimited: false,
currency: "USD",
displayName: "Credits (USD)",
};
}
function normalizeCookie(raw: string): string {
const t = (raw || "").trim();
if (!t) return "";
// Accept bare session value — pass through; full Cookie header preferred.
return t;
}
export function resolveHyperAgentCookie(
apiKey?: string,
providerSpecificData?: Record<string, unknown> | null
): string {
const direct = normalizeCookie(apiKey || "");
if (direct) return direct;
return normalizeCookie(
readPs(providerSpecificData, ["cookie", "sessionCookie", "authCookie", "Cookie"])
);
}
export async function getHyperAgentUsage(
apiKey?: string,
providerSpecificData?: Record<string, unknown> | null
) {
const cookie = resolveHyperAgentCookie(apiKey, providerSpecificData);
if (!cookie) {
return {
message:
"HyperAgent session cookie not available. Paste the full Cookie header from hyperagent.com (DevTools → Network → any request → Cookie).",
};
}
try {
const res = await fetch(USAGE_URL, {
method: "GET",
headers: {
accept: "application/json",
cookie,
origin: "https://hyperagent.com",
referer: "https://hyperagent.com/settings/billing",
"user-agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
},
});
if (!res.ok) {
const bodyText = await res.text().catch(() => "");
return {
message: `HyperAgent usage HTTP ${res.status}${bodyText ? `: ${bodyText.slice(0, 200)}` : ""}`,
plan: "HyperAgent",
};
}
const json = (await res.json()) as {
plan?: { id?: string; name?: string; monthlyPriceUsd?: number; includedUsageUsd?: number };
orbCostsAndUsage?: { totalCost?: string | number; subtotalCost?: string | number };
creditData?: {
subscriptionCredits?: unknown;
creditBlocks?: Array<{
id?: string;
initialUsd?: number;
remainingUsd?: number;
usedUsd?: number;
expiryDate?: string | null;
status?: string;
}>;
};
openInvoiceTotalUsd?: number;
bonusCreditsUsedThisPeriodUsd?: number;
};
const blocks = json.creditData?.creditBlocks || [];
const active = blocks.find((b) => (b.status || "").toLowerCase() === "active") || blocks[0];
if (!active) {
return {
message: "No active HyperAgent credit block (creditData.creditBlocks empty).",
plan: json.plan?.name || "HyperAgent",
};
}
const credits = buildHyperAgentCreditsQuota(active);
const planName = readStr(json.plan?.name) || "HyperAgent";
return {
plan: planName,
quotas: {
credits,
},
remainingUsd: credits.remaining,
drawnUsd: credits.used,
availableUsd: credits.total,
totalCost: toNumber(json.orbCostsAndUsage?.totalCost),
};
} catch (err) {
return {
message: `HyperAgent usage failed: ${err instanceof Error ? err.message : String(err)}`,
plan: "HyperAgent",
};
}
}