fix
This commit is contained in:
@@ -256,11 +256,14 @@ type chatResponse struct {
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
var errEmptyModelResponse = errors.New("empty model response")
|
||||
var (
|
||||
errEmptyModelResponse = errors.New("empty model response")
|
||||
errLengthCappedResponse = errors.New("response truncated at max_tokens")
|
||||
)
|
||||
|
||||
// maxTokensReasoningBudget is used when a capped completion returns empty
|
||||
// content with finish_reason=length (reasoning models).
|
||||
const maxTokensReasoningBudget = 4096
|
||||
func isLengthBudgetErr(err error) bool {
|
||||
return errors.Is(err, errEmptyModelResponse) || errors.Is(err, errLengthCappedResponse)
|
||||
}
|
||||
|
||||
func (c *OpenAIClient) Complete(ctx context.Context, system, user string) (Completion, error) {
|
||||
return c.CompleteWithOptions(ctx, system, user, CompleteOptions{})
|
||||
@@ -300,16 +303,19 @@ func (c *OpenAIClient) CompleteWithOptions(ctx context.Context, system, user str
|
||||
return comp, nil
|
||||
}
|
||||
lastErr = err
|
||||
if errors.Is(err, errEmptyModelResponse) {
|
||||
if maxTok <= 0 || maxTok < maxTokensReasoningBudget {
|
||||
if maxTok < maxTokensReasoningBudget {
|
||||
maxTok = maxTokensReasoningBudget
|
||||
if isLengthBudgetErr(err) {
|
||||
if maxTok <= 0 || maxTok < MaxTokensEnhanceRetry {
|
||||
prev := maxTok
|
||||
if maxTok < MaxTokensEnhanceRetry {
|
||||
maxTok = MaxTokensEnhanceRetry
|
||||
}
|
||||
log.Printf("openai: length-cap retry model=%s prev_max_tokens=%d next_max_tokens=%d err=%s",
|
||||
c.Model, prev, maxTok, TruncateError(err))
|
||||
retryable = true
|
||||
} else {
|
||||
// Already at reasoning budget — another 240s call will not help.
|
||||
// Already at enhance retry ceiling — another long call will not help.
|
||||
return Completion{}, fmt.Errorf(
|
||||
"openai empty response at max_tokens=%d for model %q (try a faster non-reasoning model): %w",
|
||||
"openai length-capped at max_tokens=%d for model %q (prefer a faster non-reasoning product-enhance model): %w",
|
||||
maxTok, c.Model, err)
|
||||
}
|
||||
}
|
||||
@@ -488,34 +494,41 @@ func (c *OpenAIClient) doComplete(ctx context.Context, system, user string, temp
|
||||
|
||||
stopWait := c.startWaitLogger("chat/completions", maxTokens)
|
||||
res, err := c.HTTPClient.Do(req)
|
||||
stopWait(err)
|
||||
if err != nil {
|
||||
stopWait(err)
|
||||
wrapped, retryable := classifyOpenAITransportErr(c.Model, err)
|
||||
return Completion{}, retryable, wrapped
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
stopWait(err)
|
||||
wrapped, retryable := classifyOpenAITransportErr(c.Model, err)
|
||||
return Completion{}, retryable, wrapped
|
||||
}
|
||||
var parsed chatResponse
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return Completion{}, false, fmt.Errorf("openai decode: %w", err)
|
||||
decErr := fmt.Errorf("openai decode: %w", err)
|
||||
stopWait(decErr)
|
||||
return Completion{}, false, decErr
|
||||
}
|
||||
if res.StatusCode == http.StatusTooManyRequests || res.StatusCode >= 500 {
|
||||
msg := "rate limited or server error"
|
||||
if parsed.Error != nil && parsed.Error.Message != "" {
|
||||
msg = TruncateError(errors.New(parsed.Error.Message))
|
||||
}
|
||||
return Completion{}, true, errors.New(msg)
|
||||
httpErr := errors.New(msg)
|
||||
stopWait(httpErr)
|
||||
return Completion{}, true, httpErr
|
||||
}
|
||||
if res.StatusCode >= 400 {
|
||||
msg := fmt.Sprintf("openai http %d", res.StatusCode)
|
||||
if parsed.Error != nil && parsed.Error.Message != "" {
|
||||
msg = TruncateError(errors.New(parsed.Error.Message))
|
||||
}
|
||||
return Completion{}, false, errors.New(msg)
|
||||
httpErr := errors.New(msg)
|
||||
stopWait(httpErr)
|
||||
return Completion{}, false, httpErr
|
||||
}
|
||||
text := ""
|
||||
finishReason := ""
|
||||
@@ -523,18 +536,44 @@ func (c *OpenAIClient) doComplete(ctx context.Context, system, user string, temp
|
||||
finishReason = strings.TrimSpace(parsed.Choices[0].FinishReason)
|
||||
text = SanitizeOutput(choiceMessageText(parsed.Choices[0].Message.Content, parsed.Choices[0].Message.ReasoningContent, parsed.Choices[0].Message.Reasoning))
|
||||
}
|
||||
usagePrompt := parsed.Usage.PromptTokens
|
||||
usageOut := parsed.Usage.CompletionTokens
|
||||
usageTotal := parsed.Usage.TotalTokens
|
||||
canBump := maxTokens <= 0 || maxTokens < MaxTokensEnhanceRetry
|
||||
lengthCapped := strings.EqualFold(finishReason, "length")
|
||||
if text == "" {
|
||||
// Reasoning models often return empty content when max_tokens cuts mid-thought.
|
||||
// Only retry when CompleteWithOptions can still raise max_tokens.
|
||||
canBump := maxTokens <= 0 || maxTokens < maxTokensReasoningBudget
|
||||
retryable := strings.EqualFold(finishReason, "length") && canBump
|
||||
// Never log HTTP-ok as ok=1 when content is empty (prod looked "successful" at 30–272ms).
|
||||
retryable := lengthCapped && canBump
|
||||
emptyErr := fmt.Errorf("%w (finish_reason=%s max_tokens=%d prompt_tokens=%d completion_tokens=%d)",
|
||||
errEmptyModelResponse, finishReason, maxTokens, usagePrompt, usageOut)
|
||||
stopWait(emptyErr)
|
||||
log.Printf("openai: chat content empty model=%s finish_reason=%s max_tokens=%d prompt_tokens=%d completion_tokens=%d total_tokens=%d retryable=%t",
|
||||
c.Model, finishReason, maxTokens, usagePrompt, usageOut, usageTotal, retryable)
|
||||
return Completion{}, retryable, errEmptyModelResponse
|
||||
}
|
||||
if lengthCapped {
|
||||
// Truncated JSON was previously treated as success → parse_failed → synthesize.
|
||||
// Accept only when the truncated body still parses as a JSON object.
|
||||
if _, err := ParseJSONObject(text); err != nil {
|
||||
retryable := canBump
|
||||
truncErr := fmt.Errorf("%w (finish_reason=length max_tokens=%d prompt_tokens=%d completion_tokens=%d content_runes=%d)",
|
||||
errLengthCappedResponse, maxTokens, usagePrompt, usageOut, len([]rune(text)))
|
||||
stopWait(truncErr)
|
||||
log.Printf("openai: chat content truncated model=%s finish_reason=length max_tokens=%d prompt_tokens=%d completion_tokens=%d total_tokens=%d content_runes=%d retryable=%t",
|
||||
c.Model, maxTokens, usagePrompt, usageOut, usageTotal, len([]rune(text)), retryable)
|
||||
return Completion{}, retryable, errLengthCappedResponse
|
||||
}
|
||||
}
|
||||
stopWait(nil)
|
||||
log.Printf("openai: chat content ok model=%s finish_reason=%s content_runes=%d prompt_tokens=%d completion_tokens=%d total_tokens=%d",
|
||||
c.Model, finishReason, len([]rune(text)), usagePrompt, usageOut, usageTotal)
|
||||
return Completion{
|
||||
Text: text,
|
||||
PromptTokens: parsed.Usage.PromptTokens,
|
||||
OutputTokens: parsed.Usage.CompletionTokens,
|
||||
TotalTokens: parsed.Usage.TotalTokens,
|
||||
PromptTokens: usagePrompt,
|
||||
OutputTokens: usageOut,
|
||||
TotalTokens: usageTotal,
|
||||
Model: parsed.Model,
|
||||
Raw: map[string]any{
|
||||
"model": parsed.Model,
|
||||
@@ -678,7 +717,7 @@ func inventHeuristicDescription(system, user, name string) string {
|
||||
name = "Product"
|
||||
}
|
||||
cat := labeledPromptValue(user, "category:")
|
||||
if isPromptLabelTitle(cat) {
|
||||
if isUnusableCategoryValue(cat, name) {
|
||||
cat = ""
|
||||
}
|
||||
lang := languageCodeFromEnhancePrompt(system, user)
|
||||
|
||||
Reference in New Issue
Block a user