diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js index 664ab033..91e7f403 100644 --- a/open-sse/config/providers.js +++ b/open-sse/config/providers.js @@ -338,4 +338,11 @@ export const PROVIDERS = { headers: { "x-opencode-client": "desktop" }, noAuth: true }, + // Azure OpenAI - requires azureEndpoint, apiVersion, deployment in providerSpecificData + // Uses api-key header for authentication instead of Bearer token + azure: { + baseUrl: "", // Constructed dynamically by AzureExecutor based on credentials + format: "openai", + headers: {} + }, }; diff --git a/open-sse/executors/azure.js b/open-sse/executors/azure.js new file mode 100644 index 00000000..574cda22 --- /dev/null +++ b/open-sse/executors/azure.js @@ -0,0 +1,57 @@ +import { DefaultExecutor } from "./default.js"; + +export class AzureExecutor extends DefaultExecutor { + constructor() { + super("azure"); + } + + buildUrl(model, stream, urlIndex = 0, credentials = null) { + const azureEndpoint = credentials?.providerSpecificData?.azureEndpoint + || process.env.AZURE_ENDPOINT + || "https://api.openai.com"; + + const apiVersion = credentials?.providerSpecificData?.apiVersion + || process.env.AZURE_API_VERSION + || "2024-10-01-preview"; + + const deployment = credentials?.providerSpecificData?.deployment + || model + || process.env.AZURE_DEPLOYMENT + || "gpt-4"; + + const endpoint = azureEndpoint.replace(/\/$/, ""); + return `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`; + } + + buildHeaders(credentials, stream = true) { + const headers = { + "Content-Type": "application/json", + ...this.config.headers + }; + + const apiKey = credentials?.apiKey + || credentials?.accessToken + || process.env.OPENAI_API_KEY; + + if (apiKey) { + headers["api-key"] = apiKey; + } + + const organization = credentials?.providerSpecificData?.organization + || process.env.AZURE_ORGANIZATION; + + if (organization) { + headers["OpenAI-Organization"] = organization; + } + + if (stream) { + headers["Accept"] = "text/event-stream"; + } + + return headers; + } + + transformRequest(model, body, stream, credentials) { + return body; + } +} diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js index f77212c4..e76e5544 100644 --- a/open-sse/executors/index.js +++ b/open-sse/executors/index.js @@ -1,4 +1,5 @@ import { AntigravityExecutor } from "./antigravity.js"; +import { AzureExecutor } from "./azure.js"; import { GeminiCLIExecutor } from "./gemini-cli.js"; import { GithubExecutor } from "./github.js"; import { IFlowExecutor } from "./iflow.js"; @@ -13,6 +14,7 @@ import { DefaultExecutor } from "./default.js"; const executors = { antigravity: new AntigravityExecutor(), + azure: new AzureExecutor(), "gemini-cli": new GeminiCLIExecutor(), github: new GithubExecutor(), iflow: new IFlowExecutor(), @@ -41,6 +43,7 @@ export function hasSpecializedExecutor(provider) { export { BaseExecutor } from "./base.js"; export { AntigravityExecutor } from "./antigravity.js"; +export { AzureExecutor } from "./azure.js"; export { GeminiCLIExecutor } from "./gemini-cli.js"; export { GithubExecutor } from "./github.js"; export { IFlowExecutor } from "./iflow.js"; diff --git a/src/shared/components/EditConnectionModal.js b/src/shared/components/EditConnectionModal.js index f9c84207..6bb13cd2 100644 --- a/src/shared/components/EditConnectionModal.js +++ b/src/shared/components/EditConnectionModal.js @@ -14,6 +14,12 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on priority: 1, apiKey: "", }); + const [azureData, setAzureData] = useState({ + azureEndpoint: "", + apiVersion: "2024-10-01-preview", + deployment: "", + organization: "", + }); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); const [validating, setValidating] = useState(false); @@ -27,12 +33,22 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on priority: connection.priority || 1, apiKey: "", }); + // Load Azure-specific data if present + if (connection.provider === "azure" && connection.providerSpecificData) { + setAzureData({ + azureEndpoint: connection.providerSpecificData.azureEndpoint || "", + apiVersion: connection.providerSpecificData.apiVersion || "2024-10-01-preview", + deployment: connection.providerSpecificData.deployment || "", + organization: connection.providerSpecificData.organization || "", + }); + } setTestResult(null); setValidationResult(null); } }, [connection]); const isOAuth = connection?.authType === "oauth"; + const isAzure = connection?.provider === "azure"; const isCompatible = connection ? (isOpenAICompatibleProvider(connection.provider) || isAnthropicCompatibleProvider(connection.provider)) : false; @@ -106,6 +122,17 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on updates.lastErrorAt = null; } } + + // Add Azure-specific data if this is an Azure connection + if (isAzure) { + updates.providerSpecificData = { + azureEndpoint: azureData.azureEndpoint, + apiVersion: azureData.apiVersion, + deployment: azureData.deployment, + organization: azureData.organization, + }; + } + await onSave(updates); } finally { setSaving(false); @@ -162,7 +189,43 @@ export default function EditConnectionModal({ isOpen, connection, proxyPools, on )} - {!isCompatible && ( + {isAzure && ( +
+

Azure OpenAI Configuration

+
+ setAzureData({ ...azureData, azureEndpoint: e.target.value })} + placeholder="https://your-resource.openai.azure.com" + hint="Your Azure OpenAI resource endpoint URL" + /> + setAzureData({ ...azureData, deployment: e.target.value })} + placeholder="gpt-4" + hint="The deployment name in your Azure resource" + /> + setAzureData({ ...azureData, apiVersion: e.target.value })} + placeholder="2024-10-01-preview" + hint="Azure OpenAI API version to use" + /> + setAzureData({ ...azureData, organization: e.target.value })} + placeholder="Organization ID" + hint="Optional: Your Azure organization ID" + /> +
+
+ )} + + {!isCompatible && !isAzure && (