fix(maritalk):Update Model list & Implementation of the latest API (#1856)

Integrated into release/v3.7.8
This commit is contained in:
backryun
2026-05-02 04:11:13 +09:00
committed by GitHub
parent c766aa5403
commit d2ce20dc5c
10 changed files with 214 additions and 6 deletions

View 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`;
}

View File

@@ -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,
},

View File

@@ -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":

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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", () => {

View File

@@ -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",

View File

@@ -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";

View File

@@ -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");