Skip to content
Open
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
20 changes: 16 additions & 4 deletions src/rkllama/api/server_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,23 @@ def get_tokenizer(model_name):
logger.debug("Local Tokenizer doesn't exists!")

# Get model specific tokenizer from Huggin Face specified in Modelfile

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in the comment: “Huggin Face” should be “Hugging Face”.

Suggested change
# Get model specific tokenizer from Huggin Face specified in Modelfile
# Get model specific tokenizer from Hugging Face specified in Modelfile

Copilot uses AI. Check for mistakes.
model_in_hf = get_property_modelfile(model_name, "HUGGINGFACE_PATH", rkllama.config.get_path("models")).replace('"', '').replace("'", "")
logger.info(f"Download the tokenizer only one time from Hugging face repo: {model_in_hf}")

tokenizer_repo = get_property_modelfile(model_name, "TOKENIZER", rkllama.config.get_path("models"))
if tokenizer_repo:
tokenizer_repo = tokenizer_repo.replace('"', '').replace("'", "")
else:
# Fallback to HUGGINGFACE_PATH if TOKENIZER is not configured
tokenizer_repo = get_property_modelfile(model_name, "HUGGINGFACE_PATH", rkllama.config.get_path("models"))
if tokenizer_repo:
tokenizer_repo = tokenizer_repo.replace('"', '').replace("'", "")

if not tokenizer_repo:
logger.error(f"No TOKENIZER or HUGGINGFACE_PATH configured for model {model_name} in Modelfile.")
raise ValueError(f"No tokenizer repository configured for model {model_name}. Please set TOKENIZER or HUGGINGFACE_PATH in Modelfile.")

logger.info(f"Download the tokenizer only one time from Hugging face repo: {tokenizer_repo}")

# Get the tokenizer configured for the model
tokenizer = AutoTokenizer.from_pretrained(model_in_hf, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(tokenizer_repo, trust_remote_code=True)

# Save to the disk the local tokenizer for future use
tokenizer.save_pretrained(local_tokenizer_path)
Expand Down
121 changes: 118 additions & 3 deletions src/rkllama/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def print_color(message, color):
variables.worker_manager_rkllm = WorkerManager()


def create_modelfile(huggingface_path, From, system="", model_name=None):
def create_modelfile(huggingface_path, From, system="", model_name=None, tokenizer_repo=None):
struct_modelfile = f"""
FROM="{From}"

Expand Down Expand Up @@ -96,6 +96,9 @@ def create_modelfile(huggingface_path, From, system="", model_name=None):

"""

if tokenizer_repo:
struct_modelfile += f'TOKENIZER="{tokenizer_repo}"\n\n'

# Use config for models path
path = os.path.join(rkllama.config.get_path("models"), model_name)

Expand All @@ -108,6 +111,107 @@ def create_modelfile(huggingface_path, From, system="", model_name=None):
f.write(struct_modelfile)


def hf_repo_has_config(repo: str, fs=None) -> bool:
fs = fs or HfFileSystem()
try:
fs.info(f"{repo}/config.json")
return True
except Exception:
return False
Comment on lines +114 to +120

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hf_repo_has_config catches all exceptions and returns False, which can silently treat auth/network failures as “missing config.json”. Consider only returning False for “not found” errors (e.g., EntryNotFound/FileNotFound) and logging (or re-raising) unexpected exceptions so pull output/logs are actionable when Hugging Face is unavailable or credentials are missing.

Copilot uses AI. Check for mistakes.


def get_hf_model_metadata(repo: str) -> dict | None:
try:
response = requests.get(f"https://huggingface.co/api/models/{repo}", timeout=10)
if response.status_code == 200:
Comment on lines +123 to +126

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotations use PEP 604 unions (e.g., dict | None, str | None), but the project metadata declares requires-python >=3.9. Python 3.9 will fail to import this module due to syntax error. Either switch these annotations to Optional[...] / Union[...] or bump the project’s minimum supported Python version to 3.10+.

Copilot uses AI. Check for mistakes.
return response.json()
except Exception as e:
Comment on lines +126 to +128

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_hf_model_metadata returns None for any non-200 response without logging the status/body, which makes tokenizer resolution failures hard to diagnose (e.g., 404 vs 401 vs rate limit). Consider logging at least the status code (debug/info) and/or using response.raise_for_status() with controlled handling.

Suggested change
if response.status_code == 200:
return response.json()
except Exception as e:
response.raise_for_status()
try:
return response.json()
except ValueError as e:
logger.debug(f"Unable to parse Hugging Face metadata JSON for {repo}: {e}")
except requests.HTTPError as e:
response = e.response
status_code = response.status_code if response is not None else "unknown"
response_body = ""
if response is not None:
response_body = response.text[:500].replace("\n", "\\n")
logger.debug(
f"Unable to load Hugging Face metadata for {repo}: HTTP {status_code}"
+ (f" - {response_body}" if response_body else "")
)
except requests.RequestException as e:

Copilot uses AI. Check for mistakes.
logger.debug(f"Unable to load Hugging Face metadata for {repo}: {e}")
return None


def extract_base_model_candidates(metadata: dict) -> list[str]:
candidates = []
if not metadata:
return candidates

base_model = metadata.get("base_model")
if isinstance(base_model, str) and base_model:
candidates.append(base_model)
elif isinstance(base_model, list):
candidates.extend([m for m in base_model if isinstance(m, str) and m])

card_data = metadata.get("cardData", {}) or {}
for key in ("base_model", "base model", "Base Model", "parent_model", "parent_model_id"):
value = card_data.get(key)
if isinstance(value, str) and value:
candidates.append(value)
elif isinstance(value, list):
candidates.extend([m for m in value if isinstance(m, str) and m])

parent = metadata.get("parent")
if isinstance(parent, dict):
parent_model = parent.get("modelId") or parent.get("id")
if isinstance(parent_model, str) and parent_model:
candidates.append(parent_model)

parents = metadata.get("parents")
if isinstance(parents, list):
for item in parents:
if isinstance(item, str):
candidates.append(item)
elif isinstance(item, dict):
parent_model = item.get("modelId") or item.get("id")
if isinstance(parent_model, str) and parent_model:
candidates.append(parent_model)

# Remove duplicates while preserving order
seen = set()
return [c for c in candidates if c not in seen and not seen.add(c)]


def resolve_tokenizer_repo(repo: str, fs=None, max_depth=3) -> str | None:
"""Resolve the best tokenizer repo by searching for config.json in the repo hierarchy."""
fs = fs or HfFileSystem()

if hf_repo_has_config(repo, fs):
return repo

checked = {repo}
queue = [repo]
depth = 0

while queue and depth < max_depth:
next_queue = []
for current in queue:
metadata = get_hf_model_metadata(current)
if not metadata:
continue

candidates = extract_base_model_candidates(metadata)
for candidate in candidates:
if candidate in checked:
continue
checked.add(candidate)

if hf_repo_has_config(candidate, fs):
return candidate

next_queue.append(candidate)

queue = next_queue
depth += 1

# Last-resort heuristics for common RKLLM/finetune repo naming
for pattern in ("-rk3588", "-rkllm", "-rkllama", "-w8a8", "-w8a8_g128", "-w8a8_opt"):
if pattern in repo:
candidate = repo.replace(pattern, "")
if candidate not in checked and hf_repo_has_config(candidate, fs):
return candidate

return None


def load_model(model_name, huggingface_path=None, system="", From=None, request_options=None, loaded_by=None):

# Auto-detect caller from request headers if not explicitly provided
Expand Down Expand Up @@ -278,8 +382,19 @@ def generate_progress():
# Define a file to download
local_filename = os.path.join(model_dir, file)

# Create fonfiguration file for model
create_modelfile(huggingface_path=repo, From=file, model_name=model_name)
# Determine whether the target repo already contains a valid Hugging Face config
tokenizer_repo = None
if hf_repo_has_config(repo):
yield f"Repository {repo} contains config.json; TOKENIZER not required.\n"
else:
tokenizer_repo = resolve_tokenizer_repo(repo)
Comment on lines +387 to +390

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In /pull, you already instantiate fs = HfFileSystem() for the file download metadata, but hf_repo_has_config(repo) / resolve_tokenizer_repo(repo) create their own HfFileSystem instances. Pass the existing fs into these helpers to avoid redundant client setup and extra remote calls (and to ensure consistent auth/caching behavior).

Suggested change
if hf_repo_has_config(repo):
yield f"Repository {repo} contains config.json; TOKENIZER not required.\n"
else:
tokenizer_repo = resolve_tokenizer_repo(repo)
try:
repo_has_config = hf_repo_has_config(repo, fs=fs)
except TypeError:
repo_has_config = hf_repo_has_config(repo)
if repo_has_config:
yield f"Repository {repo} contains config.json; TOKENIZER not required.\n"
else:
try:
tokenizer_repo = resolve_tokenizer_repo(repo, fs=fs)
except TypeError:
tokenizer_repo = resolve_tokenizer_repo(repo)

Copilot uses AI. Check for mistakes.
if tokenizer_repo:
yield f"Repository {repo} does not expose config.json. Using tokenizer repo {tokenizer_repo}.\n"
else:
yield f"Repository {repo} does not contain config.json and no upstream tokenizer repo was resolved. TOKENIZER will not be set.\n"

# Create configuration file for model
create_modelfile(huggingface_path=repo, From=file, model_name=model_name, tokenizer_repo=tokenizer_repo)

yield f"Downloading {file} ({total_size / (1024**2):.2f} MB)...\n"

Expand Down