| import { type ActionFunctionArgs } from '@remix-run/cloudflare'; | |
| import { createDataStream } from 'ai'; | |
| import { MAX_RESPONSE_SEGMENTS, MAX_TOKENS } from '~/lib/.server/llm/constants'; | |
| import { CONTINUE_PROMPT } from '~/lib/common/prompts/prompts'; | |
| import { streamText, type Messages, type StreamingOptions } from '~/lib/.server/llm/stream-text'; | |
| import SwitchableStream from '~/lib/.server/llm/switchable-stream'; | |
| import type { IProviderSetting } from '~/types/model'; | |
| export async function action(args: ActionFunctionArgs) { | |
| return chatAction(args); | |
| } | |
| function parseCookies(cookieHeader: string): Record<string, string> { | |
| const cookies: Record<string, string> = {}; | |
| const items = cookieHeader.split(';').map((cookie) => cookie.trim()); | |
| items.forEach((item) => { | |
| const [name, ...rest] = item.split('='); | |
| if (name && rest) { | |
| const decodedName = decodeURIComponent(name.trim()); | |
| const decodedValue = decodeURIComponent(rest.join('=').trim()); | |
| cookies[decodedName] = decodedValue; | |
| } | |
| }); | |
| return cookies; | |
| } | |
| async function chatAction({ context, request }: ActionFunctionArgs) { | |
| const { messages, files, promptId, contextOptimization } = await request.json<{ | |
| messages: Messages; | |
| files: any; | |
| promptId?: string; | |
| contextOptimization: boolean; | |
| }>(); | |
| const cookieHeader = request.headers.get('Cookie'); | |
| const apiKeys = JSON.parse(parseCookies(cookieHeader || '').apiKeys || '{}'); | |
| const providerSettings: Record<string, IProviderSetting> = JSON.parse( | |
| parseCookies(cookieHeader || '').providers || '{}', | |
| ); | |
| const stream = new SwitchableStream(); | |
| const cumulativeUsage = { | |
| completionTokens: 0, | |
| promptTokens: 0, | |
| totalTokens: 0, | |
| }; | |
| try { | |
| const options: StreamingOptions = { | |
| toolChoice: 'none', | |
| onFinish: async ({ text: content, finishReason, usage }) => { | |
| console.log('usage', usage); | |
| if (usage) { | |
| cumulativeUsage.completionTokens += usage.completionTokens || 0; | |
| cumulativeUsage.promptTokens += usage.promptTokens || 0; | |
| cumulativeUsage.totalTokens += usage.totalTokens || 0; | |
| } | |
| if (finishReason !== 'length') { | |
| return stream | |
| .switchSource( | |
| createDataStream({ | |
| async execute(dataStream) { | |
| dataStream.writeMessageAnnotation({ | |
| type: 'usage', | |
| value: { | |
| completionTokens: cumulativeUsage.completionTokens, | |
| promptTokens: cumulativeUsage.promptTokens, | |
| totalTokens: cumulativeUsage.totalTokens, | |
| }, | |
| }); | |
| }, | |
| onError: (error: any) => `Custom error: ${error.message}`, | |
| }), | |
| ) | |
| .then(() => stream.close()); | |
| } | |
| if (stream.switches >= MAX_RESPONSE_SEGMENTS) { | |
| throw Error('Cannot continue message: Maximum segments reached'); | |
| } | |
| const switchesLeft = MAX_RESPONSE_SEGMENTS - stream.switches; | |
| console.log(`Reached max token limit (${MAX_TOKENS}): Continuing message (${switchesLeft} switches left)`); | |
| messages.push({ role: 'assistant', content }); | |
| messages.push({ role: 'user', content: CONTINUE_PROMPT }); | |
| const result = await streamText({ | |
| messages, | |
| env: context.cloudflare.env, | |
| options, | |
| apiKeys, | |
| files, | |
| providerSettings, | |
| promptId, | |
| contextOptimization, | |
| }); | |
| return stream.switchSource(result.toDataStream()); | |
| }, | |
| }; | |
| const result = await streamText({ | |
| messages, | |
| env: context.cloudflare.env, | |
| options, | |
| apiKeys, | |
| files, | |
| providerSettings, | |
| promptId, | |
| contextOptimization, | |
| }); | |
| stream.switchSource(result.toDataStream()); | |
| return new Response(stream.readable, { | |
| status: 200, | |
| headers: { | |
| contentType: 'text/plain; charset=utf-8', | |
| }, | |
| }); | |
| } catch (error: any) { | |
| console.error(error); | |
| if (error.message?.includes('API key')) { | |
| throw new Response('Invalid or missing API key', { | |
| status: 401, | |
| statusText: 'Unauthorized', | |
| }); | |
| } | |
| throw new Response(null, { | |
| status: 500, | |
| statusText: 'Internal Server Error', | |
| }); | |
| } | |
| } | |