Merge origin/master (v0.5.91) into gitea/new_feature
Resolve conflicts: - streamingHandler.js: adopt upstreamResponseHeaders while keeping 0-token detail row avoidance - capabilities.js: preserve user-asserted caps and globalThis slots without local caching of catalogSource - AddCustomModelModal.js & providers/[id]/page.js: wire STT transport marker with custom model edits/assertions - models/custom/route.js & aliasRepo.js: persist custom model transport and invalidate user caps - usageRepo.js: key byApiKey live stats by full API key and keep tail in maskApiKey - UsageStats.js: lazy load charts dynamically
This commit is contained in:
@@ -25,10 +25,25 @@ function deriveUuid(seed) {
|
||||
function generateFakeUserID(sessionId, apiKey) {
|
||||
const deviceId = apiKey ? createHash("sha256").update(`device:${apiKey}`).digest("hex") : randomBytes(32).toString("hex");
|
||||
const accountUuid = apiKey ? deriveUuid(`account:${apiKey}`) : randomUUID();
|
||||
const sessionUuid = sessionId || randomUUID();
|
||||
const cleanSessionId = typeof sessionId === "string" ? sessionId.replace(/^claude:/i, "").trim() : null;
|
||||
const sessionUuid = cleanSessionId || randomUUID();
|
||||
return `{"device_id":"${deviceId}","account_uuid":"${accountUuid}","session_id":"${sessionUuid}"}`;
|
||||
}
|
||||
|
||||
export function extractClaudeSessionIdFromUserId(userId) {
|
||||
if (typeof userId !== "string" || !userId) return null;
|
||||
if (userId[0] === "{") {
|
||||
try {
|
||||
const sid = JSON.parse(userId)?.session_id;
|
||||
return typeof sid === "string" && sid ? sid.replace(/^claude:/i, "").trim() || null : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const clean = userId.replace(/^claude:/i, "").trim();
|
||||
return clean || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloak tools before sending to Claude provider (anti-ban):
|
||||
* - Rename client tools with the CLAUDE_TOOL_SUFFIX ("_ide") in tools[] and messages[]
|
||||
@@ -89,14 +104,29 @@ export function cloakClaudeTools(body) {
|
||||
};
|
||||
}
|
||||
|
||||
// Strip a trailing CLAUDE_TOOL_SUFFIX from a cloaked name as a last-resort
|
||||
// fallback when the name isn't in toolNameMap (e.g. map lost across a retry/
|
||||
// reconnect). Never strips decoy names — those are meant to reach the client
|
||||
// unresolved so it can see "tool unavailable" instead of silently no-oping.
|
||||
function stripCloakSuffix(name) {
|
||||
if (typeof name !== "string" || !name.endsWith(CLAUDE_TOOL_SUFFIX)) return null;
|
||||
if (CC_DEFAULT_TOOLS.has(name)) return null;
|
||||
const original = name.slice(0, -CLAUDE_TOOL_SUFFIX.length);
|
||||
return original.length > 0 ? original : null;
|
||||
}
|
||||
|
||||
// Decloak tool_use names in non-streaming Claude response body (INPUT side)
|
||||
export function decloakToolNames(body, toolNameMap) {
|
||||
if (!toolNameMap?.size || !Array.isArray(body?.content)) return body;
|
||||
if (!Array.isArray(body?.content)) return body;
|
||||
const content = body.content.map(block => {
|
||||
if (block?.type === "tool_use" && toolNameMap.has(block.name)) {
|
||||
if (block?.type !== "tool_use") return block;
|
||||
if (toolNameMap?.has(block.name)) {
|
||||
return { ...block, name: toolNameMap.get(block.name) };
|
||||
}
|
||||
return block;
|
||||
// toolNameMap missing/stale for this name — fall back to suffix stripping
|
||||
// rather than forwarding an unresolvable "<tool>_ide" name to the client.
|
||||
const fallback = stripCloakSuffix(block.name);
|
||||
return fallback ? { ...block, name: fallback } : block;
|
||||
});
|
||||
return { ...body, content };
|
||||
}
|
||||
@@ -111,19 +141,21 @@ export function decloakToolNames(body, toolNameMap) {
|
||||
* name appears exactly once per call — on the content_block_start event of
|
||||
* a tool_use block; argument deltas carry no name.
|
||||
*
|
||||
* Unknown names (e.g. a CC decoy tool the model called anyway) pass through
|
||||
* unchanged, matching the non-streaming decloak behavior.
|
||||
* Falls back to stripping the literal CLAUDE_TOOL_SUFFIX when the name isn't
|
||||
* in toolNameMap (map lost across a retry/reconnect), matching the
|
||||
* non-streaming decloak behavior. Decoy tool names (real CC tool names) and
|
||||
* anything else pass through unchanged.
|
||||
*
|
||||
* @param {object|null} chunk - Parsed SSE event (may be null on stream flush)
|
||||
* @param {Map|null} toolNameMap - Suffixed → original name map from cloakClaudeTools()
|
||||
* @returns {object|null} The chunk, with the tool_use name restored when cloaked
|
||||
*/
|
||||
export function decloakStreamChunk(chunk, toolNameMap) {
|
||||
if (!toolNameMap?.size || !chunk || typeof chunk !== "object") return chunk;
|
||||
if (!chunk || typeof chunk !== "object") return chunk;
|
||||
if (chunk.type !== "content_block_start") return chunk;
|
||||
const block = chunk.content_block;
|
||||
if (block?.type !== "tool_use" || typeof block.name !== "string") return chunk;
|
||||
const original = toolNameMap.get(block.name);
|
||||
const original = toolNameMap?.get(block.name) || stripCloakSuffix(block.name);
|
||||
if (!original) return chunk;
|
||||
return { ...chunk, content_block: { ...block, name: original } };
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ export function buildErrorBody(statusCode, message) {
|
||||
* @param {string} message - Error message
|
||||
* @returns {Response} HTTP Response object
|
||||
*/
|
||||
export function errorResponse(statusCode, message) {
|
||||
export function errorResponse(statusCode, message, extraHeaders = null) {
|
||||
return new Response(JSON.stringify(buildErrorBody(statusCode, message)), {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
...extraHeaders
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -95,13 +96,13 @@ export async function parseUpstreamError(response, executor = null) {
|
||||
* @param {number} [resetsAtMs] - Optional precise cooldown expiry (ms epoch) for provider-specific quota errors
|
||||
* @returns {{ success: false, status: number, error: string, response: Response, resetsAtMs?: number }}
|
||||
*/
|
||||
export function createErrorResult(statusCode, message, resetsAtMs) {
|
||||
export function createErrorResult(statusCode, message, resetsAtMs, extraHeaders = null) {
|
||||
return {
|
||||
success: false,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
resetsAtMs,
|
||||
response: errorResponse(statusCode, message)
|
||||
response: errorResponse(statusCode, message, extraHeaders)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ export function createErrorResult(statusCode, message, resetsAtMs) {
|
||||
* @param {string} retryAfterHuman - Human-readable retry info e.g. "reset after 30s"
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman) {
|
||||
export function unavailableResponse(statusCode, message, retryAfter, retryAfterHuman, extraHeaders = null) {
|
||||
const retryAfterSec = Math.max(Math.ceil((new Date(retryAfter).getTime() - Date.now()) / 1000), 1);
|
||||
const msg = `${message} (${retryAfterHuman})`;
|
||||
return new Response(
|
||||
@@ -121,8 +122,10 @@ export function unavailableResponse(statusCode, message, retryAfter, retryAfterH
|
||||
{
|
||||
status: statusCode,
|
||||
headers: {
|
||||
...extraHeaders,
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": String(retryAfterSec)
|
||||
// Intentionally mis-cased to prevent duplicate headers
|
||||
"retry-after": String(retryAfterSec)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
12
open-sse/utils/upstreamHeaders.js
Normal file
12
open-sse/utils/upstreamHeaders.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const FORWARDED = new Set(["retry-after", "x-should-retry"]);
|
||||
const FORWARDED_PREFIX = "anthropic-ratelimit-";
|
||||
|
||||
export function upstreamResponseHeaders(headers) {
|
||||
const out = {};
|
||||
if (typeof headers?.forEach !== "function") return out;
|
||||
headers.forEach((value, name) => {
|
||||
const key = name.toLowerCase();
|
||||
if (FORWARDED.has(key) || key.startsWith(FORWARDED_PREFIX)) out[key] = value;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user