Instructions to use tpls/gemma4-tool-shim with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use tpls/gemma4-tool-shim with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="tpls/gemma4-tool-shim")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("tpls/gemma4-tool-shim", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use tpls/gemma4-tool-shim with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "tpls/gemma4-tool-shim" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tpls/gemma4-tool-shim", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/tpls/gemma4-tool-shim
- SGLang
How to use tpls/gemma4-tool-shim with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "tpls/gemma4-tool-shim" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tpls/gemma4-tool-shim", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "tpls/gemma4-tool-shim" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tpls/gemma4-tool-shim", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use tpls/gemma4-tool-shim with Docker Model Runner:
docker model run hf.co/tpls/gemma4-tool-shim
| """Pure parser for the gemma-4 coder's native/leaked tool markup β no dependencies. | |
| The gemma-4 coder GGUF emits tool calls as text in `content` (`<|tool_call>call:NAME{...}`, | |
| `<|tool>NAME{...}`, `<call:NAME{...}>`, `NAME(k=v)<|/tool|>`) instead of structured | |
| tool_calls, because `llama.cpp --jinja` doesn't recognise its native format. This module | |
| is the single source of truth for detecting those calls and parsing their arguments, | |
| plus the serve-side tools-in-prompt block. Wrap it in a litellm callback (see | |
| litellm_shim.py) or call it directly (see standalone.py). Pure stdlib, unit-tested. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import re | |
| from typing import Any | |
| # Stop strings: the turn separators the broken GGUF never treats as EOG β they bound | |
| # the runaway `<|turn>user` loop. Injected by both the shim (pre_call) and the steer | |
| # sweep (so its probes get a clean, bounded response instead of runaway garbage). We | |
| # deliberately do NOT stop on `<channel|>` (the model may emit a thinking channel | |
| # before the call); the parser takes the first call and ignores trailing hallucination. | |
| STOP_STRINGS = ["<|turn>", "<turn|>", "<|turn|>"] | |
| # Fancy/single quotes the model emits inside arg strings -> plain double quote. | |
| _FANCY = str.maketrans({"β": '"', "β": '"', "β": '"', "β": '"', "'": '"'}) | |
| # Leaked tool-call markers vary by build / token-typing damage: | |
| # b9755: <|tool>NAME{...} (or <|tool|> / <tool|>) | |
| # b8733: <call:NAME{...}> (the <|tool_call> token stripped to '<') | |
| # raw: <|tool_call>call:NAME{...} | |
| # Anchor on a tool marker OR a `call:` prefix, then NAME{ body }. | |
| # NAME char class = MCP's tool-name set (SEP-986): [A-Za-z0-9_.-], which is OpenAI's / | |
| # Anthropic's ^[a-zA-Z0-9_-]{1,64}$ PLUS the dot used for namespacing (weather.get_weather, | |
| # fs.read_file). The dot is the one that bit v3: a bare `[A-Za-z_]\w*` truncated | |
| # `weather.get_weather`β`weather`, then no `{` β the call was dropped. First char is held | |
| # to [A-Za-z_] (stricter than the specs, which permit a leading digit) as parser safety β | |
| # real names don't start with a digit and it avoids matching `call:123{...}` junk. | |
| _TOOL_RE = re.compile( | |
| r"(?:<\|?tool(?:_call)?\|?>\s*(?:call:)?|call:)\s*([A-Za-z_][\w.\-]*)\s*\{(.*?)\}", | |
| re.S, | |
| ) | |
| # Paren form the model also leaks: NAME(k=v, ...) instead of NAME{...}. This is a | |
| # *coder* model, so a bare NAME(...) is almost always real code β match ONLY when | |
| # anchored on a tool marker: a LEADING <|tool>/<|tool_call> OR a TRAILING tool-close | |
| # token (<|/tool|>, <tool|>, β¦). Body is paren-balanced-free ([^()]*). | |
| _TOOL_PAREN_LEAD = re.compile( | |
| r"<\|?tool(?:_call)?\|?>\s*(?:call:)?\s*([A-Za-z_][\w.\-]*)\s*\(([^()]*)\)", | |
| re.S, | |
| ) | |
| _TOOL_PAREN_CLOSE = re.compile( | |
| r"\b([A-Za-z_][\w.\-]*)\s*\(([^()]*)\)\s*<\|?/?\s*tool(?:_call)?\|?>", | |
| re.S, | |
| ) | |
| # strip a leaked thinking channel: <|channel>...<channel|> | |
| _CHANNEL_RE = re.compile(r"<\|?(?:channel|think|thought)\|?>.*?<(?:channel|think|thought)\|>", re.S) | |
| # residual control-token junk to scrub. Matches ONLY known gemma control tokens | |
| # (<|name>, <name|>, <|name|>) β never bare <>/brackets, so code output from this | |
| # *coder* model (C++ generics, HTML, comparisons) is left intact. | |
| _CTRL_NAMES = "tool_call|tool_response|tool|channel|turn|think|thought|start_of_turn|end_of_turn" | |
| _JUNK_RE = re.compile(rf"<\|?(?:{_CTRL_NAMES})\|?>") | |
| def _coerce(v: str) -> Any: | |
| v = v.strip() | |
| if len(v) >= 2 and v[0] in "\"'ββββ" and v[-1] in "\"'ββββ": | |
| return v[1:-1] | |
| low = v.lower() | |
| if low == "true": | |
| return True | |
| if low == "false": | |
| return False | |
| if low == "null": | |
| return None | |
| try: | |
| return int(v) | |
| except ValueError: | |
| try: | |
| return float(v) | |
| except ValueError: | |
| return v | |
| def _split_top(s: str) -> list[str]: | |
| """Split on top-level commas only (ignore commas nested in {}/[]/()).""" | |
| parts, depth, cur = [], 0, "" | |
| for ch in s: | |
| if ch in "{[(": | |
| depth += 1 | |
| elif ch in "}])": | |
| depth = max(0, depth - 1) | |
| if ch == "," and depth == 0: | |
| parts.append(cur) | |
| cur = "" | |
| else: | |
| cur += ch | |
| if cur.strip(): | |
| parts.append(cur) | |
| return parts | |
| def _parse_args(body: str) -> dict: | |
| if not body.strip(): | |
| return {} | |
| # Fast path: normalise fancy quotes, quote bare keys, try strict JSON. | |
| norm = body.translate(_FANCY) | |
| norm = re.sub(r"([{,]\s*)([A-Za-z_]\w*)\s*:", r'\1"\2":', "{" + norm + "}") | |
| try: | |
| out = json.loads(norm) | |
| if isinstance(out, dict): | |
| return out | |
| except Exception: | |
| pass | |
| # Tolerant fallback: split top-level commas, coerce scalars. Accept BOTH `:` | |
| # (brace form {k: v}) and `=` (paren form (k=v)) as the key/value separator. | |
| out: dict = {} | |
| for part in _split_top(body): | |
| m = re.search(r"[:=]", part) | |
| if not m: | |
| continue | |
| k, v = part[: m.start()], part[m.start() + 1 :] | |
| out[k.strip().strip("\"'ββββ")] = _coerce(v) | |
| return out | |
| def find_tool_calls(text: str) -> tuple[list[dict], str]: | |
| """Return (calls, leftover_content) where calls is a list of | |
| {"name": str, "arguments": dict} parsed from leaked gemma tool markup. Pure β | |
| no litellm types. The shim wraps this into OpenAI tool_calls; the sweep counts it.""" | |
| if not text or ("tool" not in text and "call:" not in text): | |
| return [], text or "" | |
| # Gather candidates from the brace form {β¦} and both anchored paren forms (k=v), | |
| # then merge by position and drop overlaps (a call matching both a leading marker | |
| # and a trailing close shows up twice β keep one). | |
| raw: list[tuple[int, int, str, str]] = [] | |
| for rx in (_TOOL_RE, _TOOL_PAREN_LEAD, _TOOL_PAREN_CLOSE): | |
| for m in rx.finditer(text): | |
| raw.append((m.start(), m.end(), m.group(1), m.group(2))) | |
| if not raw: | |
| return [], text | |
| raw.sort(key=lambda t: (t[0], -(t[1] - t[0]))) # earliest first; longer span wins a tie | |
| spans: list[tuple[int, int, str, str]] = [] | |
| for s, e, name, body in raw: | |
| if any(s < pe and e > ps for ps, pe, _, _ in spans): # overlaps a kept span | |
| continue | |
| spans.append((s, e, name, body)) | |
| spans.sort(key=lambda t: t[0]) | |
| calls = [{"name": name, "arguments": _parse_args(body)} for _, _, name, body in spans] | |
| first_start = spans[0][0] | |
| # Residual assistant content = only the preamble BEFORE the first call (everything | |
| # after is more calls or a hallucinated result). Scrub control tokens / thinking | |
| # channels; edge-strip the stray '<' a '<call:' wrapper leaves (edges only). | |
| preamble = _CHANNEL_RE.sub("", text[:first_start]) | |
| leftover = _JUNK_RE.sub("", preamble).strip().strip("<>").strip() | |
| if len(re.sub(r"\W", "", leftover)) < 2: | |
| leftover = "" | |
| return calls, leftover | |
| def repair_native_args(name: str, arguments: Any) -> tuple[str, bool]: | |
| """Repair the `arguments` string of a tool_call the llama.cpp NATIVE parser | |
| already captured (the canonical-call path: with --jinja a model whose calls | |
| the native peg-gemma4 parser recognises lands them in tool_calls.arguments, | |
| often as malformed JSON, with empty content β so the content shim never sees | |
| them). Returns (json_string, changed). | |
| Parity guarantee: if `arguments` already parses to a JSON object it is | |
| returned verbatim with changed=False. The production huihui coder leaks its | |
| calls into *content* and leaves native tool_calls empty, so this never fires | |
| for it β only a present-but-malformed native arg string is re-parsed, through | |
| the same tolerant parser the content path already uses. | |
| """ | |
| if not isinstance(arguments, str): | |
| return arguments, False | |
| s = arguments.strip() | |
| try: | |
| if isinstance(json.loads(s), dict): | |
| return arguments, False # already valid JSON object β leave verbatim | |
| except Exception: | |
| pass | |
| # Malformed: reconstruct a `call:NAME{body}` shape and reuse find_tool_calls. | |
| body = s if (s.startswith("{") and s.endswith("}")) else "{" + s + "}" | |
| calls, _ = find_tool_calls(f"call:{name}{body}") | |
| if calls: | |
| return json.dumps(calls[0]["arguments"], ensure_ascii=False), True | |
| # Last resort: parse the bare body directly (handles non-brace blobs). | |
| parsed = _parse_args(s.strip().lstrip("{").rstrip("}")) | |
| if parsed: | |
| return json.dumps(parsed, ensure_ascii=False), True | |
| return arguments, False | |
| def clean_content(text: str) -> str: | |
| """Scrub the broken GGUF's leaked control tokens / thinking channels from plain | |
| content, without touching bare <>/brackets (code-safe).""" | |
| if not text: | |
| return text | |
| out = _CHANNEL_RE.sub("", text) | |
| out = _JUNK_RE.sub("", out) | |
| return out.strip() | |
| # ββ serve-side tools-in-prompt βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # A model fine-tuned with tool DEFINITIONS folded into the prompt only behaves if it | |
| # sees the SAME block at serve time. render_tools_prompt() reproduces that block; call | |
| # fold_tools_prompt(messages, tools) before sending so train == serve. | |
| TOOLS_HEADER = "# Available tools" | |
| _NAME_BAD = re.compile(r"[^A-Za-z0-9_.\-]+") | |
| def _sanitize_tool_name(name: str) -> str: | |
| clean = _NAME_BAD.sub("_", str(name)).strip("_") or "tool" | |
| if not re.match(r"[A-Za-z_]", clean): | |
| clean = "_" + clean | |
| return clean | |
| def _normalize_tool_schema(tool: Any) -> dict | None: | |
| """OpenAI-nested or flat tool schema β {name, args, required, description}, or None.""" | |
| if isinstance(tool, dict) and isinstance(tool.get("function"), dict): | |
| tool = tool["function"] | |
| if not isinstance(tool, dict) or not tool.get("name"): | |
| return None | |
| params = tool.get("parameters") | |
| if not isinstance(params, dict): | |
| params = tool.get("arguments") if isinstance(tool.get("arguments"), dict) else {} | |
| props = params.get("properties") | |
| args = sorted(props) if isinstance(props, dict) else [] | |
| required = params.get("required") | |
| required = [str(r) for r in required] if isinstance(required, list) else [] | |
| return { | |
| "name": _sanitize_tool_name(tool["name"]), | |
| "args": args, | |
| "required": required, | |
| "description": str(tool.get("description") or "").strip(), | |
| } | |
| def render_tools_prompt(tools: list | None) -> str: | |
| """Render tool DEFINITIONS as the prompt block, or "" if none valid. Mirrors the | |
| corpus-side render_tools_block (contract-tested for byte equality).""" | |
| norm = [t for t in (_normalize_tool_schema(x) for x in (tools or [])) if t] | |
| if not norm: | |
| return "" | |
| lines = [ | |
| TOOLS_HEADER, | |
| "To use a tool, emit <|tool_call>call:NAME followed by a compact-JSON argument " | |
| "object. Available functions (required args marked *):", | |
| ] | |
| for t in norm: | |
| sig = ", ".join((a + "*" if a in t["required"] else a) for a in t["args"]) | |
| desc = f" β {t['description']}" if t["description"] else "" | |
| lines.append(f"- {t['name']}({sig}){desc}") | |
| return "\n".join(lines) | |
| def fold_tools_prompt(messages: list, tools: list | None) -> list: | |
| """Prepend the rendered tools block to the FIRST user message (serve-side mirror of | |
| the corpus fold). Returns a NEW list; no-op when there are no valid tools.""" | |
| block = render_tools_prompt(tools) | |
| if not block or not isinstance(messages, list): | |
| return messages | |
| out = [dict(m) if isinstance(m, dict) else m for m in messages] | |
| for m in out: | |
| if isinstance(m, dict) and m.get("role") == "user" and isinstance(m.get("content"), str): | |
| m["content"] = f"{block}\n\n{m['content']}".strip() | |
| break | |
| return out | |