Files
OmniRoute/open-sse/services/hyperagentModels.ts
NOXX - Commiter 2d48bb6dca fix(hyperagent): default 1M context for fable/opus/sonnet (#8496)
* fix(hyperagent): default 1M context for fable/opus/sonnet

HyperAgent Claude-family models (fable, opus, sonnet) were falling through
getTokenLimit to the generic 128k default. Agentic tool-loop prompts with
large catalogs then failed with context_length_exceeded (~137k tokens).

- defaultContextLength + per-model contextLength = 1_000_000 on hyperagent registry
- DEFAULT_LIMITS.hyperagent / ha = 1M
- Resolve hyperagent/ha (and fable/opus wire ids) before models.dev DB fallback

Verified: getTokenLimit('hyperagent','fable-latest') === 1000000; context-manager tests 30/30.

* fix(sse): scope hyperagent 1M context fix to the registry, drop unscoped model-name match

The step-1b branch in resolveTokenLimit() matched fable/opus/sonnet model
name substrings for ANY provider, before the models.dev DB lookup. That
collided with anthropic/claude, kiro, windsurf and bluesminds registries,
which serve the same Claude model ids (e.g. claude-opus-4.7-max,
claude-sonnet-5) with their own accurate per-model contextLength — those
were being clobbered to 1M instead of their real (often 200k) limit.

The registry-level defaultContextLength added on the hyperagent provider
entry already fixes the reported bug (getTokenLimit('hyperagent', ...) ===
1_000_000) on its own, scoped correctly by provider. Remove the redundant,
unscoped substring branch and add regression coverage for every hyperagent
fallback model id plus the cross-provider collision.

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:06 -03:00

179 lines
5.7 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;
/** Context window for OmniRoute getTokenLimit / compression (Claude-family → 1M). */
contextLength?: number;
}
/** Default context for Fable / Opus / Sonnet on HyperAgent (1M tokens). */
export const HYPERAGENT_DEFAULT_CONTEXT_LENGTH = 1_000_000;
/** Valid selectable models (live-validated). */
export const HYPERAGENT_FALLBACK_MODELS: HyperAgentModel[] = [
{
id: "fable-latest",
name: "Fable 5",
subagent: "fable",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
{
id: "claude-fable-5",
name: "Claude Fable 5",
subagent: "fable",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
{
id: "opus-latest",
name: "Claude Opus Latest",
subagent: "opus",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
{
id: "claude-opus-4-8",
name: "Claude Opus 4.8",
subagent: "opus",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
{
id: "sonnet-latest",
name: "Claude Sonnet Latest",
subagent: "sonnet",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
subagent: "sonnet",
runtimeId: "claude-agents-sdk",
contextLength: HYPERAGENT_DEFAULT_CONTEXT_LENGTH,
},
];
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";
}