Compare commits

...

576 Commits

Author SHA1 Message Date
opencode 262fa184fd release: v1.0.122
Update Nix Hashes / update (push) Has been cancelled
2025-11-30 00:35:34 +00:00
Github Action 9a7e1c154d Update Nix flake.lock and hashes 2025-11-29 23:56:14 +00:00
Sebastian Herrlinger 5bf9193dfa bump opentui to v0.1.53, fixing an event listener leak in the line number renderable (perf) 2025-11-30 00:52:32 +01:00
Sebastian Herrlinger 180fb3f39d tweak spinner to be bg independent 2025-11-30 00:50:30 +01:00
GitHub Action 7e6b7314f4 chore: format code 2025-11-29 21:41:00 +00:00
Dax Raad a262508fb8 core: clarify general agent can execute multiple parallel tasks 2025-11-29 16:40:15 -05:00
Dax Raad 80ff24b65a ci: allow snapshot builds from any branch starting with snapshot- prefix 2025-11-29 21:30:38 +00:00
opencode 012aa67e42 release: v1.0.121
Update Nix Hashes / update (push) Has been cancelled
2025-11-29 21:30:38 +00:00
Github Action 0a1f12a583 Update Nix flake.lock and hashes 2025-11-29 21:20:24 +00:00
Sebastian Herrlinger f17dc812d0 upgrade opentui to v0.1.52, fixing #4906 - key repeat handling 2025-11-29 22:18:08 +01:00
kavin 1854d85ccc fix(tui): add missing fg color to permission keybind hints (#4899)
Co-authored-by: Github Action <action@github.com>
2025-11-29 14:22:20 -06:00
Adam 2c4d1fb8b4 chore: cleanup duplicate markup 2025-11-29 06:18:51 -06:00
GitHub Action d8fa7cf65d ignore: update download stats 2025-11-29 2025-11-29 12:04:12 +00:00
Dorian Karter 7d8d360138 fix: minor ui bug for transparent backgrounds (#4886) 2025-11-28 23:58:44 -06:00
Dax Raad d80880350d core: improve explore agent description to clarify tool availability 2025-11-28 21:47:45 -05:00
GitHub Action b693ed0dbd chore: format code 2025-11-29 02:44:40 +00:00
Dax Raad 83f961a7c2 Merge remote-tracking branch 'origin/dev' into dev 2025-11-28 21:44:02 -05:00
Dax Raad a093917db1 core: update generated types to include explore agent configuration 2025-11-28 21:43:56 -05:00
opencode 52716db649 release: v1.0.120
Update Nix Hashes / update (push) Has been cancelled
2025-11-29 02:34:23 +00:00
Dax Raad 9ca4b464ea tui: improve task display in session view to show tool names and completion status 2025-11-28 21:19:15 -05:00
Dax Raad 204a31b6bb Merge remote-tracking branch 'origin/dev' into dev 2025-11-28 21:13:13 -05:00
Dax Raad 813d287a09 core: add explore agent for fast codebase navigation and improve task UI display 2025-11-28 21:13:07 -05:00
Adam 4dd9f33eba fix: diffs double rendering when CSR'd 2025-11-28 20:08:50 -06:00
Adam 5953378a12 fix: theme-color value 2025-11-28 19:57:01 -06:00
Github Action b419eed295 Update Nix flake.lock and hashes 2025-11-29 01:55:02 +00:00
Sebastian Herrlinger 52deb7f352 opentui diffs 2025-11-29 02:52:17 +01:00
Dax Raad a4f3aecbaa ignore 2025-11-28 20:29:58 -05:00
GitHub Action 49ff6a852a chore: format code 2025-11-28 22:43:33 +00:00
Justin Vogt 7f537d2e98 docs: Add tip to tools about subagents re todos (#4875) 2025-11-28 15:49:24 -06:00
Aiden Cline 753443b16f ci: add community contributors to the changelog 2025-11-28 15:44:21 -06:00
Ariane Emory 33c63be980 feat: persist thinking blocks display to KV and indicate its current display state in the command_list (resolves #4582) (#4810)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-28 15:01:05 -06:00
Aiden Cline b6efca42b4 ci: use haiku 2025-11-28 14:41:32 -06:00
Github Action fa6eadc39a Update Nix flake.lock and hashes 2025-11-28 20:32:46 +00:00
Aiden Cline 8789acefa6 bump openrouter & google ai sdk packages 2025-11-28 14:30:22 -06:00
Aiden Cline 0e280017e6 Revert "fix: title gen when first msg(s) are shell invocations (#4874)"
This reverts commit 17e8322c29.
2025-11-28 12:18:02 -06:00
Aiden Cline 17e8322c29 fix: title gen when first msg(s) are shell invocations (#4874)
Co-authored-by: GitHub Action <action@github.com>
2025-11-28 11:55:22 -06:00
Sergio Garcia 96eda740cd docs: formatter: false and lsp: false (#4833)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-11-28 11:22:48 -06:00
jaov fa84612357 tweak: gemini retry message to not be explicitly about gemini 3 (#4864)
Co-authored-by: jesuso <j.ochoa@norteconecta.net>
2025-11-28 11:14:53 -06:00
Dax Raad cf1f63eda3 ci stuff 2025-11-28 10:59:52 -05:00
Dax Raad 9704f5ce89 add otui-diffs 2025-11-28 10:59:07 -05:00
GitHub Action 0eaec2af82 ignore: update download stats 2025-11-28 2025-11-28 12:04:42 +00:00
Adam 398d35dc97 fix: theme-color value 2025-11-28 05:48:07 -06:00
Adam 5efeaae093 fix: desktop and share layouts 2025-11-28 05:35:35 -06:00
Adam cb2dd34a5e fix: unified diff as default 2025-11-28 05:35:35 -06:00
Christoph 7112a706b8 core: add built-in Dart LSP server and formatter (#4841) 2025-11-28 00:33:45 -06:00
Github Action 025a47d01f Update Nix flake.lock and hashes 2025-11-28 06:27:49 +00:00
DS 13f89fdb8f fix: filter empty messages in toModelMessage (#4811) 2025-11-28 00:26:48 -06:00
Aiden Cline cc78d50ef6 bump anthropic package 2025-11-28 00:25:16 -06:00
Adam a8985b1849 fix(desktop): layout 2025-11-27 20:15:49 -06:00
Adam 6a1552f65c fix: unwrap solid store part 2025-11-27 20:15:49 -06:00
Dax Raad 776091cc23 ci: add bun version check to pre-push hook to ensure version consistency 2025-11-27 15:50:23 -05:00
Dax Raad f385524f48 fix lock 2025-11-27 15:48:16 -05:00
Dax Raad 350982e636 tui: simplify model dialog ordering logic to reduce complexity 2025-11-27 14:38:51 -05:00
Dax Raad 5854455815 tui: improve provider dialog text clarity for better user guidance 2025-11-27 14:09:53 -05:00
Dax Raad 9ecaf618db tui: fix provider sorting to prioritize recommended options 2025-11-27 13:54:42 -05:00
Dax Raad 95b667d21e tui: remove cancel keybind hint from prompt dialog to simplify UI 2025-11-27 13:48:34 -05:00
Dax Raad a0b689c140 tui: hide favorite keybind in model dialog when disconnected to prevent errors 2025-11-27 13:42:36 -05:00
GitHub Action ea52ed41be chore: format code 2025-11-27 17:15:41 +00:00
Jay V 5a50d54fda ignore: lock 2025-11-27 12:14:50 -05:00
Jay V 35d118b0c4 ignore: add reply-to support for enterprise form emails 2025-11-27 12:12:44 -05:00
Albert O'Shea ea7c213f5d nix: fix workflow failing on PRs (#4820)
Co-authored-by: Github Action <action@github.com>
2025-11-27 10:05:51 -06:00
Frank 70dd6dd394 doc: slashing kimi k2 thinking price 2025-11-27 09:58:57 -05:00
GitHub Action 049510afbd ignore: update download stats 2025-11-27 2025-11-27 12:04:30 +00:00
Adam c120447fd0 fix: desktop layout and scroll gutters 2025-11-27 05:41:50 -06:00
Adam feb1f36126 fix: session turn margins 2025-11-27 05:25:39 -06:00
GitHub Action d6ef47bb2d chore: format code 2025-11-27 11:06:46 +00:00
Adam 50fd416d49 fix: simpler sanitize 2025-11-27 05:05:55 -06:00
opencode aef6904247 release: v1.0.119
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 03:15:40 +00:00
Dax Raad 0bf40faf95 core: prevent codesearch and websearch tools when webfetch permission is denied 2025-11-26 22:08:50 -05:00
GitHub Action c90987c4b0 chore: format code 2025-11-27 03:08:33 +00:00
Dax Raad 0e08655407 core: prevent external diff tools from interfering with snapshot generation 2025-11-26 22:07:51 -05:00
opencode 427887db9c release: v1.0.118
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 02:43:07 +00:00
Dax Raad a718622498 tui: prevent footer from shrinking when terminal is resized 2025-11-26 21:35:56 -05:00
Github Action 4e83107d79 Update Nix flake.lock and hashes 2025-11-27 01:33:35 +00:00
GitHub Action 04b6e72820 chore: format code 2025-11-27 01:32:04 +00:00
Dax Raad 501a2539c7 revert ts lsp to monorepo root 2025-11-26 20:31:26 -05:00
opencode 6a9856d480 release: v1.0.117
Update Nix Hashes / update (push) Has been cancelled
2025-11-27 01:29:00 +00:00
Dax Raad 2c8d42d997 roll back vtsls 2025-11-26 20:18:40 -05:00
Dax Raad 9c237f0bfb temporarily restrict codesearch and websearch to opencode zen users. need to figure out how to opt out for enterprise users who do not want this 2025-11-26 20:13:20 -05:00
Dax 63bfe76720 tui design refinement (#4809) 2025-11-26 20:11:39 -05:00
Aiden Cline 99d7ff47c4 enable parcel file watcher, expand parcel ignore patterns, replace fs watcher for git branches with parcel (#4805) 2025-11-26 17:33:43 -06:00
Aiden Cline 3ff0eb3065 Revert "fix: disable virtual extmarks for file/agent mentions (#4731)"
This reverts commit 673dbeee09.
2025-11-26 14:55:39 -06:00
GitHub Action 4d2b265dc4 chore: format code 2025-11-26 19:10:51 +00:00
rosmur 1854245bd3 docs: add llama.cpp provider for local LLM inference (#4769)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-26 13:10:15 -06:00
Aiden Cline 4d07034930 fix: svg paste bug 2025-11-26 12:38:48 -06:00
Dax Raad 98031173b6 lil branch refactor 2025-11-26 12:34:48 -05:00
GitHub Action e8e474597c chore: format code 2025-11-26 17:26:46 +00:00
Tommy D. Rossi 382758790c fix(tui): dedupe file references in prompt (#4775) 2025-11-26 11:26:05 -06:00
opencode c33920f59d release: v1.0.115
Update Nix Hashes / update (push) Has been cancelled
2025-11-26 16:57:35 +00:00
Jensen 33f004d4b6 fix(tui): show bootstrap errors instead of {} to trace (#4779)
Co-authored-by: Github Action <action@github.com>
2025-11-26 10:49:55 -06:00
Yug Agarwal 8963b536ee docs: IO.NET Provider (#4762) 2025-11-26 10:44:05 -06:00
GitHub Action 51455e2a1e ignore: update download stats 2025-11-26 2025-11-26 12:04:44 +00:00
Adam 30d6a26e3e fix: useData 2025-11-26 06:03:13 -06:00
Adam cd4fabd11b fix: scroll gutter padding 2025-11-26 05:58:30 -06:00
Github Action 9a8b8f26ac Update Nix flake.lock and hashes 2025-11-26 11:31:37 +00:00
Adam 2f73b16b57 deps: update pierre diffs 2025-11-26 05:29:31 -06:00
opencode-agent[bot] df9952c291 Renamed vcs.changed to vcs.branch.updated (#4771)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-26 01:17:59 -06:00
Dmitry Halushka ee946d8128 fix: transform MCP tool schemas for Google/Gemini compatibility (#4538)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-26 00:58:20 -06:00
Aiden Cline ec8f2e078e Remove keybinds from favorites feature, keep functionality 2025-11-26 00:45:33 -06:00
shuv 335f46122b Add favorites to model selector (#23) (#4343)
Co-authored-by: Github Action <action@github.com>
2025-11-26 00:41:41 -06:00
Meysam Najafi Fard 73eae191e9 fix: handle remote image URLs in paste handler (#4691)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-26 00:34:22 -06:00
Aiden Cline 14e823e938 ignore: fix type issue 2025-11-26 00:14:04 -06:00
Aiden Cline 2fbd462e6e Reapply "feat(github): add ability to react to PR Review Comments in Workflow (#4705)"
This reverts commit e1cc98d448.
2025-11-25 23:51:46 -06:00
Aiden Cline e1cc98d448 Revert "feat(github): add ability to react to PR Review Comments in Workflow (#4705)"
This reverts commit 0ce64962d4.
2025-11-25 23:51:23 -06:00
U Cirello 0ce64962d4 feat(github): add ability to react to PR Review Comments in Workflow (#4705)
Co-authored-by: GitHub Action <action@github.com>
2025-11-25 23:45:19 -06:00
george larson 338229193f docs: add Venice.ai provider (#4748)
Co-authored-by: George Larson <georgeglarson@users.noreply.github.com>
2025-11-25 23:40:52 -06:00
Ariane Emory 57644a4be8 feat: add a diff_style option to allow disabling columnar diffs (resolve #4677) (#4756)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-25 23:34:58 -06:00
Frank da2099137a zen: trial 2025-11-25 23:44:57 -05:00
Aiden Cline 09bc8d9ca4 show current git branch in tui (#4765)
Co-authored-by: GitHub Action <action@github.com>
2025-11-25 21:39:20 -06:00
Dax Raad d95f724303 enable exa code/websearch 2025-11-25 22:06:26 -05:00
Frank c413c3ed8f wip: zen 2025-11-25 22:01:34 -05:00
Adam 5f56be0ad4 fix: pierre separators 2025-11-25 20:39:20 -06:00
Adam ef441d5cff chore: cleanup pierre stuff 2025-11-25 20:39:20 -06:00
GitHub Action 16a188c524 chore: format code 2025-11-26 02:33:36 +00:00
Dax Raad 50c40a8d99 tui: fix event subscription cleanup in SDK context 2025-11-25 21:32:56 -05:00
opencode 4114c8715c release: v1.0.114
Update Nix Hashes / update (push) Has been cancelled
2025-11-26 00:32:22 +00:00
GitHub Action ced5fdbe70 chore: format code 2025-11-26 00:24:10 +00:00
Dax Raad b16aa81e0d switch to vtsls for typescript lsp 2025-11-25 19:23:23 -05:00
Aiden Cline b44971668c fix: global prefix handling w/ aws bedrock (#4757) 2025-11-26 00:21:59 +00:00
opencode 0ff4c284e2 release: v1.0.113
Update Nix Hashes / update (push) Has been cancelled
2025-11-26 00:21:59 +00:00
Dax Raad e8db95be16 switch typescript lsp to be one per package to ensure it loads when typescript is not installed at root 2025-11-25 19:14:07 -05:00
Shantur Rathore 69c2dd53ad config: add setCacheKey in provider options (#4738)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-25 17:53:56 -06:00
Frank 14a910bd64 wip: zen 2025-11-25 18:04:25 -05:00
GitHub Action 52f97ffdc9 chore: format code 2025-11-25 22:59:55 +00:00
Frank a1e87f6cd9 wip: zen 2025-11-25 17:58:59 -05:00
Frank c2fc41dcd5 wip: zen 2025-11-25 17:57:24 -05:00
Frank b62c7943e7 zen: trial 2025-11-25 17:57:24 -05:00
Tommy D. Rossi 64caeeb12d fix(tui): abort in-progress generation on undo (#4744) 2025-11-25 16:35:34 -06:00
Adam e8ac4a1e99 fix: build error 2025-11-25 16:27:53 -06:00
Adam 19c8654195 fix: missing deps 2025-11-25 16:27:26 -06:00
Haris Gušić 00d7aed797 fix: prompt submit error with opencode -c (#4496)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-25 16:27:06 -06:00
Adam 4477132448 fix: sanitize absolute paths 2025-11-25 16:02:31 -06:00
Aiden Cline eaeea45ace ci: change changelog model 2025-11-25 15:36:26 -06:00
Aiden Cline e404bf33b1 update install script to handle musl & avx 2025-11-25 15:35:10 -06:00
Aiden Cline 79a7edea5e ci: update changelog prompt 2025-11-25 15:34:49 -06:00
Jaga Santagostino 2b05fe2859 docs: improve notes on mobile (#4747) 2025-11-25 15:27:28 -06:00
GitHub Action f8996f0a90 chore: format code 2025-11-25 21:10:31 +00:00
Dax Raad eb04cdac41 better overloaded message 2025-11-25 21:10:31 +00:00
opencode 125938c7a1 release: v1.0.112
Update Nix Hashes / update (push) Has been cancelled
2025-11-25 21:10:30 +00:00
opencode ec1260d8aa release: v1.0.111
Update Nix Hashes / update (push) Has been cancelled
2025-11-25 20:54:40 +00:00
Dax Raad 9eabbe2e76 retry anthropic overloaded errors 2025-11-25 15:47:34 -05:00
Adam 5f35c579e2 fix: accordion styles 2025-11-25 14:46:22 -06:00
Adam 99a23bdc8f fix: css code splitting off for desktop 2025-11-25 14:33:49 -06:00
Aiden Cline d914a08896 tweak stats command to show avg & median # of tokens per session 2025-11-25 14:14:06 -06:00
Jaga Santagostino ceccc30cb8 docs: reduce mobile padding (#4745) 2025-11-25 14:04:55 -06:00
Youssef Achy 3366a71155 Theme selector UX (current theme indicator and easier selection) (#4623)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-25 13:53:01 -06:00
Adam cd67804412 fix: scroll gutter 2025-11-25 13:49:46 -06:00
Aiden Cline 4a95db6013 fix: add missing compacted event 2025-11-25 13:47:10 -06:00
Adam 8bf552ae25 fix: markdown inline code style 2025-11-25 13:11:21 -06:00
Adam ccd0c2382f fix(share): style issues 2025-11-25 12:53:43 -06:00
Adam d74663bf53 fix(share): don't highlight words unless split 2025-11-25 12:21:39 -06:00
Aiden Cline 020ee56f25 fix: dont auto continue if compaction was manual 2025-11-25 12:11:09 -06:00
Adam 87b295bc3d fix(share): metadata and popover close delay 2025-11-25 12:09:22 -06:00
Github Action b9b1e92788 Update Nix flake.lock and hashes 2025-11-25 17:45:45 +00:00
Dax Raad 4000705701 core(enterprise): use aws4fetch for smaller bundle size on edge runtimes 2025-11-25 12:43:36 -05:00
Tommy D. Rossi 673dbeee09 fix: disable virtual extmarks for file/agent mentions (#4731) 2025-11-25 11:37:46 -06:00
Ariane Emory 5288041782 tweak: alphabetical (mostly) opencode models output (#4725)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-11-25 11:37:05 -06:00
Ariane Emory 4273fa9ccf fix: merge plugin selections (resolves #4565) (#4724)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-25 11:34:21 -06:00
Aiden Cline 01bcb9dff9 ci: setup bun 2025-11-25 11:27:29 -06:00
Github Action 101f86ad1f Update Nix flake.lock and hashes 2025-11-25 16:56:01 +00:00
GitHub Action 2575dc2db0 chore: format code 2025-11-25 16:46:00 +00:00
Adam b4e6f128d7 fix: favicons across all web properties 2025-11-25 10:45:18 -06:00
Albert O'Shea 7d5e6718dc nix: set dontFixup = true (#4735) 2025-11-25 10:16:53 -06:00
Github Action 072aa7569c Update Nix flake.lock and hashes 2025-11-25 13:50:22 +00:00
GitHub Action 62871283e2 chore: format code 2025-11-25 13:49:03 +00:00
Adam 0f1dd24f97 fix(enterprise): custom element defs 2025-11-25 07:48:18 -06:00
Adam 995f551e80 fix(enterprise): custom element defs 2025-11-25 07:47:25 -06:00
Adam 15facd8cfd feat(share): SSR'd diffs 2025-11-25 07:40:16 -06:00
GitHub Action 57bd47a446 ignore: update download stats 2025-11-25 2025-11-25 12:05:06 +00:00
Aiden Cline 0568c943ab better scroll speed default for windows 2025-11-25 01:16:21 -06:00
Shantur Rathore b1aaa8570e prompt_async: Allows to receive prompt and return immediately, start … (#4664)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-24 23:52:57 -06:00
Aiden Cline 3b2aa7e91d Revert "Support basic auth in opencode sdk (#4697)"
This reverts commit b9253d0b3b.
2025-11-24 23:12:00 -06:00
Aiden Cline 997aacf7f0 ci: adjust nix hash 2025-11-24 22:50:32 -06:00
Dax Raad 3183978c6b fixed suspense in enterprise 2025-11-24 23:33:47 -05:00
Frank 6a0b20456f zen: remove official support for unified endpoint 2025-11-24 23:33:23 -05:00
Frank 8841112b07 zen: add zai logo 2025-11-24 23:33:23 -05:00
Aiden Cline 16dbac6026 Revert "config: add setCacheKey in provider options (#4654)"
This reverts commit 9b6d03c497.
2025-11-24 22:30:56 -06:00
Shantur Rathore 9b6d03c497 config: add setCacheKey in provider options (#4654)
Co-authored-by: GitHub Action <action@github.com>
2025-11-24 22:20:52 -06:00
Aiden Cline d3ea044619 ignore: comment out thingy 2025-11-24 22:15:00 -06:00
Frank 364355c8e1 zen: graduate kimi k2 thinking 2025-11-24 23:14:46 -05:00
Frank 88c4e95428 zen: sticky session 2025-11-24 23:14:46 -05:00
Dax Raad 9403f6ced5 log data 2025-11-24 23:13:28 -05:00
GitHub Action 56fe84e516 chore: format code 2025-11-25 03:59:28 +00:00
Dax Raad 69d1381ba3 core: refactor share system to separate session IDs from share IDs
- Generate shorter share IDs from session IDs for better URL structure
- Update API routes to use shareID parameter instead of sessionID
- Improve sync mechanism with better data queuing and deduplication
- Maintain backward compatibility while improving security and organization
2025-11-24 22:58:47 -05:00
Dax Raad ccec8c4792 tui: align session panel content with consistent left padding 2025-11-25 03:08:12 +00:00
opencode 83c47e0ed7 release: v1.0.110
Update Nix Hashes / update (push) Has been cancelled
2025-11-25 03:08:12 +00:00
Dax Raad 6d630901b6 tui: use standard middle dot separator for better terminal compatibility 2025-11-24 22:00:44 -05:00
Adam 320622fc27 fix(tui): spacing 2025-11-24 20:44:30 -06:00
Dax Raad fb8ef1f27b tui: prevent re-fetching already synced sessions when switching sessions 2025-11-24 21:43:11 -05:00
Adam 333948711d fix(desktop): content animations 2025-11-24 20:35:51 -06:00
Adam 001b4be0ae fix(desktop): message summaries 2025-11-24 19:56:12 -06:00
Github Action 427c62d052 Update Nix flake.lock and hashes 2025-11-25 00:03:08 +00:00
Sebastian Herrlinger 99097d418b bump opentui to v0.1.50
- fix another segfault
- fix weird tmux window/pane title
- fix selection in textarea (use shift+arrow keys)
2025-11-25 01:00:48 +01:00
Adam 75654afeaa fix(share): responsiveness 2025-11-24 17:59:37 -06:00
GitHub Action f10d18956a chore: format code 2025-11-24 23:06:21 +00:00
Andrew Joslin b9253d0b3b Support basic auth in opencode sdk (#4697) 2025-11-24 17:05:35 -06:00
opencode 2458e7597b release: v1.0.109
Update Nix Hashes / update (push) Has been cancelled
2025-11-24 22:30:42 +00:00
Aiden Cline 23a721f0a2 remove hardcoded openrouter provider 2025-11-24 16:11:12 -06:00
Frank 9bd6be5c6d zen: byok for gemini 2025-11-24 17:09:54 -05:00
Adam Hosker dd6113c9d1 Add --refresh flag to models command (#4707) 2025-11-24 16:06:51 -06:00
Adam d39bcd9c55 fix(desktop): layout 2025-11-24 15:57:48 -06:00
Frank 823d7da4c1 zen: display reasoning tokens 2025-11-24 16:54:54 -05:00
Frank 7fff191c57 wip: zen 2025-11-24 16:54:54 -05:00
Aiden Cline 9e44085a69 adjust bundled provider logic, fix tree shaking stuff (#4708) 2025-11-24 15:48:54 -06:00
Adam 5230b91b25 fix(console): remove extra favicon 2025-11-24 15:48:11 -06:00
GitHub Action 493b3d72e4 chore: format code 2025-11-24 21:17:40 +00:00
Adam 337590c450 fix(console): favicon issues 2025-11-24 15:10:40 -06:00
Adam 8e1c7cfe89 wip(share): enterprise favicon 2025-11-24 15:10:40 -06:00
Adam c60cb6c329 wip(share): more styling 2025-11-24 15:10:40 -06:00
Adam acf1dd8500 wip(share): more styling 2025-11-24 15:10:40 -06:00
Adam 3fb57044d1 wip(share): more styling 2025-11-24 15:10:39 -06:00
Frank 46a76a778a zen: add opus 4.5 2025-11-24 15:39:38 -05:00
Aiden Cline a9a2c23736 add autoupdate: notify 2025-11-24 14:32:38 -06:00
Aiden Cline ccde319937 ci: nix hash 2025-11-24 14:26:55 -06:00
Frank 0ed7fac5fb wip: zen 2025-11-24 15:24:44 -05:00
GitHub Action 80b9cd1292 chore: format code 2025-11-24 19:53:00 +00:00
Sebastian Herrlinger b6c1df41fb textarea highlight cursor color 2025-11-24 20:51:07 +01:00
opencode 125af820d0 release: v1.0.108
Update Nix Hashes / update (push) Has been cancelled
2025-11-24 19:16:14 +00:00
GitHub Action 8167e90801 chore: format code 2025-11-24 18:40:01 +00:00
Sebastian Herrlinger 82ebf66cba non-corpo loading spinner 2025-11-24 19:39:09 +01:00
Frankie Seabrook 883ed4d424 docs: add Ollama Cloud provider setup to providers.mdx (#4693) 2025-11-24 10:51:16 -06:00
GitHub Action e6bf1754c3 ignore: update download stats 2025-11-24 2025-11-24 12:04:34 +00:00
Carli Samuele bcb494d5d1 TUI: fix: add null check for user.time in duration calculation (#4679) 2025-11-24 02:06:09 -06:00
Ariane Emory 75c0c0a098 tweak: display MCPs in alphabetic order in the sidebar. (#4680)
Co-authored-by: Github Action <action@github.com>
2025-11-24 02:02:04 -06:00
GitHub Action 840d2694b4 chore: format code 2025-11-24 07:51:59 +00:00
Aiden Cline abdc7b276a fix: persist light vs dark mode 2025-11-24 01:51:13 -06:00
Aiden Cline d4f6deb9ef tweak: modified files sidebar 2025-11-24 01:51:13 -06:00
opencode 502e85b903 release: v1.0.107
Update Nix Hashes / update (push) Has been cancelled
2025-11-24 07:06:41 +00:00
Aiden Cline ac1e2bfd49 bump copilot plugin, give better error message for copilot (#4678) 2025-11-24 00:40:15 -06:00
Aiden Cline b9b071c744 fix: fatal: undefined is not an object (evaluating 'color.buffer') 2025-11-23 23:56:15 -06:00
GitHub Action 83186b6fed chore: format code 2025-11-24 04:55:13 +00:00
Albert O'Shea a3a239967f nix: bundle js dist with bun and patch tree-sitter wasm paths (#4644)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Github Action <action@github.com>
2025-11-23 22:54:29 -06:00
Huang Qi b4fd4bb257 fix: add explicit fallback model and prevent direct opencode provider calls (#4653)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-23 22:51:15 -06:00
GitHub Action eb009d5959 chore: format code 2025-11-23 20:59:37 +00:00
Frank 4a81bd0f50 wip: zen 2025-11-23 15:58:55 -05:00
Frank bbc9142fc5 wip: zen 2025-11-23 15:21:47 -05:00
Aiden Cline 7413c2715c tweak: slight improvements to title gen 2025-11-23 14:04:39 -06:00
Dax Raad 7579c3bb15 ci: remove log 2025-11-23 15:00:29 -05:00
Dax Raad 24683058fd ci: ignore 2025-11-23 14:56:24 -05:00
Dax Raad bf81e3108c ci: ignore 2025-11-23 14:56:24 -05:00
Dax Raad 5ade90416e ci: ignore 2025-11-23 14:44:05 -05:00
GitHub Action 8f2a8086c0 chore: format code 2025-11-23 19:37:19 +00:00
Dax Raad 38b70f7877 ci: secret 2025-11-23 14:36:36 -05:00
Dax Raad 5e112a17a5 token 2025-11-23 19:35:39 +00:00
Dax Raad 59a3e7e3cc ci: ignore 2025-11-23 19:35:39 +00:00
Github Action 2c93f065cb Update Nix flake.lock and hashes 2025-11-23 19:35:39 +00:00
opencode 488d33c1ed release: v1.0.106
Update Nix Hashes / update (push) Has been cancelled
2025-11-23 19:35:38 +00:00
Dax Raad de4660ac12 ci: ignore 2025-11-23 14:28:10 -05:00
Aiden Cline 27ae341684 fix bash tool wsl 2025-11-23 12:29:15 -06:00
Github Action 25b3846694 Update Nix flake.lock and hashes 2025-11-23 17:57:34 +00:00
Dax Raad 8e2f9f6544 ci: stuff 2025-11-23 12:55:33 -05:00
Github Action 76b5870f89 Update Nix flake.lock and hashes 2025-11-23 17:45:24 +00:00
Dax Raad 604891e793 ci: stuff 2025-11-23 12:43:23 -05:00
Dax Raad 5814df7eaa sync 2025-11-23 12:43:23 -05:00
Aiden Cline 2509d03f42 tweak: fix bool 2025-11-23 11:43:09 -06:00
Github Action a256df9823 Update Nix flake.lock and hashes 2025-11-23 17:09:33 +00:00
Dax Raad af96ec5a30 ignore: update @solidjs/start dependency and fix console redirect handling
Updates the @solidjs/start dependency to latest version and removes deprecated getResponseHeaders usage from auth callback. Also adds error handling for workspace ID lookup to prevent redirect failures.
2025-11-23 12:07:28 -05:00
GitHub Action 55df80b80e ignore: update download stats 2025-11-23 2025-11-23 12:04:01 +00:00
Ariane Emory 7d11986a0a feature: optional selectedListItemText element in themes and luminance-based fallback to solve 4369 (#4572)
Co-authored-by: knanao <nao.7ken@gmail.com>
Co-authored-by: knanao <k@knanao.com>
2025-11-23 01:51:07 -06:00
Aiden Cline d75d90c53e ci: fix action 2025-11-23 01:36:45 -06:00
Tyler Limbach 35fead2eca fix: make tui session text respect theme fg (#4618) (#4620) 2025-11-23 01:34:31 -06:00
Github Action 9bb2efd3ef Update Nix flake.lock and hashes 2025-11-23 07:31:17 +00:00
Aiden Cline 30ffcaa667 tweak: start bundling in some ai-sdk packages (#4649) 2025-11-23 01:29:20 -06:00
terakael ba11455786 fix: prevent permission shortcuts when ctrl/meta pressed (#4631)
Co-authored-by: terakael <terakael@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-22 23:58:10 -06:00
Dax Raad d69ba27f84 fix suspense 2025-11-23 00:16:06 -05:00
Dax Raad 448b72d046 fix logout 2025-11-23 00:12:58 -05:00
Dax Raad d96d89bcb8 fix logout 2025-11-22 23:59:50 -05:00
Dax Raad 7a013d4f40 fixed 2025-11-22 23:56:37 -05:00
GitHub Action e4597df3b9 chore: format code 2025-11-23 04:54:26 +00:00
Dax Raad 2b014fcd75 fix auth 2025-11-22 23:53:41 -05:00
Github Action c2bf6975f8 Update Nix flake.lock and hashes 2025-11-23 00:44:43 +00:00
Sebastian Herrlinger 733e1f79ac bump opentui to v0.1.49
- fix stdin handling for suspend/resume
- fix textarea cursor movement segfault for wcdwidth (alacritty/tmux)
2025-11-23 01:42:33 +01:00
GitHub Action 2a6cbfd5fd chore: format code 2025-11-23 00:20:07 +00:00
Adam 6173b69a8b wip(share): more styling 2025-11-22 18:19:02 -06:00
opencode fc72cfe784 release: v1.0.105
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 22:39:18 +00:00
Dax Raad 768c81cdfd core: prevent concurrent bun package installs that could cause corruption or conflicts 2025-11-22 17:32:53 -05:00
Dax Raad bcea8ed593 tui: fix message completion timing and duration display in session view 2025-11-22 17:28:50 -05:00
GitHub Action f93bb1dd21 chore: format code 2025-11-22 22:27:23 +00:00
Ravi Kumar b92e8510f9 fix: auto upgrade toast message (#4625)
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: opencode <opencode@sst.dev>
2025-11-22 16:26:46 -06:00
opencode 1b692ec7eb release: v1.0.104
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 20:03:20 +00:00
Dax Raad 4c97e2e8bb reset cache 2025-11-22 14:56:35 -05:00
Dax Raad b652198979 load desktop app 2025-11-22 14:40:11 -05:00
Dax Raad 31c4a1d853 docs: add Docker installation option for OpenCode 2025-11-22 14:13:04 -05:00
Goni Zahavy 6afdb5c0e5 tui: added ctrl-z terminal suspension support (#3983) 2025-11-22 12:48:23 -06:00
shuv bdcf864678 proper fix for image attachments (#4562)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-22 12:47:57 -06:00
opencode 0dd5039668 release: v1.0.103
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 18:21:49 +00:00
Dax Raad a0831eade1 tui: remove bullet prefix from timestamps in session view for cleaner display 2025-11-22 13:11:08 -05:00
GitHub Action f1dc9818b6 chore: format code 2025-11-22 18:04:34 +00:00
Dax Raad b52b7c6ded more flickering fixes 2025-11-22 13:03:44 -05:00
Dax Raad e03a41144a tui: keep assistant footer from crashing after compaction 2025-11-22 18:00:54 +00:00
opencode 37bb07e7a3 release: v1.0.100
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 18:00:54 +00:00
Dax Raad 78a6325b64 improve model footer 2025-11-22 12:54:02 -05:00
GitHub Action c96923d2c9 chore: format code 2025-11-22 17:50:32 +00:00
Valerio Di Maggio 59742fbfee Showed end time for agent loop and changed message time to show date if not current day (#4503)
Co-authored-by: GitHub Action <action@github.com>
2025-11-22 11:49:50 -06:00
Dax Raad 2938a25ec5 sync 2025-11-22 16:43:47 +00:00
opencode d163eb3888 release: v1.0.99
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 16:43:46 +00:00
Dax Raad 75c29d4d1c summary optimizaitons 2025-11-22 11:32:49 -05:00
Dax Raad e103fb1f93 ci: add Node.js setup to deploy workflow for consistent runtime environment 2025-11-22 10:47:18 -05:00
Dax Raad bd79ff87cc fix missing vite 2025-11-22 10:45:06 -05:00
Dax Raad ac21ec2f46 upgrade bun lock version 2025-11-22 10:42:09 -05:00
Github Action 5bcf017c10 Update Nix flake.lock and hashes 2025-11-22 15:41:29 +00:00
Brendan Allan 85d99198b5 Use devinxi-ed Solid Start (#4635)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-11-22 10:39:25 -05:00
GitHub Action 7f183f7404 ignore: update download stats 2025-11-22 2025-11-22 12:04:02 +00:00
opencode 85284df725 release: v1.0.98
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 06:00:23 +00:00
Dax Raad 87054ee983 fix flickering/layout shift during work 2025-11-22 00:49:35 -05:00
GitHub Action 81245c2548 chore: format code 2025-11-22 05:15:55 +00:00
Dax Raad 6f82b321d8 tauri 2025-11-22 00:15:01 -05:00
opencode f4593c6653 release: v1.0.97
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 05:02:16 +00:00
Dax Raad 15902cf54d core: add missing system libraries to docker image so the agent starts successfully 2025-11-21 23:52:01 -05:00
GitHub Action d1102c33ac chore: format code 2025-11-22 04:48:52 +00:00
Dax Raad aabbd3383c fix dockerfile 2025-11-21 23:48:09 -05:00
opencode afb55cb7d4 release: v1.0.95
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 04:27:20 +00:00
Dax Raad ade794a937 ci: ignore 2025-11-21 23:08:35 -05:00
Dax Raad 34271a82ff release: v1.0.94
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 23:06:37 -05:00
Dax Raad b20a31098a sync 2025-11-21 22:58:20 -05:00
Dax Raad b5a039e5ae ci stuff 2025-11-21 22:53:58 -05:00
GitHub Action 986cc0a01c chore: format code 2025-11-22 03:31:29 +00:00
opencode b20fd36c48 release: v1.0.92
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 03:31:28 +00:00
Dax Raad e4e6bf66e1 publish tar.gz for linux 2025-11-21 22:24:36 -05:00
opencode 3ae27273c6 release: v1.0.91
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 02:19:56 +00:00
Dax Raad eefb3c43dd fix arg parsing 2025-11-21 21:13:43 -05:00
Github Action cc229e726e Update Nix flake.lock and hashes 2025-11-22 01:43:03 +00:00
Dax 49408c00e9 enterprise (#4617)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-11-21 20:41:27 -05:00
opencode 76192fbced release: v1.0.90
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 00:16:10 +00:00
GitHub Action a1c76c79de chore: format code 2025-11-22 00:09:32 +00:00
Dax Raad db9e2b1aac ci: disable automatic config loading during CLI builds to prevent configuration interference 2025-11-21 19:08:51 -05:00
opencode 45c4970d68 release: v1.0.89
Update Nix Hashes / update (push) Has been cancelled
2025-11-22 00:00:23 +00:00
Tommy D. Rossi 1d7a9309d6 feat: lower opacity for thinking summaries (#4610) 2025-11-21 17:35:47 -06:00
Dax Raad f5ac98251e tui: split revert memo into smaller tracked computations to prevent unnecessary re-evaluations 2025-11-21 18:27:25 -05:00
GitHub Action 78239045ba chore: format code 2025-11-21 22:42:39 +00:00
Dax Raad d8b60875c4 tui: batch SDK events to reduce render churn and improve responsiveness
Intelligently queue events and flush them in 16ms batches. Events processed within 16ms of the last flush are batched together to reduce the number of store updates and re-renders, preventing jank when receiving rapid consecutive events. Events after a 16ms gap are processed immediately to avoid adding latency.
2025-11-21 17:42:02 -05:00
opencode 48949a6e9d release: v1.0.88
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 22:37:46 +00:00
GitHub Action 082a330ea3 chore: format code 2025-11-21 22:30:46 +00:00
Aiden Cline adf7df0d5c tweak: bash tool behavior w/ /bin/zsh 2025-11-21 16:30:00 -06:00
opencode a76ad48563 release: v1.0.87
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 21:58:10 +00:00
Dax Raad 00f991162f if finish reason is unknown, continue 2025-11-21 16:51:32 -05:00
Frank d6cdd24fad doc: update gpt pricing 2025-11-21 15:48:54 -05:00
GitHub Action c9473756df chore: format code 2025-11-21 20:33:42 +00:00
Ivan Starkov b5d0c56b4c fix: make bash tool respect $SHELL (#3494)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-21 14:33:04 -06:00
opencode 96a2f5268c release: v1.0.86
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 20:32:47 +00:00
Aiden Cline 5083f9c9c2 bump bun to 1.3.3 2025-11-21 13:56:46 -06:00
Frank e34df15ff5 update logic for local and dev providers 2025-11-21 13:31:33 -05:00
GitHub Action 26ec87803a chore: format code 2025-11-21 17:57:33 +00:00
Frank 037e8d4555 wip: zen 2025-11-21 12:50:51 -05:00
Adam 08a366c4dc feat(install): better install script visuals, custom progress bar and next steps (#4589) 2025-11-21 07:29:52 -06:00
GitHub Action 670e1523e0 ignore: update download stats 2025-11-21 2025-11-21 12:04:49 +00:00
Patrick Wolf 416f2964b5 fix: preserve agent context during compaction (#4556) 2025-11-21 02:13:10 -06:00
Ian Maurer e018e16898 fix(cli): ensure clean exit on provider/model errors (#4223)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-21 02:01:19 -06:00
Aiden Cline d16c8c9f0f ignore: update sdk 2025-11-21 01:25:06 -06:00
Aiden Cline fffe20cbe5 add provider whitelist 2025-11-21 01:24:44 -06:00
Aiden Cline f6da3c467b ignore: sync sdk 2025-11-21 01:04:02 -06:00
geril07 c0d9f21c0f feat: whitelist/blacklist config options for provider (#3416)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-21 01:01:30 -06:00
GitHub Action a67b616139 chore: format code 2025-11-21 06:03:38 +00:00
opencode 2991547974 release: v1.0.85
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 06:03:38 +00:00
Dax Raad b59def2e4a hide gpt5 nano 2025-11-21 00:58:02 -05:00
opencode d842353f39 release: v1.0.84
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 05:55:03 +00:00
Dax Raad d20ef569de tui: replace text shimmer with animated progress bar during model processing 2025-11-21 00:47:27 -05:00
Dax Raad ca2b871810 tui: simplify popular providers list to show only top 6 instead of applying provider priority sorting 2025-11-21 00:32:21 -05:00
Dax 23ea8ba1ce Tui onboarding (#4569)
Co-authored-by: GitHub Action <action@github.com>
2025-11-21 00:21:06 -05:00
Zak c417fec246 tweak: adjust invalid directory error message (#4567)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-20 22:36:07 -06:00
Aiden Cline 5413b16b57 fix: split not a function error 2025-11-20 22:33:59 -06:00
Spoon 43c021ed80 feat: add field to allow user only messages (#4554)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-20 19:13:42 -06:00
GitHub Action 3b005d29d7 chore: format code 2025-11-21 00:49:39 +00:00
opencode 635f70f477 release: v1.0.83
Update Nix Hashes / update (push) Has been cancelled
2025-11-21 00:49:39 +00:00
Aiden Cline adbb6037ac fix: undefined err 2025-11-20 18:43:58 -06:00
opencode 598d6d00e4 release: v1.0.82
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 22:44:37 +00:00
Aiden Cline cf934357c9 fix: make use openrouter ai sdk package to fix interlevened thinking models 2025-11-20 16:32:44 -06:00
Jay 8063e645c7 docs: Clarify model provider recommendations in README
Updated the README to clarify model provider recommendations.
2025-11-20 17:21:19 -05:00
Aiden Cline 8ab206b443 tweak: navigate to child session if it is asking for permissions 2025-11-20 15:46:49 -06:00
Aiden Cline ec5c96e10d Revert "Image tag fix for real this time (#4540)"
This reverts commit eb975bb89c.
2025-11-20 14:14:02 -06:00
Aiden Cline d2a61290b9 Revert "ignore: write todo"
This reverts commit 5aa2078852.
2025-11-20 14:13:56 -06:00
Aiden Cline 10faf9e717 ignore: fix mdx 2025-11-20 14:07:25 -06:00
Aiden Cline cba239bc8f change default child session cycle keybind to <leader>right, and <leader>left 2025-11-20 14:01:02 -06:00
jian 6f5e3ddfb3 chore: rm empty file (#4552) 2025-11-20 13:33:09 -06:00
GitHub Action d412ba264a chore: format code 2025-11-20 19:17:06 +00:00
Aiden Cline 9780f2b792 docs: update model list 2025-11-20 13:16:27 -06:00
Aiden Cline 5aa2078852 ignore: write todo 2025-11-20 12:35:34 -06:00
shuv eb975bb89c Image tag fix for real this time (#4540)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-20 12:34:39 -06:00
GitHub Action 9479fe3ce6 chore: format code 2025-11-20 17:35:43 +00:00
William Tan 4393cf8dbe tweak: Prefer VISUAL environment variable over EDITOR per Unix convention (#4549)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-20 11:27:16 -06:00
opencode 447a4ca8c3 release: v1.0.81
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 17:16:31 +00:00
Aiden Cline 40ac2549ff fix: aur build 2025-11-20 11:04:09 -06:00
cc-bjojac a9c56b813a Make the MAX_OUTPUT_LENGTH in bash.ts overridable by OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH (#4497)
Co-authored-by: Björn Jacobs <bjoern.jacobs.ext@bundesanzeiger.de>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-20 10:45:13 -06:00
Haris Gušić b1b73c9deb fix: Autocomplete with existing space after trigger (#4121)
Co-authored-by: GitHub Action <action@github.com>
2025-11-20 10:37:45 -06:00
Aiden Cline 774377330b fix: lockfile 2025-11-20 10:16:34 -06:00
processtrader e7a157ef8f fix: replace ruby-lsp with rubocop for better LSP performance (#4543) 2025-11-20 09:54:47 -06:00
Frank 3989b9fc7f wip: zen 2025-11-20 10:31:35 -05:00
GitHub Action 8bfcdf4831 chore: format code 2025-11-20 14:55:06 +00:00
Frank 3632ba3785 zen: show token breakdown 2025-11-20 09:54:22 -05:00
Albert O'Shea b7b3824d76 nix: update flake.lock as part of the github workflow (#4535)
Co-authored-by: Github Action <action@github.com>
2025-11-20 08:43:13 -06:00
Tommy D. Rossi b12efb2023 fix: do not print ai sdk warnings (#4541)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-20 08:42:24 -06:00
Roberto Gongora bd91cf220c docs: typo in PHP Intelephense documentation (#4545) 2025-11-20 08:41:31 -06:00
Asmae_ELAZRAK 9eb6731c21 docs: add cortecs docs (#4547) 2025-11-20 08:34:04 -06:00
GitHub Action 11373755d9 ignore: update download stats 2025-11-20 2025-11-20 12:04:55 +00:00
Aiden Cline 00b5e9f6ca chore: rm unused code 2025-11-20 00:10:51 -06:00
Aiden Cline 6b3f424e4d tweak: let user name export file 2025-11-19 23:58:40 -06:00
Aiden Cline e7dfeec9c4 fix: make toast wrap 2025-11-19 22:50:27 -06:00
Aiden Cline 97893bd7e6 tweak: make /export go to cwd 2025-11-19 22:45:40 -06:00
Frank bfefdb3752 zen: add gemini icon 2025-11-19 21:14:31 -05:00
Frank 12b79c581e zen: update gemini provider name 2025-11-19 20:58:22 -05:00
Frank ac9b4c7ebf docs: add gemini 3 pricing 2025-11-19 20:50:43 -05:00
GitHub Action 208af232ff chore: format code 2025-11-20 01:27:50 +00:00
opencode 600c6b4973 release: v1.0.80
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 01:27:49 +00:00
Dax Raad 61007a9b94 refactor: switch to Switch/Match pattern for assistant message status rendering 2025-11-19 20:18:15 -05:00
opencode 52fe1a5ac5 release: v1.0.79
Update Nix Hashes / update (push) Has been cancelled
2025-11-20 01:11:20 +00:00
althafdemiandra 468927e06a chore: bump ai-sdk to v5.0.97 (#4518)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-19 18:44:33 -06:00
Aiden Cline 61562dd9f0 make aur build check if u are glibc system or a musl system (#4519) 2025-11-19 18:36:33 -06:00
Github Action c86dd91310 Update Nix hashes 2025-11-19 23:30:37 +00:00
Sebastian Herrlinger 9c85a37811 bump opentui version to v0.1.47
- fixing cursor issues with some graphemes in textarea
- proper suspend/resume
2025-11-20 00:28:25 +01:00
Aiden Cline 51bba6e634 tweak: default to disabling fetch timeout in provider options 2025-11-19 16:20:29 -06:00
Daniel Polito e1089bc5de Adding LSP: PHP Intelephense (#4504)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 16:01:18 -06:00
Aiden Cline 618c654aa0 ignore: todo fix test case 2025-11-19 15:18:21 -06:00
Iljo 4703e859bd Add YAML language server support (#4508)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 14:47:04 -06:00
Aiden Cline a1dc4ebbe4 ignore: flaky test be a lil less flaky plz 2025-11-19 14:46:06 -06:00
Aiden Cline e4e6096510 ignore: fix hanging test 2025-11-19 14:38:12 -06:00
Aiden Cline c472734933 tweak: make getUsage function handle missing usage data 2025-11-19 14:29:19 -06:00
Aiden Cline 9d068c20bb fix: openrouter ai sdk package support 2025-11-19 14:22:51 -06:00
Aiden Cline 48e4f2f45d tweak: add bun install retries 2025-11-19 13:04:20 -06:00
Aiden Cline bbf4574476 fix: make external_directory permission wildcarding more sane 2025-11-19 12:55:02 -06:00
Adam 8bad513140 Revert "feat(cli): better install script output"
This reverts commit 24bb293136.
2025-11-19 12:44:35 -06:00
Aiden Cline 1ff5d888c2 fix: make bash tool use external_directory perm 2025-11-19 12:31:34 -06:00
Dax Raad 5d25758400 use bash description as task title 2025-11-19 13:23:29 -05:00
Dax 16fdc90976 fix: resolve issue 4475 (#4505) 2025-11-19 13:10:09 -05:00
Aiden Cline 793542230f tweak: bash description 2025-11-19 11:31:12 -06:00
Tommy D. Rossi 9de1242d9b fix: show reasoning summaries for gemini models (#4491)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 11:25:18 -06:00
Aiden Cline b3afa84058 Revert "Added subagents to agents modal, non-selectable (#4460)"
This reverts commit 90044196bf.
2025-11-19 11:00:38 -06:00
Aiden Cline 024a10bbb5 ci: auto label nix 2025-11-19 10:51:11 -06:00
Adam bef9ac96e2 fix(web): stats 2025-11-19 10:05:39 -06:00
Adam 24bb293136 feat(cli): better install script output 2025-11-19 09:30:41 -06:00
Adam 45180104fe fix(desktop): message animation 2025-11-19 06:04:20 -06:00
Adam edd86e3fb7 fix(desktop): text part styling 2025-11-19 06:04:20 -06:00
Adam 4a72d57534 fix(desktop): pre styling 2025-11-19 06:04:19 -06:00
Aiden Cline 0068cb305f tweak: toast 2025-11-19 00:51:07 -06:00
opencode-agent[bot] 90044196bf Added subagents to agents modal, non-selectable (#4460)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-19 00:40:47 -06:00
Shantur Rathore 963a926db2 allow task tool to have resume capabilities (#4204)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-11-19 00:17:26 -06:00
Frank 0d3d48bb59 zen: fix cost in graph 2025-11-18 23:43:26 -05:00
GitHub Action 66eaba4bdc chore: format code 2025-11-19 01:02:57 +00:00
Dax Raad 21b6e5404e feat: add @opencode-ai/util package with utility functions 2025-11-18 20:02:10 -05:00
GitHub Action a0fe59ab75 chore: format code 2025-11-18 23:49:29 +00:00
Aiden Cline 81ebf56cf1 feat: add top level lsp: false and formatter: false to allow disabling all formatters or lsps at once 2025-11-18 17:48:40 -06:00
opencode 429708e3d5 release: v1.0.78
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 23:36:35 +00:00
shuv d50f825c6d fix: pass model info to ReadTool to enable image support check (#4473)
Co-authored-by: GitHub Action <action@github.com>
2025-11-18 17:20:03 -06:00
K Whiteside 47bfae52c0 fix: permission checks for external_directory and doom_loop (#4433)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: AerionDyseti <AerionDyseti@users.noreply.github.com>
2025-11-18 17:18:23 -06:00
Frank 52cf9e3423 wip: zen 2025-11-18 17:42:48 -05:00
GitHub Action a9b6debfa2 chore: format code 2025-11-18 22:24:29 +00:00
Eric Juden d6bf475749 docs: Improving Plugin Documentation - Adding Events (#4438) 2025-11-18 16:23:46 -06:00
opencode f22580e943 release: v1.0.77
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 22:17:39 +00:00
Dax Raad 6d98db57c7 better gemini retry errors 2025-11-18 17:11:29 -05:00
OpeOginni 59f127a250 fix: allow for theme references (#4450) 2025-11-18 14:26:42 -06:00
Adam 3068e7dcf7 fix(desktop): animating too much 2025-11-18 14:24:26 -06:00
GitHub Action f83d62191a chore: format code 2025-11-18 20:23:29 +00:00
Longlone 3b72857124 fix: update reasoningEffort logic for gpt-5.1 models in SessionPrompt-ensureTitle (#4456)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-18 14:22:44 -06:00
opencode 68cd105d9d release: v1.0.76
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 20:12:38 +00:00
Aiden Cline e09af2cb4b fix windows bash tool issue 2025-11-18 14:06:45 -06:00
Adam 14bd3b1d30 chore(desktop): remove logging 2025-11-18 13:52:29 -06:00
Adam 3a9c2152f7 fix(desktop): reactivity issue on route change 2025-11-18 13:45:27 -06:00
Frank 7283bfa480 zen: gemini 2025-11-18 14:28:31 -05:00
opencode-agent[bot] 37d5099728 Added opencode agent list command to show all available agents with details. (#4446)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-18 13:25:53 -06:00
GitHub Action d45fc030b2 chore: format code 2025-11-18 18:35:26 +00:00
Adam c7042c807f fix(desktop): only animate response once 2025-11-18 12:34:34 -06:00
opencode 202f6f1be9 release: v1.0.75
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 18:16:14 +00:00
Dax Raad 759635eefa fix gpt compaction issue 2025-11-18 13:10:00 -05:00
Aiden Cline a9981441ae tweak: use temperature 1 for gemini 3 pro 2025-11-18 11:49:39 -06:00
Adam 71302de4f1 fix(desktop): css typo 2025-11-18 11:40:50 -06:00
Adam 333b8e907b fix(desktop): busy state and reactivity 2025-11-18 11:35:23 -06:00
GitHub Action 13f319b64f chore: format code 2025-11-18 17:16:07 +00:00
opencode b573eadd9e release: v1.0.74
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 17:16:06 +00:00
Dax Raad 50bfff89c0 fix model dialog sorting 2025-11-18 12:10:19 -05:00
Adam fc5fc2c570 wip(desktop): new layout work 2025-11-18 17:07:34 +00:00
Adam 4069999b78 wip(desktop): new layout work 2025-11-18 17:07:34 +00:00
opencode 5ba9b47b3c release: v1.0.73
Update Nix Hashes / update (push) Has been cancelled
2025-11-18 17:07:33 +00:00
Dax Raad 7c0cc94023 rework default model 2025-11-18 12:01:41 -05:00
GitHub Action 3ed1bd2e8e ignore: update download stats 2025-11-18 2025-11-18 12:04:35 +00:00
Aiden Cline ce6436280a ci: ignore update nix hash job 2025-11-18 01:26:30 -06:00
Aiden Cline e49204bd33 ignore: fix snapshot (#4444)
Co-authored-by: opencode <opencode@sst.dev>
2025-11-18 01:22:38 -06:00
Aiden Cline 856c87d05c fix: snapshot? 2025-11-18 01:02:52 -06:00
Aiden Cline de35c3fb84 ci: ignore 2025-11-18 00:53:08 -06:00
Aiden Cline 4359719f9a ignore: format 2025-11-18 00:49:17 -06:00
Albert O'Shea 5e13527416 feat: nix support for the nix folks (#3924)
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-11-18 00:46:49 -06:00
Frank aba94c658f wip: zen 2025-11-18 01:27:31 -05:00
opencode-agent[bot] 6e318ba567 Added width constraints to toast component for proper text wrapping. (#4441)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-18 00:23:36 -06:00
GitHub Action ddddecf88a chore: format code 2025-11-18 05:46:02 +00:00
Frank 16cb77c094 zen: add usage graph 2025-11-18 00:45:14 -05:00
Jake Nelson a5564f730e feat: add Swift syntax highlighting support (#4434) 2025-11-17 21:53:03 -06:00
GitHub Action a15c97bbfe chore: format code 2025-11-18 03:19:47 +00:00
Aiden Cline a398eed8b8 Revert "Updated scroll_speed to allow any positive number" (#4437) 2025-11-17 21:19:06 -06:00
opencode-agent[bot] a10fd8ca5c Updated scroll_speed to allow any positive number (#4436)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-17 21:18:33 -06:00
opencode ff7513238b release: v1.0.72 2025-11-18 03:03:54 +00:00
GitHub Action af1cd60d3e chore: format code 2025-11-18 02:53:13 +00:00
Aiden Cline c66def2049 fix: noreply 2025-11-17 20:52:25 -06:00
opencode 008ccb4729 release: v1.0.71 2025-11-18 01:59:40 +00:00
Dax Raad bc232045a1 respect server suggestion for default model 2025-11-17 20:53:48 -05:00
GitHub Action 16cab556df chore: format code 2025-11-18 01:27:22 +00:00
Jay V 66148df74b docs: clarify custom tools can execute scripts in any language with Python example 2025-11-17 20:26:27 -05:00
opencode 4611e08f09 release: v1.0.70 2025-11-17 23:45:49 +00:00
Sebastian Herrlinger bf6204f577 upgrade opentui to v0.1.46
- enable bracketed paste (and more) on win
- fix word wrapping with CJK and at wrap/chunk boundaries
- old style meta+arrow
- allow <1 scroll speed for slowdown
2025-11-18 00:22:21 +01:00
Daniel Hofheinz 17cde9feb7 docs: add built-in agents reference to README (#3047)
Co-authored-by: Jay V <air@live.ca>
2025-11-17 17:19:14 -05:00
Aiden Cline 7eccbdc4ac fix /exit 2025-11-17 16:13:41 -06:00
Aiden Cline ab072290fc Revert "fix: system theme background to use 'none' for terminal transparency (#4408)"
This reverts commit f4a4514a9f.
2025-11-17 16:03:27 -06:00
GitHub Action ad9d83748c chore: format code 2025-11-17 21:46:10 +00:00
Aiden Cline 55b57e1aae ci: tweak 2025-11-17 15:45:23 -06:00
Aiden Cline 21b7877beb docs: tweak wording 2025-11-17 21:26:46 +00:00
opencode de50234a1a release: v1.0.69 2025-11-17 21:26:46 +00:00
opencode-agent[bot] d60102ba52 Added /thinking slash command to toggle thinking blocks visibility in OpenTUI. (#4424)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-11-17 15:16:35 -06:00
Haris Gušić 066a876f3d docs(contributing): Add "Setting up a Debugger" section (#4421)
Co-authored-by: GitHub Action <action@github.com>
2025-11-17 14:28:06 -06:00
Haris Gušić c07a241ca8 chore: Remove obsolete 'any' type annotation (#4423) 2025-11-17 14:27:43 -06:00
Aiden Cline 0a2fffa9b5 tweak: whitelist 2025-11-17 13:18:13 -06:00
Dax Raad bdfa213ccf deprecated session.idle event 2025-11-17 11:42:45 -05:00
Aiden Cline 7f0b2ce1ac Reapply "fix: system theme background to use 'none' for terminal transparency" (#4415)
This reverts commit a5365ce294.
2025-11-17 10:39:53 -06:00
Dax Raad 0a2d7af179 core: honor retry-after values exceeding 10 minutes instead of discarding them 2025-11-17 11:33:28 -05:00
Dax Raad 37652f48fb ignore 2025-11-17 11:32:07 -05:00
Dax Raad 8b19c6c7e4 better retry display 2025-11-17 11:31:10 -05:00
Aiden Cline a5365ce294 Revert "fix: system theme background to use 'none' for terminal transparency" (#4415) 2025-11-17 10:24:20 -06:00
Jensen f4a4514a9f fix: system theme background to use 'none' for terminal transparency (#4408) 2025-11-17 10:22:31 -06:00
opencode-agent[bot] 154006469c Updated help dialog to use dynamic keybind (#4414)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-17 10:18:59 -06:00
Dax a1214fff2e Refactor agent loop (#4412) 2025-11-17 10:57:18 -05:00
GitHub Action 9fd43ec616 ignore: update download stats 2025-11-17 2025-11-17 12:04:41 +00:00
Luke Parker 5731c268b6 fix: Line count on win (#4401) 2025-11-17 01:08:22 -06:00
Keath Milligan f4d892d4e1 fix: handle Git Bash path mapping on windows (#4380) 2025-11-17 01:06:44 -06:00
Aiden Cline 10b3702938 chore: update type 2025-11-17 00:07:23 -06:00
Tyler Gannon e96442310c chore: replace z.union with z.enum for cleaner OpenAPI generation (#4394) 2025-11-17 00:06:40 -06:00
Spoon 5c722bf8c4 fix(batch): simple UX feedback (#4396) 2025-11-17 00:02:05 -06:00
Youssef Achy 58cc5cdf2a add support for azure cognitive services provider (#4397) 2025-11-17 00:01:45 -06:00
opencode-agent[bot] 3c6dcad2af Fixed OPENCODE_CONFIG_DIR to load config files. (#4400)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-16 23:48:36 -06:00
Jay 2535f9febf docs: Add clarification for projects using 'opencode' name
Added a section to clarify the affiliation of related projects.
2025-11-16 20:51:41 -05:00
Aiden Cline 25678fa504 fix: vercel gateway options 2025-11-16 18:39:31 -06:00
Sebastian Herrlinger d7f4f3ec1f bump opentui version to 0.1.45, fixing highlighting on windows 2025-11-16 23:56:11 +01:00
Aiden Cline 16ccb39459 docs: permissions 2025-11-16 16:40:48 -06:00
Aiden Cline f8630fb188 ignore: rm 2025-11-16 16:32:04 -06:00
Baptiste Cavallo 72e604744d fix(batch): restore per-tool UI feedback + UX improvements (#4387) 2025-11-16 16:31:41 -06:00
opencode-agent[bot] 832be6e7eb Added copy option to message context menu (#4389)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-16 15:35:05 -06:00
opencode 8ba48ed71d release: v1.0.68 2025-11-16 20:38:48 +00:00
Aiden Cline cf266f6162 fix: promptCacheKey set unnecessarily 2025-11-16 14:32:57 -06:00
GitHub Action 1e6589526d ignore: update download stats 2025-11-16 2025-11-16 12:04:11 +00:00
Frank f6b3ffaf64 wip: zen 2025-11-16 03:32:13 -05:00
GitHub Action 5d765d63d4 chore: format code 2025-11-16 08:30:36 +00:00
Frank 0e12dd62a3 zen: usage paging 2025-11-16 03:29:52 -05:00
opencode 2b957b5d1c release: v1.0.67 2025-11-16 07:49:52 +00:00
GitHub Action 31c7a0157c chore: format code 2025-11-16 07:44:06 +00:00
Aiden Cline e728b94bca fix: panic when theme has 'none' 2025-11-16 01:43:23 -06:00
opencode 49040c0130 release: v1.0.66 2025-11-16 07:27:25 +00:00
Aiden Cline 0d05238ee6 fix: initial val 2025-11-16 01:14:49 -06:00
Aiden Cline 9b8a7da1e6 fix: history jsonl file corruption cases (#4364) 2025-11-16 00:50:13 -06:00
Zeno Jiricek 61fd21182c docs: mise installation command (#2938) 2025-11-15 21:44:28 -06:00
GitHub Action 487c2b5e76 chore: format code 2025-11-16 03:38:13 +00:00
xiaojie.zj 0e4703b227 add: add zenmux doc and header (#3597)
Co-authored-by: xiaojie.zj <xiaojie.zj@antgroup.com>
2025-11-15 21:37:30 -06:00
Alvin Johansson 84e0232bd5 Add Flexoki theme (#3986) 2025-11-15 21:28:13 -06:00
Luke Parker 35fbb011b2 fix: Diff view now ignores line endings changes/windows autocrlf (#4356) 2025-11-15 21:18:39 -06:00
Aiden Cline 6527a123f0 fix aur build (#4359) 2025-11-15 20:16:19 -06:00
Aiden Cline 0377cfd37c fix: omit ref for todo tool 2025-11-15 19:19:36 -06:00
Aiden Cline edc933d816 tweak: make zod error more prompty 2025-11-15 13:19:24 -06:00
GitHub Action 0d608f6014 ignore: update download stats 2025-11-15 2025-11-15 12:04:09 +00:00
Chris Olszewski 69a45ef7d7 fix: snapshot history when running from git worktrees (#4312) 2025-11-15 01:02:00 -06:00
Baptiste Cavallo 1056b36eae experimental batch tool (#2983)
Co-authored-by: GitHub Action <action@github.com>
2025-11-15 00:54:36 -06:00
Aiden Cline 35c737ac68 tweak: only show dropdown for 3+ items (#4345) 2025-11-14 23:45:48 -06:00
Abílio Costa 725a2c2e95 docs: clarify that config files are merged, not replaced (#4342)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-14 17:49:47 -06:00
Tyler Gannon c724d2392f fix: replace union type with enum "true"/"false" in /find/file endpoint (#4338) 2025-11-14 17:48:23 -06:00
Frank f5230d1f02 fix: incorrect sonnet price calculation 2025-11-14 18:46:43 -05:00
GitHub Action 078111bd96 chore: format code 2025-11-14 22:44:36 +00:00
sredfern 736f8882f5 fix(provider): support local file paths for custom providers (#4323) 2025-11-14 16:43:59 -06:00
Brian Cheung 37cf365927 feat: support images in mcp tool responses (#4100)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-11-14 15:00:52 -06:00
Aiden Cline b939470302 fix: add azure exclusion 2025-11-14 11:54:00 -06:00
Aiden Cline ef4b2baedc set verbosity to low for gpt-5.1 (match codex) 2025-11-14 11:52:29 -06:00
Dax Raad 64d28ea457 fix sdk types 2025-11-14 12:42:46 -05:00
Dax Raad 2520780846 fix sdk types 2025-11-14 12:42:46 -05:00
Shantur Rathore 986c60353e set promptCacheKey for openai compatible providers (#4203)
Co-authored-by: GitHub Action <action@github.com>
2025-11-14 11:41:01 -06:00
Dax Raad 5fc26c958a add global.event.subscribe() to sdk 2025-11-14 12:32:43 -05:00
Frank c1cf9cda6a doc: add baseten provider 2025-11-14 12:19:58 -05:00
GitHub Action 10d376eab2 ignore: update download stats 2025-11-14 2025-11-14 12:04:48 +00:00
Frank 53fc8a861b zen: add gpt-5-nano model 2025-11-14 00:59:42 -05:00
Frank 1d8330331c zen: use gpt-5-nano as small model 2025-11-14 00:59:00 -05:00
Frank 7a03c7fe38 zen: add gpt5.1 to docs 2025-11-13 23:47:38 -05:00
Frank 09bd32169c zen: hide alpha models 2025-11-13 23:10:06 -05:00
Dax Raad 7ec32f834e improve read tool end-of-file detection to prevent infinite loops 2025-11-13 21:41:06 -05:00
GitHub Action 205492c7e8 chore: format code 2025-11-14 01:16:58 +00:00
Aiden Cline 4c2e888709 no mr llm, you may not read that 2025-11-13 19:16:07 -06:00
1545 changed files with 23461 additions and 7038 deletions
@@ -1,3 +1,7 @@
#
# This file is intentionally in the wrong dir, will move and add later....
#
name: Guidelines Check
on:
+16 -8
View File
@@ -28,14 +28,14 @@ jobs:
const versionPattern = /[v]?1\.0\./i;
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
// Check for "nix" keyword
const nixPattern = /\bnix\b/i;
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
const labels = [];
if (isWebRelated) {
// Add web label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['web']
});
labels.push('web');
// Assign to adamdotdevin
await github.rest.issues.addAssignees({
@@ -46,10 +46,18 @@ jobs:
});
} else if (isVersionRelated) {
// Only add opentui if NOT web-related
labels.push('opentui');
}
if (isNixRelated) {
labels.push('nix');
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['opentui']
labels: labels
});
}
+4
View File
@@ -17,6 +17,10 @@ jobs:
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@v4
with:
node-version: "24"
- run: bun sst deploy --stage=${{ github.ref_name }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+4 -4
View File
@@ -21,18 +21,18 @@ jobs:
- name: Check for duplicate issues
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: |
{
"bash": {
"gh issue*": "allow",
"*": "deny"
},
"*": "deny"
},
"webfetch": "deny"
}
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created:'
opencode run -m opencode/claude-haiku-4-5 "A new issue has been created:'
Issue number:
${{ github.event.issue.number }}
+5 -1
View File
@@ -3,6 +3,8 @@ name: opencode
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
opencode:
@@ -21,9 +23,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- uses: ./.github/actions/setup-bun
- name: Run opencode
uses: sst/opencode/github@latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/glm-4.6
model: opencode/claude-haiku-4-5
+7
View File
@@ -61,6 +61,13 @@ jobs:
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish
run: |
./script/publish.ts
+4 -1
View File
@@ -1,11 +1,14 @@
name: snapshot
on:
workflow_dispatch:
push:
branches:
- dev
- windows
- test-bedrock
- v0
- otui-diffs
- snapshot-*
concurrency: ${{ github.workflow }}-${{ github.ref }}
+6
View File
@@ -28,3 +28,9 @@ jobs:
bun turbo test
env:
CI: true
- name: Check SDK is up to date
run: |
bun ./packages/sdk/js/script/build.ts
git diff --exit-code packages/sdk/js/src/gen packages/sdk/js/dist
continue-on-error: false
+102
View File
@@ -0,0 +1,102 @@
name: Update Nix Hashes
permissions:
contents: write
on:
workflow_dispatch:
push:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
pull_request:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
jobs:
update:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
SYSTEM: x86_64-linux
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
ref: ${{ github.head_ref || github.ref_name }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- name: Setup Nix
uses: DeterminateSystems/nix-installer-action@v20
- name: Configure git
run: |
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Update flake.lock
run: |
set -euo pipefail
echo "📦 Updating flake.lock..."
nix flake update
echo "✅ flake.lock updated successfully"
- name: Update node_modules hash
run: |
set -euo pipefail
echo "🔄 Updating node_modules hash..."
nix/scripts/update-hashes.sh
echo "✅ node_modules hash updated successfully"
- name: Commit hash changes
env:
TARGET_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
set -euo pipefail
echo "🔍 Checking for changes in tracked Nix files..."
summarize() {
local status="$1"
{
echo "### Nix Hash Update"
echo ""
echo "- ref: ${GITHUB_REF_NAME}"
echo "- status: ${status}"
} >> "$GITHUB_STEP_SUMMARY"
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
echo "✅ No changes detected. Hashes are already up to date."
summarize "no changes"
exit 0
fi
echo "📝 Changes detected:"
echo "$STATUS"
echo "🔗 Staging files..."
git add "${FILES[@]}"
echo "💾 Committing changes..."
git commit -m "Update Nix flake.lock and hashes"
echo "✅ Changes committed"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
echo "🌳 Pulling latest from branch: $BRANCH"
git pull --rebase origin "$BRANCH"
echo "🚀 Pushing changes to branch: $BRANCH"
git push origin HEAD:"$BRANCH"
echo "✅ Changes pushed successfully"
summarize "committed $(git rev-parse --short HEAD)"
+5
View File
@@ -13,3 +13,8 @@ dist
.turbo
**/.serena
.serena/
/result
refs
Session.vim
opencode.json
a.out
+7
View File
@@ -1,2 +1,9 @@
#!/bin/sh
# Check if bun version matches package.json
EXPECTED_VERSION=$(grep '"packageManager"' package.json | sed 's/.*"bun@\([^"]*\)".*/\1/')
CURRENT_VERSION=$(bun --version)
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
echo "Error: Bun version $CURRENT_VERSION does not match expected version $EXPECTED_VERSION from package.json"
exit 1
fi
bun typecheck
+23
View File
@@ -0,0 +1,23 @@
---
description: "Find issue(s) on github"
model: opencode/claude-haiku-4-5
---
Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
$ARGUMENTS
Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
Please list any matching issues with:
- Issue number and title
- Brief explanation of why it matches the query
- Link to the issue
If no clear matches are found, say so.
-4
View File
@@ -1,4 +0,0 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"]
}
+27
View File
@@ -0,0 +1,27 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080",
},
},
},
"mcp": {
"exa": {
"type": "remote",
"url": "https://mcp.exa.ai/mcp",
},
"morph": {
"type": "local",
"command": ["bunx", "@morphllm/morphmcp"],
"environment": {
"ENABLED_TOOLS": "warp_grep",
},
},
},
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "bun",
"request": "attach",
"name": "opencode (attach)",
"url": "ws://localhost:6499/"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"oven.bun-vscode"
]
}
+32
View File
@@ -42,6 +42,38 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
> [!NOTE]
> After touching `packages/opencode/src/server/server.ts`, run "./packages/sdk/js/script/build.ts" to regenerate the JS sdk.
### Setting up a Debugger
Bun debugging is currently rough around the edges. We hope this guide helps you get set up and avoid some pain points.
The most reliable way to debug OpenCode is to run it manually in a terminal via `bun run --inspect=<url> dev ...` and attach
your debugger via that URL. Other methods can result in breakpoints being mapped incorrectly, at least in VSCode (YMMV).
Caveats:
- `*.tsx` files won't have their breakpoints correctly mapped. This seems due to Bun currently not supporting source maps on code transformed
via `BunPlugin`s (currently necessary due to our dependency on `@opentui/solid`). Currently, the best you can do in terms of debugging `*.tsx`
files is writing a `debugger;` statement. Debugging facilities like stepping won't work, but at least you will be informed if a specific code
is triggered.
- If you want to run the OpenCode TUI and have breakpoints triggered in the server code, you might need to run `bun dev spawn` instead of
the usual `bun dev`. This is because `bun dev` runs the server in a worker thread and breakpoints might not work there.
Other tips and tricks:
- You might want to use `--inspect-wait` or `--inspect-brk` instead of `--inspect`, depending on your workflow
- Specifying `--inspect=ws://localhost:6499/` on every invocation can be tiresome, you may want to `export BUN_OPTIONS=--inspect=ws://localhost:6499/` instead
#### VSCode Setup
If you use VSCode, you can use our example configurations [.vscode/settings.example.json](.vscode/settings.example.json) and [.vscode/launch.example.json](.vscode/launch.example.json).
Some debug methods that can be problematic:
- Debug configurations with `"request": "launch"` can have breakpoints incorrectly mapped and thus unusable
- The same problem arises when running OpenCode in the VSCode `JavaScript Debug Terminal`
With that said, you may want to try these methods, as they might work for you.
## Pull Request Expectations
- Try to keep pull requests small and focused.
+24 -2
View File
@@ -28,8 +28,10 @@ curl -fsSL https://opencode.ai/install | bash
npm i -g opencode-ai@latest # or bun/pnpm/yarn
scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use --pin -g ubi:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
```
> [!TIP]
@@ -50,6 +52,22 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agents
OpenCode includes two built-in agents you can switch between,
you can switch between these using the `Tab` key.
- **build** - Default, full access agent for development work
- **plan** - Read-only agent for analysis and code exploration
- Denies file edits by default
- Asks permission before running bash commands
- Ideal for exploring unfamiliar codebases or planning changes
Also, included is a **general** subagent for complex searches and multi-step tasks.
This is used internally and can be invoked using `@general` in messages.
Learn more about [agents](https://opencode.ai/docs/agents).
### Documentation
For more info on how to configure OpenCode [**head over to our docs**](https://opencode.ai/docs).
@@ -58,6 +76,10 @@ For more info on how to configure OpenCode [**head over to our docs**](https://o
If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request.
### Building on OpenCode
If you are working on a project that's related to OpenCode and is using "opencode" as a part of its name; for example, "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in anyway.
### FAQ
#### How is this different than Claude Code?
@@ -65,7 +87,7 @@ If you're interested in contributing to OpenCode, please read our [contributing
It's very similar to Claude Code in terms of capability. Here are the key differences:
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen); OpenCode can be used with Claude, OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- Out of the box LSP support
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
+16
View File
@@ -139,3 +139,19 @@
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
+770 -691
View File
File diff suppressed because it is too large Load Diff
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1764384123,
"narHash": "sha256-UoliURDJFaOolycBZYrjzd9Cc66zULEyHqGFH3QHEq0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "59b6c96beacc898566c9be1052ae806f3835f87d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+107
View File
@@ -0,0 +1,107 @@
{
description = "OpenCode development flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs =
{
nixpkgs,
...
}:
let
systems = [
"aarch64-linux"
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
lib = nixpkgs.lib;
forEachSystem = lib.genAttrs systems;
pkgsFor = system: nixpkgs.legacyPackages.${system};
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
bunTarget = {
"aarch64-linux" = "bun-linux-arm64";
"x86_64-linux" = "bun-linux-x64";
"aarch64-darwin" = "bun-darwin-arm64";
"x86_64-darwin" = "bun-darwin-x64";
};
defaultNodeModules = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
hashesFile = "${./nix}/hashes.json";
hashesData =
if builtins.pathExists hashesFile then builtins.fromJSON (builtins.readFile hashesFile) else { };
nodeModulesHash = hashesData.nodeModules or defaultNodeModules;
modelsDev = forEachSystem (
system:
let
pkgs = pkgsFor system;
in
pkgs."models-dev"
);
in
{
devShells = forEachSystem (
system:
let
pkgs = pkgsFor system;
in
{
default = pkgs.mkShell {
packages = with pkgs; [
bun
nodejs_20
pkg-config
openssl
git
];
};
}
);
packages = forEachSystem (
system:
let
pkgs = pkgsFor system;
mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
hash = nodeModulesHash;
};
mkPackage = pkgs.callPackage ./nix/opencode.nix { };
in
{
default = mkPackage {
version = packageJson.version;
src = ./.;
scripts = ./nix/scripts;
target = bunTarget.${system};
modelsDev = "${modelsDev.${system}}/dist/_api.json";
mkNodeModules = mkNodeModules;
};
}
);
apps = forEachSystem (
system:
let
pkgs = pkgsFor system;
in
{
opencode-dev = {
type = "app";
meta = {
description = "Nix devshell shell for OpenCode";
runtimeInputs = [ pkgs.bun ];
};
program = "${
pkgs.writeShellApplication {
name = "opencode-dev";
text = ''
exec bun run dev "$@"
'';
}
}/bin/opencode-dev";
};
}
);
};
}
+26
View File
@@ -30,6 +30,24 @@ Leave the following comment on a GitHub PR. opencode will implement the requeste
Delete the attachment from S3 when the note is removed /oc
```
#### Review specific code lines
Leave a comment directly on code lines in the PR's "Files" tab. opencode will automatically detect the file, line numbers, and diff context to provide precise responses.
```
[Comment on specific lines in Files tab]
/oc add error handling here
```
When commenting on specific lines, opencode receives:
- The exact file being reviewed
- The specific lines of code
- The surrounding diff context
- Line number information
This allows for more targeted requests without needing to specify file paths or line numbers manually.
## Installation
Run the following command in the terminal from your GitHub repo:
@@ -51,6 +69,8 @@ This will walk you through installing the GitHub app, creating the workflow, and
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
opencode:
@@ -135,3 +155,9 @@ Replace the image URL `https://github.com/user-attachments/assets/xxxxxxxx` with
```
MOCK_EVENT='{"eventName":"issue_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"issue":{"number":4,"pull_request":{}},"comment":{"id":1,"body":"hey opencode, summarize thread"}}}'
```
### PR review comment event
```
MOCK_EVENT='{"eventName":"pull_request_review_comment","repo":{"owner":"sst","repo":"hello-world"},"actor":"fwang","payload":{"pull_request":{"number":7},"comment":{"id":1,"body":"hey opencode, add error handling","path":"src/components/Button.tsx","diff_hunk":"@@ -45,8 +45,11 @@\n- const handleClick = () => {\n- console.log('clicked')\n+ const handleClick = useCallback(() => {\n+ console.log('clicked')\n+ doSomething()\n+ }, [doSomething])","line":47,"original_line":45,"position":10,"commit_id":"abc123","original_commit_id":"def456"}}}'
```
+44 -7
View File
@@ -5,7 +5,7 @@ import { graphql } from "@octokit/graphql"
import * as core from "@actions/core"
import * as github from "@actions/github"
import type { Context as GitHubContext } from "@actions/github/lib/context"
import type { IssueCommentEvent } from "@octokit/webhooks-types"
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { spawn } from "node:child_process"
@@ -124,7 +124,7 @@ let exitCode = 0
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
try {
assertContextEvent("issue_comment")
assertContextEvent("issue_comment", "pull_request_review_comment")
assertPayloadKeyword()
await assertOpencodeConnected()
@@ -241,19 +241,43 @@ function createOpencode() {
}
function assertPayloadKeyword() {
const payload = useContext().payload as IssueCommentEvent
const payload = useContext().payload as IssueCommentEvent | PullRequestReviewCommentEvent
const body = payload.comment.body.trim()
if (!body.match(/(?:^|\s)(?:\/opencode|\/oc)(?=$|\s)/)) {
throw new Error("Comments must mention `/opencode` or `/oc`")
}
}
function getReviewCommentContext() {
const context = useContext()
if (context.eventName !== "pull_request_review_comment") {
return null
}
const payload = context.payload as PullRequestReviewCommentEvent
return {
file: payload.comment.path,
diffHunk: payload.comment.diff_hunk,
line: payload.comment.line,
originalLine: payload.comment.original_line,
position: payload.comment.position,
commitId: payload.comment.commit_id,
originalCommitId: payload.comment.original_commit_id,
}
}
async function assertOpencodeConnected() {
let retry = 0
let connected = false
do {
try {
await client.app.get<true>()
await client.app.log<true>({
body: {
service: "github-workflow",
level: "info",
message: "Prepare to react to Github Workflow event",
},
})
connected = true
break
} catch (e) {}
@@ -383,11 +407,24 @@ async function createComment() {
}
async function getUserPrompt() {
const context = useContext()
const payload = context.payload as IssueCommentEvent | PullRequestReviewCommentEvent
const reviewContext = getReviewCommentContext()
let prompt = (() => {
const payload = useContext().payload as IssueCommentEvent
const body = payload.comment.body.trim()
if (body === "/opencode" || body === "/oc") return "Summarize this thread"
if (body.includes("/opencode") || body.includes("/oc")) return body
if (body === "/opencode" || body === "/oc") {
if (reviewContext) {
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
}
return "Summarize this thread"
}
if (body.includes("/opencode") || body.includes("/oc")) {
if (reviewContext) {
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
}
return body
}
throw new Error("Comments must mention `/opencode` or `/oc`")
})()
+10 -4
View File
@@ -97,8 +97,12 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
],
})
const ZEN_MODELS1 = new sst.Secret("ZEN_MODELS1")
const ZEN_MODELS2 = new sst.Secret("ZEN_MODELS2")
const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS1"),
new sst.Secret("ZEN_MODELS2"),
new sst.Secret("ZEN_MODELS3"),
new sst.Secret("ZEN_MODELS4"),
]
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!) },
@@ -112,6 +116,8 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
// CONSOLE
////////////////
const bucket = new sst.cloudflare.Bucket("ConsoleData")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
@@ -128,15 +134,15 @@ new sst.cloudflare.x.SolidStart("Console", {
domain,
path: "packages/console/app",
link: [
bucket,
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
STRIPE_SECRET_KEY,
ZEN_MODELS1,
ZEN_MODELS2,
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
AWS_SES_SECRET_ACCESS_KEY,
...ZEN_MODELS,
...($dev
? [
new sst.Secret("CLOUDFLARE_DEFAULT_ACCOUNT_ID", process.env.CLOUDFLARE_DEFAULT_ACCOUNT_ID!),
+17
View File
@@ -0,0 +1,17 @@
import { SECRET } from "./secret"
import { domain } from "./stage"
const storage = new sst.cloudflare.Bucket("EnterpriseStorage")
const enterprise = new sst.cloudflare.x.SolidStart("Enterprise", {
domain: "enterprise." + domain,
path: "packages/enterprise",
buildCommand: "bun run build:cloudflare",
environment: {
OPENCODE_STORAGE_ADAPTER: "r2",
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
OPENCODE_STORAGE_ACCESS_KEY_ID: SECRET.R2AccessKey.value,
OPENCODE_STORAGE_SECRET_ACCESS_KEY: SECRET.R2SecretKey.value,
OPENCODE_STORAGE_BUCKET: storage.name,
},
})
+4
View File
@@ -0,0 +1,4 @@
export const SECRET = {
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
}
+199 -28
View File
@@ -2,9 +2,8 @@
set -euo pipefail
APP=opencode
MUTED='\033[0;2m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
ORANGE='\033[38;2;255;140;0m'
NC='\033[0m' # No Color
@@ -12,39 +11,94 @@ requested_version=${VERSION:-}
raw_os=$(uname -s)
os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
# Normalize various Unix-like identifiers
case "$raw_os" in
Darwin*) os="darwin" ;;
Linux*) os="linux" ;;
MINGW*|MSYS*|CYGWIN*) os="windows" ;;
esac
arch=$(uname -m)
esac
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
elif [[ "$arch" == "x86_64" ]]; then
fi
if [[ "$arch" == "x86_64" ]]; then
arch="x64"
fi
filename="$APP-$os-$arch.zip"
if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
if [ "$rosetta_flag" = "1" ]; then
arch="arm64"
fi
fi
case "$filename" in
*"-linux-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
combo="$os-$arch"
case "$combo" in
linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
;;
*"-darwin-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-windows-"*)
[[ "$arch" == "x64" ]] || exit 1
;;
*)
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
exit 1
*)
echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
exit 1
;;
esac
archive_ext=".zip"
if [ "$os" = "linux" ]; then
archive_ext=".tar.gz"
fi
is_musl=false
if [ "$os" = "linux" ]; then
if [ -f /etc/alpine-release ]; then
is_musl=true
fi
if command -v ldd >/dev/null 2>&1; then
if ldd --version 2>&1 | grep -qi musl; then
is_musl=true
fi
fi
fi
needs_baseline=false
if [ "$arch" = "x64" ]; then
if [ "$os" = "linux" ]; then
if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
needs_baseline=true
fi
fi
if [ "$os" = "darwin" ]; then
avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
if [ "$avx2" != "1" ]; then
needs_baseline=true
fi
fi
fi
target="$os-$arch"
if [ "$needs_baseline" = "true" ]; then
target="$target-baseline"
fi
if [ "$is_musl" = "true" ]; then
target="$target-musl"
fi
filename="$APP-$target$archive_ext"
if [ "$os" = "linux" ]; then
if ! command -v tar >/dev/null 2>&1; then
echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
exit 1
fi
else
if ! command -v unzip >/dev/null 2>&1; then
echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
exit 1
fi
fi
INSTALL_DIR=$HOME/.opencode/bin
mkdir -p "$INSTALL_DIR"
@@ -67,8 +121,8 @@ print_message() {
local color=""
case $level in
info) color="${GREEN}" ;;
warning) color="${YELLOW}" ;;
info) color="${NC}" ;;
warning) color="${NC}" ;;
error) color="${RED}" ;;
esac
@@ -86,19 +140,119 @@ check_version() {
installed_version=$(echo $installed_version | awk '{print $2}')
if [[ "$installed_version" != "$specific_version" ]]; then
print_message info "Installed version: ${YELLOW}$installed_version."
print_message info "${MUTED}Installed version: ${NC}$installed_version."
else
print_message info "Version ${YELLOW}$specific_version${GREEN} already installed"
print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
exit 0
fi
fi
}
unbuffered_sed() {
if echo | sed -u -e "" >/dev/null 2>&1; then
sed -nu "$@"
elif echo | sed -l -e "" >/dev/null 2>&1; then
sed -nl "$@"
else
local pad="$(printf "\n%512s" "")"
sed -ne "s/$/\\${pad}/" "$@"
fi
}
print_progress() {
local bytes="$1"
local length="$2"
[ "$length" -gt 0 ] || return 0
local width=50
local percent=$(( bytes * 100 / length ))
[ "$percent" -gt 100 ] && percent=100
local on=$(( percent * width / 100 ))
local off=$(( width - on ))
local filled=$(printf "%*s" "$on" "")
filled=${filled// /■}
local empty=$(printf "%*s" "$off" "")
empty=${empty// /・}
printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
}
download_with_progress() {
local url="$1"
local output="$2"
if [ -t 2 ]; then
exec 4>&2
else
exec 4>/dev/null
fi
local tmp_dir=${TMPDIR:-/tmp}
local basename="${tmp_dir}/opencode_install_$$"
local tracefile="${basename}.trace"
rm -f "$tracefile"
mkfifo "$tracefile"
# Hide cursor
printf "\033[?25l" >&4
trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
(
curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
) &
local curl_pid=$!
unbuffered_sed \
-e 'y/ACDEGHLNORTV/acdeghlnortv/' \
-e '/^0000: content-length:/p' \
-e '/^<= recv data/p' \
"$tracefile" | \
{
local length=0
local bytes=0
while IFS=" " read -r -a line; do
[ "${#line[@]}" -lt 2 ] && continue
local tag="${line[0]} ${line[1]}"
if [ "$tag" = "0000: content-length:" ]; then
length="${line[2]}"
length=$(echo "$length" | tr -d '\r')
bytes=0
elif [ "$tag" = "<= recv" ]; then
local size="${line[3]}"
bytes=$(( bytes + size ))
if [ "$length" -gt 0 ]; then
print_progress "$bytes" "$length"
fi
fi
done
}
wait $curl_pid
local ret=$?
echo "" >&4
return $ret
}
download_and_install() {
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
mkdir -p opencodetmp && cd opencodetmp
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
# Fallback to standard curl on Windows or if custom progress fails
curl -# -L -o "$filename" "$url"
fi
if [ "$os" = "linux" ]; then
tar -xzf "$filename"
else
unzip -q "$filename"
fi
mv opencode "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
cd .. && rm -rf opencodetmp
@@ -117,7 +271,7 @@ add_to_path() {
elif [[ -w $config_file ]]; then
echo -e "\n# opencode" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
else
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " $command"
@@ -191,3 +345,20 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
echo "$INSTALL_DIR" >> $GITHUB_PATH
print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
fi
echo -e ""
echo -e "${MUTED}  ${NC} ▄ "
echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
echo -e ""
echo -e ""
echo -e "${MUTED}To get started, navigate to a project and run:${NC}"
echo -e "opencode ${MUTED}Use free models${NC}"
echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}"
echo -e "opencode help ${MUTED}List commands and options${NC}"
echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""
echo -e ""
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bun
import solidPlugin from "./node_modules/@opentui/solid/scripts/solid-plugin"
import path from "path"
import fs from "fs"
const dir = process.cwd()
const parser = fs.realpathSync(path.join(dir, "node_modules/@opentui/core/parser.worker.js"))
const worker = "./src/cli/cmd/tui/worker.ts"
const version = process.env.OPENCODE_VERSION ?? "local"
const channel = process.env.OPENCODE_CHANNEL ?? "local"
fs.rmSync(path.join(dir, "dist"), { recursive: true, force: true })
const result = await Bun.build({
entrypoints: ["./src/index.ts", worker, parser],
outdir: "./dist",
target: "bun",
sourcemap: "none",
tsconfig: "./tsconfig.json",
plugins: [solidPlugin],
external: ["@opentui/core"],
define: {
OPENCODE_VERSION: `'${version}'`,
OPENCODE_CHANNEL: `'${channel}'`,
// Leave undefined so runtime picks bundled/dist worker or fallback in code.
OPENCODE_WORKER_PATH: "undefined",
OTUI_TREE_SITTER_WORKER_PATH: 'new URL("./cli/cmd/tui/parser.worker.js", import.meta.url).href',
},
})
if (!result.success) {
console.error("bundle failed")
for (const log of result.logs) console.error(log)
process.exit(1)
}
const parserOut = path.join(dir, "dist/src/cli/cmd/tui/parser.worker.js")
fs.mkdirSync(path.dirname(parserOut), { recursive: true })
await Bun.write(parserOut, Bun.file(parser))
+3
View File
@@ -0,0 +1,3 @@
{
"nodeModules": "sha256-+PJZG5jNxBGkxblpnNa4lvfBi9YEvHaGQRE0+avNwHY="
}
+52
View File
@@ -0,0 +1,52 @@
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
args:
stdenvNoCC.mkDerivation {
pname = "opencode-node_modules";
version = args.version;
src = args.src;
impureEnvVars =
lib.fetchers.proxyImpureEnvVars
++ [
"GIT_PROXY_COMMAND"
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun cacert curl ];
dontConfigure = true;
buildPhase = ''
runHook preBuild
export HOME=$(mktemp -d)
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
bun install \
--cpu="*" \
--os="*" \
--frozen-lockfile \
--ignore-scripts \
--no-progress \
--linker=isolated
bun --bun ${args.canonicalizeScript}
bun --bun ${args.normalizeBinsScript}
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
while IFS= read -r dir; do
rel="''${dir#./}"
dest="$out/$rel"
mkdir -p "$(dirname "$dest")"
cp -R "$dir" "$dest"
done < <(find . -type d -name node_modules -prune | sort)
runHook postInstall
'';
dontFixup = true;
outputHashAlgo = "sha256";
outputHashMode = "recursive";
outputHash = hash;
}
+135
View File
@@ -0,0 +1,135 @@
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
args:
let
scripts = args.scripts;
mkModules =
attrs:
args.mkNodeModules (
attrs
// {
canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
}
);
in
stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode";
version = args.version;
src = args.src;
node_modules = mkModules {
version = finalAttrs.version;
src = finalAttrs.src;
};
nativeBuildInputs = [
bun
makeBinaryWrapper
];
env.MODELS_DEV_API_JSON = args.modelsDev;
env.OPENCODE_VERSION = args.version;
env.OPENCODE_CHANNEL = "stable";
dontConfigure = true;
buildPhase = ''
runHook preBuild
cp -r ${finalAttrs.node_modules}/node_modules .
cp -r ${finalAttrs.node_modules}/packages .
(
cd packages/opencode
chmod -R u+w ./node_modules
mkdir -p ./node_modules/@opencode-ai
rm -f ./node_modules/@opencode-ai/{script,sdk,plugin}
ln -s $(pwd)/../../packages/script ./node_modules/@opencode-ai/script
ln -s $(pwd)/../../packages/sdk/js ./node_modules/@opencode-ai/sdk
ln -s $(pwd)/../../packages/plugin ./node_modules/@opencode-ai/plugin
cp ${./bundle.ts} ./bundle.ts
chmod +x ./bundle.ts
bun run ./bundle.ts
)
runHook postBuild
'';
installPhase = ''
runHook preInstall
cd packages/opencode
if [ ! -d dist ]; then
echo "ERROR: dist directory missing after bundle step"
exit 1
fi
mkdir -p $out/lib/opencode
cp -r dist $out/lib/opencode/
chmod -R u+w $out/lib/opencode/dist
# Select bundled worker assets deterministically (sorted find output)
worker_file=$(find "$out/lib/opencode/dist" -type f \( -path '*/tui/worker.*' -o -name 'worker.*' \) | sort | head -n1)
parser_worker_file=$(find "$out/lib/opencode/dist" -type f -name 'parser.worker.*' | sort | head -n1)
if [ -z "$worker_file" ]; then
echo "ERROR: bundled worker not found"
exit 1
fi
main_wasm=$(printf '%s\n' "$out"/lib/opencode/dist/tree-sitter-*.wasm | sort | head -n1)
wasm_list=$(find "$out/lib/opencode/dist" -maxdepth 1 -name 'tree-sitter-*.wasm' -print)
for patch_file in "$worker_file" "$parser_worker_file"; do
[ -z "$patch_file" ] && continue
[ ! -f "$patch_file" ] && continue
if [ -n "$wasm_list" ] && grep -q 'tree-sitter' "$patch_file"; then
# Rewrite wasm references to absolute store paths to avoid runtime resolve failures.
bun --bun ${scripts + "/patch-wasm.ts"} "$patch_file" "$main_wasm" $wasm_list
fi
done
mkdir -p $out/lib/opencode/node_modules
cp -r ../../node_modules/.bun $out/lib/opencode/node_modules/
mkdir -p $out/lib/opencode/node_modules/@opentui
mkdir -p $out/bin
makeWrapper ${bun}/bin/bun $out/bin/opencode \
--add-flags "run" \
--add-flags "$out/lib/opencode/dist/src/index.js" \
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
--argv0 opencode
runHook postInstall
'';
postInstall = ''
for pkg in $out/lib/opencode/node_modules/.bun/@opentui+core-* $out/lib/opencode/node_modules/.bun/@opentui+solid-* $out/lib/opencode/node_modules/.bun/@opentui+core@* $out/lib/opencode/node_modules/.bun/@opentui+solid@*; do
if [ -d "$pkg" ]; then
pkgName=$(basename "$pkg" | sed 's/@opentui+\([^@]*\)@.*/\1/')
ln -sf ../.bun/$(basename "$pkg")/node_modules/@opentui/$pkgName \
$out/lib/opencode/node_modules/@opentui/$pkgName
fi
done
'';
dontFixup = true;
meta = {
description = "AI coding agent built for the terminal";
longDescription = ''
OpenCode is a terminal-based agent that can build anything.
It combines a TypeScript/JavaScript core with a Go-based TUI
to provide an interactive AI coding experience.
'';
homepage = "https://github.com/sst/opencode";
license = lib.licenses.mit;
platforms = [
"aarch64-linux"
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
mainProgram = "opencode";
};
})
+115
View File
@@ -0,0 +1,115 @@
import solidPlugin from "./packages/opencode/node_modules/@opentui/solid/scripts/solid-plugin"
import path from "path"
import fs from "fs"
const version = "@VERSION@"
const pkg = path.join(process.cwd(), "packages/opencode")
const parser = fs.realpathSync(path.join(pkg, "./node_modules/@opentui/core/parser.worker.js"))
const worker = "./src/cli/cmd/tui/worker.ts"
const target = process.env["BUN_COMPILE_TARGET"]
if (!target) {
throw new Error("BUN_COMPILE_TARGET not set")
}
process.chdir(pkg)
const manifestName = "opencode-assets.manifest"
const manifestPath = path.join(pkg, manifestName)
const readTrackedAssets = () => {
if (!fs.existsSync(manifestPath)) return []
return fs
.readFileSync(manifestPath, "utf8")
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0)
}
const removeTrackedAssets = () => {
for (const file of readTrackedAssets()) {
const filePath = path.join(pkg, file)
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { force: true })
}
}
}
const assets = new Set<string>()
const addAsset = async (p: string) => {
const file = path.basename(p)
const dest = path.join(pkg, file)
await Bun.write(dest, Bun.file(p))
assets.add(file)
}
removeTrackedAssets()
const result = await Bun.build({
conditions: ["browser"],
tsconfig: "./tsconfig.json",
plugins: [solidPlugin],
sourcemap: "external",
entrypoints: ["./src/index.ts", parser, worker],
define: {
OPENCODE_VERSION: `'@VERSION@'`,
OTUI_TREE_SITTER_WORKER_PATH: "/$bunfs/root/" + path.relative(pkg, parser).replace(/\\/g, "/"),
OPENCODE_CHANNEL: "'latest'",
},
compile: {
target,
outfile: "opencode",
execArgv: ["--user-agent=opencode/" + version, '--env-file=""', "--"],
windows: {},
},
})
if (!result.success) {
console.error("Build failed!")
for (const log of result.logs) {
console.error(log)
}
throw new Error("Compilation failed")
}
const assetOutputs = result.outputs?.filter((x) => x.kind === "asset") ?? []
for (const x of assetOutputs) {
await addAsset(x.path)
}
const bundle = await Bun.build({
entrypoints: [worker],
tsconfig: "./tsconfig.json",
plugins: [solidPlugin],
target: "bun",
outdir: "./.opencode-worker",
sourcemap: "none",
})
if (!bundle.success) {
console.error("Worker build failed!")
for (const log of bundle.logs) {
console.error(log)
}
throw new Error("Worker compilation failed")
}
const workerAssets = bundle.outputs?.filter((x) => x.kind === "asset") ?? []
for (const x of workerAssets) {
await addAsset(x.path)
}
const output = bundle.outputs.find((x) => x.kind === "entry-point")
if (!output) {
throw new Error("Worker build produced no entry-point output")
}
const dest = path.join(pkg, "opencode-worker.js")
await Bun.write(dest, Bun.file(output.path))
fs.rmSync(path.dirname(output.path), { recursive: true, force: true })
const list = Array.from(assets)
await Bun.write(manifestPath, list.length > 0 ? list.join("\n") + "\n" : "")
console.log("Build successful!")
+113
View File
@@ -0,0 +1,113 @@
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
import { join, relative } from "path"
type SemverLike = {
valid: (value: string) => string | null
rcompare: (left: string, right: string) => number
}
type Entry = {
dir: string
version: string
label: string
}
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const linkRoot = join(bunRoot, "node_modules")
const directories = (await readdir(bunRoot)).sort()
const versions = new Map<string, Entry[]>()
for (const entry of directories) {
const full = join(bunRoot, entry)
const info = await lstat(full)
if (!info.isDirectory()) {
continue
}
const parsed = parseEntry(entry)
if (!parsed) {
continue
}
const list = versions.get(parsed.name) ?? []
list.push({ dir: full, version: parsed.version, label: entry })
versions.set(parsed.name, list)
}
const semverModule = (await import(join(bunRoot, "node_modules/semver"))) as
| SemverLike
| {
default: SemverLike
}
const semver = "default" in semverModule ? semverModule.default : semverModule
const selections = new Map<string, Entry>()
for (const [slug, list] of versions) {
list.sort((a, b) => {
const left = semver.valid(a.version)
const right = semver.valid(b.version)
if (left && right) {
const delta = semver.rcompare(left, right)
if (delta !== 0) {
return delta
}
}
if (left && !right) {
return -1
}
if (!left && right) {
return 1
}
return b.version.localeCompare(a.version)
})
selections.set(slug, list[0])
}
await rm(linkRoot, { recursive: true, force: true })
await mkdir(linkRoot, { recursive: true })
const rewrites: string[] = []
for (const [slug, entry] of Array.from(selections.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
const parts = slug.split("/")
const leaf = parts.pop()
if (!leaf) {
continue
}
const parent = join(linkRoot, ...parts)
await mkdir(parent, { recursive: true })
const linkPath = join(parent, leaf)
const desired = join(entry.dir, "node_modules", slug)
const exists = await lstat(desired)
.then((info) => info.isDirectory())
.catch(() => false)
if (!exists) {
continue
}
const relativeTarget = relative(parent, desired)
const resolved = relativeTarget.length === 0 ? "." : relativeTarget
await rm(linkPath, { recursive: true, force: true })
await symlink(resolved, linkPath)
rewrites.push(slug + " -> " + resolved)
}
rewrites.sort()
console.log("[canonicalize-node-modules] rebuilt", rewrites.length, "links")
for (const line of rewrites.slice(0, 20)) {
console.log(" ", line)
}
if (rewrites.length > 20) {
console.log(" ...")
}
function parseEntry(label: string) {
const marker = label.startsWith("@") ? label.indexOf("@", 1) : label.indexOf("@")
if (marker <= 0) {
return null
}
const name = label.slice(0, marker).replace(/\+/g, "/")
const version = label.slice(marker + 1)
if (!name || !version) {
return null
}
return { name, version }
}
+138
View File
@@ -0,0 +1,138 @@
import { lstat, mkdir, readdir, rm, symlink } from "fs/promises"
import { join, relative } from "path"
type PackageManifest = {
name?: string
bin?: string | Record<string, string>
}
const root = process.cwd()
const bunRoot = join(root, "node_modules/.bun")
const bunEntries = (await safeReadDir(bunRoot)).sort()
let rewritten = 0
for (const entry of bunEntries) {
const modulesRoot = join(bunRoot, entry, "node_modules")
if (!(await exists(modulesRoot))) {
continue
}
const binRoot = join(modulesRoot, ".bin")
await rm(binRoot, { recursive: true, force: true })
await mkdir(binRoot, { recursive: true })
const packageDirs = await collectPackages(modulesRoot)
for (const packageDir of packageDirs) {
const manifest = await readManifest(packageDir)
if (!manifest) {
continue
}
const binField = manifest.bin
if (!binField) {
continue
}
const seen = new Set<string>()
if (typeof binField === "string") {
const fallback = manifest.name ?? packageDir.split("/").pop()
if (fallback) {
await linkBinary(binRoot, fallback, packageDir, binField, seen)
}
} else {
const entries = Object.entries(binField).sort((a, b) => a[0].localeCompare(b[0]))
for (const [name, target] of entries) {
await linkBinary(binRoot, name, packageDir, target, seen)
}
}
}
}
console.log(`[normalize-bun-binaries] rewrote ${rewritten} links`)
async function collectPackages(modulesRoot: string) {
const found: string[] = []
const topLevel = (await safeReadDir(modulesRoot)).sort()
for (const name of topLevel) {
if (name === ".bin" || name === ".bun") {
continue
}
const full = join(modulesRoot, name)
if (!(await isDirectory(full))) {
continue
}
if (name.startsWith("@")) {
const scoped = (await safeReadDir(full)).sort()
for (const child of scoped) {
const scopedDir = join(full, child)
if (await isDirectory(scopedDir)) {
found.push(scopedDir)
}
}
continue
}
found.push(full)
}
return found.sort()
}
async function readManifest(dir: string) {
const file = Bun.file(join(dir, "package.json"))
if (!(await file.exists())) {
return null
}
const data = (await file.json()) as PackageManifest
return data
}
async function linkBinary(binRoot: string, name: string, packageDir: string, target: string, seen: Set<string>) {
if (!name || !target) {
return
}
const normalizedName = normalizeBinName(name)
if (seen.has(normalizedName)) {
return
}
const resolved = join(packageDir, target)
const script = Bun.file(resolved)
if (!(await script.exists())) {
return
}
seen.add(normalizedName)
const destination = join(binRoot, normalizedName)
const relativeTarget = relative(binRoot, resolved) || "."
await rm(destination, { force: true })
await symlink(relativeTarget, destination)
rewritten++
}
async function exists(path: string) {
try {
await lstat(path)
return true
} catch {
return false
}
}
async function isDirectory(path: string) {
try {
const info = await lstat(path)
return info.isDirectory()
} catch {
return false
}
}
async function safeReadDir(path: string) {
try {
return await readdir(path)
} catch {
return []
}
}
function normalizeBinName(name: string) {
const slash = name.lastIndexOf("/")
if (slash >= 0) {
return name.slice(slash + 1)
}
return name
}
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bun
import fs from "fs"
import path from "path"
/**
* Rewrite tree-sitter wasm references inside a JS file to absolute paths.
* argv: [node, script, file, mainWasm, ...wasmPaths]
*/
const [, , file, mainWasm, ...wasmPaths] = process.argv
if (!file || !mainWasm) {
console.error("usage: patch-wasm <file> <mainWasm> [wasmPaths...]")
process.exit(1)
}
const content = fs.readFileSync(file, "utf8")
const byName = new Map<string, string>()
for (const wasm of wasmPaths) {
const name = path.basename(wasm)
byName.set(name, wasm)
}
let next = content
for (const [name, wasmPath] of byName) {
next = next.replaceAll(name, wasmPath)
}
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
next = next.replace(/(\.\/)+/g, "./")
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
if (next !== content) fs.writeFileSync(file, next)
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
SYSTEM=${SYSTEM:-x86_64-linux}
DEFAULT_HASH_FILE=${MODULES_HASH_FILE:-nix/hashes.json}
HASH_FILE=${HASH_FILE:-$DEFAULT_HASH_FILE}
if [ ! -f "$HASH_FILE" ]; then
cat >"$HASH_FILE" <<EOF
{
"nodeModules": "$DUMMY"
}
EOF
fi
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if ! git ls-files --error-unmatch "$HASH_FILE" >/dev/null 2>&1; then
git add -N "$HASH_FILE" >/dev/null 2>&1 || true
fi
fi
export DUMMY
export NIX_KEEP_OUTPUTS=1
export NIX_KEEP_DERIVATIONS=1
cleanup() {
rm -f "${JSON_OUTPUT:-}" "${BUILD_LOG:-}" "${TMP_EXPR:-}"
}
trap cleanup EXIT
write_node_modules_hash() {
local value="$1"
local temp
temp=$(mktemp)
jq --arg value "$value" '.nodeModules = $value' "$HASH_FILE" >"$temp"
mv "$temp" "$HASH_FILE"
}
TARGET="packages.${SYSTEM}.default"
MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules"
CORRECT_HASH=""
DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")"
echo "Setting dummy node_modules outputHash for ${SYSTEM}..."
write_node_modules_hash "$DUMMY"
BUILD_LOG=$(mktemp)
JSON_OUTPUT=$(mktemp)
echo "Building node_modules for ${SYSTEM} to discover correct outputHash..."
echo "Attempting to realize derivation: ${DRV_PATH}"
REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true)
BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true)
if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then
echo "Realized node_modules output: $BUILD_PATH"
CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true)
fi
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)"
if [ -z "$CORRECT_HASH" ]; then
CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)"
fi
if [ -z "$CORRECT_HASH" ]; then
echo "Searching for kept failed build directory..."
KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1)
if [ -z "$KEPT_DIR" ]; then
KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1)
fi
if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then
echo "Found kept build directory: $KEPT_DIR"
if [ -d "$KEPT_DIR/build" ]; then
HASH_PATH="$KEPT_DIR/build"
else
HASH_PATH="$KEPT_DIR"
fi
echo "Attempting to hash: $HASH_PATH"
ls -la "$HASH_PATH" || true
if [ -d "$HASH_PATH/node_modules" ]; then
CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true)
echo "Computed hash from kept build: $CORRECT_HASH"
fi
fi
fi
fi
if [ -z "$CORRECT_HASH" ]; then
echo "Failed to determine correct node_modules hash for ${SYSTEM}."
echo "Build log:"
cat "$BUILD_LOG"
exit 1
fi
write_node_modules_hash "$CORRECT_HASH"
jq -e --arg hash "$CORRECT_HASH" '.nodeModules == $hash' "$HASH_FILE" >/dev/null
echo "node_modules hash updated for ${SYSTEM}: $CORRECT_HASH"
rm -f "$BUILD_LOG"
unset BUILD_LOG
+16 -12
View File
@@ -4,12 +4,13 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.2",
"packageManager": "bun@1.3.3",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
"prepare": "husky",
"random": "echo 'Random script'"
"random": "echo 'Random script'",
"hello": "echo 'Hello World!'"
},
"workspaces": {
"packages": [
@@ -19,33 +20,37 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.0",
"@types/bun": "1.3.3",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/precision-diffs": "0.4.4",
"@solidjs/meta": "0.29.4",
"@pierre/precision-diffs": "0.5.7",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.8",
"ai": "5.0.97",
"hono": "4.7.10",
"hono-openapi": "1.1.1",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-js": "1.9.9",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.42.3",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8"
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
"solid-js": "1.9.10",
"vite-plugin-solid": "2.11.10"
}
},
"devDependencies": {
@@ -56,8 +61,10 @@
"turbo": "2.5.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*"
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:"
},
"repository": {
"type": "git",
@@ -76,9 +83,6 @@
"tree-sitter-bash",
"web-tree-sitter"
],
"patchedDependencies": {
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
},
"overrides": {
"@types/bun": "catalog:",
"@types/node": "catalog:"
-1
View File
@@ -3,7 +3,6 @@ dist
.output
.vercel
.netlify
.vinxi
app.config.timestamp_*.js
# Environment
-23
View File
@@ -1,23 +0,0 @@
import { defineConfig } from "@solidjs/start/config"
export default defineConfig({
middleware: "./src/middleware.ts",
vite: {
server: {
allowedHosts: true,
},
build: {
rollupOptions: {
external: ["cloudflare:workers"],
},
minify: false,
},
},
server: {
compatibilityDate: "2024-09-19",
preset: "cloudflare_module",
cloudflare: {
nodeCompat: true,
},
},
})
+17 -12
View File
@@ -1,32 +1,37 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.122",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"dev": "vite dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.65"
"build": "./script/generate-sitemap.ts && vite build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vite start"
},
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
"@jsx-email/render": "1.1.1",
"@kobalte/core": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/console-core": "workspace:*",
"@opencode-ai/console-mail": "workspace:*",
"@openauthjs/openauth": "catalog:",
"@kobalte/core": "catalog:",
"@jsx-email/render": "1.1.1",
"@opencode-ai/console-resource": "workspace:*",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"vinxi": "^0.5.7",
"vite": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
"wrangler": "4.50.0"
},
"engines": {
"node": ">=22"
+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/apple-touch-icon.png
+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/favicon-96x96.png
@@ -1,23 +0,0 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="400" fill="#FDFCFC"/>
<path d="M96 122.001V70.001H148V122.001H96Z" fill="#17181C"/>
<path d="M148.004 122.001V70.001H200.004V122.001H148.004Z" fill="#17181C"/>
<path d="M200.008 122.001V70.001H252.008V122.001H200.008Z" fill="#17181C"/>
<path d="M251.996 122.001V70.001H303.996V122.001H251.996Z" fill="#17181C"/>
<path d="M251.996 173.988V121.988H303.996V173.988H251.996Z" fill="#17181C"/>
<path d="M96 225.998V173.998H148V225.998H96Z" fill="#CFCECD"/>
<rect width="52" height="52" transform="translate(148.004 173.998)" fill="#17181C"/>
<path d="M148.004 225.998V173.998H200.004V225.998H148.004Z" fill="#17181C" fill-opacity="0.1"/>
<path d="M200.008 225.998V173.998H252.008V225.998H200.008Z" fill="#17181C"/>
<path d="M252.016 225.998V173.998H304.016V225.998H252.016Z" fill="#CFCECD"/>
<rect width="52" height="52" transform="translate(96 226.002)" fill="#17181C"/>
<path d="M96 278.002V226.002H148V278.002H96Z" fill="#17181C" fill-opacity="0.1"/>
<rect width="52" height="52" transform="translate(148.004 226.002)" fill="white"/>
<path d="M148.004 278.002V226.002H200.004V278.002H148.004Z" fill="#CFCECD"/>
<path d="M200.008 278.002V226.002H252.008V278.002H200.008Z" fill="#CFCECD"/>
<path d="M252.016 278.002V226.002H304.016V278.002H252.016Z" fill="#CFCECD"/>
<path d="M96 330.012V278.012H148V330.012H96Z" fill="#17181C"/>
<path d="M148.004 330.012V278.012H200.004V330.012H148.004Z" fill="#17181C"/>
<path d="M200.008 329.99V277.99H252.008V329.99H200.008Z" fill="#17181C"/>
<path d="M251.996 330.012V278.012H303.996V330.012H251.996Z" fill="#17181C"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/favicon.ico
-4
View File
@@ -1,4 +0,0 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="400" fill="#0E0E0E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M312 340H88V60H312V340ZM256 116H144V284H256V116Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 42 B

+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/favicon.svg

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 42 B

+2 -1
View File
@@ -2,4 +2,5 @@ User-agent: *
Allow: /
# Disallow shared content pages
Disallow: /s/
Disallow: /s/
Disallow: /share/
+1
View File
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/site.webmanifest
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/web-app-manifest-192x192.png
@@ -0,0 +1 @@
../../../ui/src/assets/favicon/web-app-manifest-512x512.png
+3 -1
View File
@@ -1,7 +1,8 @@
import { MetaProvider, Title, Meta } from "@solidjs/meta"
import { Router } from "@solidjs/router"
import { FileRoutes } from "@solidjs/start/router"
import { ErrorBoundary, Suspense } from "solid-js"
import { Suspense } from "solid-js"
import { Favicon } from "@opencode-ai/ui/favicon"
import "@ibm/plex/css/ibm-plex.css"
import "./app.css"
@@ -13,6 +14,7 @@ export default function App() {
<MetaProvider>
<Title>opencode</Title>
<Meta name="description" content="OpenCode - The AI coding agent built for the terminal." />
<Favicon />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
@@ -202,6 +202,14 @@ export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
)
}
export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 50 50" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
</svg>
)
}
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
@@ -212,3 +220,30 @@ export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg>
)
}
export function IconChevronLeft(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 20 20" fill="none">
<path d="M12 15L7 10L12 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
</svg>
)
}
export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 20 20" fill="none">
<path d="M8 5L13 10L8 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
</svg>
)
}
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
)
}
+2 -2
View File
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "250",
commits: "3,500",
contributors: "300",
commits: "4,000",
monthlyUsers: "300,000",
},
} as const
@@ -1,4 +1,4 @@
import { useSession } from "vinxi/http"
import { useSession } from "@solidjs/start/http"
export interface AuthSession {
account?: Record<
@@ -9,7 +9,6 @@ export default createHandler(
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
{assets}
+4
View File
@@ -1 +1,5 @@
/// <reference types="@solidjs/start/env" />
export declare module "@solidjs/start/server" {
export type APIEvent = { request: Request }
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineMiddleware } from "vinxi/http"
import { createMiddleware } from "@solidjs/start/middleware"
export default defineMiddleware({
export default createMiddleware({
onBeforeResponse() {},
})
@@ -36,6 +36,7 @@ ${body.email}`.trim()
to: "contact@anoma.ly",
subject: `Enterprise Inquiry from ${body.name}`,
body: emailContent,
replyTo: body.email,
})
return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
@@ -19,6 +19,7 @@ export async function GET(input: APIEvent) {
return {
...value,
account: {
...value.account,
[id]: {
id,
email: decoded.subject.properties.email,
@@ -0,0 +1,17 @@
import { redirect } from "@solidjs/router"
import { APIEvent } from "@solidjs/start"
import { useAuthSession } from "~/context/auth.session"
export async function GET(event: APIEvent) {
const auth = await useAuthSession()
const current = auth.data.current
if (current)
await auth.update((val) => {
delete val.account?.[current]
const first = Object.keys(val.account ?? {})[0]
val.current = first
event!.locals.actor = undefined
return val
})
return redirect("/zen")
}
@@ -0,0 +1,7 @@
import { APIEvent } from "@solidjs/start"
import { useAuthSession } from "~/context/auth.session"
export async function GET(input: APIEvent) {
const session = await useAuthSession()
return Response.json(session.data)
}
+2 -3
View File
@@ -1,6 +1,6 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
import { HttpHeader } from "@solidjs/start"
// import { HttpHeader } from "@solidjs/start"
import video from "../asset/lander/opencode-min.mp4"
import videoPoster from "../asset/lander/opencode-poster.png"
import { IconCopy, IconCheck } from "../component/icon"
@@ -42,10 +42,9 @@ export default function Home() {
return (
<main data-page="opencode">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>OpenCode | The AI coding agent built for the terminal</Title>
<Link rel="canonical" href={config.baseUrl} />
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<Meta property="og:image" content="/social-share.png" />
<Meta name="twitter:image" content="/social-share.png" />
<div data-component="container">
@@ -12,6 +12,7 @@
[data-slot="item"] {
color: var(--color-danger);
text-decoration: none;
}
}
}
@@ -1,4 +1,4 @@
import { action, redirect } from "@solidjs/router"
import { action } from "@solidjs/router"
import { getRequestEvent } from "solid-js/web"
import { useAuthSession } from "~/context/auth.session"
import { Dropdown } from "~/component/dropdown"
@@ -17,18 +17,15 @@ const logout = action(async () => {
event!.locals.actor = undefined
return val
})
throw redirect("/zen")
})
}, "auth.logout")
export function UserMenu(props: { email: string | null | undefined }) {
return (
<div data-component="user-menu">
<Dropdown trigger={props.email ?? ""} align="right">
<form action={logout} method="post">
<button type="submit" formaction={logout} data-slot="item">
Logout
</button>
</form>
<a href="/auth/logout" data-slot="item">
Logout
</a>
</Dropdown>
</div>
)
@@ -6,7 +6,6 @@ import { UserMenu } from "./user-menu"
import { withActor } from "~/context/auth.withActor"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Link } from "@solidjs/meta"
const getUserEmail = query(async (workspaceID: string) => {
"use server"
@@ -22,7 +21,6 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
const userEmail = createAsync(() => getUserEmail(params.id!))
return (
<main data-page="workspace">
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<header data-component="workspace-header">
<div data-slot="header-brand">
<A href="/" data-component="site-title">
@@ -0,0 +1,145 @@
.root {
[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);
height: 400px;
display: flex;
align-items: center;
justify-content: center;
p {
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
[data-slot="filter-container"] {
margin-bottom: 0;
display: flex;
align-items: center;
gap: var(--space-3);
[data-component="dropdown"] {
[data-slot="trigger"] {
border: 1px solid var(--color-border);
background-color: var(--color-bg);
padding: var(--space-2) var(--space-3);
border-radius: var(--border-radius-sm);
color: var(--color-text);
font-size: var(--font-size-sm);
line-height: 1.5;
&:hover {
border-color: var(--color-accent);
}
&:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-alpha);
}
}
[data-slot="chevron"] {
opacity: 0.6;
}
[data-slot="dropdown"] {
min-width: 200px;
max-height: 300px;
overflow-y: auto;
padding: var(--space-1);
}
}
}
[data-slot="month-picker"] {
display: flex;
align-items: center;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
padding: 0;
}
[data-slot="month-button"] {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none !important;
color: var(--color-text);
cursor: pointer;
padding: var(--space-2) var(--space-3);
border-radius: var(--border-radius-xs);
transition: background-color 0.2s;
line-height: 1;
&:hover {
background-color: var(--color-bg-hover);
}
svg {
display: block;
width: 16px;
height: 16px;
stroke-width: 2;
}
}
[data-slot="month-label"] {
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--color-text);
line-height: 1.5;
min-width: 140px;
text-align: center;
white-space: nowrap;
}
[data-slot="model-item"] {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition: background-color 0.2s;
font-size: var(--font-size-sm);
color: var(--color-text);
border: none !important;
background: none;
width: 100%;
text-align: left;
white-space: nowrap;
&:hover {
background: var(--color-bg-hover);
}
span {
flex: 1;
user-select: none;
}
}
[data-slot="chart-container"] {
padding: var(--space-6);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
height: 400px;
}
@media (max-width: 40rem) {
[data-slot="chart-container"] {
height: 300px;
padding: var(--space-4);
}
[data-component="empty-state"] {
height: 300px;
}
}
}
@@ -0,0 +1,423 @@
import { and, Database, eq, gte, inArray, isNull, lte, or, sql, sum } from "@opencode-ai/console-core/drizzle/index.js"
import { UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createEffect, createMemo, onCleanup, Show, For } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Dropdown } from "~/component/dropdown"
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
import styles from "./graph-section.module.css"
import {
Chart,
BarController,
BarElement,
CategoryScale,
LinearScale,
Tooltip,
Legend,
type ChartConfiguration,
} from "chart.js"
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend)
async function getCosts(workspaceID: string, year: number, month: number) {
"use server"
return withActor(async () => {
const startDate = new Date(year, month, 1)
const endDate = new Date(year, month + 1, 0)
// First query: get usage data without joining keys
const usageData = await Database.use((tx) =>
tx
.select({
date: sql<string>`DATE(${UsageTable.timeCreated})`,
model: UsageTable.model,
totalCost: sum(UsageTable.cost),
keyId: UsageTable.keyID,
})
.from(UsageTable)
.where(
and(
eq(UsageTable.workspaceID, workspaceID),
gte(UsageTable.timeCreated, startDate),
lte(UsageTable.timeCreated, endDate),
),
)
.groupBy(sql`DATE(${UsageTable.timeCreated})`, UsageTable.model, UsageTable.keyID)
.then((x) =>
x.map((r) => ({
...r,
totalCost: r.totalCost ? parseInt(r.totalCost) : 0,
})),
),
)
// Get unique key IDs from usage
const usageKeyIds = new Set(usageData.map((r) => r.keyId).filter((id) => id !== null))
// Second query: get all existing keys plus any keys from usage
const keysData = await Database.use((tx) =>
tx
.select({
keyId: KeyTable.id,
keyName: KeyTable.name,
userEmail: AuthTable.subject,
timeDeleted: KeyTable.timeDeleted,
})
.from(KeyTable)
.innerJoin(UserTable, and(eq(KeyTable.userID, UserTable.id), eq(KeyTable.workspaceID, UserTable.workspaceID)))
.innerJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
.where(
and(
eq(KeyTable.workspaceID, workspaceID),
usageKeyIds.size > 0
? or(inArray(KeyTable.id, Array.from(usageKeyIds)), isNull(KeyTable.timeDeleted))
: isNull(KeyTable.timeDeleted),
),
)
.orderBy(AuthTable.subject, KeyTable.name),
)
return {
usage: usageData,
keys: keysData.map((key) => ({
id: key.keyId,
displayName:
key.timeDeleted !== null
? `${key.userEmail} - ${key.keyName} (deleted)`
: `${key.userEmail} - ${key.keyName}`,
})),
}
}, workspaceID)
}
const queryCosts = query(getCosts, "costs.get")
const MODEL_COLORS: Record<string, string> = {
"claude-sonnet-4-5": "#D4745C",
"claude-sonnet-4": "#E8B4A4",
"claude-opus-4": "#C8A098",
"claude-haiku-4-5": "#F0D8D0",
"claude-3-5-haiku": "#F8E8E0",
"gpt-5.1": "#4A90E2",
"gpt-5.1-codex": "#6BA8F0",
"gpt-5": "#7DB8F8",
"gpt-5-codex": "#9FCAFF",
"gpt-5-nano": "#B8D8FF",
"grok-code": "#8B5CF6",
"big-pickle": "#10B981",
"kimi-k2": "#F59E0B",
"qwen3-coder": "#EC4899",
"glm-4.6": "#14B8A6",
}
function getModelColor(model: string): string {
if (MODEL_COLORS[model]) return MODEL_COLORS[model]
const hash = model.split("").reduce((acc, char) => char.charCodeAt(0) + ((acc << 5) - acc), 0)
const hue = Math.abs(hash) % 360
return `hsl(${hue}, 50%, 65%)`
}
function formatDateLabel(dateStr: string): string {
const date = new Date()
const [y, m, d] = dateStr.split("-").map(Number)
date.setFullYear(y)
date.setMonth(m - 1)
date.setDate(d)
date.setHours(0, 0, 0, 0)
const month = date.toLocaleDateString("en-US", { month: "short" })
const day = date.getUTCDate().toString().padStart(2, "0")
return `${month} ${day}`
}
function addOpacityToColor(color: string, opacity: number): string {
if (color.startsWith("#")) {
const r = parseInt(color.slice(1, 3), 16)
const g = parseInt(color.slice(3, 5), 16)
const b = parseInt(color.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
}
if (color.startsWith("hsl")) return color.replace(")", `, ${opacity})`).replace("hsl", "hsla")
return color
}
export function GraphSection() {
let canvasRef: HTMLCanvasElement | undefined
let chartInstance: Chart | undefined
const params = useParams()
const now = new Date()
const [store, setStore] = createStore({
data: null as Awaited<ReturnType<typeof getCosts>> | null,
year: now.getFullYear(),
month: now.getMonth(),
key: null as string | null,
model: null as string | null,
modelDropdownOpen: false,
keyDropdownOpen: false,
})
const initialData = createAsync(() => queryCosts(params.id!, store.year, store.month))
const onPreviousMonth = async () => {
const month = store.month === 0 ? 11 : store.month - 1
const year = store.month === 0 ? store.year - 1 : store.year
const data = await getCosts(params.id!, year, month)
setStore({ month, year, data })
}
const onNextMonth = async () => {
const month = store.month === 11 ? 0 : store.month + 1
const year = store.month === 11 ? store.year + 1 : store.year
setStore({ month, year, data: await getCosts(params.id!, year, month) })
}
const onSelectModel = (model: string | null) => setStore({ model, modelDropdownOpen: false })
const onSelectKey = (keyID: string | null) => setStore({ key: keyID, keyDropdownOpen: false })
const getData = createMemo(() => store.data ?? initialData())
const getModels = createMemo(() => {
const data = getData()
if (!data?.usage) return []
return Array.from(new Set(data.usage.map((row) => row.model))).sort()
})
const getDates = createMemo(() => {
const daysInMonth = new Date(store.year, store.month + 1, 0).getDate()
return Array.from({ length: daysInMonth }, (_, i) => {
const date = new Date(store.year, store.month, i + 1)
return date.toISOString().split("T")[0]
})
})
const getKeyName = (keyID: string | null): string => {
if (!keyID || !store.data?.keys) return "All Keys"
const found = store.data.keys.find((k) => k.id === keyID)
return found?.displayName ?? "All Keys"
}
const formatMonthYear = () =>
new Date(store.year, store.month, 1).toLocaleDateString("en-US", { month: "long", year: "numeric" })
const isCurrentMonth = () => store.year === now.getFullYear() && store.month === now.getMonth()
const chartConfig = createMemo((): ChartConfiguration | null => {
const data = getData()
const dates = getDates()
if (!data?.usage?.length) return null
const dailyData = new Map<string, Map<string, number>>()
for (const dateKey of dates) dailyData.set(dateKey, new Map())
data.usage
.filter((row) => (store.key ? row.keyId === store.key : true))
.forEach((row) => {
const dayMap = dailyData.get(row.date)
if (!dayMap) return
dayMap.set(row.model, (dayMap.get(row.model) ?? 0) + row.totalCost)
})
const filteredModels = store.model === null ? getModels() : [store.model]
const datasets = filteredModels.map((model) => {
const color = getModelColor(model)
return {
label: model,
data: dates.map((date) => (dailyData.get(date)?.get(model) || 0) / 100_000_000),
backgroundColor: color,
hoverBackgroundColor: color,
borderWidth: 0,
}
})
return {
type: "bar",
data: {
labels: dates.map(formatDateLabel),
datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
stacked: true,
grid: {
display: false,
},
ticks: {
maxRotation: 0,
autoSkipPadding: 20,
color: "rgba(255, 255, 255, 0.5)",
font: {
family: "monospace",
size: 11,
},
},
},
y: {
stacked: true,
beginAtZero: true,
grid: {
color: "rgba(255, 255, 255, 0.1)",
},
ticks: {
color: "rgba(255, 255, 255, 0.5)",
font: {
family: "monospace",
size: 11,
},
callback: (value) => {
const num = Number(value)
return num >= 1000 ? `$${(num / 1000).toFixed(1)}k` : `$${num.toFixed(0)}`
},
},
},
},
plugins: {
tooltip: {
mode: "index",
intersect: false,
backgroundColor: "rgba(0, 0, 0, 0.9)",
titleColor: "rgba(255, 255, 255, 0.9)",
bodyColor: "rgba(255, 255, 255, 0.8)",
borderColor: "rgba(255, 255, 255, 0.1)",
borderWidth: 1,
padding: 12,
displayColors: true,
callbacks: {
label: (context) => {
const value = context.parsed.y
if (!value || value === 0) return
return `${context.dataset.label}: $${value.toFixed(2)}`
},
},
},
legend: {
display: true,
position: "bottom",
labels: {
color: "rgba(255, 255, 255, 0.7)",
font: {
size: 12,
},
padding: 16,
boxWidth: 16,
boxHeight: 16,
usePointStyle: false,
},
onHover: (event, legendItem, legend) => {
const chart = legend.chart
chart.data.datasets?.forEach((dataset, i) => {
const meta = chart.getDatasetMeta(i)
const baseColor = getModelColor(dataset.label || "")
const color = i === legendItem.datasetIndex ? baseColor : addOpacityToColor(baseColor, 0.3)
meta.data.forEach((bar: any) => {
bar.options.backgroundColor = color
})
})
chart.update("none")
},
onLeave: (event, legendItem, legend) => {
const chart = legend.chart
chart.data.datasets?.forEach((dataset, i) => {
const meta = chart.getDatasetMeta(i)
const baseColor = getModelColor(dataset.label || "")
meta.data.forEach((bar: any) => {
bar.options.backgroundColor = baseColor
})
})
chart.update("none")
},
},
},
},
}
})
createEffect(() => {
const config = chartConfig()
if (!config || !canvasRef) return
if (chartInstance) chartInstance.destroy()
chartInstance = new Chart(canvasRef, config)
})
onCleanup(() => chartInstance?.destroy())
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Cost</h2>
<p>Usage costs broken down by model.</p>
</div>
<Show when={getData()}>
<div data-slot="filter-container">
<div data-slot="month-picker">
<button data-slot="month-button" onClick={onPreviousMonth}>
<IconChevronLeft />
</button>
<span data-slot="month-label">{formatMonthYear()}</span>
<button data-slot="month-button" onClick={onNextMonth} disabled={isCurrentMonth()}>
<IconChevronRight />
</button>
</div>
<Dropdown
trigger={store.model === null ? "All Models" : store.model}
open={store.modelDropdownOpen}
onOpenChange={(open) => setStore({ modelDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectModel(null)}>
<span>All Models</span>
</button>
<For each={getModels()}>
{(model) => (
<button data-slot="model-item" onClick={() => onSelectModel(model)}>
<span>{model}</span>
</button>
)}
</For>
</>
</Dropdown>
<Dropdown
trigger={getKeyName(store.key)}
open={store.keyDropdownOpen}
onOpenChange={(open) => setStore({ keyDropdownOpen: open })}
>
<>
<button data-slot="model-item" onClick={() => onSelectKey(null)}>
<span>All Keys</span>
</button>
<For each={getData()?.keys || []}>
{(key) => (
<button data-slot="model-item" onClick={() => onSelectKey(key.id)}>
<span>{key.displayName}</span>
</button>
)}
</For>
</>
</Dropdown>
</div>
</Show>
<Show
when={chartConfig()}
fallback={
<div data-component="empty-state">
<p>No usage data available for the selected period.</p>
</div>
}
>
<div data-slot="chart-container">
<canvas ref={canvasRef} />
</div>
</Show>
</section>
)
}
@@ -5,6 +5,7 @@ import { NewUserSection } from "./new-user-section"
import { UsageSection } from "./usage-section"
import { ModelSection } from "./model-section"
import { ProviderSection } from "./provider-section"
import { GraphSection } from "./graph-section"
import { IconLogo } from "~/component/icon"
import { querySessionInfo, queryBillingInfo, createCheckoutUrl, formatBalance } from "../common"
@@ -66,6 +67,9 @@ export default function () {
<div data-slot="sections">
<NewUserSection />
<Show when={userInfo()?.isAdmin}>
<GraphSection />
</Show>
<ModelSection />
<Show when={userInfo()?.isAdmin}>
<ProviderSection />
@@ -5,11 +5,21 @@ import { withActor } from "~/context/auth.withActor"
import { ZenData } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
import {
IconAlibaba,
IconAnthropic,
IconGemini,
IconMoonshotAI,
IconOpenAI,
IconStealth,
IconXai,
IconZai,
} from "~/component/icon"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
if (modelId.startsWith("gpt")) return "OpenAI"
if (modelId.startsWith("gemini")) return "Google"
if (modelId.startsWith("kimi")) return "Moonshot AI"
if (modelId.startsWith("glm")) return "Z.ai"
if (modelId.startsWith("qwen")) return "Alibaba"
@@ -22,9 +32,19 @@ const getModelsInfo = query(async (workspaceID: string) => {
return withActor(async () => {
return {
all: Object.entries(ZenData.list().models)
.filter(([id, _model]) => !["claude-3-5-haiku", "minimax-m2"].includes(id))
.filter(([id, _model]) => !id.startsWith("an-"))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("alpha-"))
.sort(([idA, modelA], [idB, modelB]) => {
const priority = ["big-pickle", "grok", "claude", "gpt", "gemini"]
const getPriority = (id: string) => {
const index = priority.findIndex((p) => id.startsWith(p))
return index === -1 ? Infinity : index
}
const pA = getPriority(idA)
const pB = getPriority(idB)
if (pA !== pB) return pA - pB
return modelA.name.localeCompare(modelB.name)
})
.map(([id, model]) => ({ id, name: model.name })),
disabled: await Model.listDisabled(),
}
@@ -96,6 +116,8 @@ export function ModelSection() {
return <IconOpenAI width={16} height={16} />
case "Anthropic":
return <IconAnthropic width={16} height={16} />
case "Google":
return <IconGemini width={16} height={16} />
case "Moonshot AI":
return <IconMoonshotAI width={16} height={16} />
case "Z.ai":
@@ -8,6 +8,7 @@ import styles from "./provider-section.module.css"
const PROVIDERS = [
{ name: "OpenAI", key: "openai", prefix: "sk-" },
{ name: "Anthropic", key: "anthropic", prefix: "sk-ant-" },
{ name: "Google Gemini", key: "google", prefix: "AI" },
] as const
type Provider = (typeof PROVIDERS)[number]
@@ -1,24 +1,23 @@
.root {
/* Empty state */
[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);
p {
line-height: 1.5;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
}
/* Table container */
[data-slot="usage-table"] {
overflow-x: auto;
}
/* Table element */
[data-slot="usage-table-element"] {
width: 100%;
border-collapse: collapse;
@@ -48,7 +47,6 @@
&[data-slot="usage-model"] {
font-family: var(--font-sans);
font-weight: 400;
color: var(--color-text-secondary);
max-width: 200px;
word-break: break-word;
@@ -56,33 +54,133 @@
&[data-slot="usage-cost"] {
color: var(--color-text);
font-weight: 500;
}
[data-slot="tokens-with-breakdown"] {
position: relative;
display: flex;
align-items: center;
gap: var(--space-2);
}
[data-slot="breakdown-button"] {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
transition: color 0.15s ease;
&:hover {
color: var(--color-text);
}
svg {
width: 16px;
height: 16px;
}
}
[data-slot="breakdown-popup"] {
position: absolute;
left: 0;
top: 100%;
margin-top: var(--space-2);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
padding: var(--space-2);
z-index: 10;
min-width: 180px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-size: var(--font-size-xs);
@media (prefers-color-scheme: dark) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
}
tbody tr {
&:last-child td {
border-bottom: none;
tbody tr:last-child td {
border-bottom: none;
}
}
/* Pagination */
[data-slot="pagination"] {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding: var(--space-4) 0;
border-top: 1px solid var(--color-border-muted);
margin-top: var(--space-2);
button {
padding: var(--space-2) var(--space-4);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
color: var(--color-text);
font-size: var(--font-size-sm);
cursor: pointer;
transition: all 0.15s ease;
svg {
width: 16px;
height: 16px;
stroke-width: 2;
}
&:hover:not(:disabled) {
background: var(--color-bg-tertiary);
border-color: var(--color-border-hover);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
}
@media (max-width: 40rem) {
/* Mobile responsive */
@media (max-width: 40rem) {
[data-slot="usage-table-element"] {
th,
td {
padding: var(--space-2) var(--space-3);
font-size: var(--font-size-xs);
}
th {
&:nth-child(2) /* Model */ {
display: none;
}
}
td {
&:nth-child(2) /* Model */ {
display: none;
}
/* Hide Model column on mobile */
th:nth-child(2),
td:nth-child(2) {
display: none;
}
}
}
/* Breakdown popup content */
[data-slot="breakdown-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-1) 0;
}
[data-slot="breakdown-label"] {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
[data-slot="breakdown-value"] {
color: var(--color-text);
font-weight: 500;
font-size: var(--font-size-xs);
}
}
@@ -1,81 +1,73 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { query, useParams, createAsync } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
import { formatDateUTC, formatDateForTable } from "../common"
import { withActor } from "~/context/auth.withActor"
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
import styles from "./usage-section.module.css"
import { createStore } from "solid-js/store"
const getUsageInfo = query(async (workspaceID: string) => {
const PAGE_SIZE = 50
async function getUsageInfo(workspaceID: string, page: number) {
"use server"
return withActor(async () => {
return await Billing.usages()
return await Billing.usages(page, PAGE_SIZE)
}, workspaceID)
}, "usage.list")
}
const queryUsageInfo = query(getUsageInfo, "usage.list")
export function UsageSection() {
const params = useParams()
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
const usage = createAsync(() => getUsageInfo(params.id!))
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
// DUMMY DATA FOR TESTING
// const usage = () => [
// {
// timeCreated: new Date(Date.now() - 86400000 * 0).toISOString(), // Today
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 1247,
// outputTokens: 423,
// cost: 125400000, // $1.254
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 0.5).toISOString(), // 12 hours ago
// model: "claude-3-haiku-20240307",
// inputTokens: 892,
// outputTokens: 156,
// cost: 23500000, // $0.235
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // Yesterday
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 2134,
// outputTokens: 687,
// cost: 234700000, // $2.347
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 1.3).toISOString(), // 1.3 days ago
// model: "gpt-4o-mini",
// inputTokens: 567,
// outputTokens: 234,
// cost: 8900000, // $0.089
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 2).toISOString(), // 2 days ago
// model: "claude-3-opus-20240229",
// inputTokens: 1893,
// outputTokens: 945,
// cost: 445600000, // $4.456
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 2.7).toISOString(), // 2.7 days ago
// model: "gpt-4o",
// inputTokens: 1456,
// outputTokens: 532,
// cost: 156800000, // $1.568
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 3).toISOString(), // 3 days ago
// model: "claude-3-haiku-20240307",
// inputTokens: 634,
// outputTokens: 89,
// cost: 12300000, // $0.123
// },
// {
// timeCreated: new Date(Date.now() - 86400000 * 4).toISOString(), // 4 days ago
// model: "claude-3-5-sonnet-20241022",
// inputTokens: 3245,
// outputTokens: 1123,
// cost: 387200000, // $3.872
// },
// ]
createEffect(() => {
setStore({ usage: usage() })
}, [usage])
createEffect(() => {
if (!openBreakdownId()) return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
setOpenBreakdownId(null)
}
}
document.addEventListener("click", handleClickOutside)
return () => document.removeEventListener("click", handleClickOutside)
})
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
const canGoPrev = createMemo(() => store.page > 0)
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
}
const calculateTotalOutputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
return u.outputTokens + (u.reasoningTokens ?? 0)
}
const goPrev = async () => {
const usage = await getUsageInfo(params.id!, store.page - 1)
setStore({
page: store.page - 1,
usage,
})
}
const goNext = async () => {
const usage = await getUsageInfo(params.id!, store.page + 1)
setStore({
page: store.page + 1,
usage,
})
}
return (
<section class={styles.root}>
@@ -85,7 +77,7 @@ export function UsageSection() {
</div>
<div data-slot="usage-table">
<Show
when={usage() && usage()!.length > 0}
when={hasResults()}
fallback={
<div data-component="empty-state">
<p>Make your first API call to get started.</p>
@@ -103,17 +95,80 @@ export function UsageSection() {
</tr>
</thead>
<tbody>
<For each={usage()!}>
{(usage) => {
<For each={store.usage}>
{(usage, index) => {
const date = createMemo(() => new Date(usage.timeCreated))
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
const totalOutputTokens = createMemo(() => calculateTotalOutputTokens(usage))
const inputBreakdownId = `input-breakdown-${index()}`
const outputBreakdownId = `output-breakdown-${index()}`
const isInputOpen = createMemo(() => openBreakdownId() === inputBreakdownId)
const isOutputOpen = createMemo(() => openBreakdownId() === outputBreakdownId)
const isClaude = usage.model.toLowerCase().includes("claude")
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-tokens">
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
<button
data-slot="breakdown-button"
onClick={(e) => {
e.stopPropagation()
setOpenBreakdownId(isInputOpen() ? null : inputBreakdownId)
}}
>
<IconBreakdown />
</button>
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
<Show when={isInputOpen()}>
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Input</span>
<span data-slot="breakdown-value">{usage.inputTokens}</span>
</div>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Read</span>
<span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
</div>
<Show when={isClaude}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Write</span>
<span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
</div>
</Show>
</div>
</Show>
</div>
</td>
<td data-slot="usage-tokens">
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
<button
data-slot="breakdown-button"
onClick={(e) => {
e.stopPropagation()
setOpenBreakdownId(isOutputOpen() ? null : outputBreakdownId)
}}
>
<IconBreakdown />
</button>
<span onClick={() => setOpenBreakdownId(null)}>{totalOutputTokens()}</span>
<Show when={isOutputOpen()}>
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Output</span>
<span data-slot="breakdown-value">{usage.outputTokens}</span>
</div>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Reasoning</span>
<span data-slot="breakdown-value">{usage.reasoningTokens ?? 0}</span>
</div>
</div>
</Show>
</div>
</td>
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
</tr>
)
@@ -121,6 +176,16 @@ export function UsageSection() {
</For>
</tbody>
</table>
<Show when={canGoPrev() || canGoNext()}>
<div data-slot="pagination">
<button disabled={!canGoPrev()} onClick={goPrev}>
<IconChevronLeft />
</button>
<button disabled={!canGoNext()} onClick={goNext}>
<IconChevronRight />
</button>
</div>
</Show>
</Show>
</div>
</section>
+12 -5
View File
@@ -1,7 +1,7 @@
import "./index.css"
import { createAsync, query, redirect } from "@solidjs/router"
import { Title, Meta, Link } from "@solidjs/meta"
import { HttpHeader } from "@solidjs/start"
// import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg"
import { config } from "~/config"
import zenLogoDark from "../../asset/zen-ornate-dark.svg"
@@ -18,23 +18,24 @@ import { Legal } from "~/component/legal"
import { Footer } from "~/component/footer"
import { Header } from "~/component/header"
import { getLastSeenWorkspaceID } from "../workspace/common"
import { IconGemini, IconZai } from "~/component/icon"
const checkLoggedIn = query(async () => {
"use server"
const workspaceID = await getLastSeenWorkspaceID()
const workspaceID = await getLastSeenWorkspaceID().catch(() => {})
if (workspaceID) throw redirect(`/workspace/${workspaceID}`)
}, "checkLoggedIn.get")
export default function Home() {
createAsync(() => checkLoggedIn())
const loggedin = createAsync(() => checkLoggedIn())
return (
<main data-page="zen">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>OpenCode Zen | A curated set of reliable optimized models for coding agents</Title>
<Link rel="canonical" href={`${config.baseUrl}/zen`} />
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<Meta property="og:image" content="/social-share-zen.png" />
<Meta name="twitter:image" content="/social-share-zen.png" />
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
<div data-component="container">
<Header zen />
@@ -81,6 +82,9 @@ export default function Home() {
/>
</svg>
</div>
<div>
<IconGemini width="24" height="24" />
</div>
<div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -111,6 +115,9 @@ export default function Home() {
/>
</svg>
</div>
<div>
<IconZai width="24" height="24" />
</div>
</div>
<a href="/auth">
<span>Get started with Zen </span>
@@ -0,0 +1,26 @@
import { Resource, waitUntil } from "@opencode-ai/console-resource"
export function createDataDumper(sessionId: string, requestId: string) {
if (Resource.App.stage !== "production") return
let data: Record<string, any> = {}
let modelName: string | undefined
return {
provideModel: (model?: string) => (modelName = model),
provideRequest: (request: string) => (data.request = request),
provideResponse: (response: string) => (data.response = response),
provideStream: (chunk: string) => (data.response = (data.response ?? "") + chunk),
flush: () => {
if (!modelName) return
const str = new Date().toISOString().replace(/[^0-9]/g, "")
const yyyymmdd = str.substring(0, 8)
const hh = str.substring(8, 10)
waitUntil(
Resource.ConsoleData.put(`${yyyymmdd}/${hh}/${modelName}/${sessionId}/${requestId}.json`, JSON.stringify(data)),
)
},
}
}
@@ -13,11 +13,20 @@ import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js"
import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js"
import { logger } from "./logger"
import { AuthError, CreditsError, MonthlyLimitError, UserLimitError, ModelError, RateLimitError } from "./error"
import { createBodyConverter, createStreamPartConverter, createResponseConverter } from "./provider/provider"
import {
createBodyConverter,
createStreamPartConverter,
createResponseConverter,
ProviderHelper,
UsageInfo,
} from "./provider/provider"
import { anthropicHelper } from "./provider/anthropic"
import { googleHelper } from "./provider/google"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter } from "./rateLimiter"
import { createDataDumper } from "./dataDumper"
import { createTrialLimiter } from "./trialLimiter"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
@@ -30,6 +39,8 @@ export async function handler(
opts: {
format: ZenData.Format
parseApiKey: (headers: Headers) => string | undefined
parseModel: (url: string, body: any) => string
parseIsStream: (url: string, body: any) => boolean
},
) {
type AuthInfo = Awaited<ReturnType<typeof authenticate>>
@@ -43,20 +54,28 @@ export async function handler(
]
try {
const url = input.request.url
const body = await input.request.json()
const model = opts.parseModel(url, body)
const isStream = opts.parseIsStream(url, body)
const ip = input.request.headers.get("x-real-ip") ?? ""
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
const requestId = input.request.headers.get("x-opencode-request") ?? ""
logger.metric({
is_tream: !!body.stream,
session: input.request.headers.get("x-opencode-session"),
request: input.request.headers.get("x-opencode-request"),
is_tream: isStream,
session: sessionId,
request: requestId,
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, body.model)
const modelInfo = validateModel(zenData, model)
const dataDumper = createDataDumper(sessionId, requestId)
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => {
const providerInfo = selectProvider(zenData, modelInfo, ip, retry)
const providerInfo = selectProvider(zenData, modelInfo, sessionId, isTrial ?? false, retry)
const authInfo = await authenticate(modelInfo, providerInfo)
validateBilling(authInfo, modelInfo)
validateModelSettings(authInfo)
@@ -64,7 +83,7 @@ export async function handler(
logger.metric({ provider: providerInfo.id })
const startTimestamp = Date.now()
const reqUrl = providerInfo.modifyUrl(providerInfo.api)
const reqUrl = providerInfo.modifyUrl(providerInfo.api, providerInfo.model, isStream)
const reqBody = JSON.stringify(
providerInfo.modifyBody({
...createBodyConverter(opts.format, providerInfo.format)(body),
@@ -98,10 +117,14 @@ export async function handler(
})
}
return { providerInfo, authInfo, res, startTimestamp }
return { providerInfo, authInfo, reqBody, res, startTimestamp }
}
const { providerInfo, authInfo, res, startTimestamp } = await retriableRequest()
const { providerInfo, authInfo, reqBody, res, startTimestamp } = await retriableRequest()
// Store model request
dataDumper?.provideModel(providerInfo.storeModel)
dataDumper?.provideRequest(reqBody)
// Scrub response headers
const resHeaders = new Headers()
@@ -114,14 +137,18 @@ export async function handler(
logger.debug("STATUS: " + res.status + " " + res.statusText)
// Handle non-streaming response
if (!body.stream) {
if (!isStream) {
const responseConverter = createResponseConverter(providerInfo.format, opts.format)
const json = await res.json()
const body = JSON.stringify(responseConverter(json))
logger.metric({ response_length: body.length })
logger.debug("RESPONSE: " + body)
dataDumper?.provideResponse(body)
dataDumper?.flush()
const tokensInfo = providerInfo.normalizeUsage(json.usage)
await trialLimiter?.track(tokensInfo)
await rateLimiter?.track()
await trackUsage(authInfo, modelInfo, providerInfo, json.usage)
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
await reload(authInfo)
return new Response(body, {
status: res.status,
@@ -149,10 +176,13 @@ export async function handler(
response_length: responseLength,
"timestamp.last_byte": Date.now(),
})
dataDumper?.flush()
await rateLimiter?.track()
const usage = usageParser.retrieve()
if (usage) {
await trackUsage(authInfo, modelInfo, providerInfo, usage)
const tokensInfo = providerInfo.normalizeUsage(usage)
await trialLimiter?.track(tokensInfo)
await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
await reload(authInfo)
}
c.close()
@@ -168,8 +198,9 @@ export async function handler(
}
responseLength += value.length
buffer += decoder.decode(value, { stream: true })
dataDumper?.provideStream(buffer)
const parts = buffer.split("\n\n")
const parts = buffer.split(providerInfo.streamSeparator)
buffer = parts.pop() ?? ""
for (let part of parts) {
@@ -257,8 +288,18 @@ export async function handler(
return { id: modelId, ...modelData }
}
function selectProvider(zenData: ZenData, modelInfo: ModelInfo, ip: string, retry: RetryOptions) {
function selectProvider(
zenData: ZenData,
modelInfo: ModelInfo,
sessionId: string,
isTrial: boolean,
retry: RetryOptions,
) {
const provider = (() => {
if (isTrial) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.trial!.provider)
}
if (retry.retryCount === MAX_RETRIES) {
return modelInfo.providers.find((provider) => provider.id === modelInfo.fallbackProvider)
}
@@ -268,9 +309,13 @@ export async function handler(
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.flatMap((provider) => Array<typeof provider>(provider.weight ?? 1).fill(provider))
// Use the last 2 characters of IP address to select a provider
const lastChars = ip.slice(-2)
const index = parseInt(lastChars, 16) % providers.length
// Use the last 4 characters of session ID to select a provider
let h = 0
const l = sessionId.length
for (let i = l - 4; i < l; i++) {
h = (h * 31 + sessionId.charCodeAt(i)) | 0 // 32-bit int
}
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
return providers[index || 0]
})()
@@ -283,6 +328,7 @@ export async function handler(
...(() => {
const format = zenData.providers[provider.id].format
if (format === "anthropic") return anthropicHelper
if (format === "google") return googleHelper
if (format === "openai") return openaiHelper
return oaCompatHelper
})(),
@@ -291,7 +337,7 @@ export async function handler(
async function authenticate(modelInfo: ModelInfo, providerInfo: ProviderInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey) {
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
throw new AuthError("Missing API key.")
}
@@ -409,9 +455,14 @@ export async function handler(
providerInfo.apiKey = authInfo.provider.credentials
}
async function trackUsage(authInfo: AuthInfo, modelInfo: ModelInfo, providerInfo: ProviderInfo, usage: any) {
async function trackUsage(
authInfo: AuthInfo,
modelInfo: ModelInfo,
providerInfo: ProviderInfo,
usageInfo: UsageInfo,
) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
providerInfo.normalizeUsage(usage)
usageInfo
const modelCost =
modelInfo.cost200K &&
@@ -30,6 +30,7 @@ export const anthropicHelper = {
service_tier: "standard_only",
}
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
@@ -0,0 +1,74 @@
import { ProviderHelper } from "./provider"
/*
{
promptTokenCount: 11453,
candidatesTokenCount: 71,
totalTokenCount: 11625,
cachedContentTokenCount: 8100,
promptTokensDetails: [
{modality: "TEXT",tokenCount: 11453}
],
cacheTokensDetails: [
{modality: "TEXT",tokenCount: 8100}
],
thoughtsTokenCount: 101
}
*/
type Usage = {
promptTokenCount?: number
candidatesTokenCount?: number
totalTokenCount?: number
cachedContentTokenCount?: number
promptTokensDetails?: { modality: string; tokenCount: number }[]
cacheTokensDetails?: { modality: string; tokenCount: number }[]
thoughtsTokenCount?: number
}
export const googleHelper = {
format: "google",
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) =>
`${providerApi}/models/${model}:${isStream ? "streamGenerateContent?alt=sse" : "generateContent"}`,
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => {
headers.set("x-goog-api-key", apiKey)
},
modifyBody: (body: Record<string, any>) => {
return body
},
streamSeparator: "\r\n\r\n",
createUsageParser: () => {
let usage: Usage
return {
parse: (chunk: string) => {
if (!chunk.startsWith("data: ")) return
let json
try {
json = JSON.parse(chunk.slice(6)) as { usageMetadata?: Usage }
} catch (e) {
return
}
if (!json.usageMetadata) return
usage = json.usageMetadata
},
retrieve: () => usage,
}
},
normalizeUsage: (usage: Usage) => {
const inputTokens = usage.promptTokenCount ?? 0
const outputTokens = usage.candidatesTokenCount ?? 0
const reasoningTokens = usage.thoughtsTokenCount ?? 0
const cacheReadTokens = usage.cachedContentTokenCount ?? 0
return {
inputTokens: inputTokens - cacheReadTokens,
outputTokens,
reasoningTokens,
cacheReadTokens,
cacheWrite5mTokens: undefined,
cacheWrite1hTokens: undefined,
}
},
} satisfies ProviderHelper
@@ -33,6 +33,7 @@ export const oaCompatHelper = {
...(body.stream ? { stream_options: { include_usage: true } } : {}),
}
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
@@ -21,6 +21,7 @@ export const openaiHelper = {
modifyBody: (body: Record<string, any>) => {
return body
},
streamSeparator: "\n\n",
createUsageParser: () => {
let usage: Usage
@@ -24,23 +24,26 @@ import {
toOaCompatibleResponse,
} from "./openai-compatible"
export type UsageInfo = {
inputTokens: number
outputTokens: number
reasoningTokens?: number
cacheReadTokens?: number
cacheWrite5mTokens?: number
cacheWrite1hTokens?: number
}
export type ProviderHelper = {
format: ZenData.Format
modifyUrl: (providerApi: string) => string
modifyUrl: (providerApi: string, model?: string, isStream?: boolean) => string
modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
modifyBody: (body: Record<string, any>) => Record<string, any>
streamSeparator: string
createUsageParser: () => {
parse: (chunk: string) => void
retrieve: () => any
}
normalizeUsage: (usage: any) => {
inputTokens: number
outputTokens: number
reasoningTokens?: number
cacheReadTokens?: number
cacheWrite5mTokens?: number
cacheWrite1hTokens?: number
}
normalizeUsage: (usage: any) => UsageInfo
}
export interface CommonMessage {
@@ -0,0 +1,43 @@
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { UsageInfo } from "./provider/provider"
export function createTrialLimiter(limit: number | undefined, ip: string) {
if (!limit) return
if (!ip) return
let trial: boolean
return {
isTrial: async () => {
const data = await Database.use((tx) =>
tx
.select({
usage: IpTable.usage,
})
.from(IpTable)
.where(eq(IpTable.ip, ip))
.then((rows) => rows[0]),
)
trial = (data?.usage ?? 0) < limit
return trial
},
track: async (usageInfo: UsageInfo) => {
if (!trial) return
const usage =
usageInfo.inputTokens +
usageInfo.outputTokens +
(usageInfo.reasoningTokens ?? 0) +
(usageInfo.cacheReadTokens ?? 0) +
(usageInfo.cacheWrite5mTokens ?? 0) +
(usageInfo.cacheWrite1hTokens ?? 0)
await Database.use((tx) =>
tx
.insert(IpTable)
.values({ ip, usage })
.onDuplicateKeyUpdate({ set: { usage: sql`${IpTable.usage} + ${usage}` } }),
)
},
}
}
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "oa-compat",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
parseModel: (url: string, body: any) => body.model,
parseIsStream: (url: string, body: any) => !!body.stream,
})
}
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "anthropic",
parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
parseModel: (url: string, body: any) => body.model,
parseIsStream: (url: string, body: any) => !!body.stream,
})
}
@@ -0,0 +1,13 @@
import type { APIEvent } from "@solidjs/start/server"
import { handler } from "~/routes/zen/util/handler"
export function POST(input: APIEvent) {
return handler(input, {
format: "google",
parseApiKey: (headers: Headers) => headers.get("x-goog-api-key") ?? undefined,
parseModel: (url: string, body: any) => url.split("/").pop()?.split(":")?.[0] ?? "",
parseIsStream: (url: string, body: any) =>
// ie. url: https://opencode.ai/zen/v1/models/gemini-3-pro:streamGenerateContent?alt=sse'
url.split("/").pop()?.split(":")?.[1]?.startsWith("streamGenerateContent") ?? false,
})
}
@@ -5,5 +5,7 @@ export function POST(input: APIEvent) {
return handler(input, {
format: "openai",
parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
parseModel: (url: string, body: any) => body.model,
parseIsStream: (url: string, body: any) => !!body.stream,
})
}
+1 -1
View File
@@ -12,7 +12,7 @@
"allowJs": true,
"strict": true,
"noEmit": true,
"types": ["vinxi/types/client"],
"types": ["vite/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
+25
View File
@@ -0,0 +1,25 @@
import { defineConfig, PluginOption } from "vite"
import { solidStart } from "@solidjs/start/config"
import { nitro } from "nitro/vite"
export default defineConfig({
plugins: [
solidStart() as PluginOption,
nitro({
compatibilityDate: "2024-09-19",
preset: "cloudflare_module",
cloudflare: {
nodeCompat: true,
},
}),
],
server: {
allowedHosts: true,
},
build: {
rollupOptions: {
external: ["cloudflare:workers"],
},
minify: false,
},
})
@@ -0,0 +1,8 @@
CREATE TABLE `ip` (
`ip` varchar(45) 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),
`usage` int,
CONSTRAINT `ip_ip_pk` PRIMARY KEY(`ip`)
);
@@ -0,0 +1,981 @@
{
"version": "5",
"dialect": "mysql",
"id": "9d5d9885-7ec5-45f6-ac53-45a8e25dede7",
"prevId": "8b7fa839-a088-408e-84a4-1a07325c0290",
"tables": {
"account": {
"name": "account",
"columns": {
"id": {
"name": "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
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"account_id_pk": {
"name": "account_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"auth": {
"name": "auth",
"columns": {
"id": {
"name": "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
},
"provider": {
"name": "provider",
"type": "enum('email','github','google')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"subject": {
"name": "subject",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"account_id": {
"name": "account_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"provider": {
"name": "provider",
"columns": ["provider", "subject"],
"isUnique": true
},
"account_id": {
"name": "account_id",
"columns": ["account_id"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"auth_id_pk": {
"name": "auth_id_pk",
"columns": ["id"]
}
},
"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,
"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
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_method_type": {
"name": "payment_method_type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"monthly_limit": {
"name": "monthly_limit",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"monthly_usage": {
"name": "monthly_usage",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_monthly_usage_updated": {
"name": "time_monthly_usage_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload_trigger": {
"name": "reload_trigger",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload_amount": {
"name": "reload_amount",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload_error": {
"name": "reload_error",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_reload_error": {
"name": "time_reload_error",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_reload_locked_till": {
"name": "time_reload_locked_till",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": ["customer_id"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"payment": {
"name": "payment",
"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
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"invoice_id": {
"name": "invoice_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_refunded": {
"name": "time_refunded",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"usage": {
"name": "usage",
"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
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"input_tokens": {
"name": "input_tokens",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"output_tokens": {
"name": "output_tokens",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_write_5m_tokens": {
"name": "cache_write_5m_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_write_1h_tokens": {
"name": "cache_write_1h_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"key_id": {
"name": "key_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"ip": {
"name": "ip",
"columns": {
"ip": {
"name": "ip",
"type": "varchar(45)",
"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
},
"usage": {
"name": "usage",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"ip_ip_pk": {
"name": "ip_ip_pk",
"columns": ["ip"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"key": {
"name": "key",
"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
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "varchar(30)",
"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": {}
},
"model": {
"name": "model",
"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
},
"model": {
"name": "model",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"model_workspace_model": {
"name": "model_workspace_model",
"columns": ["workspace_id", "model"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"model_workspace_id_id_pk": {
"name": "model_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"provider": {
"name": "provider",
"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
},
"provider": {
"name": "provider",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentials": {
"name": "credentials",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"workspace_provider": {
"name": "workspace_provider",
"columns": ["workspace_id", "provider"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"provider_workspace_id_id_pk": {
"name": "provider_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
},
"account_id": {
"name": "account_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"color": {
"name": "color",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('admin','member')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"monthly_limit": {
"name": "monthly_limit",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"monthly_usage": {
"name": "monthly_usage",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_monthly_usage_updated": {
"name": "time_monthly_usage_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_account_id": {
"name": "user_account_id",
"columns": ["workspace_id", "account_id"],
"isUnique": true
},
"user_email": {
"name": "user_email",
"columns": ["workspace_id", "email"],
"isUnique": true
},
"global_account_id": {
"name": "global_account_id",
"columns": ["account_id"],
"isUnique": false
},
"global_email": {
"name": "global_email",
"columns": ["email"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"workspace": {
"name": "workspace",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"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
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": ["slug"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": ["id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
@@ -267,6 +267,13 @@
"when": 1761928273807,
"tag": "0037_messy_jackal",
"breakpoints": true
},
{
"idx": 38,
"version": "5",
"when": 1764110043942,
"tag": "0038_famous_magik",
"breakpoints": true
}
]
}
+2 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.65",
"version": "1.0.122",
"private": true,
"type": "module",
"dependencies": {
@@ -30,6 +30,7 @@
"update-models": "script/update-models.ts",
"promote-models-to-dev": "script/promote-models.ts dev",
"promote-models-to-prod": "script/promote-models.ts production",
"pull-models-from-dev": "script/pull-models.ts dev",
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
+10 -9
View File
@@ -11,20 +11,21 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
// read the secret
const ret = await $`bun sst secret list`.cwd(root).text()
const value1 = ret
.split("\n")
.find((line) => line.startsWith("ZEN_MODELS1"))
?.split("=")[1]
const value2 = ret
.split("\n")
.find((line) => line.startsWith("ZEN_MODELS2"))
?.split("=")[1]
const lines = ret.split("\n")
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
// validate value
ZenData.validate(JSON.parse(value1 + value2))
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bun
import { $ } from "bun"
import path from "path"
import { ZenData } from "../src/model"
const stage = process.argv[2]
if (!stage) throw new Error("Stage is required")
const root = path.resolve(process.cwd(), "..", "..", "..")
// read the secret
const ret = await $`bun sst secret list --stage ${stage}`.cwd(root).text()
const lines = ret.split("\n")
const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
// validate value
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1}`
await $`bun sst secret set ZEN_MODELS2 ${value2}`
await $`bun sst secret set ZEN_MODELS3 ${value3}`
await $`bun sst secret set ZEN_MODELS4 ${value4}`
+17 -12
View File
@@ -9,21 +9,20 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
const models = await $`bun sst secret list`.cwd(root).text()
// read the line starting with "ZEN_MODELS"
const oldValue1 = models
.split("\n")
.find((line) => line.startsWith("ZEN_MODELS1"))
?.split("=")[1]
const oldValue2 = models
.split("\n")
.find((line) => line.startsWith("ZEN_MODELS2"))
?.split("=")[1]
const lines = models.split("\n")
const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[1]
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
// store the prettified json to a temp file
const filename = `models-${Date.now()}.json`
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2), null, 2))
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
console.log("tempFile", tempFile.name)
// open temp file in vim and read the file on close
@@ -32,6 +31,12 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
ZenData.validate(JSON.parse(newValue))
// update the secret
const mid = Math.floor(newValue.length / 2)
await $`bun sst secret set ZEN_MODELS1 ${newValue.slice(0, mid)}`
await $`bun sst secret set ZEN_MODELS2 ${newValue.slice(mid)}`
const chunk = Math.ceil(newValue.length / 4)
const newValue1 = newValue.slice(0, chunk)
const newValue2 = newValue.slice(chunk, chunk * 2)
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
const newValue4 = newValue.slice(chunk * 3)
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
+2
View File
@@ -22,6 +22,7 @@ export namespace AWS {
to: z.string(),
subject: z.string(),
body: z.string(),
replyTo: z.string().optional(),
}),
async (input) => {
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
@@ -35,6 +36,7 @@ export namespace AWS {
Destination: {
ToAddresses: [input.to],
},
...(input.replyTo && { ReplyToAddresses: [input.replyTo] }),
Content: {
Simple: {
Subject: {
+3 -2
View File
@@ -57,14 +57,15 @@ export namespace Billing {
)
}
export const usages = async () => {
export const usages = async (page = 0, pageSize = 50) => {
return await Database.use((tx) =>
tx
.select()
.from(UsageTable)
.where(eq(UsageTable.workspaceID, Actor.workspace()))
.orderBy(sql`${UsageTable.timeCreated} DESC`)
.limit(100),
.limit(pageSize)
.offset(page * pageSize),
)
}
+11 -2
View File
@@ -8,7 +8,7 @@ import { Actor } from "./actor"
import { Resource } from "@opencode-ai/console-resource"
export namespace ZenData {
const FormatSchema = z.enum(["anthropic", "openai", "oa-compat"])
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
export type Format = z.infer<typeof FormatSchema>
const ModelCostSchema = z.object({
@@ -24,6 +24,12 @@ export namespace ZenData {
cost: ModelCostSchema,
cost200K: ModelCostSchema.optional(),
allowAnonymous: z.boolean().optional(),
trial: z
.object({
limit: z.number(),
provider: z.string(),
})
.optional(),
rateLimit: z.number().optional(),
fallbackProvider: z.string().optional(),
providers: z.array(
@@ -32,6 +38,7 @@ export namespace ZenData {
model: z.string(),
weight: z.number().optional(),
disabled: z.boolean().optional(),
storeModel: z.string().optional(),
}),
),
})
@@ -53,7 +60,9 @@ export namespace ZenData {
})
export const list = fn(z.void(), () => {
const json = JSON.parse(Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value)
const json = JSON.parse(
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
)
return ModelsSchema.parse(json)
})
}
@@ -0,0 +1,12 @@
import { mysqlTable, int, primaryKey, varchar } from "drizzle-orm/mysql-core"
import { timestamps } from "../drizzle/types"
export const IpTable = mysqlTable(
"ip",
{
ip: varchar("ip", { length: 45 }).notNull(),
...timestamps,
usage: int("usage"),
},
(table) => [primaryKey({ columns: [table.ip] })],
)
+18
View File
@@ -74,6 +74,14 @@ declare module "sst" {
type: "sst.sst.Secret"
value: string
}
R2AccessKey: {
type: "sst.sst.Secret"
value: string
}
R2SecretKey: {
type: "sst.sst.Secret"
value: string
}
STRIPE_SECRET_KEY: {
type: "sst.sst.Secret"
value: string
@@ -94,6 +102,14 @@ declare module "sst" {
type: "sst.sst.Secret"
value: string
}
ZEN_MODELS3: {
type: "sst.sst.Secret"
value: string
}
ZEN_MODELS4: {
type: "sst.sst.Secret"
value: string
}
}
}
// cloudflare
@@ -104,6 +120,8 @@ declare module "sst" {
AuthApi: cloudflare.Service
AuthStorage: cloudflare.KVNamespace
Bucket: cloudflare.R2Bucket
ConsoleData: cloudflare.R2Bucket
EnterpriseStorage: cloudflare.R2Bucket
GatewayKv: cloudflare.KVNamespace
LogProcessor: cloudflare.Service
}

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