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: {