From 9766782648617e232fbc4e40ea96a0e567c7cc73 Mon Sep 17 00:00:00 2001 From: polwex Date: Wed, 23 Jul 2025 05:41:58 +0700 Subject: something like that. man anthropic is old --- src/gemini.ts | 121 ++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 42 deletions(-) (limited to 'src/gemini.ts') 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 = new Map(); 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 { - 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 { + // 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 { + 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 { + async send(input: string | Content, systemPrompt?: string): AsyncRes { 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 } } -- cgit v1.2.3