Ctrl K
ComunidadGuías…

src/cloudflare-provider.ts

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

Archivo original ↗Preguntar a KiBot
import { deploymentServesManyInstallations } from './config/installation-scope.ts';
import { ModelAccessError } from './config/model-access.ts';
import { registerPiProvider } from './config/pi-provider-registry.ts';
import { isRecord } from './security/content-validation.ts';
import {
  cloudflareBindingProvider,
  type CloudflareAIBinding,
} from '@flue/runtime/cloudflare/workers-ai';

import { withWorkersAiOverflowPolicy } from './config/workers-ai-overflow.ts';
import { withWorkersAiPayloadPolicy } from './config/workers-ai-payload.ts';
import {
  CURRENT_WORKERS_AI_MODEL_ID,
  isWorkersAiGlmModel,
  withReviewedWorkersAiOutputCaps,
  workersAiGlmOutputLimit,
} from './config/workers-ai-models.ts';
import { decorateAttachmentProvider } from './slack/attachment-model-context.ts';

const SEED_CLOUDFLARE_RESPONSE_TIMEOUT_MS = 90_000;
const GPT_OSS_MODEL_ID = '@cf/openai/gpt-oss-120b';
const GPT_OSS_DEFAULT_MAX_TOKENS = 8_192;

/**
 * Register the Workers AI binding without routing prompts through AI Gateway.
 *
 * Importing this module is side-effect free. The Cloudflare-only entry calls
 * this helper with its ambient `env.AI` binding; the shared Node app never
 * imports it or registers a keyless `cloudflare/*` provider. A deployment
 * serving many installations registers a closed one (closedCloudflareProvider):
 * its binding serves the deployment's own judgments (Clef), never a tenant's
 * model request, which the model-access cell and readiness refuse anyway.
 * Returns whether the open provider was registered.
 */
export function registerCloudflareBindingProvider(
  binding: CloudflareAIBinding,
  env?: Record<string, unknown>,
): boolean {
  if (servesManyInstallations(env)) {
    registerPiProvider(closedCloudflareProvider(binding));
    return false;
  }
  registerPiProvider(createCloudflareBindingProvider(binding));
  return true;
}

/** A tenancy value the deployment cannot read is treated as serving many: the closed provider, never the open one. */
function servesManyInstallations(env: Record<string, unknown> | undefined): boolean {
  try {
    return deploymentServesManyInstallations(env);
  } catch {
    return true;
  }
}

/**
 * The `cloudflare` provider a deployment serving many installations
 * registers: it lists no model, so no `cloudflare/<model>` id resolves, and a
 * request that reaches it is refused before the binding. It must exist:
 * Flue's generated entry installs its own binding provider when none is
 * registered, outside the model-access proxy and through the default AI
 * Gateway, which retains payloads.
 */
function closedCloudflareProvider(binding: CloudflareAIBinding): ReturnType<typeof createCloudflareBindingProvider> {
  const open = createCloudflareBindingProvider(binding);
  const refuse = (): never => {
    throw new ModelAccessError('provider_not_offered', 'Workers AI models are not offered to an installation of a deployment serving many installations.');
  };
  const closed: Record<string | symbol, unknown> = { ...open, getModels: () => [], getAllModels: () => [], stream: refuse, streamSimple: refuse };
  // Flue resolves an unlisted `cloudflare/<model>` id through this template; the closed provider has none.
  delete closed[Symbol.for('flue.dynamicModelTemplate')];
  return closed as unknown as typeof open;
}

/** Pure provider seam used to apply the same attachment policy as Pi APIs. */
export function createCloudflareBindingProvider(binding: CloudflareAIBinding) {
  const provider = cloudflareBindingProvider(cloudflareBindingProviderOptions(binding));
  const models = withReviewedWorkersAiOutputCaps(provider.getModels());
  return decorateAttachmentProvider({ ...provider, ...withWorkersAiOverflowPolicy(provider), getModels: () => models });
}

/** Pure construction seam: keeps gateway privacy and payload policy testable. */
export function cloudflareBindingProviderOptions(
  binding: CloudflareAIBinding,
): { binding: CloudflareAIBinding; gateway: false } {
  return {
    binding: withCloudflareModelPolicies(binding),
      // Flue otherwise supplies `{ id: 'default' }`, which creates an AI
      // Gateway whose default logs retain request and response payloads.
    gateway: false,
  };
}

function withCloudflareModelPolicies(binding: CloudflareAIBinding): CloudflareAIBinding {
  return {
    run(modelId, inputs, options) {
      const wireInputs = withWorkersAiPayloadPolicy(modelId, inputs);
      if (modelId === GPT_OSS_MODEL_ID) {
        // Flue's binding streamSimple does not supply the model's output limit.
        // Workers AI otherwise defaults to 256 tokens, which GPT-OSS can spend
        // entirely on thinking. Use its documented max_tokens field, translating
        // Flue's explicit limit when present, without changing reasoning settings.
        const { max_completion_tokens, max_tokens, ...rest } = wireInputs;
        return binding.run(modelId, {
          ...rest,
          max_tokens: max_tokens ?? max_completion_tokens ?? GPT_OSS_DEFAULT_MAX_TOKENS,
        }, options);
      }
      if (!isWorkersAiGlmModel(modelId)) {
        return binding.run(modelId, wireInputs, options);
      }

      // Workers AI enables GLM thinking by default. The Pi provider represents
      // `thinkingLevel: 'off'` by omitting `reasoning_effort`, which therefore
      // leaves that server-side default enabled. Apply Cloudflare's explicit
      // chat-template policy at the binding boundary, remove any conflicting
      // effort value, cap each generation to the same model-specific ceiling as
      // the app's REST Workers AI path, and abort a provider stream that still
      // fails to settle. GLM 5.3 needs its reasoning parser enabled (below).
      // This policy is deliberately limited to the GLM
      // family Chickpea seeds and curates. glm-5.2 has otherwise held the
      // shared Slack relay alarm until its 15-minute platform deadline.
      const {
        reasoning_effort: _reasoningEffort,
        chat_template_kwargs,
        max_completion_tokens: requestedMaxTokens,
        ...rest
      } = wireInputs;
      const existingTemplateOptions = isRecord(chat_template_kwargs)
        ? chat_template_kwargs
        : {};
      const maxCompletionTokens =
        typeof requestedMaxTokens === 'number' &&
        Number.isFinite(requestedMaxTokens) &&
        requestedMaxTokens > 0
          ? Math.min(requestedMaxTokens, workersAiGlmOutputLimit(modelId))
          : workersAiGlmOutputLimit(modelId);
      const timeoutSignal = AbortSignal.timeout(SEED_CLOUDFLARE_RESPONSE_TIMEOUT_MS);
      const callerSignal = options?.signal;
      const signal =
        callerSignal instanceof AbortSignal
          ? AbortSignal.any([callerSignal, timeoutSignal])
          : timeoutSignal;
      return binding.run(
        modelId,
        {
          ...rest,
          max_completion_tokens: maxCompletionTokens,
          chat_template_kwargs: {
            ...existingTemplateOptions,
            // GLM 5.3 Flash still generates thinking when this flag is false,
            // but Workers AI then labels it as ordinary content (ending in
            // </think>). Keep its parser enabled so thinking stays private.
            // Older curated GLM endpoints retain their non-thinking policy.
            enable_thinking: modelId === CURRENT_WORKERS_AI_MODEL_ID,
          },
        },
        { ...options, signal },
      );
    },
  };
}