mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat: unify LLM streaming with thinking display and Web/cache API shims
- Extract runSimpleLlmStream to @openchatlab/node-runtime for shared Electron/Server streaming with thinking_start/delta/end event handling - Simplify Electron IPC ai.ts by delegating to shared stream function - Add thinking/thinkingDone fields to LlmStreamChunk and preload types - Add collapsible thinking process display in SQL LAB with auto-scroll - Implement Web chatStream via SSE with proper error propagation - Add /_web/cache/save-to-downloads and show-in-folder server routes - Add cacheApi shim in Web registry for server-side file operations - Increase SQL LAB maxTokens to 4096 to accommodate reasoning models - Detect finishReason:length and show token limit error message - Add i18n keys for thinking/thinkingProcess/errorTokenLimit (4 locales)
This commit is contained in:
1 parent
58a558b7aa
commit
cdb2bfea7c
14 files changed
+384
-82
No files matched your search
@@ -132,6 +132,10 @@ export type { RemoteModel, FetchRemoteModelsResult, RemoteApiOptions } from './r
|
||||
// Re-exports from @mariozechner/pi-agent-core
|
||||
export type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core'
|
||||
|
||||
// LLM simple streaming
|
||||
export { runSimpleLlmStream } from './llm-stream'
|
||||
export type { LlmStreamChunk, RunSimpleLlmStreamOptions } from './llm-stream'
|
||||
|
||||
// Re-exports from @mariozechner/pi-ai
|
||||
export { Type, completeSimple, streamSimple } from '@mariozechner/pi-ai'
|
||||
export type {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Shared LLM simple streaming — used by both Electron and Server.
|
||||
*
|
||||
* Wraps pi-ai's streamSimple with message preprocessing,
|
||||
* event mapping, and error handling.
|
||||
*/
|
||||
|
||||
import { streamSimple } from '@mariozechner/pi-ai'
|
||||
import type { Model as PiModel, Api as PiApi, Message as PiMessage } from '@mariozechner/pi-ai'
|
||||
|
||||
export interface LlmStreamChunk {
|
||||
content: string
|
||||
isFinished: boolean
|
||||
finishReason?: 'stop' | 'length' | 'error'
|
||||
error?: string
|
||||
/** When present, this chunk carries thinking/reasoning content rather than final text. */
|
||||
thinking?: string
|
||||
/** Signals the end of a thinking block (thinking is empty string, thinkingDone is true). */
|
||||
thinkingDone?: boolean
|
||||
}
|
||||
|
||||
export interface RunSimpleLlmStreamOptions {
|
||||
messages: Array<{ role: string; content: string }>
|
||||
apiKey: string
|
||||
piModel: PiModel<PiApi>
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
onChunk: (chunk: LlmStreamChunk) => void
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
function toPiMessages(messages: Array<{ role: string; content: string }>, timestamp: number): PiMessage[] {
|
||||
return messages.map((m) => ({
|
||||
role: m.role as 'user' | 'assistant',
|
||||
content: m.content,
|
||||
timestamp,
|
||||
})) as unknown as PiMessage[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a simple LLM streaming call.
|
||||
* Separates system message, streams text deltas via onChunk callback.
|
||||
*/
|
||||
export async function runSimpleLlmStream(options: RunSimpleLlmStreamOptions): Promise<void> {
|
||||
const { messages, apiKey, piModel, temperature, maxTokens, onChunk, abortSignal } = options
|
||||
|
||||
const systemMsg = messages.find((m) => m.role === 'system')
|
||||
const nonSystemMsgs = messages.filter((m) => m.role !== 'system')
|
||||
const now = Date.now()
|
||||
|
||||
const eventStream = streamSimple(
|
||||
piModel,
|
||||
{
|
||||
systemPrompt: systemMsg?.content,
|
||||
messages: toPiMessages(nonSystemMsgs, now),
|
||||
},
|
||||
{ apiKey, temperature, maxTokens, signal: abortSignal }
|
||||
)
|
||||
|
||||
let hasTerminalChunk = false
|
||||
|
||||
try {
|
||||
for await (const event of eventStream) {
|
||||
if (abortSignal?.aborted) {
|
||||
if (!hasTerminalChunk) {
|
||||
hasTerminalChunk = true
|
||||
onChunk({ content: '', isFinished: true, finishReason: 'stop' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'thinking_start') {
|
||||
onChunk({ content: '', isFinished: false, thinking: '' })
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'thinking_delta') {
|
||||
onChunk({ content: '', isFinished: false, thinking: event.delta })
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'thinking_end') {
|
||||
onChunk({ content: '', isFinished: false, thinking: '', thinkingDone: true })
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'text_delta') {
|
||||
onChunk({ content: event.delta, isFinished: false })
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'done') {
|
||||
hasTerminalChunk = true
|
||||
onChunk({ content: '', isFinished: true, finishReason: event.reason === 'length' ? 'length' : 'stop' })
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'error') {
|
||||
hasTerminalChunk = true
|
||||
const errorMsg =
|
||||
event.error?.content
|
||||
?.filter((c) => c.type === 'text')
|
||||
.map((c) => ('text' in c ? c.text : ''))
|
||||
.join('') || 'Unknown LLM error'
|
||||
onChunk({ content: '', isFinished: true, finishReason: 'error', error: errorMsg })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTerminalChunk) {
|
||||
onChunk({ content: '', isFinished: true, finishReason: 'stop' })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!hasTerminalChunk) {
|
||||
const msg = error instanceof Error ? error.message : String(error)
|
||||
onChunk({ content: '', isFinished: true, finishReason: 'error', error: msg })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,8 @@ export type { TempDbMeta, ExportedSession } from './merger/temp-db'
|
||||
|
||||
// Re-exports: @mariozechner/pi-agent-core & @mariozechner/pi-ai
|
||||
export type { AgentTool, AgentToolResult } from './ai'
|
||||
export { Type, completeSimple, streamSimple } from './ai'
|
||||
export { Type, completeSimple, streamSimple, runSimpleLlmStream } from './ai'
|
||||
export type { LlmStreamChunk, RunSimpleLlmStreamOptions } from './ai'
|
||||
export type { PiModel, PiApi, PiMessage, PiUsage, PiTextContent, PiAssistantMessage } from './ai'
|
||||
|
||||
// ==================== RAG ====================
|
||||
|
||||
Reference in new issue
Block a user