Structured outputs for regular replies; streaming can be enabled/disabled

This commit is contained in:
2026-03-01 19:25:15 -08:00
parent 15cffb3b66
commit 907a7caec6
7 changed files with 218 additions and 18 deletions

View File

@@ -6,11 +6,15 @@ import { serializeMessageHistory } from '../util';
import { logError, logInfo } from '../../logging';
import { LLMConfig } from '../commands/types';
const USER_PROMPT = `Continue the following Discord conversation by completing the next message, playing the role of Hatsune Miku. The conversation must progress forward, and you must avoid repeating yourself.
const USER_PROMPT = `Complete the next message as Hatsune Miku. Return JSON with only the "content" field filled in.
Each message is represented as a line of JSON. Refer to other users by their "name" instead of their "author" field whenever possible.
Conversation (last line is yours to complete):
The conversation is as follows. The last line is the message you have to complete. Please ONLY return the string contents of the "content" field, that go in place of the ellipses. Do not include the enclosing quotation marks in your response.
`;
const USER_PROMPT_STREAMING = `Complete the next message as Hatsune Miku. Output ONLY the raw message content (no JSON, no quotes).
Conversation (last line is yours to complete):
`;
@@ -83,18 +87,35 @@ export class OpenAIProvider implements LLMProvider {
temperature: params?.temperature || 0.5,
top_p: params?.top_p || 0.9,
max_tokens: params?.max_new_tokens || 128,
response_format: {
type: 'json_schema',
json_schema: {
name: 'miku_message',
schema: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'The message content as Hatsune Miku',
},
},
required: ['content'],
additionalProperties: false,
},
},
},
});
let content = response.choices[0].message.content;
if (!content) {
throw new TypeError('OpenAI API returned no message.');
}
if (content.lastIndexOf('</think>') > -1) {
content = content.slice(content.lastIndexOf('</think>') + 8);
}
logInfo(`[openai] API response: ${content}`);
return content;
// Parse JSON and extract content field
const parsed = JSON.parse(content);
return parsed.content || '';
} catch (err) {
logError(`[openai] API Error: ` + err);
throw err;
@@ -134,7 +155,7 @@ export class OpenAIProvider implements LLMProvider {
model: this.model,
messages: [
{ role: 'system', content: sysprompt },
{ role: 'user', content: USER_PROMPT + messageHistoryTxt },
{ role: 'user', content: USER_PROMPT_STREAMING + messageHistoryTxt },
],
temperature: params?.temperature || 0.5,
top_p: params?.top_p || 0.9,
@@ -215,7 +236,29 @@ Return ONLY valid JSON, no other text.`;
temperature: params?.temperature || 0.7,
top_p: params?.top_p || 0.9,
max_tokens: params?.max_new_tokens || 256,
response_format: { type: 'json_object' },
response_format: {
type: 'json_schema',
json_schema: {
name: 'voice_message_response',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
description:
'Your spoken response as Miku (keep it concise, 1-3 sentences)',
},
instruct: {
type: 'string',
description:
'A one-sentence instruction describing the expression/tone to use',
},
},
required: ['message', 'instruct'],
additionalProperties: false,
},
},
},
});
let content = response.choices[0].message.content;