Skip to content
Merged
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
16 changes: 15 additions & 1 deletion src/chrome/src/providers/azure-openai.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseLLMProvider } from './base.js';
import { fetchWithFallback } from './fetch-with-fallback.js';
import { configuredMaxTokensField } from './provider-compatibility.js';

/**
* Azure OpenAI provider (deployment-based OpenAI-compatible API).
Expand Down Expand Up @@ -62,11 +63,24 @@ export class AzureOpenAIProvider extends BaseLLMProvider {
}

_addMaxTokens(body, options) {
// Azure OpenAI follows the legacy OpenAI contract here.
// The Compatibility panel's "max tokens field" is the explicit
// per-deployment switch. `_addConfiguredMaxTokens` already honors
// `compat.maxTokensField` (and `config.maxTokensField`) through
// `configuredMaxTokensField`, so a reasoning deployment set to
// `max_completion_tokens` in Settings sends the new field with no
// name-guessing here.
this._addConfiguredMaxTokens(body, options, 'max_tokens');
}

_addTemperature(body, options) {
// Azure deployment names do not reveal the model behind them (prod-chat
// may back o3-mini; o365-assistant may back gpt-35-turbo), so never guess
// the contract from the name. A reasoning deployment opts in by setting
// the Compatibility "max tokens field" to `max_completion_tokens` (which
// also drops temperature — reasoning models reject any non-default), or
// by enabling `omitTemperature` directly.
if (this.config.omitTemperature) return;
if (configuredMaxTokensField(this.config, 'max_tokens') === 'max_completion_tokens') return;
body.temperature = options.temperature ?? 0.7;
}

Expand Down
15 changes: 15 additions & 0 deletions src/firefox/src/providers/azure-openai.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BaseLLMProvider } from './base.js';
import { fetchWithTimeout } from './fetch-timeout.js';
import { configuredMaxTokensField } from './provider-compatibility.js';

/**
* Azure OpenAI provider (deployment-based OpenAI-compatible API).
Expand Down Expand Up @@ -60,10 +61,24 @@ export class AzureOpenAIProvider extends BaseLLMProvider {
}

_addMaxTokens(body, options) {
// The Compatibility panel's "max tokens field" is the explicit
// per-deployment switch. `_addConfiguredMaxTokens` already honors
// `compat.maxTokensField` (and `config.maxTokensField`) through
// `configuredMaxTokensField`, so a reasoning deployment set to
// `max_completion_tokens` in Settings sends the new field with no
// name-guessing here.
this._addConfiguredMaxTokens(body, options, 'max_tokens');
}

_addTemperature(body, options) {
// Azure deployment names do not reveal the model behind them (prod-chat
// may back o3-mini; o365-assistant may back gpt-35-turbo), so never guess
// the contract from the name. A reasoning deployment opts in by setting
// the Compatibility "max tokens field" to `max_completion_tokens` (which
// also drops temperature — reasoning models reject any non-default), or
// by enabling `omitTemperature` directly.
if (this.config.omitTemperature) return;
if (configuredMaxTokensField(this.config, 'max_tokens') === 'max_completion_tokens') return;
body.temperature = options.temperature ?? 0.7;
}

Expand Down
59 changes: 59 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -49153,6 +49153,65 @@ test('Azure OpenAI Ask streams require DONE and distinguish terminal API errors'
}
});

test('Azure OpenAI does not guess the wire contract from the deployment name', () => {
for (const Provider of [AzureOpenAIProviderCh, AzureOpenAIProviderFx]) {
// Deployment names don't reveal the model behind them: o1-named
// deployments stay legacy, and a name that merely starts with o3/o4
// must not flip a gpt-35-turbo deployment to the new contract.
for (const deployment of ['o1', 'o1-mini', 'o3-mini', 'o4-mini', 'gpt-5', 'gpt-4.1', 'o365-assistant', 'prod-chat']) {
const provider = new Provider({
providerName: 'azure-openai',
baseUrl: 'https://example.openai.azure.com',
model: deployment,
apiVersion: '2025-09-01',
apiKey: 'test-key',
});
const body = provider._buildRequestBody(
[{ role: 'user', content: 'hello' }],
{ maxTokens: 2048 },
);
assert.equal(body.max_tokens, 2048, `${Provider.name}/${deployment}: default contract keeps max_tokens`);
assert.equal(body.max_completion_tokens, undefined, `${Provider.name}/${deployment}: no name-guessing into the new contract`);
assert.equal(body.temperature, 0.7, `${Provider.name}/${deployment}: default contract keeps temperature`);
}
}
});

test('Azure OpenAI switches to max_completion_tokens and omits temperature via the explicit compat field', () => {
for (const Provider of [AzureOpenAIProviderCh, AzureOpenAIProviderFx]) {
const provider = new Provider({
providerName: 'azure-openai',
baseUrl: 'https://example.openai.azure.com',
model: 'prod-chat',
apiVersion: '2025-09-01',
apiKey: 'test-key',
compat: { maxTokensField: 'max_completion_tokens' },
});
const body = provider._buildRequestBody(
[{ role: 'user', content: 'hello' }],
{ maxTokens: 2048 },
);
assert.equal(body.max_completion_tokens, 2048, `${Provider.name}: explicit field must use max_completion_tokens`);
assert.equal(body.max_tokens, undefined, `${Provider.name}: legacy field must be absent`);
assert.equal(body.temperature, undefined, `${Provider.name}: reasoning contract omits temperature`);
}
});

test('Azure OpenAI omitTemperature config suppresses temperature on any deployment', () => {
for (const Provider of [AzureOpenAIProviderCh, AzureOpenAIProviderFx]) {
const provider = new Provider({
providerName: 'azure-openai',
baseUrl: 'https://example.openai.azure.com',
model: 'custom-reasoning-deployment',
apiVersion: '2025-09-01',
apiKey: 'test-key',
omitTemperature: true,
});
const body = provider._buildRequestBody([{ role: 'user', content: 'hello' }]);
assert.equal(body.temperature, undefined, `${Provider.name}: omitTemperature must be respected`);
}
});

test('Ask stream aggregation exposes text live but withholds tool calls until response.completed', async () => {
for (const [label, AgentClass] of [['chrome', AgentCh], ['firefox', AgentFx]]) {
let releaseCompleted;
Expand Down
Loading