Skip to content

Commit a9030b6

Browse files
authored
feat(schema): wire OpenAI ReasoningExtension into Reasoning block (#1063)
1 parent 30f5961 commit a9030b6

4 files changed

Lines changed: 167 additions & 6 deletions

File tree

adk/middlewares/summarization/finalizer_builder.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ type TypedFinalizerBuilder[M adk.MessageType] struct {
6060

6161
// FinalizerBuilder is a backward-compatible alias for TypedFinalizerBuilder
6262
// specialized with *schema.Message.
63-
//
64-
// Deprecated: use TypedFinalizerBuilder[*schema.Message] instead.
6563
type FinalizerBuilder = TypedFinalizerBuilder[*schema.Message]
6664

6765
// NewTypedFinalizer creates a new TypedFinalizerBuilder.
@@ -88,8 +86,6 @@ func NewTypedFinalizer[M adk.MessageType]() *TypedFinalizerBuilder[M] {
8886

8987
// NewFinalizer creates a new FinalizerBuilder that builds a FinalizeFunc
9088
// by chaining handlers.
91-
//
92-
// Deprecated: use NewTypedFinalizer[*schema.Message] instead.
9389
func NewFinalizer() *FinalizerBuilder {
9490
return &FinalizerBuilder{}
9591
}

schema/agentic_message.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ type ContentBlock struct {
165165
MCPToolApprovalResponse *MCPToolApprovalResponse `json:"mcp_tool_approval_response,omitempty"`
166166

167167
// StreamingMeta contains metadata for streaming responses.
168+
// Only set for streaming responses.
168169
StreamingMeta *StreamingMeta `json:"streaming_meta,omitempty"`
169170

170171
// Extra contains additional information for the content block.
@@ -285,6 +286,9 @@ type Reasoning struct {
285286
// Signature contains encrypted reasoning tokens.
286287
// Required by some models when passing reasoning text back.
287288
Signature string `json:"signature,omitempty"`
289+
290+
// OpenAIExtension is the extension for OpenAI.
291+
OpenAIExtension *openai.ReasoningExtension `json:"openai_extension,omitempty"`
288292
}
289293

290294
type FunctionToolCall struct {
@@ -1303,20 +1307,35 @@ func genericGetTFromContentBlocks[T any](blocks []*ContentBlock, checkAndGetter
13031307
return ret, nil
13041308
}
13051309

1306-
func concatReasoning(reasons []*Reasoning) (*Reasoning, error) {
1310+
func concatReasoning(reasons []*Reasoning) (ret *Reasoning, err error) {
13071311
if len(reasons) == 0 {
13081312
return nil, fmt.Errorf("no reasoning found")
13091313
}
13101314

1311-
ret := &Reasoning{}
1315+
ret = &Reasoning{}
1316+
1317+
openaiExtensions := make([]*openai.ReasoningExtension, 0, len(reasons))
13121318

13131319
for _, r := range reasons {
1320+
if r == nil {
1321+
continue
1322+
}
13141323
if r.Text != "" {
13151324
ret.Text += r.Text
13161325
}
13171326
if r.Signature != "" {
13181327
ret.Signature += r.Signature
13191328
}
1329+
if r.OpenAIExtension != nil {
1330+
openaiExtensions = append(openaiExtensions, r.OpenAIExtension)
1331+
}
1332+
}
1333+
1334+
if len(openaiExtensions) > 0 {
1335+
ret.OpenAIExtension, err = openai.ConcatReasoningExtensions(openaiExtensions)
1336+
if err != nil {
1337+
return nil, fmt.Errorf("failed to concat openai reasoning extensions: %w", err)
1338+
}
13201339
}
13211340

13221341
return ret, nil

schema/openai/extension.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ type AssistantGenTextExtension struct {
3838
Annotations []*TextAnnotation `json:"annotations,omitempty"`
3939
}
4040

41+
type ReasoningExtension struct {
42+
// Content is the reasoning text content.
43+
Content []*ReasoningContent `json:"content,omitempty"`
44+
}
45+
46+
type ReasoningContent struct {
47+
// Index specifies the index position of this content in the final response.
48+
// Only available in streaming response.
49+
Index *int `json:"index,omitempty"`
50+
51+
Text string `json:"text,omitempty"`
52+
}
53+
4154
type ResponseError struct {
4255
Code ResponseErrorCode `json:"code,omitempty"`
4356
Message string `json:"message,omitempty"`
@@ -57,6 +70,8 @@ type OutputRefusal struct {
5770
}
5871

5972
type TextAnnotation struct {
73+
// Index specifies the index position of this annotation in the final response.
74+
// Only available in streaming response.
6075
Index int `json:"index,omitempty"`
6176

6277
Type TextAnnotationType `json:"type,omitempty"`
@@ -167,6 +182,62 @@ func ConcatAssistantGenTextExtensions(chunks []*AssistantGenTextExtension) (*Ass
167182
return ret, nil
168183
}
169184

185+
// ConcatReasoningExtensions concatenates multiple ReasoningExtension chunks into a single one.
186+
func ConcatReasoningExtensions(chunks []*ReasoningExtension) (*ReasoningExtension, error) {
187+
if len(chunks) == 0 {
188+
return nil, fmt.Errorf("no reasoning extension found")
189+
}
190+
191+
ret := &ReasoningExtension{}
192+
193+
var (
194+
indices []int
195+
indexToContent = map[int]*ReasoningContent{}
196+
hasIndexed bool
197+
hasUnindexed bool
198+
)
199+
200+
for _, ext := range chunks {
201+
if ext == nil {
202+
continue
203+
}
204+
for _, c := range ext.Content {
205+
if c == nil {
206+
continue
207+
}
208+
209+
if c.Index == nil {
210+
hasUnindexed = true
211+
ret.Content = append(ret.Content, &ReasoningContent{Text: c.Text})
212+
continue
213+
}
214+
215+
hasIndexed = true
216+
idx := *c.Index
217+
if existing, ok := indexToContent[idx]; ok {
218+
existing.Text += c.Text
219+
} else {
220+
indexToContent[idx] = &ReasoningContent{Text: c.Text}
221+
indices = append(indices, idx)
222+
}
223+
}
224+
}
225+
226+
if hasIndexed && hasUnindexed {
227+
return nil, fmt.Errorf("reasoning content chunks mix indexed and non-indexed content")
228+
}
229+
230+
if hasIndexed {
231+
sort.Ints(indices)
232+
ret.Content = make([]*ReasoningContent, 0, len(indices))
233+
for _, idx := range indices {
234+
ret.Content = append(ret.Content, indexToContent[idx])
235+
}
236+
}
237+
238+
return ret, nil
239+
}
240+
170241
// ConcatResponseMetaExtensions concatenates multiple ResponseMetaExtension chunks into a single one.
171242
func ConcatResponseMetaExtensions(chunks []*ResponseMetaExtension) (*ResponseMetaExtension, error) {
172243
if len(chunks) == 0 {

schema/openai/extension_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"testing"
2121

2222
"github.com/stretchr/testify/assert"
23+
24+
"github.com/cloudwego/eino/internal/generic"
2325
)
2426

2527
func TestConcatResponseMetaExtensions(t *testing.T) {
@@ -191,3 +193,76 @@ func TestConcatAssistantGenTextExtensions(t *testing.T) {
191193
assert.Error(t, err)
192194
})
193195
}
196+
197+
func TestConcatReasoningExtensions(t *testing.T) {
198+
t.Run("empty chunks - error", func(t *testing.T) {
199+
_, err := ConcatReasoningExtensions(nil)
200+
assert.Error(t, err)
201+
})
202+
203+
t.Run("single extension", func(t *testing.T) {
204+
ext := &ReasoningExtension{
205+
Content: []*ReasoningContent{
206+
{Text: "hello", Index: generic.PtrOf(0)},
207+
},
208+
}
209+
210+
result, err := ConcatReasoningExtensions([]*ReasoningExtension{ext})
211+
assert.NoError(t, err)
212+
assert.Len(t, result.Content, 1)
213+
assert.Equal(t, "hello", result.Content[0].Text)
214+
})
215+
216+
t.Run("streaming scenario - same index concatenated, ordered by index", func(t *testing.T) {
217+
exts := []*ReasoningExtension{
218+
{Content: []*ReasoningContent{{Text: "he", Index: generic.PtrOf(0)}}},
219+
{Content: []*ReasoningContent{{Text: "first", Index: generic.PtrOf(1)}}},
220+
{Content: []*ReasoningContent{{Text: "llo", Index: generic.PtrOf(0)}}},
221+
}
222+
223+
result, err := ConcatReasoningExtensions(exts)
224+
assert.NoError(t, err)
225+
assert.Len(t, result.Content, 2)
226+
assert.Nil(t, result.Content[0].Index)
227+
assert.Equal(t, "hello", result.Content[0].Text)
228+
assert.Nil(t, result.Content[1].Index)
229+
assert.Equal(t, "first", result.Content[1].Text)
230+
})
231+
232+
t.Run("nil extension and nil content skipped", func(t *testing.T) {
233+
exts := []*ReasoningExtension{
234+
nil,
235+
{Content: []*ReasoningContent{nil, {Text: "ok", Index: generic.PtrOf(0)}}},
236+
}
237+
238+
result, err := ConcatReasoningExtensions(exts)
239+
assert.NoError(t, err)
240+
assert.Len(t, result.Content, 1)
241+
assert.Equal(t, "ok", result.Content[0].Text)
242+
})
243+
244+
t.Run("all unindexed - appended in arrival order", func(t *testing.T) {
245+
exts := []*ReasoningExtension{
246+
{Content: []*ReasoningContent{{Text: "he"}}},
247+
{Content: []*ReasoningContent{{Text: "llo"}}},
248+
}
249+
250+
result, err := ConcatReasoningExtensions(exts)
251+
assert.NoError(t, err)
252+
assert.Len(t, result.Content, 2)
253+
assert.Nil(t, result.Content[0].Index)
254+
assert.Equal(t, "he", result.Content[0].Text)
255+
assert.Nil(t, result.Content[1].Index)
256+
assert.Equal(t, "llo", result.Content[1].Text)
257+
})
258+
259+
t.Run("mixed indexed and unindexed - error", func(t *testing.T) {
260+
exts := []*ReasoningExtension{
261+
{Content: []*ReasoningContent{{Text: "indexed", Index: generic.PtrOf(0)}}},
262+
{Content: []*ReasoningContent{{Text: "no meta"}}},
263+
}
264+
265+
_, err := ConcatReasoningExtensions(exts)
266+
assert.Error(t, err)
267+
})
268+
}

0 commit comments

Comments
 (0)