Releases: PlainsightAI/openfilter
Release list
Release v1.1.2
Fixed
- GPU usage percent emitted as float for Cloud Monitoring (FILTER-585):
Metrics.gpu_thread_funcnow castsgpu_usage_percenttofloatbefore export. NVML reportsgpu_utilas an integer, but the Cloud Monitoring descriptoropenfilter_gpu_usage_percentis type-locked to DOUBLE, so each export batch had one point rejected withvalue type for metric must be DOUBLE, but is INT64. The cast is targeted to this single summary field; the per-devicegpuNseries and the legitimately INT64 descriptors (gpu_accessible,camera_connected) are unchanged.
Release v1.1.1
Fixed
- Multi-topic Subject Data Streaming support for Webvis: Webvis now maintains isolated frame subject metadata per-topic, exposing them individually at
/{topic}/datawhile providing a combined overview of all topic data at/dataor/main/datafor multi-topic pipelines. Backwards compatibility for single-topic pipelines is fully preserved. Added cheap concurrency snapshots of current data and reordered FastAPI routes to ensure that/datarequests are not incorrectly matched by the/topicwildcard image endpoint.
Release v1.1.0
Added
-
OpenTelemetry MQ hop spans + per-frame trace propagation (PLAT-866, #99): Every filter-to-filter hop now produces
mq.send/mq.recvouter hop spans (attributes:topic,payload_bytes,frame.id,frame.format,mq.id), with nestedframe.serialize/frame.deserializeCPU sub-spans andzmq.send_multipart/zmq.recv_multipartkernel-syscall sub-spans.frame.encode_jpg/frame.decode_jpgcodec sub-spans fire on the jpg transport path only. Per-frame W3C trace context (traceparent/tracestate) now travels inside the ZMQ envelope via the standardopentelemetry.propagate.inject/extract(TraceContextTextMapPropagator); the consumer-side{FilterClass}.processspan (PLAT-848) parents on the per-frame context extracted from the envelope, so filter A's frame-17 spans and filter B's frame-17 spans share a single trace ID and nest correctly. TheTRACEPARENTenv-var fallback (PLAT-848 behavior) is retained for source filters that never callmq.recv(e.g.VideoIn). When tracing is disabled (the default,TELEMETRY_EXPORTER_TYPE=silent), the hot path short-circuits cleanly: codec / kernel sub-spans bail on a singleis_recording()check before any tracer lookup orSpanallocation, and the per-recvtime_ns()brackets + per-part byte tally inZMQReceiver.recv_once/ZMQSender.send_maybeare gated on the module-level hop tracer being set (so they don't run either). No new environment variables. -
Windowed async batch dispatch for
process_batch()(FILTER-457, FILTER-458, FILTER-459, FILTER-460, #95): four composable, opt-in additions to the runtime batch path.process_batch()result slots may now be deferredCallables thatmq.sendresolves lazily, so a filter can hand batch work to a worker thread instead of blocking the loop thread on the batch's wall-clock duration. A newaccumulate_windowconfig decouples the runtime ring buffer frombatch_size, with a publicFilter.select_batch()hook to pick which frames are dispatched.Filter.flush_batch()plusbatch_trigger: "auto" | "manual"give filters explicit control over when a batch drains. Abatch_workerspool runsprocess_batch()on aThreadPoolExecutorwith semaphore backpressure, so the loop accumulates the next batch while previous batches are still in flight. All four preserve existing behavior when unset (batch_workers=1,batch_trigger="auto",accumulate_windowunset).
Release v1.0.0
A new foundation for production Vision AI. OpenFilter 1.0 transitions the runtime from an evolving framework into a stable, production-grade platform. The new typed configuration surface is purely additive — the legacy dict-based FilterConfig coexists alongside it, so unmigrated filters keep working through the runtime fallback. Behavioral changes that do require filter-side updates are scoped to the items called out under ### Breaking Changes below.
Declarative Configuration Foundations
Each filter can now publish a build-time JSON Schema artifact alongside the runtime validation OpenFilter has always done — consumers can validate, document, and render configuration UIs against a filter without ever running it.
- Typed
FilterConfigBasewithemit_schema()(FILTER-441, #86): filters can declare their config surface as a pydantic model and emit it as JSON Schema (draft 2020-12) for build-time consumption.Managed/Resolvehelpers mark fields as orchestrator-controlled or platform-resolved. FilterOutputSchema+frame.datashape catalog (FILTER-444, #88): declarativeframe.datashape declaration. The catalog atopenfilter.filter_runtime.shapesships canonical types —BoundingBox,Polygon,Mask,Keypoint,Detection,DetectionSet,Track,TrackSet,Pose,PoseSet,KeypointSet,OCRSpan,OCRSpanSet,ClassificationResult— with stable$ids underhttps://schemas.plainsight.ai/shapes/<kebab>/v1. Filters reference shapes via$refinstead of negotiating dialects out-of-band.openfilter emit-schemaCLI (FILTER-442, #87): emits a filter's JSON Schema to stdout or-o <path>. Auto-detects the canonical class in a module or acceptsmodule:Classto disambiguate.
Reference migrations ship in filter-template and filter-sam3-detector; every other filter in the library keeps working unchanged through the runtime fallback.
Backward-Compatible Runtime Improvements
Two runtime improvements teams have been asking for, landed underneath the compatibility surface:
- Zero-copy shared-memory transport between co-located filters (#82) — eliminates inter-stage serialization on the same host.
- Batched inference via
process_batch()andbatch_size > 1(#61, v0.1.29) — meaningful throughput gains for SAM3, YOLO, and transformer-OCR filters that benefit from batched forward passes.
Production-Grade Engineering
- First-class observability: per-filter OpenTelemetry distributed tracing (#79), OpenLineage events (v0.1.5), per-frame timing metrics, and a turn-key Grafana stack (v0.1.21–22). Wall-clock latency dashboards separate
process()time from ZMQ and queue overhead. - Supply-chain hygiene: SLSA provenance and SBOM attestations on every Docker image; token auth and CORS for HTTP-exposed filters (v0.1.30).
- GPU and deployment ergonomics: framework-agnostic GPU detection via
ctypes(v0.1.28); GKE-compatibleLD_LIBRARY_PATHinjection (v0.1.26), so CUDA-dependent images deploy without per-cluster fixups. - Filter library refresh: new
ImageIn/ImageOutfilters for still-image pipelines (#21, #29);VideoInmoved offvidgeartocv2/ PyAV (#66, #67) for stability against odd-codec sources.
Breaking Changes
- OTLP gRPC
insecureflag now inferred from endpoint scheme (#90): the exporter factory and tracing builder useurllib.parse.urlparseon the configured endpoint —http://infers plaintext,https://infers TLS, and barehost:portinfers TLS (secure default, matches the OTel SDK). Deployments configuring bare-host endpoints against plaintext collectors must prefixhttp://to maintain prior behavior. Explicitinsecure=inexporter_config/ kwargs always wins. - Received
Frame.imagearrays are read-only (v0.1.28, re-surfaced for the SemVer-stable cut):topicmsgs2framessetsimage.flags.writeable = Falseon both the zero-copy ZMQ and SHM transport paths — this prevents corruption of shared ring slots and re-enables theFrame.copy()share-buffer fast path andFrame.jpgencode caching, both of which gate on read-only status. Filters that mutateframe.imagein place (e.g.cv2.rectangle(frame.image, ...),frame.image[y, x] = ...) must take a local copy first —image = frame.image.copy()— or construct a new frame viaFrame(new_image, frame, frame.format). Symptom if not migrated:ValueError: assignment destination is read-onlyraised from the mutating call.
Stability commitments
Surfaces committed under 1.0 — breaking changes will require a 2.0 bump:
openfilter.filter_runtime.FilterConfigBaseand itsManaged/Resolvefield shorthands, including theResolveHintliteral that parameterizes themopenfilter.filter_runtime.FilterOutputSchemaand the shape catalog inopenfilter.filter_runtime.shapes- The
openfilter emit-schemaCLI
The legacy dict-based FilterConfig continues to coexist with FilterConfigBase — unmigrated filters keep working unchanged.
Removed
OTLP_GRPC_ENDPOINT_SECURITYenvironment variable (#90): was previously a no-op —os.getenv(name, True)returned the literalTrueonly when unset; any set value came back as a truthy string, soinsecure=Trueregardless of operator intent.
Fixed
- Silent
$idinheritance onFilterOutputSchemasubclasses (FILTER-452, #88): subclasses no longer silently inherit a parent's$id.__init_subclass__refuses ambiguous construction at class-definition time. test_topo_balance_stepshutdown race on Python 3.10 (FILTER-461, #94): theTestFilterOldtopology tests poll for runner termination instead of asserting on a singlestep()call after the shutdown sentinel. Test-only change; no runtime impact.
Infrastructure
gh release createusessecrets.GH_BOT_USER_PAT(FILTER-462, #97) so the release-tag push triggerscascade-on-tag.yamlautomatically. Workflowpermissions:tightened tocontents: read.- Tag-triggered bump-PR cascade (DT-145, #85):
cascade-on-tag.yamlworkflow +scripts/cascade/*replace the previouscloudbuild-cascade.yamlmechanism. Fires on release-semver tag push, discovers eligiblefilter-*consumers, opens mechanical bump PRs throughgh-actions-public/open-mechanical-pr. - Cascade widens consumer pyproject upper bounds when target excludes them (#93): 1.0+ targets widen to next-major (
<2.0.0).
Release v0.2.1
This release rolls up the v0.2.0 declarative-configuration work (FILTER-441 / FILTER-442 / FILTER-444 / FILTER-452) and the v0.2.1 OTLP TLS inference change (#90) under a single tag. v0.2.0 was never tagged — its contents ship here under v0.2.1. Also folded in: the DT-145 cascade infrastructure (#85 / #93) and the FILTER-461 test-fragility fix (#94), all merged after the original v0.2.0 changelog window.
Added
- Typed
FilterConfigBasewithemit_schema()(FILTER-441): Newopenfilter.filter_runtime.FilterConfigBaselets filters declare their config surface as a pydantic model and emit it as JSON Schema (draft 2020-12) for build-time consumption. IncludesManaged/Resolvehelpers for marking fields as orchestrator-controlled or platform-resolved, plusMANAGED_KEY/RESOLVE_KEY/PREFLIGHT_KEYextensions stamped onto the emitted schema. Fully opt-in — existingdict-basedFilterConfigcontinues to work unchanged. (#86) openfilter emit-schemaCLI (FILTER-442): New CLI subcommand writes a filter's JSON Schema to stdout or-o <path>. Auto-detects the canonical class in a module or accepts an explicitmodule:Classqualifier. Default--kind configemits aFilterConfigBaseschema;--kind outputemits aFilterOutputSchema(FILTER-444).--include-managedsurfaces orchestrator-controlled fields for platform inspection; default is operator-facing surface only. (#87)FilterOutputSchema+frame.datashape catalog (FILTER-444): Newopenfilter.filter_runtime.FilterOutputSchemalets filters declare what they place onframe.dataas a build-time JSON Schema. The catalog atopenfilter.filter_runtime.shapesships canonical shapes —BoundingBox,Polygon,Mask,Keypoint,Detection,DetectionSet,Track,TrackSet,Pose,PoseSet,KeypointSet,OCRSpan,OCRSpanSet,ClassificationResult— with stable$ids underhttps://schemas.plainsight.ai/shapes/<kebab>/v1. Filters reference catalog shapes via$refinstead of negotiating dialects out-of-band. Coordinate conventions (pixel-space for bbox/polygon/mask; normalized[0, 1]for keypoints) are documented per shape with the production filter that motivated each choice. Catalog shapes carry runtime-only invariants enforced by pydantic validators:BoundingBoxxyxy ordering (zero-area allowed, inverted rejected),ClassificationResultparallel-array length equality, andPose17-keypoint arity whenskeleton="coco-17". The whole FILTER-444 surface is re-exported fromopenfilter.filter_runtimesofrom openfilter.filter_runtime import FilterOutputSchema, Detectionworks the same way the FILTER-441 import idiom does. (#88)
Changed
- OTLP gRPC
insecureflag now inferred from endpoint scheme: The exporter factory and tracing builder now useurllib.parse.urlparseon the configured endpoint —http://infers plaintext,https://infers TLS, and barehost:portinfers TLS (secure default, matches the OTel SDK). Explicitinsecure=inexporter_config/ kwargs always wins. The defaulthttp://localhost:4317endpoint still infersinsecure=True, so local collectors keep working with no config change. The metrics factory's missing localhost fallback was also fixed in this change — an unset endpoint no longer falls through to the SDK's TLS default against a plaintext local collector. (#90)
Fixed
- Silent
$idinheritance onFilterOutputSchemasubclasses (FILTER-452): A subclass of any$id-bearingFilterOutputSchema(catalog shape or filter-author output) that did not explicitly override__schema_id__silently inherited the parent's$idon the wire, causing two distinct classes to claim the same JSON Schema identity for$refresolution.__init_subclass__now refuses to construct such subclasses at class-definition time; authors must override__schema_id__with their own URI or set it toNoneas an explicit opt-out. (#88) test_topo_balance_stepshutdown race on Python 3.10 (FILTER-461): After PR #82's zero-copy IPC tightened pipeline timing, fourTestFilterOldtopology tests' shutdown assertions started failing on 3.10 — a singlerunner.step()immediately afterqout.put(None)was racing the filters' drain-and-report path. Newwait_for_runner_exithelper pollsstep()up to 5 s for the exit-codes list. Test-only change, no runtime impact. (#94)
Removed
OTLP_GRPC_ENDPOINT_SECURITYenvironment variable: removed from the metrics exporter factory. This variable was previously a full no-op —os.getenv(name, True)returned the literalTrueonly when unset; any set value came back as a string, and every non-empty string is truthy, soinsecure=Trueregardless of what an operator wrote. No deployment was getting TLS through this var. The actual behavior change for operators is narrow: those running with a barehost:portendpoint now get TLS, where they used to get plaintext. Setinsecure=Trueinexporter_config(or use anhttp://URL) to keep plaintext. (#90)
Infrastructure
- Tag-triggered bump-PR cascade (DT-145): New
cascade-on-tag.yamlworkflow andscripts/cascade/{discover.sh,bump-and-pr.sh,bump-strategy.sh,check_constraint.py}replace the previouscloudbuild-cascade.yamlmechanism (scripts/build-filters.shremoved in the same change). Fires on release-semver tag push, discovers eligiblefilter-*consumers via the GitHub Contents API + PEP 621 specifier intersection, opens mechanical bump PRs throughPlainsightAI/gh-actions-public/open-mechanical-pr. Auto-merge enabled by default on bot PRs;workflow_dispatchinputs (dry_run,single_filter,filter_subset,auto_merge_override) support staged rollouts. (#85) - Cascade widens consumer pyproject upper bounds when target excludes them:
bump-strategy.shnow rewrites<X/<=Xupper bounds in consumer pyproject pins when the target openfilter version would otherwise be excluded. Rule: 0.X targets widen to next-minor; 1.0+ targets widen to next-major. Without this, every consumer pinning>=0.1.30,<0.2.0(the org-canonical pattern) would skip the cascade for any minor/major openfilter bump — 30 of 55filter-*repos in the current sweep. Lower-bound exclusions (>=X) and!=Xexclusions still skip per the prior behavior. (#93)
Release v0.1.30
Added
- Token authentication and configurable CORS for HTTP filters: Webvis and REST filters now support opt-in token-based auth (
auth_token/FILTER_AUTH_TOKEN) and configurable CORS origins (cors_origins/FILTER_CORS_ORIGINS). Auth accepts?token=query params (for<img>MJPEG embeds) orAuthorization: Bearerheaders. CORS preflight bypasses auth. Both are fully backwards compatible — unset means no auth andAccess-Control-Allow-Origin: *.
Changed
- Shared security scan workflow: Replaced standalone Grype security scan with the shared
PlainsightAI/gh-actions-publicreusable workflow. - GitHub Actions bumped to latest versions:
actions/checkoutv4→v6,actions/setup-pythonv5→v6,actions/upload-artifactv4→v7,actions/download-artifactv4.1.3→v8,docker/setup-buildx-actionv3→v4,docker/login-actionv3→v4,docker/build-push-actionv5→v6,dorny/paths-filterv3→v4,mukunku/tag-exists-actionv1.6.0→v1.7.0. Resolves Node.js <24 deprecation warnings. - Docker images now include SLSA provenance and SBOM attestations via
docker/build-push-actionv6 defaults.
Fixed
- CVE-2026-25645: Bumped
requestsfrom ~=2.32.5 to ~=2.33.0. - GHSA-8rrh-rw8j-w5fx: Bumped
wheelfrom ~=0.45.1 to ~=0.46.2. - GHSA-cxww-7g56-2vh6: Bumped
actions/download-artifactfrom v4.1.3 to v8. - Docker image build race condition: Added a
wait-for-pypistep to the release workflow that polls PyPI until the newly published package is indexed before starting Docker image builds. Previously, some images could fail withNo matching distribution foundif PyPI index propagation hadn't completed.
Release v0.1.29
Added
- Frame accumulation support for batched processing: Filters can now set
batch_size > 1to accumulate frames and process them in batches viaprocess_batch(). Includes timeout-based flushing, proper locking, and backward compatibility with single-frameprocess(). - Single batch watcher thread: Replaced per-timeout
threading.Timerwith a single long-lived daemon thread for batch flush monitoring, reducing thread churn in high-throughput filters.
Release v0.1.28
Changed
- Framework-agnostic GPU detection via ctypes: Replaced
torch.cudaandnvidia-smisubprocess calls with directctypes.CDLLprobing of CUDA/NVML shared libraries. Filters no longer need PyTorch installed just for GPU detection.
Release v0.1.27
Fixed
- Remove eager imports from
filters/__init__.py: The package-level__init__.pyeagerly importedVideoOut,ImageOut, andImageIn, which pulled in optional dependencies like PyAV (av). This crashed containers that only needed a subset of filters (e.g.,video-incontainers that don't haveavinstalled). Since no code uses package-level imports (all consumers import directly from submodules), the re-exports were removed entirely.
Release v0.1.26
Added
OPENFILTER_APPEND_LD_LIBRARY_PATHandOPENFILTER_APPEND_PATHenv vars: Read at filter startup (before torch import) and appended to the existingLD_LIBRARY_PATH/PATHvalues. Allows Kubernetes controllers to inject GPU driver paths without overriding container-image-baked paths. On GKE, the NVIDIA device plugin mounts drivers but does not setLD_LIBRARY_PATH; the pipelines controller can now inject the paths automatically via these variables.