{"slug": "probelock-lockfile-for-llm-tool-calling", "title": "Probelock – lockfile for LLM tool calling", "summary": "Probelock, a new open-source tool, provides a lockfile for LLM tool calling that records model capabilities and fails CI when a model or quant swap lowers scores. It derives probes from existing tool schemas and uses code-based scoring instead of LLM judges, enabling reproducible comparisons across model variants.", "body_md": "**A capability lockfile for local models.** It records what a model does on a set\nof tool-calling and output checks, and fails CI when a model/quant/runtime swap\nlowers a score.\n\n```\nllama-3.1-8b @ Q8_0 (ollama)  →  llama-3.1-8b @ Q4_K_M (ollama)\nCapability            Baseline   Candidate     Δ   Status\narg_validity              1.00        0.67  -0.33  REGRESSION\narity_robustness          1.00        0.67  -0.33  REGRESSION\nformat_adherence          1.00        1.00  +0.00  ok\nneedle_in_tools           1.00        0.33  -0.67  REGRESSION\nno_hallucinated_tool      1.00        0.67  -0.33  REGRESSION\nrequired_args             1.00        1.00  +0.00  ok\nstructured_output         1.00        0.33  -0.67  REGRESSION\ntool_discrimination       1.00        0.33  -0.67  REGRESSION\ntool_permission           1.00        0.67  -0.33  REGRESSION\ntool_restraint            1.00        0.67  -0.33  REGRESSION\ntool_selection            1.00        0.67  -0.33  REGRESSION\n\nFAIL — capabilities regressed or removed: arg_validity, arity_robustness,\nneedle_in_tools, no_hallucinated_tool, structured_output, tool_discrimination,\ntool_permission, tool_restraint, tool_selection\n```\n\nHere the Q4 quant scores 0.33–0.67 on several capabilities where Q8 scored 1.00.\n`probelock gate`\n\nexits non-zero when a capability drops past the threshold.\n\npromptfoo is a test framework you author. probelock is a lockfile you commit.\n\n**Probes are derived from your tool schemas.** Point it at the OpenAI-style tool definitions your agent already ships, and it generates a fixed, reproducible battery of capability checks. You write no test cases.**No LLM judge.** Every probe is scored by code: JSON-schema validation, exact match, or a tool-name check. Run it twice on the same model and the numbers match. (promptfoo relies on assertions you write and often on model-graded evals, which vary across runs.)**It compares a model against its own baseline,** across a model/quant/runtime swap, rather than producing an absolute leaderboard. You only ever compare like with like, on your box, with your tools, so the \"benchmarks are gameable/hardware-dependent\" objection does not apply.\n\n## Install & run (only needs [uv](https://docs.astral.sh/uv/))\n\nRun it without installing, or install it into the current environment:\n\n```\nuvx probelock --help          # run the latest release\npip install probelock         # or install it\n```\n\nTo run an unreleased revision straight from git:\n\n```\nuvx --from git+https://github.com/kelkalot/probelock probelock --help\n```\n\nThe examples below use `uv run`\n\nfrom a checkout of this repo. No model is required\nfor the demo — a deterministic `SimulatedClient`\n\nstands in for two quant levels of\nthe same model:\n\n```\n# from the probelock/ project dir\nuv run probelock derive --tools examples/agent_tools.json          # see the probe battery\nuv run probelock probe  --tools examples/agent_tools.json --simulate fixtures/profile_q8.json -o q8.lock\nuv run probelock probe  --tools examples/agent_tools.json --simulate fixtures/profile_q4.json -o q4.lock\nuv run probelock diff   q8.lock q4.lock\nuv run probelock gate   --baseline q8.lock --candidate q4.lock     # exits non-zero\n```\n\nAgainst a local model, swap `--simulate`\n\nfor an OpenAI-compatible endpoint:\n\n```\nuv run probelock probe --tools examples/agent_tools.json \\\n    --endpoint http://localhost:11434/v1 --model llama3.1:8b-instruct-q4_K_M \\\n    --quant Q4_K_M --runtime ollama --timeout 120 -o q4.lock\n```\n\nA probe the model rejects (e.g. \"model does not support tools\") or that times out\nscores **0** for that capability and the run continues, so a model that cannot\ntool-call still produces a lockfile. An unreachable server, a 404 (wrong model or\nURL), or a run where every probe fails aborts the run, so a misconfiguration never\nbecomes a poisoned all-zeros baseline.\n\n`examples/agent_tools.json`\n\nis a 3-tool schema for the walkthrough above, not a\nsensitivity benchmark — validation testing found it insensitive to real capability\ndrift that a 10-tool schema with overlapping tool names and richer argument\nconstraints caught cleanly (see [ VALIDATION.md](/kelkalot/probelock/blob/main/VALIDATION.md)). A schema with too\nfew tools, or arguments with no real constraints to violate, under-reports\nregressions. Point\n\n`--tools`\n\nat your own agent's actual tool definitions before\ntrusting `gate`\n\nin CI.probelock speaks one protocol — OpenAI `/v1/chat/completions`\n\nwith OpenAI-style\ntools — so anything that exposes it works with `--endpoint`\n\n. For providers that\ndo not (Anthropic, Gemini, …), route through a unified SDK with `--via`\n\n. Every path\nis deterministic; none of them put an LLM in the loop.\n\n| You have… | Use |\n|---|---|\nOllama, vLLM, llama.cpp server, LM Studio, HF TGI, OpenAI, OpenRouter, Together… |\n`--endpoint <url>/v1 --model <name>` (vLLM needs `--enable-auto-tool-choice` ; llama.cpp needs `--jinja` ) |\nAnthropic / Gemini / Mistral / Bedrock / … (any-llm) |\n`--via anyllm --model anthropic/claude-3-5-sonnet` |\n| Any of 100+ providers (litellm SDK) | `--via litellm --model anthropic/claude-3-5-sonnet` |\nA running LiteLLM proxy |\n`--endpoint http://litellm:4000/v1 --model <name>` (no extra) |\nIn-process HF `transformers` / MLX (no server) |\nnot yet — add a small `Client` adapter |\n\n```\npip install 'probelock[anyllm]'   # or 'probelock[litellm]'\nprobelock probe --tools tools.json --via anyllm --model mistral/mistral-large-latest \\\n    --samples 5 --temperature 0.7 -o candidate.lock\n```\n\n`--via`\n\nclients reuse the same caching, sampling, and error semantics as\n`--endpoint`\n\n; they are thin adapters over each SDK's OpenAI-shaped response. Add a\nnew backend by implementing the tiny `Client`\n\nprotocol — that is the only seam.\n\n[ demo/](/kelkalot/probelock/blob/main/demo) has runs against a local Ollama server: a committed\n\n`qwen3.5:9b`\n\nbaseline vs a `gemma3:1b`\n\ncandidate (which does not support tool-calling). See\n[for the transcript, or replay it:](/kelkalot/probelock/blob/main/demo/DEMO.md)\n\n`demo/DEMO.md`\n\n```\nasciinema play demo/probelock-demo.cast   # or: bash demo/demo.sh\n```\n\nThe tool-calling capabilities drop `1.00 → 0.00`\n\nand the gate exits non-zero.\n`tool_restraint`\n\n, `tool_permission`\n\n, and `no_hallucinated_tool`\n\nstay `1.00`\n\n(a\nmodel that cannot call tools cannot misuse one), and `gemma3:1b`\n\nscores `1.00`\n\non\n`format_adherence`\n\nvs `0.50`\n\nfor `qwen3.5:9b`\n\n. The diff is per-capability.\n\nAlso committed: `qwen3.5:9b`\n\nvs `lfm2.5-thinking:1.2b`\n\n:\n\n```\nuv run probelock diff demo/qwen3.5-9b.lock demo/lfm2.5-thinking.lock\n```\n\nThe 1.2B model matches `qwen3.5:9b`\n\non tool selection, discrimination,\n`needle_in_tools`\n\n, `arg_validity`\n\n, `required_args`\n\n, and the three safety probes;\n`structured_output`\n\nand `arity_robustness`\n\ndrop `1.00 → 0.33`\n\n.\n\n| Capability | What it checks | Scorer |\n|---|---|---|\n`tool_selection` |\nCalls the right tool for the task | tool-name match |\n`tool_discrimination` |\nCalls the right tool and no other (picks precisely) |\ntool-name set |\n`needle_in_tools` |\nFinds the right tool when many (15+) are offered | tool-name match |\n`arg_validity` |\nEmitted args validate against the tool's JSON schema | `jsonschema` |\n`required_args` |\nAll required args present and non-empty | key presence |\n`arity_robustness` |\nFills every parameter (required + optional) when asked |\nall-present |\n`structured_output` |\nEmits schema-valid JSON on demand (no tools, no fences) | parse + `jsonschema` |\n`json_mode` (opt-in, `--json-mode` ) |\nSame, but via the server's native `response_format` API instead of a prompt |\nparse + `jsonschema` |\n`tool_restraint` |\nDoes not call a tool for a task that needs none (over-trigger) |\nno tool call |\n`tool_permission` |\nDoes not call a tool it was explicitly forbidden to use |\nforbidden tool absent |\n`no_hallucinated_tool` |\nDoes not fabricate a call to a tool that was not offered |\ncalled ⊆ offered |\n`format_adherence` |\nFollows an exact output constraint | exact match |\n\nThree are **negative** probes (a higher score means the bad behavior did not\nhappen): `tool_restraint`\n\n(over-triggering), `tool_permission`\n\n(calling a forbidden\ntool), and `no_hallucinated_tool`\n\n(fabricating a tool). All probes are derived from\nthe tool schemas, not hand-authored.\n\n```\ntool schemas ──▶ derive probes ──▶ Client ──▶ ResponseMessage ──▶ deterministic scorer ──▶ Lockfile\n (your agent)    (zero authoring)  (model)    (the only model      (no LLM judge)          (commit it)\n                                              -touching part)\n                                                                    Lockfile + Lockfile ──▶ diff / gate\n```\n\nThe only nondeterministic part is the `Client`\n\n; everything else is pure, so the\nsame inputs produce the same lockfile and the same diff. At temperature 0 the\nclient caches identical requests, so the probes that share one request (the tool\nchecks for a given tool) hit the network once. The `SimulatedClient`\n\ncrafts correct or\nincorrect responses that the real scorers grade, so the scoring path runs even\nwith no model present.\n\nSchema-derived probes are single-turn and synthetic — great for catching schema-level\nregressions, blind to what breaks after several turns of real context, a tool result\nfeeding back in, or ambiguous phrasing. `--traces`\n\nadds a second source: real,\nalready-recorded agent decision points (e.g. exported from litellm's OpenTelemetry\ncallback), replayed through the exact same deterministic scorers.\n\n```\nuv run probelock derive --tools tools.json --traces traces.json      # see what gets added\nuv run probelock probe  --tools tools.json --traces traces.json \\\n    --endpoint http://localhost:11434/v1 --model llama3.1:8b -o candidate.lock\n```\n\n`--tools`\n\nis optional here: traced probes replay their own embedded tool definitions,\nso a trace-only run (`probe --traces traces.json ...`\n\n) needs no schema file. The same\nholds for `--mined`\n\nbelow. Provide `--tools`\n\nwhen you also want the synthetic battery.\n\nA traces file is a small, stable JSON schema probelock defines itself — **not** raw\nOpenTelemetry — because OTel's own span attribute layout is not stable across libraries or\nversions (litellm has already changed where it puts request/response attributes once, and\nhas a newer, differently-shaped opt-in integration). Converting your export into this shape\nis a one-time step you own; see\n[ examples/otel_traces_to_probelock.py](/kelkalot/probelock/blob/main/examples/otel_traces_to_probelock.py) for a\ndocumented starting point and\n\n[for the target shape:](/kelkalot/probelock/blob/main/fixtures/sample_traces.json)\n\n`fixtures/sample_traces.json`\n\n```\n{\n  \"version\": 1,\n  \"records\": [\n    {\n      \"id\": \"checkout-flow-turn-3\",\n      \"messages\": [{\"role\": \"user\", \"content\": \"...\"}],\n      \"tools\": [ /* OpenAI-style tool defs actually offered at this turn */ ],\n      \"response\": {\"content\": null, \"tool_calls\": [{\"name\": \"...\", \"arguments\": \"{...}\"}]}\n    }\n  ]\n}\n```\n\nTrace-derived probes join the *same* capabilities as schema-derived ones — `tool_selection`\n\n,\n`tool_discrimination`\n\n, `arg_validity`\n\n, `required_args`\n\n, and `structured_output`\n\n— since these\nmap cleanly onto \"replay this real context, check the candidate still behaves validly\" (probe\nids carry a `::traced::`\n\nmarker if you want to inspect the split). The rest stay purely\nschema-derived: `needle_in_tools`\n\n, `tool_permission`\n\n, `no_hallucinated_tool`\n\n, and\n`tool_restraint`\n\nneed a synthetic perturbation (an injected distractor tool, a forbidden-tool\ninstruction, a removed tool) that a passively recorded trace does not naturally contain;\n`format_adherence`\n\nneeds an exact-text prompt, not a tool-calling decision point; and\n`arity_robustness`\n\nneeds its own explicit \"fill EVERY parameter, including optional ones\"\ninstruction to mean anything — a real conversation was never asked for that, so replaying it\nwould only test whichever optional fields happened to get filled in that one exchange, not\nrobustness.\n\n**Unlike a tool schema, a traces file contains real conversation content.** `probe --traces`\n\nprints a warning every time, and the lockfile records a `traces_fingerprint`\n\nso a `diff`\n\nflags a baseline/candidate pair whose trace inputs differ — but review and redact the file\nyourself before committing it, the same way you would review any fixture with real data in it.\n\nTested against a real llama.cpp regression (commit-level, not synthetic): `gate`\n\nfails on\nthe regressed commit and passes on an adjacent, unrelated commit. See\n[ VALIDATION.md](/kelkalot/probelock/blob/main/VALIDATION.md) for the test setup and results, and\n\n[to reproduce it.](/kelkalot/probelock/blob/main/fixtures/gptoss_regression_trace.json)\n\n`fixtures/gptoss_regression_trace.json`\n\nIf your stack does not already log requests, the recording proxy captures them with one line changed in the agent:\n\n```\nprobelock proxy --listen 127.0.0.1:8484 \\\n                --upstream http://127.0.0.1:11434 \\\n                --out traces/agent.jsonl\n# agent side: base_url = \"http://127.0.0.1:8484/v1\"\n```\n\nEvery request is forwarded to the upstream unchanged (streaming included — SSE flows\ntoken by token and is reassembled for the record afterwards, tool-call deltas and all);\neach completed chat-completions exchange is appended asynchronously as one `trace-v1`\n\nJSONL record. Recording is strictly non-invasive: on any internal logging error the\nrequest is still served and a warning goes to stderr. Failed or truncated exchanges\n(upstream errors, mid-stream disconnects) are logged with a failing status so `ingest`\n\nskips them instead of mining half-generated responses. Multi-turn conversations are\nstitched into sessions without any agent cooperation (restarting the proxy mid-conversation\nsplits that conversation into two sessions — harmless, but it weakens confirmation\nevidence, so prefer restarting between runs), `--max-size`\n\n/ `--max-age`\n\nrotate\nthe log, and the file is created `0600`\n\n— it holds **verbatim conversation content**;\nkeep it out of version control (redaction happens later, at `ingest`\n\n).\n\n`--traces`\n\n(above) replays a *curated* export you assembled by hand. `probelock ingest`\n\ngoes one step earlier: point it at a raw request/response log of real agent traffic —\nthe proxy's output, or your own logging — and it mines probes for you: multi-turn,\nrealistic regression tests with near-zero authoring effort, still scored by the same\ndeterministic checks (LLMs may appear in *trace generation* — that is your own agent —\nbut never in *scoring*).\n\n```\nprobelock ingest traces/agent.jsonl --out probes/mined.json   # everything lands \"pending\"\nprobelock traces review probes/mined.json                     # activate probes (y/n/e/a/s/q)\nprobelock probe --tools tools.json --mined probes/mined.json \\\n    --endpoint http://localhost:11434/v1 --model llama3.1:8b -o candidate.lock\n```\n\n`ingest`\n\naccepts several logs at once (`probelock ingest agent.jsonl agent-*.jsonl`\n\n) —\npass a rotated set together so sessions spanning a rotation boundary keep their\nconfirmation evidence.\n\nSeveral input formats are supported (`--format`\n\n, or `auto`\n\n):\n\n`--format` |\nShape |\n|---|---|\n`trace-v1` |\nthe native record the recording proxy writes (one JSON object per line, `request` /`response.message` ) |\n`openai-jsonl` |\nthe verbatim chat-completions request body next to the verbatim response, per line |\n`anthropic-jsonl` |\nlogged Anthropic Messages API calls (`request` /`response` ); content blocks, `tool_use` /`tool_result` , and `system` are translated to the canonical shape |\n`otel-genai` |\nan OTLP-JSON span export, read via the OpenTelemetry GenAI semantic-convention attributes (`gen_ai.prompt` /`gen_ai.completion` , blob or indexed form) — scoped to the spec, not any one library's layout; spans without those attributes are skipped and counted |\n\n`auto`\n\ndetects the JSONL shapes and OTel documents. See the `fixtures/sample_*`\n\nfiles for\neach. For OTel exporters that do not follow the semantic convention,\n[ examples/otel_traces_to_probelock.py](/kelkalot/probelock/blob/main/examples/otel_traces_to_probelock.py) remains the\nconversion recipe.\n\nDeduplication is exact-hash by default (deterministic). `--cluster embeddings --embed-endpoint URL --embed-model NAME`\n\ninstead groups *near-duplicate* contexts by\nembedding cosine similarity (via an OpenAI-compatible `/v1/embeddings`\n\nendpoint you\nalready run). This is opt-in and **not deterministic** — the grouping depends on the\nembedding model and version, so probelock prints a caveat and records `cluster: embeddings`\n\nin each affected probe's provenance. Everything downstream (scoring, gating)\nstays deterministic; only which contexts merged does not.\n\nRaw traffic includes model mistakes, so **provenance determines trust** — every probe\nrecords how many sessions support it and which rule confirmed it, and that decides how\nmuch review it needs:\n\n| Category | Check at replay | Mined from | Review |\n|---|---|---|---|\n`traced_schema_validity` |\nsome call's args validate against the called tool's schema | every tool-calling exchange (no inference) | `--auto-accept schema_validity` is safe |\n`traced_tool_selection` |\ncalls the confirmed tool | exchanges confirmed good: the result fed back and the conversation moved on (no error payload, no corrected-args retry, no re-ask), or the same context produced the same call in ≥ `--min-agreement` distinct sessions |\nreview, or `--auto-accept-all --i-know-what-im-doing` |\n`traced_no_tool` |\nanswers in text, calls nothing | unanimous text answers across ≥ `--min-agreement-notool` (default 3) distinct sessions, no re-ask, preferring contexts lexically distant from the tools |\nindividual review only — a mislabeled probe freezes a model mistake as expected behavior |\n\nThe traced capabilities are deliberately separate names in the lockfile: a drop in multi-turn trace probes while single-turn synthetic probes hold steady is itself diagnostic (\"context-length-sensitive regression\").\n\nPrivacy defaults are conservative. Argument values in frozen contexts are always\nreplaced with structure-preserving placeholders (`\"query\": \"<str:47ch>\"`\n\n); message\ncontent stays verbatim (that is what makes replay realistic), so mined probes carry\n`\"sensitive\": true`\n\nand `probe -o`\n\nrefuses to include them in a written lockfile\nwithout `--allow-sensitive`\n\n. If you want committable probes, opt in to scrubbing with\n`ingest --redact-patterns emails,phones,paths`\n\n. Identical contexts are deduplicated\n(timestamps stripped, whitespace collapsed), sampling keeps up to `--per-capability`\n\nprobes per (tool, category) preferring longer contexts and later turns, and everything\nthe pipeline skips (failed calls, forced `tool_choice`\n\n, oversized contexts, ambiguous\nagreement) is counted and reported — never silently dropped.\n\nThe full pipeline is validated against real agent traffic on real local models —\nincluding a runtime swap the gate catches and real frozen-mistake probes the review\nstep rejects — in [ VALIDATION-TRACES.md](/kelkalot/probelock/blob/main/VALIDATION-TRACES.md).\n\n`diff`\n\ncompares two lockfiles; `trend`\n\ncompares N, in the order you give them — a\nquantization ladder or the same model over time — so you can see *where* a capability\nholds and *where* it cliffs:\n\n```\nuv run probelock trend Q8_0.lock Q6_K.lock Q5_K_M.lock Q4_K_M.lock Q3_K_M.lock Q2_K.lock\nCapability          Q8_0   Q6_K   Q5_K_M   Q4_K_M   Q3_K_M   Q2_K       Δ   Trend\nstructured_output   1.00   1.00     1.00     0.67     0.33   0.33   -0.67   ↓ regressed\ntool_restraint      1.00   1.00     1.00     1.00     1.00   1.00   +0.00   = stable\ntool_selection      1.00   1.00     0.67     1.00     0.67   1.00   +0.00   ~ unstable\n```\n\nEach row is annotated by its whole-ladder behavior: `regressed`\n\n(net drop past\n`--max-drop`\n\n), `improved`\n\n, `unstable`\n\n(net-flat but it dipped along the way — a signal a\ntwo-point diff of the endpoints would miss), `stable`\n\n, `removed`\n\n(present early but gone\nfrom the last rung — a dropped capability, counted as a regression), or `partial`\n\n(present in fewer than two lockfiles). `--format markdown|json|html`\n\nmirror `diff`\n\n; the\nHTML view draws a sparkline per capability. `trend`\n\nnever fails on a regression (use\n`gate`\n\npairwise for CI); it exits non-zero only on bad input.\n\nThe filename stem is each column's header, so name your lockfiles for the axis\n(`Q8_0.lock`\n\n, `Q4_K_M.lock`\n\n).\n\nWith one sample per probe, a capability backed by 3 tools quantizes to\n`{0, 0.33, 0.67, 1.0}`\n\n— a single flip moves it 0.33, far past the default 0.05\ngate. So:\n\nruns each probe N times and records the pass-`probe --samples N [--temperature T]`\n\n*rate*(raise the temperature for sampling variance).only fails on a drop that is statistically significant for the recorded trial count (a one-sided two-proportion test). Sub-significant drops are shown as`gate --confidence 0.95`\n\nand do`noisy ↓`\n\n**not** fail the gate; raise`--samples`\n\nto confirm or clear them.\n\nA total collapse (e.g. `1.00 → 0.00`\n\n) is significant even at low N; a single-flip\n`1.00 → 0.67`\n\nover 3 trials is `noisy`\n\nuntil you raise `--samples`\n\n.\n\n`probelock init`\n\nscaffolds a `probelock.tools.json`\n\nand a\n`.github/workflows/probelock.yml`\n\nto start from. Commit a baseline lockfile, then\ngate each candidate:\n\n```\n- run: uvx probelock probe\n       --tools tools.json --endpoint $LLM_URL --model $MODEL --samples 5 --temperature 0.7 -o candidate.lock\n- run: uvx probelock gate\n       --baseline probelock.lock --candidate candidate.lock --max-drop 0.05 --confidence 0.95\n```\n\nOr use the composite GitHub Action ([ action.yml](/kelkalot/probelock/blob/main/action.yml)), which wraps those\ntwo steps end-to-end:\n\n```\n- uses: kelkalot/probelock@v0\n  with:\n    tools: tools.json\n    baseline: probelock.lock\n    endpoint: ${{ secrets.LLM_ENDPOINT }}\n    model: ${{ vars.LLM_MODEL }}\n```\n\nTo show the result on a pull request, render the diff as Markdown (or `--format html`\n\nfor a self-contained page):\n\n```\nprobelock diff probelock.lock candidate.lock --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n```\n\n- Proxy hardening: a static Go/Rust binary beside the reference Python implementation, and streaming-reassembly edge cases (multi-line SSE events, resume-after-disconnect).\n- In-process backends (HF\n`transformers`\n\n/ MLX) via a small`Client`\n\nadapter, no server required. - Emit OpenTelemetry spans from\n`probe`\n\nruns, so a probe run shows up alongside your other agent traces in whatever backend you already use — a follow-on to trace-derived probes above (that direction consumes traces; this one produces them).\n\nApache-2.0 — see [LICENSE](/kelkalot/probelock/blob/main/LICENSE).\n\nBuilt with [Claude Code](https://claude.com/claude-code).", "url": "https://wpnews.pro/news/probelock-lockfile-for-llm-tool-calling", "canonical_source": "https://github.com/kelkalot/probelock", "published_at": "2026-07-09 07:16:50+00:00", "updated_at": "2026-07-09 07:42:51.560397+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Probelock", "Ollama", "promptfoo", "uv"], "alternates": {"html": "https://wpnews.pro/news/probelock-lockfile-for-llm-tool-calling", "markdown": "https://wpnews.pro/news/probelock-lockfile-for-llm-tool-calling.md", "text": "https://wpnews.pro/news/probelock-lockfile-for-llm-tool-calling.txt", "jsonld": "https://wpnews.pro/news/probelock-lockfile-for-llm-tool-calling.jsonld"}}