refactor: move backend files to backend/ directory

Reorganize project structure:
- backend/cmd/openteam/ — entry point
- backend/internal/ — core packages
- backend/middleware/ — HTTP middleware
- backend/router/ — route setup
- backend/wire/ — dependency injection
- backend/pkg/ — shared utilities
- backend/go.mod, go.sum — Go module files

Updated Makefile to work from backend/ directory.
Removed old lowercase makefile.
This commit is contained in:
Sakurasan
2026-08-30 12:02:52 +08:00
parent ef3025dd80
commit 902ecaeacc
64 changed files with 12 additions and 107 deletions
+61
View File
@@ -0,0 +1,61 @@
package convert
// ChatCompletionRequest represents an OpenAI Chat Completions request
type ChatCompletionRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
N *int `json:"n,omitempty"`
Stream bool `json:"stream,omitempty"`
Stop interface{} `json:"stop,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
LogitBias map[string]int `json:"logit_bias,omitempty"`
User string `json:"user,omitempty"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
ResponseFormat interface{} `json:"response_format,omitempty"`
Seed *int `json:"seed,omitempty"`
}
// ChatCompletionResponse represents an OpenAI Chat Completions response
type ChatCompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage *Usage `json:"usage,omitempty"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
}
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
FinishReason string `json:"finish_reason"`
}
// ChatCompletionStreamChunk represents a streaming chunk
type ChatCompletionStreamChunk struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []StreamChoice `json:"choices"`
Usage *Usage `json:"usage,omitempty"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
}
type StreamChoice struct {
Index int `json:"index"`
Delta StreamDelta `json:"delta"`
FinishReason *string `json:"finish_reason"`
}
type StreamDelta struct {
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}
@@ -0,0 +1,212 @@
package convert
import (
"encoding/json"
"fmt"
)
// ChatToMessages converts a Chat Completions request to Anthropic Messages format
func ChatToMessages(req *ChatCompletionRequest) (*MessagesRequest, error) {
msgs := make([]Message, 0, len(req.Messages))
var systemParts []ContentPart
for _, m := range req.Messages {
if m.Role == "system" {
// Extract system message content
switch v := m.Content.(type) {
case string:
systemParts = append(systemParts, ContentPart{
Type: "text",
Text: v,
})
case []interface{}:
for _, part := range v {
if p, ok := part.(map[string]interface{}); ok {
if t, ok := p["type"].(string); ok && t == "text" {
if text, ok := p["text"].(string); ok {
systemParts = append(systemParts, ContentPart{
Type: "text",
Text: text,
})
}
}
}
}
}
continue
}
msgs = append(msgs, m)
}
out := &MessagesRequest{
Model: req.Model,
Messages: msgs,
Stream: req.Stream,
}
if len(systemParts) > 0 {
out.System = systemParts
}
if req.MaxTokens != nil {
out.MaxTokens = *req.MaxTokens
} else {
defaultMax := 4096
out.MaxTokens = defaultMax
}
if req.Temperature != nil {
out.Temperature = req.Temperature
}
if req.TopP != nil {
out.TopP = req.TopP
}
if req.Tools != nil {
out.Tools = req.Tools
}
return out, nil
}
// MessagesToChat converts an Anthropic Messages response to Chat Completions format
func MessagesToChat(resp *MessagesResponse) (*ChatCompletionResponse, error) {
choices := make([]Choice, 0)
for _, block := range resp.Content {
switch block.Type {
case "text":
choices = append(choices, Choice{
Index: len(choices),
Message: Message{
Role: "assistant",
Content: block.Text,
},
FinishReason: mapStopReason(resp.StopReason),
})
case "tool_use":
toolCall := ToolCall{
ID: block.ID,
Type: "function",
Function: FunctionCall{
Name: block.Name,
Arguments: toJSON(block.Input),
},
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
ToolCalls: []ToolCall{toolCall},
},
FinishReason: "tool_calls",
})
} else {
choices[0].Message.ToolCalls = append(choices[0].Message.ToolCalls, toolCall)
choices[0].FinishReason = "tool_calls"
}
}
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
Content: "",
},
FinishReason: "stop",
})
}
return &ChatCompletionResponse{
ID: resp.ID,
Object: "chat.completion",
Model: resp.Model,
Choices: choices,
Usage: &Usage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.PromptTokens + resp.Usage.CompletionTokens,
},
}, nil
}
// MessagesStreamToChatStream converts Anthropic streaming chunks to Chat Completions format
func MessagesStreamToChatStream(anthropicEvents []AnthropicStreamEvent, model string) []ChatCompletionStreamChunk {
var chunks []ChatCompletionStreamChunk
id := fmt.Sprintf("chatcmpl-%d", len(anthropicEvents))
for _, event := range anthropicEvents {
switch event.Type {
case "message_start":
// Initial chunk with role
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
},
}},
})
case "content_block_delta":
if event.Delta != nil && event.Delta.Text != "" {
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Content: event.Delta.Text,
},
}},
})
}
case "message_delta":
finishReason := "stop"
if event.Delta != nil && event.Delta.StopReason != "" {
finishReason = mapStopReason(event.Delta.StopReason)
}
chunk := ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
FinishReason: &finishReason,
}},
}
if event.Usage != nil {
chunk.Usage = event.Usage
}
chunks = append(chunks, chunk)
}
}
return chunks
}
func mapStopReason(reason string) string {
switch reason {
case "end_turn", "stop_sequence":
return "stop"
case "tool_use":
return "tool_calls"
case "max_tokens":
return "length"
default:
return "stop"
}
}
func toJSON(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(b)
}
@@ -0,0 +1,298 @@
package convert
import (
"encoding/json"
"fmt"
)
// ChatToResponses converts a Chat Completions request to Responses API format
func ChatToResponses(req *ChatCompletionRequest) (*ResponsesRequest, error) {
var inputItems []InputItem
var instructions string
for _, m := range req.Messages {
if m.Role == "system" {
if s, ok := m.Content.(string); ok {
if instructions != "" {
instructions += "\n\n"
}
instructions += s
}
continue
}
item := InputItem{
Role: m.Role,
Content: m.Content,
}
inputItems = append(inputItems, item)
}
out := &ResponsesRequest{
Model: req.Model,
Input: inputItems,
Instructions: instructions,
Stream: req.Stream,
}
if req.MaxTokens != nil {
out.MaxOutputTokens = req.MaxTokens
}
if req.Temperature != nil {
out.Temperature = req.Temperature
}
if req.TopP != nil {
out.TopP = req.TopP
}
if req.Tools != nil {
out.Tools = req.Tools
}
return out, nil
}
// ResponsesToChat converts a Responses API response to Chat Completions format
func ResponsesToChat(resp *ResponsesResponse) (*ChatCompletionResponse, error) {
choices := make([]Choice, 0)
for _, output := range resp.Output {
switch output.Type {
case "message":
for _, content := range output.Content {
switch content.Type {
case "output_text":
choices = append(choices, Choice{
Index: len(choices),
Message: Message{
Role: "assistant",
Content: content.Text,
},
FinishReason: "stop",
})
case "function_call":
toolCall := ToolCall{
ID: content.ID,
Type: "function",
Function: FunctionCall{
Name: content.Name,
Arguments: toJSON(content.Input),
},
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
ToolCalls: []ToolCall{toolCall},
},
FinishReason: "tool_calls",
})
} else {
choices[0].Message.ToolCalls = append(choices[0].Message.ToolCalls, toolCall)
choices[0].FinishReason = "tool_calls"
}
}
}
case "function_call_output":
// This would be in a user message context
continue
}
}
if len(choices) == 0 {
choices = append(choices, Choice{
Index: 0,
Message: Message{
Role: "assistant",
Content: "",
},
FinishReason: "stop",
})
}
return &ChatCompletionResponse{
ID: resp.ID,
Object: "chat.completion",
Model: resp.Model,
Choices: choices,
Usage: &Usage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.PromptTokens + resp.Usage.CompletionTokens,
},
}, nil
}
// ResponsesStreamToChatStream converts Responses API streaming to Chat Completions format
func ResponsesStreamToChatStream(events []ResponsesStreamEvent, model string) []ChatCompletionStreamChunk {
var chunks []ChatCompletionStreamChunk
id := fmt.Sprintf("chatcmpl-%d", len(events))
for _, event := range events {
switch event.Type {
case "response.created":
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
},
}},
})
case "response.output_item.added":
if event.Item != nil && event.Item.Type == "message" {
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
},
}},
})
}
case "response.content_part.delta":
if event.Delta != "" {
chunks = append(chunks, ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
Delta: StreamDelta{
Content: event.Delta,
},
}},
})
}
case "response.completed":
finishReason := "stop"
chunk := ChatCompletionStreamChunk{
ID: id,
Object: "chat.completion.chunk",
Model: model,
Choices: []StreamChoice{{
Index: 0,
FinishReason: &finishReason,
}},
}
chunks = append(chunks, chunk)
}
}
return chunks
}
// MessagesToResponses converts an Anthropic Messages request to Responses API format
func MessagesToResponses(req *MessagesRequest) (*ResponsesRequest, error) {
var inputItems []InputItem
var instructions string
// Handle system message
if req.System != nil {
switch v := req.System.(type) {
case string:
instructions = v
case []ContentPart:
for _, p := range v {
if p.Type == "text" {
if instructions != "" {
instructions += "\n\n"
}
instructions += p.Text
}
}
}
}
for _, m := range req.Messages {
item := InputItem{
Role: m.Role,
Content: m.Content,
}
inputItems = append(inputItems, item)
}
out := &ResponsesRequest{
Model: req.Model,
Input: inputItems,
Instructions: instructions,
Stream: req.Stream,
}
out.MaxOutputTokens = &req.MaxTokens
if req.Temperature != nil {
out.Temperature = req.Temperature
}
if req.TopP != nil {
out.TopP = req.TopP
}
if req.Tools != nil {
out.Tools = req.Tools
}
return out, nil
}
// ResponsesToMessages converts a Responses API response to Anthropic Messages format
func ResponsesToMessages(resp *ResponsesResponse) (*MessagesResponse, error) {
var content []ContentBlock
for _, output := range resp.Output {
switch output.Type {
case "message":
for _, c := range output.Content {
switch c.Type {
case "output_text":
content = append(content, ContentBlock{
Type: "text",
Text: c.Text,
})
case "function_call":
content = append(content, ContentBlock{
Type: "tool_use",
ID: c.ID,
Name: c.Name,
})
}
}
}
}
var stopReason string
if len(content) > 0 {
last := content[len(content)-1]
if last.Type == "tool_use" {
stopReason = "tool_use"
} else {
stopReason = "end_turn"
}
} else {
stopReason = "end_turn"
}
return &MessagesResponse{
ID: resp.ID,
Type: "message",
Role: "assistant",
Content: content,
Model: resp.Model,
StopReason: stopReason,
Usage: resp.Usage,
}, nil
}
// toJSON is a helper to convert a value to JSON string
func toJSONStr(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(b)
}
@@ -0,0 +1,231 @@
package convert
import (
"testing"
)
func TestChatToMessages(t *testing.T) {
maxTokens := 1024
temp := 0.7
req := &ChatCompletionRequest{
Model: "claude-3-sonnet-20240229",
Messages: []Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "Hello!"},
},
MaxTokens: &maxTokens,
Temperature: &temp,
}
result, err := ChatToMessages(req)
if err != nil {
t.Fatalf("ChatToMessages() error = %v", err)
}
if result.Model != "claude-3-sonnet-20240229" {
t.Errorf("Model = %q, want %q", result.Model, "claude-3-sonnet-20240229")
}
if len(result.Messages) != 1 {
t.Errorf("Messages length = %d, want 1", len(result.Messages))
}
if result.Messages[0].Role != "user" {
t.Errorf("Messages[0].Role = %q, want %q", result.Messages[0].Role, "user")
}
if result.System == nil {
t.Error("System is nil, want non-nil")
}
if result.MaxTokens != 1024 {
t.Errorf("MaxTokens = %d, want 1024", result.MaxTokens)
}
}
func TestMessagesToChat(t *testing.T) {
resp := &MessagesResponse{
ID: "msg-123",
Model: "claude-3-sonnet-20240229",
Content: []ContentBlock{
{Type: "text", Text: "Hello! How can I help?"},
},
StopReason: "end_turn",
Usage: Usage{
PromptTokens: 10,
CompletionTokens: 20,
},
}
result, err := MessagesToChat(resp)
if err != nil {
t.Fatalf("MessagesToChat() error = %v", err)
}
if result.ID != "msg-123" {
t.Errorf("ID = %q, want %q", result.ID, "msg-123")
}
if result.Object != "chat.completion" {
t.Errorf("Object = %q, want %q", result.Object, "chat.completion")
}
if len(result.Choices) != 1 {
t.Errorf("Choices length = %d, want 1", len(result.Choices))
return
}
if result.Choices[0].Message.Role != "assistant" {
t.Errorf("Choices[0].Message.Role = %q, want %q", result.Choices[0].Message.Role, "assistant")
}
if result.Choices[0].Message.Content != "Hello! How can I help?" {
t.Errorf("Choices[0].Message.Content = %q, want %q", result.Choices[0].Message.Content, "Hello! How can I help?")
}
if result.Choices[0].FinishReason != "stop" {
t.Errorf("FinishReason = %q, want %q", result.Choices[0].FinishReason, "stop")
}
if result.Usage.TotalTokens != 30 {
t.Errorf("Usage.TotalTokens = %d, want 30", result.Usage.TotalTokens)
}
}
func TestChatToResponses(t *testing.T) {
maxTokens := 2048
req := &ChatCompletionRequest{
Model: "gpt-4o",
Messages: []Message{
{Role: "system", Content: "You are a helpful assistant."},
{Role: "user", Content: "What is 2+2?"},
},
MaxTokens: &maxTokens,
}
result, err := ChatToResponses(req)
if err != nil {
t.Fatalf("ChatToResponses() error = %v", err)
}
if result.Model != "gpt-4o" {
t.Errorf("Model = %q, want %q", result.Model, "gpt-4o")
}
if len(result.Input) != 1 {
t.Errorf("Input length = %d, want 1", len(result.Input))
}
if result.Input[0].Role != "user" {
t.Errorf("Input[0].Role = %q, want %q", result.Input[0].Role, "user")
}
if result.Instructions != "You are a helpful assistant." {
t.Errorf("Instructions = %q, want %q", result.Instructions, "You are a helpful assistant.")
}
}
func TestResponsesToChat(t *testing.T) {
resp := &ResponsesResponse{
ID: "resp-123",
Model: "gpt-4o",
Status: "completed",
Output: []OutputItem{
{
Type: "message",
Content: []OutputContent{
{Type: "output_text", Text: "2+2 equals 4."},
},
},
},
Usage: Usage{
PromptTokens: 15,
CompletionTokens: 10,
},
}
result, err := ResponsesToChat(resp)
if err != nil {
t.Fatalf("ResponsesToChat() error = %v", err)
}
if result.ID != "resp-123" {
t.Errorf("ID = %q, want %q", result.ID, "resp-123")
}
if len(result.Choices) != 1 {
t.Errorf("Choices length = %d, want 1", len(result.Choices))
return
}
if result.Choices[0].Message.Content != "2+2 equals 4." {
t.Errorf("Content = %q, want %q", result.Choices[0].Message.Content, "2+2 equals 4.")
}
}
func TestMapStopReason(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"end_turn", "stop"},
{"stop_sequence", "stop"},
{"tool_use", "tool_calls"},
{"max_tokens", "length"},
{"unknown", "stop"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := mapStopReason(tt.input)
if result != tt.expected {
t.Errorf("mapStopReason(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestMessagesToChatToolUse(t *testing.T) {
resp := &MessagesResponse{
ID: "msg-456",
Model: "claude-3-sonnet-20240229",
Content: []ContentBlock{
{Type: "text", Text: "Let me search for that."},
{Type: "tool_use", ID: "toolu-123", Name: "web_search"},
},
StopReason: "tool_use",
Usage: Usage{
PromptTokens: 20,
CompletionTokens: 30,
},
}
result, err := MessagesToChat(resp)
if err != nil {
t.Fatalf("MessagesToChat() error = %v", err)
}
if len(result.Choices) != 1 {
t.Errorf("Choices length = %d, want 1", len(result.Choices))
return
}
if result.Choices[0].FinishReason != "tool_calls" {
t.Errorf("FinishReason = %q, want %q", result.Choices[0].FinishReason, "tool_calls")
}
if len(result.Choices[0].Message.ToolCalls) != 1 {
t.Errorf("ToolCalls length = %d, want 1", len(result.Choices[0].Message.ToolCalls))
return
}
if result.Choices[0].Message.ToolCalls[0].ID != "toolu-123" {
t.Errorf("ToolCall ID = %q, want %q", result.Choices[0].Message.ToolCalls[0].ID, "toolu-123")
}
if result.Choices[0].Message.ToolCalls[0].Function.Name != "web_search" {
t.Errorf("Function.Name = %q, want %q", result.Choices[0].Message.ToolCalls[0].Function.Name, "web_search")
}
}
@@ -0,0 +1,37 @@
package convert
// MessagesRequest represents an Anthropic Messages API request
type MessagesRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
MaxTokens int `json:"max_tokens"`
System interface{} `json:"system,omitempty"` // string or []ContentPart
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
StopSequences []string `json:"stop_sequences,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
// MessagesResponse represents an Anthropic Messages API response
type MessagesResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Role string `json:"role"`
Content []ContentBlock `json:"content"`
Model string `json:"model"`
StopReason string `json:"stop_reason"`
StopSequence string `json:"stop_sequence,omitempty"`
Usage Usage `json:"usage"`
}
// AnthropicStreamEvent represents an Anthropic streaming event
type AnthropicStreamEvent struct {
Type string `json:"type"`
Index int `json:"index,omitempty"`
Delta *Delta `json:"delta,omitempty"`
Usage *Usage `json:"usage,omitempty"`
}
@@ -0,0 +1,59 @@
package convert
// ResponsesRequest represents an OpenAI Responses API request
type ResponsesRequest struct {
Model string `json:"model"`
Input []InputItem `json:"input"`
Instructions string `json:"instructions,omitempty"`
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice interface{} `json:"tool_choice,omitempty"`
Stream bool `json:"stream,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}
// InputItem represents a single input item
type InputItem struct {
Role string `json:"role"`
Content interface{} `json:"content,omitempty"`
}
// ResponsesResponse represents an OpenAI Responses API response
type ResponsesResponse struct {
ID string `json:"id"`
Object string `json:"object"`
CreatedAt int64 `json:"created_at"`
Status string `json:"status"`
Model string `json:"model"`
Output []OutputItem `json:"output"`
Usage Usage `json:"usage"`
Error interface{} `json:"error,omitempty"`
Incomplete *Incomplete `json:"incomplete,omitempty"`
}
type OutputItem struct {
Type string `json:"type"`
Content []OutputContent `json:"content,omitempty"`
Role string `json:"role,omitempty"`
}
type OutputContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input interface{} `json:"input,omitempty"`
}
type Incomplete struct {
Reason string `json:"reason"`
}
// ResponsesStreamEvent represents a Responses API streaming event
type ResponsesStreamEvent struct {
Type string `json:"type"`
Item *OutputItem `json:"item,omitempty"`
Delta string `json:"delta,omitempty"`
}
+171
View File
@@ -0,0 +1,171 @@
package convert
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// SSEWriter writes Server-Sent Events
type SSEWriter struct {
writer io.Writer
flusher http.Flusher
}
// NewSSEWriter creates a new SSE writer
func NewSSEWriter(w http.ResponseWriter) *SSEWriter {
flusher, _ := w.(http.Flusher)
return &SSEWriter{
writer: w,
flusher: flusher,
}
}
// WriteEvent writes a single SSE event
func (w *SSEWriter) WriteEvent(event string, data interface{}) error {
var dataStr string
switch v := data.(type) {
case string:
dataStr = v
default:
b, err := json.Marshal(v)
if err != nil {
return err
}
dataStr = string(b)
}
_, err := fmt.Fprintf(w.writer, "event: %s\ndata: %s\n\n", event, dataStr)
if err != nil {
return err
}
if w.flusher != nil {
w.flusher.Flush()
}
return nil
}
// WriteChunk writes a streaming chunk in SSE format
func (w *SSEWriter) WriteChunk(chunk interface{}) error {
b, err := json.Marshal(chunk)
if err != nil {
return err
}
_, err = fmt.Fprintf(w.writer, "data: %s\n\n", string(b))
if err != nil {
return err
}
if w.flusher != nil {
w.flusher.Flush()
}
return nil
}
// WriteDone writes the [DONE] marker
func (w *SSEWriter) WriteDone() error {
_, err := fmt.Fprintf(w.writer, "data: [DONE]\n\n")
if err != nil {
return err
}
if w.flusher != nil {
w.flusher.Flush()
}
return nil
}
// SSEParser parses Server-Sent Events from a reader
type SSEParser struct {
reader *bufio.Reader
}
// NewSSEParser creates a new SSE parser
func NewSSEParser(r io.Reader) *SSEParser {
return &SSEParser{
reader: bufio.NewReader(r),
}
}
// SSEEvent represents a parsed SSE event
type SSEEvent struct {
Event string
Data string
}
// ReadEvent reads the next SSE event
func (p *SSEParser) ReadEvent() (*SSEEvent, error) {
event := &SSEEvent{}
for {
line, err := p.reader.ReadString('\n')
if err != nil {
return nil, err
}
line = strings.TrimRight(line, "\r\n")
if line == "" {
// Empty line means end of event
if event.Data != "" || event.Event != "" {
return event, nil
}
continue
}
if strings.HasPrefix(line, "event:") {
event.Event = strings.TrimSpace(line[6:])
} else if strings.HasPrefix(line, "data:") {
data := strings.TrimSpace(line[5:])
if event.Data != "" {
event.Data += "\n" + data
} else {
event.Data = data
}
}
// Ignore comments (lines starting with :) and unknown fields
}
}
// ParseChatStreamChunk parses an OpenAI Chat Completions streaming chunk
func ParseChatStreamChunk(data string) (*ChatCompletionStreamChunk, error) {
if data == "[DONE]" {
return nil, io.EOF
}
var chunk ChatCompletionStreamChunk
err := json.Unmarshal([]byte(data), &chunk)
if err != nil {
return nil, err
}
return &chunk, nil
}
// ParseMessagesStreamEvent parses an Anthropic Messages streaming event
func ParseMessagesStreamEvent(data string) (*AnthropicStreamEvent, error) {
var event AnthropicStreamEvent
err := json.Unmarshal([]byte(data), &event)
if err != nil {
return nil, err
}
return &event, nil
}
// ParseResponsesStreamChunk parses an OpenAI Responses API streaming chunk
func ParseResponsesStreamChunk(data string) (*ResponsesStreamEvent, error) {
if data == "[DONE]" {
return nil, io.EOF
}
var event ResponsesStreamEvent
err := json.Unmarshal([]byte(data), &event)
if err != nil {
return nil, err
}
return &event, nil
}
+99
View File
@@ -0,0 +1,99 @@
package convert
// Common types shared across all protocols
// Message represents a unified message format
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content,omitempty"` // string or []ContentPart
Name string `json:"name,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
// ContentPart represents a part of a multi-part message content
type ContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *ImageURL `json:"image_url,omitempty"`
Source *ImageSource `json:"source,omitempty"`
ToolUse *ToolUse `json:"tool_use,omitempty"`
ToolResult *ToolResult `json:"tool_result,omitempty"`
}
type ImageURL struct {
URL string `json:"url"`
Detail string `json:"detail,omitempty"`
}
type ImageSource struct {
Type string `json:"type"`
MediaType string `json:"media_type"`
Data string `json:"data"`
}
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function FunctionCall `json:"function"`
}
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type ToolUse struct {
ID string `json:"id"`
Name string `json:"name"`
Input interface{} `json:"input"`
}
type ToolResult struct {
ToolUseID string `json:"tool_use_id"`
Content string `json:"content"`
}
// Tool definition
type Tool struct {
Type string `json:"type"`
Function ToolDefinition `json:"function,omitempty"`
Name string `json:"name,omitempty"` // Anthropic style
Input interface{} `json:"input_schema,omitempty"` // Anthropic style
}
type ToolDefinition struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters interface{} `json:"parameters,omitempty"`
}
// StreamEvent represents a unified streaming event
type StreamEvent struct {
Type string `json:"type"` // "message_start", "content_block_start", "content_block_delta", "message_delta", "message_stop"
Delta *Delta `json:"delta,omitempty"`
Usage *Usage `json:"usage,omitempty"`
}
type Delta struct {
Type string `json:"type,omitempty"`
Text string `json:"text,omitempty"`
StopReason string `json:"stop_reason,omitempty"`
ContentBlock *ContentBlock `json:"content_block,omitempty"`
}
type ContentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input interface{} `json:"input,omitempty"`
}
// Usage represents token usage
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens,omitempty"`
CacheReadTokens int `json:"cache_read_input_tokens,omitempty"`
}