推理参数映射和保留字段保护
策略的具体实现如下。
ts
function snapshotConfiguration(
config: CompatibleModelConfig,
signal?: AbortSignal,
): CompatibleModelConfig {
const read = <ObjectType extends object, Key extends keyof ObjectType>(
source: ObjectType,
key: Key,
): ObjectType[Key] => {
signal?.throwIfAborted();
const value = source[key];
signal?.throwIfAborted();
return value;
};
try {
signal?.throwIfAborted();
if ('maxTokens' in config) throw new TypeError('Use maxOutputTokens');
// Pin each configuration getter once. Validation and I/O consume this same snapshot.
const baseUrl = read(config, 'baseUrl'),
apiKey = read(config, 'apiKey'),
model = read(config, 'model');
const maxOutputTokens = read(config, 'maxOutputTokens'),
temperature = read(config, 'temperature');
const extensions = read(config, 'requestBody'),
reasoning = read(config, 'reasoning');
const explicit = { ...extensions };
signal?.throwIfAborted();
const fixed = { baseUrl, apiKey, model, maxOutputTokens, temperature, requestBody: explicit };
assertRequestExtensions(fixed);
let generated: Readonly<Record<string, unknown>> = {};
if (reasoning !== undefined) {
if (!reasoning) throw new TypeError('Invalid provider reasoning level');
const level = read(reasoning, 'level'),
format = read(reasoning, 'format');
if (!['off', 'low', 'medium', 'high'].includes(level))
throw new TypeError('Invalid provider reasoning level');
if (format === 'custom') {
const mapper = read(
reasoning as Extract<
NonNullable<CompatibleModelConfig['reasoning']>,
{ format: 'custom' }
>,
'map',
);
if (typeof mapper !== 'function') throw new TypeError('Custom reasoning requires a mapper');
generated = synchronous(
mapper.call(reasoning, level),
'Reasoning mappers must be synchronous',
);
signal?.throwIfAborted();
if (!generated || typeof generated !== 'object' || Array.isArray(generated))
throw new TypeError('Reasoning mapper must return request fields');
} else if (format === 'reasoning-effort')
generated = { reasoning_effort: level === 'off' ? 'none' : level };
else if (format === 'thinking')
generated = { thinking: { type: level === 'off' ? 'disabled' : 'enabled' } };
else if (format === 'enable-thinking') generated = { enable_thinking: level !== 'off' };
else if (format === 'thinking-budget') {
const configured = read(
reasoning as Extract<
NonNullable<CompatibleModelConfig['reasoning']>,
{ format: 'thinking-budget' }
>,
'budgets',
);
const budgets = {
low: configured ? (read(configured, 'low') ?? 1024) : 1024,
medium: configured ? (read(configured, 'medium') ?? 4096) : 4096,
high: configured ? (read(configured, 'high') ?? 8192) : 8192,
};
if (Object.values(budgets).some((value) => !Number.isSafeInteger(value) || value < 1))
throw new TypeError('Thinking budgets must be positive safe integers');
generated = {
thinking:
level === 'off'
? { type: 'disabled' }
: { type: 'enabled', budget_tokens: budgets[level] },
};
} else throw new TypeError('Unknown provider reasoning format');
}
const generatedFields = { ...generated };
signal?.throwIfAborted();
assertRequestExtensions({ ...fixed, requestBody: generatedFields });
const requestBody = structuredClone({ ...generatedFields, ...explicit });
signal?.throwIfAborted();
// Validate the actual materialized object, never re-read the caller's getters.
assertRequestExtensions({ ...fixed, requestBody });
return Object.freeze({ baseUrl, apiKey, model, maxOutputTokens, temperature, requestBody });
} catch (error) {
signal?.throwIfAborted();
throw error;
}
}
function assertRequestExtensions(config: CompatibleModelConfig): void {
if (
config.maxOutputTokens !== undefined &&
(!Number.isSafeInteger(config.maxOutputTokens) || config.maxOutputTokens < 1)
) {
throw new TypeError('Provider maxOutputTokens must be a positive safe integer');
}
for (const key of reservedRequestFields) {
if (config.requestBody && Object.hasOwn(config.requestBody, key)) {
throw new TypeError(`Provider requestBody cannot override ${key}`);
}
}
}