mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* 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>
164 lines
5.0 KiB
TypeScript
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",
|
|
};
|
|
}
|
|
}
|