storycode / zerogpu_backend.py
Claude
Scaffold StoryCode: grounded code-story explainer for vibe coders
71d239c
Raw
History Blame Contribute Delete
1.92 kB
"""In-Space ZeroGPU fallback for when Modal isn't used.
Only active when USE_ZEROGPU_FALLBACK=1. Requires the Space to run on ZeroGPU
hardware (HF PRO) and the heavy deps (torch, transformers) added to
requirements.txt. Kept out of the default path so the Space stays a light CPU
container. Loads MiniCPM4.1-8B locally and mimics llm.chat_json's JSON contract.
NOTE: without vLLM's guided decoding we can't *force* schema-valid JSON, so we
prompt firmly for JSON and defensively parse. Treat this as break-glass, not the
primary backend.
"""
from __future__ import annotations
import json
import config
_model = None
_tokenizer = None
def _load():
global _model, _tokenizer
if _model is not None:
return
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
_tokenizer = AutoTokenizer.from_pretrained(config.MODEL_ID, trust_remote_code=True)
_model = AutoModelForCausalLM.from_pretrained(
config.MODEL_ID, trust_remote_code=True, torch_dtype=torch.bfloat16,
).eval().cuda()
def chat_json(messages: list[dict], schema: dict) -> dict:
import spaces # noqa: F401 (import guarded for non-ZeroGPU envs)
@spaces.GPU(duration=120)
def _run():
_load()
prompt = "\n\n".join(m["content"] for m in messages)
prompt += "\n\nRespond with ONLY a JSON object matching: " + json.dumps(schema)
out = _model.chat(_tokenizer, prompt, temperature=0.4, max_new_tokens=900)
text = out[0] if isinstance(out, tuple) else out
return _coerce(text)
return _run()
def _coerce(raw: str) -> dict:
s = (raw or "").strip().removeprefix("```json").removeprefix("```").removesuffix("```")
start, end = s.find("{"), s.rfind("}")
if start != -1 and end != -1:
try:
return json.loads(s[start:end + 1])
except (json.JSONDecodeError, ValueError):
pass
return {}