Compare commits

..
Author SHA1 Message Date
opencode f0f6e9cad7 release: v0.6.6 2025-09-08 20:20:35 +00:00
Mani Sundararajan bbaae459c6 feat: make npm package install work on windows (#2419) 2025-09-08 16:14:18 -04:00
Frank eb3c820fb8 wip: zen 2025-09-08 15:57:29 -04:00
Frank 3468808fc6 wip: zen 2025-09-08 15:51:01 -04:00
Frank cd42503e2c Zen: telemetry 2025-09-08 15:46:59 -04:00
Aiden Cline 1cea8b9e77 tweak: reenable todowrite & todoread for qwen models (#2499) 2025-09-08 13:21:16 -05:00
Douglas Dennis d8fd7b155f fix: aws bedrock add check for govcloud (#2495) 2025-09-08 11:54:06 -05:00
GitHub Action 248a644fb0 ignore: update download stats 2025-09-08 2025-09-08 12:04:58 +00:00
Aiden Cline c8ff81bae4 fix: silent error if bad flag was passed (#2486) 2025-09-07 23:14:38 -05:00
Aiden Cline 74469a0d3d fix: shell invocations are dropped if last interaction was revert (#2485) 2025-09-07 21:45:13 -05:00
Aiden Cline 4d481dea7e fix: dont paste collapse if in bash mode (#2482) 2025-09-07 20:24:49 -05:00
opencode 7df32eac2a release: v0.6.5 2025-09-07 19:44:44 +00:00
Ytzhakandrekram1-node 4654fb88de fix: max output tokens when setting budget thinking tokens (#2056)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-07 13:15:53 -05:00
GitHub Action e915a3720e ignore: update download stats 2025-09-07 2025-09-07 12:03:51 +00:00
Aiden Cline 93c2f5060e fix: title gen w/ gpt-5-nano (#2473) 2025-09-06 22:50:16 -05:00
Aiden Cline 564143071e fix: title not generated if first msg is shell invocation (#2451) 2025-09-06 09:47:42 -05:00
GitHub Action 3cdfc529a0 ignore: update download stats 2025-09-06 2025-09-06 12:03:41 +00:00
Aiden Cline bffe547417 fix: command model selection (#2448) 2025-09-05 20:54:39 -05:00
Aiden Cline dc99005e65 fix: default to last used model (#2443) 2025-09-05 17:25:25 -05:00
Adam 8ffedbe157 fix: file read response 2025-09-05 15:58:56 -05:00
spoons-and-mirrors 900fe5ca04 tweak(edit): separate edit tool error message with clearer guidance to avoid llm doom editing loop (#2051) 2025-09-05 12:36:13 -04:00
GitHub Action 66a5d58221 ignore: update download stats 2025-09-05 2025-09-05 12:04:16 +00:00
Michał Olender 8426a0d595 Update index.tsx (#2423) 2025-09-04 17:44:26 -10:00
Aiden Cline 9186c3feae fix: webfetch prompt mistake (#2424) 2025-09-04 13:35:25 -05:00
Adam f171250033 fix: better file/content return 2025-09-04 12:39:49 -05:00
GitHub Action d440ba32ab ignore: update download stats 2025-09-04 2025-09-04 12:04:06 +00:00
Adam f7ab6beaf3 fix: worktree file/content never includes patch 2025-09-04 06:10:07 -05:00
Jay V 85ac243752 ignore: 404 2025-09-04 01:39:54 -07:00
Jay V 03522471a1 docs: fix 2025-09-04 01:03:23 -07:00
Jay V 42b440be0c docs: handle base path 2025-09-04 00:53:45 -07:00
Jay V 133ae42c55 ignore: zen 2025-09-04 00:17:06 -07:00
Zack Jackson e001af2709 feat: add createOpencodeTui() function to SDK for programmatic TUI launching (#2410) 2025-09-04 02:49:44 -04:00
Aiden Cline a97612287f fix: file fuzzy search (#2409) 2025-09-03 23:20:16 -05:00
Jay V d13467d869 ignore: mobile styles 2025-09-03 19:33:01 -07:00
Jay V 368bd97952 ignore: lander 2025-09-03 19:05:49 -07:00
Jay V d0104278fa docs: lander 2025-09-03 18:30:04 -07:00
Jay V 222244719b ignore: cloud 2025-09-03 17:21:46 -07:00
Jay V 21008d733f docs: link 2025-09-03 17:12:51 -07:00
Jay V 2808e95ac7 ignore: zen 2025-09-03 15:53:31 -07:00
Frank 70e0d71ac2 wip console 2025-09-03 15:57:41 -04:00
Frank 93f507d330 wip console 2025-09-03 15:35:46 -04:00
Dax Raad 7119ace940 wip: cloud 2025-09-03 14:57:02 -04:00
Dax Raad 4e24e04aec ignore: opencode auth stuff 2025-09-03 14:43:50 -04:00
Dax Raad 469a83a55b wip: email 2025-09-03 14:32:05 -04:00
Dax Raad e8f54b9b38 wip: fix logout 2025-09-03 14:24:37 -04:00
Frank 01b18456a3 wip console 2025-09-03 14:09:39 -04:00
Frank 1acd5445b5 wip console 2025-09-03 14:07:57 -04:00
Dax Raad ff89305ebe wip: logout 2025-09-03 14:07:23 -04:00
Dax Raad 605f78944d wip: css 2025-09-03 14:05:10 -04:00
Dax Raad 9f7e14dc7e wip: css hack 2025-09-03 13:58:08 -04:00
Dax Raad cb7f3cf2f1 wip: cloud 2025-09-03 13:58:08 -04:00
Dax Raad 4406096974 wip: cloud 2025-09-03 13:58:08 -04:00
Frank fefaad6226 wip cloud smart 2025-09-03 13:52:01 -04:00
Frank 3f9b569575 script package 2025-09-03 13:43:32 -04:00
Dax Raad 1e4f5710aa wip: cloud 2025-09-03 13:17:32 -04:00
Dax Raad 48a79b1173 wip: make less shit 2025-09-03 13:10:57 -04:00
Dax Raad 59e550271d wip: cloud 2025-09-03 12:32:03 -04:00
Frank afebe920b2 wip console 2025-09-03 11:50:18 -04:00
Frank 6921702605 wip console 2025-09-03 11:40:59 -04:00
Frank 9c6783e88e wip console 2025-09-03 11:31:16 -04:00
Frank f1a60a0a93 wip: generate config.json 2025-09-03 11:24:13 -04:00
Jay V 4b9cae82e6 ignore: zen 2025-09-03 11:16:49 -04:00
Jay V fdf08ecfab ignore: zen 2025-09-03 11:10:07 -04:00
Jay V 22f5c26eec docs: edits 2025-09-03 11:05:43 -04:00
opencode b6de122ddc release: v0.6.4 2025-09-03 13:31:11 +00:00
Frank 0f8cb69bff wip console 2025-09-03 09:24:23 -04:00
GitHub Action fca2bddc3b ignore: update download stats 2025-09-03 2025-09-03 12:04:06 +00:00
Frank f65e20b8ce wip console 2025-09-03 06:53:30 -04:00
Frank 93f2805bc2 wip: console 2025-09-03 06:37:40 -04:00
Frank 9ad4dc9296 wip: console 2025-09-03 06:27:53 -04:00
Frank 23af974bd3 wip: console 2025-09-03 06:22:44 -04:00
Frank 36ea46ee67 wip: console 2025-09-03 06:15:08 -04:00
Frank 4d2cc9d858 wip: console 2025-09-03 06:12:17 -04:00
Dax Raad 610ffbdd61 wip: console 2025-09-03 01:33:49 -04:00
Brendan Allan 854f9227a2 Patch Start to preload route css in SSR (#2389) 2025-09-03 01:28:34 -04:00
Dax Raad 8d368fdfd2 wip: zen 2025-09-02 23:56:10 -04:00
Dax Raad 1c31c2dd97 wip: zen 2025-09-02 23:30:48 -04:00
Frank c1d754bec9 wip cloud 2025-09-02 23:28:54 -04:00
Dax Raad c67b721787 docs: remove remaining directory query param mentions from SDK docs 2025-09-02 22:25:32 -04:00
Dax Raad 11e41e7564 docs: remove directory query param mentions from SDK docs 2025-09-02 22:25:32 -04:00
Dax Raad afd42bf46d docs: fix SDK usage to use path/query/body, correct return types, and update examples 2025-09-02 22:25:32 -04:00
Aiden Cline f740663ded fix: more durable @ references for commands (#2386) 2025-09-02 21:24:56 -05:00
Jay V 751b81af34 docs: zen 2025-09-02 21:29:03 -04:00
Jay V b725bcd2cd ignore: adding public files 2025-09-02 21:25:09 -04:00
Frank c278e16e4e generate api key 2025-09-02 20:38:36 -04:00
Frank 4e629c5b64 wip: cloud 2025-09-02 20:01:13 -04:00
Dax Raad 4624f0a260 ci: ignore 2025-09-02 19:32:21 -04:00
Dax Raad 2e16d685eb wip: zen 2025-09-02 18:00:48 -04:00
Jay V e544cccc70 ignore: zen 2025-09-02 17:30:51 -04:00
Jay V c141b88087 ignore: zen 2025-09-02 17:28:35 -04:00
Jay V 023c4532c1 ignore: cloud lander 2025-09-02 17:28:35 -04:00
Dax Raad 042802848d wip: zen 2025-09-02 16:38:50 -04:00
Dax Raad a8aa44bd3f docs: simplify config example to show only model 2025-09-02 16:38:50 -04:00
Dax Raad db2a3a171e docs: clarify config behavior and remove theme example 2025-09-02 16:38:50 -04:00
Dax Raad 38a4bee1be docs: add config example to SDK server creation 2025-09-02 16:38:50 -04:00
Dax Raad 8952b3d246 support OPENCODE_CONFIG_CONTENT 2025-09-02 16:38:50 -04:00
Aiden Cline d6350a7fa6 tweak: update ls tool to use rg (#2367) 2025-09-02 10:40:20 -05:00
Yuta URANO ae83138832 docs: update log level configuration in troubleshooting guide (#2374) 2025-09-02 10:31:04 -05:00
OpeOginni 3ee4280dfa fix: local subdirectory subagents not being picked up (#2376) 2025-09-02 09:46:00 -05:00
GitHub Action 26fbf9e647 ignore: update download stats 2025-09-02 2025-09-02 12:04:59 +00:00
Adam 97a41062c9 fix: file.list relative to root 2025-09-02 06:20:08 -05:00
Dax Raad 4a76224268 wip: typechecking 2025-09-02 03:18:30 -04:00
Dax Raad 810c9cff1d wip: cloud 2025-09-02 03:18:30 -04:00
47d4c87bdd make @file references in custom slash commands more robust (#2203)
Co-authored-by: Adam Spiers <opencode@adamspiers.org>
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-01 21:14:27 -05:00
opencode a9875c5531 release: v0.6.3 2025-09-02 01:52:01 +00:00
Dax Raad 4c261ab1db switch gpt-5 to default to codex prompt + high reasoning 2025-09-01 21:46:03 -04:00
opencode 2fc8263032 release: v0.6.2 2025-09-02 01:03:43 +00:00
Aiden Cline a431b8922c fix: ensure opencode still works if no commits present (#2363) 2025-09-01 20:57:14 -04:00
Aiden Cline 0a01d20850 fix: ensure enabled lsps are all logged (#2364) 2025-09-01 17:43:31 -05:00
opencode 7b62c10553 release: v0.6.1 2025-09-01 22:07:53 +00:00
Dax Raad 61c7196bd9 catch migration failures 2025-09-01 18:00:40 -04:00
opencode 365fdd9ff8 release: v0.6.0 2025-09-01 21:43:13 +00:00
Dax Raad f6bc9238df docs: sdk 2025-09-01 17:35:52 -04:00
Aiden Cline 26f75d4e68 fix: tui attachment bound (#2361) 2025-09-01 16:33:36 -05:00
Jay V 8ba8d3c7e3 docs: update email 2025-09-01 17:30:32 -04:00
Dax f993541e0b Refactor to support multiple instances inside single opencode process (#2360)
This release has a bunch of minor breaking changes if you are using opencode plugins or sdk

1. storage events have been removed (we might bring this back but had some issues)
2. concept of `app` is gone - there is a new concept called `project` and endpoints to list projects and get the current project
3. plugin receives `directory` which is cwd and `worktree` which is where the root of the project is if it's a git repo
4. the session.chat function has been renamed to session.prompt in sdk. it no longer requires model to be passed in (model is now an object)
5. every endpoint takes an optional `directory` parameter to operate as though opencode is running in that directory
2025-09-01 17:15:49 -04:00
Aiden Cline e2df3eb44d add --command to opencode run (#2348) 2025-09-01 14:19:18 -05:00
Dax Raad 38f9ce05f6 wip: cloud 2025-09-01 11:53:43 -04:00
Dax Raad a6e09363b8 wip: cloud 2025-09-01 11:47:58 -04:00
GitHub Action 49629bb58e ignore: update download stats 2025-09-01 2025-09-01 12:04:32 +00:00
Dax Raad 2bb5b9b13a wip: cloud 2025-09-01 04:03:07 -04:00
Dax Raad 41338d1bf9 wip: cloud 2025-09-01 03:55:48 -04:00
Dax Raad 41ee9c94c7 wip: cloud 2025-09-01 03:53:49 -04:00
Dax Raad 9c16db0f36 wip: cloud 2025-09-01 03:51:45 -04:00
Dax Raad 721869353b wip: sync 2025-09-01 03:15:38 -04:00
Dax Raad 6d22ade771 wip: cloud 2025-09-01 03:13:05 -04:00
Dax Raad fbcceeb781 wip: cloud 2025-09-01 03:10:40 -04:00
Dax Raad 95775d68b7 wip: cloud 2025-09-01 03:04:49 -04:00
Dax Raad cf11669618 wip: cloud 2025-09-01 03:04:07 -04:00
Dax Raad 65dc19e85a wip: cloud 2025-09-01 02:57:47 -04:00
Dax Raad cfcfceca6d wip: cloud 2025-09-01 02:55:10 -04:00
Dax Raad 9f8899a9f9 wip: cloud 2025-09-01 02:28:21 -04:00
Dax Raad 449a063fe2 wip: cloud 2025-09-01 02:21:36 -04:00
Régis Blanc 37530359ee fix: ensure gopls lsp id matches docs (#2344) 2025-08-31 21:52:08 -05:00
Aiden Cline 65f0bea146 ignore: better error logging (#2346) 2025-08-31 17:11:04 -05:00
Beshoy Girgis e4cc05a975 feat: Allow provider timeout override (#1982) 2025-08-31 14:06:02 -04:00
Aiden Cline 029612d8d5 fix: ensure shell cmds can be properly aborted (#2339) 2025-08-31 12:48:30 -05:00
Aiden Cline e9826e8a22 fix: adjust title generation prompt to prevent direct response instead of title gen (#2338) 2025-08-31 11:01:19 -05:00
GitHub Action ad5f209dc8 ignore: update download stats 2025-08-31 2025-08-31 12:04:06 +00:00
Andre van Tonderandrekram1-node fcfeac57c5 fix: resolve virtual envs for python LSP (#2155)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-08-30 23:53:03 -05:00
Aiden Cline 2946898934 fix: ensure command uses currently selected model (#2336) 2025-08-30 15:41:06 -05:00
Aiden Cline b4d95545e0 add support for lsp workspace/didChangeConfiguration (#2334) 2025-08-30 14:49:13 -05:00
Jay V d3bbaa141c ignore: cloud 2025-08-30 15:28:35 -04:00
Jay V 8714f23509 ignore: cloud styles 2025-08-30 15:27:46 -04:00
167 changed files with 5230 additions and 5423 deletions
+2
View File
@@ -24,4 +24,6 @@ jobs:
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
+9
View File
@@ -64,3 +64,12 @@
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
+55 -8
View File
@@ -26,10 +26,11 @@
},
"cloud/core": {
"name": "@opencode/cloud-core",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@opencode/cloud-resource": "workspace:*",
"@planetscale/database": "1.19.0",
"drizzle-orm": "0.41.0",
"postgres": "3.4.7",
"stripe": "18.0.0",
@@ -37,11 +38,12 @@
},
"devDependencies": {
"drizzle-kit": "0.30.5",
"mysql2": "3.14.4",
},
},
"cloud/function": {
"name": "@opencode/cloud-function",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -65,9 +67,21 @@
"@cloudflare/workers-types": "^4.20250830.0",
},
},
"cloud/scripts": {
"name": "@opencode/cloud-scripts",
"version": "0.6.4",
"dependencies": {
"@opencode/cloud-core": "workspace:*",
"tsx": "4.20.5",
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
},
},
"packages/function": {
"name": "@opencode/function",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -82,7 +96,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.5.28",
"version": "0.6.4",
"bin": {
"opencode": "./bin/opencode",
},
@@ -132,7 +146,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
},
@@ -143,7 +157,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@hey-api/openapi-ts": "0.81.0",
},
@@ -155,7 +169,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "0.5.28",
"version": "0.6.4",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -194,6 +208,9 @@
"web-tree-sitter",
"tree-sitter-bash",
],
"patchedDependencies": {
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch",
},
"overrides": {
"zod": "3.25.76",
},
@@ -661,6 +678,8 @@
"@opencode/cloud-resource": ["@opencode/cloud-resource@workspace:cloud/resource"],
"@opencode/cloud-scripts": ["@opencode/cloud-scripts@workspace:cloud/scripts"],
"@opencode/function": ["@opencode/function@workspace:packages/function"],
"@opencode/web": ["@opencode/web@workspace:packages/web"],
@@ -747,6 +766,8 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@planetscale/database": ["@planetscale/database@1.19.0", "", {}, "sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA=="],
"@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="],
"@poppinss/dumper": ["@poppinss/dumper@0.6.4", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ=="],
@@ -1161,6 +1182,8 @@
"aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -1631,6 +1654,8 @@
"gel": ["gel@2.1.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-amd-module-type": ["get-amd-module-type@6.0.1", "", { "dependencies": { "ast-module-types": "^6.0.1", "node-source-walk": "^7.0.1" } }, "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ=="],
@@ -1769,7 +1794,7 @@
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="],
@@ -1845,6 +1870,8 @@
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
@@ -1957,6 +1984,8 @@
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
@@ -2145,6 +2174,10 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
@@ -2523,6 +2556,8 @@
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
"serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="],
"seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
@@ -2607,6 +2642,8 @@
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"ssri": ["ssri@10.0.6", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ=="],
"sst": ["sst@3.17.12", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.12", "sst-darwin-x64": "3.17.12", "sst-linux-arm64": "3.17.12", "sst-linux-x64": "3.17.12", "sst-linux-x86": "3.17.12", "sst-win32-arm64": "3.17.12", "sst-win32-x64": "3.17.12", "sst-win32-x86": "3.17.12" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-UwUbucNZRLp9GHgPAwkat1sBsNGaJfHsLXZHCMKsolCW7CEuugJfvBl2vOyJrhKP4N+Xnv1QFh0BGsOmN0kQeA=="],
@@ -2733,6 +2770,8 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.20.5", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw=="],
"tuf-js": ["tuf-js@2.2.1", "", { "dependencies": { "@tufjs/models": "2.0.1", "debug": "^4.3.4", "make-fetch-happen": "^13.0.1" } }, "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
@@ -3113,6 +3152,8 @@
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
@@ -3129,6 +3170,8 @@
"drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
"encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -3187,6 +3230,8 @@
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"netlify/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"nitropack/c12": ["c12@3.2.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.5.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-ixkEtbYafL56E6HiFuonMm1ZjoKtIo7TH68/uiEq4DAwv9NcUX2nJ95F8TrbMeNjqIkZpruo3ojXQJ+MGG5gcQ=="],
@@ -3243,6 +3288,8 @@
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"raw-body/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
+1
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "@solidjs/start/config"
export default defineConfig({
middleware: "./src/middleware.ts",
vite: {
server: {
allowedHosts: true,
+3 -2
View File
@@ -2,11 +2,12 @@
"name": "@opencode/cloud-app",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build",
"build": "vinxi build && ../../packages/opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.5.29"
"version": "0.6.6"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Disallow shared content pages
Disallow: /s/
+182
View File
@@ -0,0 +1,182 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
},
"defs": {
"type": "object",
"description": "Color definitions that can be referenced in the theme",
"patternProperties": {
"^[a-zA-Z][a-zA-Z0-9_]*$": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code (0-255)"
},
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
}
]
}
},
"additionalProperties": false
},
"theme": {
"type": "object",
"description": "Theme color definitions",
"properties": {
"primary": { "$ref": "#/definitions/colorValue" },
"secondary": { "$ref": "#/definitions/colorValue" },
"accent": { "$ref": "#/definitions/colorValue" },
"error": { "$ref": "#/definitions/colorValue" },
"warning": { "$ref": "#/definitions/colorValue" },
"success": { "$ref": "#/definitions/colorValue" },
"info": { "$ref": "#/definitions/colorValue" },
"text": { "$ref": "#/definitions/colorValue" },
"textMuted": { "$ref": "#/definitions/colorValue" },
"background": { "$ref": "#/definitions/colorValue" },
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
"backgroundElement": { "$ref": "#/definitions/colorValue" },
"border": { "$ref": "#/definitions/colorValue" },
"borderActive": { "$ref": "#/definitions/colorValue" },
"borderSubtle": { "$ref": "#/definitions/colorValue" },
"diffAdded": { "$ref": "#/definitions/colorValue" },
"diffRemoved": { "$ref": "#/definitions/colorValue" },
"diffContext": { "$ref": "#/definitions/colorValue" },
"diffHunkHeader": { "$ref": "#/definitions/colorValue" },
"diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
"diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
"diffAddedBg": { "$ref": "#/definitions/colorValue" },
"diffRemovedBg": { "$ref": "#/definitions/colorValue" },
"diffContextBg": { "$ref": "#/definitions/colorValue" },
"diffLineNumber": { "$ref": "#/definitions/colorValue" },
"diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
"diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
"markdownText": { "$ref": "#/definitions/colorValue" },
"markdownHeading": { "$ref": "#/definitions/colorValue" },
"markdownLink": { "$ref": "#/definitions/colorValue" },
"markdownLinkText": { "$ref": "#/definitions/colorValue" },
"markdownCode": { "$ref": "#/definitions/colorValue" },
"markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
"markdownEmph": { "$ref": "#/definitions/colorValue" },
"markdownStrong": { "$ref": "#/definitions/colorValue" },
"markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
"markdownListItem": { "$ref": "#/definitions/colorValue" },
"markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
"markdownImage": { "$ref": "#/definitions/colorValue" },
"markdownImageText": { "$ref": "#/definitions/colorValue" },
"markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
"syntaxComment": { "$ref": "#/definitions/colorValue" },
"syntaxKeyword": { "$ref": "#/definitions/colorValue" },
"syntaxFunction": { "$ref": "#/definitions/colorValue" },
"syntaxVariable": { "$ref": "#/definitions/colorValue" },
"syntaxString": { "$ref": "#/definitions/colorValue" },
"syntaxNumber": { "$ref": "#/definitions/colorValue" },
"syntaxType": { "$ref": "#/definitions/colorValue" },
"syntaxOperator": { "$ref": "#/definitions/colorValue" },
"syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
},
"required": ["primary", "secondary", "accent", "text", "textMuted", "background"],
"additionalProperties": false
}
},
"required": ["theme"],
"additionalProperties": false,
"definitions": {
"colorValue": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value (same for dark and light)"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code (0-255, same for dark and light)"
},
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color in the theme or defs"
},
{
"type": "object",
"properties": {
"dark": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value for dark mode"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code for dark mode"
},
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for dark mode"
}
]
},
"light": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value for light mode"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code for light mode"
},
{
"type": "string",
"enum": ["none"],
"description": "No color (uses terminal default)"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for light mode"
}
]
}
},
"required": ["dark", "light"],
"additionalProperties": false,
"description": "Separate colors for dark and light modes"
}
]
}
}
}
+1
View File
@@ -8,6 +8,7 @@ import "./app.css";
export default function App() {
return (
<Router
explicitLinks={true}
root={props => (
<MetaProvider>
<Title>opencode</Title>
+1 -1
View File
@@ -1,7 +1,7 @@
import { useSession } from "vinxi/http"
export interface AuthSession {
account: Record<
account?: Record<
string,
{
id: string
+60 -56
View File
@@ -2,7 +2,7 @@ import { getRequestEvent } from "solid-js/web"
import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
import { query, redirect } from "@solidjs/router"
import { redirect } from "@solidjs/router"
import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
import { Actor } from "@opencode/cloud-core/actor.js"
@@ -14,66 +14,70 @@ export const AuthClient = createClient({
issuer: import.meta.env.VITE_AUTH_URL,
})
export const getActor = query(async (): Promise<Actor.Info> => {
export const getActor = async (workspace?: string): Promise<Actor.Info> => {
"use server"
const evt = getRequestEvent()
if (!evt) throw new Error("No request event")
const url = new URL(evt.request.headers.has("x-server-id") ? evt.request.headers.get("referer")! : evt.request.url)
const auth = await useAuthSession()
const splits = url.pathname.split("/").filter(Boolean)
if (splits[0] !== "workspace") {
if (auth.data.current) {
const current = auth.data.account[auth.data.current]
if (evt.locals.actor) return evt.locals.actor
evt.locals.actor = (async () => {
const auth = await useAuthSession()
if (!workspace) {
const account = auth.data.account ?? {}
const current = account[auth.data.current ?? ""]
if (current) {
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
if (Object.keys(account).length > 0) {
const current = Object.values(account)[0]
await auth.update((val) => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
type: "public",
properties: {},
}
}
if (Object.keys(auth.data.account ?? {}).length > 0) {
const current = Object.values(auth.data.account)[0]
await auth.update((val) => ({
...val,
current: current.id,
}))
return {
type: "account",
properties: {
email: current.email,
accountID: current.id,
},
}
}
return {
type: "public",
properties: {},
}
}
const workspaceHint = splits[1]
const accounts = Object.keys(auth.data.account)
const result = await Database.transaction(async (tx) => {
return await tx
.select({
user: UserTable,
const accounts = Object.keys(auth.data.account ?? {})
if (accounts.length) {
const result = await Database.transaction(async (tx) => {
return await tx
.select({
user: UserTable,
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspace)))
.limit(1)
.execute()
.then((x) => x[0])
})
.from(AccountTable)
.innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
.where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspaceHint)))
.limit(1)
.execute()
.then((x) => x[0])
})
if (result) {
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
},
if (result) {
return {
type: "user",
properties: {
userID: result.user.id,
workspaceID: result.user.workspaceID,
},
}
}
}
}
throw redirect("/auth/authorize")
}, "actor")
throw redirect("/auth/authorize")
})()
return evt.locals.actor
}
+2 -11
View File
@@ -1,16 +1,7 @@
import { Actor } from "@opencode/cloud-core/actor.js"
import { getActor } from "./auth"
import { query } from "@solidjs/router"
export async function withActor<T>(fn: () => T) {
const actor = await getActor()
export async function withActor<T>(fn: () => T, workspace?: string) {
const actor = await getActor(workspace)
return Actor.provide(actor.type, actor.properties, fn)
}
export function actorQuery<T>(cb: () => T, name: string) {
"use server"
return query(async () => {
const actor = await getActor()
return withActor(cb)
}, name)
}
+4 -1
View File
@@ -1,6 +1,7 @@
// @refresh reload
import { createHandler, StartServer } from "@solidjs/start/server"
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
@@ -20,4 +21,6 @@ export default createHandler(() => (
</html>
)}
/>
))
), {
mode: "async",
})
+5
View File
@@ -0,0 +1,5 @@
import { defineMiddleware } from "vinxi/http"
export default defineMiddleware({
onBeforeResponse() {},
})
+130
View File
@@ -0,0 +1,130 @@
[data-page="not-found"] {
--color-text: hsl(224, 10%, 10%);
--color-text-secondary: hsl(224, 7%, 46%);
--color-text-dimmed: hsl(224, 6%, 63%);
--color-text-inverted: hsl(0, 0%, 100%);
--color-border: hsl(224, 6%, 77%);
}
[data-page="not-found"] {
@media (prefers-color-scheme: dark) {
--color-text: hsl(0, 0%, 100%);
--color-text-secondary: hsl(224, 6%, 66%);
--color-text-dimmed: hsl(224, 7%, 46%);
--color-text-inverted: hsl(224, 10%, 10%);
--color-border: hsl(224, 6%, 36%);
}
}
[data-page="not-found"] {
--padding: 3rem;
--vertical-padding: 1.5rem;
--heading-font-size: 1.375rem;
@media (max-width: 30rem) {
--padding: 1rem;
--vertical-padding: 0.75rem;
--heading-font-size: 1rem;
}
font-family: var(--font-mono);
color: var(--color-text);
padding: calc(var(--padding) + 1rem);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
[data-component="content"] {
max-width: 40rem;
width: 100%;
border: 1px solid var(--color-border);
}
[data-component="top"] {
padding: var(--padding);
display: flex;
flex-direction: column;
align-items: center;
gap: calc(var(--vertical-padding) / 2);
text-align: center;
[data-slot="logo-link"] {
text-decoration: none;
}
img {
height: auto;
width: clamp(200px, 85vw, 400px);
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
[data-slot="title"] {
line-height: 1.25;
font-weight: 500;
text-align: center;
font-size: var(--heading-font-size);
color: var(--color-text);
text-transform: uppercase;
margin: 0;
}
}
[data-component="actions"] {
border-top: 1px solid var(--color-border);
display: flex;
[data-slot="action"] {
flex: 1;
text-align: center;
line-height: 1.4;
padding: var(--vertical-padding) 1rem;
text-transform: uppercase;
font-size: 1rem;
a {
display: block;
width: 100%;
height: 100%;
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
}
[data-slot="action"] + [data-slot="action"] {
border-left: 1px solid var(--color-border);
}
@media (max-width: 40rem) {
flex-direction: column;
[data-slot="action"] + [data-slot="action"] {
border-left: none;
border-top: 1px solid var(--color-border);
}
}
}
}
+32 -13
View File
@@ -1,19 +1,38 @@
import { Title } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
import "./[...404].css"
import { Title } from "@solidjs/meta"
import { HttpStatusCode } from "@solidjs/start"
import logoLight from "../asset/logo-ornate-light.svg"
import logoDark from "../asset/logo-ornate-dark.svg"
export default function NotFound() {
return (
<main>
<Title>Not Found</Title>
<main data-page="not-found">
<Title>Not Found | opencode</Title>
<HttpStatusCode code={404} />
<h1>Page Not Found</h1>
<p>
Visit{" "}
<a href="https://start.solidjs.com" target="_blank">
start.solidjs.com
</a>{" "}
to learn how to build SolidStart apps.
</p>
<div data-component="content">
<section data-component="top">
<a href="/" data-slot="logo-link">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
</a>
<h1 data-slot="title">404 - Page Not Found</h1>
</section>
<section data-component="actions">
<div data-slot="action">
<a href="/">Home</a>
</div>
<div data-slot="action">
<a href="/docs">Docs</a>
</div>
<div data-slot="action">
<a href="https://github.com/sst/opencode">GitHub</a>
</div>
<div data-slot="action">
<a href="/discord">Discord</a>
</div>
</section>
</div>
</main>
);
)
}
+1 -1
View File
@@ -27,5 +27,5 @@ export async function GET(input: APIEvent) {
current: id,
}
})
return redirect("/")
return redirect("/auth")
}
+13
View File
@@ -0,0 +1,13 @@
import { Account } from "@opencode/cloud-core/account.js"
import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server"
import { withActor } from "~/context/auth.withActor"
export async function GET(input: APIEvent) {
try {
const workspaces = await withActor(async () => Account.workspaces())
return redirect(`/workspace/${workspaces[0].id}`)
} catch {
return redirect("/auth/authorize")
}
}
-7
View File
@@ -1,7 +0,0 @@
import type { APIEvent } from "@solidjs/start/server"
import { AuthClient } from "~/context/auth"
export async function GET(input: APIEvent) {
const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
return Response.redirect(result.url, 302)
}
+6 -2
View File
@@ -1,9 +1,13 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource } from "@opencode/cloud-resource"
import { json } from "@solidjs/router"
import { Database } from "@opencode/cloud-core/drizzle/index.js"
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
export async function GET(evt: APIEvent) {
return json({
data: Resource.Database.host,
data: await Database.use(async (tx) => {
const result = await tx.$count(UserTable)
return result
}),
})
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "@solidjs/router"
export async function GET() {
return redirect("https://discord.gg/opencode")
}
+20
View File
@@ -0,0 +1,20 @@
import type { APIEvent } from "@solidjs/start/server"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
const response = await fetch(targetUrl, {
method: req.method,
headers: req.headers,
body: req.body,
})
return response
}
export const GET = handler
export const POST = handler
export const PUT = handler
export const DELETE = handler
export const OPTIONS = handler
export const PATCH = handler
+20
View File
@@ -0,0 +1,20 @@
import type { APIEvent } from "@solidjs/start/server"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
const response = await fetch(targetUrl, {
method: req.method,
headers: req.headers,
body: req.body,
})
return response
}
export const GET = handler
export const POST = handler
export const PUT = handler
export const DELETE = handler
export const OPTIONS = handler
export const PATCH = handler
@@ -1,576 +1,308 @@
/**
* @deprecated Use zen/v1/chat/completions instead
*/
import { Resource } from "@opencode/cloud-resource"
import { Billing } from "@opencode/cloud-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode/cloud-core/schema/billing.sql.js"
import { Identifier } from "@opencode/cloud-core/identifier.js"
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Actor } from "@opencode/cloud-core/actor.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "@opencode/cloud-core/identifier.js"
const SUPPORTED_MODELS = {
const MODELS = {
// "anthropic/claude-sonnet-4": {
// input: 0.0000015,
// output: 0.000006,
// reasoning: 0.0000015,
// cacheRead: 0.0000001,
// cacheWrite: 0.0000001,
// model: () =>
// createAnthropic({
// apiKey: Resource.ANTHROPIC_API_KEY.value,
// })("claude-sonnet-4-20250514"),
// },
// "openai/gpt-4.1": {
// input: 0.0000015,
// output: 0.000006,
// reasoning: 0.0000015,
// cacheRead: 0.0000001,
// cacheWrite: 0.0000001,
// model: () =>
// createOpenAI({
// apiKey: Resource.OPENAI_API_KEY.value,
// })("gpt-4.1"),
// },
// "zhipuai/glm-4.5-flash": {
// input: 0,
// output: 0,
// reasoning: 0,
// cacheRead: 0,
// cacheWrite: 0,
// model: () =>
// createOpenAICompatible({
// name: "Zhipu AI",
// baseURL: "https://api.z.ai/api/paas/v4",
// apiKey: Resource.ZHIPU_API_KEY.value,
// })("glm-4.5-flash"),
// auth: true,
// api: "https://api.anthropic.com",
// apiKey: Resource.ANTHROPIC_API_KEY.value,
// model: "claude-sonnet-4-20250514",
// cost: {
// input: 0.0000015,
// output: 0.000006,
// reasoning: 0.0000015,
// cacheRead: 0.0000001,
// cacheWrite: 0.0000001,
// },
// headerMappings: {},
// },
"qwen/qwen3-coder": {
id: "qwen/qwen3-coder",
auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: {
input: 0.00000038,
output: 0.00000153,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
},
headerMappings: {},
},
"grok-code": {
id: "x-ai/grok-code-fast-1",
auth: false,
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
cost: {
input: 0,
output: 0,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
},
headerMappings: {
"x-grok-conv-id": "x-opencode-session",
"x-grok-req-id": "x-opencode-request",
},
},
}
class AuthError extends Error {}
class CreditsError extends Error {}
class ModelError extends Error {}
export async function POST(input: APIEvent) {
// Check auth header
const authHeader = input.request.headers.get("authorization")
if (!authHeader || !authHeader.startsWith("Bearer "))
return Response.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
{ status: 401 },
)
const apiKey = authHeader.split(" ")[1]
try {
const url = new URL(input.request.url)
const body = await input.request.json()
const MODEL = validateModel()
const apiKey = await authenticate()
await checkCredits()
// Check against KeyTable
const keyRecord = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
// Request to model provider
const res = await fetch(new URL(url.pathname.replace(/^\/gateway/, "") + url.search, MODEL.api), {
method: "POST",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
headers.set("authorization", `Bearer ${MODEL.apiKey}`)
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
return headers
})(),
body: JSON.stringify({
...body,
model: MODEL.model,
stream_options: {
include_usage: true,
},
}),
})
// Scrub response headers
const resHeaders = new Headers()
const keepHeaders = ["content-type", "cache-control"]
for (const [k, v] of res.headers.entries()) {
if (keepHeaders.includes(k.toLowerCase())) {
resHeaders.set(k, v)
}
}
// Handle non-streaming response
if (!body.stream) {
const body = await res.json()
await trackUsage(body)
return new Response(JSON.stringify(body), {
status: res.status,
statusText: res.statusText,
headers: resHeaders,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
}
if (!keyRecord)
return Response.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
// Handle streaming response
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
const decoder = new TextDecoder()
let buffer = ""
function pump(): Promise<void> {
return (
reader?.read().then(async ({ done, value }) => {
if (done) {
c.close()
return
}
buffer += decoder.decode(value, { stream: true })
const parts = buffer.split("\n\n")
buffer = parts.pop() ?? ""
const usage = parts
.map((part) => part.trim())
.filter((part) => part.startsWith("data: "))
.map((part) => {
try {
return JSON.parse(part.slice(6))
} catch (e) {
return {}
}
})
.find((part) => part.usage)
if (usage) await trackUsage(usage)
c.enqueue(value)
return pump()
}) || Promise.resolve()
)
}
return pump()
},
{ status: 401 },
)
})
/*
return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
try {
// Check balance
const customer = await Billing.get()
if (customer.balance <= 0) {
return Response.json(
{
error: {
message: "Insufficient balance",
type: "insufficient_quota",
param: null,
code: "insufficient_quota",
},
},
{ status: 401 },
return new Response(stream, {
status: res.status,
statusText: res.statusText,
headers: resHeaders,
})
function validateModel() {
if (!(body.model in MODELS)) {
throw new ModelError(`Model ${body.model} not supported`)
}
return MODELS[body.model as keyof typeof MODELS]
}
async function authenticate() {
try {
const authHeader = input.request.headers.get("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) throw new AuthError("Missing API key.")
const apiKey = authHeader.split(" ")[1]
const key = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
if (!key) throw new AuthError("Invalid API key.")
return key
} catch (e) {
console.log(e)
// ignore error if model does not require authentication
if (!MODEL.auth) return
throw e
}
}
const body = await input.request.json<ChatCompletionCreateParamsBase>()
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
if (!model) throw new Error(`Unsupported model: ${body.model}`)
async function checkCredits() {
if (!apiKey || !MODEL.auth) return
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await model.doStream({
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.stream) {
console.log("!!! CHUNK !!! : " + chunk.type)
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: chunk.input,
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
error: {
message: typeof chunk.error === "string" ? chunk.error : chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason:
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop",
},
],
usage: {
prompt_tokens: chunk.usage.inputTokens,
completion_tokens: chunk.usage.outputTokens,
total_tokens: chunk.usage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.usage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.usage.cachedInputTokens,
},
},
}
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await model.doGenerate({
...requestBody,
})
await trackUsage(body.model, response.usage, response.providerMetadata)
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
const tools = transformTools()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" as const }
if (body.response_format.type === "json_schema")
return {
type: "json" as const,
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" as const }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
tools: tools.tools,
toolChoice: tools.toolChoice,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.map((tool) => {
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters!,
}
const billing = await Database.use((tx) =>
tx
.select({
balance: BillingTable.balance,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
.then((rows) => rows[0]),
)
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = { type: "auto" as const }
} else if (tool_choice === "none") {
aiSdkToolChoice = { type: "none" as const }
} else if (tool_choice === "required") {
aiSdkToolChoice = { type: "required" as const }
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool" as const,
toolName: tool_choice.function.name,
}
}
if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
async function trackUsage(chunk: any) {
console.log(`trackUsage ${apiKey}`)
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
if (!apiKey) return
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
const usage = chunk.usage
const inputTokens = usage.prompt_tokens ?? 0
const outputTokens = usage.completion_tokens ?? 0
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0
//const cacheWriteTokens = providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? 0
const cacheWriteTokens = 0
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
const inputCost = MODEL.cost.input * inputTokens
const outputCost = MODEL.cost.output * outputTokens
const reasoningCost = MODEL.cost.reasoning * reasoningTokens
const cacheReadCost = MODEL.cost.cacheRead * cacheReadTokens
const cacheWriteCost = MODEL.cost.cacheWrite * cacheWriteTokens
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
const cost = centsToMicroCents(costInCents)
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
if (!modelData) throw new Error(`Unsupported model: ${model}`)
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
const reasoningTokens = usage.reasoningTokens ?? 0
const cacheReadTokens = usage.cachedInputTokens ?? 0
const cacheWriteTokens =
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0
const inputCost = modelData.input * inputTokens
const outputCost = modelData.output * outputTokens
const reasoningCost = modelData.reasoning * reasoningTokens
const cacheReadCost = modelData.cacheRead * cacheReadTokens
const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
await Billing.consume({
model,
await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID: apiKey.workspaceID,
id: Identifier.create("usage"),
model: MODEL.id,
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWriteTokens,
costInCents,
cost,
})
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${cost}`,
})
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
})
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, keyRecord.id)),
)
}
} catch (error: any) {
return Response.json({ error: { message: error.message } }, { status: 500 })
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, apiKey.id)),
)
}
})
*/
} catch (error: any) {
if (error instanceof AuthError) {
return new Response(
JSON.stringify({
error: {
message: error.message,
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
}),
{
status: 401,
},
)
}
if (error instanceof CreditsError) {
return new Response(
JSON.stringify({
error: {
message: error.message,
type: "insufficient_quota",
param: null,
code: "insufficient_quota",
},
}),
{
status: 401,
},
)
}
if (error instanceof ModelError) {
return new Response(JSON.stringify({ error: { message: error.message } }), {
status: 401,
})
}
console.log(error)
return new Response(JSON.stringify({ error: { message: error.message } }), {
status: 500,
})
}
}
+59 -109
View File
@@ -2,6 +2,7 @@
--color-text: hsl(224, 10%, 10%);
--color-text-secondary: hsl(224, 7%, 46%);
--color-text-dimmed: hsl(224, 6%, 63%);
--color-text-inverted: hsl(0, 0%, 100%);
--color-border: hsl(224, 6%, 77%);
}
@@ -11,6 +12,7 @@
--color-text: hsl(0, 0%, 100%);
--color-text-secondary: hsl(224, 6%, 66%);
--color-text-dimmed: hsl(224, 7%, 46%);
--color-text-inverted: hsl(224, 10%, 10%);
--color-border: hsl(224, 6%, 36%);
}
@@ -19,12 +21,12 @@
[data-page="home"] {
--padding: 3rem;
--vertical-padding: 1.5rem;
--heading-font-size: 1.5625rem;
--heading-font-size: 1.375rem;
@media (max-width: 30rem) {
--padding: 1rem;
--vertical-padding: 0.75rem;
--heading-font-size: 1.375rem;
--heading-font-size: 1rem;
}
font-family: var(--font-mono);
@@ -41,19 +43,19 @@
[data-component="content"] {
max-width: 67.5rem;
margin: 0 auto;
border: 2px solid var(--color-border);
border: 1px solid var(--color-border);
}
[data-component="top"] {
padding: var(--padding);
display: flex;
flex-direction: column;
align-items: start;
align-items: center;
gap: calc(var(--vertical-padding) / 2);
img {
height: auto;
width: clamp(200px, 70vw, 400px);
width: clamp(200px, 85vw, 552px);
}
[data-slot="logo dark"] {
@@ -71,29 +73,32 @@
[data-slot="title"] {
line-height: 1.25;
font-weight: 500;
text-align: center;
font-size: var(--heading-font-size);
color: var(--color-text-secondary);
text-transform: uppercase;
}
}
[data-component="cta"] {
border-top: 2px solid var(--color-border);
display: grid;
grid-template-columns: auto 1fr auto;
align-items: stretch;
border-top: 1px solid var(--color-border);
display: flex;
& > div + div {
border-left: 2px solid var(--color-border);
border-left: 1px solid var(--color-border);
}
[data-slot="left"],
[data-slot="right"] {
[data-slot="left"] {
flex: 0 0 auto;
text-align: center;
line-height: 1.4;
padding: var(--vertical-padding) 2rem;
text-transform: uppercase;
font-size: 1.125rem;
@media (max-width: 30rem) {
font-size: 1rem;
padding-bottom: calc(var(--vertical-padding) + 4px);
}
@@ -103,52 +108,17 @@
}
}
[data-slot="center"] {
[data-slot="right"] {
flex: 1;
padding: var(--vertical-padding) 1rem;
}
@media (max-width: 55rem) {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
[data-slot="left"] {
grid-column: 1;
grid-row: 1;
}
@media (max-width: 50rem) {
flex-direction: column;
[data-slot="right"] {
grid-column: 2;
grid-row: 1;
}
[data-slot="center"] {
grid-column: 1 / -1;
grid-row: 2;
border-left: none;
border-top: 2px solid var(--color-border);
}
@media (max-width: 22rem) {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
[data-slot="left"] {
grid-column: 1;
grid-row: 1;
}
[data-slot="right"] {
grid-column: 1;
grid-row: 2;
border-top: 2px solid var(--color-border);
border-left: none;
}
[data-slot="center"] {
grid-column: 1;
grid-row: 3;
}
border-top: 1px solid var(--color-border);
}
}
@@ -168,7 +138,7 @@
@media (max-width: 24rem) {
font-size: 0.875rem;
}
@media (max-width: 64rem) {
@media (max-width: 56rem) {
[data-slot="protocol"] {
display: none;
}
@@ -189,7 +159,7 @@
}
[data-component="features"] {
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
padding: var(--padding);
[data-slot="list"] {
@@ -199,10 +169,21 @@
li {
margin-bottom: var(--space-4);
line-height: 1.6;
strong {
text-transform: uppercase;
font-weight: 700;
font-weight: 600;
}
label {
line-height: 1;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.03125rem;
background: var(--color-border);
padding: 0.125rem 0.375rem;
color: var(--color-text-inverted);
}
}
@@ -213,7 +194,7 @@
}
[data-component="install"] {
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
@@ -240,21 +221,21 @@
}
&:nth-child(2) {
border-left: 2px solid var(--color-border);
border-left: 1px solid var(--color-border);
@media (max-width: 40rem) {
border-left: none;
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
}
}
&:nth-child(3) {
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
}
&:nth-child(4) {
border-top: 2px solid var(--color-border);
border-left: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
border-left: 1px solid var(--color-border);
@media (max-width: 40rem) {
border-left: none;
@@ -303,38 +284,7 @@
}
[data-component="screenshots"] {
--images-height: 600px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: var(--images-height);
border-top: 2px solid var(--color-border);
& > div.left {
display: flex;
grid-row: 1;
grid-column: 1;
}
& > div.right {
display: grid;
grid-template-rows: 1fr 1fr;
grid-row: 1;
grid-column: 2;
border-left: 2px solid var(--color-border);
& > div.row1 {
display: flex;
grid-row: 1;
border-bottom: 2px solid var(--color-border);
height: calc(var(--images-height) / 2);
}
& > div.row2 {
display: flex;
grid-row: 2;
height: calc(var(--images-height) / 2);
}
}
border-top: 1px solid var(--color-border);
figure {
flex: 1;
@@ -384,24 +334,24 @@
}
}
& > div.left figure {
& > [data-slot="left"] figure {
height: var(--images-height);
box-sizing: border-box;
}
& > div.right figure {
& > [data-slot="right"] figure {
height: calc(var(--images-height) / 2);
box-sizing: border-box;
}
& > div.left img {
& > [data-slot="left"] img {
width: 100%;
height: 100%;
min-width: 0;
object-fit: contain;
}
& > div.right img {
& > [data-slot="right"] img {
width: 100%;
height: calc(100% - 2rem);
object-fit: contain;
@@ -415,30 +365,30 @@
grid-template-rows: auto auto;
}
& > div.left {
& > [data-slot="left"] {
grid-row: 1;
grid-column: 1;
}
& > div.right {
& > [data-slot="right"] {
grid-row: 2;
grid-column: 1;
border-left: none;
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
& > div.row1,
& > div.row2 {
& > [data-slot="row1"],
& > [data-slot="row2"] {
height: auto;
}
}
& > div.left figure,
& > div.right figure {
& > [data-slot="left"] figure,
& > [data-slot="right"] figure {
height: auto;
}
& > div.left img,
& > div.right img {
& > [data-slot="left"] img,
& > [data-slot="right"] img {
width: 100%;
height: auto;
max-height: none;
@@ -475,7 +425,7 @@
}
[data-component="footer"] {
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
display: flex;
flex-direction: row;
@@ -487,7 +437,7 @@
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 2px solid var(--color-border);
border-left: 1px solid var(--color-border);
}
/* Small desktop: first two columns shrink to content, third expands */
@@ -516,7 +466,7 @@
[data-slot="cell"]:nth-child(3) {
flex: 1 0 100%;
border-left: none;
border-top: 2px solid var(--color-border);
border-top: 1px solid var(--color-border);
}
}
}
+30 -53
View File
@@ -1,13 +1,13 @@
import { Title } from "@solidjs/meta"
import { onCleanup, onMount } from "solid-js"
import "./index.css"
import { Title } from "@solidjs/meta"
import { Match, onCleanup, onMount, Switch } from "solid-js"
import logoLight from "../asset/logo-ornate-light.svg"
import logoDark from "../asset/logo-ornate-dark.svg"
import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
import IMG_VSCODE from "../asset/lander/screenshot-vscode.png"
import IMG_GITHUB from "../asset/lander/screenshot-github.png"
import { IconCopy, IconCheck } from "../component/icon"
import { createAsync, query, redirect } from "@solidjs/router"
import { createAsync, query, redirect, A } from "@solidjs/router"
import { getActor } from "~/context/auth"
import { withActor } from "~/context/auth.withActor"
import { Account } from "@opencode/cloud-core/account.js"
@@ -21,20 +21,17 @@ function CopyStatus() {
)
}
const isLoggedIn = query(async () => {
const defaultWorkspace = query(async () => {
"use server"
const actor = await getActor()
if (actor.type === "account") {
const workspaces = await withActor(() => Account.workspaces())
throw redirect(`/workspace/${workspaces[0].id}`)
return workspaces[0].id
}
return false
}, "isLoggedIn")
}, "defaultWorkspace")
export default function Home() {
createAsync(() => isLoggedIn(), {
deferStream: true,
})
const workspace = createAsync(() => defaultWorkspace())
onMount(() => {
const commands = document.querySelectorAll("[data-copy]")
for (const button of commands) {
@@ -62,51 +59,51 @@ export default function Home() {
<section data-component="top">
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
<h1 data-slot="title">The AI coding agent built for the terminal.</h1>
<h1 data-slot="title">The AI coding agent built for the terminal</h1>
</section>
<section data-component="cta">
<div data-slot="left">
<a href="/docs">Get Started</a>
<a href="/docs">
Get Started
</a>
</div>
<div data-slot="center">
<div data-slot="right">
<button data-copy data-slot="command">
<span>
<span>curl -fsSL&nbsp;</span>
<span>curl -fsSL </span>
<span data-slot="protocol">https://</span>
<span data-slot="highlight">opencode.ai/install</span>
&nbsp;| bash
<span> | bash</span>
</span>
<CopyStatus />
</button>
</div>
<div data-slot="right">
<a href="/auth/authorize" target="_self">
Login
</a>
</div>
</section>
<section data-component="features">
<ul data-slot="list">
<li>
<strong>Native TUI</strong>: A responsive, native, themeable terminal UI.
<strong>Native TUI</strong> A responsive, native, themeable terminal UI
</li>
<li>
<strong>LSP enabled</strong>: Automatically loads the right LSPs for the LLM.
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
</li>
<li>
<strong>Multi-session</strong>: Start multiple agents in parallel on the same project.
<strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode <label>New</label>
</li>
<li>
<strong>Shareable links</strong>: Share a link to any sessions for reference or to debug.
<strong>Multi-session</strong> Start multiple agents in parallel on the same project
</li>
<li>
<strong>Claude Pro</strong>: Log in with Anthropic to use your Claude Pro or Max account.
<strong>Shareable links</strong> Share a link to any sessions for reference or to debug
</li>
<li>
<strong>Use any model</strong>: Supports 75+ LLM providers through{" "}
<a href="https://models.dev">Models.dev</a>, including local models.
<strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
</li>
<li>
<strong>Use any model</strong> Supports 75+ LLM providers through{" "}
<a href="https://models.dev">Models.dev</a>, including local models
</li>
</ul>
</section>
@@ -151,32 +148,12 @@ export default function Home() {
</section>
<section data-component="screenshots">
<div class="left">
<figure>
<figcaption>opencode TUI with the tokyonight theme</figcaption>
<a href="/docs/cli">
<img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
</a>
</figure>
</div>
<div class="right">
<div class="row1">
<figure>
<figcaption>opencode in VS Code</figcaption>
<a href="/docs/ide">
<img src={IMG_VSCODE} alt="opencode in VS Code" />
</a>
</figure>
</div>
<div class="row2">
<figure>
<figcaption>opencode in GitHub</figcaption>
<a href="/docs/github">
<img src={IMG_GITHUB} alt="opencode in GitHub" />
</a>
</figure>
</div>
</div>
<figure>
<figcaption>opencode TUI with the tokyonight theme</figcaption>
<a href="/docs/cli">
<img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
</a>
</figure>
</section>
<footer data-component="footer">
+20
View File
@@ -0,0 +1,20 @@
import type { APIEvent } from "@solidjs/start/server"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
const url = new URL(req.url)
const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
const response = await fetch(targetUrl, {
method: req.method,
headers: req.headers,
body: req.body,
})
return response
}
export const GET = handler
export const POST = handler
export const PUT = handler
export const DELETE = handler
export const OPTIONS = handler
export const PATCH = handler
+4 -2
View File
@@ -26,6 +26,8 @@ export async function POST(input: APIEvent) {
if (!amount) throw new Error("Amount not found")
if (!paymentID) throw new Error("Payment ID not found")
const chargedAmount = 2000
await Actor.provide("system", { workspaceID }, async () => {
const customer = await Billing.get()
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
@@ -50,7 +52,7 @@ export async function POST(input: APIEvent) {
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
balance: sql`${BillingTable.balance} + ${centsToMicroCents(chargedAmount)}`,
customerID,
paymentMethodID: paymentMethod.id,
paymentMethodLast4: paymentMethod.card!.last4,
@@ -59,7 +61,7 @@ export async function POST(input: APIEvent) {
await tx.insert(PaymentTable).values({
workspaceID,
id: Identifier.create("payment"),
amount: centsToMicroCents(amount),
amount: centsToMicroCents(chargedAmount),
paymentID,
customerID,
})
+74 -7
View File
@@ -1,9 +1,69 @@
[data-page="workspace"] {
line-height: 1;
@media (max-width: 30rem) {
padding: var(--space-4);
gap: var(--space-5);
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-accent);
}
&:active {
transform: translateY(1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
}
}
&[data-color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&[data-color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
}
}
}
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
/* Workspace Header */
@@ -19,9 +79,7 @@
background-color: var(--color-bg);
@media (max-width: 30rem) {
padding: 0 var(--space-4);
margin: calc(-1 * var(--space-4));
margin-bottom: var(--space-5);
padding: var(--space-4) var(--space-4);
}
}
@@ -48,13 +106,22 @@
align-items: center;
font-size: var(--font-size-sm);
span {
[data-slot="user"] {
color: var(--color-text-muted);
}
@media (max-width: 30rem) {
[data-slot="user"] {
display: none;
}
}
a,
button {
appearance: none;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: var(--color-text);
text-decoration: underline;
+28 -9
View File
@@ -1,38 +1,57 @@
import "./workspace.css"
import { useAuthSession } from "~/context/auth.session"
import { IconLogo } from "../component/icon"
import { action, redirect, RouteSectionProps } from "@solidjs/router"
import { withActor } from "~/context/auth.withActor"
import { query, action, redirect, createAsync, RouteSectionProps, Navigate, useNavigate, useParams, A } from "@solidjs/router"
import { User } from "@opencode/cloud-core/user.js"
import { Actor } from "@opencode/cloud-core/actor.js"
import { getRequestEvent } from "solid-js/web"
const getUserInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
return await User.fromID(actor.properties.userID)
}, workspaceID)
}, "userInfo")
const logout = action(async () => {
"use server"
const auth = await useAuthSession()
const event = getRequestEvent()
const current = auth.data.current
if (current)
await auth.update((val) => {
delete val.account[current]
delete val.account?.[current]
const first = Object.keys(val.account ?? {})[0]
val.current = first
event!.locals.actor = undefined
return val
})
return redirect("/")
throw redirect("/")
})
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userInfo = createAsync(() => getUserInfo(params.id))
return (
<main data-page="workspace">
<header data-component="workspace-header">
<div data-slot="header-brand">
<a href="/" data-component="site-title">
<A href="/" data-component="site-title">
<IconLogo />
</a>
</A>
</div>
<div data-slot="header-actions">
<span>name@example.com</span>
<span data-slot="user">{userInfo()?.email}</span>
<form action={logout} method="post">
<button type="submit" formaction={logout}>Logout</button>
<button type="submit" formaction={logout}>
Logout
</button>
</form>
</div>
</header>
<div data-slot="content">{props.children}</div>
<div>{props.children}</div>
</main>
)
}
+352 -398
View File
@@ -1,5 +1,4 @@
/* Root container */
[data-slot="root"] {
[data-page="workspace-[id]"] {
max-width: 64rem;
padding: var(--space-10) var(--space-4);
margin: 0 auto;
@@ -8,468 +7,423 @@
flex-direction: column;
gap: var(--space-10);
@media (max-width: 30rem) {
padding-top: var(--space-4);
padding-bottom: var(--space-4);
gap: var(--space-8);
}
[data-slot="sections"] {
display: flex;
flex-direction: column;
gap: var(--space-16);
@media (max-width: 30rem) {
gap: var(--space-8);
}
section {
display: flex;
flex-direction: column;
gap: var(--space-6);
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-md);
}
}
p {
line-height: 1.4;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
}
section:not(:last-child) {
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--space-16);
}
}
}
/* Common elements */
button {
padding: var(--space-3) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
font-weight: 500;
text-transform: uppercase;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-accent);
}
&:active {
transform: translateY(1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background-color: var(--color-bg);
border-color: var(--color-border);
transform: none;
@media (max-width: 30rem) {
padding-bottom: var(--space-8);
}
}
}
&[color="primary"] {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-text);
[data-component="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
&:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&[color="ghost"] {
background-color: transparent;
border-color: transparent;
color: var(--color-text-muted);
&:hover {
background-color: var(--color-surface-hover);
border-color: var(--color-border);
color: var(--color-text);
}
}
}
a {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: var(--space-0-75);
text-decoration-thickness: 1px;
}
[data-slot="empty-state"] {
padding: var(--space-20) var(--space-6);
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
flex-direction: column;
gap: var(--space-2);
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
margin: 0;
}
}
/* Title section */
[data-slot="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
line-height: 1.25;
}
}
p {
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
p {
line-height: 1.5;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
}
/* Section titles */
[data-slot="section-title"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h2 {
font-size: var(--font-size-md);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
color: var(--color-text-secondary);
text-transform: uppercase;
/* Title section */
[data-component="title-section"] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding-bottom: var(--space-8);
border-bottom: 1px solid var(--color-border);
@media (max-width: 30rem) {
font-size: var(--font-size-lg);
line-height: 1.25;
padding-bottom: var(--space-6);
}
h1 {
font-size: var(--font-size-2xl);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.03125rem;
margin: 0;
text-transform: uppercase;
@media (max-width: 30rem) {
font-size: var(--font-size-xl);
}
}
p {
line-height: 1.5;
font-size: var(--font-size-md);
color: var(--color-text-muted);
a {
color: var(--color-text-muted);
}
}
}
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
/* API Keys Section */
[data-slot="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
background-color: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
max-width: 32rem;
input {
padding: var(--space-2) var(--space-3);
/* API Keys Section */
[data-component="api-keys-section"] {
[data-slot="create-form"] {
display: flex;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
@media (max-width: 30rem) {
gap: var(--space-2);
}
input {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
background-color: var(--color-bg);
color: var(--color-text);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
font-family: var(--font-mono);
&:focus {
outline: none;
border-color: var(--color-accent);
}
&::placeholder {
color: var(--color-text-disabled);
}
}
[data-slot="form-actions"] {
display: flex;
gap: var(--space-2);
justify-content: flex-end;
}
}
[data-slot="api-keys-table"] {
overflow-x: auto;
}
[data-slot="api-keys-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
&:nth-child(3) /* Date */ {
display: none;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="key-name"] {
color: var(--color-text);
font-family: var(--font-sans);
font-weight: 500;
}
&[data-slot="key-value"] {
font-family: var(--font-mono);
div {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--space-2);
}
}
&[data-slot="key-date"] {
color: var(--color-text);
}
&[data-slot="key-actions"] {
font-family: var(--font-sans);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(3) /* Date */ {
display: none;
}
}
td {
&:nth-child(3) /* Date */ {
display: none;
}
}
}
}
}
}
/* Balance Section */
[data-slot="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
/* Balance Section */
[data-component="balance-section"] {
[data-slot="balance"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
min-width: 14.5rem;
width: fit-content;
[data-slot="amount"] {
padding: var(--space-3-5) var(--space-4);
background-color: var(--color-bg-surface);
border-radius: var(--border-radius-sm);
display: flex;
align-items: baseline;
gap: var(--space-1);
justify-content: flex-end;
&[data-state="danger"] {
[data-slot="value"] {
color: var(--color-danger);
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
color: var(--color-text-muted);
font-weight: 400;
}
&.danger {
[data-slot="value"] {
color: var(--color-danger);
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
[data-slot="currency"] {
position: relative;
bottom: 2px;
font-size: var(--font-size-lg);
/* Payments Section */
[data-component="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
font-weight: 400;
text-transform: uppercase;
}
[data-slot="value"] {
font-size: var(--font-size-3xl);
font-weight: 500;
color: var(--color-text);
}
}
}
}
/* Payments Section */
[data-slot="payments-section"] {
[data-slot="payments-table"] {
overflow-x: auto;
}
[data-slot="payments-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
font-weight: 400;
&[data-slot="payment-date"] {
color: var(--color-text);
}
&[data-slot="payment-id"] {
font-family: var(--font-mono);
font-weight: 400;
color: var(--color-text-muted);
max-width: 200px;
word-break: break-word;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
}
}
}
}
}
/* Usage Section */
[data-component="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
max-width: 200px;
word-break: break-word;
text-transform: uppercase;
}
&[data-slot="payment-amount"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
th {
&:nth-child(2) /* Payment ID */ {
display: none;
&[data-slot="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
td {
&:nth-child(2) /* Payment ID */ {
display: none;
tbody tr {
&:last-child td {
border-bottom: none;
}
}
}
}
}
/* Usage Section */
[data-slot="usage-section"] {
[data-slot="usage-table"] {
overflow-x: auto;
}
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-sm);
thead {
border-bottom: 1px solid var(--color-border);
}
th {
padding: var(--space-3) var(--space-4);
text-align: left;
font-weight: normal;
color: var(--color-text-muted);
text-transform: uppercase;
}
td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-muted);
color: var(--color-text-muted);
font-family: var(--font-mono);
&[data-slot="usage-date"] {
color: var(--color-text);
}
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
}
&[data-slot="usage-cost"] {
color: var(--color-text);
}
}
tbody tr {
&:last-child td {
border-bottom: none;
}
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
@media (max-width: 40rem) {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
td {
&:nth-child(2) /* Model */ {
display: none;
}
}
}
}
+409 -442
View File
@@ -1,36 +1,66 @@
import "./[id].css"
import { Billing } from "@opencode/cloud-core/billing.js"
import { Key } from "@opencode/cloud-core/key.js"
import { action, createAsync, query, useAction, useSubmission, json } from "@solidjs/router"
import { createSignal, For, onMount, Show } from "solid-js"
import { getActor } from "~/context/auth"
import {
json,
query,
action,
useParams,
useAction,
createAsync,
useSubmission,
} from "@solidjs/router"
import { createMemo, createSignal, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { IconCopy, IconCheck } from "~/component/icon"
import "./[id].css"
import { User } from "@opencode/cloud-core/user.js"
import { Actor } from "@opencode/cloud-core/actor.js"
function formatDateForTable(date: Date) {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "short",
hour: "numeric",
minute: "2-digit",
hour12: true,
}
return date.toLocaleDateString("en-GB", options).replace(",", ",")
}
function formatDateUTC(date: Date) {
const options: Intl.DateTimeFormatOptions = {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
timeZone: "UTC",
}
return date.toLocaleDateString("en-US", options)
}
/////////////////////////////////////
// Keys related queries and actions
/////////////////////////////////////
const listKeys = query(() => {
const listKeys = query(async (workspaceID: string) => {
"use server"
return withActor(() => Key.list())
return withActor(() => Key.list(), workspaceID)
}, "key.list")
const createKey = action(async (name: string) => {
const createKey = action(async (workspaceID: string, name: string) => {
"use server"
return json(
withActor(() => Key.create({ name })),
withActor(() => Key.create({ name }), workspaceID),
{ revalidate: listKeys.key },
)
}, "key.create")
const removeKey = action(async (id: string) => {
const removeKey = action(async (workspaceID: string, id: string) => {
"use server"
return json(
withActor(() => Key.remove({ id })),
withActor(() => Key.remove({ id }), workspaceID),
{ revalidate: listKeys.key },
)
}, "key.remove")
@@ -39,240 +69,224 @@ const removeKey = action(async (id: string) => {
// Billing related queries and actions
/////////////////////////////////////
const getBillingInfo = query(async () => {
const getBalanceInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
const actor = Actor.assert("user")
const [user, billing, payments, usage] = await Promise.all([
User.fromID(actor.properties.userID),
Billing.get(),
Billing.payments(),
Billing.usages(),
])
return { user, billing, payments, usage }
})
}, "billingInfo")
return await Billing.get()
}, workspaceID)
}, "balanceInfo")
const createCheckoutUrl = action(async (successUrl: string, cancelUrl: string) => {
const getUsageInfo = query(async (workspaceID: string) => {
"use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }))
return withActor(async () => {
return await Billing.usages()
}, workspaceID)
}, "usageInfo")
const getPaymentsInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return await Billing.payments()
}, workspaceID)
}, "paymentsInfo")
const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
"use server"
return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
}, "checkoutUrl")
const createPortalUrl = action(async (returnUrl: string) => {
"use server"
return withActor(() => Billing.generatePortalUrl({ returnUrl }))
}, "portalUrl")
// const createPortalUrl = action(async (workspaceID: string, returnUrl: string) => {
// "use server"
// return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
// }, "portalUrl")
const dummyUsageData = [
{
model: "claude-3-5-sonnet-20241022",
inputTokens: 1250,
outputTokens: 890,
reasoningTokens: 150,
cacheReadTokens: 0,
cacheWriteTokens: 45,
cost: 12340000,
timeCreated: new Date("2025-01-28T10:30:00Z"),
},
{
model: "claude-3-haiku-20240307",
inputTokens: 2100,
outputTokens: 450,
reasoningTokens: null,
cacheReadTokens: 120,
cacheWriteTokens: 0,
cost: 5670000,
timeCreated: new Date("2025-01-27T15:22:00Z"),
},
{
model: "claude-3-5-sonnet-20241022",
inputTokens: 850,
outputTokens: 1200,
reasoningTokens: 220,
cacheReadTokens: 30,
cacheWriteTokens: 15,
cost: 18990000,
timeCreated: new Date("2025-01-27T09:15:00Z"),
},
{
model: "claude-3-opus-20240229",
inputTokens: 3200,
outputTokens: 1800,
reasoningTokens: 400,
cacheReadTokens: 0,
cacheWriteTokens: 100,
cost: 45670000,
timeCreated: new Date("2025-01-26T14:45:00Z"),
},
{
model: "claude-3-haiku-20240307",
inputTokens: 650,
outputTokens: 280,
reasoningTokens: null,
cacheReadTokens: 200,
cacheWriteTokens: 0,
cost: 2340000,
timeCreated: new Date("2025-01-25T16:18:00Z"),
},
]
function KeysSection() {
// Dummy data for testing
const dummyKeys = [
{
id: "key_1",
name: "Development API Key",
key: "oc_dev_1234567890abcdef1234567890abcdef12345678",
timeCreated: new Date("2024-01-15T10:30:00Z"),
},
{
id: "key_2",
name: "Production API Key",
key: "oc_prod_abcdef1234567890abcdef1234567890abcdef12",
timeCreated: new Date("2024-02-01T14:22:00Z"),
},
{
id: "key_3",
name: "Testing Environment",
key: "oc_test_9876543210fedcba9876543210fedcba98765432",
timeCreated: new Date("2024-02-10T09:15:00Z"),
},
]
const dummyPaymentData = [
{
id: "pay_1Ab2Cd3Ef4Gh5678",
amount: 2000000000,
timeCreated: new Date("2025-01-28T14:32:00Z"),
},
{
id: "pay_9Ij8Kl7Mn6Op5432",
amount: 1000000000,
timeCreated: new Date("2025-01-25T09:18:00Z"),
},
{
id: "pay_5Qr4St3Uv2Wx1098",
amount: 5000000000,
timeCreated: new Date("2025-01-20T16:45:00Z"),
},
{
id: "pay_7Yz6Ab5Cd4Ef3210",
amount: 1500000000,
timeCreated: new Date("2025-01-15T11:22:00Z"),
},
{
id: "pay_3Gh2Ij1Kl0Mn9876",
amount: 3000000000,
timeCreated: new Date("2025-01-10T13:55:00Z"),
},
]
const params = useParams()
const keys = createAsync(() => listKeys(params.id))
// const keys = () => dummyKeys
const [showForm, setShowForm] = createSignal(false)
const [name, setName] = createSignal("")
const removeAction = useAction(removeKey)
const createAction = useAction(createKey)
const createSubmission = useSubmission(createKey)
const [copiedId, setCopiedId] = createSignal<string | null>(null)
const dummyApiKeyData = [
{
id: "key_1Ab2Cd3Ef4Gh5678",
name: "Production API",
key: "oc_live_sk_1Ab2Cd3Ef4Gh567890123456789012345678901234567890",
timeCreated: new Date("2025-01-28T14:32:00Z"),
timeUsed: new Date("2025-01-29T09:15:00Z"),
},
{
id: "key_9Ij8Kl7Mn6Op5432",
name: "Development Key",
key: "oc_test_sk_9Ij8Kl7Mn6Op543210987654321098765432109876543210",
timeCreated: new Date("2025-01-25T09:18:00Z"),
timeUsed: null,
},
{
id: "key_5Qr4St3Uv2Wx1098",
name: "CI/CD Pipeline",
key: "oc_live_sk_5Qr4St3Uv2Wx109876543210987654321098765432109876",
timeCreated: new Date("2025-01-20T16:45:00Z"),
timeUsed: new Date("2025-01-28T12:30:00Z"),
},
]
export default function () {
const actor = createAsync(() => getActor())
onMount(() => {
console.log("MOUNTED", actor())
})
/////////////////
// Keys section
/////////////////
const keys = createAsync(() => listKeys())
const createKeyAction = useAction(createKey)
const removeKeyAction = useAction(removeKey)
const createKeySubmission = useSubmission(createKey)
const [showCreateForm, setShowCreateForm] = createSignal(false)
const [keyName, setKeyName] = createSignal("")
const [copiedKeyId, setCopiedKeyId] = createSignal<string | null>(null)
const formatDate = (date: Date) => {
return date.toLocaleDateString()
}
const formatDateForTable = (date: Date) => {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "short",
hour: "numeric",
minute: "2-digit",
hour12: true,
}
return date.toLocaleDateString("en-GB", options).replace(",", ",")
}
const formatDateUTC = (date: Date) => {
const options: Intl.DateTimeFormatOptions = {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
timeZone: "UTC",
}
return date.toLocaleDateString("en-US", options)
}
const formatKey = (key: string) => {
function formatKey(key: string) {
if (key.length <= 11) return key
return `${key.slice(0, 7)}...${key.slice(-4)}`
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
const copyKeyToClipboard = async (text: string, keyId: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedKeyId(keyId)
setTimeout(() => setCopiedKeyId(null), 1500)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
const handleCreateKey = async () => {
if (!keyName().trim()) return
async function handleCreateKey() {
if (!name().trim()) return
try {
await createKeyAction(keyName().trim())
setKeyName("")
setShowCreateForm(false)
await createAction(params.id, name().trim())
setName("")
setShowForm(false)
} catch (error) {
console.error("Failed to create API key:", error)
}
}
const handleDeleteKey = async (keyId: string) => {
async function copyKeyToClipboard(text: string, keyId: string) {
try {
await navigator.clipboard.writeText(text)
setCopiedId(keyId)
setTimeout(() => setCopiedId(null), 1500)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
async function handleDeleteKey(keyId: string) {
if (!confirm("Are you sure you want to delete this API key?")) {
return
}
try {
await removeKeyAction(keyId)
await removeAction(params.id, keyId)
} catch (error) {
console.error("Failed to delete API key:", error)
}
}
/////////////////
// Billing section
/////////////////
const billingInfo = createAsync(() => getBillingInfo())
return (
<section data-component="api-keys-section">
<div data-slot="section-title">
<h2>API Keys</h2>
<p>Manage your API keys for accessing opencode services.</p>
</div>
<Show
when={!showForm()}
fallback={
<div data-slot="create-form">
<input
data-component="input"
type="text"
placeholder="Enter key name"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
/>
<div data-slot="form-actions">
<button
data-color="ghost"
onClick={() => {
setShowForm(false)
setName("")
}}
>
Cancel
</button>
<button
data-color="primary"
disabled={createSubmission.pending || !name().trim()}
onClick={handleCreateKey}
>
{createSubmission.pending ? "Creating..." : "Create"}
</button>
</div>
</div>
}
>
<button
data-color="primary"
onClick={() => {
console.log("clicked")
setShowForm(true)
}}
>
Create API Key
</button>
</Show>
<div data-slot="api-keys-table">
<Show
when={keys()?.length}
fallback={
<div data-component="empty-state">
<p>Create an opencode Gateway API key</p>
</div>
}
>
<table data-slot="api-keys-table-element">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody>
<For each={keys()!}>
{(key) => (
<tr>
<td data-slot="key-name">{key.name}</td>
<td data-slot="key-value">
<div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
<span>{formatKey(key.key)}</span>
<Show
when={copiedId() === key.id}
fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
>
<IconCheck style={{ width: "14px", height: "14px" }} />
</Show>
</div>
</td>
<td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
{formatDateForTable(key.timeCreated)}
</td>
<td data-slot="key-actions">
<button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
Delete
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
</div>
</section>
)
}
function BalanceSection() {
const params = useParams()
const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents
const balanceInfo = createAsync(() => getBalanceInfo(params.id))
// const balanceInfo = () => dummyBalanceInfo
const createCheckoutUrlAction = useAction(createCheckoutUrl)
const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
const handleBuyCredits = async () => {
async function handleBuyCredits() {
try {
const baseUrl = window.location.href
const checkoutUrl = await createCheckoutUrlAction(baseUrl, baseUrl)
const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
if (checkoutUrl) {
window.location.href = checkoutUrl
}
@@ -282,241 +296,194 @@ export default function () {
}
return (
<div data-slot="root">
{/* Title */}
<section data-slot="title-section">
<h1>Gateway</h1>
<section data-component="balance-section">
<div data-slot="section-title">
<h2>Balance</h2>
<p>Add credits to your account.</p>
</div>
<div data-slot="balance">
<div
data-slot="amount"
data-state={(() => {
const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
return balanceStr === "0.00" || balanceStr === "-0.00" ? "danger" : undefined
})()}
>
<span data-slot="currency">$</span>
<span data-slot="value">
{(() => {
const balanceStr = ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
return balanceStr === "-0.00" ? "0.00" : balanceStr
})()}
</span>
</div>
<button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</div>
</section>
)
}
function UsageSection() {
const params = useParams()
const dummyUsage = [
{
id: "usage_1",
model: "claude-3-sonnet-20240229",
inputTokens: 1250,
outputTokens: 890,
cost: 125000000, // $1.25 in cents
timeCreated: "2024-02-10T15:30:00Z",
},
{
id: "usage_2",
model: "gpt-4-turbo-preview",
inputTokens: 2100,
outputTokens: 1456,
cost: 340000000, // $3.40 in cents
timeCreated: "2024-02-09T09:45:00Z",
},
{
id: "usage_3",
model: "claude-3-haiku-20240307",
inputTokens: 850,
outputTokens: 620,
cost: 45000000, // $0.45 in cents
timeCreated: "2024-02-08T13:22:00Z",
},
{
id: "usage_4",
model: "gpt-3.5-turbo",
inputTokens: 1800,
outputTokens: 1200,
cost: 85000000, // $0.85 in cents
timeCreated: "2024-02-07T11:15:00Z",
},
]
const usage = createAsync(() => getUsageInfo(params.id))
// const usage = () => dummyUsage
return (
<section data-component="usage-section">
<div data-slot="section-title">
<h2>Usage History</h2>
<p>Recent API usage and costs.</p>
</div>
<div data-slot="usage-table">
<Show
when={usage() && usage()!.length > 0}
fallback={
<div data-component="empty-state">
<p>Make your first API call to get started.</p>
</div>
}
>
<table data-slot="usage-table-element">
<thead>
<tr>
<th>Date</th>
<th>Model</th>
<th>Input</th>
<th>Output</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<For each={usage()!}>
{(usage) => {
const date = createMemo(() => new Date(usage.timeCreated))
return (
<tr>
<td data-slot="usage-date" title={formatDateUTC(date())}>
{formatDateForTable(date())}
</td>
<td data-slot="usage-model">{usage.model}</td>
<td data-slot="usage-tokens">{usage.inputTokens}</td>
<td data-slot="usage-tokens">{usage.outputTokens}</td>
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</Show>
</div>
</section>
)
}
function PaymentsSection() {
const params = useParams()
const dummyPayments = [
{
id: "pi_1234567890",
amount: 5000000000, // $50.00 in cents
timeCreated: "2024-02-01T10:00:00Z",
},
{
id: "pi_0987654321",
amount: 2500000000, // $25.00 in cents
timeCreated: "2024-01-15T14:30:00Z",
},
]
const payments = createAsync(() => getPaymentsInfo(params.id))
// const payments = () => dummyPayments
return payments() && payments()!.length > 0 && (
<section data-component="payments-section">
<div data-slot="section-title">
<h2>Payments History</h2>
<p>Recent payment transactions.</p>
</div>
<div data-slot="payments-table">
<table data-slot="payments-table-element">
<thead>
<tr>
<th>Date</th>
<th>Payment ID</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<For each={payments()!}>
{(payment) => {
const date = new Date(payment.timeCreated)
return (
<tr>
<td data-slot="payment-date" title={formatDateUTC(date)}>
{formatDateForTable(date)}
</td>
<td data-slot="payment-id">{payment.id}</td>
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</section>
)
}
export default function() {
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
<h1>Zen</h1>
<p>
Coding models optimized for use with opencode. <a href="/docs">Learn more</a>.
Curated list of models provided by opencode. <a target="_blank" href="/docs/zen">Learn more</a>.
</p>
</section>
<div data-slot="sections">
{/* Actor Section */}
<section data-slot="actor-section">
<div data-slot="section-title">
<h2>Actor</h2>
<p>Current authenticated user information and session details.</p>
</div>
<div>{JSON.stringify(actor())}</div>
</section>
{/* API Keys Section */}
<section data-slot="api-keys-section">
<div data-slot="section-title">
<h2>API Keys</h2>
<p>Manage your API keys for accessing opencode services.</p>
</div>
<Show
when={!showCreateForm()}
fallback={
<div data-slot="create-form">
<input
data-component="input"
type="text"
placeholder="Enter key name"
value={keyName()}
onInput={(e) => setKeyName(e.currentTarget.value)}
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
/>
<div data-slot="form-actions">
<button
color="primary"
disabled={createKeySubmission.pending || !keyName().trim()}
onClick={handleCreateKey}
>
{createKeySubmission.pending ? "Creating..." : "Create"}
</button>
<button
color="ghost"
onClick={() => {
setShowCreateForm(false)
setKeyName("")
}}
>
Cancel
</button>
</div>
</div>
}
>
<button
color="primary"
onClick={() => {
console.log("clicked")
setShowCreateForm(true)
}}
>
Create API Key
</button>
</Show>
<div data-slot="api-keys-table">
<Show
when={keys()?.length}
fallback={
<div data-slot="empty-state">
<p>Create an opencode Gateway API key</p>
</div>
}
>
<table data-slot="api-keys-table-element">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody>
<For each={keys()!}>
{/* Real data: keys() */}
{(key) => (
<tr>
<td data-slot="key-name">{key.name}</td>
<td data-slot="key-value">
<div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
<span>{formatKey(key.key)}</span>
<Show
when={copiedKeyId() === key.id}
fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
>
<IconCheck style={{ width: "14px", height: "14px" }} />
</Show>
</div>
</td>
<td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
{formatDateForTable(key.timeCreated)}
</td>
<td data-slot="key-actions">
<button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
Delete
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
</div>
</section>
{/* Balance Section */}
<section data-slot="balance-section">
<div data-slot="section-title">
<h2>Balance</h2>
<p>Add credits to your account.</p>
</div>
<div data-slot="balance">
<div
data-slot="amount"
classList={{
danger: (() => {
const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
return balanceStr === "0.00" || balanceStr === "-0.00"
})(),
}}
>
<span data-slot="currency">$</span>
<span data-slot="value">
{(() => {
const balanceStr = ((billingInfo()?.billing?.balance ?? 0) / 100000000).toFixed(2)
return balanceStr === "-0.00" ? "0.00" : balanceStr
})()}
</span>
</div>
<button color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</div>
</section>
{/* Payments Section */}
<Show when={dummyPaymentData.length > 0}>
{/* Real data condition: billingInfo() && billingInfo()!.payments.length > 0 */}
<section data-slot="payments-section">
<div data-slot="section-title">
<h2>Payments History</h2>
<p>Recent payment transactions.</p>
</div>
<div data-slot="payments-table">
<table data-slot="payments-table-element">
<thead>
<tr>
<th>Date</th>
<th>Payment ID</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<For each={dummyPaymentData}>
{/* Real data: billingInfo()?.payments */}
{(payment) => {
const date = new Date(payment.timeCreated)
return (
<tr>
<td data-slot="payment-date" title={formatDateUTC(date)}>
{formatDateForTable(date)}
</td>
<td data-slot="payment-id">{payment.id}</td>
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</section>
</Show>
{/* Usage Section */}
<section data-slot="usage-section">
<div data-slot="section-title">
<h2>Usage History</h2>
<p>Recent API usage and costs.</p>
</div>
<div data-slot="usage-table">
<Show
when={dummyUsageData.length > 0}
fallback={
<div data-slot="empty-state">
<p>Make your first API call to get started.</p>
</div>
}
>
<table data-slot="usage-table-element">
<thead>
<tr>
<th>Date</th>
<th>Model</th>
<th>Tokens</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
<For each={dummyUsageData}>
{(usage) => {
const totalTokens = usage.inputTokens + usage.outputTokens + (usage.reasoningTokens || 0)
const date = new Date(usage.timeCreated)
return (
<tr>
<td data-slot="usage-date" title={formatDateUTC(date)}>
{formatDateForTable(date)}
</td>
<td data-slot="usage-model">{usage.model}</td>
<td data-slot="usage-tokens">{totalTokens.toLocaleString()}</td>
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
</tr>
)
}}
</For>
</tbody>
</table>
</Show>
</div>
</section>
<KeysSection />
<BalanceSection />
<UsageSection />
<PaymentsSection />
</div>
</div>
)
@@ -0,0 +1,315 @@
import { Resource } from "@opencode/cloud-resource"
import type { APIEvent } from "@solidjs/start/server"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "@opencode/cloud-core/identifier.js"
const MODELS = {
// "anthropic/claude-sonnet-4": {
// auth: true,
// api: "https://api.anthropic.com",
// apiKey: Resource.ANTHROPIC_API_KEY.value,
// model: "claude-sonnet-4-20250514",
// cost: {
// input: 0.0000015,
// output: 0.000006,
// reasoning: 0.0000015,
// cacheRead: 0.0000001,
// cacheWrite: 0.0000001,
// },
// headerMappings: {},
// },
"qwen/qwen3-coder": {
id: "qwen/qwen3-coder" as const,
auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: {
input: 0.00000038,
output: 0.00000153,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
},
headerMappings: {},
},
"grok-code": {
id: "x-ai/grok-code-fast-1" as const,
auth: false,
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
cost: {
input: 0,
output: 0,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
},
headerMappings: {
"x-grok-conv-id": "x-opencode-session",
"x-grok-req-id": "x-opencode-request",
},
},
}
const FREE_WORKSPACES = [
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
]
class AuthError extends Error {}
class CreditsError extends Error {}
class ModelError extends Error {}
export async function POST(input: APIEvent) {
try {
const url = new URL(input.request.url)
const body = await input.request.json()
logMetric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
request: input.request.headers.get("x-opencode-request"),
})
const MODEL = validateModel()
const apiKey = await authenticate()
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
await checkCredits()
// Request to model provider
const res = await fetch(new URL(url.pathname.replace(/^\/zen/, "") + url.search, MODEL.api), {
method: "POST",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
headers.set("authorization", `Bearer ${MODEL.apiKey}`)
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
return headers
})(),
body: JSON.stringify({
...body,
model: MODEL.model,
stream_options: {
include_usage: true,
},
}),
})
// Scrub response headers
const resHeaders = new Headers()
const keepHeaders = ["content-type", "cache-control"]
for (const [k, v] of res.headers.entries()) {
if (keepHeaders.includes(k.toLowerCase())) {
resHeaders.set(k, v)
}
}
// Handle non-streaming response
if (!body.stream) {
const json = await res.json()
const body = JSON.stringify(json)
logMetric({ response_length: body.length })
await trackUsage(json)
return new Response(body, {
status: res.status,
statusText: res.statusText,
headers: resHeaders,
})
}
// Handle streaming response
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
const decoder = new TextDecoder()
let buffer = ""
let responseLength = 0
let startTimestamp = Date.now()
let receivedFirstByte = false
function pump(): Promise<void> {
return (
reader?.read().then(async ({ done, value }) => {
if (done) {
logMetric({ response_length: responseLength })
c.close()
return
}
if (!receivedFirstByte) {
receivedFirstByte = true
logMetric({ time_to_first_byte: Date.now() - startTimestamp })
}
buffer += decoder.decode(value, { stream: true })
responseLength += value.length
const parts = buffer.split("\n\n")
buffer = parts.pop() ?? ""
const usage = parts
.map((part) => part.trim())
.filter((part) => part.startsWith("data: "))
.map((part) => {
try {
return JSON.parse(part.slice(6))
} catch (e) {
return {}
}
})
.find((part) => part.usage)
if (usage) await trackUsage(usage)
c.enqueue(value)
return pump()
}) || Promise.resolve()
)
}
return pump()
},
})
return new Response(stream, {
status: res.status,
statusText: res.statusText,
headers: resHeaders,
})
function validateModel() {
if (!(body.model in MODELS)) {
throw new ModelError(`Model ${body.model} not supported`)
}
const model = MODELS[body.model as keyof typeof MODELS]
logMetric({ model: model.id })
return model
}
async function authenticate() {
try {
const authHeader = input.request.headers.get("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) throw new AuthError("Missing API key.")
const apiKey = authHeader.split(" ")[1]
const key = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
if (!key) throw new AuthError("Invalid API key.")
logMetric({
api_key: key.id,
workspace: key.workspaceID,
})
return key
} catch (e) {
// ignore error if model does not require authentication
if (!MODEL.auth) return
throw e
}
}
async function checkCredits() {
if (!apiKey || !MODEL.auth || isFree) return
const billing = await Database.use((tx) =>
tx
.select({
balance: BillingTable.balance,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
.then((rows) => rows[0]),
)
if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
}
async function trackUsage(chunk: any) {
const usage = chunk.usage
const inputTokens = usage.prompt_tokens ?? 0
const outputTokens = usage.completion_tokens ?? 0
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens ?? 0
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0
//const cacheWriteTokens = providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? 0
const cacheWriteTokens = 0
const inputCost = MODEL.cost.input * inputTokens * 100
const outputCost = MODEL.cost.output * outputTokens * 100
const reasoningCost = MODEL.cost.reasoning * reasoningTokens * 100
const cacheReadCost = MODEL.cost.cacheRead * cacheReadTokens * 100
const cacheWriteCost = MODEL.cost.cacheWrite * cacheWriteTokens * 100
const totalCostInCent = inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost
logMetric({
"tokens.input": inputTokens,
"tokens.output": outputTokens,
"tokens.reasoning": reasoningTokens,
"tokens.cache_read": cacheReadTokens,
"tokens.cache_write": cacheWriteTokens,
"cost.input": Math.round(inputCost),
"cost.output": Math.round(outputCost),
"cost.reasoning": Math.round(reasoningCost),
"cost.cache_read": Math.round(cacheReadCost),
"cost.cache_write": Math.round(cacheWriteCost),
"cost.total": Math.round(totalCostInCent),
})
if (!apiKey) return
const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID: apiKey.workspaceID,
id: Identifier.create("usage"),
model: MODEL.id,
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWriteTokens,
cost,
})
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${cost}`,
})
.where(eq(BillingTable.workspaceID, apiKey.workspaceID))
})
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, apiKey.id)),
)
}
} catch (error: any) {
logMetric({
"error.type": error.constructor.name,
"error.message": error.message,
})
if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError)
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 401 })
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 500 })
}
function logMetric(values: Record<string, any>) {
console.log(`_metric:${JSON.stringify(values)}`)
}
}
+1
View File
@@ -3,6 +3,7 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
+2 -2
View File
@@ -1,4 +1,4 @@
import { Resource } from "@opencode/cloud-resource"
import { Resource } from "sst"
import { defineConfig } from "drizzle-kit"
export default defineConfig({
@@ -6,7 +6,7 @@ export default defineConfig({
strict: true,
schema: ["./src/**/*.sql.ts"],
verbose: true,
dialect: "postgresql",
dialect: "mysql",
dbCredentials: {
database: Resource.Database.database,
host: Resource.Database.host,
@@ -1,66 +0,0 @@
CREATE TABLE "billing" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_method_id" varchar(255),
"payment_method_last4" varchar(4),
"balance" bigint NOT NULL,
"reload" boolean,
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "payment" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"customer_id" varchar(255),
"payment_id" varchar(255),
"amount" bigint NOT NULL,
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "usage" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"request_id" varchar(255),
"model" varchar(255) NOT NULL,
"input_tokens" integer NOT NULL,
"output_tokens" integer NOT NULL,
"reasoning_tokens" integer,
"cache_read_tokens" integer,
"cache_write_tokens" integer,
"cost" bigint NOT NULL,
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" text NOT NULL,
"name" varchar(255) NOT NULL,
"time_seen" timestamp with time zone,
"color" integer,
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
CREATE TABLE "workspace" (
"id" varchar(30) PRIMARY KEY NOT NULL,
"slug" varchar(255),
"name" varchar(255),
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");
@@ -0,0 +1,89 @@
CREATE TABLE `account` (
`id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`email` varchar(255) NOT NULL,
CONSTRAINT `email` UNIQUE(`email`)
);
--> statement-breakpoint
CREATE TABLE `billing` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`customer_id` varchar(255),
`payment_method_id` varchar(255),
`payment_method_last4` varchar(4),
`balance` bigint NOT NULL,
`reload` boolean,
CONSTRAINT `billing_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
);
--> statement-breakpoint
CREATE TABLE `payment` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`customer_id` varchar(255),
`payment_id` varchar(255),
`amount` bigint NOT NULL,
CONSTRAINT `payment_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
);
--> statement-breakpoint
CREATE TABLE `usage` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`model` varchar(255) NOT NULL,
`input_tokens` int NOT NULL,
`output_tokens` int NOT NULL,
`reasoning_tokens` int,
`cache_read_tokens` int,
`cache_write_tokens` int,
`cost` bigint NOT NULL,
CONSTRAINT `usage_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
);
--> statement-breakpoint
CREATE TABLE `key` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`user_id` text NOT NULL,
`name` varchar(255) NOT NULL,
`key` varchar(255) NOT NULL,
`time_used` timestamp(3),
CONSTRAINT `key_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`),
CONSTRAINT `global_key` UNIQUE(`key`)
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`email` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`time_seen` timestamp(3),
`color` int,
CONSTRAINT `user_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`),
CONSTRAINT `user_email` UNIQUE(`workspace_id`,`email`)
);
--> statement-breakpoint
CREATE TABLE `workspace` (
`id` varchar(30) NOT NULL,
`slug` varchar(255),
`name` varchar(255),
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
CONSTRAINT `workspace_id` PRIMARY KEY(`id`),
CONSTRAINT `slug` UNIQUE(`slug`)
);
@@ -0,0 +1,2 @@
ALTER TABLE `key` ADD `actor` json;--> statement-breakpoint
ALTER TABLE `key` DROP COLUMN `user_id`;
@@ -1,8 +0,0 @@
CREATE TABLE "account" (
"id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"email" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");
@@ -1,14 +0,0 @@
CREATE TABLE "key" (
"id" varchar(30) NOT NULL,
"workspace_id" varchar(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"user_id" text NOT NULL,
"name" varchar(255) NOT NULL,
"key" varchar(255) NOT NULL,
"time_used" timestamp with time zone,
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
);
--> statement-breakpoint
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");
@@ -1 +0,0 @@
ALTER TABLE "usage" DROP COLUMN "request_id";
+324 -190
View File
@@ -1,85 +1,142 @@
{
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"version": "5",
"dialect": "mysql",
"id": "aee779c5-db1d-4655-95ec-6451c18455be",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.billing": {
"name": "billing",
"schema": "",
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraint": {}
},
"billing": {
"name": "billing",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
@@ -90,74 +147,72 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.payment": {
"payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
@@ -168,104 +223,100 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.usage": {
"usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
@@ -276,102 +327,179 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.user": {
"name": "user",
"schema": "",
"key": {
"name": "key",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_used": {
"name": "time_used",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"color": {
"name": "color",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
"workspace_id",
"email"
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
@@ -382,80 +510,86 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.workspace": {
"workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
"slug"
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
+292 -212
View File
@@ -1,139 +1,142 @@
{
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
"version": "7",
"dialect": "postgresql",
"version": "5",
"dialect": "mysql",
"id": "79b7ee25-1c1c-41ff-9bbf-754af257102b",
"prevId": "aee779c5-db1d-4655-95ec-6451c18455be",
"tables": {
"public.account": {
"account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
"email"
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.billing": {
"billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
@@ -144,74 +147,72 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.payment": {
"payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
@@ -222,104 +223,100 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.usage": {
"usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
@@ -330,102 +327,179 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.user": {
"name": "user",
"schema": "",
"key": {
"name": "key",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"actor": {
"name": "actor",
"type": "json",
"primaryKey": false,
"notNull": true
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_used": {
"name": "time_used",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
"key"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"color": {
"name": "color",
"type": "integer",
"type": "int",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
"workspace_id",
"email"
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
@@ -436,80 +510,86 @@
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
},
"public.workspace": {
"workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"default": "now()"
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
"slug"
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
"checkConstraint": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
@@ -1,615 +0,0 @@
{
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"request_id": {
"name": "request_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
@@ -1,609 +0,0 @@
{
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email": {
"name": "email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.billing": {
"name": "billing",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"billing_workspace_id_workspace_id_fk": {
"name": "billing_workspace_id_workspace_id_fk",
"tableFrom": "billing",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment": {
"name": "payment",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"payment_workspace_id_workspace_id_fk": {
"name": "payment_workspace_id_workspace_id_fk",
"tableFrom": "payment",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.usage": {
"name": "usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"input_tokens": {
"name": "input_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"output_tokens": {
"name": "output_tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cache_write_tokens": {
"name": "cache_write_tokens",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"usage_workspace_id_workspace_id_fk": {
"name": "usage_workspace_id_workspace_id_fk",
"tableFrom": "usage",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.key": {
"name": "key",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_used": {
"name": "time_used",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": [
{
"expression": "key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"key_workspace_id_workspace_id_fk": {
"name": "key_workspace_id_workspace_id_fk",
"tableFrom": "key",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"color": {
"name": "color",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"user_workspace_id_workspace_id_fk": {
"name": "user_workspace_id_workspace_id_fk",
"tableFrom": "user",
"tableTo": "workspace",
"columnsFrom": [
"workspace_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": [
"workspace_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.workspace": {
"name": "workspace",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": true,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7 -21
View File
@@ -1,33 +1,19 @@
{
"version": "7",
"dialect": "postgresql",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1754518198186,
"tag": "0000_amused_mojo",
"version": "5",
"when": 1756796050935,
"tag": "0000_fluffy_raza",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1754609655262,
"tag": "0001_thankful_chat",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1754627626945,
"tag": "0002_stale_jackal",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1754672464106,
"tag": "0003_tranquil_spencer_smythe",
"version": "5",
"when": 1756871639102,
"tag": "0001_serious_whistler",
"breakpoints": true
}
]
+6 -3
View File
@@ -1,12 +1,13 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.5.29",
"version": "0.6.6",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@opencode/cloud-resource": "workspace:*",
"@planetscale/database": "1.19.0",
"drizzle-orm": "0.41.0",
"postgres": "3.4.7",
"stripe": "18.0.0",
@@ -16,9 +17,11 @@
"./*": "./src/*"
},
"scripts": {
"db": "sst shell drizzle-kit"
"db": "sst shell drizzle-kit",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"drizzle-kit": "0.30.5"
"drizzle-kit": "0.30.5",
"mysql2": "3.14.4"
}
}
+1 -43
View File
@@ -4,8 +4,6 @@ import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql"
import { Actor } from "./actor"
import { fn } from "./util/fn"
import { z } from "zod"
import { Identifier } from "./identifier"
import { centsToMicroCents } from "./util/price"
import { User } from "./user"
import { Resource } from "@opencode/cloud-resource"
@@ -52,46 +50,6 @@ export namespace Billing {
)
}
export const consume = fn(
z.object({
requestID: z.string().optional(),
model: z.string(),
inputTokens: z.number(),
outputTokens: z.number(),
reasoningTokens: z.number().optional(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
costInCents: z.number(),
}),
async (input) => {
const workspaceID = Actor.workspace()
const cost = centsToMicroCents(input.costInCents)
return await Database.transaction(async (tx) => {
await tx.insert(UsageTable).values({
workspaceID,
id: Identifier.create("usage"),
requestID: input.requestID,
model: input.model,
inputTokens: input.inputTokens,
outputTokens: input.outputTokens,
reasoningTokens: input.reasoningTokens,
cacheReadTokens: input.cacheReadTokens,
cacheWriteTokens: input.cacheWriteTokens,
cost,
})
const [updated] = await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${cost}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
.returning()
return updated.balance
})
},
)
export const generateCheckoutUrl = fn(
z.object({
successUrl: z.string(),
@@ -112,7 +70,7 @@ export namespace Billing {
product_data: {
name: "opencode credits",
},
unit_amount: 2000, // $20 minimum
unit_amount: 2123, // $20 minimum + Stripe fee 4.4% + $0.30
},
quantity: 1,
},
+24 -34
View File
@@ -1,41 +1,33 @@
import { drizzle } from "drizzle-orm/postgres-js"
import { drizzle } from "drizzle-orm/planetscale-serverless"
import { Resource } from "@opencode/cloud-resource"
export * from "drizzle-orm"
import postgres from "postgres"
import { Client } from "@planetscale/database"
const init = () => {
const client = postgres({
idle_timeout: 30000,
connect_timeout: 30000,
host: Resource.Database.host,
database: Resource.Database.database,
user: Resource.Database.username,
password: Resource.Database.password,
port: Resource.Database.port,
ssl: {
rejectUnauthorized: false,
},
max: 1,
})
return drizzle(client, {})
}
const createClient = "NODE_ENV" in process.env ? memo(init) : init
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
import { MySqlTransaction, type MySqlTransactionConfig } from "drizzle-orm/mysql-core"
import type { ExtractTablesWithRelations } from "drizzle-orm"
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
import type { PlanetScalePreparedQueryHKT, PlanetscaleQueryResultHKT } from "drizzle-orm/planetscale-serverless"
import { Context } from "../context"
import { memo } from "../util/memo"
export namespace Database {
export type Transaction = PgTransaction<
PostgresJsQueryResultHKT,
Record<string, unknown>,
ExtractTablesWithRelations<Record<string, unknown>>
export type Transaction = MySqlTransaction<
PlanetscaleQueryResultHKT,
PlanetScalePreparedQueryHKT,
Record<string, never>,
ExtractTablesWithRelations<Record<string, never>>
>
export type TxOrDb = Transaction | ReturnType<typeof createClient>
const client = memo(() => {
const result = new Client({
host: Resource.Database.host,
username: Resource.Database.username,
password: Resource.Database.password,
})
const db = drizzle(result, {})
return db
})
export type TxOrDb = Transaction | ReturnType<typeof client>
const TransactionContext = Context.create<{
tx: TxOrDb
@@ -48,14 +40,13 @@ export namespace Database {
return tx.transaction(callback)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await TransactionContext.provide(
{
effects,
tx: client,
tx: client(),
},
() => callback(client),
() => callback(client()),
)
await Promise.all(effects.map((x) => x()))
return result
@@ -76,15 +67,14 @@ export namespace Database {
}
}
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: MySqlTransactionConfig) {
try {
const { tx } = TransactionContext.use()
return callback(tx)
} catch (err) {
if (err instanceof Context.NotFound) {
const client = createClient()
const effects: (() => void | Promise<void>)[] = []
const result = await client.transaction(async (tx) => {
const result = await client().transaction(async (tx) => {
return TransactionContext.provide({ tx, effects }, () => callback(tx))
}, config)
await Promise.all(effects.map((x) => x()))
+6 -2
View File
@@ -1,4 +1,5 @@
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
import { bigint, timestamp, varchar } from "drizzle-orm/mysql-core"
export const ulid = (name: string) => varchar(name, { length: 30 })
@@ -15,7 +16,7 @@ export const id = () => ulid("id").notNull()
export const utc = (name: string) =>
timestamp(name, {
withTimezone: true,
fsp: 3,
})
export const currency = (name: string) =>
@@ -25,5 +26,8 @@ export const currency = (name: string) =>
export const timestamps = {
timeCreated: utc("time_created").notNull().defaultNow(),
timeUpdated: utc("time_updated")
.notNull()
.default(sql`CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)`),
timeDeleted: utc("time_deleted"),
}
+23 -47
View File
@@ -7,73 +7,49 @@ import { KeyTable } from "./schema/key.sql"
export namespace Key {
export const list = async () => {
const user = Actor.assert("user")
const workspace = Actor.workspace()
const keys = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
name: KeyTable.name,
key: KeyTable.key,
userID: KeyTable.userID,
timeCreated: KeyTable.timeCreated,
timeUsed: KeyTable.timeUsed,
})
.select()
.from(KeyTable)
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
.where(eq(KeyTable.workspaceID, workspace))
.orderBy(sql`${KeyTable.timeCreated} DESC`),
)
return keys
}
export const create = fn(z.object({ name: z.string().min(1).max(255) }), async (input) => {
const user = Actor.assert("user")
const workspaceID = Actor.workspace()
const { name } = input
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let randomPart = ""
for (let i = 0; i < 64; i++) {
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
let secretKey = "sk-"
const array = new Uint32Array(64)
crypto.getRandomValues(array)
for (let i = 0, l = array.length; i < l; i++) {
secretKey += chars[array[i] % chars.length]
}
const secretKey = `sk-${randomPart}`
const keyID = Identifier.create("key")
const keyRecord = await Database.use((tx) =>
tx
.insert(KeyTable)
.values({
id: Identifier.create("key"),
workspaceID: user.properties.workspaceID,
userID: user.properties.userID,
name,
key: secretKey,
timeUsed: null,
})
.returning(),
await Database.use((tx) =>
tx.insert(KeyTable).values({
id: keyID,
workspaceID,
actor: Actor.use(),
name,
key: secretKey,
timeUsed: null,
}),
)
return {
key: secretKey,
id: keyRecord[0].id,
name: keyRecord[0].name,
created: keyRecord[0].timeCreated,
}
return keyID
})
export const remove = fn(z.object({ id: z.string() }), async (input) => {
const user = Actor.assert("user")
const { id } = input
const result = await Database.use((tx) =>
tx
.delete(KeyTable)
.where(and(eq(KeyTable.id, id), eq(KeyTable.workspaceID, user.properties.workspaceID)))
.returning({ id: KeyTable.id }),
const workspace = Actor.workspace()
await Database.use((tx) =>
tx.delete(KeyTable).where(and(eq(KeyTable.id, input.id), eq(KeyTable.workspaceID, workspace))),
)
if (result.length === 0) {
throw new Error("Key not found")
}
return { id: result[0].id }
})
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { id, timestamps } from "../drizzle/types"
export const AccountTable = pgTable(
export const AccountTable = mysqlTable(
"account",
{
id: id(),
+9 -9
View File
@@ -1,8 +1,8 @@
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
import { bigint, boolean, int, mysqlTable, varchar } from "drizzle-orm/mysql-core"
import { timestamps, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const BillingTable = pgTable(
export const BillingTable = mysqlTable(
"billing",
{
...workspaceColumns,
@@ -16,7 +16,7 @@ export const BillingTable = pgTable(
(table) => [...workspaceIndexes(table)],
)
export const PaymentTable = pgTable(
export const PaymentTable = mysqlTable(
"payment",
{
...workspaceColumns,
@@ -28,17 +28,17 @@ export const PaymentTable = pgTable(
(table) => [...workspaceIndexes(table)],
)
export const UsageTable = pgTable(
export const UsageTable = mysqlTable(
"usage",
{
...workspaceColumns,
...timestamps,
model: varchar("model", { length: 255 }).notNull(),
inputTokens: integer("input_tokens").notNull(),
outputTokens: integer("output_tokens").notNull(),
reasoningTokens: integer("reasoning_tokens"),
cacheReadTokens: integer("cache_read_tokens"),
cacheWriteTokens: integer("cache_write_tokens"),
inputTokens: int("input_tokens").notNull(),
outputTokens: int("output_tokens").notNull(),
reasoningTokens: int("reasoning_tokens"),
cacheReadTokens: int("cache_read_tokens"),
cacheWriteTokens: int("cache_write_tokens"),
cost: bigint("cost", { mode: "number" }).notNull(),
},
(table) => [...workspaceIndexes(table)],
+4 -3
View File
@@ -1,13 +1,14 @@
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
import { mysqlTable, varchar, uniqueIndex, json } from "drizzle-orm/mysql-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
import { Actor } from "../actor"
export const KeyTable = pgTable(
export const KeyTable = mysqlTable(
"key",
{
...workspaceColumns,
...timestamps,
userID: text("user_id").notNull(),
actor: json("actor").$type<Actor.Info>(),
name: varchar("name", { length: 255 }).notNull(),
key: varchar("key", { length: 255 }).notNull(),
timeUsed: utc("time_used"),
+4 -4
View File
@@ -1,16 +1,16 @@
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
import { text, mysqlTable, uniqueIndex, varchar, int } from "drizzle-orm/mysql-core"
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const UserTable = pgTable(
export const UserTable = mysqlTable(
"user",
{
...workspaceColumns,
...timestamps,
email: text("email").notNull(),
email: varchar("email", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }).notNull(),
timeSeen: utc("time_seen"),
color: integer("color"),
color: int("color"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
)
+2 -6
View File
@@ -1,7 +1,7 @@
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { timestamps, ulid } from "../drizzle/types"
export const WorkspaceTable = pgTable(
export const WorkspaceTable = mysqlTable(
"workspace",
{
id: ulid("id").notNull().primaryKey(),
@@ -17,9 +17,5 @@ export function workspaceIndexes(table: any) {
primaryKey({
columns: [table.workspaceID, table.id],
}),
foreignKey({
foreignColumns: [WorkspaceTable.id],
columns: [table.workspaceID],
}),
]
}
+9 -2
View File
@@ -1,11 +1,18 @@
export function memo<T>(fn: () => T) {
export function memo<T>(fn: () => T, cleanup?: (input: T) => Promise<void>) {
let value: T | undefined
let loaded = false
return (): T => {
const result = (): T => {
if (loaded) return value as T
loaded = true
value = fn()
return value as T
}
result.reset = async () => {
if (cleanup && value) await cleanup(value)
loaded = false
value = undefined
}
return result
}
+1 -1
View File
@@ -26,7 +26,7 @@ export namespace Workspace {
await tx.insert(BillingTable).values({
workspaceID,
id: Identifier.create("billing"),
balance: centsToMicroCents(100),
balance: 0,
})
})
await Actor.provide(
+4 -1
View File
@@ -1,9 +1,12 @@
{
"name": "@opencode/cloud-function",
"version": "0.5.29",
"version": "0.6.6",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"@types/node": "catalog:",
+6 -5
View File
@@ -10,6 +10,7 @@ import { Account } from "@opencode/cloud-core/account.js"
import { Workspace } from "@opencode/cloud-core/workspace.js"
import { Actor } from "@opencode/cloud-core/actor.js"
import { Resource } from "@opencode/cloud-resource"
import { Database } from "@opencode/cloud-core/drizzle/index.js"
type Env = {
AuthStorage: KVNamespace
@@ -33,7 +34,7 @@ const MY_THEME: Theme = {
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return issuer({
const result = await issuer({
theme: MY_THEME,
providers: {
github: GithubProvider({
@@ -99,15 +100,14 @@ export default {
let email: string | undefined
if (response.provider === "github") {
const userResponse = await fetch("https://api.github.com/user", {
const emails = (await fetch("https://api.github.com/user/emails", {
headers: {
Authorization: `Bearer ${response.tokenset.access}`,
"User-Agent": "opencode",
Accept: "application/vnd.github+json",
},
})
const user = (await userResponse.json()) as { email: string }
email = user.email
}).then((x) => x.json())) as any
email = emails.find((x: any) => x.primary && x.verified)?.email
} else if (response.provider === "google") {
if (!response.id.email_verified) throw new Error("Google email not verified")
email = response.id.email as string
@@ -135,5 +135,6 @@ export default {
return ctx.subject("account", accountID, { accountID, email })
},
}).fetch(request, env, ctx)
return result
},
}
-596
View File
@@ -1,596 +0,0 @@
import { Hono, MiddlewareHandler } from "hono"
import { type ProviderMetadata, type LanguageModelUsage } from "ai"
import { createAnthropic } from "@ai-sdk/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
import { Actor } from "@opencode/cloud-core/actor.js"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { Billing } from "@opencode/cloud-core/billing.js"
import { Resource } from "@opencode/cloud-resource"
type Env = {}
const SUPPORTED_MODELS = {
"anthropic/claude-sonnet-4": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createAnthropic({
apiKey: Resource.ANTHROPIC_API_KEY.value,
})("claude-sonnet-4-20250514"),
},
"openai/gpt-4.1": {
input: 0.0000015,
output: 0.000006,
reasoning: 0.0000015,
cacheRead: 0.0000001,
cacheWrite: 0.0000001,
model: () =>
createOpenAI({
apiKey: Resource.OPENAI_API_KEY.value,
})("gpt-4.1"),
},
"zhipuai/glm-4.5-flash": {
input: 0,
output: 0,
reasoning: 0,
cacheRead: 0,
cacheWrite: 0,
model: () =>
createOpenAICompatible({
name: "Zhipu AI",
baseURL: "https://api.z.ai/api/paas/v4",
apiKey: Resource.ZHIPU_API_KEY.value,
})("glm-4.5-flash"),
},
}
const GatewayAuth: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header("authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json(
{
error: {
message: "Missing API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
const apiKey = authHeader.split(" ")[1]
// Check against KeyTable
const keyRecord = await Database.use((tx) =>
tx
.select({
id: KeyTable.id,
workspaceID: KeyTable.workspaceID,
})
.from(KeyTable)
.where(eq(KeyTable.key, apiKey))
.then((rows) => rows[0]),
)
if (!keyRecord) {
return c.json(
{
error: {
message: "Invalid API key.",
type: "invalid_request_error",
param: null,
code: "unauthorized",
},
},
401,
)
}
c.set("keyRecord", keyRecord)
await next()
}
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
.get("/", (c) => c.text("Hello, world!"))
.post("/v1/chat/completions", GatewayAuth, async (c) => {
const keyRecord = c.get("keyRecord")!
return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
try {
// Check balance
const customer = await Billing.get()
if (customer.balance <= 0) {
return c.json(
{
error: {
message: "Insufficient balance",
type: "insufficient_quota",
param: null,
code: "insufficient_quota",
},
},
401,
)
}
const body = await c.req.json<ChatCompletionCreateParamsBase>()
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
if (!model) throw new Error(`Unsupported model: ${body.model}`)
const requestBody = transformOpenAIRequestToAiSDK()
return body.stream ? await handleStream() : await handleGenerate()
async function handleStream() {
const result = await model.doStream({
...requestBody,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const id = `chatcmpl-${Date.now()}`
const created = Math.floor(Date.now() / 1000)
try {
for await (const chunk of result.stream) {
console.log("!!! CHUNK !!! : " + chunk.type)
switch (chunk.type) {
case "text-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "reasoning-delta": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
reasoning_content: chunk.delta,
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "tool-call": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: chunk.toolCallId,
type: "function",
function: {
name: chunk.toolName,
arguments: chunk.input,
},
},
],
},
finish_reason: null,
},
],
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
break
}
case "error": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
error: {
message: typeof chunk.error === "string" ? chunk.error : chunk.error,
type: "server_error",
},
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
case "finish": {
const data = {
id,
object: "chat.completion.chunk",
created,
model: body.model,
choices: [
{
index: 0,
delta: {},
finish_reason:
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
}[chunk.finishReason] || "stop",
},
],
usage: {
prompt_tokens: chunk.usage.inputTokens,
completion_tokens: chunk.usage.outputTokens,
total_tokens: chunk.usage.totalTokens,
completion_tokens_details: {
reasoning_tokens: chunk.usage.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: chunk.usage.cachedInputTokens,
},
},
}
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
break
}
//case "stream-start":
//case "response-metadata":
case "text-start":
case "text-end":
case "reasoning-start":
case "reasoning-end":
case "tool-input-start":
case "tool-input-delta":
case "tool-input-end":
case "raw":
default:
// Log unknown chunk types for debugging
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
break
}
}
} catch (error) {
controller.error(error)
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
async function handleGenerate() {
const response = await model.doGenerate({
...requestBody,
})
await trackUsage(body.model, response.usage, response.providerMetadata)
return c.json({
id: `chatcmpl-${Date.now()}`,
object: "chat.completion" as const,
created: Math.floor(Date.now() / 1000),
model: body.model,
choices: [
{
index: 0,
message: {
role: "assistant" as const,
content: response.content?.find((c) => c.type === "text")?.text ?? "",
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
tool_calls: response.content
?.filter((c) => c.type === "tool-call")
.map((toolCall) => ({
id: toolCall.toolCallId,
type: "function" as const,
function: {
name: toolCall.toolName,
arguments: toolCall.input,
},
})),
},
finish_reason:
(
{
stop: "stop",
length: "length",
"content-filter": "content_filter",
"tool-calls": "tool_calls",
error: "stop",
other: "stop",
unknown: "stop",
} as const
)[response.finishReason] || "stop",
},
],
usage: {
prompt_tokens: response.usage?.inputTokens,
completion_tokens: response.usage?.outputTokens,
total_tokens: response.usage?.totalTokens,
completion_tokens_details: {
reasoning_tokens: response.usage?.reasoningTokens,
},
prompt_tokens_details: {
cached_tokens: response.usage?.cachedInputTokens,
},
},
})
}
function transformOpenAIRequestToAiSDK() {
const prompt = transformMessages()
const tools = transformTools()
return {
prompt,
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
temperature: body.temperature ?? undefined,
topP: body.top_p ?? undefined,
frequencyPenalty: body.frequency_penalty ?? undefined,
presencePenalty: body.presence_penalty ?? undefined,
providerOptions: body.reasoning_effort
? {
anthropic: {
reasoningEffort: body.reasoning_effort,
},
}
: undefined,
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
responseFormat: (() => {
if (!body.response_format) return { type: "text" as const }
if (body.response_format.type === "json_schema")
return {
type: "json" as const,
schema: body.response_format.json_schema.schema,
name: body.response_format.json_schema.name,
description: body.response_format.json_schema.description,
}
if (body.response_format.type === "json_object") return { type: "json" as const }
throw new Error("Unsupported response format")
})(),
seed: body.seed ?? undefined,
tools: tools.tools,
toolChoice: tools.toolChoice,
}
function transformTools() {
const { tools, tool_choice } = body
if (!tools || tools.length === 0) {
return { tools: undefined, toolChoice: undefined }
}
const aiSdkTools = tools.map((tool) => {
return {
type: tool.type,
name: tool.function.name,
description: tool.function.description,
inputSchema: tool.function.parameters!,
}
})
let aiSdkToolChoice
if (tool_choice == null) {
aiSdkToolChoice = undefined
} else if (tool_choice === "auto") {
aiSdkToolChoice = { type: "auto" as const }
} else if (tool_choice === "none") {
aiSdkToolChoice = { type: "none" as const }
} else if (tool_choice === "required") {
aiSdkToolChoice = { type: "required" as const }
} else if (tool_choice.type === "function") {
aiSdkToolChoice = {
type: "tool" as const,
toolName: tool_choice.function.name,
}
}
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
}
function transformMessages() {
const { messages } = body
const prompt: LanguageModelV2Prompt = []
for (const message of messages) {
switch (message.role) {
case "system": {
prompt.push({
role: "system",
content: message.content as string,
})
break
}
case "user": {
if (typeof message.content === "string") {
prompt.push({
role: "user",
content: [{ type: "text", text: message.content }],
})
} else {
const content = message.content.map((part) => {
switch (part.type) {
case "text":
return { type: "text" as const, text: part.text }
case "image_url":
return {
type: "file" as const,
mediaType: "image/jpeg" as const,
data: part.image_url.url,
}
default:
throw new Error(`Unsupported content part type: ${(part as any).type}`)
}
})
prompt.push({
role: "user",
content,
})
}
break
}
case "assistant": {
const content: Array<
| { type: "text"; text: string }
| {
type: "tool-call"
toolCallId: string
toolName: string
input: any
}
> = []
if (message.content) {
content.push({
type: "text",
text: message.content as string,
})
}
if (message.tool_calls) {
for (const toolCall of message.tool_calls) {
content.push({
type: "tool-call",
toolCallId: toolCall.id,
toolName: toolCall.function.name,
input: JSON.parse(toolCall.function.arguments),
})
}
}
prompt.push({
role: "assistant",
content,
})
break
}
case "tool": {
prompt.push({
role: "tool",
content: [
{
type: "tool-result",
toolName: "placeholder",
toolCallId: message.tool_call_id,
output: {
type: "text",
value: message.content as string,
},
},
],
})
break
}
default: {
throw new Error(`Unsupported message role: ${message.role}`)
}
}
}
return prompt
}
}
async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
if (!modelData) throw new Error(`Unsupported model: ${model}`)
const inputTokens = usage.inputTokens ?? 0
const outputTokens = usage.outputTokens ?? 0
const reasoningTokens = usage.reasoningTokens ?? 0
const cacheReadTokens = usage.cachedInputTokens ?? 0
const cacheWriteTokens =
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0
const inputCost = modelData.input * inputTokens
const outputCost = modelData.output * outputTokens
const reasoningCost = modelData.reasoning * reasoningTokens
const cacheReadCost = modelData.cacheRead * cacheReadTokens
const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
await Billing.consume({
model,
inputTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWriteTokens,
costInCents,
})
await Database.use((tx) =>
tx
.update(KeyTable)
.set({ timeUsed: sql`now()` })
.where(eq(KeyTable.id, keyRecord.id)),
)
}
} catch (error: any) {
return c.json({ error: { message: error.message } }, 500)
}
})
})
.all("*", (c) => c.text("Not Found"))
export type ApiType = typeof app
export default app
+49
View File
@@ -0,0 +1,49 @@
import { Resource } from "@opencode/cloud-resource"
import type { TraceItem } from "@cloudflare/workers-types"
export default {
async tail(events: TraceItem[]) {
for (const event of events) {
if (!event.event) continue
if (!("request" in event.event)) continue
if (event.event.request.method !== "POST") continue
const url = new URL(event.event.request.url)
if (url.pathname !== "/zen/v1/chat/completions") return
let metrics = {
event_type: "completions",
"cf.continent": event.event.request.cf?.continent,
"cf.country": event.event.request.cf?.country,
"cf.city": event.event.request.cf?.city,
"cf.region": event.event.request.cf?.region,
"cf.latitude": event.event.request.cf?.latitude,
"cf.longitude": event.event.request.cf?.longitude,
"cf.timezone": event.event.request.cf?.timezone,
duration: event.wallTime,
request_length: parseInt(event.event.request.headers["content-length"] ?? "0"),
status: event.event.response?.status ?? 0,
ip: event.event.request.headers["x-real-ip"],
}
for (const log of event.logs) {
for (const message of log.message) {
if (!message.startsWith("_metric:")) continue
metrics = { ...metrics, ...JSON.parse(message.slice(8)) }
}
}
console.log(JSON.stringify(metrics, null, 2))
const ret = await fetch("https://api.honeycomb.io/1/events/zen", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Honeycomb-Event-Time": (event.eventTimestamp ?? Date.now()).toString(),
"X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value,
},
body: JSON.stringify(metrics),
})
console.log(ret.status)
console.log(await ret.text())
}
},
}
+7 -11
View File
@@ -14,18 +14,14 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"BASETEN_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
}
"DATABASE_USERNAME": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"database": string
"host": string
@@ -54,7 +50,7 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -70,7 +66,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -84,7 +80,7 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
"LogProcessor": cloudflare.Service
}
}
+7 -11
View File
@@ -14,18 +14,14 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"BASETEN_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
}
"DATABASE_USERNAME": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"database": string
"host": string
@@ -54,7 +50,7 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -70,7 +66,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -84,7 +80,7 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
"LogProcessor": cloudflare.Service
}
}
+1
View File
@@ -0,0 +1 @@
src/scrap.ts
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@opencode/cloud-scripts",
"version": "0.6.6",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"start": "tsx",
"shell": "sst shell"
},
"dependencies": {
"@opencode/cloud-core": "workspace:*",
"tsx": "4.20.5"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:"
}
}
+1
View File
@@ -0,0 +1 @@
// placeholder
+9
View File
@@ -0,0 +1,9 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["@cloudflare/workers-types", "node"]
}
}
+2 -2
View File
@@ -33,8 +33,8 @@ export const api = new sst.cloudflare.Worker("Api", {
},
})
export const web = new sst.cloudflare.x.Astro("Web", {
domain,
new sst.cloudflare.x.Astro("Web", {
domain: "docs." + domain,
path: "packages/web",
environment: {
// For astro config
+57 -29
View File
@@ -5,15 +5,38 @@ import { domain } from "./stage"
// DATABASE
////////////////
const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
const cluster = planetscale.getDatabaseOutput({
name: "opencode",
organization: "anomalyco",
})
const branch =
$app.stage === "production"
? planetscale.getBranchOutput({
name: "production",
organization: cluster.organization,
database: cluster.name,
})
: new planetscale.Branch("DatabaseBranch", {
database: cluster.name,
organization: cluster.organization,
name: $app.stage,
parentBranch: "production",
})
const password = new planetscale.Password("DatabasePassword", {
name: $app.stage,
database: cluster.name,
organization: cluster.organization,
branch: branch.name,
})
export const database = new sst.Linkable("Database", {
properties: {
host: `aws-us-east-2-${$app.stage === "thdxr" ? "2" : "1"}.pg.psdb.cloud`,
database: "postgres",
username: DATABASE_USERNAME.value,
password: DATABASE_PASSWORD.value,
port: 5432,
host: password.accessHostUrl,
database: cluster.name,
username: password.username,
password: password.plaintext,
port: 3306,
},
})
@@ -45,8 +68,8 @@ export const auth = new sst.cloudflare.Worker("AuthApi", {
// GATEWAY
////////////////
export const stripeWebhook = new WebhookEndpoint("StripeWebhook", {
url: $interpolate`https://console.${domain}/stripe/webhook`,
export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
url: $interpolate`https://${domain}/stripe/webhook`,
enabledEvents: [
"checkout.session.async_payment_failed",
"checkout.session.async_payment_succeeded",
@@ -77,8 +100,8 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhook", {
})
const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
const ZHIPU_API_KEY = new sst.Secret("ZHIPU_API_KEY")
const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
properties: { value: auth.url.apply((url) => url!) },
@@ -86,27 +109,22 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
properties: { value: stripeWebhook.secret },
})
export const gateway = new sst.cloudflare.Worker("GatewayApi", {
domain: `api.gateway.${domain}`,
handler: "cloud/function/src/gateway.ts",
url: true,
link: [
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ANTHROPIC_API_KEY,
OPENAI_API_KEY,
ZHIPU_API_KEY,
],
})
////////////////
// CONSOLE
////////////////
export const console = new sst.cloudflare.x.SolidStart("Console", {
domain: `console.${domain}`,
let logProcessor
if ($app.stage === "production" || $app.stage === "frank") {
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
logProcessor = new sst.cloudflare.Worker("LogProcessor", {
handler: "cloud/function/src/log-processor.ts",
link: [HONEYCOMB_API_KEY],
})
}
new sst.cloudflare.x.SolidStart("Console", {
domain,
path: "cloud/app",
link: [
database,
@@ -114,12 +132,22 @@ export const console = new sst.cloudflare.x.SolidStart("Console", {
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ANTHROPIC_API_KEY,
OPENAI_API_KEY,
ZHIPU_API_KEY,
XAI_API_KEY,
BASETEN_API_KEY,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),
//VITE_API_URL: gateway.url.apply((url) => url!),
VITE_AUTH_URL: auth.url.apply((url) => url!),
},
transform: {
server: {
transform: {
worker: {
placement: { mode: "smart" },
tailConsumers: logProcessor ? [{ service: logProcessor.nodes.worker.scriptName }] : [],
},
},
},
},
})
-1
View File
@@ -1,6 +1,5 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather": {
"type": "local",
+3 -1
View File
@@ -55,5 +55,7 @@
"overrides": {
"zod": "3.25.76"
},
"patchedDependencies": {}
"patchedDependencies": {
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.5.29",
"version": "0.6.6",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+7 -11
View File
@@ -14,18 +14,14 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"BASETEN_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
"Console": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"DATABASE_PASSWORD": {
"type": "sst.sst.Secret"
"value": string
}
"DATABASE_USERNAME": {
"type": "sst.sst.Secret"
"value": string
}
"Database": {
"database": string
"host": string
@@ -54,7 +50,7 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"OPENAI_API_KEY": {
"HONEYCOMB_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -70,7 +66,7 @@ declare module "sst" {
"type": "sst.cloudflare.Astro"
"url": string
}
"ZHIPU_API_KEY": {
"XAI_API_KEY": {
"type": "sst.sst.Secret"
"value": string
}
@@ -84,7 +80,7 @@ declare module "sst" {
"AuthApi": cloudflare.Service
"AuthStorage": cloudflare.KVNamespace
"Bucket": cloudflare.R2Bucket
"GatewayApi": cloudflare.Service
"LogProcessor": cloudflare.Service
}
}
+4 -2
View File
@@ -52,5 +52,7 @@ echo It seems that your package manager failed to install the right version of t
exit /b 1
:execute
rem Execute the binary with all arguments
"%resolved%" %*
rem Execute the binary with all arguments in the same console window
rem Use start /b /wait to ensure it runs in the current shell context for all shells
start /b /wait "" "%resolved%" %*
exit /b %ERRORLEVEL%
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.5.29",
"version": "0.6.6",
"name": "opencode",
"type": "module",
"private": true,
+5
View File
@@ -70,6 +70,11 @@ function findBinary() {
function main() {
try {
if (os.platform() === "win32") {
console.log("Windows detected, skipping postinstall")
return
}
const binaryPath = findBinary()
const binScript = path.join(__dirname, "bin", "opencode")
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env node
import fs from "fs"
import path from "path"
import os from "os"
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
function main() {
if (os.platform() !== "win32") {
console.log("Non-Windows platform detected, skipping preinstall")
return
}
const binDir = path.join(__dirname, "bin")
const unixScript = path.join(binDir, "opencode")
console.log("Windows detected: Configuring bin scripts for Windows")
if (fs.existsSync(unixScript)) {
console.log("Removing Unix shell script from bin/")
fs.unlinkSync(unixScript)
}
}
try {
main()
} catch (error) {
console.error("Preinstall script error:", error.message)
process.exit(0)
}
+2
View File
@@ -66,6 +66,7 @@ for (const [os, arch] of targets) {
await $`mkdir -p ./dist/${pkg.name}`
await $`cp -r ./bin ./dist/${pkg.name}/bin`
await $`cp ./script/preinstall.mjs ./dist/${pkg.name}/preinstall.mjs`
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
await Bun.file(`./dist/${pkg.name}/package.json`).write(
JSON.stringify(
@@ -75,6 +76,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
[pkg.name]: `./bin/${pkg.name}`,
},
scripts: {
preinstall: "node ./preinstall.mjs",
postinstall: "node ./postinstall.mjs",
},
version,
+12 -7
View File
@@ -103,12 +103,13 @@ export const AuthLoginCommand = cmd({
await ModelsDev.refresh().catch(() => {})
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
openrouter: 4,
vercel: 5,
opencode: 0,
anthropic: 1,
"github-copilot": 2,
openai: 3,
google: 4,
openrouter: 5,
vercel: 6,
}
let provider = await prompts.autocomplete({
message: "Select provider",
@@ -124,7 +125,7 @@ export const AuthLoginCommand = cmd({
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
hint: priority[x.id] <= 1 ? "recommended" : undefined,
})),
),
{
@@ -244,6 +245,10 @@ export const AuthLoginCommand = cmd({
return
}
if (provider === "opencode") {
prompts.log.info("Create an api key at https://opencode.ai/auth")
}
if (provider === "vercel") {
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token")
}
+8 -5
View File
@@ -93,10 +93,13 @@ export const GithubInstallCommand = cmd({
async function promptProvider() {
const priority: Record<string, number> = {
anthropic: 0,
"github-copilot": 1,
openai: 2,
google: 3,
opencode: 0,
anthropic: 1,
"github-copilot": 2,
openai: 3,
google: 4,
openrouter: 5,
vercel: 6,
}
let provider = await prompts.select({
message: "Select provider",
@@ -111,7 +114,7 @@ export const GithubInstallCommand = cmd({
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
hint: priority[x.id] <= 1 ? "recommended" : undefined,
})),
),
})
+32 -6
View File
@@ -10,6 +10,7 @@ import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
import { Identifier } from "../../id/id"
import { Agent } from "../../agent/agent"
import { Command } from "../../command"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -35,6 +36,10 @@ export const RunCommand = cmd({
array: true,
default: [],
})
.option("command", {
describe: "the command to run, use message for args",
type: "string",
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
@@ -64,12 +69,19 @@ export const RunCommand = cmd({
if (!process.stdin.isTTY) message += "\n" + (await Bun.stdin.text())
if (message.trim().length === 0) {
UI.error("Message cannot be empty")
if (message.trim().length === 0 && !args.command) {
UI.error("You must provide a message or a command")
return
}
await bootstrap(process.cwd(), async () => {
if (args.command) {
const exists = await Command.get(args.command)
if (!exists) {
UI.error(`Command "${args.command}" not found`)
return
}
}
const session = await (async () => {
if (args.continue) {
const it = Session.list()
@@ -172,12 +184,26 @@ export const RunCommand = cmd({
UI.error(err)
})
if (args.command) {
await Session.command({
messageID: Identifier.ascending("message"),
sessionID: session.id,
agent: agent.name,
model: providerID + "/" + modelID,
command: args.command,
arguments: message,
})
return
}
const messageID = Identifier.ascending("message")
const result = await Session.chat({
const result = await Session.prompt({
sessionID: session.id,
messageID,
providerID,
modelID,
model: {
providerID,
modelID,
},
agent: agent.name,
parts: [
{
@@ -190,7 +216,7 @@ export const RunCommand = cmd({
const isPiped = !process.stdout.isTTY
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
const match = result.parts.findLast((x: any) => x.type === "text") as any
if (match) process.stdout.write(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
+21 -1
View File
@@ -34,6 +34,11 @@ export namespace Config {
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
if (Flag.OPENCODE_CONFIG_CONTENT) {
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
process.env[value.key] = value.token
@@ -45,7 +50,7 @@ export namespace Config {
result.agent = result.agent || {}
const markdownAgents = [
...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)),
...(await Filesystem.globUp(".opencode/agent/*.md", Instance.directory, Instance.worktree)),
...(await Filesystem.globUp(".opencode/agent/**/*.md", Instance.directory, Instance.worktree)),
]
for (const item of markdownAgents) {
const content = await Bun.file(item).text()
@@ -397,6 +402,21 @@ export namespace Config {
.object({
apiKey: z.string().optional(),
baseURL: z.string().optional(),
timeout: z
.union([
z
.number()
.int()
.positive()
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
z.literal(false).describe("Disable timeout for this provider entirely."),
])
.optional()
.describe(
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
),
})
.catchall(z.any())
.optional(),
+38 -9
View File
@@ -1,7 +1,7 @@
import { z } from "zod"
import { Bus } from "../bus"
import { $ } from "bun"
import { createPatch } from "diff"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
import fs from "fs"
import ignore from "ignore"
@@ -28,6 +28,7 @@ export namespace File {
.object({
name: z.string(),
path: z.string(),
absolute: z.string(),
type: z.enum(["file", "directory"]),
ignored: z.boolean(),
})
@@ -36,6 +37,34 @@ export namespace File {
})
export type Node = z.infer<typeof Node>
export const Content = z
.object({
content: z.string(),
diff: z.string().optional(),
patch: z
.object({
oldFileName: z.string(),
newFileName: z.string(),
oldHeader: z.string().optional(),
newHeader: z.string().optional(),
hunks: z.array(
z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}),
),
index: z.string().optional(),
})
.optional(),
})
.openapi({
ref: "FileContent",
})
export type Content = z.infer<typeof Content>
export const Event = {
Edited: Bus.event(
"file.edited",
@@ -124,17 +153,17 @@ export namespace File {
.catch(() => "")
.then((x) => x.trim())
if (project.vcs === "git") {
const rel = path.relative(Instance.worktree, full)
const diff = await $`git diff ${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
const diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (diff.trim()) {
const original = await $`git show HEAD:${rel}`.cwd(Instance.worktree).quiet().nothrow().text()
const patch = createPatch(file, original, content, "old", "new", {
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity,
})
return { type: "patch", content: patch }
const diff = formatPatch(patch)
return { content, patch, diff }
}
}
return { type: "raw", content }
return { content }
}
export async function list(dir?: string) {
@@ -154,13 +183,13 @@ export namespace File {
if (exclude.includes(entry.name)) continue
const fullPath = path.join(resolved, entry.name)
const relativePath = path.relative(Instance.directory, fullPath)
const relativeToRoot = path.relative(Instance.worktree, fullPath)
const type = entry.isDirectory() ? "directory" : "file"
nodes.push({
name: entry.name,
path: relativePath,
absolute: fullPath,
type,
ignored: ignored(type === "directory" ? relativeToRoot + "/" : relativeToRoot),
ignored: ignored(type === "directory" ? relativePath + "/" : relativePath),
})
}
return nodes.sort((a, b) => {
+1
View File
@@ -2,6 +2,7 @@ export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
+5 -1
View File
@@ -84,7 +84,11 @@ const cli = yargs(hideBin(process.argv))
.command(ExportCommand)
.command(GithubCommand)
.fail((msg) => {
if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {
if (
msg.startsWith("Unknown argument") ||
msg.startsWith("Not enough non-option arguments") ||
msg.startsWith("Invalid values:")
) {
cli.showHelp("log")
}
process.exit(1)
+8 -1
View File
@@ -59,7 +59,8 @@ export namespace LSPClient {
return null
})
connection.onRequest("workspace/configuration", async () => {
return [{}]
// Return server initialization options
return [input.server.initialization ?? {}]
})
connection.listen()
@@ -108,6 +109,12 @@ export namespace LSPClient {
await connection.sendNotification("initialized", {})
if (input.server.initialization) {
await connection.sendNotification("workspace/didChangeConfiguration", {
settings: input.server.initialization,
})
}
const files: {
[path: string]: number
} = {}
+1
View File
@@ -70,6 +70,7 @@ export namespace LSP {
}
servers[name] = {
...existing,
id: name,
root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing.extensions,
spawn: async (root) => {
+19 -1
View File
@@ -193,7 +193,7 @@ export namespace LSPServer {
}
export const Gopls: Info = {
id: "golang",
id: "gopls",
root: async (file) => {
const work = await NearestRoot(["go.work"])(file)
if (work) return work
@@ -298,6 +298,23 @@ export namespace LSPServer {
args.push(...["run", js])
}
args.push("--stdio")
const initialization: Record<string, string> = {}
const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
(p): p is string => p !== undefined,
)
for (const venvPath of potentialVenvPaths) {
const isWindows = process.platform === "win32"
const potentialPythonPath = isWindows
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python")
if (await Bun.file(potentialPythonPath).exists()) {
initialization["pythonPath"] = potentialPythonPath
break
}
}
const proc = spawn(binary, args, {
cwd: root,
env: {
@@ -307,6 +324,7 @@ export namespace LSPServer {
})
return {
process: proc,
initialization,
}
},
}
+11
View File
@@ -53,6 +53,17 @@ export namespace Project {
.map((x) => x.trim())
.toSorted(),
)
if (!id) {
const project: Info = {
id: "global",
worktree: "/",
time: {
created: Date.now(),
},
}
await Storage.write<Info>(["project", "global"], project)
return project
}
worktree = path.dirname(
await $`git rev-parse --path-format=absolute --git-common-dir`
.quiet()
+57 -3
View File
@@ -1,4 +1,5 @@
import z from "zod"
import path from "path"
import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
@@ -9,6 +10,7 @@ import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { Instance } from "../project/instance"
import { Global } from "../global"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -79,7 +81,8 @@ export namespace Provider {
switch (regionPrefix) {
case "us": {
const modelRequiresPrefix = ["claude", "deepseek"].some((m) => modelID.includes(m))
if (modelRequiresPrefix) {
const isGovCloud = region.startsWith("us-gov")
if (modelRequiresPrefix && !isGovCloud) {
modelID = `${regionPrefix}.${modelID}`
}
break
@@ -153,7 +156,10 @@ export namespace Provider {
options: Record<string, any>
}
} = {}
const models = new Map<string, { info: ModelsDev.Model; language: LanguageModel }>()
const models = new Map<
string,
{ providerID: string; modelID: string; info: ModelsDev.Model; language: LanguageModel }
>()
const sdk = new Map<string, SDK>()
log.info("init")
@@ -320,9 +326,16 @@ export namespace Provider {
const pkg = provider.npm ?? provider.id
const mod = await import(await BunProc.install(pkg, "latest"))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
let options = { ...s.providers[provider.id]?.options }
if (options["timeout"] !== undefined) {
// Only override fetch if user explicitly sets timeout
options["fetch"] = async (input: any, init?: any) => {
return await fetch(input, { ...init, timeout: options["timeout"] })
}
}
const loaded = fn({
name: provider.id,
...s.providers[provider.id]?.options,
...options,
})
s.sdk.set(provider.id, loaded)
return loaded as SDK
@@ -355,10 +368,14 @@ export namespace Provider {
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
log.info("found", { providerID, modelID })
s.models.set(key, {
providerID,
modelID,
info,
language,
})
return {
modelID,
providerID,
info,
language,
}
@@ -406,6 +423,43 @@ export namespace Provider {
export async function defaultModel() {
const cfg = await Config.get()
if (cfg.model) return parseModel(cfg.model)
// this will be adjusted when migration to opentui is complete,
// for now we just read the tui state toml file directly
//
// NOTE: cannot just import file as toml without cleaning due to lack of
// support for date/time references in Bun toml parser: https://github.com/oven-sh/bun/issues/22426
const lastused = await Bun.file(path.join(Global.Path.state, "tui"))
.text()
.then((text) => {
// remove the date/time references since Bun toml parser doesn't support yet
const cleaned = text
.split("\n")
.filter((line) => !line.trim().startsWith("last_used ="))
.join("\n")
const state = Bun.TOML.parse(cleaned) as {
recently_used_models?: {
provider_id: string
model_id: string
}[]
}
const models = state?.recently_used_models ?? []
if (models.length > 0) {
return {
providerID: models[0].provider_id,
modelID: models[0].model_id,
}
}
})
.catch((error) => {
log.error("failed to find last used model", {
error,
})
return undefined
})
if (lastused) return lastused
const provider = await list()
.then((val) => Object.values(val))
.then((x) => x.find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)))
+15 -1
View File
@@ -91,11 +91,25 @@ export namespace ProviderTransform {
}
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
result["reasoningEffort"] = "minimal"
result["reasoningEffort"] = "high"
if (providerID !== "azure") {
result["textVerbosity"] = "low"
}
}
return result
}
export function maxOutputTokens(providerID: string, outputLimit: number, options: Record<string, any>): number {
if (providerID === "anthropic") {
const thinking = options["thinking"]
if (typeof thinking === "object" && thinking !== null) {
const type = thinking["type"]
const budgetTokens = thinking["budgetTokens"]
if (type === "enabled" && typeof budgetTokens === "number" && budgetTokens > 0) {
return outputLimit - budgetTokens
}
}
}
return outputLimit
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ export const ProjectRoute = new Hono()
"/current",
describeRoute({
description: "Get the current project",
operationId: "butt.current",
operationId: "project.current",
responses: {
200: {
description: "Current project",
+7 -9
View File
@@ -53,6 +53,9 @@ export namespace Server {
const app = new Hono()
export const App = app
.onError((err, c) => {
log.error("failed", {
error: err,
})
if (err instanceof NamedError) {
return c.json(err.toObject(), {
status: 400,
@@ -606,7 +609,7 @@ export namespace Server {
"/session/:id/message",
describeRoute({
description: "Create and send a new message to a session",
operationId: "session.chat",
operationId: "session.prompt",
responses: {
200: {
description: "Created message",
@@ -629,11 +632,11 @@ export namespace Server {
id: z.string().openapi({ description: "Session ID" }),
}),
),
zValidator("json", Session.ChatInput.omit({ sessionID: true })),
zValidator("json", Session.PromptInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").id
const body = c.req.valid("json")
const msg = await Session.chat({ ...body, sessionID })
const msg = await Session.prompt({ ...body, sessionID })
return c.json(msg)
},
)
@@ -971,12 +974,7 @@ export namespace Server {
description: "File content",
content: {
"application/json": {
schema: resolver(
z.object({
type: z.enum(["raw", "patch"]),
content: z.string(),
}),
),
schema: resolver(File.Content),
},
},
},
+168 -99
View File
@@ -1,5 +1,6 @@
import path from "path"
import os from "os"
import path from "path"
import fs from "fs/promises"
import { spawn } from "child_process"
import { Decimal } from "decimal.js"
import { z, ZodSchema } from "zod"
@@ -364,11 +365,45 @@ export namespace Session {
return part
}
export const ChatInput = z.object({
async function cleanupRevert(session: Info) {
if (!session.revert) return
const sessionID = session.id
let msgs = await messages(sessionID)
const messageID = session.revert.messageID
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
msgs = preserve
for (const msg of remove) {
await Storage.remove(["message", sessionID, msg.info.id])
await Bus.publish(MessageV2.Event.Removed, { sessionID: sessionID, messageID: msg.info.id })
}
const last = preserve.at(-1)
if (session.revert.partID && last) {
const partID = session.revert.partID
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
last.parts = preserveParts
for (const part of removeParts) {
await Storage.remove(["part", last.info.id, part.id])
await Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: sessionID,
messageID: last.info.id,
partID: part.id,
})
}
}
await update(sessionID, (draft) => {
draft.revert = undefined
})
}
export const PromptInput = z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message").optional(),
providerID: z.string(),
modelID: z.string(),
model: z
.object({
providerID: z.string(),
modelID: z.string(),
})
.optional(),
agent: z.string().optional(),
system: z.string().optional(),
tools: z.record(z.boolean()).optional(),
@@ -407,10 +442,10 @@ export namespace Session {
]),
),
})
export type ChatInput = z.infer<typeof ChatInput>
export type ChatInput = z.infer<typeof PromptInput>
export async function chat(
input: z.infer<typeof ChatInput>,
export async function prompt(
input: z.infer<typeof PromptInput>,
): Promise<{ info: MessageV2.Assistant; parts: MessageV2.Part[] }> {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
@@ -420,31 +455,7 @@ export namespace Session {
// Process revert cleanup first, before creating new messages
const session = await get(input.sessionID)
if (session.revert) {
let msgs = await messages(input.sessionID)
const messageID = session.revert.messageID
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
msgs = preserve
for (const msg of remove) {
await Storage.remove(["message", input.sessionID, msg.info.id])
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
}
const last = preserve.at(-1)
if (session.revert.partID && last) {
const partID = session.revert.partID
const [preserveParts, removeParts] = splitWhen(last.parts, (x) => x.id === partID)
last.parts = preserveParts
for (const part of removeParts) {
await Storage.remove(["part", last.info.id, part.id])
await Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,
messageID: last.info.id,
partID: part.id,
})
}
}
await update(input.sessionID, (draft) => {
draft.revert = undefined
})
cleanupRevert(session)
}
const userMsg: MessageV2.Info = {
id: input.messageID ?? Identifier.ascending("message"),
@@ -522,10 +533,10 @@ export namespace Session {
break
}
}
offset = Math.max(start - 2, 0)
if (end) {
limit = end - offset + 2
}
}
offset = Math.max(start - 1, 0)
if (end) {
limit = end - offset
}
}
const args = { filePath, offset, limit }
@@ -652,7 +663,16 @@ export namespace Session {
})
}
const model = await Provider.getModel(input.providerID, input.modelID)
const agent = await Agent.get(inputAgent)
const model = await (async () => {
if (input.model) {
return input.model
}
if (agent.model) {
return agent.model
}
return Provider.defaultModel()
})().then((x) => Provider.getModel(x.providerID, x.modelID))
let msgs = await messages(input.sessionID)
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
@@ -667,29 +687,40 @@ export namespace Session {
await summarize({
sessionID: input.sessionID,
providerID: input.providerID,
modelID: input.modelID,
providerID: model.providerID,
modelID: model.info.id,
})
return chat(input)
return prompt(input)
}
}
using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
if (msgs.filter((m) => m.info.role === "user").length === 1 && !session.parentID && isDefaultTitle(session.title)) {
const small = (await Provider.getSmallModel(input.providerID)) ?? model
const numRealUserMsgs = msgs.filter(
(m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
).length
if (numRealUserMsgs === 1 && !session.parentID && isDefaultTitle(session.title)) {
const small = (await Provider.getSmallModel(model.providerID)) ?? model
const options = {
...ProviderTransform.options(small.providerID, small.modelID, input.sessionID),
...small.info.options,
}
if (small.providerID === "openai") {
options["reasoningEffort"] = "minimal"
}
if (small.providerID === "google") {
options["thinkingConfig"] = {
thinkingBudget: 0,
}
}
generateText({
maxOutputTokens: small.info.reasoning ? 1024 : 20,
maxOutputTokens: small.info.reasoning ? 1500 : 20,
providerOptions: {
[input.providerID]: {
...small.info.options,
...ProviderTransform.options(input.providerID, small.info.id, input.sessionID),
},
[model.providerID]: options,
},
messages: [
...SystemPrompt.title(input.providerID).map(
...SystemPrompt.title(model.providerID).map(
(x): ModelMessage => ({
role: "system",
content: x,
@@ -724,7 +755,6 @@ export namespace Session {
})
}
const agent = await Agent.get(inputAgent)
if (agent.name === "plan") {
msgs.at(-1)?.parts.push({
id: Identifier.ascending("part"),
@@ -747,12 +777,12 @@ export namespace Session {
synthetic: true,
})
}
let system = SystemPrompt.header(input.providerID)
let system = SystemPrompt.header(model.providerID)
system.push(
...(() => {
if (input.system) return [input.system]
if (agent.prompt) return [agent.prompt]
return SystemPrompt.provider(input.modelID)
return SystemPrompt.provider(model.modelID)
})(),
)
system.push(...(await SystemPrompt.environment()))
@@ -777,8 +807,8 @@ export namespace Session {
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
modelID: model.modelID,
providerID: model.providerID,
time: {
created: Date.now(),
},
@@ -796,10 +826,10 @@ export namespace Session {
const enabledTools = pipe(
agent.tools,
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
mergeDeep(await ToolRegistry.enabled(model.providerID, model.modelID, agent)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
for (const item of await ToolRegistry.tools(model.providerID, model.modelID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
tools[item.id] = tool({
id: item.id as any,
@@ -909,16 +939,16 @@ export namespace Session {
"chat.params",
{
model: model.info,
provider: await Provider.getProvider(input.providerID),
provider: await Provider.getProvider(model.providerID),
message: userMsg,
},
{
temperature: model.info.temperature
? (agent.temperature ?? ProviderTransform.temperature(input.providerID, input.modelID))
? (agent.temperature ?? ProviderTransform.temperature(model.providerID, model.modelID))
: undefined,
topP: agent.topP ?? ProviderTransform.topP(input.providerID, input.modelID),
topP: agent.topP ?? ProviderTransform.topP(model.providerID, model.modelID),
options: {
...ProviderTransform.options(input.providerID, input.modelID, input.sessionID),
...ProviderTransform.options(model.providerID, model.modelID, input.sessionID),
...model.info.options,
...agent.options,
},
@@ -962,8 +992,8 @@ export namespace Session {
reasoning: 0,
cache: { read: 0, write: 0 },
},
modelID: input.modelID,
providerID: input.providerID,
modelID: model.modelID,
providerID: model.providerID,
mode: inputAgent,
time: {
created: Date.now(),
@@ -987,7 +1017,7 @@ export namespace Session {
}
},
headers:
input.providerID === "opencode"
model.providerID === "opencode"
? {
"x-opencode-session": input.sessionID,
"x-opencode-request": userMsg.id,
@@ -995,7 +1025,7 @@ export namespace Session {
: undefined,
maxRetries: 3,
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
maxOutputTokens: outputLimit,
maxOutputTokens: ProviderTransform.maxOutputTokens(model.providerID, outputLimit, params.options),
abortSignal: abort.signal,
stopWhen: async ({ steps }) => {
if (steps.length >= 1000) {
@@ -1010,7 +1040,7 @@ export namespace Session {
return false
},
providerOptions: {
[input.providerID]: params.options,
[model.providerID]: params.options,
},
temperature: params.temperature,
topP: params.topP,
@@ -1031,7 +1061,7 @@ export namespace Session {
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, input.providerID, input.modelID)
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
}
return args.params
},
@@ -1044,7 +1074,7 @@ export namespace Session {
const unprocessed = queued.find((x) => !x.processed)
if (unprocessed) {
unprocessed.processed = true
return chat(unprocessed.input)
return prompt(unprocessed.input)
}
for (const item of queued) {
item.callback(result)
@@ -1061,6 +1091,10 @@ export namespace Session {
export type ShellInput = z.infer<typeof ShellInput>
export async function shell(input: ShellInput) {
using abort = lock(input.sessionID)
const session = await get(input.sessionID)
if (session.revert) {
cleanupRevert(session)
}
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: input.sessionID,
@@ -1143,6 +1177,7 @@ export namespace Session {
const proc = spawn(shell, args, {
cwd: Instance.directory,
signal: abort.signal,
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
@@ -1150,6 +1185,11 @@ export namespace Session {
},
})
abort.signal.addEventListener("abort", () => {
if (!proc.pid) return
process.kill(-proc.pid)
})
let output = ""
proc.stdout?.on("data", (chunk) => {
@@ -1211,26 +1251,20 @@ export namespace Session {
})
export type CommandInput = z.infer<typeof CommandInput>
const bashRegex = /!`([^`]+)`/g
const fileRegex = /@([^\s]+)/g
/**
* Regular expression to match @ file references in text
* Matches @ followed by file paths, excluding commas, periods at end of sentences, and backticks
* Does not match when preceded by word characters or backticks (to avoid email addresses and quoted references)
*/
export const fileRegex = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
export async function command(input: CommandInput) {
log.info("command", input)
const command = await Command.get(input.command)
const agent = command.agent ?? input.agent ?? "build"
const fmtModel = (model: { providerID: string; modelID: string }) => `${model.providerID}/${model.modelID}`
const model =
command.model ??
(command.agent && (await Agent.get(command.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
input.model ??
(input.agent && (await Agent.get(input.agent).then((x) => (x.model ? fmtModel(x.model) : undefined)))) ??
fmtModel(await Provider.defaultModel())
let template = command.template.replace("$ARGUMENTS", input.arguments)
// intentionally doing match regex doing bash regex replacements
// this is because bash commands can output "@" references
const fileMatches = template.matchAll(fileRegex)
const bash = Array.from(template.matchAll(bashRegex))
if (bash.length > 0) {
const results = await Promise.all(
@@ -1253,24 +1287,57 @@ export namespace Session {
},
] as ChatInput["parts"]
for (const match of fileMatches) {
const filename = match[1]
const filepath = filename.startsWith("~/")
? path.join(os.homedir(), filename.slice(2))
: path.join(Instance.worktree, filename)
const matches = Array.from(template.matchAll(fileRegex))
await Promise.all(
matches.map(async (match) => {
const name = match[1]
const filepath = name.startsWith("~/")
? path.join(os.homedir(), name.slice(2))
: path.resolve(Instance.worktree, name)
parts.push({
type: "file",
url: `file://${filepath}`,
filename,
mime: "text/plain",
})
}
const stats = await fs.stat(filepath).catch(() => undefined)
if (!stats) {
const agent = await Agent.get(name)
if (agent) {
parts.push({
type: "agent",
name: agent.name,
})
}
return
}
return chat({
if (stats.isDirectory()) return
parts.push({
type: "file",
url: `file://${filepath}`,
filename: name,
mime: "text/plain",
})
}),
)
const model = await (async () => {
if (command.model) {
return Provider.parseModel(command.model)
}
if (command.agent) {
const agent = await Agent.get(command.agent)
if (agent.model) {
return agent.model
}
}
if (input.model) {
return Provider.parseModel(input.model)
}
return undefined
})()
return prompt({
sessionID: input.sessionID,
messageID: input.messageID,
...Provider.parseModel(model!),
model,
agent,
parts,
})
@@ -1637,7 +1704,7 @@ export namespace Session {
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const model = await Provider.getModel(input.providerID, input.modelID)
const system = [
...SystemPrompt.summarize(input.providerID),
...SystemPrompt.summarize(model.providerID),
...(await SystemPrompt.environment()),
...(await SystemPrompt.custom()),
]
@@ -1655,7 +1722,7 @@ export namespace Session {
summary: true,
cost: 0,
modelID: input.modelID,
providerID: input.providerID,
providerID: model.providerID,
tokens: {
input: 0,
output: 0,
@@ -1764,11 +1831,13 @@ export namespace Session {
providerID: string
messageID: string
}) {
await Session.chat({
await Session.prompt({
sessionID: input.sessionID,
messageID: input.messageID,
providerID: input.providerID,
modelID: input.modelID,
model: {
providerID: input.providerID,
modelID: input.modelID,
},
parts: [
{
id: Identifier.ascending("part"),
+15 -22
View File
@@ -1,31 +1,24 @@
You are a title generator. You output ONLY a thread title. Nothing else.
<task>
Generate a conversation thread title from the user message.
Convert the user message into a thread title.
Output: Single line, ≤50 chars, no explanations.
</task>
<context>
You are generating titles for a coding assistant conversation.
</context>
<rules>
- Max 50 chars, single line
- Focus on the specific action or question
- Keep technical terms, numbers, and filenames exactly as written
- Preserve HTTP status codes (401, 404, 500, etc.) as numbers
- For file references, include the filename
- Avoid filler words: the, this, my, a, an, properly
- NEVER assume their tech stack or domain
- Use -ing verbs consistently for actions
- Write like a chat thread title, not a blog post
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to message content—only extract title
</rules>
<examples>
"debug 500 errors in production" → "Debugging production 500 errors"
"refactor user service" → "Refactoring user service"
"why is app.js failing" → "Analyzing app.js failure"
"implement rate limiting" → "Implementing rate limiting"
"debug 500 errors in production" → Debugging production 500 errors
"refactor user service" → Refactoring user service
"why is app.js failing" → Analyzing app.js failure
"implement rate limiting" → Implementing rate limiting
</examples>
<format>
Return only the thread title text on a single line with no newlines, explanations, or additional formatting.
You should NEVER reply to the user's message. You can only generate titles.
</format>
Output the title now:
+2 -2
View File
@@ -14,7 +14,7 @@ import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_COPILOT_GPT_5 from "./prompt/copilot-gpt-5.txt"
import PROMPT_CODEX from "./prompt/codex.txt"
export namespace SystemPrompt {
export function header(providerID: string) {
@@ -23,7 +23,7 @@ export namespace SystemPrompt {
}
export function provider(modelID: string) {
if (modelID.includes("gpt-5")) return [PROMPT_COPILOT_GPT_5]
if (modelID.includes("gpt-5")) return [PROMPT_CODEX]
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
+8 -1
View File
@@ -15,6 +15,7 @@ export namespace Storage {
async (dir) => {
const project = path.resolve(dir, "../project")
for await (const projectDir of new Bun.Glob("*").scan({ cwd: project, onlyFiles: false })) {
log.info(`migrating project ${projectDir}`)
let projectID = projectDir
const fullProjectDir = path.join(project, projectDir)
let worktree = "/"
@@ -29,6 +30,7 @@ export namespace Storage {
if (worktree) break
}
if (!worktree) continue
if (!(await fs.exists(worktree))) continue
const [id] = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
@@ -57,6 +59,7 @@ export namespace Storage {
}),
)
log.info(`migrating sessions for project ${projectID}`)
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
cwd: fullProjectDir,
absolute: true,
@@ -68,6 +71,7 @@ export namespace Storage {
})
const session = await Bun.file(sessionFile).json()
await Bun.write(dest, JSON.stringify(session))
log.info(`migrating messages for session ${session.id}`)
for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
cwd: fullProjectDir,
absolute: true,
@@ -80,6 +84,7 @@ export namespace Storage {
const message = await Bun.file(msgFile).json()
await Bun.write(dest, JSON.stringify(message))
log.info(`migrating parts for message ${message.id}`)
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
{
cwd: fullProjectDir,
@@ -110,7 +115,9 @@ export namespace Storage {
for (let index = migration; index < MIGRATIONS.length; index++) {
log.info("running migration", { index })
const migration = MIGRATIONS[index]
await migration(dir)
await migration(dir).catch((e) => {
log.error("failed to run migration", { error: e, index })
})
await Bun.write(path.join(dir, "migration"), (index + 1).toString())
}
return {
+8 -1
View File
@@ -594,6 +594,8 @@ export function replace(content: string, oldString: string, newString: string, r
throw new Error("oldString and newString must be different")
}
let notFound = true
for (const replacer of [
SimpleReplacer,
LineTrimmedReplacer,
@@ -608,6 +610,7 @@ export function replace(content: string, oldString: string, newString: string, r
for (const search of replacer(content, oldString)) {
const index = content.indexOf(search)
if (index === -1) continue
notFound = false
if (replaceAll) {
return content.replaceAll(search, newString)
}
@@ -616,5 +619,9 @@ export function replace(content: string, oldString: string, newString: string, r
return content.substring(0, index) + newString + content.substring(index + search.length)
}
}
throw new Error("oldString not found in content or was found multiple times")
if (notFound) {
throw new Error("oldString not found in content")
}
throw new Error("oldString found multiple times and requires more code context to uniquely identify the intended match")
}

Some files were not shown because too many files have changed in this diff Show More