diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/claude.ts | 104 | ||||
| -rw-r--r-- | src/gemini.ts | 121 | ||||
| -rw-r--r-- | src/generic.ts | 165 | ||||
| -rw-r--r-- | src/types/index.ts | 33 |
4 files changed, 247 insertions, 176 deletions
diff --git a/src/claude.ts b/src/claude.ts index 2a56bc1..629f9c2 100644 --- a/src/claude.ts +++ b/src/claude.ts @@ -1,26 +1,21 @@ import Claude from "@anthropic-ai/sdk"; import { RESPONSE_LENGTH } from "./logic/constants"; -import type { - AIModelAPI, - ChatMessage, - OChoice, - OChunk, - OMessage, -} from "./types"; +import type { AIModelAPI, ChatMessage } from "./types"; import { BOOKWORM_SYS } from "./prompts"; import type { AsyncRes } from "sortug"; +import type { MessageCreateParamsStreaming } from "@anthropic-ai/sdk/resources"; type Message = Claude.Messages.MessageParam; export default class ClaudeAPI implements AIModelAPI { - private model: string = "claude-3-7-sonnet-20250219"; + private model: string = "claude-opus-4-20250514"; tokenizer: (text: string) => number; maxTokens: number; // model: string = "claude-3-5-sonnet-20241022"; constructor( + model: string, maxTokens = 200_000, tokenizer: (text: string) => number = (text) => text.length / 3, - model?: string, ) { this.maxTokens = maxTokens; this.tokenizer = tokenizer; @@ -36,30 +31,17 @@ export default class ClaudeAPI implements AIModelAPI { }); } - private mapMessagesR1(input: ChatMessage[]): Message[] { - return input.reduce((acc: Message[], m, i) => { - const prev = acc[i - 1]; - const role: any = m.author === "claude" ? "assistant" : "user"; - const msg = { role, content: m.text }; - if (prev?.role === role) acc[i - 1] = msg; - else acc = [...acc, msg]; - return acc; - }, []); - } - - public async send(sys: string, input: ChatMessage[]) { - const messages = this.mapMessages(input); + public async send(input: string | ChatMessage[], sys?: string) { + const msgs: ChatMessage[] = + typeof input === "string" + ? [{ author: "user", text: input, sent: 0 }] + : input; + const messages = this.mapMessages(msgs); const truncated = this.truncateHistory(messages); - const res = await this.apiCall(sys, truncated); + const res = await this.apiCall(truncated, sys); return res; } - public async sendR1(input: ChatMessage[]) { - const messages = this.mapMessagesR1(input); - const truncated = this.truncateHistory(messages); - const res = await this.apiCall("", truncated, true); - return res; - } public async sendDoc(data: string) { const sys = BOOKWORM_SYS; const msg: Message = { @@ -75,24 +57,22 @@ export default class ClaudeAPI implements AIModelAPI { }, ], }; - const res = await this.apiCall(sys, [msg]); + const res = await this.apiCall([msg], sys); return res; } public async stream( - sys: string, - input: ChatMessage[], + input: string | ChatMessage[], handle: (c: any) => void, + sys?: string, ) { - const messages = this.mapMessages(input); + const msgs: ChatMessage[] = + typeof input === "string" + ? [{ author: "user", text: input, sent: 0 }] + : input; + const messages = this.mapMessages(msgs); const truncated = this.truncateHistory(messages); - await this.apiCallStream(sys, truncated, handle); - } - - public async streamR1(input: ChatMessage[], handle: (c: any) => void) { - const messages = this.mapMessagesR1(input); - const truncated = this.truncateHistory(messages); - await this.apiCallStream("", truncated, handle, true); + await this.apiCallStream(truncated, handle, sys); } private truncateHistory(messages: Message[]): Message[] { @@ -108,26 +88,30 @@ export default class ClaudeAPI implements AIModelAPI { // TODO // https://docs.anthropic.com/en/api/messages-examples#putting-words-in-claudes-mouth private async apiCall( - system: string, messages: Message[], - isR1: boolean = false, - ): Promise<AsyncRes<string[]>> { + system?: string, + ): Promise<AsyncRes<string>> { try { const claud = new Claude(); - // const list = await claud.models.list(); - // console.log(list.data); - const res = await claud.messages.create({ + const params = { model: this.model, max_tokens: RESPONSE_LENGTH, - system, messages, - }); - return { - ok: res.content.reduce((acc: string[], item) => { - if (item.type === "tool_use") return acc; - else return [...acc, item.text]; - }, []), }; + const res = await claud.messages.create( + system ? { ...params, system } : params, + ); + const resm: string = res.content.reduce((acc: string, item) => { + if (item.type === "tool_use") return acc; + else if (item.type === "text") return `${acc}\n${item.text}`; + else return acc; + }, ""); + // const resm = res.content.reduce((acc: string[], item) => { + // if (item.type === "tool_use") return acc; + // else if (item.type === "text") return [...acc, item.text] + // else return acc; + // }, []); + return { ok: resm }; } catch (e) { console.log(e, "error in claude api"); return { error: `${e}` }; @@ -135,20 +119,22 @@ export default class ClaudeAPI implements AIModelAPI { } private async apiCallStream( - system: string, messages: Message[], handle: (c: any) => void, - isR1: boolean = false, + system?: string, ): Promise<void> { try { const claud = new Claude(); - const stream = await claud.messages.create({ + const params = { model: this.model, max_tokens: RESPONSE_LENGTH, - system, messages, - stream: true, - }); + stream: true as true, + }; + const fparams: MessageCreateParamsStreaming = system + ? { ...params, system } + : params; + const stream = await claud.messages.create(fparams); for await (const part of stream) { if (part.type === "message_start") continue; diff --git a/src/gemini.ts b/src/gemini.ts index 3e636c2..5c7267b 100644 --- a/src/gemini.ts +++ b/src/gemini.ts @@ -1,21 +1,19 @@ +import mime from "mime-types"; import { Chat, + createPartFromBase64, + createPartFromUri, + createUserContent, GoogleGenAI, type Content, type GeneratedImage, type GeneratedVideo, + type Part, } from "@google/genai"; -import { RESPONSE_LENGTH } from "./logic/constants"; -import type { - AIModelAPI, - ChatMessage, - OChoice, - OChunk, - OMessage, -} from "./types"; -import type { AsyncRes } from "sortug"; +import type { AIModelAPI, InputToken } from "./types"; +import type { AsyncRes, Result } from "sortug"; -export default class GeminiAPI { +export default class GeminiAPI implements AIModelAPI { tokenizer: (text: string) => number; maxTokens: number; private model: string; @@ -23,70 +21,109 @@ export default class GeminiAPI { chats: Map<string, Chat> = new Map<string, Chat>(); constructor( + model?: string, maxTokens = 200_000, tokenizer: (text: string) => number = (text) => text.length / 3, - model?: string, ) { this.maxTokens = maxTokens; this.tokenizer = tokenizer; const gem = new GoogleGenAI({ apiKey: Bun.env["GEMINI_API_KEY"]! }); this.api = gem; - this.model = model || "gemini-2.5-pro-preview-05-06 "; + this.model = model || "gemini-2.5-pro"; } - createChat({ name, history }: { name?: string; history?: Content[] }) { - const chat = this.api.chats.create({ model: this.model, history }); - this.chats.set(name ? name : Date.now().toString(), chat); + // input data in gemini gets pretty involved + // + // data + // Union type + // data can be only one of the following: + // text + // string + // Inline text. + + // inlineData + // object (Blob) + // Inline media bytes. + + // functionCall + // object (FunctionCall) + // A predicted FunctionCall returned from the model that contains a string representing the FunctionDeclaration.name with the arguments and their values. + + // functionResponse + // object (FunctionResponse) + // The result output of a FunctionCall that contains a string representing the FunctionDeclaration.name and a structured JSON object containing any output from the function is used as context to the model. + + // fileData + // object (FileData) + // URI based data. + + // executableCode + // object (ExecutableCode) + // Code generated by the model that is meant to be executed. + + // codeExecutionResult + // object (CodeExecutionResult) + // Result of executing the ExecutableCode. + + // metadata + // Union type + public setModel(model: string) { + this.model = model; } - async followChat(name: string, message: string): AsyncRes<string> { - const chat = this.chats.get(name); - if (!chat) return { error: "no chat with that name" }; - else { - const response = await chat.sendMessage({ message }); - const text = response.text; - return { ok: text || "" }; - } + private contentFromImage(imageString: string): Result<Part> { + // TODO + const mimeType = mime.lookup(imageString); + if (!mimeType) return { error: "no mimetype" }; + const url = URL.parse(imageString); + if (url) { + const part = createPartFromUri(imageString, mimeType); + return { ok: part }; + } else return { ok: createPartFromBase64(imageString, mimeType) }; } - async followChatStream( - name: string, - message: string, - handler: (data: string) => void, - ) { - const chat = this.chats.get(name); - if (!chat) throw new Error("no chat!"); - else { - const response = await chat.sendMessageStream({ message }); - for await (const chunk of response) { - const text = chunk.text; - handler(text || ""); - } + public buildInput(tokens: InputToken[]): Result<Content> { + try { + const input = createUserContent( + tokens.map((t) => { + if ("text" in t) return t.text; + if ("img" in t) { + const imagePart = this.contentFromImage(t.img); + if ("error" in imagePart) throw new Error("image failed"); + else return imagePart.ok; + } + return "oy vey"; + }), + ); + return { ok: input }; + } catch (e) { + return { error: `${e}` }; } } - async send(message: string, systemPrompt?: string): AsyncRes<string> { + async send(input: string | Content, systemPrompt?: string): AsyncRes<string> { try { const opts = { model: this.model, - contents: message, + contents: input, }; const fopts = systemPrompt ? { ...opts, config: { systemInstruction: systemPrompt } } : opts; const response = await this.api.models.generateContent(fopts); - return { ok: response.text || "" }; + if (!response.text) return { error: "no text in response" }; + return { ok: response.text }; } catch (e) { return { error: `${e}` }; } } - async sendStream( + async stream( + input: string | Content, handler: (s: string) => void, - message: string, systemPrompt?: string, ) { const opts = { model: this.model, - contents: message, + contents: input, }; const fopts = systemPrompt ? { ...opts, config: { systemInstruction: systemPrompt } } diff --git a/src/generic.ts b/src/generic.ts index 50c4435..ac6b55b 100644 --- a/src/generic.ts +++ b/src/generic.ts @@ -1,9 +1,13 @@ import OpenAI from "openai"; import { MAX_TOKENS, RESPONSE_LENGTH } from "./logic/constants"; -import type { AIModelAPI, ChatMessage, OChoice } from "./types"; +import type { AIModelAPI, ChatMessage, InputToken, OChoice } from "./types"; import type { AsyncRes } from "sortug"; - -type Message = OpenAI.Chat.Completions.ChatCompletionMessageParam; +import type { + ResponseCreateParamsBase, + ResponseCreateParamsNonStreaming, + ResponseCreateParamsStreaming, + ResponseInput, +} from "openai/resources/responses/responses.mjs"; type Props = { baseURL: string; @@ -31,25 +35,35 @@ export default class OpenAIAPI implements AIModelAPI { public setModel(model: string) { this.model = model; } - private mapMessages(input: ChatMessage[]): Message[] { - return input.map((m) => { - return { role: m.author as any, content: m.text, name: m.author }; - }); + + public buildInput(tokens: InputToken[]): ResponseInput { + return [ + { + role: "user", + content: tokens.map((t) => + "text" in t + ? { type: "input_text", text: t.text } + : "img" in t + ? { type: "input_image", image_url: t.img, detail: "auto" } + : { type: "input_text", text: "oy vey" }, + ), + }, + ]; } - public async send(sys: string, input: ChatMessage[]): AsyncRes<string[]> { - const messages = this.mapMessages(input); - const sysMsg: Message = { role: "system", content: sys }; - const allMessages = [sysMsg, ...messages]; - console.log("before truncation", allMessages); - const truncated = this.truncateHistory(allMessages); - const res = await this.apiCall(truncated); + // OpenAI SDK has three kinds ReponseInputContent: text image and file + // images can be URLs or base64 dataurl thingies + // + public async send( + input: string | ResponseInput, + sys?: string, + ): AsyncRes<string> { + const params = sys ? { instructions: sys, input } : { input }; + const res = await this.apiCall(params); if ("error" in res) return res; else { try { - // TODO type this properly - const choices: OChoice[] = res.ok; - return { ok: choices.map((c) => c.message.content!) }; + return { ok: res.ok.output_text }; } catch (e) { return { error: `${e}` }; } @@ -57,41 +71,29 @@ export default class OpenAIAPI implements AIModelAPI { } public async stream( - sys: string, - input: ChatMessage[], + input: string, handle: (c: string) => void, + sys?: string, ) { - const messages = this.mapMessages(input); - const sysMsg: Message = { role: "system", content: sys }; - const allMessages = [sysMsg, ...messages]; - const truncated = this.truncateHistory(allMessages); - await this.apiCallStream(truncated, handle); - } - - private truncateHistory(messages: Message[]): Message[] { - const totalTokens = messages.reduce((total, message) => { - return total + this.tokenizer(message.content as string); - }, 0); - while (totalTokens > this.maxTokens && messages.length > 1) { - // Always keep the system message if it exists - const startIndex = messages[0].role === "system" ? 1 : 0; - messages.splice(startIndex, 1); - } - return messages; + const params = sys ? { instructions: sys, input } : { input }; + await this.apiCallStream(params, handle); } // TODO custom temperature? - private async apiCall(messages: Message[]): AsyncRes<OChoice[]> { - console.log({ messages }, "at the very end"); + private async apiCall( + params: ResponseCreateParamsNonStreaming, + ): AsyncRes<OpenAI.Responses.Response> { try { - const completion = await this.api.chat.completions.create({ - temperature: 1.3, - model: this.model, - messages, - max_tokens: RESPONSE_LENGTH, + const res = await this.api.responses.create({ + ...params, + // temperature: 1.3, + model: params.model || this.model, + input: params.input, + max_output_tokens: params.max_output_tokens || RESPONSE_LENGTH, + stream: false, }); - if (!completion) return { error: "null response from openai" }; - return { ok: completion.choices }; + // TODO damn there's a lot of stuff here + return { ok: res }; } catch (e) { console.log(e, "error in openai api"); return { error: `${e}` }; @@ -99,30 +101,65 @@ export default class OpenAIAPI implements AIModelAPI { } private async apiCallStream( - messages: Message[], - handle: (c: string) => void, - ): Promise<void> { + params: ResponseCreateParamsBase, + handler: (c: string) => void, + ) { + // temperature: 1.3, + const pms: ResponseCreateParamsStreaming = { + ...params, + stream: true, + model: params.model || this.model, + input: params.input, + max_output_tokens: params.max_output_tokens || RESPONSE_LENGTH, + }; try { - const stream = await this.api.chat.completions.create({ - temperature: 1.3, - model: this.model, - messages, - max_tokens: RESPONSE_LENGTH, - stream: true, - }); - - for await (const chunk of stream) { - for (const choice of chunk.choices) { - console.log({ choice }); - if (!choice.delta) continue; - const cont = choice.delta.content; - if (!cont) continue; - handle(cont); + const stream = await this.api.responses.create(pms); + for await (const event of stream) { + console.log(event); + switch (event.type) { + // TODO deal with audio and whatever + case "response.output_text.delta": + handler(event.delta); + break; + case "response.completed": + break; + default: + break; } + // if (event.type === "response.completed") + // wtf how do we use this } } catch (e) { console.log(e, "error in openai api"); - handle(`Error streaming OpenAI, ${e}`); + return { error: `${e}` }; } } + + // private async apiCallStream( + // messages: Message[], + // handle: (c: string) => void, + // ): Promise<void> { + // try { + // const stream = await this.api.chat.completions.create({ + // temperature: 1.3, + // model: this.model, + // messages, + // max_tokens: RESPONSE_LENGTH, + // stream: true, + // }); + + // for await (const chunk of stream) { + // for (const choice of chunk.choices) { + // console.log({ choice }); + // if (!choice.delta) continue; + // const cont = choice.delta.content; + // if (!cont) continue; + // handle(cont); + // } + // } + // } catch (e) { + // console.log(e, "error in openai api"); + // handle(`Error streaming OpenAI, ${e}`); + // } + // } } diff --git a/src/types/index.ts b/src/types/index.ts index b276457..6c16e0d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,3 @@ -import type OpenAI from "openai"; import type { AsyncRes } from "sortug"; export type ChatMessage = { author: string; @@ -7,23 +6,35 @@ export type ChatMessage = { reasoning?: string; }; +export type InputToken = { text: string } | { img: string }; +// me +export type RequestOptions = { + textOutput: boolean; +}; +export const defaultOptions: RequestOptions = { + textOutput: true, +}; // openai -export type OChoice = OpenAI.Chat.Completions.ChatCompletion.Choice; -export type OChunk = OpenAI.Chat.Completions.ChatCompletionChunk.Choice; -export type OMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam; export type ContentType = { text: string } | { audio: Response }; -export type AIModelChoice = - | { name: "deepseek" | "chatgpt" | "claude" | "gemini" | "grok" } - | { other: { baseURL: string; apiKey: string } }; + export interface AIModelAPI { setModel: (model: string) => void; tokenizer: (text: string) => number; maxTokens: number; - send: (systemPrompt: string, input: ChatMessage[]) => AsyncRes<string[]>; + send: (input: string, systemPrompt?: string) => AsyncRes<string>; stream: ( - systemPrompt: string, - input: ChatMessage[], - handler: (data: any) => void, + input: string, + handler: (data: string) => void, + systemPrompt?: string, ) => void; } + +export type LLMChoice = + | { gemini: string } + | { claude: string } + | { chatgpt: string } + | { grok: string } + | { deepseek: string } + | { kimi: string } + | { openai: { url: string; apiKey: string; model: string } }; |
