@qadanm/opencode-smart-router 0.0.0-stage → 0.6.1
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/CHANGELOG.md +286 -0
- package/CONTRIBUTING.md +103 -0
- package/LICENSE +21 -0
- package/README.md +425 -2
- package/SECURITY.md +174 -0
- package/config.example.json +107 -0
- package/dist/npm/index.js +40 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +151 -0
- package/docs/LIMITATIONS.md +230 -0
- package/docs/MODELS.md +124 -0
- package/docs/OPENDESIGN-INTEGRATION.md +635 -0
- package/docs/QA.md +142 -0
- package/docs/SHARED-CORE.md +114 -0
- package/install +75 -0
- package/package.json +94 -4
- package/scripts/install.sh +324 -0
- package/scripts/uninstall.sh +91 -0
- package/src/allowance.ts +135 -0
- package/src/core/classify/classifier.ts +305 -0
- package/src/core/config-writer.ts +167 -0
- package/src/core/config.ts +1047 -0
- package/src/core/context/strategy.ts +188 -0
- package/src/core/context-health.ts +39 -0
- package/src/core/failures.ts +398 -0
- package/src/core/history/opencode.ts +173 -0
- package/src/core/models/protocol.ts +22 -0
- package/src/core/models/registry.ts +204 -0
- package/src/core/models/seed.ts +781 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +487 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +630 -0
- package/src/core/providers/enablement.ts +143 -0
- package/src/core/providers/opendesign-credits.ts +206 -0
- package/src/core/providers/opendesign-models.ts +654 -0
- package/src/core/providers/opendesign.ts +474 -0
- package/src/core/quota/calibration.ts +166 -0
- package/src/core/quota/commandcode.ts +471 -0
- package/src/core/quota/credentials.ts +490 -0
- package/src/core/quota/ledger.ts +1304 -0
- package/src/core/quota/opencode-go.ts +148 -0
- package/src/core/quota/opendesign.ts +1043 -0
- package/src/core/quota/reconcile.ts +155 -0
- package/src/core/quota/report.ts +261 -0
- package/src/core/quota/service.ts +273 -0
- package/src/core/reprompt.ts +48 -0
- package/src/core/routing/engine.ts +978 -0
- package/src/core/types.ts +866 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1675 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +33 -0
- package/src/rpc.ts +204 -0
- package/src/server/containment.ts +131 -0
- package/src/server/controller.ts +3405 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +701 -0
- package/src/tui/index.tsx +168 -0
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreditPoolSnapshot,
|
|
3
|
+
ProviderQuotaSnapshot,
|
|
4
|
+
ProviderSettings,
|
|
5
|
+
QuotaWindow,
|
|
6
|
+
UsageEvent,
|
|
7
|
+
} from "../types.js"
|
|
8
|
+
import { clamp, describePlan, pct, redact } from "../util.js"
|
|
9
|
+
import {
|
|
10
|
+
OPENDESIGN_DEFAULT_CONTROL_BASE_URL,
|
|
11
|
+
OPENDESIGN_USER_AGENT,
|
|
12
|
+
failureClassForStatus,
|
|
13
|
+
normalizeOpenDesignControlBaseURL,
|
|
14
|
+
openDesignControlEndpoint,
|
|
15
|
+
type FetchLike,
|
|
16
|
+
} from "../providers/opendesign.js"
|
|
17
|
+
import { OPEN_DESIGN_MODEL_BY_ID } from "../providers/opendesign-models.js"
|
|
18
|
+
import { OPENDESIGN_DESIGN_PLAN_POOL, OPENDESIGN_PLAN_MODEL_POOL, isEscalationOnlyPool, resolvePlan } from "../plans/catalog.js"
|
|
19
|
+
import { creditMultiplierFor } from "../providers/opendesign-credits.js"
|
|
20
|
+
import { calibrationSamples, effectiveMarginPct } from "./calibration.js"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* OpenDesign quota integration.
|
|
24
|
+
*
|
|
25
|
+
* OpenDesign publishes no quota API for an inference key. Re-verified on
|
|
26
|
+
* 2026-10-30 against the live gateway: `GET /v1/models` returns 200 with the
|
|
27
|
+
* same key, while `/v1/usage`, `/v1/me`, `/v1/credits`, `/v1/billing`,
|
|
28
|
+
* `/v1/account`, `/v1/subscription`, `/v1/limits` and `/v1/quota` all return
|
|
29
|
+
* 404. The key is valid and authorized for inference; there is simply no usage
|
|
30
|
+
* surface for it. The account-control host rejects an inference key with HTTP
|
|
31
|
+
* 401. The upstream feature request for a key-readable usage endpoint is still
|
|
32
|
+
* open (nexu-io/open-design#8674), as is the control-key lifecycle request
|
|
33
|
+
* (#8870).
|
|
34
|
+
*
|
|
35
|
+
* Three states are therefore supported:
|
|
36
|
+
*
|
|
37
|
+
* - `verified` A control key was explicitly configured, so the account's own
|
|
38
|
+
* coding-plan usage windows are read directly.
|
|
39
|
+
* - `estimated` Only the inference key is available, so the router models its
|
|
40
|
+
* own credit consumption against the published plan caps.
|
|
41
|
+
* - `unknown` Nothing can be established safely.
|
|
42
|
+
*
|
|
43
|
+
* The estimated state is a lower bound: usage from the OpenDesign desktop app
|
|
44
|
+
* or any other client on the same subscription is invisible to the router. A
|
|
45
|
+
* configurable margin is applied and the uncertainty is reported, never hidden.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export type OpenDesignQuotaState =
|
|
49
|
+
| "verified"
|
|
50
|
+
| "estimated"
|
|
51
|
+
| "stale"
|
|
52
|
+
| "unknown"
|
|
53
|
+
| "exhausted"
|
|
54
|
+
| "unavailable"
|
|
55
|
+
|
|
56
|
+
export interface OpenDesignQuotaInput {
|
|
57
|
+
readonly settings: ProviderSettings
|
|
58
|
+
readonly apiKey?: string
|
|
59
|
+
readonly controlKey?: string
|
|
60
|
+
readonly controlBaseURL?: string
|
|
61
|
+
readonly usage: ReadonlyArray<UsageEvent>
|
|
62
|
+
readonly fetchImpl?: FetchLike
|
|
63
|
+
readonly now?: () => number
|
|
64
|
+
readonly multiplierOverrides?: Readonly<Record<string, number>>
|
|
65
|
+
/** Credits per USD as reported by the account. Defaults to the observed value. */
|
|
66
|
+
readonly creditsPerUsd?: number
|
|
67
|
+
/** Extra margin applied to locally observed usage, in percent. */
|
|
68
|
+
readonly uncertaintyMarginPct?: number
|
|
69
|
+
readonly timeoutMs?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OpenDesignQuotaResult {
|
|
73
|
+
readonly snapshot: ProviderQuotaSnapshot
|
|
74
|
+
readonly state: OpenDesignQuotaState
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Credits per USD observed from the OpenDesign account surface. */
|
|
78
|
+
export const OPENDESIGN_DEFAULT_CREDITS_PER_USD = 10_000
|
|
79
|
+
|
|
80
|
+
export const OPENDESIGN_DEFAULT_UNCERTAINTY_MARGIN_PCT = 20
|
|
81
|
+
|
|
82
|
+
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000
|
|
83
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000
|
|
84
|
+
/**
|
|
85
|
+
* The widest window this module models.
|
|
86
|
+
*
|
|
87
|
+
* Exported because the caller has to hand back at least this much history for
|
|
88
|
+
* the estimate to be complete. Keeping the number in one place stops the two
|
|
89
|
+
* from drifting into a silent under-count.
|
|
90
|
+
*/
|
|
91
|
+
export const OPENDESIGN_MONTH_MS = 30 * 24 * 60 * 60 * 1000
|
|
92
|
+
const MONTH_MS = OPENDESIGN_MONTH_MS
|
|
93
|
+
|
|
94
|
+
const unknownWindow: QuotaWindow = { usedPct: 0, remainingPct: 100, source: "unknown" }
|
|
95
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
96
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
97
|
+
|
|
98
|
+
const numberOr = (value: unknown): number | undefined => {
|
|
99
|
+
if (typeof value === "number" && Number.isFinite(value)) return value
|
|
100
|
+
if (typeof value === "string") {
|
|
101
|
+
const parsed = Number(value)
|
|
102
|
+
if (Number.isFinite(parsed)) return parsed
|
|
103
|
+
}
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const timeValue = (value: unknown): number | undefined => {
|
|
108
|
+
if (typeof value === "number" && Number.isFinite(value)) return value
|
|
109
|
+
if (typeof value === "string") {
|
|
110
|
+
const parsed = Date.parse(value)
|
|
111
|
+
if (Number.isFinite(parsed)) return parsed
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Estimate the Design Plan credits consumed by one request.
|
|
118
|
+
*
|
|
119
|
+
* This converts token counts into credits using the curated vendor list price
|
|
120
|
+
* for the model and the published Design Plan multiplier. It is an estimate of
|
|
121
|
+
* the underlying usage value, never an observed provider charge.
|
|
122
|
+
*
|
|
123
|
+
* `multiplierApplies` is false for a request funded from plan model credits:
|
|
124
|
+
* OpenDesign documents that Design Plan model multipliers do not
|
|
125
|
+
* automatically apply to that second pool, so applying one would understate
|
|
126
|
+
* its real cost.
|
|
127
|
+
*/
|
|
128
|
+
export const estimateDesignPlanCredits = (input: {
|
|
129
|
+
readonly modelID: string
|
|
130
|
+
readonly input: number
|
|
131
|
+
readonly output: number
|
|
132
|
+
readonly cacheRead: number
|
|
133
|
+
readonly cacheWrite?: number
|
|
134
|
+
readonly multiplierOverrides?: Readonly<Record<string, number>>
|
|
135
|
+
readonly now?: number
|
|
136
|
+
readonly multiplierApplies?: boolean
|
|
137
|
+
}): { creditsUsd: number; multiplier: number; evidence: string } => {
|
|
138
|
+
const spec = OPEN_DESIGN_MODEL_BY_ID.get(input.modelID)
|
|
139
|
+
const tier = spec?.cost?.[0]
|
|
140
|
+
const multiplier = creditMultiplierFor(input.modelID, {
|
|
141
|
+
now: input.now,
|
|
142
|
+
...(input.multiplierOverrides ? { overrides: input.multiplierOverrides } : {}),
|
|
143
|
+
})
|
|
144
|
+
const applies = input.multiplierApplies !== false
|
|
145
|
+
const effectiveMultiplier = applies ? multiplier.multiplier : 1
|
|
146
|
+
if (!tier) {
|
|
147
|
+
return { creditsUsd: 0, multiplier: effectiveMultiplier, evidence: "no-price" }
|
|
148
|
+
}
|
|
149
|
+
const price = (tier.input * Math.max(0, input.input) +
|
|
150
|
+
tier.output * Math.max(0, input.output) +
|
|
151
|
+
(tier.cache?.read ?? 0) * Math.max(0, input.cacheRead) +
|
|
152
|
+
(tier.cache?.write ?? 0) * Math.max(0, input.cacheWrite ?? 0)) /
|
|
153
|
+
1_000_000
|
|
154
|
+
return {
|
|
155
|
+
creditsUsd: Math.max(0, price) * effectiveMultiplier,
|
|
156
|
+
multiplier: effectiveMultiplier,
|
|
157
|
+
evidence: applies ? multiplier.evidence : "configured",
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface LocalWindowTotals {
|
|
162
|
+
readonly rollingUsd: number
|
|
163
|
+
readonly weeklyUsd: number
|
|
164
|
+
readonly monthlyUsd: number
|
|
165
|
+
readonly observedRequests: number
|
|
166
|
+
/** Credits per pool, so the two allowances are never conflated. */
|
|
167
|
+
readonly byPool: Readonly<Record<string, {
|
|
168
|
+
readonly rollingUsd: number
|
|
169
|
+
readonly weeklyUsd: number
|
|
170
|
+
readonly monthlyUsd: number
|
|
171
|
+
}>>
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Estimate local usage, split by the pool that funded each request.
|
|
176
|
+
*
|
|
177
|
+
* A Design Plan model is billed at its Design Plan multiplier while that pool
|
|
178
|
+
* has credit, then at the plain rate from plan model credits. The estimate
|
|
179
|
+
* applies the pool that the model draws from first, which is the correct
|
|
180
|
+
* attribution for forecasting: it asks "which allowance is this consuming?"
|
|
181
|
+
* rather than "what is the cheapest rate this model ever gets?".
|
|
182
|
+
*/
|
|
183
|
+
/**
|
|
184
|
+
* Start of the current fixed seven-day Design Plan window.
|
|
185
|
+
*
|
|
186
|
+
* OpenDesign anchors the window to the subscription, which the router cannot
|
|
187
|
+
* read. It can, however, be told: a pasted "resets in 0d 22h" gives the end,
|
|
188
|
+
* and the start is exactly seven days before it. Without that anchor the best
|
|
189
|
+
* available assumption is a window ending seven days from now, which is wrong
|
|
190
|
+
* for everyone but no more wrong than the rolling window it replaces.
|
|
191
|
+
*/
|
|
192
|
+
export const weeklyWindowStart = (now: number, resetsAt?: number): number => {
|
|
193
|
+
if (resetsAt !== undefined && Number.isFinite(resetsAt) && resetsAt > now) {
|
|
194
|
+
return resetsAt - WEEK_MS
|
|
195
|
+
}
|
|
196
|
+
return now - WEEK_MS
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const localTotals = (
|
|
200
|
+
usage: ReadonlyArray<UsageEvent>,
|
|
201
|
+
providerID: string,
|
|
202
|
+
now: number,
|
|
203
|
+
marginPct: number,
|
|
204
|
+
plan: ReturnType<typeof resolvePlan>,
|
|
205
|
+
multiplierOverrides?: Readonly<Record<string, number>>,
|
|
206
|
+
/** When the current seven-day window resets, when the caller knows it. */
|
|
207
|
+
windowResetsAt?: number,
|
|
208
|
+
): LocalWindowTotals => {
|
|
209
|
+
/**
|
|
210
|
+
* Window boundaries.
|
|
211
|
+
*
|
|
212
|
+
* The seven-day Design Plan window is FIXED: the vendor documents that it
|
|
213
|
+
* starts when the subscription starts and repeats every seven days, so
|
|
214
|
+
* "Wednesday 10:00 to the following Wednesday 10:00" — not "the last seven
|
|
215
|
+
* days". Bucketing by `now - 7 days` is a rolling window and overstates the
|
|
216
|
+
* current window's usage by including events from the previous one, which is
|
|
217
|
+
* the difference between "40% used" and "60% used" for most of a week.
|
|
218
|
+
*
|
|
219
|
+
* The anchor comes from the caller when known (a pasted reset time), because
|
|
220
|
+
* the true start is a property of the subscription, not of the clock.
|
|
221
|
+
*/
|
|
222
|
+
const rollingCutoff = now - FIVE_HOURS_MS
|
|
223
|
+
const weekCutoff = weeklyWindowStart(now, windowResetsAt)
|
|
224
|
+
const monthCutoff = now - MONTH_MS
|
|
225
|
+
let rollingUsd = 0
|
|
226
|
+
let weeklyUsd = 0
|
|
227
|
+
let monthlyUsd = 0
|
|
228
|
+
let observedRequests = 0
|
|
229
|
+
const pools: Record<string, { rollingUsd: number; weeklyUsd: number; monthlyUsd: number }> = {}
|
|
230
|
+
const bucket = (poolID: string) =>
|
|
231
|
+
(pools[poolID] ??= { rollingUsd: 0, weeklyUsd: 0, monthlyUsd: 0 })
|
|
232
|
+
|
|
233
|
+
for (const event of usage) {
|
|
234
|
+
if (event.providerID !== providerID) continue
|
|
235
|
+
const designPlan = plan
|
|
236
|
+
? plan.pools.find((pool) => pool.id === OPENDESIGN_DESIGN_PLAN_POOL)
|
|
237
|
+
: undefined
|
|
238
|
+
const planModels = plan
|
|
239
|
+
? plan.pools.find((pool) => pool.id === OPENDESIGN_PLAN_MODEL_POOL)
|
|
240
|
+
: undefined
|
|
241
|
+
const inDesignPlan = Boolean(designPlan) && !isEscalationOnlyPool(plan!, event.modelID)
|
|
242
|
+
const poolID = inDesignPlan
|
|
243
|
+
? OPENDESIGN_DESIGN_PLAN_POOL
|
|
244
|
+
: planModels
|
|
245
|
+
? OPENDESIGN_PLAN_MODEL_POOL
|
|
246
|
+
: OPENDESIGN_DESIGN_PLAN_POOL
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Prefer what the request actually cost.
|
|
250
|
+
*
|
|
251
|
+
* The host reports a real charge per step and it is stored in the ledger, so
|
|
252
|
+
* recomputing the price from a static table is re-deriving a number that
|
|
253
|
+
* arrived for free — and it drifts whenever the charge does not match the
|
|
254
|
+
* model: a step funded from plan model credits after the Design Plan
|
|
255
|
+
* allowance is spent is charged at the plain rate, while the table still
|
|
256
|
+
* applies the Design Plan multiplier.
|
|
257
|
+
*
|
|
258
|
+
* A reported cost of zero is not a charge, though: some hosts omit pricing,
|
|
259
|
+
* and treating "unknown" as "free" would make the estimate forget real
|
|
260
|
+
* usage. So zero falls back to the table.
|
|
261
|
+
*/
|
|
262
|
+
const reported = event.usd > 0 ? event.usd : undefined
|
|
263
|
+
const credits =
|
|
264
|
+
reported !== undefined && poolID === OPENDESIGN_DESIGN_PLAN_POOL
|
|
265
|
+
? reported * creditMultiplierFor(event.modelID, { now, ...(multiplierOverrides ? { overrides: multiplierOverrides } : {}) }).multiplier
|
|
266
|
+
: reported !== undefined
|
|
267
|
+
? reported
|
|
268
|
+
: estimateDesignPlanCredits({
|
|
269
|
+
modelID: event.modelID,
|
|
270
|
+
input: event.input,
|
|
271
|
+
output: event.output,
|
|
272
|
+
cacheRead: event.cacheRead,
|
|
273
|
+
cacheWrite: event.cacheWrite,
|
|
274
|
+
now,
|
|
275
|
+
multiplierApplies: poolID === OPENDESIGN_DESIGN_PLAN_POOL,
|
|
276
|
+
...(multiplierOverrides ? { multiplierOverrides } : {}),
|
|
277
|
+
}).creditsUsd
|
|
278
|
+
if (credits <= 0) continue
|
|
279
|
+
observedRequests += 1
|
|
280
|
+
const target = bucket(poolID)
|
|
281
|
+
if (event.at >= rollingCutoff) {
|
|
282
|
+
rollingUsd += credits
|
|
283
|
+
target.rollingUsd += credits
|
|
284
|
+
}
|
|
285
|
+
if (event.at >= weekCutoff) {
|
|
286
|
+
weeklyUsd += credits
|
|
287
|
+
target.weeklyUsd += credits
|
|
288
|
+
}
|
|
289
|
+
if (event.at >= monthCutoff) {
|
|
290
|
+
monthlyUsd += credits
|
|
291
|
+
target.monthlyUsd += credits
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const margin = 1 + Math.max(0, marginPct) / 100
|
|
295
|
+
const scaled = (value: number) => value * margin
|
|
296
|
+
return {
|
|
297
|
+
rollingUsd: scaled(rollingUsd),
|
|
298
|
+
weeklyUsd: scaled(weeklyUsd),
|
|
299
|
+
monthlyUsd: scaled(monthlyUsd),
|
|
300
|
+
observedRequests,
|
|
301
|
+
byPool: Object.fromEntries(
|
|
302
|
+
Object.entries(pools).map(([poolID, totals]) => [
|
|
303
|
+
poolID,
|
|
304
|
+
{
|
|
305
|
+
rollingUsd: scaled(totals.rollingUsd),
|
|
306
|
+
weeklyUsd: scaled(totals.weeklyUsd),
|
|
307
|
+
monthlyUsd: scaled(totals.monthlyUsd),
|
|
308
|
+
},
|
|
309
|
+
]),
|
|
310
|
+
),
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const windowFrom = (
|
|
315
|
+
usedUsd: number,
|
|
316
|
+
capUsd: number,
|
|
317
|
+
resetsAt: number | undefined,
|
|
318
|
+
source: "provider" | "estimate",
|
|
319
|
+
): QuotaWindow => {
|
|
320
|
+
const usedPct = capUsd > 0 ? pct(usedUsd, capUsd) : 0
|
|
321
|
+
return {
|
|
322
|
+
usedPct,
|
|
323
|
+
remainingPct: clamp(100 - usedPct, 0, 100),
|
|
324
|
+
usedUsd,
|
|
325
|
+
capUsd,
|
|
326
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
327
|
+
exceeded: capUsd > 0 && usedUsd >= capUsd,
|
|
328
|
+
source,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const baseSnapshot = (
|
|
333
|
+
input: OpenDesignQuotaInput,
|
|
334
|
+
now: number,
|
|
335
|
+
error?: string,
|
|
336
|
+
retryAfterMs?: number,
|
|
337
|
+
notes: ReadonlyArray<string> = [],
|
|
338
|
+
): ProviderQuotaSnapshot => {
|
|
339
|
+
const plan = resolvePlan("opendesign", input.settings.plan)
|
|
340
|
+
return {
|
|
341
|
+
providerID: "opendesign",
|
|
342
|
+
label: plan?.label ?? "OpenDesign",
|
|
343
|
+
plan: describePlan(plan?.planID, input.settings.plan),
|
|
344
|
+
source: "unknown",
|
|
345
|
+
fetchedAt: now,
|
|
346
|
+
stale: false,
|
|
347
|
+
rolling: unknownWindow,
|
|
348
|
+
weekly: unknownWindow,
|
|
349
|
+
monthly: unknownWindow,
|
|
350
|
+
models: {},
|
|
351
|
+
notes: [...notes],
|
|
352
|
+
...(error ? { error } : {}),
|
|
353
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The caps a plan exposes, honoring explicit overrides and any advertised
|
|
359
|
+
* removal of a window.
|
|
360
|
+
*
|
|
361
|
+
* OpenDesign Pro has a seven-day Design Plan window and no five-hour window.
|
|
362
|
+
* An explicit `fiveHourCapUsd` from an older Go configuration must not
|
|
363
|
+
* reintroduce the window the user's plan does not have, which is why the
|
|
364
|
+
* plan shape is consulted before the override.
|
|
365
|
+
*/
|
|
366
|
+
const capsFor = (settings: ProviderSettings) => {
|
|
367
|
+
const plan = resolvePlan("opendesign", settings.plan)
|
|
368
|
+
const designPlan = plan?.pools.find((pool) => pool.id === OPENDESIGN_DESIGN_PLAN_POOL)
|
|
369
|
+
const planModels = plan?.pools.find((pool) => pool.id === OPENDESIGN_PLAN_MODEL_POOL)
|
|
370
|
+
const monthly = settings.monthlyCreditsUsd && settings.monthlyCreditsUsd > 0
|
|
371
|
+
? settings.monthlyCreditsUsd
|
|
372
|
+
: designPlan?.monthlyUsd ?? 0
|
|
373
|
+
const weeklyFromPlan = settings.ignoreWeeklyWindow ? undefined : designPlan?.weeklyUsd
|
|
374
|
+
const fiveFromPlan = settings.ignoreFiveHourWindow ? undefined : designPlan?.fiveHourUsd
|
|
375
|
+
return {
|
|
376
|
+
monthly,
|
|
377
|
+
weekly: weeklyFromPlan ?? (settings.ignoreWeeklyWindow ? undefined : settings.weeklyCapUsd),
|
|
378
|
+
rolling: fiveFromPlan ?? (settings.ignoreFiveHourWindow ? undefined : settings.fiveHourCapUsd),
|
|
379
|
+
planModelsMonthly: planModels?.monthlyUsd ?? 0,
|
|
380
|
+
hasPlanModelPool: Boolean(planModels),
|
|
381
|
+
hasFiveHourWindow: fiveFromPlan !== undefined,
|
|
382
|
+
hasWeeklyWindow: weeklyFromPlan !== undefined,
|
|
383
|
+
plan,
|
|
384
|
+
/**
|
|
385
|
+
* True when the configured plan id matches nothing in the catalog and no
|
|
386
|
+
* explicit cap was supplied. Capacity is then unknown, not zero: reporting
|
|
387
|
+
* zero would look like a fresh, unused allowance and silently let the
|
|
388
|
+
* router over-spend.
|
|
389
|
+
*/
|
|
390
|
+
unresolved: !plan && !(settings.monthlyCreditsUsd && settings.monthlyCreditsUsd > 0),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
interface ControlFetchResult {
|
|
395
|
+
readonly ok: boolean
|
|
396
|
+
readonly body?: unknown
|
|
397
|
+
readonly error?: string
|
|
398
|
+
readonly status?: number
|
|
399
|
+
readonly retryAfterMs?: number
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const controlFetch = async (
|
|
403
|
+
url: string,
|
|
404
|
+
controlKey: string,
|
|
405
|
+
fetchImpl: FetchLike,
|
|
406
|
+
timeoutMs: number,
|
|
407
|
+
): Promise<ControlFetchResult> => {
|
|
408
|
+
const controller = new AbortController()
|
|
409
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
410
|
+
try {
|
|
411
|
+
const response = await fetchImpl(url, {
|
|
412
|
+
method: "GET",
|
|
413
|
+
headers: {
|
|
414
|
+
authorization: `Bearer ${controlKey}`,
|
|
415
|
+
accept: "application/json",
|
|
416
|
+
"user-agent": OPENDESIGN_USER_AGENT,
|
|
417
|
+
},
|
|
418
|
+
redirect: "error",
|
|
419
|
+
signal: controller.signal,
|
|
420
|
+
})
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
const retryAfter = Number(response.headers.get("retry-after"))
|
|
423
|
+
const retryAfterMs =
|
|
424
|
+
Number.isFinite(retryAfter) && retryAfter >= 0 ? Math.min(retryAfter * 1000, 15 * 60_000) : undefined
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
error: `Account control request returned HTTP ${response.status}`,
|
|
428
|
+
status: response.status,
|
|
429
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const text = await response.text()
|
|
433
|
+
if (text.length > 2 * 1024 * 1024) {
|
|
434
|
+
return { ok: false, error: "Account control response was unexpectedly large" }
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
return { ok: true, body: JSON.parse(text) as unknown }
|
|
438
|
+
} catch {
|
|
439
|
+
return { ok: false, error: "Account control response was malformed" }
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
error: controller.signal.aborted
|
|
445
|
+
? "Account control request timed out"
|
|
446
|
+
: redact(error instanceof Error ? error.message : String(error)),
|
|
447
|
+
}
|
|
448
|
+
} finally {
|
|
449
|
+
clearTimeout(timer)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
interface ControlWindow {
|
|
454
|
+
readonly kind: "rolling" | "weekly" | "monthly"
|
|
455
|
+
readonly usedCredits: number
|
|
456
|
+
readonly limitCredits: number
|
|
457
|
+
readonly remainingCredits: number
|
|
458
|
+
readonly resetsAt?: number
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const classifyWindow = (durationSeconds: number): ControlWindow["kind"] | undefined => {
|
|
462
|
+
if (durationSeconds <= 0) return undefined
|
|
463
|
+
if (durationSeconds <= 6 * 3600) return "rolling"
|
|
464
|
+
if (durationSeconds <= 8 * 86400) return "weekly"
|
|
465
|
+
if (durationSeconds <= 32 * 86400) return "monthly"
|
|
466
|
+
return undefined
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const parseControlWindows = (value: unknown): ControlWindow[] => {
|
|
470
|
+
if (!Array.isArray(value)) return []
|
|
471
|
+
const out: ControlWindow[] = []
|
|
472
|
+
for (const row of value) {
|
|
473
|
+
if (!isRecord(row)) continue
|
|
474
|
+
const duration = numberOr(row.durationSeconds)
|
|
475
|
+
if (duration === undefined) continue
|
|
476
|
+
const kind = classifyWindow(duration)
|
|
477
|
+
if (!kind) continue
|
|
478
|
+
const usedCredits = numberOr(row.usedCredits) ?? 0
|
|
479
|
+
const limitCredits = numberOr(row.limitCredits) ?? 0
|
|
480
|
+
const remainingCredits = numberOr(row.remainingCredits) ?? Math.max(0, limitCredits - usedCredits)
|
|
481
|
+
const resetsAt = timeValue(row.resetsAt)
|
|
482
|
+
out.push({ kind, usedCredits, limitCredits, remainingCredits, ...(resetsAt !== undefined ? { resetsAt } : {}) })
|
|
483
|
+
}
|
|
484
|
+
return out
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Build one snapshot per funding pool.
|
|
489
|
+
*
|
|
490
|
+
* The pools are reported side by side rather than summed. A plan card that
|
|
491
|
+
* advertises "$480 total" is advertising two allowances with different rules,
|
|
492
|
+
* so adding them into a single balance would tell a user they can spend it in
|
|
493
|
+
* one go, which is not true.
|
|
494
|
+
*/
|
|
495
|
+
const buildPoolSnapshots = (input: {
|
|
496
|
+
caps: ReturnType<typeof capsFor>
|
|
497
|
+
used: Readonly<Record<string, { rollingUsd: number; weeklyUsd: number; monthlyUsd: number }>>
|
|
498
|
+
source: "provider" | "estimate"
|
|
499
|
+
creditsPerUsd: number
|
|
500
|
+
resetsAt?: number
|
|
501
|
+
}): CreditPoolSnapshot[] => {
|
|
502
|
+
const { caps, used, source } = input
|
|
503
|
+
const out: CreditPoolSnapshot[] = []
|
|
504
|
+
const designUsed = used[OPENDESIGN_DESIGN_PLAN_POOL] ?? { rollingUsd: 0, weeklyUsd: 0, monthlyUsd: 0 }
|
|
505
|
+
if (caps.monthly > 0) {
|
|
506
|
+
out.push({
|
|
507
|
+
id: OPENDESIGN_DESIGN_PLAN_POOL,
|
|
508
|
+
label: "Design Plan credits",
|
|
509
|
+
source,
|
|
510
|
+
monthlyUsd: caps.monthly,
|
|
511
|
+
usedUsd: designUsed.monthlyUsd,
|
|
512
|
+
remainingUsd: Math.max(0, caps.monthly - designUsed.monthlyUsd),
|
|
513
|
+
usedPct: pct(designUsed.monthlyUsd, caps.monthly),
|
|
514
|
+
...(caps.weekly !== undefined
|
|
515
|
+
? { weeklyUsd: caps.weekly, weeklyUsedPct: pct(designUsed.weeklyUsd, caps.weekly) }
|
|
516
|
+
: {}),
|
|
517
|
+
...(caps.rolling !== undefined
|
|
518
|
+
? { fiveHourUsd: caps.rolling, fiveHourUsedPct: pct(designUsed.rollingUsd, caps.rolling) }
|
|
519
|
+
: {}),
|
|
520
|
+
resetKind: "fixed-7-day",
|
|
521
|
+
...(input.resetsAt !== undefined ? { resetsAt: input.resetsAt } : {}),
|
|
522
|
+
note: caps.hasFiveHourWindow
|
|
523
|
+
? "Five-hour and seven-day ceilings nest inside the monthly maximum."
|
|
524
|
+
: "Seven-day ceiling nests inside the monthly maximum. This plan has no five-hour window.",
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
if (caps.hasPlanModelPool && caps.planModelsMonthly > 0) {
|
|
528
|
+
const planUsed = used[OPENDESIGN_PLAN_MODEL_POOL] ?? { rollingUsd: 0, weeklyUsd: 0, monthlyUsd: 0 }
|
|
529
|
+
out.push({
|
|
530
|
+
id: OPENDESIGN_PLAN_MODEL_POOL,
|
|
531
|
+
label: "Plan model credits",
|
|
532
|
+
source,
|
|
533
|
+
monthlyUsd: caps.planModelsMonthly,
|
|
534
|
+
usedUsd: planUsed.monthlyUsd,
|
|
535
|
+
remainingUsd: Math.max(0, caps.planModelsMonthly - planUsed.monthlyUsd),
|
|
536
|
+
usedPct: pct(planUsed.monthlyUsd, caps.planModelsMonthly),
|
|
537
|
+
resetKind: "billing-cycle",
|
|
538
|
+
...(input.resetsAt !== undefined ? { resetsAt: input.resetsAt } : {}),
|
|
539
|
+
note: "No five-hour or seven-day window. Design Plan multipliers do not apply to this pool.",
|
|
540
|
+
})
|
|
541
|
+
}
|
|
542
|
+
return out
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const planNotes = (caps: ReturnType<typeof capsFor>): string[] => {
|
|
546
|
+
const notes: string[] = []
|
|
547
|
+
if (caps.plan) notes.push(`Plan: ${caps.plan.label}.`)
|
|
548
|
+
notes.push(
|
|
549
|
+
caps.hasFiveHourWindow
|
|
550
|
+
? "This plan has a five-hour Design Plan window."
|
|
551
|
+
: "This plan has no five-hour Design Plan window.",
|
|
552
|
+
)
|
|
553
|
+
if (caps.hasPlanModelPool && caps.planModelsMonthly > 0) {
|
|
554
|
+
notes.push(
|
|
555
|
+
`A separate plan model pool of $${caps.planModelsMonthly}/month is reported independently and is never added to the Design Plan allowance.`,
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
return notes
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Read the account's coding-plan usage windows and wallet balance.
|
|
563
|
+
*
|
|
564
|
+
* The control key is a privileged credential. It is only used when the owner
|
|
565
|
+
* explicitly configures one; the router never obtains one on its own and never
|
|
566
|
+
* asks for a browser session.
|
|
567
|
+
*/
|
|
568
|
+
const probeVerified = async (
|
|
569
|
+
input: OpenDesignQuotaInput,
|
|
570
|
+
now: number,
|
|
571
|
+
): Promise<OpenDesignQuotaResult> => {
|
|
572
|
+
const fetchImpl = input.fetchImpl ?? fetch
|
|
573
|
+
const timeoutMs = input.timeoutMs ?? 12_000
|
|
574
|
+
const controlBase = normalizeOpenDesignControlBaseURL(input.controlBaseURL ?? OPENDESIGN_DEFAULT_CONTROL_BASE_URL)
|
|
575
|
+
const controlKey = input.controlKey ?? ""
|
|
576
|
+
|
|
577
|
+
const current = await controlFetch(
|
|
578
|
+
openDesignControlEndpoint(controlBase, "/v1/workspaces/current"),
|
|
579
|
+
controlKey,
|
|
580
|
+
fetchImpl,
|
|
581
|
+
timeoutMs,
|
|
582
|
+
)
|
|
583
|
+
if (!current.ok || !isRecord(current.body)) {
|
|
584
|
+
return {
|
|
585
|
+
snapshot: baseSnapshot(input, now, current.error ?? "Could not read the current workspace", current.retryAfterMs, [
|
|
586
|
+
"Verified quota is unavailable.",
|
|
587
|
+
]),
|
|
588
|
+
state: "unavailable",
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const workspaceId =
|
|
592
|
+
typeof current.body.activeWorkspaceId === "string"
|
|
593
|
+
? current.body.activeWorkspaceId
|
|
594
|
+
: typeof current.body.workspaceId === "string"
|
|
595
|
+
? current.body.workspaceId
|
|
596
|
+
: undefined
|
|
597
|
+
if (!workspaceId) {
|
|
598
|
+
return {
|
|
599
|
+
snapshot: baseSnapshot(input, now, "The account did not report an active workspace", undefined, [
|
|
600
|
+
"Verified quota is unavailable.",
|
|
601
|
+
]),
|
|
602
|
+
state: "unavailable",
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const usage = await controlFetch(
|
|
607
|
+
openDesignControlEndpoint(controlBase, `/v1/workspaces/${encodeURIComponent(workspaceId)}/billing/coding-plan-usage`),
|
|
608
|
+
controlKey,
|
|
609
|
+
fetchImpl,
|
|
610
|
+
timeoutMs,
|
|
611
|
+
)
|
|
612
|
+
if (!usage.ok || !isRecord(usage.body)) {
|
|
613
|
+
return {
|
|
614
|
+
snapshot: baseSnapshot(input, now, usage.error ?? "Could not read coding-plan usage", usage.retryAfterMs, [
|
|
615
|
+
"Verified quota is unavailable.",
|
|
616
|
+
]),
|
|
617
|
+
state: "unavailable",
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Never combine a workspace's usage with another workspace's balance.
|
|
621
|
+
const reportedWorkspace = typeof usage.body.workspaceId === "string" ? usage.body.workspaceId : undefined
|
|
622
|
+
if (reportedWorkspace && reportedWorkspace !== workspaceId) {
|
|
623
|
+
return {
|
|
624
|
+
snapshot: baseSnapshot(input, now, "The usage response did not match the active workspace", undefined, [
|
|
625
|
+
"Balances from different workspaces are never combined.",
|
|
626
|
+
]),
|
|
627
|
+
state: "unavailable",
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const summary = await controlFetch(
|
|
632
|
+
openDesignControlEndpoint(controlBase, "/v1/billing/summary"),
|
|
633
|
+
controlKey,
|
|
634
|
+
fetchImpl,
|
|
635
|
+
timeoutMs,
|
|
636
|
+
)
|
|
637
|
+
const summaryBody = summary.ok && isRecord(summary.body) ? summary.body : undefined
|
|
638
|
+
const creditsPerUsd = numberOr(summaryBody?.creditsPerUsd) ?? input.creditsPerUsd ?? OPENDESIGN_DEFAULT_CREDITS_PER_USD
|
|
639
|
+
const balances = isRecord(summaryBody?.balances) ? (summaryBody!.balances as Record<string, unknown>) : undefined
|
|
640
|
+
const rechargeCredits = numberOr(balances?.rechargeCredits) ?? 0
|
|
641
|
+
const subscriptionCredits = numberOr(balances?.subscriptionCredits) ?? 0
|
|
642
|
+
const purchasedUsd = rechargeCredits / creditsPerUsd
|
|
643
|
+
|
|
644
|
+
const windows = parseControlWindows(usage.body.windows)
|
|
645
|
+
const caps = capsFor(input.settings)
|
|
646
|
+
const find = (kind: ControlWindow["kind"]) => windows.find((window) => window.kind === kind)
|
|
647
|
+
|
|
648
|
+
const toWindow = (window: ControlWindow | undefined, fallbackCapUsd: number): QuotaWindow => {
|
|
649
|
+
if (!window || window.limitCredits <= 0) {
|
|
650
|
+
return { usedPct: 0, remainingPct: 100, source: "unknown" }
|
|
651
|
+
}
|
|
652
|
+
const usedPct = pct(window.usedCredits, window.limitCredits)
|
|
653
|
+
return {
|
|
654
|
+
usedPct,
|
|
655
|
+
remainingPct: clamp(100 - usedPct, 0, 100),
|
|
656
|
+
usedUsd: window.usedCredits / creditsPerUsd,
|
|
657
|
+
capUsd: window.limitCredits / creditsPerUsd,
|
|
658
|
+
...(window.resetsAt !== undefined ? { resetsAt: window.resetsAt } : {}),
|
|
659
|
+
exceeded: window.usedCredits >= window.limitCredits,
|
|
660
|
+
source: "provider",
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const rolling = toWindow(find("rolling"), caps.rolling ?? 0)
|
|
665
|
+
const weekly = toWindow(find("weekly"), caps.weekly ?? 0)
|
|
666
|
+
const monthlyWindow = find("monthly")
|
|
667
|
+
const monthly = monthlyWindow
|
|
668
|
+
? toWindow(monthlyWindow, caps.monthly)
|
|
669
|
+
: {
|
|
670
|
+
usedPct: 0,
|
|
671
|
+
remainingPct: 100,
|
|
672
|
+
source: "unknown" as const,
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const periodStart = timeValue(summaryBody?.subscriptionCurrentPeriodStart)
|
|
676
|
+
const periodEnd = timeValue(summaryBody?.subscriptionCurrentPeriodEnd)
|
|
677
|
+
const monthlyRemainingUsd = monthlyWindow
|
|
678
|
+
? monthlyWindow.remainingCredits / creditsPerUsd
|
|
679
|
+
: subscriptionCredits > 0
|
|
680
|
+
? subscriptionCredits / creditsPerUsd
|
|
681
|
+
: undefined
|
|
682
|
+
|
|
683
|
+
const pools = buildPoolSnapshots({
|
|
684
|
+
caps,
|
|
685
|
+
used: {
|
|
686
|
+
[OPENDESIGN_DESIGN_PLAN_POOL]: {
|
|
687
|
+
rollingUsd: rolling.usedUsd ?? 0,
|
|
688
|
+
weeklyUsd: weekly.usedUsd ?? 0,
|
|
689
|
+
monthlyUsd: monthly.usedUsd ?? 0,
|
|
690
|
+
},
|
|
691
|
+
[OPENDESIGN_PLAN_MODEL_POOL]: {
|
|
692
|
+
rollingUsd: 0,
|
|
693
|
+
weeklyUsd: 0,
|
|
694
|
+
// The control surface reports Design Plan consumption; plan model
|
|
695
|
+
// credits are reported as a remaining balance only.
|
|
696
|
+
monthlyUsd: Math.max(0, caps.planModelsMonthly - subscriptionCredits / creditsPerUsd),
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
source: "provider",
|
|
700
|
+
creditsPerUsd,
|
|
701
|
+
...(periodEnd !== undefined ? { resetsAt: periodEnd } : {}),
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
const notes = [
|
|
705
|
+
"Provider-reported OpenDesign coding-plan usage.",
|
|
706
|
+
"Read-only probe; no inference request is made.",
|
|
707
|
+
`Workspace ${workspaceId}.`,
|
|
708
|
+
...planNotes(caps),
|
|
709
|
+
]
|
|
710
|
+
if (purchasedUsd > 0) {
|
|
711
|
+
notes.push(
|
|
712
|
+
`Wallet balance ${purchasedUsd.toFixed(2)} USD is separate from the subscription allowance and is not used for automatic routing.`,
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const snapshot: ProviderQuotaSnapshot = {
|
|
717
|
+
providerID: "opendesign",
|
|
718
|
+
label: caps.plan?.label ?? "OpenDesign",
|
|
719
|
+
plan: describePlan(caps.plan?.planID, input.settings.plan),
|
|
720
|
+
source: "provider",
|
|
721
|
+
fetchedAt: now,
|
|
722
|
+
stale: false,
|
|
723
|
+
rolling,
|
|
724
|
+
weekly,
|
|
725
|
+
monthly,
|
|
726
|
+
// A verified probe reads the real limits from the account, so it reports
|
|
727
|
+
// the provider's own grant when the catalog could not name the plan.
|
|
728
|
+
monthlyGrantUsd: caps.monthly > 0 ? caps.monthly : monthly.capUsd ?? 0,
|
|
729
|
+
...(monthlyRemainingUsd !== undefined ? { monthlyRemainingUsd } : {}),
|
|
730
|
+
extraCredits: {
|
|
731
|
+
purchasedUsd,
|
|
732
|
+
freeUsd: 0,
|
|
733
|
+
totalUsd: purchasedUsd,
|
|
734
|
+
},
|
|
735
|
+
pools,
|
|
736
|
+
planStatus: typeof summaryBody?.membershipTier === "string" ? summaryBody.membershipTier : undefined,
|
|
737
|
+
...(periodStart !== undefined ? { periodStart } : {}),
|
|
738
|
+
...(periodEnd !== undefined ? { periodEnd } : {}),
|
|
739
|
+
models: {},
|
|
740
|
+
notes,
|
|
741
|
+
}
|
|
742
|
+
const exhausted = Boolean(rolling.exceeded || weekly.exceeded || monthly.exceeded)
|
|
743
|
+
return { snapshot, state: exhausted ? "exhausted" : "verified" }
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Model the router's own credit consumption against the published plan caps.
|
|
748
|
+
*/
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Fold a user-supplied account reading into the local totals.
|
|
752
|
+
*
|
|
753
|
+
* A baseline is a real figure the user read from their own account, so it is
|
|
754
|
+
* stronger evidence than the router's own traffic — but only inside the window
|
|
755
|
+
* it was taken in. Once a window has reset, the reading describes the previous
|
|
756
|
+
* window and including it would overstate usage just as badly as omitting it
|
|
757
|
+
* understated it. So each window accepts the baseline only while the reading is
|
|
758
|
+
* newer than that window's start.
|
|
759
|
+
*/
|
|
760
|
+
const applyBaseline = (
|
|
761
|
+
totals: LocalWindowTotals,
|
|
762
|
+
baseline: ProviderSettings["baseline"],
|
|
763
|
+
now: number,
|
|
764
|
+
caps: {
|
|
765
|
+
readonly weekly?: number
|
|
766
|
+
readonly monthly: number
|
|
767
|
+
readonly planModelsMonthly: number
|
|
768
|
+
},
|
|
769
|
+
): {
|
|
770
|
+
totals: LocalWindowTotals
|
|
771
|
+
applied: boolean
|
|
772
|
+
windows: string[]
|
|
773
|
+
usedFallbackReset?: boolean
|
|
774
|
+
/** Windows whose percentage could not be converted, with the reason. */
|
|
775
|
+
unconverted?: ReadonlyArray<string>
|
|
776
|
+
} => {
|
|
777
|
+
if (!baseline) return { totals, applied: false, windows: [] }
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Convert a percentage the user copied off the account page into dollars.
|
|
781
|
+
*
|
|
782
|
+
* The page shows a percentage and the router works in dollars, so somebody has
|
|
783
|
+
* to do the division. Doing it here means it is done against the cap for the
|
|
784
|
+
* window the page was describing, which is the part users get wrong: on Pro the
|
|
785
|
+
* seven-day window is $90 and the month is $360, so "42%" means $37.80, not
|
|
786
|
+
* $151.20.
|
|
787
|
+
*/
|
|
788
|
+
const unconverted: string[] = []
|
|
789
|
+
const fromPct = (pct: number | undefined, cap: number | undefined, label: string): number => {
|
|
790
|
+
if (pct === undefined) return 0
|
|
791
|
+
if (cap === undefined || cap <= 0) {
|
|
792
|
+
unconverted.push(`the ${label} cap is unknown, so ${pct}% could not be converted`)
|
|
793
|
+
return 0
|
|
794
|
+
}
|
|
795
|
+
return (pct / 100) * cap
|
|
796
|
+
}
|
|
797
|
+
const pctWindow = baseline.pctOf ?? "weekly"
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* A reading is meaningful when the user entered it, not when it is above zero.
|
|
801
|
+
*
|
|
802
|
+
* A brand-new account reports 0%, and that is a real reading: it means the
|
|
803
|
+
* allowance is untouched. Treating zero as "no information" would throw away
|
|
804
|
+
* the best possible starting point, and treating a missing field as zero would
|
|
805
|
+
* invent usage the user never reported. So presence decides, not magnitude.
|
|
806
|
+
*/
|
|
807
|
+
const hasDesignReading =
|
|
808
|
+
baseline.designPlanUsedUsd !== undefined || baseline.designPlanUsedPct !== undefined
|
|
809
|
+
const hasPlanModelsReading =
|
|
810
|
+
baseline.planModelsUsedUsd !== undefined || baseline.planModelsUsedPct !== undefined
|
|
811
|
+
|
|
812
|
+
const designPlan = hasDesignReading
|
|
813
|
+
? Math.max(0, baseline.designPlanUsedUsd ?? 0) ||
|
|
814
|
+
fromPct(
|
|
815
|
+
baseline.designPlanUsedPct,
|
|
816
|
+
pctWindow === "weekly" ? (caps.weekly ?? caps.monthly) : caps.monthly,
|
|
817
|
+
pctWindow === "weekly" ? "seven-day Design Plan" : "monthly Design Plan",
|
|
818
|
+
)
|
|
819
|
+
: 0
|
|
820
|
+
|
|
821
|
+
const planModels = hasPlanModelsReading
|
|
822
|
+
? Math.max(0, baseline.planModelsUsedUsd ?? 0) ||
|
|
823
|
+
fromPct(baseline.planModelsUsedPct, caps.planModelsMonthly, "plan model")
|
|
824
|
+
: 0
|
|
825
|
+
|
|
826
|
+
// Only bail when there was nothing to read. A genuine 0% reading must be
|
|
827
|
+
// applied, because it is the strongest statement of "untouched allowance".
|
|
828
|
+
if (!hasDesignReading && !hasPlanModelsReading) {
|
|
829
|
+
// Carry the reason out even though nothing was applied: a percentage that
|
|
830
|
+
// could not be converted is worth saying, and silently using zero is the
|
|
831
|
+
// failure this whole mechanism exists to avoid.
|
|
832
|
+
return { totals, applied: false, windows: [], ...(unconverted.length > 0 ? { unconverted } : {}) }
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* A window accepts the reading until that window RESETS — not merely until the
|
|
837
|
+
* reading is older than the window's length.
|
|
838
|
+
*
|
|
839
|
+
* These differ, and the difference is not academic. A seven-day window that
|
|
840
|
+
* resets 23 hours from now (the account page shows this) means the reading
|
|
841
|
+
* describes the window ending in 23 hours. Seeding it into the window that
|
|
842
|
+
* begins afterwards would attribute a previous window's usage to a fresh one.
|
|
843
|
+
*
|
|
844
|
+
* When the account page gave a reset time, trust it. Otherwise fall back to
|
|
845
|
+
* age, and say that the fallback was used.
|
|
846
|
+
*/
|
|
847
|
+
const resetPassed = (resetsAt: number | undefined) => resetsAt !== undefined && now >= resetsAt
|
|
848
|
+
const withinRolling = baseline.at >= now - FIVE_HOURS_MS && !resetPassed(baseline.weeklyResetsAt)
|
|
849
|
+
const withinWeekly = baseline.at >= now - WEEK_MS && !resetPassed(baseline.weeklyResetsAt)
|
|
850
|
+
const withinMonthly = baseline.at >= now - MONTH_MS && !resetPassed(baseline.monthlyResetsAt)
|
|
851
|
+
if (!withinMonthly) return { totals, applied: false, windows: [] }
|
|
852
|
+
|
|
853
|
+
const windows: string[] = []
|
|
854
|
+
if (withinRolling) windows.push("5h")
|
|
855
|
+
if (withinWeekly) windows.push("7d")
|
|
856
|
+
windows.push("monthly")
|
|
857
|
+
|
|
858
|
+
const designRolling = withinRolling ? designPlan : 0
|
|
859
|
+
const designWeekly = withinWeekly ? designPlan : 0
|
|
860
|
+
const planRolling = withinRolling ? planModels : 0
|
|
861
|
+
const planWeekly = withinWeekly ? planModels : 0
|
|
862
|
+
|
|
863
|
+
const byPool = { ...totals.byPool }
|
|
864
|
+
const designBucket = byPool[OPENDESIGN_DESIGN_PLAN_POOL] ?? { rollingUsd: 0, weeklyUsd: 0, monthlyUsd: 0 }
|
|
865
|
+
const planBucket = byPool[OPENDESIGN_PLAN_MODEL_POOL] ?? { rollingUsd: 0, weeklyUsd: 0, monthlyUsd: 0 }
|
|
866
|
+
if (designPlan > 0) {
|
|
867
|
+
byPool[OPENDESIGN_DESIGN_PLAN_POOL] = {
|
|
868
|
+
rollingUsd: designBucket.rollingUsd + designRolling,
|
|
869
|
+
weeklyUsd: designBucket.weeklyUsd + designWeekly,
|
|
870
|
+
monthlyUsd: designBucket.monthlyUsd + designPlan,
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (planModels > 0) {
|
|
874
|
+
byPool[OPENDESIGN_PLAN_MODEL_POOL] = {
|
|
875
|
+
rollingUsd: planBucket.rollingUsd + planRolling,
|
|
876
|
+
weeklyUsd: planBucket.weeklyUsd + planWeekly,
|
|
877
|
+
monthlyUsd: planBucket.monthlyUsd + planModels,
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
applied: true,
|
|
883
|
+
windows,
|
|
884
|
+
usedFallbackReset: baseline.weeklyResetsAt === undefined,
|
|
885
|
+
...(unconverted.length > 0 ? { unconverted } : {}),
|
|
886
|
+
totals: {
|
|
887
|
+
rollingUsd: totals.rollingUsd + designRolling + planRolling,
|
|
888
|
+
weeklyUsd: totals.weeklyUsd + designWeekly + planWeekly,
|
|
889
|
+
monthlyUsd: totals.monthlyUsd + designPlan + planModels,
|
|
890
|
+
observedRequests: totals.observedRequests,
|
|
891
|
+
byPool,
|
|
892
|
+
},
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const probeEstimated = (input: OpenDesignQuotaInput, now: number): OpenDesignQuotaResult => {
|
|
897
|
+
const caps = capsFor(input.settings)
|
|
898
|
+
/**
|
|
899
|
+
* The margin is measured, not assumed.
|
|
900
|
+
*
|
|
901
|
+
* A fresh install has no evidence, so the default stands. Once the router has
|
|
902
|
+
* routed a few requests it can compare its own arithmetic with the charges the
|
|
903
|
+
* provider reported and shrink the margin to the error it actually observes.
|
|
904
|
+
* That is what removes the need to paste a balance every week: drift is
|
|
905
|
+
* corrected from traffic the router already handled.
|
|
906
|
+
*
|
|
907
|
+
* A caller that states a margin explicitly still wins — configuration is an
|
|
908
|
+
* instruction, and calibration is a convenience.
|
|
909
|
+
*/
|
|
910
|
+
const marginPct =
|
|
911
|
+
input.uncertaintyMarginPct ??
|
|
912
|
+
effectiveMarginPct(calibrationSamples(input.usage), OPENDESIGN_DEFAULT_UNCERTAINTY_MARGIN_PCT)
|
|
913
|
+
const local = localTotals(
|
|
914
|
+
input.usage,
|
|
915
|
+
"opendesign",
|
|
916
|
+
now,
|
|
917
|
+
marginPct,
|
|
918
|
+
caps.plan,
|
|
919
|
+
input.multiplierOverrides,
|
|
920
|
+
// The pasted reset time is the only trustworthy anchor for the fixed
|
|
921
|
+
// window; without it the estimator falls back to a week ending now.
|
|
922
|
+
input.settings.baseline?.weeklyResetsAt,
|
|
923
|
+
)
|
|
924
|
+
const seeded = applyBaseline(local, input.settings.baseline, now, {
|
|
925
|
+
...(caps.weekly !== undefined ? { weekly: caps.weekly } : {}),
|
|
926
|
+
monthly: caps.monthly,
|
|
927
|
+
planModelsMonthly: caps.planModelsMonthly,
|
|
928
|
+
})
|
|
929
|
+
const totals = seeded.totals
|
|
930
|
+
// A window the plan does not have is never reported as a measured 0%.
|
|
931
|
+
// Reporting "0% of a five-hour window" would imply a ceiling that could be
|
|
932
|
+
// hit, when the plan has no such limit.
|
|
933
|
+
const rolling = caps.rolling !== undefined
|
|
934
|
+
? windowFrom(totals.rollingUsd, caps.rolling, undefined, "estimate")
|
|
935
|
+
: { ...unknownWindow, usedUsd: totals.rollingUsd }
|
|
936
|
+
const weekly = caps.weekly !== undefined
|
|
937
|
+
? windowFrom(totals.weeklyUsd, caps.weekly, undefined, "estimate")
|
|
938
|
+
: { ...unknownWindow, usedUsd: totals.weeklyUsd }
|
|
939
|
+
const monthly = caps.unresolved
|
|
940
|
+
? { ...unknownWindow, usedUsd: totals.monthlyUsd }
|
|
941
|
+
: windowFrom(totals.monthlyUsd, caps.monthly, undefined, "estimate")
|
|
942
|
+
const pools = buildPoolSnapshots({
|
|
943
|
+
caps,
|
|
944
|
+
used: totals.byPool,
|
|
945
|
+
source: "estimate",
|
|
946
|
+
creditsPerUsd: OPENDESIGN_DEFAULT_CREDITS_PER_USD,
|
|
947
|
+
})
|
|
948
|
+
const notes = [
|
|
949
|
+
seeded.applied
|
|
950
|
+
? `Starts from the usage you reported on ${new Date(input.settings.baseline!.at).toISOString().slice(0, 10)} and adds every OpenDesign request this router has routed since.`
|
|
951
|
+
: "Estimated from this router's own OpenDesign usage. OpenDesign publishes no quota API for an inference key.",
|
|
952
|
+
seeded.applied
|
|
953
|
+
? `The reported reading still excludes other clients, and a ${marginPct}% margin is applied, so this remains a lower bound. There is no exact source without an account control key, and the account API-key page issues inference keys only.`
|
|
954
|
+
: `A ${marginPct}% safety margin is applied. Usage from the OpenDesign app or other clients on the same subscription is not visible, so this is a lower bound.`,
|
|
955
|
+
"Wallet balance is not included and is never used for automatic routing.",
|
|
956
|
+
...planNotes(caps),
|
|
957
|
+
]
|
|
958
|
+
for (const reason of seeded.unconverted ?? []) notes.push(`The reported percentage was not used: ${reason}.`)
|
|
959
|
+
if (seeded.applied) {
|
|
960
|
+
notes.push(
|
|
961
|
+
seeded.usedFallbackReset
|
|
962
|
+
? `The reported reading applies to: ${seeded.windows.join(", ")}. No reset time was supplied, so the router assumes the reading stays valid for its window length. Add baseline.weeklyResetsAt to be exact.`
|
|
963
|
+
: `The reported reading applies to: ${seeded.windows.join(", ")} until that window resets.`,
|
|
964
|
+
)
|
|
965
|
+
}
|
|
966
|
+
if (caps.unresolved) {
|
|
967
|
+
notes.push(
|
|
968
|
+
`The configured plan "${input.settings.plan}" is not in the catalog, so the allowance is unknown rather than zero. Set a plan id from the catalog or an explicit monthlyCreditsUsd.`,
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
if (totals.observedRequests === 0) {
|
|
972
|
+
notes.push(
|
|
973
|
+
seeded.applied
|
|
974
|
+
? "No OpenDesign requests have been routed through this router since that reading, so the figure is still exactly what you reported."
|
|
975
|
+
: "No OpenDesign requests have been routed through this router yet, so the estimate starts at zero.",
|
|
976
|
+
)
|
|
977
|
+
}
|
|
978
|
+
const snapshot: ProviderQuotaSnapshot = {
|
|
979
|
+
providerID: "opendesign",
|
|
980
|
+
label: caps.plan?.label ?? "OpenDesign",
|
|
981
|
+
plan: describePlan(caps.plan?.planID, input.settings.plan),
|
|
982
|
+
source: "estimate",
|
|
983
|
+
fetchedAt: now,
|
|
984
|
+
stale: false,
|
|
985
|
+
rolling,
|
|
986
|
+
weekly,
|
|
987
|
+
monthly,
|
|
988
|
+
monthlyGrantUsd: caps.monthly,
|
|
989
|
+
// An unresolved plan has no known allowance, so a "remaining balance" of
|
|
990
|
+
// zero would be a fabrication. Omitting the field lets the eligibility
|
|
991
|
+
// gate fall through to the unknown-quota policy rather than reporting the
|
|
992
|
+
// provider as exhausted.
|
|
993
|
+
...(caps.unresolved ? {} : { monthlyRemainingUsd: Math.max(0, caps.monthly - totals.monthlyUsd) }),
|
|
994
|
+
pools,
|
|
995
|
+
models: {},
|
|
996
|
+
notes,
|
|
997
|
+
}
|
|
998
|
+
if (caps.unresolved) return { snapshot, state: "unknown" }
|
|
999
|
+
const exhausted = Boolean(monthly.exceeded || weekly.exceeded)
|
|
1000
|
+
return { snapshot, state: exhausted ? "exhausted" : "estimated" }
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Resolve the OpenDesign quota state.
|
|
1005
|
+
*
|
|
1006
|
+
* A control key, when explicitly configured, produces verified provider
|
|
1007
|
+
* numbers. Otherwise the router falls back to its own estimate. It never
|
|
1008
|
+
* acquires a control key, never uses a browser session, and never treats a
|
|
1009
|
+
* wallet balance as permission to spend.
|
|
1010
|
+
*/
|
|
1011
|
+
export const probeOpenDesignQuota = async (
|
|
1012
|
+
input: OpenDesignQuotaInput,
|
|
1013
|
+
): Promise<OpenDesignQuotaResult> => {
|
|
1014
|
+
const now = input.now?.() ?? Date.now()
|
|
1015
|
+
if (!input.settings.enabled) {
|
|
1016
|
+
return {
|
|
1017
|
+
snapshot: baseSnapshot(input, now, "OpenDesign is disabled in router settings", undefined, [
|
|
1018
|
+
"The provider is disabled; it takes no part in routing.",
|
|
1019
|
+
]),
|
|
1020
|
+
state: "unavailable",
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (input.controlKey) {
|
|
1024
|
+
return probeVerified(input, now)
|
|
1025
|
+
}
|
|
1026
|
+
if (!input.apiKey) {
|
|
1027
|
+
return {
|
|
1028
|
+
snapshot: baseSnapshot(input, now, "No OpenDesign credential is configured", undefined, [
|
|
1029
|
+
"Add an OpenDesign API key, or configure a control key for verified quota.",
|
|
1030
|
+
]),
|
|
1031
|
+
state: "unavailable",
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return probeEstimated(input, now)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export const __opendesignQuotaInternals = {
|
|
1038
|
+
classifyWindow,
|
|
1039
|
+
parseControlWindows,
|
|
1040
|
+
localTotals,
|
|
1041
|
+
windowFrom,
|
|
1042
|
+
failureClassForStatus,
|
|
1043
|
+
}
|