Add gemini image response

This commit is contained in:
kwaroran
2025-03-13 14:18:05 +09:00
parent 8a782ab24f
commit 7d7cef4a69
10 changed files with 114 additions and 25 deletions

View File

@@ -1072,4 +1072,5 @@ export const languageEnglish = {
automaticCachePoint: "Automatic Cache Point", automaticCachePoint: "Automatic Cache Point",
experimentalChatCompression: "Experimental Chat Data Handling", experimentalChatCompression: "Experimental Chat Data Handling",
loadingChatData: "Loading Chat Data", loadingChatData: "Loading Chat Data",
outputImageModal: "Output Image Modal",
} }

View File

@@ -126,6 +126,8 @@
<Check bind:check={DBState.db.promptSettings.sendName} name={language.formatGroupInSingle} className="mt-4"/> <Check bind:check={DBState.db.promptSettings.sendName} name={language.formatGroupInSingle} className="mt-4"/>
<Check bind:check={DBState.db.promptSettings.utilOverride} name={language.utilOverride} className="mt-4"/> <Check bind:check={DBState.db.promptSettings.utilOverride} name={language.utilOverride} className="mt-4"/>
<Check bind:check={DBState.db.jsonSchemaEnabled} name={language.enableJsonSchema} className="mt-4"/> <Check bind:check={DBState.db.jsonSchemaEnabled} name={language.enableJsonSchema} className="mt-4"/>
<Check bind:check={DBState.db.outputImageModal} name={language.outputImageModal} className="mt-4"/>
<Check bind:check={DBState.db.strictJsonSchema} name={language.strictJsonSchema} className="mt-4"/> <Check bind:check={DBState.db.strictJsonSchema} name={language.strictJsonSchema} className="mt-4"/>
{#if DBState.db.showUnrecommended} {#if DBState.db.showUnrecommended}

View File

@@ -81,6 +81,14 @@
md += `> ${modals.length} non-text content(s) included\n` md += `> ${modals.length} non-text content(s) included\n`
} }
if(formated[i].thoughts && formated[i].thoughts.length > 0){
md += `> ${formated[i].thoughts.length} thought(s) included\n`
}
if(formated[i].cachePoint){
md += `> Cache point\n`
}
md += '```\n' + formated[i].content.replaceAll('```', '\\`\\`\\`') + '\n```\n' md += '```\n' + formated[i].content.replaceAll('```', '\\`\\`\\`') + '\n```\n'
} }
$doingChat = false $doingChat = false

View File

@@ -969,7 +969,7 @@ export const LLMModels: LLMModel[] = [
id: 'gemini-2.0-flash-exp', id: 'gemini-2.0-flash-exp',
provider: LLMProvider.GoogleCloud, provider: LLMProvider.GoogleCloud,
format: LLMFormat.GoogleCloud, format: LLMFormat.GoogleCloud,
flags: [LLMFlags.geminiBlockOff,LLMFlags.hasImageInput, LLMFlags.hasFirstSystemPrompt, LLMFlags.poolSupported, LLMFlags.hasAudioInput, LLMFlags.hasVideoInput, LLMFlags.hasStreaming, LLMFlags.requiresAlternateRole], flags: [LLMFlags.geminiBlockOff,LLMFlags.hasImageInput, LLMFlags.hasImageOutput, LLMFlags.poolSupported, LLMFlags.hasAudioInput, LLMFlags.hasVideoInput, LLMFlags.hasStreaming, LLMFlags.requiresAlternateRole],
parameters: ['temperature', 'top_k', 'top_p', 'presence_penalty', 'frequency_penalty'], parameters: ['temperature', 'top_k', 'top_p', 'presence_penalty', 'frequency_penalty'],
tokenizer: LLMTokenizer.GoogleCloud, tokenizer: LLMTokenizer.GoogleCloud,
}, },

View File

@@ -495,14 +495,14 @@ function trimmer(str:string){
} }
async function parseInlayAssets(data:string){ async function parseInlayAssets(data:string){
const inlayMatch = data.match(/{{(inlay|inlayed)::(.+?)}}/g) const inlayMatch = data.match(/{{(inlay|inlayed|inlayeddata)::(.+?)}}/g)
if(inlayMatch){ if(inlayMatch){
for(const inlay of inlayMatch){ for(const inlay of inlayMatch){
const inlayType = inlay.startsWith('{{inlayed') ? 'inlayed' : 'inlay' const inlayType = inlay.startsWith('{{inlayed') ? 'inlayed' : 'inlay'
const id = inlay.substring(inlay.indexOf('::') + 2, inlay.length - 2) const id = inlay.substring(inlay.indexOf('::') + 2, inlay.length - 2)
const asset = await getInlayAsset(id) const asset = await getInlayAsset(id)
let prefix = inlayType === 'inlayed' ? `<div class="risu-inlay-image">` : '' let prefix = inlayType !== 'inlay' ? `<div class="risu-inlay-image">` : ''
let postfix = inlayType === 'inlayed' ? `</div>\n\n` : '' let postfix = inlayType !== 'inlay' ? `</div>\n\n` : ''
switch(asset?.type){ switch(asset?.type){
case 'image': case 'image':
data = data.replace(inlay, `${prefix}<img src="${asset.data}"/>${postfix}`) data = data.replace(inlay, `${prefix}<img src="${asset.data}"/>${postfix}`)

View File

@@ -71,7 +71,7 @@ export async function postInlayAsset(img:{
return null return null
} }
export async function writeInlayImage(imgObj:HTMLImageElement, arg:{name?:string, ext?:string} = {}) { export async function writeInlayImage(imgObj:HTMLImageElement, arg:{name?:string, ext?:string, id?:string} = {}) {
let drawHeight = 0 let drawHeight = 0
let drawWidth = 0 let drawWidth = 0
@@ -103,7 +103,7 @@ export async function writeInlayImage(imgObj:HTMLImageElement, arg:{name?:string
const dataURI = canvas.toDataURL('image/png') const dataURI = canvas.toDataURL('image/png')
const imgid = v4() const imgid = arg.id ?? v4()
await inlayStorage.setItem(imgid, { await inlayStorage.setItem(imgid, {
name: arg.name ?? imgid, name: arg.name ?? imgid,
@@ -132,6 +132,17 @@ export async function getInlayAsset(id: string){
return img return img
} }
export async function setInlayAsset(id: string, img:{
name: string,
data: string,
ext: string,
height: number,
width: number,
type: 'image'|'video'|'audio'
}){
await inlayStorage.setItem(id, img)
}
export function supportsInlayImage(){ export function supportsInlayImage(){
const db = getDatabase() const db = getDatabase()
return getModelInfo(db.aiModel).flags.includes(LLMFlags.hasImageInput) return getModelInfo(db.aiModel).flags.includes(LLMFlags.hasImageInput)

View File

@@ -724,10 +724,19 @@ export async function sendChat(chatProcessIndex = -1,arg:{
} }
let inlays:string[] = [] let inlays:string[] = []
if(msg.role === 'char'){ if(msg.role === 'char'){
formatedChat = formatedChat.replace(/{{(inlay|inlayed)::(.+?)}}/g, '') formatedChat = formatedChat.replace(/{{(inlay|inlayed|inlayeddata)::(.+?)}}/g, (
match: string,
p1: string,
p2: string
) => {
if(p2 && p1 === 'inlayeddata'){
inlays.push(p2)
}
return ''
})
} }
else{ else{
const inlayMatch = formatedChat.match(/{{(inlay|inlayed)::(.+?)}}/g) const inlayMatch = formatedChat.match(/{{(inlay|inlayed|inlayeddata)::(.+?)}}/g)
if(inlayMatch){ if(inlayMatch){
for(const inlay of inlayMatch){ for(const inlay of inlayMatch){
inlays.push(inlay) inlays.push(inlay)
@@ -1293,7 +1302,8 @@ export async function sendChat(chatProcessIndex = -1,arg:{
isGroupChat: nowChatroom.type === 'group', isGroupChat: nowChatroom.type === 'group',
bias: {}, bias: {},
continue: arg.continue, continue: arg.continue,
chatId: generationId chatId: generationId,
imageResponse: DBState.db.outputImageModal
}, 'model', abortSignal) }, 'model', abortSignal)
let result = '' let result = ''

View File

@@ -11,7 +11,7 @@ import { risuChatParser } from "../parser.svelte";
import { SignatureV4 } from "@smithy/signature-v4"; import { SignatureV4 } from "@smithy/signature-v4";
import { HttpRequest } from "@smithy/protocol-http"; import { HttpRequest } from "@smithy/protocol-http";
import { Sha256 } from "@aws-crypto/sha256-js"; import { Sha256 } from "@aws-crypto/sha256-js";
import { supportsInlayImage } from "./files/inlays"; import { supportsInlayImage, writeInlayImage } from "./files/inlays";
import { Capacitor } from "@capacitor/core"; import { Capacitor } from "@capacitor/core";
import { getFreeOpenRouterModel } from "../model/openrouter"; import { getFreeOpenRouterModel } from "../model/openrouter";
import { runTransformers } from "./transformers"; import { runTransformers } from "./transformers";
@@ -42,6 +42,7 @@ interface requestDataArgument{
noMultiGen?:boolean noMultiGen?:boolean
schema?:string schema?:string
extractJson?:string extractJson?:string
imageResponse?:boolean
} }
interface RequestDataArgumentExtended extends requestDataArgument{ interface RequestDataArgumentExtended extends requestDataArgument{
@@ -374,13 +375,15 @@ export interface OpenAIChatExtra {
cachePoint?:boolean cachePoint?:boolean
} }
function reformater(formated:OpenAIChat[],modelInfo:LLMModel){ export function reformater(formated:OpenAIChat[],modelInfo:LLMModel|LLMFlags[]){
const flags = Array.isArray(modelInfo) ? modelInfo : modelInfo.flags
const db = getDatabase() const db = getDatabase()
let systemPrompt:OpenAIChat|null = null let systemPrompt:OpenAIChat|null = null
if(!modelInfo.flags.includes(LLMFlags.hasFullSystemPrompt)){ if(!flags.includes(LLMFlags.hasFullSystemPrompt)){
if(modelInfo.flags.includes(LLMFlags.hasFirstSystemPrompt)){ if(flags.includes(LLMFlags.hasFirstSystemPrompt)){
while(formated[0].role === 'system'){ while(formated[0].role === 'system'){
if(systemPrompt){ if(systemPrompt){
systemPrompt.content += '\n\n' + formated[0].content systemPrompt.content += '\n\n' + formated[0].content
@@ -400,7 +403,7 @@ function reformater(formated:OpenAIChat[],modelInfo:LLMModel){
} }
} }
if(modelInfo.flags.includes(LLMFlags.requiresAlternateRole)){ if(flags.includes(LLMFlags.requiresAlternateRole)){
let newFormated:OpenAIChat[] = [] let newFormated:OpenAIChat[] = []
for(let i=0;i<formated.length;i++){ for(let i=0;i<formated.length;i++){
const m = formated[i] const m = formated[i]
@@ -427,6 +430,12 @@ function reformater(formated:OpenAIChat[],modelInfo:LLMModel){
newFormated[newFormated.length-1].thoughts.push(...m.thoughts) newFormated[newFormated.length-1].thoughts.push(...m.thoughts)
} }
if(m.cachePoint){
if(!newFormated[newFormated.length-1].cachePoint){
newFormated[newFormated.length-1].cachePoint = true
}
}
continue continue
} }
else{ else{
@@ -436,7 +445,7 @@ function reformater(formated:OpenAIChat[],modelInfo:LLMModel){
formated = newFormated formated = newFormated
} }
if(modelInfo.flags.includes(LLMFlags.mustStartWithUserInput)){ if(flags.includes(LLMFlags.mustStartWithUserInput)){
if(formated.length === 0 || formated[0].role !== 'user'){ if(formated.length === 0 || formated[0].role !== 'user'){
formated.unshift({ formated.unshift({
role: 'user', role: 'user',
@@ -1804,7 +1813,7 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
const body = { const body = {
contents: reformatedChat, contents: reformatedChat,
generation_config: applyParameters({ generation_config: applyParameters({
"maxOutputTokens": maxTokens, "maxOutputTokens": maxTokens
}, para, { }, para, {
'top_p': "topP", 'top_p': "topP",
'top_k': "topK", 'top_k': "topK",
@@ -1823,6 +1832,16 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
}, },
} }
if(systemPrompt === ''){
delete body.systemInstruction
}
if(!arg.imageResponse){
body.generation_config.responseModalities = [
'TEXT', 'IMAGE'
]
}
let headers:{[key:string]:string} = {} let headers:{[key:string]:string} = {}
const PROJECT_ID=db.google.projectId const PROJECT_ID=db.google.projectId
@@ -2060,7 +2079,14 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
rDatas.push('') rDatas.push('')
} }
rDatas[rDatas.length-1] += part.text rDatas[rDatas.length-1] += part.text ?? ''
if(part.inlineData){
const imgHTML = new Image()
const id = crypto.randomUUID()
imgHTML.src = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
writeInlayImage(imgHTML)
rDatas[rDatas.length-1] += (`\n{{inlayeddata::${id}}}\n`)
}
} }
} }
@@ -2072,9 +2098,15 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
} }
if(rDatas.length > 1){ if(rDatas.length > 1){
const thought = rDatas.splice(rDatas.length-2, 1)[0] if(arg.modelInfo.flags.includes(LLMFlags.geminiThinking)){
rDatas[rDatas.length-1] = `<Thoughts>${thought}</Thoughts>\n\n${rDatas.join('\n\n')}` const thought = rDatas.splice(rDatas.length-2, 1)[0]
rDatas[rDatas.length-1] = `<Thoughts>${thought}</Thoughts>\n\n${rDatas.join('\n\n')}`
}
else{
rDatas[rDatas.length-1] = rDatas.join('\n\n')
}
} }
control.enqueue({ control.enqueue({
'0': rDatas[rDatas.length-1], '0': rDatas[rDatas.length-1],
}) })
@@ -2110,7 +2142,7 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
} }
let rDatas:string[] = [''] let rDatas:string[] = ['']
const processDataItem = (data:any) => { const processDataItem = async (data:any) => {
const parts = data?.candidates?.[0]?.content?.parts const parts = data?.candidates?.[0]?.content?.parts
if(parts){ if(parts){
@@ -2120,7 +2152,21 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
rDatas.push('') rDatas.push('')
} }
rDatas[rDatas.length-1] += part.text rDatas[rDatas.length-1] += part.text ?? ''
if(part.inlineData){
const imgHTML = new Image()
const id = crypto.randomUUID()
imgHTML.src = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
console.log('decoding', part.inlineData.mimeType, part.inlineData.data, id)
console.log('writing')
await writeInlayImage(imgHTML, {
id: id
})
console.log(JSON.stringify(rDatas))
rDatas[rDatas.length-1] += (`\n{{inlayeddata::${id}}}\n`)
console.log(JSON.stringify(rDatas))
console.log('done', id)
}
} }
} }
@@ -2141,10 +2187,10 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
// traverse responded data if it contains multipart contents // traverse responded data if it contains multipart contents
if (typeof (res.data)[Symbol.iterator] === 'function') { if (typeof (res.data)[Symbol.iterator] === 'function') {
for(const data of res.data){ for(const data of res.data){
processDataItem(data) await processDataItem(data)
} }
} else { } else {
processDataItem(res.data) await processDataItem(res.data)
} }
if(arg.extractJson && (db.jsonSchemaEnabled || arg.schema)){ if(arg.extractJson && (db.jsonSchemaEnabled || arg.schema)){
@@ -2154,10 +2200,13 @@ async function requestGoogleCloudVertex(arg:RequestDataArgumentExtended):Promise
} }
} }
if(rDatas.length > 1){ if(rDatas.length > 1 && arg.modelInfo.flags.includes(LLMFlags.geminiThinking)){
const thought = rDatas.splice(rDatas.length-2, 1)[0] const thought = rDatas.splice(rDatas.length-2, 1)[0]
rDatas[rDatas.length-1] = `<Thoughts>${thought}</Thoughts>\n\n${rDatas.join('\n\n')}` rDatas[rDatas.length-1] = `<Thoughts>${thought}</Thoughts>\n\n${rDatas.join('\n\n')}`
} }
else if(rDatas.length > 1){
rDatas[rDatas.length-1] = rDatas.join('\n\n')
}
return { return {
type: 'success', type: 'success',

View File

@@ -3,7 +3,7 @@ import { getDatabase, type character } from "../storage/database.svelte"
import { requestChatData } from "./request" import { requestChatData } from "./request"
import { alertError } from "../alert" import { alertError } from "../alert"
import { fetchNative, globalFetch, readImage } from "../globalApi.svelte" import { fetchNative, globalFetch, readImage } from "../globalApi.svelte"
import { CharEmotion } from "../stores.svelte" import { CharEmotion, DBState } from "../stores.svelte"
import type { OpenAIChat } from "./index.svelte" import type { OpenAIChat } from "./index.svelte"
import { processZip } from "./processzip" import { processZip } from "./processzip"
import { keiServerURL } from "../kei/kei" import { keiServerURL } from "../kei/kei"

View File

@@ -928,6 +928,8 @@ export interface Database{
automaticCachePoint: boolean automaticCachePoint: boolean
chatCompression: boolean chatCompression: boolean
claudeRetrivalCaching: boolean claudeRetrivalCaching: boolean
outputImageModal: boolean
} }
interface SeparateParameters{ interface SeparateParameters{
@@ -941,8 +943,11 @@ interface SeparateParameters{
presence_penalty?:number presence_penalty?:number
reasoning_effort?:number reasoning_effort?:number
thinking_tokens?:number thinking_tokens?:number
outputImageModal?:boolean
} }
type OutputModal = 'image'|'audio'|'video'
export interface customscript{ export interface customscript{
comment: string; comment: string;
in:string in:string
@@ -1258,6 +1263,7 @@ export interface botPreset{
regex?:customscript[] regex?:customscript[]
reasonEffort?:number reasonEffort?:number
thinkingTokens?:number thinkingTokens?:number
outputImageModal?:boolean
} }
@@ -1574,6 +1580,7 @@ export function saveCurrentPreset(){
image: pres?.[db.botPresetsId]?.image ?? '', image: pres?.[db.botPresetsId]?.image ?? '',
reasonEffort: db.reasoningEffort ?? 0, reasonEffort: db.reasoningEffort ?? 0,
thinkingTokens: db.thinkingTokens ?? null, thinkingTokens: db.thinkingTokens ?? null,
outputImageModal: db.outputImageModal ?? false
} }
db.botPresets = pres db.botPresets = pres
setDatabase(db) setDatabase(db)
@@ -1685,6 +1692,7 @@ export function setPreset(db:Database, newPres: botPreset){
db.presetRegex = newPres.regex ?? [] db.presetRegex = newPres.regex ?? []
db.reasoningEffort = newPres.reasonEffort ?? 0 db.reasoningEffort = newPres.reasonEffort ?? 0
db.thinkingTokens = newPres.thinkingTokens ?? null db.thinkingTokens = newPres.thinkingTokens ?? null
db.outputImageModal = newPres.outputImageModal ?? false
return db return db
} }