From e293b71abc327373ea2413a90259b30f58344bbe Mon Sep 17 00:00:00 2001 From: Vikranth Kumar Bala <75952358+VikranthBala@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:32:53 +0530 Subject: [PATCH] feat: add support for temperature, top_p to be configurable (#2200) Co-authored-by: twix03 Co-authored-by: RainbowBird --- apps/stage-pocket/src/pages/index.vue | 9 ++- apps/stage-web/src/pages/index.vue | 9 ++- .../characters/components/CharacterDialog.vue | 24 +++++-- .../src/runtime/chat-orchestrator-runtime.ts | 6 ++ .../core-agent/src/runtime/llm-service.ts | 2 + packages/core-agent/src/types/llm.ts | 11 ++++ packages/i18n/src/locales/en/settings.yaml | 4 ++ .../i18n/src/locales/zh-Hans/settings.yaml | 4 ++ .../providers/cloud/amazon-bedrock/index.ts | 1 + .../pages/settings/modules/consciousness.vue | 27 +++++++- packages/stage-ui/src/stores/chat.ts | 6 ++ .../src/stores/mods/api/context-bridge.ts | 4 +- .../src/stores/modules/consciousness.ts | 16 +++++ packages/stage-ui/src/types/character.ts | 5 ++ .../apps/api/src/routes/characters/schema.ts | 23 +++---- .../api/src/services/domain/characters.ts | 66 +++++++++++++++---- .../api/src/types/character-capability.ts | 1 + 17 files changed, 179 insertions(+), 39 deletions(-) diff --git a/apps/stage-pocket/src/pages/index.vue b/apps/stage-pocket/src/pages/index.vue index dee35258b..bb339c00f 100644 --- a/apps/stage-pocket/src/pages/index.vue +++ b/apps/stage-pocket/src/pages/index.vue @@ -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) diff --git a/apps/stage-web/src/pages/index.vue b/apps/stage-web/src/pages/index.vue index 30d88b1df..bc7d34af0 100644 --- a/apps/stage-web/src/pages/index.vue +++ b/apps/stage-web/src/pages/index.vue @@ -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) diff --git a/apps/stage-web/src/pages/settings/characters/components/CharacterDialog.vue b/apps/stage-web/src/pages/settings/characters/components/CharacterDialog.vue index 81dee6e31..04b5f1382 100644 --- a/apps/stage-web/src/pages/settings/characters/components/CharacterDialog.vue +++ b/apps/stage-web/src/pages/settings/characters/components/CharacterDialog.vue @@ -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" > + +
+ + +
diff --git a/packages/core-agent/src/runtime/chat-orchestrator-runtime.ts b/packages/core-agent/src/runtime/chat-orchestrator-runtime.ts index 20ef3a976..f3e73ec61 100644 --- a/packages/core-agent/src/runtime/chat-orchestrator-runtime.ts +++ b/packages/core-agent/src/runtime/chat-orchestrator-runtime.ts @@ -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) diff --git a/packages/core-agent/src/runtime/llm-service.ts b/packages/core-agent/src/runtime/llm-service.ts index b26a6c6e5..aef6e09c1 100644 --- a/packages/core-agent/src/runtime/llm-service.ts +++ b/packages/core-agent/src/runtime/llm-service.ts @@ -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, diff --git a/packages/core-agent/src/types/llm.ts b/packages/core-agent/src/types/llm.ts index 19ff76502..0e4c4b440 100644 --- a/packages/core-agent/src/types/llm.ts +++ b/packages/core-agent/src/types/llm.ts @@ -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 supportsTools?: boolean waitForTools?: boolean diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index 65902b8f6..4112e3f69 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -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: diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index acfaf13b4..3966436cd 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -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: diff --git a/packages/provider-inference/src/providers/cloud/amazon-bedrock/index.ts b/packages/provider-inference/src/providers/cloud/amazon-bedrock/index.ts index 7ebfe4b69..345a8a775 100644 --- a/packages/provider-inference/src/providers/cloud/amazon-bedrock/index.ts +++ b/packages/provider-inference/src/providers/cloud/amazon-bedrock/index.ts @@ -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) diff --git a/packages/stage-pages/src/pages/settings/modules/consciousness.vue b/packages/stage-pages/src/pages/settings/modules/consciousness.vue index 959395502..96562e1a8 100644 --- a/packages/stage-pages/src/pages/settings/modules/consciousness.vue +++ b/packages/stage-pages/src/pages/settings/modules/consciousness.vue @@ -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) {
+
+
+ + +
+
+
{ 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 () => { diff --git a/packages/stage-ui/src/stores/mods/api/context-bridge.ts b/packages/stage-ui/src/stores/mods/api/context-bridge.ts index 68d071bfd..bad481d07 100644 --- a/packages/stage-ui/src/stores/mods/api/context-bridge.ts +++ b/packages/stage-ui/src/stores/mods/api/context-bridge.ts @@ -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: { diff --git a/packages/stage-ui/src/stores/modules/consciousness.ts b/packages/stage-ui/src/stores/modules/consciousness.ts index 4aa02b275..dab4b494a 100644 --- a/packages/stage-ui/src/stores/modules/consciousness.ts +++ b/packages/stage-ui/src/stores/modules/consciousness.ts @@ -41,6 +41,18 @@ export const useConsciousnessStore = defineStore('consciousness', () => { return providersStore.modelLoadError[activeProvider.value] || null }) + const activeTemperature = useLocalStorageManualReset( + 'settings/consciousness/active-temperature', + 0.7, + persistenceOptions, + ) + + const activeTopP = useLocalStorageManualReset( + '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, diff --git a/packages/stage-ui/src/types/character.ts b/packages/stage-ui/src/types/character.ts index 1fb80db1d..bc36f4da8 100644 --- a/packages/stage-ui/src/types/character.ts +++ b/packages/stage-ui/src/types/character.ts @@ -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 --- diff --git a/server/apps/api/src/routes/characters/schema.ts b/server/apps/api/src/routes/characters/schema.ts index 071375f85..8387e0329 100644 --- a/server/apps/api/src/routes/characters/schema.ts +++ b/server/apps/api/src/routes/characters/schema.ts @@ -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, + }))), }) diff --git a/server/apps/api/src/services/domain/characters.ts b/server/apps/api/src/services/domain/characters.ts index d1f2555c0..7fc9f163f 100644 --- a/server/apps/api/src/services/domain/characters.ts +++ b/server/apps/api/src/services/domain/characters.ts @@ -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) { - // 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 }, diff --git a/server/apps/api/src/types/character-capability.ts b/server/apps/api/src/types/character-capability.ts index 94adf78cb..ef16d48c9 100644 --- a/server/apps/api/src/types/character-capability.ts +++ b/server/apps/api/src/types/character-capability.ts @@ -7,6 +7,7 @@ interface CharacterCapabilityBaseConfig { export interface CharacterCapabilityConfig extends CharacterCapabilityBaseConfig { llm: { temperature: number + topP?: number model: string } tts: {