feat(vercel-ai-gateway): support embeddings, images and credit usage

Extend Vercel AI Gateway beyond chat: add OpenAI-compatible embeddings
and image generation endpoints, credit balance fetch on the usage
dashboard, retry on 429, and models catalog fetcher.

Thinking/reasoning mapping is omitted pending a project-wide refactor.

Co-authored-by: Ngô Tấn Tài <tantai@newnol.io.vn>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ngô Tấn Tài
2026-06-13 10:54:51 +07:00
committed by decolua
parent d9b030011f
commit b33cbb0280
10 changed files with 164 additions and 2 deletions

View File

@@ -129,7 +129,8 @@ export const PROVIDERS = {
},
"vercel-ai-gateway": {
baseUrl: "https://ai-gateway.vercel.sh/v1/chat/completions",
format: "openai"
format: "openai",
retry: { 429: 2 }
},
glm: {
baseUrl: "https://api.z.ai/api/anthropic/v1/messages",

View File

@@ -6,6 +6,7 @@ import openaiCompatNode from "./openaiCompatNode.js";
const OPENAI_COMPAT_PROVIDERS = [
"openai", "openrouter", "mistral", "voyage-ai", "fireworks",
"together", "nebius", "github", "nvidia", "jina-ai",
"vercel-ai-gateway",
];
const ADAPTERS = {

View File

@@ -12,6 +12,7 @@ const ENDPOINTS = {
github: "https://models.github.ai/inference/embeddings",
nvidia: "https://integrate.api.nvidia.com/v1/embeddings",
"jina-ai": "https://api.jina.ai/v1/embeddings",
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v1/embeddings",
};
export default function createOpenAIEmbeddingAdapter(providerId) {

View File

@@ -17,6 +17,7 @@ const ADAPTERS = {
minimax: createOpenAIAdapter("minimax"),
openrouter: createOpenAIAdapter("openrouter"),
recraft: createOpenAIAdapter("recraft"),
"vercel-ai-gateway": createOpenAIAdapter("vercel-ai-gateway"),
xai: createOpenAIAdapter("xai"),
gemini,
codex,

View File

@@ -5,6 +5,7 @@ const ENDPOINTS = {
minimax: "https://api.minimaxi.com/v1/images/generations",
openrouter: "https://openrouter.ai/api/v1/images/generations",
recraft: "https://external.api.recraft.ai/v1/images/generations",
"vercel-ai-gateway": "https://ai-gateway.vercel.sh/v1/images/generations",
xai: "https://api.x.ai/v1/images/generations",
};

View File

@@ -30,6 +30,11 @@ const MINIMAX_USAGE_URLS = {
],
};
// Vercel AI Gateway credits endpoint
// Returns { balance: "95.50", total_used: "4.50" } (USD as decimal strings).
// Docs: https://vercel.com/docs/ai-gateway/usage
const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
// Antigravity API config (from Quotio)
const ANTIGRAVITY_CONFIG = {
quotaApiUrl: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
@@ -92,6 +97,8 @@ export async function getUsageForProvider(connection, proxyOptions = null) {
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey, provider, proxyOptions);
case "vercel-ai-gateway":
return await getVercelAiGatewayUsage(apiKey, proxyOptions);
default:
return { message: `Usage API not implemented for ${provider}` };
}
@@ -1203,6 +1210,89 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
return { message: lastErrorMessage ? `MiniMax connected. Unable to fetch usage: ${lastErrorMessage}` : "MiniMax connected. Unable to fetch usage." };
}
/**
* Vercel AI Gateway usage — credit balance for the API key
*
* Calls GET /v1/credits which returns:
* { "balance": "95.50", "total_used": "4.50" } (USD as decimal strings)
*
* We surface this as a single "Balance ($)" quota row so the existing
* QuotaTable / progress-bar UI can render it. used = total_used,
* total = balance + total_used (the original credit allotment), so the
* remaining percentage equals balance / total.
*
* Docs: https://vercel.com/docs/ai-gateway/usage
*/
async function getVercelAiGatewayUsage(apiKey, proxyOptions = null) {
if (!apiKey) {
return { message: "Vercel AI Gateway API key not available." };
}
try {
const response = await proxyAwareFetch(VERCEL_AI_GATEWAY_CREDITS_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
}, proxyOptions);
if (response.status === 401 || response.status === 403) {
return { message: "Vercel AI Gateway API key invalid or expired." };
}
if (!response.ok) {
const errorText = await response.text().catch(() => "");
const trimmed = errorText ? `: ${errorText.slice(0, 200)}` : "";
return { message: `Vercel AI Gateway credits API error (${response.status})${trimmed}` };
}
const data = await response.json();
// Vercel returns numeric strings; coerce safely.
const balance = Number(data?.balance) || 0;
const totalUsed = Number(data?.total_used) || 0;
// Vercel gives $5/month free credit. The API doesn't return the
// monthly allocation so we use the known constant as the denominator.
const MONTHLY_CREDIT = 5;
const remainingPercentage = (balance / MONTHLY_CREDIT) * 100;
if (balance <= 0 && totalUsed <= 0) {
return {
plan: "Pay-as-you-go",
message: "Vercel AI Gateway connected. No credit allocation found (BYOK or unfunded account).",
quotas: {},
};
}
// "Used (USD)": how much has been spent this month (no fixed cap → unlimited).
// "Remaining (USD)": balance remaining out of the $5 monthly allocation.
return {
plan: "Pay-as-you-go",
quotas: {
"Used (USD)": {
used: totalUsed,
total: 0,
remaining: 0,
remainingPercentage: 100,
unlimited: true,
},
"Remaining (USD)": {
used: balance,
total: MONTHLY_CREDIT,
remaining: balance,
remainingPercentage,
unlimited: false,
},
},
};
} catch (error) {
return { message: `Vercel AI Gateway error: ${error.message}` };
}
}
async function getQoderUsage(accessToken, proxyOptions = null) {
if (!accessToken) {
return { message: "Qoder usage unavailable: no access token" };