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:
Vikranth Kumar Bala
2026-09-08 00:02:53 +08:00
committed by GitHub
co-authored by twix03 RainbowBird
parent 66c6aa9b57
commit e293b71abc
17 changed files with 179 additions and 39 deletions
+7 -2
View File
@@ -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)
+7 -2
View File
@@ -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,
+11
View File
@@ -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
+6
View File
@@ -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,
+5
View File
@@ -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() })
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: {