feat(devin): bind upstream session_id and cascade_id to CPA canonical session

This commit is contained in:
sususu
2026-09-11 17:40:01 +08:00
committed by sususu98
parent f94752762b
commit cbe800aa28
3 changed files with 88 additions and 3 deletions

View File

@@ -234,6 +234,7 @@ func (e *DevinExecutor) prepareDevinHTTPRequest(ctx context.Context, auth *clipr
payload = sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FormatInteractions, req.Model, payload, opts.Stream)
}
systemPrompt, prompts, tools, temp, maxTokens, sessionID, cascadeID, thinkingLevel, budgetTokens := parseInteractionsPayload(payload, opts.OriginalRequest)
sessionID, cascadeID = resolveDevinSessionAndCascadeIDs(ctx, sessionID, cascadeID, opts)
chatModelUID := helps.ResolveDevinChatModelUID(req.Model, thinkingLevel, budgetTokens)
@@ -696,9 +697,20 @@ func parseInteractionsPayload(payload, originalRequest []byte) (
}
// 3. Session and Cascade ID
sessionID = strings.TrimSpace(root.Get("previous_interaction_id").String())
if sessionID == "" {
sessionID = uuid.New().String()
sessionID = strings.TrimSpace(firstNonEmpty(
root.Get("previous_interaction_id").String(),
root.Get("session_id").String(),
root.Get("sessionId").String(),
root.Get("conversation_id").String(),
))
if sessionID == "" && len(originalRequest) > 0 {
origRoot := gjson.ParseBytes(originalRequest)
sessionID = strings.TrimSpace(firstNonEmpty(
origRoot.Get("previous_interaction_id").String(),
origRoot.Get("session_id").String(),
origRoot.Get("sessionId").String(),
origRoot.Get("conversation_id").String(),
))
}
cascadeID = sessionID
@@ -1206,6 +1218,35 @@ func devinAuthLogFields(auth *cliproxyauth.Auth) (authID, authLabel, authType, a
return
}
func resolveDevinSessionAndCascadeIDs(ctx context.Context, sessionID, cascadeID string, opts cliproxyexecutor.Options) (string, string) {
if sessionID == "" {
if ctxSession := util.SessionIDFromContext(ctx); ctxSession != "" {
sessionID = ctxSession
} else if canon := cliproxyauth.CanonicalSessionID(opts.Headers, opts.OriginalRequest, opts.Metadata); canon != "" {
sessionID = canon
}
}
sessionID = normalizeDevinUUID(sessionID)
if cascadeID == "" {
cascadeID = sessionID
} else {
cascadeID = normalizeDevinUUID(cascadeID)
}
return sessionID, cascadeID
}
func normalizeDevinUUID(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return uuid.New().String()
}
if _, err := uuid.Parse(raw); err == nil {
return raw
}
// Deterministically map any non-UUID session string (e.g. lcp:hash, conv:id) to an RFC 4122 UUID v5
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(raw)).String()
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {

View File

@@ -2,13 +2,16 @@ package executor
import (
"bytes"
"context"
"net/http"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
@@ -240,6 +243,44 @@ func TestDevinStatusError_RetryAfter(t *testing.T) {
}
}
func TestResolveDevinSessionAndCascadeIDs(t *testing.T) {
// 1. Direct UUID preservation
rawUUID := "8176cf8a-feff-44c1-8e3e-b10f6d737ae1"
sid, cid := resolveDevinSessionAndCascadeIDs(context.Background(), rawUUID, rawUUID, cliproxyexecutor.Options{})
if sid != rawUUID || cid != rawUUID {
t.Fatalf("sid/cid = %q/%q, want %q", sid, cid, rawUUID)
}
// 2. Non-UUID mapping to deterministic UUID
sid1, cid1 := resolveDevinSessionAndCascadeIDs(context.Background(), "lcp:12345678", "", cliproxyexecutor.Options{})
sid2, cid2 := resolveDevinSessionAndCascadeIDs(context.Background(), "lcp:12345678", "", cliproxyexecutor.Options{})
if sid1 != sid2 || cid1 != cid2 {
t.Fatalf("deterministic mapping failed: %q != %q", sid1, sid2)
}
if _, err := uuid.Parse(sid1); err != nil {
t.Fatalf("mapped sid is not a valid UUID: %q", sid1)
}
// 3. Fallback to ctx session
ctx := util.WithSessionID(context.Background(), "ctx-session-abc")
sidCtx, cidCtx := resolveDevinSessionAndCascadeIDs(ctx, "", "", cliproxyexecutor.Options{})
if _, err := uuid.Parse(sidCtx); err != nil {
t.Fatalf("sidCtx is not a valid UUID: %q", sidCtx)
}
if sidCtx != cidCtx {
t.Fatalf("sidCtx %q != cidCtx %q", sidCtx, cidCtx)
}
// 4. Fallback to fresh UUID when nothing supplied
sidEmpty, cidEmpty := resolveDevinSessionAndCascadeIDs(context.Background(), "", "", cliproxyexecutor.Options{})
if _, err := uuid.Parse(sidEmpty); err != nil {
t.Fatalf("sidEmpty is not a valid UUID: %q", sidEmpty)
}
if sidEmpty != cidEmpty {
t.Fatalf("sidEmpty %q != cidEmpty %q", sidEmpty, cidEmpty)
}
}
func TestConsumeDevinFramesToInteractions(t *testing.T) {
// Synthesize a Connect stream with 2 data frames and 1 EOS trailer
var streamBuf bytes.Buffer

View File

@@ -406,6 +406,9 @@ func BuildDevinGetChatMessageRequest(
f15Bytes = protowire.AppendTag(f15Bytes, 1, protowire.BytesType)
f15Bytes = protowire.AppendString(f15Bytes, sessionID)
f15Bytes = protowire.AppendTag(f15Bytes, 2, protowire.VarintType)
f15Bytes = protowire.AppendVarint(f15Bytes, 53)
f15Bytes = protowire.AppendTag(f15Bytes, 3, protowire.VarintType)
f15Bytes = protowire.AppendVarint(f15Bytes, 4)