diff --git a/common/template-parser.ts b/common/template-parser.ts index d1f2c47bd..b7061a1a9 100644 --- a/common/template-parser.ts +++ b/common/template-parser.ts @@ -6,6 +6,43 @@ import peggy from 'peggy' import { elapsedSince } from './util' import { v4 } from 'uuid' +export type TemplateOpts = { + continue?: boolean + parts?: Partial + chat: AppSchema.Chat + + isPart?: boolean + isFinal?: boolean + + char: AppSchema.Character + replyAs?: AppSchema.Character + impersonate?: AppSchema.Character + sender: AppSchema.Profile + + lines?: string[] + characters?: Record + lastMessage?: string + + chatEmbed?: Memory.UserEmbed<{ name: string }>[] + userEmbed?: Memory.UserEmbed[] + + /** If present, history will be rendered last */ + limit?: { + context: number + encoder: TokenCounter + output?: Record + } + + /** + * Only allow repeatable placeholders. Excludes iterators, conditions, and prompt parts. + */ + repeatable?: boolean + inserts?: Map + lowpriority?: Array<{ id: string; content: string }> + + jsonValues: Record | undefined +} + const parser = loadParser() function loadParser() { @@ -62,6 +99,21 @@ type HolderDefinition = } | { value: Holder } +const SAFE_PART_HOLDERS: { [key in Holder | 'roll']?: boolean } = { + char: true, + user: true, + chat_age: true, + value: true, + idle_duration: true, + random: true, + roll: true, +} + +const FINAL_IGNORE_HOLDERS: { [key in Holder | 'roll']?: boolean } = { + system_prompt: true, + ujb: true, +} + type Holder = | 'char' | 'user' @@ -103,40 +155,6 @@ type ChatEmbedProp = 'i' | 'name' | 'text' type HistoryProp = 'i' | 'message' | 'dialogue' | 'name' | 'isuser' | 'isbot' type BotsProp = 'i' | 'personality' | 'name' -export type TemplateOpts = { - continue?: boolean - parts?: Partial - chat: AppSchema.Chat - - char: AppSchema.Character - replyAs?: AppSchema.Character - impersonate?: AppSchema.Character - sender: AppSchema.Profile - - lines?: string[] - characters?: Record - lastMessage?: string - - chatEmbed?: Memory.UserEmbed<{ name: string }>[] - userEmbed?: Memory.UserEmbed[] - - /** If present, history will be rendered last */ - limit?: { - context: number - encoder: TokenCounter - output?: Record - } - - /** - * Only allow repeatable placeholders. Excludes iterators, conditions, and prompt parts. - */ - repeatable?: boolean - inserts?: Map - lowpriority?: Array<{ id: string; content: string }> - - jsonValues: Record | undefined -} - /** * This function also returns inserts because Chat and Claude discard the * parsed string and use the inserts for their own prompt builders @@ -158,11 +176,11 @@ export async function parseTemplate( const parts = opts.parts || {} if (parts.systemPrompt) { - parts.systemPrompt = render(parts.systemPrompt, opts) + parts.systemPrompt = render(parts.systemPrompt, { ...opts, isPart: true }) } if (parts.ujb) { - parts.ujb = render(parts.ujb, opts) + parts.ujb = render(parts.ujb, { ...opts, isPart: true }) } const ast = parser.parse(template, {}) as PNode[] @@ -214,7 +232,10 @@ export async function parseTemplate( } } - const result = render(output, opts).replace(/\r\n/g, '\n').replace(/\n\n+/g, '\n\n').trim() + const result = render(output, { ...opts, isFinal: true }) + .replace(/\r\n/g, '\n') + .replace(/\n\n+/g, '\n\n') + .trim() return { parsed: result, inserts: opts.inserts ?? new Map(), @@ -450,7 +471,7 @@ function renderCondition( const output: string[] = [] for (const child of children) { if (typeof child !== 'string' && child.kind === 'else') continue - const result = renderNode(child, opts, value) + const result = renderNode(child, { ...opts, isPart: true }, value) if (result) output.push(result) } @@ -567,6 +588,14 @@ function getPlaceholder( return opts.jsonValues?.[name] || '' } + if (opts.isPart && !SAFE_PART_HOLDERS[node.value]) { + return `{{${node.value}}}` + } + + if (opts.isFinal && FINAL_IGNORE_HOLDERS[node.value]) { + return `{{${node.value}}}` + } + switch (node.value) { case 'value': return conditionText || '' diff --git a/srv/adapter/novel.ts b/srv/adapter/novel.ts index dac1b4a9f..9c12eaa0a 100644 --- a/srv/adapter/novel.ts +++ b/srv/adapter/novel.ts @@ -134,10 +134,15 @@ export const handleNovel: ModelAdapter = async function* (opts) { Authorization: `Bearer ${guest ? user.novelApiKey : decryptText(user.novelApiKey)}`, } + const maxTokens = await getMaxTokens(body.model, headers) + if (maxTokens) { + body.parameters.max_length = Math.min(body.parameters.max_length, maxTokens) + } + const stream = opts.kind !== 'summary' && opts.gen.streamResponse ? streamCompletion(headers, body, log) - : fullCompletition(headers, body, log) + : fullCompletion(headers, body, log) let accum = '' while (true) { @@ -198,11 +203,6 @@ function getModernParams(gen: Partial) { mirostat_lr: gen.mirostatLR, } - if (gen.cfgScale) { - payload.cfg_scale = gen.cfgScale - payload.cfg_uc = gen.cfgOppose || '' - } - return payload } @@ -252,7 +252,7 @@ const streamCompletion = async function* (headers: any, body: any, _log: AppLog) return { text: tokens.join('') } } -const fullCompletition = async function* (headers: any, body: any, log: AppLog) { +async function* fullCompletion(headers: any, body: any, log: AppLog) { const res = await needle('post', novelUrl(body.model), body, { json: true, // timeout: 2000, @@ -304,3 +304,24 @@ function getBaseUrl(model: string) { if (url.toLowerCase().startsWith('http')) return url return `https://${url}` } + +async function getMaxTokens(model: string, headers: any) { + try { + const config = await needle( + 'get', + 'https://api.novelai.net/user/subscription', + {}, + { json: true, headers, response_timeout: 5000 } + ) + + if (model !== 'llama-3-erato-v1' && model !== NOVEL_MODELS.kayra_v1) { + return + } + + const tier = config.body?.tier ?? 0 + if (tier !== 3) return 100 + return 150 + } catch (ex) { + return + } +} diff --git a/web/pages/Home/index.tsx b/web/pages/Home/index.tsx index 6ceae5899..f3268e9d1 100644 --- a/web/pages/Home/index.tsx +++ b/web/pages/Home/index.tsx @@ -21,7 +21,7 @@ import WizardIcon from '/web/icons/WizardIcon' import Slot from '/web/shared/Slot' import { adaptersToOptions } from '/common/adapters' import { useRef } from '/web/shared/hooks' -import { startTour } from '/web/tours' +import { canStartTour, startTour } from '/web/tours' const enum Sub { None, @@ -60,6 +60,8 @@ const HomePage: Component = () => { announceStore.getAll() emitter.on('loaded', () => { + if (!canStartTour('home')) return + settingStore.menu(true) startTour('home') }) }) diff --git a/web/pages/Memory/EditMemory.tsx b/web/pages/Memory/EditMemory.tsx index 199676ae6..d38666001 100644 --- a/web/pages/Memory/EditMemory.tsx +++ b/web/pages/Memory/EditMemory.tsx @@ -248,7 +248,10 @@ const EntryCard: Component<{ export function getBookUpdate(ref: Event | HTMLFormElement) { const inputs = getFormEntries(ref) - const { name, description } = getStrictForm(ref, { name: 'string', description: 'string?' }) + const { bookName = '', description } = getStrictForm(ref, { + bookName: 'string?', + description: 'string?', + }) const map = new Map() @@ -286,7 +289,7 @@ export function getBookUpdate(ref: Event | HTMLFormElement) { const entries = Array.from(map.values()) - const book = { name, description, entries } + const book = { name: bookName, description, entries } return book } diff --git a/web/store/settings.ts b/web/store/settings.ts index be03daf90..03183bd2d 100644 --- a/web/store/settings.ts +++ b/web/store/settings.ts @@ -59,7 +59,7 @@ const initState: SettingState = { guestAccessAllowed: canUseStorage(), initLoading: true, cfg: { loading: false, ttl: 0 }, - showMenu: true, + showMenu: false, showImpersonate: false, models: [], workers: [],