Skip to content

feat: Dynamic callback serialization via _callback_type marker#54

Open
acere wants to merge 9 commits intoawslabs:mainfrom
acere:feature/callback-serialization
Open

feat: Dynamic callback serialization via _callback_type marker#54
acere wants to merge 9 commits intoawslabs:mainfrom
acere:feature/callback-serialization

Conversation

@acere
Copy link
Copy Markdown
Collaborator

@acere acere commented Mar 31, 2026

Summary

Implements dynamic serialization/deserialization for Callback objects using a _callback_type marker (module:ClassName format), enabling any Callback subclass — built-in or third-party — to round-trip through JSON without a hardcoded registry.

Closes #53

Changes

  • Callback base class (callbacks/base.py): Added to_dict(), from_dict(), to_json(), from_json() methods. to_dict() emits a _callback_type key with the fully-qualified class path. from_dict() dynamically imports and instantiates the correct subclass, delegating to overridden from_dict() when present (e.g. CostModel). Replaced the save_to_file() / load_from_file() NotImplementedError stubs with working implementations.

  • CostModel (callbacks/cost/model.py): to_dict() now injects _callback_type alongside the existing _type from JSONableBase. from_dict() strips both markers before delegating to the constructor.

  • MlflowCallback (callbacks/mlflow.py): Added to_dict() / from_dict() overrides, replacing the old NotImplementedError stubs.

  • _RunConfig (runner.py): save() now serializes callbacks via cb.to_dict(). load() reconstructs them via Callback.from_dict().

  • Tests: Rewrote tests/unit/callbacks/test_base.py with round-trip serialization tests. Updated test_cost_model_serialization to expect the new _callback_type key.

Design

The approach mirrors the existing Endpoint serialization pattern (endpoint_type field + importlib), but uses fully-qualified module:ClassName paths instead of bare class names. This means third-party callbacks from external packages work automatically — no central registry needed.

acere added 9 commits March 30, 2026 09:58
Addresses awslabs#20 by implementing binary-safe serialization for payloads
and results containing images. This prevents double base64 encoding
and significantly reduces memory usage and serialization time.

Changes:
- Add binary serialization support to prompt_utils and results
- Update endpoints to use binary-safe serialization
- Add property-based tests for serialization correctness
- Update integration tests to verify image handling
…s endpoints

- Add multi-modal content handling (images, videos, audio, documents) to BedrockConverse, OpenAI, and SageMaker endpoints
- Implement automatic format detection using puremagic with fallback to file extensions
- Add multimodal utility functions for format conversion and content serialization
- Support both file paths and raw bytes for multi-modal content
- Add endpoint-specific format string handling (Bedrock short format, OpenAI MIME types)
- Implement comprehensive unit tests for multi-modal serialization and properties across all endpoints
- Add detailed README documentation with usage examples and security warnings for format detection
- Fix Path serialization in JSONableBase to handle os.PathLike objects
- Update pyproject.toml with optional multimodal extra for puremagic dependency
- Improve integration tests with multi-modal payload examples
- Enhance prompt utilities with multi-modal content handling
… compatibility

- Replace built-in open() calls with UPath.open() across all file operations
- Add UPath import to cost model for consistent path handling
- Update cost model save_to_file() to create parent directories automatically
- Standardize file reading in prompt_utils, results, runner, and tokenizers modules
- Improves cross-platform file system support and enables cloud storage integration
Result.stats included raw datetime objects (start_time, end_time)
from to_dict(), causing TypeError when users called json.dumps()
without a custom serializer. Now to_dict() converts datetime fields
via utc_datetime_serializer so stats is always directly serializable.
…path helper

Replace scattered `os.PathLike | str` type annotations with the proper
UPath type aliases from `upath.types`:
- `ReadablePathLike` for parameters used to load/read data
- `WritablePathLike` for parameters used to save/write data

Add `ensure_path()` utility to `llmeter.utils` to centralize the
`Path(x)` normalization boilerplate that was duplicated at the top of
nearly every function accepting a path argument. The helper handles
None passthrough and uses a lazy UPath import.

Runtime `isinstance(obj, os.PathLike)` checks in serialization helpers
are left unchanged since TypeAliases cannot be used for runtime checks.
…nstances

Cloud-backed UPath instances (e.g. S3Path) do not implement os.PathLike,
so isinstance checks against os.PathLike alone would miss them. This caused:

- Serialization: cloud UPaths skipped the path branch in JSON serializers
- runner.py: cloud UPath payloads not recognized as path references,
  leading to unnecessary re-saving or failure to load from path

Fix by checking isinstance(obj, (os.PathLike, Path)) where Path is UPath,
which catches both plain pathlib.Path (via os.PathLike) and cloud UPaths
(via UPath). Serialization keeps .as_posix() for cross-platform safety.
Rationalize scattered serialization utilities into a single unified module:

- Create llmeter/json_utils.py with LLMeterEncoder (handles bytes, datetime,
  date, time, PathLike, to_dict() objects, str() fallback) and
  llmeter_bytes_decoder (restores __llmeter_bytes__ markers to bytes).

- Remove redundant encoders: LLMeterBytesEncoder (prompt_utils),
  InvocationResponseEncoder (results), utc_datetime_serializer (results),
  and inline _default_serializer lambdas (runner, endpoints/base).

- Slim down callbacks/cost/serde.py to cost-specific helpers (JSONableBase,
  ISerializable, from_dict_with_class, from_dict_with_class_map,
  to_dict_recursive_generic). Update to Python 3.10 typing (dict/type
  builtins instead of Dict/Type).

- Standardize all to_json() methods to default to cls=LLMeterEncoder via
  kwargs.setdefault(), ensuring consistent encoding across InvocationResponse,
  Result, and JSONableBase.

- Remove serializer/deserializer/cls customization params from save_payloads,
  load_payloads, _load_data_file — hardcode LLMeterEncoder and
  llmeter_bytes_decoder since custom encoders produce files that can't be
  loaded back without metadata.

- LLMeterEncoder.default() delegates to to_dict() for objects that implement
  it, enabling json.dump(self, f, cls=LLMeterEncoder) without manual
  to_dict() calls (used in Endpoint.save).

- Convert all changed files to relative imports, run ruff check + format +
  import sorting.

- Clean up llmeter/utils.py: move upath imports to top level (it's a hard
  dependency), remove unnecessary from __future__ import annotations.

- Add docs/reference/json_utils.md and mkdocs.yml nav entry.

- Add property-based tests for datetime, date, time, PathLike, and to_dict()
  encoding (TestDatetimeSerializationProperties, TestPathSerializationProperties,
  TestToDictSerializationProperties).

- Update existing tests to use LLMeterEncoder/llmeter_bytes_decoder from
  llmeter.json_utils instead of old aliases.

All 581 unit tests pass.
Move path and datetime string conversion out of to_dict() and
to_dict_recursive_generic() — these are Python dict builders, not
serializers. Type coercion to strings is now exclusively handled by
LLMeterEncoder at JSON serialization time.

- Endpoint.to_dict(): remove PathLike → as_posix() coercion, simplify
  to a dict comprehension. Remove unused os import.
- to_dict_recursive_generic(): remove PathLike → as_posix() and
  datetime/date/time → isoformat() coercions. Keep structural recursion
  (nested dicts, lists, to_dict() delegation). Remove unused os, datetime
  imports.
Implement to_dict/from_dict on Callback base class using a
"module:ClassName" type marker (_callback_type) for dynamic dispatch.
Any Callback subclass — built-in or third-party — can now round-trip
through JSON without a hardcoded registry.

- Callback.to_dict() emits _callback_type with fully-qualified class path
- Callback.from_dict() dynamically imports and instantiates the correct
  subclass, delegating to overridden from_dict when present
- CostModel.to_dict() injects _callback_type alongside existing _type
- CostModel.from_dict() strips both markers before construction
- MlflowCallback gains to_dict/from_dict (replaces NotImplementedError stubs)
- _RunConfig.save()/load() now serialize and restore callbacks
- Replace old Callback base tests with serialization round-trip tests
@acere acere requested a review from athewsey March 31, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Dynamic callback serialization mechanism

1 participant