Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/lib/llm-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,53 @@ describe("Azure OpenAI provider", () => {
expect(body.temperature).toBeUndefined()
})

it("auto-detects GPT-5 from an Azure deployment name that does not start with the model id", () => {
const cfg = getProviderConfig(mkConfig({
provider: "azure",
model: "my-gpt-5-chat",
customEndpoint: "https://example-resource.openai.azure.com",
}))
const body = cfg.buildBody(
[{ role: "user", content: "hi" }],
{ max_tokens: 2048 },
) as Record<string, unknown>

expect(body.max_tokens).toBeUndefined()
expect(body.max_completion_tokens).toBe(2048)
})

it("auto-detects the dashless gpt5 spelling and o-series names in Azure deployments", () => {
for (const model of ["prod-gpt5", "o3-reasoning"]) {
const cfg = getProviderConfig(mkConfig({
provider: "azure",
model,
customEndpoint: "https://example-resource.openai.azure.com",
}))
const body = cfg.buildBody(
[{ role: "user", content: "hi" }],
{ max_tokens: 1000 },
) as Record<string, unknown>

expect(body.max_tokens, model).toBeUndefined()
expect(body.max_completion_tokens, model).toBe(1000)
}
})

it("keeps max_tokens for non-GPT-5 Azure deployments", () => {
const cfg = getProviderConfig(mkConfig({
provider: "azure",
model: "gpt-4o-chat",
customEndpoint: "https://example-resource.openai.azure.com",
}))
const body = cfg.buildBody(
[{ role: "user", content: "hi" }],
{ max_tokens: 3000 },
) as Record<string, unknown>

expect(body.max_tokens).toBe(3000)
expect(body.max_completion_tokens).toBeUndefined()
})

it("applies explicit Azure model family to custom Azure endpoints", () => {
const cfg = getProviderConfig(mkConfig({
provider: "custom",
Expand Down
28 changes: 24 additions & 4 deletions src/lib/llm-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,33 @@ function isGlmVisionModel(model: string): boolean {
|| /(?:^|[-_.])glm[-_.]4v(?:[-_.]|$)/i.test(normalized)
}

// GPT-5 / o-series (o1, o3, …) reasoning models require
// `max_completion_tokens` instead of `max_tokens`. Azure deployment names are
// user-chosen and frequently don't *start* with the underlying model id
// (e.g. "prod-gpt5", "my-gpt-5-chat", "o3-reasoning"), so match a family token
// anywhere in the name using word boundaries. `gpt-?5` also covers the common
// dashless "gpt5" spelling. See issue #610.
function deploymentNameSuggestsStrictCompletion(name: string): boolean {
const n = name.trim().toLowerCase()
return /(?:^|[-_.])gpt-?5(?:[-_.]|$)/.test(n) || /(?:^|[-_.])o\d+(?:[-_.]|$)/.test(n)
}

function isOpenAiStrictCompletionModel(config: LlmConfig): boolean {
if ((config.provider === "azure" || (config.provider === "custom" && isAzureOpenAiEndpoint(config.customEndpoint)))
&& config.azureModelFamily === "gpt5") {
return true
}
const isAzureLike =
config.provider === "azure" ||
(config.provider === "custom" && isAzureOpenAiEndpoint(config.customEndpoint))

// Explicit family override always wins.
if (isAzureLike && config.azureModelFamily === "gpt5") return true

const model = config.model.trim().toLowerCase()

// When the Azure family is left on "auto" (the default), infer it from the
// deployment name with a boundary-aware match anywhere in the string so
// GPT-5 / o-series deployments get `max_completion_tokens` without requiring
// the user to manually set the family. See issue #610.
if (isAzureLike && deploymentNameSuggestsStrictCompletion(model)) return true

const strictModel =
/^gpt-5(?:[.\-_]|$)/.test(model) || /^o\d+(?:[.\-_]|$)/.test(model)
if (!strictModel) return false
Expand Down