summaryrefslogtreecommitdiffhomepage
path: root/packages/cloud/app/src/routes
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-18 10:59:01 -0400
committerFrank <[email protected]>2025-09-18 10:59:01 -0400
commit4ceabdffa07b1af8d99eb73622a4d549d99ec6d2 (patch)
tree72e2ae62084a9e24cc76caffbd1f30dafc69ea56 /packages/cloud/app/src/routes
parentc87480cf931a6f8f8b55552558ef521f1918b578 (diff)
downloadopencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.tar.gz
opencode-4ceabdffa07b1af8d99eb73622a4d549d99ec6d2.zip
wip: zen
Diffstat (limited to 'packages/cloud/app/src/routes')
-rw-r--r--packages/cloud/app/src/routes/[...404].css130
-rw-r--r--packages/cloud/app/src/routes/[...404].tsx38
-rw-r--r--packages/cloud/app/src/routes/auth/authorize.ts7
-rw-r--r--packages/cloud/app/src/routes/auth/callback.ts31
-rw-r--r--packages/cloud/app/src/routes/auth/index.ts13
-rw-r--r--packages/cloud/app/src/routes/debug/index.ts13
-rw-r--r--packages/cloud/app/src/routes/discord.ts5
-rw-r--r--packages/cloud/app/src/routes/docs/[...path].ts20
-rw-r--r--packages/cloud/app/src/routes/docs/index.ts20
-rw-r--r--packages/cloud/app/src/routes/index.css504
-rw-r--r--packages/cloud/app/src/routes/index.tsx183
-rw-r--r--packages/cloud/app/src/routes/s/[id].ts20
-rw-r--r--packages/cloud/app/src/routes/stripe/webhook.ts98
-rw-r--r--packages/cloud/app/src/routes/workspace.css127
-rw-r--r--packages/cloud/app/src/routes/workspace.tsx67
-rw-r--r--packages/cloud/app/src/routes/workspace/[id].css115
-rw-r--r--packages/cloud/app/src/routes/workspace/[id].tsx50
-rw-r--r--packages/cloud/app/src/routes/workspace/index.tsx0
-rw-r--r--packages/cloud/app/src/routes/zen/handler.ts594
-rw-r--r--packages/cloud/app/src/routes/zen/v1/chat/completions.ts54
-rw-r--r--packages/cloud/app/src/routes/zen/v1/messages.ts61
-rw-r--r--packages/cloud/app/src/routes/zen/v1/responses.ts52
22 files changed, 0 insertions, 2202 deletions
diff --git a/packages/cloud/app/src/routes/[...404].css b/packages/cloud/app/src/routes/[...404].css
deleted file mode 100644
index 1edbd0a5a..000000000
--- a/packages/cloud/app/src/routes/[...404].css
+++ /dev/null
@@ -1,130 +0,0 @@
-[data-page="not-found"] {
- --color-text: hsl(224, 10%, 10%);
- --color-text-secondary: hsl(224, 7%, 46%);
- --color-text-dimmed: hsl(224, 6%, 63%);
- --color-text-inverted: hsl(0, 0%, 100%);
-
- --color-border: hsl(224, 6%, 77%);
-}
-
-[data-page="not-found"] {
- @media (prefers-color-scheme: dark) {
- --color-text: hsl(0, 0%, 100%);
- --color-text-secondary: hsl(224, 6%, 66%);
- --color-text-dimmed: hsl(224, 7%, 46%);
- --color-text-inverted: hsl(224, 10%, 10%);
-
- --color-border: hsl(224, 6%, 36%);
- }
-}
-
-[data-page="not-found"] {
- --padding: 3rem;
- --vertical-padding: 1.5rem;
- --heading-font-size: 1.375rem;
-
- @media (max-width: 30rem) {
- --padding: 1rem;
- --vertical-padding: 0.75rem;
- --heading-font-size: 1rem;
- }
-
- font-family: var(--font-mono);
- color: var(--color-text);
- padding: calc(var(--padding) + 1rem);
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- [data-component="content"] {
- max-width: 40rem;
- width: 100%;
- border: 1px solid var(--color-border);
- }
-
- [data-component="top"] {
- padding: var(--padding);
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: calc(var(--vertical-padding) / 2);
- text-align: center;
-
- [data-slot="logo-link"] {
- text-decoration: none;
- }
-
- img {
- height: auto;
- width: clamp(200px, 85vw, 400px);
- }
-
- [data-slot="logo dark"] {
- display: none;
- }
-
- @media (prefers-color-scheme: dark) {
- [data-slot="logo light"] {
- display: none;
- }
- [data-slot="logo dark"] {
- display: block;
- }
- }
-
- [data-slot="title"] {
- line-height: 1.25;
- font-weight: 500;
- text-align: center;
- font-size: var(--heading-font-size);
- color: var(--color-text);
- text-transform: uppercase;
- margin: 0;
- }
- }
-
- [data-component="actions"] {
- border-top: 1px solid var(--color-border);
- display: flex;
-
- [data-slot="action"] {
- flex: 1;
- text-align: center;
- line-height: 1.4;
- padding: var(--vertical-padding) 1rem;
- text-transform: uppercase;
- font-size: 1rem;
-
- a {
- display: block;
- width: 100%;
- height: 100%;
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
- }
-
- [data-slot="action"] + [data-slot="action"] {
- border-left: 1px solid var(--color-border);
- }
-
- @media (max-width: 40rem) {
- flex-direction: column;
-
- [data-slot="action"] + [data-slot="action"] {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/[...404].tsx b/packages/cloud/app/src/routes/[...404].tsx
deleted file mode 100644
index ba2842b5a..000000000
--- a/packages/cloud/app/src/routes/[...404].tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import "./[...404].css"
-import { Title } from "@solidjs/meta"
-import { HttpStatusCode } from "@solidjs/start"
-import logoLight from "../asset/logo-ornate-light.svg"
-import logoDark from "../asset/logo-ornate-dark.svg"
-
-export default function NotFound() {
- return (
- <main data-page="not-found">
- <Title>Not Found | opencode</Title>
- <HttpStatusCode code={404} />
- <div data-component="content">
- <section data-component="top">
- <a href="/" data-slot="logo-link">
- <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
- <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
- </a>
- <h1 data-slot="title">404 - Page Not Found</h1>
- </section>
-
- <section data-component="actions">
- <div data-slot="action">
- <a href="/">Home</a>
- </div>
- <div data-slot="action">
- <a href="/docs">Docs</a>
- </div>
- <div data-slot="action">
- <a href="https://github.com/sst/opencode">GitHub</a>
- </div>
- <div data-slot="action">
- <a href="/discord">Discord</a>
- </div>
- </section>
- </div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/auth/authorize.ts b/packages/cloud/app/src/routes/auth/authorize.ts
deleted file mode 100644
index 166466ef8..000000000
--- a/packages/cloud/app/src/routes/auth/authorize.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { AuthClient } from "~/context/auth"
-
-export async function GET(input: APIEvent) {
- const result = await AuthClient.authorize(new URL("./callback", input.request.url).toString(), "code")
- return Response.redirect(result.url, 302)
-}
diff --git a/packages/cloud/app/src/routes/auth/callback.ts b/packages/cloud/app/src/routes/auth/callback.ts
deleted file mode 100644
index 23025b54d..000000000
--- a/packages/cloud/app/src/routes/auth/callback.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { redirect } from "@solidjs/router"
-import type { APIEvent } from "@solidjs/start/server"
-import { AuthClient } from "~/context/auth"
-import { useAuthSession } from "~/context/auth.session"
-
-export async function GET(input: APIEvent) {
- const url = new URL(input.request.url)
- const code = url.searchParams.get("code")
- if (!code) throw new Error("No code found")
- const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
- if (result.err) {
- throw new Error(result.err.message)
- }
- const decoded = AuthClient.decode(result.tokens.access, {} as any)
- if (decoded.err) throw new Error(decoded.err.message)
- const session = await useAuthSession()
- const id = decoded.subject.properties.accountID
- await session.update((value) => {
- return {
- ...value,
- account: {
- [id]: {
- id,
- email: decoded.subject.properties.email,
- },
- },
- current: id,
- }
- })
- return redirect("/auth")
-}
diff --git a/packages/cloud/app/src/routes/auth/index.ts b/packages/cloud/app/src/routes/auth/index.ts
deleted file mode 100644
index 308ae2d1d..000000000
--- a/packages/cloud/app/src/routes/auth/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Account } from "@opencode/cloud-core/account.js"
-import { redirect } from "@solidjs/router"
-import type { APIEvent } from "@solidjs/start/server"
-import { withActor } from "~/context/auth.withActor"
-
-export async function GET(input: APIEvent) {
- try {
- const workspaces = await withActor(async () => Account.workspaces())
- return redirect(`/workspace/${workspaces[0].id}`)
- } catch {
- return redirect("/auth/authorize")
- }
-}
diff --git a/packages/cloud/app/src/routes/debug/index.ts b/packages/cloud/app/src/routes/debug/index.ts
deleted file mode 100644
index 8c7eb7bd8..000000000
--- a/packages/cloud/app/src/routes/debug/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { json } from "@solidjs/router"
-import { Database } from "@opencode/cloud-core/drizzle/index.js"
-import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
-
-export async function GET(evt: APIEvent) {
- return json({
- data: await Database.use(async (tx) => {
- const result = await tx.$count(UserTable)
- return result
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/discord.ts b/packages/cloud/app/src/routes/discord.ts
deleted file mode 100644
index 7088295da..000000000
--- a/packages/cloud/app/src/routes/discord.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from "@solidjs/router"
-
-export async function GET() {
- return redirect("https://discord.gg/opencode")
-}
diff --git a/packages/cloud/app/src/routes/docs/[...path].ts b/packages/cloud/app/src/routes/docs/[...path].ts
deleted file mode 100644
index f07781583..000000000
--- a/packages/cloud/app/src/routes/docs/[...path].ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/docs/index.ts b/packages/cloud/app/src/routes/docs/index.ts
deleted file mode 100644
index f07781583..000000000
--- a/packages/cloud/app/src/routes/docs/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/index.css b/packages/cloud/app/src/routes/index.css
deleted file mode 100644
index fe95bb7ea..000000000
--- a/packages/cloud/app/src/routes/index.css
+++ /dev/null
@@ -1,504 +0,0 @@
-[data-page="home"] {
- --color-text: hsl(224, 10%, 10%);
- --color-text-secondary: hsl(224, 7%, 46%);
- --color-text-dimmed: hsl(224, 6%, 63%);
- --color-text-inverted: hsl(0, 0%, 100%);
-
- --color-border: hsl(224, 6%, 77%);
-}
-
-[data-page="home"] {
- @media (prefers-color-scheme: dark) {
- --color-text: hsl(0, 0%, 100%);
- --color-text-secondary: hsl(224, 6%, 66%);
- --color-text-dimmed: hsl(224, 7%, 46%);
- --color-text-inverted: hsl(224, 10%, 10%);
-
- --color-border: hsl(224, 6%, 36%);
- }
-}
-
-[data-page="home"] {
- --padding: 3rem;
- --vertical-padding: 1.5rem;
- --heading-font-size: 1.375rem;
-
- @media (max-width: 30rem) {
- --padding: 1rem;
- --vertical-padding: 0.75rem;
- --heading-font-size: 1rem;
- }
-
- display: flex;
- gap: var(--vertical-padding);
- flex-direction: column;
- font-family: var(--font-mono);
- color: var(--color-text);
- padding: calc(var(--padding) + 1rem);
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- [data-component="content"] {
- max-width: 67.5rem;
- margin: 0 auto;
- border: 1px solid var(--color-border);
- }
-
- [data-component="top"] {
- padding: calc(var(--padding) * 1.5) var(--padding) var(--padding);
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: calc(var(--vertical-padding) / 2);
-
- img {
- height: auto;
- width: clamp(200px, 85vw, 552px);
- }
-
- [data-slot="logo dark"] {
- display: none;
- }
-
- @media (prefers-color-scheme: dark) {
- [data-slot="logo light"] {
- display: none;
- }
- [data-slot="logo dark"] {
- display: block;
- }
- }
-
- [data-slot="title"] {
- line-height: 1.25;
- font-weight: 500;
- text-align: center;
- font-size: var(--heading-font-size);
- color: var(--color-text-secondary);
- text-transform: uppercase;
- }
-
- [data-slot="login"] {
- position: absolute;
- top: 0;
- right: 0;
- border-width: 0 0 1px 1px;
- border-style: solid;
- border-color: var(--color-border);
- background-color: var(--color-bg);
-
- @media (max-width: 30rem) {
- display: none;
- }
-
- a {
- display: block;
- padding: 0.5rem 1rem calc(0.5rem + 4px);
- }
- }
- }
-
- [data-component="cta"] {
- border-top: 1px solid var(--color-border);
- display: flex;
-
- & > div + div {
- border-left: 1px solid var(--color-border);
- }
-
- [data-slot="left"] {
- flex: 0 0 auto;
- text-align: center;
- line-height: 1.4;
- padding: var(--vertical-padding) 2rem;
- text-transform: uppercase;
- font-size: 1.125rem;
-
- @media (max-width: 30rem) {
- font-size: 1rem;
- padding-bottom: calc(var(--vertical-padding) + 4px);
- }
-
- @media (max-width: 30rem) {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
- }
- }
-
- [data-slot="center"] {
- display: none;
-
- @media (max-width: 30rem) {
- display: block;
- flex: 1;
- text-align: center;
- padding: var(--vertical-padding) 0.5rem;
- border-top: 1px solid var(--color-border);
- border-left: none;
- }
- }
-
- [data-slot="right"] {
- flex: 1;
- padding: var(--vertical-padding) 1rem;
- }
-
- @media (max-width: 50rem) {
- flex-direction: column;
-
- [data-slot="right"] {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
-
- [data-slot="command"] {
- all: unset;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- color: var(--color-text-secondary);
- font-size: 1.125rem;
- font-family: var(--font-mono);
- gap: var(--space-2);
- width: 100%;
-
- & > span {
- @media (max-width: 24rem) {
- font-size: 0.875rem;
- }
- @media (max-width: 56rem) {
- [data-slot="protocol"] {
- display: none;
- }
- }
- @media (max-width: 38rem) {
- text-align: center;
- span:first-child {
- display: block;
- }
- }
- }
- }
-
- [data-slot="highlight"] {
- color: var(--color-text);
- font-weight: 500;
- }
- }
-
- [data-component="features"] {
- border-top: 1px solid var(--color-border);
- padding: var(--padding);
-
- [data-slot="list"] {
- padding-left: var(--space-4);
- margin: 0;
- list-style: disc;
-
- li {
- margin-bottom: var(--space-4);
- line-height: 1.6;
-
- strong {
- text-transform: uppercase;
- font-weight: 600;
- }
-
- label {
- line-height: 1;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 0.03125rem;
- background: var(--color-border);
- padding: 0.125rem 0.375rem;
- color: var(--color-text-inverted);
- }
- }
-
- li:last-child {
- margin-bottom: 0;
- }
- }
- }
-
- [data-component="install"] {
- border-top: 1px solid var(--color-border);
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr 1fr;
-
- @media (max-width: 40rem) {
- grid-template-columns: 1fr;
- grid-template-rows: auto;
- }
- }
-
- [data-component="method"] {
- display: flex;
- padding: calc(var(--vertical-padding) / 2) calc(var(--padding) / 2) calc(var(--vertical-padding) / 2 + 0.125rem);
- flex-direction: column;
- text-align: left;
- gap: var(--space-2-5);
-
- @media (max-width: 30rem) {
- gap: 0.3125rem;
- }
-
- @media (max-width: 40rem) {
- text-align: left;
- }
-
- &:nth-child(2) {
- border-left: 1px solid var(--color-border);
-
- @media (max-width: 40rem) {
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
-
- &:nth-child(3) {
- border-top: 1px solid var(--color-border);
- }
-
- &:nth-child(4) {
- border-top: 1px solid var(--color-border);
- border-left: 1px solid var(--color-border);
-
- @media (max-width: 40rem) {
- border-left: none;
- }
- }
-
- [data-component="title"] {
- letter-spacing: -0.03125rem;
- text-transform: uppercase;
- font-weight: normal;
- font-size: 1rem;
- flex-shrink: 0;
- color: var(--color-text-dimmed);
-
- @media (max-width: 30rem) {
- font-size: 0.75rem;
- }
- }
-
- [data-slot="button"] {
- all: unset;
- cursor: pointer;
- display: flex;
- align-items: center;
- color: var(--color-text-secondary);
- gap: var(--space-2-5);
- font-size: 1rem;
-
- @media (max-width: 24rem) {
- font-size: 0.875rem;
- }
-
- strong {
- color: var(--color-text);
- font-weight: 500;
- }
-
- @media (max-width: 40rem) {
- justify-content: flex-start;
- }
-
- @media (max-width: 30rem) {
- justify-content: center;
- }
- }
- }
-
- [data-component="screenshots"] {
- border-top: 1px solid var(--color-border);
-
- figure {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: calc(var(--padding) / 4);
- padding: calc(var(--padding) / 2);
- border-width: 0;
- border-style: solid;
- border-color: var(--color-border);
- min-height: 0;
- overflow: hidden;
-
- & > div,
- figcaption {
- display: flex;
- align-items: center;
- }
-
- & > div {
- flex: 1;
- min-height: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- a {
- display: flex;
- flex: 1;
- min-height: 0;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- }
-
- figcaption {
- letter-spacing: -0.03125rem;
- text-transform: uppercase;
- color: var(--color-text-dimmed);
- flex-shrink: 0;
-
- @media (max-width: 30rem) {
- font-size: 0.75rem;
- }
- }
- }
-
- & > [data-slot="left"] figure {
- height: var(--images-height);
- box-sizing: border-box;
- }
-
- & > [data-slot="right"] figure {
- height: calc(var(--images-height) / 2);
- box-sizing: border-box;
- }
-
- & > [data-slot="left"] img {
- width: 100%;
- height: 100%;
- min-width: 0;
- object-fit: contain;
- }
-
- & > [data-slot="right"] img {
- width: 100%;
- height: calc(100% - 2rem);
- object-fit: contain;
- display: block;
- }
-
- @media (max-width: 30rem) {
- & {
- --images-height: auto;
- grid-template-columns: 1fr;
- grid-template-rows: auto auto;
- }
-
- & > [data-slot="left"] {
- grid-row: 1;
- grid-column: 1;
- }
-
- & > [data-slot="right"] {
- grid-row: 2;
- grid-column: 1;
- border-left: none;
- border-top: 1px solid var(--color-border);
-
- & > [data-slot="row1"],
- & > [data-slot="row2"] {
- height: auto;
- }
- }
-
- & > [data-slot="left"] figure,
- & > [data-slot="right"] figure {
- height: auto;
- }
-
- & > [data-slot="left"] img,
- & > [data-slot="right"] img {
- width: 100%;
- height: auto;
- max-height: none;
- }
- }
- }
-
- [data-component="copy-status"] {
- @media (max-width: 38rem) {
- display: none;
- }
-
- [data-slot="copy"] {
- display: block;
- width: var(--space-4);
- height: var(--space-4);
- color: var(--color-text-dimmed);
-
- [data-copied] & {
- display: none;
- }
- }
-
- [data-slot="check"] {
- display: none;
- width: var(--space-4);
- height: var(--space-4);
- color: var(--color-text);
-
- [data-copied] & {
- display: block;
- }
- }
- }
-
- [data-component="footer"] {
- border-top: 1px solid var(--color-border);
- display: flex;
- flex-direction: row;
-
- [data-slot="cell"] {
- flex: 1;
- text-align: center;
- text-transform: uppercase;
- padding: var(--vertical-padding) 0.5rem;
- }
-
- [data-slot="cell"] + [data-slot="cell"] {
- border-left: 1px solid var(--color-border);
- }
-
- /* Mobile: third column on its own row */
- @media (max-width: 30rem) {
- flex-wrap: wrap;
-
- [data-slot="cell"]:nth-child(1),
- [data-slot="cell"]:nth-child(2) {
- flex: 1;
- }
-
- [data-slot="cell"]:nth-child(3) {
- flex: 1 0 100%;
- border-left: none;
- border-top: 1px solid var(--color-border);
- }
- }
- }
-
- [data-component="legal"] {
- color: var(--color-text-dimmed);
- text-align: center;
-
- a {
- color: var(--color-text-dimmed);
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/index.tsx b/packages/cloud/app/src/routes/index.tsx
deleted file mode 100644
index 9075f4079..000000000
--- a/packages/cloud/app/src/routes/index.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import "./index.css"
-import { Title } from "@solidjs/meta"
-import { onCleanup, onMount } from "solid-js"
-import logoLight from "../asset/logo-ornate-light.svg"
-import logoDark from "../asset/logo-ornate-dark.svg"
-import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
-import { IconCopy, IconCheck } from "../component/icon"
-import { createAsync, query } from "@solidjs/router"
-import { getActor } from "~/context/auth"
-import { withActor } from "~/context/auth.withActor"
-import { Account } from "@opencode/cloud-core/account.js"
-
-function CopyStatus() {
- return (
- <div data-component="copy-status">
- <IconCopy data-slot="copy" />
- <IconCheck data-slot="check" />
- </div>
- )
-}
-
-const defaultWorkspace = query(async () => {
- "use server"
- const actor = await getActor()
- if (actor.type === "account") {
- const workspaces = await withActor(() => Account.workspaces())
- return workspaces[0].id
- }
-}, "defaultWorkspace")
-
-export default function Home() {
- const workspace = createAsync(() => defaultWorkspace())
- onMount(() => {
- const commands = document.querySelectorAll("[data-copy]")
- for (const button of commands) {
- const callback = () => {
- const text = button.textContent
- if (text) {
- navigator.clipboard.writeText(text)
- button.setAttribute("data-copied", "")
- setTimeout(() => {
- button.removeAttribute("data-copied")
- }, 1500)
- }
- }
- button.addEventListener("click", callback)
- onCleanup(() => {
- button.removeEventListener("click", callback)
- })
- }
- })
-
- return (
- <main data-page="home">
- <Title>opencode | AI coding agent built for the terminal</Title>
-
- <div data-component="content">
- <section data-component="top">
- <img data-slot="logo light" src={logoLight} alt="opencode logo light" />
- <img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
- <h1 data-slot="title">The AI coding agent built for the terminal</h1>
- <div data-slot="login">
- <a href="/auth">opencode zen</a>
- </div>
- </section>
-
- <section data-component="cta">
- <div data-slot="left">
- <a href="/docs">Get Started</a>
- </div>
- <div data-slot="center">
- <a href="/auth">opencode zen</a>
- </div>
- <div data-slot="right">
- <button data-copy data-slot="command">
- <span>
- <span>curl -fsSL </span>
- <span data-slot="protocol">https://</span>
- <span data-slot="highlight">opencode.ai/install</span>
- <span> | bash</span>
- </span>
- <CopyStatus />
- </button>
- </div>
- </section>
-
- <section data-component="features">
- <ul data-slot="list">
- <li>
- <strong>Native TUI</strong> A responsive, native, themeable terminal UI
- </li>
- <li>
- <strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
- </li>
- <li>
- <strong>opencode zen</strong> A <a href="/docs/zen">curated list of models</a> provided by opencode{" "}
- <label>New</label>
- </li>
- <li>
- <strong>Multi-session</strong> Start multiple agents in parallel on the same project
- </li>
- <li>
- <strong>Shareable links</strong> Share a link to any sessions for reference or to debug
- </li>
- <li>
- <strong>Claude Pro</strong> Log in with Anthropic to use your Claude Pro or Max account
- </li>
- <li>
- <strong>Use any model</strong> Supports 75+ LLM providers through{" "}
- <a href="https://models.dev">Models.dev</a>, including local models
- </li>
- </ul>
- </section>
-
- <section data-component="install">
- <div data-component="method">
- <h3 data-component="title">npm</h3>
- <button data-copy data-slot="button">
- <span>
- npm install -g <strong>opencode-ai</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">bun</h3>
- <button data-copy data-slot="button">
- <span>
- bun install -g <strong>opencode-ai</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">homebrew</h3>
- <button data-copy data-slot="button">
- <span>
- brew install <strong>sst/tap/opencode</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- <div data-component="method">
- <h3 data-component="title">paru</h3>
- <button data-copy data-slot="button">
- <span>
- paru -S <strong>opencode-bin</strong>
- </span>
- <CopyStatus />
- </button>
- </div>
- </section>
-
- <section data-component="screenshots">
- <figure>
- <figcaption>opencode TUI with the tokyonight theme</figcaption>
- <a href="/docs/cli">
- <img src={IMG_SPLASH} alt="opencode TUI with tokyonight theme" />
- </a>
- </figure>
- </section>
-
- <footer data-component="footer">
- <div data-slot="cell">
- <a href="https://x.com/opencode">X.com</a>
- </div>
- <div data-slot="cell">
- <a href="https://github.com/sst/opencode">GitHub</a>
- </div>
- <div data-slot="cell">
- <a href="https://opencode.ai/discord">Discord</a>
- </div>
- </footer>
- </div>
-
- <div data-component="legal">
- <span>
- ©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
- </span>
- </div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/s/[id].ts b/packages/cloud/app/src/routes/s/[id].ts
deleted file mode 100644
index 3fd1305a0..000000000
--- a/packages/cloud/app/src/routes/s/[id].ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-
-async function handler(evt: APIEvent) {
- const req = evt.request.clone()
- const url = new URL(req.url)
- const targetUrl = `https://docs.opencode.ai/docs${url.pathname}${url.search}`
- const response = await fetch(targetUrl, {
- method: req.method,
- headers: req.headers,
- body: req.body,
- })
- return response
-}
-
-export const GET = handler
-export const POST = handler
-export const PUT = handler
-export const DELETE = handler
-export const OPTIONS = handler
-export const PATCH = handler
diff --git a/packages/cloud/app/src/routes/stripe/webhook.ts b/packages/cloud/app/src/routes/stripe/webhook.ts
deleted file mode 100644
index 925ede1ac..000000000
--- a/packages/cloud/app/src/routes/stripe/webhook.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { Billing } from "@opencode/cloud-core/billing.js"
-import type { APIEvent } from "@solidjs/start/server"
-import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { BillingTable, PaymentTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { Resource } from "@opencode/cloud-resource"
-
-export async function POST(input: APIEvent) {
- const body = await Billing.stripe().webhooks.constructEventAsync(
- await input.request.text(),
- input.request.headers.get("stripe-signature")!,
- Resource.STRIPE_WEBHOOK_SECRET.value,
- )
-
- console.log(body.type, JSON.stringify(body, null, 2))
- if (body.type === "customer.updated") {
- // check default payment method changed
- const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
- if (!("default_payment_method" in prevInvoiceSettings)) return
-
- const customerID = body.data.object.id
- const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
-
- if (!customerID) throw new Error("Customer ID not found")
- if (!paymentMethodID) throw new Error("Payment method ID not found")
-
- const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
- await Database.use(async (tx) => {
- await tx
- .update(BillingTable)
- .set({
- paymentMethodID,
- paymentMethodLast4: paymentMethod.card!.last4,
- })
- .where(eq(BillingTable.customerID, customerID))
- })
- }
- if (body.type === "checkout.session.completed") {
- const workspaceID = body.data.object.metadata?.workspaceID
- const customerID = body.data.object.customer as string
- const paymentID = body.data.object.payment_intent as string
- const amount = body.data.object.amount_total
-
- if (!workspaceID) throw new Error("Workspace ID not found")
- if (!customerID) throw new Error("Customer ID not found")
- if (!amount) throw new Error("Amount not found")
- if (!paymentID) throw new Error("Payment ID not found")
-
- await Actor.provide("system", { workspaceID }, async () => {
- const customer = await Billing.get()
- if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
-
- // set customer metadata
- if (!customer?.customerID) {
- await Billing.stripe().customers.update(customerID, {
- metadata: {
- workspaceID,
- },
- })
- }
-
- // get payment method for the payment intent
- const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
- expand: ["payment_method"],
- })
- const paymentMethod = paymentIntent.payment_method
- if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
-
- await Database.transaction(async (tx) => {
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} + ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
- customerID,
- paymentMethodID: paymentMethod.id,
- paymentMethodLast4: paymentMethod.card!.last4,
- reload: true,
- reloadError: null,
- timeReloadError: null,
- })
- .where(eq(BillingTable.workspaceID, workspaceID))
- await tx.insert(PaymentTable).values({
- workspaceID,
- id: Identifier.create("payment"),
- amount: centsToMicroCents(Billing.CHARGE_AMOUNT),
- paymentID,
- customerID,
- })
- })
- })
- }
-
- console.log("finished handling")
-
- return Response.json("ok", { status: 200 })
-}
diff --git a/packages/cloud/app/src/routes/workspace.css b/packages/cloud/app/src/routes/workspace.css
deleted file mode 100644
index ed94365f0..000000000
--- a/packages/cloud/app/src/routes/workspace.css
+++ /dev/null
@@ -1,127 +0,0 @@
-[data-page="workspace"] {
- line-height: 1;
-
- /* Common elements */
- button {
- padding: var(--space-3) var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--border-radius-sm);
- background-color: var(--color-bg);
- color: var(--color-text);
- font-size: var(--font-size-sm);
- font-family: var(--font-sans);
- font-weight: 500;
- text-transform: uppercase;
- cursor: pointer;
- transition: all 0.15s ease;
-
- &:hover:not(:disabled) {
- background-color: var(--color-surface-hover);
- border-color: var(--color-accent);
- }
-
- &:active {
- transform: translateY(1px);
- }
-
- &:disabled {
- opacity: 0.5;
- transform: none;
- }
-
- &[data-color="primary"] {
- background-color: var(--color-primary);
- border-color: var(--color-primary);
- color: var(--color-primary-text);
-
- &:hover:not(:disabled) {
- background-color: var(--color-primary-hover);
- border-color: var(--color-primary-hover);
- }
- }
-
- &[data-color="ghost"] {
- background-color: transparent;
- border-color: transparent;
- color: var(--color-text-muted);
-
- &:hover:not(:disabled) {
- background-color: var(--color-surface-hover);
- border-color: var(--color-border);
- color: var(--color-text);
- }
- }
- }
-
- a {
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- }
-
- /* Workspace Header */
- [data-component="workspace-header"] {
- position: sticky;
- top: 0;
- z-index: 100;
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--space-4) var(--space-4);
- border-bottom: 1px solid var(--color-border);
- background-color: var(--color-bg);
-
- @media (max-width: 30rem) {
- padding: var(--space-4) var(--space-4);
- }
- }
-
- [data-slot="header-brand"] {
- flex: 0 0 auto;
- padding-top: 4px;
-
- svg {
- width: 138px;
- }
-
- [data-component="site-title"] {
- font-size: var(--font-size-lg);
- font-weight: 600;
- color: var(--color-text);
- text-decoration: none;
- letter-spacing: -0.02em;
- }
- }
-
- [data-slot="header-actions"] {
- display: flex;
- gap: var(--space-4);
- align-items: center;
- font-size: var(--font-size-sm);
-
- [data-slot="user"] {
- color: var(--color-text-muted);
- }
-
- @media (max-width: 30rem) {
- [data-slot="user"] {
- display: none;
- }
- }
-
- a,
- button {
- appearance: none;
- background: none;
- border: none;
- cursor: pointer;
- padding: 0;
- color: var(--color-text);
- text-decoration: underline;
- text-underline-offset: var(--space-0-75);
- text-decoration-thickness: 1px;
- text-transform: uppercase;
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/workspace.tsx b/packages/cloud/app/src/routes/workspace.tsx
deleted file mode 100644
index 3f08a70a0..000000000
--- a/packages/cloud/app/src/routes/workspace.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import "./workspace.css"
-import { useAuthSession } from "~/context/auth.session"
-import { IconLogo } from "../component/icon"
-import { withActor } from "~/context/auth.withActor"
-import {
- query,
- action,
- redirect,
- createAsync,
- RouteSectionProps,
- Navigate,
- useNavigate,
- useParams,
- A,
-} from "@solidjs/router"
-import { User } from "@opencode/cloud-core/user.js"
-import { Actor } from "@opencode/cloud-core/actor.js"
-import { getRequestEvent } from "solid-js/web"
-
-const getUserInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- const actor = Actor.assert("user")
- return await User.fromID(actor.properties.userID)
- }, workspaceID)
-}, "userInfo")
-
-const logout = action(async () => {
- "use server"
- const auth = await useAuthSession()
- const event = getRequestEvent()
- const current = auth.data.current
- if (current)
- await auth.update((val) => {
- delete val.account?.[current]
- const first = Object.keys(val.account ?? {})[0]
- val.current = first
- event!.locals.actor = undefined
- return val
- })
- throw redirect("/")
-})
-
-export default function WorkspaceLayout(props: RouteSectionProps) {
- const params = useParams()
- const userInfo = createAsync(() => getUserInfo(params.id))
- return (
- <main data-page="workspace">
- <header data-component="workspace-header">
- <div data-slot="header-brand">
- <A href="/" data-component="site-title">
- <IconLogo />
- </A>
- </div>
- <div data-slot="header-actions">
- <span data-slot="user">{userInfo()?.email}</span>
- <form action={logout} method="post">
- <button type="submit" formaction={logout}>
- Logout
- </button>
- </form>
- </div>
- </header>
- <div>{props.children}</div>
- </main>
- )
-}
diff --git a/packages/cloud/app/src/routes/workspace/[id].css b/packages/cloud/app/src/routes/workspace/[id].css
deleted file mode 100644
index 8b318a19f..000000000
--- a/packages/cloud/app/src/routes/workspace/[id].css
+++ /dev/null
@@ -1,115 +0,0 @@
-[data-page="workspace-[id]"] {
- max-width: 64rem;
- padding: var(--space-10) var(--space-4);
- margin: 0 auto;
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: var(--space-10);
-
- @media (max-width: 30rem) {
- padding-top: var(--space-4);
- padding-bottom: var(--space-4);
-
- gap: var(--space-8);
- }
-
- [data-slot="sections"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-16);
-
- @media (max-width: 30rem) {
- gap: var(--space-8);
- }
-
- section {
- display: flex;
- flex-direction: column;
- gap: var(--space-8);
-
- @media (max-width: 30rem) {
- gap: var(--space-6);
- }
-
- /* Section titles */
- [data-slot="section-title"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-
- h2 {
- font-size: var(--font-size-md);
- font-weight: 600;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- color: var(--color-text-secondary);
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-md);
- }
- }
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-md);
- color: var(--color-text-muted);
-
- a {
- color: var(--color-text-muted);
- }
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-sm);
- }
- }
- }
- }
- section:not(:last-child) {
- border-bottom: 1px solid var(--color-border);
- padding-bottom: var(--space-16);
-
- @media (max-width: 30rem) {
- padding-bottom: var(--space-8);
- }
- }
- }
-
- /* Title section */
- [data-component="title-section"] {
- display: flex;
- flex-direction: column;
- gap: var(--space-2);
- padding-bottom: var(--space-8);
- border-bottom: 1px solid var(--color-border);
-
- @media (max-width: 30rem) {
- padding-bottom: var(--space-6);
- }
-
- h1 {
- font-size: var(--font-size-2xl);
- font-weight: 500;
- line-height: 1.2;
- letter-spacing: -0.03125rem;
- margin: 0;
- text-transform: uppercase;
-
- @media (max-width: 30rem) {
- font-size: var(--font-size-xl);
- }
- }
-
- p {
- line-height: 1.5;
- font-size: var(--font-size-md);
- color: var(--color-text-muted);
-
- a {
- color: var(--color-text-muted);
- }
- }
- }
-}
diff --git a/packages/cloud/app/src/routes/workspace/[id].tsx b/packages/cloud/app/src/routes/workspace/[id].tsx
deleted file mode 100644
index 4a2c3424d..000000000
--- a/packages/cloud/app/src/routes/workspace/[id].tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import "./[id].css"
-import { Billing } from "@opencode/cloud-core/billing.js"
-import { query, useParams, createAsync } from "@solidjs/router"
-import { Show } from "solid-js"
-import { withActor } from "~/context/auth.withActor"
-import { MonthlyLimitSection } from "~/component/workspace/monthly-limit-section"
-import { NewUserSection } from "~/component/workspace/new-user-section"
-import { BillingSection } from "~/component/workspace/billing-section"
-import { PaymentSection } from "~/component/workspace/payment-section"
-import { UsageSection } from "~/component/workspace/usage-section"
-import { KeySection } from "~/component/workspace/key-section"
-
-const getBillingInfo = query(async (workspaceID: string) => {
- "use server"
- return withActor(async () => {
- return await Billing.get()
- }, workspaceID)
-}, "billing.get")
-
-export default function () {
- const params = useParams()
- const balanceInfo = createAsync(() => getBillingInfo(params.id))
-
- return (
- <div data-page="workspace-[id]">
- <section data-component="title-section">
- <h1>Zen</h1>
- <p>
- Curated list of models provided by opencode.{" "}
- <a target="_blank" href="/docs/zen">
- Learn more
- </a>
- .
- </p>
- </section>
-
- <div data-slot="sections">
- <NewUserSection />
- <KeySection />
- <BillingSection />
- <Show when={true}>
- {/*<Show when={balanceInfo()?.reload}>*/}
- <MonthlyLimitSection />
- </Show>
- <UsageSection />
- <PaymentSection />
- </div>
- </div>
- )
-}
diff --git a/packages/cloud/app/src/routes/workspace/index.tsx b/packages/cloud/app/src/routes/workspace/index.tsx
deleted file mode 100644
index e69de29bb..000000000
--- a/packages/cloud/app/src/routes/workspace/index.tsx
+++ /dev/null
diff --git a/packages/cloud/app/src/routes/zen/handler.ts b/packages/cloud/app/src/routes/zen/handler.ts
deleted file mode 100644
index ab1fc6599..000000000
--- a/packages/cloud/app/src/routes/zen/handler.ts
+++ /dev/null
@@ -1,594 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import path from "node:path"
-import { and, Database, eq, isNull, lt, or, sql } from "@opencode/cloud-core/drizzle/index.js"
-import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
-import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
-import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
-import { Identifier } from "@opencode/cloud-core/identifier.js"
-import { Resource } from "@opencode/cloud-resource"
-import { Billing } from "../../../../core/src/billing"
-import { Actor } from "@opencode/cloud-core/actor.js"
-
-type ModelCost = {
- input: number
- output: number
- cacheRead?: number
- cacheWrite5m?: number
- cacheWrite1h?: number
-}
-
-type Model = {
- id: string
- auth: boolean
- cost: ModelCost | ((usage: any) => ModelCost)
- headerMappings: Record<string, string>
- providers: Record<
- string,
- {
- api: string
- apiKey: string
- model: string
- weight?: number
- }
- >
-}
-
-export async function handler(
- input: APIEvent,
- opts: {
- modifyBody?: (body: any) => any
- setAuthHeader: (headers: Headers, apiKey: string) => void
- parseApiKey: (headers: Headers) => string | undefined
- onStreamPart: (chunk: string) => void
- getStreamUsage: () => any
- normalizeUsage: (body: any) => {
- inputTokens: number
- outputTokens: number
- reasoningTokens?: number
- cacheReadTokens?: number
- cacheWrite5mTokens?: number
- cacheWrite1hTokens?: number
- }
- },
-) {
- class AuthError extends Error {}
- class CreditsError extends Error {}
- class MonthlyLimitError extends Error {}
- class ModelError extends Error {}
-
- const MODELS: Record<string, Model> = {
- "claude-opus-4-1": {
- id: "claude-opus-4-1" as const,
- auth: true,
- cost: {
- input: 0.000015,
- output: 0.000075,
- cacheRead: 0.0000015,
- cacheWrite5m: 0.00001875,
- cacheWrite1h: 0.00003,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-opus-4-1-20250805",
- },
- },
- },
- "claude-sonnet-4": {
- id: "claude-sonnet-4" as const,
- auth: true,
- cost: (usage: any) => {
- const totalInputTokens =
- usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
- return totalInputTokens <= 200_000
- ? {
- input: 0.000003,
- output: 0.000015,
- cacheRead: 0.0000003,
- cacheWrite5m: 0.00000375,
- cacheWrite1h: 0.000006,
- }
- : {
- input: 0.000006,
- output: 0.0000225,
- cacheRead: 0.0000006,
- cacheWrite5m: 0.0000075,
- cacheWrite1h: 0.000012,
- }
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-sonnet-4-20250514",
- },
- },
- },
- "claude-3-5-haiku": {
- id: "claude-3-5-haiku" as const,
- auth: true,
- cost: {
- input: 0.0000008,
- output: 0.000004,
- cacheRead: 0.00000008,
- cacheWrite5m: 0.000001,
- cacheWrite1h: 0.0000016,
- },
- headerMappings: {},
- providers: {
- anthropic: {
- api: "https://api.anthropic.com",
- apiKey: Resource.ANTHROPIC_API_KEY.value,
- model: "claude-3-5-haiku-20241022",
- },
- },
- },
- "gpt-5": {
- id: "gpt-5" as const,
- auth: true,
- cost: {
- input: 0.00000125,
- output: 0.00001,
- cacheRead: 0.000000125,
- },
- headerMappings: {},
- providers: {
- openai: {
- api: "https://api.openai.com",
- apiKey: Resource.OPENAI_API_KEY.value,
- model: "gpt-5",
- },
- },
- },
- "qwen3-coder": {
- id: "qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000045,
- output: 0.0000018,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 4,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- "kimi-k2": {
- id: "kimi-k2" as const,
- auth: true,
- cost: {
- input: 0.0000006,
- output: 0.0000025,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "moonshotai/Kimi-K2-Instruct-0905",
- //weight: 4,
- },
- //fireworks: {
- // api: "https://api.fireworks.ai/inference",
- // apiKey: Resource.FIREWORKS_API_KEY.value,
- // model: "accounts/fireworks/models/kimi-k2-instruct-0905",
- // weight: 1,
- //},
- },
- },
- "grok-code": {
- id: "grok-code" as const,
- auth: false,
- cost: {
- input: 0,
- output: 0,
- cacheRead: 0,
- },
- headerMappings: {
- "x-grok-conv-id": "x-opencode-session",
- "x-grok-req-id": "x-opencode-request",
- },
- providers: {
- xai: {
- api: "https://api.x.ai",
- apiKey: Resource.XAI_API_KEY.value,
- model: "grok-code",
- },
- },
- },
- // deprecated
- "qwen/qwen3-coder": {
- id: "qwen/qwen3-coder" as const,
- auth: true,
- cost: {
- input: 0.00000038,
- output: 0.00000153,
- },
- headerMappings: {},
- providers: {
- baseten: {
- api: "https://inference.baseten.co",
- apiKey: Resource.BASETEN_API_KEY.value,
- model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
- weight: 5,
- },
- fireworks: {
- api: "https://api.fireworks.ai/inference",
- apiKey: Resource.FIREWORKS_API_KEY.value,
- model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
- weight: 1,
- },
- },
- },
- }
-
- const FREE_WORKSPACES = [
- "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // frank
- ]
-
- const logger = {
- metric: (values: Record<string, any>) => {
- console.log(`_metric:${JSON.stringify(values)}`)
- },
- log: console.log,
- debug: (message: string) => {
- if (Resource.App.stage === "production") return
- console.debug(message)
- },
- }
-
- try {
- const url = new URL(input.request.url)
- const body = await input.request.json()
- logger.debug(JSON.stringify(body))
- logger.metric({
- is_tream: !!body.stream,
- session: input.request.headers.get("x-opencode-session"),
- request: input.request.headers.get("x-opencode-request"),
- })
- const MODEL = validateModel()
- const apiKey = await authenticate()
- const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
- await checkCreditsAndLimit()
- const providerName = selectProvider()
- const providerData = MODEL.providers[providerName]
- logger.metric({ provider: providerName })
-
- // Request to model provider
- const startTimestamp = Date.now()
- const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
- method: "POST",
- headers: (() => {
- const headers = input.request.headers
- headers.delete("host")
- headers.delete("content-length")
- opts.setAuthHeader(headers, providerData.apiKey)
- Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
- headers.set(k, headers.get(v)!)
- })
- return headers
- })(),
- body: JSON.stringify({
- ...(opts.modifyBody?.(body) ?? body),
- model: providerData.model,
- }),
- })
-
- // Scrub response headers
- const resHeaders = new Headers()
- const keepHeaders = ["content-type", "cache-control"]
- for (const [k, v] of res.headers.entries()) {
- if (keepHeaders.includes(k.toLowerCase())) {
- resHeaders.set(k, v)
- }
- }
-
- // Handle non-streaming response
- if (!body.stream) {
- const json = await res.json()
- const body = JSON.stringify(json)
- logger.metric({ response_length: body.length })
- logger.debug(body)
- await trackUsage(json.usage)
- await reload()
- return new Response(body, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
- }
-
- // Handle streaming response
- const stream = new ReadableStream({
- start(c) {
- const reader = res.body?.getReader()
- const decoder = new TextDecoder()
- let buffer = ""
- let responseLength = 0
-
- function pump(): Promise<void> {
- return (
- reader?.read().then(async ({ done, value }) => {
- if (done) {
- logger.metric({ response_length: responseLength })
- const usage = opts.getStreamUsage()
- if (usage) {
- await trackUsage(usage)
- await reload()
- }
- c.close()
- return
- }
-
- if (responseLength === 0) {
- logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
- }
- responseLength += value.length
- buffer += decoder.decode(value, { stream: true })
-
- const parts = buffer.split("\n\n")
- buffer = parts.pop() ?? ""
-
- for (const part of parts) {
- logger.debug(part)
- opts.onStreamPart(part.trim())
- }
-
- c.enqueue(value)
-
- return pump()
- }) || Promise.resolve()
- )
- }
-
- return pump()
- },
- })
-
- return new Response(stream, {
- status: res.status,
- statusText: res.statusText,
- headers: resHeaders,
- })
-
- function validateModel() {
- if (!(body.model in MODELS)) {
- throw new ModelError(`Model ${body.model} not supported`)
- }
- const model = MODELS[body.model as keyof typeof MODELS]
- logger.metric({ model: model.id })
- return model
- }
-
- async function authenticate() {
- try {
- const apiKey = opts.parseApiKey(input.request.headers)
- if (!apiKey) throw new AuthError("Missing API key.")
-
- const key = await Database.use((tx) =>
- tx
- .select({
- id: KeyTable.id,
- workspaceID: KeyTable.workspaceID,
- })
- .from(KeyTable)
- .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
- .then((rows) => rows[0]),
- )
-
- if (!key) throw new AuthError("Invalid API key.")
- logger.metric({
- api_key: key.id,
- workspace: key.workspaceID,
- })
- return key
- } catch (e) {
- // ignore error if model does not require authentication
- if (!MODEL.auth) return
- throw e
- }
- }
-
- async function checkCreditsAndLimit() {
- if (!apiKey || !MODEL.auth || isFree) return
-
- const billing = await Database.use((tx) =>
- tx
- .select({
- balance: BillingTable.balance,
- paymentMethodID: BillingTable.paymentMethodID,
- monthlyLimit: BillingTable.monthlyLimit,
- monthlyUsage: BillingTable.monthlyUsage,
- timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
- })
- .from(BillingTable)
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- .then((rows) => rows[0]),
- )
-
- if (!billing.paymentMethodID) throw new CreditsError("No payment method")
- if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
- if (
- billing.monthlyLimit &&
- billing.monthlyUsage &&
- billing.timeMonthlyUsageUpdated &&
- billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100)
- ) {
- const now = new Date()
- const currentYear = now.getUTCFullYear()
- const currentMonth = now.getUTCMonth()
- const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear()
- const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth()
- if (currentYear === dateYear && currentMonth === dateMonth)
- throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`)
- }
- }
-
- function selectProvider() {
- const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
- Array<string>(provider.weight ?? 1).fill(name),
- )
- return picks[Math.floor(Math.random() * picks.length)]
- }
-
- async function trackUsage(usage: any) {
- const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
- opts.normalizeUsage(usage)
-
- const modelCost = typeof MODEL.cost === "function" ? MODEL.cost(usage) : MODEL.cost
-
- const inputCost = modelCost.input * inputTokens * 100
- const outputCost = modelCost.output * outputTokens * 100
- const reasoningCost = (() => {
- if (!reasoningTokens) return undefined
- return modelCost.output * reasoningTokens * 100
- })()
- const cacheReadCost = (() => {
- if (!cacheReadTokens) return undefined
- if (!modelCost.cacheRead) return undefined
- return modelCost.cacheRead * cacheReadTokens * 100
- })()
- const cacheWrite5mCost = (() => {
- if (!cacheWrite5mTokens) return undefined
- if (!modelCost.cacheWrite5m) return undefined
- return modelCost.cacheWrite5m * cacheWrite5mTokens * 100
- })()
- const cacheWrite1hCost = (() => {
- if (!cacheWrite1hTokens) return undefined
- if (!modelCost.cacheWrite1h) return undefined
- return modelCost.cacheWrite1h * cacheWrite1hTokens * 100
- })()
- const totalCostInCent =
- inputCost +
- outputCost +
- (reasoningCost ?? 0) +
- (cacheReadCost ?? 0) +
- (cacheWrite5mCost ?? 0) +
- (cacheWrite1hCost ?? 0)
-
- logger.metric({
- "tokens.input": inputTokens,
- "tokens.output": outputTokens,
- "tokens.reasoning": reasoningTokens,
- "tokens.cache_read": cacheReadTokens,
- "tokens.cache_write_5m": cacheWrite5mTokens,
- "tokens.cache_write_1h": cacheWrite1hTokens,
- "cost.input": Math.round(inputCost),
- "cost.output": Math.round(outputCost),
- "cost.reasoning": reasoningCost ? Math.round(reasoningCost) : undefined,
- "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined,
- "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined,
- "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined,
- "cost.total": Math.round(totalCostInCent),
- })
-
- if (!apiKey) return
-
- const cost = isFree ? 0 : centsToMicroCents(totalCostInCent)
- await Database.transaction(async (tx) => {
- await tx.insert(UsageTable).values({
- workspaceID: apiKey.workspaceID,
- id: Identifier.create("usage"),
- model: MODEL.id,
- provider: providerName,
- inputTokens,
- outputTokens,
- reasoningTokens,
- cacheReadTokens,
- cacheWrite5mTokens,
- cacheWrite1hTokens,
- cost,
- })
- await tx
- .update(BillingTable)
- .set({
- balance: sql`${BillingTable.balance} - ${cost}`,
- monthlyUsage: sql`
- CASE
- WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${cost}
- ELSE ${cost}
- END
- `,
- timeMonthlyUsageUpdated: sql`now()`,
- })
- .where(eq(BillingTable.workspaceID, apiKey.workspaceID))
- })
-
- await Database.use((tx) =>
- tx
- .update(KeyTable)
- .set({ timeUsed: sql`now()` })
- .where(eq(KeyTable.id, apiKey.id)),
- )
- }
-
- async function reload() {
- if (!apiKey) return
-
- const lock = await Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- timeReloadLockedTill: sql`now() + interval 1 minute`,
- })
- .where(
- and(
- eq(BillingTable.workspaceID, apiKey.workspaceID),
- eq(BillingTable.reload, true),
- lt(BillingTable.balance, centsToMicroCents(Billing.CHARGE_THRESHOLD)),
- or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
- ),
- ),
- )
- if (lock.rowsAffected === 0) return
-
- await Actor.provide("system", { workspaceID: apiKey.workspaceID }, async () => {
- await Billing.reload()
- })
- }
- } catch (error: any) {
- logger.metric({
- "error.type": error.constructor.name,
- "error.message": error.message,
- })
-
- // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
- if (
- error instanceof AuthError ||
- error instanceof CreditsError ||
- error instanceof MonthlyLimitError ||
- error instanceof ModelError
- )
- return new Response(
- JSON.stringify({
- type: "error",
- error: { type: error.constructor.name, message: error.message },
- }),
- { status: 401 },
- )
-
- return new Response(
- JSON.stringify({
- type: "error",
- error: {
- type: "error",
- message: error.message,
- },
- }),
- { status: 500 },
- )
- }
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts b/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
deleted file mode 100644
index 801557324..000000000
--- a/packages/cloud/app/src/routes/zen/v1/chat/completions.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- prompt_tokens?: number
- completion_tokens?: number
- total_tokens?: number
- prompt_tokens_details?: {
- text_tokens?: number
- audio_tokens?: number
- image_tokens?: number
- cached_tokens?: number
- }
- completion_tokens_details?: {
- reasoning_tokens?: number
- audio_tokens?: number
- accepted_prediction_tokens?: number
- rejected_prediction_tokens?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- ...(body.stream ? { stream_options: { include_usage: true } } : {}),
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- if (!chunk.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(chunk.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = json.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.prompt_tokens ?? 0,
- outputTokens: usage.completion_tokens ?? 0,
- reasoningTokens: usage.completion_tokens_details?.reasoning_tokens ?? undefined,
- cacheReadTokens: usage.prompt_tokens_details?.cached_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/messages.ts b/packages/cloud/app/src/routes/zen/v1/messages.ts
deleted file mode 100644
index 1fd85d5c7..000000000
--- a/packages/cloud/app/src/routes/zen/v1/messages.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- cache_creation?: {
- ephemeral_5m_input_tokens?: number
- ephemeral_1h_input_tokens?: number
- }
- cache_creation_input_tokens?: number
- cache_read_input_tokens?: number
- input_tokens?: number
- output_tokens?: number
- server_tool_use?: {
- web_search_requests?: number
- }
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- modifyBody: (body: any) => ({
- ...body,
- service_tier: "standard_only",
- }),
- setAuthHeader: (headers: Headers, apiKey: string) => headers.set("x-api-key", apiKey),
- parseApiKey: (headers: Headers) => headers.get("x-api-key") ?? undefined,
- onStreamPart: (chunk: string) => {
- const data = chunk.split("\n")[1]
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { usage?: Usage }
- } catch (e) {
- return
- }
-
- if (!json.usage) return
- usage = {
- ...usage,
- ...json.usage,
- cache_creation: {
- ...usage?.cache_creation,
- ...json.usage.cache_creation,
- },
- server_tool_use: {
- ...usage?.server_tool_use,
- ...json.usage.server_tool_use,
- },
- }
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => ({
- inputTokens: usage.input_tokens ?? 0,
- outputTokens: usage.output_tokens ?? 0,
- cacheReadTokens: usage.cache_read_input_tokens ?? undefined,
- cacheWrite5mTokens: usage.cache_creation?.ephemeral_5m_input_tokens ?? undefined,
- cacheWrite1hTokens: usage.cache_creation?.ephemeral_1h_input_tokens ?? undefined,
- }),
- })
-}
diff --git a/packages/cloud/app/src/routes/zen/v1/responses.ts b/packages/cloud/app/src/routes/zen/v1/responses.ts
deleted file mode 100644
index 486c129b9..000000000
--- a/packages/cloud/app/src/routes/zen/v1/responses.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { APIEvent } from "@solidjs/start/server"
-import { handler } from "~/routes/zen/handler"
-
-type Usage = {
- input_tokens?: number
- input_tokens_details?: {
- cached_tokens?: number
- }
- output_tokens?: number
- output_tokens_details?: {
- reasoning_tokens?: number
- }
- total_tokens?: number
-}
-
-export function POST(input: APIEvent) {
- let usage: Usage
- return handler(input, {
- setAuthHeader: (headers: Headers, apiKey: string) => {
- headers.set("authorization", `Bearer ${apiKey}`)
- },
- parseApiKey: (headers: Headers) => headers.get("authorization")?.split(" ")[1],
- onStreamPart: (chunk: string) => {
- const [event, data] = chunk.split("\n")
- if (event !== "event: response.completed") return
- if (!data.startsWith("data: ")) return
-
- let json
- try {
- json = JSON.parse(data.slice(6)) as { response?: { usage?: Usage } }
- } catch (e) {
- return
- }
-
- if (!json.response?.usage) return
- usage = json.response.usage
- },
- getStreamUsage: () => usage,
- normalizeUsage: (usage: Usage) => {
- const inputTokens = usage.input_tokens ?? 0
- const outputTokens = usage.output_tokens ?? 0
- const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? undefined
- const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? undefined
- return {
- inputTokens: inputTokens - (cacheReadTokens ?? 0),
- outputTokens: outputTokens - (reasoningTokens ?? 0),
- reasoningTokens,
- cacheReadTokens,
- }
- },
- })
-}