summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-07-23 05:41:58 +0700
committerpolwex <polwex@sortug.com>2025-07-23 05:41:58 +0700
commit9766782648617e232fbc4e40ea96a0e567c7cc73 (patch)
tree084943c250a0ad6d7a3227a6b10b237ee07b2388 /src
parent42dd99bfac9777a4ecc6700b87edf26a5c984de6 (diff)
something like that. man anthropic is old
Diffstat (limited to 'src')
-rw-r--r--src/claude.ts104
-rw-r--r--src/gemini.ts121
-rw-r--r--src/generic.ts165
-rw-r--r--src/types/index.ts33
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 } };