History

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

sususu committed 2 months ago

Commit cbe800a1 parent f9475273 files · +88 −3Browse files

Unified · Split

3 files
  1. internal/runtime/executor/devin_executor.go +44 −3
  2. internal/runtime/executor/devin_executor_test.go +41 −0
  3. internal/runtime/executor/helps/devin_wire.go +3 −0
internal/runtime/executor/devin_executor.go+44−3
@@ -234,6 +234,7 @@ func (e *DevinExecutor) prepareDevinHTTPRequest(ctx context.Context, auth *clipr · 233 unmodified lines
234234 payload = sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FormatInteractions, req.Model, payload, opts.Stream)
235235 }
236236 systemPrompt, prompts, tools, temp, maxTokens, sessionID, cascadeID, thinkingLevel, budgetTokens := parseInteractionsPayload(payload, opts.OriginalRequest)
237+ sessionID, cascadeID = resolveDevinSessionAndCascadeIDs(ctx, sessionID, cascadeID, opts)
237238
238239 chatModelUID := helps.ResolveDevinChatModelUID(req.Model, thinkingLevel, budgetTokens)
239240
@@ -696,9 +697,20 @@ func parseInteractionsPayload(payload, originalRequest []byte) ( · 456 unmodified lines
696697 }
697698
698699 // 3. Session and Cascade ID
699− sessionID = strings.TrimSpace(root.Get("previous_interaction_id").String())
700− if sessionID == "" {
701− sessionID = uuid.New().String()
700+ sessionID = strings.TrimSpace(firstNonEmpty(
701+ root.Get("previous_interaction_id").String(),
702+ root.Get("session_id").String(),
703+ root.Get("sessionId").String(),
704+ root.Get("conversation_id").String(),
705+ ))
706+ if sessionID == "" && len(originalRequest) > 0 {
707+ origRoot := gjson.ParseBytes(originalRequest)
708+ sessionID = strings.TrimSpace(firstNonEmpty(
709+ origRoot.Get("previous_interaction_id").String(),
710+ origRoot.Get("session_id").String(),
711+ origRoot.Get("sessionId").String(),
712+ origRoot.Get("conversation_id").String(),
713+ ))
702714 }
703715 cascadeID = sessionID
704716
@@ -1206,6 +1218,35 @@ func devinAuthLogFields(auth *cliproxyauth.Auth) (authID, authLabel, authType, a · 501 unmodified lines
12061218 return
12071219 }
12081220
1221+func resolveDevinSessionAndCascadeIDs(ctx context.Context, sessionID, cascadeID string, opts cliproxyexecutor.Options) (string, string) {
1222+ if sessionID == "" {
1223+ if ctxSession := util.SessionIDFromContext(ctx); ctxSession != "" {
1224+ sessionID = ctxSession
1225+ } else if canon := cliproxyauth.CanonicalSessionID(opts.Headers, opts.OriginalRequest, opts.Metadata); canon != "" {
1226+ sessionID = canon
1227+ }
1228+ }
1229+ sessionID = normalizeDevinUUID(sessionID)
1230+ if cascadeID == "" {
1231+ cascadeID = sessionID
1232+ } else {
1233+ cascadeID = normalizeDevinUUID(cascadeID)
1234+ }
1235+ return sessionID, cascadeID
1236+}
1237+
1238+func normalizeDevinUUID(raw string) string {
1239+ raw = strings.TrimSpace(raw)
1240+ if raw == "" {
1241+ return uuid.New().String()
1242+ }
1243+ if _, err := uuid.Parse(raw); err == nil {
1244+ return raw
1245+ }
1246+ // Deterministically map any non-UUID session string (e.g. lcp:hash, conv:id) to an RFC 4122 UUID v5
1247+ return uuid.NewSHA1(uuid.NameSpaceOID, []byte(raw)).String()
1248+}
1249+
12091250 func firstNonEmpty(values ...string) string {
12101251 for _, v := range values {
12111252 if strings.TrimSpace(v) != "" {
internal/runtime/executor/devin_executor_test.go+41−0
@@ -2,13 +2,16 @@ package executor · 1 unmodified line
22
33 import (
44 "bytes"
5+ "context"
56 "net/http"
67 "strings"
78 "testing"
89 "time"
910
11+ "github.com/google/uuid"
1012 "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
1113 "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
14+ "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
1215 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
1316 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
1417 sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
@@ -240,6 +243,44 @@ func TestDevinStatusError_RetryAfter(t *testing.T) { · 225 unmodified lines
240243 }
241244 }
242245
246+func TestResolveDevinSessionAndCascadeIDs(t *testing.T) {
247+ // 1. Direct UUID preservation
248+ rawUUID := "8176cf8a-feff-44c1-8e3e-b10f6d737ae1"
249+ sid, cid := resolveDevinSessionAndCascadeIDs(context.Background(), rawUUID, rawUUID, cliproxyexecutor.Options{})
250+ if sid != rawUUID || cid != rawUUID {
251+ t.Fatalf("sid/cid = %q/%q, want %q", sid, cid, rawUUID)
252+ }
253+
254+ // 2. Non-UUID mapping to deterministic UUID
255+ sid1, cid1 := resolveDevinSessionAndCascadeIDs(context.Background(), "lcp:12345678", "", cliproxyexecutor.Options{})
256+ sid2, cid2 := resolveDevinSessionAndCascadeIDs(context.Background(), "lcp:12345678", "", cliproxyexecutor.Options{})
257+ if sid1 != sid2 || cid1 != cid2 {
258+ t.Fatalf("deterministic mapping failed: %q != %q", sid1, sid2)
259+ }
260+ if _, err := uuid.Parse(sid1); err != nil {
261+ t.Fatalf("mapped sid is not a valid UUID: %q", sid1)
262+ }
263+
264+ // 3. Fallback to ctx session
265+ ctx := util.WithSessionID(context.Background(), "ctx-session-abc")
266+ sidCtx, cidCtx := resolveDevinSessionAndCascadeIDs(ctx, "", "", cliproxyexecutor.Options{})
267+ if _, err := uuid.Parse(sidCtx); err != nil {
268+ t.Fatalf("sidCtx is not a valid UUID: %q", sidCtx)
269+ }
270+ if sidCtx != cidCtx {
271+ t.Fatalf("sidCtx %q != cidCtx %q", sidCtx, cidCtx)
272+ }
273+
274+ // 4. Fallback to fresh UUID when nothing supplied
275+ sidEmpty, cidEmpty := resolveDevinSessionAndCascadeIDs(context.Background(), "", "", cliproxyexecutor.Options{})
276+ if _, err := uuid.Parse(sidEmpty); err != nil {
277+ t.Fatalf("sidEmpty is not a valid UUID: %q", sidEmpty)
278+ }
279+ if sidEmpty != cidEmpty {
280+ t.Fatalf("sidEmpty %q != cidEmpty %q", sidEmpty, cidEmpty)
281+ }
282+}
283+
243284 func TestConsumeDevinFramesToInteractions(t *testing.T) {
244285 // Synthesize a Connect stream with 2 data frames and 1 EOS trailer
245286 var streamBuf bytes.Buffer
internal/runtime/executor/helps/devin_wire.go+3−0
@@ -406,6 +406,9 @@ func BuildDevinGetChatMessageRequest( · 405 unmodified lines
406406 f15Bytes = protowire.AppendTag(f15Bytes, 1, protowire.BytesType)
407407 f15Bytes = protowire.AppendString(f15Bytes, sessionID)
408408
409+ f15Bytes = protowire.AppendTag(f15Bytes, 2, protowire.VarintType)
410+ f15Bytes = protowire.AppendVarint(f15Bytes, 53)
411+
409412 f15Bytes = protowire.AppendTag(f15Bytes, 3, protowire.VarintType)
410413 f15Bytes = protowire.AppendVarint(f15Bytes, 4)
411414

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t