fix(inference-billing): fix inference billing when stream is true via API, add drag-and-drop functionality to deployed chat (#1606)

* fix(inference): fix inference billing when stream is true via API

* add drag-and-drop to deployed chat
This commit is contained in:
Waleed
2025-10-11 20:05:59 -07:00
committed by waleed
parent ee77dea2d6
commit 34fc8f84f5
4 changed files with 75 additions and 12 deletions

View File

@@ -123,8 +123,9 @@ export async function executeWorkflow(
workflowTriggerType?: 'api' | 'chat' // Which trigger block type to look for (default: 'api')
onStream?: (streamingExec: any) => Promise<void> // Callback for streaming agent responses
onBlockComplete?: (blockId: string, output: any) => Promise<void> // Callback when any block completes
skipLoggingComplete?: boolean // When true, skip calling loggingSession.safeComplete (for streaming)
}
): Promise<any> {
): Promise<ExecutionResult> {
const workflowId = workflow.id
const executionId = uuidv4()
@@ -378,13 +379,20 @@ export async function executeWorkflow(
.where(eq(userStats.userId, actorUserId))
}
await loggingSession.safeComplete({
endedAt: new Date().toISOString(),
totalDurationMs: totalDuration || 0,
finalOutput: result.output || {},
traceSpans: (traceSpans || []) as any,
workflowInput: processedInput,
})
if (!streamConfig?.skipLoggingComplete) {
await loggingSession.safeComplete({
endedAt: new Date().toISOString(),
totalDurationMs: totalDuration || 0,
finalOutput: result.output || {},
traceSpans: traceSpans || [],
workflowInput: processedInput,
})
} else {
result._streamingMetadata = {
loggingSession,
processedInput,
}
}
return result
} catch (error: any) {

View File

@@ -35,6 +35,8 @@ export const ChatInput: React.FC<{
const [inputValue, setInputValue] = useState('')
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([])
const [uploadErrors, setUploadErrors] = useState<string[]>([])
const [dragCounter, setDragCounter] = useState(0)
const isDragOver = dragCounter > 0
// Check if speech-to-text is available in the browser
const isSttAvailable =
@@ -234,11 +236,42 @@ export const ChatInput: React.FC<{
{/* Text Input Area with Controls */}
<motion.div
className='rounded-2xl border border-gray-200 bg-white shadow-sm md:rounded-3xl'
className={`rounded-2xl border shadow-sm transition-all duration-200 md:rounded-3xl ${
isDragOver
? 'border-purple-500 bg-purple-50/50 dark:border-purple-500 dark:bg-purple-950/20'
: 'border-gray-200 bg-white'
}`}
onClick={handleActivate}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
onDragEnter={(e) => {
e.preventDefault()
e.stopPropagation()
if (!isStreaming) {
setDragCounter((prev) => prev + 1)
}
}}
onDragOver={(e) => {
e.preventDefault()
e.stopPropagation()
if (!isStreaming) {
e.dataTransfer.dropEffect = 'copy'
}
}}
onDragLeave={(e) => {
e.preventDefault()
e.stopPropagation()
setDragCounter((prev) => Math.max(0, prev - 1))
}}
onDrop={(e) => {
e.preventDefault()
e.stopPropagation()
setDragCounter(0)
if (!isStreaming) {
handleFileSelect(e.dataTransfer.files)
}
}}
>
{/* File Previews */}
{attachedFiles.length > 0 && (
@@ -341,7 +374,7 @@ export const ChatInput: React.FC<{
value={inputValue}
onChange={handleInputChange}
className='flex w-full resize-none items-center overflow-hidden bg-transparent text-base outline-none placeholder:text-gray-400 md:font-[330]'
placeholder={isActive ? '' : ''}
placeholder={isDragOver ? 'Drop files here...' : isActive ? '' : ''}
rows={1}
style={{
minHeight: window.innerWidth >= 768 ? '24px' : '28px',
@@ -366,14 +399,14 @@ export const ChatInput: React.FC<{
className='-translate-y-1/2 absolute top-1/2 left-0 transform select-none text-base text-gray-400 md:hidden'
style={{ paddingTop: '3px', paddingBottom: '3px' }}
>
{PLACEHOLDER_MOBILE}
{isDragOver ? 'Drop files here...' : PLACEHOLDER_MOBILE}
</div>
{/* Desktop placeholder */}
<div
className='-translate-y-1/2 absolute top-1/2 left-0 hidden transform select-none font-[330] text-base text-gray-400 md:block'
style={{ paddingTop: '4px', paddingBottom: '4px' }}
>
{PLACEHOLDER_DESKTOP}
{isDragOver ? 'Drop files here...' : PLACEHOLDER_DESKTOP}
</div>
</>
)}

View File

@@ -186,6 +186,11 @@ export interface ExecutionResult {
error?: string // Error message if execution failed
logs?: BlockLog[] // Execution logs for all blocks
metadata?: ExecutionMetadata
_streamingMetadata?: {
// Internal metadata for streaming execution
loggingSession: any
processedInput: any
}
}
/**

View File

@@ -118,6 +118,7 @@ export async function createStreamingResponse(
workflowTriggerType: streamConfig.workflowTriggerType,
onStream: onStreamCallback,
onBlockComplete: onBlockCompleteCallback,
skipLoggingComplete: true, // We'll complete logging after tokenization
})
if (result.logs && streamedContent.size > 0) {
@@ -135,6 +136,22 @@ export async function createStreamingResponse(
processStreamingBlockLogs(result.logs, streamedContent)
}
// Complete the logging session with updated trace spans that include cost data
if (result._streamingMetadata?.loggingSession) {
const { buildTraceSpans } = await import('@/lib/logs/execution/trace-spans/trace-spans')
const { traceSpans, totalDuration } = buildTraceSpans(result)
await result._streamingMetadata.loggingSession.safeComplete({
endedAt: new Date().toISOString(),
totalDurationMs: totalDuration || 0,
finalOutput: result.output || {},
traceSpans: (traceSpans || []) as any,
workflowInput: result._streamingMetadata.processedInput,
})
result._streamingMetadata = undefined
}
// Create a minimal result with only selected outputs
const minimalResult = {
success: result.success,