Expose version/build metadata and improve provider error messaging
This commit is contained in:
@@ -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}
|
||||
}
|
||||
|
@@ -1,5 +1,10 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChatMessage represents a single message within a chat completion request or response.
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
@@ -50,6 +55,48 @@ type ChatCompletionStreamEvent struct {
|
||||
// ChatCompletionStreamHandler consumes streaming completion events.
|
||||
type ChatCompletionStreamHandler func(ChatCompletionStreamEvent) error
|
||||
|
||||
// RequestError captures an error response returned by the API together with the HTTP status code.
|
||||
type RequestError struct {
|
||||
Status int
|
||||
Response ErrorResponse
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *RequestError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
msg := strings.TrimSpace(e.Response.Error.Message)
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("api error (status %d)", e.Status)
|
||||
}
|
||||
return fmt.Sprintf("api error (status %d): %s", e.Status, msg)
|
||||
}
|
||||
|
||||
// StatusCode returns the originating HTTP status code.
|
||||
func (e *RequestError) StatusCode() int {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return e.Status
|
||||
}
|
||||
|
||||
// Message returns the raw message provided by the API, if any.
|
||||
func (e *RequestError) Message() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Response.Error.Message
|
||||
}
|
||||
|
||||
// Type returns the OpenAI error type string, when present.
|
||||
func (e *RequestError) Type() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Response.Error.Type
|
||||
}
|
||||
|
||||
// APIError captures structured error responses returned by the API.
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
|
Reference in New Issue
Block a user