Skip to content

Commit e02ed26

Browse files
committed
Merge branch 'main' into feature/add-minimax-provider
2 parents 688e606 + eaf171a commit e02ed26

26 files changed

Lines changed: 1384 additions & 55 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Security Scan (Frame SAST)
2+
3+
# Runs the Frame neuro-symbolic SAST tool (https://github.com/lambdasec/frame)
4+
# on the Python files changed by a pull request. Scanning only the PR's changed
5+
# files surfaces issues introduced by the change without failing on pre-existing
6+
# findings elsewhere in the tree. The job fails only on high/critical severity.
7+
8+
on:
9+
pull_request:
10+
branches: [ main ]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
frame-scan:
17+
name: Frame SAST (changed files)
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout (full history for diff)
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: '3.12'
29+
30+
- name: Install Frame (pinned)
31+
run: |
32+
git clone https://github.com/lambdasec/frame.git /tmp/frame
33+
git -C /tmp/frame checkout 3223ac44320b4870782d9aad03514b4d3c876e0a
34+
pip install "/tmp/frame[scan]"
35+
36+
- name: Scan Python files changed in this PR
37+
env:
38+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
39+
run: |
40+
set -uo pipefail
41+
42+
# Added/copied/modified/renamed Python files in this PR (skip deletions).
43+
mapfile -t FILES < <(git diff --name-only --diff-filter=ACMR "$BASE_SHA" HEAD -- '*.py')
44+
45+
if [ "${#FILES[@]}" -eq 0 ]; then
46+
echo "No Python files changed in this PR - nothing to scan."
47+
exit 0
48+
fi
49+
50+
echo "Scanning ${#FILES[@]} changed Python file(s) (fail on high/critical):"
51+
printf ' %s\n' "${FILES[@]}"
52+
53+
FAIL=0
54+
for f in "${FILES[@]}"; do
55+
# File may have been renamed away or removed in a later commit.
56+
[ -f "$f" ] || continue
57+
echo "::group::Frame scan $f"
58+
if ! frame scan "$f" --fail-on high; then
59+
FAIL=1
60+
echo "::error file=$f::Frame flagged a high/critical severity issue in $f"
61+
fi
62+
echo "::endgroup::"
63+
done
64+
65+
if [ "$FAIL" -ne 0 ]; then
66+
echo "Frame SAST found high/critical severity issue(s) in changed files."
67+
exit 1
68+
fi
69+
echo "No high/critical severity issues in changed files."

.github/workflows/test.yml

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ jobs:
3636
pip install -r tests/requirements.txt
3737
pip install -e .
3838
39+
- name: Cache HuggingFace models
40+
uses: actions/cache@v3
41+
with:
42+
path: ~/.cache/huggingface
43+
key: ${{ runner.os }}-hf-codelion-dhara-250m
44+
restore-keys: |
45+
${{ runner.os }}-hf-
46+
3947
- name: Run unit tests (no server required)
4048
run: |
4149
# Set up local inference environment
@@ -81,20 +89,31 @@ jobs:
8189
pip install -r tests/requirements.txt
8290
pip install -e .
8391
92+
- name: Cache HuggingFace models
93+
uses: actions/cache@v3
94+
with:
95+
path: ~/.cache/huggingface
96+
key: ${{ runner.os }}-hf-codelion-dhara-250m
97+
restore-keys: |
98+
${{ runner.os }}-hf-
99+
84100
- name: Start optillm server
85101
run: |
86102
echo "Starting optillm server for integration tests..."
87-
OPTILLM_API_KEY=optillm python optillm.py --model google/gemma-3-270m-it --port 8000 &
103+
OPTILLM_API_KEY=optillm python optillm.py --model codelion/dhara-250m --port 8000 &
88104
echo $! > server.pid
89105
90106
# Wait for server to be ready
91107
echo "Waiting for server to start..."
92108
sleep 15
93-
109+
94110
# Test server health
95111
curl -s http://localhost:8000/health || echo "Server health check failed"
96112
env:
97113
OPTILLM_API_KEY: optillm
114+
# Bound generation for the small test model, which does not reliably emit
115+
# an EOS token and would otherwise ramble up to the 4096 default per call.
116+
OPTILLM_MAX_TOKENS: "128"
98117
HF_TOKEN: ${{ secrets.HF_TOKEN }}
99118

100119
- name: Run integration tests (server required)
@@ -174,12 +193,20 @@ jobs:
174193
pip install -r tests/requirements.txt
175194
pip install -e .
176195
196+
- name: Cache HuggingFace models
197+
uses: actions/cache@v3
198+
with:
199+
path: ~/.cache/huggingface
200+
key: ${{ runner.os }}-hf-codelion-dhara-250m
201+
restore-keys: |
202+
${{ runner.os }}-hf-
203+
177204
- name: Start optillm server with conversation logging
178205
run: |
179206
echo "Starting optillm server with conversation logging..."
180207
mkdir -p /tmp/optillm_conversations
181208
OPTILLM_API_KEY=optillm python optillm.py \
182-
--model google/gemma-3-270m-it \
209+
--model codelion/dhara-250m \
183210
--port 8000 \
184211
--log-conversations \
185212
--conversation-log-dir /tmp/optillm_conversations &
@@ -193,6 +220,9 @@ jobs:
193220
curl -s http://localhost:8000/health || echo "Server health check failed"
194221
env:
195222
OPTILLM_API_KEY: optillm
223+
# Bound generation for the small test model, which does not reliably emit
224+
# an EOS token and would otherwise ramble up to the 4096 default per call.
225+
OPTILLM_MAX_TOKENS: "128"
196226
HF_TOKEN: ${{ secrets.HF_TOKEN }}
197227

198228
- name: Run conversation logging tests

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
</p>
1010

1111
<p align="center">
12-
<a href="https://github.com/algorithmicsuperintelligence/optillm/stargazers"><img src="https://img.shields.io/github/stars/algorithmicsuperintelligence/optillm?style=social" alt="GitHub stars"></a>
1312
<a href="https://pypi.org/project/optillm/"><img src="https://img.shields.io/pypi/v/optillm" alt="PyPI version"></a>
14-
<a href="https://pypi.org/project/optillm/"><img src="https://img.shields.io/pypi/dm/optillm" alt="PyPI downloads"></a>
13+
<a href="https://pepy.tech/projects/optillm"><img src="https://static.pepy.tech/personalized-badge/optillm?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=GREEN&left_text=downloads/month" alt="PyPI Downloads"></a>
1514
<a href="https://github.com/algorithmicsuperintelligence/optillm/blob/main/LICENSE"><img src="https://img.shields.io/github/license/algorithmicsuperintelligence/optillm" alt="License"></a>
1615
</p>
1716

@@ -190,7 +189,7 @@ optillm
190189
| MCP Client | `mcp` | Implements the model context protocol (MCP) client, enabling you to use any LLM with any MCP Server |
191190
| Router | `router` | Uses the [optillm-modernbert-large](https://huggingface.co/codelion/optillm-modernbert-large) model to route requests to different approaches based on the user prompt |
192191
| Chain-of-Code | `coc` | Implements a chain of code approach that combines CoT with code execution and LLM based code simulation |
193-
| Memory | `memory` | Implements a short term memory layer, enables you to use unbounded context length with any LLM |
192+
| Memory | `memory` | Implements a short term memory layer, enables you to use unbounded context length with any LLM. Set `OPTILLM_MEMORY_FILE` to opt in to file-backed persistence so memories survive across requests |
194193
| Privacy | `privacy` | Anonymize PII data in request and deanonymize it back to original value in response |
195194
| Read URLs | `readurls` | Reads all URLs found in the request, fetches the content at the URL and adds it to the context |
196195
| Execute Code | `executecode` | Enables use of code interpreter to execute python code in requests and LLM generated responses |

optillm/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Version information
2-
__version__ = "0.3.14"
2+
__version__ = "0.3.22"
3+
4+
import os as _os
5+
6+
# An empty Hugging Face token is not the same as "no token". huggingface_hub 1.x
7+
# (pulled in by transformers 5) sends an empty token as the literal header
8+
# "Authorization: Bearer ", which the HTTP stack rejects with
9+
# "Illegal header value b'Bearer '". This happens whenever HF_TOKEN is present but
10+
# blank, e.g. a forked-PR CI run where ${{ secrets.HF_TOKEN }} resolves to "".
11+
# Treat any blank HF token env var as unset so anonymous access is used instead.
12+
for _hf_token_var in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_HUB_TOKEN", "HF_HUB_TOKEN"):
13+
if _hf_token_var in _os.environ and not _os.environ[_hf_token_var].strip():
14+
del _os.environ[_hf_token_var]
315

416
# Import from server module
517
from .server import (

optillm/autothink/processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def process(self, messages: List[Dict[str, str]]) -> str:
243243
thinking_messages,
244244
continue_final_message=True,
245245
return_tensors="pt"
246-
).to(self.model.device)
246+
).input_ids.to(self.model.device)
247247

248248
# Reset and update token history in steering hooks
249249
if self.steering_hooks:

optillm/deepconf/processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def generate_single_trace(self, messages: List[Dict[str, str]],
108108
messages,
109109
return_tensors="pt",
110110
add_generation_prompt=True
111-
).to(self.model.device)
111+
).input_ids.to(self.model.device)
112112

113113
# Initialize generation state
114114
kv_cache = DynamicCache()

optillm/inference.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import numpy as np
55
from typing import Dict, List, Optional, Tuple, Any, Union
6-
from dataclasses import dataclass
6+
from dataclasses import dataclass, field
77
from collections import OrderedDict, defaultdict
88
import torch.nn.functional as F
99
import torch.nn as nn
@@ -89,6 +89,27 @@ def count_reasoning_tokens(text: str, tokenizer=None) -> int:
8989
MLX_AVAILABLE = False
9090
logger.debug("MLX framework not available - falling back to PyTorch")
9191

92+
93+
# Hard ceiling of 4096 by default. Can be lowered via OPTILLM_MAX_TOKENS so a
94+
# single local generation is bounded even when the request (or an approach's
95+
# internal calls) sends no max_tokens -- important for small local models that
96+
# do not reliably emit an EOS token (e.g. the dhara test model), which would
97+
# otherwise ramble up to the full default on every call.
98+
DEFAULT_MAX_NEW_TOKENS = 4096
99+
100+
101+
def _default_max_new_tokens() -> int:
102+
"""Default ``max_new_tokens`` for local generation (env-overridable)."""
103+
raw = os.environ.get("OPTILLM_MAX_TOKENS")
104+
if raw is None:
105+
return DEFAULT_MAX_NEW_TOKENS
106+
try:
107+
return max(1, int(raw))
108+
except (TypeError, ValueError):
109+
logger.warning("Ignoring invalid OPTILLM_MAX_TOKENS=%r; using %d", raw, DEFAULT_MAX_NEW_TOKENS)
110+
return DEFAULT_MAX_NEW_TOKENS
111+
112+
92113
@dataclass
93114
class ModelConfig:
94115
base_model_id: str
@@ -98,7 +119,7 @@ class ModelConfig:
98119
quantization_bits: int = 4
99120
device_preference: Optional[str] = None
100121
# Default generation parameters
101-
max_new_tokens: int = 4096
122+
max_new_tokens: int = field(default_factory=_default_max_new_tokens)
102123
do_sample: bool = True
103124
top_p: float = 0.9
104125
top_k: int = 50
@@ -292,7 +313,7 @@ def suggest_mlx_alternative(model_id: str) -> str:
292313
class MLXModelConfig:
293314
"""Configuration for MLX models"""
294315
model_id: str
295-
max_new_tokens: int = 4096
316+
max_new_tokens: int = field(default_factory=_default_max_new_tokens)
296317
temperature: float = 0.7
297318
top_p: float = 0.9
298319
repetition_penalty: float = 1.0
@@ -1268,16 +1289,46 @@ def setup_tokenizer(self, tokenizer: AutoTokenizer) -> AutoTokenizer:
12681289

12691290
return tokenizer
12701291

1292+
def _resolve_eos_token_ids(self):
1293+
"""Resolve the effective end-of-sequence token id(s) for generation.
1294+
1295+
Prefer the model's own ``generation_config.eos_token_id``. Chat models
1296+
commonly set it to the chat-turn end token (e.g. ``<|im_end|>``), which
1297+
can differ from the tokenizer's ``eos_token_id`` (often the base-model
1298+
``<|end_of_text|>``). Passing only the tokenizer eos to ``generate`` there
1299+
means the model never stops on its real turn-end token and rambles up to
1300+
``max_new_tokens`` -- e.g. dhara-250m's ChatML ends at ``<|im_end|>`` but
1301+
its tokenizer eos is ``<|end_of_text|>``.
1302+
1303+
The tokenizer eos is merged in as a fallback so a model that only emits
1304+
the base eos still terminates. Returns an int, a list of ints, or None.
1305+
"""
1306+
ids: List[int] = []
1307+
gen_cfg = getattr(self.current_model, "generation_config", None)
1308+
gc_eos = getattr(gen_cfg, "eos_token_id", None) if gen_cfg is not None else None
1309+
if isinstance(gc_eos, int):
1310+
ids.append(gc_eos)
1311+
elif isinstance(gc_eos, (list, tuple)):
1312+
ids.extend(int(x) for x in gc_eos if isinstance(x, int))
1313+
tok_eos = self.tokenizer.eos_token_id
1314+
if isinstance(tok_eos, int):
1315+
ids.append(tok_eos)
1316+
seen = set()
1317+
resolved = [x for x in ids if not (x in seen or seen.add(x))]
1318+
if not resolved:
1319+
return None
1320+
return resolved[0] if len(resolved) == 1 else resolved
1321+
12711322
def get_optimized_generation_config(self, generation_params: Optional[Dict[str, Any]] = None) -> Dict:
12721323
"""Get optimized generation config"""
12731324
config = {
1274-
"max_new_tokens": generation_params.get("max_new_tokens", 4096),
1325+
"max_new_tokens": generation_params.get("max_new_tokens", _default_max_new_tokens()),
12751326
"do_sample": generation_params.get("temperature", 1.0) > 0,
12761327
"temperature": generation_params.get("temperature", 1.0),
12771328
"top_p": generation_params.get("top_p", 0.95),
12781329
"num_return_sequences": generation_params.get("num_return_sequences", 1),
12791330
"pad_token_id": self.tokenizer.pad_token_id,
1280-
"eos_token_id": self.tokenizer.eos_token_id,
1331+
"eos_token_id": self._resolve_eos_token_ids(),
12811332
"return_dict_in_generate": True,
12821333
"output_scores": generation_params.get("logprobs", False),
12831334
"use_cache": True
@@ -1571,13 +1622,13 @@ def process_batch(
15711622
if batch_prompts: # If there are any uncached prompts
15721623
# Configure generation parameters
15731624
base_params = {
1574-
"max_new_tokens": generation_params.get("max_new_tokens", 4096) if generation_params else self.model_config.max_new_tokens,
1625+
"max_new_tokens": generation_params.get("max_new_tokens", _default_max_new_tokens()) if generation_params else self.model_config.max_new_tokens,
15751626
"do_sample": generation_params.get("temperature", 1.0) > 0 if generation_params else self.model_config.do_sample,
15761627
"temperature": generation_params.get("temperature", 1.0) if generation_params else self.model_config.temperature,
15771628
"top_p": generation_params.get("top_p", 1.0) if generation_params else self.model_config.top_p,
15781629
"num_return_sequences": n,
15791630
"pad_token_id": self.tokenizer.pad_token_id,
1580-
"eos_token_id": self.tokenizer.eos_token_id,
1631+
"eos_token_id": self._resolve_eos_token_ids(),
15811632
}
15821633

15831634
# Add optional parameters if specified
@@ -1900,7 +1951,7 @@ def create(
19001951

19011952
# Use directly available parameters for entropy decoding
19021953
entropy_params = {
1903-
"max_new_tokens": max_tokens if max_tokens is not None else 4096,
1954+
"max_new_tokens": max_tokens if max_tokens is not None else _default_max_new_tokens(),
19041955
"temperature": temperature,
19051956
"top_p": top_p,
19061957
"top_k": top_k,
@@ -2046,7 +2097,7 @@ def create(
20462097
"temperature": temperature,
20472098
"top_p": top_p,
20482099
"num_return_sequences": n,
2049-
"max_new_tokens": max_tokens if max_tokens is not None else 4096,
2100+
"max_new_tokens": max_tokens if max_tokens is not None else _default_max_new_tokens(),
20502101
"presence_penalty": presence_penalty,
20512102
"frequency_penalty": frequency_penalty,
20522103
"stop_sequences": [stop] if isinstance(stop, str) else stop,

0 commit comments

Comments
 (0)