Expose version/build metadata and improve provider error messaging

This commit is contained in:
2025-10-01 22:21:44 +02:00
parent a3e6b105d0
commit 7d4d56671f
5 changed files with 278 additions and 27 deletions

View File

@@ -22,6 +22,55 @@ type Client struct {
httpClient *http.Client
}
type contentPart struct {
Type string `json:"type"`
Text string `json:"text"`
}
func extractRoleAndContent(raw json.RawMessage) (string, string) {
if len(raw) == 0 || string(raw) == "null" {
return "", ""
}
var envelope map[string]json.RawMessage
if err := json.Unmarshal(raw, &envelope); err != nil {
return "", ""
}
var role string
if value, ok := envelope["role"]; ok {
_ = json.Unmarshal(value, &role)
}
value, ok := envelope["content"]
if !ok {
return role, ""
}
var text string
if err := json.Unmarshal(value, &text); err == nil {
return role, text
}
var parts []contentPart
if err := json.Unmarshal(value, &parts); err == nil {
var builder strings.Builder
for _, part := range parts {
if part.Text != "" {
builder.WriteString(part.Text)
}
}
return role, builder.String()
}
var single contentPart
if err := json.Unmarshal(value, &single); err == nil {
return role, single.Text
}
return role, ""
}
// ClientOption customizes client construction.
type ClientOption func(*Client)
@@ -131,10 +180,10 @@ func (c *Client) StreamChatCompletion(ctx context.Context, req ChatCompletionReq
ID string `json:"id"`
Object string `json:"object"`
Choices []struct {
Index int `json:"index"`
Message ChatMessage `json:"message"`
Delta ChatMessage `json:"delta"`
FinishReason string `json:"finish_reason"`
Index int `json:"index"`
Message json.RawMessage `json:"message"`
Delta json.RawMessage `json:"delta"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage Usage `json:"usage"`
}
@@ -145,6 +194,7 @@ func (c *Client) StreamChatCompletion(ctx context.Context, req ChatCompletionReq
finish := ""
var usage Usage
usageReceived := false
var lastMessageText string
for scanner.Scan() {
line := scanner.Text()
@@ -190,19 +240,22 @@ func (c *Client) StreamChatCompletion(ctx context.Context, req ChatCompletionReq
finishReason := ""
if len(chunk.Choices) > 0 {
choice := chunk.Choices[0]
if choice.Message.Role != "" {
role = choice.Message.Role
choiceRole, choiceContent := extractRoleAndContent(choice.Message)
if choiceRole != "" {
role = choiceRole
}
if choice.Delta.Role != "" {
role = choice.Delta.Role
deltaRole, deltaContent := extractRoleAndContent(choice.Delta)
if deltaRole != "" {
role = deltaRole
}
if choice.Delta.Content != "" {
chunkText = choice.Delta.Content
} else if choice.Message.Content != "" && builder.Len() == 0 {
chunkText = choice.Message.Content
if deltaContent != "" {
chunkText = deltaContent
}
if choice.Message.Content != "" && builder.Len() == 0 && chunkText == "" {
chunkText = choice.Message.Content
if chunkText == "" && builder.Len() == 0 && choiceContent != "" {
chunkText = choiceContent
}
if choiceContent != "" {
lastMessageText = choiceContent
}
if choice.FinishReason != "" {
finishReason = choice.FinishReason
@@ -237,7 +290,23 @@ func (c *Client) StreamChatCompletion(ctx context.Context, req ChatCompletionReq
content := strings.TrimSpace(builder.String())
if content == "" {
return nil, errors.New("stream response contained no content")
if trimmed := strings.TrimSpace(lastMessageText); trimmed != "" {
content = trimmed
}
}
if content == "" {
aggregated.Choices = []ChatCompletionChoice{{
Index: 0,
Message: ChatMessage{
Role: role,
Content: "",
},
FinishReason: finish,
}}
if usageReceived {
aggregated.Usage = usage
}
return &aggregated, nil
}
aggregated.Choices = []ChatCompletionChoice{{
@@ -280,8 +349,13 @@ func decodeSuccess(r io.Reader) (*ChatCompletionResponse, error) {
func decodeError(r io.Reader, status int) error {
var apiErr ErrorResponse
if err := json.NewDecoder(r).Decode(&apiErr); err != nil {
return fmt.Errorf("api error (status %d): failed to decode body: %w", status, err)
return &RequestError{
Status: status,
Response: ErrorResponse{
Error: APIError{Message: fmt.Sprintf("failed to decode error body: %v", err)},
},
}
}
return fmt.Errorf("api error (status %d): %s", status, apiErr.Error.Message)
return &RequestError{Status: status, Response: apiErr}
}