Retain generation response evidence
This commit is contained in:
@@ -101,24 +101,29 @@ func (c *OpenAICompatibleClient) Generate(ctx context.Context, req domain.Genera
|
||||
}
|
||||
|
||||
// PreparedGeneration holds one encoded request and resolved authentication for
|
||||
// repeated attempts. Each Execute call creates a fresh HTTP request and context.
|
||||
// repeated attempts. Each execution creates a fresh HTTP request and context.
|
||||
type PreparedGeneration struct {
|
||||
endpoint string
|
||||
payload []byte
|
||||
apiKey string
|
||||
timeoutSeconds int
|
||||
httpClient *http.Client
|
||||
endpoint string
|
||||
payload []byte
|
||||
apiKey string
|
||||
timeoutSeconds int
|
||||
httpClient *http.Client
|
||||
structuredOutputRequested bool
|
||||
}
|
||||
|
||||
// AttemptOutcome describes one HTTP execution without exposing response bodies
|
||||
// or headers to the orchestration layer.
|
||||
type AttemptOutcome struct {
|
||||
Response *domain.GenerateResponse
|
||||
Err error
|
||||
StatusCode int
|
||||
Usage domain.TokenUsage
|
||||
UsagePresence *domain.TokenUsagePresence
|
||||
RetryNotBefore time.Time
|
||||
Response *domain.GenerateResponse
|
||||
Err error
|
||||
StatusCode int
|
||||
Usage domain.TokenUsage
|
||||
UsagePresence *domain.TokenUsagePresence
|
||||
RetryNotBefore time.Time
|
||||
Evidence HTTPExecutionEvidence
|
||||
StructuredOutputRequested *bool
|
||||
ResponseDecoded bool
|
||||
GenerationCompleted bool
|
||||
}
|
||||
|
||||
// PrepareGeneration validates and freezes one built-in request before dispatch.
|
||||
@@ -177,12 +182,23 @@ func (c *OpenAICompatibleClient) PrepareGeneration(req domain.GenerateRequest) (
|
||||
return &PreparedGeneration{
|
||||
endpoint: endpoint, payload: payload, apiKey: apiKey,
|
||||
timeoutSeconds: req.Target.TimeoutSeconds, httpClient: httpClient,
|
||||
structuredOutputRequested: wireReq.ResponseFormat != nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute dispatches one attempt. The returned response is non-nil only when
|
||||
// the successful envelope contains a usable first choice.
|
||||
func (p *PreparedGeneration) Execute(ctx context.Context) AttemptOutcome {
|
||||
return p.execute(ctx, false)
|
||||
}
|
||||
|
||||
// ExecuteWithEvidence performs one prepared attempt and retains HTTP and
|
||||
// decoded-generation facts even when a later context check returns an error.
|
||||
func (p *PreparedGeneration) ExecuteWithEvidence(ctx context.Context) AttemptOutcome {
|
||||
return p.execute(ctx, true)
|
||||
}
|
||||
|
||||
func (p *PreparedGeneration) execute(ctx context.Context, captureEvidence bool) AttemptOutcome {
|
||||
requestContext := ctx
|
||||
if p.timeoutSeconds > 0 {
|
||||
var cancel context.CancelFunc
|
||||
@@ -198,13 +214,29 @@ func (p *PreparedGeneration) Execute(ctx context.Context) AttemptOutcome {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.apiKey)
|
||||
}
|
||||
|
||||
httpResp, err := p.httpClient.Do(httpReq)
|
||||
var (
|
||||
httpResp *http.Response
|
||||
outcome AttemptOutcome
|
||||
)
|
||||
if captureEvidence {
|
||||
httpResp, err, outcome.Evidence = doWithEvidence(p.httpClient, httpReq)
|
||||
if outcome.Evidence.DispatchInvoked {
|
||||
requested := p.structuredOutputRequested
|
||||
outcome.StructuredOutputRequested = &requested
|
||||
}
|
||||
} else {
|
||||
httpResp, err = p.httpClient.Do(httpReq)
|
||||
}
|
||||
if httpResp != nil && (captureEvidence || err == nil) {
|
||||
outcome.StatusCode = httpResp.StatusCode
|
||||
}
|
||||
if err != nil {
|
||||
return AttemptOutcome{Err: &requestFailedError{cause: err}}
|
||||
outcome.Err = &requestFailedError{cause: err}
|
||||
return outcome
|
||||
}
|
||||
receivedAt := time.Now()
|
||||
defer httpResp.Body.Close()
|
||||
outcome := AttemptOutcome{StatusCode: httpResp.StatusCode}
|
||||
outcome.StatusCode = httpResp.StatusCode
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
outcome.RetryNotBefore = retryNotBefore(httpResp.Header.Values("Retry-After"), receivedAt)
|
||||
@@ -220,6 +252,11 @@ func (p *PreparedGeneration) Execute(ctx context.Context) AttemptOutcome {
|
||||
}
|
||||
if httpResp.ContentLength > maxOpenAIChatResponseBytes {
|
||||
outcome.Err = openAIChatResponseTooLargeError()
|
||||
if captureEvidence {
|
||||
if contextErr := observedContextError(requestContext, ctx, outcome.Err); contextErr != nil {
|
||||
outcome.Err = contextErr
|
||||
}
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
@@ -231,6 +268,7 @@ func (p *PreparedGeneration) Execute(ctx context.Context) AttemptOutcome {
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
outcome.ResponseDecoded = true
|
||||
outcome.Usage = domain.TokenUsage{
|
||||
PromptTokens: tokenCount(wireResp.Usage.PromptTokens), CompletionTokens: tokenCount(wireResp.Usage.CompletionTokens),
|
||||
TotalTokens: tokenCount(wireResp.Usage.TotalTokens), CachedTokens: tokenCount(wireResp.Usage.PromptTokensDetails.CachedTokens),
|
||||
@@ -244,14 +282,30 @@ func (p *PreparedGeneration) Execute(ctx context.Context) AttemptOutcome {
|
||||
|
||||
if len(wireResp.Choices) == 0 {
|
||||
outcome.Err = fmt.Errorf("%w: no choices returned", ErrMalformedResponse)
|
||||
if captureEvidence {
|
||||
if contextErr := observedContextError(requestContext, ctx, outcome.Err); contextErr != nil {
|
||||
outcome.Err = contextErr
|
||||
}
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
content := wireResp.Choices[0].Message.Content
|
||||
if content == nil {
|
||||
outcome.Err = fmt.Errorf("%w: first choice has missing message content", ErrMalformedResponse)
|
||||
if captureEvidence {
|
||||
if contextErr := observedContextError(requestContext, ctx, outcome.Err); contextErr != nil {
|
||||
outcome.Err = contextErr
|
||||
}
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
outcome.Response = &domain.GenerateResponse{Content: *content, Usage: outcome.Usage, UsagePresence: outcome.UsagePresence}
|
||||
outcome.GenerationCompleted = true
|
||||
if captureEvidence {
|
||||
if contextErr := observedContextError(requestContext, ctx, nil); contextErr != nil {
|
||||
outcome.Err = contextErr
|
||||
}
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user