src/cloudflare-provider.ts
Rama main · réplica de lectura del repositorio original.
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 },
);
},
};
}