Skip to content

Commit 6682c9a

Browse files
brrockcgoinglove
andauthored
feat: add openAI compatible provider support (#92)
This is a good starting point for ai sdk v5 and a temporary solution. # how it works So the user edits the config file generated by postinstall. On vercel a json env variable generated by an app I made. I may introduce importing from config file in that. --------- Co-authored-by: choi sung keun <86150470+cgoinglove@users.noreply.github.com> Co-authored-by: cgoing <neo.cgoing@gmail.com>
1 parent 09d1b8b commit 6682c9a

31 files changed

Lines changed: 541 additions & 185 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ local-data
5252
.cursorrules
5353
.cursor
5454
*.ignore
55-
.mcp-config.json
55+
.mcp-config.json
56+
openai-compatible.config.ts

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ Step-by-step setup guides for running and configuring MCP Client Chatbot.
221221

222222
- Configure Google and GitHub OAuth for secure user login support.
223223

224+
#### [Adding openAI like providers](docs/tips-guides/adding-openAI-like-providers.md)
225+
- Adding openAI like ai providers
224226
<br/>
225227

226228
## 💡 Tips
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Adding openAI like provider
2+
3+
## What is an openAI like provider
4+
5+
It is an api that is like the openAI one. They are used as llm providers.
6+
7+
## Adding providers - docker and local deployment - file method
8+
9+
1. Set up the app as you normally would
10+
2. Open `openai-compatible.config.ts` in an IDE
11+
3. Uncomment the example and remove the word example
12+
4. Modify the api url and other data to match your provider - to add more just copy and paste it and edit it, also add your secret key
13+
5. Run `pnpm openai-compatiable:parse` to update env when you change schema
14+
15+
## Adding providers - vercel or anywhere else - ui based method
16+
17+
1. Go to [this website](https://mcp-client-chatbot-openai-like.vercel.app/)
18+
2. Press generate JSON and copy it
19+
3. Put in this into your env as the `OPENAI_COMPATIBLE_DATA` variable
20+
4. Add the env variables you defined for provider secrets
21+
22+
### Editing
23+
24+
Copy the contents of the env into the import section at the top, then regenerate and update your env

next.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { NextConfig } from "next";
22
import createNextIntlPlugin from "next-intl/plugin";
3+
4+
35
export default () => {
46
const nextConfig: NextConfig = {
57
cleanDistDir: true,

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"format": "biome format --write",
1818
"check-types": "tsc --noEmit",
1919
"initial:env": "tsx scripts/initial-env.ts",
20+
"openai-compatiable:init": "tsx scripts/init-openai-compatiable.ts",
21+
"openai-compatiable:parse": "tsx scripts/parse-openai-compatiable.ts",
2022
"postinstall": "tsx scripts/postinstall.ts",
2123
"clean": "tsx scripts/clean.ts",
2224
"db:generate": "drizzle-kit generate",
@@ -39,6 +41,7 @@
3941
"@ai-sdk/anthropic": "^1.2.12",
4042
"@ai-sdk/google": "^1.2.18",
4143
"@ai-sdk/openai": "^1.3.22",
44+
"@ai-sdk/openai-compatible": "^0.2.14",
4245
"@ai-sdk/react": "^1.2.12",
4346
"@ai-sdk/xai": "^1.2.16",
4447
"@modelcontextprotocol/sdk": "^1.12.1",
@@ -126,10 +129,7 @@
126129
"vitest": "^3.1.4"
127130
},
128131
"lint-staged": {
129-
"*.{js,json,mjs,ts,yaml,tsx,css}": [
130-
"pnpm format",
131-
"pnpm lint:fix"
132-
]
132+
"*.{js,json,mjs,ts,yaml,tsx,css}": ["pnpm format", "pnpm lint:fix"]
133133
},
134134
"packageManager": "pnpm@10.2.1",
135135
"engines": {

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/init-openai-compatiable.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import path from "path";
2+
import * as fs from "fs";
3+
4+
const CONFIG_TEMPLATE = `
5+
import { type OpenAICompatibleProvider } from "./src/lib/ai/create-openai-compatiable";
6+
7+
const providers: OpenAICompatibleProvider[] = [
8+
// example
9+
// {
10+
// provider: "Groq",
11+
// apiKey: "123",
12+
// baseUrl: "https://api.groq.com/openai/v1",
13+
// models: [
14+
// {
15+
// apiName: "llama3-8b-8192",
16+
// uiName: "Llama 3 8B",
17+
// supportsTools: true,
18+
// },
19+
// {
20+
// apiName: "mixtral-8x7b-32768",
21+
// uiName: "Mixtral 8x7B",
22+
// supportsTools: true,
23+
// },
24+
// {
25+
// apiName: "gemma-7b-it",
26+
// uiName: "Gemma 7B IT",
27+
// supportsTools: false,
28+
// },
29+
// ],
30+
// },
31+
];
32+
33+
export default providers;
34+
35+
`.trim();
36+
37+
const ROOT = process.cwd();
38+
const FILE_NAME = "openai-compatible.config.ts";
39+
const CONFIG_PATH = path.join(ROOT, FILE_NAME);
40+
41+
function createConfigFile() {
42+
if (!fs.existsSync(CONFIG_PATH)) {
43+
try {
44+
fs.writeFileSync(CONFIG_PATH, CONFIG_TEMPLATE, "utf-8");
45+
console.log(`${FILE_NAME} file has been created.`);
46+
} catch (error) {
47+
console.error(`Error occurred while creating ${FILE_NAME} file.`);
48+
console.error(error);
49+
return false;
50+
}
51+
} else {
52+
console.info(`${FILE_NAME} file already exists. Skipping...`);
53+
}
54+
}
55+
56+
createConfigFile();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import "load-env";
4+
import logger from "logger";
5+
import { openaiCompatibleModelsSafeParse } from "lib/ai/create-openai-compatiable";
6+
7+
const ROOT = process.cwd();
8+
const FILE_NAME = "openai-compatible.config.ts";
9+
const CONFIG_PATH = path.join(ROOT, FILE_NAME);
10+
11+
async function load() {
12+
try {
13+
const config = await import(CONFIG_PATH).then((m) => m.default);
14+
return openaiCompatibleModelsSafeParse(config);
15+
} catch (error) {
16+
logger.error(error);
17+
return [];
18+
}
19+
}
20+
21+
/**
22+
* Reads a .env file, modifies a specific key's value, and writes it back.
23+
*
24+
* @param {string} envFilePath - The absolute path to the .env file.
25+
* @param {string} keyToModify - The key of the variable to add or edit (e.g., 'DATA').
26+
* @param {string} newValue - The new value for the variable.
27+
* @returns {boolean} - True if successful, false otherwise.
28+
*/
29+
function updateEnvVariable(
30+
envFilePath: string,
31+
keyToModify: string,
32+
newValue: string,
33+
): boolean {
34+
try {
35+
let envContent = "";
36+
if (fs.existsSync(envFilePath)) {
37+
envContent = fs.readFileSync(envFilePath, "utf8");
38+
}
39+
40+
const envVars: { [key: string]: string } = {};
41+
const lines = envContent.split("\n");
42+
43+
lines.forEach((line) => {
44+
const trimmedLine = line.trim();
45+
if (trimmedLine.startsWith("#") || trimmedLine === "") {
46+
return;
47+
}
48+
49+
const parts = trimmedLine.split("=");
50+
if (parts.length >= 2) {
51+
const key = parts[0];
52+
const value = parts.slice(1).join("=");
53+
envVars[key] = value;
54+
}
55+
});
56+
57+
envVars[keyToModify] = newValue;
58+
59+
let newEnvContent = "";
60+
for (const key in envVars) {
61+
if (Object.prototype.hasOwnProperty.call(envVars, key)) {
62+
newEnvContent += `${key}=${envVars[key]}\n`;
63+
}
64+
}
65+
66+
newEnvContent = newEnvContent.trim();
67+
68+
fs.writeFileSync(envFilePath, newEnvContent, "utf8");
69+
console.log(
70+
`Successfully updated ${keyToModify} in ${envFilePath} to: \n\n${newValue}\n`,
71+
);
72+
return true;
73+
} catch (error) {
74+
console.error(`Error updating .env file: ${error}`);
75+
return false;
76+
}
77+
}
78+
79+
const envPath = path.join(ROOT, ".env");
80+
81+
const openaiCompatibleProviders = await load();
82+
83+
const success = updateEnvVariable(
84+
envPath,
85+
"OPENAI_COMPATIBLE_DATA",
86+
JSON.stringify(openaiCompatibleProviders),
87+
);
88+
89+
if (success) {
90+
console.log("Operation completed. Check your .env file!");
91+
} else {
92+
console.log("Operation failed.");
93+
}

scripts/postinstall.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ async function main() {
3939
console.error("File based MCP config is not supported in Docker.");
4040
process.exit(1);
4141
}
42-
console.log("Running in Docker, nothing to do.");
4342
} else {
4443
console.log(
4544
"Running in a normal environment, performing initial environment setup.",
4645
);
4746
await runCommand("pnpm initial:env", "Initial environment setup");
47+
await runCommand(
48+
"pnpm openai-compatiable:init",
49+
"Initial openAI compatiable config setup",
50+
);
4851
}
4952
}
5053

src/app/(chat)/mcp/test/[id]/page.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,19 @@ import { appStore } from "@/app/store";
5454
import {
5555
Select,
5656
SelectContent,
57+
SelectGroup,
5758
SelectItem,
59+
SelectLabel,
5860
SelectTrigger,
5961
SelectValue,
6062
} from "ui/select";
61-
import { customModelProvider } from "lib/ai/models";
6263
import { MCPToolInfo } from "app-types/mcp";
6364
import { Label } from "ui/label";
6465
import { safe } from "ts-safe";
6566
import { useObjectState } from "@/hooks/use-object-state";
6667
import { useTranslations } from "next-intl";
68+
import { useChatModels } from "@/hooks/queries/use-chat-models";
69+
import { ChatModel } from "app-types/chat";
6770

6871
// Type definitions
6972
type SchemaProperty = {
@@ -264,34 +267,31 @@ const GenerateExampleInputJsonDialog = ({
264267
children,
265268
onGenerated,
266269
}: PropsWithChildren<GenerateExampleInputJsonDialogProps>) => {
267-
const currentModelName = appStore((state) => state.model);
270+
const currentModelName = appStore((state) => state.chatModel);
268271
const t = useTranslations();
269272

273+
const { data: providers } = useChatModels();
274+
270275
const [option, setOption] = useObjectState({
271276
open: false,
272277
model: currentModelName,
273278
prompt: "",
274279
loading: false,
275280
});
276-
const modelList = useMemo(() => {
277-
return customModelProvider.modelsInfo.flatMap((v) =>
278-
v.models.map((v) => v.name),
279-
);
280-
}, []);
281281

282282
const generateExampleSchema = useCallback(() => {
283283
safe(() => setOption({ loading: true }))
284284
.map(() =>
285285
generateExampleToolSchemaAction({
286-
modelName: option.model,
286+
model: option.model,
287287
toolInfo: toolInfo,
288288
prompt: option.prompt,
289289
}),
290290
)
291291
.ifOk((result) => {
292292
onGenerated(JSON.stringify(result, null, 2));
293293
})
294-
.ifOk(() => {
294+
.watch(() => {
295295
setOption({
296296
loading: false,
297297
prompt: "",
@@ -317,17 +317,31 @@ const GenerateExampleInputJsonDialog = ({
317317
<div className="flex flex-col gap-2 py-4 text-foreground">
318318
<Label>Model</Label>
319319
<Select
320-
value={option.model}
321-
onValueChange={(value) => setOption({ model: value })}
320+
value={JSON.stringify(option.model ?? "{}")}
321+
onValueChange={(value) => {
322+
const model = JSON.parse(value) as ChatModel;
323+
setOption({ model });
324+
}}
322325
>
323326
<SelectTrigger className="min-w-48">
324327
<SelectValue placeholder="Select a model" />
325328
</SelectTrigger>
326329
<SelectContent>
327-
{modelList.map((model) => (
328-
<SelectItem key={model} value={model}>
329-
{model}
330-
</SelectItem>
330+
{providers?.map((provider) => (
331+
<SelectGroup key={provider.provider}>
332+
<SelectLabel>{provider.provider}</SelectLabel>
333+
{provider.models.map((model) => (
334+
<SelectItem
335+
key={model.name}
336+
value={JSON.stringify({
337+
provider: provider.provider,
338+
model: model.name,
339+
})}
340+
>
341+
{model.name}
342+
</SelectItem>
343+
))}
344+
</SelectGroup>
331345
))}
332346
</SelectContent>
333347
</Select>

0 commit comments

Comments
 (0)