633 lines · 24.3 KB · Go
RawDownload
package executor import (	"bytes"	"net/http"	"sort"	"strconv"	"strings"	"sync"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") const codexIncompleteStreamMessage = "stream error: stream disconnected before completion: stream closed before response.completed" type codexIncompleteStreamError struct {	statusErr} func newCodexIncompleteStreamError() codexIncompleteStreamError {	return codexIncompleteStreamError{statusErr: statusErr{		code: http.StatusRequestTimeout,		msg:  codexIncompleteStreamMessage,	}}} func (codexIncompleteStreamError) IsRequestScoped() bool {	return true} type codexEmptyIncompleteStreamError struct {	statusErr} func newCodexEmptyIncompleteStreamError() codexEmptyIncompleteStreamError {	return codexEmptyIncompleteStreamError{statusErr: statusErr{		code: http.StatusBadGateway,		msg:  helps.CodexEmptyIncompleteStreamMessage,	}}} func (codexEmptyIncompleteStreamError) IsRequestScoped() bool {	return true} // Streamed Codex responses may emit response.output_item.done events while leaving// response.completed.response.output empty. Keep the stream path aligned with the// already-patched non-stream path by reconstructing response.output from those items.func collectCodexOutputItemDone(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback *[][]byte) {	itemResult := gjson.GetBytes(eventData, "item")	if !itemResult.Exists() || itemResult.Type != gjson.JSON {		return	}	outputIndexResult := gjson.GetBytes(eventData, "output_index")	if outputIndexResult.Exists() {		outputItemsByIndex[outputIndexResult.Int()] = []byte(itemResult.Raw)		return	}	*outputItemsFallback = append(*outputItemsFallback, []byte(itemResult.Raw))} func hydrateCodexCompletedOutputItemIDs(eventData []byte, outputItems []gjson.Result, outputItemsByIndex map[int64][]byte) []byte {	patchedData := eventData	for outputIndex, outputItem := range outputItems {		itemData := []byte(outputItem.Raw)		itemID := gjson.GetBytes(itemData, "id")		if itemID.Exists() && itemID.Type != gjson.Null && (itemID.Type != gjson.String || strings.TrimSpace(itemID.String()) != "") {			continue		} 		completedItem, ok := outputItemsByIndex[int64(outputIndex)]		if !ok {			continue		}		completedID := gjson.GetBytes(completedItem, "id")		if completedID.Type != gjson.String || strings.TrimSpace(completedID.String()) == "" {			continue		} 		updatedData, errSet := sjson.SetRawBytes(patchedData, "response.output."+strconv.Itoa(outputIndex)+".id", []byte(completedID.Raw))		if errSet != nil {			continue		}		patchedData = updatedData	}	return patchedData} func patchCodexCompletedOutput(eventData []byte, outputItemsByIndex map[int64][]byte, outputItemsFallback [][]byte) []byte {	outputResult := gjson.GetBytes(eventData, "response.output")	if outputResult.Exists() && outputResult.IsArray() && len(outputResult.Array()) > 0 {		return hydrateCodexCompletedOutputItemIDs(eventData, outputResult.Array(), outputItemsByIndex)	} 	shouldPatchOutput := (!outputResult.Exists() || !outputResult.IsArray() || len(outputResult.Array()) == 0) && (len(outputItemsByIndex) > 0 || len(outputItemsFallback) > 0)	if !shouldPatchOutput {		return eventData	} 	indexes := make([]int64, 0, len(outputItemsByIndex))	for idx := range outputItemsByIndex {		indexes = append(indexes, idx)	}	sort.Slice(indexes, func(i, j int) bool {		return indexes[i] < indexes[j]	}) 	items := make([][]byte, 0, len(outputItemsByIndex)+len(outputItemsFallback))	for _, idx := range indexes {		items = append(items, outputItemsByIndex[idx])	}	items = append(items, outputItemsFallback...) 	outputArray := []byte("[]")	if len(items) > 0 {		var buf bytes.Buffer		totalLen := 2		for _, item := range items {			totalLen += len(item)		}		if len(items) > 1 {			totalLen += len(items) - 1		}		buf.Grow(totalLen)		buf.WriteByte('[')		for i, item := range items {			if i > 0 {				buf.WriteByte(',')			}			buf.Write(item)		}		buf.WriteByte(']')		outputArray = buf.Bytes()	} 	completedDataPatched, _ := sjson.SetRawBytes(eventData, "response.output", outputArray)	return completedDataPatched} func codexTerminalStreamContextLengthErr(eventData []byte) (statusErr, bool) {	streamErr, body, ok := codexTerminalStreamErr(eventData)	if !ok || !codexTerminalErrorIsContextLength(body) {		return statusErr{}, false	}	return streamErr, true} func codexTerminalStreamErr(eventData []byte) (statusErr, []byte, bool) {	return codexTerminalStreamErrWithCooling(eventData, false)} func codexTerminalStreamErrWithCooling(eventData []byte, modelLevelCooling bool) (statusErr, []byte, bool) {	body, ok := codexTerminalFailureBody(eventData)	if !ok || !codexTerminalStreamErrShouldHandle(body) {		return statusErr{}, nil, false	}	return newCodexStatusErrWithCooling(http.StatusBadRequest, body, modelLevelCooling), body, true} func codexTerminalFailureErr(eventData []byte) (statusErr, []byte, bool) {	return codexTerminalFailureErrWithCooling(eventData, false)} func codexTerminalFailureErrWithCooling(eventData []byte, modelLevelCooling bool) (statusErr, []byte, bool) {	if streamErr, body, ok := codexTerminalStreamErrWithCooling(eventData, modelLevelCooling); ok {		return streamErr, body, true	}	body, ok := codexTerminalFailureBody(eventData)	if !ok {		return statusErr{}, nil, false	}	return newCodexStatusErrWithCooling(codexTerminalFailureStatus(body), body, modelLevelCooling), body, true} func codexTerminalFailureStatus(body []byte) int {	for _, path := range []string{"error.status_code", "error.status"} {		if status := int(gjson.GetBytes(body, path).Int()); status >= 400 && status <= 599 {			return status		}	} 	errorType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.type").String()))	errorCode := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.code").String()))	switch {	case errorCode == "cyber_policy":		return http.StatusBadRequest	case errorType == "not_found_error", errorCode == "not_found", errorCode == "model_not_found":		return http.StatusNotFound	case errorType == "authentication_error", errorCode == "invalid_api_key", errorCode == "unauthorized":		return http.StatusUnauthorized	case errorType == "permission_error", errorCode == "forbidden", errorCode == "permission_denied":		return http.StatusForbidden	case errorType == "rate_limit_error", errorCode == "rate_limit_exceeded":		return http.StatusTooManyRequests	case errorType == "invalid_request_error", errorType == "bad_request_error":		return http.StatusBadRequest	default:		return http.StatusBadGateway	}} func codexTerminalFailureBody(eventData []byte) ([]byte, bool) {	eventType := gjson.GetBytes(eventData, "type").String()	var body []byte	switch eventType {	case "error":		body = codexTerminalErrorBody(eventData, "error")		if len(body) == 0 {			body = codexTerminalTopLevelErrorBody(eventData)		}	case "response.failed":		body = codexTerminalErrorBody(eventData, "response.error")		if len(body) == 0 {			body = codexTerminalErrorBody(eventData, "error")		}	default:		return nil, false	}	if len(body) == 0 {		body = []byte(`{"error":{"message":"upstream stream failed without error details"}}`)	}	if seq := gjson.GetBytes(eventData, "sequence_number"); seq.Exists() {		body, _ = sjson.SetBytes(body, "sequence_number", seq.Int())	}	return body, true} func codexTerminalStreamErrShouldHandle(body []byte) bool {	if codexTerminalErrorIsContextLength(body) {		return true	}	if isCodexUsageLimitError(body) || isCodexModelCapacityError(body) {		return true	}	code, _, ok := codexStatusErrorClassification(http.StatusBadRequest, body)	return ok && code == "thinking_signature_invalid"} func codexTerminalErrorBody(eventData []byte, path string) []byte {	errorResult := gjson.GetBytes(eventData, path)	if !errorResult.Exists() {		return nil	}	body := []byte(`{"error":{}}`)	if errorResult.Type == gjson.JSON {		body, _ = sjson.SetRawBytes(body, "error", []byte(errorResult.Raw))	} else if message := strings.TrimSpace(errorResult.String()); message != "" {		body, _ = sjson.SetBytes(body, "error.message", message)	}	if strings.TrimSpace(gjson.GetBytes(body, "error.message").String()) == "" {		if message := strings.TrimSpace(gjson.GetBytes(eventData, "response.error.message").String()); message != "" {			body, _ = sjson.SetBytes(body, "error.message", message)		}	}	if strings.TrimSpace(gjson.GetBytes(body, "error.message").String()) == "" {		if code := strings.TrimSpace(gjson.GetBytes(body, "error.code").String()); code != "" {			body, _ = sjson.SetBytes(body, "error.message", code)		}	}	if strings.TrimSpace(gjson.GetBytes(body, "error.message").String()) == "" {		if errorType := strings.TrimSpace(gjson.GetBytes(body, "error.type").String()); errorType != "" {			body, _ = sjson.SetBytes(body, "error.message", errorType)		}	}	return body} func codexTerminalTopLevelErrorBody(eventData []byte) []byte {	message := strings.TrimSpace(gjson.GetBytes(eventData, "message").String())	code := strings.TrimSpace(gjson.GetBytes(eventData, "code").String())	errorType := strings.TrimSpace(gjson.GetBytes(eventData, "error_type").String())	param := strings.TrimSpace(gjson.GetBytes(eventData, "param").String())	if message == "" && code == "" && errorType == "" && param == "" {		return nil	} 	body := []byte(`{"error":{}}`)	if message != "" {		body, _ = sjson.SetBytes(body, "error.message", message)	}	if code != "" {		body, _ = sjson.SetBytes(body, "error.code", code)	}	if errorType != "" {		body, _ = sjson.SetBytes(body, "error.type", errorType)	}	if param != "" {		body, _ = sjson.SetBytes(body, "error.param", param)	}	if strings.TrimSpace(gjson.GetBytes(body, "error.message").String()) == "" {		if code != "" {			body, _ = sjson.SetBytes(body, "error.message", code)		} else if errorType != "" {			body, _ = sjson.SetBytes(body, "error.message", errorType)		}	}	return body} func codexTerminalErrorIsContextLength(body []byte) bool {	errorCode := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.code").String()))	message := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.message").String()))	return errorCode == "context_length_exceeded" ||		errorCode == "context_too_large" ||		strings.Contains(message, "context window") ||		strings.Contains(message, "context length") ||		strings.Contains(message, "too many tokens")} func newCodexStatusErr(statusCode int, body []byte) statusErr {	return newCodexStatusErrWithCooling(statusCode, body, false)} func newCodexStatusErrWithCooling(statusCode int, body []byte, modelLevelCooling bool) statusErr {	errCode := statusCode	isUsageLimit := isCodexUsageLimitError(body)	credentialScoped := isUsageLimit && !modelLevelCooling	if isCodexModelCapacityError(body) || isUsageLimit {		errCode = http.StatusTooManyRequests	}	body = classifyCodexStatusError(errCode, body)	err := statusErr{code: errCode, msg: string(body), credentialScoped: credentialScoped}	if retryAfter := parseCodexRetryAfter(errCode, body, time.Now()); retryAfter != nil {		err.retryAfter = retryAfter	}	return err} func classifyCodexStatusError(statusCode int, body []byte) []byte {	code, errType, ok := codexStatusErrorClassification(statusCode, body)	if !ok {		return body	}	message := gjson.GetBytes(body, "error.message").String()	if message == "" {		message = gjson.GetBytes(body, "message").String()	}	if message == "" {		message = strings.TrimSpace(string(body))	}	if message == "" {		message = http.StatusText(statusCode)	}	out := []byte(`{"error":{}}`)	out, _ = sjson.SetBytes(out, "error.message", message)	out, _ = sjson.SetBytes(out, "error.type", errType)	out, _ = sjson.SetBytes(out, "error.code", code)	return out} func codexStatusErrorClassification(statusCode int, body []byte) (code string, errType string, ok bool) {	errorMessage := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.message").String()))	if errorMessage == "" {		errorMessage = strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "message").String()))	}	lower := strings.ToLower(strings.TrimSpace(string(body)))	upstreamCode := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.code").String()))	upstreamType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "error.type").String()))	isInvalidRequest := upstreamType == "" || upstreamType == "invalid_request_error" 	switch {	case statusCode == http.StatusRequestEntityTooLarge || upstreamCode == "context_length_exceeded" || upstreamCode == "context_too_large" || isInvalidRequest && (strings.Contains(errorMessage, "context length") || strings.Contains(errorMessage, "context_length") || strings.Contains(errorMessage, "maximum context") || strings.Contains(errorMessage, "too many tokens")):		return "context_too_large", "invalid_request_error", true	case strings.Contains(lower, "invalid signature in thinking block") || strings.Contains(lower, "invalid_encrypted_content"):		return "thinking_signature_invalid", "invalid_request_error", true	case upstreamCode == "previous_response_not_found" || strings.Contains(lower, "previous_response_not_found") || strings.Contains(lower, "previous_response_id") && strings.Contains(lower, "not found"):		return "previous_response_not_found", "invalid_request_error", true	case statusCode == http.StatusUnauthorized || upstreamType == "authentication_error" || upstreamCode == "invalid_api_key" || strings.Contains(lower, "invalid or expired token") || strings.Contains(lower, "refresh_token_reused"):		return "auth_unavailable", "authentication_error", true	default:		return "", "", false	}} func isCodexModelCapacityError(errorBody []byte) bool {	if len(errorBody) == 0 {		return false	}	candidates := []string{		gjson.GetBytes(errorBody, "error.message").String(),		gjson.GetBytes(errorBody, "message").String(),		string(errorBody),	}	for _, candidate := range candidates {		lower := strings.ToLower(strings.TrimSpace(candidate))		if lower == "" {			continue		}		if strings.Contains(lower, "model is at capacity") ||			strings.Contains(lower, "model_at_capacity") ||			strings.Contains(lower, "model_is_at_capacity") ||			(strings.Contains(lower, "model") && strings.Contains(lower, "at capacity")) {			return true		}	}	return false} // isCodexUsageLimitError reports whether the error body represents a Codex// quota/plan-limit exhaustion (error.type == "usage_limit_reached"). This is the// signal Codex emits when a credential's usage quota is depleted, and it carries// reset timing (resets_at/resets_in_seconds) parsed by parseCodexRetryAfter.// Transient per-minute rate limits (rate_limit_error/rate_limit_exceeded) are// intentionally excluded, as they should be retried rather than cooled down.func isCodexUsageLimitError(errorBody []byte) bool {	if len(errorBody) == 0 {		return false	}	candidates := []string{		gjson.GetBytes(errorBody, "error.type").String(),		gjson.GetBytes(errorBody, "type").String(),	}	for _, candidate := range candidates {		if strings.EqualFold(strings.TrimSpace(candidate), "usage_limit_reached") {			return true		}	}	return false} func parseCodexRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration {	if statusCode != http.StatusTooManyRequests || len(errorBody) == 0 {		return nil	}	for _, quota := range []gjson.Result{gjson.GetBytes(errorBody, "error"), gjson.ParseBytes(errorBody)} {		if !strings.EqualFold(strings.TrimSpace(quota.Get("type").String()), "usage_limit_reached") {			continue		}		if resetsAt := quota.Get("resets_at").Int(); resetsAt > 0 {			resetAtTime := time.Unix(resetsAt, 0)			if resetAtTime.After(now) {				retryAfter := resetAtTime.Sub(now)				return &retryAfter			}		}		if resetsInSeconds := quota.Get("resets_in_seconds").Int(); resetsInSeconds > 0 {			retryAfter := time.Duration(resetsInSeconds) * time.Second			return &retryAfter		}	}	return nil} // codexBootstrapNowMu protects codexBootstrapNow across concurrent tests and goroutines.var (	codexBootstrapNowMu sync.RWMutex	codexBootstrapNow   = time.Now) func nowCodexBootstrap() time.Time {	codexBootstrapNowMu.RLock()	fn := codexBootstrapNow	codexBootstrapNowMu.RUnlock()	if fn != nil {		return fn()	}	return time.Now()} func setCodexBootstrapNowForTest(fn func() time.Time) func() {	codexBootstrapNowMu.Lock()	orig := codexBootstrapNow	codexBootstrapNow = fn	codexBootstrapNowMu.Unlock()	return func() {		codexBootstrapNowMu.Lock()		codexBootstrapNow = orig		codexBootstrapNowMu.Unlock()	}} // codexBootstrapMaxBufferedFrames bounds how many upstream frames may be held back while probing// for a rejection embedded in an HTTP 200 stream. It counts frames read from the upstream, not// chunks handed downstream: a frame the downstream translator does not recognise renders as zero// chunks, so a chunk count is a bound only for the formats that happen to render every frame.//// The two transports spend the budget differently: the SSE executor charges one unit per line it// holds, the websocket executor one per message it reads whether or not it holds it. So the same// number protects 15 heartbeats under the three-line event:/data:/blank shape a keepalive arrives// in - 45 lines, with the rejection frame's own event: line spending a 46th - and more under terser// framings; config.example.yaml lists the measured count for each. It is sized for that worst case// rather than for a fixed event count, because deriving the unit from the framing is what lets an// upstream evade the bound.const codexBootstrapMaxBufferedFrames = 48 // codexBootstrapMaxBufferedBytes caps what a single bootstrap retains, counted over the upstream// frames and the chunks they translate into. A frame budget alone would not bound that: on SSE// scanner.Buffer allows 50MB per line, and the websocket dialer sets no read limit at all. The check// runs before the frame is taken, so one oversized frame cannot be admitted on the strength of an// empty buffer - but it bounds what is retained, not the peak: the transport has already// materialised the frame by the time it is consulted.const codexBootstrapMaxBufferedBytes = 1 << 20 // isCodexBootstrapBufferableEvent reports whether a frame may be held back before the downstream// response headers are committed, i.e. whether nothing observable has happened yet.//// The list is closed on purpose. "Nothing has happened yet" cannot be derived from the absence of a

Lines 1–500 of 633.