| |
| |
| package translator |
|
|
| import ( |
| "fmt" |
| "strings" |
| "sync/atomic" |
| "time" |
| ) |
|
|
| |
| |
| type StreamState struct { |
| |
| ResponseID string |
| |
| CreatedAt int64 |
| |
| Model string |
| |
| SequenceNum int |
| |
| FinishReason string |
| |
| NativeFinishReason string |
| |
| InputTokens int64 |
| |
| OutputTokens int64 |
| |
| UsageSeen bool |
| } |
|
|
| |
| func (s *StreamState) Reset() { |
| s.ResponseID = "" |
| s.CreatedAt = 0 |
| s.Model = "" |
| s.SequenceNum = 0 |
| s.FinishReason = "" |
| s.NativeFinishReason = "" |
| s.InputTokens = 0 |
| s.OutputTokens = 0 |
| s.UsageSeen = false |
| } |
|
|
| |
| func (s *StreamState) NextSequence() int { |
| s.SequenceNum++ |
| return s.SequenceNum |
| } |
|
|
| |
| func (s *StreamState) EnsureTimestamp() int64 { |
| if s.CreatedAt == 0 { |
| s.CreatedAt = time.Now().Unix() |
| } |
| return s.CreatedAt |
| } |
|
|
| |
| type ToolCallState struct { |
| |
| ID string |
| |
| Name string |
| |
| Index int |
| |
| Arguments strings.Builder |
| |
| Complete bool |
| } |
|
|
| |
| func (t *ToolCallState) GetArguments() string { |
| args := t.Arguments.String() |
| if args == "" { |
| return "{}" |
| } |
| return args |
| } |
|
|
| |
| func (t *ToolCallState) WriteArguments(partial string) { |
| t.Arguments.WriteString(partial) |
| } |
|
|
| |
| type ToolCallAccumulator struct { |
| |
| calls map[int]*ToolCallState |
| |
| ordered []int |
| |
| nextIndex int |
| } |
|
|
| |
| func NewToolCallAccumulator() *ToolCallAccumulator { |
| return &ToolCallAccumulator{ |
| calls: make(map[int]*ToolCallState), |
| ordered: make([]int, 0), |
| nextIndex: 0, |
| } |
| } |
|
|
| |
| func (t *ToolCallAccumulator) Get(index int) *ToolCallState { |
| return t.calls[index] |
| } |
|
|
| |
| func (t *ToolCallAccumulator) GetOrCreate(index int) *ToolCallState { |
| if tc, exists := t.calls[index]; exists { |
| return tc |
| } |
| tc := &ToolCallState{Index: index} |
| t.calls[index] = tc |
| t.ordered = append(t.ordered, index) |
| return tc |
| } |
|
|
| |
| func (t *ToolCallAccumulator) CreateNext(id, name string) *ToolCallState { |
| tc := &ToolCallState{ |
| ID: id, |
| Name: name, |
| Index: t.nextIndex, |
| } |
| t.calls[t.nextIndex] = tc |
| t.ordered = append(t.ordered, t.nextIndex) |
| t.nextIndex++ |
| return tc |
| } |
|
|
| |
| func (t *ToolCallAccumulator) MarkComplete(index int) *ToolCallState { |
| if tc, exists := t.calls[index]; exists { |
| tc.Complete = true |
| return tc |
| } |
| return nil |
| } |
|
|
| |
| func (t *ToolCallAccumulator) Remove(index int) { |
| delete(t.calls, index) |
| |
| newOrdered := make([]int, 0, len(t.calls)) |
| for _, idx := range t.ordered { |
| if idx != index { |
| newOrdered = append(newOrdered, idx) |
| } |
| } |
| t.ordered = newOrdered |
| } |
|
|
| |
| func (t *ToolCallAccumulator) HasAny() bool { |
| return len(t.calls) > 0 |
| } |
|
|
| |
| func (t *ToolCallAccumulator) Count() int { |
| return len(t.calls) |
| } |
|
|
| |
| func (t *ToolCallAccumulator) GetOrdered() []*ToolCallState { |
| result := make([]*ToolCallState, 0, len(t.ordered)) |
| for _, idx := range t.ordered { |
| if tc, exists := t.calls[idx]; exists { |
| result = append(result, tc) |
| } |
| } |
| return result |
| } |
|
|
| |
| func (t *ToolCallAccumulator) Reset() { |
| t.calls = make(map[int]*ToolCallState) |
| t.ordered = make([]int, 0) |
| t.nextIndex = 0 |
| } |
|
|
| |
| type ContentBuffer struct { |
| |
| Text strings.Builder |
| |
| Reasoning strings.Builder |
| } |
|
|
| |
| func (c *ContentBuffer) Reset() { |
| c.Text.Reset() |
| c.Reasoning.Reset() |
| } |
|
|
| |
| func (c *ContentBuffer) HasContent() bool { |
| return c.Text.Len() > 0 || c.Reasoning.Len() > 0 |
| } |
|
|
| |
| type StreamingState struct { |
| StreamState |
| |
| ToolCalls *ToolCallAccumulator |
| |
| Content ContentBuffer |
| |
| InTextBlock bool |
| |
| InToolBlock bool |
| |
| InReasoningBlock bool |
| |
| CurrentMessageID string |
| |
| CurrentToolCallID string |
| |
| CurrentReasoningID string |
| } |
|
|
| |
| func NewStreamingState() *StreamingState { |
| return &StreamingState{ |
| ToolCalls: NewToolCallAccumulator(), |
| } |
| } |
|
|
| |
| func (s *StreamingState) Reset() { |
| s.StreamState.Reset() |
| s.ToolCalls.Reset() |
| s.Content.Reset() |
| s.InTextBlock = false |
| s.InToolBlock = false |
| s.InReasoningBlock = false |
| s.CurrentMessageID = "" |
| s.CurrentToolCallID = "" |
| s.CurrentReasoningID = "" |
| } |
|
|
| |
| type FinishReasonMapper struct { |
| toOpenAI map[string]string |
| fromOpenAI map[string]string |
| } |
|
|
| |
| func NewFinishReasonMapper() *FinishReasonMapper { |
| return &FinishReasonMapper{ |
| toOpenAI: map[string]string{ |
| |
| "end_turn": "stop", |
| "tool_use": "tool_calls", |
| "max_tokens": "length", |
| "stop_sequence": "stop", |
| |
| "STOP": "stop", |
| "MAX_TOKENS": "length", |
| "SAFETY": "content_filter", |
| "RECITATION": "content_filter", |
| "OTHER": "stop", |
| |
| "completed": "stop", |
| }, |
| fromOpenAI: map[string]string{ |
| "stop": "end_turn", |
| "tool_calls": "tool_use", |
| "length": "max_tokens", |
| "content_filter": "SAFETY", |
| }, |
| } |
| } |
|
|
| |
| func (m *FinishReasonMapper) ToOpenAI(reason string) string { |
| if mapped, ok := m.toOpenAI[reason]; ok { |
| return mapped |
| } |
| return "stop" |
| } |
|
|
| |
| func (m *FinishReasonMapper) FromOpenAI(reason string) string { |
| if mapped, ok := m.fromOpenAI[reason]; ok { |
| return mapped |
| } |
| return "STOP" |
| } |
|
|
| |
| func (m *FinishReasonMapper) RegisterMapping(providerReason, openAIReason string) { |
| m.toOpenAI[providerReason] = openAIReason |
| m.fromOpenAI[openAIReason] = providerReason |
| } |
|
|
| |
| var DefaultFinishReasonMapper = NewFinishReasonMapper() |
|
|
| |
| var ( |
| responseIDCounter uint64 |
| functionCallIDCounter uint64 |
| ) |
|
|
| |
| func GenerateResponseID(prefix string) string { |
| counter := atomic.AddUint64(&responseIDCounter, 1) |
| if prefix != "" { |
| return fmt.Sprintf("%s_%x_%d", prefix, time.Now().UnixNano(), counter) |
| } |
| return fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), counter) |
| } |
|
|
| |
| func GenerateFunctionCallID(name string) string { |
| counter := atomic.AddUint64(&functionCallIDCounter, 1) |
| if name != "" { |
| return fmt.Sprintf("call_%s_%d_%d", name, time.Now().UnixNano(), counter) |
| } |
| return fmt.Sprintf("call_%d_%d", time.Now().UnixNano(), counter) |
| } |
|
|