Skip to main content

needlepath — Python client

The framework-free core client for the Needlepath hosted context-selection service. HTTP only, zero required dependencies, one transport/auth seam.

Framework integrations are separate distributions and depend on this one:

Distribution Seam
needlepath-langchain AgentMiddleware (wrap_model_call, wrap_tool_call) + the LangGraph recipe
needlepath-litellm LiteLLM proxy CustomGuardrail
llama-index-postprocessor-needlepath BaseNodePostprocessor

Install

pip install needlepath
pip install "needlepath[httpx]"   # optional: httpx-backed async transport

Python 3.9+. No required runtime dependencies — the default transport is stdlib urllib, so installing this client can never move a version you already pinned.

Quickstart

from needlepath import NeedlepathClient, ContextRecord, TaskSpec

client = NeedlepathClient(
    api_key="np_live_...",  # or NEEDLEPATH_API_KEY
    operating_point="np-2026-07-r2",  # required — see "Pin the operating point"
)

result = client.select(
    records=[
        ContextRecord(text=tool_output, kind="tool_result", id="lookup-1"),
        ContextRecord(text=doc, kind="external_data", id="doc-7", title="SOP"),
    ],
    task=TaskSpec(prompt="what is the invoice total for A-1002?"),
    max_context_tokens=4000,
)

context = result.rendered_context if result.applied else my_original_context

select() never raises. Read result.applied; when it is False, use your original context. That is the whole integration contract.

Async

from needlepath import AsyncNeedlepathClient

async with AsyncNeedlepathClient(operating_point="np-2026-07-r2") as client:
    result = await client.select(records=..., task=..., max_context_tokens=4000)

Identical semantics — the decision logic is literally the same functions. Uses httpx.AsyncClient when httpx is importable, a worker thread otherwise; it never blocks the event loop either way.

Configuration

Argument Environment Default
api_key NEEDLEPATH_API_KEY none (no Authorization header)
operating_point NEEDLEPATH_OPERATING_POINT required
base_url NEEDLEPATH_BASE_URL https://api.nextmoca.com
timeout 10.0 s, total, retries included
max_retries 2
max_request_bytes 6 * 1024 * 1024
shadow False

The base URL is a documented default, not a hard-coded one: a durable custom domain replaces it, and swapping it is a config change rather than a release.

http:// is refused for any non-loopback host. TLS-only is an inherited binding constraint; loopback is exempt so the localhost benchmark adapter still works.

The four binding rules, and where they live

1. Fail open, always

Timeout, 5xx, a violated contract, an empty selection, a stood-down gate — every one of them returns a SelectionResult with applied=False, the exception on .error, and a machine-readable .reason. Nothing is raised on the selection path.

if not result.applied:
    log.info("needlepath passthrough", extra=result.metadata())
    context = my_original_context

result.reason is an open enum (needlepath.Reason). Today:

reason applied meaning
ok yes safe to apply
shadow no shadow mode — measured, deliberately not applied
disabled no the integration was constructed disabled
no_records no nothing to select over; no call was made
below_trigger no under the configured trigger; no call was made
empty_selection no zero records selected
engine_fallback no fallback_used — the engine wants the full context
selection_error no the response carried a selection_error
no_per_record_detail no per-record detail was required and absent
payload_too_large no over the client-side guard; no call was made
timeout / transport_error / http_error / contract_error no error paths
unexpected_error no a defect in this client — still fails open

Treat an unrecognized reason as "do not apply". result.failed separates the error paths from the benign declines, which is the distinction that decides whether a passthrough deserves a WARNING or a DEBUG.

The empty-selection rule is not a nicety. NEXPE-413 is the precedent: the engine returned an empty selection as a success, and at a framework boundary that has to degrade to passthrough, never to an empty context.

select_or_raise() exists for the two cases where fail-open is wrong — diagnostics, and a caller that is itself a benchmark and must not silently substitute a passthrough for a measurement. Do not use it in an adapter.

2. Pin the operating point

operating_point is required at construction, and this is the one failure that raises rather than fails open. Failing open on a bad request is degradation a customer absorbs; failing open on "no operating point was pinned" accepts exactly the behaviour drift the rule exists to prevent. It fails at wire-up, never under load.

Labels are immutable: np-2026-07-r2 means one frozen configuration forever, and a retune mints a new label. client.operating_points() lists what the endpoint serves — call it once at wire-up if you want the assertion, never per request.

3. Guard the 6 MB cap client-side

The serialized body is measured before it is sent. Over the guard, you get reason="payload_too_large" and no network call, rather than a platform error from the far side of the wire.

The guard measures the JSON body. Lambda's 6 MB limit applies to the whole invocation payload — the API Gateway event envelope sits on top — so a request that passes at 5.99 MB can still be rejected upstream. Lower max_request_bytes if you run near the cap.

4. Shadow mode is a one-line switch

client = NeedlepathClient(operating_point="np-2026-07-r2", shadow=True)

The call is made exactly as a live call would be — same body, same billing, same operating point — the full response comes back and is reported, and the selection is never applied. Measure what Needlepath would have saved you; change nothing.

budget.mode = "shadow" is not on the wire yet. BudgetSpec.mode on the published contract accepts fixed and adaptive only; adding shadow is a public-repo contract change (§0.1, sequenced in E7). The wire half is behind needlepath.features.WIRE_SHADOW_MODE; flip it in the same change that lands the field. Client-side behaviour does not change when you do — it is already correct.

Retries

Retried: 429, 502, 503, 504, and transport failures. Retry-After is honoured in delta-seconds form. Backoff is full-jitter exponential, and every retry is bounded by the timeout deadline — retries can never exceed the budget the caller gave you.

500 is not retried. A 500 from this endpoint is engine_error: a deterministic failure on this exact input. Retrying buys nothing, doubles the latency before you fail open, and spends a second billable invocation on a request that will fail identically.

POST is safe to retry here because the endpoint is stateless and idempotent by contract (ADAPTER_API_DESIGN.md invariant 1).

Metadata

result.metadata() is a flat, metadata-only dict built for a framework's metadata blob:

{"needlepath.applied": False, "needlepath.reason": "engine_fallback",
 "needlepath.tokens_before": 8400, "needlepath.tokens_saved": 0,
 "needlepath.gate_reason": "standdown:flat_gap", ...}

Nothing derived from record text is in it. One consequence is non-obvious: selection_error is built server-side as f"{type(exc).__name__}: {exc}" and can carry fragments of your input in an exception message — the Lambda deliberately declines to log it for that reason — so only its type appears here. The full string stays on result.response.selection_error.

Testing your integration

from needlepath.testing import FakeTransport, response_payload, ok, error

transport = FakeTransport([error(503), ok(response_payload("np-1"))])
client = NeedlepathClient(operating_point="np-2026-07-r2", transport=transport)

needlepath.testing is public API, not a private helper. Every rule above is a rule about what happens when the service misbehaves, and you cannot test that against a healthy service.

Wire contract

INTERFACE.md in context-selection-bench is the published contract; the dataclasses here are an independent implementation of it, deliberately copied rather than imported so a research harness never lands in a customer's dependency tree. tests/test_wire_compatibility.py asserts they have not drifted.

Unknown response fields are kept on response.extra, never rejected: /v1 is additive, and usage (E2) is already scheduled to arrive that way.

The gate outcome and reason are an open enum. Nothing in this SDK branches on them and nothing you build on it should — a third outcome is a planned additive change.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

needlepath-0.1.1.tar.gz (52.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

needlepath-0.1.1-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

File details

Details for the file needlepath-0.1.1.tar.gz.

File metadata

  • Download URL: needlepath-0.1.1.tar.gz
  • Upload date:
  • Size: 52.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for needlepath-0.1.1.tar.gz
Algorithm Hash digest
SHA256 9dc99b7bb343799684eea0003cc52c6f2b83e9c4e2ce89d5020cf1fd73614090
MD5 fb2464d26277f5c31a60fe2951a3e7f3
BLAKE2b-256 6b9c63b5cca1c3d8085041a62c56bf1cd627f90d28dedc6b5640fd5294238e2b

See more details on using hashes here.

File details

Details for the file needlepath-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: needlepath-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 40.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for needlepath-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 95569707f0cf19f10e0725b8f63b9ecbd86be0d86930cd04a49833bc5d817e79
MD5 48c8b84c4635f1313343e05e0a2dce31
BLAKE2b-256 144909714b82698f081a6933409f4a724aa8bbc08fb646230b8a29e49ba3c8a2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page