Skip to content
English

推理参数映射和保留字段保护 ​

策略的具体实现如下。

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}`);
    }
  }
}

接口参考、使用示例与开发指南。