feat(ai): split shared provider config from Anki runtime

This commit is contained in:
2026-03-08 16:10:51 -07:00
parent f10e905dbd
commit 9e46176519
19 changed files with 457 additions and 133 deletions

View File

@@ -13,7 +13,6 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
const media = isObject(ac.media) ? (ac.media as Record<string, unknown>) : {};
const metadata = isObject(ac.metadata) ? (ac.metadata as Record<string, unknown>) : {};
const proxy = isObject(ac.proxy) ? (ac.proxy as Record<string, unknown>) : {};
const aiSource = isObject(ac.ai) ? ac.ai : isObject(ac.openRouter) ? ac.openRouter : {};
const legacyKeys = new Set([
'audioField',
'imageField',
@@ -42,19 +41,11 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
'autoUpdateNewCards',
]);
if (ac.openRouter !== undefined) {
context.warn(
'ankiConnect.openRouter',
ac.openRouter,
context.resolved.ankiConnect.ai,
'Deprecated key; use ankiConnect.ai instead.',
);
}
const { nPlusOne: _nPlusOneConfigFromAnkiConnect, ...ankiConnectWithoutNPlusOne } = ac as Record<
string,
unknown
>;
const {
nPlusOne: _nPlusOneConfigFromAnkiConnect,
ai: _ankiAiConfig,
...ankiConnectWithoutNPlusOne
} = ac as Record<string, unknown>;
const ankiConnectWithoutLegacy = Object.fromEntries(
Object.entries(ankiConnectWithoutNPlusOne).filter(([key]) => !legacyKeys.has(key)),
);
@@ -70,10 +61,6 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
? (ac.fields as (typeof context.resolved)['ankiConnect']['fields'])
: {}),
},
ai: {
...context.resolved.ankiConnect.ai,
...(aiSource as (typeof context.resolved)['ankiConnect']['ai']),
},
media: {
...context.resolved.ankiConnect.media,
...(isObject(ac.media)
@@ -219,6 +206,56 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
);
}
if (isObject(ac.ai)) {
const aiEnabled = asBoolean(ac.ai.enabled);
if (aiEnabled !== undefined) {
context.resolved.ankiConnect.ai.enabled = aiEnabled;
} else if (ac.ai.enabled !== undefined) {
context.warn(
'ankiConnect.ai.enabled',
ac.ai.enabled,
context.resolved.ankiConnect.ai.enabled,
'Expected boolean.',
);
}
const aiModel = asString(ac.ai.model);
if (aiModel !== undefined) {
context.resolved.ankiConnect.ai.model = aiModel;
} else if (ac.ai.model !== undefined) {
context.warn(
'ankiConnect.ai.model',
ac.ai.model,
context.resolved.ankiConnect.ai.model,
'Expected string.',
);
}
const aiSystemPrompt = asString(ac.ai.systemPrompt);
if (aiSystemPrompt !== undefined) {
context.resolved.ankiConnect.ai.systemPrompt = aiSystemPrompt;
} else if (ac.ai.systemPrompt !== undefined) {
context.warn(
'ankiConnect.ai.systemPrompt',
ac.ai.systemPrompt,
context.resolved.ankiConnect.ai.systemPrompt,
'Expected string.',
);
}
} else {
const aiEnabled = asBoolean(ac.ai);
if (aiEnabled !== undefined) {
context.resolved.ankiConnect.ai.enabled = aiEnabled;
} else if (ac.ai !== undefined) {
context.warn(
'ankiConnect.ai',
ac.ai,
context.resolved.ankiConnect.ai.enabled,
'Expected boolean or object.',
);
}
}
if (Array.isArray(ac.tags)) {
const normalizedTags = ac.tags
.filter((entry): entry is string => typeof entry === 'string')

View File

@@ -4,6 +4,46 @@ import { asBoolean, asNumber, asString, isObject } from './shared';
export function applyIntegrationConfig(context: ResolveContext): void {
const { src, resolved, warn } = context;
if (isObject(src.ai)) {
const booleanKeys = ['enabled'] as const;
for (const key of booleanKeys) {
const value = asBoolean(src.ai[key]);
if (value !== undefined) {
resolved.ai[key] = value;
} else if (src.ai[key] !== undefined) {
warn(`ai.${key}`, src.ai[key], resolved.ai[key], 'Expected boolean.');
}
}
const stringKeys = ['apiKey', 'apiKeyCommand', 'baseUrl', 'model', 'systemPrompt'] as const;
for (const key of stringKeys) {
const value = asString(src.ai[key]);
if (value !== undefined) {
resolved.ai[key] = value;
} else if (src.ai[key] !== undefined) {
warn(`ai.${key}`, src.ai[key], resolved.ai[key], 'Expected string.');
}
}
const requestTimeoutMs = asNumber(src.ai.requestTimeoutMs);
if (
requestTimeoutMs !== undefined &&
Number.isInteger(requestTimeoutMs) &&
requestTimeoutMs > 0
) {
resolved.ai.requestTimeoutMs = requestTimeoutMs;
} else if (src.ai.requestTimeoutMs !== undefined) {
warn(
'ai.requestTimeoutMs',
src.ai.requestTimeoutMs,
resolved.ai.requestTimeoutMs,
'Expected positive integer.',
);
}
} else if (src.ai !== undefined) {
warn('ai', src.ai, resolved.ai, 'Expected object.');
}
if (isObject(src.anilist)) {
const enabled = asBoolean(src.anilist.enabled);
if (enabled !== undefined) {