Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions laiser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,6 @@
MAX_NEW_TOKENS = 1000
GENERATION_SEED = 42

# Deterministic decoding defaults.
#
# LAiSER defaults every LLM backend to greedy decoding so that repeated runs over
# the same input yield the same generation. DEFAULT_TEMPERATURE = 0.0 selects the
# highest-probability token at each step; DEFAULT_TOP_P = 1.0 leaves nucleus
# filtering inactive so that temperature alone governs the decoding behaviour.
# GENERATION_SEED is supplied to every backend that accepts a sampling seed, so
# that runs remain reproducible even when a caller deliberately raises the
# temperature above zero. Callers may override both via SkillExtractorRefactored.
DEFAULT_TEMPERATURE = float(os.getenv("LAISER_TEMPERATURE", "0.0"))
DEFAULT_TOP_P = float(os.getenv("LAISER_TOP_P", "1.0"))

# SCQF Level Descriptors
SCQF_LEVELS: Dict[int, str] = {
1: "Basic awareness of simple concepts.",
Expand Down
14 changes: 4 additions & 10 deletions laiser/llm_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
VLLM_AVAILABLE = False
SamplingParams = None

from laiser.config import DEFAULT_TEMPERATURE, DEFAULT_TOP_P, GENERATION_SEED
from laiser.utils import get_top_esco_skills

# Import llm_router with error handling
Expand Down Expand Up @@ -176,7 +175,7 @@ def get_completion_batch(queries: list, model, tokenizer, batch_size=2) -> list:
generated_ids = model.generate(
**model_inputs,
max_new_tokens=1000,
do_sample=DEFAULT_TEMPERATURE > 0.0,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)

Expand Down Expand Up @@ -260,14 +259,14 @@ def get_completion(input_text, text_columns, input_type, model, tokenizer) -> st
generated_ids = model.generate(
**model_inputs,
max_new_tokens=1000,
do_sample=DEFAULT_TEMPERATURE > 0.0,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)

generated_ids = model.generate(
**model_inputs,
max_new_tokens=1000,
do_sample=DEFAULT_TEMPERATURE > 0.0,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
decoded = tokenizer.decode(generated_ids[0], skip_special_tokens=False)
Expand Down Expand Up @@ -477,12 +476,7 @@ def vllm_generate(

result = []

sampling_params = SamplingParams(
max_tokens=1000,
temperature=DEFAULT_TEMPERATURE,
top_p=DEFAULT_TOP_P,
seed=GENERATION_SEED,
)
sampling_params = SamplingParams(max_tokens=1000, seed=42)

for i in range(0, len(queries), batch_size):
prompts = [
Expand Down
15 changes: 0 additions & 15 deletions laiser/llm_models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

import requests

from laiser.config import DEFAULT_TEMPERATURE, DEFAULT_TOP_P

_CODE_FENCE_RE = re.compile(r"```(?:json)?|```", re.IGNORECASE)


Expand All @@ -23,17 +21,7 @@ def anthropic_generate(
model: str = "claude-haiku-4-5-20251001",
timeout: int = 60,
max_tokens: int = 4096,
temperature: float = DEFAULT_TEMPERATURE,
top_p: float = DEFAULT_TOP_P,
) -> str:
"""Generate text with the Anthropic Messages API.

Decoding is deterministic by default: ``temperature`` defaults to
``config.DEFAULT_TEMPERATURE`` (0.0, i.e. greedy decoding). ``top_p`` is
only transmitted when it departs from 1.0, because the Messages API
discourages setting both ``temperature`` and ``top_p`` in one request.
The API does not accept a sampling seed.
"""
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
if not api_key:
raise ValueError("Anthropic API key not provided")
Expand All @@ -47,11 +35,8 @@ def anthropic_generate(
payload = {
"model": model,
"max_tokens": max_tokens,
"temperature": temperature,
"messages": [{"role": "user", "content": prompt}],
}
if top_p is not None and top_p != 1.0:
payload["top_p"] = top_p

try:
resp = requests.post(url, headers=headers, json=payload, timeout=timeout)
Expand Down
34 changes: 3 additions & 31 deletions laiser/llm_models/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@
from google.genai import errors as genai_errors
from google.genai import types

from laiser.config import DEFAULT_TEMPERATURE, GENERATION_SEED

DEFAULT_GEMINI_MODEL = os.getenv("LAISER_GEMINI_MODEL", "gemini-2.5-flash")
DEFAULT_GEMINI_TIMEOUT = float(os.getenv("LAISER_GEMINI_TIMEOUT", "60"))
DEFAULT_MAX_OUTPUT_TOKENS = int(os.getenv("LAISER_GEMINI_MAX_OUTPUT_TOKENS", "1000"))


# Older google-genai releases do not expose a sampling seed on
# GenerateContentConfig. Detect support once so that requesting a seed never
# breaks generation on those versions.
_SUPPORTS_SEED = "seed" in getattr(types.GenerateContentConfig, "model_fields", {})


class GeminiAPI:
"""Small wrapper to keep backward compatibility with older imports."""

Expand All @@ -31,17 +23,13 @@ def __init__(
model_name: Optional[str] = None,
timeout: float = DEFAULT_GEMINI_TIMEOUT,
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS,
temperature: float = DEFAULT_TEMPERATURE,
seed: Optional[int] = GENERATION_SEED,
):
if not api_key:
raise ValueError("Gemini API key is required")
self.api_key = api_key
self.model_name = model_name or DEFAULT_GEMINI_MODEL
self.timeout = timeout
self.max_output_tokens = max_output_tokens
self.temperature = temperature
self.seed = seed
self.client = genai.Client(api_key=api_key)

def generate(self, prompt: str) -> str:
Expand All @@ -55,9 +43,7 @@ def generate_with_config(
response_schema: Optional[Any] = None,
) -> str:
try:
config_kwargs = {"temperature": self.temperature}
if self.seed is not None and _SUPPORTS_SEED:
config_kwargs["seed"] = self.seed
config_kwargs = {"temperature": 0.0, "max_output_tokens": self.max_output_tokens}
if response_mime_type:
config_kwargs["response_mime_type"] = response_mime_type
if response_schema is not None:
Expand Down Expand Up @@ -90,23 +76,9 @@ def gemini_generate(
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS,
response_mime_type: Optional[str] = None,
response_schema: Optional[Any] = None,
temperature: float = DEFAULT_TEMPERATURE,
seed: Optional[int] = GENERATION_SEED,
) -> str:
"""Send `prompt` to Gemini and return generated text.

Decoding is deterministic by default: ``temperature`` defaults to
``config.DEFAULT_TEMPERATURE`` (0.0, i.e. greedy decoding) and
``config.GENERATION_SEED`` is supplied wherever the installed google-genai
release accepts a sampling seed.
"""
client = GeminiAPI(
api_key=api_key,
model_name=model_name,
timeout=timeout,
temperature=temperature,
seed=seed,
)
"""Send `prompt` to Gemini and return generated text."""
client = GeminiAPI(api_key=api_key, model_name=model_name, timeout=timeout)
return client.generate_with_config(
prompt,
response_mime_type=response_mime_type,
Expand Down
65 changes: 9 additions & 56 deletions laiser/llm_models/hugging_face_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@
[1.0.0] 6/30/2025 Anket Patil Modularize LLM generation logic for transformers and vLLM
"""

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

from laiser.config import DEFAULT_TEMPERATURE, DEFAULT_TOP_P, GENERATION_SEED

try:
from vllm import SamplingParams

Expand All @@ -63,24 +60,7 @@
SamplingParams = None # Optional fallback


def llm_generate(
prompt: str,
tokenizer,
model,
model_id: str,
use_gpu: bool,
temperature: float = DEFAULT_TEMPERATURE,
seed: int = GENERATION_SEED,
):
"""Generate text with a local HuggingFace Transformers model.

Decoding is deterministic by default. A temperature of 0.0 disables
sampling entirely (``do_sample=False``, i.e. greedy decoding), which also
prevents a checkpoint's bundled ``generation_config.json`` from silently
re-enabling sampling. When a caller raises the temperature above zero,
``seed`` is applied through ``torch.manual_seed`` so the run stays
reproducible.
"""
def llm_generate(prompt: str, tokenizer, model, model_id: str, use_gpu: bool):
if tokenizer is None or model is None:
quantization_config = BitsAndBytesConfig(load_in_8bit=True)

Expand All @@ -91,50 +71,23 @@ def llm_generate(

inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

do_sample = temperature is not None and temperature > 0.0
if seed is not None:
torch.manual_seed(seed)

generation_kwargs = {
"max_new_tokens": 100,
"pad_token_id": tokenizer.pad_token_id,
"eos_token_id": tokenizer.eos_token_id,
"do_sample": do_sample,
}
if do_sample:
generation_kwargs["temperature"] = temperature
generation_kwargs["top_p"] = DEFAULT_TOP_P

outputs = model.generate(**inputs, **generation_kwargs)
outputs = model.generate(
**inputs,
max_new_tokens=100,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)

return tokenizer.decode(outputs[0], skip_special_tokens=True)


def llm_generate_vllm(
prompt,
llm,
temperature: float = DEFAULT_TEMPERATURE,
seed: int = GENERATION_SEED,
max_tokens: int = 200,
):
"""Generate text with a local vLLM engine.

Decoding is deterministic by default: ``temperature`` defaults to
``config.DEFAULT_TEMPERATURE`` (0.0, i.e. greedy decoding) rather than to
vLLM's own default of 1.0, and ``config.GENERATION_SEED`` is passed so that
runs remain reproducible if a caller raises the temperature.
"""
def llm_generate_vllm(prompt, llm):
if not VLLM_AVAILABLE:
raise ImportError(
"vLLM is not installed. Please install it to use this function."
)

sampling_params = SamplingParams(
max_tokens=max_tokens,
temperature=temperature,
top_p=DEFAULT_TOP_P,
seed=seed,
)
sampling_params = SamplingParams(max_tokens=200, seed=42)
result = llm.generate([prompt], sampling_params=sampling_params)
raw_text = result[0].outputs[0].text.strip()
return raw_text
30 changes: 2 additions & 28 deletions laiser/llm_models/llama_cpp_handler.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,15 @@
import gc
import inspect
import os
import re
from pathlib import Path
from typing import Any, List, Optional

from laiser.config import DEFAULT_TEMPERATURE, DEFAULT_TOP_P, GENERATION_SEED

try:
from llama_cpp import Llama # type: ignore
except ImportError: # pragma: no cover
Llama = None


def _llama_accepts_seed(llama: Any) -> bool:
"""Report whether this llama-cpp-python build accepts a per-call seed.

The ``seed`` argument to ``create_chat_completion`` was added in later
releases; probing the signature keeps deterministic seeding from raising a
TypeError on older installations.
"""
try:
return "seed" in inspect.signature(llama.create_chat_completion).parameters
except (TypeError, ValueError):
return False


def _strip_fences(text: str) -> str:
if not text:
return ""
Expand All @@ -43,7 +27,7 @@ def __init__(
n_ctx: int = 4096,
n_threads: Optional[int] = None,
n_gpu_layers: int = -1,
temperature: float = DEFAULT_TEMPERATURE,
temperature: float = 0.2,
chat_format: str = "chatml",
):
if Llama is None:
Expand Down Expand Up @@ -121,16 +105,9 @@ def llama_cpp_chat(
system: str = "You are a helpful assistant that outputs in JSON.",
max_tokens: Optional[int] = None,
stop: Optional[List[str]] = None,
temperature: Optional[float] = DEFAULT_TEMPERATURE,
seed: Optional[int] = GENERATION_SEED,
temperature: Optional[float] = None,
) -> str:
"""Generate a chat completion with a local llama.cpp model.

Decoding is deterministic by default: ``temperature`` defaults to
``config.DEFAULT_TEMPERATURE`` (0.0, i.e. greedy decoding) and
``config.GENERATION_SEED`` is supplied so that runs remain reproducible if
a caller raises the temperature.
"""
if llama is None:
raise ValueError(
"llama is None; expected an initialized llama_cpp.Llama instance."
Expand All @@ -148,9 +125,6 @@ def llama_cpp_chat(
kwargs["stop"] = stop
if temperature is not None:
kwargs["temperature"] = temperature
kwargs["top_p"] = DEFAULT_TOP_P
if seed is not None and _llama_accepts_seed(llama):
kwargs["seed"] = seed

resp = llama.create_chat_completion(**kwargs)
return _strip_fences(resp["choices"][0]["message"]["content"])
Loading
Loading