npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-hetzner-inference

v0.1.2

Published

Hetzner Experiments Platform Inference API for the pi coding agent: free open-weight models, live catalog discovery and rate-limit visibility

Readme

pi-hetzner-inference

Use the Hetzner Experiments Platform Inference API from pi — free open-weight models, registered as a first-class pi provider.

  • Four documented models registered with correct context windows and modalities, priced at $0
  • Live catalog from GET /v1/models, cached, so a model Hetzner adds or removes shows up without a package update
  • Per-key rate-limit budget in the status bar, and an explanation when a turn stalls on HTTP 429
  • Optional hetzner_ask tool: hand bulk text work to a free model while an expensive model stays in the driver's seat

The service is an experiment, not a product. No SLA, no guaranteed availability, and it can be changed or withdrawn at any time. Hetzner's Experiments Platform documentation says metrics are collected to evaluate experiments; its public Inference documentation does not specify prompt, completion, or image retention. Do not send sensitive or production-critical data.

Install

pi install npm:pi-hetzner-inference

Then authenticate, either interactively:

/login hetzner

or through the environment:

export HETZNER_INFERENCE_API_KEY=<your-token>

A stored credential (/login) wins over the environment variable. With neither, the provider stays registered but its models are hidden from /model and --list-models — nothing errors.

The one exception is --model hetzner/... on the command line: pi resolves that model at startup, so without a token it fails with No API key found for hetzner. Authenticate first (start pi without --model, run /login hetzner), or export the variable before launching.

Use

pi --model hetzner/Kimi-K2.7-Code

or /model hetzner/... inside a session. Commands:

| Command | What it does | | --- | --- | | /hetzner status | Auth source, catalog origin and age, 60s rate-limit window, last 429, live reachability check | | /hetzner models | Registered models with context window, modalities and architecture notes | | /hetzner refresh | Re-read /v1/models now, ignoring the cache, and re-register the provider | | /hetzner quiet [on\|off] | Silence the experiment notice | | /hetzner ask [on\|off] | Enable or disable the hetzner_ask tool (needs /reload) |

Models

| Model | Total tokens | Modalities | Agent loop | Thinking | Architecture | | --- | --- | --- | --- | --- | --- | | Kimi-K2.7-Code | 262k | text + image | yes | on, switchable | MoE 1T total / 32B active, code-tuned | | DeepSeek-V4-Flash-0731 | 512k | text | yes | on, switchable | MoE 304B total / 13B active | | Qwen/Qwen3.6-35B-A3B-FP8 | 262k | text + image | yes | on, switchable | MoE 35B total / 3B active | | GLM-5.2-NVFP4 | 512k | text | yes | on, switchable | MoE 744B total / 40B active |

All four think before answering, and the deployment returns that thinking on a separate reasoning channel, so pi shows it as a collapsible block rather than mixing it into the answer. Thinking is billed as output either way, and think:off genuinely stops it on every model — each one's switch was measured rather than assumed.

Thinking is also why maxTokens should not be set low on Qwen in particular: it reasons at length (around 230 output tokens to answer a one-word question), and with only 256 tokens of budget it never reached the tool call at all — it stopped at finish_reason: "length" mid-thought. With room it calls tools reliably. If a model looks incapable of tool use, check the finish reason before believing it.

The token figure is the server's max_model_len, and it caps input plus requested output together: a 262k-token prompt is rejected if max_tokens is 16. So the context window pi is told about is max_model_len - maxTokens (229k input + 32k output for Kimi, by default). Without that reservation pi would consider a turn to fit that the server then refuses — recoverable, but a wasted round trip. Lower maxTokens if you would rather trade output room for input room.

Latency is uneven. GLM-5.2-NVFP4 answered a one-word prompt in 2.3 seconds on one run, took 45 seconds on another, and exceeded 60 seconds on two more. Expect that from an experimental platform, and re-check any model with node scripts/probe.mjs --model <id> --timeout 300000.

/v1/models returns ids only, so the metadata above lives in src/catalog.ts and is overlaid onto whatever the endpoint reports. An id this package does not recognise is still registered, with conservative defaults (128k total budget, text only, with output room reserved) and a note in /hetzner models.

Rate limits

The documented limits are per API key, over a 60 second window: 10M input and 200k output tokens. Exceeding either returns HTTP 429.

pi already retries 429 responses using the server's Retry-After (up to 60s), so this extension does not retry anything. What it adds is visibility:

hetzner in 1.2M/10M · out 34k/200k (60s)

The status line counts token usage pi observed in this session, including anything spent by hetzner_ask. Requests made with the same key from elsewhere are invisible to it. At 80% of either limit you get a warning with the time until the window frees up; on a 429 you get a notification saying pi is retrying and when.

The API sent no x-ratelimit-* headers when probed, so the local window is the only signal available. If that changes, those headers are authoritative and /hetzner status shows them.

hetzner_ask (opt-in)

/hetzner ask on
/reload

Registers one sequential tool. The main model can hand it self-contained text work — summarising a long log or diff, translating, extracting fields — and it answers from a registered Hetzner model. The delegate gets no tools, no repository access and no conversation history, so everything it needs must be in the call. task is limited to 4,000 characters, input to 1,000,000 characters, and an optional model id to 200 characters. Input is marked as untrusted data, and returned text is marked as untrusted rather than authority for consequential tool actions.

Thinking is switched off for the delegate. This work is mechanical, and reasoning is billed against the same per-key output budget the main model is spending — so paying for a reasoning block that is then discarded is waste. Models with no measured switch are left alone rather than sent a guessed key, and /hetzner ask reports which case applied in the tool result.

It is off by default: an extra tool in every system prompt is intrusive, and it is pointless when the main model is already a Hetzner one.

Settings

Environment variables win over ~/.pi/agent/hetzner-inference.json, which wins over the defaults. The extension writes that settings file and the model-id cache described below, both with mode 0600. API tokens live only in pi's credential store.

| Setting | Env | Default | Meaning | | --- | --- | --- | --- | | quiet | PI_HETZNER_QUIET | false | Suppress the experiment notice | | discovery | PI_HETZNER_DISCOVERY | true | Allow /v1/models lookups | | discoveryTtlHours | PI_HETZNER_DISCOVERY_TTL_HOURS | 12 | Cache lifetime before a background refresh | | budget | PI_HETZNER_BUDGET | true | Show the rate-limit status line | | ask | PI_HETZNER_ASK | false | Register hetzner_ask | | askModel | PI_HETZNER_ASK_MODEL | first available | Model id used by hetzner_ask | | maxTokens | PI_HETZNER_MAX_TOKENS | 32768 | Output cap advertised for every model |

Discovery caches only the reported model ids, in ~/.pi/agent/cache/hetzner-inference-models.json. Responses are limited to 1 MB and 1,000 models; ids are limited to 200 printable URL-safe characters. Metadata always comes from the current package version, so upgrading takes effect immediately instead of being shadowed by a stale cache. With discovery disabled, the cache is ignored. Startup performs no network I/O: the static catalog is registered synchronously, and an opportunistic refresh runs without blocking session startup. Expected skips (disabled, offline, no token, or a fresh cache) are reported separately from transport, authentication, and response-shape failures.

Measured behaviour

The Hetzner documentation covers models, chat completions and image input, but says nothing about function calling, streaming usage or base64 image input — all of which decide whether a coding agent can use the service at all. scripts/probe.mjs measures them:

export HETZNER_INFERENCE_API_KEY=<token>
npm run probe -- --strict --timeout 300000 --json .probe-report.json
npm run probe -- --strict --overflow --timeout 300000  # also requires pi's auto-compaction path (~2MB per model)

Results from 2026-08-11:

| Capability | Result | | --- | --- | | tools accepted, tool call emitted | yes — all four | | Forced tool_choice | yes | | Tool-result round trip | yes | | Streaming | yes, with usage in the stream | | Base64 data: image input | Qwen and Kimi yes; DeepSeek and GLM reject it as "not a multimodal model" | | Separate thinking channel | reasoning on all four — not reasoning_content | | Switching thinking off | chat_template_kwargs.thinking on Kimi, DeepSeek and GLM; .enable_thinking on Qwen and GLM | | reasoning_effort | accepted, but pi never sends it under thinkingFormat: "chat-template" | | max_completion_tokens | GLM rejected it on one run and accepted it on a later one — so this package pins max_tokens | | Rate-limit response headers | none sent | | Context-overflow error | matches pi's overflow patterns, so pi auto-compacts and retries | | Trivial prompt latency | 0.8–3.4s, with 30s, 45s and >60s outliers on GLM |

GLM-5.2-NVFP4 needs a longer deadline than the probe's default to be measured at all (npm run probe -- --model GLM-5.2-NVFP4 --timeout 300000); it answered a one-word prompt in 29.8s on the run that produced its rows above. --strict is the maintainer release gate: it requires every known model and every load-bearing capability to pass. Overflow is reported as skipped unless --overflow is requested, while timeouts and incomplete observations are inconclusive and still make a strict run exit nonzero. JSON reports include the measurement time and per-check verdicts.

Three consequences worth knowing:

max_completion_tokens support moves under you. GLM-5.2-NVFP4 rejected that field outright on one probe run and accepted it on a later one the same day. max_tokens has been accepted by every model on every run, and the server phrases its own errors in terms of it, so maxTokensField: "max_tokens" is pinned rather than left to pi's URL-based auto-detection. Treat every capability here as a property of today's deployment, not a guarantee.

The thinking channel is reasoning, not reasoning_content. Every model bills output tokens for thinking a one-word reply — around 230 on Qwen, 91 on GLM, 26 on DeepSeek, 17 on Kimi — and that text comes back under reasoning. pi reads reasoning_content, reasoning and reasoning_text in that order and renders whichever it finds, so the thinking is displayed, not lost. Two things follow: the maxTokens floor of 2048 stays (on a 32-token budget thinking consumed the whole budget and the visible reply came back empty), and reasoning: true plus the measured thinkingFormat is what makes think:off actually stop the reasoning instead of merely hiding it.

The thinking switch differs per model. Qwen answers to chat_template_kwargs.enable_thinking, Kimi and DeepSeek to chat_template_kwargs.thinking, GLM to either — and where a model does not recognise a key it still returns HTTP 200 and ignores it. Accepting a parameter proves nothing here; only the reasoning channel going quiet does. That is why src/catalog.ts carries one measured key per model rather than a shared default.

So src/catalog.ts sets supportsUsageInStreaming: true, reasoning: true with the per-model thinking switch on all four, image input only on Qwen and Kimi, and keeps the remaining OpenAI-platform compat flags off (system instead of developer, max_tokens, no strict function schemas, no grammar tools) because those were not exercised and false is the behaviour that is known to work.

To override any of this without patching the package, use modelOverrides in ~/.pi/agent/models.json — it applies to extension-registered models. For example, to pin a model to the other thinking switch, or to turn reasoning off for one entirely:

{
  "providers": {
    "hetzner": {
      "modelOverrides": {
        "GLM-5.2-NVFP4": {
          "reasoning": true,
          "compat": { "thinkingFormat": "chat-template", "chatTemplateKwargs": { "thinking": { "$var": "thinking.enabled" } } }
        }
      }
    }
  }
}

Without the extension

If all you want is the four models, ~/.pi/agent/models.json is enough:

{
  "providers": {
    "hetzner": {
      "baseUrl": "https://inference.hetzner.com/api/v1",
      "api": "openai-completions",
      "apiKey": "$HETZNER_INFERENCE_API_KEY",
      "compat": { "supportsDeveloperRole": false, "maxTokensField": "max_tokens" },
      "models": [
        { "id": "Kimi-K2.7-Code", "contextWindow": 229376, "maxTokens": 32768, "input": ["text", "image"] },
        { "id": "GLM-5.2-NVFP4", "contextWindow": 479232, "maxTokens": 32768 },
        { "id": "DeepSeek-V4-Flash-0731", "contextWindow": 479232, "maxTokens": 32768 },
        { "id": "Qwen/Qwen3.6-35B-A3B-FP8", "contextWindow": 229376, "maxTokens": 32768, "input": ["text", "image"] }
      ]
    }
  }
}

You lose catalog discovery, rate-limit visibility, the commands and hetzner_ask. Use whichever fits; do not use both for the same provider id.

Development

npm install
npm run typecheck
npm test                       # node:test, no network
PI_OFFLINE=1 PI_HETZNER_DISCOVERY=false pi -e ./src/index.ts --list-models
npm run probe -- --strict --timeout 300000  # live release gate; requires HETZNER_INFERENCE_API_KEY

The extension has no runtime dependencies. All pi imports are type-only, and typebox is provided by pi itself.

License

MIT. The models are provided by their respective developers under their own licenses (MIT for DeepSeek-V4-Flash and GLM-5.2, Modified MIT for Kimi-K2.7, Apache 2.0 for Qwen3.6) and come with no warranty of any kind.