Return generation attempt results as one value

This commit is contained in:
2026-11-05 14:25:07 +00:00
parent 8c0fb15727
commit df208e132b
2 changed files with 54 additions and 39 deletions

View File

@@ -672,6 +672,11 @@ tests pass on Go 1.27.3; the internal source description is updated.
response/error behavior, timing facts and cancellation; no new event or HTTP fact response/error behavior, timing facts and cancellation; no new event or HTTP fact
is fabricated for an injected client. is fabricated for an injected client.
Completion: built-in attempts return a private result containing outcome,
dispatch, capacity, duration, and start-time facts. Generation, use-case, and
engine tests pass on Go 1.27.3; permit lifetime and injected dispatch are
unchanged.
## Stage 26: Share Generation Accounting Projections ## Stage 26: Share Generation Accounting Projections
**Catalog:** 25, completion. **Files:** generation accounting/evidence/observer tests. **Catalog:** 25, completion. **Files:** generation accounting/evidence/observer tests.

View File

@@ -126,66 +126,66 @@ func (c *Coordinator) generate(
return finish(nil, err) return finish(nil, err)
} }
ordinal := attempts + 1 ordinal := attempts + 1
outcome, dispatched, capacityWait, generationDuration, startedAt := c.execute(ctx, req.Target.BackendID, prepared, captureEvidence) result := c.execute(ctx, req.Target.BackendID, prepared, captureEvidence)
if dispatched { if result.Dispatched {
attempts++ attempts++
} }
if captureEvidence { if captureEvidence {
attempt := AttemptEvidence{ attempt := AttemptEvidence{
StartedAt: startedAt.UTC(), StartedAt: result.StartedAt.UTC(),
Dispatched: dispatched, Dispatched: result.Dispatched,
ResponseReceived: outcome.Evidence.ResponseReceived, ResponseReceived: result.Outcome.Evidence.ResponseReceived,
HTTP: cloneHTTPEvidence(outcome.Evidence), HTTP: cloneHTTPEvidence(result.Outcome.Evidence),
CapacityWait: capacityWait, CapacityWait: result.CapacityWait,
RequestDuration: generationDuration, RequestDuration: result.RequestDuration,
Failure: ClassifyFailure(outcome.Err), Failure: ClassifyFailure(result.Outcome.Err),
ResponseDecoded: outcome.ResponseDecoded, ResponseDecoded: result.Outcome.ResponseDecoded,
GenerationCompleted: outcome.GenerationCompleted, GenerationCompleted: result.Outcome.GenerationCompleted,
} }
if outcome.StructuredOutputRequested != nil { if result.Outcome.StructuredOutputRequested != nil {
requested := *outcome.StructuredOutputRequested requested := *result.Outcome.StructuredOutputRequested
attempt.StructuredOutputRequested = &requested attempt.StructuredOutputRequested = &requested
} }
if dispatched { if result.Dispatched {
usage.Add(outcome.Usage, outcome.UsagePresence) usage.Add(result.Outcome.Usage, result.Outcome.UsagePresence)
var attemptUsage observation.UsageAccumulator var attemptUsage observation.UsageAccumulator
attemptUsage.Add(outcome.Usage, outcome.UsagePresence) attemptUsage.Add(result.Outcome.Usage, result.Outcome.UsagePresence)
attempt.Usage = attemptUsage.Observed() attempt.Usage = attemptUsage.Observed()
} }
evidence.Attempts = append(evidence.Attempts, attempt) evidence.Attempts = append(evidence.Attempts, attempt)
} }
if recorder != nil { if recorder != nil {
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: ordinal, recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: ordinal,
Dispatched: dispatched, CapacityWait: capacityWait, GenerationDuration: generationDuration, Dispatched: result.Dispatched, CapacityWait: result.CapacityWait, GenerationDuration: result.RequestDuration,
Outcome: attemptOutcome(outcome.Err), Failure: ClassifyFailure(outcome.Err), Outcome: attemptOutcome(result.Outcome.Err), Failure: ClassifyFailure(result.Outcome.Err),
StatusCode: outcome.StatusCode, Usage: outcome.Usage, UsagePresence: outcome.UsagePresence}) StatusCode: result.Outcome.StatusCode, Usage: result.Outcome.Usage, UsagePresence: result.Outcome.UsagePresence})
} }
if outcome.Err == nil { if result.Outcome.Err == nil {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
if captureEvidence { if captureEvidence {
return finish(outcome.Response, err) return finish(result.Outcome.Response, err)
} }
return finish(nil, err) return finish(nil, err)
} }
return finish(outcome.Response, nil) return finish(result.Outcome.Response, nil)
} }
if !dispatched || !eligibleStatus(outcome) { if !result.Dispatched || !eligibleStatus(result.Outcome) {
if captureEvidence { if captureEvidence {
return finish(outcome.Response, outcome.Err) return finish(result.Outcome.Response, result.Outcome.Err)
} }
return finish(nil, outcome.Err) return finish(nil, result.Outcome.Err)
} }
if retriesUsed == req.TransientRetries { if retriesUsed == req.TransientRetries {
if retriesUsed == 0 { if retriesUsed == 0 {
return finish(nil, outcome.Err) return finish(nil, result.Outcome.Err)
} }
return finish(nil, &ExhaustedError{attempts: attempts, cause: outcome.Err}) return finish(nil, &ExhaustedError{attempts: attempts, cause: result.Outcome.Err})
} }
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return finish(nil, err) return finish(nil, err)
} }
retryOrdinal := retriesUsed + 1 retryOrdinal := retriesUsed + 1
delay := retryDelay(c.backoff(retryOrdinal), outcome.RetryNotBefore, c.now()) delay := retryDelay(c.backoff(retryOrdinal), result.Outcome.RetryNotBefore, c.now())
waitStart := c.now() waitStart := c.now()
waitErr := c.wait(ctx, delay) waitErr := c.wait(ctx, delay)
waitDuration := elapsed(waitStart, c.now()) waitDuration := elapsed(waitStart, c.now())
@@ -306,27 +306,37 @@ func (c *Coordinator) generateInjected(
return finish(response, nil) return finish(response, nil)
} }
func (c *Coordinator) execute(ctx context.Context, backendID string, prepared *llm.PreparedGeneration, captureEvidence bool) (outcome llm.AttemptOutcome, dispatched bool, capacityWait, generationDuration time.Duration, startedAt time.Time) { type attemptResult struct {
start := c.now() Outcome llm.AttemptOutcome
startedAt = start Dispatched bool
CapacityWait time.Duration
RequestDuration time.Duration
StartedAt time.Time
}
func (c *Coordinator) execute(ctx context.Context, backendID string, prepared *llm.PreparedGeneration, captureEvidence bool) attemptResult {
result := attemptResult{StartedAt: c.now()}
release, err := c.manager.Acquire(ctx, backendID) release, err := c.manager.Acquire(ctx, backendID)
capacityWait = c.capacityWait(backendID, start) result.CapacityWait = c.capacityWait(backendID, result.StartedAt)
if err != nil { if err != nil {
return llm.AttemptOutcome{Err: err}, false, capacityWait, 0, startedAt result.Outcome.Err = err
return result
} }
defer release() defer release()
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return llm.AttemptOutcome{Err: err}, false, capacityWait, 0, startedAt result.Outcome.Err = err
return result
} }
dispatchStart := c.now() dispatchStart := c.now()
if captureEvidence { if captureEvidence {
outcome = prepared.ExecuteWithEvidence(ctx) result.Outcome = prepared.ExecuteWithEvidence(ctx)
dispatched = outcome.Evidence.DispatchInvoked result.Dispatched = result.Outcome.Evidence.DispatchInvoked
} else { } else {
outcome = prepared.Execute(ctx) result.Outcome = prepared.Execute(ctx)
dispatched = true result.Dispatched = true
} }
return outcome, dispatched, capacityWait, elapsed(dispatchStart, c.now()), startedAt result.RequestDuration = elapsed(dispatchStart, c.now())
return result
} }
func (c *Coordinator) capacityWait(backendID string, start time.Time) time.Duration { func (c *Coordinator) capacityWait(backendID string, start time.Time) time.Duration {