smoltalk 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,120 @@
1
1
  # Smoltalk
2
2
 
3
+ Smoltalk is a package that exposes a common interface across different LLM providers. It exists because I think it's important to have an npm package that allows users to try out different kinds of LLMs, and prevents vendor lock-in. Using a different LLM should be as simple as switching out a model name.
4
+
3
5
  ## Install
4
6
 
5
7
  ```bash
6
8
  pnpm install smoltalk
7
9
  ```
8
10
 
9
- ## Usage
11
+ ## Quickstart
10
12
 
11
13
  ```typescript
12
14
  import { getClient } from "smoltalk";
13
15
 
14
16
  const client = getClient({
15
- apiKey: process.env.GEMINI_API_KEY || "",
17
+ openAiApiKey: process.env.OPENAI_API_KEY || "",
18
+ googleApiKey: process.env.GEMINI_API_KEY || "",
16
19
  logLevel: "debug",
17
20
  model: "gemini-2.0-flash-lite",
18
21
  });
19
22
 
20
23
  async function main() {
21
- const resp = await client.text("Hello, how are you?");
24
+ const resp = await client.prompt("Hello, how are you?");
22
25
  console.log(resp);
23
26
  }
24
27
 
25
28
  main();
26
- ```
29
+ ```
30
+
31
+ ## Longer tutorial
32
+ To use Smoltak, you first create a client:
33
+
34
+ ```ts
35
+ import { getClient } from "smoltalk";
36
+
37
+ const client = getClient({
38
+ openAiApiKey: process.env.OPENAI_API_KEY || "",
39
+ googleApiKey: process.env.GEMINI_API_KEY || "",
40
+ logLevel: "debug",
41
+ model: "gemini-2.0-flash-lite",
42
+ });
43
+ ```
44
+
45
+ Then you can call different methods on the client. The simplest is `prompt`:
46
+
47
+ ```ts
48
+ const resp = await client.prompt("Hello, how are you?");
49
+ ```
50
+
51
+ If you want tool calling, structured output, etc., `text` may be a cleaner option:
52
+
53
+ ```ts
54
+ let messages: Message[] = [];
55
+ messages.push(
56
+ userMessage(
57
+ "Please use the add function to add the following numbers: 3 and 5"
58
+ )
59
+ );
60
+ const resp = await client.text({
61
+ messages,
62
+ });
63
+ ```
64
+
65
+ Here is an example with tool calling:
66
+
67
+ ```ts
68
+ function add({ a, b }: { a: number; b: number }): number {
69
+ return a + b;
70
+ }
71
+
72
+ const addTool = {
73
+ name: "add",
74
+ description: "Adds two numbers together and returns the result.",
75
+ schema: z.object({
76
+ a: z.number().describe("The first number to add"),
77
+ b: z.number().describe("The second number to add"),
78
+ }),
79
+ };
80
+
81
+ const resp = await client.text({
82
+ messages,
83
+ tools: [addTool]
84
+ });
85
+
86
+ ```
87
+
88
+ Here is an example with structured output:
89
+
90
+ ```ts
91
+ const resp = await client.text({
92
+ messages,
93
+ responseFormat: z.object({
94
+ result: z.number(),
95
+ });
96
+ });
97
+ ```
98
+
99
+ A couple of design decisions to note:
100
+ - You specify different API keys using different parameter names. This means you could set a couple of different API keys and then be able to change the model name without worrying about the keys, which makes things easier for code generation.
101
+ - The schema for tools and structured outputs is defined using Zod.
102
+ - Parameter names are camel case, as that is the naming convention in TypeScript. They are converted to snake case for you if required by the APIs.
103
+
104
+ ## Prior art
105
+ - Langchain
106
+ OpenRouter
107
+ - Vercel AI
108
+
109
+ These are all good options, but they are quite heavy, and I wanted a lighter option. That said, you may be better off with one of the above alternatives:
110
+ - They are backed by a business and are more likely to be responsive.
111
+ - They support way more functionality and providers. Smoltalk currently supports just a subset of functionality for OpenAI and Google.
112
+
113
+ ## Functionality
114
+ Smoltalk pretty much lets you generate text using an OpenAI or Google model, with support for function calling and structured output, and that's it. I will add functionality and providers sporadically when I have time and need.
115
+
116
+ ## Contributing
117
+ This repo could use some help! Any of the following contributions would be helpful:
118
+ - Adding support for API parameters or endpoints
119
+ - Adding support for different providers
120
+ - Updating the list of models
@@ -1,3 +1,4 @@
1
+ import { FunctionCall } from "@google/genai";
1
2
  export type ToolCallOptions = {};
2
3
  export declare class ToolCall {
3
4
  private _id;
@@ -8,4 +9,8 @@ export declare class ToolCall {
8
9
  get id(): string;
9
10
  get name(): string;
10
11
  get arguments(): Record<string, any>;
12
+ toOpenAI(): any;
13
+ toGoogle(): {
14
+ functionCall: FunctionCall;
15
+ };
11
16
  }
@@ -30,4 +30,22 @@ export class ToolCall {
30
30
  get arguments() {
31
31
  return this._arguments;
32
32
  }
33
+ toOpenAI() {
34
+ return {
35
+ id: this._id,
36
+ type: "function",
37
+ function: {
38
+ name: this.name,
39
+ arguments: JSON.stringify(this.arguments),
40
+ },
41
+ };
42
+ }
43
+ toGoogle() {
44
+ return {
45
+ functionCall: {
46
+ name: this.name,
47
+ args: this.arguments,
48
+ },
49
+ };
50
+ }
33
51
  }
@@ -2,30 +2,28 @@ import { BaseMessage, MessageClass } from "./BaseMessage.js";
2
2
  import { TextPart } from "../../types.js";
3
3
  import { ChatCompletionMessageParam } from "openai/resources";
4
4
  import { Content } from "@google/genai";
5
+ import { ToolCall } from "../ToolCall.js";
5
6
  export declare class AssistantMessage extends BaseMessage implements MessageClass {
6
7
  _role: "assistant";
7
8
  _content: string | Array<TextPart> | null;
8
9
  _name?: string;
9
10
  _audio?: any | null;
10
- _function_call?: any | null;
11
11
  _refusal?: string | null;
12
- _tool_calls?: Array<any>;
12
+ _toolCalls?: ToolCall[];
13
13
  _rawData?: any;
14
14
  constructor(content: string | Array<TextPart> | null, options?: {
15
15
  name?: string;
16
16
  audio?: any | null;
17
- function_call?: any | null;
18
17
  refusal?: string | null;
19
- tool_calls?: Array<any>;
18
+ toolCalls?: ToolCall[];
20
19
  rawData?: any;
21
20
  });
22
21
  get content(): string;
23
22
  get role(): "assistant";
24
23
  get name(): string | undefined;
25
24
  get audio(): any | null | undefined;
26
- get function_call(): any | null | undefined;
27
25
  get refusal(): string | null | undefined;
28
- get tool_calls(): Array<any> | undefined;
26
+ get toolCalls(): ToolCall[] | undefined;
29
27
  get rawData(): any;
30
28
  toOpenAIMessage(): ChatCompletionMessageParam;
31
29
  toGoogleMessage(): Content;
@@ -4,18 +4,16 @@ export class AssistantMessage extends BaseMessage {
4
4
  _content;
5
5
  _name;
6
6
  _audio;
7
- _function_call;
8
7
  _refusal;
9
- _tool_calls;
8
+ _toolCalls;
10
9
  _rawData;
11
10
  constructor(content, options = {}) {
12
11
  super();
13
12
  this._content = content;
14
13
  this._name = options.name;
15
14
  this._audio = options.audio;
16
- this._function_call = options.function_call;
17
15
  this._refusal = options.refusal;
18
- this._tool_calls = options.tool_calls;
16
+ this._toolCalls = options.toolCalls;
19
17
  this._rawData = options.rawData;
20
18
  }
21
19
  get content() {
@@ -35,22 +33,33 @@ export class AssistantMessage extends BaseMessage {
35
33
  get audio() {
36
34
  return this._audio;
37
35
  }
38
- get function_call() {
39
- return this._function_call;
40
- }
41
36
  get refusal() {
42
37
  return this._refusal;
43
38
  }
44
- get tool_calls() {
45
- return this._tool_calls;
39
+ get toolCalls() {
40
+ return this._toolCalls;
46
41
  }
47
42
  get rawData() {
48
43
  return this._rawData;
49
44
  }
50
45
  toOpenAIMessage() {
51
- return { role: this.role, content: this.content, name: this.name };
46
+ return {
47
+ role: this.role,
48
+ content: this.content,
49
+ name: this.name,
50
+ tool_calls: this.toolCalls?.map((tc) => tc.toOpenAI()),
51
+ };
52
52
  }
53
53
  toGoogleMessage() {
54
- return { role: "model", parts: [{ text: this.content }] };
54
+ const parts = [];
55
+ if (this.content) {
56
+ parts.push({ text: this.content });
57
+ }
58
+ if (this.toolCalls) {
59
+ for (const tc of this.toolCalls) {
60
+ parts.push(tc.toGoogle());
61
+ }
62
+ }
63
+ return { role: "model", parts };
55
64
  }
56
65
  }
@@ -23,7 +23,11 @@ export class UserMessage extends BaseMessage {
23
23
  return this._rawData;
24
24
  }
25
25
  toOpenAIMessage() {
26
- return { role: this.role, content: this.content, name: this.name };
26
+ return {
27
+ role: this.role,
28
+ content: this.content,
29
+ name: this.name,
30
+ };
27
31
  }
28
32
  toGoogleMessage() {
29
33
  return { role: this.role, parts: [{ text: this.content }] };
@@ -17,9 +17,8 @@ export declare function userMessage(content: string, options?: {
17
17
  export declare function assistantMessage(content: string | Array<TextPart> | null, options?: {
18
18
  name?: string;
19
19
  audio?: any | null;
20
- function_call?: any | null;
21
20
  refusal?: string | null;
22
- tool_calls?: Array<any>;
21
+ toolCalls?: Array<any>;
23
22
  rawData?: any;
24
23
  }): AssistantMessage;
25
24
  export declare function developerMessage(content: string | Array<TextPart>, options?: {
@@ -1,9 +1,9 @@
1
1
  import { GoogleGenAI } from "@google/genai";
2
+ import { ToolCall } from "../classes/ToolCall.js";
2
3
  import { getLogger } from "../logger.js";
3
4
  import { success, } from "../types.js";
5
+ import { zodToGoogleTool } from "../util/tool.js";
4
6
  import { BaseClient } from "./baseClient.js";
5
- import { ToolCall } from "../classes/ToolCall.js";
6
- import { convertOpenAIToolToGoogle } from "../util/common.js";
7
7
  export class SmolGoogle extends BaseClient {
8
8
  client;
9
9
  logger;
@@ -22,11 +22,19 @@ export class SmolGoogle extends BaseClient {
22
22
  }
23
23
  async text(config) {
24
24
  const messages = config.messages.map((msg) => msg.toGoogleMessage());
25
- const tools = (config.tools || []).map((tool) => convertOpenAIToolToGoogle(tool));
25
+ const tools = (config.tools || []).map((tool) => {
26
+ return zodToGoogleTool(tool.name, tool.schema, {
27
+ description: tool.description,
28
+ });
29
+ });
26
30
  const genConfig = {};
27
31
  if (tools.length > 0) {
28
32
  genConfig.tools = [{ functionDeclarations: tools }];
29
33
  }
34
+ if (config.responseFormat) {
35
+ genConfig.responseMimeType = "application/json";
36
+ genConfig.responseJsonSchema = config.responseFormat.toJSONSchema();
37
+ }
30
38
  const request = {
31
39
  contents: messages,
32
40
  model: this.model,
@@ -4,6 +4,7 @@ import { ToolCall } from "../classes/ToolCall.js";
4
4
  import { isFunctionToolCall } from "../util.js";
5
5
  import { getLogger } from "../logger.js";
6
6
  import { BaseClient } from "./baseClient.js";
7
+ import { zodToOpenAITool } from "../util/tool.js";
7
8
  export class SmolOpenAi extends BaseClient {
8
9
  client;
9
10
  logger;
@@ -22,12 +23,26 @@ export class SmolOpenAi extends BaseClient {
22
23
  }
23
24
  async text(config) {
24
25
  const messages = config.messages.map((msg) => msg.toOpenAIMessage());
25
- const completion = await this.client.chat.completions.create({
26
+ const request = {
26
27
  model: this.model,
27
28
  messages,
28
- tools: config.tools,
29
- response_format: config.responseFormat,
30
- });
29
+ tools: config.tools?.map((tool) => {
30
+ return zodToOpenAITool(tool.name, tool.schema, {
31
+ description: tool.description,
32
+ });
33
+ }),
34
+ };
35
+ if (config.responseFormat) {
36
+ request.response_format = {
37
+ type: "json_schema",
38
+ json_schema: {
39
+ name: config.responseFormatName || "response",
40
+ schema: config.responseFormat.toJSONSchema(),
41
+ },
42
+ };
43
+ }
44
+ this.logger.debug("Sending request to OpenAI:", JSON.stringify(request, null, 2));
45
+ const completion = await this.client.chat.completions.create(request);
31
46
  this.logger.debug("Response from OpenAI:", JSON.stringify(completion, null, 2));
32
47
  const message = completion.choices[0].message;
33
48
  const output = message.content;
@@ -43,9 +58,6 @@ export class SmolOpenAi extends BaseClient {
43
58
  }
44
59
  }
45
60
  }
46
- if (toolCalls.length > 0) {
47
- this.logger.debug("Tool calls detected:", toolCalls);
48
- }
49
61
  return success({ output, toolCalls });
50
62
  }
51
63
  }
package/dist/types.d.ts CHANGED
@@ -1,19 +1,24 @@
1
1
  export * from "./types/result.js";
2
2
  import { LogLevel } from "egonlog";
3
- import { ModelName } from "./models.js";
3
+ import { ZodType } from "zod";
4
4
  import { Message } from "./classes/message/index.js";
5
- import { Result } from "./types/result.js";
6
5
  import { ToolCall } from "./classes/ToolCall.js";
7
- import { OpenAIToolDefinition } from "./util/common.js";
6
+ import { ModelName } from "./models.js";
7
+ import { Result } from "./types/result.js";
8
8
  export type PromptConfig = {
9
9
  messages: Message[];
10
- tools?: OpenAIToolDefinition[];
10
+ tools?: {
11
+ name: string;
12
+ description?: string;
13
+ schema: ZodType;
14
+ }[];
11
15
  instructions?: string;
12
16
  maxTokens?: number;
13
17
  temperature?: number;
14
18
  numSuggestions?: number;
15
19
  parallelToolCalls?: boolean;
16
- responseFormat?: any;
20
+ responseFormat?: ZodType;
21
+ responseFormatName?: string;
17
22
  rawAttributes?: Record<string, any>;
18
23
  };
19
24
  export type SmolConfig = {
@@ -1,15 +1,23 @@
1
1
  import { FunctionDeclaration } from "@google/genai";
2
- /**
3
- * OpenAI tool definition format
4
- */
5
- export interface OpenAIToolDefinition {
2
+ import { z } from "zod";
3
+ type OpenAIToolParameters = {
4
+ type: "object";
5
+ properties: Record<string, any>;
6
+ required?: string[];
7
+ additionalProperties?: boolean;
8
+ };
9
+ type OpenAITool = {
6
10
  type: "function";
7
11
  function: {
8
12
  name: string;
9
- description?: string;
10
- parameters: Record<string, unknown>;
13
+ description: string;
14
+ parameters: OpenAIToolParameters;
11
15
  };
12
- }
16
+ };
17
+ export declare function zodToOpenAITool(name: string, schema: z.ZodType, options?: Partial<{
18
+ description?: string;
19
+ strict?: boolean;
20
+ }>): OpenAITool;
13
21
  /**
14
22
  * Converts an OpenAI tool definition to a Google FunctionDeclaration format
15
23
  *
@@ -51,4 +59,9 @@ export interface OpenAIToolDefinition {
51
59
  * // }
52
60
  * ```
53
61
  */
54
- export declare function convertOpenAIToolToGoogle(openAITool: OpenAIToolDefinition): FunctionDeclaration;
62
+ export declare function openAIToGoogleTool(openAITool: OpenAITool): FunctionDeclaration;
63
+ export declare function zodToGoogleTool(name: string, schema: z.ZodType, options?: Partial<{
64
+ description?: string;
65
+ strict?: boolean;
66
+ }>): FunctionDeclaration;
67
+ export {};
@@ -1,3 +1,38 @@
1
+ export function zodToOpenAITool(name, schema, options = {}) {
2
+ // Convert Zod schema to JSON Schema
3
+ const jsonSchema = schema.toJSONSchema();
4
+ let description = "";
5
+ if (options?.description) {
6
+ description = options.description;
7
+ }
8
+ else if (typeof jsonSchema === "object" &&
9
+ "description" in jsonSchema &&
10
+ typeof jsonSchema.description === "string") {
11
+ description = jsonSchema.description;
12
+ }
13
+ // Build the parameters object
14
+ const parameters = {
15
+ type: "object",
16
+ properties: jsonSchema.properties || {},
17
+ required: jsonSchema.required || [],
18
+ };
19
+ const strict = options?.strict || false;
20
+ /* The additionalProperties field in an OpenAI schema,
21
+ which is based on JSON Schema, controls how the API handles
22
+ properties not explicitly listed in the properties section of an object.
23
+ By default, additionalProperties is set to true, which allows
24
+ the JSON object to contain any extra properties not defined in the schema.
25
+ */
26
+ parameters.additionalProperties = !strict;
27
+ return {
28
+ type: "function",
29
+ function: {
30
+ name,
31
+ description,
32
+ parameters,
33
+ },
34
+ };
35
+ }
1
36
  /**
2
37
  * Removes properties that Google's API doesn't support from JSON schemas
3
38
  */
@@ -66,10 +101,14 @@ function removeUnsupportedProperties(obj) {
66
101
  * // }
67
102
  * ```
68
103
  */
69
- export function convertOpenAIToolToGoogle(openAITool) {
104
+ export function openAIToGoogleTool(openAITool) {
70
105
  return {
71
106
  name: openAITool.function.name,
72
107
  description: openAITool.function.description,
73
108
  parametersJsonSchema: removeUnsupportedProperties(openAITool.function.parameters),
74
109
  };
75
110
  }
111
+ export function zodToGoogleTool(name, schema, options = {}) {
112
+ const openAITool = zodToOpenAITool(name, schema, options);
113
+ return openAIToGoogleTool(openAITool);
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "scripts": {
@@ -8,7 +8,8 @@
8
8
  "test:tsc": "tsc -p tests/tsconfig.json",
9
9
  "coverage": "vitest --coverage",
10
10
  "build": "rm -rf dist && tsc",
11
- "start": "cd dist && node index.js"
11
+ "start": "cd dist && node index.js",
12
+ "doc": "typedoc --disableSources --out docs lib && prettier docs/ --write"
12
13
  },
13
14
  "files": [
14
15
  "./dist"
@@ -31,12 +32,16 @@
31
32
  "license": "ISC",
32
33
  "devDependencies": {
33
34
  "@types/node": "^25.0.3",
35
+ "prettier": "^3.7.4",
36
+ "termcolors": "github:egonSchiele/termcolors",
37
+ "typedoc": "^0.28.15",
34
38
  "typescript": "^5.9.3",
35
- "vitest": "^4.0.16"
39
+ "vitest": "^4.0.16",
40
+ "zod": "^4.3.5"
36
41
  },
37
42
  "dependencies": {
38
43
  "@google/genai": "^1.34.0",
39
44
  "egonlog": "^0.0.2",
40
45
  "openai": "^6.15.0"
41
46
  }
42
- }
47
+ }