feat: add support for temperature, top_p to be configurable (#2200)
Co-authored-by: twix03 <sathwikbalaa@gmail.com> Co-authored-by: RainbowBird <git@luoling.moe>
This commit is contained in:
co-authored by
twix03
RainbowBird
parent
66c6aa9b57
commit
e293b71abc
@@ -57,7 +57,7 @@ const hearingPipeline = useHearingSpeechInputPipeline()
|
||||
const { removeStreamingTranscriptionConsumer, transcribeForRecording, transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline
|
||||
const { supportsStreamInput } = storeToRefs(hearingPipeline)
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel, activeTemperature, activeTopP } = storeToRefs(consciousnessStore)
|
||||
const chatStore = useChatStore()
|
||||
|
||||
/** Identifies this page in the shared streaming transcription session. */
|
||||
@@ -91,7 +91,12 @@ async function sendVoiceInputTextToChat(text: string | undefined) {
|
||||
|
||||
const provider = await consciousnessStore.getChatProviderInstance(providerId)
|
||||
|
||||
await chatStore.ingest(text, { model, chatProvider: provider })
|
||||
await chatStore.ingest(text, {
|
||||
model,
|
||||
chatProvider: provider,
|
||||
temperature: activeTemperature.value,
|
||||
topP: activeTopP.value,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to send chat from voice:', error)
|
||||
|
||||
@@ -59,7 +59,7 @@ const hearingPipeline = useHearingSpeechInputPipeline()
|
||||
const { removeStreamingTranscriptionConsumer, stopStreamingTranscription, transcribeForMediaStream, transcribeForRecording } = hearingPipeline
|
||||
const { supportsStreamInput } = storeToRefs(hearingPipeline)
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
|
||||
const { activeProvider: activeChatProvider, activeModel: activeChatModel, activeTemperature, activeTopP } = storeToRefs(consciousnessStore)
|
||||
const chatStore = useChatStore()
|
||||
|
||||
/** Identifies this page in the shared streaming transcription session. */
|
||||
@@ -93,7 +93,12 @@ async function sendVoiceInputTextToChat(text: string | undefined) {
|
||||
|
||||
const provider = await consciousnessStore.getChatProviderInstance(providerId)
|
||||
|
||||
await chatStore.ingest(text, { model, chatProvider: provider })
|
||||
await chatStore.ingest(text, {
|
||||
model,
|
||||
chatProvider: provider,
|
||||
temperature: activeTemperature.value,
|
||||
topP: activeTopP.value,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to send chat from voice:', error)
|
||||
|
||||
@@ -40,6 +40,7 @@ const form = reactive({
|
||||
// Capability: LLM
|
||||
llmModel: '',
|
||||
llmTemperature: 0.7,
|
||||
llmTopP: 1.0,
|
||||
|
||||
// Capability: TTS
|
||||
ttsVoiceId: '',
|
||||
@@ -61,6 +62,7 @@ watch(() => props.character, (char) => {
|
||||
|
||||
form.llmModel = llm?.config.llm?.model || ''
|
||||
form.llmTemperature = llm?.config.llm?.temperature || 0.7
|
||||
form.llmTopP = llm?.config.llm?.topP || 1.0
|
||||
|
||||
form.ttsVoiceId = tts?.config.tts?.voiceId || ''
|
||||
form.ttsSpeed = tts?.config.tts?.speed || 1.0
|
||||
@@ -74,6 +76,7 @@ watch(() => props.character, (char) => {
|
||||
form.description = ''
|
||||
form.llmModel = 'gpt-4o-mini'
|
||||
form.llmTemperature = 0.7
|
||||
form.llmTopP = 1.0
|
||||
form.ttsVoiceId = ''
|
||||
form.ttsSpeed = 1.0
|
||||
}
|
||||
@@ -108,6 +111,7 @@ async function handleSubmit() {
|
||||
llm: {
|
||||
model: form.llmModel,
|
||||
temperature: form.llmTemperature,
|
||||
topP: form.llmTopP,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -153,15 +157,9 @@ async function handleSubmit() {
|
||||
characterId: form.characterId,
|
||||
version: form.version,
|
||||
coverUrl: form.coverUrl,
|
||||
capabilities: payload.capabilities,
|
||||
})
|
||||
trackCharacterUpdated({ character_id: props.character.id })
|
||||
// Capabilities/I18n update not supported in simple UpdateCharacterSchema yet?
|
||||
// Checking types/character.ts: UpdateCharacterSchema only has version, coverUrl, characterId.
|
||||
// So deep update is not supported by the simple endpoint yet?
|
||||
// The plan said "update(id, payload)".
|
||||
// The backend `update` endpoint only updates the `character` table fields.
|
||||
// To update relations, we'd need specific endpoints or a smarter update endpoint.
|
||||
// I will only update basic info for now.
|
||||
}
|
||||
else {
|
||||
await characterStore.create(payload)
|
||||
@@ -270,6 +268,18 @@ const isOpen = computed({
|
||||
class="w-full border border-neutral-200 rounded-lg bg-white px-3 py-2 text-sm outline-none dark:border-neutral-700 focus:border-primary-500 dark:bg-neutral-800 focus:ring-2 focus:ring-primary-500/20"
|
||||
>
|
||||
</div>
|
||||
<!-- Use number input for top_p properly -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-sm text-neutral-700 font-medium dark:text-neutral-300">Top P</label>
|
||||
<input
|
||||
v-model.number="form.llmTopP"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
class="w-full border border-neutral-200 rounded-lg bg-white px-3 py-2 text-sm outline-none dark:border-neutral-700 focus:border-primary-500 dark:bg-neutral-800 focus:ring-2 focus:ring-primary-500/20"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
@@ -62,6 +62,10 @@ export interface ChatOrchestratorSendOptions {
|
||||
toolReferences?: ChatToolReference[]
|
||||
/** Original transport input metadata used by bridge/devtools observers. */
|
||||
input?: ChatStreamEventContext['input']
|
||||
/** Temperature for the LLM request. */
|
||||
temperature?: number
|
||||
/** Top_p for the LLM request. */
|
||||
topP?: number
|
||||
}
|
||||
|
||||
interface QueuedSend {
|
||||
@@ -766,6 +770,8 @@ export function createChatOrchestratorRuntime(deps: ChatOrchestratorRuntimeDeps)
|
||||
roundId: correlation.roundId,
|
||||
},
|
||||
tools: options.tools,
|
||||
temperature: options.temperature,
|
||||
topP: options.topP,
|
||||
waitForTools: true,
|
||||
onMessages: (messages) => {
|
||||
const currentTurnMessages = messages.slice(providerInputMessageCount)
|
||||
|
||||
@@ -198,6 +198,8 @@ export async function streamFrom({
|
||||
messages: sanitized,
|
||||
headers: options?.headers,
|
||||
streamOptions: { includeUsage: true },
|
||||
temperature: options?.temperature,
|
||||
topP: options?.topP,
|
||||
stopWhen: stepCountAtLeast(10),
|
||||
tools,
|
||||
toolChoice: options?.toolChoice,
|
||||
|
||||
@@ -34,6 +34,17 @@ export interface StreamOptions {
|
||||
conversationId: string
|
||||
roundId: string
|
||||
}
|
||||
/**
|
||||
* The temperature parameter controls the randomness of the model's output.
|
||||
* A lower value results in more deterministic and focused output, while a
|
||||
* higher value increases creativity and diversity.
|
||||
*/
|
||||
temperature?: number
|
||||
/**
|
||||
* The top_p parameter controls nucleus sampling. A value of 0.1 means only
|
||||
* the tokens comprising the top 10% probability mass are considered.
|
||||
*/
|
||||
topP?: number
|
||||
toolsCompatibility?: Map<string, boolean>
|
||||
supportsTools?: boolean
|
||||
waitForTools?: boolean
|
||||
|
||||
@@ -685,6 +685,10 @@ pages:
|
||||
search_results: Found {count} of {total} models
|
||||
show_less: Show less
|
||||
show_more: Show more
|
||||
temperature_label: Temperature
|
||||
temperature_description: Control the randomness of the model's output. A lower value (e.g. 0.2) results in more deterministic, precise, and analytical output, while a higher value (e.g. 1.0) increases creativity and diversity.
|
||||
top_p_label: Top P
|
||||
top_p_description: Control the diversity of the model's output via nucleus sampling. A lower value (e.g. 0.2) limits the output to only high-probability tokens, while a higher value (e.g. 1.0) includes a broader set of possibilities.
|
||||
subtitle: Select a default model from the provider
|
||||
title: Model
|
||||
model-options:
|
||||
|
||||
@@ -660,6 +660,10 @@ pages:
|
||||
search_results: 找到 {count} / {total} 个模型
|
||||
show_less: 显示更多
|
||||
show_more: 收起
|
||||
temperature_label: 温度
|
||||
temperature_description: 控制模型输出的随机性。较低的温度(例如 0.2)会使输出更精确和确定;较高的温度(例如 1.0)会增加创造力和多样性。
|
||||
top_p_label: Top P
|
||||
top_p_description: 控制模型输出的多样性(核采样)。较低的值(例如 0.2)会将输出限制在概率较高的词汇;较高的值(例如 1.0)则会包含更广泛的可能性。
|
||||
subtitle: 选择一个默认模型
|
||||
title: 模型
|
||||
model-options:
|
||||
|
||||
@@ -108,6 +108,7 @@ function createBedrockConverseProvider(config: {
|
||||
inferenceConfig: {
|
||||
maxTokens: body.max_tokens || 4096,
|
||||
...(body.temperature !== undefined && { temperature: body.temperature }),
|
||||
...(body.top_p !== undefined && { topP: body.top_p }),
|
||||
},
|
||||
}
|
||||
if (system)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consci
|
||||
import { useConsciousnessSettingsStore } from '@proj-airi/stage-ui/stores/modules/consciousness-settings'
|
||||
import { useProviderConfigStore } from '@proj-airi/stage-ui/stores/providers/config'
|
||||
import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider'
|
||||
import { FieldCheckbox } from '@proj-airi/ui'
|
||||
import { FieldCheckbox, FieldRange } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -29,6 +29,8 @@ const {
|
||||
providerModels,
|
||||
isLoadingActiveProviderModels,
|
||||
activeProviderModelError,
|
||||
activeTemperature,
|
||||
activeTopP,
|
||||
} = storeToRefs(consciousnessStore)
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -304,6 +306,29 @@ async function updateReasoning(value: boolean) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-if="activeProvider" :class="['bg-neutral-50 dark:bg-[rgba(0,0,0,0.3)]', 'rounded-xl', 'p-4', 'flex flex-col gap-4', 'mt-4']">
|
||||
<div :class="['flex flex-col gap-4']">
|
||||
<FieldRange
|
||||
v-model="activeTemperature"
|
||||
:label="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.temperature_label')"
|
||||
:description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.temperature_description')"
|
||||
:min="0"
|
||||
:max="2"
|
||||
:step="0.1"
|
||||
:format-value="value => value.toFixed(1)"
|
||||
/>
|
||||
<FieldRange
|
||||
v-model="activeTopP"
|
||||
:label="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.top_p_label')"
|
||||
:description="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.top_p_description')"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
:format-value="value => value.toFixed(1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
text="neutral-200/50 dark:neutral-600/20" pointer-events-none
|
||||
|
||||
@@ -58,6 +58,10 @@ export interface ChatSendPayload {
|
||||
text: string
|
||||
/** Request-specific tools selected by their model-facing names. */
|
||||
tools?: ChatToolReference[]
|
||||
/** Request-specific temperature override. */
|
||||
temperature?: number
|
||||
/** Request-specific top_p override. */
|
||||
topP?: number
|
||||
}
|
||||
|
||||
/** The durable messages appended while one chat request executes. */
|
||||
@@ -401,6 +405,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
attachments: payload.attachments,
|
||||
input: payload.input,
|
||||
toolReferences: payload.tools,
|
||||
temperature: payload.temperature ?? consciousnessStore.activeTemperature,
|
||||
topP: payload.topP ?? consciousnessStore.activeTopP,
|
||||
// Resolve this function after the request reaches the per-session queue.
|
||||
// The history then contains tool names from every earlier queued turn.
|
||||
tools: async () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
const contextObservability = useContextObservabilityStore()
|
||||
const characterOrchestratorStore = useCharacterOrchestratorStore()
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const { activeProvider, activeModel } = storeToRefs(consciousnessStore)
|
||||
const { activeProvider, activeModel, activeTemperature, activeTopP } = storeToRefs(consciousnessStore)
|
||||
const streamingControl = useLlmStreamingControlStore()
|
||||
|
||||
type SparkNotifyBridgeMessage
|
||||
@@ -724,6 +724,8 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
await chatOrchestrator.ingest(messageText, {
|
||||
model: activeModel.value,
|
||||
chatProvider,
|
||||
temperature: activeTemperature.value,
|
||||
topP: activeTopP.value,
|
||||
input: {
|
||||
type: 'input:text',
|
||||
data: {
|
||||
|
||||
@@ -41,6 +41,18 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
|
||||
return providersStore.modelLoadError[activeProvider.value] || null
|
||||
})
|
||||
|
||||
const activeTemperature = useLocalStorageManualReset<number>(
|
||||
'settings/consciousness/active-temperature',
|
||||
0.7,
|
||||
persistenceOptions,
|
||||
)
|
||||
|
||||
const activeTopP = useLocalStorageManualReset<number>(
|
||||
'settings/consciousness/active-top-p',
|
||||
1.0,
|
||||
persistenceOptions,
|
||||
)
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
if (!modelSearchQuery.value.trim()) {
|
||||
return providerModels.value
|
||||
@@ -109,6 +121,8 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
|
||||
function resetState() {
|
||||
activeProvider.reset()
|
||||
resetModelSelection()
|
||||
activeTemperature.reset()
|
||||
activeTopP.reset()
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -116,6 +130,8 @@ export const useConsciousnessStore = defineStore('consciousness', () => {
|
||||
configured,
|
||||
activeProvider,
|
||||
activeModel,
|
||||
activeTemperature,
|
||||
activeTopP,
|
||||
customModelName: activeCustomModelName,
|
||||
expandedDescriptions,
|
||||
modelSearchQuery,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const CharacterCapabilityConfigSchema = object({
|
||||
apiBaseUrl: string(),
|
||||
llm: optional(object({
|
||||
temperature: number(),
|
||||
topP: optional(number()),
|
||||
model: string(),
|
||||
})),
|
||||
tts: optional(object({
|
||||
@@ -171,6 +172,10 @@ export const UpdateCharacterSchema = object({
|
||||
version: optional(string()),
|
||||
coverUrl: optional(string()),
|
||||
characterId: optional(string()),
|
||||
capabilities: optional(array(object({
|
||||
type: CharacterCapabilityTypeSchema,
|
||||
config: CharacterCapabilityConfigSchema,
|
||||
}))),
|
||||
})
|
||||
|
||||
// --- Type Exports ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-valibot'
|
||||
import { array, literal, number, object, optional, pipe, string, transform, union } from 'valibot'
|
||||
import { array, literal, number, object, optional, string, union } from 'valibot'
|
||||
|
||||
import * as schema from '../../schemas/characters'
|
||||
|
||||
@@ -17,6 +17,7 @@ export const CharacterCapabilityConfigSchema = object({
|
||||
apiBaseUrl: string(),
|
||||
llm: optional(object({
|
||||
temperature: number(),
|
||||
topP: optional(number()),
|
||||
model: string(),
|
||||
})),
|
||||
tts: optional(object({
|
||||
@@ -63,11 +64,6 @@ export const InsertCharacterCoverSchema = createInsertSchema(schema.characterCov
|
||||
export const CharacterPromptSchema = createSelectSchema(schema.characterPrompts)
|
||||
export const InsertCharacterPromptSchema = createInsertSchema(schema.characterPrompts)
|
||||
|
||||
const DateSchema = pipe(
|
||||
string(),
|
||||
transform(v => new Date(v)),
|
||||
)
|
||||
|
||||
export const CreateCharacterSchema = object({
|
||||
// TODO: Replace createInsertSchema-derived request bodies with explicit HTTP DTO schemas.
|
||||
// The current shape still leaks persistence fields such as ownerId/creatorId into the API boundary.
|
||||
@@ -102,16 +98,13 @@ export const CreateCharacterSchema = object({
|
||||
|
||||
// TODO: Split update request schema from DB insert schema.
|
||||
// This route should reject server-managed fields like id/ownerId/creatorId/timestamps instead of allowing them here.
|
||||
export const UpdateCharacterSchema = createInsertSchema(schema.character, {
|
||||
id: optional(string()),
|
||||
export const UpdateCharacterSchema = object({
|
||||
version: optional(string()),
|
||||
coverUrl: optional(string()),
|
||||
avatarUrl: optional(string()),
|
||||
creatorRole: optional(string()),
|
||||
priceCredit: optional(string()),
|
||||
creatorId: optional(string()),
|
||||
ownerId: optional(string()),
|
||||
characterId: optional(string()),
|
||||
createdAt: optional(DateSchema),
|
||||
updatedAt: optional(DateSchema),
|
||||
capabilities: optional(array(createInsertSchema(schema.characterCapabilities, {
|
||||
characterId: optional(string()),
|
||||
type: CharacterCapabilityTypeSchema,
|
||||
config: CharacterCapabilityConfigSchema,
|
||||
}))),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Database } from '../../libs/db'
|
||||
import type { EngagementMetrics } from '../../otel'
|
||||
|
||||
import { useLogger } from '@guiiai/logg'
|
||||
import { and, eq, isNull, or, sql } from 'drizzle-orm'
|
||||
import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm'
|
||||
|
||||
import * as schema from '../../schemas/characters'
|
||||
import * as userCharacterSchema from '../../schemas/user-character'
|
||||
@@ -190,16 +190,60 @@ export function createCharacterService(db: Database, metrics?: EngagementMetrics
|
||||
return inserted
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<schema.NewCharacter>) {
|
||||
// TODO: Return a stable single-object response shape for HTTP callers.
|
||||
// leaking Drizzle returning() arrays across the service boundary makes route contracts drift.
|
||||
const result = await db.update(schema.character)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(schema.character.id, id),
|
||||
isNull(schema.character.deletedAt),
|
||||
))
|
||||
.returning()
|
||||
async update(id: string, data: {
|
||||
version?: string
|
||||
coverUrl?: string
|
||||
characterId?: string
|
||||
capabilities?: {
|
||||
type: 'llm' | 'tts' | 'vlm' | 'asr'
|
||||
config: any
|
||||
}[]
|
||||
}) {
|
||||
const { capabilities, ...characterData } = data
|
||||
|
||||
const result = await db.transaction(async (tx) => {
|
||||
let updatedChar
|
||||
if (Object.keys(characterData).length > 0) {
|
||||
const [res] = await tx.update(schema.character)
|
||||
.set({ ...characterData, updatedAt: new Date() })
|
||||
.where(and(
|
||||
eq(schema.character.id, id),
|
||||
isNull(schema.character.deletedAt),
|
||||
))
|
||||
.returning()
|
||||
updatedChar = res
|
||||
}
|
||||
|
||||
if (capabilities) {
|
||||
const submittedTypes = capabilities.map(c => c.type)
|
||||
if (submittedTypes.length > 0) {
|
||||
await tx.delete(schema.characterCapabilities)
|
||||
.where(and(
|
||||
eq(schema.characterCapabilities.characterId, id),
|
||||
inArray(schema.characterCapabilities.type, submittedTypes),
|
||||
))
|
||||
|
||||
await tx.insert(schema.characterCapabilities).values(
|
||||
capabilities.map(c => ({ ...c, characterId: id })),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedChar) {
|
||||
return updatedChar
|
||||
}
|
||||
|
||||
const fallback = await tx.query.character.findFirst({
|
||||
where: and(
|
||||
eq(schema.character.id, id),
|
||||
isNull(schema.character.deletedAt),
|
||||
),
|
||||
})
|
||||
if (!fallback)
|
||||
throw new Error('Character not found')
|
||||
return fallback
|
||||
})
|
||||
|
||||
logger.withFields({ id }).log('Updated character')
|
||||
return result
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ interface CharacterCapabilityBaseConfig {
|
||||
export interface CharacterCapabilityConfig extends CharacterCapabilityBaseConfig {
|
||||
llm: {
|
||||
temperature: number
|
||||
topP?: number
|
||||
model: string
|
||||
}
|
||||
tts: {
|
||||
|
||||
Reference in New Issue
Block a user