File size: 3,952 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
 * Fallback Policy — Domain Layer (T-19)
 *
 * Declarative fallback chain for model routing.
 * When a primary provider is unavailable, the policy engine
 * resolves to alternative providers in priority order.
 *
 * State is persisted in SQLite via domainState.js.
 *
 * @module domain/fallbackPolicy
 */

import {
  saveFallbackChain,
  loadFallbackChain,
  loadAllFallbackChains,
  deleteFallbackChain,
  deleteAllFallbackChains,
} from "../lib/db/domainState";

/**
 * @typedef {Object} FallbackEntry
 * @property {string} provider - Provider ID
 * @property {number} [priority=0] - Lower = higher priority
 * @property {boolean} [enabled=true] - Whether this fallback is active
 */

/** @type {Map<string, FallbackEntry[]>} In-memory cache backed by SQLite */
const fallbackChains = new Map();

/** @type {boolean} Whether we've loaded from DB yet */
let _loaded = false;

/**
 * Ensure in-memory cache is hydrated from SQLite.
 */
function ensureLoaded() {
  if (_loaded) return;
  try {
    const all = loadAllFallbackChains();
    for (const [model, chain] of Object.entries(all)) {
      fallbackChains.set(model, chain);
    }
  } catch {
    // DB may not be ready yet (build phase), that's ok
  }
  _loaded = true;
}

/**
 * Register a fallback chain for a model.
 *
 * @param {string} model - Model identifier (e.g. "gpt-4o")
 * @param {FallbackEntry[]} chain - Ordered list of fallback providers
 */
export function registerFallback(model, chain) {
  ensureLoaded();
  const sorted = [...chain]
    .map((e) => ({
      provider: e.provider,
      priority: e.priority ?? 0,
      enabled: e.enabled ?? true,
    }))
    .sort((a, b) => a.priority - b.priority);

  fallbackChains.set(model, sorted);
  try {
    saveFallbackChain(model, sorted);
  } catch {
    // Non-critical: in-memory still works
  }
}

/**
 * Resolve the fallback chain for a model.
 * Returns only enabled providers, sorted by priority.
 *
 * @param {string} model
 * @param {string[]} [excludeProviders=[]] - Providers to skip (e.g. already tried)
 * @returns {FallbackEntry[]} Ordered list of fallback providers
 */
export function resolveFallbackChain(model, excludeProviders = []) {
  ensureLoaded();
  const chain = fallbackChains.get(model);
  if (!chain) return [];

  const excludeSet = new Set(excludeProviders);
  return chain.filter((e) => e.enabled && !excludeSet.has(e.provider));
}

/**
 * Get the next provider in the fallback chain.
 *
 * @param {string} model
 * @param {string[]} [excludeProviders=[]]
 * @returns {string | null} Next provider ID or null if chain exhausted
 */
export function getNextFallback(model, excludeProviders = []) {
  const chain = resolveFallbackChain(model, excludeProviders);
  return chain.length > 0 ? chain[0].provider : null;
}

/**
 * Check if a model has any fallback providers configured.
 *
 * @param {string} model
 * @returns {boolean}
 */
export function hasFallback(model) {
  ensureLoaded();
  const chain = fallbackChains.get(model);
  return !!chain && chain.some((e) => e.enabled);
}

/**
 * Remove a fallback chain for a model.
 *
 * @param {string} model
 * @returns {boolean} true if removed
 */
export function removeFallback(model) {
  ensureLoaded();
  const removed = fallbackChains.delete(model);
  if (removed) {
    try {
      deleteFallbackChain(model);
    } catch {
      // Non-critical
    }
  }
  return removed;
}

/**
 * Get all registered fallback chains (for dashboard).
 *
 * @returns {Record<string, FallbackEntry[]>}
 */
export function getAllFallbackChains() {
  ensureLoaded();
  /** @type {Record<string, FallbackEntry[]>} */
  const result = {};
  for (const [model, chain] of fallbackChains.entries()) {
    result[model] = chain;
  }
  return result;
}

/**
 * Reset all fallback chains (for testing).
 */
export function resetAllFallbacks() {
  fallbackChains.clear();
  _loaded = false;
  try {
    deleteAllFallbackChains();
  } catch {
    // Non-critical
  }
}