Skip to content

lasagnastack.llm.openrouter

lasagnastack.llm.openrouter

OpenRouterClient

OpenRouterClient(
    api_key: str | None = None,
    model: str | None = None,
    reasoning_max_tokens: int = 4000,
    reasoning_effort: str | None = None,
    total_max_tokens: int | None = None,
)

Bases: LLMClient

LLM client backed by OpenRouter (OpenAI-compatible API).

Supports text-only generation. Video input is not available on OpenRouter; use GeminiClient for Stage 2 (analyse) when video upload is required.

Initialise the OpenRouter client.

Parameters:

Name Type Description Default
api_key str | None

OpenRouter API key. Falls back to the LSNSTK_LLM_OPENROUTER_API_KEY environment variable.

None
model str | None

Model name to use ("openrouter/<provider>/<model>" or bare "<provider>/<model>"). Falls back to the LSNSTK_LLM_MODEL environment variable, then "openrouter/meta-llama/llama-3.1-8b-instruct".

None
reasoning_max_tokens int

Token budget forwarded as reasoning.max_tokens when no effort level is set. Set to 0 to disable reasoning entirely.

4000
reasoning_effort str | None

Qualitative effort level ("none" / "minimal" / "low" / "medium" / "high" / "xhigh"). When set, takes priority over reasoning_max_tokens and maps to reasoning.effort in the request.

None
total_max_tokens int | None

Passed as max_tokens in the API request — the ceiling on all output tokens combined (reasoning + visible response). Required for Anthropic models where max_tokens must be strictly greater than the reasoning budget.

None
Source code in src/lasagnastack/llm/openrouter.py
def __init__(
    self,
    api_key: str | None = None,
    model: str | None = None,
    reasoning_max_tokens: int = 4000,
    reasoning_effort: str | None = None,
    total_max_tokens: int | None = None,
) -> None:
    """Initialise the OpenRouter client.

    Args:
        api_key: OpenRouter API key. Falls back to the
            ``LSNSTK_LLM_OPENROUTER_API_KEY`` environment variable.
        model: Model name to use (``"openrouter/<provider>/<model>"``
            or bare ``"<provider>/<model>"``). Falls back to the
            ``LSNSTK_LLM_MODEL`` environment variable, then
            ``"openrouter/meta-llama/llama-3.1-8b-instruct"``.
        reasoning_max_tokens: Token budget forwarded as
            ``reasoning.max_tokens`` when no effort level is set. Set
            to ``0`` to disable reasoning entirely.
        reasoning_effort: Qualitative effort level (``"none"`` /
            ``"minimal"`` / ``"low"`` / ``"medium"`` / ``"high"`` /
            ``"xhigh"``). When set, takes priority over
            ``reasoning_max_tokens`` and maps to ``reasoning.effort``
            in the request.
        total_max_tokens: Passed as ``max_tokens`` in the API request —
            the ceiling on all output tokens combined (reasoning + visible
            response). Required for Anthropic models where ``max_tokens``
            must be strictly greater than the reasoning budget.
    """
    super().__init__(
        reasoning_max_tokens=reasoning_max_tokens,
        reasoning_effort=reasoning_effort,
        total_max_tokens=total_max_tokens,
    )
    raw_model = model or os.getenv(
        "LSNSTK_LLM_MODEL", "openrouter/meta-llama/llama-3.1-8b-instruct"
    )
    self._model = raw_model.removeprefix("openrouter/")
    resolved_key = api_key or os.getenv("LSNSTK_LLM_OPENROUTER_API_KEY")
    if not resolved_key:
        raise ValueError(
            "OpenRouter API key not found. "
            "Set LSNSTK_LLM_OPENROUTER_API_KEY in .env or pass api_key=."
        )
    self._client = OpenAI(
        api_key=resolved_key,
        base_url=_OPENROUTER_BASE_URL,
        timeout=_REQUEST_TIMEOUT_SEC,
        max_retries=0,
    )

    self._total_input_tokens: int = 0
    self._total_output_tokens: int = 0
    self._total_thinking_tokens: int = 0
    self._total_cost_usd: float = 0.0
    self._call_count: int = 0
    self._stats_lock = threading.Lock()

session_stats property

session_stats: dict[str, float | int]

Aggregated token and cost totals across all calls on this instance.

Returns:

Type Description
dict[str, float | int]

A dict with keys total_input_tokens, total_output_tokens,

dict[str, float | int]

total_thinking_tokens, total_cost_usd, and

dict[str, float | int]

llm_call_count. Cost is read directly from the usage.cost

dict[str, float | int]

field returned by OpenRouter — no static pricing table is needed.

generate

generate(
    prompt: str,
    response_schema: type[BaseModel],
    *,
    temperature: float = 0.4,
) -> BaseModel

Send a text-only prompt; return a validated response_schema instance.

Parameters:

Name Type Description Default
prompt str

The plain-text prompt to send.

required
response_schema type[BaseModel]

Pydantic model class that defines the expected JSON structure of the response.

required
temperature float

Sampling temperature passed to the model.

0.4

Returns:

Type Description
BaseModel

A validated instance of response_schema.

Source code in src/lasagnastack/llm/openrouter.py
def generate(
    self,
    prompt: str,
    response_schema: type[BaseModel],
    *,
    temperature: float = 0.4,
) -> BaseModel:
    """Send a text-only prompt; return a validated response_schema instance.

    Args:
        prompt: The plain-text prompt to send.
        response_schema: Pydantic model class that defines the expected
            JSON structure of the response.
        temperature: Sampling temperature passed to the model.

    Returns:
        A validated instance of ``response_schema``.
    """
    return self._generate_contents(prompt, response_schema, temperature)

generate_with_video

generate_with_video(
    video_path: Path,
    prompt: str,
    response_schema: type[BaseModel],
    *,
    temperature: float = 0.4,
) -> BaseModel

Not supported — OpenRouter has no video upload API.

Parameters:

Name Type Description Default
video_path Path

Unused.

required
prompt str

Unused.

required
response_schema type[BaseModel]

Unused.

required
temperature float

Unused.

0.4

Raises:

Type Description
NotImplementedError

Always. Use GeminiClient for Stage 2.

Source code in src/lasagnastack/llm/openrouter.py
def generate_with_video(
    self,
    video_path: Path,
    prompt: str,
    response_schema: type[BaseModel],
    *,
    temperature: float = 0.4,
) -> BaseModel:
    """Not supported — OpenRouter has no video upload API.

    Args:
        video_path: Unused.
        prompt: Unused.
        response_schema: Unused.
        temperature: Unused.

    Raises:
        NotImplementedError: Always. Use ``GeminiClient`` for Stage 2.
    """
    raise NotImplementedError(
        "OpenRouterClient does not support video input. "
        "Use GeminiClient for Stage 2 (analyse)."
    )