Skip to content

Commit ff56803

Browse files
Merge pull request #128 from patrickfleith/feature/continued-llms-refactoring-and-tests
Feature/continued llms refactoring and tests
2 parents f8fd5e1 + 66f669e commit ff56803

13 files changed

Lines changed: 1175 additions & 977 deletions

datafast/examples/mcq_example.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
def main():
1212
# 1. Define the configuration
1313
config = MCQDatasetConfig(
14-
# hf_dataset_name="patrickfleith/space_engineering_environment_effects_texts",
14+
hf_dataset_name="patrickfleith/space_engineering_environment_effects_texts",
1515
# local_file_path="datafast/examples/data/mcq/sample.csv",
1616
# local_file_path="datafast/examples/data/mcq/sample.txt",
17-
local_file_path="datafast/examples/data/mcq/sample.jsonl",
17+
#local_file_path="datafast/examples/data/mcq/sample.jsonl",
1818
text_column="text", # Column containing the text to generate questions from
19-
sample_count=3, # Process only 3 samples for testing
19+
sample_count=2, # Process only 3 samples for testing
2020
num_samples_per_prompt=2,# Generate 2 questions per document
2121
min_document_length=100, # Skip documents shorter than 100 chars
2222
max_document_length=20000,# Skip documents longer than 20000 chars
@@ -32,7 +32,7 @@ def main():
3232

3333
# 3. Generate the dataset
3434
dataset = MCQDataset(config)
35-
num_expected_rows = dataset.get_num_expected_rows(providers, source_data_num_rows=3)
35+
num_expected_rows = dataset.get_num_expected_rows(providers, source_data_num_rows=2)
3636
print(f"\nExpected number of rows: {num_expected_rows}")
3737
dataset.generate(providers)
3838

@@ -55,5 +55,5 @@ def main():
5555
if __name__ == "__main__":
5656
from dotenv import load_dotenv
5757

58-
load_dotenv("secrets.env")
58+
load_dotenv()
5959
main()

datafast/llms.py

Lines changed: 180 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import time
1111
import traceback
12+
import warnings
1213

1314
# Pydantic
1415
from pydantic import BaseModel
@@ -249,9 +250,11 @@ def generate(
249250
response: list[ModelResponse] = litellm.batch_completion(
250251
**completion_params)
251252

252-
# Record timestamp for rate limiting
253+
# Record timestamp for rate limiting (one timestamp per batch item)
253254
if self.rpm_limit is not None:
254-
self._request_timestamps.append(time.monotonic())
255+
current_time = time.monotonic()
256+
for _ in range(len(batch_to_send)):
257+
self._request_timestamps.append(current_time)
255258

256259
# Extract content from each response
257260
results = []
@@ -280,7 +283,15 @@ def generate(
280283

281284

282285
class OpenAIProvider(LLMProvider):
283-
"""OpenAI provider using litellm."""
286+
"""OpenAI provider using litellm.responses endpoint.
287+
288+
Note: This provider uses the new responses endpoint which has different
289+
parameter support compared to the standard completion endpoint:
290+
- temperature, top_p, and frequency_penalty are not supported
291+
- Uses text_format instead of response_format
292+
- Supports reasoning parameter for controlling reasoning effort
293+
- Does not support batch operations (will process sequentially with warning)
294+
"""
284295

285296
@property
286297
def provider_name(self) -> str:
@@ -294,29 +305,187 @@ def __init__(
294305
self,
295306
model_id: str = "gpt-5-mini-2025-08-07",
296307
api_key: str | None = None,
297-
temperature: float | None = None,
298308
max_completion_tokens: int | None = None,
309+
reasoning_effort: str = "low",
310+
temperature: float | None = None,
299311
top_p: float | None = None,
300312
frequency_penalty: float | None = None,
301313
):
302314
"""Initialize the OpenAI provider.
303315
304316
Args:
305-
model_id: The model ID (defaults to gpt-5-mini-2025-08-07)
317+
model_id: The model ID (defaults to gpt-5-mini)
306318
api_key: API key (if None, will get from environment)
307-
temperature: The sampling temperature to be used, between 0 and 2. Higher values like 0.8 produce more random outputs, while lower values like 0.2 make outputs more focused and deterministic
308319
max_completion_tokens: An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens.
309-
top_p: Nucleus sampling parameter (0.0 to 1.0)
310-
frequency_penalty: Penalty for token frequency (-2.0 to 2.0)
320+
reasoning_effort: Reasoning effort level - "low", "medium", or "high" (defaults to "low")
321+
temperature: DEPRECATED - Not supported by responses endpoint
322+
top_p: DEPRECATED - Not supported by responses endpoint
323+
frequency_penalty: DEPRECATED - Not supported by responses endpoint
311324
"""
325+
# Warn about deprecated parameters
326+
if temperature is not None:
327+
warnings.warn(
328+
"temperature parameter is not supported by OpenAI responses endpoint and will be ignored",
329+
UserWarning,
330+
stacklevel=2
331+
)
332+
if top_p is not None:
333+
warnings.warn(
334+
"top_p parameter is not supported by OpenAI responses endpoint and will be ignored",
335+
UserWarning,
336+
stacklevel=2
337+
)
338+
if frequency_penalty is not None:
339+
warnings.warn(
340+
"frequency_penalty parameter is not supported by OpenAI responses endpoint and will be ignored",
341+
UserWarning,
342+
stacklevel=2
343+
)
344+
345+
# Store reasoning effort
346+
self.reasoning_effort = reasoning_effort
347+
348+
# Call parent init with None for unsupported params
312349
super().__init__(
313350
model_id=model_id,
314351
api_key=api_key,
315-
temperature=temperature,
352+
temperature=None,
316353
max_completion_tokens=max_completion_tokens,
317-
top_p=top_p,
318-
frequency_penalty=frequency_penalty,
354+
top_p=None,
355+
frequency_penalty=None,
319356
)
357+
358+
def generate(
359+
self,
360+
prompt: str | list[str] | None = None,
361+
messages: list[Messages] | Messages | None = None,
362+
response_format: Type[T] | None = None,
363+
) -> str | list[str] | T | list[T]:
364+
"""
365+
Generate responses from the LLM using the responses endpoint.
366+
367+
Note: Batch operations are processed sequentially as the responses endpoint
368+
does not support native batching.
369+
370+
Args:
371+
prompt: Single text prompt (str) or list of text prompts for batch processing
372+
messages: Single message list or list of message lists for batch processing
373+
response_format: Optional Pydantic model class for structured output
374+
375+
Returns:
376+
Single string/model or list of strings/models depending on input type.
377+
378+
Raises:
379+
ValueError: If neither prompt nor messages is provided, or if both are provided.
380+
RuntimeError: If there's an error during generation.
381+
"""
382+
# Validate inputs
383+
if prompt is None and messages is None:
384+
raise ValueError("Either prompts or messages must be provided")
385+
if prompt is not None and messages is not None:
386+
raise ValueError("Provide either prompts or messages, not both")
387+
388+
# Determine if this is a single input or batch input
389+
single_input = False
390+
batch_prompts = None
391+
batch_messages = None
392+
393+
if prompt is not None:
394+
if isinstance(prompt, str):
395+
# Single prompt - convert to batch
396+
batch_prompts = [prompt]
397+
single_input = True
398+
elif isinstance(prompt, list):
399+
# Already a list of prompts
400+
batch_prompts = prompt
401+
single_input = False
402+
else:
403+
raise ValueError("prompt must be a string or list of strings")
404+
405+
if messages is not None:
406+
if isinstance(messages, list) and len(messages) > 0:
407+
# Check if it's a single message list or batch
408+
if isinstance(messages[0], dict):
409+
# Single message list - convert to batch
410+
batch_messages = [messages]
411+
single_input = True
412+
elif isinstance(messages[0], list):
413+
# Already a batch of message lists
414+
batch_messages = messages
415+
single_input = False
416+
else:
417+
raise ValueError("Invalid messages format")
418+
else:
419+
raise ValueError("messages cannot be empty")
420+
421+
try:
422+
# Convert batch prompts to messages if needed
423+
batch_to_send = []
424+
if batch_prompts is not None:
425+
for one_prompt in batch_prompts:
426+
batch_to_send.append([{"role": "user", "content": one_prompt}])
427+
else:
428+
batch_to_send = batch_messages
429+
430+
# Warn if batch processing is being used
431+
if len(batch_to_send) > 1:
432+
warnings.warn(
433+
f"OpenAI responses endpoint does not support batch operations. "
434+
f"Processing {len(batch_to_send)} requests sequentially.",
435+
UserWarning,
436+
stacklevel=2
437+
)
438+
439+
# Process each request sequentially
440+
results = []
441+
for message_list in batch_to_send:
442+
# Enforce rate limit per request
443+
self._respect_rate_limit()
444+
445+
# Prepare completion parameters
446+
completion_params = {
447+
"model": self._get_model_string(),
448+
"input": message_list,
449+
"reasoning": {"effort": self.reasoning_effort},
450+
}
451+
452+
# Add max_output_tokens if specified
453+
if self.max_completion_tokens is not None:
454+
completion_params["max_output_tokens"] = self.max_completion_tokens
455+
456+
# Add text_format if response_format is provided
457+
if response_format is not None:
458+
completion_params["text_format"] = response_format
459+
460+
# Call LiteLLM responses endpoint
461+
response = litellm.responses(**completion_params)
462+
463+
# Record timestamp for rate limiting
464+
if self.rpm_limit is not None:
465+
self._request_timestamps.append(time.monotonic())
466+
467+
# Extract content from response
468+
# Response structure: response.output[1].content[0].text
469+
content = response.output[1].content[0].text
470+
471+
if response_format is not None:
472+
# Strip code fences before validation
473+
content = self._strip_code_fences(content)
474+
results.append(response_format.model_validate_json(content))
475+
else:
476+
# Strip leading/trailing whitespace for text responses
477+
results.append(content.strip() if content else content)
478+
479+
# Return single result for backward compatibility
480+
if single_input and len(results) == 1:
481+
return results[0]
482+
return results
483+
484+
except Exception as e:
485+
error_trace = traceback.format_exc()
486+
raise RuntimeError(
487+
f"Error generating response with {self.provider_name}:\n{error_trace}"
488+
)
320489

321490

322491
class AnthropicProvider(LLMProvider):

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ config = ClassificationDatasetConfig(
9595
providers = [
9696
OpenAIProvider(model_id="gpt-5-mini-2025-08-07"),
9797
AnthropicProvider(model_id="claude-haiku-4-5-20251001"),
98-
GeminiProvider(model_id="gemini-2.5-flash"),
98+
GeminiProvider(model_id="gemini-2.0-flash"),
9999
OpenRouterProvider(model_id="z-ai/glm-4.6")
100100
]
101101
```

docs/llms.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ gemini_llm = GeminiProvider()
3333
# Ollama (default: gemma3:4b)
3434
ollama_llm = OllamaProvider()
3535

36-
# OpenRouter (default: openai/gpt-4.1-mini)
36+
# OpenRouter (default: openai/gpt-5-mini)
3737
openrouter_llm = OpenRouterProvider()
3838
```
3939

@@ -42,12 +42,38 @@ openrouter_llm = OpenRouterProvider()
4242
```python
4343
openai_llm = OpenAIProvider(
4444
model_id="gpt-5-mini-2025-08-07", # Custom model
45-
temperature=0.2, # Lower temperature for more deterministic outputs
46-
max_completion_tokens=100, # Limit token generation
47-
top_p=0.9, # Nucleus sampling parameter
48-
frequency_penalty=0.1 # Penalty for frequent tokens
45+
max_completion_tokens=1000, # Limit token generation (don't set this too low for reasoning models)
46+
reasoning_effort="medium" # Reasoning effort: "low", "medium", or "high"
4947
)
48+
```
49+
50+
!!! warning "OpenAI Provider Changes"
51+
`OpenAIProvider` now uses the `responses` endpoint. The following parameters are **deprecated** and will trigger warnings:
52+
- `temperature`
53+
- `top_p`
54+
- `frequency_penalty`
55+
56+
Use `reasoning_effort` ("low", "medium", "high") instead to control generation behavior.
5057

58+
```python
59+
# Anthropic with custom parameters
60+
anthropic_llm = AnthropicProvider(
61+
model_id="claude-haiku-4-5-20251001",
62+
temperature=0.7,
63+
max_completion_tokens=1000
64+
)
65+
```
66+
67+
!!! warning "Anthropic Provider Limitations"
68+
`AnthropicProvider` only supports the following parameters:
69+
- `temperature` (0.0 to 1.0)
70+
- `max_completion_tokens`
71+
72+
The following parameters are **not supported** by Anthropic Claude 4.5 models:
73+
- `top_p`
74+
- `frequency_penalty`
75+
76+
```python
5177
# Ollama with custom API endpoint
5278
ollama_llm = OllamaProvider(
5379
model_id="llama3.2:latest",

0 commit comments

Comments
 (0)