@jonsoc/console-app 1.1.34

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 (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. package/vite.config.ts +25 -0
@@ -0,0 +1,784 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { and, Database, eq, isNull, lt, or, sql } from "@jonsoc/console-core/drizzle/index.js"
3
+ import { KeyTable } from "@jonsoc/console-core/schema/key.sql.js"
4
+ import { BillingTable, SubscriptionTable, UsageTable } from "@jonsoc/console-core/schema/billing.sql.js"
5
+ import { centsToMicroCents } from "@jonsoc/console-core/util/price.js"
6
+ import { getWeekBounds } from "@jonsoc/console-core/util/date.js"
7
+ import { Identifier } from "@jonsoc/console-core/identifier.js"
8
+ import { Billing } from "@jonsoc/console-core/billing.js"
9
+ import { Actor } from "@jonsoc/console-core/actor.js"
10
+ import { WorkspaceTable } from "@jonsoc/console-core/schema/workspace.sql.js"
11
+ import { ZenData } from "@jonsoc/console-core/model.js"
12
+ import { Black, BlackData } from "@jonsoc/console-core/black.js"
13
+ import { UserTable } from "@jonsoc/console-core/schema/user.sql.js"
14
+ import { ModelTable } from "@jonsoc/console-core/schema/model.sql.js"
15
+ import { ProviderTable } from "@jonsoc/console-core/schema/provider.sql.js"
16
+ import { logger } from "./logger"
17
+ import {
18
+ AuthError,
19
+ CreditsError,
20
+ MonthlyLimitError,
21
+ SubscriptionError,
22
+ UserLimitError,
23
+ ModelError,
24
+ RateLimitError,
25
+ } from "./error"
26
+ import { createBodyConverter, createStreamPartConverter, createResponseConverter, UsageInfo } from "./provider/provider"
27
+ import { anthropicHelper } from "./provider/anthropic"
28
+ import { googleHelper } from "./provider/google"
29
+ import { openaiHelper } from "./provider/openai"
30
+ import { oaCompatHelper } from "./provider/openai-compatible"
31
+ import { createRateLimiter } from "./rateLimiter"
32
+ import { createDataDumper } from "./dataDumper"
33
+ import { createTrialLimiter } from "./trialLimiter"
34
+ import { createStickyTracker } from "./stickyProviderTracker"
35
+
36
+ type ZenData = Awaited<ReturnType<typeof ZenData.list>>
37
+ type RetryOptions = {
38
+ excludeProviders: string[]
39
+ retryCount: number
40
+ }
41
+
42
+ export async function handler(
43
+ input: APIEvent,
44
+ opts: {
45
+ format: ZenData.Format
46
+ parseApiKey: (headers: Headers) => string | undefined
47
+ parseModel: (url: string, body: any) => string
48
+ parseIsStream: (url: string, body: any) => boolean
49
+ },
50
+ ) {
51
+ type AuthInfo = Awaited<ReturnType<typeof authenticate>>
52
+ type ModelInfo = Awaited<ReturnType<typeof validateModel>>
53
+ type ProviderInfo = Awaited<ReturnType<typeof selectProvider>>
54
+
55
+ const MAX_RETRIES = 3
56
+ const FREE_WORKSPACES = [
57
+ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
58
+ "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // jonsoc bench
59
+ ]
60
+
61
+ try {
62
+ const url = input.request.url
63
+ const body = await input.request.json()
64
+ const model = opts.parseModel(url, body)
65
+ const isStream = opts.parseIsStream(url, body)
66
+ const ip = input.request.headers.get("x-real-ip") ?? ""
67
+ const sessionId = input.request.headers.get("x-jonsoc-session") ?? ""
68
+ const requestId = input.request.headers.get("x-jonsoc-request") ?? ""
69
+ const projectId = input.request.headers.get("x-jonsoc-project") ?? ""
70
+ const ocClient = input.request.headers.get("x-jonsoc-client") ?? ""
71
+ logger.metric({
72
+ is_tream: isStream,
73
+ session: sessionId,
74
+ request: requestId,
75
+ client: ocClient,
76
+ })
77
+ const zenData = ZenData.list()
78
+ const modelInfo = validateModel(zenData, model)
79
+ const dataDumper = createDataDumper(sessionId, requestId, projectId)
80
+ const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
81
+ const isTrial = await trialLimiter?.isTrial()
82
+ const rateLimiter = createRateLimiter(modelInfo.rateLimit, ip)
83
+ await rateLimiter?.check()
84
+ const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
85
+ const stickyProvider = await stickyTracker?.get()
86
+ const authInfo = await authenticate(modelInfo)
87
+ const billingSource = validateBilling(authInfo, modelInfo)
88
+
89
+ const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
90
+ const providerInfo = selectProvider(
91
+ model,
92
+ zenData,
93
+ authInfo,
94
+ modelInfo,
95
+ sessionId,
96
+ isTrial ?? false,
97
+ retry,
98
+ stickyProvider,
99
+ )
100
+ validateModelSettings(authInfo)
101
+ updateProviderKey(authInfo, providerInfo)
102
+ logger.metric({ provider: providerInfo.id })
103
+
104
+ const startTimestamp = Date.now()
105
+ const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream)
106
+ const reqBody = JSON.stringify(
107
+ providerInfo.modifyBody({
108
+ ...createBodyConverter(opts.format, providerInfo.format)(body),
109
+ model: providerInfo.model,
110
+ }),
111
+ )
112
+ logger.debug("REQUEST URL: " + reqUrl)
113
+ logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...")
114
+ const res = await fetch(reqUrl, {
115
+ method: "POST",
116
+ headers: (() => {
117
+ const headers = new Headers(input.request.headers)
118
+ providerInfo.modifyHeaders(headers, body, providerInfo.apiKey)
119
+ Object.entries(providerInfo.headerMappings ?? {}).forEach(([k, v]) => {
120
+ headers.set(k, headers.get(v)!)
121
+ })
122
+ headers.delete("host")
123
+ headers.delete("content-length")
124
+ headers.delete("x-jonsoc-request")
125
+ headers.delete("x-jonsoc-session")
126
+ headers.delete("x-jonsoc-project")
127
+ headers.delete("x-jonsoc-client")
128
+ return headers
129
+ })(),
130
+ body: reqBody,
131
+ })
132
+
133
+ // Try another provider => stop retrying if using fallback provider
134
+ if (
135
+ res.status !== 200 &&
136
+ // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
137
+ res.status !== 404 &&
138
+ // ie. cannot change codex model providers mid-session
139
+ modelInfo.stickyProvider !== "strict" &&
140
+ modelInfo.fallbackProvider &&
141
+ providerInfo.id !== modelInfo.fallbackProvider
142
+ ) {
143
+ return retriableRequest({
144
+ excludeProviders: [...retry.excludeProviders, providerInfo.id],
145
+ retryCount: retry.retryCount + 1,
146
+ })
147
+ }
148
+
149
+ return { providerInfo, reqBody, res, startTimestamp }
150
+ }
151
+
152
+ const { providerInfo, reqBody, res, startTimestamp } = await retriableRequest()
153
+
154
+ // Store model request
155
+ dataDumper?.provideModel(providerInfo.storeModel)
156
+ dataDumper?.provideRequest(reqBody)
157
+
158
+ // Store sticky provider
159
+ await stickyTracker?.set(providerInfo.id)
160
+
161
+ // Temporarily change 404 to 400 status code b/c solid start automatically override 404 response
162
+ const resStatus = res.status === 404 ? 400 : res.status
163
+
164
+ // Scrub response headers
165
+ const resHeaders = new Headers()
166
+ const keepHeaders = ["content-type", "cache-control"]
167
+ for (const [k, v] of res.headers.entries()) {
168
+ if (keepHeaders.includes(k.toLowerCase())) {
169
+ resHeaders.set(k, v)
170
+ }
171
+ }
172
+ logger.debug("STATUS: " + res.status + " " + res.statusText)
173
+
174
+ // Handle non-streaming response
175
+ if (!isStream) {
176
+ const responseConverter = createResponseConverter(providerInfo.format, opts.format)
177
+ const json = await res.json()
178
+ const body = JSON.stringify(responseConverter(json))
179
+ logger.metric({ response_length: body.length })
180
+ logger.debug("RESPONSE: " + body)
181
+ dataDumper?.provideResponse(body)
182
+ dataDumper?.flush()
183
+ const tokensInfo = providerInfo.normalizeUsage(json.usage)
184
+ await trialLimiter?.track(tokensInfo)
185
+ await rateLimiter?.track()
186
+ const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
187
+ await reload(authInfo, costInfo)
188
+ return new Response(body, {
189
+ status: resStatus,
190
+ statusText: res.statusText,
191
+ headers: resHeaders,
192
+ })
193
+ }
194
+
195
+ // Handle streaming response
196
+ const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
197
+ const usageParser = providerInfo.createUsageParser()
198
+ const binaryDecoder = providerInfo.createBinaryStreamDecoder()
199
+ const stream = new ReadableStream({
200
+ start(c) {
201
+ const reader = res.body?.getReader()
202
+ const decoder = new TextDecoder()
203
+ const encoder = new TextEncoder()
204
+
205
+ let buffer = ""
206
+ let responseLength = 0
207
+
208
+ function pump(): Promise<void> {
209
+ return (
210
+ reader?.read().then(async ({ done, value: rawValue }) => {
211
+ if (done) {
212
+ logger.metric({
213
+ response_length: responseLength,
214
+ "timestamp.last_byte": Date.now(),
215
+ })
216
+ dataDumper?.flush()
217
+ await rateLimiter?.track()
218
+ const usage = usageParser.retrieve()
219
+ if (usage) {
220
+ const tokensInfo = providerInfo.normalizeUsage(usage)
221
+ await trialLimiter?.track(tokensInfo)
222
+ const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, billingSource, tokensInfo)
223
+ await reload(authInfo, costInfo)
224
+ }
225
+ c.close()
226
+ return
227
+ }
228
+
229
+ if (responseLength === 0) {
230
+ const now = Date.now()
231
+ logger.metric({
232
+ time_to_first_byte: now - startTimestamp,
233
+ "timestamp.first_byte": now,
234
+ })
235
+ }
236
+
237
+ const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue
238
+ if (!value) return
239
+
240
+ responseLength += value.length
241
+ buffer += decoder.decode(value, { stream: true })
242
+ dataDumper?.provideStream(buffer)
243
+
244
+ const parts = buffer.split(providerInfo.streamSeparator)
245
+ buffer = parts.pop() ?? ""
246
+
247
+ for (let part of parts) {
248
+ logger.debug("PART: " + part)
249
+
250
+ part = part.trim()
251
+ usageParser.parse(part)
252
+
253
+ if (providerInfo.format !== opts.format) {
254
+ part = streamConverter(part)
255
+ c.enqueue(encoder.encode(part + "\n\n"))
256
+ }
257
+ }
258
+
259
+ if (providerInfo.format === opts.format) {
260
+ c.enqueue(value)
261
+ }
262
+
263
+ return pump()
264
+ }) || Promise.resolve()
265
+ )
266
+ }
267
+
268
+ return pump()
269
+ },
270
+ })
271
+
272
+ return new Response(stream, {
273
+ status: resStatus,
274
+ statusText: res.statusText,
275
+ headers: resHeaders,
276
+ })
277
+ } catch (error: any) {
278
+ logger.metric({
279
+ "error.type": error.constructor.name,
280
+ "error.message": error.message,
281
+ })
282
+
283
+ // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
284
+ if (
285
+ error instanceof AuthError ||
286
+ error instanceof CreditsError ||
287
+ error instanceof MonthlyLimitError ||
288
+ error instanceof UserLimitError ||
289
+ error instanceof ModelError
290
+ )
291
+ return new Response(
292
+ JSON.stringify({
293
+ type: "error",
294
+ error: { type: error.constructor.name, message: error.message },
295
+ }),
296
+ { status: 401 },
297
+ )
298
+
299
+ if (error instanceof RateLimitError || error instanceof SubscriptionError) {
300
+ const headers = new Headers()
301
+ if (error instanceof SubscriptionError && error.retryAfter) {
302
+ headers.set("retry-after", String(error.retryAfter))
303
+ }
304
+ return new Response(
305
+ JSON.stringify({
306
+ type: "error",
307
+ error: { type: error.constructor.name, message: error.message },
308
+ }),
309
+ { status: 429, headers },
310
+ )
311
+ }
312
+
313
+ return new Response(
314
+ JSON.stringify({
315
+ type: "error",
316
+ error: {
317
+ type: "error",
318
+ message: error.message,
319
+ },
320
+ }),
321
+ { status: 500 },
322
+ )
323
+ }
324
+
325
+ function validateModel(zenData: ZenData, reqModel: string) {
326
+ if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
327
+
328
+ const modelId = reqModel as keyof typeof zenData.models
329
+ const modelData = Array.isArray(zenData.models[modelId])
330
+ ? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
331
+ : zenData.models[modelId]
332
+
333
+ if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
334
+
335
+ logger.metric({ model: modelId })
336
+
337
+ return { id: modelId, ...modelData }
338
+ }
339
+
340
+ function selectProvider(
341
+ reqModel: string,
342
+ zenData: ZenData,
343
+ authInfo: AuthInfo,
344
+ modelInfo: ModelInfo,
345
+ sessionId: string,
346
+ isTrial: boolean,
347
+ retry: RetryOptions,
348
+ stickyProvider: string | undefined,
349
+ ) {
350
+ const modelProvider = (() => {
351
+ if (authInfo?.provider?.credentials) {
352
+ return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider)
353
+ }
354
+
355
+ if (isTrial) {
356
+ return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
357
+ }
358
+
359
+ if (stickyProvider) {
360
+ const provider = modelInfo.providers.find((provider) => provider.id === stickyProvider)
361
+ if (provider) return provider
362
+ }
363
+
364
+ if (retry.retryCount === MAX_RETRIES) {
365
+ return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
366
+ }
367
+
368
+ const providers = modelInfo.providers
369
+ .filter((provider) => !provider.disabled)
370
+ .filter((provider) => !retry.excludeProviders.includes(provider.id))
371
+ .flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
372
+
373
+ // Use the last 4 characters of session ID to select a provider
374
+ let h = 0
375
+ const l = sessionId.length
376
+ for (let i = l - 4; i < l; i++) {
377
+ h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int
378
+ }
379
+ const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
380
+ return providers[index || 0]
381
+ })()
382
+
383
+ if (!modelProvider) throw new ModelError("No provider available")
384
+ if (!(modelProvider.id in zenData.providers)) throw new ModelError(`Provider ${modelProvider.id} not supported`)
385
+
386
+ return {
387
+ ...modelProvider,
388
+ ...zenData.providers[modelProvider.id],
389
+ ...(() => {
390
+ const format = zenData.providers[modelProvider.id].format
391
+ const providerModel = modelProvider.model
392
+ if (format === "anthropic") return anthropicHelper({ reqModel, providerModel })
393
+ if (format === "google") return googleHelper({ reqModel, providerModel })
394
+ if (format === "openai") return openaiHelper({ reqModel, providerModel })
395
+ return oaCompatHelper({ reqModel, providerModel })
396
+ })(),
397
+ }
398
+ }
399
+
400
+ async function authenticate(modelInfo: ModelInfo) {
401
+ const apiKey = opts.parseApiKey(input.request.headers)
402
+ if (!apiKey || apiKey === "public") {
403
+ if (modelInfo.allowAnonymous) return
404
+ throw new AuthError("Missing API key.")
405
+ }
406
+
407
+ const data = await Database.use((tx) =>
408
+ tx
409
+ .select({
410
+ apiKey: KeyTable.id,
411
+ workspaceID: KeyTable.workspaceID,
412
+ billing: {
413
+ balance: BillingTable.balance,
414
+ paymentMethodID: BillingTable.paymentMethodID,
415
+ monthlyLimit: BillingTable.monthlyLimit,
416
+ monthlyUsage: BillingTable.monthlyUsage,
417
+ timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
418
+ reloadTrigger: BillingTable.reloadTrigger,
419
+ timeReloadLockedTill: BillingTable.timeReloadLockedTill,
420
+ subscription: BillingTable.subscription,
421
+ },
422
+ user: {
423
+ id: UserTable.id,
424
+ monthlyLimit: UserTable.monthlyLimit,
425
+ monthlyUsage: UserTable.monthlyUsage,
426
+ timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
427
+ },
428
+ subscription: {
429
+ id: SubscriptionTable.id,
430
+ rollingUsage: SubscriptionTable.rollingUsage,
431
+ fixedUsage: SubscriptionTable.fixedUsage,
432
+ timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
433
+ timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
434
+ },
435
+ provider: {
436
+ credentials: ProviderTable.credentials,
437
+ },
438
+ timeDisabled: ModelTable.timeCreated,
439
+ })
440
+ .from(KeyTable)
441
+ .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
442
+ .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID))
443
+ .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID)))
444
+ .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, modelInfo.id)))
445
+ .leftJoin(
446
+ ProviderTable,
447
+ modelInfo.byokProvider
448
+ ? and(
449
+ eq(ProviderTable.workspaceID, KeyTable.workspaceID),
450
+ eq(ProviderTable.provider, modelInfo.byokProvider),
451
+ )
452
+ : sql`false`,
453
+ )
454
+ .leftJoin(
455
+ SubscriptionTable,
456
+ and(
457
+ eq(SubscriptionTable.workspaceID, KeyTable.workspaceID),
458
+ eq(SubscriptionTable.userID, KeyTable.userID),
459
+ isNull(SubscriptionTable.timeDeleted),
460
+ ),
461
+ )
462
+ .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
463
+ .then((rows) => rows[0]),
464
+ )
465
+
466
+ if (!data) throw new AuthError("Invalid API key.")
467
+ logger.metric({
468
+ api_key: data.apiKey,
469
+ workspace: data.workspaceID,
470
+ isSubscription: data.subscription ? true : false,
471
+ subscription: data.billing.subscription?.plan,
472
+ })
473
+
474
+ return {
475
+ apiKeyId: data.apiKey,
476
+ workspaceID: data.workspaceID,
477
+ billing: data.billing,
478
+ user: data.user,
479
+ subscription: data.subscription,
480
+ provider: data.provider,
481
+ isFree: FREE_WORKSPACES.includes(data.workspaceID),
482
+ isDisabled: !!data.timeDisabled,
483
+ }
484
+ }
485
+
486
+ function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo) {
487
+ if (!authInfo) return "anonymous"
488
+ if (authInfo.provider?.credentials) return "free"
489
+ if (authInfo.isFree) return "free"
490
+ if (modelInfo.allowAnonymous) return "free"
491
+
492
+ // Validate subscription billing
493
+ if (authInfo.billing.subscription && authInfo.subscription) {
494
+ try {
495
+ const sub = authInfo.subscription
496
+ const plan = authInfo.billing.subscription.plan
497
+
498
+ const formatRetryTime = (seconds: number) => {
499
+ const days = Math.floor(seconds / 86400)
500
+ if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
501
+ const hours = Math.floor(seconds / 3600)
502
+ const minutes = Math.ceil((seconds % 3600) / 60)
503
+ if (hours >= 1) return `${hours}hr ${minutes}min`
504
+ return `${minutes}min`
505
+ }
506
+
507
+ // Check weekly limit
508
+ if (sub.fixedUsage && sub.timeFixedUpdated) {
509
+ const result = Black.analyzeWeeklyUsage({
510
+ plan,
511
+ usage: sub.fixedUsage,
512
+ timeUpdated: sub.timeFixedUpdated,
513
+ })
514
+ if (result.status === "rate-limited")
515
+ throw new SubscriptionError(
516
+ `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
517
+ result.resetInSec,
518
+ )
519
+ }
520
+
521
+ // Check rolling limit
522
+ if (sub.rollingUsage && sub.timeRollingUpdated) {
523
+ const result = Black.analyzeRollingUsage({
524
+ plan,
525
+ usage: sub.rollingUsage,
526
+ timeUpdated: sub.timeRollingUpdated,
527
+ })
528
+ if (result.status === "rate-limited")
529
+ throw new SubscriptionError(
530
+ `Subscription quota exceeded. Retry in ${formatRetryTime(result.resetInSec)}.`,
531
+ result.resetInSec,
532
+ )
533
+ }
534
+
535
+ return "subscription"
536
+ } catch (e) {
537
+ if (!authInfo.billing.subscription.useBalance) throw e
538
+ }
539
+ }
540
+
541
+ // Validate pay as you go billing
542
+ const billing = authInfo.billing
543
+ if (!billing.paymentMethodID)
544
+ throw new CreditsError(
545
+ `No payment method. Add a payment method here: https://jonsoc.com/workspace/${authInfo.workspaceID}/billing`,
546
+ )
547
+ if (billing.balance <= 0)
548
+ throw new CreditsError(
549
+ `Insufficient balance. Manage your billing here: https://jonsoc.com/workspace/${authInfo.workspaceID}/billing`,
550
+ )
551
+
552
+ const now = new Date()
553
+ const currentYear = now.getUTCFullYear()
554
+ const currentMonth = now.getUTCMonth()
555
+ if (
556
+ billing.monthlyLimit &&
557
+ billing.monthlyUsage &&
558
+ billing.timeMonthlyUsageUpdated &&
559
+ billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) &&
560
+ currentYear === billing.timeMonthlyUsageUpdated.getUTCFullYear() &&
561
+ currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth()
562
+ )
563
+ throw new MonthlyLimitError(
564
+ `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}. Manage your limits here: https://jonsoc.com/workspace/${authInfo.workspaceID}/billing`,
565
+ )
566
+
567
+ if (
568
+ authInfo.user.monthlyLimit &&
569
+ authInfo.user.monthlyUsage &&
570
+ authInfo.user.timeMonthlyUsageUpdated &&
571
+ authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) &&
572
+ currentYear === authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() &&
573
+ currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth()
574
+ )
575
+ throw new UserLimitError(
576
+ `You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}. Manage your limits here: https://jonsoc.com/workspace/${authInfo.workspaceID}/members`,
577
+ )
578
+
579
+ return "balance"
580
+ }
581
+
582
+ function validateModelSettings(authInfo: AuthInfo) {
583
+ if (!authInfo) return
584
+ if (authInfo.isDisabled) throw new ModelError("Model is disabled")
585
+ }
586
+
587
+ function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) {
588
+ if (!authInfo?.provider?.credentials) return
589
+ providerInfo.apiKey = authInfo.provider.credentials
590
+ }
591
+
592
+ async function trackUsage(
593
+ authInfo: AuthInfo,
594
+ modelInfo: ModelInfo,
595
+ providerInfo: ProviderInfo,
596
+ billingSource: ReturnType<typeof validateBilling>,
597
+ usageInfo: UsageInfo,
598
+ ) {
599
+ const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
600
+ usageInfo
601
+
602
+ const modelCost =
603
+ modelInfo.cost200K &&
604
+ inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000
605
+ ? modelInfo.cost200K
606
+ : modelInfo.cost
607
+
608
+ const inputCost = modelCost.input * inputTokens * 100
609
+ const outputCost = modelCost.output * outputTokens * 100
610
+ const reasoningCost = (() => {
611
+ if (!reasoningTokens) return undefined
612
+ return modelCost.output * reasoningTokens * 100
613
+ })()
614
+ const cacheReadCost = (() => {
615
+ if (!cacheReadTokens) return undefined
616
+ if (!modelCost.cacheRead) return undefined
617
+ return modelCost.cacheRead * cacheReadTokens * 100
618
+ })()
619
+ const cacheWrite5mCost = (() => {
620
+ if (!cacheWrite5mTokens) return undefined
621
+ if (!modelCost.cacheWrite5m) return undefined
622
+ return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
623
+ })()
624
+ const cacheWrite1hCost = (() => {
625
+ if (!cacheWrite1hTokens) return undefined
626
+ if (!modelCost.cacheWrite1h) return undefined
627
+ return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
628
+ })()
629
+ const totalCostInCent =
630
+ inputCost +
631
+ outputCost +
632
+ (reasoningCost ?? 0) +
633
+ (cacheReadCost ?? 0) +
634
+ (cacheWrite5mCost ?? 0) +
635
+ (cacheWrite1hCost ?? 0)
636
+
637
+ logger.metric({
638
+ "tokens.input": inputTokens,
639
+ "tokens.output": outputTokens,
640
+ "tokens.reasoning": reasoningTokens,
641
+ "tokens.cache_read": cacheReadTokens,
642
+ "tokens.cache_write_5m": cacheWrite5mTokens,
643
+ "tokens.cache_write_1h": cacheWrite1hTokens,
644
+ "cost.input": Math.round(inputCost),
645
+ "cost.output": Math.round(outputCost),
646
+ "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
647
+ "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
648
+ "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
649
+ "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
650
+ "cost.total": Math.round(totalCostInCent),
651
+ })
652
+
653
+ if (billingSource === "anonymous") return
654
+ authInfo = authInfo!
655
+
656
+ const cost = authInfo.provider?.credentials ? 0 : centsToMicroCents(totalCostInCent)
657
+ await Database.use((db) =>
658
+ Promise.all([
659
+ db.insert(UsageTable).values({
660
+ workspaceID: authInfo.workspaceID,
661
+ id: Identifier.create("usage"),
662
+ model: modelInfo.id,
663
+ provider: providerInfo.id,
664
+ inputTokens,
665
+ outputTokens,
666
+ reasoningTokens,
667
+ cacheReadTokens,
668
+ cacheWrite5mTokens,
669
+ cacheWrite1hTokens,
670
+ cost,
671
+ keyID: authInfo.apiKeyId,
672
+ enrichment: billingSource === "subscription" ? { plan: "sub" } : undefined,
673
+ }),
674
+ db
675
+ .update(KeyTable)
676
+ .set({ timeUsed: sql`now()` })
677
+ .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
678
+ ...(billingSource === "subscription"
679
+ ? (() => {
680
+ const plan = authInfo.billing.subscription!.plan
681
+ const black = BlackData.getLimits({ plan })
682
+ const week = getWeekBounds(new Date())
683
+ const rollingWindowSeconds = black.rollingWindow * 3600
684
+ return [
685
+ db
686
+ .update(SubscriptionTable)
687
+ .set({
688
+ fixedUsage: sql`
689
+ CASE
690
+ WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}
691
+ ELSE ${cost}
692
+ END
693
+ `,
694
+ timeFixedUpdated: sql`now()`,
695
+ rollingUsage: sql`
696
+ CASE
697
+ WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost}
698
+ ELSE ${cost}
699
+ END
700
+ `,
701
+ timeRollingUpdated: sql`
702
+ CASE
703
+ WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.timeRollingUpdated}
704
+ ELSE now()
705
+ END
706
+ `,
707
+ })
708
+ .where(
709
+ and(
710
+ eq(SubscriptionTable.workspaceID, authInfo.workspaceID),
711
+ eq(SubscriptionTable.userID, authInfo.user.id),
712
+ ),
713
+ ),
714
+ ]
715
+ })()
716
+ : [
717
+ db
718
+ .update(BillingTable)
719
+ .set({
720
+ balance: authInfo.isFree
721
+ ? sql`${BillingTable.balance} - ${0}`
722
+ : sql`${BillingTable.balance} - ${cost}`,
723
+ monthlyUsage: sql`
724
+ CASE
725
+ WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
726
+ ELSE ${cost}
727
+ END
728
+ `,
729
+ timeMonthlyUsageUpdated: sql`now()`,
730
+ })
731
+ .where(eq(BillingTable.workspaceID, authInfo.workspaceID)),
732
+ db
733
+ .update(UserTable)
734
+ .set({
735
+ monthlyUsage: sql`
736
+ CASE
737
+ WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${cost}
738
+ ELSE ${cost}
739
+ END
740
+ `,
741
+ timeMonthlyUsageUpdated: sql`now()`,
742
+ })
743
+ .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))),
744
+ ]),
745
+ ]),
746
+ )
747
+
748
+ return { costInMicroCents: cost }
749
+ }
750
+
751
+ async function reload(authInfo: AuthInfo, costInfo: Awaited<ReturnType<typeof trackUsage>>) {
752
+ if (!authInfo) return
753
+ if (authInfo.isFree) return
754
+ if (authInfo.provider?.credentials) return
755
+ if (authInfo.subscription) return
756
+
757
+ if (!costInfo) return
758
+
759
+ const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100)
760
+ if (authInfo.billing.balance - costInfo.costInMicroCents >= reloadTrigger) return
761
+ if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return
762
+
763
+ const lock = await Database.use((tx) =>
764
+ tx
765
+ .update(BillingTable)
766
+ .set({
767
+ timeReloadLockedTill: sql`now() + interval 1 minute`,
768
+ })
769
+ .where(
770
+ and(
771
+ eq(BillingTable.workspaceID, authInfo.workspaceID),
772
+ eq(BillingTable.reload, true),
773
+ lt(BillingTable.balance, reloadTrigger),
774
+ or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
775
+ ),
776
+ ),
777
+ )
778
+ if (lock.rowsAffected === 0) return
779
+
780
+ await Actor.provide("system", { workspaceID: authInfo.workspaceID }, async () => {
781
+ await Billing.reload()
782
+ })
783
+ }
784
+ }