@qadanm/opencode-smart-router 0.0.0-stage → 0.4.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/CHANGELOG.md +118 -0
- package/CONTRIBUTING.md +96 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/SECURITY.md +174 -0
- package/config.example.json +108 -0
- package/dist/allowance.js +487 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/index.js +11111 -0
- package/dist/npm/index.js +40 -0
- package/dist/tui.js +587 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +120 -0
- package/docs/LIMITATIONS.md +213 -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 +72 -0
- package/install +75 -0
- package/package.json +92 -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 +1030 -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 +203 -0
- package/src/core/models/seed.ts +771 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +481 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +578 -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 +628 -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 +1273 -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 +919 -0
- package/src/core/types.ts +825 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1670 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +48 -0
- package/src/rpc.ts +204 -0
- package/src/server/controller.ts +3281 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +671 -0
- package/src/tui/index.tsx +168 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ProviderQuotaSnapshot, ProviderSettings, QuotaWindow } from "../types.js"
|
|
2
|
+
import { clamp, describePlan, pct, redact } from "../util.js"
|
|
3
|
+
import { resolvePlan } from "../plans/catalog.js"
|
|
4
|
+
|
|
5
|
+
const USAGE_URL = "https://opencode.ai/zen/go/v1/usage"
|
|
6
|
+
const USER_AGENT = "opencode-smart-router/0.1"
|
|
7
|
+
const MAX_BODY_BYTES = 256_000
|
|
8
|
+
|
|
9
|
+
export interface GoUsageProbeInput {
|
|
10
|
+
readonly accessToken: string
|
|
11
|
+
readonly settings: ProviderSettings
|
|
12
|
+
readonly fetchImpl?: (input: string, init?: RequestInit) => Promise<Response>
|
|
13
|
+
readonly now?: () => number
|
|
14
|
+
readonly timeoutMs?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GoUsageProbeResult {
|
|
18
|
+
readonly snapshot: ProviderQuotaSnapshot
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
22
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
23
|
+
|
|
24
|
+
const timeValue = (value: unknown): number | undefined => {
|
|
25
|
+
if (typeof value !== "string") return undefined
|
|
26
|
+
const parsed = Date.parse(value)
|
|
27
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parseMeter = (value: unknown): QuotaWindow | undefined => {
|
|
31
|
+
if (!isRecord(value)) return undefined
|
|
32
|
+
const percentValue = value.percent
|
|
33
|
+
if (typeof percentValue !== "number" || !Number.isFinite(percentValue)) return undefined
|
|
34
|
+
const usedPct = clamp(percentValue, 0, 100)
|
|
35
|
+
return {
|
|
36
|
+
usedPct,
|
|
37
|
+
remainingPct: clamp(100 - usedPct, 0, 100),
|
|
38
|
+
resetsAt: timeValue(value.resetsAt),
|
|
39
|
+
exceeded: usedPct >= 100,
|
|
40
|
+
source: "provider",
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const unknownWindow: QuotaWindow = { usedPct: 0, remainingPct: 100, source: "unknown" }
|
|
45
|
+
|
|
46
|
+
const baseSnapshot = (
|
|
47
|
+
input: GoUsageProbeInput,
|
|
48
|
+
now: number,
|
|
49
|
+
error?: string,
|
|
50
|
+
retryAfterMs?: number,
|
|
51
|
+
): ProviderQuotaSnapshot => ({
|
|
52
|
+
providerID: "opencode-go",
|
|
53
|
+
label: input.settings.plan === "go-plus" ? "OpenCode Go Plus" : "OpenCode Go",
|
|
54
|
+
plan: describePlan(resolvePlan("opencode-go", input.settings.plan)?.planID, input.settings.plan),
|
|
55
|
+
source: "unknown",
|
|
56
|
+
fetchedAt: now,
|
|
57
|
+
stale: false,
|
|
58
|
+
rolling: unknownWindow,
|
|
59
|
+
weekly: unknownWindow,
|
|
60
|
+
monthly: unknownWindow,
|
|
61
|
+
models: {},
|
|
62
|
+
notes: ["Usage endpoint is read-only and consumes no inference allowance."],
|
|
63
|
+
error,
|
|
64
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read OpenCode Go's provider-reported rolling, weekly, and monthly meters.
|
|
69
|
+
* The endpoint accepts the OAuth access token from OpenCode's Go connection.
|
|
70
|
+
* It is a best-effort integration, not part of the public inference schema.
|
|
71
|
+
*/
|
|
72
|
+
export const probeOpenCodeGoQuota = async (input: GoUsageProbeInput): Promise<GoUsageProbeResult> => {
|
|
73
|
+
const now = input.now?.() ?? Date.now()
|
|
74
|
+
const fetchImpl = input.fetchImpl ?? fetch
|
|
75
|
+
const timeoutMs = input.timeoutMs ?? 8_000
|
|
76
|
+
if (!input.accessToken) {
|
|
77
|
+
return { snapshot: baseSnapshot(input, now, "No OpenCode Go access token is available") }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const controller = new AbortController()
|
|
81
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetchImpl(USAGE_URL, {
|
|
84
|
+
method: "GET",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${input.accessToken}`,
|
|
87
|
+
Accept: "application/json",
|
|
88
|
+
"User-Agent": USER_AGENT,
|
|
89
|
+
},
|
|
90
|
+
redirect: "error",
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
})
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const retryAfter = Number(response.headers.get("retry-after"))
|
|
95
|
+
const retryAfterMs =
|
|
96
|
+
Number.isFinite(retryAfter) && retryAfter >= 0
|
|
97
|
+
? Math.min(retryAfter * 1000, 15 * 60_000)
|
|
98
|
+
: undefined
|
|
99
|
+
return {
|
|
100
|
+
snapshot: baseSnapshot(input, now, `Usage endpoint returned HTTP ${response.status}`, retryAfterMs),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const text = await response.text()
|
|
104
|
+
if (text.length > MAX_BODY_BYTES) {
|
|
105
|
+
return { snapshot: baseSnapshot(input, now, "Usage response was unexpectedly large") }
|
|
106
|
+
}
|
|
107
|
+
const body = JSON.parse(text) as unknown
|
|
108
|
+
const usage = isRecord(body) ? body.usage : undefined
|
|
109
|
+
if (!isRecord(usage)) {
|
|
110
|
+
return { snapshot: baseSnapshot(input, now, "Unrecognized usage response") }
|
|
111
|
+
}
|
|
112
|
+
const rolling = parseMeter(usage.rolling)
|
|
113
|
+
const weekly = parseMeter(usage.weekly)
|
|
114
|
+
const monthly = parseMeter(usage.monthly)
|
|
115
|
+
if (!rolling && !weekly && !monthly) {
|
|
116
|
+
return { snapshot: baseSnapshot(input, now, "Usage response contained no recognized meters") }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const notes = ["Provider-reported OpenCode Go usage.", "Read-only probe; no inference request is made."]
|
|
120
|
+
const snapshot: ProviderQuotaSnapshot = {
|
|
121
|
+
providerID: "opencode-go",
|
|
122
|
+
label: input.settings.plan === "go-plus" ? "OpenCode Go Plus" : "OpenCode Go",
|
|
123
|
+
plan: describePlan(resolvePlan("opencode-go", input.settings.plan)?.planID, input.settings.plan),
|
|
124
|
+
source: "provider",
|
|
125
|
+
fetchedAt: now,
|
|
126
|
+
stale: false,
|
|
127
|
+
rolling: rolling ?? unknownWindow,
|
|
128
|
+
weekly: weekly ?? unknownWindow,
|
|
129
|
+
monthly: monthly ?? unknownWindow,
|
|
130
|
+
models: {},
|
|
131
|
+
notes,
|
|
132
|
+
}
|
|
133
|
+
return { snapshot }
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
snapshot: baseSnapshot(
|
|
137
|
+
input,
|
|
138
|
+
now,
|
|
139
|
+
controller.signal.aborted ? "Usage request timed out" : redact(error instanceof Error ? error.message : String(error)),
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timer)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Exposed for tests. */
|
|
148
|
+
export const __goInternals = { parseMeter, timeValue, pct }
|