diff --git a/.env.example b/.env.example index 328413b20..c6c95eb8e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# [Optional] Set the user id for OpenAI and Helicone to track usage +USER_ID= + # [Recommended for local deployments] Backend API key for OpenAI, so that users don't need one (UI > this > '') OPENAI_API_KEY= # [Optional] Sets the "OpenAI-Organization" header field to support organization users (UI > this > '') diff --git a/src/common/types/env.d.ts b/src/common/types/env.d.ts index c7fe31c01..3843dbd19 100644 --- a/src/common/types/env.d.ts +++ b/src/common/types/env.d.ts @@ -4,6 +4,8 @@ declare namespace NodeJS { // available to the server-side interface ProcessEnv { + // OpenAI and Helicone + USER_ID: string; // LLM: OpenAI OPENAI_API_KEY: string; diff --git a/src/modules/llms/openai/OpenAISourceSetup.tsx b/src/modules/llms/openai/OpenAISourceSetup.tsx index 9bbbd6e34..5603bf308 100644 --- a/src/modules/llms/openai/OpenAISourceSetup.tsx +++ b/src/modules/llms/openai/OpenAISourceSetup.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Box, Button, FormControl, FormHelperText, FormLabel, Input, Switch } from '@mui/joy'; import SyncIcon from '@mui/icons-material/Sync'; +import { Box, Button, FormControl, FormHelperText, FormLabel, Input, Switch } from '@mui/joy'; import { apiQuery } from '~/modules/trpc/trpc.client'; @@ -12,9 +12,9 @@ import { Link } from '~/common/components/Link'; import { settingsCol1Width, settingsGap } from '~/common/theme'; import { DLLM, DModelSource, DModelSourceId } from '../llm.types'; -import { OpenAI } from './openai.types'; -import { hasServerKeyOpenAI, isValidOpenAIApiKey, LLMOptionsOpenAI, ModelVendorOpenAI } from './openai.vendor'; import { useModelsStore, useSourceSetup } from '../store-llms'; +import { OpenAI } from './openai.types'; +import { LLMOptionsOpenAI, ModelVendorOpenAI, hasServerKeyOpenAI, isValidOpenAIApiKey } from './openai.vendor'; export function OpenAISourceSetup(props: { sourceId: DModelSourceId }) { @@ -25,7 +25,7 @@ export function OpenAISourceSetup(props: { sourceId: DModelSourceId }) { // external state const { source, sourceLLMs, updateSetup, - normSetup: { heliKey, oaiHost, oaiKey, oaiOrg, moderationCheck }, + normSetup: { heliKey, oaiHost, oaiKey, oaiOrg, moderationCheck, userId }, } = useSourceSetup(props.sourceId, ModelVendorOpenAI.normalizeSetup); const hasModels = !!sourceLLMs.length; @@ -110,6 +110,23 @@ export function OpenAISourceSetup(props: { sourceId: DModelSourceId }) { /> } + {showAdvanced && + + + User ID + + + helicone, + OpenAI + + + updateSetup({ userId: event.target.value })} + sx={{ flexGrow: 1 }} + /> + } + {showAdvanced && diff --git a/src/modules/llms/openai/openai.router.ts b/src/modules/llms/openai/openai.router.ts index 04ea7749c..3ecc402e8 100644 --- a/src/modules/llms/openai/openai.router.ts +++ b/src/modules/llms/openai/openai.router.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; import { createTRPCRouter, publicProcedure } from '~/modules/trpc/trpc.server'; import { fetchJsonOrTRPCError } from '~/modules/trpc/trpc.serverutils'; @@ -19,12 +19,14 @@ const accessSchema = z.object({ oaiHost: z.string().trim(), heliKey: z.string().trim(), moderationCheck: z.boolean(), + userId: z.string().trim().optional(), }); export const modelSchema = z.object({ id: z.string(), temperature: z.number().min(0).max(1).optional(), maxTokens: z.number().min(1).max(1000000), + userId: z.string().optional() }); export const historySchema = z.array(z.object({ @@ -202,6 +204,9 @@ export function openAIAccess(access: AccessSchema, apiPath: string): { headers: // Organization ID const oaiOrg = access.oaiOrg || process.env.OPENAI_API_ORG_ID || ''; + // User ID + const userId = access.userId || process.env.USER_ID || ''; + // API host let oaiHost = access.oaiHost || process.env.OPENAI_API_HOST || DEFAULT_OPENAI_HOST; if (!oaiHost.startsWith('http')) @@ -221,19 +226,23 @@ export function openAIAccess(access: AccessSchema, apiPath: string): { headers: ...(oaiKey && { Authorization: `Bearer ${oaiKey}` }), 'Content-Type': 'application/json', ...(oaiOrg && { 'OpenAI-Organization': oaiOrg }), - ...(heliKey && { 'Helicone-Auth': `Bearer ${heliKey}` }), + ...(heliKey && { 'Helicone-Auth': `Bearer ${heliKey}`, 'Helicone-User-Id': userId }), }, url: oaiHost + apiPath, }; } export function openAIChatCompletionPayload(model: ModelSchema, history: HistorySchema, functions: FunctionsSchema | null, n: number, stream: boolean): OpenAI.Wire.ChatCompletion.Request { + // User ID + const userId = model.userId || process.env.USER_ID || ''; + return { model: model.id, messages: history, ...(functions && { functions: functions, function_call: 'auto' }), ...(model.temperature && { temperature: model.temperature }), ...(model.maxTokens && { max_tokens: model.maxTokens }), + ...(userId && { user: userId }), n, stream, }; diff --git a/src/modules/llms/openai/openai.vendor.ts b/src/modules/llms/openai/openai.vendor.ts index d917b5c29..ee1c48b1f 100644 --- a/src/modules/llms/openai/openai.vendor.ts +++ b/src/modules/llms/openai/openai.vendor.ts @@ -1,7 +1,7 @@ import { apiAsync } from '~/modules/trpc/trpc.client'; -import { DLLM, ModelVendor } from '../llm.types'; import { VChatFunctionIn, VChatMessageIn, VChatMessageOrFunctionCallOut, VChatMessageOut } from '../llm.client'; +import { DLLM, ModelVendor } from '../llm.types'; import { OpenAIIcon } from './OpenAIIcon'; import { OpenAILLMOptions } from './OpenAILLMOptions'; @@ -19,6 +19,7 @@ export interface SourceSetupOpenAI { oaiHost: string; // use OpenAI-compatible non-default hosts (full origin path) heliKey: string; // helicone key (works in conjunction with oaiHost) moderationCheck: boolean; + userId: string; // user id for OpenAi and helicone if heliKey is present } export interface LLMOptionsOpenAI { @@ -45,6 +46,7 @@ export const ModelVendorOpenAI: ModelVendor oaiOrg: '', oaiHost: '', heliKey: '', + userId: '', moderationCheck: false, ...partialSetup, }),