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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veryfront",
"version": "0.1.987",
"version": "0.1.988",
"license": "Apache-2.0",
"nodeModulesDir": "auto",
"minimumDependencyAge": {
Expand Down
8 changes: 6 additions & 2 deletions extensions/ext-llm-openai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Any model accessible through the OpenAI Chat Completions, Responses, or Embeddin

- **Flagship:** `gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `gpt-4o`, `gpt-4o-mini`
- **Frontier:** `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
- **Reasoning:** `o3`, `o4-mini`, `o1`, `o1-mini`, `o3-mini` (sampling parameters are automatically dropped with warnings)
- **Reasoning:** `gpt-5.4-nano`, current `gpt-5`/`gpt-5.x` reasoning models, `o3`, `o4-mini`, `o1`, `o3-mini` (sampling parameters are automatically dropped with warnings)
- **Embeddings:** `text-embedding-3-small`, `text-embedding-3-large`
- **OpenAI-compatible:** Any third-party model reachable via an OpenAI-compatible endpoint (set `OPENAI_BASE_URL`)

Expand All @@ -74,7 +74,11 @@ The extension accepts configuration through `LLMProviderConfig` when creating ru

## Model-Specific Behavior

### Reasoning Models (o3, o4-mini, o1)
### Reasoning Models (GPT-5.x, o3, o4-mini, o1)

Default reasoning params are applied only for native `openai` and `veryfront-cloud` providers.
OpenAI-compatible providers require explicit `reasoning` options. `gpt-5-chat-latest`,
`gpt-5.1`, `o1-mini`, and `o1-preview` are left unmodified by default.

Reasoning models automatically:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,102 @@ function createWarningCollector() {
}

describe("ext-llm-openai/openai-chat-request-builder", () => {
it("sets default reasoning effort for GPT-5.5 chat requests", () => {
const warnings = createWarningCollector();

const body = buildOpenAIChatRequest(
"gpt-5.5",
"openai",
{
prompt: [{ role: "user", content: [{ type: "text", text: "Think carefully." }] }],
temperature: 0.2,
},
true,
warnings,
);

assertEquals(body.reasoning_effort, "medium");
assertEquals(body.temperature, undefined);
assertEquals(warnings.drain().map((warning) => warning.setting), ["temperature"]);
});

it("does not set default reasoning effort for GPT-5 chat snapshots", () => {
const warnings = createWarningCollector();

const body = buildOpenAIChatRequest(
"gpt-5-chat-latest",
"openai",
{
prompt: [{ role: "user", content: [{ type: "text", text: "Be concise." }] }],
temperature: 0.2,
},
true,
warnings,
);

assertEquals(body.reasoning_effort, undefined);
assertEquals(body.temperature, 0.2);
assertEquals(warnings.drain(), []);
});

it("does not set default reasoning effort for legacy o1 chat variants", () => {
const warnings = createWarningCollector();

const body = buildOpenAIChatRequest(
"o1-mini",
"openai",
{
prompt: [{ role: "user", content: [{ type: "text", text: "Be concise." }] }],
temperature: 0.2,
},
true,
warnings,
);

assertEquals(body.reasoning_effort, undefined);
assertEquals(body.temperature, undefined);
assertEquals(warnings.drain().map((warning) => warning.setting), ["temperature"]);
});

it("does not set default reasoning effort for OpenAI-compatible providers but still drops rejected sampling params", () => {
const warnings = createWarningCollector();

const body = buildOpenAIChatRequest(
"gpt-5.5",
"azure",
{
prompt: [{ role: "user", content: [{ type: "text", text: "Be concise." }] }],
temperature: 0.2,
},
true,
warnings,
);

assertEquals(body.reasoning_effort, undefined);
assertEquals(body.temperature, undefined);
assertEquals(warnings.drain().map((warning) => warning.setting), ["temperature"]);
});

it("drops rejected sampling params when explicit reasoning is disabled", () => {
const warnings = createWarningCollector();

const body = buildOpenAIChatRequest(
"o3-mini",
"openai",
{
prompt: [{ role: "user", content: [{ type: "text", text: "Be concise." }] }],
reasoning: { enabled: false },
temperature: 0.2,
},
true,
warnings,
);

assertEquals(body.reasoning_effort, undefined);
assertEquals(body.temperature, undefined);
assertEquals(warnings.drain().map((warning) => warning.setting), ["temperature"]);
});

it("preserves chat request shaping, provider option merge order, and warnings", () => {
const prompt: RuntimePromptMessage[] = [
{ role: "system", content: "You are concise." },
Expand Down
53 changes: 15 additions & 38 deletions extensions/ext-llm-openai/src/openai-chat-request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import {
unwrapToolInputSchema,
} from "veryfront/provider/shared";
import type { OpenAICompatibleChatRequest, RuntimePromptMessage } from "veryfront/provider/shared";

type ProviderReasoningEffort = "low" | "medium" | "high" | "max";

type ProviderReasoningOption = {
enabled?: boolean;
effort?: ProviderReasoningEffort;
budgetTokens?: number;
};
import {
type OpenAIProviderReasoningOption,
rejectsOpenAISamplingParams,
resolveOpenAIReasoningConfig,
} from "./openai-reasoning-models.ts";

export type RuntimeToolDefinition =
| {
Expand Down Expand Up @@ -44,7 +41,7 @@ export type OpenAICompatibleLanguageOptions = {
providerOptions?: Record<string, unknown>;
includeRawChunks?: boolean;
abortSignal?: AbortSignal;
reasoning?: ProviderReasoningOption;
reasoning?: OpenAIProviderReasoningOption;
userId?: string;
serviceTier?: "auto" | "default" | "flex" | "scale";
parallelToolCalls?: boolean;
Expand Down Expand Up @@ -75,10 +72,6 @@ type WarningCollector = {
}>;
};

function isOpenAIReasoningModel(modelId: string): boolean {
return /^o[134](-|$)/.test(modelId);
}

function isNativeOpenAIModel(modelId: string): boolean {
return /^(gpt-|o[134](-|$)|chatgpt-)/.test(modelId);
}
Expand All @@ -87,36 +80,18 @@ function isFixedSamplingModel(modelId: string): boolean {
return /^kimi-k2\.5/.test(modelId);
}

function resolveOpenAIReasoningEffort(
option: ProviderReasoningOption | undefined,
): "low" | "medium" | "high" | undefined {
if (!option || option.enabled !== true) {
return undefined;
}
switch (option.effort) {
case "low":
return "low";
case "high":
case "max":
return "high";
case "medium":
default:
return "medium";
}
}

export function buildOpenAIChatRequest(
modelId: string,
providerName: string,
options: OpenAICompatibleLanguageOptions,
stream: boolean,
warnings: WarningCollector,
): OpenAICompatibleChatRequest {
const isReasoningModel = isOpenAIReasoningModel(modelId);
const reasoningEffort = resolveOpenAIReasoningEffort(options.reasoning);
const reasoningEnabled = isReasoningModel || reasoningEffort !== undefined;
const reasoning = resolveOpenAIReasoningConfig(modelId, providerName, options.reasoning);
const reasoningEnabled = reasoning !== undefined;
const samplingRejected = rejectsOpenAISamplingParams(modelId);
const fixedSampling = isFixedSamplingModel(modelId);
const dropSamplingParams = reasoningEnabled || fixedSampling;
const dropSamplingParams = reasoningEnabled || samplingRejected || fixedSampling;

// OpenAI Chat Completions has no top_k surface.
if (options.topK !== undefined) {
Expand All @@ -128,7 +103,7 @@ export function buildOpenAIChatRequest(
});
}

// Reasoning models (o1 / o3 / o4) and models with fixed sampling params
// Reasoning models and models with fixed sampling params
// reject sampling params outright. Emit warnings.
if (dropSamplingParams) {
const dropped: Array<[keyof typeof options, string]> = [
Expand All @@ -145,7 +120,9 @@ export function buildOpenAIChatRequest(
setting: key,
details: fixedSampling
? `Dropped because this model uses fixed sampling parameters.`
: `Dropped because OpenAI reasoning models reject ${openaiName}. Reasoning was active for this request.`,
: samplingRejected
? `Dropped because this model rejects ${openaiName}.`
: `Dropped because reasoning was active for this request and OpenAI rejects ${openaiName} with reasoning.`,
});
}
}
Expand Down Expand Up @@ -178,7 +155,7 @@ export function buildOpenAIChatRequest(
...(!dropSamplingParams && options.frequencyPenalty !== undefined
? { frequency_penalty: options.frequencyPenalty }
: {}),
...(reasoningEffort !== undefined ? { reasoning_effort: reasoningEffort } : {}),
...(reasoning !== undefined ? { reasoning_effort: reasoning.effort } : {}),
...(typeof options.userId === "string" && options.userId.length > 0
? { user: options.userId }
: {}),
Expand Down
59 changes: 59 additions & 0 deletions extensions/ext-llm-openai/src/openai-reasoning-models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import {
getDefaultOpenAIReasoningEffort,
rejectsOpenAISamplingParams,
} from "./openai-reasoning-models.ts";

describe("ext-llm-openai/openai-reasoning-models", () => {
it("defaults known reasoning models while excluding chat snapshots and legacy o1 variants", () => {
const cases: Array<[string, "medium" | undefined]> = [
["gpt-5", "medium"],
["gpt-5-mini", "medium"],
["gpt-5.4-nano", "medium"],
["gpt-5.5", "medium"],
["gpt-5.1", undefined],
["gpt-5-chat-latest", undefined],
["o1", "medium"],
["o1-2024-12-17", "medium"],
["o1-mini", undefined],
["o1-preview", undefined],
["o3-mini", "medium"],
["o4-mini", "medium"],
];

for (const [modelId, expected] of cases) {
assertEquals(getDefaultOpenAIReasoningEffort(modelId), expected, modelId);
}
});

it("only enables default reasoning params for native OpenAI providers", () => {
assertEquals(getDefaultOpenAIReasoningEffort("gpt-5.4-nano", "openai"), "medium");
assertEquals(getDefaultOpenAIReasoningEffort("gpt-5.4-nano", "veryfront-cloud"), "medium");
assertEquals(getDefaultOpenAIReasoningEffort("gpt-5.4-nano", "azure"), undefined);
assertEquals(getDefaultOpenAIReasoningEffort("gpt-5.4-nano", "moonshot"), undefined);
});

it("detects models that reject sampling params separately from default reasoning params", () => {
const cases: Array<[string, boolean]> = [
["gpt-5", true],
["gpt-5-mini", true],
["gpt-5.4-nano", true],
["gpt-5.5", true],
["gpt-5.1", false],
["gpt-5-chat-latest", false],
["o1", true],
["o1-2024-12-17", true],
["o1-mini", true],
["o1-preview", true],
["o1-pro", true],
["o3-mini", true],
["o4-mini", true],
["gpt-4o-mini", false],
];

for (const [modelId, expected] of cases) {
assertEquals(rejectsOpenAISamplingParams(modelId), expected, modelId);
}
});
});
Loading