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
234 payload = sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FormatInteractions, req.Model, payload, opts.Stream)234 payload = sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FormatInteractions, req.Model, payload, opts.Stream)
235 }235 }
236 systemPrompt, prompts, tools, temp, maxTokens, sessionID, cascadeID, thinkingLevel, budgetTokens := parseInteractionsPayload(payload, opts.OriginalRequest)236 systemPrompt, prompts, tools, temp, maxTokens, sessionID, cascadeID, thinkingLevel, budgetTokens := parseInteractionsPayload(payload, opts.OriginalRequest)
237 sessionID, cascadeID = resolveDevinSessionAndCascadeIDs(ctx, sessionID, cascadeID, opts)
237 238
238 chatModelUID := helps.ResolveDevinChatModelUID(req.Model, thinkingLevel, budgetTokens)239 chatModelUID := helps.ResolveDevinChatModelUID(req.Model, thinkingLevel, budgetTokens)
239 240
@@ -696,9 +697,20 @@ func parseInteractionsPayload(payload, originalRequest []byte) ( · 456 unmodified lines
696 }697 }
697 698
698 // 3. Session and Cascade ID699 // 3. Session and Cascade ID
699 sessionID = strings.TrimSpace(root.Get("previous_interaction_id").String())700 sessionID = strings.TrimSpace(firstNonEmpty(
700 if sessionID == "" {701 root.Get("previous_interaction_id").String(),
701 sessionID = uuid.New().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 ))
702 }714 }
703 cascadeID = sessionID715 cascadeID = sessionID
704 716
@@ -1206,6 +1218,35 @@ func devinAuthLogFields(auth *cliproxyauth.Auth) (authID, authLabel, authType, a · 501 unmodified lines
1206 return1218 return
1207}1219}
1208 1220
1221func 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
1238func 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
1209func firstNonEmpty(values ...string) string {1250func firstNonEmpty(values ...string) string {
1210 for _, v := range values {1251 for _, v := range values {
1211 if strings.TrimSpace(v) != "" {1252 if strings.TrimSpace(v) != "" {
internal/runtime/executor/devin_executor_test.go+41−0
@@ -2,13 +2,16 @@ package executor · 1 unmodified line
2 2
3import (3import (
4 "bytes"4 "bytes"
5 "context"
5 "net/http"6 "net/http"
6 "strings"7 "strings"
7 "testing"8 "testing"
8 "time"9 "time"
9 10
11 "github.com/google/uuid"
10 "github.com/router-for-me/CLIProxyAPI/v7/internal/config"12 "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
11 "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"13 "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
14 "github.com/router-for-me/CLIProxyAPI/v7/internal/util"
12 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"15 cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
13 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"16 cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
14 sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"17 sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
@@ -240,6 +243,44 @@ func TestDevinStatusError_RetryAfter(t *testing.T) { · 225 unmodified lines
240 }243 }
241}244}
242 245
246func 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
243func TestConsumeDevinFramesToInteractions(t *testing.T) {284func TestConsumeDevinFramesToInteractions(t *testing.T) {
244 // Synthesize a Connect stream with 2 data frames and 1 EOS trailer285 // Synthesize a Connect stream with 2 data frames and 1 EOS trailer
245 var streamBuf bytes.Buffer286 var streamBuf bytes.Buffer
internal/runtime/executor/helps/devin_wire.go+3−0
@@ -406,6 +406,9 @@ func BuildDevinGetChatMessageRequest( · 405 unmodified lines
406 f15Bytes = protowire.AppendTag(f15Bytes, 1, protowire.BytesType)406 f15Bytes = protowire.AppendTag(f15Bytes, 1, protowire.BytesType)
407 f15Bytes = protowire.AppendString(f15Bytes, sessionID)407 f15Bytes = protowire.AppendString(f15Bytes, sessionID)
408 408
409 f15Bytes = protowire.AppendTag(f15Bytes, 2, protowire.VarintType)
410 f15Bytes = protowire.AppendVarint(f15Bytes, 53)
411
409 f15Bytes = protowire.AppendTag(f15Bytes, 3, protowire.VarintType)412 f15Bytes = protowire.AppendTag(f15Bytes, 3, protowire.VarintType)
410 f15Bytes = protowire.AppendVarint(f15Bytes, 4)413 f15Bytes = protowire.AppendVarint(f15Bytes, 4)
411 414

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