589 — Social-Platforms AI Provider Analysis
Purpose
The system uses the SocialPlatform model (apps/backend/src/modules/socials/models/SocialPlatform.ts) as a unified store for every external-platform integration — social-media posting, email, SMS, payment, shipping, storage, CRM, auth, Google services, and AI/LLM providers. AI providers are stored with category: "ai" and a metadata.role tag (e.g. ai_search_chat, ai_search_embed). The existing ai-platforms.ts resolver (apps/backend/src/mastra/services/ai-platforms.ts:getAiPlatformForRole) already resolves an AI provider from DB for a given role. The digest AI summary (#589 item 4) should reuse this same resolution path instead of hardcoding process.env.OPENROUTER_API_KEY.
Provider registry & services
The social-provider module (apps/backend/src/modules/social-provider/index.ts:SOCIAL_PROVIDER_MODULE) exposes a SocialProviderService (apps/backend/src/modules/social-provider/service.ts) with a getProvider(name) method.
| Key(s) | Service file | Interface / capabilities |
|---|---|---|
"twitter" / "x" | apps/backend/src/modules/social-provider/twitter-service.ts | TwitterService — OAuth1/OAuth2, tweet create, media upload, tweet metrics, PKCE flow. Reads client id/secret from process.env.X_CLIENT_ID / X_CLIENT_SECRET / TWITTER_* fallbacks |
"instagram" | apps/backend/src/modules/social-provider/instagram-service.ts | InstagramService — Graph API via Facebook Login (no own OAuth), container creation, carousel, polling, publish. Reads no env vars |
"facebook" | apps/backend/src/modules/social-provider/facebook-service.ts | FacebookService — OAuth, page token, feed/photo/video/album posting. Reads FACEBOOK_CLIENT_ID / FACEBOOK_CLIENT_SECRET |
"linkedin" | apps/backend/src/modules/social-provider/linkedin-service.ts | LinkedInService — only getAuthUrl(). Reads LINKEDIN_CLIENT_ID |
"content-publishing" | apps/backend/src/modules/social-provider/content-publishing-service.ts | ContentPublishingService — orchestrates Facebook + Instagram cross-publish, image transforms |
"whatsapp" | apps/backend/src/modules/social-provider/whatsapp-service.ts | WhatsAppService — text/image/doc/video/interactive/template sends, media download, multi-number via withSender(), decrypts creds from SocialPlatform.api_config |
| — (Google) | apps/backend/src/modules/social-provider/google-connection-service.ts | GoogleConnectionService — stateless OAuth wrapper (auth URL, code exchange, refresh, userinfo). Not a provider in the registry; instantiated per-call with row-level clientId/clientSecret |
Important: the provider registry handles social-posting and OAuth services only. AI/LLM providers are not registered here — they are stored as SocialPlatform rows with category: "ai" and resolved via the separate ai-platforms.ts resolver.
The providerTokenMap (apps/backend/src/modules/social-provider/social-provider-registry.ts) maps twitter/x/instagram/facebook/linkedin → SOCIAL_PROVIDER_MODULE for Medusa's container DI. It does not include any AI providers.
api_config shape (per provider / category)
The SocialPlatform.api_config column (apps/backend/src/modules/socials/models/SocialPlatform.ts:34) is a model.json().nullable() blob. Its shape is determined by the value of api_config.provider (for the admin forms, sourced from data.provider_type). Single source of truth for the per-category field map: apps/backend/src/admin/components/social-platforms/api-config.ts:buildApiConfig().
Categories relevant to this analysis
category: "communication" (WhatsApp) — apps/backend/src/admin/components/social-platforms/api-config.ts:70-89
{ provider, phone_number_id, waba_id, access_token, app_secret,
webhook_verify_token, label, country_codes, is_default }
Secrets (access_token, app_secret, webhook_verify_token) are encrypted to *_encrypted blobs by the credentials subscriber. The typed interface is WhatsAppPlatformApiConfig (apps/backend/src/modules/socials/types/whatsapp-platform.ts).
category: "ai" — built by the backfill script, not by buildApiConfig() (no case "ai" in that switch). The shape is defined in apps/backend/src/scripts/backfill-ai-platforms-from-env.ts:227-232:
{ api_key, default_model?, account_id?, base_url? }
With metadata containing { provider_type, role, is_default, source }. Credentials are encrypted via the same subscriber mechanism (encryptSocialPlatformCredentials).
The api_config key naming: the admin form uses provider_type (translated from api_config.provider), while api_config itself stores provider. Secrets are stored as both plaintext and *_encrypted. See apps/backend/src/api/admin/social-platforms/secrets.ts for the redaction/restore contract.
Which providers are AI/LLM-capable
None of the social-provider registry services are AI/LLM-capable. They handle social posting, OAuth, and messaging only.
AI/LLM capability lives entirely in a separate resolution path:
-
getAiPlatformForRole(container, role)(apps/backend/src/mastra/services/ai-platforms.ts:134-231) — readsSocialPlatformrows wherecategory: "ai",status: "active",metadata.role === role, picks the default one (or most-recently-updated), decryptsapi_key, normalisesbaseUrl, returnsAiPlatformConfig. -
buildChatModel(config, modelOverride?)(apps/backend/src/mastra/services/ai-platforms.ts:246-265) — builds an AI SDK model from the config. ForproviderType === "openrouter"it callscreateOpenRouter({ apiKey }); for all other types (dashscope,cloudflare,vercel_ai_gateway,custom) it callscreateOpenAI({ baseURL, apiKey }). -
Supported
provider_typevalues (apps/backend/src/mastra/services/ai-platforms.ts:41-47):openrouter,dashscope,cloudflare,vercel_ai_gateway,fal,custom. -
AiRolevalues (apps/backend/src/mastra/services/ai-platforms.ts:49-56):ai_search_chat,ai_search_embed,ai_product_description,ai_image_gen(plus string escape hatch). The digest summary role would be a new value likeai_digest_summary. -
The
ai_extract_platformvisual-flow operation (apps/backend/src/modules/visual_flows/operations/ai-extract-platform.ts:63-192) already demonstrates the exact pattern: it callsgetAiPlatformForRole(ctx.container, role), thenbuildChatModel(config, modelOverride), thengenerateText({ model }). This is the reference implementation for #589 item 4.
How the digest AI gen wires OpenRouter today
In apps/backend/src/modules/visual_flows/operations/partner-analytics-digest.ts:277-289, when generate_ai_summary: true:
const aiGenerate: DigestAiGenerate | null = parsed.generate_ai_summary
? async ({ system, prompt, model }) => {
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
})
const result = await generateText({
model: openrouter(model) as any,
system,
messages: [{ role: "user", content: prompt }],
})
return result.text ?? ""
}
: null
This is a hardcoded createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }) call. It cannot be changed via admin UI — only by redeploying with a different env var. The default model is google/gemini-2.5-flash-preview from DIGEST_AI_DEFAULT_MODEL (apps/backend/src/workflows/analytics/partner-digest-ai-lib.ts:28).
Proposed resolution contract inputs
A resolveDigestAiProvider(container) helper that replaces the hardcoded createOpenRouter call should:
- Call
getAiPlatformForRole(container, "ai_digest_summary")— reads the admin-configured default AI platform for the digest-summary role.- If
null(nothing configured): resolve to a fallback (same as today'senv.OPENROUTER_API_KEY), or throw a human-readable error telling the admin to configure it (per theai_extract_platformpattern atapps/backend/src/modules/visual_flows/operations/ai-extract-platform.ts:136-142).
- If
- Call
buildChatModel(config, modelOverride)— returns an AI SDK model instance regardless of provider type.- The
modelOverridearg maps fromparsed.ai_summary_model(the digest operation already has this field atapps/backend/src/modules/visual_flows/operations/partner-analytics-digest.ts:75).
- The
- Return an async
DigestAiGeneratefunction — same seam type asapps/backend/src/workflows/analytics/partner-digest-ai-lib.ts:35-39— that callsgenerateText({ model, system, messages }).
Inputs the helper would read:
| Input | Source | Purpose |
|---|---|---|
container | workflow context | Resolve SOCIALS_MODULE for DB lookup |
role | constant "ai_digest_summary" | Match metadata.role on the platform row |
modelOverride | opts?.ai_summary_model (from operation options) | Override the platform's default_model at call site |
Output: DigestAiGenerate — (args: { system, prompt, model }) => Promise<string>.
Cloudflare: Workers AI (free) vs AI Gateway (paid / BYOK) — #613
The cloudflare provider derives base URL https://api.cloudflare.com/client/v4/accounts/<account_id>/ai/v1 (apps/backend/src/mastra/services/ai-platforms.ts ~L207), which is the Workers AI OpenAI-compatible endpoint. Whether a call is free depends entirely on the model id:
- Free — Workers AI native models (
@cf/…, e.g.@cf/meta/llama-3.1-8b-instruct,@cf/meta/llama-3.3-70b-instruct-fp8-fast,@cf/baai/bge-base-en-v1.5for embeddings). These run on a free daily Neuron allocation — no gateway balance, no BYOK.PROVIDER_DEFAULTS.cloudflare.defaultModelHintis already@cf/meta/llama-3.1-8b-instruct. - Paid — any non-
@cf/…model id (e.g.minimax/m3,openai/gpt-4o). These route through the paid AI Gateway / BYOK path and fail withInsufficient balance; add money to your gateway or use BYOK(code 2021) unless a balance / BYOK key is configured. This was the live failure that surfaced #613 (theai_digest_summaryCF platform'sdefault_modelwasminimax/m3).
Admin guard (#613): the External Platforms admin warns when a cloudflare AI platform's default_model is set to a non-@cf/… id — both live in the Create AI provider form (under the Default model field) and on the platform detail page. The check is the pure helper getCloudflareModelWarning(providerType, defaultModel) in apps/backend/src/admin/components/social-platforms/cloudflare-model-warning.ts (empty model → no warning, provider default @cf/… is used). Setting the prod ai_digest_summary platform's default_model to a @cf/… model is an operator action (admin-API authored, not seeded).
Gotchas
-
No
case "ai"inbuildApiConfig: The admin form'sapi-config.ts:buildApiConfig()has nocase "ai"branch. AI platforms are created by the backfill script (apps/backend/src/scripts/backfill-ai-platforms-from-env.ts) or manually via the admin External Platforms UI, which storesapi_configas a raw JSON blob. Adding a new role via the UI requires the admin to setmetadata.roleandmetadata.provider_typemanually. There is no admin UI dropdown for the digest-summary role yet. -
ai_digest_summaryrole does not exist: TheAiRoleunion inapps/backend/src/mastra/services/ai-platforms.ts:49-56does not includeai_digest_summary. The string escape hatch(string & {})at line 56 means it will work at runtime, but there's no enum entry and no doc hint. -
Default model mismatch: The digest currently defaults to
google/gemini-2.5-flash-preview(apps/backend/src/workflows/analytics/partner-digest-ai-lib.ts:28), whileai-platforms.ts:$PROVIDER_DEFAULTS.openrouter.defaultModelHintismeta-llama/llama-3.3-70b-instruct:free(apps/backend/src/mastra/services/ai-platforms.ts:76). If the admin-configured platform has nodefault_model, the resolved model will differ from today's hardcoded one. -
The
providerTokenMapdoes not include AI:apps/backend/src/modules/social-provider/social-provider-registry.tsonly maps social platforms. The digest should NOT useSocialProviderService.getProvider()— it should usegetAiPlatformForRole()fromai-platforms.tsinstead. -
Encryption path: The
ai-platforms.tsresolver decryptsapi_keyviadecryptApiKey(apps/backend/src/mastra/services/ai-platforms.ts:187), which handles both plaintext and encrypted blobs. The digest's current env-var path reads plaintext only. Switching to the DB resolver means theapi_keymay arrive decrypted from anapi_key_encryptedblob — thebuildChatModelpath handles this transparently. -
digestHasDataguard: ThecomposeDigestAiSummaryfunction (apps/backend/src/workflows/analytics/partner-digest-ai-lib.ts:119-134) skips the AI call entirely when!digestHasData(digest). This guard runs before anygeneratecall, so a missing AI provider config only manifests for digests that actually have data. -
Existing
generate_ai_summarydefaults tofalse: The digest operation (apps/backend/src/modules/visual_flows/operations/partner-analytics-digest.ts:73) requires explicit opt-in. Replacing the provider resolution doesn't change the UX — flows that don't set the flag still never touch the model. -
The
ai_extract_platformoperation is the precedent:apps/backend/src/modules/visual_flows/operations/ai-extract-platform.tsalready does exactly what #589 item 4 needs — resolves an AI provider from DB for a visual-flow context. The digest should follow the same pattern.
Open questions / (unverified)
- What backfill/migration creates the
ai_digest_summaryrole rows for existing deployments? The backfill script only handlesai_search_chat,ai_search_embed,ai_product_description,ai_image_gen— it does not seed a digest role. Admin must create it manually, or the backfill script needs an update. - Should
resolveDigestAiProviderreturnnull(caller falls back to env var) or throw (admin must configure) when no platform is found? Theai_extract_platformop throws with a clear message (ai-extract-platform.ts:142); the digest operation hascontinue_on_errorsemantics. The resolution should probably returnnulland let the caller's existing best-effort path (return null, leaveai_summaryunset) handle it. - The
DIGEST_AI_DEFAULT_MODELconstant (partner-digest-ai-lib.ts:28) is referenced as a fallback — after the switch, should it be removed entirely (the platform'sdefault_modelreplaces it) or kept as a secondary fallback when no platform default is set either?