2024-02-07 14:17:11 +09:00
"use client" ;
2024-07-05 20:59:45 +09:00
// azure and openai, using same models. so using same LLMApi.
2023-06-29 00:12:35 +09:00
import {
2023-11-10 03:43:30 +09:00
ApiPath ,
2024-09-30 02:19:20 +09:00
OPENAI_BASE_URL ,
2023-07-11 00:19:43 +09:00
DEFAULT_MODELS ,
2023-06-29 00:12:35 +09:00
OpenaiPath ,
2024-07-05 20:59:45 +09:00
Azure ,
2023-06-29 00:12:35 +09:00
REQUEST_TIMEOUT_MS ,
2023-11-10 03:43:30 +09:00
ServiceProvider ,
2023-06-29 00:12:35 +09:00
} from "@/app/constant" ;
2024-08-29 01:21:26 +09:00
import {
ChatMessageTool ,
useAccessStore ,
useAppConfig ,
useChatStore ,
2024-08-29 20:55:09 +09:00
usePluginStore ,
2024-08-29 01:21:26 +09:00
} from "@/app/store" ;
2024-07-05 20:59:45 +09:00
import { collectModelsWithDefaultModel } from "@/app/utils/model" ;
2024-08-02 21:58:21 +09:00
import {
preProcessImageContent ,
uploadImage ,
base64Image2Blob ,
2024-08-29 18:14:23 +09:00
stream ,
2024-08-02 21:58:21 +09:00
} from "@/app/utils/chat" ;
2024-07-12 13:00:25 +09:00
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare" ;
2024-08-10 12:09:07 +09:00
import { DalleSize , DalleQuality , DalleStyle } from "@/app/typing" ;
2023-05-15 02:33:46 +09:00
2024-02-20 19:04:32 +09:00
import {
ChatOptions ,
getHeaders ,
LLMApi ,
LLMModel ,
LLMUsage ,
MultimodalContent ,
2024-08-27 17:21:02 +09:00
SpeechOptions ,
2024-02-20 19:04:32 +09:00
} from "../api" ;
2023-05-15 02:33:46 +09:00
import Locale from "../../locales" ;
2023-08-14 22:36:29 +09:00
import { getClientConfig } from "@/app/config/client" ;
2024-02-20 19:04:32 +09:00
import {
getMessageTextContent ,
isVisionModel ,
2024-08-02 19:00:42 +09:00
isDalle3 as _isDalle3 ,
2024-02-20 19:04:32 +09:00
} from "@/app/utils" ;
2024-10-16 22:57:07 +09:00
import { fetch } from "@/app/utils/stream" ;
2023-05-15 00:00:17 +09:00
2023-07-05 00:16:24 +09:00
export interface OpenAIListModelResponse {
object : string ;
data : Array < {
id : string ;
object : string ;
root : string ;
} > ;
}
2024-07-07 22:59:56 +09:00
export interface RequestPayload {
2024-04-08 19:29:08 +09:00
messages : {
role : "system" | "user" | "assistant" ;
content : string | MultimodalContent [ ] ;
} [ ] ;
stream? : boolean ;
model : string ;
temperature : number ;
presence_penalty : number ;
frequency_penalty : number ;
top_p : number ;
max_tokens? : number ;
2024-11-07 20:45:27 +09:00
max_completion_tokens? : number ;
2024-04-08 19:29:08 +09:00
}
2024-08-02 19:00:42 +09:00
export interface DalleRequestPayload {
model : string ;
prompt : string ;
2024-08-02 21:58:21 +09:00
response_format : "url" | "b64_json" ;
2024-08-02 19:00:42 +09:00
n : number ;
2024-08-02 19:50:48 +09:00
size : DalleSize ;
2024-08-10 12:09:07 +09:00
quality : DalleQuality ;
style : DalleStyle ;
2024-08-02 19:00:42 +09:00
}
2023-05-15 00:00:17 +09:00
export class ChatGPTApi implements LLMApi {
2023-07-11 00:19:43 +09:00
private disableListModels = true ;
2024-09-18 16:37:21 +09:00
path ( path : string ) : string {
2023-11-10 03:43:30 +09:00
const accessStore = useAccessStore . getState ( ) ;
2023-08-14 22:36:29 +09:00
2024-04-16 15:50:48 +09:00
let baseUrl = "" ;
2023-11-10 03:43:30 +09:00
2024-07-05 21:15:56 +09:00
const isAzure = path . includes ( "deployments" ) ;
2024-04-16 15:50:48 +09:00
if ( accessStore . useCustomConfig ) {
if ( isAzure && ! accessStore . isValidAzure ( ) ) {
throw Error (
"incomplete azure config, please check it in your settings page" ,
) ;
}
baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl ;
}
2023-11-10 03:43:30 +09:00
if ( baseUrl . length === 0 ) {
2023-08-14 22:36:29 +09:00
const isApp = ! ! getClientConfig ( ) ? . isApp ;
2024-07-05 21:15:56 +09:00
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI ;
2024-09-30 02:44:27 +09:00
baseUrl = isApp ? OPENAI_BASE_URL : apiPath ;
2023-06-29 00:12:35 +09:00
}
2023-11-10 03:43:30 +09:00
if ( baseUrl . endsWith ( "/" ) ) {
baseUrl = baseUrl . slice ( 0 , baseUrl . length - 1 ) ;
}
2024-07-05 21:15:56 +09:00
if (
! baseUrl . startsWith ( "http" ) &&
! isAzure &&
! baseUrl . startsWith ( ApiPath . OpenAI )
) {
2023-11-10 03:43:30 +09:00
baseUrl = "https://" + baseUrl ;
2023-05-16 01:22:11 +09:00
}
2023-11-10 03:43:30 +09:00
2024-02-07 14:17:11 +09:00
console . log ( "[Proxy Endpoint] " , baseUrl , path ) ;
2024-07-12 13:00:25 +09:00
// try rebuild url, when using cloudflare ai gateway in client
return cloudflareAIGatewayUrl ( [ baseUrl , path ] . join ( "/" ) ) ;
2023-05-15 00:00:17 +09:00
}
2024-08-02 21:58:21 +09:00
async extractMessage ( res : any ) {
2024-08-02 19:00:42 +09:00
if ( res . error ) {
return "```\n" + JSON . stringify ( res , null , 4 ) + "\n```" ;
}
2024-08-02 21:58:21 +09:00
// dalle3 model return url, using url create image message
2024-08-02 19:00:42 +09:00
if ( res . data ) {
2024-08-02 21:58:21 +09:00
let url = res . data ? . at ( 0 ) ? . url ? ? "" ;
const b64_json = res . data ? . at ( 0 ) ? . b64_json ? ? "" ;
if ( ! url && b64_json ) {
// uploadImage
url = await uploadImage ( base64Image2Blob ( b64_json , "image/png" ) ) ;
}
2024-08-02 19:00:42 +09:00
return [
{
type : "image_url" ,
image_url : {
url ,
} ,
} ,
] ;
}
2024-08-02 23:16:08 +09:00
return res . choices ? . at ( 0 ) ? . message ? . content ? ? res ;
2023-05-15 00:00:17 +09:00
}
2024-08-27 17:21:02 +09:00
async speech ( options : SpeechOptions ) : Promise < ArrayBuffer > {
const requestPayload = {
model : options.model ,
input : options.input ,
voice : options.voice ,
response_format : options.response_format ,
speed : options.speed ,
} ;
console . log ( "[Request] openai speech payload: " , requestPayload ) ;
const controller = new AbortController ( ) ;
options . onController ? . ( controller ) ;
try {
2024-09-18 16:37:21 +09:00
const speechPath = this . path ( OpenaiPath . SpeechPath ) ;
2024-08-27 17:21:02 +09:00
const speechPayload = {
method : "POST" ,
body : JSON.stringify ( requestPayload ) ,
signal : controller.signal ,
headers : getHeaders ( ) ,
} ;
// make a fetch request
const requestTimeoutId = setTimeout (
( ) = > controller . abort ( ) ,
REQUEST_TIMEOUT_MS ,
) ;
const res = await fetch ( speechPath , speechPayload ) ;
clearTimeout ( requestTimeoutId ) ;
return await res . arrayBuffer ( ) ;
} catch ( e ) {
console . log ( "[Request] failed to make a speech request" , e ) ;
throw e ;
}
}
2023-05-15 00:00:17 +09:00
async chat ( options : ChatOptions ) {
const modelConfig = {
. . . useAppConfig . getState ( ) . modelConfig ,
. . . useChatStore . getState ( ) . currentSession ( ) . mask . modelConfig ,
. . . {
2023-05-15 02:33:46 +09:00
model : options.config.model ,
2024-07-05 20:59:45 +09:00
providerName : options.config.providerName ,
2023-05-15 00:00:17 +09:00
} ,
} ;
2024-08-02 19:00:42 +09:00
let requestPayload : RequestPayload | DalleRequestPayload ;
2023-05-15 00:00:17 +09:00
2024-08-02 19:00:42 +09:00
const isDalle3 = _isDalle3 ( options . config . model ) ;
2024-09-13 14:18:07 +09:00
const isO1 = options . config . model . startsWith ( "o1" ) ;
2024-08-02 19:00:42 +09:00
if ( isDalle3 ) {
2024-08-02 19:50:48 +09:00
const prompt = getMessageTextContent (
options . messages . slice ( - 1 ) ? . pop ( ) as any ,
) ;
2024-08-02 19:00:42 +09:00
requestPayload = {
model : options.config.model ,
prompt ,
2024-08-02 21:58:21 +09:00
// URLs are only valid for 60 minutes after the image has been generated.
response_format : "b64_json" , // using b64_json, and save image in CacheStorage
2024-08-02 19:00:42 +09:00
n : 1 ,
size : options.config?.size ? ? "1024x1024" ,
2024-08-10 12:09:07 +09:00
quality : options.config?.quality ? ? "standard" ,
style : options.config?.style ? ? "vivid" ,
2024-08-02 19:00:42 +09:00
} ;
} else {
const visionModel = isVisionModel ( options . config . model ) ;
const messages : ChatOptions [ "messages" ] = [ ] ;
for ( const v of options . messages ) {
const content = visionModel
? await preProcessImageContent ( v . content )
: getMessageTextContent ( v ) ;
2024-09-13 17:25:04 +09:00
if ( ! ( isO1 && v . role === "system" ) )
2024-09-13 14:18:07 +09:00
messages . push ( { role : v.role , content } ) ;
2024-08-02 19:00:42 +09:00
}
2024-09-13 14:18:07 +09:00
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
2024-08-02 19:00:42 +09:00
requestPayload = {
messages ,
2024-11-21 12:46:10 +09:00
stream : options.config.stream ,
2024-08-02 19:00:42 +09:00
model : modelConfig.model ,
2024-09-13 14:18:07 +09:00
temperature : ! isO1 ? modelConfig.temperature : 1 ,
presence_penalty : ! isO1 ? modelConfig.presence_penalty : 0 ,
frequency_penalty : ! isO1 ? modelConfig.frequency_penalty : 0 ,
top_p : ! isO1 ? modelConfig.top_p : 1 ,
2024-08-02 19:00:42 +09:00
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
} ;
2024-11-07 20:45:27 +09:00
// O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
if ( isO1 ) {
requestPayload [ "max_completion_tokens" ] = modelConfig . max_tokens ;
}
2024-08-02 19:00:42 +09:00
// add max_tokens to vision model
2024-09-13 13:56:28 +09:00
if ( visionModel ) {
2024-08-02 19:00:42 +09:00
requestPayload [ "max_tokens" ] = Math . max ( modelConfig . max_tokens , 4000 ) ;
}
2024-02-27 18:28:01 +09:00
}
2023-05-15 00:00:17 +09:00
console . log ( "[Request] openai payload: " , requestPayload ) ;
2024-11-21 12:46:10 +09:00
const shouldStream = ! isDalle3 && ! ! options . config . stream ;
2023-05-15 00:00:17 +09:00
const controller = new AbortController ( ) ;
2023-05-15 02:33:46 +09:00
options . onController ? . ( controller ) ;
2023-05-15 00:00:17 +09:00
try {
2024-07-05 20:59:45 +09:00
let chatPath = "" ;
2024-07-06 00:56:10 +09:00
if ( modelConfig . providerName === ServiceProvider . Azure ) {
2024-07-05 20:59:45 +09:00
// find model, and get displayName as deployName
const { models : configModels , customModels : configCustomModels } =
useAppConfig . getState ( ) ;
2024-07-06 01:05:59 +09:00
const {
defaultModel ,
customModels : accessCustomModels ,
useCustomConfig ,
} = useAccessStore . getState ( ) ;
2024-07-05 20:59:45 +09:00
const models = collectModelsWithDefaultModel (
configModels ,
[ configCustomModels , accessCustomModels ] . join ( "," ) ,
defaultModel ,
) ;
const model = models . find (
( model ) = >
2024-07-06 00:56:10 +09:00
model . name === modelConfig . model &&
model ? . provider ? . providerName === ServiceProvider . Azure ,
2024-07-05 20:59:45 +09:00
) ;
2024-07-05 21:15:56 +09:00
chatPath = this . path (
2024-08-02 19:00:42 +09:00
( isDalle3 ? Azure.ImagePath : Azure.ChatPath ) (
2024-07-05 21:20:21 +09:00
( model ? . displayName ? ? model ? . name ) as string ,
2024-07-06 01:05:59 +09:00
useCustomConfig ? useAccessStore . getState ( ) . azureApiVersion : "" ,
2024-07-05 21:15:56 +09:00
) ,
) ;
2024-07-05 20:59:45 +09:00
} else {
2024-08-02 19:00:42 +09:00
chatPath = this . path (
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath ,
) ;
2024-07-05 20:59:45 +09:00
}
2023-05-15 00:00:17 +09:00
if ( shouldStream ) {
2024-09-22 19:53:51 +09:00
let index = - 1 ;
2024-08-30 18:31:20 +09:00
const [ tools , funcs ] = usePluginStore
2024-08-29 20:55:09 +09:00
. getState ( )
2024-08-31 00:39:08 +09:00
. getAsTools (
2024-09-07 22:32:18 +09:00
useChatStore . getState ( ) . currentSession ( ) . mask ? . plugin || [ ] ,
2024-08-31 00:39:08 +09:00
) ;
2024-09-02 22:45:47 +09:00
// console.log("getAsTools", tools, funcs);
2024-08-29 18:14:23 +09:00
stream (
chatPath ,
requestPayload ,
getHeaders ( ) ,
2024-08-31 00:39:08 +09:00
tools as any ,
2024-08-30 18:31:20 +09:00
funcs ,
2024-08-29 18:14:23 +09:00
controller ,
2024-08-29 18:28:15 +09:00
// parseSSE
2024-08-29 18:14:23 +09:00
( text : string , runTools : ChatMessageTool [ ] ) = > {
2024-08-29 18:28:15 +09:00
// console.log("parseSSE", text, runTools);
2024-08-29 18:14:23 +09:00
const json = JSON . parse ( text ) ;
const choices = json . choices as Array < {
delta : {
content : string ;
tool_calls : ChatMessageTool [ ] ;
2024-08-29 00:58:46 +09:00
} ;
2024-08-29 18:14:23 +09:00
} > ;
const tool_calls = choices [ 0 ] ? . delta ? . tool_calls ;
if ( tool_calls ? . length > 0 ) {
const id = tool_calls [ 0 ] ? . id ;
const args = tool_calls [ 0 ] ? . function ? . arguments ;
if ( id ) {
2024-09-22 19:59:49 +09:00
index += 1 ;
2024-08-29 18:14:23 +09:00
runTools . push ( {
id ,
type : tool_calls [ 0 ] ? . type ,
2024-08-29 00:58:46 +09:00
function : {
2024-08-29 18:14:23 +09:00
name : tool_calls [ 0 ] ? . function ? . name as string ,
arguments : args ,
2024-08-29 00:58:46 +09:00
} ,
2024-08-29 18:14:23 +09:00
} ) ;
} else {
// @ts-ignore
runTools [ index ] [ "function" ] [ "arguments" ] += args ;
2023-05-16 02:58:58 +09:00
}
2023-05-16 02:25:16 +09:00
}
2024-08-29 18:14:23 +09:00
return choices [ 0 ] ? . delta ? . content ;
2023-05-16 02:25:16 +09:00
} ,
2024-08-29 18:28:15 +09:00
// processToolMessage, include tool_calls message and tool call results
(
requestPayload : RequestPayload ,
toolCallMessage : any ,
toolCallResult : any [ ] ,
) = > {
2024-09-22 19:53:51 +09:00
// reset index value
index = - 1 ;
2024-08-29 18:28:15 +09:00
// @ts-ignore
requestPayload ? . messages ? . splice (
// @ts-ignore
requestPayload ? . messages ? . length ,
0 ,
toolCallMessage ,
. . . toolCallResult ,
) ;
2023-05-16 02:25:16 +09:00
} ,
2024-08-29 18:14:23 +09:00
options ,
) ;
2023-05-15 00:00:17 +09:00
} else {
2024-08-29 18:14:23 +09:00
const chatPayload = {
method : "POST" ,
body : JSON.stringify ( requestPayload ) ,
signal : controller.signal ,
headers : getHeaders ( ) ,
} ;
2024-08-29 00:58:46 +09:00
2024-08-29 18:14:23 +09:00
// make a fetch request
const requestTimeoutId = setTimeout (
( ) = > controller . abort ( ) ,
2024-10-14 17:31:17 +09:00
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS , // dalle3 using b64_json is slow.
2024-08-29 18:14:23 +09:00
) ;
2024-08-29 00:58:46 +09:00
2023-05-15 00:00:17 +09:00
const res = await fetch ( chatPath , chatPayload ) ;
2023-05-16 09:59:30 +09:00
clearTimeout ( requestTimeoutId ) ;
2023-05-15 00:00:17 +09:00
const resJson = await res . json ( ) ;
2024-08-02 21:58:21 +09:00
const message = await this . extractMessage ( resJson ) ;
2024-11-04 18:00:45 +09:00
options . onFinish ( message , res ) ;
2023-05-15 00:00:17 +09:00
}
} catch ( e ) {
2023-08-01 11:16:36 +09:00
console . log ( "[Request] failed to make a chat request" , e ) ;
2023-05-15 02:33:46 +09:00
options . onError ? . ( e as Error ) ;
2023-05-15 00:00:17 +09:00
}
}
async usage() {
2023-05-15 02:33:46 +09:00
const formatDate = ( d : Date ) = >
` ${ d . getFullYear ( ) } - ${ ( d . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , "0" ) } - ${ d
. getDate ( )
. toString ( )
. padStart ( 2 , "0" ) } ` ;
const ONE_DAY = 1 * 24 * 60 * 60 * 1000 ;
const now = new Date ( ) ;
const startOfMonth = new Date ( now . getFullYear ( ) , now . getMonth ( ) , 1 ) ;
const startDate = formatDate ( startOfMonth ) ;
const endDate = formatDate ( new Date ( Date . now ( ) + ONE_DAY ) ) ;
const [ used , subs ] = await Promise . all ( [
fetch (
this . path (
2023-06-13 01:39:29 +09:00
` ${ OpenaiPath . UsagePath } ?start_date= ${ startDate } &end_date= ${ endDate } ` ,
2023-05-15 02:33:46 +09:00
) ,
{
method : "GET" ,
headers : getHeaders ( ) ,
} ,
) ,
2023-06-13 01:39:29 +09:00
fetch ( this . path ( OpenaiPath . SubsPath ) , {
2023-05-15 02:33:46 +09:00
method : "GET" ,
headers : getHeaders ( ) ,
} ) ,
] ) ;
2023-05-19 01:27:25 +09:00
if ( used . status === 401 ) {
2023-05-15 02:33:46 +09:00
throw new Error ( Locale . Error . Unauthorized ) ;
}
2023-05-19 01:27:25 +09:00
if ( ! used . ok || ! subs . ok ) {
throw new Error ( "Failed to query usage from openai" ) ;
}
2023-05-15 02:33:46 +09:00
const response = ( await used . json ( ) ) as {
total_usage? : number ;
error ? : {
type : string ;
message : string ;
} ;
} ;
const total = ( await subs . json ( ) ) as {
hard_limit_usd? : number ;
} ;
if ( response . error && response . error . type ) {
throw Error ( response . error . message ) ;
}
if ( response . total_usage ) {
response . total_usage = Math . round ( response . total_usage ) / 100 ;
}
if ( total . hard_limit_usd ) {
total . hard_limit_usd = Math . round ( total . hard_limit_usd * 100 ) / 100 ;
}
2023-05-15 00:00:17 +09:00
return {
2023-05-15 02:33:46 +09:00
used : response.total_usage ,
total : total.hard_limit_usd ,
2023-05-15 00:00:17 +09:00
} as LLMUsage ;
}
2023-07-05 00:16:24 +09:00
async models ( ) : Promise < LLMModel [ ] > {
2023-07-11 00:19:43 +09:00
if ( this . disableListModels ) {
return DEFAULT_MODELS . slice ( ) ;
}
2023-07-05 00:16:24 +09:00
const res = await fetch ( this . path ( OpenaiPath . ListModelPath ) , {
method : "GET" ,
headers : {
. . . getHeaders ( ) ,
} ,
} ) ;
const resJson = ( await res . json ( ) ) as OpenAIListModelResponse ;
2024-09-07 02:42:56 +09:00
const chatModels = resJson . data ? . filter (
( m ) = > m . id . startsWith ( "gpt-" ) || m . id . startsWith ( "chatgpt-" ) ,
) ;
2023-07-05 00:16:24 +09:00
console . log ( "[Models]" , chatModels ) ;
2023-07-09 19:03:06 +09:00
if ( ! chatModels ) {
return [ ] ;
}
2024-08-05 21:26:48 +09:00
//由于目前 OpenAI 的 disableListModels 默认为 true, 所以当前实际不会运行到这场
let seq = 1000 ; //同 Constant.ts 中的排序保持一致
2023-07-09 19:03:06 +09:00
return chatModels . map ( ( m ) = > ( {
name : m.id ,
available : true ,
2024-08-05 21:26:48 +09:00
sorted : seq ++ ,
2023-12-24 03:39:06 +09:00
provider : {
id : "openai" ,
providerName : "OpenAI" ,
providerType : "openai" ,
2024-08-05 21:26:48 +09:00
sorted : 1 ,
2023-12-24 03:39:06 +09:00
} ,
2023-07-09 19:03:06 +09:00
} ) ) ;
2023-07-05 00:16:24 +09:00
}
2023-05-15 00:00:17 +09:00
}
2023-06-13 01:39:29 +09:00
export { OpenaiPath } ;