From be317d0779c74c7f3ef5e31521dc0fe8e8f6780a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Jul 2026 11:21:43 -0700 Subject: [PATCH 1/2] feat(providers): add Meta Muse Spark 1.1 provider - New BYOK-only meta provider for Meta's Model API (launched today) - muse-spark-1.1: 1M context, streaming, tool-calling, reasoning_effort (minimal->xhigh), structured output - Meta icon mark only (no wordmark), theme-safe gradient IDs via useId() - Not added to hosted models list - BYOK only, no auto-billing --- apps/sim/components/icons.tsx | 56 +++ apps/sim/lib/tokenization/constants.ts | 5 + apps/sim/providers/attachments.ts | 11 +- apps/sim/providers/meta/index.ts | 635 +++++++++++++++++++++++++ apps/sim/providers/meta/utils.ts | 14 + apps/sim/providers/models.ts | 33 ++ apps/sim/providers/registry.ts | 2 + apps/sim/providers/types.ts | 1 + apps/sim/providers/utils.ts | 1 + 9 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 apps/sim/providers/meta/index.ts create mode 100644 apps/sim/providers/meta/utils.ts diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index c213134b0e1..c2b6ed56e2e 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3687,6 +3687,62 @@ export const SakanaIcon = (props: SVGProps) => ( ) +export function MetaIcon(props: SVGProps) { + const id = useId() + const gradient1Id = `meta_gradient_1_${id}` + const gradient2Id = `meta_gradient_2_${id}` + + return ( + + Meta + + + + + + + + + + + + + + + + + ) +} + export function GeminiIcon(props: SVGProps) { const id = useId() const gradientId = `gemini_gradient_${id}` diff --git a/apps/sim/lib/tokenization/constants.ts b/apps/sim/lib/tokenization/constants.ts index 484a397f848..34d166ba2ed 100644 --- a/apps/sim/lib/tokenization/constants.ts +++ b/apps/sim/lib/tokenization/constants.ts @@ -61,6 +61,11 @@ export const TOKENIZATION_CONFIG = { confidence: 'medium', supportedMethods: ['heuristic', 'fallback'], }, + meta: { + avgCharsPerToken: 4, + confidence: 'medium', + supportedMethods: ['heuristic', 'fallback'], + }, ollama: { avgCharsPerToken: 4, confidence: 'low', diff --git a/apps/sim/providers/attachments.ts b/apps/sim/providers/attachments.ts index b87307ea81e..d6b37b2dce1 100644 --- a/apps/sim/providers/attachments.ts +++ b/apps/sim/providers/attachments.ts @@ -36,6 +36,7 @@ export type AttachmentProvider = | 'deepseek' | 'cerebras' | 'sakana' + | 'meta' export interface PreparedProviderAttachment { file: UserFile @@ -119,7 +120,12 @@ const BEDROCK_DOCUMENT_FORMATS = new Set([ const BEDROCK_IMAGE_FORMATS = new Set(['png', 'jpeg', 'jpg', 'gif', 'webp']) const BEDROCK_VIDEO_FORMATS = new Set(['mp4', 'mov', 'mkv', 'webm']) -const UNSUPPORTED_FILE_PROVIDERS = new Set(['deepseek', 'cerebras', 'sakana']) +const UNSUPPORTED_FILE_PROVIDERS = new Set([ + 'deepseek', + 'cerebras', + 'sakana', + 'meta', +]) const PROVIDER_SUPPORTED_LABELS: Record = { openai: 'images and documents through the Responses API input_image/input_file parts', @@ -139,6 +145,7 @@ const PROVIDER_SUPPORTED_LABELS: Record = { deepseek: 'no file attachments in the current API adapter', cerebras: 'no file attachments in the current API adapter', sakana: 'no file attachments in the current API adapter', + meta: 'no file attachments in the current API adapter', } export function getAttachmentProvider(providerId: ProviderId | string): AttachmentProvider | null { @@ -159,6 +166,7 @@ export function getAttachmentProvider(providerId: ProviderId | string): Attachme if (providerId === 'deepseek') return 'deepseek' if (providerId === 'cerebras') return 'cerebras' if (providerId === 'sakana') return 'sakana' + if (providerId === 'meta') return 'meta' return null } @@ -307,6 +315,7 @@ function isMimeTypeSupportedByProvider( case 'deepseek': case 'cerebras': case 'sakana': + case 'meta': return false default: { const _exhaustive: never = provider diff --git a/apps/sim/providers/meta/index.ts b/apps/sim/providers/meta/index.ts new file mode 100644 index 00000000000..2355373ff88 --- /dev/null +++ b/apps/sim/providers/meta/index.ts @@ -0,0 +1,635 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import OpenAI from 'openai' +import type { StreamingExecution } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { formatMessagesForProvider } from '@/providers/attachments' +import { createReadableStreamFromMetaStream } from '@/providers/meta/utils' +import { getProviderDefaultModel, getProviderModels } from '@/providers/models' +import { createStreamingExecution } from '@/providers/streaming-execution' +import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' +import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' +import type { + ProviderConfig, + ProviderRequest, + ProviderResponse, + TimeSegment, +} from '@/providers/types' +import { ProviderError } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + prepareToolsWithUsageControl, + sumToolCosts, + trackForcedToolUsage, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +const logger = createLogger('MetaProvider') + +const META_BASE_URL = 'https://api.meta.ai/v1' + +export const metaProvider: ProviderConfig = { + id: 'meta', + name: 'Meta', + description: "Meta's Muse Spark models via the Meta Model API (OpenAI-compatible)", + version: '1.0.0', + models: getProviderModels('meta'), + defaultModel: getProviderDefaultModel('meta'), + + executeRequest: async ( + request: ProviderRequest + ): Promise => { + if (!request.apiKey) { + throw new Error('API key is required for Meta') + } + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + + try { + const meta = new OpenAI({ + apiKey: request.apiKey, + baseURL: META_BASE_URL, + }) + + const allMessages = [] + + if (request.systemPrompt) { + allMessages.push({ + role: 'system', + content: request.systemPrompt, + }) + } + + if (request.context) { + allMessages.push({ + role: 'user', + content: request.context, + }) + } + + if (request.messages) { + allMessages.push(...request.messages) + } + const formattedMessages = formatMessagesForProvider(allMessages, 'meta') + + const tools = request.tools?.length + ? request.tools.map((tool) => adaptOpenAIChatToolSchema(tool)) + : undefined + + const payload: any = { + model: request.model, + messages: formattedMessages, + } + + if (request.temperature !== undefined) payload.temperature = request.temperature + if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens + if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { + payload.reasoning_effort = request.reasoningEffort + } + + const responseFormatPayload = request.responseFormat + ? { + type: 'json_schema' as const, + json_schema: { + name: request.responseFormat.name || 'response_schema', + schema: request.responseFormat.schema || request.responseFormat, + strict: request.responseFormat.strict !== false, + }, + } + : undefined + + let preparedTools: ReturnType | null = null + let hasActiveTools = false + + if (tools?.length) { + preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'openai') + const { tools: filteredTools, toolChoice } = preparedTools + + if (filteredTools?.length && toolChoice) { + payload.tools = filteredTools + payload.tool_choice = toolChoice + hasActiveTools = true + + logger.info('Meta request configuration:', { + toolCount: filteredTools.length, + toolChoice: + typeof toolChoice === 'string' + ? toolChoice + : toolChoice.type === 'function' + ? `force:${toolChoice.function.name}` + : 'unknown', + model: request.model, + }) + } + } + + // Structured output and tool calling cannot be sent together — OpenAI-compatible + // backends reject a request that carries both `response_format` and active + // `tools`/`tool_choice`. Defer the schema until after the tool loop completes. + const deferResponseFormat = !!responseFormatPayload && hasActiveTools + if (responseFormatPayload && !deferResponseFormat) { + payload.response_format = responseFormatPayload + } + + if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) { + logger.info('Using streaming response for Meta request (no tools)') + + const streamResponse = await meta.chat.completions.create( + { + ...payload, + stream: true, + stream_options: { include_usage: true }, + }, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const streamingResult = createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { kind: 'simple', segmentName: request.model }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { input: 0, output: 0, total: 0 }, + isStreaming: true, + createStream: ({ output }) => + createReadableStreamFromMetaStream(streamResponse as any, (content, usage) => { + output.content = content + output.tokens = { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.total_tokens, + } + + const costResult = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } + }), + }) + + return streamingResult + } + + const initialCallTime = Date.now() + const originalToolChoice = payload.tool_choice + const forcedTools = preparedTools?.forcedTools || [] + let usedForcedTools: string[] = [] + + let currentResponse = await meta.chat.completions.create( + payload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + const firstResponseTime = Date.now() - initialCallTime + + let content = currentResponse.choices[0]?.message?.content || '' + + const tokens = { + input: currentResponse.usage?.prompt_tokens || 0, + output: currentResponse.usage?.completion_tokens || 0, + total: currentResponse.usage?.total_tokens || 0, + } + const toolCalls = [] + const toolResults: Record[] = [] + const currentMessages = [...formattedMessages] + let iterationCount = 0 + let hasUsedForcedTool = false + let modelTime = firstResponseTime + let toolsTime = 0 + + const timeSegments: TimeSegment[] = [ + { + type: 'model', + name: request.model, + startTime: initialCallTime, + endTime: initialCallTime + firstResponseTime, + duration: firstResponseTime, + }, + ] + + if ( + typeof originalToolChoice === 'object' && + currentResponse.choices[0]?.message?.tool_calls + ) { + const toolCallsResponse = currentResponse.choices[0].message.tool_calls + const result = trackForcedToolUsage( + toolCallsResponse, + originalToolChoice, + logger, + 'openai', + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = result.hasUsedForcedTool + usedForcedTools = result.usedForcedTools + } + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (currentResponse.choices[0]?.message?.content) { + content = currentResponse.choices[0].message.content + } + + const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls + + enrichLastModelSegmentFromChatCompletions( + timeSegments, + currentResponse, + toolCallsInResponse, + { model: request.model, provider: 'meta' } + ) + + if (!toolCallsInResponse || toolCallsInResponse.length === 0) { + break + } + + const toolsStartTime = Date.now() + + const toolExecutionPromises = toolCallsInResponse.map(async (toolCall) => { + const toolCallStartTime = Date.now() + const toolName = toolCall.function.name + + try { + const toolArgs = JSON.parse(toolCall.function.arguments) + const tool = request.tools?.find((t) => t.id === toolName) + + // Every tool_call in the assistant message must be answered by a matching + // `tool` message, or the next request violates the OpenAI message contract. + // Emit an error result for an unknown tool rather than dropping it. + if (!tool) { + const toolCallEndTime = Date.now() + return { + toolCall, + toolName, + toolParams: {}, + result: { + success: false, + output: undefined, + error: `Tool "${toolName}" is not available`, + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } + + const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request) + const result = await executeTool(toolName, executionParams, { + signal: request.abortSignal, + }) + const toolCallEndTime = Date.now() + + return { + toolCall, + toolName, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } catch (error) { + const toolCallEndTime = Date.now() + logger.error('Error processing tool call:', { error, toolName }) + + return { + toolCall, + toolName, + toolParams: {}, + result: { + success: false, + output: undefined, + error: getErrorMessage(error, 'Tool execution failed'), + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + } + } + }) + + const executionResults = await Promise.allSettled(toolExecutionPromises) + + currentMessages.push({ + role: 'assistant', + content: null, + tool_calls: toolCallsInResponse.map((tc) => ({ + id: tc.id, + type: 'function', + function: { + name: tc.function.name, + arguments: tc.function.arguments, + }, + })), + }) + + for (const settledResult of executionResults) { + if (settledResult.status === 'rejected' || !settledResult.value) continue + + const { toolCall, toolName, toolParams, result, startTime, endTime, duration } = + settledResult.value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime: startTime, + endTime: endTime, + duration: duration, + toolCallId: toolCall.id, + }) + + let resultContent: any + if (result.success && result.output) { + toolResults.push(result.output) + resultContent = result.output + } else { + resultContent = { + error: true, + message: result.error || 'Tool execution failed', + tool: toolName, + } + } + + toolCalls.push({ + name: toolName, + arguments: toolParams, + startTime: new Date(startTime).toISOString(), + endTime: new Date(endTime).toISOString(), + duration: duration, + result: resultContent, + success: result.success, + }) + + currentMessages.push({ + role: 'tool', + tool_call_id: toolCall.id, + content: JSON.stringify(resultContent), + }) + } + + const thisToolsTime = Date.now() - toolsStartTime + toolsTime += thisToolsTime + + const nextPayload = { + ...payload, + messages: currentMessages, + } + + if ( + typeof originalToolChoice === 'object' && + hasUsedForcedTool && + forcedTools.length > 0 + ) { + const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) + + if (remainingTools.length > 0) { + nextPayload.tool_choice = { + type: 'function', + function: { name: remainingTools[0] }, + } + logger.info(`Forcing next tool: ${remainingTools[0]}`) + } else { + nextPayload.tool_choice = 'auto' + logger.info('All forced tools have been used, switching to auto tool_choice') + } + } + + const nextModelStartTime = Date.now() + currentResponse = await meta.chat.completions.create( + nextPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + if ( + typeof nextPayload.tool_choice === 'object' && + currentResponse.choices[0]?.message?.tool_calls + ) { + const toolCallsResponse = currentResponse.choices[0].message.tool_calls + const result = trackForcedToolUsage( + toolCallsResponse, + nextPayload.tool_choice, + logger, + 'openai', + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = result.hasUsedForcedTool + usedForcedTools = result.usedForcedTools + } + + const nextModelEndTime = Date.now() + const thisModelTime = nextModelEndTime - nextModelStartTime + + timeSegments.push({ + type: 'model', + name: request.model, + startTime: nextModelStartTime, + endTime: nextModelEndTime, + duration: thisModelTime, + }) + + modelTime += thisModelTime + + if (currentResponse.choices[0]?.message?.content) { + content = currentResponse.choices[0].message.content + } + + if (currentResponse.usage) { + tokens.input += currentResponse.usage.prompt_tokens || 0 + tokens.output += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + iterationCount++ + } + + if (iterationCount === MAX_TOOL_ITERATIONS) { + enrichLastModelSegmentFromChatCompletions( + timeSegments, + currentResponse, + currentResponse.choices[0]?.message?.tool_calls, + { model: request.model, provider: 'meta' } + ) + } + } catch (error) { + logger.error('Error in Meta request:', { error }) + throw error + } + + if (request.stream) { + logger.info('Using streaming for final Meta response after tool processing') + + // The tool loop is complete: this final pass only produces the textual answer. + // Force `tool_choice: 'none'` so the model cannot emit fresh tool calls that the + // text-only stream adapter would silently drop. + const streamingPayload: any = { + ...payload, + messages: currentMessages, + tool_choice: 'none', + stream: true, + stream_options: { include_usage: true }, + } + if (deferResponseFormat && responseFormatPayload) { + streamingPayload.response_format = responseFormatPayload + streamingPayload.parallel_tool_calls = false + } + + const streamResponse = await meta.chat.completions.create( + streamingPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) + + const streamingResult = createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + timeSegments, + }, + initialTokens: { + input: tokens.input, + output: tokens.output, + total: tokens.total, + }, + initialCost: { + input: accumulatedCost.input, + output: accumulatedCost.output, + toolCost: undefined as number | undefined, + total: accumulatedCost.total, + }, + toolCalls: + toolCalls.length > 0 + ? { + list: toolCalls, + count: toolCalls.length, + } + : undefined, + isStreaming: true, + createStream: ({ output }) => + createReadableStreamFromMetaStream(streamResponse as any, (content, usage) => { + output.content = content + output.tokens = { + input: tokens.input + usage.prompt_tokens, + output: tokens.output + usage.completion_tokens, + total: tokens.total + usage.total_tokens, + } + + const streamCost = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + const tc = sumToolCosts(toolResults) + output.cost = { + input: accumulatedCost.input + streamCost.input, + output: accumulatedCost.output + streamCost.output, + toolCost: tc || undefined, + total: accumulatedCost.total + streamCost.total + tc, + } + }), + }) + + return streamingResult + } + + // Tools were active, so `response_format` was withheld from the loop. Make one final + // tool-free call to obtain the structured response now that the tool work is done. + if (deferResponseFormat && responseFormatPayload) { + logger.info('Applying deferred JSON schema response format after tool processing') + + const finalFormatStartTime = Date.now() + const finalPayload: any = { + ...payload, + messages: currentMessages, + response_format: responseFormatPayload, + tool_choice: 'none', + parallel_tool_calls: false, + } + + currentResponse = await meta.chat.completions.create( + finalPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const finalFormatEndTime = Date.now() + timeSegments.push({ + type: 'model', + name: request.model, + startTime: finalFormatStartTime, + endTime: finalFormatEndTime, + duration: finalFormatEndTime - finalFormatStartTime, + }) + modelTime += finalFormatEndTime - finalFormatStartTime + + const formattedContent = currentResponse.choices[0]?.message?.content + if (formattedContent) { + content = formattedContent + } + + if (currentResponse.usage) { + tokens.input += currentResponse.usage.prompt_tokens || 0 + tokens.output += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + enrichLastModelSegmentFromChatCompletions( + timeSegments, + currentResponse, + currentResponse.choices[0]?.message?.tool_calls, + { model: request.model, provider: 'meta' } + ) + } + + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + return { + content, + model: request.model, + tokens, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + toolResults: toolResults.length > 0 ? toolResults : undefined, + timing: { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + modelTime: modelTime, + toolsTime: toolsTime, + firstResponseTime: firstResponseTime, + iterations: iterationCount + 1, + timeSegments: timeSegments, + }, + } + } catch (error) { + const providerEndTime = Date.now() + const providerEndTimeISO = new Date(providerEndTime).toISOString() + const totalDuration = providerEndTime - providerStartTime + + logger.error('Error in Meta request:', { + error, + duration: totalDuration, + }) + + throw new ProviderError(toError(error).message, { + startTime: providerStartTimeISO, + endTime: providerEndTimeISO, + duration: totalDuration, + }) + } + }, +} diff --git a/apps/sim/providers/meta/utils.ts b/apps/sim/providers/meta/utils.ts new file mode 100644 index 00000000000..1f72345ec5c --- /dev/null +++ b/apps/sim/providers/meta/utils.ts @@ -0,0 +1,14 @@ +import type { ChatCompletionChunk } from 'openai/resources/chat/completions' +import type { CompletionUsage } from 'openai/resources/completions' +import { createOpenAICompatibleStream } from '@/providers/utils' + +/** + * Creates a ReadableStream from a Meta Model API streaming response. + * Uses the shared OpenAI-compatible streaming utility. + */ +export function createReadableStreamFromMetaStream( + metaStream: AsyncIterable, + onComplete?: (content: string, usage: CompletionUsage) => void +): ReadableStream { + return createOpenAICompatibleStream(metaStream, 'Meta', onComplete) +} diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index e30af3a0b31..1a427ada26e 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -19,6 +19,7 @@ import { GeminiIcon, GroqIcon, LitellmIcon, + MetaIcon, MistralIcon, OllamaIcon, OpenAIIcon, @@ -2290,6 +2291,38 @@ export const PROVIDER_DEFINITIONS: Record = { }, ], }, + meta: { + id: 'meta', + name: 'Meta', + description: "Meta's Muse Spark models via the Meta Model API (OpenAI-compatible)", + defaultModel: 'muse-spark-1.1', + modelPatterns: [/^muse-spark/], + icon: MetaIcon, + color: '#0082FB', + capabilities: { + temperature: { min: 0, max: 2 }, + toolUsageControl: true, + }, + models: [ + { + id: 'muse-spark-1.1', + pricing: { + input: 1.25, + cachedInput: 0.15, + output: 4.25, + updatedAt: '2026-07-09', + }, + capabilities: { + reasoningEffort: { + values: ['minimal', 'low', 'medium', 'high', 'xhigh'], + }, + }, + contextWindow: 1048576, + releaseDate: '2026-07-09', + recommended: true, + }, + ], + }, mistral: { id: 'mistral', name: 'Mistral AI', diff --git a/apps/sim/providers/registry.ts b/apps/sim/providers/registry.ts index cb7d1a9cd0f..a221966914c 100644 --- a/apps/sim/providers/registry.ts +++ b/apps/sim/providers/registry.ts @@ -11,6 +11,7 @@ import { fireworksProvider } from '@/providers/fireworks' import { googleProvider } from '@/providers/google' import { groqProvider } from '@/providers/groq' import { litellmProvider } from '@/providers/litellm' +import { metaProvider } from '@/providers/meta' import { mistralProvider } from '@/providers/mistral' import { ollamaProvider } from '@/providers/ollama' import { ollamaCloudProvider } from '@/providers/ollama-cloud' @@ -36,6 +37,7 @@ const providerRegistry: Record = { cerebras: cerebrasProvider, groq: groqProvider, sakana: sakanaProvider, + meta: metaProvider, vllm: vllmProvider, litellm: litellmProvider, mistral: mistralProvider, diff --git a/apps/sim/providers/types.ts b/apps/sim/providers/types.ts index d13c2369774..2cb08a6f6c6 100644 --- a/apps/sim/providers/types.ts +++ b/apps/sim/providers/types.ts @@ -12,6 +12,7 @@ export type ProviderId = | 'cerebras' | 'groq' | 'sakana' + | 'meta' | 'mistral' | 'ollama' | 'ollama-cloud' diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 487313e6564..f5cd3c6dd85 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -153,6 +153,7 @@ export const providers: Record = { cerebras: buildProviderMetadata('cerebras'), groq: buildProviderMetadata('groq'), sakana: buildProviderMetadata('sakana'), + meta: buildProviderMetadata('meta'), mistral: buildProviderMetadata('mistral'), bedrock: buildProviderMetadata('bedrock'), openrouter: buildProviderMetadata('openrouter'), From 6e3270636f2330a1a5689747a6c76f48102fdf00 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 9 Jul 2026 11:31:17 -0700 Subject: [PATCH 2/2] fix(providers): stop sending unsupported tool_choice values to Meta Meta's Chat Completions endpoint only supports tool_choice: "auto" - "none", "required", and named-function choices all return HTTP 400 (confirmed against the official meta-model-cookbook tool-calling recipe). Never set tool_choice on the request (auto is already the default; forced-tool usage control degrades gracefully to auto with a warning log instead of failing every tool-using run), and drop `tools` entirely from the two post-tool-loop tool-free completion calls instead of trying to force tool_choice: "none". --- apps/sim/providers/meta/index.ts | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/sim/providers/meta/index.ts b/apps/sim/providers/meta/index.ts index 2355373ff88..87af96e660d 100644 --- a/apps/sim/providers/meta/index.ts +++ b/apps/sim/providers/meta/index.ts @@ -109,9 +109,20 @@ export const metaProvider: ProviderConfig = { if (filteredTools?.length && toolChoice) { payload.tools = filteredTools - payload.tool_choice = toolChoice hasActiveTools = true + // Meta's Chat Completions endpoint only supports tool_choice: "auto" — + // "none", "required", and named-function choices all return HTTP 400 + // (confirmed via the official meta-model-cookbook tool-calling recipe). + // "auto" is already the endpoint default, so we never set the field; a + // forced tool choice degrades to auto rather than failing the request. + if (typeof toolChoice === 'object') { + logger.warn( + 'Meta does not support forcing a specific tool; falling back to auto tool_choice', + { requestedTool: toolChoice.function.name, model: request.model } + ) + } + logger.info('Meta request configuration:', { toolCount: filteredTools.length, toolChoice: @@ -467,18 +478,18 @@ export const metaProvider: ProviderConfig = { logger.info('Using streaming for final Meta response after tool processing') // The tool loop is complete: this final pass only produces the textual answer. - // Force `tool_choice: 'none'` so the model cannot emit fresh tool calls that the - // text-only stream adapter would silently drop. + // Meta rejects tool_choice: "none" (only "auto" is supported), so instead of + // forcing tool_choice we omit `tools` from this call entirely — with no tools + // declared, the model cannot emit a fresh tool call for the text-only adapter to drop. + const { tools: _omittedTools, ...streamingBasePayload } = payload const streamingPayload: any = { - ...payload, + ...streamingBasePayload, messages: currentMessages, - tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } if (deferResponseFormat && responseFormatPayload) { streamingPayload.response_format = responseFormatPayload - streamingPayload.parallel_tool_calls = false } const streamResponse = await meta.chat.completions.create( @@ -548,16 +559,17 @@ export const metaProvider: ProviderConfig = { // Tools were active, so `response_format` was withheld from the loop. Make one final // tool-free call to obtain the structured response now that the tool work is done. + // Meta rejects tool_choice: "none", so `tools` is dropped from this payload instead + // (see the streaming pass above for the same constraint). if (deferResponseFormat && responseFormatPayload) { logger.info('Applying deferred JSON schema response format after tool processing') const finalFormatStartTime = Date.now() + const { tools: _omittedDeferredTools, ...deferredBasePayload } = payload const finalPayload: any = { - ...payload, + ...deferredBasePayload, messages: currentMessages, response_format: responseFormatPayload, - tool_choice: 'none', - parallel_tool_calls: false, } currentResponse = await meta.chat.completions.create(