merge: update from decolua/9router upstream (SAML, providers, opencode session headers)

This commit is contained in:
MUH. IQRAM BAHRING committed 2026-08-15 03:54:03 +08:00
commit f555236545
103 files changed
+10682 -569

No files matched your search

+19 -5
View File
@@ -2,11 +2,11 @@ import { detectFormat, getTargetFormat, resolveTransport } from "../services/pro
import { translateRequest } from "../translator/index.js";
import { applyThinking, extractThinking, stripThinkingSuffix } from "../translator/concerns/thinkingUnified.js";
import { FORMATS } from "../translator/formats.js";
import { normalizeClaudePassthrough } from "../translator/formats/claude.js";
import { normalizeClaudePassthrough, anchorClaudeCache } from "../translator/formats/claude.js";
import { createStreamController } from "../utils/streamHandler.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { createRequestLogger } from "../utils/requestLogger.js";
import { getModelTargetFormat, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
import { getModelTargetFormat, getModelSupportedFormats, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
import { PROVIDERS } from "../config/providers.js";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS, TOKEN_SAVER_HEADER } from "../config/runtimeConfig.js";
@@ -84,10 +84,20 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, model);
// Multi-endpoint providers: pick transport matching sourceFormat → zero translation
// Multi-endpoint providers: pick transport matching sourceFormat → zero translation.
// Per-model guard: only use the transport when the model declares support for that
// sourceFormat — opencode-go models differ in endpoint support (kimi/glm only do
// /chat/completions), so without this guard a claude-format request would wrongly
// route kimi to /messages.
const modelSupportedFormats = getModelSupportedFormats(alias, model);
const runtimeTransport = resolveTransport(provider, sourceFormat);
const targetFormat = modelTargetFormat || runtimeTransport?.format || getTargetFormat(provider, credentials);
if (runtimeTransport && credentials) credentials.runtimeTransport = runtimeTransport;
// Per-model guard: when a model declares supportedFormats, only use the
// sourceFormat-matched transport if that format is declared (opencode-go models
// differ — kimi/glm only do /chat/completions). Undeclared models keep the
// upstream default (use the transport), preserving behavior for glm/deepseek/...
const useTransport = (!modelSupportedFormats || modelSupportedFormats.includes(sourceFormat)) ? runtimeTransport : null;
const targetFormat = modelTargetFormat || useTransport?.format || getTargetFormat(provider, credentials);
if (useTransport && credentials) credentials.runtimeTransport = useTransport;
const stripList = getModelStrip(alias, model);
const upstreamModel = getModelUpstreamId(alias, model);
@@ -281,6 +291,10 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
if (xf.length && log?.line) log.line(reqTag, "⚙", xf.join(" · "));
// Pin cache breakpoints to the final body — every saver above can reshape
// system/tools/messages, and a stale anchor costs a full prefix rewrite.
if (passthrough && clientTool === "claude") anchorClaudeCache(translatedBody);
const executor = getExecutor(provider);
trackPendingRequest(model, provider, connectionId, true);
appendRequestLog({ model, provider, connectionId, status: "PENDING" }).catch(() => { });
+7 -6
View File
@@ -44,13 +44,14 @@ export function extractUsageFromResponse(responseBody) {
};
}
// Gemini format
if (responseBody.usageMetadata) {
// Gemini format. Antigravity / gemini-cli wrap the payload in { response: {...} }.
const usageMetadata = responseBody.usageMetadata || responseBody.response?.usageMetadata;
if (usageMetadata) {
return {
prompt_tokens: responseBody.usageMetadata.promptTokenCount || 0,
completion_tokens: responseBody.usageMetadata.candidatesTokenCount || 0,
cached_tokens: responseBody.usageMetadata.cachedContentTokenCount || 0,
reasoning_tokens: responseBody.usageMetadata.thoughtsTokenCount || 0
prompt_tokens: usageMetadata.promptTokenCount || 0,
completion_tokens: usageMetadata.candidatesTokenCount || 0,
cached_tokens: usageMetadata.cachedContentTokenCount || 0,
reasoning_tokens: usageMetadata.thoughtsTokenCount || 0
};
}
+21
View File
@@ -29,6 +29,8 @@
* @property {Record<string,unknown>} [providerSpecificData]
*/
import { assertPublicUrl } from "../../../src/shared/utils/ssrfGuard.js";
// ── Helpers ─────────────────────────────────────────────────────────────
/**
@@ -63,12 +65,31 @@ export function getProviderSetting(params, key) {
/**
* Resolve base URL with optional override from providerOptions.baseUrl.
*
* The override is client-controlled and therefore SSRF-hardened: only public
* http(s) URLs are accepted (internal/private/loopback/metadata addresses are
* rejected via assertPublicUrl). The provider's own configured baseUrl is
* trusted as-is (admin-controlled).
*
* @param {SearchProviderConfig} config
* @param {SearchRequestParams} params
* @returns {string}
*/
export function resolveBaseUrl(config, params) {
const override = getProviderSetting(params, "baseUrl");
if (override) {
// SSRF guard: client-supplied base URLs must be public http(s) only.
let parsed;
try {
parsed = new URL(override);
} catch {
throw new Error(`Invalid baseUrl: ${override}`);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`Invalid baseUrl protocol: ${parsed.protocol}`);
}
assertPublicUrl(override);
}
return (override || config.baseUrl).replace(/\/+$/, "");
}
@@ -51,6 +51,25 @@ async function huggingface({ baseUrl, apiKey, text, modelId }) {
return responseToBase64(res, "wav");
}
// Fish Audio: model travels in an HTTP header, the voice is a reference_id, returns binary
async function fishAudio({ baseUrl, apiKey, text, modelId, voiceId }) {
const res = await fetch(baseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
"model": modelId || "s2.1-pro-free",
},
body: JSON.stringify({
text,
format: "mp3",
...(voiceId ? { reference_id: voiceId } : {}),
}),
});
if (!res.ok) await throwUpstreamError(res);
return responseToBase64(res, "mp3");
}
// Inworld: Basic auth, JSON { audioContent }
async function inworld({ baseUrl, apiKey, text, modelId, voiceId }) {
const res = await fetch(baseUrl, {
@@ -166,4 +185,5 @@ export const FORMAT_HANDLERS = {
tortoise,
openai: openaiCompat,
"minimax-tts": minimaxTts,
"fish-audio": fishAudio,
};