@@ -30,6 +30,45 @@ import { ResponseCreateParamsNonStreaming } from "openai/resources/responses/res
3030// TODO: Rename this to OpenAICompatibleHandler. Also, I think the
3131// `OpenAINativeHandler` can subclass from this, since it's obviously
3232// compatible with the OpenAI API. We can also rename it to `OpenAIHandler`.
33+ /**
34+ * URL auto-detection overview
35+ *
36+ * Decision tree (host and path based):
37+ * 1) Azure AI Inference Service:
38+ * - Detected when host ends with ".services.ai.azure.com"
39+ * - Uses OpenAI Chat Completions API shape with a path override
40+ * (see OPENAI_AZURE_AI_INFERENCE_PATH) when making requests.
41+ *
42+ * 2) Azure OpenAI:
43+ * - Detected when host is "openai.azure.com" or ends with ".openai.azure.com"
44+ * or when options.openAiUseAzure is explicitly true.
45+ * - Within Azure OpenAI, the API "flavor" is chosen by URL path:
46+ * - Responses API:
47+ * * Path contains "/v1/responses" or ends with "/responses"
48+ * * Also auto-detected for portal-style URLs (e.g. "/openai/responses?api-version=2025-04-01-preview")
49+ * which itself is not valid in request, are normalized to "/openai/v1" with apiVersion "preview".
50+ * - Chat Completions API:
51+ * * Path contains "/chat/completions"
52+ * - Default:
53+ * * Falls back to Chat Completions if none of the above match.
54+ *
55+ * 3) Generic OpenAI-compatible endpoints:
56+ * - Anything else (OpenAI, OpenRouter, LM Studio, vLLM, etc.)
57+ * - Flavor is again selected by URL path as above:
58+ * - "/v1/responses" or ending with "/responses" => Responses API
59+ * - "/chat/completions" => Chat Completions
60+ * - otherwise defaults to Chat Completions for backward compatibility.
61+ *
62+ * Examples:
63+ * - https://api.openai.com/v1 -> Chat Completions (default)
64+ * - https://api.openai.com/v1/responses -> Responses API
65+ * - https://api.openai.com/v1/chat/completions -> Chat Completions
66+ * - https://myres.openai.azure.com/openai/v1/responses?api-version=preview
67+ * -> Azure OpenAI + Responses API
68+ * - https://myres.openai.azure.com/openai/responses?api-version=2025-04-01-preview
69+ * -> normalized to base /openai/v1 + apiVersion "preview" (Responses)
70+ * - https://test.services.ai.azure.com -> Azure AI Inference Service (Chat Completions with path override)
71+ */
3372export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
3473 protected options : ApiHandlerOptions
3574 private client : OpenAI
@@ -773,16 +812,55 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
773812 }
774813 }
775814
815+ /**
816+ * Detects Grok xAI endpoints.
817+ * - Returns true when the host contains "x.ai" (e.g., "api.x.ai").
818+ * - Used to omit stream_options for streaming requests because Grok may not support them.
819+ *
820+ * Examples:
821+ * - https://api.x.ai/v1 -> true
822+ * - https://api.openai.com/v1 -> false
823+ */
776824 private _isGrokXAI ( baseUrl ?: string ) : boolean {
777825 const urlHost = this . _getUrlHost ( baseUrl )
778826 return urlHost . includes ( "x.ai" )
779827 }
780828
829+ /**
830+ * Detects Azure AI Inference Service endpoints (distinct from Azure OpenAI).
831+ * - Returns true when host ends with ".services.ai.azure.com".
832+ * - These endpoints require a special path override when calling the Chat Completions API.
833+ *
834+ * Examples:
835+ * - https://myenv.services.ai.azure.com -> true
836+ * - https://myres.openai.azure.com -> false (this is Azure OpenAI, not AI Inference)
837+ */
781838 private _isAzureAiInference ( baseUrl ?: string ) : boolean {
782839 const urlHost = this . _getUrlHost ( baseUrl )
783840 return urlHost . endsWith ( ".services.ai.azure.com" )
784841 }
785842
843+ /**
844+ * Detects Azure OpenAI "Responses API" URLs by host and path.
845+ * - Host must be "openai.azure.com" or end with ".openai.azure.com"
846+ * - Path may be one of:
847+ * • "/openai/v1/responses" (preferred v1 path)
848+ * • "/openai/responses" (portal/legacy style)
849+ * • any path ending with "/responses"
850+ * - Trailing slashes are trimmed before matching.
851+ *
852+ * This is used to favor the Responses API flavor on Azure OpenAI when the base URL already
853+ * points to a Responses path.
854+ *
855+ * Examples (true):
856+ * - https://myres.openai.azure.com/openai/v1/responses?api-version=preview
857+ * - https://myres.openai.azure.com/openai/responses?api-version=2025-04-01-preview
858+ * - https://openai.azure.com/openai/v1/responses
859+ *
860+ * Examples (false):
861+ * - https://myres.openai.azure.com/openai/v1/chat/completions
862+ * - https://api.openai.com/v1/responses (not an Azure host)
863+ */
786864 private _isAzureOpenAiResponses ( baseUrl ?: string ) : boolean {
787865 try {
788866 if ( ! baseUrl ) return false
@@ -801,10 +879,36 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
801879 }
802880
803881 /**
804- * Normalize Azure "responses" portal URLs to SDK-friendly base and version.
805- * - Input (portal sometimes shows): https://{res}.openai.azure.com/openai/responses?api-version=2025-04-01-preview
806- * - Output: baseURL=https://{res}.openai.azure.com/openai/v1, apiVersionOverride="preview"
807- * No-op for already-correct or non-Azure URLs.
882+ * Normalizes Azure OpenAI "Responses" portal URLs to an SDK-friendly base and version.
883+ *
884+ * Why:
885+ * - The Azure portal often presents a non-v1 Responses endpoint such as:
886+ * https://{res}.openai.azure.com/openai/responses?api-version=2025-04-01-preview
887+ * which is not the ideal base for SDK clients. We convert it to:
888+ * baseURL = https://{res}.openai.azure.com/openai/v1
889+ * apiVersionOverride = "preview"
890+ *
891+ * What it does:
892+ * - If the input is an Azure OpenAI host and its path is exactly "/openai/responses"
893+ * with api-version=2025-04-01-preview, we:
894+ * • return { baseURL: "https://{host}/openai/v1", apiVersionOverride: "preview" }
895+ * - If the input is already "/openai/v1/responses", we similarly normalize the base to "/openai/v1"
896+ * and set apiVersionOverride to "preview".
897+ * - Otherwise, returns the original URL unchanged.
898+ *
899+ * Scope:
900+ * - Only applies to Azure OpenAI hosts ("openai.azure.com" or "*.openai.azure.com").
901+ * - Non-Azure URLs or already SDK-friendly bases are returned as-is.
902+ *
903+ * Examples:
904+ * - In: https://sample.openai.azure.com/openai/responses?api-version=2025-04-01-preview
905+ * Out: baseURL=https://sample.openai.azure.com/openai/v1, apiVersionOverride="preview"
906+ *
907+ * - In: https://sample.openai.azure.com/openai/v1/responses?api-version=preview
908+ * Out: baseURL=https://sample.openai.azure.com/openai/v1, apiVersionOverride="preview"
909+ *
910+ * - In: https://api.openai.com/v1/responses
911+ * Out: baseURL unchanged (non-Azure)
808912 */
809913 private _normalizeAzureResponsesBaseUrlAndVersion ( inputBaseUrl : string ) : {
810914 baseURL : string
@@ -866,6 +970,25 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
866970
867971 // --- Responses helpers ---
868972
973+ /**
974+ * Determines which OpenAI-compatible API flavor to use based on the URL path.
975+ * - This is purely path-based and provider-agnostic (works for OpenAI, Azure OpenAI after normalization, etc.).
976+ *
977+ * Rules:
978+ * - If path contains "/v1/responses" OR ends with "/responses" => "responses"
979+ * - Else if path contains "/chat/completions" => "chat"
980+ * - Else default to "chat" for backward compatibility
981+ *
982+ * Notes:
983+ * - Trailing slashes are not required to match; we rely on substring checks.
984+ * - Azure "portal" style URLs are normalized beforehand where applicable.
985+ *
986+ * Examples:
987+ * - https://api.openai.com/v1/responses -> "responses"
988+ * - https://api.openai.com/v1/chat/completions -> "chat"
989+ * - https://myres.openai.azure.com/openai/v1 -> "chat" (default)
990+ * - https://myres.openai.azure.com/openai/v1/responses -> "responses"
991+ */
869992 private _resolveApiFlavor ( baseUrl : string ) : "responses" | "chat" {
870993 // Auto-detect by URL path
871994 const url = this . _safeParseUrl ( baseUrl )
0 commit comments