feat: add native Meta Model API provider
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user