Skip to content

Commit 11241df

Browse files
Hureruclaude
andcommitted
feat(transformer): add StreamEvent model and infrastructure
- Add StreamEvent types with 13 event kinds (message_start, text_delta, thinking_delta, tool_call_*, usage_delta, etc.) - Add ProviderExtensions for preserving provider-specific metadata - Add StreamAggregator for metrics collection - Add bidirectional conversion helpers between StreamEvent and InternalLLMResponse - Add OutboundStreamEventTransformer and InboundStreamEventTransformer interfaces - Add comprehensive roundtrip tests This establishes the foundation for migrating streaming paths from chunk-based to event-based architecture, preserving full semantic fidelity for thinking, signatures, tool calls, and usage across all providers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9ad20d7 commit 11241df

7 files changed

Lines changed: 1220 additions & 20 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package model
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
)
7+
8+
type ProviderExtensionNamespace string
9+
10+
const (
11+
ProviderExtensionNamespaceCommon ProviderExtensionNamespace = "common"
12+
ProviderExtensionNamespaceAnthropic ProviderExtensionNamespace = "anthropic"
13+
ProviderExtensionNamespaceGemini ProviderExtensionNamespace = "gemini"
14+
ProviderExtensionNamespaceOpenAI ProviderExtensionNamespace = "openai"
15+
ProviderExtensionNamespaceVolcengine ProviderExtensionNamespace = "volcengine"
16+
)
17+
18+
type OctopusExtension struct {
19+
ProviderExtensions *ProviderExtensions `json:"provider_extensions,omitempty"`
20+
}
21+
22+
type ProviderExtensions struct {
23+
Common *CommonExtension `json:"common,omitempty"`
24+
Anthropic *AnthropicExtension `json:"anthropic,omitempty"`
25+
Gemini *GeminiExtension `json:"gemini,omitempty"`
26+
OpenAI *OpenAIExtension `json:"openai,omitempty"`
27+
Volcengine *VolcengineExtension `json:"volcengine,omitempty"`
28+
}
29+
30+
type CommonExtension struct {
31+
Raw json.RawMessage `json:"raw,omitempty"`
32+
}
33+
34+
type AnthropicExtension struct {
35+
Beta []string `json:"beta,omitempty"`
36+
CacheControl *CacheControl `json:"cache_control,omitempty"`
37+
MCPServers json.RawMessage `json:"mcp_servers,omitempty"`
38+
Container json.RawMessage `json:"container,omitempty"`
39+
ServerTool json.RawMessage `json:"server_tool,omitempty"`
40+
}
41+
42+
type GeminiExtension struct {
43+
ThoughtSignature string `json:"thought_signature,omitempty"`
44+
CachedContentRef *string `json:"cached_content_ref,omitempty"`
45+
SpeechConfig json.RawMessage `json:"speech_config,omitempty"`
46+
}
47+
48+
type OpenAIExtension struct {
49+
ResponsesPassthroughRequired bool `json:"responses_passthrough_required,omitempty"`
50+
ResponsesPassthroughReason string `json:"responses_passthrough_reason,omitempty"`
51+
RawResponseItems json.RawMessage `json:"raw_response_items,omitempty"`
52+
}
53+
54+
type VolcengineExtension struct {
55+
Raw json.RawMessage `json:"raw,omitempty"`
56+
}
57+
58+
func GeminiThoughtSignatureExtension(signature string) *OctopusExtension {
59+
signature = strings.TrimSpace(signature)
60+
if signature == "" {
61+
return nil
62+
}
63+
return &OctopusExtension{
64+
ProviderExtensions: &ProviderExtensions{
65+
Gemini: &GeminiExtension{ThoughtSignature: signature},
66+
},
67+
}
68+
}
69+
70+
func GeminiThoughtSignatureFromExtension(ext *OctopusExtension) string {
71+
if ext == nil || ext.ProviderExtensions == nil || ext.ProviderExtensions.Gemini == nil {
72+
return ""
73+
}
74+
return strings.TrimSpace(ext.ProviderExtensions.Gemini.ThoughtSignature)
75+
}
76+
77+
func (r *InternalLLMRequest) GetGeminiExtensions() GeminiExtension {
78+
if r == nil {
79+
return GeminiExtension{}
80+
}
81+
ext := GeminiExtension{
82+
CachedContentRef: r.GeminiCachedContentRef,
83+
SpeechConfig: r.GeminiSpeechConfig,
84+
}
85+
if r.ProviderExtensions != nil && r.ProviderExtensions.Gemini != nil {
86+
mergeGeminiExtension(&ext, r.ProviderExtensions.Gemini)
87+
}
88+
return ext
89+
}
90+
91+
func (r *InternalLLMRequest) GetAnthropicExtensions() AnthropicExtension {
92+
if r == nil {
93+
return AnthropicExtension{}
94+
}
95+
ext := AnthropicExtension{
96+
MCPServers: r.AnthropicMCPServers,
97+
Container: r.AnthropicContainer,
98+
}
99+
if r.ProviderExtensions != nil && r.ProviderExtensions.Anthropic != nil {
100+
mergeAnthropicExtension(&ext, r.ProviderExtensions.Anthropic)
101+
}
102+
return ext
103+
}
104+
105+
func (r *InternalLLMRequest) GetOpenAIExtensions() OpenAIExtension {
106+
if r == nil {
107+
return OpenAIExtension{}
108+
}
109+
ext := OpenAIExtension{
110+
RawResponseItems: r.RawInputItems,
111+
}
112+
if r.RequiresOpenAIResponsesPassthrough() {
113+
ext.ResponsesPassthroughRequired = true
114+
ext.ResponsesPassthroughReason = r.OpenAIResponsesPassthroughReasonText()
115+
}
116+
if r.ProviderExtensions != nil && r.ProviderExtensions.OpenAI != nil {
117+
mergeOpenAIExtension(&ext, r.ProviderExtensions.OpenAI)
118+
}
119+
return ext
120+
}
121+
122+
func (m *Message) GetAnthropicExtensions() AnthropicExtension {
123+
if m == nil || m.ProviderExtensions == nil || m.ProviderExtensions.Anthropic == nil {
124+
return AnthropicExtension{}
125+
}
126+
return *m.ProviderExtensions.Anthropic
127+
}
128+
129+
func (p *MessageContentPart) GetAnthropicExtensions() AnthropicExtension {
130+
if p == nil || p.ProviderExtensions == nil || p.ProviderExtensions.Anthropic == nil {
131+
return AnthropicExtension{}
132+
}
133+
return *p.ProviderExtensions.Anthropic
134+
}
135+
136+
func (tc *ToolCall) GetGeminiExtensions() GeminiExtension {
137+
ext := GeminiExtension{}
138+
if tc == nil {
139+
return ext
140+
}
141+
if tc.ThoughtSignature != "" {
142+
ext.ThoughtSignature = tc.ThoughtSignature
143+
}
144+
if tc.ProviderExtensions != nil && tc.ProviderExtensions.Gemini != nil {
145+
mergeGeminiExtension(&ext, tc.ProviderExtensions.Gemini)
146+
}
147+
return ext
148+
}
149+
150+
func (tc *ToolCall) GetAnthropicExtensions() AnthropicExtension {
151+
if tc == nil || tc.ProviderExtensions == nil || tc.ProviderExtensions.Anthropic == nil {
152+
return AnthropicExtension{}
153+
}
154+
return *tc.ProviderExtensions.Anthropic
155+
}
156+
157+
func mergeGeminiExtension(dst *GeminiExtension, src *GeminiExtension) {
158+
if dst == nil || src == nil {
159+
return
160+
}
161+
if sig := strings.TrimSpace(src.ThoughtSignature); sig != "" {
162+
dst.ThoughtSignature = sig
163+
}
164+
if src.CachedContentRef != nil {
165+
dst.CachedContentRef = src.CachedContentRef
166+
}
167+
if len(src.SpeechConfig) > 0 {
168+
dst.SpeechConfig = src.SpeechConfig
169+
}
170+
}
171+
172+
func mergeAnthropicExtension(dst *AnthropicExtension, src *AnthropicExtension) {
173+
if dst == nil || src == nil {
174+
return
175+
}
176+
if len(src.Beta) > 0 {
177+
dst.Beta = append(dst.Beta[:0], src.Beta...)
178+
}
179+
if src.CacheControl != nil {
180+
dst.CacheControl = src.CacheControl
181+
}
182+
if len(src.MCPServers) > 0 {
183+
dst.MCPServers = src.MCPServers
184+
}
185+
if len(src.Container) > 0 {
186+
dst.Container = src.Container
187+
}
188+
if len(src.ServerTool) > 0 {
189+
dst.ServerTool = src.ServerTool
190+
}
191+
}
192+
193+
func mergeOpenAIExtension(dst *OpenAIExtension, src *OpenAIExtension) {
194+
if dst == nil || src == nil {
195+
return
196+
}
197+
if src.ResponsesPassthroughRequired {
198+
dst.ResponsesPassthroughRequired = true
199+
}
200+
if reason := strings.TrimSpace(src.ResponsesPassthroughReason); reason != "" {
201+
dst.ResponsesPassthroughReason = reason
202+
}
203+
if len(src.RawResponseItems) > 0 {
204+
dst.RawResponseItems = src.RawResponseItems
205+
}
206+
}

internal/transformer/model/interface.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ type Outbound interface {
3232
TransformStream(ctx context.Context, eventData []byte) (*InternalLLMResponse, error)
3333
}
3434

35+
type OutboundStreamEventTransformer interface {
36+
// TransformStreamEvent converts provider stream bytes into explicit stream events.
37+
TransformStreamEvent(ctx context.Context, eventData []byte) ([]StreamEvent, error)
38+
}
39+
40+
type InboundStreamEventTransformer interface {
41+
// TransformStreamEvents converts explicit stream events into the inbound wire format.
42+
TransformStreamEvents(ctx context.Context, events []StreamEvent) ([]byte, error)
43+
}
44+
3545
/*
3646
请求流程
3747
非流式

0 commit comments

Comments
 (0)