Files
OmniRoute/open-sse/services/hyperagentModels.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

163 lines
5.1 KiB
TypeScript

/**
* HyperAgent (hyperagent.com) model catalog.
*
* Wire IDs were validated live against PATCH /api/threads/{id} (2026-07-21):
* Main modelId: fable-latest | opus-latest | sonnet-latest | claude-fable-5 |
* claude-opus-4-8 | claude-sonnet-5
* Subagent (defaultSubagentModel): short family names only — fable | opus | sonnet | haiku
*
* Pricing keys like bare "fable" are NOT valid chat modelIds (API returns model_unknown).
* Model is applied on the THREAD via PATCH, not in the /chat body.
*/
export interface HyperAgentModel {
/** Wire modelId for PATCH /api/threads/{id}. */
id: string;
/** Pretty picker / /v1/models name. */
name: string;
/** Short subagent override (defaultSubagentModel). */
subagent: "fable" | "opus" | "sonnet" | "haiku";
/** Agent runtime for Claude family models. */
runtimeId?: string;
}
/** Valid selectable models (live-validated). */
export const HYPERAGENT_FALLBACK_MODELS: HyperAgentModel[] = [
{ id: "fable-latest", name: "Fable 5", subagent: "fable", runtimeId: "claude-agents-sdk" },
{
id: "claude-fable-5",
name: "Claude Fable 5",
subagent: "fable",
runtimeId: "claude-agents-sdk",
},
{
id: "opus-latest",
name: "Claude Opus Latest",
subagent: "opus",
runtimeId: "claude-agents-sdk",
},
{
id: "claude-opus-4-8",
name: "Claude Opus 4.8",
subagent: "opus",
runtimeId: "claude-agents-sdk",
},
{
id: "sonnet-latest",
name: "Claude Sonnet Latest",
subagent: "sonnet",
runtimeId: "claude-agents-sdk",
},
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
subagent: "sonnet",
runtimeId: "claude-agents-sdk",
},
];
export function stripHyperAgentModelPrefix(model: string): string {
let m = (model || "").trim();
if (m.startsWith("hyperagent/")) m = m.slice("hyperagent/".length);
else if (m.startsWith("ha/")) m = m.slice(3);
else if (m.startsWith("hyper/")) m = m.slice("hyper/".length);
return m;
}
/** Alias / pretty-name / legacy pricing-key → catalog wire id. */
const ALIASES: Record<string, string> = {
// Fable
fable: "fable-latest",
"fable-5": "fable-latest",
fable5: "fable-latest",
"claude-fable-5": "claude-fable-5",
"claude-fable": "fable-latest",
"fable-latest": "fable-latest",
// Opus
opus: "opus-latest",
"opus-latest": "opus-latest",
"opus-4-8": "claude-opus-4-8",
"opus-4.8": "claude-opus-4-8",
"claude-opus-4-8": "claude-opus-4-8",
"claude-opus-4.8": "claude-opus-4-8",
"claude-opus-latest": "opus-latest",
// Sonnet
sonnet: "sonnet-latest",
"sonnet-latest": "sonnet-latest",
"sonnet-5": "claude-sonnet-5",
"claude-sonnet-5": "claude-sonnet-5",
"claude-sonnet-latest": "sonnet-latest",
// Haiku subagent only — map main requests to sonnet-latest as closest chat model
haiku: "sonnet-latest",
"haiku-4": "sonnet-latest",
"claude-haiku-4": "sonnet-latest",
};
export function resolveHyperAgentModel(model: unknown): HyperAgentModel | null {
const raw = typeof model === "string" ? stripHyperAgentModelPrefix(model) : "";
if (!raw) return null;
const lower = raw.toLowerCase().trim();
const compact = lower.replace(/[\s_]+/g, "-");
const catalog = HYPERAGENT_FALLBACK_MODELS;
// Exact wire id
const byId = catalog.find((m) => m.id.toLowerCase() === lower || m.id.toLowerCase() === compact);
if (byId) return byId;
// Pretty name
const byName = catalog.find((m) => m.name.toLowerCase() === lower);
if (byName) return byName;
// Alias table
const aliasId = ALIASES[compact] || ALIASES[lower];
if (aliasId) {
const hit = catalog.find((m) => m.id === aliasId);
if (hit) return hit;
}
// Partial contains
return (
catalog.find((m) => compact.includes(m.id.toLowerCase())) ||
catalog.find((m) => m.name.toLowerCase().includes(compact.replace(/-/g, " "))) ||
null
);
}
/** Client-facing / OpenAI response model id (wire id). */
export function clientFacingHyperAgentModelId(model: unknown): string {
const resolved = resolveHyperAgentModel(model);
if (resolved) return resolved.id;
const stripped = typeof model === "string" ? stripHyperAgentModelPrefix(model) : "";
return stripped || "opus-latest";
}
/**
* Wire modelId for PATCH /api/threads/{id}.
* Never returns bare "fable" (invalid) — maps to fable-latest.
*/
export function wireHyperAgentModelId(model: unknown): string {
return clientFacingHyperAgentModelId(model);
}
/**
* Short subagent family for defaultSubagentModel.
* Live API accepts only: fable | opus | sonnet | haiku (not *-latest).
* User request: subagent matches the selected model family.
*/
export function wireHyperAgentSubagentModelId(model: unknown): string {
const resolved = resolveHyperAgentModel(model);
if (resolved?.subagent) return resolved.subagent;
const wire = wireHyperAgentModelId(model).toLowerCase();
if (wire.includes("fable")) return "fable";
if (wire.includes("sonnet")) return "sonnet";
if (wire.includes("haiku")) return "haiku";
if (wire.includes("opus")) return "opus";
return "opus";
}
export function wireHyperAgentRuntimeId(model: unknown): string {
const resolved = resolveHyperAgentModel(model);
return resolved?.runtimeId || "claude-agents-sdk";
}