Files
OmniRoute/open-sse/services/toolLimitDetector.ts
Denis Kotsyuba b1e27258c0 fix(chatcore): exempt opencode client from the default 128-tool truncation (#6193)
* fix(chatcore): exempt opencode client from the default 128-tool truncation

The default MAX_TOOLS_LIMIT (128) cap made truncateToolList blind-slice
tools.slice(0, 128), dropping opencode's built-in task tool and part of
its MCP tools when the inbound list exceeded 128 — so models routed
through OmniRoute could not launch subagents or reach all their tools.

Detect the opencode client (any x-opencode-* header, or 'opencode' in
the user-agent) and bypass ONLY the speculative 128 default. A known
provider ceiling (proactive PROVIDER_TOOL_LIMITS or a detected limit)
always wins and still truncates, even for opencode, so upstreams with
real hard limits (e.g. grok-cli 200) keep their 400-avoidance guard.
Non-opencode clients are unchanged.

- requestFormat.ts: add isOpencodeClient(headers, userAgent) + expose it
  on resolveChatCoreRequestFormat.
- toolLimitDetector.ts: add getKnownToolLimit(); getEffectiveToolLimit
  becomes getKnownToolLimit(provider) ?? DEFAULT_LIMIT (byte-identical
  for existing callers).
- upstreamBody.ts: truncateToolList takes bypassDefaultToolLimit and
  encodes the precedence; fix cosmetic debug-log count.
- chatCore.ts: thread the flag into prepareUpstreamBody.
- tests: extend tool-limit-detector unit tests.

* refactor(tools): accept nullable provider in tool-limit resolvers

Address PR review: widen getKnownToolLimit / getEffectiveToolLimit to
(provider: string | null | undefined) to match the call sites in
truncateToolList, and add unit assertions covering null/undefined
providers (getKnownToolLimit -> null, getEffectiveToolLimit -> 128).

---------

Co-authored-by: DKotsyuba <16292493+DKotsyuba@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:30:30 -03:00

88 lines
2.6 KiB
TypeScript

import { MAX_TOOLS_LIMIT } from "../config/constants.ts";
const DETECTED_LIMITS = new Map<string, { limit: number; timestamp: number }>();
const TTL_MS = 24 * 60 * 60 * 1000;
const DEFAULT_LIMIT = MAX_TOOLS_LIMIT;
const PROVIDER_TOOL_LIMITS: Record<string, number> = {
"grok-cli": 200,
"nvidia": 1536,
};
const _detectedLimitsSweep = setInterval(() => {
const now = Date.now();
for (const [key, entry] of DETECTED_LIMITS) {
if (now - entry.timestamp > TTL_MS) DETECTED_LIMITS.delete(key);
}
}, 60_000);
if (typeof _detectedLimitsSweep === "object" && "unref" in _detectedLimitsSweep) {
(_detectedLimitsSweep as { unref?: () => void }).unref?.();
}
export function getKnownToolLimit(provider: string | null | undefined): number | null {
const proactiveLimit = PROVIDER_TOOL_LIMITS[provider];
if (proactiveLimit !== undefined) {
return proactiveLimit;
}
const cached = DETECTED_LIMITS.get(provider);
if (cached && Date.now() - cached.timestamp < TTL_MS) {
return cached.limit;
}
return null;
}
export function getEffectiveToolLimit(provider: string | null | undefined): number {
return getKnownToolLimit(provider) ?? DEFAULT_LIMIT;
}
export function setDetectedToolLimit(provider: string, limit: number): void {
const current = getEffectiveToolLimit(provider);
if (limit < current) {
DETECTED_LIMITS.set(provider, { limit, timestamp: Date.now() });
}
}
const TOOL_LIMIT_PATTERNS = [
/'tools':\s*maximum\s+number\s+of\s+items\s+is\s+(\d+)/i,
/Maximum\s+number\s+of\s+tools\s+(?:allowed\s+)?(?:is\s+)?(\d+)/i,
/Too\s+many\s+tools\.?\s*(?:Maximum\s+)?(\d+)/i,
/\d+\s+tools\s+have\s+been\s+provided\s+but\s+(?:the\s+)?maximum\s+is\s+(\d+)/i,
/tool.*limit.*(\d+)/i,
/tools.*exceeded.*(\d+)/i,
];
export function parseToolLimitFromError(errorMessage: string): number | null {
for (const pattern of TOOL_LIMIT_PATTERNS) {
const match = errorMessage.match(pattern);
if (match && match[1]) {
const limit = parseInt(match[1], 10);
if (limit > 0 && limit <= 10000) {
return limit;
}
}
}
return null;
}
const TOOL_LIMIT_ERROR_INDICATORS = [
"maximum number of tools",
"too many tools",
"tools limit",
"'tools'",
"maximum number of items",
];
export function shouldDetectLimit(errorMessage: string, statusCode: number): boolean {
if (statusCode !== 400) return false;
const lower = errorMessage.toLowerCase();
return TOOL_LIMIT_ERROR_INDICATORS.some((indicator) => lower.includes(indicator));
}
export function getDetectedToolLimit(provider: string): number {
return getEffectiveToolLimit(provider);
}
export function clearDetectedLimits(): void {
DETECTED_LIMITS.clear();
}