From d2ce20dc5c0b8f36272efed0df316d33bd87cb7a Mon Sep 17 00:00:00 2001 From: backryun Date: Sat, 2 May 2026 04:11:13 +0900 Subject: [PATCH] fix(maritalk):Update Model list & Implementation of the latest API (#1856) Integrated into release/v3.7.8 --- open-sse/config/maritalk.ts | 18 +++++ open-sse/config/providerRegistry.ts | 9 +-- open-sse/config/registryUtils.ts | 6 +- open-sse/executors/default.ts | 12 ++++ open-sse/services/provider.ts | 5 ++ src/lib/providers/validation.ts | 70 +++++++++++++++++++ tests/unit/executor-default-base.test.ts | 12 ++++ tests/unit/provider-service.test.ts | 13 ++++ .../provider-validation-specialty.test.ts | 69 ++++++++++++++++++ tests/unit/registry-utils.test.ts | 6 ++ 10 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 open-sse/config/maritalk.ts diff --git a/open-sse/config/maritalk.ts b/open-sse/config/maritalk.ts new file mode 100644 index 0000000000..a966058fd6 --- /dev/null +++ b/open-sse/config/maritalk.ts @@ -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`; +} diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 2830b4411b..60b11ae6bc 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -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 = { "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 = { 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 = { 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, }, diff --git a/open-sse/config/registryUtils.ts b/open-sse/config/registryUtils.ts index 71e9f72f2f..6ca45f1da6 100644 --- a/open-sse/config/registryUtils.ts +++ b/open-sse/config/registryUtils.ts @@ -16,7 +16,7 @@ export interface BaseProvider { 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

( /** * 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": diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 0faee7c3cd..367ff163b4 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -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 diff --git a/open-sse/services/provider.ts b/open-sse/services/provider.ts index 56a8d51509..29a188f98e 100644 --- a/open-sse/services/provider.ts +++ b/open-sse/services/provider.ts @@ -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; diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index c38fff5297..04487c55f9 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -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 = { + "Content-Type": "application/json", + }; + + if (apiKey) { + headers.Authorization = `Key ${apiKey}`; + } + + return applyCustomUserAgent(headers, providerSpecificData); +} + function buildTokenHeaders(apiKey: string, providerSpecificData: any = {}) { const headers: Record = { "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, diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index b78e86c737..0358fa87b8 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -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", () => { diff --git a/tests/unit/provider-service.test.ts b/tests/unit/provider-service.test.ts index e6a4c229b9..9712dc2d4e 100644 --- a/tests/unit/provider-service.test.ts +++ b/tests/unit/provider-service.test.ts @@ -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", diff --git a/tests/unit/provider-validation-specialty.test.ts b/tests/unit/provider-validation-specialty.test.ts index 2d7efc9828..215b636654 100644 --- a/tests/unit/provider-validation-specialty.test.ts +++ b/tests/unit/provider-validation-specialty.test.ts @@ -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"; diff --git a/tests/unit/registry-utils.test.ts b/tests/unit/registry-utils.test.ts index ffcb7b0fb2..ada9128028 100644 --- a/tests/unit/registry-utils.test.ts +++ b/tests/unit/registry-utils.test.ts @@ -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");