Skip to content

Commit dc2344d

Browse files
committed
resolve merge conflicts
2 parents ebfffde + d7dca83 commit dc2344d

File tree

6 files changed

+182
-108
lines changed

6 files changed

+182
-108
lines changed

image.go

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package openai
33
import (
44
"bytes"
55
"context"
6+
"io"
67
"net/http"
7-
"os"
88
"strconv"
99
)
1010

@@ -132,33 +132,59 @@ func (c *Client) CreateImage(ctx context.Context, request ImageRequest) (respons
132132
return
133133
}
134134

135+
// WrapReader wraps an io.Reader with filename and Content-type.
136+
func WrapReader(rdr io.Reader, filename string, contentType string) io.Reader {
137+
return file{rdr, filename, contentType}
138+
}
139+
140+
type file struct {
141+
io.Reader
142+
name string
143+
contentType string
144+
}
145+
146+
func (f file) Name() string {
147+
if f.name != "" {
148+
return f.name
149+
} else if named, ok := f.Reader.(interface{ Name() string }); ok {
150+
return named.Name()
151+
}
152+
return ""
153+
}
154+
155+
func (f file) ContentType() string {
156+
return f.contentType
157+
}
158+
135159
// ImageEditRequest represents the request structure for the image API.
160+
// Use WrapReader to wrap an io.Reader with filename and Content-type.
136161
type ImageEditRequest struct {
137-
Image *os.File `json:"image,omitempty"`
138-
Mask *os.File `json:"mask,omitempty"`
139-
Prompt string `json:"prompt,omitempty"`
140-
Model string `json:"model,omitempty"`
141-
N int `json:"n,omitempty"`
142-
Size string `json:"size,omitempty"`
143-
ResponseFormat string `json:"response_format,omitempty"`
144-
Quality string `json:"quality,omitempty"`
145-
User string `json:"user,omitempty"`
162+
Image io.Reader `json:"image,omitempty"`
163+
Mask io.Reader `json:"mask,omitempty"`
164+
Prompt string `json:"prompt,omitempty"`
165+
Model string `json:"model,omitempty"`
166+
N int `json:"n,omitempty"`
167+
Size string `json:"size,omitempty"`
168+
ResponseFormat string `json:"response_format,omitempty"`
169+
Quality string `json:"quality,omitempty"`
170+
User string `json:"user,omitempty"`
146171
}
147172

148173
// CreateEditImage - API call to create an image. This is the main endpoint of the DALL-E API.
149174
func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest) (response ImageResponse, err error) {
150175
body := &bytes.Buffer{}
151176
builder := c.createFormBuilder(body)
152177

153-
// image
154-
err = builder.CreateFormFile("image", request.Image)
178+
// image, filename verification can be postponed
179+
err = builder.CreateFormFileReader("image", request.Image, "")
155180
if err != nil {
156181
return
157182
}
158183

159184
// mask, it is optional
160185
if request.Mask != nil {
161-
err = builder.CreateFormFile("mask", request.Mask)
186+
// filename verification can be postponed
187+
err = builder.CreateFormFileReader("mask", request.Mask, "")
162188
if err != nil {
163189
return
164190
}
@@ -205,13 +231,14 @@ func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest)
205231
}
206232

207233
// ImageVariRequest represents the request structure for the image API.
234+
// Use WrapReader to wrap an io.Reader with filename and Content-type.
208235
type ImageVariRequest struct {
209-
Image *os.File `json:"image,omitempty"`
210-
Model string `json:"model,omitempty"`
211-
N int `json:"n,omitempty"`
212-
Size string `json:"size,omitempty"`
213-
ResponseFormat string `json:"response_format,omitempty"`
214-
User string `json:"user,omitempty"`
236+
Image io.Reader `json:"image,omitempty"`
237+
Model string `json:"model,omitempty"`
238+
N int `json:"n,omitempty"`
239+
Size string `json:"size,omitempty"`
240+
ResponseFormat string `json:"response_format,omitempty"`
241+
User string `json:"user,omitempty"`
215242
}
216243

217244
// CreateVariImage - API call to create an image variation. This is the main endpoint of the DALL-E API.
@@ -220,8 +247,8 @@ func (c *Client) CreateVariImage(ctx context.Context, request ImageVariRequest)
220247
body := &bytes.Buffer{}
221248
builder := c.createFormBuilder(body)
222249

223-
// image
224-
err = builder.CreateFormFile("image", request.Image)
250+
// image, filename verification can be postponed
251+
err = builder.CreateFormFileReader("image", request.Image, "")
225252
if err != nil {
226253
return
227254
}

image_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ func TestImageFormBuilderFailures(t *testing.T) {
5454
}
5555

5656
mockFailedErr := fmt.Errorf("mock form builder fail")
57-
mockBuilder.mockCreateFormFile = func(string, *os.File) error {
57+
mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
5858
return mockFailedErr
5959
}
6060
_, err := client.CreateEditImage(ctx, req)
6161
checks.ErrorIs(t, err, mockFailedErr, "CreateImage should return error if form builder fails")
6262

63-
mockBuilder.mockCreateFormFile = func(name string, _ *os.File) error {
63+
mockBuilder.mockCreateFormFileReader = func(name string, _ io.Reader, _ string) error {
6464
if name == "mask" {
6565
return mockFailedErr
6666
}
@@ -119,13 +119,13 @@ func TestVariImageFormBuilderFailures(t *testing.T) {
119119
req := ImageVariRequest{}
120120

121121
mockFailedErr := fmt.Errorf("mock form builder fail")
122-
mockBuilder.mockCreateFormFile = func(string, *os.File) error {
122+
mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
123123
return mockFailedErr
124124
}
125125
_, err := client.CreateVariImage(ctx, req)
126126
checks.ErrorIs(t, err, mockFailedErr, "CreateVariImage should return error if form builder fails")
127127

128-
mockBuilder.mockCreateFormFile = func(string, *os.File) error {
128+
mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
129129
return nil
130130
}
131131

internal/form_builder.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"fmt"
55
"io"
66
"mime/multipart"
7+
"net/textproto"
78
"os"
8-
"path"
9+
"path/filepath"
10+
"strings"
911
)
1012

1113
type FormBuilder interface {
@@ -30,8 +32,50 @@ func (fb *DefaultFormBuilder) CreateFormFile(fieldname string, file *os.File) er
3032
return fb.createFormFile(fieldname, file, file.Name())
3133
}
3234

35+
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
36+
37+
func escapeQuotes(s string) string {
38+
return quoteEscaper.Replace(s)
39+
}
40+
41+
// CreateFormFileReader creates a form field with a file reader.
42+
// The filename in Content-Disposition is required.
3343
func (fb *DefaultFormBuilder) CreateFormFileReader(fieldname string, r io.Reader, filename string) error {
34-
return fb.createFormFile(fieldname, r, path.Base(filename))
44+
if filename == "" {
45+
if f, ok := r.(interface{ Name() string }); ok {
46+
filename = f.Name()
47+
}
48+
}
49+
var contentType string
50+
if f, ok := r.(interface{ ContentType() string }); ok {
51+
contentType = f.ContentType()
52+
}
53+
54+
h := make(textproto.MIMEHeader)
55+
h.Set(
56+
"Content-Disposition",
57+
fmt.Sprintf(
58+
`form-data; name="%s"; filename="%s"`,
59+
escapeQuotes(fieldname),
60+
escapeQuotes(filepath.Base(filename)),
61+
),
62+
)
63+
// content type is optional, but it can be set
64+
if contentType != "" {
65+
h.Set("Content-Type", contentType)
66+
}
67+
68+
fieldWriter, err := fb.writer.CreatePart(h)
69+
if err != nil {
70+
return err
71+
}
72+
73+
_, err = io.Copy(fieldWriter, r)
74+
if err != nil {
75+
return err
76+
}
77+
78+
return nil
3579
}
3680

3781
func (fb *DefaultFormBuilder) createFormFile(fieldname string, r io.Reader, filename string) error {

internal/form_builder_test.go

Lines changed: 32 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package openai //nolint:testpackage // testing private field
22

33
import (
44
"errors"
5-
"strings"
5+
"io"
66

77
"github.com/sashabaranov/go-openai/internal/test/checks"
88

@@ -87,94 +87,47 @@ func TestFormBuilderWithClosedFile(t *testing.T) {
8787
checks.ErrorIs(t, err, os.ErrClosed, "formbuilder should return error if file is closed")
8888
}
8989

90-
func TestMultiPartFormUploads(t *testing.T) {
91-
body := &bytes.Buffer{}
92-
builder := NewFormBuilder(body)
93-
94-
t.Run("MultipleFiles", func(t *testing.T) {
95-
file1, _ := os.CreateTemp(t.TempDir(), "*.png")
96-
file2, _ := os.CreateTemp(t.TempDir(), "*.jpg")
97-
defer file1.Close()
98-
defer file2.Close()
99-
100-
checks.NoError(t, builder.CreateFormFile("image1", file1), "PNG file upload failed")
101-
checks.NoError(t, builder.CreateFormFile("image2", file2), "JPG file upload failed")
102-
checks.NoError(t, builder.WriteField("description", "test images"), "Field write failed")
103-
})
104-
105-
t.Run("LargeFileConcurrent", func(t *testing.T) {
106-
bigFile, _ := os.CreateTemp(t.TempDir(), "*.bin")
107-
defer bigFile.Close()
108-
_, err := bigFile.Write(make([]byte, 1024*1024*5)) // 5MB test file
109-
checks.NoError(t, err, "Failed to write large file data")
110-
checks.NoError(t, builder.CreateFormFile("bigfile", bigFile), "Large file upload failed")
111-
checks.NoError(t, builder.WriteField("note", "large file test"), "Field write failed")
112-
})
90+
type failingReader struct {
91+
}
11392

114-
t.Run("MixedContentTypes", func(t *testing.T) {
115-
csvFile, _ := os.CreateTemp(t.TempDir(), "*.csv")
116-
textFile, _ := os.CreateTemp(t.TempDir(), "*.txt")
117-
defer csvFile.Close()
118-
defer textFile.Close()
93+
var errMockFailingReaderError = errors.New("mock reader failed")
11994

120-
checks.NoError(t, builder.CreateFormFile("data", csvFile), "CSV file upload failed")
121-
checks.NoError(t, builder.CreateFormFile("text", textFile), "Text file upload failed")
122-
checks.NoError(t, builder.WriteField("format", "mixed"), "Field write failed")
123-
})
95+
func (*failingReader) Read([]byte) (int, error) {
96+
return 0, errMockFailingReaderError
12497
}
12598

126-
func TestFormDataContentType(t *testing.T) {
127-
body := &bytes.Buffer{}
128-
builder := NewFormBuilder(body)
129-
contentType := builder.FormDataContentType()
130-
if !strings.HasPrefix(contentType, "multipart/form-data") {
131-
t.Fatalf("Content-Type格式错误,期望multipart/form-data开头,实际得到:%s", contentType)
132-
}
99+
type readerWithNameAndContentType struct {
100+
io.Reader
133101
}
134102

135-
func TestCreateFormFileReader(t *testing.T) {
136-
body := &bytes.Buffer{}
137-
builder := NewFormBuilder(body)
138-
139-
t.Run("SpecialCharacters", func(t *testing.T) {
140-
checks.NoError(t, builder.CreateFormFileReader("field", strings.NewReader("content"), "测 试@file.txt"), "特殊字符文件名应处理成功")
141-
})
142-
143-
t.Run("InvalidReader", func(t *testing.T) {
144-
err := builder.CreateFormFileReader("field", &failingReader{}, "valid.txt")
145-
checks.HasError(t, err, "无效reader应返回错误")
146-
})
103+
func (*readerWithNameAndContentType) Name() string {
104+
return ""
147105
}
148106

149-
type failingReader struct{}
150-
151-
func (r *failingReader) Read(_ []byte) (int, error) {
152-
return 0, errors.New("mock read error")
107+
func (*readerWithNameAndContentType) ContentType() string {
108+
return "image/png"
153109
}
154110

155-
func TestWriteFieldEdgeCases(t *testing.T) {
156-
mockErr := errors.New("mock write error")
157-
t.Run("EmptyFieldName", func(t *testing.T) {
158-
body := &bytes.Buffer{}
159-
builder := NewFormBuilder(body)
160-
err := builder.WriteField("", "valid-value")
161-
checks.HasError(t, err, "should return error for empty field name")
162-
})
111+
func TestFormBuilderWithReader(t *testing.T) {
112+
file, err := os.CreateTemp(t.TempDir(), "")
113+
if err != nil {
114+
t.Fatalf("Error creating tmp file: %v", err)
115+
}
116+
defer file.Close()
117+
builder := NewFormBuilder(&failingWriter{})
118+
err = builder.CreateFormFileReader("file", file, file.Name())
119+
checks.ErrorIs(t, err, errMockFailingWriterError, "formbuilder should return error if writer fails")
163120

164-
t.Run("EmptyValue", func(t *testing.T) {
165-
body := &bytes.Buffer{}
166-
builder := NewFormBuilder(body)
167-
err := builder.WriteField("valid-field", "")
168-
checks.NoError(t, err, "should allow empty value")
169-
})
121+
builder = NewFormBuilder(&bytes.Buffer{})
122+
reader := &failingReader{}
123+
err = builder.CreateFormFileReader("file", reader, "")
124+
checks.ErrorIs(t, err, errMockFailingReaderError, "formbuilder should return error if copy reader fails")
170125

171-
t.Run("MockWriterFailure", func(t *testing.T) {
172-
mockBuilder := &mockFormBuilder{
173-
mockWriteField: func(_, _ string) error {
174-
return mockErr
175-
},
176-
}
177-
err := mockBuilder.WriteField("field", "value")
178-
checks.ErrorIs(t, err, mockErr, "should propagate write error")
179-
})
126+
successReader := &bytes.Buffer{}
127+
err = builder.CreateFormFileReader("file", successReader, "")
128+
checks.NoError(t, err, "formbuilder should not return error")
129+
130+
rnc := &readerWithNameAndContentType{Reader: &bytes.Buffer{}}
131+
err = builder.CreateFormFileReader("file", rnc, "")
132+
checks.NoError(t, err, "formbuilder should not return error")
180133
}

jsonschema/json.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,12 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
126126
}
127127
jsonTag := field.Tag.Get("json")
128128
var required = true
129-
if jsonTag == "" {
129+
switch {
130+
case jsonTag == "-":
131+
continue
132+
case jsonTag == "":
130133
jsonTag = field.Name
131-
} else if strings.HasSuffix(jsonTag, ",omitempty") {
134+
case strings.HasSuffix(jsonTag, ",omitempty"):
132135
jsonTag = strings.TrimSuffix(jsonTag, ",omitempty")
133136
required = false
134137
}

0 commit comments

Comments
 (0)