1554 lines
50 KiB
Go
1554 lines
50 KiB
Go
package openai
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"iop/packages/go/streamgate"
|
|
)
|
|
|
|
// errHotPathTurnTerminal is returned by the outer turn when a stage tries to
|
|
// open, release, or terminate after the single public terminal has committed.
|
|
var errHotPathTurnTerminal = errors.New("hot path outer turn already committed a terminal")
|
|
|
|
// hotPathDispositionKind is the closed terminal vocabulary shared by the
|
|
// stage runtime, the HTTP-turn sequencer, and Light cleanup/orphan handoff.
|
|
// Endpoint codecs translate these values later; no wire status or body shape
|
|
// is owned here.
|
|
type hotPathDispositionKind string
|
|
|
|
const (
|
|
hotPathDispositionSuccess hotPathDispositionKind = "success"
|
|
hotPathDispositionToolTurn hotPathDispositionKind = "tool_turn"
|
|
hotPathDispositionLength hotPathDispositionKind = "length"
|
|
hotPathDispositionProviderError hotPathDispositionKind = "provider_error"
|
|
hotPathDispositionValidationError hotPathDispositionKind = "validation_error"
|
|
hotPathDispositionTimeout hotPathDispositionKind = "timeout"
|
|
hotPathDispositionCallerCancel hotPathDispositionKind = "caller_cancel"
|
|
)
|
|
|
|
type hotPathTerminalDisposition struct {
|
|
Kind hotPathDispositionKind
|
|
Cause string
|
|
Source string
|
|
StageID string
|
|
Generation uint64
|
|
}
|
|
|
|
func (d hotPathTerminalDisposition) valid() bool {
|
|
switch d.Kind {
|
|
case hotPathDispositionSuccess,
|
|
hotPathDispositionToolTurn,
|
|
hotPathDispositionLength,
|
|
hotPathDispositionProviderError,
|
|
hotPathDispositionValidationError,
|
|
hotPathDispositionTimeout,
|
|
hotPathDispositionCallerCancel:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func hotPathDispositionForSuccess(reason string, hasTools bool) hotPathDispositionKind {
|
|
if hasTools || reason == "tool_calls" || reason == "tool_use" || reason == "function_call" {
|
|
return hotPathDispositionToolTurn
|
|
}
|
|
if hotPathIsProviderLengthTerminal(reason) {
|
|
return hotPathDispositionLength
|
|
}
|
|
return hotPathDispositionSuccess
|
|
}
|
|
|
|
func hotPathDispositionForError(err error) hotPathDispositionKind {
|
|
switch {
|
|
case errors.Is(err, context.Canceled):
|
|
return hotPathDispositionCallerCancel
|
|
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, errRunTimedOut):
|
|
return hotPathDispositionTimeout
|
|
default:
|
|
return hotPathDispositionProviderError
|
|
}
|
|
}
|
|
|
|
type hotPathDispositionError struct {
|
|
disposition hotPathTerminalDisposition
|
|
err error
|
|
}
|
|
|
|
func (e *hotPathDispositionError) Error() string {
|
|
if e == nil || e.err == nil {
|
|
return "hot path terminal disposition"
|
|
}
|
|
return e.err.Error()
|
|
}
|
|
|
|
func (e *hotPathDispositionError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.err
|
|
}
|
|
|
|
func hotPathDispositionFromError(err error) (hotPathTerminalDisposition, bool) {
|
|
var dispositionErr *hotPathDispositionError
|
|
if errors.As(err, &dispositionErr) && dispositionErr.disposition.valid() {
|
|
return dispositionErr.disposition, true
|
|
}
|
|
return hotPathTerminalDisposition{}, false
|
|
}
|
|
|
|
func newHotPathDispositionError(kind hotPathDispositionKind, source, stageID string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return &hotPathDispositionError{
|
|
disposition: hotPathTerminalDisposition{
|
|
Kind: kind, Cause: err.Error(), Source: source, StageID: strings.TrimSpace(stageID),
|
|
},
|
|
err: err,
|
|
}
|
|
}
|
|
|
|
// hotPathStageMeta is the protocol-neutral correlation a provider stage
|
|
// contributes to one HTTP turn. It carries model/provider/path identity for the
|
|
// stage-scoped runtime but never credentials, provider targets, or caller
|
|
// endpoint wire state.
|
|
type hotPathStageMeta struct {
|
|
StageID string
|
|
Protocol string // "openai" | "anthropic"
|
|
Model string
|
|
Provider string
|
|
ExecutionPath string
|
|
ResponseID string
|
|
AttemptID string
|
|
}
|
|
|
|
func (m hotPathStageMeta) token() string {
|
|
return hotPathFirstNonEmpty(m.StageID, m.ResponseID, m.AttemptID, "stage")
|
|
}
|
|
|
|
// hotPathTurnUsage is the deduplicated, aggregated token usage across every
|
|
// internal stage of one HTTP turn.
|
|
type hotPathTurnUsage struct {
|
|
InputTokens int
|
|
OutputTokens int
|
|
ReasoningTokens int
|
|
CachedInputTokens int
|
|
Reported bool
|
|
}
|
|
|
|
// hotPathStageTerminal is the typed transition evidence a stage runtime's
|
|
// terminal is converted into by the stage release sink. It is never rendered to
|
|
// the caller: the outer turn alone owns whether and when a single public
|
|
// terminal is committed.
|
|
type hotPathStageTerminal struct {
|
|
Success bool
|
|
Reason string
|
|
ErrType string
|
|
ErrCode string
|
|
Usage hotPathStageUsage
|
|
HasUsage bool
|
|
Disposition hotPathTerminalDisposition
|
|
}
|
|
|
|
// hotPathReleasedDelta records one progressively released public delta in turn
|
|
// order. Tests use it as the ordering oracle; production renderers consume the
|
|
// compatibility accumulator instead.
|
|
type hotPathReleasedDelta struct {
|
|
Kind streamgate.EventKind
|
|
Text string
|
|
PublicID string
|
|
Name string
|
|
Args string
|
|
}
|
|
|
|
type hotPathReleaseCallback func(hotPathReleasedDelta) error
|
|
|
|
// hotPathReleaseCallbackError marks a failure from the endpoint-owned release
|
|
// callback. Only this boundary means the caller can no longer receive output;
|
|
// release preparation failures must retain their normal stage-runtime meaning.
|
|
type hotPathReleaseCallbackError struct {
|
|
err error
|
|
}
|
|
|
|
func (e *hotPathReleaseCallbackError) Error() string { return e.err.Error() }
|
|
|
|
func (e *hotPathReleaseCallbackError) Unwrap() error { return e.err }
|
|
|
|
type hotPathTurnTool struct {
|
|
publicID string
|
|
providerID string
|
|
name string
|
|
args strings.Builder
|
|
}
|
|
|
|
// hotPathOutputBudget keeps the three caller-cap states distinct. Remaining
|
|
// zero is exhausted only when Limited is true; an unlimited turn never uses a
|
|
// sentinel provider value.
|
|
type hotPathOutputBudget struct {
|
|
Limited bool
|
|
Remaining int
|
|
Exhausted bool
|
|
MissingUsage bool
|
|
}
|
|
|
|
type hotPathTurnError struct {
|
|
errType string
|
|
code string
|
|
}
|
|
|
|
// hotPathOuterTurn is the single protocol-neutral sequencer that survives stage
|
|
// replacement inside one HTTP request. It owns the public block/tool id remap,
|
|
// deduplicated usage aggregation, the caller output-cap budget, response-start
|
|
// suppression, the single terminal guard, and a compatibility accumulator that
|
|
// later caller codecs render. It holds nothing on behalf of the stage runtimes:
|
|
// nonterminal deltas are appended as they are released.
|
|
type hotPathOuterTurn struct {
|
|
releaseMu sync.Mutex
|
|
mu sync.Mutex
|
|
|
|
publicResponseID string
|
|
channel string
|
|
outputCapTokens int // 0 => no caller token cap
|
|
|
|
started bool
|
|
terminalCommitted bool
|
|
terminalReason string
|
|
terminalError *hotPathTurnError
|
|
disposition *hotPathTerminalDisposition
|
|
activeStage *hotPathActiveStageController
|
|
activeGeneration uint64
|
|
|
|
stageSeq int
|
|
|
|
toolPublic map[string]*hotPathTurnTool
|
|
toolOrder []*hotPathTurnTool
|
|
toolSeq int
|
|
toolID func() (string, error)
|
|
|
|
usageSeen map[string]struct{}
|
|
usage hotPathTurnUsage
|
|
previewUsage hotPathStageUsage
|
|
reasoningSignature string
|
|
missingUsage bool
|
|
capExhausted bool
|
|
|
|
content strings.Builder
|
|
reasoning strings.Builder
|
|
|
|
released []hotPathReleasedDelta
|
|
release hotPathReleaseCallback
|
|
}
|
|
|
|
// newHotPathOuterTurn builds one HTTP-turn sequencer. publicResponseID is the
|
|
// turn-scoped identity exposed to the caller regardless of internal stage
|
|
// response ids. Token budgeting is configured separately and never derives
|
|
// token counts from the caller-visible payload.
|
|
func newHotPathOuterTurn(publicResponseID string) *hotPathOuterTurn {
|
|
publicResponseID = strings.TrimSpace(publicResponseID)
|
|
return &hotPathOuterTurn{
|
|
publicResponseID: publicResponseID,
|
|
channel: streamGateChannelDefault,
|
|
toolPublic: make(map[string]*hotPathTurnTool),
|
|
usageSeen: make(map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
// bindPublicResponseID fixes the first provider-owned response identity for
|
|
// the HTTP turn. Later stages may have different provider response identities,
|
|
// but they cannot replace the already-bound public outer identity.
|
|
func (t *hotPathOuterTurn) bindPublicResponseID(responseID string) error {
|
|
if t == nil {
|
|
return errors.New("hot path outer turn is unavailable")
|
|
}
|
|
responseID = strings.TrimSpace(responseID)
|
|
if responseID == "" {
|
|
return errors.New("hot path public response identity is empty")
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.publicResponseID == "" {
|
|
t.publicResponseID = responseID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) publicResponseIdentity() (string, bool) {
|
|
if t == nil {
|
|
return "", false
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.publicResponseID, t.publicResponseID != ""
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) setReleaseCallback(callback hotPathReleaseCallback) error {
|
|
if t == nil {
|
|
return errors.New("hot path outer turn is unavailable")
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if len(t.released) > 0 {
|
|
return errors.New("hot path release callback was attached after visible output")
|
|
}
|
|
t.release = callback
|
|
return nil
|
|
}
|
|
|
|
// setToolIDAllocator fixes the caller-owned tool identity allocator before a
|
|
// progressively released Light stage can expose its first tool fragment.
|
|
func (t *hotPathOuterTurn) setToolIDAllocator(allocate func() (string, error)) error {
|
|
if t == nil || allocate == nil {
|
|
return errors.New("hot path tool identity allocator is unavailable")
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.toolID != nil || len(t.toolOrder) > 0 {
|
|
return errors.New("hot path tool identity allocator is already fixed")
|
|
}
|
|
t.toolID = allocate
|
|
return nil
|
|
}
|
|
|
|
// newHotPathCallerCappedOuterTurn keeps the caller limit in provider-reported
|
|
// tokens. The outer turn never truncates content or fabricates token usage from
|
|
// characters or bytes.
|
|
func newHotPathCallerCappedOuterTurn(publicResponseID string, outputCapTokens int) *hotPathOuterTurn {
|
|
outer := newHotPathOuterTurn(publicResponseID)
|
|
if outputCapTokens > 0 {
|
|
outer.outputCapTokens = outputCapTokens
|
|
}
|
|
return outer
|
|
}
|
|
|
|
// beginStage assigns the next stage-scope index. Tool ids are remapped per
|
|
// stage index so the same provider tool id emitted by two internal stages never
|
|
// collides in the public turn.
|
|
func (t *hotPathOuterTurn) beginStage() int {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.stageSeq++
|
|
return t.stageSeq
|
|
}
|
|
|
|
// openResponse records the single outer envelope open. The first internal stage
|
|
// opens it; every nested provider response-start is suppressed. It fails closed
|
|
// once the turn terminal is committed.
|
|
func (t *hotPathOuterTurn) openResponse(streamgate.ResponseStart) error {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.terminalCommitted {
|
|
return errHotPathTurnTerminal
|
|
}
|
|
t.started = true
|
|
return nil
|
|
}
|
|
|
|
// releaseDelta appends one progressively released nonterminal delta and remaps
|
|
// tool ids into the turn scope. Caller-visible payload is never locally
|
|
// truncated; output budgeting is based only on provider-reported token usage.
|
|
// It fails closed after the turn terminal is committed.
|
|
func (t *hotPathOuterTurn) releaseDelta(stageSeq int, ev streamgate.ReleaseEvent) error {
|
|
_, err := t.releaseDeltaRecorded(stageSeq, ev)
|
|
return err
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) releaseDeltaRecorded(stageSeq int, ev streamgate.ReleaseEvent) (*hotPathReleasedDelta, error) {
|
|
t.releaseMu.Lock()
|
|
defer t.releaseMu.Unlock()
|
|
t.mu.Lock()
|
|
if t.terminalCommitted {
|
|
t.mu.Unlock()
|
|
return nil, errHotPathTurnTerminal
|
|
}
|
|
t.started = true
|
|
var released hotPathReleasedDelta
|
|
switch ev.Kind() {
|
|
case streamgate.EventKindTextDelta:
|
|
text, err := ev.AsTextDelta()
|
|
if err != nil {
|
|
t.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
t.content.WriteString(text)
|
|
released = hotPathReleasedDelta{Kind: ev.Kind(), Text: text}
|
|
case streamgate.EventKindReasoningDelta:
|
|
text, err := ev.AsReasoningDelta()
|
|
if err != nil {
|
|
t.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
t.reasoning.WriteString(text)
|
|
released = hotPathReleasedDelta{Kind: ev.Kind(), Text: text}
|
|
case streamgate.EventKindToolCallFragment:
|
|
call, err := ev.AsToolCallFragment()
|
|
if err != nil {
|
|
t.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
tool, err := t.remapToolLocked(stageSeq, call)
|
|
if err != nil {
|
|
t.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
tool.args.WriteString(call.Arguments)
|
|
released = hotPathReleasedDelta{
|
|
Kind: ev.Kind(), PublicID: tool.publicID, Name: tool.name, Args: call.Arguments,
|
|
}
|
|
default:
|
|
t.mu.Unlock()
|
|
return nil, fmt.Errorf("hot path outer turn cannot release event kind %q", ev.Kind())
|
|
}
|
|
t.released = append(t.released, released)
|
|
callback := t.release
|
|
t.mu.Unlock()
|
|
if callback != nil {
|
|
if err := callback(released); err != nil {
|
|
return nil, &hotPathReleaseCallbackError{err: err}
|
|
}
|
|
}
|
|
return &released, nil
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) setToolNameLocked(tool *hotPathTurnTool, name string) {
|
|
if tool == nil || name == "" {
|
|
return
|
|
}
|
|
tool.name = name
|
|
}
|
|
|
|
// remapToolLocked resolves the turn-scoped public tool identity for one provider
|
|
// fragment. Fragments that share a stage index and provider id assemble under
|
|
// one public id; a provider id reused by another stage gets a fresh public id.
|
|
func (t *hotPathOuterTurn) remapToolLocked(stageSeq int, call streamgate.ToolCall) (*hotPathTurnTool, error) {
|
|
key := fmt.Sprintf("%d\x00%s", stageSeq, call.ID)
|
|
if tool, ok := t.toolPublic[key]; ok {
|
|
if tool.name == "" && call.Name != "" {
|
|
t.setToolNameLocked(tool, call.Name)
|
|
}
|
|
return tool, nil
|
|
}
|
|
t.toolSeq++
|
|
publicID := fmt.Sprintf("%s-tool-%d", t.publicResponseID, t.toolSeq)
|
|
if t.toolID != nil {
|
|
allocated, err := t.toolID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("allocate hot path public tool identity: %w", err)
|
|
}
|
|
if !validLogicalRequestID(allocated) {
|
|
return nil, errors.New("allocated hot path public tool identity is invalid")
|
|
}
|
|
publicID = allocated
|
|
}
|
|
tool := &hotPathTurnTool{
|
|
publicID: publicID,
|
|
providerID: call.ID,
|
|
}
|
|
t.setToolNameLocked(tool, call.Name)
|
|
t.toolPublic[key] = tool
|
|
t.toolOrder = append(t.toolOrder, tool)
|
|
return tool, nil
|
|
}
|
|
|
|
// recordStageTerminal folds a held stage terminal's usage into the turn without
|
|
// committing any public terminal.
|
|
func (t *hotPathOuterTurn) recordStageTerminal(term hotPathStageTerminal) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if term.Success && t.outputCapTokens > 0 && (!term.HasUsage || !term.Usage.Reported) {
|
|
t.missingUsage = true
|
|
}
|
|
if term.HasUsage {
|
|
t.aggregateUsageLocked(term.Usage)
|
|
}
|
|
}
|
|
|
|
// selectDisposition elects the logical terminal intent once. Public HTTP-turn
|
|
// commitment remains separate so a provider/validation failure can first emit
|
|
// a caller-owned cleanup tool frontier while preserving the original terminal
|
|
// responsibility for the following continuation.
|
|
func (t *hotPathOuterTurn) selectDisposition(disposition hotPathTerminalDisposition) bool {
|
|
if t == nil || !disposition.valid() {
|
|
return false
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.selectDispositionLocked(disposition)
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) selectDispositionLocked(disposition hotPathTerminalDisposition) bool {
|
|
if t.disposition != nil {
|
|
return false
|
|
}
|
|
selected := disposition
|
|
t.disposition = &selected
|
|
return true
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) terminalDisposition() (hotPathTerminalDisposition, bool) {
|
|
if t == nil {
|
|
return hotPathTerminalDisposition{}, false
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.disposition == nil {
|
|
return hotPathTerminalDisposition{}, false
|
|
}
|
|
return *t.disposition, true
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) activeStageDisposition(kind hotPathDispositionKind, source, cause string) hotPathTerminalDisposition {
|
|
disposition := hotPathTerminalDisposition{Kind: kind, Source: source, Cause: strings.TrimSpace(cause)}
|
|
if t == nil {
|
|
return disposition
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.activeStage != nil {
|
|
disposition.StageID = t.activeStage.stageID
|
|
disposition.Generation = t.activeStage.generation
|
|
}
|
|
return disposition
|
|
}
|
|
|
|
// cancelActiveStage elects timeout/caller-cancel ownership and aborts only the
|
|
// controller registered for the current generation. A caller cancel also
|
|
// closes the public release gate immediately, which keeps the wire silent even
|
|
// if a stale source callback arrives after context cancellation.
|
|
func (t *hotPathOuterTurn) cancelActiveStage(kind hotPathDispositionKind, source string, cause error) bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
var active *hotPathActiveStageController
|
|
disposition := hotPathTerminalDisposition{Kind: kind, Source: source}
|
|
if cause != nil {
|
|
disposition.Cause = cause.Error()
|
|
}
|
|
t.mu.Lock()
|
|
if t.activeStage != nil {
|
|
active = t.activeStage
|
|
disposition.StageID = active.stageID
|
|
disposition.Generation = active.generation
|
|
}
|
|
won := t.selectDispositionLocked(disposition)
|
|
if won && kind == hotPathDispositionCallerCancel {
|
|
t.terminalCommitted = true
|
|
t.terminalReason = string(kind)
|
|
t.terminalError = &hotPathTurnError{errType: string(kind), code: string(kind)}
|
|
}
|
|
t.mu.Unlock()
|
|
if won && active != nil {
|
|
_ = active.AbortAttempt(context.Background())
|
|
}
|
|
return won
|
|
}
|
|
|
|
// aggregateUsageLocked sums normalized stage usage, deduplicating by provider
|
|
// response id so a stage that reports usage twice (or a duplicate provider
|
|
// response id across stages) is only counted once.
|
|
func (t *hotPathOuterTurn) aggregateUsageLocked(u hotPathStageUsage) {
|
|
if u.ResponseID != "" {
|
|
if _, seen := t.usageSeen[u.ResponseID]; seen {
|
|
return
|
|
}
|
|
t.usageSeen[u.ResponseID] = struct{}{}
|
|
}
|
|
t.usage.InputTokens += u.InputTokens
|
|
t.usage.OutputTokens += u.OutputTokens
|
|
t.usage.ReasoningTokens += u.ReasoningTokens
|
|
t.usage.CachedInputTokens += u.CachedInputTokens
|
|
if u.Reported {
|
|
t.usage.Reported = true
|
|
}
|
|
}
|
|
|
|
// commitTerminalSuccess commits the single public success terminal. It returns
|
|
// true only for the first terminal; every later success, error, or cancel is a
|
|
// guarded no-op so exactly one outer terminal ever wins. A visible tool owns
|
|
// the current HTTP terminal even at cap; exhaustion becomes length only when
|
|
// there is no caller continuation frontier.
|
|
func (t *hotPathOuterTurn) commitTerminalSuccess(reason string) bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.terminalCommitted {
|
|
return false
|
|
}
|
|
t.selectDispositionLocked(hotPathTerminalDisposition{
|
|
Kind: hotPathDispositionForSuccess(reason, len(t.toolOrder) > 0),
|
|
Cause: strings.TrimSpace(reason), Source: "outer_turn",
|
|
})
|
|
t.terminalCommitted = true
|
|
reason = strings.TrimSpace(reason)
|
|
switch {
|
|
case len(t.toolOrder) > 0:
|
|
if reason != "tool_calls" && reason != "tool_use" {
|
|
reason = "tool_calls"
|
|
}
|
|
case reason == "":
|
|
reason = "stop"
|
|
}
|
|
t.terminalReason = reason
|
|
return true
|
|
}
|
|
|
|
// commitTerminalError commits the single public error/cancel terminal under the
|
|
// same exactly-once guard as commitTerminalSuccess.
|
|
func (t *hotPathOuterTurn) commitTerminalError(errType, code string) bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.terminalCommitted {
|
|
return false
|
|
}
|
|
kind := hotPathDispositionProviderError
|
|
if strings.Contains(strings.ToLower(errType), "invalid") || strings.Contains(strings.ToLower(code), "validation") {
|
|
kind = hotPathDispositionValidationError
|
|
}
|
|
t.selectDispositionLocked(hotPathTerminalDisposition{
|
|
Kind: kind, Cause: hotPathFirstNonEmpty(code, errType), Source: "outer_turn",
|
|
})
|
|
t.terminalCommitted = true
|
|
t.terminalError = &hotPathTurnError{errType: strings.TrimSpace(errType), code: strings.TrimSpace(code)}
|
|
t.terminalReason = strings.TrimSpace(errType)
|
|
return true
|
|
}
|
|
|
|
// accumulator returns the compatibility view a later caller codec renders: the
|
|
// turn public id, remapped tool calls with assembled arguments, aggregated
|
|
// usage, and the resolved terminal reason.
|
|
func (t *hotPathOuterTurn) accumulator() normalizedStageOutput {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
out := normalizedStageOutput{
|
|
ResponseID: t.publicResponseID,
|
|
Content: t.content.String(),
|
|
Reasoning: t.reasoning.String(),
|
|
}
|
|
for _, tool := range t.toolOrder {
|
|
out.ToolCalls = append(out.ToolCalls, normalizedToolCall{
|
|
ID: tool.publicID,
|
|
ProviderCallID: hotPathFirstNonEmpty(tool.providerID, tool.publicID),
|
|
Name: tool.name,
|
|
RawArgs: tool.args.String(),
|
|
})
|
|
}
|
|
if t.usage.Reported {
|
|
usage := &openAIUsage{
|
|
PromptTokens: t.usage.InputTokens,
|
|
CompletionTokens: t.usage.OutputTokens,
|
|
TotalTokens: t.usage.InputTokens + t.usage.OutputTokens,
|
|
ReasoningTokens: t.usage.ReasoningTokens,
|
|
CachedInputTokens: t.usage.CachedInputTokens,
|
|
}
|
|
out.OpenAIUsage = usage
|
|
out.Usage, _ = json.Marshal(usage)
|
|
}
|
|
out.TerminalReason = t.terminalReasonLocked()
|
|
return out
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) terminalReasonLocked() string {
|
|
if t.terminalReason != "" {
|
|
return t.terminalReason
|
|
}
|
|
if len(t.toolOrder) > 0 {
|
|
return "tool_calls"
|
|
}
|
|
if t.capExhausted {
|
|
return "length"
|
|
}
|
|
return "stop"
|
|
}
|
|
|
|
// releasedDeltas returns a defensive copy of the ordered release log.
|
|
func (t *hotPathOuterTurn) releasedDeltas() []hotPathReleasedDelta {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return append([]hotPathReleasedDelta(nil), t.released...)
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) turnUsage() hotPathTurnUsage {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.usage
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) setPreviewUsage(usage hotPathStageUsage) {
|
|
if t == nil || !usage.Reported {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
t.previewUsage = usage
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) currentPreviewUsage() (hotPathStageUsage, bool) {
|
|
if t == nil {
|
|
return hotPathStageUsage{}, false
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.previewUsage, t.previewUsage.Reported
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) setReasoningSignature(signature string) {
|
|
if t == nil || signature == "" {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
t.reasoningSignature = signature
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) currentReasoningSignature() string {
|
|
if t == nil {
|
|
return ""
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.reasoningSignature
|
|
}
|
|
|
|
// reportedOutputTokens returns only deduplicated provider-reported output
|
|
// usage. Caller-visible payload length is intentionally unrelated.
|
|
func (t *hotPathOuterTurn) reportedOutputTokens() int {
|
|
if t == nil {
|
|
return 0
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.usage.OutputTokens
|
|
}
|
|
|
|
// outputBudget reports whether another provider stage may be dispatched from
|
|
// this HTTP turn. It is deliberately independent from current-terminal tool
|
|
// ownership: an exhausted budget blocks a later provider stage, but does not
|
|
// discard a visible tool call that still requires a caller result frontier.
|
|
func (t *hotPathOuterTurn) outputBudget() hotPathOutputBudget {
|
|
if t == nil {
|
|
return hotPathOutputBudget{}
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.outputCapTokens <= 0 {
|
|
return hotPathOutputBudget{}
|
|
}
|
|
remaining := t.outputCapTokens - t.usage.OutputTokens
|
|
exhausted := remaining <= 0
|
|
if remaining < 0 {
|
|
remaining = 0
|
|
}
|
|
return hotPathOutputBudget{
|
|
Limited: true, Remaining: remaining, Exhausted: exhausted,
|
|
MissingUsage: t.missingUsage,
|
|
}
|
|
}
|
|
|
|
// commitLengthTerminal marks provider-usage-driven exhaustion before the
|
|
// endpoint codec renders the one public length terminal.
|
|
func (t *hotPathOuterTurn) commitLengthTerminal() bool {
|
|
if t == nil {
|
|
return false
|
|
}
|
|
t.mu.Lock()
|
|
t.capExhausted = true
|
|
t.mu.Unlock()
|
|
return t.commitTerminalSuccess("length")
|
|
}
|
|
|
|
// projectToolIdentities installs the public/provider mapping allocated by the
|
|
// logical-request or workspace frontier without changing the accumulator's
|
|
// capped arguments or ordering. It must run before that frontier is registered.
|
|
func (t *hotPathOuterTurn) projectToolIdentities(calls []normalizedToolCall) error {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if len(calls) != len(t.toolOrder) {
|
|
return fmt.Errorf("hot path tool projection count %d does not match accumulated count %d", len(calls), len(t.toolOrder))
|
|
}
|
|
for index, call := range calls {
|
|
publicID := strings.TrimSpace(call.ID)
|
|
if publicID == "" {
|
|
return fmt.Errorf("hot path tool projection %d is missing public identity", index)
|
|
}
|
|
tool := t.toolOrder[index]
|
|
tool.publicID = publicID
|
|
tool.providerID = hotPathFirstNonEmpty(call.ProviderCallID, tool.providerID, publicID)
|
|
if call.Name != "" {
|
|
t.setToolNameLocked(tool, call.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// recordCollectedStage is the compatibility bridge for existing collectors.
|
|
// It keeps the outer turn's accounting and terminal ownership authoritative
|
|
// while legacy endpoint renderers still consume normalizedStageOutput rather
|
|
// than ReleaseEvent values directly. Stage output is recorded once per
|
|
// provider response identity, matching normal stage-runtime aggregation.
|
|
func (t *hotPathOuterTurn) recordCollectedStage(output normalizedStageOutput) {
|
|
if t == nil || output.OpenAIUsage == nil {
|
|
return
|
|
}
|
|
t.recordStageTerminal(hotPathStageTerminal{HasUsage: true, Usage: hotPathStageUsage{
|
|
ResponseID: output.ResponseID, InputTokens: output.OpenAIUsage.PromptTokens,
|
|
OutputTokens: output.OpenAIUsage.CompletionTokens, ReasoningTokens: output.OpenAIUsage.ReasoningTokens,
|
|
CachedInputTokens: output.OpenAIUsage.CachedInputTokens, Reported: true,
|
|
}})
|
|
}
|
|
|
|
// hotPathCollectedStageSource adapts the pre-existing compatibility collector
|
|
// to the stage-scoped runtime. It is intentionally transitional: provider
|
|
// transport sources can replace it without changing outer-turn ownership or
|
|
// endpoint rendering, while every collected stage already follows the same
|
|
// response-start/delta/held-terminal lifecycle.
|
|
type hotPathCollectedStageSource struct {
|
|
events []streamgate.NormalizedEvent
|
|
index int
|
|
usage hotPathStageUsage
|
|
}
|
|
|
|
func newHotPathCollectedStageSource(output normalizedStageOutput) (*hotPathCollectedStageSource, error) {
|
|
now := time.Now()
|
|
events := make([]streamgate.NormalizedEvent, 0, 4+len(output.Deltas)+len(output.ToolCalls))
|
|
start, err := streamgate.NewResponseStartEvent(streamGateChannelDefault, 200, nil, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, start)
|
|
if len(output.Deltas) > 0 {
|
|
for _, delta := range output.Deltas {
|
|
var event streamgate.NormalizedEvent
|
|
switch delta.Kind {
|
|
case normalizedStageDeltaReasoning:
|
|
event, err = streamgate.NewReasoningDeltaEvent(streamGateChannelDefault, delta.Text, now)
|
|
case normalizedStageDeltaText:
|
|
event, err = streamgate.NewTextDeltaEvent(streamGateChannelDefault, delta.Text, now)
|
|
case normalizedStageDeltaTool:
|
|
event, err = streamgate.NewToolCallFragmentEvent(
|
|
streamGateChannelDefault, delta.ToolID, delta.ToolName, delta.Arguments, now,
|
|
)
|
|
default:
|
|
err = fmt.Errorf("unsupported normalized stage delta kind %q", delta.Kind)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
} else {
|
|
if output.Reasoning != "" {
|
|
event, err := streamgate.NewReasoningDeltaEvent(streamGateChannelDefault, output.Reasoning, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
if output.Content != "" {
|
|
event, err := streamgate.NewTextDeltaEvent(streamGateChannelDefault, output.Content, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
for _, call := range output.ToolCalls {
|
|
providerID := hotPathFirstNonEmpty(call.ProviderCallID, call.ID)
|
|
event, err := streamgate.NewToolCallFragmentEvent(streamGateChannelDefault, providerID, call.Name, directToolArguments(call), now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
}
|
|
terminal, err := streamgate.NewTerminalEvent(streamGateChannelDefault, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, terminal)
|
|
source := &hotPathCollectedStageSource{events: events, usage: hotPathStageUsage{ResponseID: output.ResponseID}}
|
|
if output.OpenAIUsage != nil {
|
|
source.usage.InputTokens = output.OpenAIUsage.PromptTokens
|
|
source.usage.OutputTokens = output.OpenAIUsage.CompletionTokens
|
|
source.usage.ReasoningTokens = output.OpenAIUsage.ReasoningTokens
|
|
source.usage.CachedInputTokens = output.OpenAIUsage.CachedInputTokens
|
|
source.usage.Reported = true
|
|
} else if len(output.Usage) > 0 {
|
|
var usage anthropicUsage
|
|
if err := json.Unmarshal(output.Usage, &usage); err == nil {
|
|
source.usage.InputTokens = usage.InputTokens
|
|
source.usage.OutputTokens = usage.OutputTokens
|
|
source.usage.CachedInputTokens = usage.CacheReadInputTokens
|
|
source.usage.Reported = true
|
|
}
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
func (s *hotPathCollectedStageSource) NextEvent(context.Context) (streamgate.NormalizedEvent, error) {
|
|
if s.index >= len(s.events) {
|
|
return streamgate.NormalizedEvent{}, errors.New("hot path collected stage exhausted")
|
|
}
|
|
event := s.events[s.index]
|
|
s.index++
|
|
return event, nil
|
|
}
|
|
|
|
func (s *hotPathCollectedStageSource) stageUsage() (hotPathStageUsage, bool) {
|
|
return s.usage, s.usage.Reported
|
|
}
|
|
|
|
type hotPathCollectedStageController struct{}
|
|
|
|
func (hotPathCollectedStageController) AbortAttempt(context.Context) error { return nil }
|
|
func (hotPathCollectedStageController) CloseAttempt(context.Context) error { return nil }
|
|
|
|
func runHotPathCollectedStage(ctx context.Context, outer *hotPathOuterTurn, stageID string, output normalizedStageOutput) error {
|
|
if err := outer.bindPublicResponseID(output.ResponseID); err != nil {
|
|
return err
|
|
}
|
|
source, err := newHotPathCollectedStageSource(output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = runHotPathStage(ctx, outer, hotPathStageMeta{
|
|
StageID: stageID, Model: "hot-path-collected", Provider: "collector",
|
|
ExecutionPath: "collected", ResponseID: output.ResponseID, AttemptID: output.ResponseID,
|
|
}, source, source, hotPathCollectedStageController{})
|
|
return err
|
|
}
|
|
|
|
// hotPathRemainingOutputTokens is retained as a narrow compatibility helper for
|
|
// tests and builders. Exhaustion is zero; callers that need to distinguish it
|
|
// from unlimited use hotPathOutputBudget directly.
|
|
func hotPathRemainingOutputTokens(cap int, outer *hotPathOuterTurn) int {
|
|
if cap <= 0 {
|
|
return cap
|
|
}
|
|
if outer == nil {
|
|
return cap
|
|
}
|
|
outer.mu.Lock()
|
|
defer outer.mu.Unlock()
|
|
used := outer.usage.OutputTokens
|
|
if used >= cap {
|
|
return 0
|
|
}
|
|
return cap - used
|
|
}
|
|
|
|
// hotPathCompatibilityOutput preserves endpoint-required provider metadata
|
|
// from the final stage while projecting every caller-visible payload, public
|
|
// tool identity, aggregate usage, and terminal reason from the outer turn.
|
|
func hotPathCompatibilityOutput(outer *hotPathOuterTurn, final normalizedStageOutput, protocol string) normalizedStageOutput {
|
|
if outer == nil || final.CallerStageOnly {
|
|
return final
|
|
}
|
|
result := cloneNormalizedStageOutput(final)
|
|
accumulated := outer.accumulator()
|
|
result.Content = accumulated.Content
|
|
result.Reasoning = accumulated.Reasoning
|
|
result.ToolCalls = cloneNormalizedStageOutput(accumulated).ToolCalls
|
|
if accumulated.OpenAIUsage != nil {
|
|
result.OpenAIUsage = accumulated.OpenAIUsage
|
|
usage := make(map[string]any)
|
|
_ = json.Unmarshal(final.Usage, &usage)
|
|
if protocol == "anthropic" {
|
|
delete(usage, "prompt_tokens")
|
|
delete(usage, "completion_tokens")
|
|
delete(usage, "total_tokens")
|
|
delete(usage, "reasoning_tokens")
|
|
delete(usage, "cached_input_tokens")
|
|
usage["input_tokens"] = accumulated.OpenAIUsage.PromptTokens
|
|
usage["output_tokens"] = accumulated.OpenAIUsage.CompletionTokens
|
|
if accumulated.OpenAIUsage.CachedInputTokens > 0 {
|
|
usage["cache_read_input_tokens"] = accumulated.OpenAIUsage.CachedInputTokens
|
|
}
|
|
} else {
|
|
usage["prompt_tokens"] = accumulated.OpenAIUsage.PromptTokens
|
|
usage["completion_tokens"] = accumulated.OpenAIUsage.CompletionTokens
|
|
usage["total_tokens"] = accumulated.OpenAIUsage.PromptTokens + accumulated.OpenAIUsage.CompletionTokens
|
|
}
|
|
result.Usage, _ = json.Marshal(usage)
|
|
}
|
|
result.TerminalReason = accumulated.TerminalReason
|
|
return result
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) capExhaustedFlag() bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.capExhausted
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) isTerminalCommitted() bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.terminalCommitted
|
|
}
|
|
|
|
// hotPathStageReleaseSink is the boundary between one stage-scoped Core runtime
|
|
// and the shared outer turn. Nonterminal deltas are forwarded immediately; the
|
|
// stage terminal is captured as typed transition evidence and folded into the
|
|
// turn without committing any public terminal.
|
|
type hotPathStageReleaseSink struct {
|
|
outer *hotPathOuterTurn
|
|
active *hotPathActiveStageController
|
|
stageSeq int
|
|
usage hotPathStageUsageProbe
|
|
identity hotPathStageIdentityProbe
|
|
terminalReason hotPathStageTerminalReasonProbe
|
|
terminalCause hotPathStageTerminalCauseProbe
|
|
signature hotPathStageSignatureProbe
|
|
|
|
mu sync.Mutex
|
|
terminal *hotPathStageTerminal
|
|
content strings.Builder
|
|
reasoning strings.Builder
|
|
tools map[string]*hotPathProjectedTool
|
|
toolOrder []string
|
|
deltas []normalizedStageDelta
|
|
progressive bool
|
|
}
|
|
|
|
type hotPathProjectedTool struct {
|
|
name string
|
|
args strings.Builder
|
|
}
|
|
|
|
func (s *hotPathStageReleaseSink) CommitResponseStart(_ context.Context, rs streamgate.ResponseStart) (streamgate.CommitState, error) {
|
|
if s.active != nil && !s.active.isCurrent() {
|
|
return streamgate.CommitStateStreamOpen, nil
|
|
}
|
|
if err := s.outer.openResponse(rs); err != nil {
|
|
return "", err
|
|
}
|
|
return streamgate.CommitStateStreamOpen, nil
|
|
}
|
|
|
|
func (s *hotPathStageReleaseSink) Release(_ context.Context, ev streamgate.ReleaseEvent) (streamgate.CommitState, error) {
|
|
if s.active != nil && !s.active.isCurrent() {
|
|
return streamgate.CommitStateStreamOpen, nil
|
|
}
|
|
if s.identity != nil {
|
|
responseID, ok := s.identity.stageIdentity()
|
|
if !ok {
|
|
return "", errors.New("hot path live stage is missing provider response identity")
|
|
}
|
|
if err := s.outer.bindPublicResponseID(responseID); err != nil {
|
|
return "", err
|
|
}
|
|
} else if _, ok := s.outer.publicResponseIdentity(); !ok {
|
|
return "", errors.New("hot path stage cannot release without a public response identity")
|
|
}
|
|
if s.usage != nil {
|
|
if usage, ok := s.usage.stageUsage(); ok {
|
|
s.outer.setPreviewUsage(usage)
|
|
}
|
|
}
|
|
if s.signature != nil {
|
|
s.outer.setReasoningSignature(s.signature.stageReasoningSignature())
|
|
}
|
|
released, err := s.outer.releaseDeltaRecorded(s.stageSeq, ev)
|
|
if err != nil {
|
|
var callbackErr *hotPathReleaseCallbackError
|
|
if !errors.As(err, &callbackErr) {
|
|
return "", err
|
|
}
|
|
stageID := ""
|
|
if s.active != nil {
|
|
stageID = s.active.stageID
|
|
}
|
|
return "", newHotPathDispositionError(hotPathDispositionCallerCancel, "caller_write", stageID, callbackErr)
|
|
}
|
|
if released != nil {
|
|
s.mu.Lock()
|
|
s.progressive = true
|
|
s.mu.Unlock()
|
|
if err := s.recordReleased(ev, *released); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return streamgate.CommitStateStreamOpen, nil
|
|
}
|
|
|
|
func (s *hotPathStageReleaseSink) recordReleased(ev streamgate.ReleaseEvent, released hotPathReleasedDelta) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
switch released.Kind {
|
|
case streamgate.EventKindTextDelta:
|
|
s.content.WriteString(released.Text)
|
|
s.deltas = append(s.deltas, normalizedStageDelta{Kind: normalizedStageDeltaText, Text: released.Text})
|
|
case streamgate.EventKindReasoningDelta:
|
|
s.reasoning.WriteString(released.Text)
|
|
s.deltas = append(s.deltas, normalizedStageDelta{Kind: normalizedStageDeltaReasoning, Text: released.Text})
|
|
case streamgate.EventKindToolCallFragment:
|
|
call, err := ev.AsToolCallFragment()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tool := s.tools[call.ID]
|
|
if tool == nil {
|
|
tool = &hotPathProjectedTool{}
|
|
s.tools[call.ID] = tool
|
|
s.toolOrder = append(s.toolOrder, call.ID)
|
|
}
|
|
if call.Name != "" {
|
|
tool.name = call.Name
|
|
}
|
|
tool.args.WriteString(released.Args)
|
|
s.deltas = append(s.deltas, normalizedStageDelta{
|
|
Kind: normalizedStageDeltaTool, ToolID: call.ID, ToolName: tool.name, Arguments: released.Args,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *hotPathStageReleaseSink) CommitTerminal(_ context.Context, tr streamgate.TerminalResult) (streamgate.CommitState, error) {
|
|
if s.active != nil && !s.active.isCurrent() {
|
|
return streamgate.CommitStateTerminalCommitted, nil
|
|
}
|
|
term := hotPathStageTerminal{Success: tr.Success()}
|
|
if tr.Error() {
|
|
if desc := tr.ExternalDesc(); desc != nil {
|
|
term.ErrType = desc.Type()
|
|
term.ErrCode = desc.Code()
|
|
}
|
|
} else {
|
|
term.Reason = hotPathTerminalReasonOrStop("")
|
|
if s.terminalReason != nil {
|
|
term.Reason = hotPathTerminalReasonOrStop(s.terminalReason.stageTerminalReason())
|
|
}
|
|
}
|
|
if term.Success {
|
|
term.Disposition = hotPathTerminalDisposition{
|
|
Kind: hotPathDispositionForSuccess(term.Reason, false), Cause: term.Reason,
|
|
Source: "stage_terminal", StageID: s.active.stageID, Generation: s.active.generation,
|
|
}
|
|
} else {
|
|
kind := hotPathDispositionProviderError
|
|
if s.terminalCause != nil && s.terminalCause.stageTerminalCause().valid() {
|
|
kind = s.terminalCause.stageTerminalCause().Kind
|
|
}
|
|
term.Disposition = hotPathTerminalDisposition{
|
|
Kind: kind, Cause: hotPathFirstNonEmpty(term.ErrCode, term.ErrType),
|
|
Source: "stage_terminal", StageID: s.active.stageID, Generation: s.active.generation,
|
|
}
|
|
}
|
|
if s.usage != nil {
|
|
if u, ok := s.usage.stageUsage(); ok {
|
|
term.Usage = u
|
|
term.HasUsage = true
|
|
}
|
|
}
|
|
if term.Success && s.identity != nil {
|
|
if _, ok := s.identity.stageIdentity(); !ok {
|
|
return "", errors.New("hot path live stage completed without provider response identity")
|
|
}
|
|
}
|
|
s.mu.Lock()
|
|
if s.terminal != nil {
|
|
s.mu.Unlock()
|
|
return streamgate.CommitStateTerminalCommitted, nil
|
|
}
|
|
s.terminal = &term
|
|
s.mu.Unlock()
|
|
s.outer.recordStageTerminal(term)
|
|
return streamgate.CommitStateTerminalCommitted, nil
|
|
}
|
|
|
|
func (s *hotPathStageReleaseSink) stageOutput() (normalizedStageOutput, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
responseID := ""
|
|
if s.identity != nil {
|
|
responseID, _ = s.identity.stageIdentity()
|
|
}
|
|
output := normalizedStageOutput{
|
|
ResponseID: responseID, Content: s.content.String(), Reasoning: s.reasoning.String(),
|
|
Deltas: append([]normalizedStageDelta(nil), s.deltas...), ProgressivelyReleased: s.progressive,
|
|
}
|
|
if s.signature != nil {
|
|
output.ReasoningSignature = s.signature.stageReasoningSignature()
|
|
}
|
|
for _, providerID := range s.toolOrder {
|
|
tool := s.tools[providerID]
|
|
call, err := normalizedToolCallFromParts(providerID, tool.name, tool.args.String())
|
|
if err != nil {
|
|
return normalizedStageOutput{}, err
|
|
}
|
|
output.ToolCalls = append(output.ToolCalls, call)
|
|
}
|
|
if s.terminal != nil {
|
|
output.TerminalReason = hotPathTerminalReasonOrStop(s.terminal.Reason)
|
|
} else {
|
|
output.TerminalReason = hotPathTerminalReasonOrStop("")
|
|
}
|
|
if len(output.ToolCalls) > 0 {
|
|
output.TerminalReason = "tool_calls"
|
|
}
|
|
if s.terminal != nil && s.terminal.HasUsage {
|
|
u := s.terminal.Usage
|
|
output.OpenAIUsage = &openAIUsage{
|
|
PromptTokens: u.InputTokens, CompletionTokens: u.OutputTokens,
|
|
TotalTokens: u.InputTokens + u.OutputTokens, ReasoningTokens: u.ReasoningTokens,
|
|
CachedInputTokens: u.CachedInputTokens,
|
|
}
|
|
output.Usage, _ = json.Marshal(output.OpenAIUsage)
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
// stageTerminal returns the held stage terminal evidence, if the stage runtime
|
|
// committed one.
|
|
func (s *hotPathStageReleaseSink) stageTerminal() (hotPathStageTerminal, bool) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.terminal == nil {
|
|
return hotPathStageTerminal{}, false
|
|
}
|
|
return *s.terminal, true
|
|
}
|
|
|
|
var _ streamgate.ReleaseSink = (*hotPathStageReleaseSink)(nil)
|
|
|
|
func hotPathTerminalReasonOrStop(reason string) string {
|
|
reason = strings.TrimSpace(reason)
|
|
if reason == "" {
|
|
return "stop"
|
|
}
|
|
return reason
|
|
}
|
|
|
|
// hotPathStageAttemptController owns one real stage transport. Implementations
|
|
// must make both operations idempotent: Core invokes AbortAttempt for errors or
|
|
// cancellation and CloseAttempt after a successful terminal.
|
|
type hotPathStageAttemptController interface {
|
|
streamgate.AttemptController
|
|
CloseAttempt(context.Context) error
|
|
}
|
|
|
|
// hotPathStageAttemptOwner preserves one stage transport's ownership when Core
|
|
// reaches the same cleanup path through both an error terminal and final
|
|
// resource cleanup. The wrapped transport observes at most one abort and one
|
|
// graceful close request.
|
|
type hotPathStageAttemptOwner struct {
|
|
controller hotPathStageAttemptController
|
|
|
|
abortOnce sync.Once
|
|
abortErr error
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
func newHotPathStageAttemptOwner(controller hotPathStageAttemptController) *hotPathStageAttemptOwner {
|
|
return &hotPathStageAttemptOwner{controller: controller}
|
|
}
|
|
|
|
func (o *hotPathStageAttemptOwner) AbortAttempt(ctx context.Context) error {
|
|
o.abortOnce.Do(func() {
|
|
o.abortErr = o.controller.AbortAttempt(ctx)
|
|
})
|
|
return o.abortErr
|
|
}
|
|
|
|
func (o *hotPathStageAttemptOwner) CloseAttempt(ctx context.Context) error {
|
|
o.closeOnce.Do(func() {
|
|
o.closeErr = o.controller.CloseAttempt(ctx)
|
|
})
|
|
return o.closeErr
|
|
}
|
|
|
|
// hotPathActiveStageController is the generation-fenced registration stored by
|
|
// one outer turn. It shares the same idempotent owner with the Core attempt, so
|
|
// a context watcher, Core abort, stale callback, and final resource cleanup can
|
|
// never issue duplicate CancelRun calls.
|
|
type hotPathActiveStageController struct {
|
|
outer *hotPathOuterTurn
|
|
stageID string
|
|
generation uint64
|
|
owner *hotPathStageAttemptOwner
|
|
|
|
actionOnce sync.Once
|
|
actionErr error
|
|
finishOnce sync.Once
|
|
}
|
|
|
|
func (t *hotPathOuterTurn) registerActiveStage(stageID string, controller hotPathStageAttemptController) (*hotPathActiveStageController, error) {
|
|
if t == nil || controller == nil {
|
|
return nil, errors.New("hot path active stage controller is unavailable")
|
|
}
|
|
stageID = strings.TrimSpace(stageID)
|
|
if stageID == "" {
|
|
return nil, errors.New("hot path active stage identity is empty")
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.terminalCommitted {
|
|
return nil, errHotPathTurnTerminal
|
|
}
|
|
if t.activeStage != nil {
|
|
return nil, fmt.Errorf("hot path stage %q is still active", t.activeStage.stageID)
|
|
}
|
|
t.activeGeneration++
|
|
active := &hotPathActiveStageController{
|
|
outer: t, stageID: stageID, generation: t.activeGeneration,
|
|
owner: newHotPathStageAttemptOwner(controller),
|
|
}
|
|
t.activeStage = active
|
|
return active, nil
|
|
}
|
|
|
|
func (c *hotPathActiveStageController) isCurrent() bool {
|
|
if c == nil || c.outer == nil {
|
|
return false
|
|
}
|
|
c.outer.mu.Lock()
|
|
defer c.outer.mu.Unlock()
|
|
return c.outer.activeStage == c && c.outer.activeGeneration == c.generation
|
|
}
|
|
|
|
func (c *hotPathActiveStageController) unregister() {
|
|
if c == nil || c.outer == nil {
|
|
return
|
|
}
|
|
c.finishOnce.Do(func() {
|
|
c.outer.mu.Lock()
|
|
if c.outer.activeStage == c && c.outer.activeGeneration == c.generation {
|
|
c.outer.activeStage = nil
|
|
}
|
|
c.outer.mu.Unlock()
|
|
})
|
|
}
|
|
|
|
func (c *hotPathActiveStageController) AbortAttempt(ctx context.Context) error {
|
|
if c == nil || c.owner == nil {
|
|
return nil
|
|
}
|
|
c.actionOnce.Do(func() {
|
|
c.actionErr = c.owner.AbortAttempt(ctx)
|
|
c.unregister()
|
|
})
|
|
return c.actionErr
|
|
}
|
|
|
|
func (c *hotPathActiveStageController) CloseAttempt(ctx context.Context) error {
|
|
if c == nil || c.owner == nil {
|
|
return nil
|
|
}
|
|
c.actionOnce.Do(func() {
|
|
c.actionErr = c.owner.CloseAttempt(ctx)
|
|
c.unregister()
|
|
})
|
|
return c.actionErr
|
|
}
|
|
|
|
var _ hotPathStageAttemptController = (*hotPathActiveStageController)(nil)
|
|
|
|
// hotPathStageNoRecoveryDispatcher / hotPathStageNoRecoveryRebuilder satisfy the
|
|
// required Core recovery seams for a stage runtime configured with zero fault
|
|
// recovery. They are never invoked and fail closed if they ever are.
|
|
type hotPathStageNoRecoveryDispatcher struct{}
|
|
|
|
func (hotPathStageNoRecoveryDispatcher) DispatchAttempt(context.Context, streamgate.RebuiltRequest) (streamgate.AttemptBinding, error) {
|
|
return streamgate.AttemptBinding{}, errors.New("hot path stage runtime does not recover")
|
|
}
|
|
|
|
type hotPathStageNoRecoveryRebuilder struct{}
|
|
|
|
func (hotPathStageNoRecoveryRebuilder) RebuildRequest(context.Context, streamgate.RecoveryRequestSnapshotRef, streamgate.RecoveryPlan) (streamgate.RebuiltRequestDraft, error) {
|
|
return streamgate.RebuiltRequestDraft{}, errors.New("hot path stage runtime does not rebuild")
|
|
}
|
|
|
|
// newHotPathStageRuntime builds a stage-scoped Core runtime for one provider
|
|
// stage. The runtime commits a terminal exactly once per stage, but its release
|
|
// sink converts that into held evidence, so the runtime lifecycle ends while the
|
|
// outer turn survives for the next stage on the same HTTP request.
|
|
func newHotPathStageRuntime(outer *hotPathOuterTurn, meta hotPathStageMeta, source streamgate.NormalizedEventSource, usage hotPathStageUsageProbe, controller hotPathStageAttemptController) (*streamgate.RequestRuntime, *hotPathStageReleaseSink, error) {
|
|
if outer == nil {
|
|
return nil, nil, errors.New("hot path stage runtime requires an outer turn")
|
|
}
|
|
if source == nil {
|
|
return nil, nil, errors.New("hot path stage runtime requires an event source")
|
|
}
|
|
if controller == nil {
|
|
return nil, nil, errors.New("hot path stage runtime requires an attempt controller")
|
|
}
|
|
|
|
stageSeq := outer.beginStage()
|
|
identity, _ := source.(hotPathStageIdentityProbe)
|
|
terminalReason, _ := source.(hotPathStageTerminalReasonProbe)
|
|
terminalCause, _ := source.(hotPathStageTerminalCauseProbe)
|
|
signature, _ := source.(hotPathStageSignatureProbe)
|
|
|
|
opts, err := streamgate.NewRuntimeOptions(
|
|
streamgate.DefaultMaxEvidenceRunes,
|
|
streamgate.DefaultMaxBufferRunes,
|
|
streamgate.DefaultMaxIngressSnapshotBytes,
|
|
0,
|
|
streamgate.GateCoordinatorOptions{},
|
|
streamgate.RecoveryCoordinatorOptions{},
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
registry, err := openAIStreamGateRegistrySnapshotWith()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
snapRef, err := streamgate.NewRecoveryRequestSnapshotRef(
|
|
openAIStreamGateSafeToken("stage", meta.token()),
|
|
0, 0, uint64(streamgate.DefaultMaxIngressSnapshotBytes),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
model := hotPathFirstNonEmpty(meta.Model, "hot-path-stage")
|
|
provider := hotPathFirstNonEmpty(meta.Provider, "hot-path-provider")
|
|
execPath := hotPathFirstNonEmpty(meta.ExecutionPath, "normalized")
|
|
active, err := outer.registerActiveStage(meta.StageID, controller)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sink := &hotPathStageReleaseSink{
|
|
outer: outer, active: active, stageSeq: stageSeq, usage: usage, identity: identity,
|
|
terminalReason: terminalReason, terminalCause: terminalCause, signature: signature,
|
|
tools: make(map[string]*hotPathProjectedTool),
|
|
}
|
|
|
|
binding, err := streamgate.NewAttemptBinding(
|
|
openAIStreamGateSafeToken("attempt", hotPathFirstNonEmpty(meta.AttemptID, meta.ResponseID, meta.StageID)),
|
|
model, provider, execPath, source, active,
|
|
)
|
|
if err != nil {
|
|
_ = active.AbortAttempt(context.Background())
|
|
return nil, nil, err
|
|
}
|
|
|
|
snapshot, err := streamgate.NewRequestRuntimeSnapshot(
|
|
openAIStreamGateSafeToken("stage-req", meta.token()),
|
|
streamGateConfigGeneration, streamGateEnvironment, "hot-path-stage", "hot-path",
|
|
opts, registry, nil, snapRef,
|
|
hotPathStageNoRecoveryDispatcher{}, hotPathStageNoRecoveryRebuilder{},
|
|
nil, nil, sink,
|
|
)
|
|
if err != nil {
|
|
_ = active.AbortAttempt(context.Background())
|
|
return nil, nil, err
|
|
}
|
|
|
|
rt, err := streamgate.NewRequestRuntime(snapshot, model, binding)
|
|
if err != nil {
|
|
_ = active.AbortAttempt(context.Background())
|
|
return nil, nil, err
|
|
}
|
|
return rt, sink, nil
|
|
}
|
|
|
|
// runHotPathStage runs one stage runtime to its held stage terminal and returns
|
|
// the typed evidence. The outer turn is untouched by stage completion, so the
|
|
// caller can immediately build the next stage on the same turn.
|
|
func runHotPathStage(ctx context.Context, outer *hotPathOuterTurn, meta hotPathStageMeta, source streamgate.NormalizedEventSource, usage hotPathStageUsageProbe, controller hotPathStageAttemptController) (hotPathStageTerminal, error) {
|
|
rt, sink, err := newHotPathStageRuntime(outer, meta, source, usage, controller)
|
|
if err != nil {
|
|
return hotPathStageTerminal{}, err
|
|
}
|
|
term, _, runErr := runHotPathRequestRuntime(ctx, outer, rt, sink)
|
|
if runErr != nil {
|
|
return hotPathStageTerminal{}, wrapHotPathDispositionError(outer, meta.StageID, runErr)
|
|
}
|
|
return term, nil
|
|
}
|
|
|
|
func runHotPathStreamingStage(ctx context.Context, outer *hotPathOuterTurn, meta hotPathStageMeta, source streamgate.NormalizedEventSource, usage hotPathStageUsageProbe, controller hotPathStageAttemptController) (normalizedStageOutput, hotPathStageTerminal, error) {
|
|
rt, sink, err := newHotPathStageRuntime(outer, meta, source, usage, controller)
|
|
if err != nil {
|
|
return normalizedStageOutput{}, hotPathStageTerminal{}, err
|
|
}
|
|
term, _, runErr := runHotPathRequestRuntime(ctx, outer, rt, sink)
|
|
if runErr != nil {
|
|
return normalizedStageOutput{}, hotPathStageTerminal{}, wrapHotPathDispositionError(outer, meta.StageID, runErr)
|
|
}
|
|
output, err := sink.stageOutput()
|
|
if err != nil {
|
|
return normalizedStageOutput{}, hotPathStageTerminal{}, err
|
|
}
|
|
return output, term, nil
|
|
}
|
|
|
|
func runHotPathRequestRuntime(
|
|
ctx context.Context,
|
|
outer *hotPathOuterTurn,
|
|
rt *streamgate.RequestRuntime,
|
|
sink *hotPathStageReleaseSink,
|
|
) (hotPathStageTerminal, bool, error) {
|
|
watchStop := make(chan struct{})
|
|
watchDone := make(chan struct{})
|
|
go func() {
|
|
defer close(watchDone)
|
|
select {
|
|
case <-ctx.Done():
|
|
kind := hotPathDispositionForError(ctx.Err())
|
|
if kind == hotPathDispositionCallerCancel || kind == hotPathDispositionTimeout {
|
|
outer.cancelActiveStage(kind, "caller_context", ctx.Err())
|
|
}
|
|
case <-watchStop:
|
|
}
|
|
}()
|
|
|
|
runErr := rt.Run(ctx)
|
|
close(watchStop)
|
|
<-watchDone
|
|
term, committed := sink.stageTerminal()
|
|
if runErr != nil {
|
|
kind := hotPathDispositionForError(runErr)
|
|
source := "stage_runtime"
|
|
if disposition, ok := hotPathDispositionFromError(runErr); ok {
|
|
kind = disposition.Kind
|
|
source = disposition.Source
|
|
}
|
|
if kind == hotPathDispositionCallerCancel || kind == hotPathDispositionTimeout {
|
|
outer.cancelActiveStage(kind, source, runErr)
|
|
} else {
|
|
outer.selectDisposition(outer.activeStageDisposition(kind, source, runErr.Error()))
|
|
}
|
|
} else if committed && !term.Success {
|
|
if !term.Disposition.valid() {
|
|
term.Disposition = outer.activeStageDisposition(hotPathDispositionProviderError, "stage_terminal", term.ErrCode)
|
|
}
|
|
outer.selectDisposition(term.Disposition)
|
|
}
|
|
_ = rt.CloseRequestResources(context.Background(), runErr == nil && committed && term.Success)
|
|
return term, committed, runErr
|
|
}
|
|
|
|
func wrapHotPathDispositionError(outer *hotPathOuterTurn, stageID string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if _, ok := hotPathDispositionFromError(err); ok {
|
|
return err
|
|
}
|
|
if disposition, ok := outer.terminalDisposition(); ok {
|
|
return &hotPathDispositionError{disposition: disposition, err: err}
|
|
}
|
|
return &hotPathDispositionError{disposition: hotPathTerminalDisposition{
|
|
Kind: hotPathDispositionForError(err), Cause: err.Error(), Source: "stage_runtime", StageID: stageID,
|
|
}, err: err}
|
|
}
|
|
|
|
func hotPathFirstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|