Share generation attempt accounting
This commit is contained in:
@@ -694,6 +694,12 @@ unchanged.
|
|||||||
injected usage compatibility and panic/cancellation cases exercise the real
|
injected usage compatibility and panic/cancellation cases exercise the real
|
||||||
coordinator. Expectations remain independent of the extracted projection helpers.
|
coordinator. Expectations remain independent of the extracted projection helpers.
|
||||||
|
|
||||||
|
Completion: built-in and injected attempts share evidence, usage, and observer
|
||||||
|
projection after their permit scopes end, while retaining their distinct HTTP
|
||||||
|
and usage facts. Race-enabled generation, use-case, and observation tests and
|
||||||
|
focused public retry, smoke, capacity, usage, and observation tests pass on
|
||||||
|
Go 1.27.3; an injected-observer test verifies permit release before callbacks.
|
||||||
|
|
||||||
## Stage 27: Cover Request Privacy And Correct Public Documentation
|
## Stage 27: Cover Request Privacy And Correct Public Documentation
|
||||||
|
|
||||||
**Catalog:** 11, 12, 13, 14. **Scope:** focused tests/comments, not new behavior.
|
**Catalog:** 11, 12, 13, 14. **Scope:** focused tests/comments, not new behavior.
|
||||||
|
|||||||
@@ -130,36 +130,7 @@ func (c *Coordinator) generate(
|
|||||||
if result.Dispatched {
|
if result.Dispatched {
|
||||||
attempts++
|
attempts++
|
||||||
}
|
}
|
||||||
if captureEvidence {
|
recordAttempt(result, false, captureEvidence, &evidence, &usage, recorder, generation, ordinal)
|
||||||
attempt := AttemptEvidence{
|
|
||||||
StartedAt: result.StartedAt.UTC(),
|
|
||||||
Dispatched: result.Dispatched,
|
|
||||||
ResponseReceived: result.Outcome.Evidence.ResponseReceived,
|
|
||||||
HTTP: cloneHTTPEvidence(result.Outcome.Evidence),
|
|
||||||
CapacityWait: result.CapacityWait,
|
|
||||||
RequestDuration: result.RequestDuration,
|
|
||||||
Failure: ClassifyFailure(result.Outcome.Err),
|
|
||||||
ResponseDecoded: result.Outcome.ResponseDecoded,
|
|
||||||
GenerationCompleted: result.Outcome.GenerationCompleted,
|
|
||||||
}
|
|
||||||
if result.Outcome.StructuredOutputRequested != nil {
|
|
||||||
requested := *result.Outcome.StructuredOutputRequested
|
|
||||||
attempt.StructuredOutputRequested = &requested
|
|
||||||
}
|
|
||||||
if result.Dispatched {
|
|
||||||
usage.Add(result.Outcome.Usage, result.Outcome.UsagePresence)
|
|
||||||
var attemptUsage observation.UsageAccumulator
|
|
||||||
attemptUsage.Add(result.Outcome.Usage, result.Outcome.UsagePresence)
|
|
||||||
attempt.Usage = attemptUsage.Observed()
|
|
||||||
}
|
|
||||||
evidence.Attempts = append(evidence.Attempts, attempt)
|
|
||||||
}
|
|
||||||
if recorder != nil {
|
|
||||||
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: ordinal,
|
|
||||||
Dispatched: result.Dispatched, CapacityWait: result.CapacityWait, GenerationDuration: result.RequestDuration,
|
|
||||||
Outcome: attemptOutcome(result.Outcome.Err), Failure: ClassifyFailure(result.Outcome.Err),
|
|
||||||
StatusCode: result.Outcome.StatusCode, Usage: result.Outcome.Usage, UsagePresence: result.Outcome.UsagePresence})
|
|
||||||
}
|
|
||||||
if result.Outcome.Err == nil {
|
if result.Outcome.Err == nil {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
if captureEvidence {
|
if captureEvidence {
|
||||||
@@ -206,104 +177,57 @@ func (c *Coordinator) generate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Coordinator) generateInjected(
|
func (c *Coordinator) generateInjected(ctx context.Context, req domain.GenerateRequest, captureEvidence bool) (*domain.GenerateResponse, error, GenerationEvidence) {
|
||||||
ctx context.Context,
|
|
||||||
req domain.GenerateRequest,
|
|
||||||
captureEvidence bool,
|
|
||||||
) (*domain.GenerateResponse, error, GenerationEvidence) {
|
|
||||||
evidence := GenerationEvidence{}
|
evidence := GenerationEvidence{}
|
||||||
var usage observation.UsageAccumulator
|
var usage observation.UsageAccumulator
|
||||||
finish := func(response *domain.GenerateResponse, err error) (*domain.GenerateResponse, error, GenerationEvidence) {
|
|
||||||
if captureEvidence {
|
|
||||||
evidence.Usage = usage.Observed()
|
|
||||||
}
|
|
||||||
return response, err, evidence
|
|
||||||
}
|
|
||||||
recorder := observation.FromContext(ctx)
|
|
||||||
generation := req.Generation
|
generation := req.Generation
|
||||||
if generation == 0 {
|
if generation == 0 {
|
||||||
generation = 1
|
generation = 1
|
||||||
}
|
}
|
||||||
start := c.now()
|
result := c.executeInjected(ctx, req)
|
||||||
release, err := c.manager.Acquire(ctx, req.Target.BackendID)
|
recordAttempt(result, true, captureEvidence, &evidence, &usage, observation.FromContext(ctx), generation, 1)
|
||||||
capacityWait := c.capacityWait(req.Target.BackendID, start)
|
if captureEvidence {
|
||||||
if err != nil {
|
evidence.Usage = usage.Observed()
|
||||||
|
}
|
||||||
|
if result.Outcome.Err != nil {
|
||||||
|
return nil, result.Outcome.Err, evidence
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
if captureEvidence {
|
if captureEvidence {
|
||||||
evidence.Attempts = append(evidence.Attempts, AttemptEvidence{
|
return result.Outcome.Response, err, evidence
|
||||||
StartedAt: start.UTC(),
|
|
||||||
CapacityWait: capacityWait,
|
|
||||||
Failure: ClassifyFailure(err),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if recorder != nil {
|
return nil, err, evidence
|
||||||
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: 1, CapacityWait: capacityWait, Outcome: domain.ExecutionOutcomeError, Failure: ClassifyFailure(err)})
|
}
|
||||||
}
|
return result.Outcome.Response, nil, evidence
|
||||||
return finish(nil, err)
|
}
|
||||||
|
|
||||||
|
func (c *Coordinator) executeInjected(ctx context.Context, req domain.GenerateRequest) attemptResult {
|
||||||
|
result := attemptResult{StartedAt: c.now()}
|
||||||
|
release, err := c.manager.Acquire(ctx, req.Target.BackendID)
|
||||||
|
result.CapacityWait = c.capacityWait(req.Target.BackendID, result.StartedAt)
|
||||||
|
if err != nil {
|
||||||
|
result.Outcome.Err = err
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
defer release()
|
defer release()
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
release()
|
result.Outcome.Err = err
|
||||||
if captureEvidence {
|
return result
|
||||||
evidence.Attempts = append(evidence.Attempts, AttemptEvidence{
|
|
||||||
StartedAt: start.UTC(),
|
|
||||||
CapacityWait: capacityWait,
|
|
||||||
Failure: ClassifyFailure(err),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if recorder != nil {
|
|
||||||
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: 1, CapacityWait: capacityWait, Outcome: domain.ExecutionOutcomeError, Failure: ClassifyFailure(err)})
|
|
||||||
}
|
|
||||||
return finish(nil, err)
|
|
||||||
}
|
}
|
||||||
dispatchStart := c.now()
|
start := c.now()
|
||||||
response, err := c.injected.Generate(ctx, req)
|
result.Dispatched = true
|
||||||
duration := elapsed(dispatchStart, c.now())
|
requested := req.StructuredOutput != nil
|
||||||
if err == nil && response == nil {
|
result.Outcome.StructuredOutputRequested = &requested
|
||||||
err = llm.ErrMalformedResponse
|
result.Outcome.Response, result.Outcome.Err = c.injected.Generate(ctx, req)
|
||||||
|
result.RequestDuration = elapsed(start, c.now())
|
||||||
|
if result.Outcome.Err == nil && result.Outcome.Response == nil {
|
||||||
|
result.Outcome.Err = llm.ErrMalformedResponse
|
||||||
}
|
}
|
||||||
if captureEvidence {
|
if result.Outcome.Err == nil {
|
||||||
attempt := AttemptEvidence{
|
result.Outcome.Usage = result.Outcome.Response.Usage
|
||||||
StartedAt: start.UTC(),
|
result.Outcome.UsagePresence = result.Outcome.Response.UsagePresence
|
||||||
Dispatched: true,
|
|
||||||
ResponseReceived: response != nil,
|
|
||||||
RequestDuration: duration,
|
|
||||||
Failure: ClassifyFailure(err),
|
|
||||||
GenerationCompleted: err == nil && response != nil,
|
|
||||||
}
|
|
||||||
requested := req.StructuredOutput != nil
|
|
||||||
attempt.StructuredOutputRequested = &requested
|
|
||||||
var reportedUsage domain.TokenUsage
|
|
||||||
var reportedPresence *domain.TokenUsagePresence
|
|
||||||
if err == nil && response != nil {
|
|
||||||
reportedUsage = response.Usage
|
|
||||||
reportedPresence = response.UsagePresence
|
|
||||||
}
|
|
||||||
usage.Add(reportedUsage, reportedPresence)
|
|
||||||
var attemptUsage observation.UsageAccumulator
|
|
||||||
attemptUsage.Add(reportedUsage, reportedPresence)
|
|
||||||
attempt.Usage = attemptUsage.Observed()
|
|
||||||
evidence.Attempts = append(evidence.Attempts, attempt)
|
|
||||||
}
|
}
|
||||||
release()
|
return result
|
||||||
if recorder != nil {
|
|
||||||
data := observation.AttemptData{Generation: generation, Attempt: 1, Dispatched: true, CapacityWait: capacityWait, GenerationDuration: duration,
|
|
||||||
Outcome: attemptOutcome(err), Failure: ClassifyFailure(err)}
|
|
||||||
if err == nil {
|
|
||||||
data.Usage = response.Usage
|
|
||||||
data.UsagePresence = response.UsagePresence
|
|
||||||
}
|
|
||||||
recorder.Attempt(data)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return finish(nil, err)
|
|
||||||
}
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
if captureEvidence {
|
|
||||||
return finish(response, err)
|
|
||||||
}
|
|
||||||
return finish(nil, err)
|
|
||||||
}
|
|
||||||
return finish(response, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type attemptResult struct {
|
type attemptResult struct {
|
||||||
@@ -314,6 +238,49 @@ type attemptResult struct {
|
|||||||
StartedAt time.Time
|
StartedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordAttempt runs after the attempt has released its permit, including when
|
||||||
|
// the recorder calls application code. Injected clients expose no HTTP facts.
|
||||||
|
func recordAttempt(result attemptResult, injected, capture bool, evidence *GenerationEvidence, usage *observation.UsageAccumulator, recorder *observation.Recorder, generation, ordinal uint64) {
|
||||||
|
outcome := result.Outcome
|
||||||
|
failure := ClassifyFailure(outcome.Err)
|
||||||
|
if capture {
|
||||||
|
attempt := AttemptEvidence{
|
||||||
|
StartedAt: result.StartedAt.UTC(), Dispatched: result.Dispatched,
|
||||||
|
CapacityWait: result.CapacityWait, RequestDuration: result.RequestDuration,
|
||||||
|
Failure: failure,
|
||||||
|
}
|
||||||
|
if injected {
|
||||||
|
attempt.ResponseReceived = outcome.Response != nil
|
||||||
|
attempt.GenerationCompleted = outcome.Err == nil && outcome.Response != nil
|
||||||
|
if result.Dispatched {
|
||||||
|
attempt.CapacityWait = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attempt.ResponseReceived = outcome.Evidence.ResponseReceived
|
||||||
|
attempt.HTTP = cloneHTTPEvidence(outcome.Evidence)
|
||||||
|
attempt.ResponseDecoded = outcome.ResponseDecoded
|
||||||
|
attempt.GenerationCompleted = outcome.GenerationCompleted
|
||||||
|
}
|
||||||
|
if outcome.StructuredOutputRequested != nil {
|
||||||
|
requested := *outcome.StructuredOutputRequested
|
||||||
|
attempt.StructuredOutputRequested = &requested
|
||||||
|
}
|
||||||
|
if result.Dispatched {
|
||||||
|
usage.Add(outcome.Usage, outcome.UsagePresence)
|
||||||
|
var single observation.UsageAccumulator
|
||||||
|
single.Add(outcome.Usage, outcome.UsagePresence)
|
||||||
|
attempt.Usage = single.Observed()
|
||||||
|
}
|
||||||
|
evidence.Attempts = append(evidence.Attempts, attempt)
|
||||||
|
}
|
||||||
|
if recorder != nil {
|
||||||
|
recorder.Attempt(observation.AttemptData{Generation: generation, Attempt: ordinal,
|
||||||
|
Dispatched: result.Dispatched, CapacityWait: result.CapacityWait, GenerationDuration: result.RequestDuration,
|
||||||
|
Outcome: attemptOutcome(outcome.Err), Failure: failure, StatusCode: outcome.StatusCode,
|
||||||
|
Usage: outcome.Usage, UsagePresence: outcome.UsagePresence})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Coordinator) execute(ctx context.Context, backendID string, prepared *llm.PreparedGeneration, captureEvidence bool) attemptResult {
|
func (c *Coordinator) execute(ctx context.Context, backendID string, prepared *llm.PreparedGeneration, captureEvidence bool) attemptResult {
|
||||||
result := attemptResult{StartedAt: c.now()}
|
result := attemptResult{StartedAt: c.now()}
|
||||||
release, err := c.manager.Acquire(ctx, backendID)
|
release, err := c.manager.Acquire(ctx, backendID)
|
||||||
|
|||||||
@@ -144,6 +144,32 @@ func (c fixedClient) Generate(context.Context, domain.GenerateRequest) (*domain.
|
|||||||
return c.response, c.err
|
return c.response, c.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInjectedObserverRunsAfterPermitRelease(t *testing.T) {
|
||||||
|
configured := testCoordinator(t, func(*http.Request) (*http.Response, error) { t.Fatal("unexpected HTTP request"); return nil, nil })
|
||||||
|
coordinator := NewInjected(configured.manager, fixedClient{response: &domain.GenerateResponse{}})
|
||||||
|
called := false
|
||||||
|
recorder, err := observation.New(func(event domain.ExecutionEvent) {
|
||||||
|
if event.Kind != domain.ExecutionAttempt {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
called = true
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
release, err := coordinator.manager.Acquire(ctx, "limited")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("observer could not acquire permit: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
release()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := coordinator.Generate(observation.WithRecorder(context.Background(), recorder), testRequest(0)); err != nil || !called {
|
||||||
|
t.Fatalf("generation error = %v, observer called = %v", err, called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestObservedInjectedUsageAndRepairGeneration(t *testing.T) {
|
func TestObservedInjectedUsageAndRepairGeneration(t *testing.T) {
|
||||||
manager, err := capacity.NewManager(nil)
|
manager, err := capacity.NewManager(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user