Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions callbacks/langfuse/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ require (
github.com/stretchr/testify v1.10.0
)

replace github.com/cloudwego/eino-ext/libs/acl/langfuse => ../../libs/acl/langfuse

require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
Expand Down
39 changes: 31 additions & 8 deletions callbacks/langfuse/langfuse.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,11 @@ type CallbackHandler struct {
}

type langfuseStateKey struct{}

type langfuseState struct {
traceID string
observationID string
startTime time.Time // startTime is used to calculate duration in milliseconds
}

func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo, input callbacks.CallbackInput) context.Context {
Expand All @@ -197,6 +199,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
}
if info.Component == components.ComponentOfChatModel {
mcbi := model.ConvCallbackInput(input)
startTime := time.Now()

body := &langfuse.GenerationEventBody{
BaseObservationEventBody: langfuse.BaseObservationEventBody{
Expand All @@ -206,7 +209,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
},
TraceID: state.traceID,
ParentObservationID: state.observationID,
StartTime: time.Now(),
StartTime: startTime,
},
InMessages: mcbi.Messages,
}
Expand All @@ -222,6 +225,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
return context.WithValue(ctx, langfuseStateKey{}, &langfuseState{
traceID: state.traceID,
observationID: generationID,
startTime: startTime,
})
}

Expand All @@ -230,6 +234,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
log.Printf("marshal input error: %v, runinfo: %+v", err, info)
return ctx
}
startTime := time.Now()
spanID, err := c.cli.CreateSpan(&langfuse.SpanEventBody{
BaseObservationEventBody: langfuse.BaseObservationEventBody{
BaseEventBody: langfuse.BaseEventBody{
Expand All @@ -238,7 +243,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
Input: in,
TraceID: state.traceID,
ParentObservationID: state.observationID,
StartTime: time.Now(),
StartTime: startTime,
},
})
if err != nil {
Expand All @@ -248,6 +253,7 @@ func (c *CallbackHandler) OnStart(ctx context.Context, info *callbacks.RunInfo,
return context.WithValue(ctx, langfuseStateKey{}, &langfuseState{
traceID: state.traceID,
observationID: spanID,
startTime: startTime,
})
}

Expand All @@ -264,16 +270,18 @@ func (c *CallbackHandler) OnEnd(ctx context.Context, info *callbacks.RunInfo, ou

if info.Component == components.ComponentOfChatModel {
mcbo := model.ConvCallbackOutput(output)
endTime := time.Now()

body := &langfuse.GenerationEventBody{
BaseObservationEventBody: langfuse.BaseObservationEventBody{
BaseEventBody: langfuse.BaseEventBody{
ID: state.observationID,
},
StartTime: state.startTime, // ← 必须保留原始 startTime
},
OutMessage: mcbo.Message,
EndTime: time.Now(),
CompletionStartTime: time.Now(),
EndTime: endTime,
CompletionStartTime: endTime,
}
if mcbo.TokenUsage != nil {
body.Usage = &langfuse.Usage{
Expand All @@ -283,6 +291,12 @@ func (c *CallbackHandler) OnEnd(ctx context.Context, info *callbacks.RunInfo, ou
}
}

// Calculate duration in milliseconds
if !state.startTime.IsZero() {
duration := int64(endTime.Sub(state.startTime).Milliseconds())
body.Duration = &duration
}

err := c.cli.EndGeneration(body)
if err != nil {
log.Printf("end generation error: %v, runinfo: %+v", err, info)
Expand All @@ -295,15 +309,24 @@ func (c *CallbackHandler) OnEnd(ctx context.Context, info *callbacks.RunInfo, ou
log.Printf("marshal output error: %v, runinfo: %+v", err, info)
return ctx
}
err = c.cli.EndSpan(&langfuse.SpanEventBody{
endTime := time.Now()
spanBody := &langfuse.SpanEventBody{
BaseObservationEventBody: langfuse.BaseObservationEventBody{
BaseEventBody: langfuse.BaseEventBody{
ID: state.observationID,
},
Output: out,
Output: out,
StartTime: state.startTime, // Must preserve the original startTime for correct latency calculation
},
EndTime: time.Now(),
})
EndTime: endTime,
}
// Calculate duration in milliseconds
if !state.startTime.IsZero() {
duration := int64(endTime.Sub(state.startTime).Milliseconds())
spanBody.Duration = &duration
}

err = c.cli.EndSpan(spanBody)
if err != nil {
log.Printf("end span fail: %v, runinfo: %+v", err, info)
}
Expand Down
4 changes: 3 additions & 1 deletion libs/acl/langfuse/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ type BaseObservationEventBody struct {
type SpanEventBody struct {
BaseObservationEventBody

EndTime time.Time `json:"endTime,omitempty"`
EndTime time.Time `json:"endTime,omitempty"`
Duration *int64 `json:"duration,omitempty"` // Duration in milliseconds
}

type Usage struct {
Expand All @@ -247,6 +248,7 @@ type GenerationEventBody struct {
PromptVersion int `json:"promptVersion,omitempty"`
ModelParameters any `json:"modelParameters,omitempty"`
Usage *Usage `json:"usage,omitempty"`
Duration *int64 `json:"duration,omitempty"` // Duration in milliseconds
}

type EventEventBody struct {
Expand Down
Loading