Merge upstream v0.4.0 into Gitea fork
This commit is contained in:
@@ -18,13 +18,16 @@ import type {
|
||||
AppearanceSettings,
|
||||
BackgroundModelRole,
|
||||
CompactionSettings,
|
||||
CustomModelOverride,
|
||||
GeneralSettings,
|
||||
DictationSettings,
|
||||
GraphSettings,
|
||||
ModelsSettings,
|
||||
RoleModelOverride,
|
||||
} from "../core/settings.ts";
|
||||
import { BACKGROUND_MODEL_ROLES, COMPACTION_THRESHOLD_FLOOR, EXTRACTION_CHUNK_TOKENS_FLOOR, MODEL_SLOTS } from "../core/settings.ts";
|
||||
import { BACKGROUND_MODEL_ROLES, COMPACTION_THRESHOLD_FLOOR, CUSTOM_MODEL_PIN_FLOOR, EXTRACTION_CHUNK_TOKENS_FLOOR, MODEL_SLOTS } from "../core/settings.ts";
|
||||
import { WRITABLE_TOOL_KEYS } from "../core/pending.ts";
|
||||
import { isDictationLanguage } from "../core/dictation.ts";
|
||||
|
||||
/** Parsed-or-rejected — `ok: false` carries the 400's error message. */
|
||||
export type SettingsBodyResult<T> =
|
||||
@@ -105,6 +108,15 @@ export function parseGeneralSettingsBody(body: unknown): SettingsBodyResult<Gene
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
/** claudeCode: explicit app-wide opt-in. Empty section resets to off. */
|
||||
export function parseClaudeCodeSettingsBody(body: unknown): SettingsBodyResult<{ enabled?: boolean }> {
|
||||
const raw = asRecord(body);
|
||||
if (!raw) return { ok: false, error: "Invalid JSON body" };
|
||||
if (raw.enabled === undefined) return { ok: true, settings: {} };
|
||||
if (typeof raw.enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
|
||||
return { ok: true, settings: { enabled: raw.enabled } };
|
||||
}
|
||||
|
||||
/** agent.models: only the six known background roles; each override's
|
||||
* fields mirror ChatSettings (model pair + independent thinkingLevel). */
|
||||
function parseRoleOverride(value: unknown, role: string): { ok: true; override: RoleModelOverride } | { ok: false; error: string } {
|
||||
@@ -401,8 +413,56 @@ export function parseCompactionSettingsBody(body: unknown): SettingsBodyResult<C
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
/** The section→parser map for PUT /api/settings/[section] (the route also
|
||||
* carries the special prompts sub-resource). */
|
||||
/** dictation: { language?: one of Nova-3's supported language codes } */
|
||||
export function parseDictationSettingsBody(body: unknown): SettingsBodyResult<DictationSettings> {
|
||||
const raw = asRecord(body);
|
||||
if (!raw) return { ok: false, error: "Invalid JSON body" };
|
||||
if (raw.language === undefined) return { ok: true, settings: {} };
|
||||
if (!isDictationLanguage(raw.language)) return { ok: false, error: "Unsupported dictation language" };
|
||||
return { ok: true, settings: { language: raw.language } };
|
||||
}
|
||||
|
||||
/** The custom endpoint's per-model pins (PATCH /api/custom-provider): the
|
||||
* whole desired map, replace-whole like every section. Strict write side — a
|
||||
* token count must be a whole number at or above the floor (the read side
|
||||
* clamps, this rejects — the compaction discipline). `reasoning` is the
|
||||
* tri-state capability pin, not a backend execution toggle. */
|
||||
export function parseCustomModelOverridesBody(
|
||||
body: unknown,
|
||||
): SettingsBodyResult<Record<string, CustomModelOverride>> {
|
||||
const raw = asRecord(body);
|
||||
if (!raw) return { ok: false, error: "Invalid JSON body" };
|
||||
const map = asRecord(raw.overrides);
|
||||
if (!map) return { ok: false, error: "overrides must be a { modelId: { context?, maxTokens?, reasoning? } } map" };
|
||||
const out: Record<string, CustomModelOverride> = {};
|
||||
for (const [key, value] of Object.entries(map)) {
|
||||
const id = key.trim();
|
||||
if (!id) return { ok: false, error: "override keys must be non-empty model ids" };
|
||||
const entry = asRecord(value);
|
||||
if (!entry) return { ok: false, error: `override for ${id} must be an object` };
|
||||
const pin: CustomModelOverride = {};
|
||||
for (const field of ["context", "maxTokens"] as const) {
|
||||
const v = entry[field];
|
||||
if (v === undefined) continue;
|
||||
if (typeof v !== "number" || !Number.isInteger(v) || v < CUSTOM_MODEL_PIN_FLOOR) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `${field} for ${id} must be a whole number of tokens at least ${CUSTOM_MODEL_PIN_FLOOR}`,
|
||||
};
|
||||
}
|
||||
pin[field] = v;
|
||||
}
|
||||
if (entry.reasoning !== undefined) {
|
||||
if (typeof entry.reasoning !== "boolean") return { ok: false, error: `reasoning for ${id} must be true or false` };
|
||||
pin.reasoning = entry.reasoning;
|
||||
}
|
||||
Object.defineProperty(out, id, { value: pin, enumerable: true, configurable: true, writable: true });
|
||||
}
|
||||
return { ok: true, settings: out };
|
||||
}
|
||||
|
||||
/** The section→parser map for PUT /api/settings/[section]. The custom endpoint
|
||||
* is NOT a section — it connects via POST /api/custom-provider. */
|
||||
export const SETTINGS_SECTIONS = {
|
||||
general: parseGeneralSettingsBody,
|
||||
chat: parseSettingsBody,
|
||||
@@ -411,6 +471,8 @@ export const SETTINGS_SECTIONS = {
|
||||
graph: parseGraphSettingsBody,
|
||||
compaction: parseCompactionSettingsBody,
|
||||
models: parseModelsSettingsBody,
|
||||
dictation: parseDictationSettingsBody,
|
||||
claudeCode: parseClaudeCodeSettingsBody,
|
||||
} as const;
|
||||
|
||||
export type SettingsSectionName = keyof typeof SETTINGS_SECTIONS;
|
||||
|
||||
Reference in New Issue
Block a user