Skip to content

Commit

Permalink
Merge pull request #163 from enricoros/feature-azure-openai
Browse files Browse the repository at this point in the history
Land restructuring of the LLMs folder and partial Azure support. Full support will come next.
  • Loading branch information
enricoros authored Sep 23, 2023
2 parents ce08f6f + 91353ce commit 5272fa9
Show file tree
Hide file tree
Showing 63 changed files with 1,122 additions and 662 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ OPENAI_API_HOST=
# [Optional, Helicone] Helicone API key: https://www.helicone.ai/keys
HELICONE_API_KEY=

# [Optional] Azure OpenAI Service credentials for the server-side (if set, both must be set)
AZURE_OPENAI_API_ENDPOINT=
AZURE_OPENAI_API_KEY=

# [Optional] Anthropic credentials for the server-side
ANTHROPIC_API_KEY=
ANTHROPIC_API_HOST=
Expand Down
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ let nextConfig = {
// defaults to TRUE, unless API Keys are set at build time; this flag is used by the UI
HAS_SERVER_KEYS_GOOGLE_CSE: !!process.env.GOOGLE_CLOUD_API_KEY && !!process.env.GOOGLE_CSE_ID,
HAS_SERVER_KEY_ANTHROPIC: !!process.env.ANTHROPIC_API_KEY,
HAS_SERVER_KEY_AZURE_OPENAI: !!process.env.AZURE_OPENAI_API_KEY && !!process.env.AZURE_OPENAI_API_ENDPOINT,
HAS_SERVER_KEY_ELEVENLABS: !!process.env.ELEVENLABS_API_KEY,
HAS_SERVER_KEY_OPENAI: !!process.env.OPENAI_API_KEY,
HAS_SERVER_KEY_PRODIA: !!process.env.PRODIA_API_KEY,
Expand Down
4 changes: 2 additions & 2 deletions pages/api/elevenlabs/speech.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';

import { createEmptyReadableStream, throwResponseNotOk } from '~/modules/llms/transports/server/openai.streaming';
import { elevenlabsAccess, elevenlabsVoiceId, ElevenlabsWire, speechInputSchema } from '~/modules/elevenlabs/elevenlabs.router';
import { createEmptyReadableStream, throwResponseNotOk } from '../llms/stream';


/* NOTE: Why does this file even exist?
Expand All @@ -10,7 +10,7 @@ This file is a workaround for a limitation in tRPC; it does not support ArrayBuf
and that would force us to use base64 encoding for the audio data, which would be a waste of
bandwidth. So instead, we use this file to make the request to ElevenLabs, and then return the
response as an ArrayBuffer. Unfortunately this means duplicating the code in the server-side
and client-side vs. the TRPC implementation. So at lease we recycle the input structures.
and client-side vs. the tRPC implementation. So at lease we recycle the input structures.
*/

Expand Down
205 changes: 1 addition & 204 deletions pages/api/llms/stream.ts
Original file line number Diff line number Diff line change
@@ -1,207 +1,4 @@
import { NextRequest, NextResponse } from 'next/server';
import { createParser as createEventsourceParser, EventSourceParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';

import { AnthropicWire } from '~/modules/llms/anthropic/anthropic.types';
import { OpenAI } from '~/modules/llms/openai/openai.types';
import { anthropicAccess, anthropicCompletionRequest } from '~/modules/llms/anthropic/anthropic.router';
import { chatStreamSchema, openAIAccess, openAIChatCompletionPayload } from '~/modules/llms/openai/openai.router';


/**
* Vendor stream parsers
* - The vendor can decide to terminate the connection (close: true), transmitting anything in 'text' before doing so
* - The vendor can also throw from this function, which will error and terminate the connection
*/
type AIStreamParser = (data: string) => { text: string, close: boolean };


// The peculiarity of our parser is the injection of a JSON structure at the beginning of the stream, to
// communicate parameters before the text starts flowing to the client.
function parseOpenAIStream(): AIStreamParser {
let hasBegun = false;
let hasWarned = false;

return data => {

const json: OpenAI.Wire.ChatCompletion.ResponseStreamingChunk = JSON.parse(data);

// an upstream error will be handled gracefully and transmitted as text (throw to transmit as 'error')
if (json.error)
return { text: `[OpenAI Issue] ${json.error.message || json.error}`, close: true };

if (json.choices.length !== 1)
throw new Error(`[OpenAI Issue] Expected 1 completion, got ${json.choices.length}`);

const index = json.choices[0].index;
if (index !== 0 && index !== undefined /* LocalAI hack/workaround until https://github.com/go-skynet/LocalAI/issues/788 */)
throw new Error(`[OpenAI Issue] Expected completion index 0, got ${index}`);
let text = json.choices[0].delta?.content /*|| json.choices[0]?.text*/ || '';

// hack: prepend the model name to the first packet
if (!hasBegun) {
hasBegun = true;
const firstPacket: OpenAI.API.Chat.StreamingFirstResponse = {
model: json.model,
};
text = JSON.stringify(firstPacket) + text;
}

// if there's a warning, log it once
if (json.warning && !hasWarned) {
hasWarned = true;
console.log('/api/llms/stream: OpenAI stream warning:', json.warning);
}

// workaround: LocalAI doesn't send the [DONE] event, but similarly to OpenAI, it sends a "finish_reason" delta update
const close = !!json.choices[0].finish_reason;
return { text, close };
};
}


// Anthropic event stream parser
function parseAnthropicStream(): AIStreamParser {
let hasBegun = false;

return data => {

const json: AnthropicWire.Complete.Response = JSON.parse(data);
let text = json.completion;

// hack: prepend the model name to the first packet
if (!hasBegun) {
hasBegun = true;
const firstPacket: OpenAI.API.Chat.StreamingFirstResponse = {
model: json.model,
};
text = JSON.stringify(firstPacket) + text;
}

return { text, close: false };
};
}


/**
* Creates a TransformStream that parses events from an EventSource stream using a custom parser.
* @returns {TransformStream<Uint8Array, string>} TransformStream parsing events.
*/
export function createEventStreamTransformer(vendorTextParser: AIStreamParser): TransformStream<Uint8Array, Uint8Array> {
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
let eventSourceParser: EventSourceParser;

return new TransformStream({
start: async (controller): Promise<void> => {
eventSourceParser = createEventsourceParser(
(event: ParsedEvent | ReconnectInterval) => {

// ignore 'reconnect-interval' and events with no data
if (event.type !== 'event' || !('data' in event))
return;

// event stream termination, close our transformed stream
if (event.data === '[DONE]') {
controller.terminate();
return;
}

try {
const { text, close } = vendorTextParser(event.data);
if (text)
controller.enqueue(textEncoder.encode(text));
if (close)
controller.terminate();
} catch (error: any) {
// console.log(`/api/llms/stream: parse issue: ${error?.message || error}`);
controller.enqueue(textEncoder.encode(`[Stream Issue] ${error?.message || error}`));
controller.terminate();
}
},
);
},

// stream=true is set because the data is not guaranteed to be final and un-chunked
transform: (chunk: Uint8Array) => {
eventSourceParser.feed(textDecoder.decode(chunk, { stream: true }));
},
});
}

export async function throwResponseNotOk(response: Response) {
if (!response.ok) {
const errorPayload: object | null = await response.json().catch(() => null);
throw new Error(`${response.status} · ${response.statusText}${errorPayload ? ' · ' + JSON.stringify(errorPayload) : ''}`);
}
}

export function createEmptyReadableStream<T = Uint8Array>(): ReadableStream<T> {
return new ReadableStream({
start: (controller) => controller.close(),
});
}


export default async function handler(req: NextRequest): Promise<Response> {

// inputs - reuse the tRPC schema
const { vendorId, access, model, history } = chatStreamSchema.parse(await req.json());

// begin event streaming from the OpenAI API
let upstreamResponse: Response;
let vendorStreamParser: AIStreamParser;
try {

// prepare the API request data
let headersUrl: { headers: HeadersInit, url: string };
let body: object;
switch (vendorId) {
case 'anthropic':
headersUrl = anthropicAccess(access as any, '/v1/complete');
body = anthropicCompletionRequest(model, history, true);
vendorStreamParser = parseAnthropicStream();
break;

case 'openai':
headersUrl = openAIAccess(access as any, '/v1/chat/completions');
body = openAIChatCompletionPayload(model, history, null, null, 1, true);
vendorStreamParser = parseOpenAIStream();
break;
}

// POST to our API route
upstreamResponse = await fetch(headersUrl.url, {
method: 'POST',
headers: headersUrl.headers,
body: JSON.stringify(body),
});
await throwResponseNotOk(upstreamResponse);

} catch (error: any) {
const fetchOrVendorError = (error?.message || typeof error === 'string' ? error : JSON.stringify(error)) + (error?.cause ? ' · ' + error.cause : '');
console.log(`/api/llms/stream: fetch issue: ${fetchOrVendorError}`);
return new NextResponse('[OpenAI Issue] ' + fetchOrVendorError, { status: 500 });
}

/* The following code is heavily inspired by the Vercel AI SDK, but simplified to our needs and in full control.
* This replaces the former (custom) implementation that used to return a ReadableStream directly, and upon start,
* it was blindly fetching the upstream response and piping it to the client.
*
* We now use backpressure, as explained on: https://sdk.vercel.ai/docs/concepts/backpressure-and-cancellation
*
* NOTE: we have not benchmarked to see if there is performance impact by using this approach - we do want to have
* a 'healthy' level of inventory (i.e., pre-buffering) on the pipe to the client.
*/
const chatResponseStream = (upstreamResponse.body || createEmptyReadableStream())
.pipeThrough(createEventStreamTransformer(vendorStreamParser));

return new NextResponse(chatResponseStream, {
status: 200,
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
},
});
}
export { openaiStreamingResponse as default } from '~/modules/llms/transports/server/openai.streaming';

// noinspection JSUnusedGlobalSymbols
export const runtime = 'edge';
4 changes: 2 additions & 2 deletions src/apps/chat/components/applayout/ChatDrawerItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import AddIcon from '@mui/icons-material/Add';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import FileUploadIcon from '@mui/icons-material/FileUpload';

import { useChatStore } from '~/common/state/store-chats';
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
import { setLayoutDrawerAnchor } from '~/common/layout/store-applayout';
import { useChatStore } from '~/common/state/store-chats';
import { useUIPreferencesStore } from '~/common/state/store-ui';

import { ConversationItem } from './ConversationItem';
import { OpenAIIcon } from '~/modules/llms/openai/OpenAIIcon';


type ListGrouping = 'off' | 'persona';
Expand Down
3 changes: 1 addition & 2 deletions src/apps/chat/components/applayout/useLLMDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { ListItemButton, ListItemDecorator } from '@mui/joy';
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
import SettingsIcon from '@mui/icons-material/Settings';

import { DLLM, DLLMId, DModelSourceId } from '~/modules/llms/llm.types';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';

import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown';
import { useUIStateStore } from '~/common/state/store-ui';
Expand Down
2 changes: 1 addition & 1 deletion src/apps/chat/components/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
import TelegramIcon from '@mui/icons-material/Telegram';

import { ContentReducer } from '~/modules/aifn/summarize/ContentReducer';
import { LLMOptionsOpenAI } from '~/modules/llms/openai/openai.vendor';
import { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vendor';
import { useChatLLM } from '~/modules/llms/store-llms';

import { CloseableMenu } from '~/common/components/CloseableMenu';
Expand Down
4 changes: 2 additions & 2 deletions src/apps/chat/editors/chat-stream.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DLLMId } from '~/modules/llms/llm.types';
import { SystemPurposeId } from '../../../data';
import { DLLMId } from '~/modules/llms/store-llms';
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
import { autoTitle } from '~/modules/aifn/autotitle/autoTitle';
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
import { streamChat } from '~/modules/llms/llm.client';
import { streamChat } from '~/modules/llms/transports/streamChat';
import { useElevenlabsStore } from '~/modules/elevenlabs/store-elevenlabs';

import { DMessage, useChatStore } from '~/common/state/store-chats';
Expand Down
2 changes: 1 addition & 1 deletion src/apps/chat/editors/editors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DLLMId } from '~/modules/llms/llm.types';
import { DLLMId } from '~/modules/llms/store-llms';
import { SystemPurposeId, SystemPurposes } from '../../../data';

import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
Expand Down
2 changes: 1 addition & 1 deletion src/apps/chat/editors/react-tangent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Agent } from '~/modules/aifn/react/react';
import { DLLMId } from '~/modules/llms/llm.types';
import { DLLMId } from '~/modules/llms/store-llms';

import { createDEphemeral, DMessage, useChatStore } from '~/common/state/store-chats';

Expand Down
2 changes: 1 addition & 1 deletion src/apps/chat/trade/ImportChats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { Box, Button, FormControl, FormLabel, Input, Sheet, Typography } from '@
import FileUploadIcon from '@mui/icons-material/FileUpload';

import type { ChatGptSharedChatSchema } from '~/modules/sharing/import.chatgpt';
import { OpenAIIcon } from '~/modules/llms/openai/OpenAIIcon';
import { apiAsync } from '~/modules/trpc/trpc.client';

import { Brand } from '~/common/brand';
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
import { createDConversation, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';

import { ImportedOutcome, ImportOutcomeModal } from './ImportOutcomeModal';
Expand Down
3 changes: 1 addition & 2 deletions src/apps/chat/trade/trade.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { fileSave } from 'browser-fs-access';

import { defaultSystemPurposeId } from '../../../data';

import { DModelSource } from '~/modules/llms/llm.types';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DModelSource, useModelsStore } from '~/modules/llms/store-llms';

import { DConversation, useChatStore } from '~/common/state/store-chats';
import { ImportedOutcome } from './ImportOutcomeModal';
Expand Down
3 changes: 1 addition & 2 deletions src/apps/models-modal/LLMOptionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';

import { DLLMId } from '~/modules/llms/llm.types';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DLLMId, useModelsStore } from '~/modules/llms/store-llms';

import { GoodModal } from '~/common/components/GoodModal';
import { useUIStateStore } from '~/common/state/store-ui';
Expand Down
8 changes: 4 additions & 4 deletions src/apps/models-modal/ModelsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { Box, Chip, IconButton, List, ListItem, ListItemButton, Tooltip, Typogra
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';

import { DLLM, DModelSourceId, ModelVendor } from '~/modules/llms/llm.types';
import { findVendorById } from '~/modules/llms/vendor.registry';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DLLM, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
import { IModelVendor } from '~/modules/llms/vendors/IModelVendor';
import { findVendorById } from '~/modules/llms/vendors/vendor.registry';

import { useUIStateStore } from '~/common/state/store-ui';


function ModelItem(props: { llm: DLLM, vendor: ModelVendor, chipChat: boolean, chipFast: boolean, chipFunc: boolean }) {
function ModelItem(props: { llm: DLLM, vendor: IModelVendor, chipChat: boolean, chipFast: boolean, chipFunc: boolean }) {

// external state
const openLLMOptions = useUIStateStore(state => state.openLLMOptions);
Expand Down
5 changes: 2 additions & 3 deletions src/apps/models-modal/ModelsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { Checkbox, Divider } from '@mui/joy';
import { GoodModal } from '~/common/components/GoodModal';
import { useUIStateStore } from '~/common/state/store-ui';

import { DModelSourceId } from '~/modules/llms/llm.types';
import { createModelSourceForDefaultVendor } from '~/modules/llms/vendor.registry';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
import { createModelSourceForDefaultVendor } from '~/modules/llms/vendors/vendor.registry';

import { LLMOptionsModal } from './LLMOptionsModal';
import { ModelsList } from './ModelsList';
Expand Down
12 changes: 6 additions & 6 deletions src/apps/models-modal/ModelsSourceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ import CloudOutlinedIcon from '@mui/icons-material/CloudOutlined';
import ComputerIcon from '@mui/icons-material/Computer';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';

import { DModelSourceId, ModelVendor, ModelVendorId } from '~/modules/llms/llm.types';
import { createModelSourceForVendor, findAllVendors, findVendorById } from '~/modules/llms/vendor.registry';
import { hasServerKeyOpenAI } from '~/modules/llms/openai/openai.vendor';
import { useModelsStore } from '~/modules/llms/store-llms';
import { DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
import { IModelVendor, ModelVendorId } from '~/modules/llms/vendors/IModelVendor';
import { createModelSourceForVendor, findAllVendors, findVendorById } from '~/modules/llms/vendors/vendor.registry';
import { hasServerKeyOpenAI } from '~/modules/llms/vendors/openai/openai.vendor';

import { CloseableMenu } from '~/common/components/CloseableMenu';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { hideOnDesktop, hideOnMobile } from '~/common/theme';


function locationIcon(vendor?: ModelVendor | null) {
function locationIcon(vendor?: IModelVendor | null) {
if (vendor && vendor.id === 'openai' && hasServerKeyOpenAI)
return <CloudDoneOutlinedIcon />;
return !vendor ? null : vendor.location === 'local' ? <ComputerIcon /> : <CloudOutlinedIcon />;
}

function vendorIcon(vendor?: ModelVendor | null) {
function vendorIcon(vendor?: IModelVendor | null) {
const Icon = !vendor ? null : vendor.Icon;
return Icon ? <Icon /> : null;
}
Expand Down
Loading

1 comment on commit 5272fa9

@vercel
Copy link

@vercel vercel bot commented on 5272fa9 Sep 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

big-agi – ./

big-agi-enricoros.vercel.app
get.big-agi.com
big-agi-git-main-enricoros.vercel.app

Please sign in to comment.