X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/80e348724041a694002fc38f57e8868b9b9a2be9..2a528fe2981639cf099c713a6be16b8ea0e2cc67:/model.go?ds=sidebyside diff --git a/model.go b/model.go index 074ff60..78836fe 100644 --- a/model.go +++ b/model.go @@ -344,7 +344,12 @@ func (c *ModelClient) send(httpRequest *http.Request, record *ModelUsageRecord) return ModelResponse{}, errors.Join(err, readErr) } - return modelReadStream(httpResponse.Body) + response, err = modelReadStream(httpResponse.Body) + if err != nil { + return response, fmt.Errorf("read model response stream: %w", err) + } + + return response, nil } // Write appends record to the file with one write. @@ -374,73 +379,6 @@ func (u *ModelUsageLog) Write(record ModelUsageRecord) (err error) { return nil } -// modelEndpoint returns the URL and the authorizer of the endpoint in config. -// The ChatGPT backend uses the token file in dir. The OpenAI API uses the key -// in OPENAI_API_KEY, and there is no fallback from one to the other. All -// clients share the authorizer, so one refresh runs at a time. -func modelEndpoint(config Config, dir string) (string, ModelAuthorizer, error) { - if config.Endpoint == configEndpointAPI { - key := os.Getenv("OPENAI_API_KEY") - if key == "" { - return "", nil, errors.New("endpoint api needs OPENAI_API_KEY") - } - - return modelAPIURL, ModelAPIKey(key), nil - } - - return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer), nil -} - -// modelUserMessage returns a user input item with one text part for each text. -func modelUserMessage(texts ...string) any { - content := make([]ModelInputPart, len(texts)) - for n, text := range texts { - content[n] = ModelInputPart{ - Type: "input_text", - Text: text, - } - } - - return modelUserParts(content) -} - -// modelUserParts keeps text and image parts in one user message. -func modelUserParts(content []ModelInputPart) any { - return struct { - Role string `json:"role"` - Content []ModelInputPart `json:"content"` - }{ - Role: "user", - Content: content, - } -} - -// modelOutputText returns the text of the message items of output. -func modelOutputText(output []json.RawMessage) (string, error) { - var text strings.Builder - - for _, raw := range output { - var item ModelOutputItem - - err := json.Unmarshal(raw, &item) - if err != nil { - return "", fmt.Errorf("decode output item: %w", err) - } - - if item.Type != "message" { - continue - } - - for _, part := range item.Content { - if part.Type == "output_text" { - text.WriteString(part.Text) - } - } - } - - return text.String(), nil -} - // modelQuotaHeaders returns the x-codex-* headers, with lowercase names, // without the turn state. func modelQuotaHeaders(header http.Header) map[string]string { @@ -530,8 +468,11 @@ func modelReadEvent(response *ModelResponse, data []byte) (bool, error) { } response.Text, err = modelOutputText(response.Output) + if err != nil { + return true, fmt.Errorf("read completed response text: %w", err) + } - return true, err + return true, nil case "response.incomplete": reason := "unknown" if event.Response != nil && event.Response.IncompleteDetails != nil { @@ -552,3 +493,70 @@ func modelReadEvent(response *ModelResponse, data []byte) (bool, error) { return false, nil } + +// modelOutputText returns the text of the message items of output. +func modelOutputText(output []json.RawMessage) (string, error) { + var text strings.Builder + + for _, raw := range output { + var item ModelOutputItem + + err := json.Unmarshal(raw, &item) + if err != nil { + return "", fmt.Errorf("decode output item: %w", err) + } + + if item.Type != "message" { + continue + } + + for _, part := range item.Content { + if part.Type == "output_text" { + text.WriteString(part.Text) + } + } + } + + return text.String(), nil +} + +// modelEndpoint returns the URL and the authorizer of the endpoint in config. +// The ChatGPT backend uses the token file in dir. The OpenAI API uses the key +// in OPENAI_API_KEY, and there is no fallback from one to the other. All +// clients share the authorizer, so one refresh runs at a time. +func modelEndpoint(config Config, dir string) (string, ModelAuthorizer, error) { + if config.Endpoint == configEndpointAPI { + key := os.Getenv("OPENAI_API_KEY") + if key == "" { + return "", nil, errors.New("endpoint api needs OPENAI_API_KEY") + } + + return modelAPIURL, ModelAPIKey(key), nil + } + + return modelChatGPTURL, newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer), nil +} + +// modelUserMessage returns a user input item with one text part for each text. +func modelUserMessage(texts ...string) any { + content := make([]ModelInputPart, len(texts)) + for n, text := range texts { + content[n] = ModelInputPart{ + Type: "input_text", + Text: text, + } + } + + return modelUserParts(content) +} + +// modelUserParts keeps text and image parts in one user message. +func modelUserParts(content []ModelInputPart) any { + return struct { + Role string `json:"role"` + Content []ModelInputPart `json:"content"` + }{ + Role: "user", + Content: content, + } +}