fix: use azure-openai-responses for Azure custom providers (#50851) (thanks @kunalk16)

* Add azure-openai-responses

* Unit tests update for updated API

* Add entry for PR #50851

* Add comma to address PR comment

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Address PR comment on sanitization of output

* Address review comment

* Revert commits

* Revert commit

* Update changelog stating Azure OpenAI only

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add references

* Address PR comment on sanitization of output

* Address review comment

* Revert commits

* Revert commit

* Address PR comment on sanitization of output

* Address review comment

* Revert commits

* Revert commit

* Fix generated file

* Add azure openai responses to OPENAI_RESPONSES_APIS

* Add azure openai responses to createParallelToolCallsWrapper

* Adding azure openai responses to attempt.ts

* Add azure openai responses to google.ts

* Address PR comment on sanitization of output

* Revert commit

* Address PR comment on sanitization of output

* Revert commit

* Address PR comment on sanitization of output

* Revert commit

* Fix changelog

* Fix linting

* fix: cover azure responses wrapper path (#50851) (thanks @kunalk16)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Kunal Karmakar
2026-03-30 16:17:03 +05:30
committed by GitHub
parent 2fbd5e3f5f
commit 34b0a19a16
13 changed files with 75 additions and 23 deletions

View File

@@ -364,6 +364,7 @@ describe("applyExtraParamsToAgent", () => {
applyModelId: string;
model:
| Model<"openai-responses">
| Model<"azure-openai-responses">
| Model<"openai-codex-responses">
| Model<"openai-completions">
| Model<"anthropic-messages">;
@@ -418,7 +419,11 @@ describe("applyExtraParamsToAgent", () => {
function runParallelToolCallsPayloadMutationCase(params: {
applyProvider: string;
applyModelId: string;
model: Model<"openai-completions"> | Model<"openai-responses"> | Model<"anthropic-messages">;
model:
| Model<"openai-completions">
| Model<"openai-responses">
| Model<"azure-openai-responses">
| Model<"anthropic-messages">;
cfg?: Record<string, unknown>;
extraParamsOverride?: Record<string, unknown>;
payload?: Record<string, unknown>;
@@ -656,6 +661,34 @@ describe("applyExtraParamsToAgent", () => {
expect(payload.parallel_tool_calls).toBe(true);
});
it("injects parallel_tool_calls for azure-openai-responses payloads when configured", () => {
const payload = runParallelToolCallsPayloadMutationCase({
applyProvider: "azure-openai-responses",
applyModelId: "gpt-5",
cfg: {
agents: {
defaults: {
models: {
"azure-openai-responses/gpt-5": {
params: {
parallelToolCalls: true,
},
},
},
},
},
},
model: {
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-5",
baseUrl: "https://example.openai.azure.com/openai/v1",
} as unknown as Model<"azure-openai-responses">,
});
expect(payload.parallel_tool_calls).toBe(true);
});
it("does not inject parallel_tool_calls for unsupported APIs", () => {
const payload = runParallelToolCallsPayloadMutationCase({
applyProvider: "anthropic",
@@ -2664,11 +2697,11 @@ describe("applyExtraParamsToAgent", () => {
},
},
model: {
api: "openai-responses",
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-5.4",
baseUrl: "https://example.openai.azure.com/openai/v1",
} as unknown as Model<"openai-responses">,
} as unknown as Model<"azure-openai-responses">,
});
expect(payload).not.toHaveProperty("service_tier");
});
@@ -2829,7 +2862,7 @@ describe("applyExtraParamsToAgent", () => {
applyProvider: "azure-openai-responses",
applyModelId: "gpt-4o",
model: {
api: "openai-responses",
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-4o",
name: "gpt-4o",
@@ -2840,7 +2873,7 @@ describe("applyExtraParamsToAgent", () => {
contextWindow: 128_000,
maxTokens: 16_384,
compat: { supportsStore: false },
} as unknown as Model<"openai-responses">,
} as unknown as Model<"azure-openai-responses">,
});
expect(payload).not.toHaveProperty("store");
});
@@ -2917,11 +2950,11 @@ describe("applyExtraParamsToAgent", () => {
applyProvider: "azure-openai-responses",
applyModelId: "gpt-4o",
model: {
api: "openai-responses",
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-4o",
baseUrl: "https://example.openai.azure.com/openai/v1",
} as unknown as Model<"openai-responses">,
} as unknown as Model<"azure-openai-responses">,
});
expect(payload).not.toHaveProperty("context_management");
});
@@ -2945,11 +2978,11 @@ describe("applyExtraParamsToAgent", () => {
},
},
model: {
api: "openai-responses",
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-4o",
baseUrl: "https://example.openai.azure.com/openai/v1",
} as unknown as Model<"openai-responses">,
} as unknown as Model<"azure-openai-responses">,
});
expect(payload.context_management).toEqual([
{
@@ -3081,16 +3114,16 @@ describe("applyExtraParamsToAgent", () => {
expect(payload.prompt_cache_retention).toBe("24h");
});
it("keeps prompt cache fields for direct Azure OpenAI openai-responses endpoints", () => {
it("keeps prompt cache fields for direct Azure OpenAI azure-openai-responses endpoints", () => {
const payload = runResponsesPayloadMutationCase({
applyProvider: "azure-openai-responses",
applyModelId: "gpt-4o",
model: {
api: "openai-responses",
api: "azure-openai-responses",
provider: "azure-openai-responses",
id: "gpt-4o",
baseUrl: "https://example.openai.azure.com/openai/v1",
} as unknown as Model<"openai-responses">,
} as unknown as Model<"azure-openai-responses">,
payload: {
store: false,
prompt_cache_key: "session-azure",

View File

@@ -22,7 +22,7 @@ function createMockStream(): ReturnType<StreamFn> {
}
type RunExtraParamsCaseParams<
TApi extends "openai-completions" | "openai-responses",
TApi extends "openai-completions" | "openai-responses" | "azure-openai-responses",
TPayload extends Record<string, unknown>,
> = {
applyModelId?: string;
@@ -36,7 +36,7 @@ type RunExtraParamsCaseParams<
};
export function runExtraParamsCase<
TApi extends "openai-completions" | "openai-responses",
TApi extends "openai-completions" | "openai-responses" | "azure-openai-responses",
TPayload extends Record<string, unknown>,
>(params: RunExtraParamsCaseParams<TApi, TPayload>): ExtraParamsCapture<TPayload> {
const captured: ExtraParamsCapture<TPayload> = {

View File

@@ -278,7 +278,11 @@ function createParallelToolCallsWrapper(
): StreamFn {
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (model.api !== "openai-completions" && model.api !== "openai-responses") {
if (
model.api !== "openai-completions" &&
model.api !== "openai-responses" &&
model.api !== "azure-openai-responses"
) {
return underlying(model, context, options);
}
log.debug(
@@ -439,7 +443,10 @@ function applyPostPluginStreamWrappers(
log.debug(
`applying OpenAI text verbosity=${openAITextVerbosity} for ${ctx.provider}/${ctx.modelId}`,
);
ctx.agent.streamFn = createOpenAITextVerbosityWrapper(ctx.agent.streamFn, openAITextVerbosity);
ctx.agent.streamFn = createOpenAITextVerbosityWrapper(
ctx.agent.streamFn,
openAITextVerbosity,
);
}
}
}

View File

@@ -627,7 +627,9 @@ export async function sanitizeSessionHistory(params: {
);
const isOpenAIResponsesApi =
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
params.modelApi === "openai-responses" ||
params.modelApi === "openai-codex-responses" ||
params.modelApi === "azure-openai-responses";
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null;
const modelChanged = priorSnapshot

View File

@@ -99,6 +99,7 @@ function normalizeResolvedTransportApi(api: unknown): ModelDefinitionConfig["api
case "openai-codex-responses":
case "openai-completions":
case "openai-responses":
case "azure-openai-responses":
return api;
default:
return undefined;

View File

@@ -8,7 +8,7 @@ import { streamWithPayloadPatch } from "./stream-payload-utils.js";
type OpenAIServiceTier = "auto" | "default" | "flex" | "priority";
type OpenAITextVerbosity = "low" | "medium" | "high";
const OPENAI_RESPONSES_APIS = new Set(["openai-responses"]);
const OPENAI_RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses"]);
const OPENAI_RESPONSES_PROVIDERS = new Set(["openai", "azure-openai", "azure-openai-responses"]);
function isDirectOpenAIBaseUrl(baseUrl: unknown): boolean {
@@ -358,7 +358,9 @@ export function createOpenAIFastModeWrapper(baseStreamFn: StreamFn | undefined):
const underlying = baseStreamFn ?? streamSimple;
return (model, context, options) => {
if (
(model.api !== "openai-responses" && model.api !== "openai-codex-responses") ||
(model.api !== "openai-responses" &&
model.api !== "openai-codex-responses" &&
model.api !== "azure-openai-responses") ||
(model.provider !== "openai" && model.provider !== "openai-codex")
) {
return underlying(model, context, options);

View File

@@ -992,6 +992,7 @@ export async function runEmbeddedAttempt(
if (
params.model.api === "openai-responses" ||
params.model.api === "azure-openai-responses" ||
params.model.api === "openai-codex-responses"
) {
const inner = activeSession.agent.streamFn;

View File

@@ -81,7 +81,9 @@ export function resolveTranscriptPolicy(params: {
const requiresOpenAiCompatibleToolIdSanitization =
params.modelApi === "openai-completions" ||
(!isOpenAi &&
(params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses"));
(params.modelApi === "openai-responses" ||
params.modelApi === "openai-codex-responses" ||
params.modelApi === "azure-openai-responses"));
// Anthropic Claude endpoints can reject replayed `thinking` blocks unless the
// original signatures are preserved byte-for-byte. Drop them at send-time to

View File

@@ -482,7 +482,7 @@ describe("applyCustomApiConfig", () => {
const provider = result.config.models?.providers?.[providerId];
expect(provider?.baseUrl).toBe("https://user123-resource.openai.azure.com/openai/v1");
expect(provider?.api).toBe("openai-responses");
expect(provider?.api).toBe("azure-openai-responses");
expect(provider?.authHeader).toBe(false);
expect(provider?.headers).toEqual({ "api-key": "abcd1234" });
@@ -568,7 +568,7 @@ describe("applyCustomApiConfig", () => {
expect(result.providerIdRenamedFrom).toBeUndefined();
const provider = result.config.models?.providers?.[oldProviderId];
expect(provider?.baseUrl).toBe("https://my-resource.openai.azure.com/openai/v1");
expect(provider?.api).toBe("openai-responses");
expect(provider?.api).toBe("azure-openai-responses");
expect(provider?.authHeader).toBe(false);
expect(provider?.headers).toEqual({ "api-key": "key789" });
});

View File

@@ -687,7 +687,7 @@ export function applyCustomApiConfig(params: ApplyCustomApiConfigParams): Custom
normalizeOptionalProviderApiKey(existingApiKey);
const providerApi = isAzureOpenAi
? ("openai-responses" as const)
? ("azure-openai-responses" as const)
: resolveProviderApi(params.compatibility);
const azureHeaders = isAzure && normalizedApiKey ? { "api-key": normalizedApiKey } : undefined;

View File

@@ -1038,6 +1038,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
"github-copilot",
"bedrock-converse-stream",
"ollama",
"azure-openai-responses",
],
},
injectNumCtxForOpenAICompat: {
@@ -1142,6 +1143,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
"github-copilot",
"bedrock-converse-stream",
"ollama",
"azure-openai-responses",
],
},
reasoning: {

View File

@@ -10,6 +10,7 @@ export const MODEL_APIS = [
"github-copilot",
"bedrock-converse-stream",
"ollama",
"azure-openai-responses",
] as const;
export type ModelApi = (typeof MODEL_APIS)[number];