feat(opencode-zen): add OpenCode Zen (PAYG) provider with free-tier fingerprint, alias ocz
Multi-endpoint provider on https://opencode.ai/zen/v1 (openai / claude / openai-responses transports mirroring opencode-go) with 71 models across paid + free tiers. Free-tier fingerprint: opencode/1.18.x UA spoof, ses_-session header, built-in tool quartet, forced stream. Usage endpoint /zen/v1/usage wired into the dashboard.
This commit is contained in:
@@ -15,6 +15,7 @@ import { getGrokCliUsage } from "./usage/grok-cli.js";
|
||||
import { getKimiUsage } from "./usage/kimi.js";
|
||||
import { getDeepseekUsage } from "./usage/deepseek.js";
|
||||
import { getOpenCodeGoUsage } from "./usage/opencode-go.js";
|
||||
import { getOpenCodeZenUsage } from "./usage/opencode-zen.js";
|
||||
import { getGroqUsage } from "./usage/groq.js";
|
||||
import { getZedUsage } from "./usage/zed.js";
|
||||
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.js";
|
||||
@@ -59,6 +60,7 @@ const USAGE_HANDLERS = {
|
||||
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
|
||||
"opencode-go": (c) => getOpenCodeGoUsage(c.apiKey, c.proxyOptions),
|
||||
"opencode-zen": (c) => getOpenCodeZenUsage(c.apiKey, c.proxyOptions),
|
||||
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
|
||||
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
|
||||
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
|
||||
|
||||
107
open-sse/services/usage/opencode-zen.js
Normal file
107
open-sse/services/usage/opencode-zen.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* OpenCode Zen usage — GET https://opencode.ai/zen/v1/usage
|
||||
* Auth: Bearer <apiKey>
|
||||
*/
|
||||
|
||||
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
|
||||
import { parseResetTime, toFiniteNumber, U } from "./shared.js";
|
||||
|
||||
const USAGE_URL = U("opencode-zen").url;
|
||||
const QUOTA_NAMES = {
|
||||
rolling: "Rolling",
|
||||
weekly: "Weekly",
|
||||
monthly: "Monthly",
|
||||
};
|
||||
|
||||
function parsePercent(value) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getOpenCodeZenUsage(apiKey = null, proxyOptions = null) {
|
||||
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
||||
return {
|
||||
message: "OpenCode Zen API key not available. Add a key to view usage.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await proxyAwareFetch(
|
||||
USAGE_URL,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey.trim()}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
proxyOptions,
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
return {
|
||||
plan: "OpenCode Zen",
|
||||
message: "OpenCode Zen authentication failed. Check the API key.",
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
const error = await response.json().catch(() => null);
|
||||
const subscriptionRequired = error?.error?.type === "EntitlementError";
|
||||
return {
|
||||
plan: "OpenCode Zen",
|
||||
message: subscriptionRequired
|
||||
? "OpenCode Zen billing required for this API key."
|
||||
: "OpenCode Zen access forbidden for this API key.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
plan: "OpenCode Zen",
|
||||
message: `OpenCode Zen usage API error (${response.status}).`,
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!data?.usage || typeof data.usage !== "object") {
|
||||
return {
|
||||
plan: "OpenCode Zen",
|
||||
message: "OpenCode Zen usage response did not contain quota data.",
|
||||
};
|
||||
}
|
||||
|
||||
const quotas = {};
|
||||
for (const [period, name] of Object.entries(QUOTA_NAMES)) {
|
||||
const quota = data.usage[period];
|
||||
if (!quota || typeof quota !== "object") continue;
|
||||
const percent = parsePercent(quota.percent);
|
||||
if (percent === null) continue;
|
||||
const used = Math.max(0, Math.min(100, toFiniteNumber(percent, 0)));
|
||||
quotas[name] = {
|
||||
used,
|
||||
total: 100,
|
||||
remaining: 100 - used,
|
||||
remainingPercentage: 100 - used,
|
||||
resetAt: parseResetTime(quota.resetsAt),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (Object.keys(quotas).length === 0) {
|
||||
return {
|
||||
plan: "OpenCode Zen",
|
||||
message: "OpenCode Zen usage response did not contain valid quota data.",
|
||||
};
|
||||
}
|
||||
|
||||
return { plan: "OpenCode Zen", quotas };
|
||||
} catch (error) {
|
||||
return { message: `OpenCode Zen error: ${error.message}` };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user