merge: update from decolua/9router upstream (SAML, providers, opencode session headers)
This commit is contained in:
commit
f555236545
103 files changed
+10682
-569
No files matched your search
@@ -138,8 +138,42 @@ export function detectRequiredCapabilities(body) {
|
||||
if (Array.isArray(content)) for (const b of content) scanBlock(b);
|
||||
};
|
||||
|
||||
const scanMessage = (m) => {
|
||||
if (!m || typeof m !== "object") return;
|
||||
|
||||
// Ollama / Hermes images array (strings or objects)
|
||||
if (Array.isArray(m.images) && m.images.length > 0) {
|
||||
required.add("vision");
|
||||
}
|
||||
|
||||
// Vercel AI SDK / Hermes attachments / experimental_attachments
|
||||
const attachments = m.experimental_attachments || m.attachments;
|
||||
if (Array.isArray(attachments)) {
|
||||
for (const att of attachments) {
|
||||
if (!att) continue;
|
||||
const mime = att.contentType || att.mediaType || (typeof att.url === "string" && att.url.match(/^data:([^;,]+)/)?.[1]);
|
||||
if (mime) addByMime(mime);
|
||||
else if (att.url || att.data) required.add("vision");
|
||||
}
|
||||
}
|
||||
|
||||
// Direct message-level modality properties
|
||||
if (m.image_url || m.image) required.add("vision");
|
||||
if (m.audio_url || m.audio) required.add("audioInput");
|
||||
|
||||
// Scan array content blocks
|
||||
scanContent(m.content);
|
||||
|
||||
// Scan string content for embedded data URIs
|
||||
if (typeof m.content === "string") {
|
||||
if (m.content.includes("data:image/")) required.add("vision");
|
||||
else if (m.content.includes("data:audio/")) required.add("audioInput");
|
||||
else if (m.content.includes("data:application/pdf")) required.add("pdf");
|
||||
}
|
||||
};
|
||||
|
||||
// Modalities: current user turn only (trailing user run across each known shape).
|
||||
for (const m of trailingUserItems(body.messages)) scanContent(m.content); // openai / claude
|
||||
for (const m of trailingUserItems(body.messages)) scanMessage(m); // openai / claude / hermes / ollama
|
||||
for (const it of trailingUserItems(body.input)) scanContent(it.content); // responses
|
||||
const contents = body.contents || body.request?.contents; // gemini / antigravity
|
||||
for (const c of trailingUserItems(contents)) scanContent(c.parts);
|
||||
@@ -530,7 +564,10 @@ export async function handleFusionChat({ body, models, handleSingleModel, log, c
|
||||
log.info("FUSION", `Combo "${comboName}" | panel=${panel.length} [${panel.join(", ")}] | judge=${judge} | quorum=${minPanel}`);
|
||||
|
||||
// 1. Fan out to the panel in parallel: non-streaming, tools stripped (we want prose).
|
||||
const { tools, tool_choice, ...rest } = body;
|
||||
const { tools, tool_choice, stream_options, ...rest } = body;
|
||||
// Fusion runs panel models non-streaming; drop stream_options too, or providers
|
||||
// like DeepSeek reject it with "stream_options should be set along with stream = true".
|
||||
// See issue #3024.
|
||||
const panelBody = { ...rest, stream: false };
|
||||
|
||||
// Flatten tool turns to prose so panel models keep context without emitting tool_calls.
|
||||
|
||||
@@ -34,7 +34,7 @@ const USAGE_HANDLERS = {
|
||||
github: (c) => getGitHubUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
"gemini-cli": (c) => getGeminiUsage(c.accessToken, c.providerDataWithProjectId, c.proxyOptions),
|
||||
antigravity: (c) => getAntigravityUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
claude: (c) => getClaudeUsage(c.accessToken, c.proxyOptions),
|
||||
claude: (c) => getClaudeUsage(c.accessToken, c.proxyOptions, { force: c.force }),
|
||||
codex: (c) => getCodexUsage(c.accessToken, c.proxyOptions),
|
||||
kiro: (c) => getKiroUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
qoder: async (c) => {
|
||||
@@ -58,7 +58,7 @@ const USAGE_HANDLERS = {
|
||||
freebuff: (c) => getFreebuffUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
};
|
||||
|
||||
export async function getUsageForProvider(connection, proxyOptions = null) {
|
||||
export async function getUsageForProvider(connection, proxyOptions = null, options = {}) {
|
||||
const { provider, accessToken, apiKey, providerSpecificData, projectId } = connection;
|
||||
const providerDataWithProjectId = {
|
||||
...(providerSpecificData || {}),
|
||||
@@ -67,5 +67,13 @@ export async function getUsageForProvider(connection, proxyOptions = null) {
|
||||
|
||||
const handler = USAGE_HANDLERS[provider];
|
||||
if (!handler) return { message: `Usage API not implemented for ${provider}` };
|
||||
return await handler({ provider, accessToken, apiKey, providerSpecificData, providerDataWithProjectId, proxyOptions });
|
||||
return await handler({
|
||||
provider,
|
||||
accessToken,
|
||||
apiKey,
|
||||
providerSpecificData,
|
||||
providerDataWithProjectId,
|
||||
proxyOptions,
|
||||
force: options.force === true,
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,43 @@ const CLAUDE_CONFIG = {
|
||||
const OAUTH_429_COOLDOWN_MS = 180000;
|
||||
const oauthCooldown = new Map();
|
||||
|
||||
export async function getClaudeUsage(accessToken, proxyOptions = null) {
|
||||
// Dedup + short TTL cache per access token. Many tabs / many accounts / auto-refresh
|
||||
// all funnel through here; without this each call hits Anthropic and triggers 429.
|
||||
const USAGE_CACHE_TTL_MS = 300000;
|
||||
const usageCache = new Map(); // token -> { promise } | { result, expiresAt }
|
||||
|
||||
export async function getClaudeUsage(accessToken, proxyOptions = null, options = {}) {
|
||||
const force = options?.force === true;
|
||||
|
||||
// Serve in-flight or fresh cached result (skip on manual force)
|
||||
if (!force && accessToken) {
|
||||
const hit = usageCache.get(accessToken);
|
||||
if (hit?.promise) return hit.promise;
|
||||
if (hit && hit.expiresAt > Date.now()) return hit.result;
|
||||
}
|
||||
|
||||
const stale = (!force && accessToken && usageCache.get(accessToken)?.result) || null;
|
||||
|
||||
const promise = (async () => {
|
||||
const result = await fetchClaudeUsageRaw(accessToken, proxyOptions);
|
||||
// Only cache real quota data, not soft-failure {message: ...} payloads
|
||||
if (accessToken && result?.quotas) {
|
||||
usageCache.set(accessToken, {
|
||||
result,
|
||||
expiresAt: Date.now() + USAGE_CACHE_TTL_MS,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
// Soft failure (429/error): prefer the last good read over a transient error
|
||||
if (stale) return stale;
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (accessToken) usageCache.set(accessToken, { promise });
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function fetchClaudeUsageRaw(accessToken, proxyOptions = null) {
|
||||
try {
|
||||
// Skip OAuth usage call while this token is cooling down from a recent 429
|
||||
const cooldownUntil = oauthCooldown.get(accessToken);
|
||||
|
||||
@@ -161,6 +161,9 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
|
||||
if (data.models) {
|
||||
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
|
||||
const importantModels = [
|
||||
'gemini-3.7-flash-high',
|
||||
'gemini-3.7-flash-medium',
|
||||
'gemini-3.7-flash-low',
|
||||
'gemini-3.6-flash-high',
|
||||
'gemini-3.6-flash-medium',
|
||||
'gemini-3.6-flash-low',
|
||||
|
||||
Reference in new issue
Block a user