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>
This commit is contained in:
NOXX - Commiter
2026-07-26 09:53:06 +03:00
committed by GitHub
parent fae10668ba
commit 2d48bb6dca
4 changed files with 69 additions and 9 deletions

View File

@@ -4,6 +4,9 @@ import { HYPERAGENT_FALLBACK_MODELS } from "../../../../services/hyperagentModel
// HyperAgent (hyperagent.com) — unofficial reverse-engineered web session.
// Auth: browser Cookie header. Chat: POST /api/threads/{id}/chat (SSE).
// Credits: GET /api/settings/billing/usage → creditData.creditBlocks.
/** Claude-family agent models on HyperAgent — 1M context (not the 128k openai default). */
const HYPERAGENT_CONTEXT_LENGTH = 1_000_000;
export const hyperagentProvider: RegistryEntry = {
id: "hyperagent",
alias: "ha",
@@ -13,8 +16,11 @@ export const hyperagentProvider: RegistryEntry = {
authType: "apikey",
authHeader: "cookie",
passthroughModels: true,
/** Used by getTokenLimit when model-specific context is missing. */
defaultContextLength: HYPERAGENT_CONTEXT_LENGTH,
models: HYPERAGENT_FALLBACK_MODELS.map((m) => ({
id: m.id,
name: m.name,
contextLength: HYPERAGENT_CONTEXT_LENGTH,
})),
};

View File

@@ -15,6 +15,10 @@ const DEFAULT_LIMITS: Record<string, number> = {
openai: 128000,
gemini: 1000000,
codex: 400000,
// HyperAgent Claude-family agents (fable/opus/sonnet) — 1M default; was falling
// through to 128k and blocking normal agentic tool loops with huge catalogs.
hyperagent: 1_000_000,
ha: 1_000_000,
default: 128000,
};
@@ -205,6 +209,8 @@ function resolveTokenLimit(
const envOverride = getEnvOverride(provider);
if (envOverride) return { limit: envOverride, specific: true };
const lowerModel = (model || "").toLowerCase();
// 2. Check models.dev synced DB for per-model context limit
if (model) {
const dbLimit = getModelContextLimit(provider, model);
@@ -219,15 +225,14 @@ function resolveTokenLimit(
// 4. Check if model name hints at a known limit
if (model) {
const lower = model.toLowerCase();
if (lower.includes("claude")) return { limit: DEFAULT_LIMITS.claude, specific: true };
if (lower.includes("gemini")) return { limit: DEFAULT_LIMITS.gemini, specific: true };
if (lowerModel.includes("claude")) return { limit: DEFAULT_LIMITS.claude, specific: true };
if (lowerModel.includes("gemini")) return { limit: DEFAULT_LIMITS.gemini, specific: true };
if (
lower.includes("gpt") ||
lower.includes("o1") ||
lower.includes("o3") ||
lower.includes("o4") ||
lower.includes("codex")
lowerModel.includes("gpt") ||
lowerModel.includes("o1") ||
lowerModel.includes("o3") ||
lowerModel.includes("o4") ||
lowerModel.includes("codex")
)
return { limit: DEFAULT_LIMITS.codex, specific: true };
}

View File

@@ -19,40 +19,56 @@ export interface HyperAgentModel {
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" },
{
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,
},
];

View File

@@ -34,6 +34,39 @@ test("getTokenLimit: default fallback", () => {
assert.equal(getTokenLimit("unknown"), 128000);
});
// Regression for #8496: hyperagent Claude-family agents (fable/opus/sonnet) must
// resolve to the 1M context window for every fallback model id, driven solely by
// the registry's `defaultContextLength` (open-sse/config/providers/registry/hyperagent) —
// not by a provider-unscoped model-name substring match, which previously collided
// with unrelated providers serving the same Claude model ids (see
// "getTokenLimit: does not force 1M onto non-hyperagent providers" below).
const HYPERAGENT_FALLBACK_MODEL_IDS = [
"fable-latest",
"claude-fable-5",
"opus-latest",
"claude-opus-4-8",
"sonnet-latest",
"claude-sonnet-5",
];
for (const modelId of HYPERAGENT_FALLBACK_MODEL_IDS) {
test(`getTokenLimit: hyperagent/${modelId} resolves to 1M context`, () => {
assert.equal(getTokenLimit("hyperagent", modelId), 1_000_000);
});
test(`getTokenLimit: ha (alias)/${modelId} resolves to 1M context`, () => {
assert.equal(getTokenLimit("ha", modelId), 1_000_000);
});
}
test("getTokenLimit: does not force 1M onto non-hyperagent providers serving the same model ids", () => {
// windsurf declares an explicit per-model contextLength of 200000 for this exact id —
// a provider-unscoped substring match on "claude-opus-4" would have clobbered it to 1M.
assert.equal(getTokenLimit("windsurf", "claude-opus-4.7-max"), 200000);
// bluesminds likewise pins its own claude-opus-4-5 entry to 200000.
assert.equal(getTokenLimit("bluesminds", "claude-opus-4-5"), 200000);
});
// ─── compressContext ────────────────────────────────────────────────────────
test("compressContext: returns unchanged if fits", () => {