mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(maritalk):Update Model list & Implementation of the latest API (#1856)
Integrated into release/v3.7.8
This commit is contained in:
18
open-sse/config/maritalk.ts
Normal file
18
open-sse/config/maritalk.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { stripTrailingSlashes } from "../utils/urlSanitize.ts";
|
||||
|
||||
export const MARITALK_DEFAULT_BASE_URL = "https://chat.maritaca.ai/api";
|
||||
|
||||
export function normalizeMaritalkBaseUrl(value: string | null | undefined): string {
|
||||
const normalized = stripTrailingSlashes((value || MARITALK_DEFAULT_BASE_URL).trim());
|
||||
if (!normalized) return MARITALK_DEFAULT_BASE_URL;
|
||||
|
||||
return normalized.replace(/\/chat\/(?:completions|inference)$/i, "");
|
||||
}
|
||||
|
||||
export function buildMaritalkChatUrl(value: string | null | undefined): string {
|
||||
return `${normalizeMaritalkBaseUrl(value)}/chat/completions`;
|
||||
}
|
||||
|
||||
export function buildMaritalkModelsUrl(value: string | null | undefined): string {
|
||||
return `${normalizeMaritalkBaseUrl(value)}/models`;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
GLM_SHARED_HEADERS,
|
||||
GLM_SHARED_MODELS,
|
||||
} from "./glmProvider.ts";
|
||||
import { MARITALK_DEFAULT_BASE_URL } from "./maritalk.ts";
|
||||
import {
|
||||
CURSOR_REGISTRY_VERSION,
|
||||
getAntigravityProviderHeaders,
|
||||
@@ -191,7 +192,7 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
|
||||
"aisingapore/Qwen-SEA-LION-v4-32B-IT",
|
||||
"allenai/Olmo-3-32B-Think",
|
||||
]),
|
||||
moonshot: buildModels(["kimi-k2.5", "kimi-latest", "moonshot-v1-auto"]),
|
||||
moonshot: buildModels(["kimi-k2.6", "kimi-k2.5"]),
|
||||
"meta-llama": buildModels([
|
||||
"Llama-3.3-70B-Instruct",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8",
|
||||
@@ -227,7 +228,7 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
|
||||
venice: buildModels(["venice-latest"]),
|
||||
codestral: buildModels(["codestral-2405", "codestral-latest"]),
|
||||
upstage: buildModels(["solar-pro3", "solar-mini"]),
|
||||
maritalk: buildModels(["sabia-3", "sabia-3-small"]),
|
||||
maritalk: buildModels(["sabia-4", "sabia-3.1", "sabiazinho-4", "sabiazinho-3"]),
|
||||
"xiaomi-mimo": buildModels(["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-omni", "mimo-v2-flash"]),
|
||||
"inference-net": buildModels([
|
||||
"meta-llama/Llama-3.3-70B-Instruct",
|
||||
@@ -1965,9 +1966,9 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
alias: "maritalk",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://chat.maritaca.ai/api/chat/inference",
|
||||
baseUrl: MARITALK_DEFAULT_BASE_URL,
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
authHeader: "key",
|
||||
models: CHAT_OPENAI_COMPAT_MODELS.maritalk,
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface BaseProvider<M extends BaseModel = BaseModel> {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
authType: string; // "apikey" | "oauth" | "none"
|
||||
authHeader: string; // "bearer" | "token" | "xi-api-key" | "x-api-key" | "none"
|
||||
authHeader: string; // "bearer" | "key" | "token" | "xi-api-key" | "x-api-key" | "none"
|
||||
format?: string;
|
||||
models: M[];
|
||||
}
|
||||
@@ -76,7 +76,7 @@ export function getAllModelsFromRegistry<P extends BaseProvider>(
|
||||
|
||||
/**
|
||||
* Build auth headers for a provider.
|
||||
* Handles bearer, token, xi-api-key, x-api-key, and none.
|
||||
* Handles bearer, key, token, xi-api-key, x-api-key, and none.
|
||||
*/
|
||||
export function buildAuthHeaders(
|
||||
provider: BaseProvider,
|
||||
@@ -87,6 +87,8 @@ export function buildAuthHeaders(
|
||||
}
|
||||
|
||||
switch (provider.authHeader) {
|
||||
case "key":
|
||||
return { Authorization: `Key ${token}` };
|
||||
case "token":
|
||||
return { Authorization: `Token ${token}` };
|
||||
case "xi-api-key":
|
||||
|
||||
@@ -21,6 +21,7 @@ import { buildBedrockChatUrl } from "../config/bedrock.ts";
|
||||
import { buildWatsonxChatUrl } from "../config/watsonx.ts";
|
||||
import { buildOciChatUrl } from "../config/oci.ts";
|
||||
import { buildSapChatUrl, getSapResourceGroup } from "../config/sap.ts";
|
||||
import { buildMaritalkChatUrl } from "../config/maritalk.ts";
|
||||
|
||||
function normalizeBaseUrl(baseUrl) {
|
||||
return (baseUrl || "").trim().replace(/\/$/, "");
|
||||
@@ -180,6 +181,10 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl;
|
||||
return normalizeGigachatChatUrl(baseUrl);
|
||||
}
|
||||
case "maritalk": {
|
||||
const baseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl;
|
||||
return buildMaritalkChatUrl(baseUrl);
|
||||
}
|
||||
case "lm-studio":
|
||||
case "modal":
|
||||
case "reka":
|
||||
@@ -284,6 +289,13 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "maritalk": {
|
||||
const token = effectiveKey || credentials.accessToken;
|
||||
if (token) {
|
||||
headers["Authorization"] = `Key ${token}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "claude":
|
||||
case "anthropic":
|
||||
effectiveKey
|
||||
|
||||
@@ -332,6 +332,11 @@ export function buildProviderHeaders(provider, credentials, stream = true, body
|
||||
if (token) {
|
||||
headers["x-api-key"] = token;
|
||||
}
|
||||
} else if (authHeader === "key") {
|
||||
const token = credentials.apiKey || credentials.accessToken;
|
||||
if (token) {
|
||||
headers["Authorization"] = `Key ${token}`;
|
||||
}
|
||||
} else if (authHeader === "x-goog-api-key") {
|
||||
if (credentials.apiKey) {
|
||||
headers["x-goog-api-key"] = credentials.apiKey;
|
||||
|
||||
@@ -67,6 +67,10 @@ import {
|
||||
normalizeRunwayBaseUrl,
|
||||
} from "@omniroute/open-sse/config/runway.ts";
|
||||
import { PETALS_DEFAULT_MODEL, normalizePetalsBaseUrl } from "@omniroute/open-sse/config/petals.ts";
|
||||
import {
|
||||
buildMaritalkChatUrl,
|
||||
buildMaritalkModelsUrl,
|
||||
} from "@omniroute/open-sse/config/maritalk.ts";
|
||||
import { signAwsRequest } from "@omniroute/open-sse/utils/awsSigV4.ts";
|
||||
import { validateImageProviderApiKey } from "@/lib/providers/imageValidation";
|
||||
|
||||
@@ -229,6 +233,18 @@ function buildClarifaiHeaders(apiKey: string, providerSpecificData: any = {}) {
|
||||
return applyCustomUserAgent(headers, providerSpecificData);
|
||||
}
|
||||
|
||||
function buildKeyHeaders(apiKey: string, providerSpecificData: any = {}) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (apiKey) {
|
||||
headers.Authorization = `Key ${apiKey}`;
|
||||
}
|
||||
|
||||
return applyCustomUserAgent(headers, providerSpecificData);
|
||||
}
|
||||
|
||||
function buildTokenHeaders(apiKey: string, providerSpecificData: any = {}) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1544,6 +1560,59 @@ async function validateRekaProvider({ apiKey, providerSpecificData = {} }: any)
|
||||
return { valid: false, error: "Connection failed while testing Reka" };
|
||||
}
|
||||
|
||||
async function validateMaritalkProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const entry = getRegistryEntry("maritalk");
|
||||
const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl || entry?.baseUrl);
|
||||
const headers = buildKeyHeaders(apiKey, providerSpecificData);
|
||||
|
||||
try {
|
||||
const modelsRes = await validationRead(buildMaritalkModelsUrl(baseUrl), {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (modelsRes.ok) {
|
||||
return { valid: true, error: null, method: "maritalk_models" };
|
||||
}
|
||||
|
||||
if (modelsRes.status === 401 || modelsRes.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
|
||||
if (modelsRes.status === 429) {
|
||||
return {
|
||||
valid: true,
|
||||
error: null,
|
||||
method: "maritalk_models",
|
||||
warning: "Rate limited, but credentials are valid",
|
||||
};
|
||||
}
|
||||
|
||||
if (modelsRes.status >= 500) {
|
||||
return { valid: false, error: `Provider unavailable (${modelsRes.status})` };
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the chat probe when /models cannot be reached.
|
||||
}
|
||||
|
||||
const modelId =
|
||||
typeof providerSpecificData?.validationModelId === "string" &&
|
||||
providerSpecificData.validationModelId.trim()
|
||||
? providerSpecificData.validationModelId.trim()
|
||||
: entry?.models?.[0]?.id || "sabia-4";
|
||||
|
||||
return validateDirectChatProvider({
|
||||
url: buildMaritalkChatUrl(baseUrl),
|
||||
headers,
|
||||
body: {
|
||||
model: modelId,
|
||||
messages: [{ role: "user", content: "test" }],
|
||||
max_tokens: 1,
|
||||
},
|
||||
providerSpecificData,
|
||||
});
|
||||
}
|
||||
|
||||
async function validateNlpCloudProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const rawBaseUrl = normalizeBaseUrl(providerSpecificData.baseUrl) || "https://api.nlpcloud.io/v1";
|
||||
const baseUrl = rawBaseUrl.endsWith("/gpu") ? rawBaseUrl : `${rawBaseUrl.replace(/\/$/, "")}/gpu`;
|
||||
@@ -2880,6 +2949,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
poe: validatePoeProvider,
|
||||
clarifai: validateClarifaiProvider,
|
||||
reka: validateRekaProvider,
|
||||
maritalk: validateMaritalkProvider,
|
||||
nlpcloud: validateNlpCloudProvider,
|
||||
runwayml: validateRunwayProvider,
|
||||
snowflake: validateSnowflakeProvider,
|
||||
|
||||
@@ -168,6 +168,7 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U
|
||||
const sap = new DefaultExecutor("sap");
|
||||
const modal = new DefaultExecutor("modal");
|
||||
const reka = new DefaultExecutor("reka");
|
||||
const maritalk = new DefaultExecutor("maritalk");
|
||||
const snowflake = new DefaultExecutor("snowflake");
|
||||
const gigachat = new DefaultExecutor("gigachat");
|
||||
|
||||
@@ -239,6 +240,14 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U
|
||||
}),
|
||||
"https://api.reka.ai/v1/chat/completions"
|
||||
);
|
||||
assert.equal(
|
||||
maritalk.buildUrl("sabia-4", true, 0, {
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://chat.maritaca.ai/api/chat/inference",
|
||||
},
|
||||
}),
|
||||
"https://chat.maritaca.ai/api/chat/completions"
|
||||
);
|
||||
assert.equal(
|
||||
snowflake.buildUrl("llama3.3-70b", true, 0, {
|
||||
providerSpecificData: { baseUrl: "https://account.snowflakecomputing.com" },
|
||||
@@ -266,6 +275,7 @@ test("DefaultExecutor.buildHeaders handles Gemini and Claude auth modes", () =>
|
||||
const oci = new DefaultExecutor("oci");
|
||||
const sap = new DefaultExecutor("sap");
|
||||
const modal = new DefaultExecutor("modal");
|
||||
const maritalk = new DefaultExecutor("maritalk");
|
||||
|
||||
const geminiApiKeyHeaders = gemini.buildHeaders({ apiKey: "gem-key" }, true);
|
||||
const geminiOAuthHeaders = gemini.buildHeaders({ accessToken: "gem-token" }, false);
|
||||
@@ -294,6 +304,7 @@ test("DefaultExecutor.buildHeaders handles Gemini and Claude auth modes", () =>
|
||||
},
|
||||
true
|
||||
);
|
||||
const maritalkHeaders = maritalk.buildHeaders({ apiKey: "maritalk-key" }, true);
|
||||
|
||||
assert.equal(geminiApiKeyHeaders["x-goog-api-key"], "gem-key");
|
||||
assert.equal(geminiApiKeyHeaders.Accept, "text/event-stream");
|
||||
@@ -310,6 +321,7 @@ test("DefaultExecutor.buildHeaders handles Gemini and Claude auth modes", () =>
|
||||
assert.equal(sapHeaders.Authorization, "Bearer sap-key");
|
||||
assert.equal(sapHeaders["AI-Resource-Group"], "shared");
|
||||
assert.equal(modalHeaders.Authorization, "Bearer modal-key");
|
||||
assert.equal(maritalkHeaders.Authorization, "Key maritalk-key");
|
||||
});
|
||||
|
||||
test("DefaultExecutor.buildHeaders handles GLM, default auth and anthropic-compatible headers", () => {
|
||||
|
||||
@@ -101,6 +101,19 @@ test("Registry-driven headers support x-goog-api-key and bearer fallback", () =>
|
||||
assert.equal(accessTokenHeaders.Authorization, "Bearer gemini-access-token");
|
||||
});
|
||||
|
||||
test("Registry-driven headers support Key auth", () => {
|
||||
const headers = buildProviderHeaders(
|
||||
"maritalk",
|
||||
{
|
||||
apiKey: "maritalk-key",
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
assert.equal(headers.Authorization, "Key maritalk-key");
|
||||
assert.equal(headers.Accept, "text/event-stream");
|
||||
});
|
||||
|
||||
test("Unknown providers fall back to bearer auth and OpenAI format", () => {
|
||||
const headers = buildProviderHeaders(
|
||||
"custom-provider",
|
||||
|
||||
@@ -727,6 +727,75 @@ test("google PSE validator requires cx", async () => {
|
||||
assert.equal(result.error, "Programmable Search Engine ID (cx) is required");
|
||||
});
|
||||
|
||||
test("Maritalk validates with Key auth against the models endpoint", async () => {
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
assert.equal(String(url), "https://chat.maritaca.ai/api/models");
|
||||
assert.equal(init.headers.Authorization, "Key maritalk-key");
|
||||
return new Response(JSON.stringify({ data: [{ id: "sabia-4" }] }), {
|
||||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "maritalk",
|
||||
apiKey: "maritalk-key",
|
||||
});
|
||||
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.method, "maritalk_models");
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test("Maritalk falls back to chat probe when the models endpoint is unreachable", async () => {
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
|
||||
if (String(url) === "https://chat.maritaca.ai/api/models") {
|
||||
return new Response(JSON.stringify({ error: "not found" }), { status: 404 });
|
||||
}
|
||||
|
||||
assert.equal(String(url), "https://chat.maritaca.ai/api/chat/completions");
|
||||
assert.equal(init.headers.Authorization, "Key maritalk-key");
|
||||
const body = JSON.parse(String(init.body));
|
||||
assert.equal(body.model, "sabia-4");
|
||||
return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), {
|
||||
status: 200,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "maritalk",
|
||||
apiKey: "maritalk-key",
|
||||
});
|
||||
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test("Maritalk treats a rate-limited models probe as valid credentials", async () => {
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), init });
|
||||
assert.equal(String(url), "https://chat.maritaca.ai/api/models");
|
||||
assert.equal(init.headers.Authorization, "Key maritalk-key");
|
||||
return new Response(JSON.stringify({ error: "rate limited" }), {
|
||||
status: 429,
|
||||
});
|
||||
};
|
||||
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "maritalk",
|
||||
apiKey: "maritalk-key",
|
||||
});
|
||||
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.warning, "Rate limited, but credentials are valid");
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test("local OpenAI-style providers validate without sending Authorization when apiKey is blank", async () => {
|
||||
const originalAllowPrivateProviderUrls = process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS;
|
||||
process.env.OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS = "true";
|
||||
|
||||
@@ -168,6 +168,12 @@ test("buildAuthHeaders: returns Token header for token authHeader", () => {
|
||||
assert.deepEqual(headers, { Authorization: "Token hf-token" });
|
||||
});
|
||||
|
||||
test("buildAuthHeaders: returns Key header for key authHeader", () => {
|
||||
const provider = { ...MOCK_REGISTRY.nvidia, authHeader: "key", authType: "apikey" };
|
||||
const headers = buildAuthHeaders(provider, "maritalk-key");
|
||||
assert.deepEqual(headers, { Authorization: "Key maritalk-key" });
|
||||
});
|
||||
|
||||
test("buildAuthHeaders: returns x-api-key header", () => {
|
||||
const provider = { ...MOCK_REGISTRY.nvidia, authHeader: "x-api-key", authType: "apikey" };
|
||||
const headers = buildAuthHeaders(provider, "custom-key");
|
||||
|
||||
Reference in New Issue
Block a user