| import type { OpenAICompletionsResponse } from '../../shared/api/completionsClient'; |
| import type { PredictionAttributeModelVariant } from '../../shared/prediction_attribution/core/attributionResultCache'; |
| import { |
| buildContentKeyFromBusinessKey, |
| getByContentKey, |
| listMru, |
| patchPayloadRow, |
| type CachedHistoryListRow, |
| removeByContentKey, |
| touchByContentKey, |
| upsertEntry, |
| } from '../../shared/storage/cachedHistoryStore'; |
|
|
| const MAX_SIZE = 50; |
| const NAMESPACE = 'chat'; |
| const LS_LEGACY_CHAT_CACHE_MIGRATED = 'info_radar_chat_cache_model_key_migrated'; |
|
|
| export type CompletionResultCacheKey = { |
| prompt: string; |
| model: PredictionAttributeModelVariant; |
| }; |
| |
| export type ChatCompletionDraft = { |
| mode: 'raw' | 'chat'; |
| model?: PredictionAttributeModelVariant; |
| maxTokens?: number; |
| |
| raw?: string; |
| |
| system?: string; |
| user?: string; |
| useSystem?: boolean; |
| enableThinking?: boolean; |
| }; |
|
|
| export type CompletionCachedEntry = { |
| promptUsed: string; |
| response: OpenAICompletionsResponse; |
| |
| modelVariant?: PredictionAttributeModelVariant; |
| |
| draft?: ChatCompletionDraft; |
| }; |
|
|
| export function contentKeyForCacheKey(key: CompletionResultCacheKey): string { |
| return buildContentKeyFromBusinessKey({ prompt: key.prompt, model: key.model }); |
| } |
|
|
| |
| export function legacyContentKeyForPrompt(prompt: string): string { |
| return buildContentKeyFromBusinessKey({ prompt }); |
| } |
|
|
| function parseLegacyPromptOnlyBusinessKey(businessKeyJson: string): string | undefined { |
| try { |
| const o = JSON.parse(businessKeyJson) as { prompt?: unknown; model?: unknown }; |
| if (typeof o.prompt === 'string' && o.model === undefined) { |
| return o.prompt; |
| } |
| } catch { |
| |
| } |
| return undefined; |
| } |
|
|
| function legacyDraftForEntry( |
| entry: CompletionCachedEntry, |
| prompt: string |
| ): ChatCompletionDraft { |
| return { |
| mode: 'raw', |
| model: 'instruct', |
| raw: entry.promptUsed ?? prompt, |
| }; |
| } |
|
|
| |
| export async function migrateLegacyChatCacheIfNeeded(): Promise<void> { |
| if (localStorage.getItem(LS_LEGACY_CHAT_CACHE_MIGRATED)) { |
| return; |
| } |
| const rows = await listMru<CompletionCachedEntry>(NAMESPACE); |
| for (const row of rows) { |
| const prompt = parseLegacyPromptOnlyBusinessKey(row.businessKeyJson); |
| if (prompt === undefined) continue; |
| const entry = row.payload; |
| const draft = entry.draft ?? legacyDraftForEntry(entry, prompt); |
| const upgraded: CompletionCachedEntry = { |
| ...entry, |
| modelVariant: entry.modelVariant ?? 'instruct', |
| draft, |
| }; |
| const businessKeyJson = JSON.stringify({ prompt, model: 'instruct' as const }); |
| await patchPayloadRow(NAMESPACE, row.contentKey, { |
| businessKeyJson, |
| listLabel: listLabelForSave({ prompt, model: 'instruct' }, draft), |
| payload: upgraded, |
| }); |
| } |
| localStorage.setItem(LS_LEGACY_CHAT_CACHE_MIGRATED, '1'); |
| } |
|
|
| |
| export async function get(key: CompletionResultCacheKey): Promise<OpenAICompletionsResponse | undefined> { |
| const primary = contentKeyForCacheKey(key); |
| let row = await getByContentKey<CompletionCachedEntry>(NAMESPACE, primary); |
| if (!row && key.model === 'instruct') { |
| const legacy = legacyContentKeyForPrompt(key.prompt); |
| if (legacy !== primary) { |
| row = await getByContentKey<CompletionCachedEntry>(NAMESPACE, legacy); |
| } |
| } |
| return row?.payload.response; |
| } |
|
|
| export async function getCachedEntryByContentKey(raw: string): Promise<CompletionCachedEntry | undefined> { |
| if (!raw) return undefined; |
| const entry = await getByContentKey<CompletionCachedEntry>(NAMESPACE, raw); |
| return entry?.payload; |
| } |
|
|
| export async function touch(key: CompletionResultCacheKey): Promise<void> { |
| const primary = contentKeyForCacheKey(key); |
| await touchByContentKey(NAMESPACE, primary); |
| if (key.model === 'instruct') { |
| const legacy = legacyContentKeyForPrompt(key.prompt); |
| if (legacy !== primary) { |
| const row = await getByContentKey<CompletionCachedEntry>(NAMESPACE, legacy); |
| if (row) { |
| await touchByContentKey(NAMESPACE, legacy); |
| } |
| } |
| } |
| } |
|
|
| export async function touchCachedEntryByContentKey(contentKey: string): Promise<void> { |
| await touchByContentKey(NAMESPACE, contentKey); |
| } |
|
|
| export async function removeForCacheKey(key: CompletionResultCacheKey): Promise<void> { |
| await removeByContentKey(NAMESPACE, contentKeyForCacheKey(key)); |
| if (key.model === 'instruct') { |
| const legacy = legacyContentKeyForPrompt(key.prompt); |
| if (legacy !== contentKeyForCacheKey(key)) { |
| await removeByContentKey(NAMESPACE, legacy); |
| } |
| } |
| } |
|
|
| export async function listCachedHistoryRows(): Promise<CachedHistoryListRow[]> { |
| const rows = await listMru<CompletionCachedEntry>(NAMESPACE); |
| return rows.map((r) => ({ contentKey: r.contentKey, listLabel: r.listLabel })); |
| } |
|
|
| function listLabelForSave(key: CompletionResultCacheKey, draft?: ChatCompletionDraft): string { |
| if (draft?.mode === 'chat') { |
| const u = draft.user?.trim(); |
| if (u) return u; |
| } |
| if (draft?.mode === 'raw') { |
| const r = draft.raw?.trim(); |
| if (r) return r; |
| } |
| return key.prompt; |
| } |
|
|
| export async function save( |
| key: CompletionResultCacheKey, |
| response: OpenAICompletionsResponse, |
| status: 'partial' | 'complete' = 'complete', |
| draft?: ChatCompletionDraft |
| ): Promise<{ contentKey: string }> { |
| return upsertEntry({ |
| namespace: NAMESPACE, |
| businessKeyJson: JSON.stringify({ prompt: key.prompt, model: key.model }), |
| listLabel: listLabelForSave(key, draft), |
| payload: { |
| promptUsed: key.prompt, |
| response, |
| modelVariant: key.model === 'base' ? 'base' : 'instruct', |
| draft, |
| }, |
| status, |
| maxEntries: MAX_SIZE, |
| }); |
| } |
|
|
| export async function removeCachedEntryByContentKey(contentKey: string): Promise<void> { |
| await removeByContentKey(NAMESPACE, contentKey); |
| } |
|
|