feat(stream): add per-key JSON stream default mode

This commit is contained in:
Jan Leon
2026-05-30 18:33:47 +02:00
parent 31a734966c
commit 99d673fe5b
14 changed files with 192 additions and 13 deletions

View File

@@ -2136,7 +2136,10 @@ export async function handleChatCore({
const stream =
nativeCodexPassthrough && isCompactResponsesEndpoint(endpointPath)
? false
: resolveStreamFlag(body?.stream, acceptHeader, sourceFormat, streamUserAgent);
: resolveStreamFlag(body?.stream, acceptHeader, sourceFormat, {
userAgent: streamUserAgent,
streamDefaultMode: apiKeyInfo?.streamDefaultMode,
});
const settings = cachedSettings ?? (await getCachedSettings());
credentials = applyCodexGlobalFastServiceTier(provider, credentials, settings, {
model: requestedModel,

View File

@@ -2,6 +2,28 @@
* AI SDK compatibility helpers (T26).
*/
export type StreamDefaultMode = "legacy" | "json";
export interface ResolveStreamFlagOptions {
userAgent?: unknown;
streamDefaultMode?: unknown;
}
function normalizeResolveStreamFlagOptions(optionsOrUserAgent?: unknown): ResolveStreamFlagOptions {
if (
optionsOrUserAgent &&
typeof optionsOrUserAgent === "object" &&
!Array.isArray(optionsOrUserAgent)
) {
return optionsOrUserAgent as ResolveStreamFlagOptions;
}
return { userAgent: optionsOrUserAgent };
}
export function normalizeStreamDefaultMode(value: unknown): StreamDefaultMode {
return value === "json" ? "json" : "legacy";
}
/**
* Detects when a client explicitly prefers JSON (non-SSE) responses.
*/
@@ -30,12 +52,15 @@ export function resolveStreamFlag(
bodyStream: unknown,
acceptHeader: unknown,
sourceFormat?: string,
userAgent?: unknown
optionsOrUserAgent?: unknown
): boolean {
// Explicit body value always wins
if (bodyStream === true) return true;
if (bodyStream === false) return false;
const options = normalizeResolveStreamFlagOptions(optionsOrUserAgent);
const streamDefaultMode = normalizeStreamDefaultMode(options.streamDefaultMode);
const acceptsEventStream =
typeof acceptHeader === "string" && /text\/event-stream/i.test(acceptHeader);
@@ -52,7 +77,14 @@ export function resolveStreamFlag(
// does not set `stream: false`. With a wildcard/empty Accept header, the legacy
// OmniRoute fallback would force SSE upstream and fail JSON-only providers as
// STREAM_EARLY_EOF before Nextcloud could receive a response.
if (isKnownJsonOnlyClient(userAgent) && !acceptsEventStream) {
if (isKnownJsonOnlyClient(options.userAgent) && !acceptsEventStream) {
return false;
}
// Per-key compatibility mode for synchronous OpenAI-compatible clients that
// omit `stream`. This preserves legacy behavior by default while allowing an
// API key to use the OpenAI-compatible JSON default unless SSE is explicit.
if (streamDefaultMode === "json" && !acceptsEventStream) {
return false;
}

View File

@@ -80,6 +80,8 @@ interface AccessSchedule {
tz: string;
}
type StreamDefaultMode = "legacy" | "json";
interface ApiKey {
id: string;
name: string;
@@ -98,6 +100,7 @@ interface ApiKey {
rateLimits?: Array<{ limit: number; window: number }> | null;
scopes?: string[];
allowedEndpoints?: string[];
streamDefaultMode?: StreamDefaultMode;
createdAt: string;
}
@@ -480,7 +483,8 @@ export default function ApiManagerPageClient() {
accessSchedule: AccessSchedule | null,
rateLimits: Array<{ limit: number; window: number }> | null,
scopes: string[],
allowedEndpoints: string[]
allowedEndpoints: string[],
streamDefaultMode: StreamDefaultMode
) => {
if (!editingKey || !editingKey.id) return;
@@ -541,6 +545,7 @@ export default function ApiManagerPageClient() {
rateLimits,
scopes,
allowedEndpoints,
streamDefaultMode,
}),
});
@@ -791,6 +796,7 @@ export default function ApiManagerPageClient() {
: 0;
const hasThrottle = throttleDelayMs > 0;
const hasManageScope = Array.isArray(key.scopes) && key.scopes.includes("manage");
const hasJsonStreamDefault = key.streamDefaultMode === "json";
const maxSessions = typeof key.maxSessions === "number" ? key.maxSessions : 0;
const hasSessionLimit = maxSessions > 0;
const activeSessions = sessionCounts[key.id] || 0;
@@ -885,6 +891,12 @@ export default function ApiManagerPageClient() {
Auto-Resolve
</span>
)}
{hasJsonStreamDefault && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-sky-500/10 text-sky-600 dark:text-sky-400 text-[11px] font-medium">
<span className="material-symbols-outlined text-[12px]">data_object</span>
{t("streamDefaultBadge")}
</span>
)}
{hasSessionLimit && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 text-[11px] font-medium">
<span className="material-symbols-outlined text-[12px]">group</span>
@@ -1238,7 +1250,8 @@ const PermissionsModal = memo(function PermissionsModal({
accessSchedule: AccessSchedule | null,
rateLimits: Array<{ limit: number; window: number }> | null,
scopes: string[],
allowedEndpoints: string[]
allowedEndpoints: string[],
streamDefaultMode: StreamDefaultMode
) => void;
}) {
const t = useTranslations("apiManager");
@@ -1289,6 +1302,9 @@ const PermissionsModal = memo(function PermissionsModal({
const [rateLimits, setRateLimits] = useState<Array<{ limit: number; window: number }>>(
Array.isArray(apiKey?.rateLimits) ? apiKey.rateLimits : []
);
const [streamDefaultMode, setStreamDefaultMode] = useState<StreamDefaultMode>(
apiKey?.streamDefaultMode === "json" ? "json" : "legacy"
);
const [nameError, setNameError] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const [selectedConnections, setSelectedConnections] = useState<string[]>(initialConnections);
@@ -1453,7 +1469,8 @@ const PermissionsModal = memo(function PermissionsModal({
selfUsageEnabled,
selfAccountQuotaEnabled,
}),
allowAllEndpoints ? [] : selectedEndpoints
allowAllEndpoints ? [] : selectedEndpoints,
streamDefaultMode
);
}, [
onSave,
@@ -1482,6 +1499,7 @@ const PermissionsModal = memo(function PermissionsModal({
rateLimits,
allowAllEndpoints,
selectedEndpoints,
streamDefaultMode,
apiKey?.scopes,
t,
]);
@@ -1863,6 +1881,40 @@ const PermissionsModal = memo(function PermissionsModal({
</button>
</div>
{/* Stream Default Compatibility */}
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 p-3 rounded-lg border border-border bg-surface/40">
<div className="flex flex-col gap-1 min-w-0">
<p className="text-sm font-medium text-text-main">{t("streamDefaultMode")}</p>
<p className="text-xs text-text-muted">{t("streamDefaultModeDesc")}</p>
</div>
<div className="flex gap-1 p-0.5 bg-surface rounded-md shrink-0 w-full sm:w-auto">
<button
type="button"
onClick={() => setStreamDefaultMode("legacy")}
className={`inline-flex flex-1 sm:flex-none items-center justify-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-semibold transition-all ${
streamDefaultMode === "legacy"
? "bg-primary text-white"
: "text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
}`}
>
<span className="material-symbols-outlined text-[14px]">settings_backup_restore</span>
{t("streamDefaultLegacy")}
</button>
<button
type="button"
onClick={() => setStreamDefaultMode("json")}
className={`inline-flex flex-1 sm:flex-none items-center justify-center gap-1.5 px-2.5 py-1.5 rounded text-xs font-semibold transition-all ${
streamDefaultMode === "json"
? "bg-primary text-white"
: "text-text-muted hover:bg-black/5 dark:hover:bg-white/5"
}`}
>
<span className="material-symbols-outlined text-[14px]">data_object</span>
{t("streamDefaultJson")}
</button>
</div>
</div>
{/* Ban Toggle (SECURITY) */}
<div className="flex items-start justify-between gap-3 p-3 rounded-lg border border-red-500/20 bg-red-500/5">
<div className="flex flex-col gap-1">

View File

@@ -78,6 +78,8 @@ export async function PATCH(request, { params }) {
accessSchedule,
rateLimits,
scopes,
allowedEndpoints,
streamDefaultMode,
} = validation.data;
const payload: Parameters<typeof updateApiKeyPermissions>[1] = {};
@@ -95,6 +97,8 @@ export async function PATCH(request, { params }) {
if (accessSchedule !== undefined) payload.accessSchedule = accessSchedule;
if (rateLimits !== undefined) payload.rateLimits = rateLimits;
if (scopes !== undefined) payload.scopes = scopes;
if (allowedEndpoints !== undefined) payload.allowedEndpoints = allowedEndpoints;
if (streamDefaultMode !== undefined) payload.streamDefaultMode = streamDefaultMode;
const updated = await updateApiKeyPermissions(id, payload);
if (!updated) {
@@ -120,6 +124,8 @@ export async function PATCH(request, { params }) {
...(accessSchedule !== undefined && { accessSchedule }),
...(rateLimits !== undefined && { rateLimits }),
...(scopes !== undefined && { scopes }),
...(allowedEndpoints !== undefined && { allowedEndpoints }),
...(streamDefaultMode !== undefined && { streamDefaultMode }),
});
} catch (error) {
log.error("keys", "Error updating key permissions", error);

View File

@@ -83,6 +83,7 @@ export async function POST(request) {
id: apiKey.id,
machineId: apiKey.machineId,
noLog: noLog === true,
streamDefaultMode: "legacy",
},
{ status: 201 }
);

View File

@@ -1481,6 +1481,11 @@
"endpointsRestricted": "__MISSING__:Restricted to {count} endpoint{count, plural, one {} other {s}}.",
"autoResolve": "Auto-Resolve",
"autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.",
"streamDefaultMode": "Stream-Standardverhalten",
"streamDefaultModeDesc": "Steuert fehlende `stream`-Angaben für diesen Schlüssel. Im JSON-Modus werden nicht-streamende Antworten zurückgegeben, außer der Client fordert SSE explizit an.",
"streamDefaultLegacy": "Legacy",
"streamDefaultJson": "JSON-kompatibel",
"streamDefaultBadge": "JSON-Standardstream",
"keyActive": "Key Active",
"keyActiveDesc": "Enable or disable this API key. Disabled keys are immediately rejected with 403.",
"accessSchedule": "Access Schedule",

View File

@@ -1481,6 +1481,11 @@
"endpointsRestricted": "Restricted to {count} endpoint{count, plural, one {} other {s}}.",
"autoResolve": "Auto-Resolve",
"autoResolveDesc": "Auto-resolve ambiguous model names to native provider for this API key.",
"streamDefaultMode": "Stream Default Compatibility",
"streamDefaultModeDesc": "Controls omitted `stream` flags for this key. JSON mode returns non-streaming responses unless the client explicitly requests SSE.",
"streamDefaultLegacy": "Legacy",
"streamDefaultJson": "JSON Compatible",
"streamDefaultBadge": "JSON stream default",
"keyActive": "Key Active",
"keyActiveDesc": "Enable or disable this API key. Disabled keys are immediately rejected with 403.",
"accessSchedule": "Access Schedule",

View File

@@ -60,6 +60,7 @@ interface ApiKeyMetadata {
isBanned: boolean;
keyHash: string | null;
allowedEndpoints: string[];
streamDefaultMode: "legacy" | "json";
}
interface ApiKeyRow extends JsonRecord {
@@ -84,6 +85,8 @@ interface ApiKeyRow extends JsonRecord {
accessSchedule?: unknown;
rate_limits?: unknown;
rateLimits?: unknown;
stream_default_mode?: unknown;
streamDefaultMode?: unknown;
}
interface StatementLike<TRow = unknown> {
@@ -121,6 +124,7 @@ interface ApiKeyView extends JsonRecord {
isBanned?: boolean;
expiresAt?: string | null;
allowedEndpoints: string[];
streamDefaultMode: "legacy" | "json";
}
// LRU cache for API key validation (valid keys only)
@@ -156,6 +160,7 @@ const API_KEY_COLUMN_FALLBACKS = [
{ name: "is_banned", definition: "is_banned INTEGER NOT NULL DEFAULT 0" },
{ name: "key_hash", definition: "key_hash TEXT" },
{ name: "allowed_endpoints", definition: "allowed_endpoints TEXT" },
{ name: "stream_default_mode", definition: "stream_default_mode TEXT NOT NULL DEFAULT 'legacy'" },
] as const;
// Cache for model permission checks
@@ -355,7 +360,7 @@ function getPreparedStatements(db: ApiKeysDbLike): ApiKeysStatements {
"SELECT id, expires_at, revoked_at, is_active, is_banned FROM api_keys WHERE key = ? OR key_hash = ?"
);
_stmtGetKeyMetadata = db.prepare<ApiKeyRow>(
"SELECT id, name, machine_id, allowed_models, allowed_combos, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, throttle_delay_ms, max_sessions, revoked_at, expires_at, ip_allowlist, scopes, rate_limits, is_banned, key_hash, allowed_endpoints FROM api_keys WHERE key = ? OR key_hash = ?"
"SELECT id, name, machine_id, allowed_models, allowed_combos, allowed_connections, no_log, auto_resolve, is_active, access_schedule, max_requests_per_day, max_requests_per_minute, throttle_delay_ms, max_sessions, revoked_at, expires_at, ip_allowlist, scopes, rate_limits, is_banned, key_hash, allowed_endpoints, stream_default_mode FROM api_keys WHERE key = ? OR key_hash = ?"
);
_stmtInsertKey = db.prepare(
"INSERT INTO api_keys (id, name, key, machine_id, allowed_models, no_log, created_at, key_prefix, key_hash, scopes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
@@ -401,6 +406,7 @@ export async function getApiKeys() {
camelRow.isBanned = parseIsBanned(camelRow.isBanned);
camelRow.scopes = parseStringList((camelRow as JsonRecord).scopes);
camelRow.allowedEndpoints = parseStringList((camelRow as JsonRecord).allowedEndpoints);
camelRow.streamDefaultMode = parseStreamDefaultMode((camelRow as JsonRecord).streamDefaultMode);
if (typeof camelRow.id === "string" && camelRow.id.length > 0) {
setNoLog(camelRow.id, camelRow.noLog === true);
}
@@ -425,6 +431,7 @@ export async function getApiKeyById(id: string) {
camelRow.isBanned = parseIsBanned(camelRow.isBanned);
camelRow.scopes = parseStringList((camelRow as JsonRecord).scopes);
camelRow.allowedEndpoints = parseStringList((camelRow as JsonRecord).allowedEndpoints);
camelRow.streamDefaultMode = parseStreamDefaultMode((camelRow as JsonRecord).streamDefaultMode);
if (typeof camelRow.id === "string" && camelRow.id.length > 0) {
setNoLog(camelRow.id, camelRow.noLog === true);
}
@@ -552,6 +559,10 @@ function parseIsBanned(value: unknown): boolean {
return value === 1 || value === "1" || value === true;
}
function parseStreamDefaultMode(value: unknown): "legacy" | "json" {
return value === "json" ? "json" : "legacy";
}
async function hashKey(key: string): Promise<string> {
if (!key || typeof key !== "string") return "";
// CodeQL: This is intentionally SHA-256, NOT password hashing. API keys are
@@ -661,6 +672,7 @@ export async function updateApiKeyPermissions(
maxSessions?: number | null;
scopes?: string[] | null;
allowedEndpoints?: string[] | null;
streamDefaultMode?: "legacy" | "json" | null;
}
) {
const db = getDbInstance() as ApiKeysDbLike;
@@ -687,6 +699,8 @@ export async function updateApiKeyPermissions(
maxSessions: (update as { maxSessions?: number | null }).maxSessions,
scopes: (update as { scopes?: string[] | null }).scopes,
allowedEndpoints: (update as { allowedEndpoints?: string[] | null }).allowedEndpoints,
streamDefaultMode: (update as { streamDefaultMode?: "legacy" | "json" | null })
.streamDefaultMode,
};
if (
@@ -706,7 +720,8 @@ export async function updateApiKeyPermissions(
normalized.expiresAt === undefined &&
(normalized as Record<string, unknown>).maxSessions === undefined &&
(normalized as Record<string, unknown>).scopes === undefined &&
(normalized as Record<string, unknown>).allowedEndpoints === undefined
(normalized as Record<string, unknown>).allowedEndpoints === undefined &&
(normalized as Record<string, unknown>).streamDefaultMode === undefined
) {
return false;
}
@@ -730,6 +745,7 @@ export async function updateApiKeyPermissions(
maxSessions?: number;
expiresAt?: string | null;
scopes?: string;
streamDefaultMode?: "legacy" | "json";
} = { id };
if (normalized.name !== undefined) {
@@ -817,13 +833,17 @@ export async function updateApiKeyPermissions(
if (allowedEndpointsUpdate !== undefined) {
updates.push("allowed_endpoints = @allowedEndpoints");
const nextEndpoints: string[] = Array.isArray(allowedEndpointsUpdate)
? (allowedEndpointsUpdate as unknown[]).filter(
(s): s is string => typeof s === "string"
)
? (allowedEndpointsUpdate as unknown[]).filter((s): s is string => typeof s === "string")
: [];
(params as Record<string, unknown>).allowedEndpoints = JSON.stringify(nextEndpoints);
}
const streamDefaultModeUpdate = (normalized as Record<string, unknown>).streamDefaultMode;
if (streamDefaultModeUpdate !== undefined) {
updates.push("stream_default_mode = @streamDefaultMode");
params.streamDefaultMode = parseStreamDefaultMode(streamDefaultModeUpdate);
}
const scopesUpdate = (normalized as Record<string, unknown>).scopes;
const nextScopes: string[] = Array.isArray(scopesUpdate)
? (scopesUpdate as unknown[]).filter((s): s is string => typeof s === "string")
@@ -1171,6 +1191,7 @@ export async function getApiKeyMetadata(
keyHash: null,
scopes: ["manage"],
allowedEndpoints: [],
streamDefaultMode: "legacy",
};
}
@@ -1228,6 +1249,9 @@ export async function getApiKeyMetadata(
allowedEndpoints: parseStringList(
(record as JsonRecord).allowed_endpoints ?? (record as JsonRecord).allowedEndpoints
),
streamDefaultMode: parseStreamDefaultMode(
(record as JsonRecord).stream_default_mode ?? (record as JsonRecord).streamDefaultMode
),
};
if (!metadata.id) {

View File

@@ -0,0 +1,3 @@
-- 077: Per-API-key default for omitted chat completion stream flags.
ALTER TABLE api_keys ADD COLUMN stream_default_mode TEXT NOT NULL DEFAULT 'legacy';

View File

@@ -1856,6 +1856,7 @@ export const updateKeyPermissionsSchema = z
.optional(),
scopes: z.array(z.string().trim().min(1).max(64)).max(32).optional(),
allowedEndpoints: z.array(z.string().trim().min(1).max(64)).max(20).optional(),
streamDefaultMode: z.enum(["legacy", "json"]).optional(),
})
.superRefine((value, ctx) => {
if (
@@ -1873,7 +1874,8 @@ export const updateKeyPermissionsSchema = z
value.accessSchedule === undefined &&
value.rateLimits === undefined &&
value.scopes === undefined &&
value.allowedEndpoints === undefined
value.allowedEndpoints === undefined &&
value.streamDefaultMode === undefined
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,

View File

@@ -103,10 +103,14 @@ async function invokeChatCore({
const originalFetch = globalThis.fetch;
const calls = [];
const nextcloudJsonOnlyClient = /nextcloud\s+openai\/localai\s+integration/i.test(userAgent);
const jsonStreamDefault = apiKeyInfo?.streamDefaultMode === "json";
const resolvedStream =
body?.stream === true ||
(body?.stream === undefined && String(accept).toLowerCase().includes("text/event-stream")) ||
(body?.stream === undefined && !nextcloudJsonOnlyClient && !String(accept).includes("json"));
(body?.stream === undefined &&
!nextcloudJsonOnlyClient &&
!jsonStreamDefault &&
!String(accept).includes("json"));
globalThis.fetch = async (url, init = {}) => {
const parsedBody = init.body ? JSON.parse(String(init.body)) : null;
@@ -527,6 +531,28 @@ test("chatCore treats Nextcloud OpenAI integration requests as non-streaming by
assert.equal(nextcloudExplicitStream.call.headers.Accept, "text/event-stream");
});
test("chatCore honors API key JSON stream-default compatibility mode", async () => {
const jsonCompatibleDefault = await invokeChatCore({
accept: "*/*",
userAgent: "generic-openai-client",
apiKeyInfo: { id: "json-stream-default-key", streamDefaultMode: "json" },
body: { model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }] },
});
const explicitSse = await invokeChatCore({
accept: "text/event-stream",
userAgent: "generic-openai-client",
apiKeyInfo: { id: "json-stream-default-key", streamDefaultMode: "json" },
body: { model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }] },
});
assert.equal(jsonCompatibleDefault.call.headers.Accept, "application/json");
assert.equal(
jsonCompatibleDefault.result.response.headers.get("content-type"),
"application/json"
);
assert.equal(explicitSse.call.headers.Accept, "text/event-stream");
});
test("chatCore injects memories when enabled and memories are found", async () => {
await settingsDb.updateSettings({
memoryEnabled: true,

View File

@@ -62,6 +62,7 @@ test("createApiKey requires machineId and returns a persisted key with defaults"
assert.equal(byId.autoResolve, false);
assert.equal(byId.isActive, true);
assert.equal(byId.maxSessions, 0);
assert.equal(byId.streamDefaultMode, "legacy");
});
test("updateApiKeyPermissions persists settings, schedule and rate limits", async () => {
@@ -87,6 +88,7 @@ test("updateApiKeyPermissions persists settings, schedule and rate limits", asyn
maxRequestsPerMinute: 15,
throttleDelayMs: 250,
maxSessions: -3,
streamDefaultMode: "json",
});
const row = await apiKeysDb.getApiKeyById(created.id);
const metadata = await apiKeysDb.getApiKeyMetadata(created.key);
@@ -104,6 +106,8 @@ test("updateApiKeyPermissions persists settings, schedule and rate limits", asyn
assert.equal(metadata.maxRequestsPerMinute, 15);
assert.equal(metadata.throttleDelayMs, 250);
assert.equal(metadata.maxSessions, 0);
assert.equal(row.streamDefaultMode, "json");
assert.equal(metadata.streamDefaultMode, "json");
});
test("validateApiKey and deleteApiKey stay consistent after cache invalidation", async () => {

View File

@@ -64,6 +64,11 @@ test("updateKeyPermissionsSchema accepts noLog-only updates and rejects empty pa
});
assert.equal(maxSessionsOnly.success, true);
const streamDefaultOnly = schemas.validateBody(schemas.updateKeyPermissionsSchema, {
streamDefaultMode: "json",
});
assert.equal(streamDefaultOnly.success, true);
const emptyPayload = schemas.validateBody(schemas.updateKeyPermissionsSchema, {});
assert.equal(emptyPayload.success, false);
});

View File

@@ -93,6 +93,17 @@ test("T26: Nextcloud OpenAI integration defaults to non-streaming JSON", () => {
assert.equal(resolveStreamFlag(true, "application/json", "openai", ua), true);
});
test("T26: per-key JSON stream default keeps omitted stream non-streaming", () => {
const options = { streamDefaultMode: "json", userAgent: "generic-openai-client" };
assert.equal(resolveStreamFlag(undefined, undefined, "openai", options), false);
assert.equal(resolveStreamFlag(undefined, "*/*", "openai", options), false);
assert.equal(resolveStreamFlag(undefined, "application/json", "openai", options), false);
assert.equal(resolveStreamFlag(undefined, "text/event-stream", "openai", options), true);
assert.equal(resolveStreamFlag(true, "application/json", "openai", options), true);
assert.equal(resolveStreamFlag(false, "text/event-stream", "openai", options), false);
});
test("T26: explicit non-stream aliases are detected", () => {
assert.equal(hasExplicitNoStreamParam({ non_stream: true }), true);
assert.equal(hasExplicitNoStreamParam({ disable_stream: true }), true);