From 8bd547b3e4ed1490dda551ade9ea0a1b392af213 Mon Sep 17 00:00:00 2001 From: bowtiedbluefin <95500901+bowtiedbluefin@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:54:53 -0500 Subject: [PATCH] fix(morpheus): add COMPAT settings and preferred-provider mapping - Add MORPHEUS_COMPAT (supportsStore: false, supportsDeveloperRole: false) to both catalog and dynamically discovered models - Remove apiKey from discoverMorpheusModels() since endpoint is public - Add morpheus to preferred-provider mapping for auth choice routing - Add VENICE_COMPAT with matching settings (from upstream PR #2500) - Add venice-models tests (8 tests) --- src/agents/models-config.providers.ts | 6 +- src/agents/morpheus-models.ts | 21 +++-- src/agents/venice-models.test.ts | 78 +++++++++++++++++++ src/agents/venice-models.ts | 17 ++++ .../auth-choice.preferred-provider.ts | 1 + 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/agents/venice-models.test.ts diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 1bb80040a..0342b7441 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -351,8 +351,8 @@ async function buildVeniceProvider(): Promise { }; } -async function buildMorpheusProvider(apiKey?: string): Promise { - const models = await discoverMorpheusModels(apiKey); +async function buildMorpheusProvider(): Promise { + const models = await discoverMorpheusModels(); return { baseUrl: MORPHEUS_BASE_URL, api: "openai-completions", @@ -416,7 +416,7 @@ export async function resolveImplicitProviders(params: { resolveEnvApiKeyVarName("morpheus") ?? resolveApiKeyFromProfiles({ provider: "morpheus", store: authStore }); if (morpheusKey) { - providers.morpheus = { ...(await buildMorpheusProvider(morpheusKey)), apiKey: morpheusKey }; + providers.morpheus = { ...(await buildMorpheusProvider()), apiKey: morpheusKey }; } const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); diff --git a/src/agents/morpheus-models.ts b/src/agents/morpheus-models.ts index 37dbfe1ac..d1d37f271 100644 --- a/src/agents/morpheus-models.ts +++ b/src/agents/morpheus-models.ts @@ -1,7 +1,7 @@ import type { ModelDefinitionConfig } from "../config/types.js"; export const MORPHEUS_BASE_URL = "https://api.mor.org/api/v1"; -export const MORPHEUS_DEFAULT_MODEL_ID = "llama-3.3-70b"; +export const MORPHEUS_DEFAULT_MODEL_ID = "kimi-k2-thinking"; export const MORPHEUS_DEFAULT_MODEL_REF = `morpheus/${MORPHEUS_DEFAULT_MODEL_ID}`; // Morpheus is currently FREE during Open Beta (until 1/31/26). @@ -13,6 +13,11 @@ export const MORPHEUS_DEFAULT_COST = { cacheWrite: 0, }; +export const MORPHEUS_COMPAT = { + supportsStore: false, + supportsDeveloperRole: false, +} as const; + /** * Complete catalog of Morpheus Inference API models. * @@ -182,6 +187,7 @@ export function buildMorpheusModelDefinition(entry: MorpheusCatalogEntry): Model cost: MORPHEUS_DEFAULT_COST, contextWindow: entry.contextWindow, maxTokens: entry.maxTokens, + compat: MORPHEUS_COMPAT, }; } @@ -236,24 +242,16 @@ function inferModelProperties(model: MorpheusModel): { /** * Discover models from Morpheus API with fallback to static catalog. - * The /models endpoint requires authentication. + * The /models endpoint is public and doesn't require authentication. */ -export async function discoverMorpheusModels(apiKey?: string): Promise { +export async function discoverMorpheusModels(): Promise { // Skip API discovery in test environment if (process.env.NODE_ENV === "test" || process.env.VITEST) { return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); } - // If no API key provided, use static catalog - if (!apiKey?.trim()) { - return MORPHEUS_MODEL_CATALOG.map(buildMorpheusModelDefinition); - } - try { const response = await fetch(`${MORPHEUS_BASE_URL}/models`, { - headers: { - Authorization: `Bearer ${apiKey}`, - }, signal: AbortSignal.timeout(5000), }); @@ -298,6 +296,7 @@ export async function discoverMorpheusModels(apiKey?: string): Promise { + describe("VENICE_COMPAT", () => { + it("should disable store parameter support", () => { + expect(VENICE_COMPAT.supportsStore).toBe(false); + }); + + it("should disable developer role support", () => { + expect(VENICE_COMPAT.supportsDeveloperRole).toBe(false); + }); + }); + + describe("buildVeniceModelDefinition", () => { + it("should include compat settings in model definition", () => { + const entry = VENICE_MODEL_CATALOG[0]; + const model = buildVeniceModelDefinition(entry); + + expect(model.compat).toBeDefined(); + expect(model.compat?.supportsStore).toBe(false); + expect(model.compat?.supportsDeveloperRole).toBe(false); + }); + + it("should build all catalog models with compat settings", () => { + for (const entry of VENICE_MODEL_CATALOG) { + const model = buildVeniceModelDefinition(entry); + expect(model.compat).toEqual(VENICE_COMPAT); + } + }); + + it("should preserve model properties from catalog entry", () => { + const entry = VENICE_MODEL_CATALOG.find((m) => m.id === "llama-3.3-70b"); + expect(entry).toBeDefined(); + if (!entry) return; + + const model = buildVeniceModelDefinition(entry); + + expect(model.id).toBe("llama-3.3-70b"); + expect(model.name).toBe("Llama 3.3 70B"); + expect(model.reasoning).toBe(false); + expect(model.input).toContain("text"); + expect(model.contextWindow).toBe(131072); + expect(model.maxTokens).toBe(8192); + }); + }); + + describe("discoverVeniceModels", () => { + it("should return models with compat settings (static catalog fallback in test env)", async () => { + const models = await discoverVeniceModels(); + + expect(models.length).toBeGreaterThan(0); + + for (const model of models) { + expect(model.compat).toBeDefined(); + expect(model.compat?.supportsStore).toBe(false); + expect(model.compat?.supportsDeveloperRole).toBe(false); + } + }); + }); + + describe("VENICE_MODEL_CATALOG", () => { + it("should have at least 20 models", () => { + expect(VENICE_MODEL_CATALOG.length).toBeGreaterThanOrEqual(20); + }); + + it("should have both private and anonymized models", () => { + const privacyModes = new Set(VENICE_MODEL_CATALOG.map((m) => m.privacy)); + expect(privacyModes.has("private")).toBe(true); + expect(privacyModes.has("anonymized")).toBe(true); + }); + }); +}); diff --git a/src/agents/venice-models.ts b/src/agents/venice-models.ts index 32bd2f93b..55dbfa11e 100644 --- a/src/agents/venice-models.ts +++ b/src/agents/venice-models.ts @@ -13,6 +13,21 @@ export const VENICE_DEFAULT_COST = { cacheWrite: 0, }; +/** + * Venice API compatibility settings. + * + * Venice's OpenAI-compatible API doesn't support certain parameters that + * Clawdbot sends by default: + * - `store`: Venice returns HTTP 400 when this parameter is present + * - `developer` role: Not supported by Venice's API + * + * These settings ensure requests are formatted correctly for Venice. + */ +export const VENICE_COMPAT = { + supportsStore: false, + supportsDeveloperRole: false, +} as const; + /** * Complete catalog of Venice AI models. * @@ -300,6 +315,7 @@ export function buildVeniceModelDefinition(entry: VeniceCatalogEntry): ModelDefi cost: VENICE_DEFAULT_COST, contextWindow: entry.contextWindow, maxTokens: entry.maxTokens, + compat: VENICE_COMPAT, }; } @@ -381,6 +397,7 @@ export async function discoverVeniceModels(): Promise { cost: VENICE_DEFAULT_COST, contextWindow: apiModel.model_spec.availableContextTokens || 128000, maxTokens: 8192, + compat: VENICE_COMPAT, }); } } diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..d061bb451 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "zai-api-key": "zai", "synthetic-api-key": "synthetic", "venice-api-key": "venice", + "morpheus-api-key": "morpheus", "github-copilot": "github-copilot", "copilot-proxy": "copilot-proxy", "minimax-cloud": "minimax",