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:
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user