Files
tbphp dad1f05016 feat(codex): support standalone search (#674)
* feat(codex): 支持订阅渠道独立搜索

* fix(codex): 保留搜索读体失败的响应状态

* feat(logs): 添加独立搜索请求图标
2026-09-17 16:58:58 +08:00

1010 lines
33 KiB
Go

package gateway
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"gpt-load/internal/dialect"
"gpt-load/internal/execution"
platformheader "gpt-load/internal/platform/httpheader"
"gpt-load/internal/platform/redact"
"gpt-load/internal/protocol"
"gpt-load/internal/usage"
)
// ExecutionForwarder adapts the provider-neutral executor to the gateway's
// downstream commit boundary while the handler orchestration remains intact.
type ExecutionForwarder struct {
executor execution.Executor
representation *responseProcessor
usageCapture *usageCaptureBoundary
writeTimeout time.Duration
}
func NewExecutionForwarder(executor execution.Executor) *ExecutionForwarder {
return &ExecutionForwarder{
executor: executor, representation: &responseProcessor{redactor: redact.New()},
usageCapture: newUsageCaptureBoundary(),
writeTimeout: downstreamWriteTimeout,
}
}
func (forwarder *ExecutionForwarder) OpenWebsocket(ctx context.Context, input ForwardInput) (execution.WebsocketSession, execution.WebsocketResult) {
spec, err := newExecutionAttemptSpec(input)
if forwarder != nil && err == nil {
if opener, ok := forwarder.executor.(execution.WebsocketOpener); ok {
return opener.OpenWebsocket(ctx, spec)
}
}
return nil, execution.WebsocketResult{DispatchState: execution.DispatchNotSent, Error: &execution.ErrorEvidence{
Kind: execution.ErrorKindInvalidRequest, OriginHint: execution.ErrorOriginInternal,
ScopeHint: execution.ErrorScopeRequest, Code: "websocket_not_supported", Summary: "Native WebSocket is not supported.",
}}
}
func (forwarder *ExecutionForwarder) Forward(
ctx context.Context,
input ForwardInput,
) UpstreamResult {
spec, err := newExecutionAttemptSpec(input)
if err != nil || forwarder == nil || forwarder.executor == nil {
return executionInputFailure(err)
}
executionResult := forwarder.executor.Execute(ctx, spec)
if err := executionResult.Validate(); err != nil {
return invalidExecutionAttemptResult(executionResult)
}
result := upstreamFromExecutionResult(ctx, input, executionResult)
result = forwarder.prepareBufferedResult(input, result)
if (input.ClientProtocol == protocol.OpenAIImages ||
input.ClientProtocol == protocol.OpenAIEmbeddings || input.ClientProtocol == protocol.Rerank) && input.ObserveUsage &&
result.HasResponse() && result.StatusCode >= http.StatusOK &&
result.StatusCode < http.StatusMultipleChoices &&
executionResult.Usage == nil && result.Usage.State == usage.StateMissing && forwarder.usageCapture != nil {
result.Usage = forwarder.usageCapture.extractNonStreamingPlain(
input.Dialect,
result.ClassificationBody,
)
}
if input.ClientProtocol == protocol.OpenAIEmbeddings && result.Err == nil &&
result.StatusCode >= http.StatusOK && result.StatusCode < http.StatusMultipleChoices {
result.ClassificationBody = nil
}
return result
}
func invalidExecutionAttemptResult(result execution.AttemptResult) UpstreamResult {
dispatchState := execution.DispatchNotSent
if result.DispatchState == execution.DispatchLocal {
dispatchState = execution.DispatchLocal
} else if result.DispatchState == execution.DispatchMaybeSent ||
result.ResponseStarted || result.StatusCode != 0 || len(result.Header) > 0 || len(result.Body) > 0 {
dispatchState = execution.DispatchMaybeSent
}
evidence := execution.ErrorEvidence{
Kind: execution.ErrorKindInternal,
OriginHint: execution.ErrorOriginInternal,
ScopeHint: execution.ErrorScopeRequest,
Code: "attempt_result_contract_invalid",
Summary: "Attempt executor returned an invalid result.",
ReplaySafety: execution.ReplaySafetyUnknown,
}
return UpstreamResult{
Err: fmt.Errorf("%w: invalid attempt executor result", ErrUpstreamProtocol),
RequestWritten: dispatchState == execution.DispatchMaybeSent,
DispatchState: dispatchState,
ExecutionError: &evidence,
ErrorSummary: evidence.Summary,
Usage: usage.Result{State: usage.StateNotApplicable},
}
}
func (forwarder *ExecutionForwarder) ForwardStream(
ctx context.Context,
input ForwardInput,
downstream http.ResponseWriter,
) UpstreamResult {
spec, err := newExecutionAttemptSpec(input)
if err != nil || forwarder == nil || forwarder.executor == nil || downstream == nil {
return executionInputFailure(err)
}
writeTimeout := forwarder.writeTimeout
if writeTimeout <= 0 {
writeTimeout = downstreamWriteTimeout
}
controller := newStreamWriteController(downstream, writeTimeout)
defer func() { _ = controller.clear() }()
usageCapture := forwarder.usageCapture
if usageCapture == nil {
usageCapture = newUsageCaptureBoundary()
}
streamEvents := newStreamEventObserver(
input.Dialect,
usageCapture.newStreamForRequest(input.Dialect, input.ObserveUsage),
)
redactor := redact.New()
if forwarder.representation != nil && forwarder.representation.redactor != nil {
redactor = forwarder.representation.redactor
}
summarySecrets := resolvedErrorSummarySecretValues(
input.APIKey,
input.Group.HeaderRules,
input.CredentialSecrets...,
)
credentialSecrets := append([]string(nil), input.CredentialSecrets...)
if input.APIKey != "" {
credentialSecrets = append(credentialSecrets, input.APIKey)
}
streamBuffer := newSSEEventObservationBuffer(execution.SSEEventLimit(input.ClientProtocol), func(
event dialect.StreamEvent,
genericProviderError bool,
) (bool, error) {
if input.ClientProtocol == protocol.OpenAIImages &&
imagesCredentialLiteralsRemain(event.Payload, credentialSecrets) {
return false, fmt.Errorf("%w: credential remains in response event", ErrUpstreamProtocol)
}
wasTerminal := streamEvents.sawTerminal
providerError, err := streamEvents.classify(event, genericProviderError)
if err != nil {
return false, err
}
if !wasTerminal && !providerError && input.OnResponse != nil {
object, err := decodeResponsesStoreObject(event.Payload)
if err != nil {
return false, err
}
if response, exists := object["response"]; exists {
if err := input.OnResponse(response); err != nil {
return false, err
}
}
}
if !wasTerminal {
streamEvents.observeUsageEvent(event)
if providerError {
streamEvents.observeError(
event.Payload,
summarizeErrorBody(
redactor,
event.Payload,
fixedErrorSummary("upstream_sse_error"),
summarySecrets...,
),
)
}
}
return !wasTerminal && streamEvents.sawTerminal, nil
})
var responsesStoreBuffer *statelessResponsesSSEBuffer
if input.ResponsesStoreDowngraded {
responsesStoreBuffer = newStatelessResponsesSSEBuffer()
}
var (
ready *execution.StreamEvent
committed bool
firstResponse bool
downstreamErr error
errorBody []byte
streamUsage *execution.UsageEvidence
)
sink := func(event execution.StreamEvent) error {
if err := event.Validate(); err != nil {
downstreamErr = fmt.Errorf("%w: invalid execution stream event", ErrUpstreamProtocol)
return downstreamErr
}
switch event.Kind {
case execution.StreamEventReady:
copy := event.Clone()
if copy.StatusCode >= http.StatusOK && copy.StatusCode < http.StatusMultipleChoices {
if err := validateStreamContentEncoding(copy.Header); err != nil {
downstreamErr = err
return err
}
copy.Header = normalizeStreamResponseHeaders(
sanitizeForwardResponseHeaders(copy.Header, input),
)
} else {
copy.Header = sanitizeForwardResponseHeaders(copy.Header, input)
}
ready = &copy
return nil
case execution.StreamEventUsage:
if event.Usage != nil {
copy := event.Usage.Clone()
streamUsage = &copy
}
return nil
case execution.StreamEventData:
if ready == nil {
downstreamErr = fmt.Errorf("%w: execution data arrived before response metadata", ErrUpstreamProtocol)
return downstreamErr
}
if ready.StatusCode < http.StatusOK || ready.StatusCode >= http.StatusMultipleChoices {
errorBody = appendExecutionErrorBody(errorBody, event.Data)
return nil
}
observedData := event.Data
if responsesStoreBuffer != nil {
observedData, err = responsesStoreBuffer.push(event.Data)
if err != nil {
downstreamErr = executionStreamProtocolFailure(err)
return downstreamErr
}
if len(observedData) == 0 {
return nil
}
}
completeData, terminalInChunk, err := streamBuffer.push(observedData)
if err != nil {
downstreamErr = executionStreamProtocolFailure(err)
return downstreamErr
}
forwardData := observedData
if input.ClientProtocol == protocol.OpenAIImages || responsesStoreBuffer != nil || input.OnResponse != nil {
forwardData = completeData
if len(forwardData) == 0 {
return nil
}
}
if !committed {
if !firstResponse {
firstResponse = true
if input.OnFirstResponse != nil {
input.OnFirstResponse()
}
}
if streamEvents.firstEventWasProviderError() {
errorBody = appendExecutionErrorBody(errorBody, forwardData)
return nil
}
committed = true
if err := commitStream(controller, ready.StatusCode, ready.Header, forwardData); err != nil {
downstreamErr = err
return err
}
if input.OnStreamReady != nil {
input.OnStreamReady()
}
if terminalInChunk {
streamEvents.markTerminalForwarded()
}
return nil
}
written, err := controller.write(forwardData)
if err != nil {
downstreamErr = &streamFailure{
kind: streamFailureDownstreamWrite,
err: fmt.Errorf("write execution stream: %w", err),
}
return downstreamErr
}
if written != len(forwardData) {
downstreamErr = &streamFailure{
kind: streamFailureDownstreamWrite,
err: fmt.Errorf("write execution stream: %w", io.ErrShortWrite),
}
return downstreamErr
}
if err := controller.flush(); err != nil {
downstreamErr = &streamFailure{
kind: streamFailureDownstreamWrite,
err: fmt.Errorf("flush execution stream: %w", err),
}
return downstreamErr
}
if terminalInChunk {
streamEvents.markTerminalForwarded()
}
return nil
default:
downstreamErr = fmt.Errorf("%w: unsupported execution stream event", ErrUpstreamProtocol)
return downstreamErr
}
}
terminal := forwarder.executor.ExecuteStream(ctx, spec, sink)
if err := terminal.Validate(); err != nil {
terminal = invalidExecutionStreamResult(terminal, ready, committed)
}
if downstreamErr == nil && terminal.Error == nil {
if responsesStoreBuffer != nil {
if err := responsesStoreBuffer.finish(); err != nil {
downstreamErr = executionStreamProtocolFailure(err)
}
}
if downstreamErr == nil {
if err := streamBuffer.finish(); err != nil {
downstreamErr = executionStreamProtocolFailure(err)
} else if err := streamEvents.validateEOF(); err != nil {
downstreamErr = err
}
}
}
capturedUsage := streamEvents.finalizeUsage()
result := upstreamFromExecutionStreamResult(ctx, input, terminal, streamUsage)
if input.ObserveUsage && input.ClientProtocol == protocol.Anthropic &&
input.RouteMode == execution.RouteNative && capturedUsage.State != usage.StateMissing {
// 原生 Anthropic 流以实际事件为用量依据。SDK 的最大值合并会丢失
// 输入修正、明确的零、缓存 TTL 明细,以及流是否完整结束的信息。
result.Usage = capturedUsage
} else {
// 未采集到用量时,保留执行层已经取得的证据。
result.Usage = preferCapturedStreamUsage(result.Usage, capturedUsage)
}
result.Committed = committed
if len(errorBody) > 0 {
result.Body = append([]byte(nil), errorBody...)
result.ClassificationBody = append([]byte(nil), errorBody...)
}
if downstreamErr != nil {
result.Err = downstreamErr
}
if committed {
result.Stream = executionStreamObservation(ctx, terminal, downstreamErr, streamEvents)
} else if ready != nil {
result.StatusCode = ready.StatusCode
result.Header = ready.Header.Clone()
result.ResponseStarted = true
result.UpstreamRequestID = ready.UpstreamRequestID
}
if !committed && streamEvents.firstEventWasProviderError() {
summary := streamEvents.firstSummary
if summary == "" {
summary = fixedErrorSummary("upstream_sse_error")
}
result.ProviderErrorBeforeCommit = true
result.ErrorSummary = summary
result.ExecutionError = firstStreamErrorEvidence(
streamEvents.firstErrorPayload,
result.ExecutionError,
result.StatusCode,
result.UpstreamRequestID,
summary,
redactor,
summarySecrets,
)
}
if !committed && result.HasResponse() && !result.ProviderErrorBeforeCommit {
result = forwarder.prepareBufferedResult(input, result)
}
return result
}
func invalidExecutionStreamResult(
result execution.StreamResult,
ready *execution.StreamEvent,
committed bool,
) execution.StreamResult {
dispatchState := execution.DispatchNotSent
if result.DispatchState == execution.DispatchLocal && !committed && ready == nil {
dispatchState = execution.DispatchLocal
} else if committed || ready != nil || result.DispatchState == execution.DispatchMaybeSent ||
result.ResponseStarted || result.StatusCode != 0 || len(result.Header) > 0 {
dispatchState = execution.DispatchMaybeSent
}
normalized := execution.StreamResult{DispatchState: dispatchState}
if ready != nil {
normalized.ResponseStarted = true
normalized.StatusCode = ready.StatusCode
normalized.Header = ready.Header.Clone()
normalized.UpstreamRequestID = ready.UpstreamRequestID
}
evidence := execution.ErrorEvidence{
Kind: execution.ErrorKindInternal,
OriginHint: execution.ErrorOriginInternal,
ScopeHint: execution.ErrorScopeRequest,
Code: "attempt_result_contract_invalid",
Summary: "Attempt executor returned an invalid result.",
ReplaySafety: execution.ReplaySafetyUnknown,
}
if normalized.ResponseStarted {
evidence.StatusCode = normalized.StatusCode
}
normalized.Error = &evidence
return normalized
}
func firstStreamErrorEvidence(
payload []byte,
existing *execution.ErrorEvidence,
statusCode int,
requestID string,
summary string,
redactor *redact.Redactor,
knownSecrets []string,
) *execution.ErrorEvidence {
evidence := execution.ErrorEvidence{
Kind: execution.ErrorKindProvider,
StatusCode: statusCode,
Summary: summary,
RequestID: requestID,
}
if existing != nil {
evidence = existing.Clone()
evidence.Kind = execution.ErrorKindProvider
if evidence.StatusCode == 0 {
evidence.StatusCode = statusCode
}
if evidence.Summary == "" {
evidence.Summary = summary
}
if evidence.RequestID == "" {
evidence.RequestID = requestID
}
}
type errorObject struct {
Type string `json:"type"`
Code json.RawMessage `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
}
var envelope struct {
Type string `json:"type"`
Code json.RawMessage `json:"code"`
Error errorObject `json:"error"`
Response struct {
Error errorObject `json:"error"`
} `json:"response"`
}
if json.Unmarshal(payload, &envelope) != nil {
return &evidence
}
providerError := envelope.Error
if providerError.Type == "" && len(providerError.Code) == 0 &&
providerError.Status == "" && providerError.Message == "" {
providerError = envelope.Response.Error
}
typeValue := providerError.Type
if typeValue == "" && !strings.EqualFold(envelope.Type, "error") {
typeValue = envelope.Type
}
codeValue := streamErrorEvidenceScalar(providerError.Code)
if codeValue == "" {
codeValue = streamErrorEvidenceScalar(envelope.Code)
}
if codeValue == "" {
codeValue = providerError.Status
}
typeValue = sanitizeStreamErrorEvidenceValue(redactor, typeValue, knownSecrets)
codeValue = sanitizeStreamErrorEvidenceValue(redactor, codeValue, knownSecrets)
statusValue := sanitizeStreamErrorEvidenceValue(redactor, providerError.Status, knownSecrets)
if evidence.Type == "" {
evidence.Type = typeValue
}
if evidence.Code == "" {
evidence.Code = codeValue
}
if evidence.Hint == "" {
evidence.Hint = streamErrorFailureHint(
statusCode,
typeValue,
codeValue,
statusValue,
evidence.Summary,
)
}
evidence.OriginHint = execution.ErrorOriginUpstream
switch evidence.Hint {
case execution.FailureHintInvalidCredential,
execution.FailureHintRefreshRequired,
execution.FailureHintReauthorizationRequired:
evidence.ScopeHint = execution.ErrorScopeCredential
case execution.FailureHintModelUnavailable,
execution.FailureHintCandidateUnavailable:
evidence.ScopeHint = execution.ErrorScopeModel
case execution.FailureHintRequestRejected:
evidence.ScopeHint = execution.ErrorScopeRequest
case execution.FailureHintHostError:
evidence.ScopeHint = execution.ErrorScopeGroup
}
if evidence.ReplaySafety == "" && streamBootstrapCapacityRejection(evidence.Code) {
evidence.ReplaySafety = execution.ReplaySafetyRejectedBeforeProcessing
}
return &evidence
}
func streamBootstrapCapacityRejection(codeValue string) bool {
codeValue = strings.ToLower(strings.TrimSpace(codeValue))
return codeValue == "server_is_overloaded" || codeValue == "rate_limit_exceeded"
}
func streamErrorEvidenceScalar(value json.RawMessage) string {
value = json.RawMessage(strings.TrimSpace(string(value)))
if len(value) == 0 || string(value) == "null" {
return ""
}
var text string
if json.Unmarshal(value, &text) == nil {
return text
}
var number json.Number
if json.Unmarshal(value, &number) == nil {
return number.String()
}
return ""
}
func sanitizeStreamErrorEvidenceValue(
redactor *redact.Redactor,
value string,
knownSecrets []string,
) string {
value = sanitizeErrorSummary(redactor, value, knownSecrets...)
const maxEvidenceRunes = 128
runes := []rune(value)
if len(runes) > maxEvidenceRunes {
return string(runes[:maxEvidenceRunes])
}
return value
}
func streamErrorFailureHint(statusCode int, values ...string) execution.FailureHint {
markers := strings.ToLower(strings.Join(values, " "))
switch {
case statusCode == http.StatusUnauthorized:
return execution.FailureHintInvalidCredential
case containsStreamErrorMarker(markers,
"model_not_found", "model not found", "model_not_available",
"model unavailable", "deployment_not_found", "unsupported_model"):
return execution.FailureHintModelUnavailable
case statusCode == http.StatusTooManyRequests || containsStreamErrorMarker(markers,
"rate_limit", "rate limit", "too_many_requests", "quota_exceeded",
"resource_exhausted", "throttl"):
return execution.FailureHintRateLimited
case containsStreamErrorMarker(markers,
"invalid_api_key", "api_key_invalid", "authentication_error",
"authentication failed",
"invalid credential", "api key not valid"):
return execution.FailureHintInvalidCredential
case statusCode >= http.StatusInternalServerError && statusCode <= 599:
return execution.FailureHintHostError
default:
return ""
}
}
func containsStreamErrorMarker(value string, markers ...string) bool {
for _, marker := range markers {
if strings.Contains(value, marker) {
return true
}
}
return false
}
func (forwarder *ExecutionForwarder) prepareBufferedResult(
input ForwardInput,
result UpstreamResult,
) UpstreamResult {
if forwarder == nil || !result.HasResponse() {
return result
}
if input.Operation == execution.OperationWebSearch && result.ProviderErrorBeforeCommit {
return result
}
representation := forwarder.representation
if representation == nil {
representation = &responseProcessor{redactor: redact.New()}
}
secrets := append([]string(nil), input.CredentialSecrets...)
if input.APIKey != "" {
secrets = append(secrets, input.APIKey)
}
if result.StatusCode >= http.StatusOK && result.StatusCode < http.StatusMultipleChoices {
policy := classifyResponseBody(input.Request.Method, result.StatusCode)
if !policy.readBody {
if err := validateStreamContentEncoding(result.Header); err != nil ||
(result.StatusCode == http.StatusPartialContent && !hasSafeHeadContentRange(result.Header)) {
return executionRepresentationFailure(result, fmt.Errorf("%w: invalid bodyless response representation", ErrUpstreamProtocol))
}
headers := sanitizeForwardResponseHeaders(result.Header, input, secrets...)
headers, _ = normalizeBufferedResponse(input.Request.Method, result.StatusCode, headers, nil)
result.Header = headers
result.Body = nil
return result
}
prepared, err := representation.prepareSuccessRepresentation(
input, result.StatusCode, result.Header, result.Body, secrets,
)
if err != nil {
return executionRepresentationFailure(result, err)
}
result.Header = prepared.headers
result.Body = prepared.downstream
result.ClassificationBody = prepared.inspectable
return result
}
if result.ExecutionError != nil &&
(input.RouteMode == execution.RouteConverted || len(result.Body) == 0) {
result.Body = encodeClientErrorBody(input.ClientProtocol, result.StatusCode, *result.ExecutionError)
result.Header = result.Header.Clone()
if result.Header == nil {
result.Header = make(http.Header)
}
deleteHeaderField(result.Header, "Content-Encoding")
deleteHeaderField(result.Header, "Content-Length")
invalidateRewrittenBodyHeaders(result.Header)
result.Header.Set("Content-Type", "application/json")
}
prepared := representation.prepareErrorRepresentation(input, result.Header, result.Body, secrets)
result.Header = prepared.headers
result.Body = prepared.downstream
result.ClassificationBody = prepared.classification
return result
}
func encodeClientErrorBody(
clientProtocol protocol.Protocol,
status int,
evidence execution.ErrorEvidence,
) []byte {
message := strings.TrimSpace(evidence.Summary)
if message == "" {
message = "upstream request failed"
}
typeValue := strings.TrimSpace(evidence.Type)
if typeValue == "" {
switch evidence.Hint {
case execution.FailureHintInvalidCredential:
typeValue = "authentication_error"
case execution.FailureHintRateLimited:
typeValue = "rate_limit_error"
case execution.FailureHintModelUnavailable:
typeValue = "model_not_found"
default:
typeValue = "api_error"
}
}
codeValue := strings.TrimSpace(evidence.Code)
var value any
switch clientProtocol {
case protocol.Anthropic:
payload := struct {
Type string `json:"type"`
Error struct {
Type string `json:"type"`
Message string `json:"message"`
} `json:"error"`
}{Type: "error"}
payload.Error.Type = typeValue
payload.Error.Message = message
value = payload
case protocol.Gemini:
if status < 100 || status > 599 {
status = http.StatusBadGateway
}
if codeValue == "" {
codeValue = strings.ToUpper(typeValue)
}
payload := struct {
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Status string `json:"status"`
} `json:"error"`
}{}
payload.Error.Code = status
payload.Error.Message = message
payload.Error.Status = codeValue
value = payload
default:
payload := struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code,omitempty"`
} `json:"error"`
}{}
payload.Error.Message = message
payload.Error.Type = typeValue
payload.Error.Code = codeValue
value = payload
}
body, err := json.Marshal(value)
if err != nil {
return []byte(`{"error":{"message":"upstream request failed","type":"api_error"}}`)
}
return body
}
func executionRepresentationFailure(result UpstreamResult, err error) UpstreamResult {
evidence := execution.ErrorEvidence{
Kind: execution.ErrorKindInternal, OriginHint: execution.ErrorOriginInternal,
ScopeHint: execution.ErrorScopeRequest, Code: "response_representation_invalid",
Summary: "Upstream response representation could not be processed.",
ReplaySafety: execution.ReplaySafetyUnknown,
}
if result.ResponseStarted {
evidence.StatusCode = result.StatusCode
}
return UpstreamResult{
Err: err,
StatusCode: result.StatusCode,
RequestWritten: result.RequestWritten,
DispatchState: result.DispatchState,
ResponseStarted: result.ResponseStarted,
UpstreamProtocol: result.UpstreamProtocol,
AppliedReasoning: result.AppliedReasoning.Clone(),
UpstreamRequestID: result.UpstreamRequestID,
ExecutionError: &evidence,
ErrorSummary: evidence.Summary,
}
}
func newExecutionAttemptSpec(input ForwardInput) (execution.AttemptSpec, error) {
if input.Request == nil {
return execution.AttemptSpec{}, fmt.Errorf("request is required")
}
headers := cloneEndToEndHeaders(input.Request.Header)
removeDownstreamCredentials(headers)
for name, value := range input.Group.HeaderRules.Set {
headers.Set(name, strings.ReplaceAll(value, "${API_KEY}", input.APIKey))
}
for _, name := range input.Group.HeaderRules.Remove {
headers.Del(name)
}
sanitizeUpstreamRequestHeaders(headers)
headers.Set("Accept-Encoding", "identity")
spec := execution.NewAttemptSpec(execution.AttemptSpec{
RequestID: input.RequestID,
AttemptID: input.AttemptID,
Sequence: input.AttemptSequence,
ChannelID: input.ChannelID,
RouteMode: input.RouteMode,
ClientProtocol: input.ClientProtocol,
Operation: input.Operation,
RouteRequirement: input.RouteRequirement,
ResponsesStorePreference: input.ResponsesStorePreference,
ResponsesStoreDowngraded: input.ResponsesStoreDowngraded,
ClientModel: input.ExternalModel,
UpstreamModel: input.UpstreamModelID,
Method: input.Request.Method,
Path: input.Request.Path,
RawQuery: input.Request.RawQuery,
Header: headers,
ConfiguredHeaders: input.Group.HeaderRules.ConfiguredNames(),
Body: input.Request.Body,
IncludeUsage: input.ObserveUsage,
ForceCredentialRefresh: input.ForceCredentialRefresh,
ContinuityKey: input.ContinuityKey,
TargetConfig: input.TargetConfig,
Timeouts: execution.AttemptTimeouts{
FirstByte: input.Group.Timeouts.FirstByte,
Request: input.Group.Timeouts.Request,
StreamIdle: input.Group.Timeouts.StreamIdle,
},
Credential: input.Credential,
Proxy: input.Proxy,
ProxyFingerprint: input.ProxyFingerprint,
})
if spec.ResponsesStoreDowngraded {
body, err := forceStatelessResponsesRequest(spec.Body)
if err != nil {
return execution.AttemptSpec{}, err
}
spec.Body = body
platformheader.StripRepresentationMetadata(spec.Header)
}
if err := spec.Validate(); err != nil {
return execution.AttemptSpec{}, err
}
return spec, nil
}
func upstreamFromExecutionResult(
ctx context.Context,
input ForwardInput,
result execution.AttemptResult,
) UpstreamResult {
upstream := baseExecutionResult(input, result.DispatchState, result.ResponseStarted,
result.StatusCode, result.Header, result.Model, result.UpstreamRequestID,
result.Usage, result.Error)
if result.AppliedReasoning != nil {
upstream.AppliedReasoning = result.AppliedReasoning.Clone()
}
upstream.UpstreamProtocol = result.UpstreamProtocol
if input.ClientProtocol == protocol.OpenAIImages ||
input.ClientProtocol == protocol.OpenAIEmbeddings {
// AttemptResult owns Body after the executor returns. Buffered opaque
// representations consume it synchronously, so move that ownership across
// the internal boundary instead of cloning a large payload twice.
upstream.Body = result.Body
upstream.ClassificationBody = result.Body
} else {
upstream.Body = append([]byte(nil), result.Body...)
upstream.ClassificationBody = append([]byte(nil), result.Body...)
}
if !result.ResponseStarted && result.Error != nil {
upstream.Err = executionFailureError(ctx, result.Error)
}
return upstream
}
func upstreamFromExecutionStreamResult(
ctx context.Context,
input ForwardInput,
result execution.StreamResult,
streamUsage *execution.UsageEvidence,
) UpstreamResult {
usageEvidence := result.Usage
if usageEvidence == nil {
usageEvidence = streamUsage
}
upstream := baseExecutionResult(input, result.DispatchState, result.ResponseStarted,
result.StatusCode, result.Header, result.Model, result.UpstreamRequestID,
usageEvidence, result.Error)
if result.AppliedReasoning != nil {
upstream.AppliedReasoning = result.AppliedReasoning.Clone()
}
upstream.UpstreamProtocol = result.UpstreamProtocol
if !result.ResponseStarted && result.Error != nil {
upstream.Err = executionFailureError(ctx, result.Error)
}
return upstream
}
func baseExecutionResult(
input ForwardInput,
dispatchState execution.DispatchState,
responseStarted bool,
statusCode int,
header http.Header,
model string,
upstreamRequestID string,
usageEvidence *execution.UsageEvidence,
errorEvidence *execution.ErrorEvidence,
) UpstreamResult {
result := UpstreamResult{
StatusCode: statusCode,
Header: cloneEndToEndHeaders(header),
UpstreamReportedModel: model,
ResponseModelObserved: model != "",
RequestWritten: dispatchState == execution.DispatchMaybeSent,
DispatchState: dispatchState,
ResponseStarted: responseStarted,
UpstreamRequestID: upstreamRequestID,
}
if usageEvidence != nil {
result.Usage = usageEvidence.Normalized
} else if input.ObserveUsage {
result.Usage = usage.Result{State: usage.StateMissing}
} else {
result.Usage = usage.Result{State: usage.StateNotApplicable}
}
if errorEvidence != nil {
copy := errorEvidence.Clone()
result.ExecutionError = &copy
result.ErrorSummary = copy.Summary
result.ProviderErrorBeforeCommit = responseStarted &&
statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices
}
return result
}
func executionFailureError(ctx context.Context, evidence *execution.ErrorEvidence) error {
if ctx != nil && ctx.Err() != nil {
return ctx.Err()
}
if evidence == nil {
return fmt.Errorf("upstream execution failed")
}
switch evidence.Kind {
case execution.ErrorKindCanceled:
return context.Canceled
case execution.ErrorKindTimeout:
return upstreamExecutionTimeoutError{}
case execution.ErrorKindInvalidRequest, execution.ErrorKindProvider, execution.ErrorKindInternal:
return fmt.Errorf("%w: upstream execution failed", ErrUpstreamProtocol)
default:
return fmt.Errorf("upstream execution failed")
}
}
type upstreamExecutionTimeoutError struct{}
func (upstreamExecutionTimeoutError) Error() string { return "upstream execution timed out" }
func (upstreamExecutionTimeoutError) Timeout() bool { return true }
func (upstreamExecutionTimeoutError) Temporary() bool { return true }
func executionInputFailure(cause error) UpstreamResult {
if cause == nil {
cause = fmt.Errorf("execution runtime is unavailable")
}
evidence := execution.ErrorEvidence{
Kind: execution.ErrorKindInvalidRequest,
Summary: "invalid execution attempt",
}
return UpstreamResult{
Err: fmt.Errorf("%w: %v", ErrUpstreamProtocol, cause),
DispatchState: execution.DispatchNotSent,
ExecutionError: &evidence,
ErrorSummary: evidence.Summary,
}
}
func appendExecutionErrorBody(current, chunk []byte) []byte {
remaining := maxStreamingErrorBodyBytes - len(current)
if remaining <= 0 {
return current
}
if len(chunk) > remaining {
chunk = chunk[:remaining]
}
return append(current, chunk...)
}
func executionStreamProtocolFailure(cause error) error {
if cause == nil {
cause = fmt.Errorf("invalid upstream SSE stream")
}
return &streamFailure{
kind: streamFailureProtocol,
err: fmt.Errorf("%w: %v", ErrUpstreamProtocol, cause),
}
}
func preferCapturedStreamUsage(
current usage.Result,
captured usage.Result,
) usage.Result {
switch captured.State {
case usage.StateComplete:
if current.State != usage.StateComplete {
return captured
}
case usage.StatePartial:
if current.State == usage.StateMissing || current.State == "" {
return captured
}
case usage.StateMissing:
if current.State == "" {
return captured
}
}
return current
}
func executionStreamObservation(
ctx context.Context,
terminal execution.StreamResult,
downstreamErr error,
streamEvents *streamEventObserver,
) StreamObservation {
if downstreamErr != nil {
return observeStreamTermination(ctx, downstreamErr, streamEvents)
}
if terminal.Error == nil {
return streamEvents.endObservation()
}
switch terminal.Error.Kind {
case execution.ErrorKindCanceled:
return observeStreamTermination(ctx, context.Canceled, streamEvents)
case execution.ErrorKindTimeout:
return streamTerminalObservation(StreamEndIdleTimeout)
case execution.ErrorKindProvider, execution.ErrorKindHTTP:
observation := streamEvents.endObservation()
if observation.EndReason == StreamEndSSEError {
return observation
}
return StreamObservation{
EndReason: StreamEndSSEError,
ErrorSummary: terminal.Error.Summary,
}
case execution.ErrorKindInvalidRequest, execution.ErrorKindInternal:
return streamTerminalObservation(StreamEndUpstreamProtocolError)
default:
return streamTerminalObservation(StreamEndUpstreamTerminated)
}
}
var _ AttemptForwarder = (*ExecutionForwarder)(nil)