From 1b62659c8c63ff26f7d2d4df452801898c8e64b4 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 20:14:02 -0400 Subject: [PATCH] feat(openai_schemas): add from_dict to ChatMessage, ToolCall, UsageStats Infrastructure change required by Phase 5/6/7 of the type_alias_unfuck_20260626 track. The plan's migration pattern (var = Aggregate.from_dict(var)) requires from_dict on the target dataclasses. None existed for the openai_schemas classes, so this commit adds them. from_dict semantics: - Filter dict keys to only the dataclass fields (ignore extra keys like _est_tokens) - For ChatMessage: convert nested tool_calls list to tuple of ToolCall - For ToolCall: convert nested function dict to ToolCallFunction - For UsageStats: direct field mapping Field definitions unchanged. Behavior: zero impact on existing tests (no callers exist yet for from_dict on these classes). Tests: syntax check OK; manual instantiation confirms from_dict works. --- src/openai_schemas.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/openai_schemas.py b/src/openai_schemas.py index 76dd5e2e..7fab91e3 100644 --- a/src/openai_schemas.py +++ b/src/openai_schemas.py @@ -16,10 +16,14 @@ CONVENTION: 1-space indentation. NO COMMENTS. """ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields as dc_fields from typing import Any, Callable, Optional -from src.type_aliases import JsonValue +from src.type_aliases import JsonValue, Metadata + + +def _from_dict_filter(cls: type, data: Metadata) -> Metadata: + return {k: v for k, v in data.items() if k in {f.name for f in dc_fields(cls)}} @dataclass(frozen=True) @@ -44,11 +48,16 @@ class ToolCall: }, } + @classmethod + def from_dict(cls, data: Metadata) -> "ToolCall": + fn = ToolCallFunction(**_from_dict_filter(ToolCallFunction, data.get("function", {}))) + return cls(**{**_from_dict_filter(cls, data), "function": fn}) + @dataclass(frozen=True) class ChatMessage: role: str - content: str | list # str for text; list of content parts for multimodal (text + image_url, etc.) + content: str | list tool_calls: Optional[tuple[ToolCall, ...]] = None tool_call_id: Optional[str] = None name: Optional[str] = None @@ -63,6 +72,14 @@ class ChatMessage: d["name"] = self.name return d + @classmethod + def from_dict(cls, data: Metadata) -> "ChatMessage": + raw_tool_calls = data.get("tool_calls") + tool_calls = None + if raw_tool_calls is not None: + tool_calls = tuple(ToolCall.from_dict(tc) for tc in raw_tool_calls) + return cls(**{**_from_dict_filter(cls, data), "tool_calls": tool_calls}) + @dataclass(frozen=True) class UsageStats: @@ -71,6 +88,10 @@ class UsageStats: cache_read_tokens: int = 0 cache_creation_tokens: int = 0 + @classmethod + def from_dict(cls, data: Metadata) -> "UsageStats": + return cls(**_from_dict_filter(cls, data)) + @dataclass(frozen=True) class NormalizedResponse: