1import type * as Response from "effect/ai/Response"
2import * as Predicate from "effect/Predicate"
3import type { ReasoningDetails } from "../OpenRouterLanguageModel.ts"
4
5const finishReasonMap: Record<string, Response.FinishReason> = {
6 content_filter: "content-filter",
7 error: "error",
8 function_call: "tool-calls",
9 length: "length",
10 tool_calls: "tool-calls",
11 stop: "stop"
12}
13
14/** @internal */
15export const resolveFinishReason = (
16 finishReason: string | null | undefined
17): Response.FinishReason => {
18 if (Predicate.isNullish(finishReason)) {
19 return "other"
20 }
21 return Object.hasOwn(finishReasonMap, finishReason) ? finishReasonMap[finishReason] : "unknown"
22}
23
24/**
25 * Tracks ReasoningDetailUnion entries and deduplicates them based
26 * on a derived canonical key.
27 *
28 * This is used when converting messages to ensure the API request only
29 * contains unique reasoning details, preventing "Duplicate item found with id"
30 * errors in multi-turn conversations.
31 *
32 * The canonical key logic matches the OpenRouter API's deduplication exactly
33 * (see openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts):
34 * - Summary: key = summary field
35 * - Encrypted: key = id field (if truthy) or data field
36 * - Text: key = text field (if truthy) or signature field (if truthy)
37 *
38 * @internal
39 */
40export class ReasoningDetailsDuplicateTracker {
41 readonly #seenKeys = new Set<string>()
42
43 /**
44 * Attempts to track a detail.
45 *
46 * or `false` if it was skipped (no valid key) or already seen (duplicate).
47 */
48 upsert(detail: ReasoningDetails[number]): boolean {
49 const key = this.getCanonicalKey(detail)
50
51 if (Predicate.isNull(key)) {
52 return false
53 }
54
55 if (this.#seenKeys.has(key)) {
56 return false
57 }
58
59 this.#seenKeys.add(key)
60
61 return true
62 }
63
64 private getCanonicalKey(detail: ReasoningDetails[number]): string | null {
65 // This logic matches the OpenRouter API's deduplication exactly.
66 // See: openrouter-web/packages/llm-interfaces/reasonings/duplicate-tracker.ts
67 switch (detail.type) {
68 case "reasoning.summary": {
69 return detail.summary
70 }
71
72 case "reasoning.encrypted": {
73 return Predicate.isNotNullish(detail.id) ? detail.id : detail.data
74 }
75
76 case "reasoning.text": {
77 if (Predicate.isNotNullish(detail.text)) {
78 return detail.text
79 }
80
81 if (Predicate.isNotNullish(detail.signature)) {
82 return detail.signature
83 }
84
85 return null
86 }
87
88 default: {
89 // Handle unknown types gracefully
90 return null
91 }
92 }
93 }
94}