1
0
Fork 0

Adapt to polza.ai provider

This commit is contained in:
Pabloader 2026-11-02 20:50:35 +00:00
parent d5481e1a13
commit 97533daa91
7 changed files with 118 additions and 17 deletions

View File

@ -10,6 +10,7 @@
"@huggingface/jinja": "0.5.5", "@huggingface/jinja": "0.5.5",
"@huggingface/tokenizers": "0.1.1", "@huggingface/tokenizers": "0.1.1",
"@inquirer/select": "2.3.10", "@inquirer/select": "2.3.10",
"@ironarachne/made-up-names": "^3.0.0",
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"ace-builds": "1.36.3", "ace-builds": "1.36.3",
"clsx": "2.1.1", "clsx": "2.1.1",
@ -21,7 +22,7 @@
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"@types/html-minifier": "4.0.5", "@types/html-minifier": "4.0.5",
"@types/inquirer": "9.0.7", "@types/inquirer": "9.0.7",
"@types/web-bluetooth": "^0.0.21", "@types/web-bluetooth": "0.0.21",
"browser-detect": "0.2.28", "browser-detect": "0.2.28",
"eruda": "3.2.3", "eruda": "3.2.3",
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
@ -52,6 +53,12 @@
"@inquirer/type": ["@inquirer/type@1.4.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-AjOqykVyjdJQvtfkNDGUyMYGF8xN50VUxftCQWsOyIo4DFRLr6VQhW0VItGI1JIyQGCGgIpKa7hMMwNhZb4OIw=="], "@inquirer/type": ["@inquirer/type@1.4.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-AjOqykVyjdJQvtfkNDGUyMYGF8xN50VUxftCQWsOyIo4DFRLr6VQhW0VItGI1JIyQGCGgIpKa7hMMwNhZb4OIw=="],
"@ironarachne/made-up-names": ["@ironarachne/made-up-names@3.0.0", "", { "dependencies": { "@ironarachne/rng": "^3.0.1", "@ironarachne/word-generator": "^3.0.1" } }, "sha512-EYwGC1bi9r8/VVbeDgm6jzp8/igC0vs1C2KGK0LIrBcSWg10UNbd9ue6+pXrKQeuCPVdpph2I7SRAnFQ80vIFA=="],
"@ironarachne/rng": ["@ironarachne/rng@3.0.1", "", {}, "sha512-256DtrY7UDMnSp9jTz2aM0JvOuXExcGd/SZlZwSutKR0Aehcq42Q/rGj4SpxxrOu6INb/jeiB6w2eIxHNQEA8Q=="],
"@ironarachne/word-generator": ["@ironarachne/word-generator@3.0.1", "", { "dependencies": { "@ironarachne/rng": "^3.0.1" } }, "sha512-A5NurF35phpZENXDy+DWUSjnRG/o8nhs1p2JCd5tBy01Ka0t2ANpYJUnDwMNXuZT80F7sLtx2wr9Ekmgxpre+g=="],
"@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="], "@jimp/core": ["@jimp/core@1.6.1", "", { "dependencies": { "@jimp/file-ops": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^21.3.3", "mime": "3" } }, "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A=="],
"@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="], "@jimp/diff": ["@jimp/diff@1.6.1", "", { "dependencies": { "@jimp/plugin-resize": "1.6.1", "@jimp/types": "1.6.1", "@jimp/utils": "1.6.1", "pixelmatch": "^5.3.0" } }, "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ=="],

View File

@ -23,6 +23,7 @@
"@huggingface/jinja": "0.5.5", "@huggingface/jinja": "0.5.5",
"@huggingface/tokenizers": "0.1.1", "@huggingface/tokenizers": "0.1.1",
"@inquirer/select": "2.3.10", "@inquirer/select": "2.3.10",
"@ironarachne/made-up-names": "^3.0.0",
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
"ace-builds": "1.36.3", "ace-builds": "1.36.3",
"clsx": "2.1.1", "clsx": "2.1.1",

View File

@ -194,7 +194,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
const charName = currentWorld.title ?? 'Assistant'; const charName = currentWorld.title ?? 'Assistant';
const prefix = `${charName}: `; const prefix = `${charName}: `;
let accumulatedContent = continuedMessage?.content ?? ''; let accumulatedContent = continuedMessage?.content ?? '';
let accumulatedReasoning = continuedMessage?.role === 'assistant' ? continuedMessage.reasoning_content ?? '' : ''; let accumulatedReasoning = continuedMessage?.role === 'assistant'
? continuedMessage.reasoning_content ?? continuedMessage.reasoning ?? '' : '';
let tool_calls: LLM.ToolCall[] | undefined; let tool_calls: LLM.ToolCall[] | undefined;
for await (const chunk of LLM.generateStream(connection, request)) { for await (const chunk of LLM.generateStream(connection, request)) {
@ -207,7 +208,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
const delta = chunk.choices[0]?.delta; const delta = chunk.choices[0]?.delta;
if (delta?.tool_calls) { if (delta?.tool_calls) {
tool_calls = delta.tool_calls; tool_calls = Prompt.applyDelta(tool_calls, delta.tool_calls);
} }
const content = delta?.content; const content = delta?.content;
@ -217,7 +218,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
accumulatedContent = accumulatedContent.trimStart().slice(prefix.length).trimStart(); accumulatedContent = accumulatedContent.trimStart().slice(prefix.length).trimStart();
} }
} }
const reasoningContent = delta?.reasoning_content; const reasoningContent = delta?.reasoning_content ?? delta?.reasoning;
if (reasoningContent) { if (reasoningContent) {
accumulatedReasoning += reasoningContent; accumulatedReasoning += reasoningContent;
} }
@ -231,6 +232,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
role: targetRole, role: targetRole,
content: accumulatedContent, content: accumulatedContent,
reasoning_content: accumulatedReasoning, reasoning_content: accumulatedReasoning,
reasoning: accumulatedReasoning,
tool_calls, tool_calls,
}, },
}); });
@ -241,6 +243,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
role: targetRole, role: targetRole,
content: accumulatedContent, content: accumulatedContent,
reasoning_content: accumulatedReasoning, reasoning_content: accumulatedReasoning,
reasoning: accumulatedReasoning,
tool_calls, tool_calls,
}; };
dispatch({ dispatch({
@ -251,6 +254,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
}); });
if (tool_calls) { if (tool_calls) {
console.log(tool_calls);
const toolMessages: ChatMessage[] = []; const toolMessages: ChatMessage[] = [];
for (const tool of tool_calls) { for (const tool of tool_calls) {
if (abortControllerRef.current?.signal.aborted) { if (abortControllerRef.current?.signal.aborted) {

View File

@ -4,7 +4,7 @@ import { useInputState } from "@common/hooks/useInputState";
import { useUpdate } from "@common/hooks/useUpdate"; import { useUpdate } from "@common/hooks/useUpdate";
import { fuzzyMatch } from "@common/utils"; import { fuzzyMatch } from "@common/utils";
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useMemo, useRef } from "preact/hooks"; import { useCallback, useEffect, useMemo, useRef } from "preact/hooks";
import styles from "../../assets/settings-modal.module.css"; import styles from "../../assets/settings-modal.module.css";
import { useAppState } from "../../contexts/state"; import { useAppState } from "../../contexts/state";
import LLM from "../../utils/llm"; import LLM from "../../utils/llm";
@ -40,13 +40,15 @@ export const ConnectionSettings = () => {
const textModelsData = useMemo(() => modelsData?.filter(LLM.isTextModel), [modelsData]); const textModelsData = useMemo(() => modelsData?.filter(LLM.isTextModel), [modelsData]);
const imageModelsData = useMemo(() => modelsData?.filter(LLM.isImageModel), [modelsData]); const imageModelsData = useMemo(() => modelsData?.filter(LLM.isImageModel), [modelsData]);
const isLoadingModels = connectionToFetch != null && textModelsData == undefined; const isLoadingModels = connectionToFetch != null && textModelsData == undefined;
const param = useCallback((m: LLM.ModelInfoText, param: string) =>
(m.supported_parameters ?? m.top_provider?.supported_parameters)?.includes(param), []);
const [modelFilter, setModelFilter] = useInputState(""); const [modelFilter, setModelFilter] = useInputState("");
const groupedModels = useMemo(() => { const groupedModels = useMemo(() => {
const sorted = (textModelsData ?? []).sort((a, b) => { const sorted = (textModelsData ?? []).sort((a, b) => {
const aWeight = Number(a.supported_parameters.includes('tools')) * 2 + Number(a.supported_parameters.includes('reasoning')); const aWeight = Number(param(a, 'tools')) * 2 + Number(param(a, 'reasoning'));
const bWeight = Number(b.supported_parameters.includes('tools')) * 2 + Number(b.supported_parameters.includes('reasoning')); const bWeight = Number(param(b, 'tools')) * 2 + Number(param(b, 'reasoning'));
if (aWeight !== bWeight) return bWeight - aWeight; if (aWeight !== bWeight) return bWeight - aWeight;
if (a.context_length !== b.context_length) return b.context_length - a.context_length; if (a.context_length !== b.context_length) return b.context_length - a.context_length;
return a.id.localeCompare(b.id); return a.id.localeCompare(b.id);
@ -55,7 +57,7 @@ export const ConnectionSettings = () => {
return Array.from(groups.entries()) return Array.from(groups.entries())
.sort((a, b) => b[0] - a[0]) .sort((a, b) => b[0] - a[0])
.map(([context, models]) => ({ context, models })); .map(([context, models]) => ({ context, models }));
}, [textModelsData]); }, [textModelsData, param]);
const filteredGroupedModels = useMemo(() => { const filteredGroupedModels = useMemo(() => {
if (!modelFilter) return groupedModels; if (!modelFilter) return groupedModels;
@ -167,7 +169,7 @@ export const ConnectionSettings = () => {
<optgroup key={context} label={`${context} context`}> <optgroup key={context} label={`${context} context`}>
{models.map(m => ( {models.map(m => (
<option key={m.id} value={m.id}> <option key={m.id} value={m.id}>
{m.supported_parameters.includes('tools') ? '🔨' : ''}{m.supported_parameters.includes('reasoning') ? '🧠' : ''}{m.id} {m.top_provider.max_completion_tokens ? `(len: ${m.top_provider.max_completion_tokens})` : ''} {param(m, 'tools') ? '🔨' : ''}{param(m, 'reasoning') ? '🧠' : ''}{m.id} {m.top_provider.max_completion_tokens ? `(len: ${m.top_provider.max_completion_tokens})` : ''}
</option> </option>
))} ))}
</optgroup> </optgroup>

View File

@ -25,6 +25,7 @@ namespace LLM {
role: 'assistant'; role: 'assistant';
content: string; content: string;
reasoning_content?: string; reasoning_content?: string;
reasoning?: string;
tool_calls?: ToolCall[]; tool_calls?: ToolCall[];
} }
@ -101,6 +102,7 @@ namespace LLM {
role?: string; role?: string;
content?: string; content?: string;
reasoning_content?: string; reasoning_content?: string;
reasoning?: string;
tool_calls?: ToolCall[]; tool_calls?: ToolCall[];
}; };
finish_reason: 'stop' | 'tool_calls' | null; finish_reason: 'stop' | 'tool_calls' | null;
@ -168,7 +170,7 @@ namespace LLM {
object: 'model'; object: 'model';
created: number; created: number;
owned_by: string; owned_by: string;
supported_parameters: string[]; supported_parameters?: string[];
architecture?: { architecture?: {
input_modalities: Modality[]; input_modalities: Modality[];
output_modalities: Modality[]; output_modalities: Modality[];
@ -181,6 +183,7 @@ namespace LLM {
context_length: number; context_length: number;
max_completion_tokens: number; max_completion_tokens: number;
is_moderated: boolean; is_moderated: boolean;
supported_parameters?: string[];
}; };
} }
@ -232,7 +235,7 @@ namespace LLM {
}; };
const url = new URL(connection.url); const url = new URL(connection.url);
url.pathname = path; url.pathname = url.pathname.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
const response = await fetch(url, { const response = await fetch(url, {
method, method,
@ -256,7 +259,7 @@ namespace LLM {
}; };
const url = new URL(connection.url); const url = new URL(connection.url);
url.pathname = path; url.pathname = url.pathname.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');
using sse = new SSE(url.toString(), { using sse = new SSE(url.toString(), {
headers, headers,
@ -270,7 +273,7 @@ namespace LLM {
if (isMessageEvent(e)) { if (isMessageEvent(e)) {
if (e.data === '[DONE]') { if (e.data === '[DONE]') {
controller.close(); controller.close();
} else { } else if (e.data) {
controller.enqueue(e.data); controller.enqueue(e.data);
} }
} }
@ -310,7 +313,7 @@ namespace LLM {
} }
function isMessageEvent(e: unknown): e is { data: string } { function isMessageEvent(e: unknown): e is { data: string } {
return e != null && typeof e === 'object' && 'data' in e && typeof e.data === 'string'; return e != null && typeof e === 'object' && 'data' in e && typeof e.data === 'string' && Boolean(e.data);
} }
export async function getModels(connection: Connection): Promise<ModelsResponse<ModelInfo>> { export async function getModels(connection: Connection): Promise<ModelsResponse<ModelInfo>> {

View File

@ -365,11 +365,12 @@ namespace Prompt {
newMessages?: Iterable<ChatMessage>; newMessages?: Iterable<ChatMessage>;
excludedMessageIds?: Iterable<string>; excludedMessageIds?: Iterable<string>;
continueLast?: boolean; continueLast?: boolean;
keepThinking?: boolean;
} }
export function compilePrompt( export function compilePrompt(
state: AppState, state: AppState,
config: CompileConfig = {}, config: CompileConfig = { keepThinking: true },
): LLM.ChatCompletionRequest | null { ): LLM.ChatCompletionRequest | null {
const { currentStory, model, enableThinking, currentWorld } = state; const { currentStory, model, enableThinking, currentWorld } = state;
const { const {
@ -401,7 +402,13 @@ namespace Prompt {
const charName = currentWorld?.title || 'Assistant'; const charName = currentWorld?.title || 'Assistant';
const userName = state.userName || 'User'; const userName = state.userName || 'User';
const applyVars = (msgs: ChatMessage[]) => const applyVars = (msgs: ChatMessage[]) =>
msgs.map(m => ({ ...m, content: stripDataUrlImages(substituteVars(state, m.content)) })); msgs.map(m => ({
...m,
content: stripDataUrlImages(substituteVars(state, m.content)),
reasoning_content: config.keepThinking && ('reasoning_content' in m)
? m.reasoning_content
: undefined,
}));
// Chat-only world: format messages with name prefixes // Chat-only world: format messages with name prefixes
if (currentWorld?.chatOnly) { if (currentWorld?.chatOnly) {
@ -455,6 +462,27 @@ namespace Prompt {
...state.generationSettings, ...state.generationSettings,
}; };
} }
export function applyDelta(input: any, delta: any) {
if (Array.isArray(delta)) {
const result = [...(input ?? [])];
for (const item of delta) {
result[item.index] = applyDelta(result[item.index], item);
}
return result;
} else if (typeof delta === 'object') {
const result = { ...input };
for (const key of Object.keys(delta)) {
const value = delta[key];
result[key] = applyDelta(result[key], value);
}
return result;
} else if (typeof delta === 'string') {
return (input ?? '') + delta;
} else {
return delta;
}
}
} }
export default Prompt; export default Prompt;

View File

@ -2,6 +2,7 @@ import { formatErrorMessage } from "@common/errors";
import { Type, type Static, type TObject } from '@common/typebox'; import { Type, type Static, type TObject } from '@common/typebox';
import { CharacterRole, LocationScale, type AppState, type Character, type Location } from "../contexts/state"; import { CharacterRole, LocationScale, type AppState, type Character, type Location } from "../contexts/state";
import LLM from "./llm"; import LLM from "./llm";
import { getClassicRaceNamePatternSet, getNameGeneratorForPatternSet, getSupportedClassicRaceNamePatternSets, type NameGeneratorPatternSet } from "@ironarachne/made-up-names";
const VALID_SCALES = Object.values(LocationScale); const VALID_SCALES = Object.values(LocationScale);
const VALID_ROLES = Object.values(CharacterRole); const VALID_ROLES = Object.values(CharacterRole);
@ -328,6 +329,36 @@ export namespace Tools {
// Append mode: when old_text is not provided, append new_text // Append mode: when old_text is not provided, append new_text
if (args.old_text == null) { if (args.old_text == null) {
if (!isScratchpad) {
const isAppendToStory = (tc: LLM.ToolCall) => {
const jsonArgs = JSON.stringify(tc.function.arguments);
return tc.function.name === 'edit_text'
&& (
!jsonArgs.match(/\\?"old_text\\?"/)
|| jsonArgs.match(/\\?"old_text\\?":\\?"\\?"/)
)
&& (
!jsonArgs.match(/\\?"target\\?"/)
|| jsonArgs.match(/\\?"target\\?":\\?"story\\?"/)
);
}
// Check that there was a user message since the last edit_text append
const messages = appState.currentStory.chatMessages;
let hasUserMessageSinceLastAppend = true;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.role === 'user') break;
if (m.role === 'assistant' && m.tool_calls?.some(isAppendToStory)) {
hasUserMessageSinceLastAppend = false;
break;
}
}
if (!hasUserMessageSinceLastAppend) {
return `Error: You're not allowed to add more text now. Do not retry nor continue.`;
}
}
const lines = args.new_text.split(/\n+/ig).filter(l => l.trim()); const lines = args.new_text.split(/\n+/ig).filter(l => l.trim());
const numLines = lines.length; const numLines = lines.length;
@ -352,7 +383,7 @@ export namespace Tools {
? `Added text:\n${args.new_text.split('\n').filter(l => l.trim()).map(l => '> ' + l).join('\n')}\n\nNote: The rest was cropped due to ${LINES_LIMIT} lines limit!` ? `Added text:\n${args.new_text.split('\n').filter(l => l.trim()).map(l => '> ' + l).join('\n')}\n\nNote: The rest was cropped due to ${LINES_LIMIT} lines limit!`
: `Text appended to ${target} successfully.`; : `Text appended to ${target} successfully.`;
message += `\nNote: you can't continue nor add more text until user's approval. Stop any attempts and wait, even if you think the work is not finished.` message += `\nTool note: you're not allowed to add more text now. Do not retry nor continue.`
return message; return message;
} }
@ -477,6 +508,31 @@ export namespace Tools {
prompt: Type.String({ description: 'The image generation prompt' }), prompt: Type.String({ description: 'The image generation prompt' }),
}), }),
}), }),
'generate_name': tool({
handler: async (args) => {
const gen = getClassicRaceNamePatternSet(args.family);
const kind = gen[args.kind];
if (!kind) {
return `Error: unknown kind: '${args.kind}'`;
}
const subGen = getNameGeneratorForPatternSet(`${args.family}_${args.kind}`, gen[args.kind]);
const names = subGen.generate(args.amount ?? 1);
return names;
},
description: 'Generate a name. Use this tool when you need a new name, instead of inventing the name yourself.',
parameters: Type.Object({
family: Type.Enum(getSupportedClassicRaceNamePatternSets(), { description: 'Theme or race to which name belongs to' }),
kind: Type.Enum((() => {
type NameGenField = keyof Omit<NameGeneratorPatternSet, 'name'>;
const gen = getClassicRaceNamePatternSet('elf');
const keys = Object.keys(gen);
return keys.filter(k => k != 'name') as NameGenField[];
})(), { description: 'What kind of name to generate' }),
amount: Type.Optional(Type.Number({ description: "Specify the amount of names to generate. You can generate multiple names and select the most fitting. Default: 1" })),
}),
}),
}; };
export type ToolName = keyof typeof TOOLS; export type ToolName = keyof typeof TOOLS;