From 3b1ba087fecbc766bcfeedb68954328abfb5f7c6 Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:51:49 +0000 Subject: [PATCH] feat: add native Meta Model API provider --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/src/providers/openai-responses.ts | 2 + packages/ai/src/providers/openai-shared.ts | 16 ++++- packages/ai/src/registry/meta.ts | 22 +++++++ packages/ai/src/registry/registry.ts | 2 + packages/ai/test/meta-provider.test.ts | 62 +++++++++++++++++++ .../test/openai-max-output-tokens-cap.test.ts | 6 ++ packages/catalog/CHANGELOG.md | 4 ++ packages/catalog/scripts/generate-models.ts | 4 ++ packages/catalog/src/models.json | 38 +++++++++++- .../src/provider-models/descriptors.ts | 8 +++ .../src/provider-models/openai-compat.ts | 44 +++++++++++++ packages/catalog/test/meta-provider.test.ts | 44 +++++++++++++ 13 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 packages/ai/src/registry/meta.ts create mode 100644 packages/ai/test/meta-provider.test.ts create mode 100644 packages/catalog/test/meta-provider.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a8ab95d61..1fba1133a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added interactive Meta Model API key login and `MODEL_API_KEY` / `META_API_KEY` environment authentication ([#4941](https://github.com/can1357/oh-my-pi/issues/4941)). + ## [17.0.9] - 2026-07-23 ### Added diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 96c33beb6..685eef7f3 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -92,6 +92,7 @@ import { resolveOpenAICompatPolicy, resolveOpenAIOutputTokenParam, resolveOpenAIRequestSetup, + resolveOpenAIResponsesOutputClamp, shouldRetryWithoutStrictTools, } from "./openai-shared"; @@ -954,6 +955,7 @@ export function buildParams( omitMaxOutputTokens: model.omitMaxOutputTokens ?? false, isOpenRouterHost: model.compat.isOpenRouterHost, alwaysSendMaxTokens: model.compat.alwaysSendMaxTokens, + providerOutputClamp: resolveOpenAIResponsesOutputClamp(model), }); applyCommonResponsesSamplingParams(params, { ...options, maxTokens: outputToken?.value }, model); diff --git a/packages/ai/src/providers/openai-shared.ts b/packages/ai/src/providers/openai-shared.ts index 32112b8f3..7162b7799 100644 --- a/packages/ai/src/providers/openai-shared.ts +++ b/packages/ai/src/providers/openai-shared.ts @@ -1068,6 +1068,20 @@ export function resolveOpenAICompletionsOutputClamp( return undefined; } +/** + * Provider-specific Responses API output clamp. + * + * Meta documents a 131,072-token output limit for Muse Spark 1.1, so native + * Meta requests may use the model's full advertised cap instead of the + * conservative 64k OpenAI-compatible default. + */ +export function resolveOpenAIResponsesOutputClamp(model: Pick): number | undefined { + if (model.provider === "meta") { + return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS; + } + return undefined; +} + /** * Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present * (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so). @@ -2849,7 +2863,7 @@ export function applyCommonResponsesSamplingParams

loginMeta(cb), +} as const satisfies ProviderDefinition; diff --git a/packages/ai/src/registry/registry.ts b/packages/ai/src/registry/registry.ts index 2d9420c49..dff05549a 100644 --- a/packages/ai/src/registry/registry.ts +++ b/packages/ai/src/registry/registry.ts @@ -28,6 +28,7 @@ import { kimiCodeProvider } from "./kimi-code"; import { litellmProvider } from "./litellm"; import { llamaCppProvider } from "./llama-cpp"; import { lmStudioProvider } from "./lm-studio"; +import { metaProvider } from "./meta"; import { minimaxProvider } from "./minimax"; import { minimaxCodeProvider } from "./minimax-code"; import { minimaxCodeCnProvider } from "./minimax-code-cn"; @@ -105,6 +106,7 @@ const ALL = [ xiaomiTokenPlanCnProvider, firepassProvider, deepseekProvider, + metaProvider, moonshotProvider, cerebrasProvider, basetenProvider, diff --git a/packages/ai/test/meta-provider.test.ts b/packages/ai/test/meta-provider.test.ts new file mode 100644 index 000000000..8d20883ba --- /dev/null +++ b/packages/ai/test/meta-provider.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test"; +import { streamOpenAIResponses } from "@oh-my-pi/pi-ai/providers/openai-responses"; +import { loginMeta } from "@oh-my-pi/pi-ai/registry/meta"; +import type { Context, Model } from "@oh-my-pi/pi-ai/types"; +import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { Effort } from "@oh-my-pi/pi-catalog/effort"; +import { META_MUSE_STATIC_MODELS } from "@oh-my-pi/pi-catalog/provider-models/openai-compat"; + +const context: Context = { + messages: [{ role: "user", content: "hello", timestamp: Date.now() }], +}; + +function createAbortedSignal(): AbortSignal { + const controller = new AbortController(); + controller.abort(); + return controller.signal; +} + +function capturePayload(reasoning: Effort): Promise> { + const model = buildModel(META_MUSE_STATIC_MODELS[0]!) as Model<"openai-responses">; + const { promise, resolve } = Promise.withResolvers>(); + streamOpenAIResponses(model, context, { + apiKey: "meta-test-key", + reasoning, + signal: createAbortedSignal(), + onPayload: payload => resolve(payload as Record), + }); + return promise; +} + +describe("Meta Model API Responses requests", () => { + test("sends native xhigh reasoning and requests encrypted replay state", async () => { + const payload = await capturePayload(Effort.XHigh); + expect(payload.reasoning).toEqual({ effort: "xhigh", summary: "auto" }); + expect(payload.include).toEqual(["reasoning.encrypted_content"]); + }); + + test("preserves native minimal reasoning without clamping it", async () => { + const payload = await capturePayload(Effort.Minimal); + expect(payload.reasoning).toEqual({ effort: "minimal", summary: "auto" }); + }); +}); + +describe("Meta Model API login", () => { + test("validates pasted keys against the models endpoint without running inference", async () => { + let requestedUrl = ""; + let authorization = ""; + const apiKey = await loginMeta({ + onAuth: () => {}, + onPrompt: async () => " meta-test-key ", + fetch: (input, init) => { + requestedUrl = String(input); + authorization = new Headers(init?.headers).get("Authorization") ?? ""; + return Promise.resolve(Response.json({ data: [{ id: "muse-spark-1.1" }] })); + }, + }); + + expect(apiKey).toBe("meta-test-key"); + expect(requestedUrl).toBe("https://api.meta.ai/v1/models"); + expect(authorization).toBe("Bearer meta-test-key"); + }); +}); diff --git a/packages/ai/test/openai-max-output-tokens-cap.test.ts b/packages/ai/test/openai-max-output-tokens-cap.test.ts index def39d0ca..f896f37b5 100644 --- a/packages/ai/test/openai-max-output-tokens-cap.test.ts +++ b/packages/ai/test/openai-max-output-tokens-cap.test.ts @@ -187,6 +187,12 @@ describe("OpenAI-family output-token cap", () => { expect(body.max_output_tokens).toBe(OPENAI_MAX_OUTPUT_TOKENS); }); + it("lets native Meta Responses requests use the advertised model cap", async () => { + const model = getBundledModel("meta", "muse-spark-1.1") as Model<"openai-responses">; + const body = await drainResponses(model); + expect(body.max_output_tokens).toBe(131_072); + }); + it("omits default max_output_tokens for OpenRouter Responses so provider routing is not filtered", async () => { const body = await drainResponses(openRouterResponsesModel(131_072)); expect(body.max_output_tokens).toBeUndefined(); diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 19103e238..8acb78687 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added the native Meta Model API provider and Muse Spark 1.1 with Responses API reasoning replay, image input, and the full supported reasoning-effort ladder ([#4941](https://github.com/can1357/oh-my-pi/issues/4941)). + ## [17.0.9] - 2026-07-23 ### Changed diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index 09c65d17a..8a513e42e 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -36,6 +36,7 @@ import { clampKimiK27CodeMaxTokens, isFireworksKimiK2ModelId, isKimiK27CodeModelId, + META_MUSE_STATIC_MODELS, MODELS_DEV_PROVIDER_DESCRIPTORS, mapModelsDevToModels, projectOpenAIProReasoningAliases, @@ -521,6 +522,9 @@ async function generateModels() { // Mythos 5). Deduped behind upstream entries; metadata is pinned in // applyAnthropicCatalogPolicy. allModels.push(...ANTHROPIC_CURATED_FALLBACK_MODELS); + // Seed Meta's documented Muse model so first-run selection does not depend on + // credentials or live discovery. + allModels.push(...META_MUSE_STATIC_MODELS); // Seed Sakana's documented Fugu models so the provider is usable when // catalog generation has no live API key. If live `/v1/models` succeeds, // Sakana is authoritative and stale seed IDs must stay out. diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index 24eb18fe9..bf178b976 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -34656,6 +34656,42 @@ } } }, + "meta": { + "muse-spark-1.1": { + "id": "muse-spark-1.1", + "name": "Muse Spark 1.1", + "api": "openai-responses", + "provider": "meta", + "baseUrl": "https://api.meta.ai/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 1.25, + "output": 4.25, + "cacheRead": 0.15, + "cacheWrite": 0 + }, + "contextWindow": 1048576, + "maxTokens": 131072, + "thinking": { + "mode": "effort", + "efforts": [ + "minimal", + "low", + "medium", + "high", + "xhigh" + ] + }, + "compat": { + "supportsReasoningEffort": true, + "includeEncryptedReasoning": true + } + } + }, "minimax": { "MiniMax-M2": { "id": "MiniMax-M2", @@ -94546,4 +94582,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/catalog/src/provider-models/descriptors.ts b/packages/catalog/src/provider-models/descriptors.ts index 58fac8b15..64099f16d 100644 --- a/packages/catalog/src/provider-models/descriptors.ts +++ b/packages/catalog/src/provider-models/descriptors.ts @@ -26,6 +26,7 @@ import { kimiCodeModelManagerOptions, litellmModelManagerOptions, lmStudioModelManagerOptions, + metaModelManagerOptions, mistralModelManagerOptions, moonshotModelManagerOptions, nanoGptModelManagerOptions, @@ -249,6 +250,13 @@ export const CATALOG_PROVIDERS = [ envVars: ["MISTRAL_API_KEY"], createModelManagerOptions: (config: ModelManagerConfig) => mistralModelManagerOptions(config), }, + { + id: "meta", + defaultModel: "muse-spark-1.1", + envVars: ["MODEL_API_KEY", "META_API_KEY"], + createModelManagerOptions: (config: ModelManagerConfig) => metaModelManagerOptions(config), + catalogDiscovery: { label: "Meta Model API" }, + }, { id: "moonshot", defaultModel: "kimi-k2.7-code", diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts index d4fe73a53..7692acda2 100644 --- a/packages/catalog/src/provider-models/openai-compat.ts +++ b/packages/catalog/src/provider-models/openai-compat.ts @@ -2960,6 +2960,50 @@ export function coreWeaveModelManagerOptions( }); } +// --------------------------------------------------------------------------- +// 15.75 Meta Model API +// --------------------------------------------------------------------------- + +const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1"; +const META_MUSE_SPARK_COST = { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 } as const; +const META_MUSE_SPARK_THINKING: ThinkingConfig = { + mode: "effort", + efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], +}; + +export const META_MUSE_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [ + { + id: "muse-spark-1.1", + name: "Muse Spark 1.1", + api: "openai-responses", + provider: "meta", + baseUrl: META_MODEL_API_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: META_MUSE_SPARK_COST, + contextWindow: 1_048_576, + maxTokens: 131_072, + thinking: META_MUSE_SPARK_THINKING, + compat: { + supportsReasoningEffort: true, + includeEncryptedReasoning: true, + }, + }, +]; + +export interface MetaModelManagerConfig { + apiKey?: string; + baseUrl?: string; + fetch?: FetchImpl; +} + +export function metaModelManagerOptions(config?: MetaModelManagerConfig): ModelManagerOptions<"openai-responses"> { + return { + ...createSimpleOpenAIResponsesOptions("meta", META_MODEL_API_BASE_URL, config), + staticModels: META_MUSE_STATIC_MODELS, + }; +} + // --------------------------------------------------------------------------- // 16. Moonshot // --------------------------------------------------------------------------- diff --git a/packages/catalog/test/meta-provider.test.ts b/packages/catalog/test/meta-provider.test.ts new file mode 100644 index 000000000..475435977 --- /dev/null +++ b/packages/catalog/test/meta-provider.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; +import { Effort } from "@oh-my-pi/pi-catalog/effort"; +import { CATALOG_PROVIDERS } from "@oh-my-pi/pi-catalog/provider-models/descriptors"; +import { META_MUSE_STATIC_MODELS, metaModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat"; + +describe("Meta Model API provider", () => { + test("ships Muse Spark 1.1 with its documented Responses capabilities", () => { + expect(META_MUSE_STATIC_MODELS).toEqual([ + { + id: "muse-spark-1.1", + name: "Muse Spark 1.1", + api: "openai-responses", + provider: "meta", + baseUrl: "https://api.meta.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 131_072, + thinking: { + mode: "effort", + efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh], + }, + compat: { + supportsReasoningEffort: true, + includeEncryptedReasoning: true, + }, + }, + ]); + + const options = metaModelManagerOptions(); + expect(options.providerId).toBe("meta"); + expect(options.staticModels).toEqual(META_MUSE_STATIC_MODELS); + }); + + test("prefers Meta's documented key name while accepting the provider-specific alias", () => { + const descriptor = CATALOG_PROVIDERS.find(provider => provider.id === "meta"); + expect(descriptor).toMatchObject({ + defaultModel: "muse-spark-1.1", + envVars: ["MODEL_API_KEY", "META_API_KEY"], + catalogDiscovery: { label: "Meta Model API" }, + }); + }); +});