summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/reasoning-effort.ts
blob: 2a55089b473b36f91b42bc170f64763c3c51a642 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import type { ReasoningEffort } from "@dispatch/transport-contract";

/**
 * Pure helpers for the reasoning-effort selector (the thinking-depth knob).
 *
 * The canonical ladder + resolution chain are SERVER-owned (`[email protected]`
 * `ReasoningEffort`; per-turn override → persisted conversation value → default
 * `"high"`). These helpers only shape the persisted value for display: a `null`
 * from `GET /conversations/:id/reasoning-effort` means "never set ⇒ the default
 * applies", so the selector shows `high (default)` — never "off". Zero DOM,
 * zero Svelte.
 */

/** The canonical ladder, in ascending thinking-depth order (`[email protected]`). */
export const REASONING_EFFORT_LEVELS: readonly ReasoningEffort[] = [
	"low",
	"medium",
	"high",
	"xhigh",
	"max",
];

/** The server's fallback when nothing is set (the resolution chain's tail). */
export const DEFAULT_REASONING_EFFORT: ReasoningEffort = "high";

/** Narrow an untrusted string (e.g. a `<select>` value) to the ladder. */
export function isReasoningEffort(value: string): value is ReasoningEffort {
	return (REASONING_EFFORT_LEVELS as readonly string[]).includes(value);
}

/**
 * The level the selector should show as selected: the persisted value, or the
 * server default when never set (`null` = "default applies", not "off").
 */
export function effectiveEffort(persisted: ReasoningEffort | null): ReasoningEffort {
	return persisted ?? DEFAULT_REASONING_EFFORT;
}

/** One `<option>` of the selector. */
export interface EffortOption {
	readonly value: ReasoningEffort;
	readonly label: string;
}

/**
 * The selector's options: every ladder level, with the server default marked
 * `(default)` so a never-set conversation reads "high (default)".
 */
export function effortOptions(): readonly EffortOption[] {
	return REASONING_EFFORT_LEVELS.map((level) => ({
		value: level,
		label: level === DEFAULT_REASONING_EFFORT ? `${level} (default)` : level,
	}));
}

// ── Injected port (consumer-defines-port; the composition root adapts the
//    store's `PUT /conversations/:id/reasoning-effort` to this shape). ────────

/** Outcome of `PUT /conversations/:id/reasoning-effort`. */
export type ReasoningEffortSaveResult =
	| { readonly ok: true; readonly reasoningEffort: ReasoningEffort }
	| { readonly ok: false; readonly error: string };

export type SaveReasoningEffort = (
	level: ReasoningEffort,
) => Promise<ReasoningEffortSaveResult | null>;