mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 16:10:59 +00:00
bdb60de7ae
- Update go.mod and Dockerfile to Go 1.26 - Apply `go fix ./...` stdlib modernizations across 170+ files - Add `go fix` to post-implementation checklist in CLAUDE.md - Fix go fix misapplied rewrite in loop_history.go
180 lines
4.2 KiB
Go
180 lines
4.2 KiB
Go
package providers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// buildRequestBody converts internal ChatRequest to Responses API format.
|
|
func (p *CodexProvider) buildRequestBody(req ChatRequest, stream bool) map[string]any {
|
|
model := req.Model
|
|
if model == "" {
|
|
model = p.defaultModel
|
|
}
|
|
|
|
var instructions string
|
|
var input []any
|
|
|
|
for _, m := range req.Messages {
|
|
switch m.Role {
|
|
case "system":
|
|
if instructions == "" {
|
|
instructions = m.Content
|
|
} else {
|
|
instructions += "\n\n" + m.Content
|
|
}
|
|
|
|
case "user":
|
|
if len(m.Images) > 0 {
|
|
var parts []map[string]any
|
|
for _, img := range m.Images {
|
|
parts = append(parts, map[string]any{
|
|
"type": "input_image",
|
|
"image_url": fmt.Sprintf("data:%s;base64,%s", img.MimeType, img.Data),
|
|
})
|
|
}
|
|
if m.Content != "" {
|
|
parts = append(parts, map[string]any{
|
|
"type": "input_text",
|
|
"text": m.Content,
|
|
})
|
|
}
|
|
input = append(input, map[string]any{
|
|
"role": "user",
|
|
"content": parts,
|
|
})
|
|
} else {
|
|
input = append(input, map[string]any{
|
|
"role": "user",
|
|
"content": m.Content,
|
|
})
|
|
}
|
|
|
|
case "assistant":
|
|
if len(m.ToolCalls) > 0 {
|
|
for _, tc := range m.ToolCalls {
|
|
argsJSON, _ := json.Marshal(tc.Arguments)
|
|
callID := toFcID(tc.ID)
|
|
input = append(input, map[string]any{
|
|
"type": "function_call",
|
|
"id": callID,
|
|
"call_id": callID,
|
|
"name": tc.Name,
|
|
"arguments": string(argsJSON),
|
|
})
|
|
}
|
|
}
|
|
if m.Content != "" {
|
|
item := map[string]any{
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": []map[string]any{
|
|
{"type": "output_text", "text": m.Content},
|
|
},
|
|
}
|
|
if m.Phase != "" {
|
|
item["phase"] = m.Phase
|
|
}
|
|
input = append(input, item)
|
|
}
|
|
|
|
case "tool":
|
|
input = append(input, map[string]any{
|
|
"type": "function_call_output",
|
|
"call_id": toFcID(m.ToolCallID),
|
|
"output": m.Content,
|
|
})
|
|
}
|
|
}
|
|
|
|
body := map[string]any{
|
|
"model": model,
|
|
"input": input,
|
|
"stream": stream,
|
|
"store": false,
|
|
}
|
|
|
|
if instructions == "" {
|
|
instructions = "You are a helpful assistant."
|
|
}
|
|
body["instructions"] = instructions
|
|
|
|
if len(req.Tools) > 0 {
|
|
var tools []map[string]any
|
|
for _, t := range req.Tools {
|
|
tools = append(tools, map[string]any{
|
|
"type": "function",
|
|
"name": t.Function.Name,
|
|
"description": t.Function.Description,
|
|
"parameters": t.Function.Parameters,
|
|
})
|
|
}
|
|
body["tools"] = tools
|
|
}
|
|
|
|
if level, ok := req.Options[OptThinkingLevel].(string); ok && level != "" && level != "off" {
|
|
body["reasoning"] = map[string]any{"effort": level}
|
|
}
|
|
|
|
return body
|
|
}
|
|
|
|
func (p *CodexProvider) doRequest(ctx context.Context, body any) (io.ReadCloser, error) {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: marshal request: %w", p.name, err)
|
|
}
|
|
|
|
endpoint := p.apiBase + "/codex/responses"
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: create request: %w", p.name, err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
token, err := p.tokenSource.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: get auth token: %w", p.name, err)
|
|
}
|
|
httpReq.Header.Set("Authorization", "Bearer "+token)
|
|
httpReq.Header.Set("OpenAI-Beta", "responses=v1")
|
|
|
|
resp, err := p.client.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: request failed: %w", p.name, err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
resp.Body.Close()
|
|
retryAfter := ParseRetryAfter(resp.Header.Get("Retry-After"))
|
|
return nil, &HTTPError{
|
|
Status: resp.StatusCode,
|
|
Body: fmt.Sprintf("%s: %s", p.name, string(respBody)),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
return resp.Body, nil
|
|
}
|
|
|
|
// toFcID ensures a tool call ID starts with "fc_" as required by the Responses API.
|
|
func toFcID(id string) string {
|
|
if strings.HasPrefix(id, "fc_") {
|
|
return id
|
|
}
|
|
if strings.HasPrefix(id, "tool_") {
|
|
return "fc_" + id[len("tool_"):]
|
|
}
|
|
if strings.HasPrefix(id, "call_") {
|
|
return "fc_" + id[len("call_"):]
|
|
}
|
|
return "fc_" + id
|
|
}
|