Changeset 0.7.2 (#40)

This commit is contained in:
2026-02-21 19:03:19 +00:00
parent 494b1aa981
commit 416e5439ea
28 changed files with 3813 additions and 138 deletions

View File

@@ -36,13 +36,32 @@ func (p *OpenAIProvider) ChatCompletion(ctx context.Context, cfg ProviderConfig,
return nil, fmt.Errorf("no choices in response")
}
return &CompletionResponse{
result := &CompletionResponse{
Content: resp.Choices[0].Message.Content,
Model: resp.Model,
FinishReason: resp.Choices[0].FinishReason,
InputTokens: resp.Usage.PromptTokens,
OutputTokens: resp.Usage.CompletionTokens,
}, nil
}
// Extract tool calls if present
if len(resp.Choices[0].Message.ToolCalls) > 0 {
for _, tc := range resp.Choices[0].Message.ToolCalls {
result.ToolCalls = append(result.ToolCalls, ToolCall{
ID: tc.ID,
Type: tc.Type,
Function: FunctionCall{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
})
}
if result.FinishReason == "" {
result.FinishReason = "tool_calls"
}
}
return result, nil
}
// ── Stream Completion ───────────────────────
@@ -61,6 +80,9 @@ func (p *OpenAIProvider) StreamCompletion(ctx context.Context, cfg ProviderConfi
defer close(ch)
defer body.Close()
// Accumulate tool calls across stream chunks
toolCallMap := map[int]*ToolCall{} // index → accumulated call
scanner := bufio.NewScanner(body)
for scanner.Scan() {
line := scanner.Text()
@@ -84,13 +106,45 @@ func (p *OpenAIProvider) StreamCompletion(ctx context.Context, cfg ProviderConfi
continue
}
ev := StreamEvent{
Delta: chunk.Choices[0].Delta.Content,
Model: chunk.Model,
FinishReason: chunk.Choices[0].FinishReason,
choice := chunk.Choices[0]
// Accumulate tool call deltas
for _, tc := range choice.Delta.ToolCalls {
idx := 0
if tc.Index != nil {
idx = *tc.Index
}
if existing, ok := toolCallMap[idx]; ok {
// Append arguments fragment
existing.Function.Arguments += tc.Function.Arguments
} else {
// First chunk for this tool — has ID and name
toolCallMap[idx] = &ToolCall{
ID: tc.ID,
Type: "function",
Function: FunctionCall{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
}
}
}
ev := StreamEvent{
Delta: choice.Delta.Content,
Model: chunk.Model,
FinishReason: choice.FinishReason,
}
if ev.FinishReason != "" {
ev.Done = true
// Attach accumulated tool calls on final event
if ev.FinishReason == "tool_calls" && len(toolCallMap) > 0 {
for _, tc := range toolCallMap {
ev.ToolCalls = append(ev.ToolCalls, *tc)
}
}
}
ch <- ev
@@ -178,11 +232,46 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req
oaiReq.TopP = req.TopP
}
// Convert tools
for _, t := range req.Tools {
oaiReq.Tools = append(oaiReq.Tools, openaiToolDef{
Type: t.Type,
Function: openaiToolDefFunction{
Name: t.Function.Name,
Description: t.Function.Description,
Parameters: t.Function.Parameters,
},
})
}
// Convert messages — handle all roles including tool
for _, m := range req.Messages {
oaiReq.Messages = append(oaiReq.Messages, openaiMessage{
oaiMsg := openaiMessage{
Role: m.Role,
Content: m.Content,
})
}
// Assistant messages with tool calls
if len(m.ToolCalls) > 0 {
for _, tc := range m.ToolCalls {
oaiMsg.ToolCalls = append(oaiMsg.ToolCalls, openaiToolCall{
ID: tc.ID,
Type: tc.Type,
Function: openaiToolCallFunction{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
})
}
}
// Tool result messages
if m.Role == "tool" {
oaiMsg.ToolCallID = m.ToolCallID
oaiMsg.Name = m.Name
}
oaiReq.Messages = append(oaiReq.Messages, oaiMsg)
}
body, err := json.Marshal(oaiReq)
@@ -220,8 +309,34 @@ func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req
// ── OpenAI Wire Types ───────────────────────
type openaiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Role string `json:"role"`
Content string `json:"content,omitempty"`
ToolCalls []openaiToolCall `json:"tool_calls,omitempty"` // assistant requesting tools
ToolCallID string `json:"tool_call_id,omitempty"` // role=tool result
Name string `json:"name,omitempty"` // tool name for role=tool
}
type openaiToolCallFunction struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
}
type openaiToolCall struct {
Index *int `json:"index,omitempty"` // present in streaming deltas
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"` // "function"
Function openaiToolCallFunction `json:"function"`
}
type openaiToolDefFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters json.RawMessage `json:"parameters"`
}
type openaiToolDef struct {
Type string `json:"type"` // "function"
Function openaiToolDefFunction `json:"function"`
}
type openaiChatRequest struct {
@@ -231,6 +346,7 @@ type openaiChatRequest struct {
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"`
Stream bool `json:"stream"`
Tools []openaiToolDef `json:"tools,omitempty"`
}
type openaiChatResponse struct {
@@ -249,7 +365,8 @@ type openaiStreamChunk struct {
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
Content string `json:"content"`
ToolCalls []openaiToolCall `json:"tool_calls,omitempty"`
} `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`