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)
This commit is contained in:
bowtiedbluefin 2026-01-27 13:54:53 -05:00
parent 933e2bdf39
commit 8bd547b3e4
5 changed files with 109 additions and 14 deletions

View File

@ -351,8 +351,8 @@ async function buildVeniceProvider(): Promise<ProviderConfig> {
};
}
async function buildMorpheusProvider(apiKey?: string): Promise<ProviderConfig> {
const models = await discoverMorpheusModels(apiKey);
async function buildMorpheusProvider(): Promise<ProviderConfig> {
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");

View File

@ -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<ModelDefinitionConfig[]> {
export async function discoverMorpheusModels(): Promise<ModelDefinitionConfig[]> {
// 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<ModelDefi
cost: MORPHEUS_DEFAULT_COST,
contextWindow: inferred.contextWindow,
maxTokens: 8192,
compat: MORPHEUS_COMPAT,
});
}
}

View File

@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import {
buildVeniceModelDefinition,
discoverVeniceModels,
VENICE_COMPAT,
VENICE_MODEL_CATALOG,
} from "./venice-models.js";
describe("venice-models", () => {
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);
});
});
});

View File

@ -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<ModelDefinitionConfig[]> {
cost: VENICE_DEFAULT_COST,
contextWindow: apiModel.model_spec.availableContextTokens || 128000,
maxTokens: 8192,
compat: VENICE_COMPAT,
});
}
}

View File

@ -20,6 +20,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"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",