diff --git a/README.md b/README.md index 6e4fbbea..b8dc4e5d 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,15 @@ cat README.md | gh models run gpt-4o-mini "summarize this text" ### Building -Run `script/build`. +Run `script/build`. Now you can run the binary locally, e.g. `./gh-models list` + +### Releasing + +`gh extension upgrade github/gh-models` or `gh extension install github/gh-models` will pull the latest release, not the latest commit, so all changes require cutting a new release: + +```shell +git tag v0.0.x main +git push origin tag v0.0.x +``` + +This will trigger the `release` action that runs the actual production build. \ No newline at end of file diff --git a/cmd/run/run.go b/cmd/run/run.go index 11b174bd..69928884 100644 --- a/cmd/run/run.go +++ b/cmd/run/run.go @@ -391,18 +391,21 @@ func NewRunCommand() *cobra.Command { sp.Stop() for _, choice := range completion.Choices { - if choice.Delta != nil { - if choice.Delta.Content == nil { - continue - } - - messageBuilder.WriteString(*choice.Delta.Content) - io.WriteString(out, *choice.Delta.Content) + // Streamed responses from the OpenAI API have their data in `.Delta`, while + // non-streamed responses use `.Message`, so let's support both + if choice.Delta != nil && choice.Delta.Content != nil { + content := choice.Delta.Content + messageBuilder.WriteString(*content) + io.WriteString(out, *content) + } else if choice.Message != nil && choice.Message.Content != nil { + content := choice.Message.Content + messageBuilder.WriteString(*content) + io.WriteString(out, *content) + } - // Introduce a small delay in between response tokens to better simulate a conversation - if terminal.IsTerminalOutput() { - time.Sleep(10 * time.Millisecond) - } + // Introduce a small delay in between response tokens to better simulate a conversation + if terminal.IsTerminalOutput() { + time.Sleep(10 * time.Millisecond) } } } diff --git a/internal/azure_models/client.go b/internal/azure_models/client.go index db80447e..2fcfbe5f 100644 --- a/internal/azure_models/client.go +++ b/internal/azure_models/client.go @@ -31,7 +31,12 @@ func NewClient(authToken string) *Client { } func (c *Client) GetChatCompletionStream(req ChatCompletionOptions) (*ChatCompletionResponse, error) { - req.Stream = true + // Check if the model name is `o1-mini` or `o1-preview` + if req.Model == "o1-mini" || req.Model == "o1-preview" { + req.Stream = false + } else { + req.Stream = true + } bodyBytes, err := json.Marshal(req) if err != nil { @@ -60,7 +65,20 @@ func (c *Client) GetChatCompletionStream(req ChatCompletionOptions) (*ChatComple } var chatCompletionResponse ChatCompletionResponse - chatCompletionResponse.Reader = sse.NewEventReader[ChatCompletion](resp.Body) + + if req.Stream { + // Handle streamed response + chatCompletionResponse.Reader = sse.NewEventReader[ChatCompletion](resp.Body) + } else { + var completion ChatCompletion + if err := json.NewDecoder(resp.Body).Decode(&completion); err != nil { + return nil, err + } + + // Create a mock reader that returns the decoded completion + mockReader := sse.NewMockEventReader([]ChatCompletion{completion}) + chatCompletionResponse.Reader = mockReader + } return &chatCompletionResponse, nil } diff --git a/internal/azure_models/types.go b/internal/azure_models/types.go index 9a9cd0f4..c3f7acfb 100644 --- a/internal/azure_models/types.go +++ b/internal/azure_models/types.go @@ -50,7 +50,7 @@ type ChatCompletion struct { } type ChatCompletionResponse struct { - Reader *sse.EventReader[ChatCompletion] + Reader sse.Reader[ChatCompletion] } type modelCatalogSearchResponse struct { diff --git a/internal/sse/mockeventreader.go b/internal/sse/mockeventreader.go new file mode 100644 index 00000000..aa015a79 --- /dev/null +++ b/internal/sse/mockeventreader.go @@ -0,0 +1,37 @@ +package sse + +import ( + "bufio" + "bytes" + "io" +) + +// MockEventReader is a mock implementation of the sse.EventReader. This lets us use EventReader as a common interface +// for models that support streaming (like gpt-4o) and models that do not (like the o1 class of models) +type MockEventReader[T any] struct { + reader io.ReadCloser + scanner *bufio.Scanner + events []T + index int +} + +func NewMockEventReader[T any](events []T) *MockEventReader[T] { + data := []byte{} + reader := io.NopCloser(bytes.NewReader(data)) + scanner := bufio.NewScanner(reader) + return &MockEventReader[T]{reader: reader, scanner: scanner, events: events, index: 0} +} + +func (mer *MockEventReader[T]) Read() (T, error) { + if mer.index >= len(mer.events) { + var zero T + return zero, io.EOF + } + event := mer.events[mer.index] + mer.index++ + return event, nil +} + +func (mer *MockEventReader[T]) Close() error { + return mer.reader.Close() +}