@leo000001/opencode-quota-sidebar 4.0.9 → 4.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +504 -446
- package/README.zh-CN.md +516 -458
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +123 -46
- package/dist/cli_render.d.ts +4 -4
- package/dist/cli_render.js +74 -74
- package/dist/cost.d.ts +21 -4
- package/dist/cost.js +493 -264
- package/dist/format.d.ts +5 -5
- package/dist/format.js +288 -287
- package/dist/history_usage.d.ts +15 -9
- package/dist/history_usage.js +28 -22
- package/dist/index.js +15 -1
- package/dist/models_dev_pricing.d.ts +6 -0
- package/dist/models_dev_pricing.js +226 -0
- package/dist/opencode_pricing.d.ts +14 -0
- package/dist/opencode_pricing.js +273 -0
- package/dist/storage.d.ts +3 -3
- package/dist/storage.js +27 -28
- package/dist/storage_parse.d.ts +1 -1
- package/dist/storage_parse.js +51 -45
- package/dist/storage_paths.d.ts +1 -0
- package/dist/storage_paths.js +26 -11
- package/dist/title_apply.d.ts +1 -17
- package/dist/title_apply.js +17 -48
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +29 -15
- package/dist/tui.d.ts +1 -1
- package/dist/tui.tsx +481 -471
- package/dist/tui_helpers.d.ts +5 -3
- package/dist/tui_helpers.js +62 -34
- package/dist/types.d.ts +8 -10
- package/dist/usage.d.ts +9 -6
- package/dist/usage.js +27 -21
- package/dist/usage_service.d.ts +8 -7
- package/dist/usage_service.js +261 -150
- package/package.json +1 -1
package/dist/history_usage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AssistantMessage, Message } from
|
|
2
|
-
import { type HistoryPeriod, type PeriodRange, type SinceSpec } from
|
|
3
|
-
import { type UsageSummary } from
|
|
4
|
-
import type { CacheCoverageMode, CachedSessionUsage, IncrementalCursor } from
|
|
1
|
+
import type { AssistantMessage, Message } from "@opencode-ai/sdk";
|
|
2
|
+
import { type HistoryPeriod, type PeriodRange, type SinceSpec } from "./period.js";
|
|
3
|
+
import { type UsageSummary } from "./usage.js";
|
|
4
|
+
import type { CacheCoverageMode, CachedSessionUsage, IncrementalCursor } from "./types.js";
|
|
5
5
|
export type HistoryDialogRow = {
|
|
6
6
|
label: string;
|
|
7
7
|
isCurrent: boolean;
|
|
@@ -37,19 +37,21 @@ export type HistoryPersistenceHint = {
|
|
|
37
37
|
ranges: UsageSummary[];
|
|
38
38
|
totalUsage: UsageSummary;
|
|
39
39
|
fullUsage: UsageSummary | undefined;
|
|
40
|
+
pricingFingerprint?: string;
|
|
41
|
+
pricingKeys?: string[];
|
|
40
42
|
persist: boolean;
|
|
41
43
|
cursor: IncrementalCursor | undefined;
|
|
42
44
|
missing: boolean;
|
|
43
45
|
loadFailed: boolean;
|
|
44
46
|
};
|
|
45
47
|
export type LoadMessagesPageResult = {
|
|
46
|
-
status:
|
|
48
|
+
status: "ok";
|
|
47
49
|
entries: MessageEntry[];
|
|
48
50
|
nextBefore?: string;
|
|
49
51
|
} | {
|
|
50
|
-
status:
|
|
52
|
+
status: "missing";
|
|
51
53
|
} | {
|
|
52
|
-
status:
|
|
54
|
+
status: "error";
|
|
53
55
|
};
|
|
54
56
|
type SessionEntry = {
|
|
55
57
|
sessionID: string;
|
|
@@ -74,10 +76,14 @@ export type ComputeHistoryUsageDeps = {
|
|
|
74
76
|
classifyCacheMode: (message: AssistantMessage, modelCostMap: Record<string, unknown>) => CacheCoverageMode;
|
|
75
77
|
/** Check whether a set of entries has at least one resolvable API-cost message. */
|
|
76
78
|
hasResolvableApiCostMessages: (entries: MessageEntry[], modelCostMap: Record<string, unknown>) => boolean;
|
|
79
|
+
/** Build a pricing fingerprint from provider/model keys and current rates. */
|
|
80
|
+
pricingFingerprintForKeys: (pricingKeys: string[], modelCostMap: Record<string, unknown>) => string;
|
|
81
|
+
/** Whether cached usage still matches current billing + pricing semantics. */
|
|
82
|
+
isUsageBillingCurrent: (cached: CachedSessionUsage | undefined, modelCostMap: Record<string, unknown>) => boolean;
|
|
77
83
|
/** Whether the cached usage for a session needs a full recompute. */
|
|
78
|
-
shouldTrackFullUsage: (cached: CachedSessionUsage | undefined,
|
|
84
|
+
shouldTrackFullUsage: (cached: CachedSessionUsage | undefined, modelCostMap: Record<string, unknown>) => boolean;
|
|
79
85
|
/** Whether cached usage needs recompute (for persistence decision). */
|
|
80
|
-
shouldRecomputeUsageCache: (cached: CachedSessionUsage,
|
|
86
|
+
shouldRecomputeUsageCache: (cached: CachedSessionUsage, pricingFingerprint: string | undefined) => boolean;
|
|
81
87
|
throwOnLoadFailure?: boolean;
|
|
82
88
|
};
|
|
83
89
|
/**
|
package/dist/history_usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseSince, periodRanges, } from
|
|
2
|
-
import { mapConcurrent } from
|
|
3
|
-
import { accumulateMessagesAcrossCompletedRanges, accumulateMessagesInCompletedRange, emptyUsageSummary, fromCachedSessionUsage, mergeCursorFromEntries, mergeUsage,
|
|
1
|
+
import { parseSince, periodRanges, } from "./period.js";
|
|
2
|
+
import { mapConcurrent } from "./helpers.js";
|
|
3
|
+
import { accumulateMessagesAcrossCompletedRanges, accumulateMessagesInCompletedRange, emptyUsageSummary, fromCachedSessionUsage, mergeCursorFromEntries, mergeUsage, } from "./usage.js";
|
|
4
4
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
5
5
|
const RANGE_USAGE_CONCURRENCY = 5;
|
|
6
6
|
// ── Core ───────────────────────────────────────────────────────────────────
|
|
@@ -11,7 +11,7 @@ function filterRangeSessions(sessions, startAt, endAt) {
|
|
|
11
11
|
if (session.state.dirty === true)
|
|
12
12
|
return true;
|
|
13
13
|
const lastMessageTime = session.state.cursor?.lastMessageTime;
|
|
14
|
-
if (typeof lastMessageTime ===
|
|
14
|
+
if (typeof lastMessageTime === "number" &&
|
|
15
15
|
Number.isFinite(lastMessageTime) &&
|
|
16
16
|
lastMessageTime < startAt) {
|
|
17
17
|
return false;
|
|
@@ -24,13 +24,13 @@ function pageLatestTimestamp(entries) {
|
|
|
24
24
|
for (const entry of entries) {
|
|
25
25
|
const info = entry.info;
|
|
26
26
|
const completed = info.time?.completed;
|
|
27
|
-
if (typeof completed ===
|
|
27
|
+
if (typeof completed === "number" && Number.isFinite(completed)) {
|
|
28
28
|
if (completed > latest)
|
|
29
29
|
latest = completed;
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
const created = info.time?.created;
|
|
33
|
-
if (typeof created ===
|
|
33
|
+
if (typeof created === "number" && Number.isFinite(created)) {
|
|
34
34
|
if (created > latest)
|
|
35
35
|
latest = created;
|
|
36
36
|
}
|
|
@@ -55,19 +55,19 @@ function rangeIndexForTimestamp(ranges, timestamp) {
|
|
|
55
55
|
}
|
|
56
56
|
return -1;
|
|
57
57
|
}
|
|
58
|
-
function canUseCurrentSessionCache(cached, session, ranges) {
|
|
58
|
+
function canUseCurrentSessionCache(cached, session, ranges, deps, modelCostMap) {
|
|
59
59
|
if (!cached)
|
|
60
60
|
return undefined;
|
|
61
|
-
if (cached
|
|
61
|
+
if (!deps.isUsageBillingCurrent(cached, modelCostMap))
|
|
62
62
|
return undefined;
|
|
63
63
|
if (session.state.dirty === true)
|
|
64
64
|
return undefined;
|
|
65
65
|
const lastMessageTime = session.state.cursor?.lastMessageTime;
|
|
66
|
-
if (typeof lastMessageTime !==
|
|
66
|
+
if (typeof lastMessageTime !== "number" ||
|
|
67
67
|
!Number.isFinite(lastMessageTime)) {
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
|
-
if (typeof session.state.createdAt !==
|
|
70
|
+
if (typeof session.state.createdAt !== "number" ||
|
|
71
71
|
!Number.isFinite(session.state.createdAt)) {
|
|
72
72
|
return undefined;
|
|
73
73
|
}
|
|
@@ -107,13 +107,12 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
107
107
|
const endAt = ranges[ranges.length - 1].endAt;
|
|
108
108
|
const sessions = filterRangeSessions(deps.sessions, startAt, endAt);
|
|
109
109
|
const modelCostMap = await deps.getModelCostMap();
|
|
110
|
-
const hasPricing = Object.keys(modelCostMap).length > 0;
|
|
111
110
|
if (sessions.length > 0) {
|
|
112
111
|
const fetched = await mapConcurrent(sessions, RANGE_USAGE_CONCURRENCY, async (session) => {
|
|
113
112
|
const cachedHit = canUseCurrentSessionCache(session.state.usage, session, rows.map((row) => ({
|
|
114
113
|
startAt: row.range.startAt,
|
|
115
114
|
endAt: row.range.endAt,
|
|
116
|
-
})));
|
|
115
|
+
})), deps, modelCostMap);
|
|
117
116
|
if (cachedHit) {
|
|
118
117
|
const rangeUsage = rows.map(() => emptyUsageSummary());
|
|
119
118
|
rangeUsage[cachedHit.index] = cachedHit.usage;
|
|
@@ -137,14 +136,14 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
137
136
|
};
|
|
138
137
|
const rangeUsage = rows.map(() => emptyUsageSummary());
|
|
139
138
|
const totalUsage = emptyUsageSummary();
|
|
140
|
-
const trackFullUsage = deps.shouldTrackFullUsage(session.state.usage,
|
|
139
|
+
const trackFullUsage = deps.shouldTrackFullUsage(session.state.usage, modelCostMap);
|
|
141
140
|
const fullUsage = trackFullUsage ? emptyUsageSummary() : undefined;
|
|
142
141
|
let cursor;
|
|
143
|
-
|
|
142
|
+
const pricingKeys = new Set();
|
|
144
143
|
let before;
|
|
145
144
|
while (true) {
|
|
146
145
|
const load = await deps.loadMessagesPage(session.sessionID, before);
|
|
147
|
-
if (load.status !==
|
|
146
|
+
if (load.status !== "ok") {
|
|
148
147
|
return {
|
|
149
148
|
sessionID: session.sessionID,
|
|
150
149
|
dateKey: session.dateKey,
|
|
@@ -153,8 +152,8 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
153
152
|
ranges: rows.map(() => emptyUsageSummary()),
|
|
154
153
|
totalUsage: emptyUsageSummary(),
|
|
155
154
|
fullUsage: undefined,
|
|
156
|
-
loadFailed: load.status ===
|
|
157
|
-
missing: load.status ===
|
|
155
|
+
loadFailed: load.status === "error",
|
|
156
|
+
missing: load.status === "missing",
|
|
158
157
|
persist: false,
|
|
159
158
|
cursor: undefined,
|
|
160
159
|
};
|
|
@@ -169,9 +168,11 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
169
168
|
if (fullUsage) {
|
|
170
169
|
accumulateMessagesInCompletedRange(fullUsage, entries, 0, Number.POSITIVE_INFINITY, usageOptions);
|
|
171
170
|
cursor = mergeCursorFromEntries(cursor, entries);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
for (const { info } of entries) {
|
|
172
|
+
if (info.role !== "assistant")
|
|
173
|
+
continue;
|
|
174
|
+
pricingKeys.add(`${info.providerID}:${info.modelID}`);
|
|
175
|
+
}
|
|
175
176
|
}
|
|
176
177
|
// `session.messages(limit, before)` pages from newest to oldest.
|
|
177
178
|
// When we are only computing range usage (no full-session persistence),
|
|
@@ -195,9 +196,12 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
195
196
|
if (totalUsage.assistantMessages > 0) {
|
|
196
197
|
totalUsage.sessionCount = 1;
|
|
197
198
|
}
|
|
199
|
+
const pricingFingerprint = fullUsage
|
|
200
|
+
? deps.pricingFingerprintForKeys([...pricingKeys], modelCostMap)
|
|
201
|
+
: undefined;
|
|
198
202
|
const shouldPersist = !!fullUsage &&
|
|
199
203
|
(!session.state.usage ||
|
|
200
|
-
deps.shouldRecomputeUsageCache(session.state.usage,
|
|
204
|
+
deps.shouldRecomputeUsageCache(session.state.usage, pricingFingerprint));
|
|
201
205
|
return {
|
|
202
206
|
sessionID: session.sessionID,
|
|
203
207
|
dateKey: session.dateKey,
|
|
@@ -206,6 +210,8 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
206
210
|
ranges: rangeUsage,
|
|
207
211
|
totalUsage,
|
|
208
212
|
fullUsage: shouldPersist ? fullUsage : undefined,
|
|
213
|
+
pricingFingerprint,
|
|
214
|
+
pricingKeys: fullUsage ? [...pricingKeys].sort() : undefined,
|
|
209
215
|
loadFailed: false,
|
|
210
216
|
missing: false,
|
|
211
217
|
persist: shouldPersist,
|
|
@@ -218,7 +224,7 @@ export async function computeHistoryUsage(deps, period, rawSince) {
|
|
|
218
224
|
if (item.dirty)
|
|
219
225
|
return true;
|
|
220
226
|
const lastMessageTime = item.lastMessageTime;
|
|
221
|
-
if (typeof lastMessageTime ===
|
|
227
|
+
if (typeof lastMessageTime === "number" && lastMessageTime < startAt) {
|
|
222
228
|
return false;
|
|
223
229
|
}
|
|
224
230
|
return true;
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
110
110
|
statePath,
|
|
111
111
|
client: input.client,
|
|
112
112
|
directory: input.directory,
|
|
113
|
+
worktree: input.worktree,
|
|
113
114
|
persistence: {
|
|
114
115
|
markDirty,
|
|
115
116
|
scheduleSave,
|
|
@@ -120,6 +121,15 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
120
121
|
const summarizeSessionUsageForDisplay = usageService.summarizeSessionUsageForDisplay;
|
|
121
122
|
const summarizeForTool = usageService.summarizeForTool;
|
|
122
123
|
const summarizeHistoryForTool = usageService.summarizeHistoryUsage;
|
|
124
|
+
const listCurrentProviderIDsSafe = async () => {
|
|
125
|
+
return listCurrentProviderIDs({
|
|
126
|
+
client: input.client,
|
|
127
|
+
directory: input.directory,
|
|
128
|
+
}).catch((error) => {
|
|
129
|
+
debug(`listCurrentProviderIDs failed: ${String(error)}`);
|
|
130
|
+
return undefined;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
123
133
|
const activeSessionUntil = new Map();
|
|
124
134
|
let lastTuiSessionID;
|
|
125
135
|
const markSessionActive = (sessionID, now = Date.now()) => {
|
|
@@ -181,6 +191,7 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
181
191
|
getTitleView: () => resolveTitleView({ config }),
|
|
182
192
|
getQuotaSnapshots,
|
|
183
193
|
summarizeSessionUsageForDisplay,
|
|
194
|
+
listCurrentProviderIDs: listCurrentProviderIDsSafe,
|
|
184
195
|
scheduleParentRefreshIfSafe,
|
|
185
196
|
isSessionActive,
|
|
186
197
|
restoreConcurrency: RESTORE_TITLE_CONCURRENCY,
|
|
@@ -269,7 +280,10 @@ export async function QuotaSidebarPlugin(input) {
|
|
|
269
280
|
}
|
|
270
281
|
expiryToastInflight.add(sessionID);
|
|
271
282
|
try {
|
|
272
|
-
const
|
|
283
|
+
const allowedProviderIDs = await listCurrentProviderIDsSafe();
|
|
284
|
+
if (!allowedProviderIDs || allowedProviderIDs.size === 0)
|
|
285
|
+
return;
|
|
286
|
+
const quotas = await getQuotaSnapshots([...allowedProviderIDs]);
|
|
273
287
|
const nowMs = Date.now();
|
|
274
288
|
const expiryLines = quotas
|
|
275
289
|
.filter((item) => item.status === 'ok')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ModelCostRates } from "./cost.js";
|
|
2
|
+
import type { OpenCodePricingModel } from "./opencode_pricing.js";
|
|
3
|
+
export declare function modelsDevHasProvider(providerID: string): boolean;
|
|
4
|
+
export declare function loadModelsDevPricingModels(requests: OpenCodePricingModel[]): Promise<OpenCodePricingModel[]>;
|
|
5
|
+
export declare function clearModelsDevPricingCache(): void;
|
|
6
|
+
export declare function modelsDevCostToRates(cost: OpenCodePricingModel["cost"]): ModelCostRates | undefined;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { debug, mapConcurrent } from "./helpers.js";
|
|
2
|
+
import { canonicalPricingProviderID, modelCostLookupKeys, } from "./cost.js";
|
|
3
|
+
const MODELS_DEV_RAW_BASE_URL = "https://raw.githubusercontent.com/anomalyco/models.dev/dev/providers";
|
|
4
|
+
const MODELS_DEV_TIMEOUT_MS = 10_000;
|
|
5
|
+
const MODELS_DEV_POSITIVE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
6
|
+
const MODELS_DEV_NEGATIVE_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
const MODELS_DEV_PARSE_MISS_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
const MODELS_DEV_REQUEST_CONCURRENCY = 4;
|
|
9
|
+
const fileCache = new Map();
|
|
10
|
+
function modelsDevProviderDirs(providerID) {
|
|
11
|
+
const canonical = canonicalPricingProviderID(providerID);
|
|
12
|
+
if (canonical === "openai")
|
|
13
|
+
return ["openai"];
|
|
14
|
+
if (canonical === "anthropic")
|
|
15
|
+
return ["anthropic"];
|
|
16
|
+
if (canonical === "moonshotai")
|
|
17
|
+
return ["moonshotai", "kimi-for-coding"];
|
|
18
|
+
if (canonical === "minimax") {
|
|
19
|
+
return ["minimax-cn-coding-plan", "minimax-coding-plan", "minimax"];
|
|
20
|
+
}
|
|
21
|
+
if (canonical === "zhipu") {
|
|
22
|
+
return ["zai-coding-plan", "zai", "zhipuai-coding-plan", "zhipuai"];
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
function stripInlineComment(line) {
|
|
27
|
+
let inString = false;
|
|
28
|
+
let escaping = false;
|
|
29
|
+
let output = "";
|
|
30
|
+
for (const char of line) {
|
|
31
|
+
if (char === '"' && !escaping)
|
|
32
|
+
inString = !inString;
|
|
33
|
+
if (char === "#" && !inString)
|
|
34
|
+
break;
|
|
35
|
+
output += char;
|
|
36
|
+
if (escaping) {
|
|
37
|
+
escaping = false;
|
|
38
|
+
}
|
|
39
|
+
else if (char === "\\") {
|
|
40
|
+
escaping = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return output.trim();
|
|
44
|
+
}
|
|
45
|
+
function parseTomlNumber(raw) {
|
|
46
|
+
const parsed = Number(raw.replace(/_/g, "").trim());
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
+
}
|
|
49
|
+
function parseModelsDevCost(text) {
|
|
50
|
+
const rates = {};
|
|
51
|
+
const contextRates = {};
|
|
52
|
+
let section = "";
|
|
53
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
54
|
+
const line = stripInlineComment(rawLine);
|
|
55
|
+
if (!line)
|
|
56
|
+
continue;
|
|
57
|
+
const sectionMatch = /^\[([^\]]+)\]$/.exec(line);
|
|
58
|
+
if (sectionMatch) {
|
|
59
|
+
section = sectionMatch[1] || "";
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const kvMatch = /^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/.exec(line);
|
|
63
|
+
if (!kvMatch)
|
|
64
|
+
continue;
|
|
65
|
+
const key = kvMatch[1];
|
|
66
|
+
const value = parseTomlNumber(kvMatch[2] || "");
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
if (section === "cost") {
|
|
70
|
+
rates[key] = value;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (section === "cost.context_over_200k") {
|
|
74
|
+
contextRates[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const hasBase = (rates.input || 0) > 0 ||
|
|
78
|
+
(rates.output || 0) > 0 ||
|
|
79
|
+
(rates.cache_read || 0) > 0 ||
|
|
80
|
+
(rates.cache_write || 0) > 0;
|
|
81
|
+
if (!hasBase)
|
|
82
|
+
return undefined;
|
|
83
|
+
const hasContext = (contextRates.input || 0) > 0 ||
|
|
84
|
+
(contextRates.output || 0) > 0 ||
|
|
85
|
+
(contextRates.cache_read || 0) > 0 ||
|
|
86
|
+
(contextRates.cache_write || 0) > 0;
|
|
87
|
+
return {
|
|
88
|
+
input: rates.input || 0,
|
|
89
|
+
output: rates.output || 0,
|
|
90
|
+
cache_read: rates.cache_read || 0,
|
|
91
|
+
cache_write: rates.cache_write || 0,
|
|
92
|
+
...(hasContext
|
|
93
|
+
? {
|
|
94
|
+
context_over_200k: {
|
|
95
|
+
input: contextRates.input || 0,
|
|
96
|
+
output: contextRates.output || 0,
|
|
97
|
+
cache_read: contextRates.cache_read || 0,
|
|
98
|
+
cache_write: contextRates.cache_write || 0,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function modelsDevModelCandidates(model) {
|
|
105
|
+
const candidates = new Set();
|
|
106
|
+
for (const stem of [model.modelID, model.modelKey].filter((value) => Boolean(value))) {
|
|
107
|
+
for (const key of modelCostLookupKeys(model.providerID, stem)) {
|
|
108
|
+
const separator = key.indexOf(":");
|
|
109
|
+
const candidate = separator >= 0 ? key.slice(separator + 1) : key;
|
|
110
|
+
if (!candidate || candidate.includes("/"))
|
|
111
|
+
continue;
|
|
112
|
+
candidates.add(candidate);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...candidates];
|
|
116
|
+
}
|
|
117
|
+
async function fetchModelsDevCost(url) {
|
|
118
|
+
const cached = fileCache.get(url);
|
|
119
|
+
if (cached && cached.expiresAt > Date.now())
|
|
120
|
+
return cached.cost;
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timeout = setTimeout(() => controller.abort(), MODELS_DEV_TIMEOUT_MS);
|
|
123
|
+
timeout.unref?.();
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
126
|
+
if (response.status === 404) {
|
|
127
|
+
fileCache.set(url, {
|
|
128
|
+
expiresAt: Date.now() + MODELS_DEV_NEGATIVE_TTL_MS,
|
|
129
|
+
cost: null,
|
|
130
|
+
});
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
debug(`models.dev fetch failed ${response.status} for ${url}`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const parsed = parseModelsDevCost(await response.text());
|
|
138
|
+
fileCache.set(url, {
|
|
139
|
+
expiresAt: Date.now() +
|
|
140
|
+
(parsed ? MODELS_DEV_POSITIVE_TTL_MS : MODELS_DEV_PARSE_MISS_TTL_MS),
|
|
141
|
+
cost: parsed || null,
|
|
142
|
+
});
|
|
143
|
+
return parsed || null;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
debug(`models.dev fetch error for ${url}: ${String(error)}`);
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function modelsDevHasProvider(providerID) {
|
|
154
|
+
return modelsDevProviderDirs(providerID).length > 0;
|
|
155
|
+
}
|
|
156
|
+
export async function loadModelsDevPricingModels(requests) {
|
|
157
|
+
const resolved = new Map();
|
|
158
|
+
const entries = await mapConcurrent(requests, MODELS_DEV_REQUEST_CONCURRENCY, async (request) => {
|
|
159
|
+
const dirs = modelsDevProviderDirs(request.providerID);
|
|
160
|
+
if (dirs.length === 0)
|
|
161
|
+
return undefined;
|
|
162
|
+
const candidates = modelsDevModelCandidates(request);
|
|
163
|
+
if (candidates.length === 0)
|
|
164
|
+
return undefined;
|
|
165
|
+
let found = undefined;
|
|
166
|
+
for (const dir of dirs) {
|
|
167
|
+
for (const candidate of candidates) {
|
|
168
|
+
const url = `${MODELS_DEV_RAW_BASE_URL}/${dir}/models/${candidate}.toml`;
|
|
169
|
+
const cost = await fetchModelsDevCost(url);
|
|
170
|
+
if (cost === undefined || cost === null)
|
|
171
|
+
continue;
|
|
172
|
+
found = cost;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
if (found)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (!found)
|
|
179
|
+
return undefined;
|
|
180
|
+
return {
|
|
181
|
+
key: `${request.providerID}:${request.modelID}`,
|
|
182
|
+
model: {
|
|
183
|
+
providerKey: request.providerKey,
|
|
184
|
+
providerID: request.providerID,
|
|
185
|
+
modelID: request.modelID,
|
|
186
|
+
modelKey: request.modelKey,
|
|
187
|
+
cost: found,
|
|
188
|
+
options: request.options,
|
|
189
|
+
headers: request.headers,
|
|
190
|
+
api: request.api,
|
|
191
|
+
limit: request.limit,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (!entry)
|
|
197
|
+
continue;
|
|
198
|
+
resolved.set(entry.key, entry.model);
|
|
199
|
+
}
|
|
200
|
+
return [...resolved.values()];
|
|
201
|
+
}
|
|
202
|
+
export function clearModelsDevPricingCache() {
|
|
203
|
+
fileCache.clear();
|
|
204
|
+
}
|
|
205
|
+
export function modelsDevCostToRates(cost) {
|
|
206
|
+
if (!cost || typeof cost !== "object")
|
|
207
|
+
return undefined;
|
|
208
|
+
const record = cost;
|
|
209
|
+
const context = record.context_over_200k && typeof record.context_over_200k === "object"
|
|
210
|
+
? record.context_over_200k
|
|
211
|
+
: undefined;
|
|
212
|
+
return {
|
|
213
|
+
input: Number(record.input || 0),
|
|
214
|
+
output: Number(record.output || 0),
|
|
215
|
+
cacheRead: Number(record.cache_read || 0),
|
|
216
|
+
cacheWrite: Number(record.cache_write || 0),
|
|
217
|
+
contextOver200k: context
|
|
218
|
+
? {
|
|
219
|
+
input: Number(context.input || 0),
|
|
220
|
+
output: Number(context.output || 0),
|
|
221
|
+
cacheRead: Number(context.cache_read || 0),
|
|
222
|
+
cacheWrite: Number(context.cache_write || 0),
|
|
223
|
+
}
|
|
224
|
+
: undefined,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type OpenCodePricingModel = {
|
|
2
|
+
providerKey?: string;
|
|
3
|
+
providerID: string;
|
|
4
|
+
modelID: string;
|
|
5
|
+
modelKey?: string;
|
|
6
|
+
cost?: unknown;
|
|
7
|
+
options?: Record<string, unknown>;
|
|
8
|
+
headers?: Record<string, unknown>;
|
|
9
|
+
api?: Record<string, unknown>;
|
|
10
|
+
limit?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseJsonc(text: string): unknown;
|
|
13
|
+
export declare function extractOpenCodePricingModels(config: unknown): OpenCodePricingModel[];
|
|
14
|
+
export declare function loadOpenCodePricingModels(paths: string[]): Promise<OpenCodePricingModel[]>;
|