Compare commits

...

55 Commits

Author SHA1 Message Date
usrnk1 c97eb2d6fc feat(desktop): preserve session model selection 2026-06-30 14:16:10 +02:00
runvip b329fcceb2 fix(docs): fix Russian translation for index.mdx (#34001)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-06-30 16:47:44 +05:30
opencode-agent[bot] bd83177c48 chore: generate 2026-06-30 11:07:49 +00:00
Brendan Allan 06cb0de0b1 feat(app): autocomplete mcp resources (#34597) 2026-06-30 19:05:53 +08:00
opencode-agent[bot] 3ca89ac796 fix(app): autocomplete configured references (#34308)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-06-30 18:45:37 +08:00
Brendan Allan 3aa2860058 fix(app): restore prompt cursor on focus (#34175) 2026-06-30 18:40:20 +08:00
usrnk1 ad63cf1590 feat(app): hide separators around active tabs (#34591) 2026-06-30 18:38:42 +08:00
Brendan Allan aa56750f8b fix(desktop): persist last active url (#34595) 2026-06-30 18:32:30 +08:00
opencode-agent[bot] 4aaed42640 fix(app): preserve macos titlebar inset (#34594)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 18:17:55 +08:00
usrnk1 f69f677a32 feat(desktop): add hover background to session title and single-click edit (#34589) 2026-06-30 17:46:52 +08:00
opencode-agent[bot] 8289883de8 chore: generate 2026-06-30 09:11:40 +00:00
usrnk1 003c22b4a3 fix(desktop): context menu button / tab intermittent issue (#34420)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-06-30 17:09:38 +08:00
opencode-agent[bot] 1982d9832b fix(session-ui): align code copy tooltip with v2 style (#34459)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 17:01:38 +08:00
opencode-agent[bot] 90f0576222 fix(session-ui): deselect review line on draft cancel (#34585)
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
2026-06-30 16:26:20 +08:00
usrnk1 e34822db6b fix(desktop): preserve selected model during session promotion (#34466) 2026-06-30 16:23:39 +08:00
Frank b19ae6547f sync 2026-06-30 03:23:50 -04:00
opencode-agent[bot] 6b074ed718 fix(app): suppress middle-click tab auxclick (#34355)
deploy / deploy (push) Has been cancelled
nix-eval / nix-eval (push) Has been cancelled
publish / version (push) Has been cancelled
publish / build-cli (push) Has been cancelled
publish / sign-cli-windows (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Has been cancelled
publish / publish (push) Has been cancelled
storybook / storybook build (push) Has been cancelled
docs-locale-sync / sync-locales (push) Has been cancelled
generate / generate (push) Has been cancelled
typecheck / typecheck (push) Has been cancelled
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 16:32:25 +10:00
opencode-agent[bot] e687eb936b fix(app): register export logs globally (#34352)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 16:31:09 +10:00
opencode-agent[bot] 797cb530e5 chore: generate 2026-06-30 05:09:55 +00:00
Dax 0a5bed2bc2 feat(tui): add yolo permission mode (#33279)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-06-30 00:08:29 -05:00
opencode-agent[bot] ba41dadc91 ci: skip issue actions for team authors (#34556)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-06-29 23:48:58 -05:00
James Long 451876b0bd refactor(opencode): remove core service layer exports (#34518) 2026-06-29 23:32:16 -04:00
James Long 91561bbdfb refactor(opencode): remove app service layer exports (#34517) 2026-06-29 23:03:57 -04:00
opencode-agent[bot] 018ce34b39 revert(core): skip fff in node runtime (#34549)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 12:58:57 +10:00
opencode-agent[bot] ce82b21073 chore: generate 2026-06-30 02:55:10 +00:00
James Long 1f37c26574 refactor(opencode): use layer nodes in remaining harnesses (#34516) 2026-06-29 22:53:42 -04:00
opencode-agent[bot] cb3e28d207 fix(app): collapse review pane by default (#34165)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 12:47:00 +10:00
opencode-agent[bot] 8f1e13f299 fix(core): skip fff in node runtime (#34353)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
2026-06-30 12:38:47 +10:00
Luke Parker 2fe68b5e91 fix(ui): prevent tool status blank frame (#34547)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 12:26:04 +10:00
opencode-agent[bot] 2ace237938 chore: generate 2026-06-30 01:23:03 +00:00
James Long 5b62211167 refactor(opencode): build runtimes from layer nodes (#34515) 2026-06-29 21:21:28 -04:00
Frank 60b6229c4e zen: track region
deploy / deploy (push) Has been cancelled
2026-06-29 19:57:13 -04:00
opencode-agent[bot] 4caec80e67 chore: generate 2026-06-29 23:03:24 +00:00
Frank e9d4ca7e0f zen: track region 2026-06-29 18:57:32 -04:00
Aiden Cline d71454c701 fix(core): expose models.dev modes as models (#34521) 2026-06-29 17:51:06 -05:00
Dustin Deus 884c256033 fix(console): cancel upstream provider requests (#34467)
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
2026-06-29 23:28:04 +02:00
opencode-agent[bot] 78235385dd chore: generate 2026-06-29 20:53:48 +00:00
Aiden Cline fd213e6df6 fix(mcp): prefer content over structured output (#34505) 2026-06-29 15:51:52 -05:00
James Long 3726052307 refactor(opencode): use layer nodes in server tests (#34503) 2026-06-29 16:50:59 -04:00
opencode-agent[bot] 7d33a6f7c9 chore: generate 2026-06-29 20:36:56 +00:00
James Long 7a035d7fc0 refactor(opencode): bind instance bootstrap node (#34502) 2026-06-29 16:35:10 -04:00
opencode-agent[bot] 9151af7045 chore: generate 2026-06-29 20:24:42 +00:00
James Long 15bcbb1d7a refactor(opencode): migrate session tests to layer nodes (#34494) 2026-06-29 16:22:50 -04:00
opencode-agent[bot] 4d3294727c chore: generate 2026-06-29 20:07:10 +00:00
James Long aae0e89519 refactor(opencode): use layer nodes in plugin tests (#34495) 2026-06-29 16:04:58 -04:00
opencode-agent[bot] 93a7b4ab76 chore: generate 2026-06-29 19:58:09 +00:00
James Long 0ebe74b625 refactor(opencode): migrate llm tests to layer nodes (#34479) 2026-06-29 15:55:58 -04:00
James Long b9dae8593c refactor(opencode): migrate compaction and workspace tests to layer nodes (#34478) 2026-06-29 15:42:52 -04:00
opencode-agent[bot] d8f9388610 chore: generate 2026-06-29 18:52:55 +00:00
James Long be14739cce refactor(core): convert config tests to nodes (#34474) 2026-06-29 14:51:13 -04:00
James Long 762588c251 refactor(core): convert prompt tests to nodes (#34470) 2026-06-29 14:25:33 -04:00
opencode-agent[bot] d6e54e9042 chore: generate 2026-06-29 17:58:20 +00:00
James Long d4fd528152 refactor(core): convert more opencode tests to nodes (#34464) 2026-06-29 13:56:28 -04:00
opencode-agent[bot] de185559dd chore: generate 2026-06-29 16:58:51 +00:00
James Long bc5ce5eab1 refactor(core): convert opencode tests to nodes (#34453) 2026-06-29 12:56:44 -04:00
241 changed files with 5539 additions and 1717 deletions
+26 -2
View File
@@ -35,13 +35,37 @@ jobs:
const now = Date.now();
const twoHours = 2 * 60 * 60 * 1000;
const orgMemberAssociations = new Set(['OWNER', 'MEMBER']);
const agentLogin = 'opencode-agent[bot]';
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'dev',
});
const teamMembers = new Set(
Buffer.from(file.content, 'base64')
.toString()
.split('\n')
.map((line) => line.trim().toLowerCase())
.filter(Boolean)
);
function isExempt(item) {
const login = item.user?.login?.toLowerCase();
return (
login === agentLogin ||
orgMemberAssociations.has(item.author_association) ||
(login && teamMembers.has(login))
);
}
for (const item of items) {
const isPR = !!item.pull_request;
const kind = isPR ? 'PR' : 'issue';
const login = item.user?.login;
if (orgMemberAssociations.has(item.author_association)) {
core.info(`Skipping ${kind} #${item.number}; author association is ${item.author_association}`);
if (isExempt(item)) {
core.info(`Skipping ${kind} #${item.number}; author ${login || 'unknown'} is exempt`);
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
+38
View File
@@ -17,12 +17,31 @@ jobs:
with:
fetch-depth: 1
- name: Check exempt issue author
id: author
run: |
LOGIN="${{ github.event.issue.user.login }}"
ASSOCIATION="${{ github.event.issue.author_association }}"
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
[ "$ASSOCIATION" = "OWNER" ] ||
[ "$ASSOCIATION" = "MEMBER" ] ||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- uses: ./.github/actions/setup-bun
if: steps.author.outputs.skip != 'true'
- name: Install opencode
if: steps.author.outputs.skip != 'true'
run: curl -fsSL https://opencode.ai/install | bash
- name: Check duplicates and compliance
if: steps.author.outputs.skip != 'true'
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -132,12 +151,31 @@ jobs:
with:
fetch-depth: 1
- name: Check exempt issue author
id: author
run: |
LOGIN="${{ github.event.issue.user.login }}"
ASSOCIATION="${{ github.event.issue.author_association }}"
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
[ "$ASSOCIATION" = "OWNER" ] ||
[ "$ASSOCIATION" = "MEMBER" ] ||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- uses: ./.github/actions/setup-bun
if: steps.author.outputs.skip != 'true'
- name: Install opencode
if: steps.author.outputs.skip != 'true'
run: curl -fsSL https://opencode.ai/install | bash
- name: Recheck compliance
if: steps.author.outputs.skip != 'true'
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+19
View File
@@ -16,13 +16,32 @@ jobs:
with:
fetch-depth: 1
- name: Check exempt issue author
id: author
run: |
LOGIN="${{ github.event.issue.user.login }}"
ASSOCIATION="${{ github.event.issue.author_association }}"
if [ "$LOGIN" = "opencode-agent[bot]" ] ||
[ "$ASSOCIATION" = "OWNER" ] ||
[ "$ASSOCIATION" = "MEMBER" ] ||
grep -qxiF "$LOGIN" .github/TEAM_MEMBERS; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Skipping issue automation for exempt author: $LOGIN ($ASSOCIATION)"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Bun
if: steps.author.outputs.skip != 'true'
uses: ./.github/actions/setup-bun
- name: Install opencode
if: steps.author.outputs.skip != 'true'
run: curl -fsSL https://opencode.ai/install | bash
- name: Triage issue
if: steps.author.outputs.skip != 'true'
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+26 -1
View File
@@ -28,7 +28,7 @@ import {
Show,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import { CommandProvider } from "@/context/command"
import { CommandProvider, useCommand, type CommandOption } from "@/context/command"
import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file"
import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
@@ -40,6 +40,7 @@ import { LayoutProvider } from "@/context/layout"
import { ModelsProvider } from "@/context/models"
import { NotificationProvider, useNotification } from "@/context/notification"
import { PermissionProvider } from "@/context/permission"
import { usePlatform } from "@/context/platform"
import { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider, useSettings } from "@/context/settings"
@@ -300,12 +301,36 @@ function SharedProviders(props: ParentProps) {
<>
<BodyDesignClass />
<CommandProvider>
<DesktopCommands />
<HighlightsProvider>{props.children}</HighlightsProvider>
</CommandProvider>
</>
)
}
function DesktopCommands() {
const command = useCommand()
const language = useLanguage()
const platform = usePlatform()
command.register("desktop", () => {
const commands: CommandOption[] = []
if (platform.platform === "desktop" && platform.exportDebugLogs) {
commands.push({
id: "logs.export",
title: "Export logs",
category: language.t("command.category.settings"),
onSelect: () => {
void platform.exportDebugLogs?.()
},
})
}
return commands
})
return null
}
// Server-scoped providers shared by the legacy shell and the top-level new shell.
type ServerScopedShellProps = ParentProps<{
directory?: () => string | undefined
+129 -18
View File
@@ -68,6 +68,7 @@ import { promptPlaceholder } from "./prompt-input/placeholder"
import { createPromptInputTransientState } from "./prompt-input/transient-state"
import { showToast } from "@/utils/toast"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
export type PromptInputState = ReturnType<typeof usePrompt>
@@ -214,6 +215,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
let fileInputRef: HTMLInputElement | undefined
let scrollRef!: HTMLDivElement
let slashPopoverRef!: HTMLDivElement
let restoreEndOnFocus = true
const mirror = { input: false }
const inset = 56
@@ -593,6 +595,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
}
const handleFocus = () => {
if (!restoreEndOnFocus) return
restoreEndOnFocus = false
requestAnimationFrame(() => {
if (document.activeElement !== editorRef) return
setCursorPosition(editorRef, prompt.cursor() ?? promptLength(prompt.current()))
queueScroll()
})
}
const renderEditorWithCursor = (parts: Prompt) => {
const cursor = currentCursor()
renderEditor(parts)
@@ -629,24 +641,89 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
}
const referenceDescription = (reference: ReferenceInfo) =>
reference.source.type === "git" ? reference.source.repository : reference.source.path
const referenceList = createMemo(() =>
sync()
.data.reference.filter((reference) => !reference.hidden)
.map(
(reference): AtOption => ({
type: "reference",
name: reference.name,
path: reference.path,
display: reference.name,
description: reference.description ?? referenceDescription(reference),
}),
),
)
const agentList = createMemo(() =>
props.controls.agents.available
.filter((agent) => !agent.hidden && agent.mode !== "primary")
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
)
const mcpResourceList = createMemo(() =>
Object.values(sync().data.mcp_resource).map(
(resource): AtOption => ({
type: "resource",
name: resource.name,
uri: resource.uri,
client: resource.client,
display: resource.name,
description: resource.description,
mime: resource.mimeType,
}),
),
)
const handleAtSelect = (option: AtOption | undefined) => {
if (!option) return
if (option.type === "agent") {
addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
} else {
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
return
}
if (option.type === "reference") {
addPart({
type: "file",
path: option.path,
content: "@" + option.name,
start: 0,
end: 0,
mime: "application/x-directory",
filename: option.name,
})
return
}
if (option.type === "resource") {
addPart({
type: "file",
path: option.uri,
content: "@" + option.name,
start: 0,
end: 0,
mime: option.mime ?? "text/plain",
filename: option.name,
url: option.uri,
source: {
type: "resource",
text: { value: "@" + option.name, start: 0, end: 0 },
clientName: option.client,
uri: option.uri,
},
})
return
}
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
}
const atKey = (x: AtOption | undefined) => {
if (!x) return ""
return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}`
if (x.type === "agent") return `agent:${x.name}`
if (x.type === "reference") return `reference:${x.name}`
if (x.type === "resource") return `resource:${x.client}:${x.uri}`
return `file:${x.path}`
}
const {
@@ -657,30 +734,36 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onKeyDown: atOnKeyDown,
} = useFilteredList<AtOption>({
items: async (query) => {
const references = referenceList()
const agents = agentList()
const mcpResources = mcpResourceList()
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
if (!query.trim()) return [...agents, ...pinned]
if (!query.trim()) return [...references, ...agents, ...mcpResources, ...pinned]
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
.map((path) => ({ type: "file", path, display: path }))
return [...agents, ...pinned, ...fileOptions]
return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions]
},
key: atKey,
filterKeys: ["display"],
skipFilter: (item) => item.type === "file" && !item.recent,
groupBy: (item) => {
if (item.type === "reference") return "reference"
if (item.type === "agent") return "agent"
if (item.type === "resource") return "resource"
if (item.recent) return "recent"
return "file"
},
sortGroupsBy: (a, b) => {
const rank = (category: string) => {
if (category === "agent") return 0
if (category === "recent") return 1
return 2
if (category === "reference") return 0
if (category === "agent") return 1
if (category === "resource") return 2
if (category === "recent") return 3
return 4
}
return rank(a.category) - rank(b.category)
},
@@ -746,7 +829,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pill = document.createElement("span")
pill.textContent = part.content
pill.setAttribute("data-type", part.type)
if (part.type === "file") pill.setAttribute("data-path", part.path)
if (part.type === "file") {
pill.setAttribute("data-path", part.path)
if (part.mime) pill.setAttribute("data-mime", part.mime)
if (part.filename) pill.setAttribute("data-filename", part.filename)
if (part.url) pill.setAttribute("data-url", part.url)
if (part.source?.type === "resource") {
pill.setAttribute("data-source-type", part.source.type)
pill.setAttribute("data-source-client-name", part.source.clientName)
pill.setAttribute("data-source-uri", part.source.uri)
}
}
if (part.type === "agent") pill.setAttribute("data-name", part.name)
pill.setAttribute("contenteditable", "false")
pill.style.userSelect = "text"
@@ -861,12 +954,29 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pushFile = (file: HTMLElement) => {
const content = file.textContent ?? ""
const source =
file.dataset.sourceType === "resource" && file.dataset.sourceClientName && file.dataset.sourceUri
? {
type: "resource" as const,
text: {
value: content,
start: position,
end: position + content.length,
},
clientName: file.dataset.sourceClientName,
uri: file.dataset.sourceUri,
}
: undefined
parts.push({
type: "file",
path: file.dataset.path!,
content,
start: position,
end: position + content.length,
...(file.dataset.mime ? { mime: file.dataset.mime } : {}),
...(file.dataset.filename ? { filename: file.dataset.filename } : {}),
...(file.dataset.url ? { url: file.dataset.url } : {}),
...(source ? { source } : {}),
})
position += content.length
}
@@ -1380,6 +1490,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}))
const newSession = () => props.variant === "new-session"
const bindEditorRef = (el: HTMLDivElement) => {
editorRef = el
restoreEndOnFocus = true
props.ref?.(el)
}
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
title: language.t("command.agent.cycle"),
@@ -1463,10 +1578,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
<div
data-component="prompt-input"
ref={(el) => {
editorRef = el
props.ref?.(el)
}}
ref={bindEditorRef}
role="textbox"
aria-multiline="true"
aria-label={designPlaceholder()}
@@ -1481,6 +1593,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onPaste={handlePaste}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
classList={{
@@ -1501,7 +1614,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
</div>
<div class="flex h-11 items-center px-2">
<div class="flex min-w-0 flex-1 items-center gap-0">
<div class="flex min-w-0 flex-1 items-center gap-1">
{fileAttachmentInput()}
<TooltipV2
placement="top"
@@ -1644,10 +1757,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
>
<div
data-component="prompt-input"
ref={(el) => {
editorRef = el
props.ref?.(el)
}}
ref={bindEditorRef}
role="textbox"
aria-multiline="true"
aria-label={placeholder()}
@@ -1662,6 +1772,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onPaste={handlePaste}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
classList={{
@@ -100,6 +100,41 @@ describe("buildRequestParts", () => {
)
})
test("preserves reference aliases as directory file parts", () => {
const result = buildRequestParts({
prompt: [
{
type: "file",
path: "/repo/../docs",
content: "@docs",
start: 0,
end: 5,
mime: "application/x-directory",
filename: "docs",
},
],
context: [],
images: [],
text: "@docs",
messageID: "msg_reference",
sessionID: "ses_reference",
sessionDirectory: "/repo/app",
})
const filePart = result.requestParts.find((part) => part.type === "file")
expect(filePart).toBeDefined()
if (filePart?.type === "file") {
expect(filePart.mime).toBe("application/x-directory")
expect(filePart.filename).toBe("docs")
expect(filePart.url).toBe("file:///repo/../docs")
expect(filePart.source?.type).toBe("file")
if (filePart.source?.type === "file") {
expect(filePart.source.path).toBe("/repo/../docs")
expect(filePart.source.text.value).toBe("@docs")
}
}
})
test("deduplicates context files when prompt already includes same path", () => {
const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }]
@@ -99,21 +99,31 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
const path = absolute(input.sessionDirectory, attachment.path)
const source = attachment.source
? {
...attachment.source,
text: {
value: attachment.content,
start: attachment.start,
end: attachment.end,
},
}
: {
type: "file" as const,
text: {
value: attachment.content,
start: attachment.start,
end: attachment.end,
},
path,
}
return {
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
filename: getFilename(attachment.path),
source: {
type: "file",
text: {
value: attachment.content,
start: attachment.start,
end: attachment.end,
},
path,
},
mime: attachment.mime ?? "text/plain",
url: attachment.url ?? `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
filename: attachment.filename ?? getFilename(attachment.path),
source,
} satisfies PromptRequestPart
})
@@ -7,6 +7,16 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
export type AtOption =
| { type: "agent"; name: string; display: string }
| {
type: "resource"
name: string
uri: string
client: string
display: string
description?: string
mime?: string
}
| { type: "reference"; name: string; path: string; display: string; description: string }
| { type: "file"; path: string; display: string; recent?: boolean }
export interface SlashCommand {
@@ -103,6 +113,94 @@ export const PromptPopover: Component<PromptPopoverProps> = (props) => {
)
}
if (item.type === "resource") {
return (
<button
class="w-full flex items-center gap-x-2 px-2 py-0.5"
classList={{
"rounded-[4px]": props.newLayoutDesigns,
"rounded-md": !props.newLayoutDesigns,
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
}}
onClick={() => props.onAtSelect(item)}
onPointerMove={() => props.setAtActive(key)}
>
<FileIcon node={{ path: item.uri, type: "file" }} class="shrink-0 size-4" />
<div
class="flex items-center min-w-0"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
}}
>
<span
class="text-text-strong whitespace-nowrap"
classList={{ "text-v2-text-text-base": props.newLayoutDesigns }}
>
@{item.name}
</span>
<Show when={item.description}>
{(description) => (
<span
class="whitespace-nowrap truncate min-w-0 ml-2"
classList={{
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{description()}
</span>
)}
</Show>
</div>
</button>
)
}
if (item.type === "reference") {
return (
<button
class="w-full flex items-center gap-x-2 px-2 py-0.5"
classList={{
"rounded-[4px]": props.newLayoutDesigns,
"rounded-md": !props.newLayoutDesigns,
"bg-v2-overlay-simple-overlay-hover": props.newLayoutDesigns && props.atActive === key,
"bg-surface-raised-base-hover": !props.newLayoutDesigns && props.atActive === key,
}}
onClick={() => props.onAtSelect(item)}
onPointerMove={() => props.setAtActive(key)}
>
<FileIcon node={{ path: item.path, type: "directory" }} class="shrink-0 size-4" />
<div
class="flex items-center min-w-0"
classList={{
"text-[13px] leading-[calc(var(--font-size-base)*1.8)] tracking-[-0.04px] [font-weight:440]":
props.newLayoutDesigns,
"text-14-regular": !props.newLayoutDesigns,
}}
>
<span
class="text-text-strong whitespace-nowrap"
classList={{ "text-v2-text-text-base": props.newLayoutDesigns }}
>
@{item.name}
</span>
<span
class="whitespace-nowrap truncate min-w-0 ml-2"
classList={{
"text-v2-text-text-muted": props.newLayoutDesigns,
"text-text-weak": !props.newLayoutDesigns,
}}
>
{item.description}
</span>
</div>
</button>
)
}
const isDirectory = item.path.endsWith("/")
const directory = isDirectory ? item.path : getDirectory(item.path)
const filename = isDirectory ? "" : getFilename(item.path)
@@ -26,7 +26,7 @@ function openSessionContext(args: {
layout: ReturnType<typeof useLayout>
tabs: ReturnType<ReturnType<typeof useLayout>["tabs"]>
}) {
if (!args.view.reviewPanel.opened()) args.view.reviewPanel.open()
args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button")
if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all")
void args.tabs.open("context")
args.tabs.setActive("context")
@@ -64,16 +64,25 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const cost = createMemo(() => {
return usd().format(info()?.cost ?? 0)
})
const contextVisible = createMemo(() => view().reviewPanel.opened() && tabState.activeTab() === "context")
const hasOtherTabs = createMemo(() =>
tabs()
.all()
.some((tab) => tab !== "context" && tab !== "review"),
)
const openContext = () => {
if (!params.id) return
if (tabState.activeTab() === "context") {
const sessionView = view()
if (contextVisible()) {
tabs().close("context")
if (sessionView.reviewPanel.source() === "context-button" && !hasOtherTabs()) sessionView.reviewPanel.close()
return
}
openSessionContext({
view: view(),
view: sessionView,
layout,
tabs: tabs(),
})
@@ -81,7 +90,20 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const circle = () => (
<div class="flex items-center justify-center">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.usage ?? 0} />
<ProgressCircle
size={16}
strokeWidth={2}
percentage={context()?.usage ?? 0}
style={
variant() === "indicator"
? {
"--progress-circle-background": "var(--v2-background-bg-layer-04, var(--border-weak-base))",
"--progress-circle-background-overlay": "var(--v2-overlay-simple-overlay-pressed, transparent)",
"--progress-circle-progress": "var(--v2-icon-icon-base, var(--icon-base))",
}
: undefined
}
/>
</div>
)
const circleV2 = () => (
@@ -119,10 +141,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
return (
<Show when={params.id}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={buttonAppearance() === "v2"}>
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={buttonAppearance() === "v2"}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
<IconButtonV2
type="button"
variant="ghost-muted"
@@ -131,8 +153,10 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
onClick={openContext}
aria-label={language.t("context.usage.view")}
/>
</Match>
<Match when={true}>
</Tooltip>
</Match>
<Match when={true}>
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
<Button
type="button"
variant="ghost"
@@ -142,9 +166,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
>
{circle()}
</Button>
</Match>
</Switch>
</Tooltip>
</Tooltip>
</Match>
</Switch>
</Show>
)
}
@@ -21,7 +21,7 @@
position: relative;
}
[data-titlebar-tab-slot]:not(:first-child)::before {
[data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before {
content: "";
position: absolute;
top: 8px;
@@ -32,6 +32,10 @@
background: var(--v2-background-bg-layer-02);
}
[data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before {
display: none;
}
[data-titlebar-tab] [data-slot="tab-close"]::before {
content: "";
position: absolute;
@@ -13,6 +13,9 @@ import type { Session } from "@opencode-ai/sdk/v2"
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
import "./titlebar-tab-nav.css"
// MouseEvent.button uses 1 for the middle/wheel button.
const MIDDLE_MOUSE_BUTTON = 1
export function TabNavItem(props: {
ref?: Ref<HTMLDivElement>
href: string
@@ -184,7 +187,12 @@ export function TabNavItem(props: {
data-dragging={props.dragging}
data-pressed={props.pressed}
onMouseDown={(event) => {
if (event.button !== 1) return
if (event.button !== MIDDLE_MOUSE_BUTTON) return
event.preventDefault()
event.stopPropagation()
}}
onAuxClick={(event) => {
if (event.button !== MIDDLE_MOUSE_BUTTON) return
closeTab(event)
}}
>
@@ -302,7 +310,12 @@ export function DraftTabItem(props: {
class="group relative flex h-7 w-full min-w-0 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
classList={{ invisible: props.hidden }}
onMouseDown={(event) => {
if (event.button !== 1) return
if (event.button !== MIDDLE_MOUSE_BUTTON) return
event.preventDefault()
event.stopPropagation()
}}
onAuxClick={(event) => {
if (event.button !== MIDDLE_MOUSE_BUTTON) return
closeTab(event)
}}
>
@@ -85,6 +85,7 @@ function SessionTabSlot(props: {
ref={sortable.ref}
data-titlebar-tab-slot
data-tab-key={props.id}
data-active={props.active()}
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
classList={{ hidden: !session() }}
>
@@ -140,6 +141,7 @@ function DraftTabSlot(props: {
ref={sortable.ref}
data-titlebar-tab-slot
data-tab-key={props.id}
data-active={props.active()}
class="relative flex w-56 min-w-7 max-w-56 flex-shrink"
>
<DraftTabItem
+2 -1
View File
@@ -229,7 +229,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
}}
style={{
"min-height": minHeight(),
"padding-left": mac() && !mobile() ? `${84 / zoom()}px` : 0,
// Keep native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": mac() ? `${84 / zoom()}px` : 0,
width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"max-width": electronWindows()
? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))`
@@ -16,6 +16,7 @@ describe("bootstrapDirectory", () => {
status: "loading",
agent: [],
command: [],
reference: [],
project: "",
projectMeta: undefined,
icon: undefined,
@@ -35,6 +36,7 @@ describe("bootstrapDirectory", () => {
question: {},
mcp_ready: true,
mcp: {},
mcp_resource: {},
lsp_ready: true,
lsp: [],
vcs: undefined,
@@ -67,6 +69,7 @@ describe("bootstrapDirectory", () => {
},
permission: { list: async () => ({ data: [] }) },
question: { list: async () => ({ data: [] }) },
v2: { reference: { list: async () => ({ data: { data: [] } }) } },
mcp: {
status: async () => {
mcpReads.push("status")
@@ -6,6 +6,7 @@ import type {
Project,
ProviderAuthResponse,
QuestionRequest,
ReferenceInfo,
Session,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast"
@@ -18,7 +19,7 @@ import type { ServerSession } from "../server-session"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery } from "../server-sync"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
@@ -195,6 +196,13 @@ export const loadPathQuery = (scope: ServerScope, directory: string | null, sdk:
queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)),
})
export const loadReferencesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
queryOptions<ReferenceInfo[]>({
queryKey: [scope, directory, "references"] as const,
queryFn: () => retry(() => sdk.v2.reference.list().then((x) => x.data?.data ?? [])).catch(() => []),
placeholderData: [],
})
export async function bootstrapDirectory(input: {
directory: string
scope: ServerScope
@@ -277,6 +285,7 @@ export async function bootstrapDirectory(input: {
}),
),
input.mcp && (() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])))),
() => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.sdk)),
() =>
retry(() =>
input.sdk.permission.list().then((x) => {
@@ -339,6 +348,7 @@ export async function bootstrapDirectory(input: {
),
() => Promise.resolve(input.loadSessions(input.directory)),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.sdk))),
() =>
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => {
const project = getFilename(input.directory)
@@ -34,7 +34,9 @@ const queryOptionsApi = {
}),
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
mcpResources: (directory: string) => ({ queryKey: [directory, "mcpResources"], queryFn: async () => ({}) }),
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
references: (directory: string) => ({ queryKey: [directory, "references"], queryFn: async () => [] }),
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
} as unknown as QueryOptionsApi
@@ -197,14 +199,18 @@ describe("createChildStoreManager", () => {
try {
if (!manager) throw new Error("manager required")
const [store, setStore] = manager.child("/project", { bootstrap: false })
expect(querySingles.length - offset).toBe(4)
expect(querySingles.length - offset).toBe(6)
const query = querySingles[offset + 1]
const resourceQuery = querySingles[offset + 2]
if (!query) throw new Error("query required")
if (!resourceQuery) throw new Error("resource query required")
expect(query().enabled).toBe(false)
expect(resourceQuery().enabled).toBe(false)
setStore("status", "complete")
manager.child("/project", { bootstrap: false, mcp: true })
expect(query().enabled).toBe(true)
expect(resourceQuery().enabled).toBe(true)
expect(store.mcp).toEqual({ demo: { status: "disabled" } })
expect(mcpLoads).toEqual(["/project"])
@@ -185,8 +185,10 @@ export function createChildStoreManager(input: {
const pathQuery = useQuery(() => input.queryOptions.path(key))
const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
const lspQuery = useQuery(() => input.queryOptions.lsp(key))
const providerQuery = useQuery(() => input.queryOptions.providers(key))
const referenceQuery = useQuery(() => input.queryOptions.references(key))
const child = createStore<State>({
project: "",
@@ -210,6 +212,9 @@ export function createChildStoreManager(input: {
status: "loading" as const,
agent: [],
command: [],
get reference() {
return referenceQuery.isLoading ? [] : (referenceQuery.data ?? [])
},
session: [],
sessionTotal: 0,
session_status: {},
@@ -227,6 +232,9 @@ export function createChildStoreManager(input: {
get mcp() {
return mcpQuery.isLoading ? {} : (mcpQuery.data ?? {})
},
get mcp_resource() {
return mcpResourceQuery.isLoading ? {} : (mcpResourceQuery.data ?? {})
},
get lsp_ready() {
return !lspQuery.isLoading
},
@@ -112,6 +112,7 @@ export function applyDirectoryEvent(input: {
push: (directory: string) => void
directory: string
loadLsp: () => void
loadReferences?: () => void
vcsCache?: VcsCache
setSessionTodo?: (sessionID: string, todos: Todo[] | undefined) => void
retainedLimit?: number
@@ -404,5 +405,9 @@ export function applyDirectoryEvent(input: {
input.loadLsp()
break
}
case "reference.updated": {
input.loadReferences?.()
break
}
}
}
@@ -3,12 +3,14 @@ import type {
Command,
Config,
LspStatus,
McpResource,
McpStatus,
Message,
Part,
Path,
PermissionRequest,
QuestionRequest,
ReferenceInfo,
Session,
SessionStatus,
SnapshotFileDiff,
@@ -34,6 +36,7 @@ export type State = {
status: "loading" | "partial" | "complete"
agent: Agent[]
command: Command[]
reference: ReferenceInfo[]
project: string
projectMeta: ProjectMeta | undefined
icon: string | undefined
@@ -63,6 +66,9 @@ export type State = {
mcp: {
[name: string]: McpStatus
}
mcp_resource: {
[key: string]: McpResource
}
lsp_ready: boolean
lsp: LspStatus[]
vcs: VcsInfo | undefined
+31 -13
View File
@@ -28,6 +28,7 @@ const DEFAULT_SIDEBAR_WIDTH = 344
const DEFAULT_FILE_TREE_WIDTH = 200
const DEFAULT_SESSION_WIDTH = 600
const DEFAULT_TERMINAL_HEIGHT = 280
const DEFAULT_REVIEW_PANEL_OPENED = false
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
export function getAvatarColors(key?: string) {
@@ -77,6 +78,7 @@ export type LocalProject = Partial<Project> & { worktree: string; expanded: bool
export type HomeProjectSelection = { server: ServerConnection.Key; directory?: string }
export type ReviewDiffStyle = "unified" | "split"
export type ReviewPanelSource = "context-button" | "other"
export type LayoutRoute =
| { type: "home" }
@@ -210,7 +212,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
if (!isRecord(review)) return review
if (typeof review.panelOpened === "boolean") return review
const opened = isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : true
const opened =
isRecord(fileTree) && typeof fileTree.opened === "boolean" ? fileTree.opened : DEFAULT_REVIEW_PANEL_OPENED
return {
...review,
panelOpened: opened,
@@ -279,7 +282,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
diffStyle: "split" as ReviewDiffStyle,
panelOpened: true,
panelOpened: DEFAULT_REVIEW_PANEL_OPENED,
},
fileTree: {
opened: false,
@@ -302,6 +305,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
)
const [ephemeral, setEphemeral] = createStore({
reviewPanelSource: "other" as ReviewPanelSource,
})
const MAX_SESSION_KEYS = 50
const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000
@@ -662,7 +668,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) {
setStore("review", { diffStyle, panelOpened: true })
setStore("review", { diffStyle, panelOpened: DEFAULT_REVIEW_PANEL_OPENED })
return
}
setStore("review", "diffStyle", diffStyle)
@@ -777,7 +783,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const key = createSessionKeyReader(sessionKey, ensureKey)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED)
const reviewPanelSource = createMemo(() => (reviewPanelOpened() ? ephemeral.reviewPanelSource : "other"))
function setTerminalOpened(next: boolean) {
const current = store.terminal
@@ -791,16 +798,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "opened", next)
}
function setReviewPanelOpened(next: boolean) {
function setReviewPanelOpened(next: boolean, source: ReviewPanelSource) {
const nextSource = next ? source : "other"
const current = store.review
if (!current) {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
batch(() => {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
setEphemeral("reviewPanelSource", nextSource)
})
return
}
const value = current.panelOpened ?? true
if (value === next) return
setStore("review", "panelOpened", next)
const value = current.panelOpened ?? DEFAULT_REVIEW_PANEL_OPENED
if (value === next) {
if (ephemeral.reviewPanelSource !== nextSource) setEphemeral("reviewPanelSource", nextSource)
return
}
batch(() => {
setStore("review", "panelOpened", next)
setEphemeral("reviewPanelSource", nextSource)
})
}
return {
@@ -836,14 +853,15 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
reviewPanel: {
opened: reviewPanelOpened,
open() {
setReviewPanelOpened(true)
source: reviewPanelSource,
open(source: ReviewPanelSource = "other") {
setReviewPanelOpened(true, source)
},
close() {
setReviewPanelOpened(false)
setReviewPanelOpened(false, "other")
},
toggle() {
setReviewPanelOpened(!reviewPanelOpened())
setReviewPanelOpened(!reviewPanelOpened(), "other")
},
},
review: {
+42 -22
View File
@@ -1,6 +1,6 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useParams } from "@solidjs/router"
import { useParams, useSearchParams } from "@solidjs/router"
import { batch, createEffect, createMemo, startTransition } from "solid-js"
import { createStore } from "solid-js/store"
import { useModels } from "@/context/models"
@@ -18,10 +18,12 @@ type State = {
agent?: string
model?: ModelKey
variant?: string | null
source?: "manual" | "message"
}
type Saved = {
session: Record<string, State | undefined>
draft?: State
}
const WORKSPACE_KEY = "__workspace__"
@@ -37,11 +39,13 @@ const migrate = (value: unknown) => {
pick?: Record<string, State | undefined>
}
if (item.session && typeof item.session === "object") return { session: item.session }
const draft = "draft" in item && item.draft && typeof item.draft === "object" ? (item.draft as State) : undefined
if (item.session && typeof item.session === "object") return { session: item.session, draft }
if (!item.pick || typeof item.pick !== "object") return { session: {} }
return {
session: Object.fromEntries(Object.entries(item.pick).filter(([key]) => key !== WORKSPACE_KEY)),
draft,
}
}
@@ -57,6 +61,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
const params = useParams()
const [search] = useSearchParams<{ draftId?: string }>()
const sdk = useSDK()
const sync = useSync()
const serverSDK = useServerSDK()
@@ -64,12 +69,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const models = useModels()
const id = createMemo(() => params.id || undefined)
const draftID = createMemo(() => search.draftId || undefined)
const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
const [saved, setSaved] = persisted(
{
...Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"]),
...(draftID()
? Persist.draft(draftID()!, "model-selection")
: Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"])),
migrate,
},
createStore<Saved>({
@@ -80,6 +88,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const [store, setStore] = createStore<{
current?: string
draft?: State
promoting?: State
last?: {
type: "agent" | "model" | "variant"
agent?: string
@@ -123,7 +132,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const scope = createMemo<State | undefined>(() => {
const session = id()
if (!session) return store.draft
if (!session) return saved.draft ?? store.draft ?? store.promoting
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
})
@@ -136,11 +145,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!next) return
if (saved.session[session] !== undefined) {
handoff.delete(key)
setStore("promoting", undefined)
return
}
setSaved("session", session, clone(next))
handoff.delete(key)
setStore("promoting", undefined)
})
const configuredModel = () => {
@@ -175,6 +186,19 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const fallback = createMemo<ModelKey | undefined>(() => configuredModel() ?? recentModel() ?? defaultModel())
const save = (state: State) => {
const session = id()
if (session) {
setSaved("session", session, state)
return
}
if (draftID()) {
setSaved("draft", state)
return
}
setStore("draft", state)
}
const agent = {
list,
current() {
@@ -200,13 +224,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent: item.name,
model: item.model ?? prev?.model,
variant: item.variant ?? prev?.variant,
source: "manual",
} satisfies State
const session = id()
if (session) {
setSaved("session", session, next)
return
}
setStore("draft", next)
save(next)
})
},
move(direction: 1 | -1) {
@@ -260,14 +280,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const state = {
...(scope() ?? { agent: agent.current()?.name }),
...next,
source: "manual",
} satisfies State
const session = id()
if (session) {
setSaved("session", session, state)
return
}
setStore("draft", state)
save(state)
}
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
@@ -373,32 +389,36 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent,
session: {
reset() {
setStore("draft", undefined)
setStore({ draft: undefined, promoting: undefined })
if (draftID()) setSaved("draft", undefined)
},
promote(dir: string, session: string) {
const next = clone(snapshot())
if (!next) return
next.source = "manual"
const key = handoffKey(serverSDK().scope, dir, session)
handoff.set(key, next)
if (dir === sdk().directory) {
setSaved("session", session, next)
setStore("draft", undefined)
return
}
handoff.set(handoffKey(serverSDK().scope, dir, session), next)
setStore("promoting", next)
setStore("draft", undefined)
},
restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
restore(msg: { id?: string; sessionID: string; agent: string; model: ModelKey }) {
const session = id()
if (!session) return
if (msg.sessionID !== session) return
if (saved.session[session] !== undefined) return
const current = saved.session[session]
if (current?.source === "manual") return
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
setSaved("session", session, {
agent: msg.agent,
model: msg.model,
variant: msg.model?.variant ?? null,
source: "message",
})
},
},
+12 -1
View File
@@ -12,6 +12,7 @@ import { useTabs, type Tab } from "./tabs"
import { ServerConnection } from "./server"
import { requireServerKey } from "@/utils/session-route"
import { useSettings } from "./settings"
import type { FilePartSource } from "@opencode-ai/sdk/v2/client"
interface PartBase {
content: string
@@ -27,6 +28,10 @@ export interface FileAttachmentPart extends PartBase {
type: "file"
path: string
selection?: FileSelection
mime?: string
filename?: string
url?: string
source?: FilePartSource
}
export interface AgentPart extends PartBase {
@@ -73,7 +78,13 @@ function isPartEqual(partA: ContentPart, partB: ContentPart) {
case "text":
return partB.type === "text" && partA.content === partB.content
case "file":
return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection)
return (
partB.type === "file" &&
partA.path === partB.path &&
partA.mime === partB.mime &&
partA.filename === partB.filename &&
isSelectionEqual(partA.selection, partB.selection)
)
case "agent":
return partB.type === "agent" && partA.name === partB.name
case "image":
+22 -1
View File
@@ -1,4 +1,11 @@
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse } from "@opencode-ai/sdk/v2/client"
import type {
Config,
McpResource,
OpencodeClient,
Path,
Project,
ProviderAuthResponse,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path"
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -15,6 +22,7 @@ import {
loadPathQuery,
loadProjectsQuery,
loadProvidersQuery,
loadReferencesQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
@@ -56,6 +64,13 @@ export const loadMcpQuery = (scope: ServerScope, directory: string, sdk: Opencod
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
})
export const loadMcpResourcesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
queryOptions<Record<string, McpResource>>({
queryKey: [scope, directory, "mcpResources"] as const,
queryFn: () => sdk.experimental.resource.list().then((r) => r.data ?? {}),
placeholderData: {},
})
export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: [scope, directory, "lsp"] as const,
@@ -75,7 +90,9 @@ function makeQueryOptionsApi(
path: (directory: PathKey | null) =>
loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)),
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)),
references: (directory: PathKey) => loadReferencesQuery(scope, directory, sdkFor(directory)),
mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)),
mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, sdkFor(directory)),
lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
}
@@ -396,6 +413,9 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
loadLsp: () => {
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
},
loadReferences: () => {
void queryClient.fetchQuery(queryOptionsApi.references(key))
},
})
})
@@ -484,6 +504,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
},
refresh: async () => {
await queryClient.refetchQueries(queryOptionsApi.mcp(key))
await queryClient.refetchQueries(queryOptionsApi.mcpResources(key))
},
})
},
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, test } from "bun:test"
import { DESKTOP_MENU } from "./desktop-menu"
describe("desktop menu", () => {
test("exports logs through the desktop command registry", () => {
const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter(
(item) => item.type === "item" && item.label === "Export Logs...",
)
expect(items).toHaveLength(2)
expect(items.every((item) => item.type === "item" && item.command === "logs.export" && !item.action)).toBe(true)
})
})
-12
View File
@@ -943,18 +943,6 @@ export default function LegacyLayout(props: ParentProps) {
keybind: "mod+comma",
onSelect: () => openSettings(),
},
...(platform.platform === "desktop" && platform.exportDebugLogs
? [
{
id: "logs.export",
title: "Export logs",
category: language.t("command.category.settings"),
onSelect: () => {
void platform.exportDebugLogs?.()
},
},
]
: []),
{
id: "session.previous",
title: language.t("command.session.previous"),
@@ -1357,7 +1357,7 @@ export function MessageTimeline(props: {
"w-full": true,
"pb-4": true,
"pr-3": true,
"pl-4": settings.general.newLayoutDesigns(),
"pl-2": settings.general.newLayoutDesigns(),
"pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
}}
@@ -1393,8 +1393,13 @@ export function MessageTimeline(props: {
fallback={
<h1
data-slot="session-title-child"
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
onDblClick={openTitleEditor}
classList={{
"text-14-medium text-text-strong truncate": true,
"w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
settings.general.newLayoutDesigns(),
"grow-1 min-w-0": !settings.general.newLayoutDesigns(),
}}
onClick={openTitleEditor}
>
{childTitle()}
</h1>
@@ -1408,10 +1413,10 @@ export function MessageTimeline(props: {
value={title.draft}
disabled={titleMutation.isPending}
classList={{
"text-14-medium text-text-strong grow-1 min-w-0 pl-1 -ml-1": true,
"h-6 leading-4 rounded-[3px] focus:shadow-none focus:outline focus:outline-1 focus:outline-offset-[-1px] focus:outline-v2-border-border-focus":
"text-14-medium text-text-strong grow-1 min-w-0 pl-1 ml-1": true,
"grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
"rounded-[6px] -ml-2 px-2 py-1 h-6 leading-4 focus:shadow-none focus:outline focus:outline-1 focus:outline-offset-[-1px] focus:outline-v2-border-border-focus":
settings.general.newLayoutDesigns(),
"rounded-[6px]": !settings.general.newLayoutDesigns(),
}}
style={{
"--inline-input-shadow": settings.general.newLayoutDesigns()
+5
View File
@@ -369,6 +369,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}",
"zen.api.error.modelDisabled": "النموذج معطل",
"zen.api.error.regionNotAllowed":
"هذا النموذج مستضاف في الصين. إذا كنت ترغب في استخدام هذا النموذج، فعّله في إعداداتك: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}",
@@ -646,6 +648,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
"workspace.lite.providers.title": "المزودون",
"workspace.lite.providers.description": "تحكم في المزودين المستخدمين للتوجيه.",
"workspace.lite.providers.useChina": "تفعيل النماذج المستضافة في الصين",
"workspace.lite.black.message":
"أنت مشترك حاليًا في OpenCode Black أو في قائمة الانتظار. يرجى إلغاء الاشتراك أولاً إذا كنت ترغب في التبديل إلى Go.",
"workspace.lite.other.message":
+5
View File
@@ -377,6 +377,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}",
"zen.api.error.modelDisabled": "O modelo está desabilitado",
"zen.api.error.regionNotAllowed":
"Este modelo está hospedado na China. Se você quiser usar este modelo, ative-o nas suas configurações: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}",
@@ -656,6 +658,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
"workspace.lite.providers.title": "Provedores",
"workspace.lite.providers.description": "Controle quais provedores são usados para roteamento.",
"workspace.lite.providers.useChina": "Ativar modelos hospedados na China",
"workspace.lite.black.message":
"Você está atualmente inscrito no OpenCode Black ou na lista de espera. Por favor, cancele a assinatura primeiro se desejar mudar para o Go.",
"workspace.lite.other.message":
+5
View File
@@ -373,6 +373,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modellen er deaktiveret",
"zen.api.error.regionNotAllowed":
"Denne model hostes i Kina. Hvis du vil bruge denne model, skal du aktivere den i dine indstillinger: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}",
@@ -652,6 +654,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
"workspace.lite.providers.title": "Udbydere",
"workspace.lite.providers.description": "Styr, hvilke udbydere der bruges til routing.",
"workspace.lite.providers.useChina": "Aktivér modeller hostet i Kina",
"workspace.lite.black.message":
"Du abonnerer i øjeblikket på OpenCode Black eller er på venteliste. Afmeld venligst først, hvis du vil skifte til Go.",
"workspace.lite.other.message":
+5
View File
@@ -376,6 +376,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modell ist deaktiviert",
"zen.api.error.regionNotAllowed":
"Dieses Modell wird in China gehostet. Wenn du dieses Modell verwenden möchtest, aktiviere es in deinen Einstellungen: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}",
@@ -655,6 +657,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
"workspace.lite.providers.title": "Anbieter",
"workspace.lite.providers.description": "Steuere, welche Anbieter für das Routing verwendet werden.",
"workspace.lite.providers.useChina": "In China gehostete Modelle aktivieren",
"workspace.lite.black.message":
"Du hast derzeit OpenCode Black abonniert oder stehst auf der Warteliste. Bitte kündige zuerst, wenn du zu Go wechseln möchtest.",
"workspace.lite.other.message":
+5
View File
@@ -370,6 +370,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model is disabled",
"zen.api.error.regionNotAllowed":
"This model is hosted in China. If you would like to use this model, enable it in your settings: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}",
@@ -649,6 +651,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
"workspace.lite.providers.title": "Providers",
"workspace.lite.providers.description": "Control which providers are used for routing.",
"workspace.lite.providers.useChina": "Enable models hosted in China",
"workspace.lite.black.message":
"You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.",
"workspace.lite.other.message":
+5
View File
@@ -377,6 +377,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}",
"zen.api.error.modelDisabled": "El modelo está deshabilitado",
"zen.api.error.regionNotAllowed":
"Este modelo está alojado en China. Si quieres usar este modelo, actívalo en tu configuración: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}",
@@ -656,6 +658,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
"workspace.lite.providers.title": "Proveedores",
"workspace.lite.providers.description": "Controla qué proveedores se usan para el enrutamiento.",
"workspace.lite.providers.useChina": "Activar modelos alojados en China",
"workspace.lite.black.message":
"Actualmente estás suscrito a OpenCode Black o estás en la lista de espera. Por favor, cancela la suscripción primero si deseas cambiar a Go.",
"workspace.lite.other.message":
+5
View File
@@ -377,6 +377,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}",
"zen.api.error.modelDisabled": "Le modèle est désactivé",
"zen.api.error.regionNotAllowed":
"Ce modèle est hébergé en Chine. Si vous souhaitez utiliser ce modèle, activez-le dans vos paramètres : {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}",
@@ -662,6 +664,9 @@ export const dict = {
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider":
'Sélectionnez "OpenCode Go" comme fournisseur dans votre configuration opencode pour utiliser les modèles Go.',
"workspace.lite.providers.title": "Fournisseurs",
"workspace.lite.providers.description": "Contrôlez les fournisseurs utilisés pour le routage.",
"workspace.lite.providers.useChina": "Activer les modèles hébergés en Chine",
"workspace.lite.black.message":
"Vous êtes actuellement abonné à OpenCode Black ou sur liste d'attente. Veuillez d'abord vous désabonner si vous souhaitez passer à Go.",
"workspace.lite.other.message":
+5
View File
@@ -373,6 +373,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}",
"zen.api.error.modelDisabled": "Il modello è disabilitato",
"zen.api.error.regionNotAllowed":
"Questo modello è ospitato in Cina. Se vuoi usare questo modello, abilitalo nelle tue impostazioni: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}",
@@ -654,6 +656,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
"workspace.lite.providers.title": "Provider",
"workspace.lite.providers.description": "Controlla quali provider vengono usati per il routing.",
"workspace.lite.providers.useChina": "Abilita modelli ospitati in Cina",
"workspace.lite.black.message":
"Attualmente sei abbonato a OpenCode Black o sei in lista d'attesa. Annulla l'iscrizione prima se desideri passare a Go.",
"workspace.lite.other.message":
+5
View File
@@ -374,6 +374,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}",
"zen.api.error.modelDisabled": "モデルが無効です",
"zen.api.error.regionNotAllowed":
"このモデルは中国でホストされています。このモデルを使用したい場合は、設定で有効にしてください: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}",
@@ -654,6 +656,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
"workspace.lite.providers.title": "プロバイダー",
"workspace.lite.providers.description": "ルーティングに使用するプロバイダーを管理します。",
"workspace.lite.providers.useChina": "中国でホストされているモデルを有効にする",
"workspace.lite.black.message":
"現在 OpenCode Black を購読中、またはウェイティングリストに登録されています。Go に切り替える場合は、先に登録を解除してください。",
"workspace.lite.other.message":
+5
View File
@@ -368,6 +368,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}",
"zen.api.error.modelDisabled": "모델이 비활성화되었습니다",
"zen.api.error.regionNotAllowed":
"이 모델은 중국에서 호스팅됩니다. 이 모델을 사용하려면 설정에서 활성화하세요: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}",
@@ -646,6 +648,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
"workspace.lite.providers.title": "공급자",
"workspace.lite.providers.description": "라우팅에 사용할 공급자를 제어합니다.",
"workspace.lite.providers.useChina": "중국에서 호스팅되는 모델 활성화",
"workspace.lite.black.message":
"현재 OpenCode Black을 구독 중이거나 대기 명단에 등록되어 있습니다. Go로 전환하려면 먼저 구독을 취소해 주세요.",
"workspace.lite.other.message":
+5
View File
@@ -374,6 +374,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}",
"zen.api.error.modelDisabled": "Modellen er deaktivert",
"zen.api.error.regionNotAllowed":
"Denne modellen hostes i Kina. Hvis du vil bruke denne modellen, aktiver den i innstillingene dine: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}",
@@ -653,6 +655,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
"workspace.lite.providers.title": "Leverandører",
"workspace.lite.providers.description": "Kontroller hvilke leverandører som brukes til ruting.",
"workspace.lite.providers.useChina": "Aktiver modeller hostet i Kina",
"workspace.lite.black.message":
"Du abonnerer for øyeblikket på OpenCode Black eller står på venteliste. Vennligst avslutt abonnementet først hvis du vil bytte til Go.",
"workspace.lite.other.message":
+5
View File
@@ -375,6 +375,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model jest wyłączony",
"zen.api.error.regionNotAllowed":
"Ten model jest hostowany w Chinach. Jeśli chcesz korzystać z tego modelu, włącz go w swoich ustawieniach: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}",
@@ -654,6 +656,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
"workspace.lite.providers.title": "Dostawcy",
"workspace.lite.providers.description": "Kontroluj, którzy dostawcy są używani do routingu.",
"workspace.lite.providers.useChina": "Włącz modele hostowane w Chinach",
"workspace.lite.black.message":
"Obecnie subskrybujesz OpenCode Black lub jesteś na liście oczekujących. Jeśli chcesz przejść na Go, najpierw anuluj subskrypcję.",
"workspace.lite.other.message":
+5
View File
@@ -379,6 +379,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}",
"zen.api.error.modelDisabled": "Модель отключена",
"zen.api.error.regionNotAllowed":
"Эта модель размещена в Китае. Если вы хотите использовать эту модель, включите её в настройках: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}",
@@ -660,6 +662,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
"workspace.lite.providers.title": "Провайдеры",
"workspace.lite.providers.description": "Управляйте провайдерами, используемыми для маршрутизации.",
"workspace.lite.providers.useChina": "Включить модели, размещенные в Китае",
"workspace.lite.black.message":
"Вы подписаны на OpenCode Black или находитесь в списке ожидания. Пожалуйста, сначала отмените подписку, если хотите перейти на Go.",
"workspace.lite.other.message":
+5
View File
@@ -370,6 +370,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}",
"zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน",
"zen.api.error.regionNotAllowed":
"โมเดลนี้โฮสต์อยู่ในประเทศจีน หากคุณต้องการใช้โมเดลนี้ ให้เปิดใช้งานในการตั้งค่าของคุณ: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}",
@@ -649,6 +651,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
"workspace.lite.providers.title": "ผู้ให้บริการ",
"workspace.lite.providers.description": "ควบคุมผู้ให้บริการที่ใช้สำหรับการกำหนดเส้นทาง",
"workspace.lite.providers.useChina": "เปิดใช้โมเดลที่โฮสต์ในจีน",
"workspace.lite.black.message":
"ขณะนี้คุณสมัครสมาชิก OpenCode Black หรืออยู่ในรายการรอ โปรดยกเลิกการสมัครก่อนหากต้องการเปลี่ยนไปใช้ Go",
"workspace.lite.other.message":
+5
View File
@@ -377,6 +377,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}",
"zen.api.error.modelDisabled": "Model devre dışı",
"zen.api.error.regionNotAllowed":
"Bu model Çin'de barındırılıyor. Bu modeli kullanmak istiyorsanız ayarlarınızdan etkinleştirin: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}",
@@ -656,6 +658,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
"workspace.lite.providers.title": "Sağlayıcılar",
"workspace.lite.providers.description": "Yönlendirme için hangi sağlayıcıların kullanılacağını kontrol edin.",
"workspace.lite.providers.useChina": "Çin'de barındırılan modelleri etkinleştir",
"workspace.lite.black.message":
"Şu anda OpenCode Black abonesisiniz veya bekleme listesindesiniz. Go'ya geçmek istiyorsanız lütfen önce aboneliğinizi iptal edin.",
"workspace.lite.other.message":
+5
View File
@@ -374,6 +374,8 @@ export const dict = {
"zen.api.error.userMonthlyLimitReached":
"Ви досягли місячного ліміту витрат ${{amount}}. Керуйте лімітами: {{membersUrl}}",
"zen.api.error.modelDisabled": "Модель вимкнено",
"zen.api.error.regionNotAllowed":
"Ця модель розміщена в Китаї. Якщо ви хочете використовувати цю модель, увімкніть її в налаштуваннях: {{consoleGoUrl}}",
"zen.api.error.trialEnded":
"Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}",
@@ -652,6 +654,9 @@ export const dict = {
"workspace.lite.subscription.resetsIn": "Скидається через",
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
"workspace.lite.providers.title": "Провайдери",
"workspace.lite.providers.description": "Керуйте провайдерами, які використовуються для маршрутизації.",
"workspace.lite.providers.useChina": "Увімкнути моделі, розміщені в Китаї",
"workspace.lite.black.message":
"Ви вже підписані на OpenCode Black або в списку очікування. Спочатку скасуйте підписку, якщо хочете перейти на Go.",
"workspace.lite.other.message": "Інший учасник цього робочого простору вже підписаний на OpenCode Go.",
+4
View File
@@ -356,6 +356,7 @@ export const dict = {
"您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已禁用",
"zen.api.error.regionNotAllowed": "该模型部署在中国。如果你想使用该模型,请在设置中启用它:{{consoleGoUrl}}",
"zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}",
"black.meta.title": "OpenCode Black | 访问全球顶尖编程模型",
@@ -631,6 +632,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.lite.subscription.selectProvider":
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
"workspace.lite.providers.title": "提供商",
"workspace.lite.providers.description": "控制用于路由的提供商。",
"workspace.lite.providers.useChina": "启用部署在中国的模型",
"workspace.lite.black.message": "您当前已订阅 OpenCode Black 或在候补名单中。如需切换到 Go,请先取消订阅。",
"workspace.lite.other.message": "此工作区中的另一位成员已经订阅了 OpenCode Go。每个工作区只有一名成员可以订阅。",
"workspace.lite.promo.description":
+4
View File
@@ -356,6 +356,7 @@ export const dict = {
"你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}",
"zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}",
"zen.api.error.modelDisabled": "模型已停用",
"zen.api.error.regionNotAllowed": "此模型部署於中國。如果你想使用此模型,請在設定中啟用它:{{consoleGoUrl}}",
"zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}",
"black.meta.title": "OpenCode Black | 存取全球最佳編碼模型",
@@ -631,6 +632,9 @@ export const dict = {
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
"workspace.lite.subscription.selectProvider":
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
"workspace.lite.providers.title": "提供商",
"workspace.lite.providers.description": "控制用於路由的提供商。",
"workspace.lite.providers.useChina": "啟用部署在中國的模型",
"workspace.lite.black.message": "您目前已訂閱 OpenCode Black 或在候補名單中。若要切換至 Go,請先取消訂閱。",
"workspace.lite.other.message": "此工作區中的另一位成員已訂閱 OpenCode Go。每個工作區只能有一位成員訂閱。",
"workspace.lite.promo.description":
@@ -0,0 +1,5 @@
export function countryFromRequest(request: Request | undefined) {
if (!request) return undefined
const cloudflareRequest = request as Request & { cf?: { country?: string } }
return cloudflareRequest.cf?.country ?? request.headers.get("cf-ipcountry") ?? undefined
}
@@ -75,6 +75,40 @@
}
}
[data-slot="providers-section"] {
display: flex;
flex-direction: column;
gap: var(--space-3);
margin-top: var(--space-6);
padding-top: var(--space-6);
border-top: 1px solid var(--color-border-muted);
[data-slot="providers-header"] {
display: flex;
flex-direction: column;
gap: var(--space-1);
h3 {
color: var(--color-text);
font-size: var(--font-size-lg);
font-weight: 600;
line-height: 1.2;
margin: 0;
}
p {
color: var(--color-text-muted);
font-size: var(--font-size-sm);
line-height: 1.5;
margin: 0;
}
}
[data-slot="setting-row"] {
margin-top: 0;
}
}
[data-slot="toggle-label"] {
position: relative;
display: inline-block;
@@ -5,7 +5,9 @@ import { Modal } from "~/component/modal"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { Subscription } from "@opencode-ai/console-core/subscription.js"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { withActor } from "~/context/auth.withActor"
@@ -16,6 +18,8 @@ import { useLanguage } from "~/context/language"
import { formError } from "~/lib/form-error"
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
import { createReferralFromCookie } from "~/lib/referral-invite"
import { getRequestEvent } from "solid-js/web"
import { countryFromRequest } from "~/lib/request-country"
import { IconAlipay, IconUpi } from "~/component/icon"
@@ -34,9 +38,11 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
timeCreated: LiteTable.timeCreated,
lite: BillingTable.lite,
region: WorkspaceTable.region,
})
.from(BillingTable)
.innerJoin(LiteTable, eq(LiteTable.workspaceID, BillingTable.workspaceID))
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
.then((r) => r[0]),
)
@@ -48,6 +54,8 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
return {
mine,
useBalance: row.lite?.useBalance ?? false,
region:
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
rollingUsage: Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
@@ -128,6 +136,24 @@ const setLiteUseBalance = action(async (form: FormData) => {
)
}, "setLiteUseBalance")
const setGoProviderRouting = action(async (form: FormData) => {
"use server"
const workspaceID = form.get("workspaceID") as string | null
if (!workspaceID) return { error: formError.workspaceRequired }
const useChinaProviders = (form.get("useChinaProviders") as string | null) === "true"
return json(
await withActor(
() =>
Workspace.update({ region: useChinaProviders ? ["us", "eu", "sg"] : ["us", "eu", "sg", "cn"] })
.then(() => ({ error: undefined }))
.catch((e) => ({ error: e.message as string })),
workspaceID,
),
{ revalidate: queryLiteSubscription.key },
)
}, "go.providerRouting.set")
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
const i18n = useI18n()
@@ -159,6 +185,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
const checkoutAction = useAction(createLiteCheckoutUrl)
const checkoutSubmission = useSubmission(createLiteCheckoutUrl)
const useBalanceSubmission = useSubmission(setLiteUseBalance)
const providerRoutingSubmission = useSubmission(setGoProviderRouting)
const [store, setStore] = createStore({
loading: undefined as undefined | "session" | "checkout" | "alipay" | "upi",
showModal: false,
@@ -232,6 +259,28 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<span></span>
</label>
</form>
{/*
<div data-slot="providers-section">
<div data-slot="providers-header">
<h3>{i18n.t("workspace.lite.providers.title")}</h3>
<p>{i18n.t("workspace.lite.providers.description")}</p>
</div>
<form action={setGoProviderRouting} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.providers.useChina")}</p>
<input type="hidden" name="workspaceID" value={params.id} />
<input type="hidden" name="useChinaProviders" value={sub().region.includes("cn") ? "true" : "false"} />
<label data-slot="toggle-label">
<input
type="checkbox"
checked={sub().region.includes("cn")}
disabled={providerRoutingSubmission.pending}
onChange={(e) => e.currentTarget.form?.requestSubmit()}
/>
<span></span>
</label>
</form>
</div>
*/}
</section>
)}
</Show>
@@ -43,6 +43,7 @@ const updateWorkspace = action(async (form: FormData) => {
.catch((e) => ({ error: e.message as string })),
workspaceID,
),
{ revalidate: getWorkspaceInfo.key },
)
}, "workspace.update")
@@ -3,6 +3,7 @@ export class CreditsError extends Error {}
export class MonthlyLimitError extends Error {}
export class UserLimitError extends Error {}
export class ModelError extends Error {}
export class RegionError extends Error {}
class LimitError extends Error {
retryAfter?: number
@@ -21,6 +21,7 @@ import {
MonthlyLimitError,
UserLimitError,
ModelError,
RegionError,
RateLimitError,
FreeUsageLimitError,
GoUsageLimitError,
@@ -49,6 +50,8 @@ import { createModelTpmLimiter } from "./modelTpmLimiter"
import { createModelTpsLimiter } from "./modelTpsLimiter"
import { createProviderBudgetTracker } from "./providerBudgetTracker"
import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { countryFromRequest } from "~/lib/request-country"
type ZenData = Awaited<ReturnType<typeof ZenData.list>>
type RetryOptions = {
@@ -125,6 +128,24 @@ export async function handler(
: createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request)
await rateLimiter?.check()
const authInfo = await authenticate(modelInfo, zenApiKey)
const allowedRegions = authInfo?.region
? authInfo.region
: await (async () => {
if (!authInfo) return
return Actor.provide("system", { workspaceID: authInfo.workspaceID }, () =>
Workspace.setDefaultRegion({ country: countryFromRequest(input.request) }),
)
})()
/*
if (true) {
if (!allowedRegions?.includes("unavailable"))
throw new RegionError(
t("zen.api.error.regionNotAllowed", {
consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`,
}),
)
}
*/
const stickyId = sessionId ? sessionId : (authInfo?.workspaceID ?? ip)
const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, stickyId)
const stickyProvider = await stickyTracker?.get()
@@ -216,6 +237,9 @@ export async function handler(
return headers
})(),
body: reqBody,
// Propagate caller disconnects to the upstream provider request so
// abandoned Console requests do not leave orphaned inference work open.
signal: input.request.signal,
})
if (providerInfo.id.startsWith("console.")) {
@@ -311,9 +335,10 @@ export async function handler(
const streamConverter = createStreamPartConverter(providerInfo.format, opts.format)
const usageParser = providerInfo.createUsageParser()
const binaryDecoder = providerInfo.createBinaryStreamDecoder()
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
const stream = new ReadableStream({
start(c) {
const reader = res.body?.getReader()
reader = res.body?.getReader()
const decoder = new TextDecoder()
const encoder = new TextEncoder()
@@ -399,6 +424,11 @@ export async function handler(
return pump()
},
cancel() {
// When the downstream caller stops reading, release the upstream
// response body instead of keeping the provider/inference stream alive.
return reader?.cancel()
},
})
return new Response(stream, {
status: resStatus,
@@ -406,6 +436,15 @@ export async function handler(
headers: resHeaders,
})
} catch (error: any) {
// The caller disconnected before we finished. Because the outbound provider
// request shares input.request.signal, an aborted caller surfaces here as an
// AbortError. There is no client left to receive a body, so skip the error
// metric and 500 and return a quiet client-closed response.
if (input.request.signal.aborted || error?.name === "AbortError") {
logger.debug("REQUEST ABORTED BY CALLER")
return new Response(null, { status: 499 })
}
logger.metric({
"error.type": error.constructor.name,
"error.message": error.message,
@@ -419,6 +458,15 @@ export async function handler(
} catch {}
}
if (error instanceof RegionError)
return new Response(
JSON.stringify({
type: "error",
error: { type: error.constructor.name, message: error.message },
}),
{ status: 403 },
)
// 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 ||
@@ -539,70 +587,69 @@ export async function handler(
}))
}
if (retry.retryCount !== MAX_FAILOVER_RETRIES) {
let topPriority = Infinity
const providers = allProviders
.filter((provider) => provider.weight !== 0)
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.filter((provider) => {
if (provider.budgetPriority === undefined) return true
if (!providerBudget) return true
return providerBudget.qualify(provider.id, provider.budgetPriority)
})
.filter((provider) => {
if (!provider.tpmLimit) return true
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
return usage < provider.tpmLimit * 1_000_000
})
.filter((provider) => {
if (!provider.tpsGoal) return true
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
qualify: 0,
unqualify: 0,
}
const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify
return !isLowTps
})
.map((provider) => {
topPriority = Math.min(topPriority, provider.priority)
return provider
})
.filter((p) => p.priority <= topPriority)
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
// Use fallback provider if max retries reached
const fallbackProvider = allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
if (retry.retryCount === MAX_FAILOVER_RETRIES) return fallbackProvider
// Use the last 4 characters of session ID to select a provider
let h = 0
const l = stickyId.length
for (let i = l - 4; i < l; i++) {
h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int
}
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
const provider = providers[index || 0]
// sticky provider does not exist => use selected provider
if (!stickyProviderId) return provider
const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId)
if (!stickProvider) return provider
const preferBudgetProvider =
provider.budgetPriority !== undefined && providerBudget?.prefer(provider.id, provider.budgetPriority)
const preferTpsProvider = (() => {
if (!provider.tpsGoal) return false
let topPriority = Infinity
const providers = allProviders
.filter((provider) => provider.weight !== 0)
.filter((provider) => !retry.excludeProviders.includes(provider.id))
.filter((provider) => {
if (provider.budgetPriority === undefined) return true
if (!providerBudget) return true
return providerBudget.qualify(provider.id, provider.budgetPriority)
})
.filter((provider) => {
if (!provider.tpmLimit) return true
const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0
return usage < provider.tpmLimit * 1_000_000
})
.filter((provider) => {
if (!provider.tpsGoal) return true
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
qualify: 0,
unqualify: 0,
}
return tps.qualify > tps.unqualify * 3
})()
const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify
return !isLowTps
})
.map((provider) => {
topPriority = Math.min(topPriority, provider.priority)
return provider
})
.filter((p) => p.priority <= topPriority)
.flatMap((provider) => Array<typeof provider>(provider.weight).fill(provider))
if (!preferBudgetProvider && !preferTpsProvider) return stickProvider
return provider
// Use the last 4 characters of session ID to select a provider
let h = 0
const l = stickyId.length
for (let i = l - 4; i < l; i++) {
h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int
}
const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1
const provider = providers[index || 0] ?? fallbackProvider
// fallback provider
return allProviders.find((provider) => provider.id === modelInfo.fallbackProvider)
// sticky provider does not exist => use selected provider
if (!stickyProviderId) return provider
const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId)
if (!stickProvider) return provider
const preferBudgetProvider =
provider.budgetPriority !== undefined && providerBudget?.prefer(provider.id, provider.budgetPriority)
const preferTpsProvider = (() => {
if (!provider.tpsGoal) return false
const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? {
qualify: 0,
unqualify: 0,
}
return tps.qualify > tps.unqualify * 3
})()
if (!preferBudgetProvider && !preferTpsProvider) return stickProvider
return provider
})()
if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable"))
@@ -639,7 +686,10 @@ export async function handler(
tx
.select({
apiKey: KeyTable.id,
workspaceID: KeyTable.workspaceID,
workspace: {
id: WorkspaceTable.id,
region: WorkspaceTable.region,
},
billing: {
balance: BillingTable.balance,
paymentMethodID: BillingTable.paymentMethodID,
@@ -717,13 +767,13 @@ export async function handler(
if (
modelInfo.id.startsWith("alpha-") &&
Resource.App.stage === "production" &&
!ADMIN_WORKSPACES.includes(data.workspaceID)
!ADMIN_WORKSPACES.includes(data.workspace.id)
)
throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id }))
logger.metric({
api_key: data.apiKey,
workspace: data.workspaceID,
workspace: data.workspace.id,
user_id: data.user.id,
...(() => {
if (data.billing.subscription)
@@ -740,13 +790,14 @@ export async function handler(
return {
apiKeyId: data.apiKey,
workspaceID: data.workspaceID,
workspaceID: data.workspace.id,
region: data.workspace.region,
billing: data.billing,
user: data.user,
black: data.black,
lite: data.lite,
provider: data.provider,
isFree: ADMIN_WORKSPACES.includes(data.workspaceID),
isFree: ADMIN_WORKSPACES.includes(data.workspace.id),
isDisabled: !!data.timeDisabled,
}
}
@@ -0,0 +1 @@
ALTER TABLE `workspace` ADD `region` json;
@@ -0,0 +1,3093 @@
{
"version": "6",
"dialect": "mysql",
"id": "d78d7057-b247-4572-9150-2532149169db",
"prevIds": ["a946ea14-a35d-49cc-a10a-8c93c30a3cb1"],
"ddl": [
{
"name": "account",
"entityType": "tables"
},
{
"name": "auth",
"entityType": "tables"
},
{
"name": "benchmark",
"entityType": "tables"
},
{
"name": "billing",
"entityType": "tables"
},
{
"name": "coupon",
"entityType": "tables"
},
{
"name": "lite",
"entityType": "tables"
},
{
"name": "payment",
"entityType": "tables"
},
{
"name": "subscription",
"entityType": "tables"
},
{
"name": "usage",
"entityType": "tables"
},
{
"name": "ip_rate_limit",
"entityType": "tables"
},
{
"name": "ip",
"entityType": "tables"
},
{
"name": "key_rate_limit",
"entityType": "tables"
},
{
"name": "model_sticky_provider",
"entityType": "tables"
},
{
"name": "model_tpm_rate_limit",
"entityType": "tables"
},
{
"name": "model_tps_rate_limit",
"entityType": "tables"
},
{
"name": "key",
"entityType": "tables"
},
{
"name": "model",
"entityType": "tables"
},
{
"name": "provider",
"entityType": "tables"
},
{
"name": "referral_code",
"entityType": "tables"
},
{
"name": "referral_reward",
"entityType": "tables"
},
{
"name": "referral",
"entityType": "tables"
},
{
"name": "user",
"entityType": "tables"
},
{
"name": "workspace",
"entityType": "tables"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "account"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "account"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "account"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "account"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "auth"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "auth"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "auth"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "auth"
},
{
"type": "enum('email','github','google')",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "provider",
"entityType": "columns",
"table": "auth"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "subject",
"entityType": "columns",
"table": "auth"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "account_id",
"entityType": "columns",
"table": "auth"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "varchar(64)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "model",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "varchar(64)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "agent",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "mediumtext",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "result",
"entityType": "columns",
"table": "benchmark"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "customer_id",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "payment_method_id",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(32)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "payment_method_type",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(4)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "payment_method_last4",
"entityType": "columns",
"table": "billing"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "balance",
"entityType": "columns",
"table": "billing"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "monthly_limit",
"entityType": "columns",
"table": "billing"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "monthly_usage",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_monthly_usage_updated",
"entityType": "columns",
"table": "billing"
},
{
"type": "boolean",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "reload",
"entityType": "columns",
"table": "billing"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "reload_trigger",
"entityType": "columns",
"table": "billing"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "reload_amount",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "reload_error",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_reload_error",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_reload_locked_till",
"entityType": "columns",
"table": "billing"
},
{
"type": "json",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "subscription",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(28)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "subscription_id",
"entityType": "columns",
"table": "billing"
},
{
"type": "enum('20','100','200')",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "subscription_plan",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_subscription_booked",
"entityType": "columns",
"table": "billing"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_subscription_selected",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(28)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "lite_subscription_id",
"entityType": "columns",
"table": "billing"
},
{
"type": "json",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "lite",
"entityType": "columns",
"table": "billing"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "coupon"
},
{
"type": "enum('BUILDATHON','GO1MONTH50','GOFREEMONTH','GO3MONTHS100','GO6MONTHS100','GO12MONTHS100')",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "coupon"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_redeemed",
"entityType": "columns",
"table": "coupon"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "lite"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "lite"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "user_id",
"entityType": "columns",
"table": "lite"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "rolling_usage",
"entityType": "columns",
"table": "lite"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "weekly_usage",
"entityType": "columns",
"table": "lite"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "monthly_usage",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_rolling_updated",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_weekly_updated",
"entityType": "columns",
"table": "lite"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_monthly_updated",
"entityType": "columns",
"table": "lite"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "payment"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "payment"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "payment"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "payment"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "payment"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "customer_id",
"entityType": "columns",
"table": "payment"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "invoice_id",
"entityType": "columns",
"table": "payment"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "payment_id",
"entityType": "columns",
"table": "payment"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "amount",
"entityType": "columns",
"table": "payment"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_refunded",
"entityType": "columns",
"table": "payment"
},
{
"type": "json",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "enrichment",
"entityType": "columns",
"table": "payment"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "subscription"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "subscription"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "subscription"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "subscription"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "subscription"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "user_id",
"entityType": "columns",
"table": "subscription"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "rolling_usage",
"entityType": "columns",
"table": "subscription"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "fixed_usage",
"entityType": "columns",
"table": "subscription"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_rolling_updated",
"entityType": "columns",
"table": "subscription"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_fixed_updated",
"entityType": "columns",
"table": "subscription"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "usage"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "usage"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "usage"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "model",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "provider",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "input_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "output_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "reasoning_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "cache_read_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "cache_write_5m_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "cache_write_1h_tokens",
"entityType": "columns",
"table": "usage"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "cost",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(30)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "key_id",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(30)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "usage"
},
{
"type": "json",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "enrichment",
"entityType": "columns",
"table": "usage"
},
{
"type": "varchar(45)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "ip",
"entityType": "columns",
"table": "ip_rate_limit"
},
{
"type": "varchar(10)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "interval",
"entityType": "columns",
"table": "ip_rate_limit"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "count",
"entityType": "columns",
"table": "ip_rate_limit"
},
{
"type": "varchar(45)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "ip",
"entityType": "columns",
"table": "ip"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "ip"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "ip"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "ip"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "usage",
"entityType": "columns",
"table": "ip"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "key",
"entityType": "columns",
"table": "key_rate_limit"
},
{
"type": "varchar(40)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "interval",
"entityType": "columns",
"table": "key_rate_limit"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "count",
"entityType": "columns",
"table": "key_rate_limit"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "model_sticky_provider"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "model_sticky_provider"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "model_sticky_provider"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "model_sticky_provider"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "provider_id",
"entityType": "columns",
"table": "model_sticky_provider"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "model_tpm_rate_limit"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "interval",
"entityType": "columns",
"table": "model_tpm_rate_limit"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "count",
"entityType": "columns",
"table": "model_tpm_rate_limit"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "model_tps_rate_limit"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "interval",
"entityType": "columns",
"table": "model_tps_rate_limit"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "qualify",
"entityType": "columns",
"table": "model_tps_rate_limit"
},
{
"type": "int",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "unqualify",
"entityType": "columns",
"table": "model_tps_rate_limit"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "key"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "key"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "key"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "key"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "key"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "key"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "key",
"entityType": "columns",
"table": "key"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "user_id",
"entityType": "columns",
"table": "key"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_used",
"entityType": "columns",
"table": "key"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "model"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "model"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "model"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "model"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "model"
},
{
"type": "varchar(64)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "model",
"entityType": "columns",
"table": "model"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "provider"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "provider"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "provider"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "provider"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "provider"
},
{
"type": "varchar(64)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "provider",
"entityType": "columns",
"table": "provider"
},
{
"type": "text",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "credentials",
"entityType": "columns",
"table": "provider"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "referral_code"
},
{
"type": "varchar(10)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "code",
"entityType": "columns",
"table": "referral_code"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "referral_code"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "referral_code"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "referral_code"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "referral_id",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "bigint",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "amount",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_applied",
"entityType": "columns",
"table": "referral_reward"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "referral"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "referral"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "referral"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "referral"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "referral"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "invitee_account_id",
"entityType": "columns",
"table": "referral"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "user"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "user"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "user"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "user"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "user"
},
{
"type": "varchar(30)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "account_id",
"entityType": "columns",
"table": "user"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "user"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "user"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_seen",
"entityType": "columns",
"table": "user"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "color",
"entityType": "columns",
"table": "user"
},
{
"type": "enum('admin','member')",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "role",
"entityType": "columns",
"table": "user"
},
{
"type": "int",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "monthly_limit",
"entityType": "columns",
"table": "user"
},
{
"type": "bigint",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "monthly_usage",
"entityType": "columns",
"table": "user"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_monthly_usage_updated",
"entityType": "columns",
"table": "user"
},
{
"type": "varchar(30)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "varchar(255)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "workspace"
},
{
"type": "varchar(255)",
"notNull": true,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "workspace"
},
{
"type": "json",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "region",
"entityType": "columns",
"table": "workspace"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(now())",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "workspace"
},
{
"type": "timestamp(3)",
"notNull": true,
"autoIncrement": false,
"default": "(CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3))",
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "workspace"
},
{
"type": "timestamp(3)",
"notNull": false,
"autoIncrement": false,
"default": null,
"onUpdateNow": false,
"onUpdateNowFsp": null,
"charSet": null,
"collation": null,
"generated": null,
"name": "time_deleted",
"entityType": "columns",
"table": "workspace"
},
{
"columns": ["id"],
"name": "PRIMARY",
"table": "account",
"entityType": "pks"
},
{
"columns": ["id"],
"name": "PRIMARY",
"table": "auth",
"entityType": "pks"
},
{
"columns": ["id"],
"name": "PRIMARY",
"table": "benchmark",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "billing",
"entityType": "pks"
},
{
"columns": ["email", "type"],
"name": "PRIMARY",
"table": "coupon",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "lite",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "payment",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "subscription",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "usage",
"entityType": "pks"
},
{
"columns": ["ip", "interval"],
"name": "PRIMARY",
"table": "ip_rate_limit",
"entityType": "pks"
},
{
"columns": ["ip"],
"name": "PRIMARY",
"table": "ip",
"entityType": "pks"
},
{
"columns": ["key", "interval"],
"name": "PRIMARY",
"table": "key_rate_limit",
"entityType": "pks"
},
{
"columns": ["id"],
"name": "PRIMARY",
"table": "model_sticky_provider",
"entityType": "pks"
},
{
"columns": ["id", "interval"],
"name": "PRIMARY",
"table": "model_tpm_rate_limit",
"entityType": "pks"
},
{
"columns": ["id", "interval"],
"name": "PRIMARY",
"table": "model_tps_rate_limit",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "key",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "model",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "provider",
"entityType": "pks"
},
{
"columns": ["workspace_id"],
"name": "PRIMARY",
"table": "referral_code",
"entityType": "pks"
},
{
"columns": ["workspace_id", "referral_id"],
"name": "PRIMARY",
"table": "referral_reward",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "referral",
"entityType": "pks"
},
{
"columns": ["workspace_id", "id"],
"name": "PRIMARY",
"table": "user",
"entityType": "pks"
},
{
"columns": ["id"],
"name": "PRIMARY",
"table": "workspace",
"entityType": "pks"
},
{
"columns": [
{
"value": "provider",
"isExpression": false
},
{
"value": "subject",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "provider",
"entityType": "indexes",
"table": "auth"
},
{
"columns": [
{
"value": "account_id",
"isExpression": false
}
],
"isUnique": false,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "account_id",
"entityType": "indexes",
"table": "auth"
},
{
"columns": [
{
"value": "time_created",
"isExpression": false
}
],
"isUnique": false,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "time_created",
"entityType": "indexes",
"table": "benchmark"
},
{
"columns": [
{
"value": "customer_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "global_customer_id",
"entityType": "indexes",
"table": "billing"
},
{
"columns": [
{
"value": "subscription_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "global_subscription_id",
"entityType": "indexes",
"table": "billing"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "user_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "workspace_user_id",
"entityType": "indexes",
"table": "lite"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "user_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "workspace_user_id",
"entityType": "indexes",
"table": "subscription"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "time_created",
"isExpression": false
}
],
"isUnique": false,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "usage_time_created",
"entityType": "indexes",
"table": "usage"
},
{
"columns": [
{
"value": "key",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "global_key",
"entityType": "indexes",
"table": "key"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "model",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "model_workspace_model",
"entityType": "indexes",
"table": "model"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "provider",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "workspace_provider",
"entityType": "indexes",
"table": "provider"
},
{
"columns": [
{
"value": "code",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "code",
"entityType": "indexes",
"table": "referral_code"
},
{
"columns": [
{
"value": "invitee_account_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "invitee_account_id",
"entityType": "indexes",
"table": "referral"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "account_id",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "user_account_id",
"entityType": "indexes",
"table": "user"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
},
{
"value": "email",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "user_email",
"entityType": "indexes",
"table": "user"
},
{
"columns": [
{
"value": "account_id",
"isExpression": false
}
],
"isUnique": false,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "global_account_id",
"entityType": "indexes",
"table": "user"
},
{
"columns": [
{
"value": "email",
"isExpression": false
}
],
"isUnique": false,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "global_email",
"entityType": "indexes",
"table": "user"
},
{
"columns": [
{
"value": "slug",
"isExpression": false
}
],
"isUnique": true,
"using": null,
"algorithm": null,
"lock": null,
"nameExplicit": true,
"name": "slug",
"entityType": "indexes",
"table": "workspace"
}
],
"renames": []
}
@@ -1,4 +1,4 @@
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { json, primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
import { timestamps, ulid } from "../drizzle/types"
export const WorkspaceTable = mysqlTable(
@@ -7,6 +7,7 @@ export const WorkspaceTable = mysqlTable(
id: ulid("id").notNull().primaryKey(),
slug: varchar("slug", { length: 255 }),
name: varchar("name", { length: 255 }).notNull(),
region: json("region").$type<("us" | "eu" | "sg" | "cn")[]>(),
...timestamps,
},
(table) => [uniqueIndex("slug").on(table.slug)],
+25 -3
View File
@@ -11,6 +11,9 @@ import { Key } from "./key"
import { and, eq, isNull, sql } from "drizzle-orm"
export namespace Workspace {
export const Region = z.enum(["us", "eu", "sg", "cn"])
export type Region = z.infer<typeof Region>
export const create = fn(
z.object({
name: z.string().min(1),
@@ -57,22 +60,41 @@ export namespace Workspace {
export const update = fn(
z.object({
name: z.string().min(1).max(255),
name: z.string().min(1).max(255).optional(),
region: z.array(Region).min(1).optional(),
}),
async ({ name }) => {
async (input) => {
Actor.assertAdmin()
const workspaceID = Actor.workspace()
return await Database.use((tx) =>
tx
.update(WorkspaceTable)
.set({
name,
...("name" in input ? { name: input.name } : {}),
...("region" in input ? { region: input.region } : {}),
})
.where(eq(WorkspaceTable.id, workspaceID)),
)
},
)
export const setDefaultRegion = fn(
z.object({
country: z.string().optional(),
}),
async (input) => {
const region: Workspace.Region[] =
input.country?.toUpperCase() === "CN" ? ["us", "eu", "sg", "cn"] : ["us", "eu", "sg"]
await Database.use((tx) =>
tx
.update(WorkspaceTable)
.set({ region })
.where(and(eq(WorkspaceTable.id, Actor.workspace()), isNull(WorkspaceTable.region))),
)
return region
},
)
export const remove = fn(z.void(), async () => {
await Database.use((tx) =>
tx
+3
View File
@@ -1,6 +1,7 @@
export * as Observability from "./observability"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "./effect/layer-node"
import { Effect, Layer, Logger, References } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { OtlpSerialization } from "effect/unstable/observability"
@@ -19,3 +20,5 @@ export const layer = Layer.unwrap(
return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer))
}),
)
export const node = LayerNode.make({ name: "observability", layer, deps: [] })
+101 -52
View File
@@ -1,7 +1,7 @@
import { define } from "./internal"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
import { ModelsDev } from "../models-dev"
import { ProviderV2 } from "../provider"
@@ -10,7 +10,7 @@ function released(date: string) {
return Number.isFinite(time) ? time : 0
}
function cost(input: ModelsDev.Model["cost"]) {
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
const base = {
input: input?.input ?? 0,
output: input?.output ?? 0,
@@ -19,30 +19,101 @@ function cost(input: ModelsDev.Model["cost"]) {
write: input?.cache_write ?? 0,
},
}
if (!input?.context_over_200k) return [base]
return [
base,
{
tier: {
type: "context" as const,
size: 200_000,
},
input: input.context_over_200k.input,
output: input.context_over_200k.output,
...(input?.tiers?.map((item) => ({
tier: item.tier,
input: item.input,
output: item.output,
cache: {
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
read: item.cache_read ?? 0,
write: item.cache_write ?? 0,
},
},
})) ?? []),
...(input?.context_over_200k
? [
{
tier: {
type: "context" as const,
size: 200_000,
},
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
},
},
]
: []),
]
}
function variants(model: ModelsDev.Model) {
return Object.entries(model.experimental?.modes ?? {}).map(([id, item]) => ({
id: ModelV2.VariantID.make(id),
headers: { ...(item.provider?.headers ?? {}) },
body: { ...(item.provider?.body ?? {}) },
}))
function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
if (!override) return base
const next = cost(override)
const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
cache: { ...left.cache, ...right.cache },
})
const tiers = new Map(baseTiers.map((item) => [tierKey(item), item]))
for (const item of nextTiers) {
const current = tiers.get(tierKey(item))
tiers.set(tierKey(item), current ? merge(current, item) : item)
}
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
}
function modeName(model: ModelsDev.Model, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
}
function applyModel(
draft: ModelV2Info,
model: ModelsDev.Model,
input: {
readonly name?: string
readonly cost?: ModelV2Info["cost"]
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
} = {},
) {
draft.name = input.name ?? model.name
draft.family = model.family
draft.api = model.provider?.npm
? {
id: model.id,
type: "aisdk",
package: model.provider.npm,
url: model.provider.api,
}
: {
id: model.id,
type: "native",
url: model.provider?.api,
settings: {},
}
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = []
draft.time.released = released(model.release_date)
draft.cost = input.cost ?? cost(model.cost)
draft.status = model.status ?? "active"
draft.enabled = true
draft.limit = {
context: model.limit.context,
input: model.limit.input,
output: model.limit.output,
}
Object.assign(draft.request.headers, input.request?.headers ?? {})
Object.assign(draft.request.body, input.request?.body ?? {})
}
export const ModelsDevPlugin = define({
@@ -89,39 +160,17 @@ export const ModelsDevPlugin = define({
})
for (const model of Object.values(item.models)) {
const modelID = ModelV2.ID.make(model.id)
catalog.model.update(providerID, modelID, (draft) => {
draft.name = model.name
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.api = model.provider?.npm
? {
id: draft.api.id,
type: "aisdk",
package: model.provider?.npm,
url: model.provider.api,
}
: {
id: draft.api.id,
type: "native",
url: model.provider?.api,
settings: {},
}
draft.capabilities = {
tools: model.tool_call,
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
draft.variants = variants(model)
draft.time.released = released(model.release_date)
draft.cost = cost(model.cost)
draft.status = model.status ?? "active"
draft.enabled = true
draft.limit = {
context: model.limit.context,
input: model.limit.input,
output: model.limit.output,
}
})
const baseCost = cost(model.cost)
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost }))
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
applyModel(draft, model, {
name: modeName(model, mode),
cost: mergeCost(baseCost, options.cost),
request: options.provider,
}),
)
}
}
}
}),
@@ -8,8 +8,10 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Location } from "@opencode-ai/core/location"
import { ModelV2 } from "@opencode-ai/core/model"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
@@ -25,6 +27,103 @@ const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.no
const it = testEffect(layer)
describe("ModelsDevPlugin", () => {
it.effect("projects models.dev modes as separate models instead of variants", () =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
const catalog = yield* Catalog.Service
const models = ModelsDev.Service.of({
get: () =>
Effect.succeed({
acme: {
id: "acme",
name: "Acme",
env: [],
npm: "@ai-sdk/openai-compatible",
api: "https://api.acme.test/v1",
models: {
"gpt-5.4": {
id: "gpt-5.4",
name: "GPT-5.4",
family: "gpt",
release_date: "2026-01-01",
attachment: false,
reasoning: true,
temperature: true,
tool_call: true,
cost: {
input: 2.5,
output: 15,
tiers: [
{
tier: { type: "context", size: 272_000 },
input: 3,
output: 18,
cache_read: 0.25,
},
],
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
},
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
experimental: {
modes: {
fast: {
cost: { input: 5, output: 30, cache_read: 0.5 },
provider: {
headers: { "x-mode": "fast" },
body: { service_tier: "priority" },
},
},
},
},
},
},
},
} satisfies Record<string, ModelsDev.Provider>),
refresh: () => Effect.void,
})
yield* ModelsDevPlugin.effect(
host({
catalog: catalogHost(catalog),
integration: integrationHost(integrations),
}),
).pipe(Effect.provideService(ModelsDev.Service, models))
const providerID = ProviderV2.ID.make("acme")
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
expect(base?.variants).toEqual([])
expect(base?.request.body).toEqual({})
expect(fast).toMatchObject({
id: "gpt-5.4-fast",
providerID: "acme",
name: "GPT-5.4 Fast",
api: { id: "gpt-5.4" },
request: {
headers: { "x-mode": "fast" },
body: { service_tier: "priority" },
},
variants: [],
})
expect(fast?.cost).toEqual([
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },
{
tier: { type: "context", size: 272_000 },
input: 3,
output: 18,
cache: { read: 0.25, write: 0 },
},
{
tier: { type: "context", size: 200_000 },
input: 5,
output: 22.5,
cache: { read: 0.5, write: 0 },
},
])
}),
)
it.effect("registers key methods for providers with environment variables", () =>
Effect.acquireUseRelease(
Effect.sync(() => {
+27 -2
View File
@@ -17,7 +17,7 @@ import {
import type { UpdaterState } from "@opencode-ai/app/updater"
import * as Sentry from "@sentry/solid"
import type { AsyncStorage } from "@solid-primitives/storage"
import { MemoryRouter } from "@solidjs/router"
import { createMemoryHistory, MemoryRouter, type BaseRouterProps } from "@solidjs/router"
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../../package.json"
@@ -63,6 +63,7 @@ const [updaterState, setUpdaterState] = createSignal<UpdaterState>({ status: "di
void window.api.updater.subscribe(setUpdaterState)
const deepLinkEvent = "opencode:deep-link"
const lastActiveUrlKey = "opencode.desktop.last-active-url"
const emitDeepLinks = (urls: string[]) => {
if (urls.length === 0) return
@@ -77,6 +78,30 @@ const listenForDeepLinks = () => {
return window.api.onDeepLink((urls) => emitDeepLinks(urls))
}
function getLastActiveUrl() {
if (typeof localStorage !== "object") return "/"
try {
const value = localStorage.getItem(lastActiveUrlKey)
if (value?.startsWith("/") && !value.startsWith("//")) return value
} catch {}
return "/"
}
function setLastActiveUrl(value: string) {
if (typeof localStorage !== "object") return
try {
localStorage.setItem(lastActiveUrlKey, value)
} catch {}
}
function DesktopMemoryRouter(props: BaseRouterProps) {
const history = createMemoryHistory()
const initialUrl = getLastActiveUrl()
if (initialUrl !== "/") history.set({ value: initialUrl, replace: true, scroll: false })
onCleanup(history.listen(setLastActiveUrl))
return <MemoryRouter {...props} history={history} />
}
const createPlatform = (): Platform => {
const attachmentPaths = new WeakMap<File, string>()
const os = (() => {
@@ -367,7 +392,7 @@ render(() => {
<Show when={ready()} fallback={splash}>
<Show when={effectiveDefaultServer()} keyed>
{(key) => (
<AppInterface defaultServer={key} servers={servers()} router={MemoryRouter}>
<AppInterface defaultServer={key} servers={servers()} router={DesktopMemoryRouter}>
<Inner />
</AppInterface>
)}
+1 -3
View File
@@ -186,7 +186,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ac
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient.HttpClient> = Layer.effect(
Service,
Effect.gen(function* () {
const repo = yield* AccountRepo.Service
@@ -456,8 +456,6 @@ export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(FetchHttpClient.layer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [AccountRepo.node, httpClient] })
export * as Account from "./account"
+1 -3
View File
@@ -39,7 +39,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ac
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
@@ -166,8 +166,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Database.node] })
export * as AccountRepo from "./repo"
+10 -8
View File
@@ -1,7 +1,9 @@
import { Agent } from "@/agent/agent"
import { Command } from "@/command"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@@ -139,7 +141,7 @@ export const loaderLayer = Layer.effect(
}),
)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const loader = yield* Loader
@@ -199,12 +201,12 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(loaderLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Agent.defaultLayer),
Layer.provide(Command.defaultLayer),
Layer.provide(InstanceStore.defaultLayer),
)
export const loaderNode = LayerNode.make({
service: Loader,
layer: loaderLayer,
deps: [Provider.node, Agent.node, Command.node, InstanceStore.node],
})
export const node = LayerNode.make({ service: Service, layer, deps: [loaderNode] })
export * as Directory from "./directory"
+7 -5
View File
@@ -30,6 +30,7 @@ import {
type SetSessionModeResponse,
} from "@agentclientprotocol/sdk"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import type { AssistantMessage, Message, OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
import * as ACPError from "./error"
@@ -569,23 +570,24 @@ export function make(input: {
}
function makeSessionService() {
return ManagedRuntime.make(ACPSession.defaultLayer).runSync(
return ManagedRuntime.make(AppNodeBuilder.build(ACPSession.node)).runSync(
ACPSession.Service.use((service) => Effect.succeed(service)),
)
}
function makeDirectoryService(sdk: OpencodeClient) {
return ManagedRuntime.make(
Directory.layer.pipe(
Layer.provide(
AppNodeBuilder.build(Directory.node, [
[
Directory.loaderNode,
Layer.succeed(
Directory.Loader,
Directory.Loader.of({
load: (directory) => request(() => loadDirectorySnapshot(sdk, directory), "directory"),
}),
),
),
),
],
]),
).runSync(Directory.Service.use((service) => Effect.succeed(service)))
}
+3 -2
View File
@@ -1,5 +1,6 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Context, Effect, Layer, Ref } from "effect"
@@ -93,7 +94,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/AC
type State = Map<string, Info>
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const sessions = yield* Ref.make<State>(new Map())
@@ -200,7 +201,7 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: Service, layer, deps: [] })
function makeSession(input: StoreInput): Info {
return {
+13 -6
View File
@@ -1,7 +1,10 @@
import type { AgentSideConnection, Usage } from "@agentclientprotocol/sdk"
import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@opencode-ai/sdk/v2"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
@@ -132,7 +135,7 @@ export const contextLimitLoaderLayer = Layer.effect(
}),
)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const messageLoader = yield* MessageLoader
@@ -223,10 +226,14 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(contextLimitLoaderLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(InstanceStore.defaultLayer),
)
export const messageLoaderNode = LayerNode.unbound(MessageLoader, Node.tags.values.global)
export const contextLimitLoaderNode = makeGlobalNode({
service: ContextLimitLoader,
layer: contextLimitLoaderLayer,
deps: [Provider.node, InstanceStore.node],
})
export const node = makeGlobalNode({ service: Service, layer, deps: [messageLoaderNode, contextLimitLoaderNode] })
export * as UsageService from "./usage"
+1 -10
View File
@@ -85,7 +85,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ag
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -438,15 +438,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Skill.defaultLayer),
Layer.provide(locationServiceMapLayer),
)
const locationServiceMapNode = LayerNode.make({
service: LocationServiceMap.Service,
layer: locationServiceMapLayer,
+1 -3
View File
@@ -49,7 +49,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* FSUtil.Service
@@ -92,8 +92,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node] })
export * as Auth from "."
+1 -3
View File
@@ -15,7 +15,7 @@ export {
} from "@opencode-ai/core/background-job"
/** Keeps the legacy service instance-scoped while sharing the core registry engine. */
export const layer = Layer.effect(
const layer = Layer.effect(
CoreBackgroundJob.Service,
Effect.gen(function* () {
const state = yield* InstanceState.make(() => CoreBackgroundJob.make)
@@ -32,8 +32,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: CoreBackgroundJob.Service, layer, deps: [] })
export * as BackgroundJob from "./job"
+2 -1
View File
@@ -7,8 +7,9 @@ export const ScrapCommand = cmd({
builder: (yargs) => yargs,
async handler() {
const { Project } = await import("@/project/project")
const { AppNodeBuilder } = await import("@opencode-ai/core/effect/app-node-builder")
const { makeRuntime } = await import("@opencode-ai/core/effect/runtime")
const runtime = makeRuntime(Project.Service, Project.defaultLayer)
const runtime = makeRuntime(Project.Service, AppNodeBuilder.build(Project.node))
const list = await runtime.runPromise((project) => project.list())
process.stdout.write(JSON.stringify(list, null, 2) + EOL)
},
+12 -2
View File
@@ -233,7 +233,14 @@ export const RunCommand = effectCmd({
hidden: true,
describe: "cap visible interactive replay to the newest N messages",
})
.option("dangerously-skip-permissions", {
.option("interactive", {
alias: ["i"],
type: "boolean",
describe: "run in direct interactive split-footer mode",
default: false,
})
.option("auto", {
alias: ["yolo", "dangerously-skip-permissions"],
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
@@ -780,7 +787,7 @@ export const RunCommand = effectCmd({
const permission = event.properties
if (permission.sessionID !== sessionID) continue
if (args["dangerously-skip-permissions"]) {
if (args.auto) {
await client.permission.reply({
requestID: permission.id,
reply: "once",
@@ -981,9 +988,12 @@ export async function runMini(input: MiniCommandInput) {
variant: undefined,
thinking: undefined,
mini: true,
interactive: false,
replay: input.replay ?? true,
"replay-limit": input.replayLimit,
replayLimit: input.replayLimit,
auto: false,
yolo: false,
"dangerously-skip-permissions": false,
dangerouslySkipPermissions: false,
demo: input.demo ?? false,
+7
View File
@@ -105,6 +105,12 @@ export const TuiThreadCommand = cmd({
type: "string",
describe: "agent to use",
})
.option("auto", {
alias: ["yolo", "dangerously-skip-permissions"],
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
})
.option("mini", {
type: "boolean",
describe: "start the minimal interactive interface",
@@ -272,6 +278,7 @@ export const TuiThreadCommand = cmd({
model: args.model,
prompt,
fork: args.fork,
auto: args.auto,
},
}),
)
+1 -7
View File
@@ -55,7 +55,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -172,12 +172,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(MCP.defaultLayer),
Layer.provide(Skill.defaultLayer),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Config.node, MCP.node, Skill.node] })
export * as Command from "."
+1 -11
View File
@@ -172,7 +172,7 @@ function writableGlobal(info: Info) {
return next
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -671,16 +671,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EffectFlock.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Account.defaultLayer),
Layer.provide(Npm.defaultLayer),
Layer.provide(FetchHttpClient.layer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,
+5 -3
View File
@@ -2,6 +2,8 @@ export * as TuiConfig from "./tui"
import path from "path"
import { mergeDeep, unique } from "remeda"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Context, Effect, Fiber, Layer } from "effect"
import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths"
@@ -223,7 +225,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
}
})
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const directory = yield* CurrentWorkingDirectory
@@ -257,9 +259,9 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")),
)
export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer, deps: [Npm.node, FSUtil.node] })
const { runPromise } = makeRuntime(Service, defaultLayer)
const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export async function waitForDependencies() {
await runPromise((svc) => svc.waitForDependencies())
@@ -150,7 +150,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Wo
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
@@ -601,7 +601,11 @@ export const layer = Layer.effect(
}),
fallback: "",
response: "text",
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
}).pipe(
Effect.provide(
LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
),
)
: ""
if (sourcePatch) {
@@ -617,7 +621,11 @@ export const layer = Layer.effect(
body: HttpBody.jsonUnsafe({ patch: sourcePatch }),
}),
fallback: { applied: false },
}).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))))
}).pipe(
Effect.provide(
LayerNode.compile(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
),
)
}
if (input.workspaceID === null) {
@@ -885,19 +893,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Session.defaultLayer),
Layer.provide(SessionPrompt.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(Vcs.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(FetchHttpClient.layer),
Layer.provide(RuntimeFlags.defaultLayer),
)
const TIMEOUT = 5000
type HistoryEvent = {
+58 -53
View File
@@ -38,7 +38,8 @@ import { Command } from "@/command"
import { Truncate } from "@/tool/truncate"
import { ToolRegistry } from "@/tool/registry"
import { Format } from "@/format"
import { InstanceLayer } from "@/project/instance-layer"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
import { Workspace } from "@/control-plane/workspace"
@@ -51,59 +52,63 @@ import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { BackgroundJob } from "@/background/job"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { SessionProjector } from "@opencode-ai/core/session/projector"
export const AppLayer = Layer.mergeAll(
Npm.defaultLayer,
FSUtil.defaultLayer,
Database.defaultLayer,
Auth.defaultLayer,
Account.defaultLayer,
Config.defaultLayer,
Git.defaultLayer,
Storage.defaultLayer,
Snapshot.defaultLayer,
Plugin.defaultLayer,
ModelsDev.defaultLayer,
Provider.defaultLayer,
ProviderAuth.defaultLayer,
Agent.defaultLayer,
Skill.defaultLayer,
Discovery.defaultLayer,
Question.defaultLayer,
Permission.defaultLayer,
Todo.defaultLayer,
Session.defaultLayer,
SessionStatus.defaultLayer,
BackgroundJob.defaultLayer,
RuntimeFlags.defaultLayer,
EventV2Bridge.defaultLayer,
SessionRunState.defaultLayer,
SessionProcessor.defaultLayer,
SessionCompaction.defaultLayer,
SessionRevert.defaultLayer,
SessionSummary.defaultLayer,
SessionPrompt.defaultLayer,
Instruction.defaultLayer,
LLM.defaultLayer,
LSP.defaultLayer,
MCP.defaultLayer,
McpAuth.defaultLayer,
Command.defaultLayer,
Truncate.defaultLayer,
ToolRegistry.defaultLayer,
Format.defaultLayer,
Project.defaultLayer,
Vcs.defaultLayer,
Workspace.defaultLayer,
Worktree.appLayer,
Installation.defaultLayer,
ShareNext.defaultLayer,
SessionShare.defaultLayer,
).pipe(
Layer.provideMerge(Ripgrep.defaultLayer),
Layer.provideMerge(InstanceLayer.layer),
Layer.provideMerge(Observability.layer),
)
export const AppLayer = AppNodeBuilder.build(
LayerNode.group([
Npm.node,
FSUtil.node,
Database.node,
Auth.node,
Account.node,
Config.node,
Git.node,
Storage.node,
Snapshot.node,
Plugin.node,
ModelsDev.node,
Provider.node,
ProviderAuth.node,
Agent.node,
Skill.node,
Discovery.node,
Question.node,
Permission.node,
Todo.node,
Session.node,
SessionProjector.node,
SessionStatus.node,
BackgroundJob.node,
RuntimeFlags.node,
EventV2Bridge.node,
SessionRunState.node,
SessionProcessor.node,
SessionCompaction.node,
SessionRevert.node,
SessionSummary.node,
SessionPrompt.node,
Instruction.node,
LLM.node,
LSP.node,
MCP.node,
McpAuth.node,
Command.node,
Truncate.node,
ToolRegistry.node,
Format.node,
InstanceStore.node,
Project.node,
Vcs.node,
Workspace.node,
Worktree.node,
Installation.node,
ShareNext.node,
SessionShare.node,
]),
[[InstanceStore.bootstrapNode, InstanceBootstrap.node]],
).pipe(Layer.provideMerge(AppNodeBuilder.build(Ripgrep.node)), Layer.provideMerge(Observability.layer))
const rt = ManagedRuntime.make(AppLayer, { memoMap })
type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">
@@ -1,4 +1,6 @@
import { Layer, ManagedRuntime } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Plugin } from "@/plugin"
import { LSP } from "@/lsp/lsp"
@@ -10,14 +12,8 @@ import { Config } from "@/config/config"
import * as Observability from "@opencode-ai/core/observability"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
export const BootstrapLayer = Layer.mergeAll(
Config.defaultLayer,
Plugin.defaultLayer,
ShareNext.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Vcs.defaultLayer,
Snapshot.defaultLayer,
export const BootstrapLayer = AppNodeBuilder.build(
LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
).pipe(Layer.provide(Observability.layer))
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
@@ -71,9 +71,7 @@ export const layer = (overrides: Partial<Info> = {}) =>
}),
).pipe(Layer.provide(emptyConfigLayer))
export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
export const node = LayerNode.make({ service: Service, layer: defaultLayer, deps: [] })
export const node = LayerNode.make({ service: Service, layer: Service.defaultLayer.pipe(Layer.orDie), deps: [] })
export * as RuntimeFlags from "./runtime-flags"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+1 -3
View File
@@ -16,7 +16,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/En
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(Effect.fn("Env.state")(() => Effect.succeed({ ...process.env })))
@@ -36,8 +36,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer
export const node = LayerNode.make({ service: Service, layer: layer, deps: [] })
export * as Env from "."
+1 -3
View File
@@ -11,7 +11,7 @@ import { Context, Effect, Layer } from "effect"
export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
@@ -66,8 +66,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2.node] })
export * as EventV2Bridge from "./event-v2-bridge"
+1 -7
View File
@@ -28,7 +28,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Fo
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -194,12 +194,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,
+1 -3
View File
@@ -100,7 +100,7 @@ const kind = (code: string): Kind => {
export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const appProcess = yield* AppProcess.Service
@@ -343,8 +343,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [AppProcess.node] })
export * as Git from "."
+1 -3
View File
@@ -56,7 +56,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -167,8 +167,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Config.node] })
export * as Image from "./image"
+5 -6
View File
@@ -1,8 +1,9 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error"
import { ChildProcess } from "effect/unstable/process"
@@ -82,7 +83,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/In
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProcess.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const http = yield* HttpClient.HttpClient
@@ -324,14 +325,12 @@ export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | AppProce
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FetchHttpClient.layer), Layer.provide(AppProcess.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
const { runPromise } = makeRuntime(Service, defaultLayer)
const { runPromise } = makeRuntime(Service, AppNodeBuilder.build(node))
export const latest = (...args: Parameters<Interface["latest"]>) => runPromise((s) => s.latest(...args))
export const method = () => runPromise((s) => s.method())
export const upgrade = (...args: Parameters<Interface["upgrade"]>) => runPromise((s) => s.upgrade(...args))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [httpClient, AppProcess.node] })
export * as Installation from "."
+1 -7
View File
@@ -135,7 +135,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const config = yield* Config.Service
@@ -496,12 +496,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
)
export * as Diagnostic from "./diagnostic"
export const node = LayerNode.make({
+1 -3
View File
@@ -56,7 +56,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Mc
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -158,8 +158,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EffectFlock.defaultLayer), Layer.provide(FSUtil.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [FSUtil.node, EffectFlock.node] })
export * as McpAuth from "./auth"
+2 -1
View File
@@ -72,7 +72,8 @@ export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: numbe
.filter((text) => text.trim())
.join("\n\n") || "MCP tool returned an error",
)
if (result.structuredContent === undefined || result.structuredContent === null) return result
if (result.content.length > 0 || result.structuredContent === undefined || result.structuredContent === null)
return result
return {
...result,
content: [{ type: "text" as const, text: JSON.stringify(result.structuredContent) }],
+1 -11
View File
@@ -194,7 +194,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/MC
export const use = serviceUse(Service)
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
@@ -1003,16 +1003,6 @@ export const layer = Layer.effect(
export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
// --- Per-service runtime ---
export const defaultLayer = layer.pipe(
Layer.provide(McpAuth.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,
+1 -3
View File
@@ -39,7 +39,7 @@ export function evaluate(permission: string, pattern: string, ...rulesets: Permi
export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@@ -213,8 +213,6 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<st
)
}
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] })
export * as Permission from "."
+1 -7
View File
@@ -120,7 +120,7 @@ async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks:
}
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@@ -305,12 +305,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const node = LayerNode.make({
service: Service,
layer: layer,
+2 -1
View File
@@ -14,6 +14,7 @@ import {
import path from "path"
import { fileURLToPath } from "url"
import { TuiConfig } from "@/config/tui"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { errorData, errorMessage } from "@opencode-ai/tui/util/error"
import { isRecord } from "@opencode-ai/tui/util/record"
import { resolveHostAttentionSoundPaths } from "@/config/tui-host-attention"
@@ -1082,7 +1083,7 @@ async function load(input: {
const flags = await Effect.runPromise(
Effect.gen(function* () {
return yield* RuntimeFlags.Service
}).pipe(Effect.provide(RuntimeFlags.defaultLayer)),
}).pipe(Effect.provide(AppNodeBuilder.build(RuntimeFlags.node))),
)
const pluginOrigins = config.plugin_origins ?? (await TuiConfig.pluginOrigins())
const records = Flag.OPENCODE_PURE ? [] : pluginOrigins
+3 -16
View File
@@ -1,4 +1,4 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
import { Plugin } from "../plugin"
import { Format } from "../format"
import { LSP } from "@/lsp/lsp"
@@ -14,7 +14,7 @@ import { Service } from "./bootstrap-service"
export { Service } from "./bootstrap-service"
export type { Interface } from "./bootstrap-service"
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
// Yield each bootstrap dep at layer init so `run` itself has R = never.
@@ -49,20 +49,7 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
Layer.provide([
Config.defaultLayer,
Format.defaultLayer,
LSP.defaultLayer,
Plugin.defaultLayer,
Project.defaultLayer,
ShareNext.defaultLayer,
Snapshot.defaultLayer,
Vcs.defaultLayer,
]),
)
export const node = LayerNode.make({
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node],
@@ -1,11 +0,0 @@
import { Effect, Layer } from "effect"
import { InstanceStore } from "./instance-store"
export const layer = Layer.unwrap(
Effect.promise(async () => {
const { InstanceBootstrap } = await import("./bootstrap")
return InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))
}),
)
export * as InstanceLayer from "./instance-layer"
@@ -1,4 +1,5 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode, Node } from "@opencode-ai/core/effect/app-node"
import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -8,7 +9,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { type InstanceContext } from "./instance-context"
import { InstanceBootstrap } from "./bootstrap-service"
import { InstanceBootstrap as InstanceBootstrapGraph } from "./bootstrap"
import * as Project from "./project"
export interface LoadInput {
@@ -34,7 +34,7 @@ interface Entry {
readonly deferred: Deferred.Deferred<InstanceContext>
}
export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const project = yield* Project.Service
@@ -202,12 +202,12 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Project.defaultLayer))
export const bootstrapNode = LayerNode.unbound(InstanceBootstrap.Service, Node.tags.values.global)
export const node = LayerNode.make({
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [Project.node, InstanceBootstrapGraph.node],
deps: [Project.node, bootstrapNode],
})
export * as InstanceStore from "./instance-store"
+1 -12
View File
@@ -103,7 +103,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
type GitResult = { code: number; text: string; stderr: string }
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -463,17 +463,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(ProjectV2.defaultLayer),
Layer.provide(ProjectDirectories.defaultLayer),
Layer.provide(AppProcess.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Database.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
)
export const use = serviceUse(Service)
export const node = LayerNode.make({
+1 -3
View File
@@ -295,7 +295,7 @@ interface State {
export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const git = yield* Git.Service
@@ -418,8 +418,6 @@ export const layer: Layer.Layer<Service, never, Git.Service | EventV2Bridge.Serv
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Git.defaultLayer), Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Git.node, EventV2Bridge.node] })
export * as Vcs from "./vcs"
+1 -5
View File
@@ -106,7 +106,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
@@ -224,10 +224,6 @@ export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> =
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
)
export const node = LayerNode.make({ service: Service, layer: layer, deps: [Auth.node, Plugin.node] })
export * as ProviderAuth from "./auth"
+1 -13
View File
@@ -1299,7 +1299,7 @@ function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enabl
.map((item) => item.id)
}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
@@ -1949,18 +1949,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(
Layer.provide(FSUtil.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.defaultLayer),
),
)
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
const smallModelFamilyPriority = ["gemini-flash", "gpt-nano", "claude-haiku"]
export function sort<T extends { id: string }>(models: T[]) {
+1 -3
View File
@@ -61,7 +61,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
export const layer = Layer.effect(
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
@@ -156,8 +156,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
export const node = LayerNode.make({ service: Service, layer: layer, deps: [EventV2Bridge.node] })
export * as Question from "."
@@ -22,6 +22,7 @@ import { McpAuth } from "@/mcp/auth"
import { Permission } from "@/permission"
import { Plugin } from "@/plugin"
import { PluginPtyEnvironment } from "@/plugin/pty-environment"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceStore } from "@/project/instance-store"
import { Project } from "@/project/project"
import { Vcs } from "@/project/vcs"
@@ -301,7 +302,7 @@ export function createRoutes(
),
Layer.provide(locationServiceMapLayer),
Layer.provide(LayerNode.compile(app)),
Layer.provide(LayerNode.compile(app, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]])),
)
}
@@ -1,4 +1,5 @@
import { Context, Effect, Layer, Option } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import * as Socket from "effect/unstable/socket/Socket"
export const SERVER_CLOSING_EVENT = () => new Socket.CloseEvent(1001, "server closing")
@@ -13,7 +14,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/HttpApiWebSocketTracker") {}
export const layer = Layer.sync(Service)(() => {
const layer = Layer.sync(Service)(() => {
const sockets = new Set<Close>()
let closing = false
return Service.of({
@@ -44,6 +45,8 @@ export const layer = Layer.sync(Service)(() => {
})
})
export const node = LayerNode.make({ service: Service, layer, deps: [] })
export const register = (close: Close) =>
Effect.gen(function* () {
const tracker = yield* Effect.serviceOption(Service)

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