mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 13:15:23 +02:00
Compare commits
17 Commits
core-model
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee1a48e0b0 | ||
|
|
f0400495b0 | ||
|
|
240e5cb12f | ||
|
|
2760c16ade | ||
|
|
79655b5673 | ||
|
|
384fdd01d6 | ||
|
|
c1d452bcf7 | ||
|
|
f39b511316 | ||
|
|
a2df52ea7c | ||
|
|
3e5a189177 | ||
|
|
2b9231dcd1 | ||
|
|
2c8eb54d7b | ||
|
|
f314c2f161 | ||
|
|
136edfd4c5 | ||
|
|
2f9987970a | ||
|
|
25cfe915d2 | ||
|
|
0af6dc3fc2 |
60
README.md
60
README.md
@@ -68,53 +68,21 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
[Github Sponsors](https://github.com/sponsors/Siumauricio)
|
||||
|
||||
<!-- Hero Sponsors 🎖 -->
|
||||
## Sponsors
|
||||
|
||||
<!-- Add Hero Sponsors here -->
|
||||
|
||||
### Hero Sponsors 🎖
|
||||
|
||||
<div>
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||
</a>
|
||||
<a href="https://awesome.tools/" target="_blank">
|
||||
<img src=".github/sponsors/awesome.png" width="200" height="150" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Premium Supporters 🥇 -->
|
||||
|
||||
<!-- Add Premium Supporters here -->
|
||||
|
||||
### Premium Supporters 🥇
|
||||
|
||||
<div>
|
||||
<a href="https://supafort.com/?ref=dokploy"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="300"/></a>
|
||||
<a href="https://agentdock.ai/?ref=dokploy"><img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/></a>
|
||||
</div>
|
||||
|
||||
<!-- Elite Contributors 🥈 -->
|
||||
|
||||
<!-- Add Elite Contributors here -->
|
||||
|
||||
### Elite Contributors 🥈
|
||||
|
||||
<div>
|
||||
<a href="https://americancloud.com/?ref=dokploy"><img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="300"/></a>
|
||||
<a href="https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy"><img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/></a>
|
||||
</div>
|
||||
|
||||
### Supporting Members 🥉
|
||||
|
||||
<div>
|
||||
|
||||
<a href="https://cloudblast.io/?ref=dokploy"><img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" width="250px" alt="Cloudblast.io"/></a>
|
||||
|
||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||
</div>
|
||||
| Sponsor | Logo | Supporter Level |
|
||||
|---------|:----:|----------------|
|
||||
| [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) | <img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/> | 🎖 Hero Sponsor |
|
||||
| [LX Aer](https://www.lxaer.com/?ref=dokploy) | <img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [LinkDR](https://linkdr.com/?ref=dokploy) | <img src="https://dokploy.com/linkdr-logo.svg" alt="LinkDR" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [LambdaTest](https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor) | <img src="https://www.lambdatest.com/blue-logo.png" alt="LambdaTest" width="200"/> | 🎖 Hero Sponsor |
|
||||
| [Awesome Tools](https://awesome.tools/) | <img src=".github/sponsors/awesome.png" alt="Awesome Tools" width="100"/> | 🎖 Hero Sponsor |
|
||||
| [Supafort](https://supafort.com/?ref=dokploy) | <img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="200"/> | 🥇 Premium Supporter |
|
||||
| [Agentdock](https://agentdock.ai/?ref=dokploy) | <img src=".github/sponsors/agentdock.png" alt="agentdock.ai" width="100"/> | 🥇 Premium Supporter |
|
||||
| [AmericanCloud](https://americancloud.com/?ref=dokploy) | <img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" width="200"/> | 🥈 Elite Contributor |
|
||||
| [Tolgee](https://tolgee.io/?utm_source=github_dokploy&utm_medium=banner&utm_campaign=dokploy) | <img src="https://dokploy.com/tolgee-logo.png" alt="Tolgee" width="100"/> | 🥈 Elite Contributor |
|
||||
| [Cloudblast](https://cloudblast.io/?ref=dokploy) | <img src="https://cloudblast.io/img/logo-icon.193cf13e.svg" alt="Cloudblast.io" width="150"/> | 🥉 Supporting Member |
|
||||
| [Synexa](https://synexa.ai/?ref=dokploy) | <img src=".github/sponsors/synexa.png" alt="Synexa" width="100"/> | 🥉 Supporting Member |
|
||||
|
||||
### Community Backers 🤝
|
||||
|
||||
|
||||
184
apps/dokploy/__test__/env/stack-environment.test.ts
vendored
Normal file
184
apps/dokploy/__test__/env/stack-environment.test.ts
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
import { getEnviromentVariablesObject } from "@dokploy/server/index";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const projectEnv = `
|
||||
ENVIRONMENT=staging
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
|
||||
PORT=3000
|
||||
`;
|
||||
|
||||
const environmentEnv = `
|
||||
NODE_ENV=development
|
||||
API_URL=https://api.dev.example.com
|
||||
REDIS_URL=redis://localhost:6379
|
||||
DATABASE_NAME=dev_database
|
||||
SECRET_KEY=env-secret-123
|
||||
`;
|
||||
|
||||
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
|
||||
it("resolves environment variables correctly for Stack compose", () => {
|
||||
const serviceEnv = `
|
||||
FOO=\${{environment.NODE_ENV}}
|
||||
BAR=\${{environment.API_URL}}
|
||||
BAZ=test
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
FOO: "development",
|
||||
BAR: "https://api.dev.example.com",
|
||||
BAZ: "test",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves both project and environment variables for Stack compose", () => {
|
||||
const serviceEnv = `
|
||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||
NODE_ENV=\${{environment.NODE_ENV}}
|
||||
API_URL=\${{environment.API_URL}}
|
||||
DATABASE_URL=\${{project.DATABASE_URL}}
|
||||
SERVICE_PORT=4000
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ENVIRONMENT: "staging",
|
||||
NODE_ENV: "development",
|
||||
API_URL: "https://api.dev.example.com",
|
||||
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
|
||||
SERVICE_PORT: "4000",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles multiple environment references in single value for Stack compose", () => {
|
||||
const multiRefEnv = `
|
||||
HOST=localhost
|
||||
PORT=5432
|
||||
USERNAME=postgres
|
||||
PASSWORD=secret123
|
||||
`;
|
||||
|
||||
const serviceEnv = `
|
||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||
|
||||
expect(result).toEqual({
|
||||
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws error for undefined environment variables in Stack compose", () => {
|
||||
const serviceWithUndefined = `
|
||||
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||
`;
|
||||
|
||||
expect(() =>
|
||||
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||
});
|
||||
|
||||
it("allows service variables to override environment variables in Stack compose", () => {
|
||||
const serviceOverrideEnv = `
|
||||
NODE_ENV=production
|
||||
API_URL=\${{environment.API_URL}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceOverrideEnv,
|
||||
"",
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
NODE_ENV: "production",
|
||||
API_URL: "https://api.dev.example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves complex references with project, environment, and service variables for Stack compose", () => {
|
||||
const complexServiceEnv = `
|
||||
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
||||
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
||||
SERVICE_NAME=my-service
|
||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
complexServiceEnv,
|
||||
projectEnv,
|
||||
environmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
FULL_DATABASE_URL:
|
||||
"postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
||||
API_ENDPOINT: "https://api.dev.example.com/staging/api",
|
||||
SERVICE_NAME: "my-service",
|
||||
COMPLEX_VAR: "my-service-development-staging",
|
||||
});
|
||||
});
|
||||
|
||||
it("maintains precedence: service > environment > project in Stack compose", () => {
|
||||
const conflictingProjectEnv = `
|
||||
NODE_ENV=production-project
|
||||
API_URL=https://project.api.com
|
||||
DATABASE_NAME=project_db
|
||||
`;
|
||||
|
||||
const conflictingEnvironmentEnv = `
|
||||
NODE_ENV=development-environment
|
||||
API_URL=https://environment.api.com
|
||||
DATABASE_NAME=env_db
|
||||
`;
|
||||
|
||||
const serviceWithConflicts = `
|
||||
NODE_ENV=service-override
|
||||
PROJECT_ENV=\${{project.NODE_ENV}}
|
||||
ENV_VAR=\${{environment.API_URL}}
|
||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceWithConflicts,
|
||||
conflictingProjectEnv,
|
||||
conflictingEnvironmentEnv,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
NODE_ENV: "service-override",
|
||||
PROJECT_ENV: "production-project",
|
||||
ENV_VAR: "https://environment.api.com",
|
||||
DB_NAME: "env_db",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles empty environment variables in Stack compose", () => {
|
||||
const serviceWithEmpty = `
|
||||
SERVICE_VAR=test
|
||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||
`;
|
||||
|
||||
const result = getEnviromentVariablesObject(
|
||||
serviceWithEmpty,
|
||||
projectEnv,
|
||||
"",
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
SERVICE_VAR: "test",
|
||||
PROJECT_VAR: "staging",
|
||||
});
|
||||
});
|
||||
});
|
||||
81
apps/dokploy/__test__/utils/azure-ai-provider.test.ts
Normal file
81
apps/dokploy/__test__/utils/azure-ai-provider.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { normalizeAzureUrl } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
/**
|
||||
* Test for Azure OpenAI endpoint URL normalization
|
||||
* These tests verify that Azure OpenAI URLs are properly cleaned up
|
||||
* to remove duplicate /v1 paths that would cause API errors
|
||||
*/
|
||||
describe("Azure OpenAI URL Normalization", () => {
|
||||
it("should strip /openai/v1 from Azure URL", () => {
|
||||
const input = "https://workspacename.openai.azure.com/openai/v1";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
expect(result).toBe("https://workspacename.openai.azure.com");
|
||||
});
|
||||
|
||||
it("should strip /v1 from Azure URL", () => {
|
||||
const input = "https://workspacename.openai.azure.com/v1";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
expect(result).toBe("https://workspacename.openai.azure.com");
|
||||
});
|
||||
|
||||
it("should strip trailing slash from Azure URL", () => {
|
||||
const input = "https://workspacename.openai.azure.com/";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
expect(result).toBe("https://workspacename.openai.azure.com");
|
||||
});
|
||||
|
||||
it("should handle clean Azure URL without modification", () => {
|
||||
const input = "https://workspacename.openai.azure.com";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
expect(result).toBe("https://workspacename.openai.azure.com");
|
||||
});
|
||||
|
||||
it("should strip /openai/v1/ with trailing slash", () => {
|
||||
const input = "https://workspacename.openai.azure.com/openai/v1/";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
expect(result).toBe("https://workspacename.openai.azure.com");
|
||||
});
|
||||
|
||||
it("should build correct deployments endpoint for Azure", () => {
|
||||
const input = "https://workspacename.openai.azure.com/openai/v1";
|
||||
const apiUrl = normalizeAzureUrl(input);
|
||||
|
||||
const deploymentsUrl = `${apiUrl}/openai/deployments?api-version=2023-05-15`;
|
||||
|
||||
expect(deploymentsUrl).toBe(
|
||||
"https://workspacename.openai.azure.com/openai/deployments?api-version=2023-05-15",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not strip /v1 from middle of path", () => {
|
||||
const input = "https://workspacename.openai.azure.com/v1/something";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
// Should only strip trailing /v1, not /v1 in the middle
|
||||
expect(result).toBe("https://workspacename.openai.azure.com/v1/something");
|
||||
});
|
||||
|
||||
it("should handle edge case with multiple trailing /v1", () => {
|
||||
const input = "https://workspacename.openai.azure.com/openai/v1/v1";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
// Should only strip the last /v1
|
||||
expect(result).toBe("https://workspacename.openai.azure.com/openai/v1");
|
||||
});
|
||||
|
||||
it("should not strip partial matches in path segments", () => {
|
||||
const input = "https://workspacename.openai.azure.com/myopenai/v1service";
|
||||
const result = normalizeAzureUrl(input);
|
||||
|
||||
// Should not modify paths that don't end with /openai/v1 or /v1
|
||||
expect(result).toBe(
|
||||
"https://workspacename.openai.azure.com/myopenai/v1service",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.4",
|
||||
"version": "v0.26.5",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getProviderHeaders,
|
||||
getProviderName,
|
||||
type Model,
|
||||
normalizeAzureUrl,
|
||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
@@ -58,16 +59,41 @@ export const aiRouter = createTRPCRouter({
|
||||
const providerName = getProviderName(input.apiUrl);
|
||||
const headers = getProviderHeaders(input.apiUrl, input.apiKey);
|
||||
let response = null;
|
||||
let apiUrl = input.apiUrl;
|
||||
|
||||
// Validate API key for providers that require it
|
||||
if (
|
||||
providerName !== "ollama" &&
|
||||
providerName !== "gemini" &&
|
||||
!input.apiKey
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "API key must contain at least 1 character(s)",
|
||||
});
|
||||
}
|
||||
|
||||
switch (providerName) {
|
||||
case "ollama":
|
||||
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
||||
response = await fetch(`${apiUrl}/api/tags`, { headers });
|
||||
break;
|
||||
case "gemini":
|
||||
response = await fetch(
|
||||
`${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
||||
`${apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
||||
{ headers: {} },
|
||||
);
|
||||
break;
|
||||
case "azure":
|
||||
// Azure OpenAI uses deployments endpoint
|
||||
// Normalize the URL to remove trailing /openai/v1 or /v1
|
||||
apiUrl = normalizeAzureUrl(apiUrl);
|
||||
|
||||
// Azure uses deployments endpoint to list models
|
||||
response = await fetch(
|
||||
`${apiUrl}/openai/deployments?api-version=2023-05-15`,
|
||||
{ headers },
|
||||
);
|
||||
break;
|
||||
case "perplexity":
|
||||
// Perplexity doesn't have a /models endpoint, return hardcoded list
|
||||
return [
|
||||
@@ -103,12 +129,7 @@ export const aiRouter = createTRPCRouter({
|
||||
},
|
||||
] as Model[];
|
||||
default:
|
||||
if (!input.apiKey)
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "API key must contain at least 1 character(s)",
|
||||
});
|
||||
response = await fetch(`${input.apiUrl}/models`, { headers });
|
||||
response = await fetch(`${apiUrl}/models`, { headers });
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -75,13 +75,12 @@ export const stripeRouter = createTRPCRouter({
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: "subscription",
|
||||
line_items: items,
|
||||
...(stripeCustomerId && {
|
||||
customer: stripeCustomerId,
|
||||
}),
|
||||
...(stripeCustomerId
|
||||
? { customer: stripeCustomerId }
|
||||
: { customer_email: owner.email }),
|
||||
metadata: {
|
||||
adminId: owner.id,
|
||||
},
|
||||
customer_email: owner.email,
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import { getDokployUrl } from "@dokploy/server/services/admin";
|
||||
import {
|
||||
createServerDeployment,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
findServerById,
|
||||
updateServerById,
|
||||
} from "@dokploy/server/services/server";
|
||||
import {
|
||||
getDefaultMiddlewares,
|
||||
getDefaultServerTraefikConfig,
|
||||
@@ -16,6 +20,15 @@ import {
|
||||
import slug from "slugify";
|
||||
import { Client } from "ssh2";
|
||||
import { recreateDirectory } from "../utils/filesystem/directory";
|
||||
import { setupMonitoring } from "./monitoring-setup";
|
||||
|
||||
const generateToken = () => {
|
||||
const array = new Uint8Array(64);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
};
|
||||
|
||||
export const slugify = (text: string | undefined) => {
|
||||
if (!text) {
|
||||
@@ -59,6 +72,29 @@ export const serverSetup = async (
|
||||
);
|
||||
await installRequirements(serverId, onData);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
onData?.("\nConfiguring Monitoring: 🔄\n");
|
||||
|
||||
const baseUrl = await getDokployUrl();
|
||||
const token = generateToken();
|
||||
const urlCallback = `${baseUrl}/api/trpc/notification.receiveNotification`;
|
||||
|
||||
// Update server with monitoring configuration
|
||||
await updateServerById(serverId, {
|
||||
metricsConfig: {
|
||||
server: {
|
||||
...server.metricsConfig.server,
|
||||
token: token,
|
||||
urlCallback: urlCallback,
|
||||
},
|
||||
containers: server.metricsConfig.containers,
|
||||
},
|
||||
});
|
||||
|
||||
await setupMonitoring(serverId);
|
||||
onData?.("\nMonitoring Configured: ✅\n");
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
onData?.("\nSetup Server: ✅\n");
|
||||
|
||||
@@ -7,17 +7,44 @@ import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
||||
import { createOllama } from "ai-sdk-ollama";
|
||||
|
||||
/**
|
||||
* Normalize Azure OpenAI base URL by removing trailing /openai/v1 or /v1 paths
|
||||
* Azure OpenAI SDK handles path construction internally, so these need to be stripped
|
||||
* to avoid duplicate paths in the final URL (e.g., /v1/v1/chat/completions)
|
||||
*/
|
||||
export function normalizeAzureUrl(url: string): string {
|
||||
// Use a single regex to handle all variations in one pass
|
||||
// This matches: /openai/v1 or /v1 at the end, with optional trailing slash
|
||||
const normalized = url.replace(/\/(?:openai\/v1|v1)\/?$/, "");
|
||||
// Remove any remaining trailing slash
|
||||
return normalized.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export function getProviderName(apiUrl: string) {
|
||||
if (apiUrl.includes("api.openai.com")) return "openai";
|
||||
if (apiUrl.includes("azure.com")) return "azure";
|
||||
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
|
||||
if (apiUrl.includes("api.cohere.ai")) return "cohere";
|
||||
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
|
||||
if (apiUrl.includes("api.mistral.ai")) return "mistral";
|
||||
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
|
||||
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
||||
if (apiUrl.includes("generativelanguage.googleapis.com")) return "gemini";
|
||||
return "custom";
|
||||
try {
|
||||
const url = new URL(apiUrl);
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
|
||||
if (hostname === "api.openai.com") return "openai";
|
||||
// Azure OpenAI uses *.openai.azure.com subdomain
|
||||
if (
|
||||
hostname.endsWith(".openai.azure.com") ||
|
||||
hostname === "openai.azure.com"
|
||||
)
|
||||
return "azure";
|
||||
if (hostname === "api.anthropic.com") return "anthropic";
|
||||
if (hostname === "api.cohere.ai") return "cohere";
|
||||
if (hostname === "api.perplexity.ai") return "perplexity";
|
||||
if (hostname === "api.mistral.ai") return "mistral";
|
||||
if (url.port === "11434" || hostname.includes("ollama")) return "ollama";
|
||||
if (hostname === "api.deepinfra.com") return "deepinfra";
|
||||
if (hostname === "generativelanguage.googleapis.com") return "gemini";
|
||||
return "custom";
|
||||
} catch {
|
||||
// If URL parsing fails, treat as custom provider
|
||||
// This is safe because custom providers still require valid authentication
|
||||
return "custom";
|
||||
}
|
||||
}
|
||||
|
||||
export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
@@ -29,11 +56,16 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.apiUrl,
|
||||
});
|
||||
case "azure":
|
||||
case "azure": {
|
||||
// Azure OpenAI endpoints should not include /openai/v1 or /v1 at the end
|
||||
// The SDK handles the path construction internally
|
||||
const azureBaseUrl = normalizeAzureUrl(config.apiUrl);
|
||||
|
||||
return createAzure({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.apiUrl,
|
||||
baseURL: azureBaseUrl,
|
||||
});
|
||||
}
|
||||
case "anthropic":
|
||||
return createAnthropic({
|
||||
apiKey: config.apiKey,
|
||||
@@ -92,25 +124,45 @@ export const getProviderHeaders = (
|
||||
apiUrl: string,
|
||||
apiKey: string,
|
||||
): Record<string, string> => {
|
||||
// Anthropic
|
||||
if (apiUrl.includes("anthropic")) {
|
||||
try {
|
||||
const url = new URL(apiUrl);
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
|
||||
// Azure OpenAI uses *.openai.azure.com subdomain
|
||||
if (
|
||||
hostname.endsWith(".openai.azure.com") ||
|
||||
hostname === "openai.azure.com"
|
||||
) {
|
||||
return {
|
||||
"api-key": apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
if (hostname === "api.anthropic.com") {
|
||||
return {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
};
|
||||
}
|
||||
|
||||
// Mistral
|
||||
if (hostname === "api.mistral.ai") {
|
||||
return {
|
||||
Authorization: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Default (OpenAI style)
|
||||
return {
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
} catch {
|
||||
// Fallback to OpenAI-style headers if URL parsing fails
|
||||
return {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Mistral
|
||||
if (apiUrl.includes("mistral")) {
|
||||
return {
|
||||
Authorization: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
// Default (OpenAI style)
|
||||
return {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
};
|
||||
export interface Model {
|
||||
id: string;
|
||||
|
||||
@@ -134,6 +134,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
|
||||
const envVars = getEnviromentVariablesObject(
|
||||
compose.env,
|
||||
compose.environment.project.env,
|
||||
compose.environment.env,
|
||||
);
|
||||
const exports = Object.entries(envVars)
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
|
||||
Reference in New Issue
Block a user