Coverage for src / lexigram / contracts / ai / relay / ir.py: 19%

85 statements  

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

1"""Canonical intermediate representation for relay conversion. 

2 

3Every wire protocol maps into this IR and back. The IR reuses 

4``ChatMessage`` / ``ToolCall`` / ``ToolDefinition`` / ``ThinkingConfig`` 

5/ ``ThinkingResult`` from ``lexigram.contracts.ai`` so downstream 

6packages never see protocol-specific shapes. 

7""" 

8 

9from __future__ import annotations 

10 

11from dataclasses import dataclass, field 

12from typing import Any 

13 

14from lexigram.contracts.ai.agents import ToolDefinition 

15from lexigram.contracts.ai.llm import ChatMessage, ToolCall 

16from lexigram.contracts.ai.relay.types import RelayFormat, RelayUsage 

17from lexigram.contracts.ai.thinking import ThinkingConfig, ThinkingResult 

18 

19__all__ = [ 

20 "RelayRequest", 

21 "RelayResponse", 

22 "StreamDelta", 

23 "StreamState", 

24 "normalize_finish_reason", 

25] 

26 

27 

28def normalize_finish_reason(raw: str | None) -> str | None: 

29 """Normalize a wire finish reason to the canonical set. 

30 

31 Canonical values are ``stop``, ``length``, ``tool_calls``, 

32 ``function_call``, ``content_filter``, and ``other``. ``None`` and 

33 empty strings pass through as ``None``; unrecognized values map to 

34 ``other``. 

35 

36 Args: 

37 raw: Raw finish reason from any wire format (e.g. ``end_turn``, 

38 ``STOP``, ``max_tokens``, ``tool_use``, ``safety``). 

39 

40 Returns: 

41 The canonical finish reason, or ``None`` when absent. 

42 """ 

43 if not raw: 

44 return None 

45 normalized = raw.strip().lower() 

46 if normalized in {"stop", "end_turn", "stop_sequence", "completed"}: 

47 return "stop" 

48 if normalized in {"length", "max_tokens"}: 

49 return "length" 

50 if normalized in {"tool_calls", "tool_use"}: 

51 return "tool_calls" 

52 if normalized in {"function_call", "malformed_function_call"}: 

53 return "function_call" 

54 if normalized in { 

55 "content_filter", 

56 "safety", 

57 "recitation", 

58 "prohibited_content", 

59 "blocklist", 

60 "spii", 

61 "image_safety", 

62 }: 

63 return "content_filter" 

64 if normalized in {"other", "model_finish_reason_unspecified", "error"}: 

65 return "other" 

66 return "other" 

67 

68 

69@dataclass(frozen=True) 

70class RelayRequest: 

71 """Format-agnostic chat request. 

72 

73 Attributes: 

74 model: Model name as the upstream should see it (already mapped). 

75 messages: Chat messages in the canonical shape. 

76 system: Explicit system text (protocols that separate it). 

77 tools: Tool definitions, or empty list when none. 

78 tool_choice: Tool selection policy (``"auto"``, ``"none"``, or a 

79 protocol-specific dict), or ``None`` when unset. 

80 temperature: Sampling temperature, or ``None`` when unset. 

81 top_p: Nucleus sampling probability, or ``None`` when unset. 

82 top_k: Top-k sampling count, or ``None`` when unset. 

83 max_tokens: Max output tokens, or ``None`` when unset. 

84 stop_sequences: Stop strings, or empty list when none. 

85 response_format: Structured output request (e.g. 

86 ``{"type": "json_object"}``), or ``None``. 

87 stream: Whether the caller expects a streamed response. 

88 include_usage: Request usage in stream chunks. 

89 parallel_tool_calls: Whether parallel tool calls are allowed, 

90 or ``None`` when the protocol has no such knob. 

91 thinking: Thinking/reasoning config, or ``None``. 

92 metadata: Protocol-specific passthrough key/value pairs. 

93 passthrough: Unknown request fields preserved verbatim. 

94 """ 

95 

96 model: str 

97 messages: list[ChatMessage] 

98 system: str | None = None 

99 tools: list[ToolDefinition] = field(default_factory=list) 

100 tool_choice: str | dict[str, Any] | None = None 

101 temperature: float | None = None 

102 top_p: float | None = None 

103 top_k: int | None = None 

104 max_tokens: int | None = None 

105 stop_sequences: list[str] = field(default_factory=list) 

106 response_format: dict[str, Any] | None = None 

107 stream: bool = False 

108 include_usage: bool = False 

109 parallel_tool_calls: bool | None = None 

110 thinking: ThinkingConfig | None = None 

111 metadata: dict[str, Any] = field(default_factory=dict) 

112 passthrough: dict[str, Any] = field(default_factory=dict) 

113 

114 

115@dataclass(frozen=True) 

116class RelayResponse: 

117 """Format-agnostic non-streamed response. 

118 

119 Attributes: 

120 id: Upstream response id, or ``None`` when absent. 

121 model: Model name reported by the upstream. 

122 created: Epoch seconds the response was created, or ``None``. 

123 content: Generated text (empty string when the turn is tool-only). 

124 thinking: Reasoning output, or ``None``. 

125 tool_calls: Tool calls requested by the model, or empty list. 

126 tool_results: Tool results carried by the response payload as 

127 canonical ``role="tool"`` messages, or empty list. 

128 finish_reason: Normalized finish reason (``stop``, ``length``, 

129 ``tool_calls``, ``content_filter``, ``function_call``, 

130 ``other``), or ``None``. 

131 status: Upstream response status (e.g. ``completed``, 

132 ``incomplete``, ``failed``), or ``None``. 

133 incomplete_details: Reason the response was cut short (e.g. 

134 ``{"reason": "max_output_tokens"}``), or ``None``. 

135 usage: Normalized usage, or ``None`` when the upstream omitted it. 

136 passthrough: Unknown response fields preserved verbatim. 

137 """ 

138 

139 model: str 

140 id: str | None = None 

141 created: int | None = None 

142 content: str = "" 

143 thinking: ThinkingResult | None = None 

144 tool_calls: list[ToolCall] = field(default_factory=list) 

145 tool_results: list[ChatMessage] = field(default_factory=list) 

146 finish_reason: str | None = None 

147 status: str | None = None 

148 incomplete_details: dict[str, Any] | None = None 

149 usage: RelayUsage | None = None 

150 passthrough: dict[str, Any] = field(default_factory=dict) 

151 

152 

153@dataclass(frozen=True) 

154class StreamDelta: 

155 """One logical stream update in canonical form. 

156 

157 Attributes: 

158 kind: Event kind (``content``, ``role``, ``tool_call``, 

159 ``finish``, ``usage``, ``thinking``, ``status``). 

160 content: Text delta, or ``None`` when this delta is not text. 

161 thinking_delta: Reasoning text delta, or ``None``. 

162 is_thinking: Whether the delta belongs to a thinking block. 

163 tool_call_index: Index of the tool call this delta updates, 

164 or ``None``. 

165 tool_call_id: Tool call id fragment, or ``None``. 

166 tool_call_name: Tool call name fragment, or ``None``. 

167 tool_call_arguments: Partial JSON argument text, or ``None``. 

168 block_index: Claude content block index, or ``None``. 

169 output_index: OpenAI Responses output item index, or ``None``. 

170 finish_reason: Terminal finish reason for the whole stream, or ``None``. 

171 status: Target status value (e.g. ``in_progress``, ``completed``), 

172 or ``None``. 

173 usage: Final usage for the whole stream, or ``None`` (usually last chunk). 

174 role: Role announcement delta (``assistant``), or ``None``. 

175 passthrough: Unknown event fields preserved verbatim. 

176 """ 

177 

178 kind: str = "content" 

179 content: str | None = None 

180 thinking_delta: str | None = None 

181 is_thinking: bool = False 

182 tool_call_index: int | None = None 

183 tool_call_id: str | None = None 

184 tool_call_name: str | None = None 

185 tool_call_arguments: str | None = None 

186 block_index: int | None = None 

187 output_index: int | None = None 

188 finish_reason: str | None = None 

189 status: str | None = None 

190 usage: RelayUsage | None = None 

191 role: str | None = None 

192 passthrough: dict[str, Any] = field(default_factory=dict) 

193 

194 

195@dataclass(frozen=True) 

196class StreamState: 

197 """Immutable stream descriptor; one instance per upstream stream. 

198 

199 Attributes: 

200 source: Upstream wire format. 

201 target: Downstream wire format. 

202 model: Model name to stamp on emitted chunks. 

203 include_usage: Whether to emit a final usage event. 

204 tool_calls: Accumulated tool calls across chunks. 

205 thinking_signatures: Claude thinking signatures, in order. 

206 is_done: Whether the stream has been finalized. 

207 usage: Usage accumulated from upstream chunks. 

208 """ 

209 

210 source: RelayFormat 

211 target: RelayFormat 

212 model: str 

213 include_usage: bool = False 

214 tool_calls: list[ToolCall] = field(default_factory=list) 

215 thinking_signatures: list[str] = field(default_factory=list) 

216 is_done: bool = False 

217 usage: RelayUsage | None = None