@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/CONTRIBUTING.md +103 -0
  3. package/LICENSE +21 -0
  4. package/README.md +425 -2
  5. package/SECURITY.md +174 -0
  6. package/config.example.json +107 -0
  7. package/dist/npm/index.js +40 -0
  8. package/docs/ARCHITECTURE.md +104 -0
  9. package/docs/DEBUGGING-GUI.md +70 -0
  10. package/docs/INSTALL.md +151 -0
  11. package/docs/LIMITATIONS.md +230 -0
  12. package/docs/MODELS.md +124 -0
  13. package/docs/OPENDESIGN-INTEGRATION.md +635 -0
  14. package/docs/QA.md +142 -0
  15. package/docs/SHARED-CORE.md +114 -0
  16. package/install +75 -0
  17. package/package.json +94 -4
  18. package/scripts/install.sh +324 -0
  19. package/scripts/uninstall.sh +91 -0
  20. package/src/allowance.ts +135 -0
  21. package/src/core/classify/classifier.ts +305 -0
  22. package/src/core/config-writer.ts +167 -0
  23. package/src/core/config.ts +1047 -0
  24. package/src/core/context/strategy.ts +188 -0
  25. package/src/core/context-health.ts +39 -0
  26. package/src/core/failures.ts +398 -0
  27. package/src/core/history/opencode.ts +173 -0
  28. package/src/core/models/protocol.ts +22 -0
  29. package/src/core/models/registry.ts +204 -0
  30. package/src/core/models/seed.ts +781 -0
  31. package/src/core/offline/mode.ts +321 -0
  32. package/src/core/plan-page.ts +390 -0
  33. package/src/core/plans/catalog.ts +487 -0
  34. package/src/core/policy/calibration.ts +259 -0
  35. package/src/core/policy/eligibility.ts +630 -0
  36. package/src/core/providers/enablement.ts +143 -0
  37. package/src/core/providers/opendesign-credits.ts +206 -0
  38. package/src/core/providers/opendesign-models.ts +654 -0
  39. package/src/core/providers/opendesign.ts +474 -0
  40. package/src/core/quota/calibration.ts +166 -0
  41. package/src/core/quota/commandcode.ts +471 -0
  42. package/src/core/quota/credentials.ts +490 -0
  43. package/src/core/quota/ledger.ts +1304 -0
  44. package/src/core/quota/opencode-go.ts +148 -0
  45. package/src/core/quota/opendesign.ts +1043 -0
  46. package/src/core/quota/reconcile.ts +155 -0
  47. package/src/core/quota/report.ts +261 -0
  48. package/src/core/quota/service.ts +273 -0
  49. package/src/core/reprompt.ts +48 -0
  50. package/src/core/routing/engine.ts +978 -0
  51. package/src/core/types.ts +866 -0
  52. package/src/core/ui.ts +236 -0
  53. package/src/core/util.ts +114 -0
  54. package/src/gui/index.tsx +1675 -0
  55. package/src/gui/viewmodel.ts +456 -0
  56. package/src/index.ts +33 -0
  57. package/src/rpc.ts +204 -0
  58. package/src/server/containment.ts +131 -0
  59. package/src/server/controller.ts +3405 -0
  60. package/src/server/route-tracker.ts +60 -0
  61. package/src/server/shared-core.ts +701 -0
  62. 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 }