opencode-plugboard 0.18.0 → 0.19.0
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 +2 -2
- package/index.ts +27 -14
- package/package.json +1 -1
- package/src/anthropic/response.ts +8 -25
- package/src/anthropic/route.ts +2 -0
- package/src/budgets.ts +200 -0
- package/src/completions/request.ts +1 -0
- package/src/completions/response.ts +10 -1
- package/src/config.ts +98 -1
- package/src/contract.ts +14 -0
- package/src/gemini/request.ts +1 -0
- package/src/gemini/response.ts +16 -1
- package/src/gemini/route.ts +2 -0
- package/src/log.ts +3 -0
- package/src/metrics.ts +3 -3
- package/src/openai/fanout.ts +4 -0
- package/src/openai/request.ts +1 -0
- package/src/openai/response.ts +21 -3
- package/src/openai/route.ts +24 -2
- package/src/responses/request.ts +28 -14
- package/src/responses/route.ts +9 -4
- package/src/responses/store.ts +51 -0
- package/src/runtime.ts +10 -2
- package/src/stop.ts +54 -0
- package/src/usage-header.ts +46 -0
- package/src/usage.ts +43 -3
package/src/usage.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* at 0 and its call counted in `unpricedRequests`.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { APP_NAME_PATTERN } from "./config.js"
|
|
26
|
+
import { APP_NAME_PATTERN, type BudgetMetric, type BudgetWindow } from "./config.js"
|
|
27
27
|
import { isPlugError, type CallContext, type CatalogCost, type CatalogModel, type ModelCaller, type ModelRef, type PlugError, type PlugEvent, type PlugRequest, type PlugUsage, type ServedModel } from "./contract.js"
|
|
28
28
|
import { errorMessage, log, logError } from "./log.js"
|
|
29
29
|
import type { Outcome } from "./metrics.js"
|
|
@@ -115,7 +115,7 @@ export interface UsageCounts<Latency = LatencyHistogram> {
|
|
|
115
115
|
cacheMisses: number
|
|
116
116
|
/** Retries on the next target of an alias after a failure before any output. */
|
|
117
117
|
fallbacks: number
|
|
118
|
-
/** Calls refused with 429 by a concurrency cap
|
|
118
|
+
/** Calls refused with 429 by a concurrency cap, a requests-per-minute limit or a budget (also counted in `requests` and `errors.client`). */
|
|
119
119
|
limited: number
|
|
120
120
|
/** Answered calls that spent tokens but had no price in the catalog (their cost counts as 0). */
|
|
121
121
|
unpricedRequests: number
|
|
@@ -137,6 +137,17 @@ export interface UsageDay extends UsageTotals {
|
|
|
137
137
|
perModel: Record<string, ModelUsage>
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** One ceiling of an app's budget, with what its current window has used (`GET /v1/usage`, see `src/budgets.ts`). */
|
|
141
|
+
export interface BudgetStatus {
|
|
142
|
+
readonly metric: BudgetMetric
|
|
143
|
+
readonly window: BudgetWindow
|
|
144
|
+
readonly limit: number
|
|
145
|
+
/** Counted so far in the current window; a cost is in USD. */
|
|
146
|
+
readonly used: number
|
|
147
|
+
/** ISO 8601 time the current window ends and its count starts again. */
|
|
148
|
+
readonly resetsAt: string
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
export interface UsageAppReport {
|
|
141
152
|
/** Sums over the days of the range. */
|
|
142
153
|
totals: UsageTotals
|
|
@@ -144,6 +155,8 @@ export interface UsageAppReport {
|
|
|
144
155
|
models: Record<string, ModelUsage>
|
|
145
156
|
/** Days with any record in the range, oldest first. */
|
|
146
157
|
days: UsageDay[]
|
|
158
|
+
/** The app's budget state, when option `budgets` sets one for it. Read from the current window, not from the range. */
|
|
159
|
+
budgets?: BudgetStatus[]
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
/** The result of `GET /v1/usage` and of the `opencode-plugboard.usage` RPC (`src/rpc.ts`). */
|
|
@@ -235,7 +248,7 @@ export function tokenBreakdown(usage: PlugUsage): UsageTokens {
|
|
|
235
248
|
}
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
const totalTokens = (tokens: UsageTokens): number => tokens.input + tokens.cacheRead + tokens.cacheWrite + tokens.output + tokens.reasoning
|
|
251
|
+
export const totalTokens = (tokens: UsageTokens): number => tokens.input + tokens.cacheRead + tokens.cacheWrite + tokens.output + tokens.reasoning
|
|
239
252
|
|
|
240
253
|
/**
|
|
241
254
|
* The USD cost of one call, or undefined when the model has no applicable price. Mirrors OpenCode's
|
|
@@ -399,8 +412,12 @@ export interface UsageRecorder {
|
|
|
399
412
|
limited(app: string): void
|
|
400
413
|
fallback(app: string): void
|
|
401
414
|
call(input: CallUsage): void
|
|
415
|
+
/** The USD cost of a call's tokens under the serving catalog's price for `ref`, or undefined when unpriced (`x-plugboard-usage`, #52). */
|
|
416
|
+
price(ref: ModelRef, tokens: UsageTokens): Promise<number | undefined>
|
|
402
417
|
/** A report for the range and app (defaults as `resolveRange`). A bad input is a 400 `PlugError`. */
|
|
403
418
|
query(input: UsageQuery): Promise<UsageReport | PlugError>
|
|
419
|
+
/** The sum of one app's days from `from` to `to` (UTC, inclusive, `YYYY-MM-DD`), memory first. Read by the budget check; a storage failure rejects. */
|
|
420
|
+
spent(app: string, from: string, to: string): Promise<DayRecord>
|
|
404
421
|
}
|
|
405
422
|
|
|
406
423
|
export interface UsageRecorderDeps {
|
|
@@ -582,6 +599,17 @@ export function createUsageRecorder(deps: UsageRecorderDeps): UsageRecorder {
|
|
|
582
599
|
})().catch((error) => logError("usage.record_failed", { app: input.app, error: errorMessage(error) }))
|
|
583
600
|
},
|
|
584
601
|
|
|
602
|
+
price: priceOf,
|
|
603
|
+
async spent(app, from, to) {
|
|
604
|
+
const parts: DayRecord[] = []
|
|
605
|
+
const last = Date.parse(`${to}T00:00:00Z`)
|
|
606
|
+
for (let at = Date.parse(`${from}T00:00:00Z`); at <= last; at += DAY_MS) {
|
|
607
|
+
const entry = await load(app, utcDate(at), false)
|
|
608
|
+
if (entry) parts.push(entry.day)
|
|
609
|
+
}
|
|
610
|
+
return sumDays(parts)
|
|
611
|
+
},
|
|
612
|
+
|
|
585
613
|
async query(input) {
|
|
586
614
|
const at = now()
|
|
587
615
|
if (input.app !== undefined && !APP_NAME_PATTERN.test(input.app))
|
|
@@ -659,6 +687,18 @@ export function withUsage(inner: ModelCaller, recorder: UsageRecorder | undefine
|
|
|
659
687
|
if (event.type === "finish") {
|
|
660
688
|
answered = event.reason !== "error"
|
|
661
689
|
usage = event.usage
|
|
690
|
+
if (answered && context.onUsage) {
|
|
691
|
+
// The header is set before the finish reaches the front-end, so a non-streaming answer carries it.
|
|
692
|
+
const ref = served ? parseModelRef(served.model) : request.ref
|
|
693
|
+
const tokens = tokenBreakdown(usage ?? {})
|
|
694
|
+
const costUsd = ref ? await recorder.price(ref, tokens) : undefined
|
|
695
|
+
context.onUsage({
|
|
696
|
+
inputTokens: usage?.inputTokens ?? 0,
|
|
697
|
+
outputTokens: usage?.outputTokens ?? 0,
|
|
698
|
+
cacheReadTokens: usage?.cacheReadTokens ?? 0,
|
|
699
|
+
...(costUsd === undefined ? {} : { costUsd }),
|
|
700
|
+
})
|
|
701
|
+
}
|
|
662
702
|
}
|
|
663
703
|
yield event
|
|
664
704
|
}
|