Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/goccy/go-yaml v1.18.0
github.com/itchyny/json2yaml v0.1.4
github.com/muesli/reflow v0.3.0
github.com/openai/openai-go/v3 v3.51.0
github.com/openai/openai-go/v3 v3.52.0
github.com/stretchr/testify v1.10.0
github.com/tidwall/gjson v1.19.0
github.com/tidwall/pretty v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/openai/openai-go/v3 v3.51.0 h1:+ys88LqUflSr0nRM37aWxkMMpHn+zqzVJGIK89eumdM=
github.com/openai/openai-go/v3 v3.51.0/go.mod h1:Vy3y2/I2H/MbqvJGXEK8VbN5+avZV6zxux4I3eBdvaA=
github.com/openai/openai-go/v3 v3.52.0 h1:VDSjIvI5Sr2/AzGJI6219sM2Il+zBWuopvluMy6KdjE=
github.com/openai/openai-go/v3 v3.52.0/go.mod h1:Vy3y2/I2H/MbqvJGXEK8VbN5+avZV6zxux4I3eBdvaA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
Expand Down
77 changes: 22 additions & 55 deletions pkg/cmd/flagoptions.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package cmd

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"maps"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"unicode/utf8"
Expand Down Expand Up @@ -121,7 +119,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi
val := iter.Value()
newVal, err := embedFilesValue(val, embedStyle, stdin)
if err != nil {
return reflect.Value{}, err
return reflect.Value{}, errors.Join(err, closeFileUploads(result.Interface()))
}
result.SetMapIndex(key, newVal)
}
Expand All @@ -136,7 +134,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi
for i := 0; i < v.Len(); i++ {
newVal, err := embedFilesValue(v.Index(i), embedStyle, stdin)
if err != nil {
return reflect.Value{}, err
return reflect.Value{}, errors.Join(err, closeFileUploads(result.Slice(0, i).Interface()))
}
result.Index(i).Set(newVal)
}
Expand Down Expand Up @@ -275,7 +273,7 @@ func embedFilesValue(v reflect.Value, embedStyle FileEmbedStyle, stdin *onceStdi

upload, err := openFileUpload(filename)
if err != nil {
if !expectsFile {
if !expectsFile && errors.Is(err, fs.ErrNotExist) {
// For strings that start with "@" and don't look like a filename, return the string
return v, nil
}
Expand Down Expand Up @@ -324,8 +322,16 @@ func flagOptions(
// This parameter is true if stdin is already in use to pass a binary parameter by using the special value
// "-". In this case, we won't attempt to read it as a JSON/YAML blob for options setting.
ignoreStdin bool,
) ([]option.RequestOption, error) {
var options []option.RequestOption
) (options []option.RequestOption, err error) {
// Once multipart files have been opened, close them on every error path
// until ownership is transferred to multipartRequestBody below.
var pendingMultipartUploads any
defer func() {
if err != nil && pendingMultipartUploads != nil {
err = errors.Join(err, closeFileUploads(pendingMultipartUploads))
}
}()

if cmd.Bool("debug") {
options = append(options, option.WithMiddleware(debugmiddleware.NewRequestLogger().Middleware()))
}
Expand Down Expand Up @@ -434,6 +440,9 @@ func flagOptions(
return nil, err
} else {
requestContents.Body = embedded
if bodyType == MultipartFormEncoded {
pendingMultipartUploads = embedded
}
}

if headersWithFiles, err := embedFiles(requestContents.Headers, EmbedText, &stdinReader); err != nil {
Expand Down Expand Up @@ -492,22 +501,18 @@ func flagOptions(
case EmptyBody:
break
case MultipartFormEncoded:
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)

// For multipart/form-encoded, we need a map structure
bodyMap, ok := requestContents.Body.(map[string]any)
if !ok {
return nil, fmt.Errorf("Cannot send a non-map value to a form-encoded endpoint: %v\n", requestContents.Body)
}
encodingFormat := apiform.FormatBrackets
if err := apiform.MarshalWithSettings(bodyMap, writer, encodingFormat); err != nil {
return nil, err
}
if err := writer.Close(); err != nil {
multipartOptions, err := multipartRequestOptions(bodyMap, encodingFormat)
if err != nil {
return nil, err
}
options = append(options, option.WithRequestBody(writer.FormDataContentType(), buf))
pendingMultipartUploads = nil // multipartRequestBody owns them now.
options = append(options, multipartOptions...)

case ApplicationJSON:
bodyBytes, err := json.Marshal(requestContents.Body)
Expand Down Expand Up @@ -543,44 +548,6 @@ func flagOptions(
// as a file path without needing the "@" prefix.
type FilePathValue string

// fileUpload wraps an io.Reader with filename and content-type metadata for
// use as a multipart form part. The apiform encoder detects the Filename and
// ContentType methods and uses them to populate the Content-Disposition
// filename and the Content-Type header on the part.
type fileUpload struct {
io.Reader // apiform checks for reader and reads its contents during encode
filename string
contentType string
}

func (f fileUpload) Filename() string { return f.filename }
func (f fileUpload) ContentType() string { return f.contentType }
func (f fileUpload) Close() error {
if c, ok := f.Reader.(io.Closer); ok {
return c.Close()
}
return nil
}

// openFileUpload opens the file at path and returns a fileUpload whose filename
// is the path's basename and whose content type is derived from the file
// extension (falling back to application/octet-stream when unknown).
func openFileUpload(path string) (fileUpload, error) {
file, err := os.Open(path)
if err != nil {
return fileUpload{}, err
}
contentType := mime.TypeByExtension(filepath.Ext(path))
if contentType == "" {
contentType = "application/octet-stream"
}
return fileUpload{
Reader: file,
filename: filepath.Base(path),
contentType: contentType,
}, nil
}

// applyDataAliases rewrites keys in a body map based on flag `DataAliases` metadata. For top-level flags,
// `{alias: value}` becomes `{canonical: value}`. For inner flags (those registered under an outer flag
// via WithInnerFlags), the alias translation is also applied to the nested map under the outer flag's
Expand Down
Loading
Loading