Skip to content

Commit 2409bc5

Browse files
committed
Add support for custom provider
1 parent b873541 commit 2409bc5

5 files changed

Lines changed: 68 additions & 5 deletions

File tree

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,29 @@ Create an RLLM instance with sensible defaults.
100100
```typescript
101101
const rlm = createRLLM({
102102
model: 'gpt-4o-mini', // Model name
103-
provider: 'openai', // 'openai' | 'anthropic' | 'openrouter'
103+
provider: 'openai', // 'openai' | 'anthropic' | 'openrouter' | 'custom'
104104
apiKey: process.env.KEY, // Optional, uses env vars by default
105+
baseUrl: undefined, // Optional, required for 'custom' provider
105106
verbose: true, // Enable logging
106107
});
107108
```
108109

110+
### Custom Provider (OpenAI-Compatible APIs)
111+
112+
Use the `custom` provider to connect to any OpenAI-compatible API (e.g., vLLM, Ollama, LM Studio, Azure OpenAI):
113+
114+
```typescript
115+
const rlm = createRLLM({
116+
provider: 'custom',
117+
model: 'llama-3.1-8b',
118+
baseUrl: 'http://localhost:8000/v1', // Required for custom provider
119+
apiKey: 'your-api-key', // Optional, depends on your API
120+
verbose: true,
121+
});
122+
```
123+
124+
**Note:** When using `provider: 'custom'`, the `baseUrl` parameter is **required**. An error will be thrown if it's not provided.
125+
109126
### `RLLM` Methods
110127

111128
| Method | Description |

src/llm-client.test.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,34 @@
66
*/
77

88
import { describe, it, expect } from "vitest";
9+
import { LLMClient } from "./llm-client.js";
910

1011
// Test the provider configuration logic directly
1112
// (extracted from LLMClient to make it testable)
1213

13-
function getDefaultApiKey(provider: "openai" | "anthropic" | "openrouter"): string | undefined {
14+
function getDefaultApiKey(provider: "openai" | "anthropic" | "openrouter" | "custom"): string | undefined {
1415
switch (provider) {
1516
case "openai":
1617
return process.env["OPENAI_API_KEY"];
1718
case "anthropic":
1819
return process.env["ANTHROPIC_API_KEY"];
1920
case "openrouter":
2021
return process.env["OPENROUTER_API_KEY"];
22+
case "custom":
23+
return undefined;
2124
}
2225
}
2326

24-
function getDefaultBaseUrl(provider: "openai" | "anthropic" | "openrouter"): string | undefined {
27+
function getDefaultBaseUrl(provider: "openai" | "anthropic" | "openrouter" | "custom"): string | undefined {
2528
switch (provider) {
2629
case "openai":
2730
return undefined; // Uses default
2831
case "anthropic":
2932
return "https://api.anthropic.com/v1";
3033
case "openrouter":
3134
return "https://openrouter.ai/api/v1";
35+
case "custom":
36+
return undefined;
3237
}
3338
}
3439

@@ -74,5 +79,35 @@ describe("LLMClient provider configuration", () => {
7479

7580
process.env["OPENROUTER_API_KEY"] = original;
7681
});
82+
83+
it("returns undefined for custom provider (must be provided explicitly)", () => {
84+
expect(getDefaultApiKey("custom")).toBeUndefined();
85+
});
86+
});
87+
88+
describe("custom provider", () => {
89+
it("returns undefined for custom provider base URL", () => {
90+
expect(getDefaultBaseUrl("custom")).toBeUndefined();
91+
});
92+
93+
it("throws error when custom provider is used without baseUrl", () => {
94+
expect(() => {
95+
new LLMClient({
96+
provider: "custom",
97+
model: "my-model",
98+
apiKey: "test-key",
99+
});
100+
}).toThrow("Custom provider requires a baseUrl to be specified");
101+
});
102+
103+
it("creates client successfully when custom provider has baseUrl", () => {
104+
const client = new LLMClient({
105+
provider: "custom",
106+
model: "my-model",
107+
apiKey: "test-key",
108+
baseUrl: "https://my-custom-api.example.com/v1",
109+
});
110+
expect(client).toBeDefined();
111+
});
77112
});
78113
});

src/llm-client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export class LLMClient {
4242
this.model = options.model;
4343
this.provider = options.provider;
4444

45+
// Validate: custom provider requires baseUrl
46+
if (options.provider === "custom" && !options.baseUrl) {
47+
throw new Error("Custom provider requires a baseUrl to be specified");
48+
}
49+
4550
const apiKey = options.apiKey ?? this.getDefaultApiKey(options.provider);
4651
const baseUrl = options.baseUrl ?? this.getDefaultBaseUrl(options.provider);
4752

@@ -56,6 +61,8 @@ export class LLMClient {
5661
return process.env["ANTHROPIC_API_KEY"];
5762
case "openrouter":
5863
return process.env["OPENROUTER_API_KEY"];
64+
case "custom":
65+
return undefined; // Must be provided explicitly
5966
}
6067
}
6168

@@ -67,6 +74,8 @@ export class LLMClient {
6774
return "https://api.anthropic.com/v1";
6875
case "openrouter":
6976
return "https://openrouter.ai/api/v1";
77+
case "custom":
78+
return undefined; // Must be provided explicitly (validated in constructor)
7079
}
7180
}
7281

src/rlm.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,15 +306,17 @@ export class RLLM {
306306
*/
307307
export function createRLLM(options: {
308308
model?: string;
309-
provider?: "openai" | "anthropic" | "openrouter";
309+
provider?: "openai" | "anthropic" | "openrouter" | "custom";
310310
apiKey?: string;
311+
baseUrl?: string;
311312
verbose?: boolean;
312313
}): RLLM {
313314
return new RLLM({
314315
client: {
315316
provider: options.provider ?? "openai",
316317
model: options.model ?? "gpt-4o-mini",
317318
apiKey: options.apiKey,
319+
baseUrl: options.baseUrl,
318320
},
319321
verbose: options.verbose,
320322
});

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type InferContextType<S> = S extends ZodType<infer T> ? T : string;
2323
// LLM Client Types
2424
// ============================================================================
2525

26-
export type LLMProvider = "openai" | "anthropic" | "openrouter";
26+
export type LLMProvider = "openai" | "anthropic" | "openrouter" | "custom";
2727

2828
export interface ChatMessage {
2929
role: "system" | "user" | "assistant" | "tool";

0 commit comments

Comments
 (0)