Coverage for src / lexigram / contracts / ai / relay / types.py: 6%

62 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-19 05:41 +0800

1"""Shared types for the relay protocol conversion engine. 

2 

3This module owns the stable enums, JSON aliases, usage accounting, loss 

4records, conversion results, and payload unions that cross every relay 

5wire format. Concrete wire DTOs live in ``relay.dto``; the canonical IR 

6lives in ``relay.ir``; engine-side services live in the 

7``lexigram-ai-relay`` extension package. 

8""" 

9 

10from __future__ import annotations 

11 

12from dataclasses import dataclass, field 

13from enum import StrEnum 

14from typing import Any, Generic, TypeAlias, TypeVar 

15 

16from lexigram.contracts.ai.llm import TokenUsage 

17from lexigram.contracts.ai.relay.dto import ( 

18 ClaudeRequest, 

19 ClaudeResponse, 

20 GeminiRequest, 

21 GeminiResponse, 

22 OpenAIChatRequest, 

23 OpenAIChatResponse, 

24 ResponsesRequest, 

25 ResponsesResponse, 

26) 

27 

28__all__ = [ 

29 "ConversionQuality", 

30 "JsonObject", 

31 "JsonValue", 

32 "RelayConvertResult", 

33 "RelayFormat", 

34 "RelayLoss", 

35 "RelayRequestPayload", 

36 "RelayResponsePayload", 

37 "RelayUsage", 

38] 

39 

40 

41class RelayFormat(StrEnum): 

42 """The four text wire protocols supported by the relay engine. 

43 

44 Attributes: 

45 OPENAI_CHAT: OpenAI Chat Completions (``/v1/chat/completions``). 

46 OPENAI_RESPONSES: OpenAI Responses (``/v1/responses``). 

47 CLAUDE: Anthropic Messages (``/v1/messages``). 

48 GEMINI: Google Gemini ``generateContent``. 

49 """ 

50 

51 OPENAI_CHAT = "openai_chat" 

52 OPENAI_RESPONSES = "openai_responses" 

53 CLAUDE = "claude" 

54 GEMINI = "gemini" 

55 

56 

57class ConversionQuality(StrEnum): 

58 """Semantic closeness between two wire protocols. 

59 

60 Attributes: 

61 GOOD: Core structures are close; lossless in practice. 

62 FAIR: Main capabilities convert, some features adapt or drop. 

63 DISCOURAGED: Requires a multi-hop path; higher semantic-loss risk. 

64 """ 

65 

66 GOOD = "GOOD" 

67 FAIR = "FAIR" 

68 DISCOURAGED = "DISCOURAGED" 

69 

70 

71JsonValue: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None 

72"""A JSON-compatible value.""" 

73 

74JsonObject: TypeAlias = dict[str, JsonValue] 

75"""A JSON object (wire request/response fragment).""" 

76 

77 

78@dataclass(frozen=True) 

79class RelayUsage: 

80 """Unified token usage, normalized across upstream response formats. 

81 

82 Attributes: 

83 prompt_tokens: Input tokens (total, all subcategories). 

84 completion_tokens: Output tokens (total, all subcategories). 

85 cache_read_tokens: Cached input tokens (Claude ``cache_read``, OpenAI ``cached_tokens``). 

86 cache_creation_tokens: Cache-creation input tokens (Claude only). 

87 reasoning_tokens: Output tokens spent on reasoning (Claude/Gemini thinking). 

88 audio_input_tokens: Audio input tokens (OpenAI audio models). 

89 audio_output_tokens: Audio output tokens (OpenAI audio models). 

90 image_tokens: Image input tokens (OpenAI image models). 

91 input_tokens: Responses-style input count carried by the source 

92 (Claude stamps prompt+cache; Gemini leaves it zero). 

93 output_tokens: Responses-style output count carried by the source 

94 (only the OpenAI response format stamps it). 

95 total_tokens_override: Explicit total when the source reports one 

96 that is not the sum of prompt and completion (Gemini counts 

97 thinking tokens inside both). 

98 """ 

99 

100 prompt_tokens: int = 0 

101 completion_tokens: int = 0 

102 cache_read_tokens: int = 0 

103 cache_creation_tokens: int = 0 

104 reasoning_tokens: int = 0 

105 audio_input_tokens: int = 0 

106 audio_output_tokens: int = 0 

107 image_tokens: int = 0 

108 input_tokens: int = 0 

109 output_tokens: int = 0 

110 total_tokens_override: int | None = None 

111 

112 @property 

113 def total_tokens(self) -> int: 

114 """Total tokens consumed (prompt + completion or explicit override).""" 

115 if self.total_tokens_override is not None: 

116 return self.total_tokens_override 

117 return self.prompt_tokens + self.completion_tokens 

118 

119 def to_token_usage(self) -> TokenUsage: 

120 """Map to the shared ``TokenUsage`` without double counting. 

121 

122 Returns: 

123 A ``TokenUsage`` with the derived total; detailed sub-category 

124 counts remain available on this object. 

125 """ 

126 return TokenUsage( 

127 prompt_tokens=self.prompt_tokens, 

128 completion_tokens=self.completion_tokens, 

129 total_tokens=self.total_tokens, 

130 ) 

131 

132 

133@dataclass(frozen=True) 

134class RelayLoss: 

135 """A semantic loss recorded during conversion. 

136 

137 Attributes: 

138 field: Source wire field (or feature) that was dropped or adapted. 

139 target: Target format the loss applies to. 

140 reason: Machine-readable reason (e.g. ``json_mode_not_supported``). 

141 severity: ``error``, ``warning``, or ``info``. 

142 """ 

143 

144 field: str 

145 target: RelayFormat 

146 reason: str 

147 severity: str = "warning" 

148 

149 

150T = TypeVar("T") 

151 

152 

153@dataclass(frozen=True) 

154class RelayConvertResult(Generic[T]): 

155 """Outcome of a conversion with audit metadata. 

156 

157 Attributes: 

158 value: The converted object. 

159 source: Source wire format. 

160 target: Target wire format. 

161 converter_id: Converter identifier (``"<src>_to_<dst>"``). 

162 quality: Semantic closeness of the conversion. 

163 steps: Actual conversion path taken (source, canonical_ir, target, 

164 plus named adaptations when present). 

165 usage: Normalized usage when converting responses; ``None`` otherwise. 

166 losses: Semantic losses recorded during conversion. 

167 warnings: Human-readable compatibility notes. 

168 """ 

169 

170 value: T 

171 source: RelayFormat 

172 target: RelayFormat 

173 converter_id: str 

174 quality: ConversionQuality 

175 steps: tuple[str, ...] = field(default_factory=tuple) 

176 usage: RelayUsage | None = None 

177 losses: tuple[RelayLoss, ...] = field(default_factory=tuple) 

178 warnings: tuple[str, ...] = field(default_factory=tuple) 

179 

180 

181RelayRequestPayload: TypeAlias = ( 

182 OpenAIChatRequest | ResponsesRequest | ClaudeRequest | GeminiRequest 

183) 

184"""Union of every request wire DTO the relay engine accepts.""" 

185 

186RelayResponsePayload: TypeAlias = ( 

187 OpenAIChatResponse | ResponsesResponse | ClaudeResponse | GeminiResponse 

188) 

189"""Union of every non-stream response wire DTO the relay engine emits."""