Compare commits

..

17 Commits

Author SHA1 Message Date
Mauricio Siu
ee1a48e0b0 Merge branch 'canary' into copilot/fix-azure-openai-endpoint 2026-01-19 14:37:15 +01:00
Mauricio Siu
f0400495b0 refactor(README): restructure table 2026-01-16 01:18:14 -06:00
Mauricio Siu
240e5cb12f Merge pull request #3462 from Dokploy/activate-monitoring-on-remote-servers-cloud-version
feat(server): add monitoring configuration for cloud setup
2026-01-16 01:11:24 -06:00
Mauricio Siu
2760c16ade Merge pull request #3457 from Dokploy/copilot/fix-envs-in-stack-compose
Fix environment variable resolution for Stack compose deployments
2026-01-16 01:11:06 -06:00
Mauricio Siu
79655b5673 refactor(server): move token generation function to a separate utility for better organization 2026-01-16 01:07:17 -06:00
Mauricio Siu
384fdd01d6 feat(server): add monitoring configuration for cloud setup 2026-01-16 01:05:40 -06:00
copilot-swe-agent[bot]
c1d452bcf7 Complete fix for Stack compose environment variable substitution
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-15 15:43:01 +00:00
copilot-swe-agent[bot]
f39b511316 Fix environment variable resolution for Stack compose type
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-15 15:39:04 +00:00
copilot-swe-agent[bot]
a2df52ea7c Initial plan 2026-01-15 15:32:01 +00:00
Mauricio Siu
3e5a189177 Merge pull request #3455 from Dokploy/3454-subscribe-issue
chore: update dokploy version to v0.26.5 and modify Stripe session cr…
2026-01-15 09:23:45 -06:00
Mauricio Siu
2b9231dcd1 chore: update dokploy version to v0.26.5 and modify Stripe session creation logic to conditionally set customer or customer_email 2026-01-15 09:18:00 -06:00
copilot-swe-agent[bot]
2c8eb54d7b Add test for edge case: verify regex doesn't match partial path segments
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:56:14 +00:00
copilot-swe-agent[bot]
f314c2f161 Security fix: Use proper hostname validation to prevent URL substring attacks
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:54:35 +00:00
copilot-swe-agent[bot]
136edfd4c5 Address code review feedback: Improve URL normalization and remove duplication
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:48:47 +00:00
copilot-swe-agent[bot]
2f9987970a Refactor: Extract Azure URL normalization to shared utility function
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:46:40 +00:00
copilot-swe-agent[bot]
25cfe915d2 Add tests for Azure OpenAI URL normalization
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:44:30 +00:00
copilot-swe-agent[bot]
0af6dc3fc2 Fix Azure OpenAI endpoint double /v1 issue
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:43:20 +00:00
10 changed files with 432 additions and 90 deletions

View File

@@ -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 🤝

View 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",
});
});
});

View 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",
);
});
});

View File

@@ -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";

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.26.4",
"version": "v0.26.5",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -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) {

View File

@@ -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`,

View File

@@ -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");

View File

@@ -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;

View File

@@ -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])}`)