Ctrl K
ComunidadGuías…

src/models/openai-compatible.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { configuredRuntimeHome } from '../config/env.ts';
import { SecretStore } from '../control-plane/secrets.ts';
import { ProviderResponseError, ProviderUnavailable, providerError, retryProvider, type ProviderFailureCode } from '../harness/retry.ts';
import { budget } from '../harness/budget.ts';
import { bounded, type Trace } from '../harness/trace.ts';
import type { ModelRequestOptions, ProviderAdapter, ResolvedModelSelection } from './types.ts';
import { ModelAdapterError } from './types.ts';
import { applyOutputBudget, resolveOutputBudget } from './output-budget.ts';

const PROVIDER_SPECIFIC_KEYS = ['thinking', 'reasoning_effort', 'reasoning', 'enable_thinking', 'thinking_budget'];

/** Options that are safe and have the same meaning across the five transports. */
export const COMMON_REQUEST_OPTIONS = [
  'temperature', 'top_p', 'max_tokens', 'max_output_tokens', 'max_completion_tokens', 'presence_penalty', 'frequency_penalty', 'seed',
] as const;

function assertOptionKeys(adapter: ProviderAdapter, options: ModelRequestOptions) {
  const allowed = new Set([...COMMON_REQUEST_OPTIONS, ...adapter.optionKeys]);
  for (const key of Object.keys(options)) {
    if (!allowed.has(key)) throw new ModelAdapterError('unsupported_request_option', `Request option is not supported by ${adapter.type}: ${key}`, key);
  }
}

function sanitizedBody(body: Record<string, unknown>) {
  const copy = { ...body };
  for (const key of PROVIDER_SPECIFIC_KEYS) delete copy[key];
  return copy;
}

export function applyCommonRequestOptions(body: Record<string, unknown>, options: ModelRequestOptions) {
  for (const key of COMMON_REQUEST_OPTIONS) {
    if (options[key] !== undefined) body[key] = options[key];
  }
  return body;
}

export function optionString(options: ModelRequestOptions, key: string) {
  const value = options[key];
  if (value === undefined) return undefined;
  if (typeof value !== 'string' || value.trim() === '') throw new ModelAdapterError('invalid_configuration', `${key} must be a non-empty string.`, key);
  return value.trim();
}

export function optionBoolean(options: ModelRequestOptions, key: string) {
  const value = options[key];
  if (value === undefined) return undefined;
  if (typeof value !== 'boolean') throw new ModelAdapterError('invalid_configuration', `${key} must be a boolean.`, key);
  return value;
}

export function optionNumber(options: ModelRequestOptions, key: string) {
  const value = options[key];
  if (value === undefined) return undefined;
  if (typeof value !== 'number' || !Number.isFinite(value)) throw new ModelAdapterError('invalid_configuration', `${key} must be a finite number.`, key);
  return value;
}

function hasSseDoneMarker(raw: string) {
  return /^data:\s*\[DONE\]\s*$/m.test(raw);
}

function numericStatus(value: unknown) {
  return typeof value === 'number' && Number.isSafeInteger(value) && value >= 400 && value <= 599 ? value : undefined;
}

function failureCodeForStatus(status: number | undefined): ProviderFailureCode {
  if (status === 401 || status === 403) return 'provider_auth_failed';
  if (status === 408 || status === 429 || (status !== undefined && status >= 500)) return 'provider_upstream_unavailable';
  if (status !== undefined && status >= 400) return 'provider_request_rejected';
  return 'provider_protocol_error';
}

function retryableStatus(status: number | undefined) {
  return status === 408 || status === 429 || (status !== undefined && status >= 500);
}

function retryAfterDelay(value: string | null) {
  if (!value) return undefined;
  const seconds = Number(value);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
  const date = Date.parse(value);
  return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
}

function providerResponseError(raw: string, responseStatus: number, responseRetryAfterMs?: number) {
  let payload: unknown;
  try { payload = JSON.parse(raw); } catch { payload = undefined; }
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return undefined;
  const envelope = (payload as { error?: unknown }).error;
  if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) return undefined;
  const error = envelope as { code?: unknown; status?: unknown; message?: unknown; type?: unknown };
  const upstreamStatus = numericStatus(error.status) ?? numericStatus(error.code) ?? numericStatus(responseStatus);
  const failureCode = failureCodeForStatus(upstreamStatus);
  const upstreamMessage = typeof error.message === 'string' ? error.message.slice(0, 1000) : undefined;
  const upstreamCode = typeof error.code === 'string' || typeof error.code === 'number' ? error.code : undefined;
  return new ProviderResponseError(failureCode, upstreamMessage ?? 'Provider returned an error envelope', {
    status: upstreamStatus ?? (responseStatus >= 400 ? responseStatus : undefined), upstreamCode, upstreamMessage,
    retryable: retryableStatus(upstreamStatus), retryAfterMs: responseRetryAfterMs,
  });
}

function providerCredentialError() {
  // Deliberately omit the original error: provider errors and traces must not
  // become an accidental secret/configuration oracle.
  return new ProviderUnavailable('Provider credential is unavailable', { failureCode: 'provider_auth_failed', retryable: false });
}

function assertUrl(value: string) {
  try {
    const url = new URL(value);
    if (!['http:', 'https:'].includes(url.protocol)) throw new Error('unsupported protocol');
  } catch {
    throw new ModelAdapterError('invalid_configuration', 'Provider base URL must be an absolute HTTP(S) URL.', 'base_url');
  }
}

/**
 * Shared OpenAI-compatible HTTP transport. It buffers one response before the
 * SDK sees it, so a broken stream retries the same HTTP exchange rather than
 * replaying the surrounding tool loop.
 */
export function createOpenAICompatibleModel(trace: Trace, task: string, selection: ResolvedModelSelection, adapter: ProviderAdapter) {
  const providerOptions = selection.provider.requestOptions ?? {};
  const outputBudget = resolveOutputBudget(providerOptions, selection.model.maxOutputTokens);
  assertOptionKeys(adapter, providerOptions);
  assertUrl(selection.provider.baseUrl);
  if (!selection.model.identifier.trim()) throw new ModelAdapterError('invalid_configuration', 'Provider model identifier cannot be empty.', 'model_identifier');
  if (selection.provider.enabled === false || selection.model.enabled === false) {
    throw new ProviderUnavailable('Configured provider or model is unavailable', { failureCode: 'provider_configuration_error', retryable: false });
  }

  const runtimeHome = selection.runtimeHome ?? configuredRuntimeHome();
  const secrets = new SecretStore(runtimeHome);
  let requestIndex = 0;
  let unavailable = false;

  const transport: typeof fetch = async (input, init) => {
    const request = ++requestIndex;
    const body = applyOutputBudget(adapter.transformRequestBody(sanitizedBody(JSON.parse(String(init?.body ?? '{}')) as Record<string, unknown>), providerOptions), outputBudget);
    const bodyText = JSON.stringify(body);
    const wire = bounded(body);
    trace.emit('model_request', { task, provider: selection.provider.type, provider_id: selection.provider.id,
      model: selection.model.identifier, request, body_chars: wire.chars, body_sha256: wire.sha256,
      output_budget: outputBudget.effective, output_budget_source: outputBudget.source, output_budget_wire_key: outputBudget.wireKey,
      body_truncated: wire.truncated, body_excerpt: wire.excerpt });

    let apiKey: string;
    try {
      if (!selection.provider.credentialRef) throw new Error('missing credential reference');
      // Read at model-call time, not while resolving or creating the model. A
      // stable slot can therefore be rotated without changing a snapshot.
      apiKey = await secrets.read(selection.provider.credentialRef, selection.env ?? process.env);
      trace.secret(apiKey);
    } catch {
      unavailable = true;
      throw providerCredentialError();
    }

    let attempt = 0;
    try {
      return await retryProvider(async () => {
        const started = Date.now();
        attempt++;
        trace.emit('provider_attempt', { task, provider: selection.provider.type, provider_id: selection.provider.id,
          model: selection.model.identifier, request, attempt });
        try {
          const headers = new Headers(init?.headers);
          headers.set('Authorization', `Bearer ${apiKey}`);
          for (const [name, value] of Object.entries(adapter.requestHeaders?.(providerOptions) ?? {})) headers.set(name, value);
          const response = await fetch(input, { ...init, headers, body: bodyText,
            signal: AbortSignal.any([AbortSignal.timeout(budget.providerMs), ...(init?.signal ? [init.signal] : [])]) });
          const raw = await response.text();
          const responseWire = bounded(raw);
          trace.emit('model_response', { task, provider: selection.provider.type, provider_id: selection.provider.id,
            model: selection.model.identifier, request, attempt, status: response.status, duration_ms: Date.now() - started,
            raw_chars: responseWire.chars, raw_sha256: responseWire.sha256, raw_truncated: responseWire.truncated,
            raw_excerpt: responseWire.excerpt });
          const responseRetryAfterMs = retryAfterDelay(response.headers.get('retry-after'));
          const envelopeError = providerResponseError(raw, response.status, responseRetryAfterMs);
          if (envelopeError) throw envelopeError;
          if (!response.ok) throw new ProviderResponseError(failureCodeForStatus(response.status), 'Provider HTTP error', {
            status: response.status, retryable: retryableStatus(response.status), retryAfterMs: responseRetryAfterMs,
          });
          if (body.stream && !hasSseDoneMarker(raw)) throw Object.assign(new Error('Incomplete provider stream'), {
            code: 'ECONNRESET', retryAfter: response.headers.get('retry-after') ?? undefined,
          });
          return new Response(raw, { status: response.status, statusText: response.statusText, headers: response.headers });
        } catch (error) {
          trace.emit('provider_error', { task, provider: selection.provider.type, provider_id: selection.provider.id,
            model: selection.model.identifier, request, attempt, duration_ms: Date.now() - started, ...providerError(error) });
          throw error;
        }
      }, { signal: init?.signal ?? undefined, onRetry: event => trace.emit('provider_retry', { task,
        provider: selection.provider.type, provider_id: selection.provider.id, model: selection.model.identifier, request, ...event }) });
    } catch (error) {
      if (error instanceof ProviderUnavailable) unavailable = true;
      throw error;
    }
  };

  const headers = adapter.requestHeaders?.(providerOptions);
  const provider = createOpenAICompatible({
    name: adapter.apiName,
    baseURL: selection.provider.baseUrl,
    headers,
    fetch: transport,
    includeUsage: true,
    supportsStructuredOutputs: true,
  });
  return { model: provider.chatModel(selection.model.identifier), isUnavailable: () => unavailable, adapter: adapter.type };
}