feat: add native Meta Model API provider

This commit is contained in:
Brent
2026-07-23 13:51:49 +00:00
parent 639bac596d
commit 3b1ba087fe
13 changed files with 254 additions and 2 deletions
+4
View File
@@ -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
@@ -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);
+15 -1
View File
@@ -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<Model, "provider" | "maxTokens">): 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<P extends CommonResponsesPara
params.max_output_tokens = Math.min(
options.maxTokens,
model.maxTokens ?? Number.POSITIVE_INFINITY,
OPENAI_MAX_OUTPUT_TOKENS,
resolveOpenAIResponsesOutputClamp(model) ?? OPENAI_MAX_OUTPUT_TOKENS,
);
}
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
+22
View File
@@ -0,0 +1,22 @@
import { createApiKeyLogin } from "./api-key-login";
import type { OAuthLoginCallbacks } from "./oauth/types";
import type { ProviderDefinition } from "./types";
export const loginMeta = createApiKeyLogin({
providerLabel: "Meta Model API",
authUrl: "https://developer.meta.com/ai/",
instructions: "Create or copy your key from the Meta Model API dashboard",
promptMessage: "Paste your Meta Model API key",
placeholder: "Model API key",
validation: {
kind: "models-endpoint",
provider: "Meta Model API",
modelsUrl: "https://api.meta.ai/v1/models",
},
});
export const metaProvider = {
id: "meta",
name: "Meta Model API",
login: (cb: OAuthLoginCallbacks) => loginMeta(cb),
} as const satisfies ProviderDefinition;
+2
View File
@@ -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,
+62
View File
@@ -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<Record<string, unknown>> {
const model = buildModel(META_MUSE_STATIC_MODELS[0]!) as Model<"openai-responses">;
const { promise, resolve } = Promise.withResolvers<Record<string, unknown>>();
streamOpenAIResponses(model, context, {
apiKey: "meta-test-key",
reasoning,
signal: createAbortedSignal(),
onPayload: payload => resolve(payload as Record<string, unknown>),
});
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");
});
});
@@ -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();
+4
View File
@@ -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
@@ -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.
+37 -1
View File
@@ -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 @@
}
}
}
}
}
@@ -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",
@@ -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
// ---------------------------------------------------------------------------
@@ -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" },
});
});
});