basic-ai
Version:
Super basic AI API powered by gpt-4o-mini
196 lines (190 loc) • 8.43 kB
JavaScript
export const Tool = (name = '', desc = '', params, strict = false) => {
name = name.replace(/[^a-zA-Z0-9-_]/g,'').slice(0, 64)
return `{"name":"${name}","description":${JSON.stringify(desc)}${params?',"parameters":'+JSON.stringify(params):''},"strict":${!!strict}}`
}
export const Response = (name = '', desc = '', params, strict = false) => {
name = name.replace(/[^a-zA-Z0-9-_]/g,'').slice(0, 64)
return `{"name":"${name}","description":${JSON.stringify(desc)},"schema":${JSON.stringify(params)},"strict":${!!strict}}`
}
const req = {method: 'POST', headers: {'Content-Type': 'application/json', Authorization: ''}, body: ''}
let m = `{"model":"gpt-4o-mini","messages":[`, _m = 'gpt-4o-mini', dp = ''
export const config = {
pricingFor: u => u.prompt_tokens*.00000015 - u.prompt_tokens_details.cached_tokens*.000000075 + u.completion_tokens*.0000006,
endpoint: 'https://api.openai.com/v1/chat/completions',
get model(){return _m}, set model(a){
m = `{"model":${JSON.stringify(_m = a)},"messages":[`
},
get token(){ return req.headers.Authorization.slice(7) },
set token(a){ req.headers.Authorization = 'Bearer '+a },
}
class SimpleStream{
[Symbol.asyncIterator](){ return this }
next(){ return this }
#v = ''; #e = undefined; done = false
#r = null; #c = null
then(r, c){
if(this.e !== undefined){
try{c(this.e)}catch(e){Promise.reject(e)}
}else if(this.#v){
try{r({value: this.#v, done: false})}catch(e){Promise.reject(e)}
this.#v = ''
}else if(this.done){
try{r({value: null, done: true})}catch(e){Promise.reject(e)}
}else this.#r = r, this.#c = c
}
_append(v){ const r=this.#r; if(r){ this.#r = this.#c = null; try{r({value: v, done: false})}catch(e){Promise.reject(e)} }else this.#v += v }
_reject(e = null){ this.#e = e; const c=this.#c; if(c){ this.#r = this.#c = null; try{c(e)}catch(e){Promise.reject(e)} } }
_end(){ this.done = true; if(this.#r){ try{ this.#r({value: null, done: true}) }catch(e){Promise.reject(e)}; this.#r = this.#c = null } }
}
export class Chat extends Array{
constructor(ctx=10){ super(); this.context = ctx }
_sticky = 0
usage = 0
temperature = 0.5
addUserMessage(txt='', name=''){
name = name.replace(/[^a-zA-Z0-9-_]/g,'').slice(0, 64)
this.push(`{"role":"user"${name?`,"name":"${name}"`:''},"content":${JSON.stringify(txt)}}`)
const i = this.length-this.context-this._sticky
if(i>=0) this.splice(this._sticky,i)
}
addSystemMessage(txt='', name=''){
name = name.replace(/[^a-zA-Z0-9-_]/g,'').slice(0, 64)
this.push(`{"role":"system"${name?`,"name":"${name}"`:''},"content":${JSON.stringify(txt)}}`)
const i = this.length-this.context-this._sticky
if(i>=0) this.splice(this._sticky,i)
}
addAssistantMessage(txt='',name=''){
name = name.replace(/[^a-zA-Z0-9-_]/g,'').slice(0, 64)
this.push(`{"role":"assistant"${name?`,"name":"${name}"`:''},"content":${JSON.stringify(txt)}}`)
const i = this.length-this.context-this._sticky
if(i>=0) this.splice(this._sticky,i)
}
clear(sticky = false){
this.length = sticky ? (this._sticky = 0) : this._sticky
}
setSticky(clearOld = true){
if(clearOld && this._sticky) this.splice(0, this._sticky)
this._sticky = this.length
}
get(tools = null, responseJSON = false, insert = true, maxTokens = 128, name = '', userData = null){
if(!this.length) throw 'No messages!'
let t = ']'
if(tools){
t = '],"tools":[{"type":"function","function":'
for(const k in tools) (t.length>41&&(t+='},{"type":"function","function":'), t+=k)
if(t.length<=40) t=''
else t += '}]'
}
req.body = m+super.join(',')+t+`${!responseJSON?'':typeof responseJSON!='string'?',"response_format":{"type":"json_object"}':`,"response_format":{"type":"json_schema","json_schema":${responseJSON}}`},"temperature":${+this.temperature},"top_p":1,"frequency_penalty":0.5,"presence_penalty":1.5,"max_tokens":${+maxTokens}}`
return fetch(config.endpoint, req).then(a => a.json()).then(res => {
if(res.error || res.code) throw res.error || res.message
this.usage += config.pricingFor(res.usage)
const msg = res.choices[0].message
if(msg.refusal) throw msg.refusal
if(name) msg.name = name
if(msg.tool_calls === null) delete msg.tool_calls
let t = insert ? JSON.stringify(msg) : '', w = 0
const done = () => {
if(insert){
this.push(t)
const i = this.length - this.context - this._sticky
if(i >= 0) this.splice(this._sticky, i)
}
}
if(msg.tool_calls){ msg.content = ''; for(const {function:f,id} of msg.tool_calls){
for(const k in tools) if(k.slice(9,k.indexOf('"',9)) == f.name){
try{
const v = tools[k]?.(JSON.parse(f.arguments), userData)
if(!t) break
if(typeof v?.then == 'function') w++, v.then(v => (t += `,{"role":"tool","tool_call_id":"${id}","content":${JSON.stringify(''+(v??''))}}`, --w||done()), e => {
t += `{"role":"tool","tool_call_id":"${id}","content":null}`
--w||done(); throw e
})
else t += `,{"role":"tool","tool_call_id":"${id}","content":${JSON.stringify(''+(v??''))}}`
}catch(e){Promise.reject(e); if(t) t += `{"role":"tool","tool_call_id":"${id}","content":null}`}
break
}
} }
if(!w) done()
return responseJSON ? JSON.parse(msg.content) : msg.content || ''
})
}
stream(tools = null, responseJSON = false, insert = true, maxTokens = 128, name = '', userData = null){
if(!this.length) throw 'No messages!'
let t = ']'
if(tools){
t = '],"tools":[{"type":"function","function":'
for(const k in tools) (t.length>41&&(t+='},{"type":"function","function":'), t+=k)
if(t.length<=40) t=''
else t += '}]'
}
req.body = m+super.join(',')+t+`${!responseJSON?'':typeof responseJSON!='string'?',"response_format":{"type":"json_object"}':`,"response_format":{"type":"json_schema","json_schema":${responseJSON}}`},"temperature":${+this.temperature},"top_p":1,"frequency_penalty":0.5,"presence_penalty":1.5,"max_tokens":${+maxTokens},"stream":true,"stream_options":{"include_usage":true}}`
let resStr = new SimpleStream()
fetch(config.endpoint, req).then(async res => {
let v = '', msg = null
for await(const chunk of res.body.pipeThrough(new TextDecoderStream())){
v += chunk
let i = v.indexOf('\n\n')
while(i >= 0){
const line = v.slice(0, i)
const j = line.indexOf(':'), dat = line.slice(j+1).trim()
if(j < 0 || line.slice(0,j).trim() != 'data'){
// err
r.cancel()
resStr._reject(dat)
throw dat
}
v = v.slice(i+2)
i = v.indexOf('\n\n')
if(dat[0] == '[') continue
const ch = JSON.parse(dat)
if(ch.usage) this.usage += config.pricingFor(ch.usage)
if(!ch.choices.length) continue
if(!msg){ msg = ch.choices[0].delta; continue }
const d = ch.choices[0].delta
if(d.content) msg.content += d.content, resStr._append(d.content)
if(d.tool_calls){
msg.tool_calls ??= []
for(const t of d.tool_calls){
const i = t.index>>>0
if(i>=msg.tool_calls.length) delete t.index, msg.tool_calls.push(t)
else msg.tool_calls[Math.min(i,msg.tool_calls.length)].function.arguments += t.function.arguments
}
}
}
}
if(v){
msg = JSON.parse(v)
if(msg.error || msg.code) throw msg.error || msg.message
}
if(msg.refusal) throw msg.refusal
if(name) msg.name = name
if(msg.tool_calls === null) delete msg.tool_calls
let t = insert ? JSON.stringify(msg) : '', w = 0
const done = () => {
if(insert){
this.push(t)
const i = this.length - this.context - this._sticky
if(i >= 0) this.splice(this._sticky, i)
}
resStr._end()
}
if(msg.tool_calls){ msg.content = ''; for(const {function:f,id} of msg.tool_calls){
for(const k in tools) if(k.slice(9,k.indexOf('"',9)) == f.name){
try{
const v = tools[k]?.(JSON.parse(f.arguments), userData)
if(!t) break
if(typeof v?.then == 'function') w++, v.then(v => (t += `,{"role":"tool","tool_call_id":"${id}","content":${JSON.stringify(''+(v??''))}}`, --w||done()), e => {
t += `{"role":"tool","tool_call_id":"${id}","content":null}`
--w||done(); throw e
})
else t += `,{"role":"tool","tool_call_id":"${id}","content":${JSON.stringify(''+(v??''))}}`
}catch(e){Promise.reject(e); if(t) t += `{"role":"tool","tool_call_id":"${id}","content":null}`}
break
}
} }
if(!w) done()
}).catch(e => {resStr._reject(e)})
return resStr
}
}