merge: integrate origin/master (v0.5.50) into gitea/new_feature
- Resolve conflicts in chatCore handlers: keep apiKey/streamErrorPatterns from the details-filters feature, adopt origin's stripContinuityFields, customToolNames, cache-inclusive usage accounting, and Responses-API SSE→JSON conversion - Adopt origin's provider usage handlers (codebuddy-intl, qoder creds) and modality detection (audio/video inputs) - Keep requestDetails apiKey column (schema v2) + masked key persistence Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -43,17 +43,17 @@ function refillCadence(acc) {
|
||||
return "Monthly";
|
||||
}
|
||||
|
||||
export async function getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificData, proxyOptions = null) {
|
||||
async function getCodeBuddyUsage(providerId, accessToken, apiKey, providerSpecificData, proxyOptions = null) {
|
||||
const token = accessToken || apiKey;
|
||||
if (!token) {
|
||||
return { message: "CodeBuddy CN credential not available." };
|
||||
return { message: `CodeBuddy (${providerId}) credential not available.` };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await proxyAwareFetch(U(PROVIDER_ID).url, {
|
||||
const response = await proxyAwareFetch(U(providerId).url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...(PROVIDERS[PROVIDER_ID]?.headers || {}),
|
||||
...(PROVIDERS[providerId]?.headers || {}),
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
@@ -129,10 +129,18 @@ export async function getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificD
|
||||
});
|
||||
|
||||
const basePkg = refills[0] || accounts[0] || {};
|
||||
const plan = basePkg.PackageName || basePkg.SubProductName || "CodeBuddy CN";
|
||||
const plan = basePkg.PackageName || basePkg.SubProductName || "CodeBuddy";
|
||||
|
||||
return { plan, quotas };
|
||||
} catch (error) {
|
||||
return { message: `CodeBuddy CN error: ${error.message}` };
|
||||
return { message: `CodeBuddy (${providerId}) error: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificData, proxyOptions = null) {
|
||||
return getCodeBuddyUsage(PROVIDER_ID, accessToken, apiKey, providerSpecificData, proxyOptions);
|
||||
}
|
||||
|
||||
export async function getCodeBuddyIntlUsage(accessToken, apiKey, providerSpecificData, proxyOptions = null) {
|
||||
return getCodeBuddyUsage("codebuddy-intl", accessToken, apiKey, providerSpecificData, proxyOptions);
|
||||
}
|
||||
|
||||
@@ -161,7 +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-flash-agent',
|
||||
'gemini-3.6-flash-high',
|
||||
'gemini-3.6-flash-medium',
|
||||
'gemini-3.6-flash-low',
|
||||
'gemini-3.5-flash-low',
|
||||
'gemini-3.5-flash-extra-low',
|
||||
'gemini-pro-agent',
|
||||
@@ -169,10 +171,8 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
|
||||
'claude-sonnet-4-6',
|
||||
'claude-opus-4-6-thinking',
|
||||
'gpt-oss-120b-medium',
|
||||
'gemini-3-flash',
|
||||
// Image generation models
|
||||
'gemini-3.1-flash-image',
|
||||
'gemini-3-pro-image',
|
||||
];
|
||||
|
||||
for (const [modelKey, info] of Object.entries(data.models)) {
|
||||
|
||||
@@ -91,6 +91,24 @@ function resolvePlan(user, config) {
|
||||
return "Grok Build";
|
||||
}
|
||||
|
||||
// Display only; upstream remains authoritative for access and quota enforcement.
|
||||
function planFromAccessToken(accessToken) {
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url"));
|
||||
return {
|
||||
0: "Free",
|
||||
1: "SuperGrok",
|
||||
2: "X Basic",
|
||||
3: "X Premium",
|
||||
4: "X Premium Plus",
|
||||
5: "SuperGrok Heavy",
|
||||
6: "SuperGrok Lite",
|
||||
}[payload.tier] || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function makeQuota({ used, total, resetAt, unlimited = false }) {
|
||||
const safeTotal = Math.max(0, toFiniteNumber(total, 0));
|
||||
const safeUsed = Math.max(0, toFiniteNumber(used, 0));
|
||||
@@ -371,6 +389,7 @@ export async function getGrokCliUsage(accessToken, providerSpecificData = null,
|
||||
}
|
||||
|
||||
const parsed = parseGrokCliBilling(billing, user);
|
||||
parsed.plan = planFromAccessToken(accessToken) || parsed.plan;
|
||||
|
||||
if (!parsed.quotas || Object.keys(parsed.quotas).length === 0) {
|
||||
// Paid SuperGrok often returns cap=0 over REST but exposes the shared
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Misc usage handlers (Qwen, iFlow, Ollama, GLM, Vercel AI Gateway, Qoder)
|
||||
* Misc usage handlers (iFlow, Ollama, GLM, Vercel AI Gateway, Qoder)
|
||||
*/
|
||||
|
||||
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||
@@ -15,23 +15,6 @@ const GLM_QUOTA_URLS = {
|
||||
// Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings).
|
||||
const VERCEL_AI_GATEWAY_CREDITS_URL = U("vercel-ai-gateway").url;
|
||||
|
||||
/**
|
||||
* Qwen Usage
|
||||
*/
|
||||
export async function getQwenUsage(accessToken, providerSpecificData) {
|
||||
try {
|
||||
const resourceUrl = providerSpecificData?.resourceUrl;
|
||||
if (!resourceUrl) {
|
||||
return { message: "Qwen connected. No resource URL available." };
|
||||
}
|
||||
|
||||
// Qwen may have usage endpoint at resource URL
|
||||
return { message: "Qwen connected. Usage tracked per request." };
|
||||
} catch (error) {
|
||||
return { message: "Unable to fetch Qwen usage." };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* iFlow Usage
|
||||
*/
|
||||
@@ -46,23 +29,86 @@ export async function getIflowUsage(accessToken) {
|
||||
|
||||
/**
|
||||
* Ollama Cloud Usage
|
||||
* Ollama Cloud uses an API key from ollama.com/settings/keys
|
||||
* and has no public usage API — free tier has light usage limits (resets every 5h & 7d).
|
||||
* This returns an informational message with the plan details.
|
||||
* GET https://ollama.com/api/usage — session (5h) + weekly (7d) `usage` is a 0..1
|
||||
* ratio (1.0 = limit reached, e.g. weekly 100% used). No reset timestamp exposed.
|
||||
* POST https://ollama.com/api/me — plan label (fail-open).
|
||||
* Auth: Authorization: Bearer <apiKey>
|
||||
*/
|
||||
export async function getOllamaUsage(accessToken, providerSpecificData) {
|
||||
export async function getOllamaUsage(apiKey, providerSpecificData, proxyOptions = null) {
|
||||
if (!apiKey) {
|
||||
return { message: "Ollama Cloud API key not available." };
|
||||
}
|
||||
|
||||
try {
|
||||
// Ollama Cloud does not expose a public quota/usage API.
|
||||
// The provider is configured as noAuth with a notice explaining limits.
|
||||
// We return a graceful message so the UI shows a friendly state instead of an error.
|
||||
const plan = providerSpecificData?.plan || "Free";
|
||||
return {
|
||||
plan,
|
||||
message: "Ollama Cloud uses a free tier with light usage limits (resets every 5h & 7d). For detailed usage tracking, visit ollama.com/settings/keys.",
|
||||
quotas: [],
|
||||
};
|
||||
const response = await proxyAwareFetch("https://ollama.com/api/usage", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}, proxyOptions);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { message: "Ollama Cloud API key invalid or expired." };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { message: `Ollama Cloud usage API error (${response.status}).` };
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
return { message: "Ollama Cloud usage response was not JSON." };
|
||||
}
|
||||
|
||||
// Best-effort plan label from /api/me
|
||||
const me = await proxyAwareFetch("https://ollama.com/api/me", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
"Content-Length": "0",
|
||||
},
|
||||
}, proxyOptions).then((r) => (r.ok ? r.json() : null)).catch(() => null);
|
||||
|
||||
const planRaw = typeof me?.Plan === "string" ? me.Plan : "";
|
||||
const plan = planRaw
|
||||
? planRaw.charAt(0).toUpperCase() + planRaw.slice(1).toLowerCase()
|
||||
: "Ollama Cloud";
|
||||
|
||||
const limits = data?.limits && typeof data.limits === "object" ? data.limits : {};
|
||||
|
||||
// Ollama `usage` is a 0..1 ratio (1.0 = limit reached). Convert to a 0..100
|
||||
// bar. Do NOT set absolute `remaining` — QuotaTable reads remainingPercentage.
|
||||
function ratioQuota(usageRatio, resetAt = null) {
|
||||
const ratio = Math.max(0, Math.min(1, Number(usageRatio) || 0));
|
||||
const usedPct = Math.round(ratio * 100);
|
||||
return { used: usedPct, total: 100, remainingPercentage: 100 - usedPct, resetAt, unlimited: false };
|
||||
}
|
||||
|
||||
const sessionRaw = limits.session?.usage;
|
||||
const weeklyRaw = limits.weekly?.usage;
|
||||
const sessionNum = Number(sessionRaw);
|
||||
const weeklyNum = Number(weeklyRaw);
|
||||
const hasSession = sessionRaw !== undefined && sessionRaw !== null && !Number.isNaN(sessionNum);
|
||||
const hasWeekly = weeklyRaw !== undefined && weeklyRaw !== null && !Number.isNaN(weeklyNum);
|
||||
|
||||
if (!hasSession && !hasWeekly) {
|
||||
return {
|
||||
plan,
|
||||
message: "Ollama Cloud connected. No usage limits reported.",
|
||||
quotas: {},
|
||||
};
|
||||
}
|
||||
|
||||
const quotas = {};
|
||||
if (hasSession) quotas["Session (5h)"] = ratioQuota(sessionNum);
|
||||
if (hasWeekly) quotas["Weekly (7d)"] = ratioQuota(weeklyNum);
|
||||
|
||||
return { plan, quotas };
|
||||
} catch (error) {
|
||||
return { message: "Unable to fetch Ollama Cloud usage." };
|
||||
return { message: `Ollama Cloud error: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user