mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 01:00:54 -04:00
Compare commits
18 Commits
midstream-errs
...
job-id
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b234b3321 | |||
| 7f47d258e9 | |||
| d311e2b822 | |||
| b8bd88901a | |||
| f1adabcddc | |||
| 24470e52a5 | |||
| 23bbc5cd14 | |||
| ebf6fc07a1 | |||
| 04513d9692 | |||
| 146720e197 | |||
| b1f8cc04af | |||
| 082fe93e16 | |||
| 709c195905 | |||
| 3355b78d91 | |||
| b84c63d034 | |||
| 057b5a9dee | |||
| 61aefc0759 | |||
| f929f8f100 |
@@ -322,6 +322,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@corvu/drawer": "catalog:",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"bin": {
|
||||
"lildax": "./bin/lildax.cjs",
|
||||
},
|
||||
@@ -144,7 +144,7 @@
|
||||
},
|
||||
"packages/codemode": {
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"acorn": "8.15.0",
|
||||
"effect": "catalog:",
|
||||
@@ -158,7 +158,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -221,7 +221,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.82",
|
||||
"@ai-sdk/openai": "3.0.48",
|
||||
@@ -243,7 +243,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"packages/console/support": {
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@opencode-ai/console-core": "workspace:*",
|
||||
@@ -287,7 +287,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@opencode-ai/core",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -321,7 +321,6 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
@@ -381,7 +380,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
"drizzle-orm": "catalog:",
|
||||
@@ -435,7 +434,7 @@
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
@@ -447,21 +446,9 @@
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/effect-sqlite-node": {
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"version": "1.18.13",
|
||||
"dependencies": {
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@hono/standard-validator": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
@@ -482,6 +469,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
@@ -493,7 +481,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -509,7 +497,7 @@
|
||||
},
|
||||
"packages/http-recorder": {
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "4.0.0-beta.83",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.83",
|
||||
@@ -540,7 +528,7 @@
|
||||
},
|
||||
"packages/llm": {
|
||||
"name": "@opencode-ai/llm",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@smithy/eventstream-codec": "4.2.14",
|
||||
@@ -559,7 +547,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -690,7 +678,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "3.0.8",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -766,7 +754,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"cross-spawn": "catalog:",
|
||||
},
|
||||
@@ -781,7 +769,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
@@ -796,7 +784,7 @@
|
||||
},
|
||||
"packages/session-ui": {
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
|
||||
@@ -836,7 +824,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -849,7 +837,7 @@
|
||||
},
|
||||
"packages/stats/app": {
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
@@ -883,7 +871,7 @@
|
||||
},
|
||||
"packages/stats/core": {
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-athena": "3.933.0",
|
||||
"@planetscale/database": "1.19.0",
|
||||
@@ -902,7 +890,7 @@
|
||||
},
|
||||
"packages/stats/server": {
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-firehose": "3.933.0",
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -944,7 +932,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
@@ -971,7 +959,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
@@ -1022,7 +1010,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -1078,6 +1066,7 @@
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
|
||||
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
@@ -1967,8 +1956,6 @@
|
||||
|
||||
"@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
|
||||
|
||||
"@opencode-ai/effect-sqlite-node": ["@opencode-ai/effect-sqlite-node@workspace:packages/effect-sqlite-node"],
|
||||
|
||||
"@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
|
||||
|
||||
"@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=",
|
||||
"aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=",
|
||||
"aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=",
|
||||
"x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY="
|
||||
"x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=",
|
||||
"aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=",
|
||||
"aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=",
|
||||
"x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU="
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -158,6 +158,7 @@
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch"
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -5,12 +5,15 @@ import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { same } from "@/utils/same"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
|
||||
import { File } from "@opencode-ai/session-ui/file"
|
||||
import { Markdown } from "@opencode-ai/session-ui/markdown"
|
||||
import { ScrollView } from "@opencode-ai/ui/scroll-view"
|
||||
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
@@ -220,6 +223,31 @@ export function SessionContextTab() {
|
||||
{ label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) },
|
||||
] satisfies { label: string; value: () => JSX.Element }[]
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let pending: { x: number; y: number } | undefined
|
||||
@@ -328,7 +356,18 @@ export function SessionContextTab() {
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div>
|
||||
<Button
|
||||
size="small"
|
||||
variant="ghost"
|
||||
class="gap-1.5 px-2 text-text-weak hover:text-text-base"
|
||||
onClick={exportSession}
|
||||
>
|
||||
<Icon name="download" size="small" />
|
||||
<span>{language.t("context.export.session")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion multiple>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
|
||||
@@ -95,6 +95,8 @@ export const dict = {
|
||||
"command.session.share.description": "Share this session and copy the URL to clipboard",
|
||||
"command.session.unshare": "Unshare session",
|
||||
"command.session.unshare.description": "Stop sharing this session",
|
||||
"command.session.export": "Export session",
|
||||
"command.session.export.description": "Export the full session transcript as JSON",
|
||||
|
||||
"palette.search.placeholder": "Search files, commands, and sessions",
|
||||
"palette.search.placeholder.home": "Search commands and sessions",
|
||||
@@ -489,6 +491,7 @@ export const dict = {
|
||||
|
||||
"context.systemPrompt.title": "System Prompt",
|
||||
"context.rawMessages.title": "Raw messages",
|
||||
"context.export.session": "Export session",
|
||||
|
||||
"context.stats.session": "Session",
|
||||
"context.stats.messages": "Messages",
|
||||
@@ -568,6 +571,11 @@ export const dict = {
|
||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||
|
||||
"toast.session.export.success.title": "Session exported",
|
||||
"toast.session.export.success.description": "Saved session to {{filename}}",
|
||||
"toast.session.export.failed.title": "Failed to export session",
|
||||
"toast.session.export.failed.description": "An error occurred while exporting the session",
|
||||
|
||||
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
|
||||
|
||||
@@ -802,6 +810,7 @@ export const dict = {
|
||||
"common.moreOptions": "More options",
|
||||
"common.learnMore": "Learn more",
|
||||
"common.rename": "Rename",
|
||||
"common.export": "Export",
|
||||
"common.reset": "Reset",
|
||||
"common.archive": "Archive",
|
||||
"common.delete": "Delete",
|
||||
|
||||
@@ -53,6 +53,7 @@ import type {
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
||||
import { normalize } from "@opencode-ai/session-ui/session-diff"
|
||||
@@ -806,6 +807,29 @@ export function MessageTimeline(props: {
|
||||
navigate(`/${params.dir}/session`)
|
||||
}
|
||||
|
||||
const exportSession = async (sessionID: string) => {
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const archiveSession = async (sessionID: string) => {
|
||||
const session = sync().session.get(sessionID)
|
||||
if (!session) return
|
||||
@@ -1564,6 +1588,9 @@ export function MessageTimeline(props: {
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Item onSelect={() => exportSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.export")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onSelect={() => void archiveSession(id)}>
|
||||
<DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
@@ -1635,6 +1662,9 @@ export function MessageTimeline(props: {
|
||||
{language.t("session.share.action.share")}...
|
||||
</MenuV2.Item>
|
||||
</Show>
|
||||
<MenuV2.Item onSelect={() => exportSession(id)}>
|
||||
{language.t("common.export")}...
|
||||
</MenuV2.Item>
|
||||
<MenuV2.Item onSelect={() => void archiveSession(id)}>
|
||||
{language.t("common.archive")}
|
||||
</MenuV2.Item>
|
||||
|
||||
@@ -12,10 +12,11 @@ import { useSettings } from "@/context/settings"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { showToast } from "@/utils/toast"
|
||||
import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export"
|
||||
import { findLast } from "@opencode-ai/core/util/array"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { Message, Part, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionOwnership } from "./session-ownership"
|
||||
import { useLocal } from "@/context/local"
|
||||
@@ -231,6 +232,31 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
)
|
||||
}
|
||||
|
||||
const exportSession = async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
try {
|
||||
const data = await fetchSessionExport({
|
||||
sessionID,
|
||||
client: sdk().client,
|
||||
})
|
||||
const filename = sessionExportFilename(data.info)
|
||||
downloadSessionExport(filename, data)
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: language.t("toast.session.export.success.title"),
|
||||
description: language.t("toast.session.export.success.description", { filename }),
|
||||
})
|
||||
} catch (err) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("toast.session.export.failed.title"),
|
||||
description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openFile = () => {
|
||||
void openDialog(
|
||||
() => import("@/components/dialog-select-file"),
|
||||
@@ -458,6 +484,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: fork,
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.export",
|
||||
title: language.t("command.session.export"),
|
||||
description: language.t("command.session.export.description"),
|
||||
slash: "export",
|
||||
disabled: !params.id,
|
||||
onSelect: exportSession,
|
||||
}),
|
||||
]
|
||||
|
||||
const fileCmds = () => {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fetchSessionExport, sessionExportFilename } from "./session-export"
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
describe("sessionExportFilename", () => {
|
||||
test("generates filename from title", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", title: "Clone PR in worktree from fork" })).toBe(
|
||||
"clone-pr-in-worktree-from-fork.json",
|
||||
)
|
||||
})
|
||||
|
||||
test("generates filename from slug when title missing", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123", slug: "my-session-slug" })).toBe("my-session-slug.json")
|
||||
})
|
||||
|
||||
test("falls back to id when title and slug are empty", () => {
|
||||
expect(sessionExportFilename({ id: "ses_123" })).toBe("ses_123.json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchSessionExport", () => {
|
||||
test("fetches full transcript from client", async () => {
|
||||
const session = { id: "ses_1", title: "Test Session" } as Session
|
||||
const msg = { id: "msg_1", role: "user" } as Message
|
||||
const part = { id: "prt_1", type: "text", text: "hello" } as Part
|
||||
const messages = [{ info: msg, parts: [part] }]
|
||||
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: session }),
|
||||
messages: async () => ({ data: messages }),
|
||||
},
|
||||
}
|
||||
|
||||
const result = await fetchSessionExport({
|
||||
sessionID: "ses_1",
|
||||
client,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
info: session,
|
||||
messages,
|
||||
})
|
||||
})
|
||||
|
||||
test("throws when session not found", async () => {
|
||||
const client = {
|
||||
session: {
|
||||
get: async () => ({ data: null }),
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
}
|
||||
|
||||
expect(
|
||||
fetchSessionExport({
|
||||
sessionID: "ses_missing",
|
||||
client,
|
||||
}),
|
||||
).rejects.toThrow("Session not found: ses_missing")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
// Matches the exact `{ info, messages: [{ info, parts }] }` structure produced by `opencode export` CLI
|
||||
export type SessionExportData = {
|
||||
info: Session
|
||||
messages: {
|
||||
info: Message
|
||||
parts: Part[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export type SessionExportClient = {
|
||||
session: {
|
||||
get: (input: { sessionID: string }) => Promise<{ data?: Session | null }>
|
||||
messages: (input: { sessionID: string }) => Promise<{ data?: SessionExportData["messages"] | null }>
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSessionExport(input: {
|
||||
sessionID: string
|
||||
client: SessionExportClient
|
||||
}): Promise<SessionExportData> {
|
||||
const [sessionRes, messagesRes] = await Promise.all([
|
||||
input.client.session.get({ sessionID: input.sessionID }),
|
||||
input.client.session.messages({ sessionID: input.sessionID }),
|
||||
])
|
||||
|
||||
if (!sessionRes?.data) {
|
||||
throw new Error(`Session not found: ${input.sessionID}`)
|
||||
}
|
||||
if (!messagesRes?.data) {
|
||||
throw new Error(`Failed to load messages for session: ${input.sessionID}`)
|
||||
}
|
||||
|
||||
return {
|
||||
info: sessionRes.data,
|
||||
messages: messagesRes.data,
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionExportFilename(session: { id: string; title?: string; slug?: string }) {
|
||||
const name = session.title || session.slug || session.id
|
||||
const clean = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/gi, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return `${clean || session.id}.json`
|
||||
}
|
||||
|
||||
export function downloadSessionExport(filename: string, data: unknown) {
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/cli",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/codemode",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"description": "Effect-native confined code execution over schema-described tools",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-support",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"name": "@opencode-ai/core",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -90,7 +90,6 @@
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@npmcli/config": "10.8.1",
|
||||
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
|
||||
"@opencode-ai/effect-sqlite-node": "workspace:*",
|
||||
"@opencode-ai/llm": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as BackgroundJob from "./background-job"
|
||||
|
||||
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
|
||||
import { Identifier } from "./id/id"
|
||||
import { JobID } from "@opencode-ai/schema/job-id"
|
||||
import { makeGlobalNode } from "./effect/app-node"
|
||||
|
||||
export type Status = "running" | "completed" | "error" | "cancelled"
|
||||
@@ -202,7 +202,7 @@ export const make = Effect.gen(function* () {
|
||||
const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) {
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const id = input.id ?? Identifier.ascending("job")
|
||||
const id = input.id ?? JobID.create()
|
||||
const started_at = yield* Clock.currentTimeMillis
|
||||
const done = yield* Deferred.make<Info>()
|
||||
const promoted = yield* Deferred.make<Info>()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/
|
||||
@@ -191,8 +191,6 @@ function sameBytes(left: Uint8Array, right: Uint8Array) {
|
||||
return left.every((byte, index) => byte === right[index])
|
||||
}
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] })
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,13 +31,6 @@ const DARWIN_LIBRARY = [
|
||||
const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes", "/.fseventsd"]
|
||||
const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"]
|
||||
|
||||
/** Directory basenames to skip when scanning the home directory. */
|
||||
export function names(): ReadonlySet<string> {
|
||||
if (process.platform === "darwin") return new Set(DARWIN_HOME)
|
||||
if (process.platform === "win32") return new Set(WIN32_HOME)
|
||||
return new Set()
|
||||
}
|
||||
|
||||
/** Absolute paths that should never be watched, stated, or scanned. */
|
||||
export function paths(): string[] {
|
||||
if (process.platform === "darwin")
|
||||
|
||||
@@ -234,6 +234,4 @@ export const fffLayer = Layer.effect(
|
||||
|
||||
const layer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)))
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { create as createIdentifier } from "@opencode-ai/schema/identifier"
|
||||
|
||||
const prefixes = {
|
||||
job: "job",
|
||||
event: "evt",
|
||||
session: "ses",
|
||||
message: "msg",
|
||||
permission: "per",
|
||||
question: "que",
|
||||
part: "prt",
|
||||
pty: "pty",
|
||||
tool: "tool",
|
||||
workspace: "wrk",
|
||||
} as const
|
||||
|
||||
export function ascending(prefix: keyof typeof prefixes, given?: string) {
|
||||
return generateID(prefix, "ascending", given)
|
||||
}
|
||||
|
||||
export function descending(prefix: keyof typeof prefixes, given?: string) {
|
||||
return generateID(prefix, "descending", given)
|
||||
}
|
||||
|
||||
function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
|
||||
if (!given) {
|
||||
return create(prefixes[prefix], direction)
|
||||
}
|
||||
|
||||
if (!given.startsWith(prefixes[prefix])) {
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
}
|
||||
return given
|
||||
}
|
||||
|
||||
export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
|
||||
return prefix + "_" + createIdentifier(direction === "descending", timestamp)
|
||||
}
|
||||
|
||||
/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
|
||||
export function timestamp(id: string): number {
|
||||
const prefix = id.split("_")[0]
|
||||
const hex = id.slice(prefix.length + 1, prefix.length + 13)
|
||||
const encoded = BigInt("0x" + hex)
|
||||
return Number(encoded / BigInt(0x1000))
|
||||
}
|
||||
|
||||
export * as Identifier from "./id"
|
||||
@@ -76,6 +76,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] })
|
||||
|
||||
@@ -153,8 +153,6 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer.pipe(Layer.orDie),
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
export * as LayerMapExample from "./layer-map.example"
|
||||
|
||||
import { Context, Effect, Layer, LayerMap } from "effect"
|
||||
import { Npm } from "../npm"
|
||||
|
||||
/**
|
||||
* Tutorial: split global services from context-specific services.
|
||||
*
|
||||
* Use this pattern when part of the app should be constructed once at the app edge,
|
||||
* while another part should be cached per request/project/workspace key.
|
||||
*
|
||||
* In this example:
|
||||
* - Npm.Service is the global service. It is not keyed by request context and should
|
||||
* be provided once by the application runtime.
|
||||
* - ConfigService is context-specific. It is built from a RequestContext key and is
|
||||
* cached by LayerMap for that key.
|
||||
* - ConfigServiceMap.layer owns the cache. Provide it once globally, then each
|
||||
* request can provide ConfigServiceMap.get(context) to select the right instance.
|
||||
*
|
||||
* Lifetime model:
|
||||
* - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service.
|
||||
* - ConfigServiceMap.get(context) has the request/context lifetime and provides
|
||||
* ConfigService for exactly that context key.
|
||||
* - The cached ConfigService entry stays alive while something is using it. Once idle,
|
||||
* it remains cached for idleTimeToLive, then its scope is finalized.
|
||||
* - invalidate(context) removes the cache entry for future lookups. Active users keep
|
||||
* running on the old instance; the next lookup can create a fresh instance.
|
||||
*
|
||||
* Key model:
|
||||
* - Keys can be strings, structs, classes, arrays, etc.
|
||||
* - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache
|
||||
* lookup, so mutating an object after it has been used as a key is a bug.
|
||||
*/
|
||||
|
||||
export type RequestContext = {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
}
|
||||
|
||||
export class RequestContextRef extends Context.Service<RequestContextRef, RequestContext>()(
|
||||
"@opencode/example/RequestContextRef",
|
||||
) {}
|
||||
|
||||
export interface ConfigServiceShape {
|
||||
readonly directory: string
|
||||
readonly workspace: string
|
||||
readonly nextUse: () => Effect.Effect<number>
|
||||
readonly which: Npm.Interface["which"]
|
||||
}
|
||||
|
||||
export class ConfigService extends Context.Service<ConfigService, ConfigServiceShape>()(
|
||||
"@opencode/example/ConfigService",
|
||||
) {}
|
||||
|
||||
const configServiceLayer = Layer.effect(
|
||||
ConfigService,
|
||||
Effect.gen(function* () {
|
||||
const context = yield* RequestContextRef
|
||||
const npm = yield* Npm.Service
|
||||
|
||||
let useCount = 0
|
||||
|
||||
return ConfigService.of({
|
||||
directory: context.directory,
|
||||
workspace: context.workspace,
|
||||
nextUse: () => Effect.succeed(++useCount),
|
||||
which: npm.which,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export class ConfigServiceMap extends LayerMap.Service<ConfigServiceMap>()("@opencode/example/ConfigServiceMap", {
|
||||
lookup: (context: RequestContext) =>
|
||||
configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))),
|
||||
idleTimeToLive: "5 minutes",
|
||||
}) {}
|
||||
|
||||
export const appLayer = ConfigServiceMap.layer
|
||||
|
||||
export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () {
|
||||
const config = yield* ConfigService
|
||||
|
||||
return {
|
||||
directory: config.directory,
|
||||
workspace: config.workspace,
|
||||
useCount: yield* config.nextUse(),
|
||||
}
|
||||
})
|
||||
|
||||
export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) {
|
||||
return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context)))
|
||||
})
|
||||
|
||||
export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context)
|
||||
@@ -278,7 +278,6 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer: layer,
|
||||
|
||||
@@ -313,6 +313,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Location.node, Config.node] })
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export * as PublicEventManifest from "./public-event-manifest"
|
||||
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||
|
||||
export const Definitions = EventManifest.ServerDefinitions
|
||||
export const Latest = Event.latest(Definitions)
|
||||
@@ -148,6 +148,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
|
||||
|
||||
@@ -64,6 +64,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Reference.node] })
|
||||
|
||||
@@ -31,14 +31,6 @@ export type EnsureInput = {
|
||||
readonly branch?: string
|
||||
}
|
||||
|
||||
export class InvalidRepositoryError extends Schema.TaggedErrorClass<InvalidRepositoryError>()(
|
||||
"RepositoryCacheInvalidRepositoryError",
|
||||
{
|
||||
repository: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchError>()(
|
||||
"RepositoryCacheInvalidBranchError",
|
||||
{
|
||||
@@ -86,7 +78,6 @@ export class CacheOperationError extends Schema.TaggedErrorClass<CacheOperationE
|
||||
) {}
|
||||
|
||||
export type Error =
|
||||
| InvalidRepositoryError
|
||||
| InvalidBranchError
|
||||
| CloneFailedError
|
||||
| FetchFailedError
|
||||
@@ -101,9 +92,8 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/RepositoryCache") {}
|
||||
|
||||
export function isError(error: unknown): error is Error {
|
||||
function isError(error: unknown): error is Error {
|
||||
return (
|
||||
error instanceof InvalidRepositoryError ||
|
||||
error instanceof InvalidBranchError ||
|
||||
error instanceof CloneFailedError ||
|
||||
error instanceof FetchFailedError ||
|
||||
@@ -114,14 +104,7 @@ export function isError(error: unknown): error is Error {
|
||||
)
|
||||
}
|
||||
|
||||
export const parseRemote = Effect.fn("RepositoryCache.parseRemote")(function* (repository: string) {
|
||||
return yield* Effect.try({
|
||||
try: () => Repository.parseRemote(repository),
|
||||
catch: (error) => new InvalidRepositoryError({ repository, message: errorMessage(error) }),
|
||||
})
|
||||
})
|
||||
|
||||
export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
|
||||
const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) {
|
||||
return yield* Effect.try({
|
||||
try: () => Repository.validateBranch(branch),
|
||||
catch: (error) => new InvalidBranchError({ branch, message: errorMessage(error) }),
|
||||
|
||||
@@ -46,14 +46,6 @@ export class InvalidBranchError extends Schema.TaggedErrorClass<InvalidBranchErr
|
||||
|
||||
export type Error = InvalidReferenceError | UnsupportedLocalRepositoryError | InvalidBranchError
|
||||
|
||||
export function isError(error: unknown): error is Error {
|
||||
return (
|
||||
error instanceof InvalidReferenceError ||
|
||||
error instanceof UnsupportedLocalRepositoryError ||
|
||||
error instanceof InvalidBranchError
|
||||
)
|
||||
}
|
||||
|
||||
export function parse(input: string): Reference | undefined {
|
||||
const cleaned = normalizeInput(input)
|
||||
if (!cleaned) return
|
||||
|
||||
@@ -71,6 +71,4 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [SkillV2.node] })
|
||||
|
||||
@@ -227,8 +227,6 @@ const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer.pipe(Layer.provideMerge(Config.locationLayer))
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
|
||||
@@ -2,12 +2,12 @@ export * as ToolOutputStore from "./tool-output-store"
|
||||
|
||||
import path from "path"
|
||||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { ascending } from "@opencode-ai/schema/identifier"
|
||||
import { Config } from "./config"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { Global } from "./global"
|
||||
import { makeGlobalNode, makeLocationNode } from "./effect/app-node"
|
||||
import { SessionSchema } from "./session/schema"
|
||||
import { Identifier } from "./util/identifier"
|
||||
import type { ToolOutput } from "@opencode-ai/llm"
|
||||
|
||||
export const MAX_LINES = 2_000
|
||||
@@ -127,7 +127,7 @@ const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const write = Effect.fn("ToolOutputStore.write")(function* (content: string) {
|
||||
const file = path.join(directory, `tool_${Identifier.ascending()}`)
|
||||
const file = path.join(directory, `tool_${ascending()}`)
|
||||
yield* fs.ensureDir(directory).pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause })))
|
||||
yield* fs
|
||||
.writeFileString(file, content, { flag: "wx" })
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * as Identifier from "@opencode-ai/schema/identifier"
|
||||
@@ -12,12 +12,6 @@ export function getDirectory(path: string | undefined) {
|
||||
return parts.slice(0, parts.length - 1).join("/") + "/"
|
||||
}
|
||||
|
||||
export function getFileExtension(path: string | undefined) {
|
||||
if (!path) return ""
|
||||
const parts = path.split(".")
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
|
||||
export function getFilenameTruncated(path: string | undefined, maxLength: number = 20) {
|
||||
const filename = getFilename(path)
|
||||
if (filename.length <= maxLength) return filename
|
||||
@@ -27,11 +21,3 @@ export function getFilenameTruncated(path: string | undefined, maxLength: number
|
||||
if (available <= 0) return filename.slice(0, maxLength - 1) + "…"
|
||||
return filename.slice(0, available) + "…" + ext
|
||||
}
|
||||
|
||||
export function truncateMiddle(text: string, maxLength: number = 20) {
|
||||
if (text.length <= maxLength) return text
|
||||
const available = maxLength - 1 // -1 for ellipsis
|
||||
const start = Math.ceil(available / 2)
|
||||
const end = Math.floor(available / 2)
|
||||
return text.slice(0, start) + "…" + text.slice(-end)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * as V2Schema from "./v2-schema"
|
||||
|
||||
export { DateTimeUtcFromMillis } from "@opencode-ai/schema/schema"
|
||||
@@ -4,7 +4,6 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Repository } from "@opencode-ai/core/repository"
|
||||
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
|
||||
@@ -105,9 +104,6 @@ describe("RepositoryCache", () => {
|
||||
withRemote((fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const cache = yield* RepositoryCache.Service
|
||||
const invalidRepository = yield* Effect.flip(RepositoryCache.parseRemote("not-a-repo"))
|
||||
expect(invalidRepository).toBeInstanceOf(RepositoryCache.InvalidRepositoryError)
|
||||
|
||||
const invalidBranch = yield* Effect.flip(cache.ensure({ reference: fixture.reference, branch: "../unsafe" }))
|
||||
expect(invalidBranch).toBeInstanceOf(RepositoryCache.InvalidBranchError)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { Pty } from "@opencode-ai/schema/pty"
|
||||
import { Reference } from "@opencode-ai/schema/reference"
|
||||
import { SessionTodo } from "@opencode-ai/schema/session-todo"
|
||||
import { Skill } from "@opencode-ai/schema/skill"
|
||||
import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema"
|
||||
import { AbsolutePath, optional, statics } from "@opencode-ai/schema/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
|
||||
@@ -51,7 +51,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
coreSessionTodo,
|
||||
corePrompt,
|
||||
coreSkill,
|
||||
coreV2Schema,
|
||||
coreSchema,
|
||||
coreWorkspace,
|
||||
] = await Promise.all([
|
||||
@@ -73,7 +72,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
import("@opencode-ai/core/session/todo"),
|
||||
import("@opencode-ai/core/session/prompt"),
|
||||
import("@opencode-ai/core/skill"),
|
||||
import("@opencode-ai/core/v2-schema"),
|
||||
import("@opencode-ai/core/schema"),
|
||||
import("@opencode-ai/core/workspace"),
|
||||
])
|
||||
@@ -173,7 +171,6 @@ test("Core reuses the canonical shared schemas", async () => {
|
||||
[coreSkill.EmbeddedSource, Skill.EmbeddedSource],
|
||||
[coreSkill.Source, Skill.Source],
|
||||
[coreSkill.Info, Skill.Info],
|
||||
[coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis],
|
||||
[coreSchema.optional, optional],
|
||||
[coreSchema.statics, statics],
|
||||
[coreWorkspace.ID, Workspace.ID],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://opencode.ai",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { $ } from "bun"
|
||||
import * as path from "node:path"
|
||||
|
||||
import { RUST_TARGET } from "./utils"
|
||||
|
||||
if (!RUST_TARGET) throw new Error("RUST_TARGET not defined")
|
||||
|
||||
const BUNDLE_DIR = "dist"
|
||||
const BUNDLES_OUT_DIR = path.join(process.cwd(), "dist/bundles")
|
||||
|
||||
await $`mkdir -p ${BUNDLES_OUT_DIR}`
|
||||
await $`cp -r ${BUNDLE_DIR}/* ${BUNDLES_OUT_DIR}`
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"name": "@opencode-ai/effect-sqlite-node",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"effect": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
export * as NodeSqliteClient from "./index"
|
||||
|
||||
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||
import { identity } from "effect/Function"
|
||||
import * as Context from "effect/Context"
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Fiber from "effect/Fiber"
|
||||
import * as Layer from "effect/Layer"
|
||||
import * as Scope from "effect/Scope"
|
||||
import * as Semaphore from "effect/Semaphore"
|
||||
import * as Stream from "effect/Stream"
|
||||
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
||||
import * as Client from "effect/unstable/sql/SqlClient"
|
||||
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
||||
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"
|
||||
import * as Statement from "effect/unstable/sql/Statement"
|
||||
|
||||
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
||||
|
||||
export const TypeId: TypeId = "~@opencode-ai/effect-sqlite-node/NodeSqliteClient"
|
||||
export type TypeId = "~@opencode-ai/effect-sqlite-node/NodeSqliteClient"
|
||||
|
||||
export interface SqliteClient extends Client.SqlClient {
|
||||
readonly [TypeId]: TypeId
|
||||
readonly config: SqliteClientConfig
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
readonly updateValues: never
|
||||
}
|
||||
|
||||
export const SqliteClient = Context.Service<SqliteClient>("@opencode-ai/effect-sqlite-node/NodeSqliteClient")
|
||||
|
||||
export interface SqliteClientConfig {
|
||||
readonly filename: string
|
||||
readonly readonly?: boolean | undefined
|
||||
readonly create?: boolean | undefined
|
||||
readonly readwrite?: boolean | undefined
|
||||
readonly disableWAL?: boolean | undefined
|
||||
readonly timeout?: number | undefined
|
||||
readonly allowExtension?: boolean | undefined
|
||||
readonly spanAttributes?: Record<string, unknown> | undefined
|
||||
readonly transformResultNames?: ((str: string) => string) | undefined
|
||||
readonly transformQueryNames?: ((str: string) => string) | undefined
|
||||
}
|
||||
|
||||
interface SqliteConnection extends Connection {
|
||||
readonly loadExtension: (path: string) => Effect.Effect<void, SqlError>
|
||||
}
|
||||
|
||||
export const make = (
|
||||
options: SqliteClientConfig,
|
||||
): Effect.Effect<SqliteClient, never, Scope.Scope | Reactivity.Reactivity> =>
|
||||
Effect.gen(function* () {
|
||||
const compiler = Statement.makeCompilerSqlite(options.transformQueryNames)
|
||||
const transformRows = options.transformResultNames
|
||||
? Statement.defaultTransforms(options.transformResultNames).array
|
||||
: undefined
|
||||
|
||||
const makeConnection = Effect.gen(function* () {
|
||||
const db = new DatabaseSync(options.filename, {
|
||||
readOnly: options.readonly,
|
||||
timeout: options.timeout,
|
||||
allowExtension: options.allowExtension,
|
||||
enableForeignKeyConstraints: true,
|
||||
open: true,
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => db.close()))
|
||||
|
||||
if (options.disableWAL !== true && options.readonly !== true) {
|
||||
db.exec("PRAGMA journal_mode = WAL;")
|
||||
}
|
||||
|
||||
const run = (sql: string, params: ReadonlyArray<unknown> = []) =>
|
||||
Effect.withFiber<Array<Record<string, unknown>>, SqlError>((fiber) => {
|
||||
const statement = db.prepare(sql)
|
||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
||||
try {
|
||||
return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array<Record<string, unknown>>)
|
||||
} catch (cause) {
|
||||
return Effect.fail(
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const runValues = (sql: string, params: ReadonlyArray<unknown> = []) =>
|
||||
Effect.withFiber<ReadonlyArray<ReadonlyArray<unknown>>, SqlError>((fiber) => {
|
||||
const statement = db.prepare(sql)
|
||||
statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers))
|
||||
statement.setReturnArrays(true)
|
||||
try {
|
||||
return Effect.succeed(
|
||||
statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray<ReadonlyArray<unknown>>,
|
||||
)
|
||||
} catch (cause) {
|
||||
return Effect.fail(
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return identity<SqliteConnection>({
|
||||
execute(sql, params, transformRows) {
|
||||
return transformRows ? Effect.map(run(sql, params), transformRows) : run(sql, params)
|
||||
},
|
||||
executeRaw(sql, params) {
|
||||
return run(sql, params)
|
||||
},
|
||||
executeValues(sql, params) {
|
||||
return runValues(sql, params)
|
||||
},
|
||||
executeUnprepared(sql, params, transformRows) {
|
||||
return this.execute(sql, params, transformRows)
|
||||
},
|
||||
executeStream() {
|
||||
return Stream.die("executeStream not implemented")
|
||||
},
|
||||
loadExtension: (path) =>
|
||||
Effect.try({
|
||||
try: () => db.loadExtension(path),
|
||||
catch: (cause) =>
|
||||
new SqlError({
|
||||
reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const semaphore = yield* Semaphore.make(1)
|
||||
const connection = yield* makeConnection
|
||||
const acquirer = semaphore.withPermits(1)(Effect.succeed(connection))
|
||||
const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
|
||||
const fiber = Fiber.getCurrent()!
|
||||
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
||||
return Effect.as(
|
||||
Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))),
|
||||
connection,
|
||||
)
|
||||
})
|
||||
|
||||
return Object.assign(
|
||||
(yield* Client.make({
|
||||
acquirer,
|
||||
compiler,
|
||||
transactionAcquirer,
|
||||
spanAttributes: [
|
||||
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
||||
[ATTR_DB_SYSTEM_NAME, "sqlite"],
|
||||
],
|
||||
transformRows,
|
||||
})) as SqliteClient,
|
||||
{
|
||||
[TypeId]: TypeId as TypeId,
|
||||
config: options,
|
||||
loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
export const layer = (config: SqliteClientConfig): Layer.Layer<SqliteClient | Client.SqlClient> =>
|
||||
Layer.effectContext(
|
||||
Effect.map(make(config), (client) =>
|
||||
Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)),
|
||||
),
|
||||
).pipe(Layer.provide(Reactivity.layer))
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
/* biome-ignore-all lint: auto-generated */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/bun/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@effect/language-service",
|
||||
"transform": "@effect/language-service/transform",
|
||||
"namespaceImportPackages": ["effect", "@effect/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "catalog:",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { descending } from "@opencode-ai/schema/identifier"
|
||||
import { SessionID } from "@opencode-ai/schema/session-id"
|
||||
import { Share } from "../../src/core/share"
|
||||
import { Storage } from "../../src/core/storage"
|
||||
import { Identifier } from "@opencode-ai/core/util/identifier"
|
||||
|
||||
describe.concurrent("core.share", () => {
|
||||
test("should create a share", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
expect(share.sessionID).toBe(sessionID)
|
||||
@@ -15,7 +16,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should remove a share as admin", async () => {
|
||||
const share = await Share.create({ sessionID: Identifier.descending() })
|
||||
const share = await Share.create({ sessionID: SessionID.create() })
|
||||
|
||||
await Share.removeAdmin({ id: share.id })
|
||||
|
||||
@@ -23,7 +24,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should sync data to a share", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data: Share.Data[] = [
|
||||
@@ -45,7 +46,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should sync multiple batches of data", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data1: Share.Data[] = [
|
||||
@@ -79,7 +80,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should retrieve synced data", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data: Share.Data[] = [
|
||||
@@ -108,7 +109,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should retrieve data from multiple syncs", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data1: Share.Data[] = [
|
||||
@@ -154,7 +155,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should return latest data when syncing duplicate parts", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data1: Share.Data[] = [
|
||||
@@ -192,7 +193,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should return empty array for share with no data", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const result = await Share.data(share.id)
|
||||
@@ -203,7 +204,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should migrate legacy event data into the snapshot", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
const data: Share.Data[] = [
|
||||
{
|
||||
@@ -213,7 +214,7 @@ describe.concurrent("core.share", () => {
|
||||
]
|
||||
|
||||
await Storage.remove(["share_snapshot", share.id])
|
||||
await Storage.write(["share_event", share.id, Identifier.descending()], data)
|
||||
await Storage.write(["share_event", share.id, descending()], data)
|
||||
|
||||
const result = await Share.data(share.id)
|
||||
const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
|
||||
@@ -225,7 +226,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should throw error for invalid secret", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data: Share.Data[] = [
|
||||
@@ -246,7 +247,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should throw error for non-existent share", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const data: Share.Data[] = [
|
||||
{
|
||||
type: "part",
|
||||
@@ -263,7 +264,7 @@ describe.concurrent("core.share", () => {
|
||||
})
|
||||
|
||||
test("should handle different data types", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const sessionID = SessionID.create()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
const data: Share.Data[] = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"name": "@opencode-ai/http-recorder",
|
||||
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,591 +0,0 @@
|
||||
# LLM Call Site Sketches
|
||||
|
||||
Scratchpad for examples first, abstractions second. Current direction: routes
|
||||
execute, provider facades organize configured route sets, and models carry route
|
||||
values directly.
|
||||
|
||||
## Conversation Summary
|
||||
|
||||
Kit and Aidan want provider-specific LLM behavior to move out of opencode's AI
|
||||
SDK transform path and into `packages/llm` where possible. The goal is not a big
|
||||
generic transform layer; the goal is small composable route definitions backed by
|
||||
recorded golden tests.
|
||||
|
||||
Things to keep testing against:
|
||||
|
||||
- Cache placement: `cache: "auto"`, manual cache breakpoints, provider cache usage.
|
||||
- Images: golden image tests for providers/protocols that claim image support.
|
||||
- Reasoning: canonical reasoning parts/events versus provider-native knobs.
|
||||
- Auth: bearer, custom headers, multiple credentials, query auth, SigV4, OAuth, no auth.
|
||||
- OpenAI-compatible providers: DeepSeek, Together, Groq, Alibaba/DashScope, custom routers.
|
||||
- Provider switching: stale signatures, encrypted reasoning, provider metadata, incompatible parts.
|
||||
- Error quality: typed errors instead of generic SDK/server failures.
|
||||
|
||||
## Final Guide: Routes Execute, Providers Organize
|
||||
|
||||
Do not introduce a first-class `Deployment` abstraction unless it gains real
|
||||
semantics. Provider facades are ergonomic configured route groups, not execution
|
||||
registries. The executable/composable thing is still a route. Do not make route
|
||||
construction publish to a global registry; models should carry their route value
|
||||
directly.
|
||||
|
||||
Keep durable identity separate from runtime capability:
|
||||
|
||||
- Durable identity is small serializable data like `{ providerID, modelID }` for
|
||||
config, sessions, logs, and catalogs.
|
||||
- Runtime capability is a `Model` with a route value, protocol, transport, auth,
|
||||
and defaults. It is allowed to contain functions and schemas.
|
||||
- If persisted identity needs to become executable, resolve it through an app
|
||||
boundary first. Do not make `LLMRequest` recover behavior from a global route
|
||||
side table.
|
||||
|
||||
Keep unconfigured behavior values as values, not factories. A transport like
|
||||
`HttpTransport.sseJson` should be a reusable immutable value. Use a function only
|
||||
when the caller supplies options or when construction needs fresh state.
|
||||
|
||||
Use constants to remove repetition before inventing abstractions. Provider ids
|
||||
are branded once per provider facade and reused across routes; a plain exported
|
||||
object is enough for the provider-facing API unless a helper earns its keep by
|
||||
removing repeated route projection.
|
||||
|
||||
Expose default configured provider instances, and put provider-specific setup on
|
||||
`.configure(...)`. Model selectors stay pure: `model(id)`, `responses(id)`,
|
||||
`chat(id)`, etc. Endpoint/auth/resource/api-version configuration happens before
|
||||
model selection, not as a second argument to model selection.
|
||||
|
||||
Use provider/product facades consistently:
|
||||
|
||||
- One coherent provider/product config surface gets one top-level facade.
|
||||
- APIs/model kinds that share that config are methods on the facade.
|
||||
- Different products with different required config get separate top-level
|
||||
facades, not a shared namespace with unrelated children.
|
||||
- Default facades are exposed only when concrete defaults or lazy env/credential
|
||||
defaults make the facade valid.
|
||||
|
||||
Examples:
|
||||
|
||||
```ts
|
||||
OpenAI.responses("gpt-4o")
|
||||
OpenAI.chat("gpt-4o")
|
||||
OpenAI.responsesWebSocket("gpt-4o")
|
||||
|
||||
Azure.configure({ resourceName, apiKey }).responses("my-deployment")
|
||||
AmazonBedrock.configure({ region, credentials }).model("anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
CloudflareAIGateway.configure({ accountId, gatewayId, gatewayApiKey, apiKey }).model("openai/gpt-4o")
|
||||
CloudflareWorkersAI.configure({ accountId, apiKey }).model("@cf/meta/llama-3.1-8b-instruct")
|
||||
|
||||
OpenAICompatible.configure({
|
||||
provider: "custom",
|
||||
baseURL: "https://custom.example/v1",
|
||||
auth: Auth.bearer(apiKey),
|
||||
}).model("custom-model")
|
||||
```
|
||||
|
||||
Standardize the provider facade contract before abstracting construction. A
|
||||
plain object is enough at first; add a helper only if repeated route projection
|
||||
starts hiding the real provider-specific config.
|
||||
|
||||
`Route.with(...)` patch semantics should be boring and explicit:
|
||||
|
||||
- Omitted fields inherit from the original route.
|
||||
- `endpoint` patches merge with the existing endpoint, so overriding `baseURL`
|
||||
keeps the existing `path`.
|
||||
- `endpoint.query` merges by default; later values win.
|
||||
- `auth` replaces.
|
||||
- `headers` merge by default; undefined values are omitted.
|
||||
- `id` is optional in patches. Route ids are diagnostic/provider API labels, not
|
||||
global runtime registry keys.
|
||||
|
||||
1. **Route**
|
||||
- route id
|
||||
- provider id
|
||||
- protocol
|
||||
- body schema
|
||||
- body builder
|
||||
- stream event schema
|
||||
- parser/state machine
|
||||
- transport
|
||||
- method / IO shape
|
||||
- framing
|
||||
- request preparation
|
||||
- constants when unconfigured; functions only when configured
|
||||
- endpoint
|
||||
- base URL
|
||||
- static path
|
||||
- body/model-derived path
|
||||
- query params
|
||||
- auth
|
||||
- bearer
|
||||
- custom header
|
||||
- multiple credentials
|
||||
- SigV4
|
||||
- none
|
||||
- defaults
|
||||
- headers
|
||||
- generation defaults
|
||||
- provider options
|
||||
- limits
|
||||
2. **Provider Facade**
|
||||
- default configured provider instance
|
||||
- provider-specific `.configure(...)`
|
||||
- plain object/function facade over one or more routes
|
||||
- top-level export only when it represents one coherent config surface
|
||||
- no passive `Provider.make(...)` wrapper unless it gains runtime behavior
|
||||
3. **Model Selector**
|
||||
- route/provider-owned selector
|
||||
- accepts model id only
|
||||
- returns executable models
|
||||
- does not accept endpoint/auth/deployment overrides
|
||||
4. **Model**
|
||||
- model id
|
||||
- route value
|
||||
- provider id
|
||||
- configured route value at selection time
|
||||
5. **LLM Request**
|
||||
- model
|
||||
- messages/tools
|
||||
- generation/cache/reasoning/response-format options
|
||||
- request-level HTTP overlays for per-request headers/query/body additions,
|
||||
not provider endpoint/auth reconfiguration
|
||||
6. **Compile**
|
||||
- read route from model
|
||||
- merge route defaults and request overrides
|
||||
- build final URL from route endpoint
|
||||
- apply auth from the configured route
|
||||
- build body with protocol
|
||||
- execute with transport and parse with protocol
|
||||
|
||||
## Provider Facade Shape
|
||||
|
||||
The provider abstraction is a facade over configured routes, not the runtime
|
||||
execution mechanism:
|
||||
|
||||
```ts
|
||||
type ProviderFacade<APIs, Config> = {
|
||||
readonly id: ProviderID
|
||||
readonly model: (id: string) => Model
|
||||
readonly configure: (input?: Config) => ProviderFacade<APIs, Config>
|
||||
} & APIs
|
||||
```
|
||||
|
||||
Manual construction is fine and should be the default until duplication earns a
|
||||
helper:
|
||||
|
||||
```ts
|
||||
export const OpenAI = {
|
||||
id: openAIProvider,
|
||||
model: openAIResponses.model,
|
||||
responses: openAIResponses.model,
|
||||
chat: openAIChat.model,
|
||||
configure: configureOpenAI,
|
||||
} satisfies ProviderFacade<
|
||||
{
|
||||
responses: (id: string) => Model
|
||||
chat: (id: string) => Model
|
||||
},
|
||||
OpenAIConfig
|
||||
>
|
||||
```
|
||||
|
||||
If several providers repeat the same projection from route values to model
|
||||
methods, the helper can stay deliberately tiny:
|
||||
|
||||
```ts
|
||||
const configureOpenAI = (input: OpenAIConfig = {}) =>
|
||||
Provider.define({
|
||||
id: openAIProvider,
|
||||
routes: {
|
||||
responses: openAIResponses.with(openAIConfig(input)),
|
||||
chat: openAIChat.with(openAIConfig(input)),
|
||||
},
|
||||
default: "responses",
|
||||
configure: configureOpenAI,
|
||||
})
|
||||
|
||||
export const OpenAI = configureOpenAI()
|
||||
```
|
||||
|
||||
`Provider.define(...)` would only project route methods and preserve types:
|
||||
|
||||
```ts
|
||||
OpenAI.model("gpt-4o")
|
||||
OpenAI.responses("gpt-4o")
|
||||
OpenAI.chat("gpt-4o")
|
||||
OpenAI.configure({ apiKey }).responses("gpt-4o")
|
||||
```
|
||||
|
||||
It must not register routes, select routes dynamically, or participate in
|
||||
execution. Execution still reads the route value carried by the model.
|
||||
|
||||
## Ideal Call Sites
|
||||
|
||||
Define concrete routes for a native provider, then project them through a
|
||||
provider facade:
|
||||
|
||||
```ts
|
||||
const openAIProvider = ProviderID.make("openai")
|
||||
|
||||
const openAIResponses = Route.make({
|
||||
id: "openai-responses",
|
||||
provider: openAIProvider,
|
||||
protocol: OpenAIResponses.protocol,
|
||||
transport: HttpTransport.sseJson,
|
||||
endpoint: {
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
path: "/responses",
|
||||
},
|
||||
auth: Auth.envBearer("OPENAI_API_KEY"),
|
||||
})
|
||||
|
||||
const openAIChat = Route.make({
|
||||
id: "openai-chat",
|
||||
provider: openAIProvider,
|
||||
protocol: OpenAIChat.protocol,
|
||||
transport: HttpTransport.sseJson,
|
||||
endpoint: {
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
path: "/chat/completions",
|
||||
},
|
||||
auth: Auth.envBearer("OPENAI_API_KEY"),
|
||||
})
|
||||
|
||||
const openAIResponsesWebSocket = openAIResponses.with({
|
||||
id: "openai-responses-websocket",
|
||||
transport: WebSocketTransport.json,
|
||||
})
|
||||
|
||||
const openAIConfig = (input: OpenAIConfig) => ({
|
||||
endpoint: input.endpoint,
|
||||
auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined),
|
||||
headers: {
|
||||
"OpenAI-Organization": input.organization,
|
||||
"OpenAI-Project": input.project,
|
||||
},
|
||||
})
|
||||
|
||||
const configureOpenAI = (input: OpenAIConfig = {}) => {
|
||||
const responses = openAIResponses.with(openAIConfig(input))
|
||||
const responsesWebSocket = openAIResponsesWebSocket.with(openAIConfig(input))
|
||||
const chat = openAIChat.with(openAIConfig(input))
|
||||
|
||||
return {
|
||||
id: openAIProvider,
|
||||
responses: responses.model,
|
||||
responsesWebSocket: responsesWebSocket.model,
|
||||
chat: chat.model,
|
||||
model: responses.model,
|
||||
configure: configureOpenAI,
|
||||
}
|
||||
}
|
||||
|
||||
export const OpenAI = configureOpenAI()
|
||||
```
|
||||
|
||||
Specialize it functionally for concrete providers:
|
||||
|
||||
```ts
|
||||
const deepSeekProvider = ProviderID.make("deepseek")
|
||||
|
||||
const deepseekChat = openAIChat.with({
|
||||
id: "deepseek-chat",
|
||||
provider: deepSeekProvider,
|
||||
endpoint: {
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
},
|
||||
auth: Auth.envBearer("DEEPSEEK_API_KEY"),
|
||||
})
|
||||
|
||||
const configureDeepSeek = (input: OpenAICompatibleConfig = {}) => {
|
||||
const route = deepseekChat.with({
|
||||
endpoint: input.endpoint,
|
||||
auth: input.auth ?? (input.apiKey ? Auth.bearer(input.apiKey) : undefined),
|
||||
})
|
||||
|
||||
return {
|
||||
id: deepSeekProvider,
|
||||
model: route.model,
|
||||
configure: configureDeepSeek,
|
||||
}
|
||||
}
|
||||
|
||||
export const DeepSeek = {
|
||||
id: deepSeekProvider,
|
||||
model: deepseekChat.model,
|
||||
configure: configureDeepSeek,
|
||||
}
|
||||
```
|
||||
|
||||
Provider-specific configuration happens before model selection:
|
||||
|
||||
```ts
|
||||
const deepseek = DeepSeek.configure({
|
||||
endpoint: {
|
||||
baseURL: "https://proxy.example.com/v1",
|
||||
},
|
||||
auth: Auth.bearer(apiKey),
|
||||
})
|
||||
|
||||
const model = deepseek.model("deepseek-chat")
|
||||
```
|
||||
|
||||
Final request call site stays boring:
|
||||
|
||||
```ts
|
||||
const response =
|
||||
yield *
|
||||
LLM.generate(
|
||||
LLM.request({
|
||||
model: DeepSeek.model("deepseek-chat"),
|
||||
prompt: "Hello.",
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
HTTP versus WebSocket is represented as named route selectors, not as model or
|
||||
request overrides. Same protocol, different transport, different route:
|
||||
|
||||
```ts
|
||||
OpenAI.responses("gpt-4o")
|
||||
OpenAI.responsesWebSocket("gpt-4o")
|
||||
```
|
||||
|
||||
The client should not require a different public layer just because a selected
|
||||
route uses WebSocket. Use one `LLMClient.layer` with HTTP and WebSocket runtime
|
||||
capabilities available; routes that do not need WebSocket simply never touch it.
|
||||
If a WebSocket route is selected in an environment without WebSocket support,
|
||||
fail with a typed transport configuration error.
|
||||
|
||||
Azure is a route specialization with auth/path/default changes plus input
|
||||
mapping. The public API configures the Azure resource once, then selects
|
||||
deployment ids with pure model selectors:
|
||||
|
||||
```ts
|
||||
const azureProvider = ProviderID.make("azure")
|
||||
|
||||
const azureResponses = openAIResponses.with({
|
||||
id: "azure-openai-responses",
|
||||
provider: azureProvider,
|
||||
auth: Auth.envHeader("api-key", "AZURE_OPENAI_API_KEY"),
|
||||
})
|
||||
|
||||
const configureAzure = (input: AzureConfig = {}) => {
|
||||
const route = azureResponses.with({
|
||||
endpoint: {
|
||||
baseURL:
|
||||
input.baseURL ??
|
||||
Endpoint.envBaseURL(
|
||||
"AZURE_RESOURCE_NAME",
|
||||
(resourceName) => `https://${resourceName}.openai.azure.com/openai/v1`,
|
||||
),
|
||||
query: { "api-version": input.apiVersion ?? "v1" },
|
||||
},
|
||||
auth: input.apiKey ? Auth.header("api-key", input.apiKey) : Auth.envHeader("api-key", "AZURE_OPENAI_API_KEY"),
|
||||
})
|
||||
|
||||
return {
|
||||
id: azureProvider,
|
||||
model: route.model,
|
||||
responses: route.model,
|
||||
configure: configureAzure,
|
||||
}
|
||||
}
|
||||
|
||||
export const Azure = configureAzure()
|
||||
|
||||
const azure = Azure.configure({
|
||||
resourceName: "my-resource",
|
||||
apiVersion: "v1",
|
||||
})
|
||||
|
||||
const model = azure.responses("my-deployment")
|
||||
```
|
||||
|
||||
Default provider facades are only valid when required configuration has a lazy
|
||||
default source. `Azure.responses("my-deployment")` can be valid if endpoint
|
||||
resolution reads `AZURE_RESOURCE_NAME` lazily and fails with a typed
|
||||
configuration error when missing. If a provider has no sensible lazy default,
|
||||
do not expose a default model selector; expose only a configured entrypoint.
|
||||
|
||||
Cloudflare AI Gateway and Workers AI are separate product facades because their
|
||||
configuration surfaces differ. Do not make a root `Cloudflare.configure(...)`
|
||||
pretend there is one coherent Cloudflare provider configuration:
|
||||
|
||||
```ts
|
||||
const cloudflareProvider = ProviderID.make("cloudflare-ai-gateway")
|
||||
|
||||
const cloudflareOpenAIChat = openAIChat.with({
|
||||
id: "cloudflare-ai-gateway-openai-chat",
|
||||
provider: cloudflareProvider,
|
||||
auth: Auth.bearerHeader("cf-aig-authorization").andThen(Auth.bearer()),
|
||||
})
|
||||
|
||||
const configureCloudflareAIGateway = (input: CloudflareAIGatewayConfig) => {
|
||||
const route = cloudflareOpenAIChat.with({
|
||||
endpoint: {
|
||||
baseURL: `https://gateway.ai.cloudflare.com/v1/${input.accountId}/${input.gatewayId}/openai`,
|
||||
},
|
||||
auth: Auth.bearerHeader("cf-aig-authorization", input.gatewayApiKey).andThen(Auth.bearer(input.apiKey)),
|
||||
})
|
||||
|
||||
return {
|
||||
id: cloudflareProvider,
|
||||
model: (modelID: string) => route.model({ id: modelID }),
|
||||
configure: configureCloudflareAIGateway,
|
||||
}
|
||||
}
|
||||
|
||||
export const CloudflareAIGateway = {
|
||||
id: cloudflareProvider,
|
||||
configure: configureCloudflareAIGateway,
|
||||
}
|
||||
|
||||
const gateway = CloudflareAIGateway.configure({
|
||||
accountId: "account",
|
||||
gatewayId: "gateway",
|
||||
gatewayApiKey,
|
||||
apiKey,
|
||||
})
|
||||
|
||||
const model = gateway.model("openai/gpt-4o")
|
||||
```
|
||||
|
||||
If a Cloudflare product gains a full lazy env default, it can expose a direct
|
||||
selector too. Until then, omitting `CloudflareAIGateway.model(...)` makes missing
|
||||
account/gateway configuration unrepresentable.
|
||||
|
||||
opencode's dynamic runtime should construct executable models at its app
|
||||
boundary instead of exposing a giant unstructured public model constructor or a
|
||||
generic dynamic resolver:
|
||||
|
||||
```ts
|
||||
const model =
|
||||
providerID === "azure"
|
||||
? Azure.configure(resolvedAzureConfig).responses(apiModelID)
|
||||
: endpoint.websocket
|
||||
? OpenAI.responsesWebSocket(apiModelID)
|
||||
: OpenAI.responses(apiModelID)
|
||||
```
|
||||
|
||||
That boundary can branch on durable config/catalog metadata and call typed
|
||||
provider APIs directly. Transport selection belongs there too: map metadata like
|
||||
`endpoint.websocket` to `OpenAI.responsesWebSocket(apiModelID)`; otherwise use
|
||||
the normal `OpenAI.responses(apiModelID)` route. The client runtime only executes
|
||||
the route carried by the model.
|
||||
|
||||
## Competitive Shape
|
||||
|
||||
This follows the strongest parts of adjacent libraries:
|
||||
|
||||
- AI SDK: configured provider instances expose provider-specific model methods.
|
||||
- Effect AI: executable models carry provider requirements and can be resolved by
|
||||
an app boundary.
|
||||
- LiteLLM/opencode config: dynamic `providerID/modelID` branching belongs at the
|
||||
app boundary, not in the typed public provider API or a global runtime
|
||||
resolver.
|
||||
- LangChain/LlamaIndex: constructor-style config plus model id is convenient,
|
||||
but we avoid making model selection also configure endpoint/auth.
|
||||
|
||||
The chosen split is:
|
||||
|
||||
```txt
|
||||
Route = execution mechanics
|
||||
Provider facade = configured route group
|
||||
Model = selected executable model carrying route value
|
||||
App boundary = explicit durable-config -> typed-provider call
|
||||
```
|
||||
|
||||
## What This Removes
|
||||
|
||||
- No `Provider.make(...)` as a core abstraction.
|
||||
- No `Provider.make(...)` wrapper just to bind an id to model functions. Use a
|
||||
branded provider id constant and a plain exported provider facade.
|
||||
- No `Deployment.define(...)` unless future examples force it.
|
||||
- No global route registry as the normal execution path.
|
||||
- No import side effects required before a model can execute.
|
||||
- No duplicate `provider.id` object when selected models already carry provider
|
||||
id.
|
||||
- No `model(id, overrides)` escape hatch. Model selection takes the model id;
|
||||
endpoint/auth/deployment customization happens by configuring the route first.
|
||||
- No transport override on model/request. HTTP SSE versus WebSocket is a named
|
||||
route selector such as `responses` versus `responsesWebSocket`.
|
||||
- No separate public `LLMClient.layerWithWebSocket`. The runtime should expose one
|
||||
client layer with the available transport capabilities.
|
||||
- No executable `ModelRef`. The executable handle is `Model`; durable model
|
||||
identity stays separate and cannot execute on its own.
|
||||
|
||||
## Implementation Todo
|
||||
|
||||
- [x] Replace the current executable `ModelRef` with `Model`.
|
||||
- [x] Change `Model.route` to carry a route value, not a `RouteID` string.
|
||||
- [ ] Keep a separate durable model identity type for persisted/session/catalog
|
||||
data, likely `{ providerID, modelID }`, and make it clear that it cannot
|
||||
execute without resolver context.
|
||||
- [x] Change route model selectors so `route.model(id)` returns an executable
|
||||
model with the route value attached, not a globally registered route id.
|
||||
- [x] Remove the standalone `Route.model(route, defaults, mapInput)` helper;
|
||||
configured route instances own model selection.
|
||||
- [x] Remove endpoint/auth escape hatches from route model selection; callers must
|
||||
configure endpoint/auth through `route.with(...)` or provider facades before
|
||||
calling `.model(...)`.
|
||||
- [x] Remove request-shaping defaults from `Model`; selected models now carry only
|
||||
id, provider, and configured route while defaults live on routes or requests.
|
||||
- [x] Rework `LLMClient.prepare` / `stream` / `generate` to read
|
||||
`request.model.route` directly instead of calling `registeredRoute(...)`.
|
||||
- [x] Remove `Route.make(...)` global registration from the normal execution
|
||||
path; keep route ids only as diagnostics/provider API labels.
|
||||
- [x] Model endpoint as `{ baseURL, path, query }` on routes, then remove the
|
||||
current split where host/query live on the model and path lives in route
|
||||
transport setup.
|
||||
- [x] Define `Route.with(...)` with explicit patch semantics for endpoint merge,
|
||||
query merge, header merge, auth replacement, and optional diagnostic id.
|
||||
- [x] Make unconfigured transports reusable constants such as
|
||||
`HttpTransport.sseJson`; keep transport functions only for configured/fresh
|
||||
state construction.
|
||||
- [x] Collapse the public WebSocket runtime split so one `LLMClient.layer`
|
||||
exposes available transport capabilities and selected routes fail with typed
|
||||
transport config errors when a required capability is missing.
|
||||
- [x] Convert OpenAI provider APIs to provider-facade shape:
|
||||
`OpenAI.configure(config).responses(id)`, `.chat(id)`, and
|
||||
`.responsesWebSocket(id)`.
|
||||
- [x] Convert Azure to a configured facade where resource/base URL/api version
|
||||
setup happens before selecting deployment ids.
|
||||
- [x] Split Cloudflare products into separate facades such as
|
||||
`CloudflareAIGateway` and `CloudflareWorkersAI`; do not expose a shared root
|
||||
config surface unless one product actually exists.
|
||||
- [x] Migrate remaining built-in provider facades one at a time so configuration
|
||||
happens before model selection and selectors accept only ids:
|
||||
xAI, GitHub Copilot, OpenRouter, OpenAI-compatible families, Anthropic,
|
||||
Google/Gemini, and Amazon Bedrock now use configured facades such as
|
||||
`Provider.configure(options).model(id)` with named selectors where needed.
|
||||
- [ ] Decide whether a tiny `Provider.define(...)` helper is warranted after two
|
||||
or three provider conversions; start with plain objects if duplication is not
|
||||
yet painful.
|
||||
- [x] Update `packages/opencode/src/session/llm/native-request.ts` to construct
|
||||
executable models at the session boundary with explicit provider facade
|
||||
calls, mapping catalog metadata such as `endpoint.websocket` to the correct
|
||||
named route selector.
|
||||
- [ ] Update tests so direct route/provider tests assert route values are carried
|
||||
by executable models, and opencode/native tests assert boundary-based route
|
||||
selection.
|
||||
- [ ] Remove compatibility exports or stale docs only after internal call sites
|
||||
are migrated; do not keep duplicate constructor paths without an external
|
||||
compatibility need.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Default facades with required setup: should providers like Azure and Bedrock
|
||||
expose default model selectors only when all required setup has lazy env or
|
||||
credential-chain defaults? If not, omit the default selector so missing config
|
||||
is impossible at the type/API level.
|
||||
- Lazy endpoint/auth values: should `Endpoint.envBaseURL(...)` and env-backed
|
||||
auth produce typed configuration/authentication errors at compile/prepare time
|
||||
or only when executing the transport?
|
||||
- `Route.with(...)` clearing semantics: endpoint/query/header patches merge by
|
||||
default, but what is the explicit way to remove an inherited value?
|
||||
- Provider facade helper: keep plain objects until duplication hurts, or add a
|
||||
tiny `Provider.define(...)` immediately to enforce shape and method projection?
|
||||
- Auth shape: should auth stay as today's composable `Auth`, or split into an
|
||||
auth placement/strategy and credential sources?
|
||||
- Naming: is `baseURL` still the right endpoint field name, or should it be
|
||||
`origin` / `urlPrefix` to clarify that route `path` is appended?
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"name": "@opencode-ai/llm",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -21,6 +21,7 @@ await Bun.build({
|
||||
external: ["jsonc-parser", "@lydell/node-pty"],
|
||||
define: {
|
||||
OPENCODE_MODELS_DEV: generated.modelsData,
|
||||
OPENCODE_VERSION: `'${Script.version}'`,
|
||||
OPENCODE_CHANNEL: `'${Script.channel}'`,
|
||||
},
|
||||
files: {
|
||||
|
||||
@@ -9,7 +9,7 @@ let embeddedUIPromise: Promise<Record<string, string> | null> | undefined
|
||||
export const UI_UPSTREAM = new URL("https://app.opencode.ai")
|
||||
|
||||
export const csp = (hash = "") =>
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:`
|
||||
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' data:; media-src 'self' data:; connect-src * data: blob:`
|
||||
export const DEFAULT_CSP = csp()
|
||||
|
||||
export function themePreloadHash(body: string) {
|
||||
|
||||
@@ -28,6 +28,15 @@ export const RETRY_BACKOFF_FACTOR = 2
|
||||
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
|
||||
export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
|
||||
|
||||
const RETRYABLE_MESSAGE_PATTERNS = [
|
||||
/429|500|502|503|504|524/i,
|
||||
/rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i,
|
||||
/overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i,
|
||||
/terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i,
|
||||
/^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i,
|
||||
/try your request again|retry your request|resource exhausted|resource_exhausted/i,
|
||||
]
|
||||
|
||||
function cap(ms: number) {
|
||||
return Math.min(ms, RETRY_MAX_DELAY)
|
||||
}
|
||||
@@ -72,7 +81,13 @@ export function retryable(error: Err, provider: string) {
|
||||
const status = error.data.statusCode
|
||||
// 5xx errors are transient server failures and should always be retried,
|
||||
// even when the provider SDK doesn't explicitly mark them as retryable.
|
||||
if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
|
||||
if (
|
||||
!error.data.isRetryable &&
|
||||
!(status !== undefined && status >= 500) &&
|
||||
!matchesRetryableMessage(error.data.message) &&
|
||||
!matchesRetryableMessage(error.data.responseBody)
|
||||
)
|
||||
return undefined
|
||||
if (error.data.responseBody?.includes("FreeUsageLimitError")) {
|
||||
return {
|
||||
message: GO_UPSELL_MESSAGE,
|
||||
@@ -122,35 +137,19 @@ export function retryable(error: Err, provider: string) {
|
||||
return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message }
|
||||
}
|
||||
|
||||
// Check for rate limit patterns in plain text error messages
|
||||
const msg = isRecord(error.data) ? error.data.message : undefined
|
||||
if (typeof msg === "string") {
|
||||
const lower = msg.toLowerCase()
|
||||
if (
|
||||
lower.includes("rate increased too quickly") ||
|
||||
lower.includes("rate limit") ||
|
||||
lower.includes("too many requests")
|
||||
) {
|
||||
return { message: msg }
|
||||
}
|
||||
}
|
||||
|
||||
const json = parseJSON(msg)
|
||||
if (!json || typeof json !== "object") return undefined
|
||||
const code = typeof json.code === "string" ? json.code : ""
|
||||
|
||||
if (json.type === "error" && json.error?.type === "too_many_requests") {
|
||||
return { message: "Too Many Requests" }
|
||||
}
|
||||
if (code.includes("exhausted") || code.includes("unavailable")) {
|
||||
return { message: "Provider is overloaded" }
|
||||
}
|
||||
if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
|
||||
return { message: "Rate Limited" }
|
||||
}
|
||||
const message = isRecord(error.data) ? error.data.message : undefined
|
||||
if (typeof message !== "string") return undefined
|
||||
const lower = message.toLowerCase()
|
||||
if (lower.includes("too_many_requests")) return { message: "Too Many Requests" }
|
||||
if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" }
|
||||
if (matchesRetryableMessage(message)) return { message }
|
||||
return undefined
|
||||
}
|
||||
|
||||
function matchesRetryableMessage(value: unknown) {
|
||||
return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value))
|
||||
}
|
||||
|
||||
function str(value: unknown) {
|
||||
if (value === undefined || value === null) return ""
|
||||
return String(value)
|
||||
|
||||
@@ -326,7 +326,7 @@ describe("HttpApi UI fallback", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("allows embedded UI terminal wasm and theme preload CSP", () =>
|
||||
it.live("allows embedded UI terminal wasm, blob attachments, and theme preload CSP", () =>
|
||||
Effect.gen(function* () {
|
||||
const script = 'document.documentElement.dataset.theme = "dark"'
|
||||
|
||||
@@ -351,7 +351,8 @@ describe("HttpApi UI fallback", () => {
|
||||
const csp = response.headers.get("content-security-policy") ?? ""
|
||||
expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'")
|
||||
expect(csp).toContain(`'sha256-${createHash("sha256").update(script).digest("base64")}'`)
|
||||
expect(csp).toContain("connect-src * data:")
|
||||
expect(csp).toContain("img-src 'self' data: https: blob:")
|
||||
expect(csp).toContain("connect-src * data: blob:")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -604,6 +604,53 @@ it.live("session.processor effect tests retry recognized structured json errors"
|
||||
),
|
||||
)
|
||||
|
||||
it.live("session.processor effect tests retry OpenAI-compatible midstream server errors", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
Effect.gen(function* () {
|
||||
const { processors, session, provider } = yield* boot()
|
||||
|
||||
yield* llm.push(raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }))
|
||||
yield* llm.text("after")
|
||||
|
||||
const chat = yield* session.create({})
|
||||
const parent = yield* user(chat.id, "retry midstream server error")
|
||||
const msg = yield* assistant(chat.id, parent.id, path.resolve(dir))
|
||||
const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
|
||||
const handle = yield* processors.create({
|
||||
assistantMessage: msg,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
})
|
||||
|
||||
const value = yield* handle.process({
|
||||
user: {
|
||||
id: parent.id,
|
||||
sessionID: chat.id,
|
||||
role: "user",
|
||||
time: parent.time,
|
||||
agent: parent.agent,
|
||||
model: { providerID: ref.providerID, modelID: ref.modelID },
|
||||
} satisfies SessionV1.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
system: [],
|
||||
messages: [{ role: "user", content: "retry midstream server error" }],
|
||||
tools: {},
|
||||
})
|
||||
|
||||
const parts = yield* MessageV2.parts(msg.id)
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
|
||||
expect(handle.message.error).toBeUndefined()
|
||||
}),
|
||||
{ config: (url) => providerCfg(url) },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("session.processor effect tests publish retry status updates", () =>
|
||||
provideTmpdirServer(
|
||||
({ dir, llm }) =>
|
||||
|
||||
@@ -118,16 +118,21 @@ describe("session.retry.delay", () => {
|
||||
})
|
||||
|
||||
describe("session.retry.retryable", () => {
|
||||
test("maps too_many_requests json messages", () => {
|
||||
test("retries serialized too_many_requests messages", () => {
|
||||
const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Too Many Requests" })
|
||||
})
|
||||
|
||||
test("maps overloaded provider codes", () => {
|
||||
test("retries serialized overloaded provider codes", () => {
|
||||
const error = wrap(JSON.stringify({ code: "resource_exhausted" }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" })
|
||||
})
|
||||
|
||||
test("retries serialized rate_limit messages", () => {
|
||||
const message = JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } })
|
||||
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
|
||||
})
|
||||
|
||||
test("does not retry unknown json messages", () => {
|
||||
const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } }))
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined()
|
||||
@@ -163,6 +168,45 @@ describe("session.retry.retryable", () => {
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg })
|
||||
})
|
||||
|
||||
test.each([
|
||||
"Internal server error",
|
||||
"internal error",
|
||||
"server-error",
|
||||
"Provider returned error",
|
||||
"provider-returned-error",
|
||||
"terminated",
|
||||
"fetch failed",
|
||||
"connection refused",
|
||||
"connect ECONNREFUSED",
|
||||
"request ETIMEDOUT",
|
||||
"failed to fetch",
|
||||
"EAI_AGAIN",
|
||||
"response timed out",
|
||||
"Please retry your request",
|
||||
"try your request again",
|
||||
"upstream returned status 524",
|
||||
])("retries matching API error text: %s", (message) => {
|
||||
expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message })
|
||||
})
|
||||
|
||||
test("retries hyphenated service-unavailable errors", () => {
|
||||
expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({
|
||||
message: "Provider is overloaded",
|
||||
})
|
||||
})
|
||||
|
||||
test("matches retryable API response bodies", () => {
|
||||
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
|
||||
new SessionV1.APIError({
|
||||
message: "Request failed",
|
||||
isRetryable: false,
|
||||
statusCode: 400,
|
||||
responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }),
|
||||
}).toObject(),
|
||||
)
|
||||
expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" })
|
||||
})
|
||||
|
||||
test("retries transport timeout errors", () => {
|
||||
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
|
||||
expect(SessionV1.APIError.isInstance(request)).toBe(true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
import { mkdir, rm } from "node:fs/promises"
|
||||
|
||||
export const FolderWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
|
||||
experimental_workspace.register("folder", {
|
||||
name: "Folder",
|
||||
description: "Create a blank folder",
|
||||
configure(config) {
|
||||
const rand = "" + Math.random()
|
||||
|
||||
return {
|
||||
...config,
|
||||
directory: `/tmp/folder/folder-${rand}`,
|
||||
}
|
||||
},
|
||||
async create(config) {
|
||||
if (!config.directory) return
|
||||
await mkdir(config.directory, { recursive: true })
|
||||
},
|
||||
async remove(config) {
|
||||
await rm(config.directory!, { recursive: true, force: true })
|
||||
},
|
||||
target(config) {
|
||||
return {
|
||||
type: "local",
|
||||
directory: config.directory!,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export default FolderWorkspacePlugin
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Plugin } from "./index.js"
|
||||
import { tool } from "./tool.js"
|
||||
|
||||
export const ExamplePlugin: Plugin = async (_ctx) => {
|
||||
return {
|
||||
tool: {
|
||||
mytool: tool({
|
||||
description: "This is a custom tool",
|
||||
args: {
|
||||
foo: tool.schema.string().describe("foo"),
|
||||
},
|
||||
async execute(args) {
|
||||
return `Hello ${args.foo}!`
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Event as SDKEvent } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Stream } from "effect"
|
||||
|
||||
export type EventMap = {
|
||||
[Item in SDKEvent as Item["type"]]: Item
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
subscribe<Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]>
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { FileSystemEntry } from "@opencode-ai/sdk/v2/types"
|
||||
import type { Effect } from "effect"
|
||||
|
||||
export interface FileSystem {
|
||||
read(input: { readonly path: string }): Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }>
|
||||
list(input?: { readonly path?: string }): Effect.Effect<FileSystemEntry[]>
|
||||
find(input: {
|
||||
readonly query: string
|
||||
readonly type?: "file" | "directory"
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<FileSystemEntry[]>
|
||||
glob(input: {
|
||||
readonly pattern: string
|
||||
readonly path?: string
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<readonly FileSystemEntry[]>
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface Location {
|
||||
readonly directory: string
|
||||
readonly project: {
|
||||
readonly directory: string
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { Effect } from "effect"
|
||||
|
||||
export interface Npm {
|
||||
add(pkg: string): Effect.Effect<
|
||||
{
|
||||
readonly directory: string
|
||||
readonly entrypoint?: string
|
||||
},
|
||||
unknown
|
||||
>
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export interface Path {
|
||||
readonly home: string
|
||||
readonly data: string
|
||||
readonly cache: string
|
||||
readonly config: string
|
||||
readonly state: string
|
||||
readonly temp: string
|
||||
}
|
||||
@@ -95,7 +95,7 @@ const SessionsQueryCursor = SessionsCursor.annotate({
|
||||
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
|
||||
})
|
||||
|
||||
export const SessionsQuery = Schema.Struct({
|
||||
const SessionsQuery = Schema.Struct({
|
||||
...SessionsQueryFields,
|
||||
directory: AbsolutePath.pipe(Schema.optional),
|
||||
project: Project.ID.pipe(Schema.optional),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Schema } from "effect"
|
||||
import { ascending } from "./identifier"
|
||||
import { statics } from "./schema"
|
||||
|
||||
export const JobID = Schema.String.check(Schema.isStartsWith("job_")).pipe(
|
||||
Schema.brand("JobID"),
|
||||
statics((schema) => ({ create: () => schema.make("job_" + ascending()) })),
|
||||
)
|
||||
export type JobID = typeof JobID.Type
|
||||
@@ -2,12 +2,12 @@ import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { Agent } from "../src/agent"
|
||||
import { FileSystem } from "../src/filesystem"
|
||||
import { JobID } from "../src/job-id"
|
||||
import { Model } from "../src/model"
|
||||
import { Project } from "../src/project"
|
||||
import { Pty } from "../src/pty"
|
||||
import { Question } from "../src/question"
|
||||
import { Session } from "../src/session"
|
||||
import { SessionEvent } from "../src/session-event"
|
||||
import { SessionTodo } from "../src/session-todo"
|
||||
import { optional } from "../src/schema"
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("contract hygiene", () => {
|
||||
})
|
||||
|
||||
test("current ID constructors expose create", () => {
|
||||
expect(JobID.create()).toStartWith("job_")
|
||||
expect(Question.ID.create()).toStartWith("que_")
|
||||
expect(Pty.ID.create()).toStartWith("pty_")
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/server",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Effect } from "effect"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError } from "@opencode-ai/protocol/errors"
|
||||
import { SchemaErrorMiddleware } from "@opencode-ai/protocol/middleware/schema-error"
|
||||
export { SchemaErrorMiddleware } from "@opencode-ai/protocol/middleware/schema-error"
|
||||
|
||||
const REASON_LIMIT = 1024
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { SessionExecution } from "@opencode-ai/core/session/execution"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Layer, Option } from "effect"
|
||||
import { Api } from "./api"
|
||||
@@ -61,8 +60,3 @@ function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config
|
||||
Layer.provide(serviceLayer),
|
||||
)
|
||||
}
|
||||
|
||||
export const routes = createRoutes()
|
||||
|
||||
export const webHandler = () =>
|
||||
HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)), { disableLogger: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/session-ui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-app",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// @refresh reload
|
||||
import type { Asset, PageEvent } from "@solidjs/start"
|
||||
import { createHandler, StartServer } from "@solidjs/start/server"
|
||||
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { dir, localeFromRequest, tag } from "./lib/language"
|
||||
import statsStylesheetUrl from "./routes/index.css?url"
|
||||
|
||||
const statsThemePreloadScript = `;(function () {
|
||||
var preference = "system"
|
||||
@@ -18,8 +21,12 @@ export default createHandler(
|
||||
() => (
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => {
|
||||
const event = getRequestEvent()
|
||||
const event = getRequestEvent() as PageEvent | undefined
|
||||
const locale = event ? localeFromRequest(event.request) : "en"
|
||||
const stylesheet = (event?.assets as Asset[] | undefined)?.find(
|
||||
(asset): asset is Extract<Asset, { tag: "link" }> => asset.tag === "link" && asset.attrs.rel === "stylesheet",
|
||||
)
|
||||
const stylesheetHref = import.meta.env.DEV ? statsStylesheetUrl : stylesheet?.attrs.href
|
||||
|
||||
return (
|
||||
<html lang={tag(locale)} dir={dir(locale)} data-locale={locale}>
|
||||
@@ -27,6 +34,8 @@ export default createHandler(
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script id="stats-theme-preload-script">{statsThemePreloadScript}</script>
|
||||
<link rel="preload" href={ibmPlexMonoMediumLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
{stylesheetHref ? <link rel="stylesheet" href={stylesheetHref} /> : null}
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { scaleSqrt } from "d3-scale"
|
||||
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
|
||||
import {
|
||||
getStatsModelData,
|
||||
type CountryEntry,
|
||||
type ModelPeerEntry,
|
||||
type ModelUsagePoint,
|
||||
type StatsModelData,
|
||||
type UsageRange,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
import { LocaleLinks } from "../../component/locale-links"
|
||||
import { useI18n } from "../../context/i18n"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { localizedUrl } from "../../lib/language"
|
||||
import {
|
||||
findModelCatalogEntry,
|
||||
formatCatalogLabName,
|
||||
getModelCatalog,
|
||||
type ModelCatalog,
|
||||
type ModelCatalogEntry,
|
||||
} from "../model-catalog"
|
||||
import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog"
|
||||
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map"
|
||||
import { SectionHeading } from "../section-heading"
|
||||
import { runStatsEffect } from "../../stats-runtime"
|
||||
import { setStatsPageCacheHeaders } from "../stats-cache"
|
||||
@@ -51,45 +40,41 @@ import {
|
||||
} from "../stats-shell"
|
||||
|
||||
const statsUnfurlPath = "banner.png"
|
||||
const geoMapWidth = 960
|
||||
const geoMapHeight = 430
|
||||
const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const
|
||||
|
||||
type IsoCountryCode = readonly [string, string, string]
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
type ModelCatalogOption = Pick<ModelCatalogEntry, "id" | "lab" | "slug" | "name">
|
||||
type ModelPageCatalog = {
|
||||
entry: ModelCatalogEntry | null
|
||||
labs: { id: string; name: string }[]
|
||||
labModels: ModelCatalogOption[]
|
||||
}
|
||||
type StatsModelPageData = Omit<StatsModelData, "country"> & { country: CountryEntry[] }
|
||||
type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null }
|
||||
|
||||
const countryNumericIds = new Map(
|
||||
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
|
||||
)
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
marker: geoCountryMarker(country),
|
||||
}))
|
||||
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
const getModelData = query(async (lab: string, model: string) => {
|
||||
const getModelPageData = query(async (labParam: string, modelParam: string) => {
|
||||
"use server"
|
||||
return runStatsEffect(getStatsModelData(model, lab))
|
||||
}, "getStatsModelData")
|
||||
const catalog = await loadModelCatalog()
|
||||
const entry = findModelCatalogEntry(catalog, modelParam, labParam) ?? null
|
||||
const lab = entry?.lab ?? labParam
|
||||
const model = entry?.slug ?? modelParam
|
||||
const stats = lab && model ? await runStatsEffect(getStatsModelData(model, lab)) : null
|
||||
return {
|
||||
catalog: {
|
||||
entry,
|
||||
labs: catalog.labs.map((item) => ({ id: item.id, name: item.name })),
|
||||
labModels:
|
||||
catalog.labs
|
||||
.find((item) => item.id === (entry?.lab ?? providerSlug(labParam)))
|
||||
?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [],
|
||||
},
|
||||
stats: stats ? { ...stats, country: stats.country["2M"] } : null,
|
||||
} satisfies ModelPageData
|
||||
}, "getStatsModelPageData")
|
||||
|
||||
export default function StatsModel() {
|
||||
const i18n = useI18n()
|
||||
@@ -99,18 +84,9 @@ export default function StatsModel() {
|
||||
const params = useParams()
|
||||
const labParam = createMemo(() => params.lab ?? "")
|
||||
const modelParam = createMemo(() => params.model ?? "")
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const catalogEntry = createMemo(() => {
|
||||
const data = catalog()
|
||||
if (!data) return undefined
|
||||
return findModelCatalogEntry(data, modelParam(), labParam()) ?? null
|
||||
})
|
||||
const stats = createAsync(() => {
|
||||
const entry = catalogEntry()
|
||||
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
|
||||
if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null)
|
||||
return getModelData(labParam(), entry?.slug ?? modelParam())
|
||||
})
|
||||
const page = createAsync(() => getModelPageData(labParam(), modelParam()))
|
||||
const catalogEntry = createMemo(() => page()?.catalog.entry)
|
||||
const stats = createMemo(() => page()?.stats)
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback"))
|
||||
@@ -179,13 +155,13 @@ export default function StatsModel() {
|
||||
<Header githubStars={githubStars() ?? "150K"} links={modelHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<Show when={catalogEntry() || stats() !== undefined} fallback={<ModelLoading />}>
|
||||
<Show when={page() !== undefined} fallback={<ModelLoading />}>
|
||||
<Show when={catalogEntry() || stats()} fallback={<ModelNotFound lab={labParam()} model={modelParam()} />}>
|
||||
<>
|
||||
<ModelHero
|
||||
data={stats() ?? null}
|
||||
catalog={catalogEntry() ?? null}
|
||||
catalogData={catalog() ?? null}
|
||||
catalogData={page()?.catalog ?? null}
|
||||
labName={labName()}
|
||||
/>
|
||||
<ModelOverview catalog={catalogEntry() ?? null} />
|
||||
@@ -193,10 +169,10 @@ export default function StatsModel() {
|
||||
<ModelUsageSection data={stats() ?? null} />
|
||||
<ModelUniqueUsersSection data={stats() ?? null} />
|
||||
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
|
||||
<ModelGeoBreakdownSection data={stats()?.country ?? []} />
|
||||
<ModelPeersSection data={stats() ?? null} />
|
||||
<ComparisonCardsSection
|
||||
pairs={modelComparisonPairs(catalog(), catalogEntry() ?? null, stats() ?? null)}
|
||||
pairs={modelComparisonPairs(page()?.catalog.labModels, catalogEntry() ?? null, stats() ?? null)}
|
||||
title="Compare This Model"
|
||||
description="Other models to compare with this one."
|
||||
variant="featured"
|
||||
@@ -271,9 +247,9 @@ function ModelNotFound(props: { lab: string; model: string }) {
|
||||
}
|
||||
|
||||
function ModelHero(props: {
|
||||
data: StatsModelData | null
|
||||
data: StatsModelPageData | null
|
||||
catalog: ModelCatalogEntry | null
|
||||
catalogData: ModelCatalog | null
|
||||
catalogData: ModelPageCatalog | null
|
||||
labName: string
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
@@ -282,9 +258,7 @@ function ModelHero(props: {
|
||||
const modelName = () => props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback")
|
||||
const weights = () => props.catalog?.weights[0]
|
||||
const labs = () => props.catalogData?.labs ?? []
|
||||
const labModels = () =>
|
||||
props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
|
||||
(props.catalog ? [props.catalog] : [])
|
||||
const labModels = () => props.catalogData?.labModels ?? (props.catalog ? [props.catalog] : [])
|
||||
return (
|
||||
<section id="overview" data-section="model-hero">
|
||||
<nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
|
||||
@@ -403,7 +377,7 @@ function ModelHeroActionIcon(props: { kind: "weights" | "compare" }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelHeroSparkline(props: { data: StatsModelData }) {
|
||||
function ModelHeroSparkline(props: { data: StatsModelPageData }) {
|
||||
const values = () => props.data.usage.slice(-14).map((point) => point.tokens)
|
||||
return (
|
||||
<span data-slot="model-hero-sparkline" aria-hidden="true">
|
||||
@@ -466,7 +440,7 @@ function ModelOverview(props: { catalog: ModelCatalogEntry | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
||||
function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
@@ -505,7 +479,7 @@ function ModelMomentumSection(props: { data: StatsModelData | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MomentumChart(props: { data: StatsModelData; locale: string }) {
|
||||
function MomentumChart(props: { data: StatsModelPageData; locale: string }) {
|
||||
const chart = createMemo(() => momentumChart(props.data.usage, props.data.updatedAt))
|
||||
const changeState = createMemo(() => (props.data.tokenChange < 0 ? "negative" : "positive"))
|
||||
return (
|
||||
@@ -562,7 +536,7 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUsageSection(props: { data: StatsModelData | null }) {
|
||||
function ModelUsageSection(props: { data: StatsModelPageData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
@@ -582,7 +556,7 @@ function ModelUsageSection(props: { data: StatsModelData | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
|
||||
function ModelUniqueUsersSection(props: { data: StatsModelPageData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<ModelTrendSection
|
||||
@@ -606,7 +580,7 @@ function ModelUniqueUsersSection(props: { data: StatsModelData | null }) {
|
||||
}
|
||||
|
||||
function ModelTrendSection(props: {
|
||||
data: StatsModelData | null
|
||||
data: StatsModelPageData | null
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
@@ -859,7 +833,7 @@ function ModelTrendSection(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
|
||||
function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalog: ModelCatalogEntry | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<section id="efficiency" data-section="model-panel">
|
||||
@@ -910,11 +884,11 @@ function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: M
|
||||
)
|
||||
}
|
||||
|
||||
function ModelGeoBreakdownSection(props: { data: Record<UsageRange, CountryEntry[]> }) {
|
||||
function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const [activeCountry, setActiveCountry] = createSignal<string>()
|
||||
const data = createMemo(() => props.data["2M"])
|
||||
const data = createMemo(() => props.data)
|
||||
const countryById = createMemo(
|
||||
() =>
|
||||
new Map(
|
||||
@@ -1031,31 +1005,30 @@ function GeoWorldMap(props: {
|
||||
</For>
|
||||
</g>
|
||||
<g data-slot="geo-country-markers">
|
||||
<For each={worldCountryPaths}>
|
||||
<For each={worldCountryMarkers}>
|
||||
{(country) => {
|
||||
const entry = () => props.countryById.get(country.id)
|
||||
return (
|
||||
<Show when={country.marker && entry() ? country.marker : undefined}>
|
||||
{(marker) => (
|
||||
<circle
|
||||
cx={marker().x}
|
||||
cy={marker().y}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Show when={entry()}>
|
||||
<circle
|
||||
cx={country.marker.x}
|
||||
cy={country.marker.y}
|
||||
data-country-id={country.id}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
@@ -1103,7 +1076,7 @@ function GeoCountryList(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function ModelPeersSection(props: { data: StatsModelData | null }) {
|
||||
function ModelPeersSection(props: { data: StatsModelPageData | null }) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<section id="peers" data-section="model-panel">
|
||||
@@ -1172,9 +1145,9 @@ function ModelEmptyState(props: { title: string; description: string; compact?:
|
||||
}
|
||||
|
||||
function modelComparisonPairs(
|
||||
catalog: ModelCatalog | undefined,
|
||||
catalogModels: ModelCatalogOption[] | undefined,
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelData | null,
|
||||
data: StatsModelPageData | null,
|
||||
) {
|
||||
const current = modelComparisonRef(catalogEntry, data)
|
||||
if (!current) return []
|
||||
@@ -1192,9 +1165,7 @@ function modelComparisonPairs(
|
||||
},
|
||||
detail: "Usage peer",
|
||||
}))
|
||||
const catalogPairs = (
|
||||
catalogEntry && catalog ? (catalog.labs.find((lab) => lab.id === catalogEntry.lab)?.models ?? []) : []
|
||||
)
|
||||
const catalogPairs = (catalogEntry ? (catalogModels ?? []) : [])
|
||||
.filter((model) => model.id !== catalogEntry?.id)
|
||||
.slice(0, 3)
|
||||
.map((model) => ({
|
||||
@@ -1207,7 +1178,7 @@ function modelComparisonPairs(
|
||||
|
||||
function modelComparisonRef(
|
||||
catalogEntry: ModelCatalogEntry | null,
|
||||
data: StatsModelData | null,
|
||||
data: StatsModelPageData | null,
|
||||
): ComparisonModelRef | undefined {
|
||||
if (catalogEntry) return modelRefFromCatalog(catalogEntry)
|
||||
if (!data) return undefined
|
||||
@@ -1227,31 +1198,10 @@ function getProviderIconId(author: string) {
|
||||
return author.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
||||
}
|
||||
|
||||
function emptyCountryRecord(): Record<UsageRange, CountryEntry[]> {
|
||||
return {
|
||||
"1D": [],
|
||||
"1W": [],
|
||||
"2W": [],
|
||||
"1M": [],
|
||||
"2M": [],
|
||||
"3M": [],
|
||||
YTD: [],
|
||||
ALL: [],
|
||||
}
|
||||
}
|
||||
|
||||
function countryNumericId(country: string) {
|
||||
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
|
||||
}
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function formatCountryName(country: string, locale: string, i18n: ReturnType<typeof useI18n>) {
|
||||
const code = country.toUpperCase()
|
||||
if (code === "ZZ") return i18n.t("home.unknown")
|
||||
@@ -1508,7 +1458,7 @@ function formatSparklinePoint(value: number) {
|
||||
return Number(value.toFixed(2)).toString()
|
||||
}
|
||||
|
||||
function formatModelRankMoveLabel(data: StatsModelData, i18n: ReturnType<typeof useI18n>) {
|
||||
function formatModelRankMoveLabel(data: StatsModelPageData, i18n: ReturnType<typeof useI18n>) {
|
||||
if (data.rank === null) return i18n.t("model.noUsageLastWeek")
|
||||
if (data.previousRank === null) return i18n.t("model.newThisWeek")
|
||||
const change = data.previousRank - data.rank
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type LabUsageModelEntry,
|
||||
type MarketDay,
|
||||
type ModelUsagePoint,
|
||||
type StatsHomeData,
|
||||
type StatsLabData,
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
catalogSlug,
|
||||
findModelCatalogLab,
|
||||
formatCatalogLabName,
|
||||
getModelCatalog,
|
||||
loadModelCatalog,
|
||||
type ModelCatalogEntry,
|
||||
type ModelCatalogLab,
|
||||
} from "../model-catalog"
|
||||
@@ -42,15 +41,33 @@ import {
|
||||
|
||||
const statsUnfurlPath = "banner.png"
|
||||
|
||||
const getLabData = query(async (lab: string) => {
|
||||
"use server"
|
||||
return runStatsEffect(getStatsLabData(lab))
|
||||
}, "getStatsLabData")
|
||||
type RelatedCatalogLab = Pick<ModelCatalogLab, "id" | "name" | "description"> & {
|
||||
models: Pick<ModelCatalogEntry, "name">[]
|
||||
}
|
||||
|
||||
const getHomeData = query(async () => {
|
||||
type LabPageData = {
|
||||
lab: ModelCatalogLab | null
|
||||
labs: RelatedCatalogLab[]
|
||||
market: MarketDay[]
|
||||
stats: StatsLabData | null
|
||||
}
|
||||
|
||||
const getLabPageData = query(async (labParam: string) => {
|
||||
"use server"
|
||||
return runStatsEffect(getStatsHomeData())
|
||||
}, "getStatsHomeData")
|
||||
const [catalog, home] = await Promise.all([loadModelCatalog(), runStatsEffect(getStatsHomeData())])
|
||||
const lab = findModelCatalogLab(catalog, labParam) ?? null
|
||||
return {
|
||||
lab,
|
||||
labs: catalog.labs.map((entry) => ({
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
models: entry.models.map((model) => ({ name: model.name })),
|
||||
})),
|
||||
market: home.market["2M"],
|
||||
stats: lab ? await runStatsEffect(getStatsLabData(lab.id)) : null,
|
||||
} satisfies LabPageData
|
||||
}, "getStatsLabPageData")
|
||||
|
||||
type LabModelTooltipState = {
|
||||
model: ModelCatalogEntry
|
||||
@@ -67,19 +84,9 @@ export default function StatsLab() {
|
||||
setStatsPageCacheHeaders(event?.response.headers)
|
||||
const params = useParams()
|
||||
const labParam = createMemo(() => params.lab ?? "")
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const lab = createMemo(() => {
|
||||
const data = catalog()
|
||||
if (!data) return undefined
|
||||
return findModelCatalogLab(data, labParam()) ?? null
|
||||
})
|
||||
const stats = createAsync(() => {
|
||||
const entry = lab()
|
||||
if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined)
|
||||
if (!entry) return Promise.resolve(null)
|
||||
return getLabData(entry.id)
|
||||
})
|
||||
const homeStats = createAsync((): Promise<StatsHomeData | undefined> => getHomeData())
|
||||
const page = createAsync(() => getLabPageData(labParam()))
|
||||
const lab = createMemo(() => page()?.lab)
|
||||
const stats = createMemo(() => page()?.stats)
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const labName = createMemo(() => lab()?.name ?? formatCatalogLabName(labParam()))
|
||||
@@ -137,19 +144,15 @@ export default function StatsLab() {
|
||||
<Header githubStars={githubStars() ?? "150K"} links={labHeaderLinks()} brandHref={import.meta.env.BASE_URL} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
<Show when={catalog() !== undefined} fallback={<LabLoading />}>
|
||||
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={catalog()?.labs ?? []} />}>
|
||||
<Show when={page() !== undefined} fallback={<LabLoading />}>
|
||||
<Show when={lab()} fallback={<LabNotFound lab={labParam()} labs={page()?.labs ?? []} />}>
|
||||
{(data) => (
|
||||
<>
|
||||
<LabHero lab={data()} labs={catalog()?.labs ?? []} />
|
||||
<LabHero lab={data()} labs={page()?.labs ?? []} />
|
||||
<LabOverview lab={data()} data={stats() ?? null} />
|
||||
<LabUsageSection lab={data()} data={stats() ?? null} />
|
||||
<LabModelsSection lab={data()} usage={stats()?.models ?? []} />
|
||||
<LabRelatedSection
|
||||
lab={data()}
|
||||
labs={catalog()?.labs ?? []}
|
||||
market={homeStats()?.market["2M"] ?? []}
|
||||
/>
|
||||
<LabRelatedSection lab={data()} labs={page()?.labs ?? []} market={page()?.market ?? []} />
|
||||
<ComparisonCardsSection
|
||||
pairs={labComparisonPairs(data(), stats()?.models ?? [])}
|
||||
title={`${data().name} Model Comparisons`}
|
||||
@@ -182,7 +185,7 @@ function LabLoading() {
|
||||
)
|
||||
}
|
||||
|
||||
function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
|
||||
function LabNotFound(props: { lab: string; labs: RelatedCatalogLab[] }) {
|
||||
const i18n = useI18n()
|
||||
const labName = () => formatCatalogLabName(props.lab)
|
||||
return (
|
||||
@@ -194,7 +197,7 @@ function LabNotFound(props: { lab: string; labs: ModelCatalogLab[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
|
||||
function LabHero(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[] }) {
|
||||
return (
|
||||
<section id="overview" data-section="lab-hero">
|
||||
<LabHeroBreadcrumb label={props.lab.name} labs={props.labs} />
|
||||
@@ -203,7 +206,7 @@ function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
|
||||
function LabHeroBreadcrumb(props: { label: string; labs?: RelatedCatalogLab[] }) {
|
||||
const language = useLanguage()
|
||||
const labs = () => props.labs ?? []
|
||||
const current = () => labs().find((lab) => lab.name === props.label)
|
||||
@@ -668,7 +671,7 @@ function LabModelTooltip(props: { state: LabModelTooltipState }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[]; market: MarketDay[] }) {
|
||||
function LabRelatedSection(props: { lab: ModelCatalogLab; labs: RelatedCatalogLab[]; market: MarketDay[] }) {
|
||||
const related = createMemo(() => relatedLabs(props.lab, props.labs, props.market))
|
||||
return (
|
||||
<section id="related-labs" data-section="model-panel" data-variant="lab-related">
|
||||
@@ -757,9 +760,9 @@ function labComparisonPairs(lab: ModelCatalogLab, usage: LabUsageModelEntry[]) {
|
||||
)
|
||||
}
|
||||
|
||||
type RelatedLabEntry = { lab: ModelCatalogLab; share: number; tokens: number }
|
||||
type RelatedLabEntry = { lab: RelatedCatalogLab; share: number; tokens: number }
|
||||
|
||||
function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
|
||||
function relatedLabs(current: ModelCatalogLab, labs: RelatedCatalogLab[], market: MarketDay[]): RelatedLabEntry[] {
|
||||
const stats = relatedLabStats(labs, market)
|
||||
return labs
|
||||
.filter((lab) => lab.id !== current.id)
|
||||
@@ -768,8 +771,8 @@ function relatedLabs(current: ModelCatalogLab, labs: ModelCatalogLab[], market:
|
||||
.slice(0, 3)
|
||||
}
|
||||
|
||||
function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
|
||||
const labByKey = new Map<string, ModelCatalogLab>()
|
||||
function relatedLabStats(labs: RelatedCatalogLab[], market: MarketDay[]) {
|
||||
const labByKey = new Map<string, RelatedCatalogLab>()
|
||||
labs.forEach((lab) => {
|
||||
labByKey.set(lab.id, lab)
|
||||
labByKey.set(catalogSlug(lab.name), lab)
|
||||
@@ -794,7 +797,7 @@ function relatedLabStats(labs: ModelCatalogLab[], market: MarketDay[]) {
|
||||
)
|
||||
}
|
||||
|
||||
function labRelatedDescription(lab: ModelCatalogLab) {
|
||||
function labRelatedDescription(lab: RelatedCatalogLab) {
|
||||
return lab.description ?? ""
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export type ComparisonPair = {
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function modelRefFromCatalog(entry: ModelCatalogEntry): ComparisonModelRef {
|
||||
export function modelRefFromCatalog(entry: Pick<ModelCatalogEntry, "name" | "lab" | "slug">): ComparisonModelRef {
|
||||
return {
|
||||
name: entry.name,
|
||||
lab: entry.lab,
|
||||
@@ -83,11 +83,7 @@ export function ComparisonCardsSection(props: {
|
||||
|
||||
function FeaturedComparisonCard(props: { pair: ComparisonPair }) {
|
||||
return (
|
||||
<a
|
||||
data-component="compare-home-card"
|
||||
href={canonicalComparisonHref(props.pair.first, props.pair.second)}
|
||||
aria-label={`${props.pair.detail}: ${props.pair.first.name} vs ${props.pair.second.name}`}
|
||||
>
|
||||
<a data-component="compare-home-card" href={canonicalComparisonHref(props.pair.first, props.pair.second)}>
|
||||
<span data-slot="compare-home-card-head">
|
||||
<span>
|
||||
<strong>{props.pair.detail}</strong>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-110m.json?raw"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
|
||||
export const geoMapWidth = 960
|
||||
export const geoMapHeight = 430
|
||||
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
|
||||
export const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
}))
|
||||
|
||||
export const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
// The 110m topology omits small regions. Geographic centroids keep those countries interactive without shipping 50m paths.
|
||||
const fallbackCountryMarkerCoordinates = [
|
||||
["016", -170.7179, -14.3046],
|
||||
["020", 1.5606, 42.542],
|
||||
["028", -61.7945, 17.2762],
|
||||
["048", 50.5425, 26.0417],
|
||||
["052", -59.5602, 13.1811],
|
||||
["060", -64.7558, 32.3131],
|
||||
["086", 72.4453, -7.3312],
|
||||
["092", -64.4704, 18.5276],
|
||||
["132", -23.9576, 15.9551],
|
||||
["136", -80.9129, 19.43],
|
||||
["174", 43.6844, -11.879],
|
||||
["184", -159.7871, -21.2195],
|
||||
["212", -61.3576, 15.4394],
|
||||
["234", -6.8808, 62.0527],
|
||||
["239", -36.4863, -54.4641],
|
||||
["248", 19.9528, 60.2153],
|
||||
["258", -144.8045, -14.7283],
|
||||
["296", -167.9217, 0.893],
|
||||
["308", -61.6818, 12.1174],
|
||||
["316", 144.767, 13.4406],
|
||||
["334", 73.52, -53.0872],
|
||||
["336", 12.4343, 41.9021],
|
||||
["344", 114.1143, 22.3983],
|
||||
["438", 9.5357, 47.1367],
|
||||
["446", 113.509, 22.2231],
|
||||
["462", 73.4573, 3.7316],
|
||||
["470", 14.405, 35.9215],
|
||||
["480", 57.5714, -20.2779],
|
||||
["492", 7.4073, 43.7526],
|
||||
["500", -62.1856, 16.7404],
|
||||
["520", 166.9326, -0.5189],
|
||||
["531", -68.9721, 12.1957],
|
||||
["533", -69.9827, 12.521],
|
||||
["534", -63.0572, 18.0509],
|
||||
["570", -169.8704, -19.0489],
|
||||
["574", 167.9497, -29.0516],
|
||||
["580", 145.6193, 15.8288],
|
||||
["583", 153.2966, 7.5361],
|
||||
["584", 170.3313, 7.015],
|
||||
["585", 134.4056, 7.286],
|
||||
["612", -128.3167, -24.3649],
|
||||
["652", -62.841, 17.8988],
|
||||
["654", -9.7009, -12.3548],
|
||||
["659", -62.6873, 17.2647],
|
||||
["660", -63.066, 18.2243],
|
||||
["662", -60.9696, 13.8946],
|
||||
["663", -63.0599, 18.0888],
|
||||
["666", -56.3037, 46.9187],
|
||||
["670", -61.2008, 13.2251],
|
||||
["674", 12.4594, 43.9415],
|
||||
["678", 6.7235, 0.4434],
|
||||
["690", 55.476, -4.6601],
|
||||
["702", 103.817, 1.359],
|
||||
["776", -174.7998, -20.4161],
|
||||
["796", -71.9734, 21.8312],
|
||||
["831", -2.5726, 49.4678],
|
||||
["832", -2.1272, 49.2181],
|
||||
["833", -4.5388, 54.224],
|
||||
["850", -64.8028, 17.9555],
|
||||
["876", -177.3469, -13.8898],
|
||||
["882", -172.1649, -13.7536],
|
||||
] as const
|
||||
|
||||
export const worldCountryMarkers = [
|
||||
...worldCountries.features.flatMap((country) => {
|
||||
const marker = geoCountryMarker(country)
|
||||
return marker ? [{ id: String(country.id ?? "").padStart(3, "0"), marker }] : []
|
||||
}),
|
||||
...fallbackCountryMarkerCoordinates.flatMap(([id, longitude, latitude]) => {
|
||||
const marker = worldProjection([longitude, latitude])
|
||||
return marker ? [{ id, marker: { x: marker[0], y: marker[1] } }] : []
|
||||
}),
|
||||
]
|
||||
@@ -55,7 +55,7 @@ body {
|
||||
--color-background-strong: #161616;
|
||||
--color-background-strong-hover: #242424;
|
||||
--color-text: #5c5c5c;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-weak: #707070;
|
||||
--color-text-strong: #161616;
|
||||
--color-text-inverted: #ffffff;
|
||||
--color-border-weak: #0000001a;
|
||||
@@ -66,10 +66,10 @@ body {
|
||||
--stats-line-strong: #00000033;
|
||||
--stats-text: #161616;
|
||||
--stats-muted: #5c5c5c;
|
||||
--stats-faint: #808080;
|
||||
--stats-faint: #707070;
|
||||
--stats-theme-icon-active: #3a3a3a;
|
||||
--stats-accent: #3b5cf6;
|
||||
--stats-accent-text: #6c7dff;
|
||||
--stats-accent-text: #3b5cf6;
|
||||
--stats-bar-idle: #d4d4d4;
|
||||
--stats-dot: #d4d4d4;
|
||||
--stats-hero-muted: #5c5c5c;
|
||||
@@ -92,6 +92,18 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="visually-hidden"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"][data-layout="compare-detail"] {
|
||||
/* The table contains its own wide rows; keep the page itself out of the horizontal scroll chain. */
|
||||
overflow-x: visible;
|
||||
@@ -7079,7 +7091,7 @@ body {
|
||||
--color-background-strong: #ffffff;
|
||||
--color-background-strong-hover: #eeeeee;
|
||||
--color-text: #d4d4d4;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-weak: #a3a3a3;
|
||||
--color-text-strong: #ffffff;
|
||||
--color-text-inverted: #161616;
|
||||
--color-border-weak: #ffffff1a;
|
||||
@@ -7090,11 +7102,12 @@ body {
|
||||
--stats-line-strong: #ffffff33;
|
||||
--stats-text: #ffffff;
|
||||
--stats-muted: #d4d4d4;
|
||||
--stats-faint: #808080;
|
||||
--stats-faint: #a3a3a3;
|
||||
--stats-accent-text: #8190ff;
|
||||
--stats-theme-icon-active: #fafafa;
|
||||
--stats-bar-idle: #303030;
|
||||
--stats-dot: #303030;
|
||||
--stats-hero-muted: #808080;
|
||||
--stats-hero-muted: #a3a3a3;
|
||||
--stats-hero-pattern: #303030;
|
||||
--stats-logo-bg: #f1ecec;
|
||||
--stats-logo-fill: #b7b1b1;
|
||||
@@ -7254,7 +7267,7 @@ body {
|
||||
--color-background-strong: #ffffff;
|
||||
--color-background-strong-hover: #eeeeee;
|
||||
--color-text: #d4d4d4;
|
||||
--color-text-weak: #808080;
|
||||
--color-text-weak: #a3a3a3;
|
||||
--color-text-strong: #ffffff;
|
||||
--color-text-inverted: #161616;
|
||||
--color-border-weak: #ffffff1a;
|
||||
@@ -7265,11 +7278,12 @@ body {
|
||||
--stats-line-strong: #ffffff33;
|
||||
--stats-text: #ffffff;
|
||||
--stats-muted: #d4d4d4;
|
||||
--stats-faint: #808080;
|
||||
--stats-faint: #a3a3a3;
|
||||
--stats-accent-text: #8190ff;
|
||||
--stats-theme-icon-active: #fafafa;
|
||||
--stats-bar-idle: #303030;
|
||||
--stats-dot: #303030;
|
||||
--stats-hero-muted: #808080;
|
||||
--stats-hero-muted: #a3a3a3;
|
||||
--stats-hero-pattern: #303030;
|
||||
--stats-logo-bg: #f1ecec;
|
||||
--stats-logo-fill: #b7b1b1;
|
||||
@@ -8393,7 +8407,7 @@ body {
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] {
|
||||
--top-models-mobile-bar-width: 12px;
|
||||
--top-models-mobile-bar-width: 18px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] [data-slot="top-models-axis"],
|
||||
@@ -8402,7 +8416,7 @@ body {
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="market-share"][data-dense-labels="true"] {
|
||||
--market-mobile-bar-width: 12px;
|
||||
--market-mobile-bar-width: 18px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="market-share"][data-dense-labels="true"] [data-slot="market-labels"],
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { Link, Meta, Title } from "@solidjs/meta"
|
||||
import { Meta, Title } from "@solidjs/meta"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { scaleSqrt } from "d3-scale"
|
||||
import countryCodesSource from "i18n-iso-countries/codes.json?raw"
|
||||
import { feature, mesh } from "topojson-client"
|
||||
import countriesTopologySource from "world-atlas/countries-50m.json?raw"
|
||||
import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url"
|
||||
import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url"
|
||||
import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url"
|
||||
import ibmPlexMonoBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Bold-Latin1.woff2?url"
|
||||
import {
|
||||
getStatsHomeData,
|
||||
type CacheRatioEntry,
|
||||
type CountryEntry,
|
||||
type LeaderboardEntry,
|
||||
type MarketDay,
|
||||
type StatsHomeData,
|
||||
type SessionCostEntry,
|
||||
type TokenCostEntry,
|
||||
type UsagePoint,
|
||||
@@ -23,14 +15,13 @@ import {
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
|
||||
import type { GeometryCollection, Topology } from "topojson-specification"
|
||||
import { runStatsEffect } from "../stats-runtime"
|
||||
import { LocaleLinks } from "../component/locale-links"
|
||||
import { useI18n } from "../context/i18n"
|
||||
import { useLanguage } from "../context/language"
|
||||
import { localizedUrl } from "../lib/language"
|
||||
import { findModelCatalogEntry, getModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||
import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
|
||||
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
|
||||
import { SectionHeading } from "./section-heading"
|
||||
import { setStatsPageCacheHeaders } from "./stats-cache"
|
||||
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
|
||||
@@ -45,9 +36,6 @@ import {
|
||||
type ThemePreference,
|
||||
} from "./stats-shell"
|
||||
|
||||
const products = ["All Users", "Zen", "Go"] as const
|
||||
const tokenProducts = ["Zen", "Go"] as const
|
||||
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
|
||||
const comparisonPairIndexes = [
|
||||
[0, 1, "Top two by recent usage"],
|
||||
[0, 2, "Leader vs challenger"],
|
||||
@@ -69,60 +57,40 @@ const usageColors = [
|
||||
"#ff6467",
|
||||
]
|
||||
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
||||
const geoMapWidth = 960
|
||||
const geoMapHeight = 430
|
||||
|
||||
type UsageProduct = (typeof products)[number]
|
||||
type TokenProduct = (typeof tokenProducts)[number]
|
||||
type UsageRange = (typeof ranges)[number]
|
||||
type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M"
|
||||
type IsoCountryCode = readonly [string, string, string]
|
||||
type WorldCountryProperties = GeoJsonProperties & { name?: string }
|
||||
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
|
||||
|
||||
function productLabel(product: UsageProduct | TokenProduct, i18n: ReturnType<typeof useI18n>) {
|
||||
if (product === "All Users") return i18n.t("product.allUsers")
|
||||
if (product === "Zen") return i18n.t("product.zen")
|
||||
return i18n.t("product.go")
|
||||
}
|
||||
|
||||
function rangeLabel(range: UsageRange, i18n: ReturnType<typeof useI18n>) {
|
||||
if (range === "1D") return i18n.t("range.1D")
|
||||
if (range === "1W") return i18n.t("range.1W")
|
||||
if (range === "2W") return i18n.t("range.2W")
|
||||
if (range === "1M") return i18n.t("range.1M")
|
||||
return i18n.t("range.2M")
|
||||
type StatsHomePageData = {
|
||||
updatedAt: string | null
|
||||
usage: UsagePoint[]
|
||||
users: UsagePoint[]
|
||||
leaderboard: LeaderboardEntry[]
|
||||
market: MarketDay[]
|
||||
tokenCost: TokenCostEntry[]
|
||||
cacheRatio: CacheRatioEntry[]
|
||||
sessionCost: SessionCostEntry[]
|
||||
country: CountryEntry[]
|
||||
}
|
||||
|
||||
const countryNumericIds = new Map(
|
||||
(JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const),
|
||||
)
|
||||
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
|
||||
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
|
||||
...worldTopology.objects.countries,
|
||||
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
|
||||
}
|
||||
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
|
||||
GeometryObject,
|
||||
WorldCountryProperties
|
||||
>
|
||||
const worldProjection = geoEquirectangular().fitExtent(
|
||||
[
|
||||
[10, 12],
|
||||
[geoMapWidth - 10, geoMapHeight - 12],
|
||||
],
|
||||
worldCountries,
|
||||
)
|
||||
const worldPath = geoPath(worldProjection)
|
||||
const worldCountryPaths = worldCountries.features.map((country) => ({
|
||||
id: String(country.id ?? "").padStart(3, "0"),
|
||||
path: worldPath(country) ?? "",
|
||||
marker: geoCountryMarker(country),
|
||||
}))
|
||||
const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
|
||||
|
||||
const getData = query(async () => {
|
||||
"use server"
|
||||
return runStatsEffect(getStatsHomeData())
|
||||
const [stats, catalog] = await Promise.all([runStatsEffect(getStatsHomeData()), loadModelCatalog()])
|
||||
return {
|
||||
updatedAt: stats.updatedAt,
|
||||
usage: stats.usage.Go["2M"],
|
||||
users: stats.users.Go["2M"],
|
||||
leaderboard: stats.leaderboard.Go["2M"],
|
||||
market: stats.market["2M"],
|
||||
tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
|
||||
cacheRatio: stats.cacheRatio.Go,
|
||||
sessionCost: stats.sessionCost.Go,
|
||||
country: stats.country["2M"],
|
||||
} satisfies StatsHomePageData
|
||||
}, "getStatsHomeData")
|
||||
|
||||
export default function StatsHome() {
|
||||
@@ -133,7 +101,6 @@ export default function StatsHome() {
|
||||
const statsHomeUrl = localizedUrl(language.locale(), "/data/")
|
||||
const statsUnfurlUrl = new URL(statsUnfurlPath, localizedUrl("en", "/data/")).toString()
|
||||
const data = createAsync(() => getData())
|
||||
const catalog = createAsync(() => getModelCatalog())
|
||||
const githubStars = createAsync(() => getGitHubStars())
|
||||
const [themePreference, setThemePreference] = createSignal<ThemePreference>("system")
|
||||
const updateThemePreference = (preference: ThemePreference) => {
|
||||
@@ -171,10 +138,6 @@ export default function StatsHome() {
|
||||
<Meta name="twitter:description" content={i18n.t("app.description")} />
|
||||
<Meta name="twitter:image" content={statsUnfurlUrl} />
|
||||
<Meta name="twitter:image:alt" content={i18n.t("app.unfurlAlt")} />
|
||||
<Link rel="preload" href={ibmPlexMonoRegularLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoMediumLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoSemiBoldLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Link rel="preload" href={ibmPlexMonoBoldLatin1} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<Header githubStars={githubStars() ?? githubLink.fallbackStars} />
|
||||
<div data-component="container">
|
||||
<div data-component="content">
|
||||
@@ -185,12 +148,12 @@ export default function StatsHome() {
|
||||
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
|
||||
<UniqueUsersSection data={stats().users} />
|
||||
<SessionCostSection data={stats().sessionCost} />
|
||||
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
|
||||
<TokenCostSection data={stats().tokenCost} />
|
||||
<CacheRatioSection data={stats().cacheRatio} />
|
||||
<MarketShareSection data={stats().market} />
|
||||
<GeoBreakdownSection data={stats().country} />
|
||||
<ComparisonCardsSection
|
||||
pairs={homeComparisonPairs(stats().leaderboard["All Users"]["2M"])}
|
||||
pairs={homeComparisonPairs(stats().leaderboard)}
|
||||
title="Model Comparisons"
|
||||
description="Popular model pairs from the leaderboard."
|
||||
variant="featured"
|
||||
@@ -256,7 +219,8 @@ function Hero(props: { updatedAt: string | null }) {
|
||||
|
||||
return (
|
||||
<section data-section="hero">
|
||||
<p data-slot="hero-meta" aria-live="polite" aria-atomic="true" aria-label={currentUpdatedLabel()}>
|
||||
<p data-slot="hero-meta" role="status" aria-atomic="true">
|
||||
<span data-slot="visually-hidden">{currentUpdatedLabel()}</span>
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -401,32 +365,9 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
|
||||
return `${value.date}, ${value.time}`
|
||||
}
|
||||
|
||||
function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: StatsHomeData["leaderboard"] }) {
|
||||
function TopModelsSection(props: { data: UsagePoint[]; leaderboard: LeaderboardEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<UsageProduct>("Go")
|
||||
const [range, setRange] = createSignal<UsageRange>("2M")
|
||||
const [sheet, setSheet] = createSignal<"product" | "range">()
|
||||
const [activeModel, setActiveModel] = createSignal<string>()
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
const leaderboard = createMemo(() => props.leaderboard[product()][range()])
|
||||
|
||||
createEffect(() => {
|
||||
if (!sheet()) return
|
||||
if (typeof document === "undefined") return
|
||||
const htmlOverflow = document.documentElement.style.overflow
|
||||
const bodyOverflow = document.body.style.overflow
|
||||
document.documentElement.style.overflow = "hidden"
|
||||
document.body.style.overflow = "hidden"
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setSheet(undefined)
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown)
|
||||
onCleanup(() => {
|
||||
document.documentElement.style.overflow = htmlOverflow
|
||||
document.body.style.overflow = bodyOverflow
|
||||
document.removeEventListener("keydown", onKeyDown)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<section id="top-models" data-section="top-models">
|
||||
@@ -438,201 +379,23 @@ function TopModelsSection(props: { data: StatsHomeData["usage"]; leaderboard: St
|
||||
description={i18n.t("home.topModelsDescription")}
|
||||
/>
|
||||
<Show
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
when={props.data.some((item) => usageTotal(item) > 0)}
|
||||
fallback={<EmptyState title={i18n.t("home.noUsageTitle")} description={i18n.t("home.noUsageDescription")} />}
|
||||
>
|
||||
<TopModelsChart
|
||||
data={data()}
|
||||
range={range()}
|
||||
activeModel={activeModel()}
|
||||
onActiveModelChange={setActiveModel}
|
||||
/>
|
||||
<TopModelsChart data={props.data} range="2M" activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
</Show>
|
||||
<Show
|
||||
when={leaderboard().length > 0}
|
||||
when={props.leaderboard.length > 0}
|
||||
fallback={
|
||||
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
|
||||
}
|
||||
>
|
||||
<Leaderboard data={leaderboard()} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer" hidden>
|
||||
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
<div data-slot="top-models-mobile-controls">
|
||||
<MobileFilterButton
|
||||
label={i18n.t("home.productFilter")}
|
||||
value={productLabel(product(), i18n)}
|
||||
expanded={sheet() === "product"}
|
||||
onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
|
||||
/>
|
||||
<MobileFilterButton
|
||||
label={i18n.t("home.dateRange")}
|
||||
value={rangeLabel(range(), i18n)}
|
||||
expanded={sheet() === "range"}
|
||||
onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={sheet()}>
|
||||
{(kind) => (
|
||||
<MobileFilterSheet
|
||||
kind={kind()}
|
||||
product={product()}
|
||||
range={range()}
|
||||
onProductSelect={(value) => {
|
||||
setProduct(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onRangeSelect={(value) => {
|
||||
setRange(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onClose={() => setSheet(undefined)}
|
||||
/>
|
||||
)}
|
||||
<Leaderboard data={props.leaderboard} activeModel={activeModel()} onActiveModelChange={setActiveModel} />
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
data-slot="mobile-filter-button"
|
||||
type="button"
|
||||
aria-label={props.label}
|
||||
aria-expanded={props.expanded ? "true" : "false"}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<span>{props.value}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterSheet(props: {
|
||||
kind: "product" | "range"
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
|
||||
<div
|
||||
data-slot="filter-sheet-panel"
|
||||
role="radiogroup"
|
||||
aria-label={props.kind === "product" ? i18n.t("home.productFilter") : i18n.t("home.dateRange")}
|
||||
>
|
||||
<Show
|
||||
when={props.kind === "product"}
|
||||
fallback={
|
||||
<For each={ranges}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.range === item}
|
||||
data-active={props.range === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onRangeSelect(item)
|
||||
}}
|
||||
>
|
||||
{rangeLabel(item, i18n)}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<For each={products}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.product === item}
|
||||
data-active={props.product === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onProductSelect(item)
|
||||
}}
|
||||
>
|
||||
{productLabel(item, i18n)}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDown() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
||||
<path d="M5 7L8 10L11 7" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsFilters(props: {
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
return (
|
||||
<>
|
||||
<FilterPills
|
||||
items={products}
|
||||
selected={props.product}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={props.onProductSelect}
|
||||
/>
|
||||
<FilterPills
|
||||
items={ranges}
|
||||
selected={props.range}
|
||||
label={i18n.t("home.dateRange")}
|
||||
variant="range"
|
||||
formatLabel={(item) => rangeLabel(item, i18n)}
|
||||
onSelect={props.onRangeSelect}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function FilterPills<T extends string>(props: {
|
||||
items: readonly T[]
|
||||
selected: T
|
||||
label: string
|
||||
variant: "product" | "range"
|
||||
formatLabel?: (item: T) => string
|
||||
onSelect: (item: T) => void
|
||||
}) {
|
||||
return (
|
||||
<div data-component="usage-filter" data-variant={props.variant} role="radiogroup" aria-label={props.label}>
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.selected === item}
|
||||
data-active={props.selected === item ? "true" : undefined}
|
||||
onClick={() => props.onSelect(item)}
|
||||
>
|
||||
{props.formatLabel ? props.formatLabel(item) : item}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TopModelsChart(props: {
|
||||
data: UsagePoint[]
|
||||
range: UsageRange
|
||||
@@ -823,10 +586,9 @@ function TopModelsChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
|
||||
function UniqueUsersSection(props: { data: UsagePoint[] }) {
|
||||
const i18n = useI18n()
|
||||
const [activeModel, setActiveModel] = createSignal<string>()
|
||||
const data = createMemo(() => props.data.Go["2M"])
|
||||
|
||||
return (
|
||||
<section id="unique-users" data-section="unique-users">
|
||||
@@ -837,13 +599,13 @@ function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
|
||||
description={i18n.t("home.uniqueUsersDescription")}
|
||||
/>
|
||||
<Show
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
when={props.data.some((item) => usageTotal(item) > 0)}
|
||||
fallback={
|
||||
<EmptyState title={i18n.t("home.noUserDataTitle")} description={i18n.t("home.noUserDataDescription")} />
|
||||
}
|
||||
>
|
||||
<TopModelsChart
|
||||
data={data()}
|
||||
data={props.data}
|
||||
range="2M"
|
||||
metric="users"
|
||||
ariaLabel={i18n.t("home.uniqueUsersChart")}
|
||||
@@ -927,7 +689,7 @@ function isDenseColumnRange(range: UsageRange) {
|
||||
function scrollDenseChartToEnd(element: HTMLDivElement | undefined, range: UsageRange, count: number) {
|
||||
if (!element || count <= 0 || !isDenseColumnRange(range) || typeof window === "undefined") return
|
||||
window.requestAnimationFrame(() => {
|
||||
element.scrollLeft = element.scrollWidth - element.clientWidth
|
||||
element.scrollLeft = Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
}
|
||||
|
||||
@@ -966,12 +728,11 @@ function Leaderboard(props: {
|
||||
activeModel: string | undefined
|
||||
onActiveModelChange: (model: string | undefined) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const featured = createMemo(() => props.data.slice(0, 3))
|
||||
const compact = createMemo(() => props.data.slice(3))
|
||||
|
||||
return (
|
||||
<div id="leaderboard" data-component="leaderboard" role="list" aria-label={i18n.t("chart.leaderboardAria")}>
|
||||
<div id="leaderboard" data-component="leaderboard">
|
||||
<div data-slot="leaderboard-featured">
|
||||
<For each={featured()}>
|
||||
{(entry) => (
|
||||
@@ -997,7 +758,7 @@ function Leaderboard(props: {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div data-slot="leaderboard-mobile" aria-label={i18n.t("chart.scrollableLeaderboardAria")}>
|
||||
<div data-slot="leaderboard-mobile">
|
||||
<For each={props.data}>
|
||||
{(entry) => (
|
||||
<LeaderboardCard
|
||||
@@ -1029,9 +790,6 @@ function LeaderboardCard(props: {
|
||||
href={language.route(
|
||||
`${import.meta.env.BASE_URL}${modelSlug(props.entry.provider)}/${modelSlug(props.entry.model)}`,
|
||||
)}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-label={`${String(props.entry.rank).padStart(2, "0")} ${props.entry.model} ${i18n.t("chart.byAuthor", { author: props.entry.author })}`}
|
||||
onPointerEnter={() => props.onActiveModelChange(props.entry.model)}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
@@ -1084,16 +842,14 @@ function formatChange(value: number | null, i18n: ReturnType<typeof useI18n>) {
|
||||
return `${value}%`
|
||||
}
|
||||
|
||||
function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
||||
function MarketShareSection(props: { data: MarketDay[] }) {
|
||||
const i18n = useI18n()
|
||||
const [range, setRange] = createSignal<UsageRange>("2M")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const [activeAuthor, setActiveAuthor] = createSignal<string>()
|
||||
const [inspecting, setInspecting] = createSignal(false)
|
||||
const data = createMemo(() => props.data[range()])
|
||||
const authorOrder = createMemo(() => getMarketAuthorOrder(data()))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(data().length - 1, 0)))
|
||||
const activeDay = createMemo(() => data()[selectedIndex()])
|
||||
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
|
||||
const activeDay = createMemo(() => props.data[selectedIndex()])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -1118,8 +874,8 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
||||
{(day) => (
|
||||
<>
|
||||
<MarketShare
|
||||
data={data()}
|
||||
range={range()}
|
||||
data={props.data}
|
||||
range="2M"
|
||||
authorOrder={authorOrder()}
|
||||
activeIndex={selectedIndex()}
|
||||
activeAuthor={activeAuthor()}
|
||||
@@ -1151,22 +907,9 @@ function MarketShareSection(props: { data: StatsHomeData["market"] }) {
|
||||
<strong>
|
||||
{inspecting()
|
||||
? formatMarketDate(activeDay(), i18n.t("home.noData"))
|
||||
: formatMarketRange(data(), i18n.t("home.noData"))}
|
||||
: formatMarketRange(props.data, i18n.t("home.noData"))}
|
||||
</strong>
|
||||
</p>
|
||||
<div hidden>
|
||||
<FilterPills
|
||||
items={ranges}
|
||||
selected={range()}
|
||||
label={i18n.t("home.dateRange")}
|
||||
variant="range"
|
||||
onSelect={(item) => {
|
||||
setRange(item)
|
||||
setActiveAuthor(undefined)
|
||||
setInspecting(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -1202,6 +945,7 @@ function MarketShare(props: {
|
||||
{(day, index) => (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${day.date} ${formatTrillions(day.total)}`}
|
||||
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
||||
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||
@@ -1277,14 +1021,11 @@ function MarketShareList(props: {
|
||||
activeAuthor: string | undefined
|
||||
onActiveAuthorChange: (author: string) => void
|
||||
}) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
<ol data-component="market-share-list">
|
||||
<For each={props.data}>
|
||||
{(item, index) => {
|
||||
const label = () =>
|
||||
`${item.author} ${formatTrillions(item.tokens)} ${item.share.toFixed(1)} ${i18n.t("chart.percent")}`
|
||||
const content = () => (
|
||||
<>
|
||||
<span>{String(index() + 1).padStart(2, "0")}</span>
|
||||
@@ -1306,7 +1047,6 @@ function MarketShareList(props: {
|
||||
fallback={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label()}
|
||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
||||
>
|
||||
@@ -1317,7 +1057,6 @@ function MarketShareList(props: {
|
||||
{(href) => (
|
||||
<a
|
||||
href={href()}
|
||||
aria-label={label()}
|
||||
onClick={() => props.onActiveAuthorChange(item.author)}
|
||||
onFocus={() => props.onActiveAuthorChange(item.author)}
|
||||
>
|
||||
@@ -1333,23 +1072,22 @@ function MarketShareList(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
|
||||
function GeoBreakdownSection(props: { data: CountryEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const [activeCountry, setActiveCountry] = createSignal<string>()
|
||||
const data = createMemo(() => props.data["2M"])
|
||||
const countryById = createMemo(
|
||||
() =>
|
||||
new Map(
|
||||
data().flatMap((country) => {
|
||||
props.data.flatMap((country) => {
|
||||
const id = countryNumericId(country.country)
|
||||
return id ? [[id, country] as const] : []
|
||||
}),
|
||||
),
|
||||
)
|
||||
const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
|
||||
const topCountries = createMemo(() => data().slice(0, 15))
|
||||
const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
|
||||
const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
|
||||
const topCountries = createMemo(() => props.data.slice(0, 15))
|
||||
const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -1363,7 +1101,7 @@ function GeoBreakdownSection(props: { data: StatsHomeData["country"] }) {
|
||||
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
|
||||
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
|
||||
<Show
|
||||
when={data().length > 0}
|
||||
when={props.data.length > 0}
|
||||
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
|
||||
>
|
||||
<div data-component="geo-breakdown">
|
||||
@@ -1453,31 +1191,30 @@ function GeoWorldMap(props: {
|
||||
</For>
|
||||
</g>
|
||||
<g data-slot="geo-country-markers">
|
||||
<For each={worldCountryPaths}>
|
||||
<For each={worldCountryMarkers}>
|
||||
{(country) => {
|
||||
const entry = () => props.countryById.get(country.id)
|
||||
return (
|
||||
<Show when={country.marker && entry() ? country.marker : undefined}>
|
||||
{(marker) => (
|
||||
<circle
|
||||
cx={marker().x}
|
||||
cy={marker().y}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Show when={entry()}>
|
||||
<circle
|
||||
cx={country.marker.x}
|
||||
cy={country.marker.y}
|
||||
data-country-id={country.id}
|
||||
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
|
||||
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
|
||||
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
|
||||
aria-hidden="true"
|
||||
onPointerEnter={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
onClick={() => {
|
||||
const item = entry()
|
||||
if (!item) return
|
||||
props.onActiveCountryChange(item.country)
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
@@ -1507,7 +1244,6 @@ function GeoCountryList(props: {
|
||||
type="button"
|
||||
data-active={props.activeCountry === country.country ? "true" : undefined}
|
||||
style={{ "--geo-row-opacity": String(opacityScale()(country.tokens)) } as JSX.CSSProperties}
|
||||
aria-label={`${formatCountryName(country.country, language.tag(language.locale()), i18n.t("home.unknown"))} ${formatGeoTokens(country.tokens)} ${formatGeoShare(country.share)}`}
|
||||
onClick={() => props.onActiveCountryChange(country.country)}
|
||||
onPointerEnter={() => props.onActiveCountryChange(country.country)}
|
||||
onFocus={() => props.onActiveCountryChange(country.country)}
|
||||
@@ -1531,14 +1267,6 @@ function countryNumericId(country: string) {
|
||||
return countryNumericIds.get(country.toUpperCase())?.padStart(3, "0")
|
||||
}
|
||||
|
||||
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
|
||||
const bounds = worldPath.bounds(country)
|
||||
const [x, y] = worldPath.centroid(country)
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
|
||||
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
|
||||
return { x, y }
|
||||
}
|
||||
|
||||
function formatCountryName(country: string, locale: string, unknown: string) {
|
||||
const code = country.toUpperCase()
|
||||
if (code === "ZZ") return unknown
|
||||
@@ -1636,12 +1364,10 @@ function marketDateParts(label: string) {
|
||||
return { start: start ?? label, end: end ?? start ?? label }
|
||||
}
|
||||
|
||||
function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: ModelCatalog | null }) {
|
||||
function TokenCostSection(props: { data: TokenCostEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => priceTokenCostFromCatalog(props.data[product()], props.catalog))
|
||||
const visible = createMemo(() => data().slice(0, 13))
|
||||
const visible = createMemo(() => props.data.slice(0, 13))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1660,17 +1386,6 @@ function TokenCostSection(props: { data: StatsHomeData["tokenCost"]; catalog: Mo
|
||||
>
|
||||
<TokenCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1723,12 +1438,10 @@ function TokenCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
|
||||
function CacheRatioSection(props: { data: CacheRatioEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const visible = createMemo(() => data().slice(0, 16))
|
||||
const visible = createMemo(() => props.data.slice(0, 16))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1745,17 +1458,6 @@ function CacheRatioSection(props: { data: StatsHomeData["cacheRatio"] }) {
|
||||
>
|
||||
<CacheRatioChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1853,12 +1555,10 @@ function MetricBar(props: { value: number; max: number; active: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
||||
function SessionCostSection(props: { data: SessionCostEntry[] }) {
|
||||
const i18n = useI18n()
|
||||
const [product, setProduct] = createSignal<TokenProduct>("Go")
|
||||
const [activeIndex, setActiveIndex] = createSignal(2)
|
||||
const data = createMemo(() => props.data[product()])
|
||||
const visible = createMemo(() => data().slice(0, 16))
|
||||
const visible = createMemo(() => props.data.slice(0, 16))
|
||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0)))
|
||||
|
||||
return (
|
||||
@@ -1877,17 +1577,6 @@ function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) {
|
||||
>
|
||||
<SessionCostChart data={visible()} activeIndex={selectedIndex()} onActiveIndexChange={setActiveIndex} />
|
||||
</Show>
|
||||
<div data-slot="token-footer" hidden>
|
||||
<FilterPills
|
||||
items={tokenProducts}
|
||||
selected={product()}
|
||||
label={i18n.t("home.productFilter")}
|
||||
variant="product"
|
||||
formatLabel={(item) => productLabel(item, i18n)}
|
||||
onSelect={setProduct}
|
||||
/>
|
||||
<LiveIndicator />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1949,11 +1638,6 @@ function SessionCostChart(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function LiveIndicator() {
|
||||
const i18n = useI18n()
|
||||
return <span data-component="live-filter">{i18n.t("chart.live")}</span>
|
||||
}
|
||||
|
||||
function formatTokenCount(value: number) {
|
||||
if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M`
|
||||
return `${Math.round(value / 1_000)}K`
|
||||
|
||||
@@ -18,6 +18,7 @@ export default defineConfig({
|
||||
allowedHosts: true,
|
||||
},
|
||||
build: {
|
||||
minify: false,
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-core",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/stats-server",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
[14:25:48.462] [INFO] storybook v10.2.10
|
||||
[14:25:48.749] [DEBUG] Getting package.json info for /Users/davidhill/Documents/Local/opencode/packages/storybook/package.json...
|
||||
[14:25:48.997] [INFO] Starting...
|
||||
[14:25:49.095] [DEBUG] Starting preview..
|
||||
[14:25:49.098] [WARN] 🚨 Unable to index files:
|
||||
- ./../ui/src/components/accordion.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/accordion.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/app-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/app-icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/avatar.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/avatar.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/basic-tool.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/basic-tool.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/checkbox.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/checkbox.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/code.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/code.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/collapsible.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/collapsible.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/context-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/context-menu.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dialog.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dialog.stories.tsx (line 10, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff-changes.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-changes.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff-ssr.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-ssr.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dock-prompt.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dock-prompt.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dropdown-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dropdown-menu.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/favicon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/favicon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/file-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/file-icon.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/font.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/font.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/hover-card.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/hover-card.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/icon-button.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon-button.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/image-preview.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/image-preview.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/inline-input.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/inline-input.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/keybind.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/keybind.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/line-comment.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/line-comment.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/list.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/list.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/logo.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/logo.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/markdown.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/markdown.stories.tsx (line 12, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/message-nav.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-nav.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/message-part.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-part.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/popover.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/popover.stories.tsx (line 16, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/progress-circle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress-circle.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/progress.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/provider-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/provider-icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/radio-group.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/radio-group.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/resize-handle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/resize-handle.stories.tsx (line 17, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/select.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/select.stories.tsx (line 16, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/session-review.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-review.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/session-turn.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/spinner.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/spinner.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/sticky-accordion-header.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/sticky-accordion-header.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/switch.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/switch.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tabs.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tabs.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tag.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tag.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/text-field.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-field.stories.tsx (line 14, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/text-shimmer.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-shimmer.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/toast.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/toast.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tooltip.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tooltip.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/typewriter.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/typewriter.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
[14:25:49.109] [ERROR] Failed to build the preview
|
||||
[14:25:49.110] [ERROR] Error: [38;2;241;97;97mUnable to index files:
|
||||
- ./../ui/src/components/accordion.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/accordion.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/app-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/app-icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/avatar.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/avatar.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/basic-tool.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/basic-tool.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/checkbox.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/checkbox.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/code.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/code.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/collapsible.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/collapsible.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/context-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/context-menu.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dialog.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dialog.stories.tsx (line 10, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff-changes.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-changes.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff-ssr.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff-ssr.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/diff.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/diff.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dock-prompt.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dock-prompt.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/dropdown-menu.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/dropdown-menu.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/favicon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/favicon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/file-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/file-icon.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/font.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/font.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/hover-card.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/hover-card.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/icon-button.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon-button.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/image-preview.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/image-preview.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/inline-input.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/inline-input.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/keybind.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/keybind.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/line-comment.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/line-comment.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/list.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/list.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/logo.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/logo.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/markdown.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/markdown.stories.tsx (line 12, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/message-nav.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-nav.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/message-part.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/message-part.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/popover.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/popover.stories.tsx (line 16, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/progress-circle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress-circle.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/progress.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/progress.stories.tsx (line 15, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/provider-icon.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/provider-icon.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/radio-group.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/radio-group.stories.tsx (line 13, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/resize-handle.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/resize-handle.stories.tsx (line 17, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/select.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/select.stories.tsx (line 16, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/session-review.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-review.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/session-turn.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/spinner.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/spinner.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/sticky-accordion-header.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/sticky-accordion-header.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/switch.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/switch.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tabs.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tabs.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tag.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tag.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/text-field.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-field.stories.tsx (line 14, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/text-shimmer.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/text-shimmer.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/toast.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/toast.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/tooltip.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/tooltip.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export
|
||||
- ./../ui/src/components/typewriter.stories.tsx: CSF: default export must be an object /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/typewriter.stories.tsx (line 6, col 0)
|
||||
|
||||
More info: https://storybook.js.org/docs/writing-stories?ref=error#default-export[39m
|
||||
at _StoryIndexGenerator.getIndexAndStats (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:6085:15)
|
||||
at async _StoryIndexGenerator.getIndex (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:6074:13)
|
||||
at async getOptimizeDeps (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1862:15)
|
||||
at async createViteServer (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1888:19)
|
||||
at async Module.start (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/@storybook+builder-vite@10.2.10+a2a25316dbcddd7f/node_modules/@storybook/builder-vite/dist/index.js:1923:17)
|
||||
at async storybookDevServer (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:7241:83)
|
||||
at async buildOrThrow (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:4504:12)
|
||||
at async buildDevStandalone (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/core-server/index.js:7611:66)
|
||||
at async withTelemetry (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/_node-chunks/chunk-S3MWHNYJ.js:218:12)
|
||||
at async dev (file:///Users/davidhill/Documents/Local/opencode/node_modules/.bun/storybook@10.2.10+4edd68b244e756bb/node_modules/storybook/dist/bin/core.js:2734:3)
|
||||
[14:25:49.118] [WARN] Broken build, fix the error above.
|
||||
You may need to refresh the browser.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index dca128d3a790378c51a24a16d92585178343b278..da75f9d64acd2b607abd15079ce2d03b7a8d675a 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -696,7 +696,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
finishReason = { unified: "error", raw: void 0 };
|
||||
controller.enqueue({
|
||||
type: "error",
|
||||
- error: chunk.value.error.message
|
||||
+ error: chunk.value.error
|
||||
});
|
||||
return;
|
||||
}
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..eaffc446f80552ea0b26573b89daa4dfc7776e6e 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -683,7 +683,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
finishReason = { unified: "error", raw: void 0 };
|
||||
controller.enqueue({
|
||||
type: "error",
|
||||
- error: chunk.value.error.message
|
||||
+ error: chunk.value.error
|
||||
});
|
||||
return;
|
||||
}
|
||||
diff --git a/src/chat/openai-compatible-chat-language-model.ts b/src/chat/openai-compatible-chat-language-model.ts
|
||||
index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..643c3db68a6043e097edc1122e0eb53fd13495c5 100644
|
||||
--- a/src/chat/openai-compatible-chat-language-model.ts
|
||||
+++ b/src/chat/openai-compatible-chat-language-model.ts
|
||||
@@ -442,7 +442,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
|
||||
finishReason = { unified: 'error', raw: undefined };
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
- error: chunk.value.error.message,
|
||||
+ error: chunk.value.error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.18.13",
|
||||
"version": "1.18.14",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user