@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,210 @@
1
+ import { ZenData } from "@jonsoc/console-core/model.js"
2
+ import {
3
+ fromAnthropicChunk,
4
+ fromAnthropicRequest,
5
+ fromAnthropicResponse,
6
+ toAnthropicChunk,
7
+ toAnthropicRequest,
8
+ toAnthropicResponse,
9
+ } from "./anthropic"
10
+ import {
11
+ fromOpenaiChunk,
12
+ fromOpenaiRequest,
13
+ fromOpenaiResponse,
14
+ toOpenaiChunk,
15
+ toOpenaiRequest,
16
+ toOpenaiResponse,
17
+ } from "./openai"
18
+ import {
19
+ fromOaCompatibleChunk,
20
+ fromOaCompatibleRequest,
21
+ fromOaCompatibleResponse,
22
+ toOaCompatibleChunk,
23
+ toOaCompatibleRequest,
24
+ toOaCompatibleResponse,
25
+ } from "./openai-compatible"
26
+
27
+ export type UsageInfo = {
28
+ inputTokens: number
29
+ outputTokens: number
30
+ reasoningTokens?: number
31
+ cacheReadTokens?: number
32
+ cacheWrite5mTokens?: number
33
+ cacheWrite1hTokens?: number
34
+ }
35
+
36
+ export type ProviderHelper = (input: { reqModel: string; providerModel: string }) => {
37
+ format: ZenData.Format
38
+ modifyUrl: (providerApi: string, isStream?: boolean) => string
39
+ modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
40
+ modifyBody: (body: Record<string, any>) => Record<string, any>
41
+ createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
42
+ streamSeparator: string
43
+ createUsageParser: () => {
44
+ parse: (chunk: string) => void
45
+ retrieve: () => any
46
+ }
47
+ normalizeUsage: (usage: any) => UsageInfo
48
+ }
49
+
50
+ export interface CommonMessage {
51
+ role: "system" | "user" | "assistant" | "tool"
52
+ content?: string | Array<CommonContentPart>
53
+ tool_call_id?: string
54
+ tool_calls?: CommonToolCall[]
55
+ }
56
+
57
+ export interface CommonContentPart {
58
+ type: "text" | "image_url"
59
+ text?: string
60
+ image_url?: { url: string }
61
+ }
62
+
63
+ export interface CommonToolCall {
64
+ id: string
65
+ type: "function"
66
+ function: {
67
+ name: string
68
+ arguments: string
69
+ }
70
+ }
71
+
72
+ export interface CommonTool {
73
+ type: "function"
74
+ function: {
75
+ name: string
76
+ description?: string
77
+ parameters?: Record<string, any>
78
+ }
79
+ }
80
+
81
+ export interface CommonUsage {
82
+ input_tokens?: number
83
+ output_tokens?: number
84
+ total_tokens?: number
85
+ prompt_tokens?: number
86
+ completion_tokens?: number
87
+ cache_read_input_tokens?: number
88
+ cache_creation?: {
89
+ ephemeral_5m_input_tokens?: number
90
+ ephemeral_1h_input_tokens?: number
91
+ }
92
+ input_tokens_details?: {
93
+ cached_tokens?: number
94
+ }
95
+ output_tokens_details?: {
96
+ reasoning_tokens?: number
97
+ }
98
+ }
99
+
100
+ export interface CommonRequest {
101
+ model: string
102
+ max_tokens?: number
103
+ temperature?: number
104
+ top_p?: number
105
+ stop?: string | string[]
106
+ messages: CommonMessage[]
107
+ stream?: boolean
108
+ tools?: CommonTool[]
109
+ tool_choice?: "auto" | "required" | { type: "function"; function: { name: string } }
110
+ }
111
+
112
+ export interface CommonResponse {
113
+ id: string
114
+ object: "chat.completion"
115
+ created: number
116
+ model: string
117
+ choices: Array<{
118
+ index: number
119
+ message: {
120
+ role: "assistant"
121
+ content?: string
122
+ tool_calls?: CommonToolCall[]
123
+ }
124
+ finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
125
+ }>
126
+ usage?: {
127
+ prompt_tokens?: number
128
+ completion_tokens?: number
129
+ total_tokens?: number
130
+ prompt_tokens_details?: { cached_tokens?: number }
131
+ }
132
+ }
133
+
134
+ export interface CommonChunk {
135
+ id: string
136
+ object: "chat.completion.chunk"
137
+ created: number
138
+ model: string
139
+ choices: Array<{
140
+ index: number
141
+ delta: {
142
+ role?: "assistant"
143
+ content?: string
144
+ tool_calls?: Array<{
145
+ index: number
146
+ id?: string
147
+ type?: "function"
148
+ function?: {
149
+ name?: string
150
+ arguments?: string
151
+ }
152
+ }>
153
+ }
154
+ finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
155
+ }>
156
+ usage?: {
157
+ prompt_tokens?: number
158
+ completion_tokens?: number
159
+ total_tokens?: number
160
+ prompt_tokens_details?: { cached_tokens?: number }
161
+ }
162
+ }
163
+
164
+ export function createBodyConverter(from: ZenData.Format, to: ZenData.Format) {
165
+ return (body: any): any => {
166
+ if (from === to) return body
167
+
168
+ let raw: CommonRequest
169
+ if (from === "anthropic") raw = fromAnthropicRequest(body)
170
+ else if (from === "openai") raw = fromOpenaiRequest(body)
171
+ else raw = fromOaCompatibleRequest(body)
172
+
173
+ if (to === "anthropic") return toAnthropicRequest(raw)
174
+ if (to === "openai") return toOpenaiRequest(raw)
175
+ if (to === "oa-compat") return toOaCompatibleRequest(raw)
176
+ }
177
+ }
178
+
179
+ export function createStreamPartConverter(from: ZenData.Format, to: ZenData.Format) {
180
+ return (part: any): any => {
181
+ if (from === to) return part
182
+
183
+ let raw: CommonChunk | string
184
+ if (from === "anthropic") raw = fromAnthropicChunk(part)
185
+ else if (from === "openai") raw = fromOpenaiChunk(part)
186
+ else raw = fromOaCompatibleChunk(part)
187
+
188
+ // If result is a string (error case), pass it through
189
+ if (typeof raw === "string") return raw
190
+
191
+ if (to === "anthropic") return toAnthropicChunk(raw)
192
+ if (to === "openai") return toOpenaiChunk(raw)
193
+ if (to === "oa-compat") return toOaCompatibleChunk(raw)
194
+ }
195
+ }
196
+
197
+ export function createResponseConverter(from: ZenData.Format, to: ZenData.Format) {
198
+ return (response: any): any => {
199
+ if (from === to) return response
200
+
201
+ let raw: CommonResponse
202
+ if (from === "anthropic") raw = fromAnthropicResponse(response)
203
+ else if (from === "openai") raw = fromOpenaiResponse(response)
204
+ else raw = fromOaCompatibleResponse(response)
205
+
206
+ if (to === "anthropic") return toAnthropicResponse(raw)
207
+ if (to === "openai") return toOpenaiResponse(raw)
208
+ if (to === "oa-compat") return toOaCompatibleResponse(raw)
209
+ }
210
+ }
@@ -0,0 +1,41 @@
1
+ import { Database, eq, and, sql, inArray } from "@jonsoc/console-core/drizzle/index.js"
2
+ import { IpRateLimitTable } from "@jonsoc/console-core/schema/ip.sql.js"
3
+ import { RateLimitError } from "./error"
4
+ import { logger } from "./logger"
5
+
6
+ export function createRateLimiter(limit: number | undefined, rawIp: string) {
7
+ if (!limit) return
8
+
9
+ const ip = !rawIp.length ? "unknown" : rawIp
10
+ const now = Date.now()
11
+ const intervals = [buildYYYYMMDDHH(now), buildYYYYMMDDHH(now - 3_600_000), buildYYYYMMDDHH(now - 7_200_000)]
12
+
13
+ return {
14
+ track: async () => {
15
+ await Database.use((tx) =>
16
+ tx
17
+ .insert(IpRateLimitTable)
18
+ .values({ ip, interval: intervals[0], count: 1 })
19
+ .onDuplicateKeyUpdate({ set: { count: sql`${IpRateLimitTable.count} + 1` } }),
20
+ )
21
+ },
22
+ check: async () => {
23
+ const rows = await Database.use((tx) =>
24
+ tx
25
+ .select({ count: IpRateLimitTable.count })
26
+ .from(IpRateLimitTable)
27
+ .where(and(eq(IpRateLimitTable.ip, ip), inArray(IpRateLimitTable.interval, intervals))),
28
+ )
29
+ const total = rows.reduce((sum, r) => sum + r.count, 0)
30
+ logger.debug(`rate limit total: ${total}`)
31
+ if (total >= limit) throw new RateLimitError(`Rate limit exceeded. Please try again later.`)
32
+ },
33
+ }
34
+ }
35
+
36
+ function buildYYYYMMDDHH(timestamp: number) {
37
+ return new Date(timestamp)
38
+ .toISOString()
39
+ .replace(/[^0-9]/g, "")
40
+ .substring(0, 10)
41
+ }
@@ -0,0 +1,16 @@
1
+ import { Resource } from "@jonsoc/console-resource"
2
+
3
+ export function createStickyTracker(stickyProvider: "strict" | "prefer" | undefined, session: string) {
4
+ if (!stickyProvider) return
5
+ if (!session) return
6
+ const key = `sticky:${session}`
7
+
8
+ return {
9
+ get: async () => {
10
+ return await Resource.GatewayKv.get(key)
11
+ },
12
+ set: async (providerId: string) => {
13
+ await Resource.GatewayKv.put(key, providerId, { expirationTtl: 86400 })
14
+ },
15
+ }
16
+ }
@@ -0,0 +1,49 @@
1
+ import { Database, eq, sql } from "@jonsoc/console-core/drizzle/index.js"
2
+ import { IpTable } from "@jonsoc/console-core/schema/ip.sql.js"
3
+ import { UsageInfo } from "./provider/provider"
4
+ import { ZenData } from "@jonsoc/console-core/model.js"
5
+
6
+ export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
7
+ if (!trial) return
8
+ if (!ip) return
9
+
10
+ const limit =
11
+ trial.limits.find((limit) => limit.client === client)?.limit ??
12
+ trial.limits.find((limit) => limit.client === undefined)?.limit
13
+ if (!limit) return
14
+
15
+ let _isTrial: boolean
16
+
17
+ return {
18
+ isTrial: async () => {
19
+ const data = await Database.use((tx) =>
20
+ tx
21
+ .select({
22
+ usage: IpTable.usage,
23
+ })
24
+ .from(IpTable)
25
+ .where(eq(IpTable.ip, ip))
26
+ .then((rows) => rows[0]),
27
+ )
28
+
29
+ _isTrial = (data?.usage ?? 0) < limit
30
+ return _isTrial
31
+ },
32
+ track: async (usageInfo: UsageInfo) => {
33
+ if (!_isTrial) return
34
+ const usage =
35
+ usageInfo.inputTokens +
36
+ usageInfo.outputTokens +
37
+ (usageInfo.reasoningTokens ?? 0) +
38
+ (usageInfo.cacheReadTokens ?? 0) +
39
+ (usageInfo.cacheWrite5mTokens ?? 0) +
40
+ (usageInfo.cacheWrite1hTokens ?? 0)
41
+ await Database.use((tx) =>
42
+ tx
43
+ .insert(IpTable)
44
+ .values({ ip, usage })
45
+ .onDuplicateKeyUpdate({ set: { usage: sql`${IpTable.usage} + ${usage}` } }),
46
+ )
47
+ },
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { handler } from "~/routes/zen/util/handler"
3
+
4
+ export function POST(input: APIEvent) {
5
+ return handler(input, {
6
+ format: "oa-compat",
7
+ parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
8
+ parseModel: (url: string, body: any) => body.model,
9
+ parseIsStream: (url: string, body: any) => !!body.stream,
10
+ })
11
+ }
@@ -0,0 +1,11 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { handler } from "~/routes/zen/util/handler"
3
+
4
+ export function POST(input: APIEvent) {
5
+ return handler(input, {
6
+ format: "anthropic",
7
+ parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
8
+ parseModel: (url: string, body: any) => body.model,
9
+ parseIsStream: (url: string, body: any) => !!body.stream,
10
+ })
11
+ }
@@ -0,0 +1,13 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { handler } from "~/routes/zen/util/handler"
3
+
4
+ export function POST(input: APIEvent) {
5
+ return handler(input, {
6
+ format: "google",
7
+ parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
8
+ parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
9
+ parseIsStream: (url: string, body: any) =>
10
+ // ie. url: https://jonsoc.com/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
11
+ url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
12
+ })
13
+ }
@@ -0,0 +1,60 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { and, Database, eq, isNull } from "@jonsoc/console-core/drizzle/index.js"
3
+ import { KeyTable } from "@jonsoc/console-core/schema/key.sql.js"
4
+ import { WorkspaceTable } from "@jonsoc/console-core/schema/workspace.sql.js"
5
+ import { ModelTable } from "@jonsoc/console-core/schema/model.sql.js"
6
+ import { ZenData } from "@jonsoc/console-core/model.js"
7
+
8
+ export async function OPTIONS(input: APIEvent) {
9
+ return new Response(null, {
10
+ status: 200,
11
+ headers: {
12
+ "Access-Control-Allow-Origin": "*",
13
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
15
+ },
16
+ })
17
+ }
18
+
19
+ export async function GET(input: APIEvent) {
20
+ const zenData = ZenData.list()
21
+ const disabledModels = await authenticate()
22
+
23
+ return new Response(
24
+ JSON.stringify({
25
+ object: "list",
26
+ data: Object.entries(zenData.models)
27
+ .filter(([id]) => !disabledModels.includes(id))
28
+ .map(([id, _model]) => ({
29
+ id,
30
+ object: "model",
31
+ created: Math.floor(Date.now() / 1000),
32
+ owned_by: "jonsoc",
33
+ })),
34
+ }),
35
+ {
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ },
39
+ },
40
+ )
41
+
42
+ async function authenticate() {
43
+ const apiKey = input.request.headers.get("authorization")?.split(" ")[1]
44
+ if (!apiKey) return []
45
+
46
+ const disabledModels = await Database.use((tx) =>
47
+ tx
48
+ .select({
49
+ model: ModelTable.model,
50
+ })
51
+ .from(KeyTable)
52
+ .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
53
+ .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), isNull(ModelTable.timeDeleted)))
54
+ .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
55
+ .then((rows) => rows.map((row) => row.model)),
56
+ )
57
+
58
+ return disabledModels
59
+ }
60
+ }
@@ -0,0 +1,11 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+ import { handler } from "~/routes/zen/util/handler"
3
+
4
+ export function POST(input: APIEvent) {
5
+ return handler(input, {
6
+ format: "openai",
7
+ parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
8
+ parseModel: (url: string, body: any) => body.model,
9
+ parseIsStream: (url: string, body: any) => !!body.stream,
10
+ })
11
+ }
@@ -0,0 +1,21 @@
1
+ html {
2
+ line-height: 1;
3
+ background-color: var(--color-bg);
4
+ color: var(--color-text);
5
+ }
6
+
7
+ body {
8
+ font-family: var(--font-sans);
9
+ }
10
+
11
+ .sr-only {
12
+ position: absolute;
13
+ width: 1px;
14
+ height: 1px;
15
+ padding: 0;
16
+ margin: -1px;
17
+ overflow: hidden;
18
+ clip: rect(0, 0, 0, 0);
19
+ white-space: nowrap;
20
+ border-width: 0;
21
+ }
@@ -0,0 +1,102 @@
1
+ [data-component="button"] {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ gap: var(--space-2);
6
+ padding: var(--space-3) var(--space-4);
7
+ border: 1px solid transparent;
8
+ border-radius: var(--space-2);
9
+ font-family: var(--font-sans);
10
+ font-size: var(--font-size-md);
11
+ font-weight: 500;
12
+ line-height: 1.25;
13
+ cursor: pointer;
14
+ transition: all 0.2s ease-in-out;
15
+ text-decoration: none;
16
+ user-select: none;
17
+
18
+ &:disabled {
19
+ opacity: 0.5;
20
+ cursor: not-allowed;
21
+ }
22
+
23
+ &:focus {
24
+ outline: none;
25
+ box-shadow: 0 0 0 2px var(--color-primary);
26
+ }
27
+
28
+ &[data-color="primary"] {
29
+ background-color: var(--color-primary);
30
+ color: var(--color-primary-text);
31
+ border-color: var(--color-primary);
32
+
33
+ &:hover:not(:disabled) {
34
+ background-color: var(--color-primary-hover);
35
+ border-color: var(--color-primary-hover);
36
+ }
37
+
38
+ &:active:not(:disabled) {
39
+ background-color: var(--color-primary-active);
40
+ border-color: var(--color-primary-active);
41
+ }
42
+ }
43
+
44
+ &[data-color="danger"] {
45
+ background-color: var(--color-danger);
46
+ color: var(--color-danger-text);
47
+ border-color: var(--color-danger);
48
+
49
+ &:hover:not(:disabled) {
50
+ background-color: var(--color-danger-hover);
51
+ border-color: var(--color-danger-hover);
52
+ }
53
+
54
+ &:active:not(:disabled) {
55
+ background-color: var(--color-danger-active);
56
+ border-color: var(--color-danger-active);
57
+ }
58
+
59
+ &:focus {
60
+ box-shadow: 0 0 0 2px var(--color-danger);
61
+ }
62
+ }
63
+
64
+ &[data-color="warning"] {
65
+ background-color: var(--color-warning);
66
+ color: var(--color-warning-text);
67
+ border-color: var(--color-warning);
68
+
69
+ &:hover:not(:disabled) {
70
+ background-color: var(--color-warning-hover);
71
+ border-color: var(--color-warning-hover);
72
+ }
73
+
74
+ &:active:not(:disabled) {
75
+ background-color: var(--color-warning-active);
76
+ border-color: var(--color-warning-active);
77
+ }
78
+
79
+ &:focus {
80
+ box-shadow: 0 0 0 2px var(--color-warning);
81
+ }
82
+ }
83
+
84
+ &[data-size="small"] {
85
+ padding: var(--space-2) var(--space-3);
86
+ font-size: var(--font-size-sm);
87
+ gap: var(--space-1-5);
88
+ }
89
+
90
+ &[data-size="large"] {
91
+ padding: var(--space-4) var(--space-6);
92
+ font-size: var(--font-size-lg);
93
+ gap: var(--space-3);
94
+ }
95
+
96
+ [data-slot="icon"] {
97
+ display: flex;
98
+ align-items: center;
99
+ width: 1em;
100
+ height: 1em;
101
+ }
102
+ }
@@ -0,0 +1,8 @@
1
+ @import "./token/color.css";
2
+ @import "./token/font.css";
3
+ @import "./token/space.css";
4
+
5
+ @import "./component/button.css";
6
+
7
+ @import "./reset.css";
8
+ @import "./base.css";
@@ -0,0 +1,76 @@
1
+ /* 1. Use a more-intuitive box-sizing model */
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ /* 2. Remove default margin */
9
+ * {
10
+ margin: 0;
11
+ }
12
+
13
+ /* 3. Enable keyword animations */
14
+ @media (prefers-reduced-motion: no-preference) {
15
+ html {
16
+ interpolate-size: allow-keywords;
17
+ }
18
+ }
19
+
20
+ body {
21
+ /* 4. Add accessible line-height */
22
+ line-height: 1.5;
23
+ /* 5. Improve text rendering */
24
+ -webkit-font-smoothing: antialiased;
25
+ }
26
+
27
+ /* 6. Improve media defaults */
28
+ img,
29
+ picture,
30
+ video,
31
+ canvas,
32
+ svg {
33
+ display: block;
34
+ max-width: 100%;
35
+ }
36
+
37
+ /* 7. Inherit fonts for form controls */
38
+ input,
39
+ button,
40
+ textarea,
41
+ select {
42
+ font: inherit;
43
+ }
44
+
45
+ /* 8. Avoid text overflows */
46
+ p,
47
+ h1,
48
+ h2,
49
+ h3,
50
+ h4,
51
+ h5,
52
+ h6 {
53
+ overflow-wrap: break-word;
54
+ }
55
+
56
+ /* 9. Improve line wrapping */
57
+ p {
58
+ text-wrap: pretty;
59
+ }
60
+
61
+ h1,
62
+ h2,
63
+ h3,
64
+ h4,
65
+ h5,
66
+ h6 {
67
+ text-wrap: balance;
68
+ }
69
+
70
+ /*
71
+ 10. Create a root stacking context
72
+ */
73
+ #root,
74
+ #__next {
75
+ isolation: isolate;
76
+ }