666ghj/MiroFish · error · ValueError

Zep search limit must be an integer

Error message

Zep search limit must be an integer

What it means

Raised by normalize_zep_search_limit in backend/app/utils/zep.py when int(limit) raises TypeError or ValueError — i.e. limit is None, a non-numeric string ("", "ten"), or a non-numeric type like dict/list. Numeric strings ("10") and floats (10.0) convert fine and do NOT trigger this. It is a ValueError chained from the original exception, raised before any Zep Cloud call.

Source

Thrown at backend/app/utils/zep.py:50

def normalize_zep_search_query(query: Any) -> str:
    """Return a non-empty query within Zep Cloud's endpoint limit."""

    if not isinstance(query, str):
        raise ValueError("Zep search query must be a string")
    normalized = query.strip()
    if not normalized:
        raise ValueError("Zep search query must not be empty")
    return normalized[:MAX_ZEP_SEARCH_QUERY_CHARS]


def normalize_zep_search_limit(limit: Any) -> int:
    """Clamp a search result limit to the current Zep Cloud contract."""

    try:
        normalized = int(limit)
    except (TypeError, ValueError) as exc:
        raise ValueError("Zep search limit must be an integer") from exc
    if normalized < 1:
        raise ValueError("Zep search limit must be at least 1")
    return min(normalized, MAX_ZEP_SEARCH_RESULTS)


@lru_cache(maxsize=4)
def _cached_zep_client(api_key: str, timeout: float) -> Zep:
    return Zep(
        api_key=api_key,
        base_url=ZEP_CLOUD_BASE_URL,
        timeout=timeout,
    )


def get_zep_client(api_key: str | None = None, timeout: float | None = None) -> Zep:
    """Return a process-shared, explicitly configured Zep Cloud client."""

    # zep-cloud gives ZEP_API_URL precedence even when base_url is explicit.

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Resolve None/empty to an explicit default at the call site before calling: limit = int(raw) if raw else 10.
  2. Parse query/config limits with a typed parser (Pydantic field, argparse type=int) at the boundary.
  3. Pass an int literal when no configurability is needed.

Example fix

# before
results = search(query=q, limit=request.args.get("limit"))  # str or None

# after
raw = request.args.get("limit")
limit = int(raw) if raw else 10
results = search(query=q, limit=limit)
Defensive patterns

Strategy: validation

Validate before calling

def coerce_limit(raw: Any, default: int = 10) -> int:
    if raw is None or raw == "":
        return default
    return int(raw)  # surface bad input early at the boundary

Type guard

def is_int_like_limit(value: Any) -> TypeGuard[int]:
    if isinstance(value, bool):
        return False
    try:
        int(value)
        return True
    except (TypeError, ValueError):
        return False

Try / catch

try:
    limit = normalize_zep_search_limit(raw_limit)
except ValueError:
    limit = 10  # sane default for untrusted input

Prevention

When it happens

Trigger: Passing limit=None (there is no None-default handling in this function), limit="", or limit={"size": 10}. Forwarding unparsed HTTP query-string parameters (always str or None) directly as limit.

Common situations: A handler doing request.args.get("limit") and forwarding it; a caller passing None expecting a library default to apply; config values arriving as empty strings when the field is unset.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/2d0599f90ba871db. Report an issue: GitHub.