mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-03 19:21:17 +00:00
Restore Claude continuity after the continuity refactor, keep auth-affinity keys out of upstream Codex session identifiers, and only persist affinity after successful execution so retries can still rotate to healthy credentials when the first auth fails.
128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const codexAuthAffinityMetadataKey = "auth_affinity_key"
|
|
|
|
type codexContinuity struct {
|
|
Key string
|
|
Source string
|
|
}
|
|
|
|
func metadataString(meta map[string]any, key string) string {
|
|
if len(meta) == 0 {
|
|
return ""
|
|
}
|
|
raw, ok := meta[key]
|
|
if !ok || raw == nil {
|
|
return ""
|
|
}
|
|
switch v := raw.(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case []byte:
|
|
return strings.TrimSpace(string(v))
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func principalString(raw any) string {
|
|
switch v := raw.(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case fmt.Stringer:
|
|
return strings.TrimSpace(v.String())
|
|
default:
|
|
return strings.TrimSpace(fmt.Sprintf("%v", raw))
|
|
}
|
|
}
|
|
|
|
func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) codexContinuity {
|
|
if promptCacheKey := strings.TrimSpace(gjson.GetBytes(req.Payload, "prompt_cache_key").String()); promptCacheKey != "" {
|
|
return codexContinuity{Key: promptCacheKey, Source: "prompt_cache_key"}
|
|
}
|
|
if executionSession := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); executionSession != "" {
|
|
return codexContinuity{Key: executionSession, Source: "execution_session"}
|
|
}
|
|
if ginCtx := ginContextFrom(ctx); ginCtx != nil {
|
|
if ginCtx.Request != nil {
|
|
if v := strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")); v != "" {
|
|
return codexContinuity{Key: v, Source: "idempotency_key"}
|
|
}
|
|
}
|
|
if v, exists := ginCtx.Get("apiKey"); exists && v != nil {
|
|
if trimmed := principalString(v); trimmed != "" {
|
|
return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"}
|
|
}
|
|
}
|
|
}
|
|
if auth != nil {
|
|
if authID := strings.TrimSpace(auth.ID); authID != "" {
|
|
return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:auth:"+authID)).String(), Source: "auth_id"}
|
|
}
|
|
}
|
|
return codexContinuity{}
|
|
}
|
|
|
|
func applyCodexContinuityBody(rawJSON []byte, continuity codexContinuity) []byte {
|
|
if continuity.Key == "" {
|
|
return rawJSON
|
|
}
|
|
rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", continuity.Key)
|
|
return rawJSON
|
|
}
|
|
|
|
func applyCodexContinuityHeaders(headers http.Header, continuity codexContinuity) {
|
|
if headers == nil || continuity.Key == "" {
|
|
return
|
|
}
|
|
headers.Set("session_id", continuity.Key)
|
|
}
|
|
|
|
func logCodexRequestDiagnostics(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, headers http.Header, body []byte, continuity codexContinuity) {
|
|
if !log.IsLevelEnabled(log.DebugLevel) {
|
|
return
|
|
}
|
|
entry := logWithRequestID(ctx)
|
|
authID := ""
|
|
authFile := ""
|
|
if auth != nil {
|
|
authID = strings.TrimSpace(auth.ID)
|
|
authFile = strings.TrimSpace(auth.FileName)
|
|
}
|
|
selectedAuthID := metadataString(opts.Metadata, cliproxyexecutor.SelectedAuthMetadataKey)
|
|
executionSessionID := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey)
|
|
entry.Debugf(
|
|
"codex request diagnostics auth_id=%s selected_auth_id=%s auth_file=%s exec_session=%s continuity_source=%s session_id=%s prompt_cache_key=%s prompt_cache_retention=%s store=%t has_instructions=%t reasoning_effort=%s reasoning_summary=%s chatgpt_account_id=%t originator=%s model=%s source_format=%s",
|
|
authID,
|
|
selectedAuthID,
|
|
authFile,
|
|
executionSessionID,
|
|
continuity.Source,
|
|
strings.TrimSpace(headers.Get("session_id")),
|
|
gjson.GetBytes(body, "prompt_cache_key").String(),
|
|
gjson.GetBytes(body, "prompt_cache_retention").String(),
|
|
gjson.GetBytes(body, "store").Bool(),
|
|
gjson.GetBytes(body, "instructions").Exists(),
|
|
gjson.GetBytes(body, "reasoning.effort").String(),
|
|
gjson.GetBytes(body, "reasoning.summary").String(),
|
|
strings.TrimSpace(headers.Get("Chatgpt-Account-Id")) != "",
|
|
strings.TrimSpace(headers.Get("Originator")),
|
|
req.Model,
|
|
opts.SourceFormat.String(),
|
|
)
|
|
}
|