Unified · Split
| @@ -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 ID | 699 | // 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 = sessionID | 715 | cascadeID = sessionID |
| 704 | 716 | ||
| @@ -1206,6 +1218,35 @@ func devinAuthLogFields(auth *cliproxyauth.Auth) (authID, authLabel, authType, a · 501 unmodified lines | |||
| 1206 | return | 1218 | return |
| 1207 | } | 1219 | } |
| 1208 | 1220 | ||
| 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 | |||
| 1209 | func firstNonEmpty(values ...string) string { | 1250 | func firstNonEmpty(values ...string) string { |
| 1210 | for _, v := range values { | 1251 | for _, v := range values { |
| 1211 | if strings.TrimSpace(v) != "" { | 1252 | if strings.TrimSpace(v) != "" { |
| @@ -2,13 +2,16 @@ package executor · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 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 | ||
| 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 | |||
| 243 | func TestConsumeDevinFramesToInteractions(t *testing.T) { | 284 | func TestConsumeDevinFramesToInteractions(t *testing.T) { |
| 244 | // Synthesize a Connect stream with 2 data frames and 1 EOS trailer | 285 | // Synthesize a Connect stream with 2 data frames and 1 EOS trailer |
| 245 | var streamBuf bytes.Buffer | 286 | var streamBuf bytes.Buffer |
| @@ -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 | ||