package openai import ( "context" "encoding/json" "errors" "fmt" "sync" "sync/atomic" "iop/packages/go/streamgate" ) const ( openAIRebuildEndpointChat = "/v1/chat/completions" openAIRebuildEndpointResponses = "/v1/responses" openAIRebuildFamily = "openai.json" ) // openAIRepeatResumeDirective is the fixed English continuation instruction the // resume-notice builder writes when a repeat-loop continuation plan is selected. // It is byte-stable: recovery never rewrites, summarizes, or translates it, and // it is the only non-model text added to a resume body. const openAIRepeatResumeDirective = "The previous model output was stopped after a repetition loop was detected. Continue from the provided content and reasoning output without repeating already generated text." // openAIRebuildContextReserveTokens is the completion-token headroom reserved on // top of the measured resume prompt when comparing against the target model's // context window. A resume body whose prompt plus this reserve exceeds the // window (or whose window is unknown) is refused before any dispatch. const openAIRebuildContextReserveTokens = 256 var openAIRebuiltSequence atomic.Uint64 var ( errOpenAIRecoveryPatchDuplicate = errors.New("duplicate OpenAI recovery patch") errOpenAIRebuildContextOverflow = errors.New("OpenAI resume request exceeds the target model context window") errOpenAIRecoverySourceUnavailable = errors.New("OpenAI recovery source snapshot is unavailable") errOpenAIRecoverySourceCursorRange = errors.New("OpenAI recovery source cursor is out of range") ) // openAIRecoverySourceStore is the request-local recorder of the current // attempt's model output. It captures text and reasoning deltas in event order // per channel behind a bounded byte cap and exposes only a stable snapshot ref // and cursor to filters, never the raw output. The Rebuilder consumes a snapshot // once, after the attempt that produced it is aborted, to assemble an // endpoint-specific continuation body. Its ref is the ingress recovery ref so a // continuation directive can address it without leaking model output. type openAIRecoverySourceStore struct { mu sync.Mutex ref string maxBytes int content []byte reasoning []byte // suppressContent/suppressReasoning are one-attempt known-prefix guards. // consume installs the safe continuation prefix and the replacement event // source removes it only when the provider echoes it byte-for-byte. suppressContent []byte suppressReasoning []byte overflow bool consumed bool closed bool } func newOpenAIRecoverySourceStore(ingress *openAIIngressSnapshot) *openAIRecoverySourceStore { store := &openAIRecoverySourceStore{} if ref, err := ingress.recoveryRef(); err == nil { store.ref = ref.SnapshotRef() store.maxBytes = int(ref.MaxBytes()) } return store } func (s *openAIRecoverySourceStore) snapshotRef() string { if s == nil { return "" } return s.ref } func (s *openAIRecoverySourceStore) recordText(text string) { s.record(true, text) } func (s *openAIRecoverySourceStore) recordReasoning(text string) { s.record(false, text) } func (s *openAIRecoverySourceStore) record(isContent bool, text string) { if s == nil || text == "" { return } s.mu.Lock() defer s.mu.Unlock() if s.closed { return } if s.maxBytes > 0 && len(s.content)+len(s.reasoning)+len(text) > s.maxBytes { s.overflow = true return } if isContent { s.content = append(s.content, text...) } else { s.reasoning = append(s.reasoning, text...) } } // resetAttempt clears the recorded output at the start of a new attempt so a // snapshot only ever contains the single attempt that produced it. func (s *openAIRecoverySourceStore) resetAttempt() { if s == nil { return } s.mu.Lock() defer s.mu.Unlock() if s.closed { return } s.content = s.content[:0] s.reasoning = s.reasoning[:0] s.overflow = false s.consumed = false } // consume returns the recorded channels with the repeated tail removed. A // cursor at or below content length addresses the content channel. A cursor // above content length uses content-length+1 as a reasoning-channel sentinel // and addresses the reasoning prefix after it. This keeps the public directive // raw-free and single-field while preserving a rune-boundary byte cursor for // either model-output channel. func (s *openAIRecoverySourceStore) consume(cursor int) (string, string, error) { if s == nil { return "", "", streamgate.ErrIngressSnapshotClosed } s.mu.Lock() defer s.mu.Unlock() if s.closed { return "", "", streamgate.ErrIngressSnapshotClosed } if s.consumed { return "", "", errOpenAIRecoverySourceUnavailable } if s.overflow { return "", "", errOpenAIRebuildContextOverflow } if len(s.content)+len(s.reasoning) == 0 { return "", "", errOpenAIRecoverySourceUnavailable } if cursor < 0 { return "", "", errOpenAIRecoverySourceCursorRange } contentEnd, reasoningEnd := len(s.content), len(s.reasoning) if cursor <= len(s.content) { contentEnd = cursor } else { reasoningEnd = cursor - len(s.content) - 1 if reasoningEnd < 0 || reasoningEnd > len(s.reasoning) { return "", "", errOpenAIRecoverySourceCursorRange } } s.consumed = true content := string(s.content[:contentEnd]) reasoning := string(s.reasoning[:reasoningEnd]) s.suppressContent = append(s.suppressContent[:0], content...) s.suppressReasoning = append(s.suppressReasoning[:0], reasoning...) return content, reasoning, nil } func (s *openAIRecoverySourceStore) suppressKnownPrefix(isContent bool, value string) string { if s == nil || value == "" { return value } s.mu.Lock() defer s.mu.Unlock() var expected *[]byte if isContent { expected = &s.suppressContent } else { expected = &s.suppressReasoning } if len(*expected) == 0 { return value } incoming := []byte(value) switch { case len(incoming) <= len(*expected) && string((*expected)[:len(incoming)]) == value: *expected = (*expected)[len(incoming):] return "" case len(incoming) > len(*expected) && string(incoming[:len(*expected)]) == string(*expected): value = string(incoming[len(*expected):]) *expected = nil return value default: // The replacement started with novel output. Disable suppression // immediately so a coincidental later substring is never removed. *expected = nil return value } } func (s *openAIRecoverySourceStore) close() { if s == nil { return } s.mu.Lock() defer s.mu.Unlock() if s.closed { return } s.closed = true s.content = nil s.reasoning = nil s.suppressContent = nil s.suppressReasoning = nil } // openAIRecoverySourceEventSource wraps a NormalizedEventSource so every text and // reasoning delta the Core reads is recorded into the request-local source store // before it reaches any filter. It resets the store on each response start so // each attempt records only its own output. It is otherwise a transparent // passthrough and never mutates events. type openAIRecoverySourceEventSource struct { inner streamgate.NormalizedEventSource store *openAIRecoverySourceStore } func newOpenAIRecoverySourceEventSource(inner streamgate.NormalizedEventSource, store *openAIRecoverySourceStore) streamgate.NormalizedEventSource { if store == nil { return inner } return &openAIRecoverySourceEventSource{inner: inner, store: store} } func (s *openAIRecoverySourceEventSource) NextEvent(ctx context.Context) (streamgate.NormalizedEvent, error) { for { ev, err := s.inner.NextEvent(ctx) if err != nil { return ev, err } switch ev.Kind() { case streamgate.EventKindResponseStart: s.store.resetAttempt() case streamgate.EventKindTextDelta: if value, valueErr := ev.AsTextDelta(); valueErr == nil { value = s.store.suppressKnownPrefix(true, value) if value == "" { continue } s.store.recordText(value) ev, err = streamgate.NewTextDeltaEvent(ev.Channel(), value, ev.Timestamp()) if err != nil { return streamgate.NormalizedEvent{}, err } } case streamgate.EventKindReasoningDelta: if value, valueErr := ev.AsReasoningDelta(); valueErr == nil { value = s.store.suppressKnownPrefix(false, value) if value == "" { continue } s.store.recordReasoning(value) ev, err = streamgate.NewReasoningDeltaEvent(ev.Channel(), value, ev.Timestamp()) if err != nil { return streamgate.NormalizedEvent{}, err } } } return ev, err } } var _ streamgate.NormalizedEventSource = (*openAIRecoverySourceEventSource)(nil) type openAIRecoveryPatchEntry struct { cursor int value []byte guard *streamgate.IngressSnapshotRebuildGuard once sync.Once } func (e *openAIRecoveryPatchEntry) release() { if e == nil { return } e.once.Do(func() { e.value = nil if e.guard != nil { e.guard.Close() e.guard = nil } }) } // openAIRecoveryPatchStore is request-local. It retains only the typed subtree // named by a safe directive reference; it never retains a second request tree, // caller headers, or provider credentials. type openAIRecoveryPatchStore struct { mu sync.Mutex ingress *openAIIngressSnapshot continuation map[string]*openAIRecoveryPatchEntry schema map[string]*openAIRecoveryPatchEntry closed bool } func newOpenAIRecoveryPatchStore(ingress *openAIIngressSnapshot) *openAIRecoveryPatchStore { return &openAIRecoveryPatchStore{ ingress: ingress, continuation: make(map[string]*openAIRecoveryPatchEntry), schema: make(map[string]*openAIRecoveryPatchEntry), } } func marshalOpenAIPatchValue(value any) ([]byte, error) { if raw, ok := value.(json.RawMessage); ok { if !json.Valid(raw) { return nil, fmt.Errorf("invalid OpenAI recovery patch JSON") } return raw, nil } encoded, err := json.Marshal(value) if err != nil { return nil, err } return encoded, nil } func (s *openAIRecoveryPatchStore) PutContinuation(snapshotRef string, cursor int, value any) error { if _, err := streamgate.NewStableTokenRequired("snapshotRef", snapshotRef); err != nil { return err } if cursor < 0 { return fmt.Errorf("continuation cursor must be non-negative") } encoded, err := marshalOpenAIPatchValue(value) if err != nil { return err } s.mu.Lock() if s.closed { s.mu.Unlock() return streamgate.ErrIngressSnapshotClosed } if _, duplicate := s.continuation[snapshotRef]; duplicate { s.mu.Unlock() return errOpenAIRecoveryPatchDuplicate } ingress := s.ingress s.mu.Unlock() guard, err := ingress.reserveRebuild(int64(len(encoded))) if err != nil { return err } entry := &openAIRecoveryPatchEntry{cursor: cursor, value: encoded, guard: guard} s.mu.Lock() defer s.mu.Unlock() if s.closed { entry.release() return streamgate.ErrIngressSnapshotClosed } if _, duplicate := s.continuation[snapshotRef]; duplicate { entry.release() return errOpenAIRecoveryPatchDuplicate } s.continuation[snapshotRef] = entry return nil } func openAISchemaPatchKey(schemaRef, patchCode string) string { return schemaRef + ":" + patchCode } func (s *openAIRecoveryPatchStore) PutSchema(schemaRef, patchCode string, value any) error { if _, err := streamgate.NewStableTokenRequired("schemaRef", schemaRef); err != nil { return err } if _, err := streamgate.NewStableTokenRequired("patchCode", patchCode); err != nil { return err } encoded, err := marshalOpenAIPatchValue(value) if err != nil { return err } key := openAISchemaPatchKey(schemaRef, patchCode) s.mu.Lock() if s.closed { s.mu.Unlock() return streamgate.ErrIngressSnapshotClosed } if _, duplicate := s.schema[key]; duplicate { s.mu.Unlock() return errOpenAIRecoveryPatchDuplicate } ingress := s.ingress s.mu.Unlock() guard, err := ingress.reserveRebuild(int64(len(encoded))) if err != nil { return err } entry := &openAIRecoveryPatchEntry{value: encoded, guard: guard} s.mu.Lock() defer s.mu.Unlock() if s.closed { entry.release() return streamgate.ErrIngressSnapshotClosed } if _, duplicate := s.schema[key]; duplicate { entry.release() return errOpenAIRecoveryPatchDuplicate } s.schema[key] = entry return nil } func (s *openAIRecoveryPatchStore) takeContinuation(snapshotRef string, cursor int) (*openAIRecoveryPatchEntry, error) { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil, streamgate.ErrIngressSnapshotClosed } entry := s.continuation[snapshotRef] if entry == nil || entry.cursor != cursor { return nil, fmt.Errorf("OpenAI continuation patch is unavailable") } delete(s.continuation, snapshotRef) return entry, nil } func (s *openAIRecoveryPatchStore) takeSchema(schemaRef, patchCode string) (*openAIRecoveryPatchEntry, error) { s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil, streamgate.ErrIngressSnapshotClosed } key := openAISchemaPatchKey(schemaRef, patchCode) entry := s.schema[key] if entry == nil { return nil, fmt.Errorf("OpenAI schema patch is unavailable") } delete(s.schema, key) return entry, nil } func (s *openAIRecoveryPatchStore) close() { if s == nil { return } s.mu.Lock() if s.closed { s.mu.Unlock() return } s.closed = true continuation := s.continuation schema := s.schema s.continuation = make(map[string]*openAIRecoveryPatchEntry) s.schema = make(map[string]*openAIRecoveryPatchEntry) s.ingress = nil s.mu.Unlock() for _, entry := range continuation { entry.release() } for _, entry := range schema { entry.release() } } type openAIRebuiltLease struct { mu sync.Mutex ingress *openAIIngressSnapshot guard *streamgate.IngressSnapshotRebuildGuard rebuilt *streamgate.IngressSnapshot bodyAlias []byte released bool once sync.Once } func (l *openAIRebuiltLease) body() ([]byte, error) { if l == nil { return nil, fmt.Errorf("rebuilt OpenAI request is unavailable") } l.mu.Lock() defer l.mu.Unlock() if l.released || l.bodyAlias == nil { return nil, fmt.Errorf("rebuilt OpenAI request is unavailable") } return l.bodyAlias, nil } func (l *openAIRebuiltLease) release() { if l == nil { return } l.once.Do(func() { l.mu.Lock() rebuilt := l.rebuilt guard := l.guard l.bodyAlias = nil l.rebuilt = nil l.guard = nil l.ingress = nil l.released = true l.mu.Unlock() if rebuilt != nil { rebuilt.Close() } if guard != nil { guard.Close() } }) } func (l *openAIRebuiltLease) isReleased() bool { if l == nil { return true } l.mu.Lock() defer l.mu.Unlock() return l.released } type openAIRebuiltRequestStore struct { mu sync.Mutex leases map[string]*openAIRebuiltLease closed bool } func newOpenAIRebuiltRequestStore() *openAIRebuiltRequestStore { return &openAIRebuiltRequestStore{leases: make(map[string]*openAIRebuiltLease)} } func (s *openAIRebuiltRequestStore) put(ref string, lease *openAIRebuiltLease) error { s.mu.Lock() if s.closed { s.mu.Unlock() return streamgate.ErrIngressSnapshotClosed } if _, exists := s.leases[ref]; exists { s.mu.Unlock() return fmt.Errorf("duplicate rebuilt OpenAI request reference") } s.leases[ref] = lease s.mu.Unlock() return nil } func (s *openAIRebuiltRequestStore) take(ref string) (*openAIRebuiltLease, error) { s.mu.Lock() if s.closed { s.mu.Unlock() return nil, streamgate.ErrIngressSnapshotClosed } lease := s.leases[ref] if lease == nil { s.mu.Unlock() return nil, fmt.Errorf("rebuilt OpenAI request reference is unavailable") } delete(s.leases, ref) s.mu.Unlock() return lease, nil } func (s *openAIRebuiltRequestStore) close() { s.mu.Lock() if s.closed { s.mu.Unlock() return } s.closed = true leases := s.leases s.leases = make(map[string]*openAIRebuiltLease) s.mu.Unlock() for _, lease := range leases { lease.release() } } // openAIRequestRebuilder implements the Core raw-free RequestRebuilder seam. // Its endpoint and patch sources are fixed for one HTTP request. // Lifecycle is strictly linear: open → in-flight rebuilds → closed. // Close blocks until every in-flight rebuild has released its patch/lease // reservation; nil receivers return ErrIngressSnapshotClosed instead of // panicking. type openAIRequestRebuilder struct { mu sync.Mutex cond *sync.Cond inFlight int closeWaiters int closed bool closeDone bool ingress *openAIIngressSnapshot endpoint string recoverySource *openAIRecoverySourceStore contextWindowTokens int patches *openAIRecoveryPatchStore rebuilt *openAIRebuiltRequestStore } // newOpenAIRequestRebuilder binds the request-local ingress, the optional model // output recovery source, and the target model's context window (0 when // unknown) for one HTTP request. The recovery source powers the S20 resume // builder; a nil source keeps only the exact/schema/patch continuation paths. func newOpenAIRequestRebuilder(ingress *openAIIngressSnapshot, endpoint string, recoverySource *openAIRecoverySourceStore, contextWindowTokens int) (*openAIRequestRebuilder, error) { switch endpoint { case openAIRebuildEndpointChat, openAIRebuildEndpointResponses: default: return nil, fmt.Errorf("unsupported OpenAI rebuild endpoint") } if ingress == nil || ingress.isClosed() { return nil, streamgate.ErrIngressSnapshotClosed } r := &openAIRequestRebuilder{ ingress: ingress, endpoint: endpoint, recoverySource: recoverySource, contextWindowTokens: contextWindowTokens, patches: newOpenAIRecoveryPatchStore(ingress), rebuilt: newOpenAIRebuiltRequestStore(), } r.cond = sync.NewCond(&r.mu) return r, nil } func (r *openAIRequestRebuilder) PatchStore() *openAIRecoveryPatchStore { return r.patches } func (r *openAIRequestRebuilder) RebuiltStore() *openAIRebuiltRequestStore { return r.rebuilt } func (r *openAIRequestRebuilder) Close() { if r == nil { return } r.mu.Lock() if r.closeDone { r.mu.Unlock() return } r.closeWaiters++ r.cond.Broadcast() defer r.leaveCloseWaiter() r.closed = true for r.inFlight > 0 && !r.closeDone { r.cond.Wait() } if r.closeDone { r.mu.Unlock() return } r.rebuilt.close() r.patches.close() r.recoverySource.close() r.closeDone = true r.cond.Broadcast() r.mu.Unlock() } func (r *openAIRequestRebuilder) RebuildRequest(ctx context.Context, snapshotRef streamgate.RecoveryRequestSnapshotRef, plan streamgate.RecoveryPlan) (streamgate.RebuiltRequestDraft, error) { if r == nil { return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed } if err := ctx.Err(); err != nil { return streamgate.RebuiltRequestDraft{}, err } r.mu.Lock() if r.closed { r.mu.Unlock() return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed } r.inFlight++ r.mu.Unlock() defer func() { r.mu.Lock() r.inFlight-- if r.inFlight == 0 { r.cond.Broadcast() } else { r.cond.Signal() } r.mu.Unlock() }() if r.ingress == nil || r.ingress.isClosed() { return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed } if err := snapshotRef.Validate(); err != nil { return streamgate.RebuiltRequestDraft{}, err } if err := plan.Validate(); err != nil { return streamgate.RebuiltRequestDraft{}, err } if !plan.ReadyForRebuild() { return streamgate.RebuiltRequestDraft{}, streamgate.ErrRecoveryPlanNotReady } currentRef, err := r.ingress.recoveryRef() if err != nil { return streamgate.RebuiltRequestDraft{}, err } if snapshotRef.SnapshotRef() != currentRef.SnapshotRef() { return streamgate.RebuiltRequestDraft{}, fmt.Errorf("OpenAI ingress snapshot reference mismatch") } directive := plan.Directive() // S20 resume builder: a continuation directive that addresses the // request-local recovery source is assembled from recorded model output and // the fixed English directive, without any caller history or extra model // call. Continuation directives that address a pre-computed patch keep the // existing patch-store path below. if directive.Kind() == streamgate.RecoveryDirectiveKindContinuation && r.recoverySource != nil && directive.SnapshotRef() == r.recoverySource.snapshotRef() { return r.rebuildResumeContinuation(ctx, plan, directive) } var patchEntry *openAIRecoveryPatchEntry switch directive.Kind() { case streamgate.RecoveryDirectiveKindExact: if directive.RequestRef() != snapshotRef.SnapshotRef() { return streamgate.RebuiltRequestDraft{}, fmt.Errorf("OpenAI exact request reference mismatch") } case streamgate.RecoveryDirectiveKindContinuation: patchEntry, err = r.patches.takeContinuation(directive.SnapshotRef(), directive.Cursor()) case streamgate.RecoveryDirectiveKindSchema: patchEntry, err = r.patches.takeSchema(directive.SchemaRef(), directive.PatchCode()) default: err = fmt.Errorf("unsupported OpenAI recovery directive") } if err != nil { return streamgate.RebuiltRequestDraft{}, err } if patchEntry != nil { defer patchEntry.release() } if err := ctx.Err(); err != nil { return streamgate.RebuiltRequestDraft{}, err } requestRef := fmt.Sprintf("openai.rebuilt.%d", openAIRebuiltSequence.Add(1)) lease := &openAIRebuiltLease{ingress: r.ingress} retained, peak, maxBytes := currentRef.RetainedBytes(), currentRef.PeakBytes(), currentRef.MaxBytes() rebuiltPromptTokens := 0 if patchEntry == nil { body, err := r.ingress.canonicalBody() if err != nil { return streamgate.RebuiltRequestDraft{}, err } lease.bodyAlias = body rebuiltPromptTokens = estimateInputTokensBytes(body, nil, nil, nil) } else { field := "messages" if r.endpoint == openAIRebuildEndpointResponses { field = "input" } body, err := r.ingress.canonicalBody() if err != nil { return streamgate.RebuiltRequestDraft{}, err } patchPlan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{{name: field, value: patchEntry.value}}) if err != nil { return streamgate.RebuiltRequestDraft{}, err } guard, err := r.ingress.reserveRebuild(int64(patchPlan.outputSize)) if err != nil { return streamgate.RebuiltRequestDraft{}, err } output := patchPlan.apply() rebuilt, err := guard.CommitOwnedTyped(openAIRebuiltBodyViewName, output) if err != nil { guard.Close() return streamgate.RebuiltRequestDraft{}, err } lease.guard = guard lease.rebuilt = rebuilt lease.bodyAlias = output accessor := rebuilt.Accessor() retained = uint64(accessor.RetainedBytes()) maxBytes = uint64(accessor.MaxBytes()) peak = retained if current, refErr := r.ingress.recoveryRef(); refErr == nil && current.PeakBytes() > peak { peak = current.PeakBytes() } rebuiltPromptTokens = estimateInputTokensBytes(output, nil, nil, nil) } draft, err := streamgate.NewRebuiltRequestDraftWithIdempotency( plan.PlanID(), plan.IdempotencyKey(), requestRef, r.endpoint, openAIRebuildFamily, retained, peak, maxBytes, rebuiltPromptTokens, plan.RequiredCapabilities(), ) if err != nil { lease.release() return streamgate.RebuiltRequestDraft{}, err } if err := r.rebuilt.put(requestRef, lease); err != nil { lease.release() return streamgate.RebuiltRequestDraft{}, err } return draft, nil } // rebuildResumeContinuation assembles an endpoint-specific continuation body // from the request-local recovery source snapshot and the fixed English resume // directive. It consumes the snapshot once (so it only runs after the aborted // attempt recorded output), refuses to build when the target context window is // unknown or the resume prompt plus reserve would exceed it, and only then // reserves, commits, and leases the rebuilt body. A context-window refusal // returns before any lease is created so no dispatch or recovery budget is // consumed. func (r *openAIRequestRebuilder) rebuildResumeContinuation(ctx context.Context, plan streamgate.RecoveryPlan, directive streamgate.RecoveryDirective) (streamgate.RebuiltRequestDraft, error) { content, reasoning, err := r.recoverySource.consume(directive.Cursor()) if err != nil { return streamgate.RebuiltRequestDraft{}, err } if err := ctx.Err(); err != nil { return streamgate.RebuiltRequestDraft{}, err } output, err := r.buildResumeBody(content, reasoning, plan) if err != nil { return streamgate.RebuiltRequestDraft{}, err } rebuiltPromptTokens := estimateInputTokensBytes(output, nil, nil, nil) if r.contextWindowTokens <= 0 || rebuiltPromptTokens+openAIRebuildContextReserveTokens > r.contextWindowTokens { return streamgate.RebuiltRequestDraft{}, errOpenAIRebuildContextOverflow } requestRef := fmt.Sprintf("openai.rebuilt.%d", openAIRebuiltSequence.Add(1)) lease := &openAIRebuiltLease{ingress: r.ingress} guard, err := r.ingress.reserveRebuild(int64(len(output))) if err != nil { return streamgate.RebuiltRequestDraft{}, err } rebuilt, err := guard.CommitOwnedTyped(openAIRebuiltBodyViewName, output) if err != nil { guard.Close() return streamgate.RebuiltRequestDraft{}, err } lease.guard = guard lease.rebuilt = rebuilt lease.bodyAlias = output accessor := rebuilt.Accessor() retained := uint64(accessor.RetainedBytes()) maxBytes := uint64(accessor.MaxBytes()) peak := retained if current, refErr := r.ingress.recoveryRef(); refErr == nil && current.PeakBytes() > peak { peak = current.PeakBytes() } draft, err := streamgate.NewRebuiltRequestDraftWithIdempotency( plan.PlanID(), plan.IdempotencyKey(), requestRef, r.endpoint, openAIRebuildFamily, retained, peak, maxBytes, rebuiltPromptTokens, plan.RequiredCapabilities(), ) if err != nil { lease.release() return streamgate.RebuiltRequestDraft{}, err } if err := r.rebuilt.put(requestRef, lease); err != nil { lease.release() return streamgate.RebuiltRequestDraft{}, err } return draft, nil } // buildResumeBody serializes a fresh endpoint-native continuation request that // carries only the recorded assistant content/reasoning provenance and the // fixed resume directive. It drops the caller's original messages/input/ // instructions and never summarizes, truncates (beyond the content cursor // already applied), or rewrites the recorded output. func (r *openAIRequestRebuilder) buildResumeBody(content, reasoning string, plan streamgate.RecoveryPlan) ([]byte, error) { model, stream, ingressTemperature, err := r.ingressResumeHead() if err != nil { return nil, err } temperature := continuationTemperature(plan.StrategyAttempt(), ingressTemperature) if r.endpoint == openAIRebuildEndpointResponses { return buildOpenAIResponsesResumeBody(model, content, reasoning, temperature) } return buildOpenAIChatResumeBody(model, stream, content, reasoning, temperature) } // ingressResumeHead reads only the target model, stream flag, and caller // temperature from the canonical body. No caller message or input is copied. func (r *openAIRequestRebuilder) ingressResumeHead() (string, bool, *float64, error) { body, err := r.ingress.canonicalBody() if err != nil { return "", false, nil, err } var head struct { Model string `json:"model"` Stream bool `json:"stream"` Temperature *float64 `json:"temperature"` } if err := json.Unmarshal(body, &head); err != nil { return "", false, nil, fmt.Errorf("decode OpenAI resume ingress head: %w", err) } return head.Model, head.Stream, head.Temperature, nil } func continuationTemperature(strategyAttempt int, ingress *float64) *float64 { if ingress != nil { value := *ingress return &value } candidates := [...]float64{0.2, 0.4, 0.6} index := strategyAttempt - 1 if index < 0 { index = 0 } if index >= len(candidates) { index = len(candidates) - 1 } value := candidates[index] return &value } func buildOpenAIChatResumeBody(model string, stream bool, content, reasoning string, temperature *float64) ([]byte, error) { assistant := map[string]any{"role": "assistant", "content": content} if reasoning != "" { assistant["reasoning_content"] = reasoning } body := map[string]any{ "model": model, "stream": stream, "messages": []any{ assistant, map[string]any{"role": "user", "content": openAIRepeatResumeDirective}, }, } if temperature != nil { body["temperature"] = *temperature } return json.Marshal(body) } func buildOpenAIResponsesResumeBody(model, content, reasoning string, temperature *float64) ([]byte, error) { input := make([]any, 0, 2) if reasoning != "" { input = append(input, map[string]any{ "type": "reasoning", "content": []any{map[string]any{"type": "reasoning_text", "text": reasoning}}, }) } input = append(input, map[string]any{ "type": "message", "role": "assistant", "content": []any{map[string]any{"type": "output_text", "text": content}}, }) body := map[string]any{ "model": model, "stream": false, "instructions": openAIRepeatResumeDirective, "input": input, } if temperature != nil { body["temperature"] = *temperature } return json.Marshal(body) } func (r *openAIRequestRebuilder) leaveCloseWaiter() { r.mu.Lock() r.closeWaiters-- r.cond.Broadcast() r.mu.Unlock() } var _ streamgate.RequestRebuilder = (*openAIRequestRebuilder)(nil) func isOpenAIRebuildOverflow(err error) bool { return errors.Is(err, streamgate.ErrIngressSnapshotRebuildOverflow) || errors.Is(err, streamgate.ErrRecoverySnapshotLimitExceeded) }