From 88f572cfce310680affceeef4d8bfd075f2e4c66 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 16:19:00 -0400 Subject: [PATCH 01/27] refactor(tui): migrate selection views to V2 theme (#38001) --- .../tui/src/component/dialog-session-list.tsx | 13 ++-- packages/tui/src/component/dialog-stash.tsx | 5 +- .../tui/src/component/prompt/autocomplete.tsx | 24 ++++-- packages/tui/src/ui/dialog-select.tsx | 78 ++++++++++++------- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 04783790e4..b1067fa859 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -20,7 +20,7 @@ export function DialogSessionList() { const dialog = useDialog() const route = useRoute() const data = useData() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const client = useClient() const local = useLocal() const toast = useToast() @@ -109,12 +109,13 @@ export function DialogSessionList() { value: session.id, category, footer, - bg: deleting ? theme.error : undefined, + bg: deleting ? themeV2.background.action.destructive.focused : undefined, + fg: deleting ? themeV2.text.action.destructive.focused : undefined, gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running") ? () => : slot === undefined ? undefined - : () => {slot}, + : () => {slot}, } } @@ -142,12 +143,14 @@ export function DialogSessionList() { }} emptyView={ - No sessions available + No sessions available } noMatchView={ - {searchState().message} + + {searchState().message} + } onMove={() => setToDelete(undefined)} diff --git a/packages/tui/src/component/dialog-stash.tsx b/packages/tui/src/component/dialog-stash.tsx index cefe315ee3..80aa75250c 100644 --- a/packages/tui/src/component/dialog-stash.tsx +++ b/packages/tui/src/component/dialog-stash.tsx @@ -29,7 +29,7 @@ function getStashPreview(input: string, maxLength: number = 50): string { export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { const dialog = useDialog() const stash = usePromptStash() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("elevated") const shortcuts = Keymap.useShortcuts() const [toDelete, setToDelete] = createSignal() @@ -45,7 +45,8 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) { title: isDeleting ? `Press ${shortcuts.get("stash.delete")} again to confirm` : getStashPreview(entry.prompt.text), - bg: isDeleting ? theme.error : undefined, + bg: isDeleting ? themeV2.background.action.destructive.focused : undefined, + fg: isDeleting ? themeV2.text.action.destructive.focused : undefined, value: index, description: getRelativeTime(entry.timestamp), footer: lineCount > 1 ? `~${lineCount} lines` : undefined, diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 5dd80ee396..921d096819 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -12,7 +12,7 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useConfig } from "../../config" import { useLocation } from "../../context/location" -import { useTheme, selectedForeground } from "../../context/theme" +import { useTheme } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "../../util/locale" @@ -57,7 +57,7 @@ export function Autocomplete(props: { const data = useData() const keymap = Keymap.use() const keymapCommands = Keymap.useCommands() - const { theme } = useTheme() + const { themeV2 } = useTheme().contextual("overlay") const dimensions = useTerminalDimensions() const frecency = useFrecency() const config = useConfig().data @@ -698,11 +698,11 @@ export function Autocomplete(props: { width={position().width} zIndex={100} {...SplitBorder} - borderColor={theme.border} + borderColor={themeV2.border.default} > (scroll = r)} - backgroundColor={theme.backgroundMenu} + backgroundColor={themeV2.background.default} height={height()} scrollbarOptions={{ visible: false }} scrollAcceleration={scrollAcceleration()} @@ -711,7 +711,9 @@ export function Autocomplete(props: { each={options()} fallback={ - {emptyMessage()} + + {emptyMessage()} + } > @@ -719,7 +721,7 @@ export function Autocomplete(props: { { setStore("input", "mouse") @@ -734,11 +736,17 @@ export function Autocomplete(props: { }} onMouseUp={() => select()} > - + {option().display} - + {" " + option().description?.trimStart()} diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index 257de75b17..88248c92d3 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -1,6 +1,6 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { Keymap, type KeymapCommand } from "../context/keymap" -import { useTheme, selectedForeground } from "../context/theme" +import { useTheme } from "../context/theme" import { entries, filter, flatMap, groupBy, pipe } from "remeda" import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" @@ -74,6 +74,7 @@ export interface DialogSelectOption { categoryView?: JSX.Element disabled?: boolean bg?: RGBA + fg?: RGBA gutter?: () => JSX.Element margin?: JSX.Element onSelect?: (ctx: DialogContext) => void @@ -91,7 +92,7 @@ export function DialogSelect(props: DialogSelectProps) { type VisibleAction = (Action & { label: string }) | FooterHint const dialog = useDialog() - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const config = useConfig().data const scrollAcceleration = createMemo(() => getScrollAcceleration(config)) @@ -517,29 +518,44 @@ export function DialogSelect(props: DialogSelectProps) { if (!isActionItem(action.item)) return ( - + {action.item.title}{" "} - {action.item.label} + {action.item.label} ) const item = action.item const active = createMemo(() => isActionFocused(item)) const disabled = createMemo(() => isActionDisabled(item)) - const fg = selectedForeground(theme) return ( trigger(item)} > {item.title} - {item.label} + + {" " + item.label} + ) } @@ -549,11 +565,11 @@ export function DialogSelect(props: DialogSelectProps) { {props.titleView ?? ( - + {props.title} )} - dialog.clear()}> + dialog.clear()}> esc @@ -567,9 +583,9 @@ export function DialogSelect(props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} + focusedBackgroundColor={themeV2.background.formfield.focused} + cursorColor={themeV2.text.formfield.focused} + focusedTextColor={themeV2.text.formfield.focused} ref={(r) => { input = r input.traits = { status: "FILTER" } @@ -580,7 +596,7 @@ export function DialogSelect(props: DialogSelectProps) { }, 1) }} placeholder={props.placeholder ?? "Search"} - placeholderColor={theme.textMuted} + placeholderColor={themeV2.text.subdued} /> @@ -594,14 +610,14 @@ export function DialogSelect(props: DialogSelectProps) { fallback={ props.emptyView ?? ( - No items available + No items available ) } > {props.noMatchView ?? ( - No results found + No results found )} @@ -623,7 +639,10 @@ export function DialogSelect(props: DialogSelectProps) { + {category} } @@ -672,8 +691,8 @@ export function DialogSelect(props: DialogSelectProps) { backgroundColor={ active() ? actionFocused() - ? theme.backgroundElement - : (option.bg ?? theme.primary) + ? themeV2.background.surface.overlay + : (option.bg ?? themeV2.background.action.primary.focused) : RGBA.fromInts(0, 0, 0, 0) } > @@ -692,6 +711,7 @@ export function DialogSelect(props: DialogSelectProps) { active={active()} current={current()} muted={actionFocused()} + activeColor={option.fg} gutter={option.gutter} /> @@ -699,7 +719,7 @@ export function DialogSelect(props: DialogSelectProps) { {(detail) => ( {option.detailsWrap @@ -745,15 +765,15 @@ function Option(props: { titleWidth?: number truncateTitle?: boolean | "left" gutter?: () => JSX.Element + activeColor?: RGBA onMouseOver?: () => void }) { - const { theme } = useTheme() - const fg = selectedForeground(theme) + const { themeV2 } = useTheme().contextual("elevated") const text = createMemo(() => { - if (props.active && !props.muted) return fg - if (props.muted && (props.active || props.current)) return theme.textMuted - if (props.current) return theme.primary - return theme.text + if (props.active && !props.muted) return props.activeColor ?? themeV2.text.action.primary.focused + if (props.muted && (props.active || props.current)) return themeV2.text.subdued + if (props.current) return themeV2.text.formfield.selected + return themeV2.text.default }) return ( @@ -783,12 +803,14 @@ function Option(props: { ? Locale.truncateLeft(props.title, props.titleWidth ?? 61) : Locale.truncate(props.title, props.titleWidth ?? 61))} - {props.description} + + {" " + props.description} + - {props.footer} + {props.footer} From 5913c1db0bf7a5e7475b946f7681d7c3a77bcf2f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:33:14 -0500 Subject: [PATCH 02/27] chore: merge dev into v2 (#38377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] Co-authored-by: Frank Co-authored-by: Aiden Cline Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Dax Raad Co-authored-by: Dax Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Nabs Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Brendan Allan Co-authored-by: Victor Navarro Co-authored-by: Vladimir Glafirov Co-authored-by: AidenGeunGeun Co-authored-by: Mark Co-authored-by: Aiden Cline Co-authored-by: opencode Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com> Co-authored-by: Dustin Deus Co-authored-by: Jack Co-authored-by: Sebastian Co-authored-by: Jérôme Benoit Co-authored-by: Test User Co-authored-by: Simon Klee Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Qiping Li Co-authored-by: liqiping Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Matthias Reso <13337103+mreso@users.noreply.github.com> Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com> Co-authored-by: Daniel Polito Co-authored-by: opencode --- .../session-timeline-lifecycle-state.spec.ts | 17 +++++++++++++++++ .../src/pages/layout/project-avatar-state.ts | 12 +++++++----- packages/console/app/src/i18n/ar.ts | 8 ++++---- packages/console/app/src/i18n/br.ts | 8 ++++---- packages/console/app/src/i18n/da.ts | 8 ++++---- packages/console/app/src/i18n/de.ts | 8 ++++---- packages/console/app/src/i18n/en.ts | 8 ++++---- packages/console/app/src/i18n/es.ts | 8 ++++---- packages/console/app/src/i18n/fr.ts | 8 ++++---- packages/console/app/src/i18n/it.ts | 8 ++++---- packages/console/app/src/i18n/ja.ts | 8 ++++---- packages/console/app/src/i18n/ko.ts | 8 ++++---- packages/console/app/src/i18n/no.ts | 8 ++++---- packages/console/app/src/i18n/pl.ts | 8 ++++---- packages/console/app/src/i18n/ru.ts | 8 ++++---- packages/console/app/src/i18n/th.ts | 8 ++++---- packages/console/app/src/i18n/tr.ts | 8 ++++---- packages/console/app/src/i18n/uk.ts | 8 ++++---- packages/console/app/src/i18n/zh.ts | 8 ++++---- packages/console/app/src/i18n/zht.ts | 8 ++++---- packages/console/app/src/routes/go/index.tsx | 2 ++ .../routes/workspace/[id]/go/lite-section.tsx | 1 + .../session-ui/src/components/basic-tool.tsx | 5 +++-- .../session-ui/src/components/message-part.tsx | 3 ++- packages/web/src/content/docs/ar/go.mdx | 7 ++++++- packages/web/src/content/docs/bs/go.mdx | 7 ++++++- packages/web/src/content/docs/da/go.mdx | 7 ++++++- packages/web/src/content/docs/de/go.mdx | 7 ++++++- packages/web/src/content/docs/es/go.mdx | 7 ++++++- packages/web/src/content/docs/fr/go.mdx | 7 ++++++- packages/web/src/content/docs/go.mdx | 7 ++++++- packages/web/src/content/docs/it/go.mdx | 7 ++++++- packages/web/src/content/docs/ja/go.mdx | 7 ++++++- packages/web/src/content/docs/ko/go.mdx | 7 ++++++- packages/web/src/content/docs/nb/go.mdx | 7 ++++++- packages/web/src/content/docs/pl/go.mdx | 7 ++++++- packages/web/src/content/docs/pt-br/go.mdx | 7 ++++++- packages/web/src/content/docs/ru/go.mdx | 7 ++++++- packages/web/src/content/docs/th/go.mdx | 7 ++++++- packages/web/src/content/docs/tr/go.mdx | 7 ++++++- packages/web/src/content/docs/zh-cn/go.mdx | 7 ++++++- packages/web/src/content/docs/zh-tw/go.mdx | 7 ++++++- 42 files changed, 212 insertions(+), 98 deletions(-) diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index 3e2b171bca..b303071c87 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -32,6 +32,23 @@ for (const expanded of [false, true]) { }) } +test("shows and expands a running shell command without shimmering it", async ({ page }) => { + const id = "prt_shell_running_command" + const command = "sleep 10 && echo done" + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })], + settings: { shellToolPartsExpanded: false }, + }) + + const tool = page.locator(`[data-timeline-part-id="${id}"]`) + await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") + await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command) + await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0) + await tool.locator('[data-slot="collapsible-trigger"]').click() + await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running") +}) + test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { const reasoningID = "prt_reasoning_hidden" const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index 8e5dc38d67..236f6bd405 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -13,7 +13,6 @@ export function useSessionTabAvatarState( const global = useGlobal() const notification = useNotification() const permission = usePermission() - const permissionState = createMemo(() => permission.ensureServerState(server())) const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const sync = createMemo(() => { const conn = connection() @@ -22,9 +21,10 @@ export function useSessionTabAvatarState( const hasPermissions = createMemo(() => { const serverSync = sync() if (!serverSync) return false + const permissionState = permission.ensureServerState(server()) const [store] = serverSync.child(directory(), { bootstrap: false }) return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { - return !permissionState().autoResponds(item, directory()) + return !permissionState.autoResponds(item, directory()) }) }) const hasQuestions = createMemo(() => { @@ -34,9 +34,11 @@ export function useSessionTabAvatarState( return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) - const unread = createMemo( - () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, - ) + const notificationState = createMemo(() => { + if (!connection()) return + return notification.ensureServerState(server()) + }) + const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 991f7fb2d3..082e211e0b 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.banner.text": "يحصل Kimi K3 على حدود استخدام مضاعفة لفترة محدودة", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": - "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash", + "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash وHy3 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 9bef420e85..69979b0a44 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.banner.text": "Kimi K3 tem limites de uso 2x maiores por tempo limitado", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -307,7 +307,7 @@ export const dict = { "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": - "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index e7c8c8feaf..43d8d51abb 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.banner.text": "Kimi K3 får fordoblet brugsgrænse i en begrænset periode", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": - "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 465b24568f..99446d92b0 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.banner.text": "Kimi K3 erhält für begrenzte Zeit 2x Nutzungslimits", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -306,7 +306,7 @@ export const dict = { "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": - "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash", + "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -332,7 +332,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -356,7 +356,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash und Hy3 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 7d0531e6f0..690658c657 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.banner.text": "Kimi K3 gets 2× usage limits for a limited time", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": - "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash", + "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, and Hy3 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} is not supported", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index 08d30aef68..bb1a44138f 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -259,7 +259,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.banner.text": "Kimi K3 tiene límites de uso 2x mayores por tiempo limitado", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -308,7 +308,7 @@ export const dict = { "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": - "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash", + "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash y Hy3 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index a0b8444195..4d0ff288d6 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -260,7 +260,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.banner.text": "Kimi K3 bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -308,7 +308,7 @@ export const dict = { "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": - "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash", + "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -334,7 +334,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -357,7 +357,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash et Hy3 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index a5e37dfc60..effeb1fdb4 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.banner.text": "Kimi K3 offre limiti di utilizzo 2x superiori per un periodo limitato", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": - "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash e Hy3 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index aca480b719..6dfd750c6a 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.banner.text": "Kimi K3の利用上限が期間限定で2倍に", "go.meta.description": - "Goは最初の月$5、その後$10/月で、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3に対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": - "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", + "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3を含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGrok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGrok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash、Hy3が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index f1e2235d7e..a24e988d71 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.banner.text": "Kimi K3 사용 한도가 한시적으로 2배 확대됩니다", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -301,7 +301,7 @@ export const dict = { "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": - "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -348,7 +348,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash, Hy3를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index bec0e0ce5e..b5ceff412c 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -256,7 +256,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.banner.text": "Kimi K3 får 2x bruksgrense i en begrenset periode", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": - "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -354,7 +354,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash og Hy3 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 8be53855f3..3199606a8b 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -257,7 +257,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.banner.text": "Kimi K3 oferuje 2x wyższe limity użycia przez ograniczony czas", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": - "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash", + "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash i Hy3 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index abe56bbb03..821ed70e98 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -260,7 +260,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.banner.text": "Kimi K3 получает 2x лимиты использования на ограниченное время", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -309,7 +309,7 @@ export const dict = { "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": - "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash", + "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -335,7 +335,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -359,7 +359,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash и Hy3 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index a6069a1bed..2a68e94c0a 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.banner.text": "Kimi K3 เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": - "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3 อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -350,7 +350,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash และ Hy3 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 7d8bc49f50..9bdcfeaeb4 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -258,7 +258,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.banner.text": "Kimi K3 sınırlı bir süre için 2x kullanım limiti sunuyor", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -307,7 +307,7 @@ export const dict = { "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": - "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash ve Hy3 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", diff --git a/packages/console/app/src/i18n/uk.ts b/packages/console/app/src/i18n/uk.ts index ee2405b65f..1dbb0be8af 100644 --- a/packages/console/app/src/i18n/uk.ts +++ b/packages/console/app/src/i18n/uk.ts @@ -257,7 +257,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогі моделі кодування для всіх", "go.banner.text": "Kimi K3 отримує 2x ліміти використання протягом обмеженого часу", "go.meta.description": - "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими 5-годинними лімітами запитів для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими 5-годинними лімітами запитів для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3.", "go.hero.title": "Недорогі моделі кодування для всіх", "go.hero.body": "Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Щедрі ліміти та надійний доступ", "go.problem.item3": "Створено для якомога більшої кількості програмістів", "go.problem.item4": - "Включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash", + "Включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3", "go.how.title": "Як працює Go", "go.how.body": "Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.", "go.faq.q3": "Чи Go те саме, що Zen?", "go.faq.a3": - "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", + "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3.", "go.faq.q4": "Скільки коштує Go?", "go.faq.a4.p1.beforePricing": "Go коштує", "go.faq.a4.p1.pricingLink": "$5 за перший місяць", @@ -354,7 +354,7 @@ export const dict = { "go.faq.q9": "Яка різниця між безкоштовними моделями та Go?", "go.faq.a9": - "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.", + "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash та Hy3 із вищими лімітами.", "zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.", "zen.api.error.modelNotSupported": "Модель {{model}} не підтримується", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index cc8b6326f7..47e5ee8361 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -246,7 +246,7 @@ export const dict = { "go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.banner.text": "Kimi K3 限时享受 2 倍使用额度", "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", + "Go 首月 $5,之后 $10/月,提供对 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -293,7 +293,7 @@ export const dict = { "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": - "包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash", + "包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -315,7 +315,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -337,7 +337,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 8612bb8dac..77c0e8e918 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -246,7 +246,7 @@ export const dict = { "go.title": "OpenCode Go | 低成本全民編碼模型", "go.banner.text": "Kimi K3 限時享有 2 倍使用額度", "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", + "Go 首月 $5,之後 $10/月,提供對 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -293,7 +293,7 @@ export const dict = { "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": - "包含 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash", + "包含 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 與 Hy3", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -315,7 +315,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 和 Hy3 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -337,7 +337,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flash 與 Hy3,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 9dedcdcbb4..2742f49ef4 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -38,6 +38,7 @@ const models = [ "MiniMax M2.7", "DeepSeek V4 Pro", "DeepSeek V4 Flash", + "Hy3", ] function LimitsGraph(props: { href: string }) { @@ -72,6 +73,7 @@ function LimitsGraph(props: { href: string }) { { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "240ms" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" }, { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" }, + { id: "hy3", name: "Hy3", req: 4300, d: "320ms" }, { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" }, ] diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 6704f92608..88141656f3 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -321,6 +321,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
  • DeepSeek V4 Flash
  • MiMo-V2.5
  • MiMo-V2.5-Pro
  • +
  • Hy3
  • {i18n.t("workspace.lite.promo.footer")}

    diff --git a/packages/session-ui/src/components/basic-tool.tsx b/packages/session-ui/src/components/basic-tool.tsx index a3ce5c13b5..2b73db24ed 100644 --- a/packages/session-ui/src/components/basic-tool.tsx +++ b/packages/session-ui/src/components/basic-tool.tsx @@ -32,6 +32,7 @@ export interface BasicToolProps { open?: boolean onOpenChange?: (open: boolean) => void forceOpen?: boolean + allowOpenWhilePending?: boolean defer?: boolean locked?: boolean animated?: boolean @@ -176,7 +177,7 @@ export function BasicTool(props: BasicToolProps) { }) const handleOpenChange = (value: boolean) => { - if (pending()) return + if (pending() && !props.allowOpenWhilePending) return if (props.locked && !value) return setOpen(value) } @@ -247,7 +248,7 @@ export function BasicTool(props: BasicToolProps) {
    - + diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index 9f71309165..77d9a8c56a 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -2123,13 +2123,14 @@ ToolRegistry.register({ (
    - +
    diff --git a/packages/web/src/content/docs/ar/go.mdx b/packages/web/src/content/docs/ar/go.mdx index 0597f3adf1..5698d42724 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -64,6 +64,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها. @@ -87,7 +88,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | تستند التقديرات إلى أنماط الطلبات المرصودة: @@ -112,6 +114,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - Qwen3.7 Max — ‏420 input، و66,000 cached، و200 output tokens لكل طلب - Qwen3.7 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب - Qwen3.6 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب +- Hy3 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5-Pro — ‏790 input، و86,000 cached، و305 output tokens لكل طلب @@ -137,6 +140,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | يمكنك تتبّع استخدامك الحالي في **console**. @@ -188,6 +192,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/`. على سبيل المثال، بالنسبة إلى Kimi K3، ستستخدم `opencode-go/kimi-k3` في إعداداتك. diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index 26b5575a01..c9ea860d35 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -74,6 +74,7 @@ Trenutna lista modela uključuje: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Lista modela se može mijenjati dok testiramo i dodajemo nove. @@ -97,7 +98,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Procjene se zasnivaju na zapaženim obrascima zahtjeva: @@ -122,6 +124,7 @@ Procjene se zasnivaju na zapaženim obrascima zahtjeva: - Qwen3.7 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu - Qwen3.7 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu - Qwen3.6 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu +- Hy3 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5-Pro — 790 ulaznih, 86,000 keširanih, 305 izlaznih tokena po zahtjevu @@ -147,6 +150,7 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Svoju trenutnu potrošnju možete pratiti u **konzoli**. @@ -200,6 +204,7 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji koristi format `opencode-go/`. Na primjer, za Kimi K3, koristili biste diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index 280891deee..4256f4afad 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -74,6 +74,7 @@ Den nuværende liste over modeller inkluderer: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye. @@ -97,7 +98,7 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Estimaterne er baseret på observerede anmodningsmønstre: @@ -122,6 +124,7 @@ Estimaterne er baseret på observerede anmodningsmønstre: - Qwen3.7 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning - Qwen3.7 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning - Qwen3.6 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning +- Hy3 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5-Pro — 790 input, 86.000 cachelagrede, 305 output-tokens pr. anmodning @@ -147,6 +150,7 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore dit nuværende forbrug i **konsollen**. @@ -200,6 +204,7 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Dit [model id](/docs/config/#models) i din OpenCode config bruger formatet `opencode-go/`. For eksempel for Kimi K3, vil du diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index 2bb3f0b742..3de5f5f786 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -66,6 +66,7 @@ Die aktuelle Liste der Modelle umfasst: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufügen. @@ -89,7 +90,7 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -100,6 +101,7 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Die Schätzungen basieren auf beobachteten Anfragemustern: @@ -114,6 +116,7 @@ Die Schätzungen basieren auf beobachteten Anfragemustern: - Qwen3.7 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage - Qwen3.7 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage - Qwen3.6 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage +- Hy3 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5-Pro — 790 Input-, 86.000 Cached-, 305 Output-Tokens pro Anfrage @@ -139,6 +142,7 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kannst deine aktuelle Nutzung in der **Console** verfolgen. @@ -190,6 +194,7 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/`. Für Kimi K3 würdest du beispielsweise `opencode-go/kimi-k3` in deiner Config verwenden. diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index 4aa1ca46e3..de02803362 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -74,6 +74,7 @@ La lista actual de modelos incluye: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos. @@ -97,7 +98,7 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Las estimaciones se basan en los patrones de peticiones observados: @@ -122,6 +124,7 @@ Las estimaciones se basan en los patrones de peticiones observados: - Qwen3.7 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición - Qwen3.7 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición - Qwen3.6 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición +- Hy3 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5-Pro — 790 tokens de entrada, 86,000 en caché, 305 tokens de salida por petición @@ -147,6 +150,7 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puedes realizar un seguimiento de tu uso actual en la **consola**. @@ -200,6 +204,7 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode usa el formato `opencode-go/`. Por ejemplo, para Kimi K3, usarías diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index 95849616b0..fe1139d389 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -64,6 +64,7 @@ La liste actuelle des modèles comprend : - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** La liste des modèles peut changer au fur et à mesure que nous en testons et en ajoutons de nouveaux. @@ -87,7 +88,7 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Les estimations sont basées sur les schémas de requêtes observés : @@ -112,6 +114,7 @@ Les estimations sont basées sur les schémas de requêtes observés : - Qwen3.7 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête - Qwen3.7 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête - Qwen3.6 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête +- Hy3 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5-Pro — 790 tokens en entrée, 86,000 en cache, 305 tokens en sortie par requête @@ -137,6 +140,7 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Vous pouvez suivre votre utilisation actuelle dans la **console**. @@ -188,6 +192,7 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d' | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/`. Par exemple, pour Kimi K3, vous utiliseriez `opencode-go/kimi-k3` dans votre configuration. diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx index 8c46464086..bbd4225b9f 100644 --- a/packages/web/src/content/docs/go.mdx +++ b/packages/web/src/content/docs/go.mdx @@ -74,6 +74,7 @@ The current list of models includes: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** The list of models may change as we test and add new ones. @@ -97,7 +98,7 @@ The table below provides an estimated request count based on typical Go usage pa | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ The table below provides an estimated request count based on typical Go usage pa | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | The estimates are based on observed request patterns: @@ -124,6 +126,7 @@ The estimates are based on observed request patterns: - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens per request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request +- Hy3 — 830 input, 71,500 cached, 295 output tokens per request The estimates are also based on the following prices per 1M tokens and the monthly usage included with each model: @@ -147,6 +150,7 @@ The estimates are also based on the following prices per 1M tokens and the month | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | You can track your current usage in the **console**. @@ -200,6 +204,7 @@ You can also access Go models through the following API endpoints. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | The [model id](/docs/config/#models) in your OpenCode config uses the format `opencode-go/`. For example, for Kimi K3, you would diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index 65369e1e86..26c459f456 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -72,6 +72,7 @@ L'elenco attuale dei modelli include: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di nuovi. @@ -95,7 +96,7 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -106,6 +107,7 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Le stime si basano sui pattern di richieste osservati: @@ -120,6 +122,7 @@ Le stime si basano sui pattern di richieste osservati: - Qwen3.7 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta - Qwen3.7 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta - Qwen3.6 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta +- Hy3 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5-Pro — 790 di input, 86.000 in cache, 305 token di output per richiesta @@ -145,6 +148,7 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puoi monitorare il tuo utilizzo attuale nella **console**. @@ -198,6 +202,7 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Il [model id](/docs/config/#models) nella tua OpenCode config utilizza il formato `opencode-go/`. Ad esempio, per Kimi K3, useresti diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index 8bb9139e83..f2e95659a6 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -64,6 +64,7 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 新しいモデルをテストして追加するにつれて、モデルのリストは変更される場合があります。 @@ -87,7 +88,7 @@ OpenCode Goには以下の制限が含まれています: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Goには以下の制限が含まれています: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 推定値は、観測されたリクエストパターンに基づいています: @@ -112,6 +114,7 @@ OpenCode Goには以下の制限が含まれています: - Qwen3.7 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン - Qwen3.7 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン - Qwen3.6 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン +- Hy3 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5-Pro — リクエストあたり 入力 790トークン、キャッシュ 86,000トークン、出力 305トークン @@ -137,6 +140,7 @@ OpenCode Goには以下の制限が含まれています: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 現在の利用状況は**コンソール**で追跡できます。 @@ -188,6 +192,7 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode設定の[model id](/docs/config/#models)は、`opencode-go/`という形式を使用します。たとえば、Kimi K3の場合は、設定で`opencode-go/kimi-k3`を使用します。 diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index 02f88d2828..d03198ce4f 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -64,6 +64,7 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 새로운 모델을 테스트하고 추가함에 따라 이 목록은 변경될 수 있습니다. @@ -87,7 +88,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 이 예상치는 관찰된 요청 패턴을 기준으로 합니다. @@ -112,6 +114,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. - Qwen3.7 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200 - Qwen3.7 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 - Qwen3.6 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 +- Hy3 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5-Pro — 요청당 입력 790, 캐시 86,000, 출력 토큰 305 @@ -137,6 +140,7 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 현재 사용량은 **console**에서 확인할 수 있습니다. @@ -188,6 +192,7 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공 | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode config의 [model id](/docs/config/#models)는 `opencode-go/` 형식을 사용합니다. 예를 들어 Kimi K3의 경우 config에서 `opencode-go/kimi-k3`를 사용하면 됩니다. diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index 60be1cc7bb..c63d007a80 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -74,6 +74,7 @@ Den nåværende listen over modeller inkluderer: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Listen over modeller kan endres etter hvert som vi tester og legger til nye. @@ -97,7 +98,7 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Estimatene er basert på observerte forespørselsmønstre: @@ -122,6 +124,7 @@ Estimatene er basert på observerte forespørselsmønstre: - Qwen3.7 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel - Qwen3.7 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel - Qwen3.6 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel +- Hy3 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5-Pro — 790 input, 86 000 bufret, 305 output-tokens per forespørsel @@ -147,6 +150,7 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore din nåværende bruk i **konsollen**. @@ -200,6 +204,7 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Modell-ID-en](/docs/config/#models) i din OpenCode-konfigurasjon bruker formatet `opencode-go/`. For eksempel, for Kimi K3, vil du diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index 4a867bf0e7..3f542a924e 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -68,6 +68,7 @@ Obecna lista modeli obejmuje: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Lista modeli może ulec zmianie w miarę testowania i dodawania nowych. @@ -91,7 +92,7 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -102,6 +103,7 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Szacunki te opierają się na zaobserwowanych wzorcach żądań: @@ -116,6 +118,7 @@ Szacunki te opierają się na zaobserwowanych wzorcach żądań: - Qwen3.7 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie - Qwen3.7 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie - Qwen3.6 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie +- Hy3 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5-Pro — 790 tokenów wejściowych, 86 000 w pamięci podręcznej, 305 tokenów wyjściowych na żądanie @@ -141,6 +144,7 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Możesz śledzić swoje bieżące zużycie w **konsoli**. @@ -192,6 +196,7 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode używa formatu `opencode-go/`. Na przykład dla Kimi K3 należy użyć diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index 96c1addfcc..def6efd471 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -74,6 +74,7 @@ A lista atual de modelos inclui: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** A lista de modelos pode mudar conforme testamos e adicionamos novos. @@ -97,7 +98,7 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | As estimativas se baseiam nos padrões de requisições observados: @@ -122,6 +124,7 @@ As estimativas se baseiam nos padrões de requisições observados: - Qwen3.7 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição - Qwen3.7 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição - Qwen3.6 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição +- Hy3 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5-Pro — 790 tokens de entrada, 86.000 em cache, 305 tokens de saída por requisição @@ -147,6 +150,7 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Você pode acompanhar o seu uso atual no **console**. @@ -200,6 +204,7 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | O [ID do modelo](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode-go/`. Por exemplo, para o Kimi K3, você usaria diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index 62305fbdb6..0bbd43369b 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -74,6 +74,7 @@ OpenCode Go работает так же, как и любой другой пр - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Список моделей может меняться по мере того, как мы тестируем и добавляем новые. @@ -97,7 +98,7 @@ OpenCode Go включает следующие лимиты: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -108,6 +109,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Эти оценки основаны на наблюдаемых показателях запросов: @@ -122,6 +124,7 @@ OpenCode Go включает следующие лимиты: - Qwen3.7 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос - Qwen3.7 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос - Qwen3.6 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос +- Hy3 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5-Pro — 790 входных, 86,000 кешированных, 305 выходных токенов на запрос @@ -147,6 +150,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Вы можете отслеживать текущее использование в **консоли**. @@ -200,6 +204,7 @@ OpenCode Go включает следующие лимиты: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID модели](/docs/config/#models) в вашем конфиге OpenCode использует формат `opencode-go/`. Например, для Kimi K3 вам нужно diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index 36036426da..48b0c05bf1 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -64,6 +64,7 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** รายชื่อโมเดลอาจมีการเปลี่ยนแปลงเมื่อเราทำการทดสอบและเพิ่มโมเดลใหม่ๆ @@ -87,7 +88,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | การประมาณการนี้อ้างอิงจากรูปแบบการใช้งาน request ที่สังเกตพบ: @@ -112,6 +114,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens ต่อ request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request +- Hy3 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 output tokens ต่อ request @@ -137,6 +140,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | คุณสามารถติดตามการใช้งานปัจจุบันของคุณได้ใน **console** @@ -188,6 +192,7 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) ใน OpenCode config ของคุณจะใช้รูปแบบ `opencode-go/` ตัวอย่างเช่น สำหรับ Kimi K3 คุณจะใช้ `opencode-go/kimi-k3` ใน config ของคุณ diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index e24ead2959..0611ae31b7 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -64,6 +64,7 @@ Mevcut model listesi şunları içerir: - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** Test edip yenilerini ekledikçe model listesi değişebilir. @@ -87,7 +88,7 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | Tahminler, gözlemlenen istek modellerine dayanır: @@ -112,6 +114,7 @@ Tahminler, gözlemlenen istek modellerine dayanır: - Qwen3.7 Max — İstek başına 420 girdi, 66.000 önbelleğe alınmış, 200 çıktı token'ı - Qwen3.7 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı - Qwen3.6 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı +- Hy3 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5-Pro — İstek başına 790 girdi, 86.000 önbelleğe alınmış, 305 çıktı token'ı @@ -137,6 +140,7 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Mevcut kullanımınızı **konsoldan** takip edebilirsiniz. @@ -188,6 +192,7 @@ Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsi | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode yapılandırmanızdaki [model id](/docs/config/#models) formatı `opencode-go/` şeklindedir. Örneğin, Kimi K3 için yapılandırmanızda `opencode-go/kimi-k3` kullanmalısınız. diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index 604b910159..873b3a0224 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -64,6 +64,7 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 随着我们进行测试和添加新模型,该列表可能会发生变化。 @@ -87,7 +88,7 @@ OpenCode Go 包含以下限制: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 预估值基于观察到的请求模式: @@ -114,6 +116,7 @@ OpenCode Go 包含以下限制: - Qwen3.7 Max — 每次请求 420 个输入 token,66,000 个缓存 token,200 个输出 token - Qwen3.7 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token - Qwen3.6 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token +- Hy3 — 每次请求 830 个输入 token,71,500 个缓存 token,295 个输出 token 预估值还基于以下每 1M tokens 的价格以及每个模型包含的每月使用额度: @@ -137,6 +140,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 你可以在 **控制台** 中跟踪你当前的使用情况。 @@ -188,6 +192,7 @@ OpenCode Go 包含以下限制: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 你的 OpenCode 配置中的 [模型 ID](/docs/config/#models) 使用 `opencode-go/` 格式。例如,对于 Kimi K3,你将在配置中使用 `opencode-go/kimi-k3`。 diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index 6434504a3e..691abaa2a9 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -64,6 +64,7 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** +- **Hy3** 隨著我們測試並加入新模型,模型清單可能會有所變動。 @@ -87,7 +88,7 @@ OpenCode Go 包含以下限制: | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | +| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | @@ -98,6 +99,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | +| Hy3 | 4,300 | 10,750 | 21,500 | 這些預估值是基於觀察到的請求模式: @@ -112,6 +114,7 @@ OpenCode Go 包含以下限制: - Qwen3.7 Max — 每次請求 420 個輸入 token、66,000 個快取 token、200 個輸出 token - Qwen3.7 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token - Qwen3.6 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token +- Hy3 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5-Pro — 每次請求 790 個輸入 token、86,000 個快取 token、305 個輸出 token @@ -137,6 +140,7 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | +| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 您可以在 **console** 中追蹤您目前的使用量。 @@ -188,6 +192,7 @@ OpenCode Go 包含以下限制: | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 您的 OpenCode 設定中的 [model id](/docs/config/#models) 使用 `opencode-go/` 格式。例如,Kimi K3 在設定中應使用 `opencode-go/kimi-k3`。 From 4e067a20142c8081574f1143eda34e68f1a3770b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:19:06 -0500 Subject: [PATCH 03/27] test(core): remove duplicate patch integration tests (#38389) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/core/test/tool-patch.test.ts | 163 -------------------------- 1 file changed, 163 deletions(-) diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index f43b49342c..86ae95e183 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -387,17 +387,6 @@ describe("PatchTool", () => { ), ) - it.live("updates an empty file", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "empty.txt") - yield* Effect.promise(() => fs.writeFile(target, "")) - yield* executeTool(registry, call("*** Begin Patch\n*** Update File: empty.txt\n@@\n+First line\n*** End Patch")) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("First line\n") - }), - ), - ) - it.live("rejects deleting a directory", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -410,40 +399,6 @@ describe("PatchTool", () => { ), ) - it.live("supports an end-of-file anchor", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "tail.txt") - yield* Effect.promise(() => fs.writeFile(target, "first\nsecond")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n") - }), - ), - ) - - it.live("applies an end-of-file chunk to the final duplicate", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "duplicates.txt") - yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe( - "marker\nend\nmiddle\nmarker changed\nend\n", - ) - }), - ), - ) - it.live("rejects a missing second chunk context", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -513,20 +468,6 @@ describe("PatchTool", () => { ), ) - it.live("applies multiple hunks to one file", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "multi.txt") - yield* Effect.promise(() => fs.writeFile(target, "a\nb\nc\nd\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: multi.txt\n@@\n-b\n+B\n@@\n-d\n+D\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("a\nB\nc\nD\n") - }), - ), - ) - it.live("applies successive update operations to one file", () => withTempTool((directory, registry) => Effect.gen(function* () { @@ -566,110 +507,6 @@ describe("PatchTool", () => { ), ) - it.live("appends a trailing newline on update", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "no-newline.txt") - yield* Effect.promise(() => fs.writeFile(target, "no newline at end")) - yield* executeTool( - registry, - call( - "*** Begin Patch\n*** Update File: no-newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch", - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first line\nsecond line\n") - }), - ), - ) - - it.live("disambiguates change context with an @@ header", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "context.txt") - yield* Effect.promise(() => fs.writeFile(target, "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: context.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe( - "fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n", - ) - }), - ), - ) - - it.live("parses a heredoc-wrapped patch", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - yield* executeTool( - registry, - call("cat <<'EOF'\n*** Begin Patch\n*** Add File: heredoc.txt\n+with cat\n*** End Patch\nEOF"), - ) - expect(yield* Effect.promise(() => fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe( - "with cat\n", - ) - }), - ), - ) - - it.live("parses a heredoc-wrapped patch without cat", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - yield* executeTool( - registry, - call("< fs.readFile(path.join(directory, "heredoc.txt"), "utf8"))).toBe( - "without cat\n", - ) - }), - ), - ) - - it.live("matches with trailing whitespace differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "trailing.txt") - yield* Effect.promise(() => fs.writeFile(target, "line1 \nline2\nline3 \n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: trailing.txt\n@@\n-line2\n+changed\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("line1 \nchanged\nline3 \n") - }), - ), - ) - - it.live("matches with leading whitespace differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "leading.txt") - yield* Effect.promise(() => fs.writeFile(target, " line1\nline2\n line3\n")) - yield* executeTool( - registry, - call("*** Begin Patch\n*** Update File: leading.txt\n@@\n-line2\n+changed\n*** End Patch"), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(" line1\nchanged\n line3\n") - }), - ), - ) - - it.live("matches with Unicode punctuation differences", () => - withTempTool((directory, registry) => - Effect.gen(function* () { - const target = path.join(directory, "unicode.txt") - yield* Effect.promise(() => fs.writeFile(target, "He said “hello”\nsome—dash\nend\n")) - yield* executeTool( - registry, - call( - '*** Begin Patch\n*** Update File: unicode.txt\n@@\n-He said "hello"\n+He said "hi"\n*** End Patch', - ), - ) - expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe('He said "hi"\nsome—dash\nend\n') - }), - ), - ) - it.live("rejects an update with missing context", () => withTempTool((directory, registry) => Effect.gen(function* () { From 36979c96419c574a737ad9186863c3718f8115c1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:50:33 -0500 Subject: [PATCH 04/27] test(core): consolidate provider factory coverage (#38390) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- .../core/test/plugin/provider-alibaba.test.ts | 96 ----------- .../core/test/plugin/provider-cohere.test.ts | 127 -------------- .../test/plugin/provider-deepinfra.test.ts | 161 ------------------ .../core/test/plugin/provider-factory.test.ts | 60 +++++++ .../core/test/plugin/provider-gateway.test.ts | 115 ------------- .../core/test/plugin/provider-groq.test.ts | 122 ------------- .../core/test/plugin/provider-mistral.test.ts | 134 --------------- .../test/plugin/provider-perplexity.test.ts | 127 -------------- .../test/plugin/provider-togetherai.test.ts | 132 -------------- .../core/test/plugin/provider-venice.test.ts | 120 ------------- 10 files changed, 60 insertions(+), 1134 deletions(-) delete mode 100644 packages/core/test/plugin/provider-alibaba.test.ts delete mode 100644 packages/core/test/plugin/provider-cohere.test.ts delete mode 100644 packages/core/test/plugin/provider-deepinfra.test.ts create mode 100644 packages/core/test/plugin/provider-factory.test.ts delete mode 100644 packages/core/test/plugin/provider-gateway.test.ts delete mode 100644 packages/core/test/plugin/provider-groq.test.ts delete mode 100644 packages/core/test/plugin/provider-mistral.test.ts delete mode 100644 packages/core/test/plugin/provider-perplexity.test.ts delete mode 100644 packages/core/test/plugin/provider-togetherai.test.ts delete mode 100644 packages/core/test/plugin/provider-venice.test.ts diff --git a/packages/core/test/plugin/provider-alibaba.test.ts b/packages/core/test/plugin/provider-alibaba.test.ts deleted file mode 100644 index bb7f922e52..0000000000 --- a/packages/core/test/plugin/provider-alibaba.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import { createAlibaba } from "@ai-sdk/alibaba" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* AlibabaPlugin.effect(host) -}) - -describe("AlibabaPlugin", () => { - it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/alibaba", - options: { name: "alibaba" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Alibaba SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "alibaba" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Alibaba SDK provider naming", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")), - modelID: ModelV2.ID.make("qwen"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/alibaba", - options: { name: "custom-alibaba", apiKey: "test" }, - }) - const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen") - const actual = result.sdk?.languageModel("qwen") - expect(actual?.provider).toBe(expected.provider) - expect(actual?.modelId).toBe(expected.modelId) - }), - ) - - it.effect("uses the default languageModel(modelID) behavior", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const item = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("qwen-plus"), - package: "aisdk:test-provider", - }) - const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} }) - const language = result.sdk?.languageModel(item.modelID ?? item.id) - expect(language?.modelId).toBe("qwen-plus") - expect(language?.provider).toBe("alibaba.chat") - }), - ) -}) diff --git a/packages/core/test/plugin/provider-cohere.test.ts b/packages/core/test/plugin/provider-cohere.test.ts deleted file mode 100644 index a4109f74bd..0000000000 --- a/packages/core/test/plugin/provider-cohere.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" -import { ProviderV2 } from "@opencode-ai/core/provider" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const cohereOptions: Record[] = [] -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* CoherePlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -void mock.module("@ai-sdk/cohere", () => ({ - createCohere: (options: Record) => { - cohereOptions.push({ ...options }) - return { - languageModel: (modelID: string) => ({ - modelID, - provider: `${options.name ?? "cohere"}.chat`, - specificationVersion: "v3", - }), - } - }, -})) - -describe("CoherePlugin", () => { - it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), - modelID: ModelV2.ID.make("command"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "cohere" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), - modelID: ModelV2.ID.make("command"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/cohere", - options: { name: "cohere" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("uses the model provider ID as the bundled SDK name", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")), - modelID: ModelV2.ID.make("command-r-plus"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/cohere", - options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" }, - }) - - expect(cohereOptions.at(-1)).toEqual({ - name: "custom-cohere", - apiKey: "test", - baseURL: "https://cohere.example", - }) - expect(result.sdk?.languageModel("command-r-plus").provider).toBe("custom-cohere.chat") - }), - ) - - it.effect("leaves language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - const sdk = fakeSelectorSdk(calls) - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("command-r-plus"), - package: "aisdk:test-provider", - }), - sdk, - options: {}, - }) - - expect(result.language).toBeUndefined() - expect(calls).toEqual([]) - expect(result.language ?? sdk.languageModel("command-r-plus")).toBeDefined() - expect(calls).toEqual(["languageModel:command-r-plus"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-deepinfra.test.ts b/packages/core/test/plugin/provider-deepinfra.test.ts deleted file mode 100644 index 14c41e550d..0000000000 --- a/packages/core/test/plugin/provider-deepinfra.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) -const deepinfraOptions: Record[] = [] -const deepinfraLanguageModels: string[] = [] - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* DeepInfraPlugin.effect(host) -}) - -void mock.module("@ai-sdk/deepinfra", () => ({ - createDeepInfra: (options: Record) => { - const captured = { ...options } - deepinfraOptions.push(captured) - return { - languageModel: (modelID: string) => { - deepinfraLanguageModels.push(modelID) - return { modelID, provider: `${captured.name ?? "deepinfra"}.chat`, specificationVersion: "v3" } - }, - } - }, -})) - -function resetDeepInfraMock() { - deepinfraOptions.length = 0 - deepinfraLanguageModels.length = 0 -} - -describe("DeepInfraPlugin", () => { - it.effect("creates a DeepInfra SDK for @ai-sdk/deepinfra", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("passes the model provider ID as the bundled DeepInfra SDK name", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "custom-deepinfra", apiKey: "test" }, - }) - expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat") - expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }]) - }), - ) - - it.effect("uses the canonical provider ID as the bundled DeepInfra SDK name", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra", apiKey: "test" }, - }) - expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat") - expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }]) - }), - ) - - it.effect("matches only the exact bundled DeepInfra package", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const packages = [ - "unmatched-package", - "@ai-sdk/deepinfra-compatible", - "file:///tmp/@ai-sdk/deepinfra-provider.js", - ] - yield* Effect.forEach(packages, (item) => - Effect.gen(function* () { - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: item, - options: { name: "deepinfra" }, - }) - expect(ignored.sdk).toBeUndefined() - }), - ) - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - expect(result.sdk).toBeDefined() - expect(deepinfraOptions).toEqual([{ name: "deepinfra" }]) - }), - ) - - it.effect("uses the default languageModel selection for DeepInfra models", () => - Effect.gen(function* () { - resetDeepInfraMock() - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const sdkEvent = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")), - modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), - package: "aisdk:@ai-sdk/deepinfra", - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }) - const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options }) - const language = result.language ?? result.sdk.languageModel(result.model.modelID ?? result.model.id) - expect(language.provider).toBe("deepinfra.chat") - expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts new file mode 100644 index 0000000000..a884fd0ce0 --- /dev/null +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -0,0 +1,60 @@ +import { expect } from "bun:test" +import { Effect } from "effect" +import { AISDK } from "@opencode-ai/core/aisdk" +import { ModelV2 } from "@opencode-ai/core/model" +import { PluginV2 } from "@opencode-ai/core/plugin" +import { PluginHost } from "@opencode-ai/core/plugin/host" +import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" +import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" +import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra" +import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway" +import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq" +import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" +import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" +import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" +import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { testEffect } from "../lib/effect" +import { PluginTestLayer } from "./fixture" + +const modelID = ModelV2.ID.make("test-model") +const options = { name: "custom-provider", apiKey: "test", baseURL: "https://example.test" } +const providers = [ + { id: "alibaba", plugin: AlibabaPlugin, package: "@ai-sdk/alibaba", provider: "alibaba.chat" }, + { id: "cohere", plugin: CoherePlugin, package: "@ai-sdk/cohere", provider: "cohere.chat" }, + { id: "deepinfra", plugin: DeepInfraPlugin, package: "@ai-sdk/deepinfra", provider: "deepinfra.chat" }, + { id: "gateway", plugin: GatewayPlugin, package: "@ai-sdk/gateway", provider: "gateway" }, + { id: "groq", plugin: GroqPlugin, package: "@ai-sdk/groq", provider: "groq.chat" }, + { id: "mistral", plugin: MistralPlugin, package: "@ai-sdk/mistral", provider: "mistral.chat" }, + { id: "perplexity", plugin: PerplexityPlugin, package: "@ai-sdk/perplexity", provider: "perplexity" }, + { id: "togetherai", plugin: TogetherAIPlugin, package: "@ai-sdk/togetherai", provider: "togetherai.chat" }, + { id: "venice", plugin: VenicePlugin, package: "venice-ai-sdk-provider", provider: "custom-provider.chat" }, +] as const + +const it = testEffect(PluginTestLayer) + +providers.forEach((item) => + it.effect(`${item.id} loads only its exact package`, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* item.plugin.effect(host) + const model = ModelV2.Info.make({ + ...ModelV2.Info.empty(ProviderV2.ID.make(item.id), modelID), + modelID, + package: ProviderV2.aisdk(item.package), + }) + const matched = yield* aisdk.runSDK({ model, package: item.package, options }) + const ignored = yield* aisdk.runSDK({ model, package: `${item.package}/unsupported`, options }) + const language = matched.sdk?.languageModel(modelID) + + expect({ + provider: language?.provider, + modelID: language?.modelId, + version: language?.specificationVersion, + ignored: ignored.sdk === undefined, + }).toEqual({ provider: item.provider, modelID: "test-model", version: "v3", ignored: true }) + }), + ), +) diff --git a/packages/core/test/plugin/provider-gateway.test.ts b/packages/core/test/plugin/provider-gateway.test.ts deleted file mode 100644 index 722bbde9b6..0000000000 --- a/packages/core/test/plugin/provider-gateway.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect, mock } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const gatewayCalls: Record[] = [] -const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"] -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* GatewayPlugin.effect(host) -}) - -mock.module("@ai-sdk/gateway", () => ({ - createGateway(options: Record) { - gatewayCalls.push({ ...options }) - return { - languageModel(modelID: string) { - return { - modelId: modelID, - provider: options.name, - specificationVersion: "v3", - } - }, - } - }, -})) - -describe("GatewayPlugin", () => { - it.effect("creates a Gateway SDK for @ai-sdk/gateway", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "gateway" }, - }) - expect(result.sdk).toBeDefined() - expect(gatewayCalls).toHaveLength(1) - }), - ) - - it.effect("passes the model providerID as the Gateway SDK name", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-4"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "vercel", apiKey: "test-key" }, - }) - - expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }]) - expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel") - }), - ) - - it.effect("matches Vercel AI Gateway models by their @ai-sdk/gateway package", () => - Effect.gen(function* () { - gatewayCalls.length = 0 - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - for (const modelID of vercelGatewayModels) { - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), - modelID: ModelV2.ID.make(modelID), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/vercel", - options: { name: "vercel" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), - modelID: ModelV2.ID.make(modelID), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/gateway", - options: { name: "vercel" }, - }) - expect(result.sdk).toBeDefined() - } - - expect(gatewayCalls).toHaveLength(3) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-groq.test.ts b/packages/core/test/plugin/provider-groq.test.ts deleted file mode 100644 index b8900384b7..0000000000 --- a/packages/core/test/plugin/provider-groq.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import { createGroq } from "@ai-sdk/groq" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* GroqPlugin.effect(host) -}) - -describe("GroqPlugin", () => { - it.effect("creates a Groq SDK for @ai-sdk/groq", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq", - options: { name: "groq" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Groq SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "groq" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("only matches the bundled @ai-sdk/groq package exactly", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq/compat", - options: { name: "groq" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Groq SDK provider naming", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")), - modelID: ModelV2.ID.make("llama"), - package: "aisdk:@ai-sdk/groq", - }), - package: "@ai-sdk/groq", - options: { name: "custom-groq", apiKey: "test" }, - }) - const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters[0] & { - name: string - }).languageModel("llama") - const actual = result.sdk?.languageModel("llama") - expect(actual?.provider).toBe(expected.provider) - expect(actual?.modelId).toBe(expected.modelId) - }), - ) - - it.effect("uses the default languageModel(modelID) behavior", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters[0] & { - name: string - }) - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("llama-api"), - package: "aisdk:@ai-sdk/groq", - }), - sdk, - options: { name: "groq", apiKey: "test" }, - }) - const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id) - expect(language.modelId).toBe("llama-api") - expect(language.provider).toBe("groq.chat") - }), - ) -}) diff --git a/packages/core/test/plugin/provider-mistral.test.ts b/packages/core/test/plugin/provider-mistral.test.ts deleted file mode 100644 index 182873482c..0000000000 --- a/packages/core/test/plugin/provider-mistral.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { describe, expect } from "bun:test" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* MistralPlugin.effect(host) -}) - -describe("MistralPlugin", () => { - it.effect("creates a Mistral SDK for @ai-sdk/mistral", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores non-Mistral SDK packages", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const providers: string[] = [] - yield* addPlugin() - yield* aisdk.hook.sdk((event) => - Effect.sync(() => { - providers.push(event.sdk.languageModel("mistral-large").provider) - }), - ) - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "mistral" }, - }) - expect(result.sdk).toBeDefined() - expect(providers).toEqual(["mistral.chat"]) - }), - ) - - it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const providers: string[] = [] - yield* addPlugin() - yield* aisdk.hook.sdk((event) => - Effect.sync(() => { - providers.push(event.sdk.languageModel("mistral-large").provider) - }), - ) - yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/mistral", - options: { name: "custom-mistral" }, - }) - expect(providers).toEqual(["mistral.chat"]) - }), - ) - - it.effect("leaves Mistral language selection on the default sdk.languageModel(modelID) path", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - const sdk = { - languageModel: (id: string) => { - calls.push(`languageModel:${id}`) - return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3 - }, - } - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("mistral-large"), - package: "aisdk:test-provider", - }), - sdk, - options: {}, - }) - const language = result.language ?? sdk.languageModel(result.model.modelID ?? result.model.id) - expect(calls).toEqual(["languageModel:mistral-large"]) - expect(language).toBeDefined() - }), - ) -}) diff --git a/packages/core/test/plugin/provider-perplexity.test.ts b/packages/core/test/plugin/provider-perplexity.test.ts deleted file mode 100644 index d66f5dd71c..0000000000 --- a/packages/core/test/plugin/provider-perplexity.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* PerplexityPlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("PerplexityPlugin", () => { - it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("ignores packages that are not the bundled Perplexity package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity-compatible", - options: { name: "perplexity" }, - }) - expect(result.sdk).toBeUndefined() - }), - ) - - it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }) - expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") - }), - ) - - it.effect("creates bundled Perplexity SDKs for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/perplexity", - options: { name: "custom-perplexity" }, - }) - expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") - }), - ) - - it.effect("leaves Perplexity language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("sonar"), - package: "aisdk:test-provider", - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), - ) -}) diff --git a/packages/core/test/plugin/provider-togetherai.test.ts b/packages/core/test/plugin/provider-togetherai.test.ts deleted file mode 100644 index 1fffb03156..0000000000 --- a/packages/core/test/plugin/provider-togetherai.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* TogetherAIPlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("TogetherAIPlugin", () => { - it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("matches the old bundled provider package exactly", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "file:///tmp/@ai-sdk/togetherai-provider.js", - options: { name: "togetherai" }, - }) - expect(ignored.sdk).toBeUndefined() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/togetherai", - options: { name: "custom-togetherai" }, - }) - - expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat") - }), - ) - - it.effect("defaults language selection to sdk.languageModel with the model API ID", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("togetherai"), - ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - ), - modelID: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - package: "aisdk:test-provider", - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: {}, - }) - - expect(result.language).toBeUndefined() - expect(calls).toEqual([]) - expect( - result.language ?? fakeSelectorSdk(calls).languageModel(result.model.modelID ?? result.model.id), - ).toBeDefined() - expect(calls).toEqual(["languageModel:meta-llama/Llama-3.3-70B-Instruct-Turbo"]) - }), - ) -}) diff --git a/packages/core/test/plugin/provider-venice.test.ts b/packages/core/test/plugin/provider-venice.test.ts deleted file mode 100644 index 057d2c6ded..0000000000 --- a/packages/core/test/plugin/provider-venice.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { AISDK } from "@opencode-ai/core/aisdk" -import { describe, expect } from "bun:test" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" -import { PluginHost } from "@opencode-ai/core/plugin/host" -import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { testEffect } from "../lib/effect" -import { PluginTestLayer } from "./fixture" - -const it = testEffect(PluginTestLayer) - -const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const host = yield* PluginHost.make(plugin) - yield* VenicePlugin.effect(host) -}) - -function fakeSelectorSdk(calls: string[]) { - const make = (method: string) => (id: string) => { - calls.push(`${method}:${id}`) - return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3 - } - return { - responses: make("responses"), - messages: make("messages"), - chat: make("chat"), - languageModel: make("languageModel"), - } -} - -describe("VenicePlugin", () => { - it.effect("creates a Venice SDK for venice-ai-sdk-provider", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "venice-ai-sdk-provider", - options: { name: "venice" }, - }) - expect(result.sdk).toBeDefined() - }), - ) - - it.effect("uses the model provider ID as the bundled Venice SDK name", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "venice-ai-sdk-provider", - options: { name: "custom-venice", apiKey: "test" }, - }) - expect(result.sdk).toBeDefined() - expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat") - }), - ) - - it.effect("only handles the bundled venice-ai-sdk-provider package", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - yield* addPlugin() - const similar = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "file:///tmp/venice-ai-sdk-provider.js", - options: { name: "venice" }, - }) - const other = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), - package: "aisdk:test-provider", - }), - package: "@ai-sdk/openai-compatible", - options: { name: "venice" }, - }) - expect(similar.sdk).toBeUndefined() - expect(other.sdk).toBeUndefined() - }), - ) - - it.effect("leaves Venice language selection to the default languageModel fallback", () => - Effect.gen(function* () { - const plugin = yield* PluginV2.Service - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), - package: "aisdk:test-provider", - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), - ) -}) From b6e14b5a7415ad5c92f72a295339fd0564cbe8b1 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 17:56:22 -0400 Subject: [PATCH 05/27] refactor(tui): finish V2 theme migration (#38383) --- .../client/src/promise/generated/types.ts | 4 +--- .../test/fixtures/opencode-v2-openapi.json | 22 +++---------------- packages/core/src/config/agent.ts | 5 +---- packages/core/src/v1/config/agent.ts | 7 ++---- packages/core/test/config/agent.test.ts | 10 ++++++--- packages/core/test/config/config.test.ts | 4 ++-- packages/docs/agents.mdx | 7 +++--- packages/docs/openapi.json | 22 +++---------------- packages/schema/src/agent.ts | 7 +++--- packages/schema/test/contract-hygiene.test.ts | 6 +++++ packages/tui/src/component/bg-pulse.tsx | 8 +++---- packages/tui/src/context/local.tsx | 19 ++-------------- packages/tui/src/context/theme.tsx | 15 +++---------- packages/www/content/docs/(docs)/agents.mdx | 7 +++--- packages/www/public/openapi.json | 22 +++---------------- 15 files changed, 46 insertions(+), 119 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 6c869b0e2e..966ec22cc0 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -8,8 +8,6 @@ export type ModelRef = { id: string; providerID: string; variant?: string } export type ProviderSettings = { [x: string]: JsonValue } -export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - export type PermissionV2Effect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -2005,7 +2003,7 @@ export type AgentInfo = { description?: string mode: "subagent" | "primary" | "all" hidden: boolean - color?: AgentColor + color?: string steps?: number permissions: PermissionV2Ruleset } diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index fc5edf5119..075feea250 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -6,10 +6,7 @@ import { ConfigProvider } from "./provider" import { ConfigModel } from "./model" import { PositiveInt } from "../schema" -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) export class Info extends Schema.Class("ConfigV2.Agent")({ model: ConfigModel.Selection.pipe(Schema.optional), diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index b220bd7ef8..09838a9196 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -4,10 +4,7 @@ import { Schema, SchemaGetter } from "effect" import { PositiveInt } from "../../schema" import { ConfigPermissionV1 } from "./permission" -const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) const AgentSchema = Schema.StructWithRest( Schema.Struct({ @@ -29,7 +26,7 @@ const AgentSchema = Schema.StructWithRest( }), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), color: Schema.optional(Color).annotate({ - description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", + description: "Hex color code (e.g., #FF5733)", }), steps: Schema.optional(PositiveInt).annotate({ description: "Maximum number of agentic iterations before forcing text-only response", diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 0380c38e3a..855ea370e2 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Schema } from "effect" @@ -23,6 +23,10 @@ const defaultPermissions = [ { action: "external_directory", resource: "*", effect: "ask" }, ] satisfies PermissionV2.Ruleset +test("rejects named agent color tokens", () => { + expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() +}) + describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches POSIX paths against home-relative permissions", () => Effect.gen(function* () { @@ -160,7 +164,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, request: { headers: { first: "one", shared: "first" }, @@ -197,7 +201,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, model: { providerID: "anthropic", id: "claude-sonnet" }, }) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 64e08b5e79..6584cc1c94 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -738,7 +738,7 @@ describe("Config", () => { system: "Find regressions.", mode: "subagent", hidden: false, - color: "warning", + color: "#ff6b6b", steps: 12, disabled: false, permissions: [{ action: "edit", resource: "*", effect: "deny" }], @@ -824,7 +824,7 @@ describe("Config", () => { expect(reviewer?.system).toBe("Find regressions.") expect(reviewer?.mode).toBe("subagent") expect(reviewer?.hidden).toBe(false) - expect(reviewer?.color).toBe("warning") + expect(reviewer?.color).toBe("#ff6b6b") expect(reviewer?.steps).toBe(12) expect(reviewer?.disabled).toBe(false) expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }]) diff --git a/packages/docs/agents.mdx b/packages/docs/agents.mdx index a81ae6ec3c..ca7ee80a20 100644 --- a/packages/docs/agents.mdx +++ b/packages/docs/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 8806a9c469..399e9e9131 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -16,10 +16,9 @@ export type ID = typeof ID.Type export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) export type Name = typeof Name.Type -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]).annotate({ identifier: "Agent.Color" }) +export const Color = Schema.String.annotate({ identifier: "Agent.Color" }).check( + Schema.isPattern(/^#[0-9a-fA-F]{6}$/), +) export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index c441ad88c3..c662870269 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -20,6 +20,12 @@ import { PersistedRevert } from "../src/session-revert.js" import { optional } from "../src/schema.js" describe("contract hygiene", () => { + test("restricts agent colors to six-digit hex values", () => { + const decode = Schema.decodeUnknownSync(Agent.Color) + expect(decode("#ff6b6b")).toBe("#ff6b6b") + expect(() => decode("warning")).toThrow() + }) + test("keeps absolute costs distinct from model rates", () => { const usd = Money.USD.make(1) const rate = Money.USDPerMillionTokens.make(1) diff --git a/packages/tui/src/component/bg-pulse.tsx b/packages/tui/src/component/bg-pulse.tsx index 2112fe4420..064cc314f9 100644 --- a/packages/tui/src/component/bg-pulse.tsx +++ b/packages/tui/src/component/bg-pulse.tsx @@ -70,7 +70,7 @@ declare module "@opentui/solid" { extend({ go_upsell_art: GoUpsellArtRenderable }) export function BgPulse() { - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const renderer = useRenderer() let targetFps = renderer.targetFps let maxFps = renderer.maxFps @@ -91,9 +91,9 @@ export function BgPulse() { ) diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index 03716cdf12..98faea2f9d 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -23,16 +23,6 @@ import { useRoute } from "./route" import { useData } from "./data" import { usePermission } from "./permission" -export type LocalTheme = { - secondary: RGBA - accent: RGBA - success: RGBA - warning: RGBA - primary: RGBA - error: RGBA - info: RGBA -} - export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { @@ -60,7 +50,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const data = useData() const client = useClient() const toast = useToast() - const { theme, themeV2, mode } = useTheme() + const { themeV2, mode } = useTheme() const route = useRoute() const paths = useTuiPaths() const args = useArgs() @@ -128,12 +118,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (index === -1) return colors()[0] const agent = visibleAgents()[index] - if (agent?.color) { - const color = agent.color - if (color.startsWith("#")) return RGBA.fromHex(color) - // already validated by config, just satisfying TS here - return theme[color as keyof typeof theme] as RGBA - } + if (agent?.color) return RGBA.fromHex(agent.color) return colors()[index % colors().length] }, } diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 88f91cb50d..aff085e969 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -71,7 +71,6 @@ type State = { type ContextName = "elevated" | "overlay" type ThemeService = { - theme: Theme themeV2: ComponentTheme contextual(context: ContextName): ThemeService readonly selected: string @@ -280,7 +279,7 @@ const themeContext = createSimpleContext({ if (supported.includes(store.mode)) return store.mode return supported[0] ?? store.mode } - const values = createMemo(() => resolveTheme(source(), mode())) + const legacySyntaxTheme = createMemo(() => resolveTheme(source(), mode())) const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) @@ -298,21 +297,13 @@ const themeContext = createSimpleContext({ }, mode), } - createEffect(() => renderer.setBackgroundColor(values().background)) + createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) - const syntax = createSyntaxStyleMemo(() => generateSyntax(values())) - - const theme = new Proxy(values(), { - get(_target, prop) { - // @ts-expect-error Properties are forwarded to the current reactive value. - return values()[prop] - }, - }) + const syntax = createSyntaxStyleMemo(() => generateSyntax(legacySyntaxTheme())) function contextual(context: ContextName) { return contextualServices[context] } const service: ThemeService = { - theme, themeV2, contextual, get selected() { diff --git a/packages/www/content/docs/(docs)/agents.mdx b/packages/www/content/docs/(docs)/agents.mdx index b1275960cb..d5668402db 100644 --- a/packages/www/content/docs/(docs)/agents.mdx +++ b/packages/www/content/docs/(docs)/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/docs/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, From 381f6c47b46a2a4f89d37d8c698ada1b50c36057 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 18:30:31 -0400 Subject: [PATCH 06/27] docs(tui): add generated V2 theme reference (#38396) --- .github/workflows/test.yml | 5 + packages/docs/README.md | 11 ++ packages/docs/docs.json | 1 + packages/docs/index.mdx | 2 +- packages/docs/package.json | 10 +- packages/docs/script/generate-theme-tokens.ts | 136 ++++++++++++++++++ .../docs/snippets/generated/theme-tokens.mdx | 79 ++++++++++ packages/docs/themes.mdx | 129 +++++++++++++++++ packages/tui/src/theme/v2/schema.ts | 2 +- packages/tui/test/theme/v2/resolve.test.ts | 11 +- script/generate.ts | 2 + 11 files changed, 381 insertions(+), 7 deletions(-) create mode 100644 packages/docs/script/generate-theme-tokens.ts create mode 100644 packages/docs/snippets/generated/theme-tokens.mdx create mode 100644 packages/docs/themes.mdx diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b486b68a93..1ae28ea874 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -97,6 +97,11 @@ jobs: working-directory: packages/client run: bun run check:generated + - name: Check generated documentation + if: runner.os == 'Linux' + working-directory: packages/docs + run: bun run check:generated + e2e: name: e2e (${{ matrix.settings.name }}) if: github.ref_name != 'v2' && github.head_ref != 'v2' diff --git a/packages/docs/README.md b/packages/docs/README.md index 17b06848e1..1aff8cf6dc 100644 --- a/packages/docs/README.md +++ b/packages/docs/README.md @@ -19,4 +19,15 @@ bun validate bun broken-links ``` +The V2 theme token reference is generated from +`packages/tui/src/theme/v2/schema.ts`. Regenerate it after schema changes: + +```bash +bun run generate +``` + +`bun validate` checks that the committed snippet is current. The repository's +generation workflow also refreshes it on pushes to `dev`, so Mintlify always +receives the generated MDX as part of the published docs tree. + The hosted preview is available at [opencode.mintlify.site](https://opencode.mintlify.site). diff --git a/packages/docs/docs.json b/packages/docs/docs.json index ff41ab4b92..a9349f2963 100644 --- a/packages/docs/docs.json +++ b/packages/docs/docs.json @@ -38,6 +38,7 @@ "attachments", "compaction", "warming", + "themes", "formatters", "lsp", "references" diff --git a/packages/docs/index.mdx b/packages/docs/index.mdx index 9095853903..bbb3ccfdfe 100644 --- a/packages/docs/index.mdx +++ b/packages/docs/index.mdx @@ -158,6 +158,6 @@ limitations and safety details. ## Customize -Make OpenCode your own by [picking a theme](https://opencode.ai/docs/themes), [customizing +Make OpenCode your own by [picking a theme](/themes), [customizing keybinds](https://opencode.ai/docs/keybinds), [configuring formatters](/formatters), [creating commands](/commands), or editing the [OpenCode config](/config). diff --git a/packages/docs/package.json b/packages/docs/package.json index 01f35998c5..b8a9ead7ca 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -3,11 +3,15 @@ "name": "@opencode-ai/docs", "private": true, "scripts": { - "dev": "bun --bun mint dev --no-open --port 3333", - "validate": "bun --bun mint validate", + "dev": "bun run generate && bun --bun mint dev --no-open --port 3333", + "generate": "bun script/generate-theme-tokens.ts", + "check:generated": "bun script/generate-theme-tokens.ts --check", + "validate": "bun run check:generated && bun --bun mint validate", "broken-links": "bun --bun mint broken-links" }, "devDependencies": { - "mint": "4.2.666" + "effect": "catalog:", + "mint": "4.2.666", + "prettier": "3.6.2" } } diff --git a/packages/docs/script/generate-theme-tokens.ts b/packages/docs/script/generate-theme-tokens.ts new file mode 100644 index 0000000000..0c441075f9 --- /dev/null +++ b/packages/docs/script/generate-theme-tokens.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env bun + +import { Schema, SchemaAST } from "effect" +import { format } from "prettier" +import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema" + +const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx" +const root = requireObject(ThemeDefinition.ast) +const hue = requireObject(requireField(root, "hue").type) +const hueNames = hue.propertySignatures.map((field) => String(field.name)) +const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) => + String(field.name), +) +const contexts = root.propertySignatures + .map((field) => String(field.name)) + .filter((name) => name.startsWith("@context:")) +const tokens = root.propertySignatures + .filter((field) => { + const name = String(field.name) + return name !== "hue" && name !== "categorical" && !name.startsWith("@context:") + }) + .flatMap((field) => tokenPaths(field.type, String(field.name))) +const groups = Map.groupBy(tokens, (token) => + token + .split(".") + .slice(0, token.split(".").length > 2 ? 2 : 1) + .join("."), +) +const table = [...groups] + .map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("
    ")} |`) + .join("\n") +const example = { + version: 2, + light: { + hue: { + accent: "$hue.purple", + interactive: "$hue.purple", + }, + text: { + default: "$hue.neutral.900", + }, + background: { + default: "#fafafa", + }, + }, + dark: { + mergeMode: true, + text: { + default: "$hue.neutral.100", + }, + background: { + default: "#101014", + }, + }, +} satisfies ThemeFile +Schema.decodeUnknownSync(ThemeFile)(example) +const output = await format( + `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} + +\`\`\`json title="my-theme.json" +${JSON.stringify(example, null, 2)} +\`\`\` + +## Token reference + +This reference is generated from the Effect schema in +\`packages/tui/src/theme/v2/schema.ts\`. Changes to the runtime schema update +this section through \`bun run generate\`. + +### Hue tokens + +Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these +steps, or alias it to another hue with a value such as \`$hue.blue\`. + +| | Values | +| --- | --- | +| Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} | +| Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} | + +Reference a hue color as \`$hue..\`, for example +\`$hue.interactive.500\`. + +### Semantic tokens + +Semantic values can reference another token by prefixing its path with \`$\`, +for example \`$text.default\`. Stateful tokens inherit their \`default\` +value when a state is omitted. + +| Group | Tokens | +| --- | --- | +${table} + +### Contexts + +${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial +overrides of the semantic tokens above. Components apply these contexts to +surfaces that need different contrast without changing the base theme. +`, + { parser: "mdx", printWidth: 120, semi: false }, +) + +if (process.argv.includes("--check")) { + const current = await Bun.file(target).text() + if (current === output) process.exit(0) + console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/docs.") + process.exit(1) +} + +await Bun.write(target, output) + +function requireObject(ast: SchemaAST.AST): SchemaAST.Objects { + if (SchemaAST.isObjects(ast)) return ast + if (SchemaAST.isUnion(ast)) { + const object = ast.types.map(findObject).find((value) => value !== undefined) + if (object) return object + } + throw new Error(`Expected an object schema, received ${ast._tag}`) +} + +function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined { + if (SchemaAST.isObjects(ast)) return ast + if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined) + if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk()) +} + +function requireField(ast: SchemaAST.Objects, name: string) { + const field = ast.propertySignatures.find((field) => String(field.name) === name) + if (field) return field + throw new Error(`Theme schema field not found: ${name}`) +} + +function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] { + const object = findObject(ast) + if (!object || object.propertySignatures.length === 0) return [prefix] + return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`)) +} diff --git a/packages/docs/snippets/generated/theme-tokens.mdx b/packages/docs/snippets/generated/theme-tokens.mdx new file mode 100644 index 0000000000..932072f6cb --- /dev/null +++ b/packages/docs/snippets/generated/theme-tokens.mdx @@ -0,0 +1,79 @@ +{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} + +```json title="my-theme.json" +{ + "version": 2, + "light": { + "hue": { + "accent": "$hue.purple", + "interactive": "$hue.purple" + }, + "text": { + "default": "$hue.neutral.900" + }, + "background": { + "default": "#fafafa" + } + }, + "dark": { + "mergeMode": true, + "text": { + "default": "$hue.neutral.100" + }, + "background": { + "default": "#101014" + } + } +} +``` + +## Token reference + +This reference is generated from the Effect schema in +`packages/tui/src/theme/v2/schema.ts`. Changes to the runtime schema update +this section through `bun run generate`. + +### Hue tokens + +Every hue is a 9-step scale. Define a scale with all of these +steps, or alias it to another hue with a value such as `$hue.blue`. + +| | Values | +| ----- | -------------------------------------------------------------------------------------------------------- | +| Hues | `gray`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, `accent`, `interactive`, `neutral` | +| Steps | `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900` | + +Reference a hue color as `$hue..`, for example +`$hue.interactive.500`. + +### Semantic tokens + +Semantic values can reference another token by prefixing its path with `$`, +for example `$text.default`. Stateful tokens inherit their `default` +value when a state is omitted. + +| Group | Tokens | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `text` | `text.default`
    `text.subdued` | +| `text.action` | `text.action.primary.default`
    `text.action.primary.$hovered`
    `text.action.primary.$focused`
    `text.action.primary.$pressed`
    `text.action.primary.$selected`
    `text.action.primary.$disabled`
    `text.action.destructive.default`
    `text.action.destructive.$hovered`
    `text.action.destructive.$focused`
    `text.action.destructive.$pressed`
    `text.action.destructive.$selected`
    `text.action.destructive.$disabled` | +| `text.formfield` | `text.formfield.default`
    `text.formfield.$hovered`
    `text.formfield.$focused`
    `text.formfield.$pressed`
    `text.formfield.$selected`
    `text.formfield.$disabled` | +| `text.feedback` | `text.feedback.error.default`
    `text.feedback.error.subdued`
    `text.feedback.warning.default`
    `text.feedback.warning.subdued`
    `text.feedback.success.default`
    `text.feedback.success.subdued`
    `text.feedback.info.default`
    `text.feedback.info.subdued` | +| `background` | `background.default` | +| `background.surface` | `background.surface.offset`
    `background.surface.overlay` | +| `background.action` | `background.action.primary.default`
    `background.action.primary.$hovered`
    `background.action.primary.$focused`
    `background.action.primary.$pressed`
    `background.action.primary.$selected`
    `background.action.primary.$disabled`
    `background.action.destructive.default`
    `background.action.destructive.$hovered`
    `background.action.destructive.$focused`
    `background.action.destructive.$pressed`
    `background.action.destructive.$selected`
    `background.action.destructive.$disabled` | +| `background.formfield` | `background.formfield.default`
    `background.formfield.$hovered`
    `background.formfield.$focused`
    `background.formfield.$pressed`
    `background.formfield.$selected`
    `background.formfield.$disabled` | +| `background.feedback` | `background.feedback.error.default`
    `background.feedback.warning.default`
    `background.feedback.success.default`
    `background.feedback.info.default` | +| `border` | `border.default` | +| `scrollbar` | `scrollbar.default` | +| `diff.text` | `diff.text.added`
    `diff.text.removed`
    `diff.text.context`
    `diff.text.hunkHeader` | +| `diff.background` | `diff.background.added`
    `diff.background.removed`
    `diff.background.context` | +| `diff.highlight` | `diff.highlight.added`
    `diff.highlight.removed` | +| `diff.lineNumber` | `diff.lineNumber.text`
    `diff.lineNumber.background.added`
    `diff.lineNumber.background.removed` | +| `syntax` | `syntax.comment`
    `syntax.keyword`
    `syntax.function`
    `syntax.variable`
    `syntax.string`
    `syntax.number`
    `syntax.type`
    `syntax.operator`
    `syntax.punctuation` | +| `markdown` | `markdown.text`
    `markdown.heading`
    `markdown.link`
    `markdown.linkText`
    `markdown.code`
    `markdown.blockQuote`
    `markdown.emphasis`
    `markdown.strong`
    `markdown.horizontalRule`
    `markdown.listItem`
    `markdown.listEnumeration`
    `markdown.image`
    `markdown.imageText`
    `markdown.codeBlock` | + +### Contexts + +`@context:elevated` and `@context:overlay` accept partial +overrides of the semantic tokens above. Components apply these contexts to +surfaces that need different contrast without changing the base theme. diff --git a/packages/docs/themes.mdx b/packages/docs/themes.mdx new file mode 100644 index 0000000000..49f41768f6 --- /dev/null +++ b/packages/docs/themes.mdx @@ -0,0 +1,129 @@ +--- +title: "Themes" +description: "Choose a built-in TUI theme or create a custom color scheme." +--- + +import ThemeTokens from "/snippets/generated/theme-tokens.mdx" + +OpenCode includes built-in light and dark themes and can load custom themes +from your global configuration or a project directory. The default theme is +`opencode`. + +## Choose a theme + +In the full-screen TUI, run: + +```text +/themes +``` + +You can also open the picker with `ctrl+x`, then `t`, using the +default keybindings. + +Use `/settings` to change both the theme and its color mode. OpenCode supports +three modes: + +| Mode | Behavior | +| -------- | -------------------------------------------------------- | +| `system` | Follow the terminal's detected light or dark appearance. | +| `dark` | Always use the theme's dark colors. | +| `light` | Always use the theme's light colors. | + +Your selection is stored in `~/.config/opencode/cli.json`, or the equivalent +path under `$XDG_CONFIG_HOME`: + +```json title="cli.json" +{ + "theme": { + "name": "tokyonight", + "mode": "system" + } +} +``` + + + Theme selection applies to the full-screen TUI. Direct interactive runs use colors derived from the terminal palette + and honor only the color mode. + + +## Built-in themes + +OpenCode currently includes: + +| | | | +| ------------ | ------------------- | ---------------------- | +| `aura` | `ayu` | `carbonfox` | +| `catppuccin` | `catppuccin-frappe` | `catppuccin-macchiato` | +| `cobalt2` | `cursor` | `dracula` | +| `everforest` | `flexoki` | `github` | +| `gruvbox` | `kanagawa` | `lucent-orng` | +| `material` | `matrix` | `mercury` | +| `monokai` | `nightowl` | `nord` | +| `one-dark` | `opencode` | `orng` | +| `osaka-jade` | `palenight` | `rosepine` | +| `solarized` | `synthwave84` | `tokyonight` | +| `vercel` | `vesper` | `zenburn` | + +When OpenCode can read your terminal palette, the picker also includes +`system`. The `system` theme generates its colors from your terminal's +foreground, background, and ANSI palette. + +## Custom themes + +Create a JSON file in either of these locations: + +```text +~/.config/opencode/themes/my-theme.json +.opencode/themes/my-theme.json +``` + +OpenCode checks the global theme directory first, followed by every +`.opencode/themes` directory from the filesystem root down to the current +directory. A more local file with the same filename overrides an earlier one. +The filename becomes the theme name, so `my-theme.json` appears as `my-theme`. + +Custom theme files must be strict JSON. Comments and trailing commas are not +supported. + +### Format + +V2 themes organize colors into hue scales and semantic tokens. Set `version` +to `2` and define at least one of `light` or `dark`: + + + Native V2 custom theme files are not loaded directly by the current beta. Existing custom files use the V1 format and + are migrated to these tokens at runtime. This reference tracks the native V2 schema while direct file loading is + completed. + + +By default, a theme inherits OpenCode's complete theme, so you only need to +define overrides. Set `mergeMode` to `true` to inherit one mode from the other +before applying that mode's overrides. Set `standalone` to `true` only when you +intend to supply a complete independent theme. + +Each token accepts: + +- A hex color such as `"#5c9cf5"` +- `"transparent"` to use the terminal default +- A hue reference such as `"$hue.blue.500"` +- Another semantic token reference such as `"$text.default"` + +Syntax and markdown tokens accept hex colors and hue references. Other +semantic tokens can reference any semantic token. + + + +If you add or edit a custom theme while OpenCode is running, restart the TUI to +reload it. + +## Terminal colors + +Themes display most accurately in a terminal with truecolor support. Check +your terminal with: + +```bash +echo $COLORTERM +``` + +Most modern terminals report `truecolor` or `24bit`. Without truecolor, +OpenCode approximates theme colors using the available terminal palette. diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 076bdfc635..76a33f7a09 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -243,7 +243,7 @@ const MergeModeDefinition = Schema.Struct({ "@context:overlay": Schema.optional(ThemeTokensDefinition), }) export type MergeModeDefinition = Schema.Schema.Type -export const ModeDefinition = Schema.Union([FileThemeDefinition, MergeModeDefinition]) +export const ModeDefinition = Schema.Union([MergeModeDefinition, FileThemeDefinition]) export type ModeDefinition = Schema.Schema.Type const FileMetadata = { diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index 237b4c79fc..e91a79d2b2 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -192,8 +192,15 @@ test("standalone themes skip OpenCode defaults and use the red core fallback", ( }) test("uses defaults for the selected mode when it merges the other mode", () => { - const theme = resolveThemeFile({ version: 2, light: { hue: light.hue }, dark: { mergeMode: true } }, "dark") - expect(theme.background.default.toInts()).toEqual(resolveTheme(dark).background.default.toInts()) + const theme = resolveThemeFile( + { + version: 2, + light: { hue: light.hue, background: { default: "#123456" } }, + dark: { mergeMode: true }, + }, + "dark", + ) + expect(theme.background.default.toInts()).toEqual([18, 52, 86, 255]) }) test("resolves matched action variants and states", () => { diff --git a/script/generate.ts b/script/generate.ts index 8fc251d89d..dbf38f8a3c 100755 --- a/script/generate.ts +++ b/script/generate.ts @@ -6,4 +6,6 @@ await $`bun ./packages/sdk/js/script/build.ts` await $`bun dev generate > ../sdk/openapi.json`.cwd("packages/opencode") +await $`bun run generate`.cwd("packages/docs") + await $`./script/format.ts` From a817fe5e6ce078918a1488b390b4b08a1852ee14 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 22 Jul 2026 19:20:28 -0400 Subject: [PATCH 07/27] fix(schema): loosen agent color response --- packages/client/src/promise/generated/types.ts | 4 +++- packages/schema/src/agent.ts | 4 +--- packages/schema/test/agent.test.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 packages/schema/test/agent.test.ts diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 966ec22cc0..a08c7b3246 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -8,6 +8,8 @@ export type ModelRef = { id: string; providerID: string; variant?: string } export type ProviderSettings = { [x: string]: JsonValue } +export type AgentColor = string + export type PermissionV2Effect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -2003,7 +2005,7 @@ export type AgentInfo = { description?: string mode: "subagent" | "primary" | "all" hidden: boolean - color?: string + color?: AgentColor steps?: number permissions: PermissionV2Ruleset } diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 399e9e9131..8001a2967e 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -16,9 +16,7 @@ export type ID = typeof ID.Type export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) export type Name = typeof Name.Type -export const Color = Schema.String.annotate({ identifier: "Agent.Color" }).check( - Schema.isPattern(/^#[0-9a-fA-F]{6}$/), -) +export const Color = Schema.String.annotate({ identifier: "Agent.Color" }) export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/test/agent.test.ts b/packages/schema/test/agent.test.ts new file mode 100644 index 0000000000..30535ebaec --- /dev/null +++ b/packages/schema/test/agent.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test" +import { Schema } from "effect" +import { Agent } from "../src/agent.js" + +test("Agent.Color preserves configured colors at the public boundary", () => { + const encode = Schema.encodeSync(Agent.Color) + + expect(encode("info")).toBe("info") + expect(encode("custom-color")).toBe("custom-color") +}) From d86f732df325a4d5933b47a34c5759cdad784117 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 21:33:25 -0400 Subject: [PATCH 08/27] refactor(tui): generate syntax from V2 theme (#38397) --- packages/tui/src/context/theme.tsx | 6 +- packages/tui/src/theme/v2/syntax.ts | 93 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 packages/tui/src/theme/v2/syntax.ts diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index aff085e969..16ffdeaccb 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -4,10 +4,8 @@ import { DEFAULT_THEMES, addTheme, allThemes, - generateSyntax, hasTheme, isTheme, - resolveTheme, selectedForeground, setCustomThemes, setSystemTheme, @@ -16,6 +14,7 @@ import { type Theme, type ThemeJson, } from "../theme" +import { generateSyntax } from "../theme/v2/syntax" import { generateSystem, terminalMode } from "../theme/system" import { discoverThemes, themeDirectories } from "../theme/discovery" import { createComponentTheme, type ComponentTheme } from "../theme/v2/component" @@ -279,7 +278,6 @@ const themeContext = createSimpleContext({ if (supported.includes(store.mode)) return store.mode return supported[0] ?? store.mode } - const legacySyntaxTheme = createMemo(() => resolveTheme(source(), mode())) const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) @@ -299,7 +297,7 @@ const themeContext = createSimpleContext({ createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) - const syntax = createSyntaxStyleMemo(() => generateSyntax(legacySyntaxTheme())) + const syntax = createSyntaxStyleMemo(() => generateSyntax(valuesV2(), mode())) function contextual(context: ContextName) { return contextualServices[context] } diff --git a/packages/tui/src/theme/v2/syntax.ts b/packages/tui/src/theme/v2/syntax.ts new file mode 100644 index 0000000000..4a8917f089 --- /dev/null +++ b/packages/tui/src/theme/v2/syntax.ts @@ -0,0 +1,93 @@ +import { SyntaxStyle, type RGBA, type ThemeTokenStyle } from "@opentui/core" +import type { Mode, ResolvedThemeView } from "./index" + +export function generateSyntax(theme: ResolvedThemeView, mode: Mode) { + const step = mode === "light" ? 800 : 200 + const syntax = theme.syntax + const markdown = theme.markdown + const feedback = theme.text.feedback + + return SyntaxStyle.fromTheme([ + rule(["default"], theme.text.default), + rule(["prompt"], theme.hue.accent[step]), + rule(["extmark.file"], feedback.warning.default, { bold: true }), + rule(["extmark.agent"], theme.categorical[0][step], { bold: true }), + // V1 migration preserves its selected/inverse foreground in this action state. + rule(["extmark.paste"], theme.text.action.primary.focused, { + background: feedback.warning.default, + bold: true, + }), + rule(["comment", "comment.documentation"], syntax.comment, { italic: true }), + rule(["string", "symbol", "character.special", "character"], syntax.string), + rule(["number", "boolean", "constant", "float"], syntax.number), + rule(["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"], syntax.keyword, { + italic: true, + }), + rule(["keyword.type"], syntax.type, { bold: true, italic: true }), + rule(["keyword.function", "function.method"], syntax.function), + rule(["keyword"], syntax.keyword, { italic: true }), + rule(["keyword.import", "string.escape", "string.regexp", "tag.attribute", "keyword.export"], syntax.keyword), + rule(["operator", "keyword.operator", "punctuation.delimiter", "keyword.conditional.ternary"], syntax.operator), + rule( + ["variable", "variable.parameter", "function.method.call", "function.call", "property", "parameter", "field"], + syntax.variable, + ), + rule(["variable.member", "function", "constructor"], syntax.function), + rule(["type", "module", "class", "namespace"], syntax.type), + rule(["type.definition"], syntax.type, { bold: true }), + rule(["punctuation", "punctuation.bracket"], syntax.punctuation), + rule( + ["variable.builtin", "type.builtin", "function.builtin", "module.builtin", "constant.builtin", "variable.super"], + feedback.error.default, + ), + rule(["keyword.directive", "keyword.modifier", "keyword.exception"], syntax.keyword, { italic: true }), + rule(["punctuation.special", "tag.delimiter"], syntax.operator), + rule( + [ + "markup.heading", + "markup.heading.2", + "markup.heading.3", + "markup.heading.4", + "markup.heading.5", + "markup.heading.6", + ], + markdown.heading, + { bold: true }, + ), + rule(["markup.heading.1"], markdown.heading, { bold: true, underline: true }), + rule(["markup.bold", "markup.strong"], markdown.strong, { bold: true }), + rule(["markup.italic"], markdown.emphasis, { italic: true }), + rule(["markup.list"], markdown.listItem), + rule(["markup.quote"], markdown.blockQuote, { italic: true }), + rule(["markup.raw", "markup.raw.block"], markdown.code), + rule(["markup.raw.inline"], markdown.code, { background: theme.background.default }), + rule(["markup.link", "markup.link.url", "string.special", "string.special.url"], markdown.link, { + underline: true, + }), + rule(["markup.link.label"], markdown.linkText, { underline: true }), + rule(["label"], markdown.linkText), + rule(["spell", "nospell"], theme.text.default), + rule(["markup.underline"], theme.text.default, { underline: true }), + rule(["comment.error"], feedback.error.default, { italic: true, bold: true }), + rule(["comment.warning"], feedback.warning.default, { italic: true, bold: true }), + rule(["comment.todo", "comment.note"], feedback.info.default, { italic: true, bold: true }), + rule(["attribute", "annotation"], feedback.warning.default), + rule(["tag"], feedback.error.default), + rule(["markup.strikethrough", "markup.list.unchecked", "debug"], theme.text.subdued), + rule(["markup.list.checked"], feedback.success.default), + rule(["diff.plus"], theme.diff.text.added, { background: theme.diff.background.added }), + rule(["diff.minus"], theme.diff.text.removed, { background: theme.diff.background.removed }), + rule(["diff.delta"], theme.diff.text.context, { background: theme.diff.background.context }), + rule(["error"], feedback.error.default, { bold: true }), + rule(["warning"], feedback.warning.default, { bold: true }), + rule(["info"], feedback.info.default), + ]) +} + +function rule( + scope: string[], + foreground: RGBA, + style: Omit = {}, +): ThemeTokenStyle { + return { scope, style: { foreground, ...style } } +} From 48bcbd09efe4eda454ed862909956f60e94f064f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:49:08 -0500 Subject: [PATCH 09/27] fix(ai): handle incomplete responses without reasons (#38374) --- packages/ai/src/protocols/openai-responses.ts | 5 ++-- .../ai/test/provider/openai-responses.test.ts | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 87ca8c75e1..2973acf000 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -253,7 +253,7 @@ const OpenAIResponsesEvent = Schema.Struct({ Schema.Struct({ id: Schema.optional(Schema.String), service_tier: optionalNull(Schema.String), - incomplete_details: optionalNull(Schema.Struct({ reason: Schema.String })), + incomplete_details: optionalNull(Schema.Struct({ reason: Schema.optional(Schema.String) })), usage: optionalNull(OpenAIResponsesUsage), error: optionalNull(OpenAIResponsesErrorPayload), }), @@ -602,7 +602,8 @@ const mapUsage = (usage: OpenAIResponsesUsage | null | undefined) => { const mapFinishReason = (event: OpenAIResponsesEvent, hasFunctionCall: boolean): FinishReason => { const reason = event.response?.incomplete_details?.reason - if (reason === undefined || reason === null) return hasFunctionCall ? "tool-calls" : "stop" + if (reason === undefined || reason === null) + return hasFunctionCall ? "tool-calls" : event.type === "response.incomplete" ? "unknown" : "stop" if (reason === "max_output_tokens") return "length" if (reason === "content_filter") return "content-filter" return hasFunctionCall ? "tool-calls" : "unknown" diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 11466a6896..ffbd329434 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -870,6 +870,32 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("maps incomplete response reasons", () => + Effect.gen(function* () { + const generate = (incompleteDetails: object) => + LLMClient.generate(request).pipe( + Effect.provide( + fixedResponse( + sseEvents({ + type: "response.incomplete", + response: { id: "resp_incomplete", incomplete_details: incompleteDetails }, + }), + ), + ), + ) + + const length = yield* generate({ reason: "max_output_tokens" }) + const contentFilter = yield* generate({ reason: "content_filter" }) + const unknown = yield* generate({}) + + expect([length.finishReason, contentFilter.finishReason, unknown.finishReason]).toEqual([ + "length", + "content-filter", + "unknown", + ]) + }), + ) + // OpenAI's documented stream orders output text within one message item; no // provider-valid same-kind overlap is evidenced, so done boundaries close it. it.effect("closes sequential output messages before starting the next", () => From 203b9f59b73b695664d08834fba98fb630ca3421 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 22 Jul 2026 22:26:08 -0400 Subject: [PATCH 10/27] fix(core): load dynamic models for generation (#38401) --- packages/core/src/generate.ts | 80 ++-- packages/core/src/location-services.ts | 2 + packages/core/src/model-resolver.ts | 344 ++++++++++++++++++ .../core/src/plugin/provider/openai-codex.ts | 2 +- packages/core/src/session/runner/model.ts | 344 ++---------------- packages/core/test/generate.test.ts | 111 ++++++ ...r-model.test.ts => model-resolver.test.ts} | 130 ++----- packages/core/test/session-compact.test.ts | 17 +- packages/core/test/session-generate.test.ts | 17 +- .../core/test/session-runner-recorded.test.ts | 17 +- packages/core/test/session-runner.test.ts | 21 +- packages/core/test/tool-search.test.ts | 4 +- 12 files changed, 586 insertions(+), 503 deletions(-) create mode 100644 packages/core/src/model-resolver.ts create mode 100644 packages/core/test/generate.test.ts rename packages/core/test/{session-runner-model.test.ts => model-resolver.test.ts} (80%) diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index f4a79e9aa2..432827bce0 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -2,12 +2,10 @@ export * as Generate from "./generate" import { LLM, LLMClient, LLMError } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema } from "effect" -import { Catalog } from "./catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "./effect/app-node-platform" -import { Integration } from "./integration" +import { ModelResolver } from "./model-resolver" import { ModelV2 } from "./model" -import { SessionRunnerModel } from "./session/runner/model" export interface TextInput { readonly prompt: string @@ -19,10 +17,10 @@ export class ModelSelectionError extends Schema.TaggedErrorClass()( - "Generate.UnavailableError", - { message: Schema.String, service: Schema.optional(Schema.String) }, -) {} +export class UnavailableError extends Schema.TaggedErrorClass()("Generate.UnavailableError", { + message: Schema.String, + service: Schema.optional(Schema.String), +}) {} export type Error = ModelSelectionError | UnavailableError @@ -35,56 +33,34 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - const catalog = yield* Catalog.Service - const integrations = yield* Integration.Service const llm = yield* LLMClient.Service - - const selectModel = Effect.fn("Generate.selectModel")(function* (requested?: ModelV2.Ref) { - const selected = requested - ? yield* catalog.model.get(requested.providerID, requested.id) - : yield* catalog.model.default().pipe( - Effect.flatMap((model) => - model && SessionRunnerModel.supported(model) - ? Effect.succeed(model) - : Effect.map(catalog.model.available(), (models) => models.find(SessionRunnerModel.supported)), - ), - ) - if (!selected) - return yield* new ModelSelectionError({ - message: requested - ? `Model unavailable: ${requested.providerID}/${requested.id}` - : "No model specified and no supported model is available", - }) - return yield* SessionRunnerModel.withVariant(selected, requested?.variant).pipe( - Effect.mapError( - () => - new ModelSelectionError({ - message: `Variant unavailable for ${selected.providerID}/${selected.id}: ${requested?.variant}`, - }), - ), - ) - }) + const resolver = yield* ModelResolver.Service const runText = Effect.fn("Generate.text")(function* (input: TextInput) { - const selected = yield* selectModel(input.model) - const provider = yield* catalog.provider.get(selected.providerID) - const connection = yield* integrations.connection.active( - provider?.integrationID ?? Integration.ID.make(selected.providerID), + const resolved = yield* resolver.resolve(input.model).pipe( + Effect.catchTags({ + "SessionRunnerModel.VariantUnavailableError": (error) => + input.model + ? new ModelSelectionError({ message: error.message }) + : new UnavailableError({ message: error.message, service: error.providerID }), + "SessionRunnerModel.UnsupportedPackageError": (error) => + input.model + ? new ModelSelectionError({ message: error.message }) + : new UnavailableError({ message: error.message, service: error.providerID }), + }), ) - const credential = connection ? yield* integrations.connection.resolve(connection) : undefined - const model = yield* SessionRunnerModel.fromCatalogModel(selected, credential).pipe( - Effect.mapError((error) => - input.model - ? new ModelSelectionError({ message: error.message }) - : new UnavailableError({ message: error.message, service: selected.providerID }), - ), - ) - const response = yield* llm.generate(LLM.request({ model, prompt: input.prompt })).pipe( + if (!resolved) + return yield* new ModelSelectionError({ + message: input.model + ? `Model unavailable: ${input.model.providerID}/${input.model.id}` + : "No model specified and no supported model is available", + }) + const response = yield* llm.generate(LLM.request({ model: resolved.model, prompt: input.prompt })).pipe( Effect.mapError( (error: LLMError) => new UnavailableError({ message: error.message, - service: selected.providerID, + service: resolved.ref.providerID, }), ), ) @@ -106,4 +82,8 @@ export const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, Integration.node, llmClient] }) +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ModelResolver.node, llmClient], +}) diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 7be9fefe3f..adf1afe62b 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -20,6 +20,7 @@ import { Integration } from "./integration" import { Location } from "./location" import { LocationMutation } from "./location-mutation" import { LocationServiceMap } from "./location-service-map" +import { ModelResolver } from "./model-resolver" import { MCP } from "./mcp/index" import { PermissionV2 } from "./permission" import { PluginV2 } from "./plugin" @@ -58,6 +59,7 @@ const locationServiceNodes = [ Reference.node, Integration.node, Catalog.node, + ModelResolver.node, AISDK.node, PluginV2.node, PluginSupervisor.node, diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts new file mode 100644 index 0000000000..44faea8b12 --- /dev/null +++ b/packages/core/src/model-resolver.ts @@ -0,0 +1,344 @@ +export * as ModelResolver from "./model-resolver" + +import { makeLocationNode } from "@opencode-ai/util/effect/app-node" +import { Model } from "@opencode-ai/ai" +// ast-grep-ignore: no-star-import +import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" +// ast-grep-ignore: no-star-import +import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat" +// ast-grep-ignore: no-star-import +import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses" +import { Auth, type AnyRoute } from "@opencode-ai/ai/route" +import { Context, Effect, Layer, Schema } from "effect" +import { produce } from "immer" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { Credential } from "./credential" +import { Integration } from "./integration" +import { ModelV2 } from "./model" +import { Npm } from "@opencode-ai/util/npm" +import { OpenAICodex } from "./plugin/provider/openai-codex" +import { ProviderV2 } from "./provider" + +export class VariantUnavailableError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.VariantUnavailableError", + { + providerID: ProviderV2.ID, + modelID: ModelV2.ID, + variant: ModelV2.VariantID, + }, +) { + override get message() { + return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}` + } +} + +export class UnsupportedPackageError extends Schema.TaggedErrorClass()( + "SessionRunnerModel.UnsupportedPackageError", + { + providerID: ProviderV2.ID, + modelID: ModelV2.ID, + package: Schema.String, + }, +) { + override get message() { + return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}` + } +} + +export type Error = VariantUnavailableError | UnsupportedPackageError | Integration.AuthorizationError + +export interface Resolved { + /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ + readonly model: Model + /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ + readonly ref: ModelV2.Ref + /** Catalog capabilities used to shape requests before provider lowering. */ + readonly capabilities: ModelV2.Capabilities + /** Catalog pricing in dollars per million tokens. */ + readonly cost: ModelV2.Info["cost"] +} + +export interface Interface { + readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect + readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ModelResolver") {} + +const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { + if (credential?.type === "key") return Auth.value(credential.key) + if (credential?.type === "oauth") return Auth.value(credential.access) + const value = model.settings?.apiKey + if (typeof value === "string") return Auth.value(value) + return undefined +} + +const withDefaults = (model: ModelV2.Info, route: AnyRoute) => + route.with({ + provider: model.providerID, + endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, + headers: providerHeaders(model), + providerOptions: providerOptions(model), + http: model.body === undefined ? undefined : { body: model.body }, + limits: { context: model.limit.context, output: model.limit.output }, + }) + +const providerHeaders = (model: ModelV2.Info) => { + const packageName = ProviderV2.packageName(model.package) + const generated = new Map() + if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") + generated.set("OpenAI-Organization", model.settings.organization) + if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string") + generated.set("OpenAI-Project", model.settings.project) + if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") + generated.set("Authorization", `Bearer ${model.settings.authToken}`) + return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) +} + +const providerOptions = ( + model: ModelV2.Info, +): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { + if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined + const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings + if (Object.keys(settings).length === 0) return undefined + const packageName = ProviderV2.packageName(model.package) + if (packageName === "@ai-sdk/openai") return { openai: settings } + if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } + if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } + return undefined +} + +export const withVariant = ( + model: ModelV2.Info, + variantID: ModelV2.VariantID | undefined, +): Effect.Effect => { + const id = variantID === "default" ? undefined : variantID + const variant = model.variants?.find((item) => item.id === id) + if (!variant && variantID !== undefined && variantID !== "default") + return Effect.fail( + new VariantUnavailableError({ + providerID: model.providerID, + modelID: model.id, + variant: variantID, + }), + ) + return Effect.succeed( + variant + ? produce(model, (draft) => { + draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) + draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) + draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) + }) + : model, + ) +} + +export interface Dependencies { + readonly loadPackage?: (specifier: string) => Effect.Effect + readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect +} + +export const fromCatalogModel = ( + model: ModelV2.Info, + credential?: Credential.Value, + dependencies?: Dependencies, +): Effect.Effect => { + const resolved = produce(model, (draft) => { + if (draft.settings?.apiKey === "") delete draft.settings.apiKey + if (credential?.type === "key" && credential.metadata !== undefined) + draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) + }) + const packageName = ProviderV2.packageName(resolved.package) + const key = apiKey(resolved, credential) + + if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { + return Effect.succeed(codexModel(resolved, credential, key)) + } + + if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { + if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) + return Effect.succeed( + withDefaults(resolved, OpenAIResponses.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { + return Effect.succeed( + withDefaults(resolved, AnthropicMessages.route) + .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if ( + ProviderV2.isAISDK(resolved.package) && + packageName === "@ai-sdk/openai-compatible" && + typeof resolved.settings?.baseURL === "string" + ) { + return Effect.succeed( + withDefaults(resolved, OpenAICompatibleChat.route) + .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) + .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), + ) + } + if (ProviderV2.isAISDK(resolved.package)) { + if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) + const runtime = produce(resolved, (draft) => { + draft.settings = ProviderV2.mergeOverlay(draft.settings, { + ...(credential?.type === "key" ? { apiKey: credential.key } : {}), + ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), + ...credential?.metadata, + }) + }) + return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved))) + } + if (!resolved.package) return Effect.fail(unsupported(resolved)) + + const specifier = resolved.package + return Effect.gen(function* () { + const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( + Effect.mapError(() => unsupported(resolved)), + ) + const configured = { ...resolved.settings, ...credential?.metadata } + const settings = { + ...(credential ? withoutNativeAuthSettings(configured) : configured), + ...nativeCredentialSettings(specifier, credential), + headers: resolved.headers, + body: resolved.body, + limits: { context: resolved.limit.context, output: resolved.limit.output }, + } + return yield* Effect.try({ + try: () => { + const runtime = module.model(resolved.modelID ?? resolved.id, settings) + return Model.update(runtime, { + provider: resolved.providerID, + compatibility: resolved.compatibility + ? Object.assign({}, runtime.compatibility, resolved.compatibility) + : runtime.compatibility, + }) + }, + catch: () => unsupported(resolved), + }) + }) +} + +const isNativeOpenAI = (packageName: string | undefined) => + packageName === "@opencode-ai/ai/providers/openai" || + packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true + +const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { + if (!credential) return {} + if (credential.type === "key") return { apiKey: credential.key } + if ( + specifier === "@opencode-ai/ai/providers/anthropic" || + specifier === "@opencode-ai/ai/providers/anthropic-compatible" + ) + return { authToken: credential.access } + if ( + specifier === "@opencode-ai/ai/providers/google-vertex" || + specifier.startsWith("@opencode-ai/ai/providers/google-vertex/") + ) + return { accessToken: credential.access } + return { apiKey: credential.access } +} + +const withoutNativeAuthSettings = (settings: Record) => { + const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings + return rest +} + +const codexModel = ( + model: ModelV2.Info, + credential: Credential.Value | undefined, + key: ReturnType | undefined, +) => { + const account = OpenAICodex.accountID(credential) + return withDefaults(model, OpenAIResponses.route) + .with({ + endpoint: { baseURL: OpenAICodex.baseURL }, + auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( + account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), + ), + }) + .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) +} + +const unsupported = (model: ModelV2.Info) => + new UnsupportedPackageError({ + providerID: model.providerID, + modelID: model.id, + package: model.package ?? "unknown", + }) + +export const resolveModel = ( + model: ModelV2.Info, + variant: ModelV2.VariantID | undefined, + credential?: Credential.Value, + dependencies?: Dependencies, +) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies))) + +export const supported = (model: ModelV2.Info) => Boolean(model.package) + +/** Resolves catalog selections into runtime models for the current Location. */ +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const integrations = yield* Integration.Service + const npm = yield* Npm.Service + const aisdk = yield* AISDK.Service + const load = Effect.fn("ModelResolver.resolveModel")(function* ( + selected: ModelV2.Info, + variant?: ModelV2.VariantID, + ) { + const provider = yield* catalog.provider.get(selected.providerID) + const connection = yield* integrations.connection.active( + provider?.integrationID ?? Integration.ID.make(selected.providerID), + ) + const model = yield* resolveModel( + selected, + variant, + connection ? yield* integrations.connection.resolve(connection) : undefined, + { + loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), + loadAISDK: (model) => aisdk.model(model), + }, + ) + return { + model, + ref: ModelV2.Ref.make({ + id: selected.id, + providerID: selected.providerID, + ...(variant === undefined ? {} : { variant }), + }), + capabilities: selected.capabilities, + cost: selected.cost, + } + }) + return Service.of({ + resolve: Effect.fn("ModelResolver.resolve")(function* (requested) { + const selected = requested + ? yield* catalog.model.get(requested.providerID, requested.id) + : yield* catalog.model + .default() + .pipe( + Effect.flatMap((model) => + model && supported(model) + ? Effect.succeed(model) + : Effect.map(catalog.model.available(), (models) => models.find(supported)), + ), + ) + if (!selected) return undefined + return yield* load(selected, requested?.variant) + }), + resolveModel: load, + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Catalog.node, Integration.node, Npm.node, AISDK.node], +}) diff --git a/packages/core/src/plugin/provider/openai-codex.ts b/packages/core/src/plugin/provider/openai-codex.ts index 8d4389d969..eb734b29f3 100644 --- a/packages/core/src/plugin/provider/openai-codex.ts +++ b/packages/core/src/plugin/provider/openai-codex.ts @@ -1,7 +1,7 @@ export * as OpenAICodex from "./openai-codex" // TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so -// codex routing lives in SessionRunnerModel.fromCatalogModel and catalog filtering +// Codex routing lives in ModelResolver and catalog filtering. // in OpenAIPlugin, sharing this module. Once the native provider packages land // (#33689/#33925/#34462) this should collapse into the native OpenAI provider. // The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 6013ce7b74..d0902d7d67 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -2,30 +2,16 @@ export * as SessionRunnerModel from "./model" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Model } from "@opencode-ai/ai" -// ast-grep-ignore: no-star-import -import * as AnthropicMessages from "@opencode-ai/ai/protocols/anthropic-messages" -// ast-grep-ignore: no-star-import -import * as OpenAICompatibleChat from "@opencode-ai/ai/protocols/openai-compatible-chat" -// ast-grep-ignore: no-star-import -import * as OpenAIResponses from "@opencode-ai/ai/protocols/openai-responses" -import { Auth, type AnyRoute } from "@opencode-ai/ai/route" import { Context, Effect, Layer, Schema } from "effect" -import { produce } from "immer" -import { AISDK } from "../../aisdk" import { Catalog } from "../../catalog" -import { Credential } from "../../credential" -import { Integration } from "../../integration" +import { ModelResolver } from "../../model-resolver" import { ModelV2 } from "../../model" -import { Npm } from "@opencode-ai/util/npm" -import { OpenAICodex } from "../../plugin/provider/openai-codex" import { ProviderV2 } from "../../provider" import { SessionSchema } from "../schema" export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelNotSelectedError", - { - sessionID: SessionSchema.ID, - }, + { sessionID: SessionSchema.ID }, ) { override get message() { return `No model is available for session ${this.sessionID}` @@ -34,59 +20,19 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelUnavailableError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - }, + { providerID: ProviderV2.ID, modelID: ModelV2.ID }, ) { override get message() { return `Model unavailable: ${this.providerID}/${this.modelID}` } } +export const VariantUnavailableError = ModelResolver.VariantUnavailableError +export type VariantUnavailableError = ModelResolver.VariantUnavailableError +export const UnsupportedPackageError = ModelResolver.UnsupportedPackageError +export type UnsupportedPackageError = ModelResolver.UnsupportedPackageError -export class VariantUnavailableError extends Schema.TaggedErrorClass()( - "SessionRunnerModel.VariantUnavailableError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - variant: ModelV2.VariantID, - }, -) { - override get message() { - return `Variant unavailable for ${this.providerID}/${this.modelID}: ${this.variant}` - } -} - -export class UnsupportedPackageError extends Schema.TaggedErrorClass()( - "SessionRunnerModel.UnsupportedPackageError", - { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - package: Schema.String, - }, -) { - override get message() { - return `Unsupported package for ${this.providerID}/${this.modelID}: ${this.package}` - } -} - -export type Error = - | ModelNotSelectedError - | ModelUnavailableError - | VariantUnavailableError - | UnsupportedPackageError - | Integration.AuthorizationError - -export interface Resolved { - /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ - readonly model: Model - /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ - readonly ref: ModelV2.Ref - /** Catalog capabilities used to shape requests before provider lowering. */ - readonly capabilities: ModelV2.Capabilities - /** Catalog pricing in dollars per million tokens. */ - readonly cost: ModelV2.Info["cost"] -} +export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolver.Error +export type Resolved = ModelResolver.Resolved export interface Interface { readonly resolve: (session: SessionSchema.Info) => Effect.Effect @@ -94,9 +40,6 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionRunnerModel") {} -/** Test or embedding seam for supplying a model resolver directly. */ -export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve })) - /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */ export const resolved = ( model: Model, @@ -116,276 +59,31 @@ export const resolved = ( cost: options.cost, }) -const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { - if (credential?.type === "key") return Auth.value(credential.key) - if (credential?.type === "oauth") return Auth.value(credential.access) - const value = model.settings?.apiKey - if (typeof value === "string") return Auth.value(value) -} - -const withDefaults = (model: ModelV2.Info, route: AnyRoute) => - route.with({ - provider: model.providerID, - endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, - headers: providerHeaders(model), - providerOptions: providerOptions(model), - http: model.body === undefined ? undefined : { body: model.body }, - limits: { context: model.limit.context, output: model.limit.output }, - }) - -const providerHeaders = (model: ModelV2.Info) => { - const packageName = ProviderV2.packageName(model.package) - const generated = new Map() - if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") - generated.set("OpenAI-Organization", model.settings.organization) - if (packageName === "@ai-sdk/openai" && typeof model.settings?.project === "string") - generated.set("OpenAI-Project", model.settings.project) - if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") - generated.set("Authorization", `Bearer ${model.settings.authToken}`) - return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) -} - -const providerOptions = ( - model: ModelV2.Info, -): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { - if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined - const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings - if (Object.keys(settings).length === 0) return undefined - const packageName = ProviderV2.packageName(model.package) - if (packageName === "@ai-sdk/openai") return { openai: settings } - if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } - if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } -} - -export const withVariant = ( - model: ModelV2.Info, - variantID: ModelV2.VariantID | undefined, -): Effect.Effect => { - const id = variantID === "default" ? undefined : variantID - const variant = model.variants?.find((item) => item.id === id) - if (!variant && variantID !== undefined && variantID !== "default") - return Effect.fail( - new VariantUnavailableError({ - providerID: model.providerID, - modelID: model.id, - variant: variantID, - }), - ) - return Effect.succeed( - variant - ? produce(model, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) - draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) - draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) - }) - : model, - ) -} - -export interface Dependencies { - readonly loadPackage?: (specifier: string) => Effect.Effect - readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect -} - -export const fromCatalogModel = ( - model: ModelV2.Info, - credential?: Credential.Value, - dependencies: Dependencies = {}, -): Effect.Effect => { - const resolved = produce(model, (draft) => { - if (draft.settings?.apiKey === "") delete draft.settings.apiKey - if (credential?.type === "key" && credential.metadata !== undefined) - draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) - }) - const packageName = ProviderV2.packageName(resolved.package) - const key = apiKey(resolved, credential) - - if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { - return Effect.succeed(codexModel(resolved, credential, key)) - } - - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { - if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) - return Effect.succeed( - withDefaults(resolved, OpenAIResponses.route) - .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { - return Effect.succeed( - withDefaults(resolved, AnthropicMessages.route) - .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if ( - ProviderV2.isAISDK(resolved.package) && - packageName === "@ai-sdk/openai-compatible" && - typeof resolved.settings?.baseURL === "string" - ) { - return Effect.succeed( - withDefaults(resolved, OpenAICompatibleChat.route) - .with({ auth: key === undefined ? Auth.none : Auth.bearer(key) }) - .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), - ) - } - if (ProviderV2.isAISDK(resolved.package)) { - if (!dependencies.loadAISDK) return Effect.fail(unsupported(resolved)) - const runtime = produce(resolved, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, { - ...(credential?.type === "key" ? { apiKey: credential.key } : {}), - ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), - ...credential?.metadata, - }) - }) - return dependencies.loadAISDK(runtime).pipe(Effect.mapError(() => unsupported(resolved))) - } - if (!resolved.package) return Effect.fail(unsupported(resolved)) - - const specifier = resolved.package - return Effect.gen(function* () { - const module = yield* (dependencies.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( - Effect.mapError(() => unsupported(resolved)), - ) - const configured = { ...resolved.settings, ...credential?.metadata } - const settings = { - ...(credential ? withoutNativeAuthSettings(configured) : configured), - ...nativeCredentialSettings(specifier, credential), - headers: resolved.headers, - body: resolved.body, - limits: { context: resolved.limit.context, output: resolved.limit.output }, - } - return yield* Effect.try({ - try: () => { - const runtime = module.model(resolved.modelID ?? resolved.id, settings) - return Model.update(runtime, { - provider: resolved.providerID, - compatibility: resolved.compatibility - ? { ...runtime.compatibility, ...resolved.compatibility } - : runtime.compatibility, - }) - }, - catch: () => unsupported(resolved), - }) - }) -} - -const isNativeOpenAI = (packageName: string | undefined) => - packageName === "@opencode-ai/ai/providers/openai" || - packageName?.startsWith("@opencode-ai/ai/providers/openai/") === true - -const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { - if (!credential) return {} - if (credential.type === "key") return { apiKey: credential.key } - if ( - specifier === "@opencode-ai/ai/providers/anthropic" || - specifier === "@opencode-ai/ai/providers/anthropic-compatible" - ) - return { authToken: credential.access } - if ( - specifier === "@opencode-ai/ai/providers/google-vertex" || - specifier.startsWith("@opencode-ai/ai/providers/google-vertex/") - ) - return { accessToken: credential.access } - return { apiKey: credential.access } -} - -const withoutNativeAuthSettings = (settings: Record) => { - const { accessToken: _accessToken, apiKey: _apiKey, authToken: _authToken, ...rest } = settings - return rest -} - -const codexModel = ( - model: ModelV2.Info, - credential: Credential.Value | undefined, - key: ReturnType | undefined, -) => { - const account = OpenAICodex.accountID(credential) - return withDefaults(model, OpenAIResponses.route) - .with({ - endpoint: { baseURL: OpenAICodex.baseURL }, - auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen( - account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }), - ), - }) - .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) -} - -const unsupported = (model: ModelV2.Info) => - new UnsupportedPackageError({ - providerID: model.providerID, - modelID: model.id, - package: model.package ?? "unknown", - }) - -export const resolve = ( - session: SessionSchema.Info, - model: ModelV2.Info, - credential?: Credential.Value, - dependencies?: Dependencies, -) => - withVariant(model, session.model?.variant).pipe( - Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies)), - ) - -export const supported = (model: ModelV2.Info) => Boolean(model.package) - -/** Resolves models from the catalog belonging to the current Location runtime. */ const layer = Layer.effect( Service, Effect.gen(function* () { const catalog = yield* Catalog.Service - const integrations = yield* Integration.Service - const npm = yield* Npm.Service - const aisdk = yield* AISDK.Service + const resolver = yield* ModelResolver.Service return Service.of({ resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { // Location plugins populate and filter the catalog asynchronously during layer startup. - const defaultModel = session.model ? undefined : yield* catalog.model.default() - const selected = session.model - ? (yield* catalog.model.available()).find( - (model) => model.providerID === session.model?.providerID && model.id === session.model.id, - ) - : defaultModel && supported(defaultModel) - ? defaultModel - : (yield* catalog.model.available()).find(supported) - if (!selected && session.model) + if (!session.model) { + const resolved = yield* resolver.resolve() + if (resolved) return resolved + return yield* new ModelNotSelectedError({ sessionID: session.id }) + } + const selected = (yield* catalog.model.available()).find( + (model) => model.providerID === session.model?.providerID && model.id === session.model.id, + ) + if (!selected) return yield* new ModelUnavailableError({ providerID: session.model.providerID, modelID: session.model.id, }) - if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id }) - const provider = yield* catalog.provider.get(selected.providerID) - const connection = yield* integrations.connection.active( - provider?.integrationID ?? Integration.ID.make(selected.providerID), - ) - const model = yield* resolve( - session, - selected, - connection ? yield* integrations.connection.resolve(connection) : undefined, - { - loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), - loadAISDK: (model) => aisdk.model(model), - }, - ) - return { - model, - ref: ModelV2.Ref.make({ - id: selected.id, - providerID: selected.providerID, - ...(session.model?.variant === undefined ? {} : { variant: session.model.variant }), - }), - capabilities: selected.capabilities, - cost: selected.cost, - } + return yield* resolver.resolveModel(selected, session.model.variant) }), }) }), ) -export const node = makeLocationNode({ - service: Service, - layer, - deps: [Catalog.node, Integration.node, Npm.node, AISDK.node], -}) +export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] }) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts new file mode 100644 index 0000000000..d990f96ace --- /dev/null +++ b/packages/core/test/generate.test.ts @@ -0,0 +1,111 @@ +import { expect } from "bun:test" +import { LLMClient, LLMEvent, LLMResponse, Model } from "@opencode-ai/ai" +import { OpenAIChat } from "@opencode-ai/ai/protocols" +import { AISDK } from "@opencode-ai/core/aisdk" +import { Catalog } from "@opencode-ai/core/catalog" +import { Generate } from "@opencode-ai/core/generate" +import { Integration } from "@opencode-ai/core/integration" +import { ModelResolver } from "@opencode-ai/core/model-resolver" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Npm } from "@opencode-ai/util/npm" +import { Effect, Layer, Stream } from "effect" +import { testEffect } from "./lib/effect" + +const selected = ModelV2.Info.make({ + ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), + package: ProviderV2.aisdk("@ai-sdk/google"), +}) +const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) + +const catalog = Layer.mock(Catalog.Service, { + provider: { + get: () => Effect.succeed(undefined), + all: () => Effect.die("unused"), + available: () => Effect.die("unused"), + }, + model: { + get: () => Effect.succeed(selected), + all: () => Effect.die("unused"), + available: () => Effect.die("unused"), + default: () => Effect.die("unused"), + small: () => Effect.die("unused"), + }, +}) +const integrations = Layer.mock(Integration.Service, { + connection: { + active: () => Effect.succeed(undefined), + resolve: () => Effect.die("unused"), + key: () => Effect.die("unused"), + update: () => Effect.die("unused"), + remove: () => Effect.die("unused"), + }, + oauth: { + connect: () => Effect.die("unused"), + status: () => Effect.die("unused"), + complete: () => Effect.die("unused"), + cancel: () => Effect.die("unused"), + }, + command: { + connect: () => Effect.die("unused"), + status: () => Effect.die("unused"), + cancel: () => Effect.die("unused"), + }, +}) +const npm = Layer.mock(Npm.Service, { + add: () => Effect.die("unused"), + install: () => Effect.die("unused"), + which: () => Effect.die("unused"), +}) +const aisdk = Layer.mock(AISDK.Service, { + hook: { + sdk: () => Effect.die("unused"), + language: () => Effect.die("unused"), + }, + model: () => Effect.succeed(runtime), +}) +const client = Layer.mock(LLMClient.Service)({ + prepare: () => Effect.die("unused"), + stream: () => Stream.die("unused"), + generate: () => + Effect.sync(() => { + const response = LLMResponse.fromEvents([ + LLMEvent.textStart({ id: "generate" }), + LLMEvent.textDelta({ id: "generate", text: "OK" }), + LLMEvent.textEnd({ id: "generate" }), + LLMEvent.finish({ reason: "stop" }), + ]) + if (!response) throw new Error("Incomplete generate response") + return response + }), +}) + +const resolver = ModelResolver.layer.pipe(Layer.provide(Layer.mergeAll(catalog, integrations, npm, aisdk))) +const it = testEffect(Generate.layer.pipe(Layer.provide(Layer.merge(resolver, client)))) +const resolverIt = testEffect(resolver) + +it.effect("loads dynamic AI SDK models", () => + Effect.gen(function* () { + const generate = yield* Generate.Service + const result = yield* generate.text({ + prompt: "Return exactly OK", + model: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + }) + + expect(result).toBe("OK") + }), +) + +resolverIt.effect("resolves dynamic models with their catalog metadata", () => + Effect.gen(function* () { + const resolver = yield* ModelResolver.Service + const result = yield* resolver.resolve(ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id })) + + expect(result).toEqual({ + model: runtime, + ref: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + capabilities: selected.capabilities, + cost: selected.cost, + }) + }), +) diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/model-resolver.test.ts similarity index 80% rename from packages/core/test/session-runner-model.test.ts rename to packages/core/test/model-resolver.test.ts index 38fc707918..9eedc4265b 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -1,17 +1,13 @@ import { describe, expect } from "bun:test" import { LLM, Model } from "@opencode-ai/ai" import { LLMClient } from "@opencode-ai/ai/route" -import { DateTime, Effect } from "effect" -import { Money } from "@opencode-ai/schema/money" +import { Effect } from "effect" import { Headers } from "effect/unstable/http" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" -import { ProjectV2 } from "@opencode-ai/core/project" -import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" -import { SessionV2 } from "@opencode-ai/core/session" -import { AbsolutePath } from "@opencode-ai/core/schema" +import { ModelResolver } from "@opencode-ai/core/model-resolver" import { it } from "./lib/effect" interface ModelOptions { @@ -43,13 +39,13 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => limit: { context: 100, output: 20 }, }) -describe("SessionRunnerModel", () => { +describe("ModelResolver", () => { it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const resolved = yield* SessionRunnerModel.fromCatalogModel(catalog) + const resolved = yield* ModelResolver.fromCatalogModel(catalog) expect(catalog.id).toBe(ModelV2.ID.make("test-model")) expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) @@ -68,7 +64,7 @@ describe("SessionRunnerModel", () => { it.effect("keeps catalog apiKey credentials out of provider JSON", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "secret", baseURL: "https://openai.example/v1" }, }), @@ -82,7 +78,7 @@ describe("SessionRunnerModel", () => { it.effect("treats an empty configured API key as omitted", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "", baseURL: "https://openai.example/v1" }, }), @@ -101,7 +97,7 @@ describe("SessionRunnerModel", () => { it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { compatibility: { reasoningField: "vendor_reasoning" }, settings: { @@ -130,7 +126,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected OpenAI Session variant settings and bodies", () => + it.effect("overlays selected OpenAI variant settings and bodies", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, @@ -147,22 +143,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_model_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { - id: catalog.id, - providerID: catalog.providerID, - variant: ModelV2.VariantID.make("high"), - }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) expect(resolved.route.defaults.http?.body).toEqual({ @@ -177,7 +158,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected OpenAI-compatible Session variant bodies", () => + it.effect("overlays selected OpenAI-compatible variant bodies", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { settings: { baseURL: "https://compatible.example/v1" }, @@ -190,18 +171,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_compatible_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -211,27 +181,12 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("rejects an explicit unavailable Session variant during model resolution", () => + it.effect("rejects an explicit unavailable variant during model resolution", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_model_variant_unavailable"), - projectID: ProjectV2.ID.global, - title: "test", - model: { - id: catalog.id, - providerID: catalog.providerID, - variant: ModelV2.VariantID.make("unknown"), - }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const failure = yield* SessionRunnerModel.resolve(session, catalog).pipe(Effect.flip) + const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.VariantUnavailableError", @@ -243,7 +198,7 @@ describe("SessionRunnerModel", () => { }), ) - it.effect("overlays selected Anthropic Session variant settings", () => + it.effect("overlays selected Anthropic variant settings", () => Effect.gen(function* () { const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, @@ -256,18 +211,7 @@ describe("SessionRunnerModel", () => { }, ], }) - const session = SessionV2.Info.make({ - id: SessionV2.ID.make("ses_anthropic_variant"), - projectID: ProjectV2.ID.global, - title: "test", - model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: Money.USD.zero, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, - location: { directory: AbsolutePath.make("/project") }, - }) - - const resolved = yield* SessionRunnerModel.resolve(session, catalog) + const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -280,7 +224,7 @@ describe("SessionRunnerModel", () => { it.effect("maps catalog Anthropic AI SDK models into native routes", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, }), @@ -296,7 +240,7 @@ describe("SessionRunnerModel", () => { it.effect("uses resolved credentials for bearer auth", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -320,7 +264,7 @@ describe("SessionRunnerModel", () => { it.effect("prefers stored credentials over configured auth", () => Effect.gen(function* () { const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" }, headers: {}, @@ -343,7 +287,7 @@ describe("SessionRunnerModel", () => { it.effect("does not project OAuth account metadata into the request body", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -365,7 +309,7 @@ describe("SessionRunnerModel", () => { it.effect("routes ChatGPT OAuth credentials to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -400,7 +344,7 @@ describe("SessionRunnerModel", () => { it.effect("routes native OpenAI provider packages with ChatGPT credentials to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/openai", { settings: { baseURL: "https://openai.example/v1" }, }), @@ -429,7 +373,7 @@ describe("SessionRunnerModel", () => { it.effect("does not route native OpenAI-compatible packages to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/openai-compatible", { settings: { baseURL: "https://compatible.example/v1" }, }), @@ -450,7 +394,7 @@ describe("SessionRunnerModel", () => { it.effect("maps legacy OpenAI organization and project settings to headers", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { organization: "org_123", project: "proj_123" }, }), @@ -465,7 +409,7 @@ describe("SessionRunnerModel", () => { it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -496,7 +440,7 @@ describe("SessionRunnerModel", () => { it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => Effect.gen(function* () { - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, @@ -528,12 +472,12 @@ describe("SessionRunnerModel", () => { it.effect("loads dynamic native provider packages through the injected package loader", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model("@opencode-ai/ai/providers/custom", { settings: { region: "test" }, headers: { "x-package": "header" }, @@ -565,7 +509,7 @@ describe("SessionRunnerModel", () => { it.effect("maps OAuth credentials to native provider auth settings", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), @@ -588,7 +532,7 @@ describe("SessionRunnerModel", () => { ] as const yield* Effect.forEach(packages, ([specifier, key]) => - SessionRunnerModel.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, { + ModelResolver.fromCatalogModel(model(specifier, { settings: { apiKey: "configured-key" } }), credential, { loadPackage: () => Effect.succeed({ model: (modelID, settings) => { @@ -604,12 +548,12 @@ describe("SessionRunnerModel", () => { it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - const resolved = yield* SessionRunnerModel.fromCatalogModel( + const resolved = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { modelID: "gemini-api-model", settings: { project: "test" }, @@ -644,7 +588,7 @@ describe("SessionRunnerModel", () => { it.effect("rejects AISDK packages without an available loader", () => Effect.gen(function* () { - const failure = yield* SessionRunnerModel.fromCatalogModel( + const failure = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { settings: { baseURL: "https://google.example/v1" }, }), @@ -662,12 +606,12 @@ describe("SessionRunnerModel", () => { it.effect("drops an empty API key before loading an AISDK package", () => Effect.gen(function* () { - const native = yield* SessionRunnerModel.fromCatalogModel( + const native = yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) - yield* SessionRunnerModel.fromCatalogModel( + yield* ModelResolver.fromCatalogModel( model(ProviderV2.aisdk("@ai-sdk/google"), { settings: { apiKey: "", baseURL: "https://google.example/v1" }, }), @@ -685,9 +629,9 @@ describe("SessionRunnerModel", () => { it.effect("reports whether a catalog model declares a provider package", () => Effect.sync(() => { - expect(SessionRunnerModel.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) - expect(SessionRunnerModel.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) - expect(SessionRunnerModel.supported(model(undefined))).toBe(false) + expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) + expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) + expect(ModelResolver.supported(model(undefined))).toBe(false) }), ) }) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index d37cc83c12..3ad15fc730 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -49,14 +49,15 @@ const client = Layer.mock(LLMClient.Service)({ generate: () => Effect.die("unused"), }) const config = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const locations = Layer.effect( LocationServiceMap.Service, LayerMap.make( diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 3f0183167c..98e7aedc0c 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -66,14 +66,15 @@ const client = Layer.mock(LLMClient.Service)({ return response }), }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const builtins = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed( diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index d26fa37df4..fb3619e4c6 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -73,14 +73,15 @@ const model = OpenAIChat.route generation: { maxTokens: 20, temperature: 0 }, }) .model({ id: "gpt-4o-mini" }) -const models = SessionRunnerModel.layerWith(() => - Effect.succeed( - SessionRunnerModel.resolved(model, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - }), - ), -) +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: () => + Effect.succeed( + SessionRunnerModel.resolved(model, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + }), + ), +}) const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.succeed(Instructions.empty) }) const instructionContext = Layer.mock(InstructionDiscovery.Service, { load: () => Effect.succeed(Instructions.empty) }) const skillInstructions = Layer.mock(SkillInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 6ce9cf0f20..86e9d7c576 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -280,17 +280,18 @@ const echo = Layer.effectDiscard( const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] }) let modelResolveHook = Effect.void let currentModel = model -const models = SessionRunnerModel.layerWith((session) => - modelResolveHook.pipe( - Effect.as( - SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, { - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - cost: [], - variant: session.model?.variant, - }), +const models = Layer.mock(SessionRunnerModel.Service)({ + resolve: (session) => + modelResolveHook.pipe( + Effect.as( + SessionRunnerModel.resolved(session.model?.id === "replacement" ? replacementModel : currentModel, { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + cost: [], + variant: session.model?.variant, + }), + ), ), - ), -) +}) const systemContextKey = Instructions.Key.make("test/context") let systemBaseline = "Initial context" let systemRemoved = false diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 41e25953c4..4367d1b82e 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -88,8 +88,8 @@ describe("search tools", () => { expect(glob.output?.structured).toEqual({ count: FileSystem.DEFAULT_SEARCH_LIMIT }) expect(grep.output?.structured).toEqual({ matches: FileSystem.DEFAULT_SEARCH_LIMIT }) - expect(glob.output?.content).toEqual([{ type: "text", text: glob.result.value }]) - expect(grep.output?.content).toEqual([{ type: "text", text: grep.result.value }]) + expect(glob.output?.content).toEqual([{ type: "text", text: String(glob.result.value) }]) + expect(grep.output?.content).toEqual([{ type: "text", text: String(grep.result.value) }]) expect(String(glob.result.value).split("\n")).toHaveLength(FileSystem.DEFAULT_SEARCH_LIMIT) expect(grep.result.value).toStartWith(`Found ${FileSystem.DEFAULT_SEARCH_LIMIT} matches\n`) }), From f1f0f47ee22ae2972a7acef1b5d6dbb7e0578d1e Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 22:38:13 -0400 Subject: [PATCH 11/27] fix(core): migrate named agent colors (#38414) --- packages/core/src/v1/config/agent.ts | 7 +++++-- packages/core/src/v1/config/migrate.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index 09838a9196..b220bd7ef8 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -4,7 +4,10 @@ import { Schema, SchemaGetter } from "effect" import { PositiveInt } from "../../schema" import { ConfigPermissionV1 } from "./permission" -const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) +const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) const AgentSchema = Schema.StructWithRest( Schema.Struct({ @@ -26,7 +29,7 @@ const AgentSchema = Schema.StructWithRest( }), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), color: Schema.optional(Color).annotate({ - description: "Hex color code (e.g., #FF5733)", + description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", }), steps: Schema.optional(PositiveInt).annotate({ description: "Maximum number of agentic iterations before forcing text-only response", diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 6c0a342abe..61a49f57cf 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -161,7 +161,7 @@ export function migrateAgent(info: ConfigAgentV1.Info) { description: info.description, mode: info.mode, hidden: info.hidden, - color: info.color, + color: info.color === undefined ? undefined : info.color.startsWith("#") ? info.color : "#aaaaaa", steps: info.steps, disabled: info.disable, permissions: permissions(info.permission), From 6e8aefcfa07fa49ea6c9988d371353b1b76d69f7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:42:53 -0500 Subject: [PATCH 12/27] fix(ai): normalize Bedrock cache usage (#38427) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/ai/src/protocols/bedrock-converse.ts | 19 +++---- packages/ai/src/schema/events.ts | 7 +-- ...s-cachepoint-on-identical-second-call.json | 53 +++++++++++++++++++ .../bedrock-converse-cache.recorded.test.ts | 22 +++++--- .../ai/test/provider/bedrock-converse.test.ts | 33 ++++++++++++ 5 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index 801fb8a98a..16052648a9 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -436,21 +436,22 @@ const mapFinishReason = (reason: string): FinishReason => { return "unknown" } -// AWS Bedrock Converse reports `inputTokens` (inclusive total) with -// `cacheReadInputTokens` and `cacheWriteInputTokens` as subsets. Pass -// the total through and derive the non-cached breakdown. Bedrock does -// not break reasoning out of `outputTokens` for any current model. +// AWS reports inputTokens separately from cache reads and writes. +// Bedrock does not break reasoning out of outputTokens for current models. const mapUsage = (usage: BedrockUsageSchema | undefined): Usage | undefined => { if (!usage) return undefined - const cacheTotal = (usage.cacheReadInputTokens ?? 0) + (usage.cacheWriteInputTokens ?? 0) - const nonCached = ProviderShared.subtractTokens(usage.inputTokens, cacheTotal) + const inputTokens = ProviderShared.sumTokens( + usage.inputTokens, + usage.cacheReadInputTokens, + usage.cacheWriteInputTokens, + ) return new Usage({ - inputTokens: usage.inputTokens, + inputTokens, outputTokens: usage.outputTokens, - nonCachedInputTokens: nonCached, + nonCachedInputTokens: usage.inputTokens, cacheReadInputTokens: usage.cacheReadInputTokens, cacheWriteInputTokens: usage.cacheWriteInputTokens, - totalTokens: ProviderShared.totalTokens(usage.inputTokens, usage.outputTokens, usage.totalTokens), + totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens), providerMetadata: { bedrock: usage }, }) } diff --git a/packages/ai/src/schema/events.ts b/packages/ai/src/schema/events.ts index 5be1c4cf90..18454b8470 100644 --- a/packages/ai/src/schema/events.ts +++ b/packages/ai/src/schema/events.ts @@ -34,11 +34,12 @@ import { ProviderFailureClassification } from "./errors" * * **Semantics by provider**: * - * - OpenAI Chat / Responses / Gemini / Bedrock: provider reports inclusive + * - OpenAI Chat / Responses / Gemini: provider reports inclusive * `inputTokens` and an inclusive `outputTokens`; mapper subtracts to * derive the breakdown. - * - Anthropic: provider reports the breakdown natively (`input_tokens` is - * non-cached only); mapper sums to derive the inclusive `inputTokens`. + * - Anthropic and Bedrock report the input breakdown natively: Anthropic's + * `input_tokens` and Bedrock's `inputTokens` are non-cached only. Their + * mappers sum the breakdown to derive the inclusive `inputTokens`. * Anthropic does *not* break extended-thinking out of `output_tokens`, so * `reasoningTokens` is `undefined` and `outputTokens` carries the * combined total — a documented limitation of the Anthropic API. diff --git a/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json b/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json new file mode 100644 index 0000000000..8fd307e220 --- /dev/null +++ b/packages/ai/test/fixtures/recordings/bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "metadata": { + "tags": [ + "prefix:bedrock-converse-cache", + "provider:amazon-bedrock", + "protocol:bedrock-converse", + "cache" + ], + "name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call", + "recordedAt": "2026-07-23T02:29:10.955Z" + }, + "interactions": [ + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Say hi.\"}]}],\"system\":[{\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"cachePoint\":{\"type\":\"default\"}}],\"inferenceConfig\":{\"maxTokens\":16,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAAiwAAAFImcW4yCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1uIiwicm9sZSI6ImFzc2lzdGFudCJ9uwonDAAAANUAAABX0TjrFws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJIaS4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNCJ9BToCUgAAAJMAAABWcYx2aAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ubyJ9uOXHGAAAALAAAABRaYm2Hws6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0SuCAcAAABgQAAAE6znPl5CzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MTE5OH0sInAiOiJhYmNkZWYiLCJ1c2FnZSI6eyJjYWNoZURldGFpbHMiOlt7ImlucHV0VG9rZW5zIjo1NzUyLCJ0dGwiOiI1bSJ9XSwiY2FjaGVSZWFkSW5wdXRUb2tlbkNvdW50IjowLCJjYWNoZVJlYWRJbnB1dFRva2VucyI6MCwiY2FjaGVXcml0ZUlucHV0VG9rZW5Db3VudCI6NTc1MiwiY2FjaGVXcml0ZUlucHV0VG9rZW5zIjo1NzUyLCJpbnB1dFRva2VucyI6OSwib3V0cHV0VG9rZW5zIjoyLCJzZXJ2ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6NTc2M319YVPHOQ==", + "bodyEncoding": "base64" + } + }, + { + "transport": "http", + "request": { + "method": "POST", + "url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-haiku-4-5-20251001-v1%3A0/converse-stream", + "headers": { + "content-type": "application/json" + }, + "body": "{\"modelId\":\"us.anthropic.claude-haiku-4-5-20251001-v1:0\",\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"Say hi.\"}]}],\"system\":[{\"text\":\"You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. You are a concise, factual assistant. Answer precisely and avoid filler. Cite numbers when known. \"},{\"cachePoint\":{\"type\":\"default\"}}],\"inferenceConfig\":{\"maxTokens\":16,\"temperature\":0}}" + }, + "response": { + "status": 200, + "headers": { + "content-type": "application/vnd.amazon.eventstream" + }, + "body": "AAAApgAAAFIfIIWHCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PIiwicm9sZSI6ImFzc2lzdGFudCJ9AcVkFwAAAKkAAABX7Rrm2Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJIaS4ifSwicCI6ImFiY2RlZmdoaWprbG0iffxI0NkAAACiAAAAVu3N514LOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0QifWQnKBAAAACFAAAAUQBIgekLOmV2ZW50LXR5cGUHAAttZXNzYWdlU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7InAiOiJhYmNkIiwic3RvcFJlYXNvbiI6ImVuZF90dXJuIn0c+t0FAAABTQAAAE6fefyjCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6OTcwfSwicCI6ImFiY2QiLCJ1c2FnZSI6eyJjYWNoZVJlYWRJbnB1dFRva2VuQ291bnQiOjU3NTIsImNhY2hlUmVhZElucHV0VG9rZW5zIjo1NzUyLCJjYWNoZVdyaXRlSW5wdXRUb2tlbkNvdW50IjowLCJjYWNoZVdyaXRlSW5wdXRUb2tlbnMiOjAsImlucHV0VG9rZW5zIjo5LCJvdXRwdXRUb2tlbnMiOjIsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5zIjo1NzYzfX0J7IoM", + "bodyEncoding": "base64" + } + } + ] +} diff --git a/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts b/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts index 8702e4eb40..8209ab1121 100644 --- a/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts +++ b/packages/ai/test/provider/bedrock-converse-cache.recorded.test.ts @@ -13,12 +13,8 @@ const RECORDING_REGION = process.env.BEDROCK_RECORDING_REGION ?? "us-east-1" // call wouldn't deterministically prove cache mapping works. Override with // BEDROCK_CACHE_MODEL_ID if your account has access elsewhere. const model = AmazonBedrock.configure({ - credentials: { - region: RECORDING_REGION, - accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "fixture", - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "fixture", - sessionToken: process.env.AWS_SESSION_TOKEN, - }, + apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture", + region: RECORDING_REGION, }).model(process.env.BEDROCK_CACHE_MODEL_ID ?? "us.anthropic.claude-haiku-4-5-20251001-v1:0") const cacheRequest = LLM.request({ @@ -36,7 +32,7 @@ const recorded = recordedTests({ prefix: "bedrock-converse-cache", provider: "amazon-bedrock", protocol: "bedrock-converse", - requires: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], + requires: ["AWS_BEARER_TOKEN_BEDROCK"], // Two identical requests in one cassette — replay walks the cassette in // recording order so the second call replays the cached-hit interaction. }) @@ -45,10 +41,20 @@ describe("Bedrock Converse cache recorded", () => { recorded.effect.with("writes then reads cachePoint on identical second call", { tags: ["cache"] }, () => Effect.gen(function* () { const first = yield* LLMClient.generate(cacheRequest) - expect(first.usage?.cacheReadInputTokens ?? 0).toBeGreaterThanOrEqual(0) + expect(first.usage?.cacheWriteInputTokens ?? 0).toBeGreaterThan(0) + expect(first.usage?.inputTokens).toBe( + (first.usage?.nonCachedInputTokens ?? 0) + + (first.usage?.cacheReadInputTokens ?? 0) + + (first.usage?.cacheWriteInputTokens ?? 0), + ) const second = yield* LLMClient.generate(cacheRequest) expect(second.usage?.cacheReadInputTokens ?? 0).toBeGreaterThan(0) + expect(second.usage?.inputTokens).toBe( + (second.usage?.nonCachedInputTokens ?? 0) + + (second.usage?.cacheReadInputTokens ?? 0) + + (second.usage?.cacheWriteInputTokens ?? 0), + ) }), ) }) diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 59ca67333b..776032966c 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -269,6 +269,39 @@ describe("Bedrock Converse route", () => { }), ) + it.effect("adds cache reads and writes to Bedrock input usage", () => + Effect.gen(function* () { + const body = eventStreamBody( + ["messageStart", { role: "assistant" }], + ["contentBlockDelta", { contentBlockIndex: 0, delta: { text: "Hello" } }], + ["contentBlockStop", { contentBlockIndex: 0 }], + ["messageStop", { stopReason: "end_turn" }], + [ + "metadata", + { + usage: { + inputTokens: 5, + outputTokens: 2, + totalTokens: 12, + cacheReadInputTokens: 3, + cacheWriteInputTokens: 2, + }, + }, + ], + ) + const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body))) + + expect(response.usage).toMatchObject({ + inputTokens: 10, + nonCachedInputTokens: 5, + cacheReadInputTokens: 3, + cacheWriteInputTokens: 2, + outputTokens: 2, + totalTokens: 12, + }) + }), + ) + it.effect("assembles streamed tool call input", () => Effect.gen(function* () { const body = eventStreamBody( From b6f85c2250ba81d826ea118a8256db53a7a7d8b3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:20:16 -0500 Subject: [PATCH 13/27] fix(core): default custom model capabilities (#38449) Co-authored-by: Aiden Cline --- packages/core/src/catalog.ts | 2 +- packages/core/src/github-copilot/models.ts | 2 +- packages/core/test/aisdk.test.ts | 2 +- packages/core/test/config/provider.test.ts | 80 +++++++++++++++++++ packages/core/test/generate.test.ts | 2 +- .../core/test/github-copilot/models.test.ts | 4 +- .../plugin/provider-amazon-bedrock.test.ts | 42 +++++----- .../test/plugin/provider-anthropic.test.ts | 4 +- .../provider-azure-cognitive-services.test.ts | 12 +-- .../core/test/plugin/provider-azure.test.ts | 18 ++--- .../test/plugin/provider-cerebras.test.ts | 6 +- .../provider-cloudflare-ai-gateway.test.ts | 22 ++--- .../provider-cloudflare-workers-ai.test.ts | 12 +-- .../core/test/plugin/provider-dynamic.test.ts | 16 ++-- .../core/test/plugin/provider-factory.test.ts | 2 +- .../plugin/provider-github-copilot.test.ts | 30 +++---- .../core/test/plugin/provider-gitlab.test.ts | 16 ++-- .../provider-google-vertex-anthropic.test.ts | 16 ++-- .../plugin/provider-google-vertex.test.ts | 8 +- .../core/test/plugin/provider-google.test.ts | 8 +- .../plugin/provider-openai-compatible.test.ts | 10 +-- .../core/test/plugin/provider-openai.test.ts | 8 +- .../test/plugin/provider-opencode.test.ts | 14 ++-- .../test/plugin/provider-openrouter.test.ts | 4 +- .../test/plugin/provider-sap-ai-core.test.ts | 2 +- .../plugin/provider-snowflake-cortex.test.ts | 12 +-- .../core/test/plugin/provider-vercel.test.ts | 2 +- .../core/test/plugin/provider-xai.test.ts | 10 +-- packages/core/test/shared-schema.test.ts | 4 +- packages/docs/models.mdx | 12 +-- packages/schema/src/model.ts | 4 +- packages/schema/test/contract-hygiene.test.ts | 2 +- 32 files changed, 235 insertions(+), 153 deletions(-) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 9f63444790..9b8f916e70 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -116,7 +116,7 @@ const layer = Layer.effect( draft.providers.set(providerID, record) } const model = - record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo) + record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo) if (!record.models.has(modelID)) record.models.set(modelID, model) fn(model) model.id = modelID diff --git a/packages/core/src/github-copilot/models.ts b/packages/core/src/github-copilot/models.ts index 8790cefba2..52f98753b5 100644 --- a/packages/core/src/github-copilot/models.ts +++ b/packages/core/src/github-copilot/models.ts @@ -135,7 +135,7 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: const released = previous?.time.released || Date.parse(version) return ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, id), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id), id, modelID: ModelV2.ID.make(remote.id), providerID: ProviderV2.ID.githubCopilot, diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 0bc16e8f59..9e7e0d0c6e 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -12,7 +12,7 @@ const it = testEffect(AISDK.locationLayer) const model = (packageName: string, settings: Record = {}) => ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), modelID: ModelV2.ID.make("api-model"), package: ProviderV2.aisdk(packageName), settings, diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index c3c5b9c420..60597836f2 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -49,6 +49,86 @@ function withEnv(vars: Record, effect: () = const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { + it.effect("defaults custom models to agent capabilities", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const modelID = ModelV2.ID.make("chat") + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { chat: {} }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + const model = required(yield* catalog.model.get(providerID, modelID)) + expect(model.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] }) + }), + ) + + it.effect("preserves catalog capabilities unless config overrides them", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const providerID = ProviderV2.ID.make("custom") + const inheritedID = ModelV2.ID.make("inherited") + const overriddenID = ModelV2.ID.make("overridden") + yield* catalog.transform((draft) => { + draft.model.update(providerID, inheritedID, (model) => { + model.capabilities = { tools: false, input: ["text"], output: ["text"] } + }) + draft.model.update(providerID, overriddenID, (model) => { + model.capabilities = { tools: false, input: ["text"], output: ["text"] } + }) + }) + const config = Config.Service.of({ + entries: () => + Effect.succeed([ + new Config.Document({ + type: "document", + info: decode({ + providers: { + custom: { + package: "aisdk:@ai-sdk/openai-compatible", + models: { + inherited: { name: "Inherited" }, + overridden: { + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, + }, + }, + }, + }, + }), + }), + ]), + }) + + yield* addPlugin(config) + + expect((yield* catalog.model.get(providerID, inheritedID))?.capabilities).toEqual({ + tools: false, + input: ["text"], + output: ["text"], + }) + expect((yield* catalog.model.get(providerID, overriddenID))?.capabilities).toEqual({ + tools: true, + input: ["text", "image"], + output: ["text"], + }) + }), + ) + it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index d990f96ace..c0ad30d383 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -13,7 +13,7 @@ import { Effect, Layer, Stream } from "effect" import { testEffect } from "./lib/effect" const selected = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), package: ProviderV2.aisdk("@ai-sdk/google"), }) const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index 31d8f710cd..9bd07971fd 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -49,12 +49,12 @@ test("defensively syncs advertised Copilot models", async () => { try { const existing = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), name: "GPT-5 local", }) const stale = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), + ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), modelID: ModelV2.ID.make("stale"), }) const models = await CopilotModels.get(server.url.origin, {}, [existing, stale]) diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index db63910c33..39f46c49a2 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -108,7 +108,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -134,7 +134,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -169,7 +169,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -190,7 +190,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -210,7 +210,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -230,7 +230,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -251,7 +251,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -281,7 +281,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -310,7 +310,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), modelID: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -338,7 +338,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), modelID: ModelV2.ID.make("openai.gpt-5.5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -347,7 +347,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), @@ -365,7 +365,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"), }), @@ -393,7 +393,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -425,7 +425,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -434,7 +434,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -443,7 +443,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -452,7 +452,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -461,7 +461,7 @@ describe("AmazonBedrockPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -487,7 +487,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -574,7 +574,7 @@ describe("AmazonBedrockPlugin", () => { for (const item of cases) { yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), + ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), modelID: ModelV2.ID.make(item.modelID), package: ProviderV2.aisdk("test-provider"), }), @@ -594,7 +594,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index af7b80dc67..0c0bcba461 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -63,7 +63,7 @@ describe("AnthropicPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/anthropic"), }), @@ -81,7 +81,7 @@ describe("AnthropicPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: ProviderV2.aisdk("@ai-sdk/anthropic"), }), diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index f52f013105..5b9ca4bf86 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -121,7 +121,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -140,7 +140,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -149,7 +149,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) const ignored = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: "aisdk:test-provider", }), @@ -170,7 +170,7 @@ describe("AzureCognitiveServicesPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), modelID: ModelV2.ID.make("messages-deployment"), package: "aisdk:test-provider", }), @@ -179,7 +179,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), modelID: ModelV2.ID.make("chat-deployment"), package: "aisdk:test-provider", }), @@ -188,7 +188,7 @@ describe("AzureCognitiveServicesPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), modelID: ModelV2.ID.make("language-deployment"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 6cf515c0c6..227e3bfa78 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -148,7 +148,7 @@ describe("AzurePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -168,7 +168,7 @@ describe("AzurePlugin", () => { const exit = yield* aisdk .runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -189,7 +189,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -208,7 +208,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -227,7 +227,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), body: { useCompletionUrls: true }, @@ -247,7 +247,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -256,7 +256,7 @@ describe("AzurePlugin", () => { }) const ignored = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), modelID: ModelV2.ID.make("deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -280,7 +280,7 @@ describe("AzurePlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), modelID: ModelV2.ID.make("messages-deployment"), package: ProviderV2.aisdk("test-provider"), }), @@ -289,7 +289,7 @@ describe("AzurePlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), + ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), modelID: ModelV2.ID.make("language-deployment"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index eb5c4ec1bf..6722f48996 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -65,7 +65,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), @@ -88,7 +88,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), @@ -110,7 +110,7 @@ describe("CerebrasPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), ), diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index cbd2b95c9e..bd9d9e80cd 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -117,7 +117,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -139,7 +139,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -184,7 +184,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -214,7 +214,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -252,7 +252,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -307,7 +307,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -331,7 +331,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -361,7 +361,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), @@ -385,7 +385,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("anthropic/claude-sonnet-4-5"), ), @@ -417,7 +417,7 @@ describe("CloudflareAIGatewayPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index d67b8d91cc..5f4e3bc276 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -94,7 +94,7 @@ describe("CloudflareWorkersAIPlugin", () => { const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) const sdk = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: provider.package, settings: provider.settings, @@ -138,7 +138,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, @@ -178,7 +178,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, @@ -207,7 +207,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" }, @@ -233,7 +233,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("@cf/api-model"), package: "aisdk:test-provider", }), @@ -253,7 +253,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), modelID: ModelV2.ID.make("@cf/model"), package: "aisdk:@ai-sdk/anthropic", settings: { baseURL: "https://proxy.example/v1" }, diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index d0b1a8af5a..341878e6de 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -53,7 +53,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -72,7 +72,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -90,7 +90,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: ProviderV2.aisdk(fixtureProvider), }), @@ -107,7 +107,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint(fixtureProviderPath)) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), + ...ModelV2.Info.default(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), modelID: ModelV2.ID.make("test-model"), package: "aisdk:fixture-provider", }), @@ -125,7 +125,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:fixture-provider", }), @@ -143,7 +143,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:file:///missing/provider-factory.js", }), @@ -163,7 +163,7 @@ describe("DynamicProviderPlugin", () => { const exit = yield* aisdk .language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("alias"), package: "aisdk:fixture-provider", }), @@ -181,7 +181,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const language = yield* aisdk.language( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("test-model-api"), package: ProviderV2.aisdk(fixtureProvider), }), diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts index a884fd0ce0..d51d4900b4 100644 --- a/packages/core/test/plugin/provider-factory.test.ts +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -41,7 +41,7 @@ providers.forEach((item) => const host = yield* PluginHost.make(plugin) yield* item.plugin.effect(host) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make(item.id), modelID), + ...ModelV2.Info.default(ProviderV2.ID.make(item.id), modelID), modelID, package: ProviderV2.aisdk(item.package), }) diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index bb2443ae41..b39e06ac7b 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -99,7 +99,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -108,7 +108,7 @@ describe("GithubCopilotPlugin", () => { }) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -128,7 +128,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -147,7 +147,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -166,7 +166,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -175,7 +175,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), modelID: ModelV2.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), @@ -184,7 +184,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), modelID: ModelV2.ID.make("gpt-4o"), package: "aisdk:test-provider", }), @@ -193,7 +193,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), @@ -202,7 +202,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"), package: "aisdk:test-provider", }), @@ -227,7 +227,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), modelID: ModelV2.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, @@ -237,7 +237,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", settings: { endpoint: "chat" }, @@ -257,7 +257,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), @@ -266,7 +266,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), modelID: ModelV2.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), @@ -275,7 +275,7 @@ describe("GithubCopilotPlugin", () => { }) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), + ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), modelID: ModelV2.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), @@ -324,7 +324,7 @@ describe("GithubCopilotPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index ae66a61aa8..ac12ebfc9d 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -69,7 +69,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -107,7 +107,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -133,7 +133,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -175,7 +175,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", }), @@ -195,7 +195,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), modelID: ModelV2.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, @@ -229,7 +229,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), modelID: ModelV2.ID.make("duo-workflow-exact"), package: "aisdk:test-provider", }), @@ -257,7 +257,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), modelID: ModelV2.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, @@ -284,7 +284,7 @@ describe("GitLabPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), modelID: ModelV2.ID.make("claude"), package: "aisdk:test-provider", headers: { h: "v" }, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 090abb945c..3b6726d373 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -116,7 +116,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), ), @@ -143,7 +143,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty( + ...ModelV2.Info.default( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), ), @@ -167,7 +167,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), @@ -187,7 +187,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), @@ -206,7 +206,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const sdkResult = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -215,7 +215,7 @@ describe("GoogleVertexAnthropicPlugin", () => { }) const languageResult = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -238,7 +238,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), @@ -257,7 +257,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), modelID: ModelV2.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 635beba3c1..15949b4945 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -172,7 +172,7 @@ describe("GoogleVertexPlugin", () => { const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), @@ -294,7 +294,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), @@ -339,7 +339,7 @@ describe("GoogleVertexPlugin", () => { () => aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/openai-compatible", }), @@ -367,7 +367,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), + ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), modelID: ModelV2.ID.make(" gemini-2.5-pro "), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index d04e00e3b3..2dea12a2e6 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -26,7 +26,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), @@ -45,7 +45,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), + ...ModelV2.Info.default(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), modelID: ModelV2.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), @@ -63,7 +63,7 @@ describe("GooglePlugin", () => { yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", }), @@ -88,7 +88,7 @@ describe("GooglePlugin", () => { const resolved = yield* aisdk.model( ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", settings: { apiKey: "test" }, diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index a954af9f00..e1cf1ed6c8 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -26,7 +26,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const defaulted = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -35,7 +35,7 @@ describe("OpenAICompatiblePlugin", () => { }) const disabled = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -54,7 +54,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -78,7 +78,7 @@ describe("OpenAICompatiblePlugin", () => { ) yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), @@ -99,7 +99,7 @@ describe("OpenAICompatiblePlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), + ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), modelID: ModelV2.ID.make("model"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index cd0c943442..741966eebe 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -68,7 +68,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -86,7 +86,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -105,7 +105,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -125,7 +125,7 @@ describe("OpenAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), modelID: ModelV2.ID.make("gpt-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 1de22a03dd..f0b9fff8d7 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -293,7 +293,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -320,7 +320,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("free")), modelID: ModelV2.ID.make("free"), package: ProviderV2.aisdk("test-provider"), cost: cost(0), @@ -347,7 +347,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("output-only")), modelID: ModelV2.ID.make("output-only"), package: ProviderV2.aisdk("test-provider"), cost: cost(0, 1), @@ -376,7 +376,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -410,7 +410,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -438,7 +438,7 @@ describe("OpencodePlugin", () => { settings: { apiKey: "configured" }, }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), @@ -468,7 +468,7 @@ describe("OpencodePlugin", () => { package: ProviderV2.aisdk("test-provider"), }) const model = ModelV2.Info.make({ - ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), + ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), modelID: ModelV2.ID.make("paid"), package: ProviderV2.aisdk("test-provider"), cost: cost(1), diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 21ee3ae216..63520e01d3 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -54,7 +54,7 @@ describe("OpenRouterPlugin", () => { const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: ProviderV2.aisdk("test-provider"), }), @@ -65,7 +65,7 @@ describe("OpenRouterPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), modelID: ModelV2.ID.make("openai/gpt-5"), package: ProviderV2.aisdk("test-provider"), }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index 26dc3ac86c..09d99867f2 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -48,7 +48,7 @@ function withEnv(vars: Record, effect: () = function model(providerID: string) { return ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), + ...ModelV2.Info.default(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), modelID: ModelV2.ID.make("sap-model"), package: ProviderV2.aisdk(fixtureProvider), }) diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 92af623756..749f4c5520 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -58,7 +58,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), modelID: ModelV2.ID.make("gpt-4"), package: "aisdk:test-provider", }), @@ -77,7 +77,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -97,7 +97,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -121,7 +121,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -141,7 +141,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), @@ -165,7 +165,7 @@ describe("SnowflakeCortexPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), modelID: ModelV2.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 46d5fe25b8..29820b714c 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -59,7 +59,7 @@ describe("VercelPlugin", () => { yield* addPlugin() const event = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), modelID: ModelV2.ID.make("v0-1.0-md"), package: "aisdk:@ai-sdk/vercel", }), diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index 04bfc508f7..4b3e672c1e 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -62,7 +62,7 @@ describe("XAIPlugin", () => { const ignored = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -72,7 +72,7 @@ describe("XAIPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -92,7 +92,7 @@ describe("XAIPlugin", () => { const result = yield* aisdk.runSDK({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -112,7 +112,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), + ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), @@ -133,7 +133,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ model: ModelV2.Info.make({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), + ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), modelID: ModelV2.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 37b67c63a8..937cd3e1f0 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -170,8 +170,8 @@ test("Core reuses the canonical shared schemas", async () => { for (const [core, shared] of schemas) expect(core).toBe(shared) expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test"))) - expect(Model.Info.empty(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( - ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), + expect(Model.Info.default(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( + ModelV2.Info.default(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), ) expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test"))) expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe( diff --git a/packages/docs/models.mdx b/packages/docs/models.mdx index 60fd63812a..213bfac523 100644 --- a/packages/docs/models.mdx +++ b/packages/docs/models.mdx @@ -94,9 +94,10 @@ You can also map a friendly catalog ID to a different API model ID with `modelID } ``` -Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. When adding a -model that is not already in the catalog, set accurate `capabilities` and `limit` values so OpenCode can expose tools and -enforce the correct context limits. Set `disabled: true` on a model entry to hide it from the available catalog. +Here `openai/coding-default` is the selectable catalog reference, while `gpt-5.2` is sent to the provider. A model that is +not already in the catalog defaults to tool support, text and image input, and text output. Set accurate `capabilities` +and `limit` values when those defaults do not match the model or OpenCode needs to enforce its context limits. Set +`disabled: true` on a model entry to hide it from the available catalog. OpenAI-compatible models that stream reasoning through a custom assistant-message field can set `compatibility.reasoningField`: @@ -193,8 +194,9 @@ For an OpenAI-compatible server, define a provider package, endpoint, and at lea } ``` -Use the server's real model name, limits, modalities, and tool support. OpenCode cannot infer these for a model you add -manually. If the endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as +Use the server's real model name, limits, modalities, and tool support. OpenCode applies the custom-model capability +defaults described above but cannot infer the server's actual limits or whether those defaults are accurate. If the +endpoint requires a key, add `apiKey` to provider `settings` using an environment substitution such as `"apiKey": "{env:LOCAL_API_KEY}"`; do not commit secrets. ### Model references diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index 81caea654f..2f9d1dd7cc 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -106,13 +106,13 @@ export const Info = Schema.Struct({ .annotate({ identifier: "Model.Info" }) .pipe( statics(() => ({ - empty: (providerID: Provider.ID, id: ID) => + default: (providerID: Provider.ID, id: ID) => ({ id, modelID: id, providerID, name: id, - capabilities: { tools: false, input: [], output: [] }, + capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, variants: [], time: { released: 0 }, cost: [], diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index c662870269..46784ee27d 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -83,7 +83,7 @@ describe("contract hygiene", () => { test("model defaults and provider overlays preserve public invariants", () => { const id = Model.ID.make("model") - expect(Model.Info.empty(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) + expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) expect(() => Schema.decodeUnknownSync(Provider.Info)({ id: "provider", From 52c98a4eeb927eefc07652abdd79eab1e1269e8f Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Thu, 23 Jul 2026 12:12:25 +0200 Subject: [PATCH 14/27] mini: add replay settings to cli config (#38487) --- packages/cli/src/commands/commands.ts | 6 +++--- packages/cli/src/commands/handlers/mini.ts | 4 ++-- packages/cli/test/mini.test.ts | 6 +++++- packages/tui/src/config/index.tsx | 6 ++++++ packages/tui/test/config-v2.test.tsx | 12 ++++++++++++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts index c0b914fc62..a3ae15113f 100644 --- a/packages/cli/src/commands/commands.ts +++ b/packages/cli/src/commands/commands.ts @@ -140,11 +140,11 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO Flag.withDefault(false), ), replay: Flag.boolean("replay").pipe( - Flag.withDescription("Replay session history on resume and after resize"), - Flag.withDefault(true), + Flag.withDescription("Restore session history on resume and resize (disable with --no-replay)"), + Flag.optional, ), replayLimit: Flag.integer("replay-limit").pipe( - Flag.withDescription("Cap visible replay to the newest N messages"), + Flag.withDescription("Limit replay to the newest N messages (default: 200)"), Flag.optional, ), model: Flag.string("model").pipe( diff --git a/packages/cli/src/commands/handlers/mini.ts b/packages/cli/src/commands/handlers/mini.ts index 4f8a16a350..972c3bf962 100644 --- a/packages/cli/src/commands/handlers/mini.ts +++ b/packages/cli/src/commands/handlers/mini.ts @@ -28,8 +28,8 @@ export default Runtime.handler(Commands.commands.mini, (input) => model: Option.getOrUndefined(input.model), agent: Option.getOrUndefined(input.agent), prompt: Option.getOrUndefined(input.prompt), - replay: input.replay, - replayLimit: Option.getOrUndefined(input.replayLimit), + replay: Option.getOrUndefined(input.replay) ?? resolved.mini?.replay ?? true, + replayLimit: Option.getOrUndefined(input.replayLimit) ?? resolved.mini?.replay_limit, demo: input.demo, tuiConfig: resolved, config: { diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts index 34cf2a2cd2..e9304d194f 100644 --- a/packages/cli/test/mini.test.ts +++ b/packages/cli/test/mini.test.ts @@ -214,11 +214,15 @@ describe("mini command", () => { expect(result.exitCode).toBe(0) expect(result.stdout).toContain("--server string") expect(result.stdout).toContain("--prompt string") + expect(result.stdout).toContain("--replay") + expect(result.stdout).toContain("disable with --no-replay") + expect(result.stdout).toContain("--replay-limit integer") + expect(result.stdout).toContain("Limit replay to the newest N messages (default: 200)") expect(result.stdout).not.toContain("SUBCOMMANDS") }) test("routes local and explicit-server invocations into mini", async () => { - for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) { + for (const args of [["mini"], ["mini", "--no-replay"], ["mini", "--server", "http://127.0.0.1:1"]]) { const result = await cli(args) expect(result.exitCode).toBe(1) diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 3d66818390..e078eb5113 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -142,6 +142,12 @@ export const Info = Schema.Struct({ mono: Schema.optional(Schema.Boolean).annotate({ description: "Use monochrome ASCII output", }), + replay: Schema.optional(Schema.Boolean).annotate({ + description: "Restore session history on resume and terminal resize", + }), + replay_limit: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))).annotate({ + description: "Maximum number of newest messages restored during replay", + }), }), ).annotate({ description: "Mini transcript presentation settings" }), hints: Schema.optional( diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index 7a663cc117..c349ea5224 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -1,13 +1,25 @@ /** @jsxImportSource @opentui/solid */ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" +import { Schema } from "effect" import { resolve, ConfigProvider, + Info, useConfig, type Interface, } from "../src/config" +test("validates mini replay settings", () => { + const decode = Schema.decodeUnknownSync(Info) + + expect(decode({ mini: { replay: false, replay_limit: 50 } })).toEqual({ + mini: { replay: false, replay_limit: 50 }, + }) + expect(() => decode({ mini: { replay_limit: 0 } })).toThrow() + expect(() => decode({ mini: { replay_limit: 1.5 } })).toThrow() +}) + test("resolves nested config and keybind defaults", () => { const config = resolve( { From 5b1321a8ca81bcedc19bfb7782472c35c5d77d38 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 10:00:51 -0400 Subject: [PATCH 15/27] feat(tui): add turn token usage diagnostics (#38398) --- packages/tui/src/component/devtools-bar.tsx | 13 ++- packages/tui/src/component/dialog-config.tsx | 8 -- packages/tui/src/config/index.tsx | 1 + packages/tui/src/routes/session/index.tsx | 114 ++++++++++++++++++- packages/tui/src/routes/session/rows.ts | 51 ++++++++- 5 files changed, 175 insertions(+), 12 deletions(-) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index fc168260a2..03373110e2 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -59,6 +59,7 @@ export function DevToolsBar() { const canSwitchMode = () => supports(nextMode()) const runtime = createMemo(() => runtimeStatus(frontendSamples())) const timing = () => config.data.debug?.timing ?? false + const turnTokens = () => config.data.debug?.turn_tokens ?? false const offEscape = keymap.intercept( "key", @@ -352,6 +353,16 @@ export function DevToolsBar() { > {timing() ? "[x]" : "[ ]"} Time to first draw + + void config.update((draft) => { + draft.debug = { ...draft.debug, turn_tokens: !turnTokens() } + }) + } + hoverBackground + > + {turnTokens() ? "[x]" : "[ ]"} Turn token usage + {(group) => ( @@ -403,7 +414,7 @@ function PanelBox(props: ParentProps) { position="absolute" zIndex={2600} bottom={1} - left={0} + left={-1} width={42} paddingLeft={2} paddingRight={2} diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index ab84fdf623..8ff9db50c7 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -222,14 +222,6 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, - { - title: "DevTools: Timing", - category: "Debug", - path: ["debug", "timing"], - default: true, - values: [false, true], - labels: ["off", "on"], - }, ] export function DialogConfig() { diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index e078eb5113..424e8dbc9a 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -159,6 +159,7 @@ export const Info = Schema.Struct({ Schema.Struct({ devtools: Schema.optional(Schema.Boolean).annotate({ description: "Show the DevTools debug bar" }), timing: Schema.optional(Schema.Boolean).annotate({ description: "Show time-to-first-draw diagnostics" }), + turn_tokens: Schema.optional(Schema.Boolean).annotate({ description: "Show per-turn token usage diagnostics" }), }), ).annotate({ description: "Debugging settings" }), animations: Schema.optional(Schema.Boolean).annotate({ description: "Enable interface animations" }), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 69c9682ec1..e07947ba1c 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1029,11 +1029,23 @@ export function Session() { ) } -function SessionRowView(props: { +type SessionRowViewProps = { row: SessionRow message: (messageID: string) => SessionMessageInfo | undefined boundaryID?: string -}) { +} + +function SessionRowView(props: SessionRowViewProps) { + const config = useConfig() + const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true + return ( + + + + ) +} + +function SessionRowContent(props: SessionRowViewProps) { return ( @@ -1072,11 +1084,109 @@ function SessionRowView(props: { )} + + {(row) => ( + + )} + ) } +function TurnTokenUsage(props: { + messageIDs: string[] + previousCacheRead?: number + message: (messageID: string) => SessionMessageInfo | undefined +}) { + const config = useConfig() + const { themeV2 } = useTheme() + const steps = createMemo(() => { + let previousCacheRead = props.previousCacheRead + return props.messageIDs.flatMap((messageID) => { + const message = props.message(messageID) + if (message?.type !== "assistant" || !message.tokens) return [] + const total = + message.tokens.input + + message.tokens.output + + message.tokens.reasoning + + message.tokens.cache.read + + message.tokens.cache.write + if (total === 0) return [] + const newTokens = total - message.tokens.cache.read + const cacheBust = + previousCacheRead !== undefined && message.tokens.cache.read < previousCacheRead + ? previousCacheRead - message.tokens.cache.read + : undefined + previousCacheRead = message.tokens.cache.read + return [ + { + finish: message.finish === "tool-calls" ? "tool-call" : (message.finish ?? "unknown"), + newTokens, + cached: message.tokens.cache.read, + total, + cacheBust, + }, + ] + }) + }) + const columns = createMemo(() => ({ + step: Math.max("Step".length, ...steps().map((item) => item.finish.length)), + newTokens: Math.max("New".length, ...steps().map((item) => item.newTokens.toLocaleString().length)), + cached: Math.max("Cached".length, ...steps().map((item) => item.cached.toLocaleString().length)), + total: Math.max("Total".length, ...steps().map((item) => item.total.toLocaleString().length)), + })) + return ( + 0}> + + + + ◈ + + + Tokens + + + + + {"Step".padEnd(columns().step + 2)} + {"New".padStart(columns().newTokens)} + {" "} + {"Cached".padStart(columns().cached)} + {" "} + {"Total".padStart(columns().total)} + + + + {(item) => ( + + + {item.finish.padEnd(columns().step + 2)} + + {item.newTokens.toLocaleString().padStart(columns().newTokens)} + + {" "} + {item.cached.toLocaleString().padStart(columns().cached)} + {" "} + {item.total.toLocaleString().padStart(columns().total)} + + + + ! Cache bust: {item.cacheBust?.toLocaleString()} fewer cached tokens than the previous step + + + + )} + + + + ) +} + function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) { const { themeV2 } = useTheme() const shortcut = Keymap.useShortcut("session.background") diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index dc4a681bb9..837725faf8 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -27,6 +27,7 @@ export type SessionRow = completed: boolean } | { type: "assistant-footer"; messageID: string } + | { type: "turn-usage"; messageIDs: string[]; previousCacheRead?: number } export function createSessionRows(sessionID: Accessor) { const data = useData() @@ -127,6 +128,26 @@ export function createSessionRows(sessionID: Accessor) { ), ) + createEffect( + on( + () => + data.session.message.list(sessionID()).flatMap((message) => + message.type === "assistant" + ? [ + { + id: message.id, + finish: message.finish, + error: message.error, + retry: message.retry, + tokens: message.tokens, + }, + ] + : [], + ), + () => setRows(reconcile(reduce())), + ), + ) + const appendMessage = (messageID: string) => setRows( produce((draft) => { @@ -260,6 +281,10 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) + const steps: string[] = [] + let previousCacheRead: number | undefined + let turnPreviousCacheRead: number | undefined + let measured = false return [ ...messages.filter((message) => !pending.has(message.id)), ...pendingCompactions, @@ -271,20 +296,42 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S rows.push({ type: "message", messageID: message.id }) return rows } + if (steps.length === 0) turnPreviousCacheRead = previousCacheRead + steps.push(message.id) + if (message.tokens && tokenTotal(message.tokens) > 0) { + previousCacheRead = message.tokens.cache.read + measured = true + } const ordinals = { text: 0, reasoning: 0 } message.content.forEach((part) => { const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}` if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return append(rows, { messageID: message.id, partID }, part) }) - if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) { + const terminal = (message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error + if (terminal || message.retry) { completePrevious(rows) rows.push({ type: "assistant-footer", messageID: message.id }) } + if (terminal) { + if (measured) + rows.push({ + type: "turn-usage", + messageIDs: [...steps], + ...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), + }) + steps.length = 0 + turnPreviousCacheRead = undefined + measured = false + } return rows }, []) } +function tokenTotal(tokens: NonNullable) { + return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write +} + export function messageBoundaryIDs(rows: SessionRow[], messages: SessionMessageInfo[]) { const byID = new Map(messages.map((message) => [message.id, message])) const seen = new Set() @@ -309,6 +356,8 @@ function rowBoundaryMessageID(row: SessionRow, messages: Map Date: Thu, 23 Jul 2026 20:44:14 +0530 Subject: [PATCH 16/27] chore(cli): upgrade acp sdk (#38316) --- bun.lock | 4 ++-- packages/cli/package.json | 2 +- packages/cli/src/acp/agent.ts | 2 -- packages/cli/src/acp/event.ts | 7 ++----- packages/cli/src/acp/service.ts | 12 +----------- packages/cli/test/acp/event-behavior.test.ts | 1 - packages/cli/test/acp/event.test.ts | 4 +--- packages/cli/test/acp/service-directory.test.ts | 4 +--- packages/cli/test/acp/service-usage.test.ts | 5 ----- 9 files changed, 8 insertions(+), 33 deletions(-) diff --git a/bun.lock b/bun.lock index 3ea6f31f5d..72db1b9e92 100644 --- a/bun.lock +++ b/bun.lock @@ -124,7 +124,7 @@ "opencode2": "./bin/opencode2.cjs", }, "dependencies": { - "@agentclientprotocol/sdk": "0.21.0", + "@agentclientprotocol/sdk": "1.2.1", "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -1173,7 +1173,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@1.2.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-jwYUdOQR7tc+Zfch53VL4JJyUNK/46q03uUTYb+PjECsmnNl94XFXOfYLJ8RBpMNidXd1rpOAVgb0vqD98xImA=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index acf8ec75dd..f848b56279 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,7 +22,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { - "@agentclientprotocol/sdk": "0.21.0", + "@agentclientprotocol/sdk": "1.2.1", "@effect/platform-node": "catalog:", "@opencode-ai/client": "workspace:*", "@opencode-ai/plugin": "workspace:*", diff --git a/packages/cli/src/acp/agent.ts b/packages/cli/src/acp/agent.ts index cf8eb693f7..89ec88e8ca 100644 --- a/packages/cli/src/acp/agent.ts +++ b/packages/cli/src/acp/agent.ts @@ -13,7 +13,6 @@ import { type PromptRequest, type ResumeSessionRequest, type SetSessionConfigOptionRequest, - type SetSessionModelRequest, type SetSessionModeRequest, } from "@agentclientprotocol/sdk" import type { OpenCodeClient } from "@opencode-ai/client/promise" @@ -33,7 +32,6 @@ export function create(client: OpenCodeClient, connection: AgentSideConnection) unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)), setSessionConfigOption: (params: SetSessionConfigOptionRequest) => run(service.setSessionConfigOption(params)), setSessionMode: (params: SetSessionModeRequest) => run(service.setSessionMode(params)), - unstable_setSessionModel: (params: SetSessionModelRequest) => run(service.setSessionModel(params)), prompt: (params: PromptRequest) => run(service.prompt(params)), cancel: (params: CancelNotification) => run(service.cancel(params)), } satisfies Agent diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index da8bfe168b..3b2d7bc23c 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -47,7 +47,6 @@ export async function streamTurn(input: { readonly sessionID: string readonly cwd: string readonly start: TurnStart - readonly userMessageID?: string | null readonly submit: (signal: AbortSignal) => Promise readonly control: TurnControl }): Promise { @@ -231,7 +230,7 @@ export async function streamTurn(input: { if (!started) { streamController.abort() await completed.catch(() => {}) - return response(undefined, undefined, "interrupted", true, undefined, input.userMessageID) + return response(undefined, undefined, "interrupted", true, undefined) } } const terminal = await completed @@ -246,7 +245,6 @@ export async function streamTurn(input: { terminal, control.cancelled, finish, - input.userMessageID, ) } catch (error) { streamController.abort() @@ -400,7 +398,6 @@ function response( terminal: "succeeded" | "failed" | "interrupted", cancelled: boolean, finish: SessionMessageAssistant["finish"], - messageID: string | null | undefined, ): PromptResponse { const error = assistant?.error ?? executionError if (error?.type === "provider.auth") throw new ACPError.AuthRequiredError() @@ -423,7 +420,7 @@ function response( } : undefined const stopReason = resolveStopReason({ terminal, cancelled, finish, error: error?.type }) - return { stopReason, ...(usage ? { usage } : {}), ...(messageID ? { userMessageId: messageID } : {}), _meta: {} } + return { stopReason, ...(usage ? { usage } : {}), _meta: {} } } function resolveStopReason(input: { diff --git a/packages/cli/src/acp/service.ts b/packages/cli/src/acp/service.ts index f20f31c9bc..d9bf0ddc5b 100644 --- a/packages/cli/src/acp/service.ts +++ b/packages/cli/src/acp/service.ts @@ -33,8 +33,6 @@ import type { ResumeSessionResponse, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, - SetSessionModelRequest, - SetSessionModelResponse, SetSessionModeRequest, SetSessionModeResponse, } from "@agentclientprotocol/sdk" @@ -88,7 +86,6 @@ export interface Interface { forkSession(input: ForkSessionRequest): Promise setSessionConfigOption(input: SetSessionConfigOptionRequest): Promise setSessionMode(input: SetSessionModeRequest): Promise - setSessionModel(input: SetSessionModelRequest): Promise prompt(input: PromptRequest): Promise cancel(input: CancelNotification): Promise } @@ -270,13 +267,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti await selectMode(input.client, await requireSession(params.sessionId), params.modeId) return {} }, - setSessionModel: async (params) => { - const state = await requireSession(params.sessionId) - const selected = requireModel(state.catalog, params.modelId) - state.model = selected - await input.client.session.switchModel({ sessionID: state.id, model: selected }) - return {} - }, prompt: async (params) => { const state = await requireSession(params.sessionId) if (active.has(state.id)) { @@ -295,7 +285,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti sessionID: state.id, cwd: state.cwd, start: prepared.start, - userMessageID: params.messageId, control, submit: (signal) => submitPrompt(input.client, state, prepared, signal), }).finally(() => { @@ -479,6 +468,7 @@ async function registerMcpServers( function mcpConfig(server: McpServer) { if ("type" in server) { + if (server.type === "acp") throw new Error("MCP-over-ACP is not supported") return { type: "remote" as const, url: server.url, diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 1f9935f5c8..46ecc552a4 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -566,7 +566,6 @@ function turn(input: { sessionID: input.sessionID, cwd: "/workspace", start: { type: "input", id: input.inputID }, - userMessageID: `client_${input.inputID}`, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }), diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index e6d8db9deb..0075f5c913 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -85,7 +85,6 @@ test("acp prompt resolves after ordered turn updates", async () => { try { const id = "msg_prompt" - const userMessageID = "client-message" const response = await streamTurn({ client, connection: { @@ -97,7 +96,6 @@ test("acp prompt resolves after ordered turn updates", async () => { sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, - userMessageID, control: { cancelled: false, admission: new AbortController() }, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), }) @@ -112,7 +110,7 @@ test("acp prompt resolves after ordered turn updates", async () => { }, }, ]) - expect(response).toMatchObject({ stopReason: "end_turn", userMessageId: userMessageID, usage: { totalTokens: 2 } }) + expect(response).toMatchObject({ stopReason: "end_turn", usage: { totalTokens: 2 } }) } finally { events?.close() await server.stop(true) diff --git a/packages/cli/test/acp/service-directory.test.ts b/packages/cli/test/acp/service-directory.test.ts index 9690622372..a3671753c9 100644 --- a/packages/cli/test/acp/service-directory.test.ts +++ b/packages/cli/test/acp/service-directory.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { McpServer, SessionConfigOption } from "@agentclientprotocol/sdk" -import { makeACPFixture, makeSession, secondModel, testModel } from "./service-fixture" +import { makeACPFixture, makeSession, secondModel } from "./service-fixture" describe("acp service directory behavior", () => { test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => { @@ -134,7 +134,6 @@ describe("acp service directory behavior", () => { configId: "mode", value: "plan", }) - await fixture.service.setSessionModel({ sessionId: session.sessionId, modelId: "test/test-model/high" }) await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" }) expect(currentValue(selectedModel, "model")).toBe("test/second-model") @@ -148,7 +147,6 @@ describe("acp service directory behavior", () => { ).toEqual([ { model: { providerID: "test", id: secondModel.id } }, { model: { providerID: "test", id: secondModel.id, variant: "medium" } }, - { model: { providerID: "test", id: testModel.id, variant: "high" } }, ]) expect( fixture.requests diff --git a/packages/cli/test/acp/service-usage.test.ts b/packages/cli/test/acp/service-usage.test.ts index 1ad9eee7b7..e7abb74073 100644 --- a/packages/cli/test/acp/service-usage.test.ts +++ b/packages/cli/test/acp/service-usage.test.ts @@ -45,17 +45,14 @@ describe("acp service prompt routing and usage", () => { const commandResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-command", prompt: [{ type: "text", text: "/review now" }], }) const skillResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-skill", prompt: [{ type: "text", text: "/verify" }], }) const compactResult = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-compact", prompt: [{ type: "text", text: "/compact" }], }) @@ -154,13 +151,11 @@ describe("acp service prompt routing and usage", () => { const response = await fixture.service.prompt({ sessionId: session.sessionId, - messageId: "client-message", prompt: [{ type: "text", text: "hello" }], }) expect(response).toEqual({ stopReason: "end_turn", - userMessageId: "client-message", usage: { inputTokens: 100, outputTokens: 40, From 833dd2ed7f9dc9845528997c378a1fb50bc423df Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 11:22:00 -0400 Subject: [PATCH 17/27] refactor(tui): simplify turn usage reduction (#38514) --- packages/tui/src/routes/session/index.tsx | 10 --- packages/tui/src/routes/session/rows.ts | 76 ++++++++++++----------- packages/tui/test/cli/tui/data.test.tsx | 18 ++++-- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e07947ba1c..9b03a40422 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1036,16 +1036,6 @@ type SessionRowViewProps = { } function SessionRowView(props: SessionRowViewProps) { - const config = useConfig() - const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true - return ( - - - - ) -} - -function SessionRowContent(props: SessionRowViewProps) { return ( diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 837725faf8..c6b0d744c9 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,6 +1,7 @@ import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" +import { useConfig } from "../../config" import { useData } from "../../context/data" import { useClient } from "../../context/client" @@ -32,14 +33,20 @@ export type SessionRow = export function createSessionRows(sessionID: Accessor) { const data = useData() const client = useClient() + const config = useConfig() const [rows, setRows] = createStore([]) const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID + const turnTokens = () => config.data.debug?.turn_tokens === true function reduce() { const messages = data.session.message.list(sessionID()) const inputs = new Set(data.session.input.list(sessionID())) const boundary = revertBoundary() - const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs) + const rows = reduceSessionRows( + boundary ? messages.filter((message) => message.id < boundary) : messages, + inputs, + turnTokens(), + ) partitionPending(rows, pendingPermissions()) const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID)) rows.splice( @@ -129,23 +136,7 @@ export function createSessionRows(sessionID: Accessor) { ) createEffect( - on( - () => - data.session.message.list(sessionID()).flatMap((message) => - message.type === "assistant" - ? [ - { - id: message.id, - finish: message.finish, - error: message.error, - retry: message.retry, - tokens: message.tokens, - }, - ] - : [], - ), - () => setRows(reconcile(reduce())), - ), + on(turnTokens, () => setRows(reconcile(reduce()))), ) const appendMessage = (messageID: string) => @@ -267,9 +258,12 @@ export function createSessionRows(sessionID: Accessor) { data.on("session.step.ended", (event) => { if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return appendFooter(event.data.assistantMessageID) + if (turnTokens()) setRows(reconcile(reduce())) }), data.on("session.step.failed", (event) => { - if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID) + if (event.data.sessionID !== sessionID()) return + appendFooter(event.data.assistantMessageID) + if (turnTokens()) setRows(reconcile(reduce())) }), ] onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe())) @@ -277,14 +271,17 @@ export function createSessionRows(sessionID: Accessor) { return rows } -export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set()) { +export function reduceSessionRows( + messages: SessionMessageInfo[], + inputs = new Set(), + turnTokens = false, +) { const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) - const steps: string[] = [] - let previousCacheRead: number | undefined - let turnPreviousCacheRead: number | undefined - let measured = false + const usage = turnTokens + ? { steps: [] as SessionMessageAssistant[], previousTurnCacheRead: undefined as number | undefined } + : undefined return [ ...messages.filter((message) => !pending.has(message.id)), ...pendingCompactions, @@ -296,12 +293,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S rows.push({ type: "message", messageID: message.id }) return rows } - if (steps.length === 0) turnPreviousCacheRead = previousCacheRead - steps.push(message.id) - if (message.tokens && tokenTotal(message.tokens) > 0) { - previousCacheRead = message.tokens.cache.read - measured = true - } + usage?.steps.push(message) const ordinals = { text: 0, reasoning: 0 } message.content.forEach((part) => { const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}` @@ -313,21 +305,31 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S completePrevious(rows) rows.push({ type: "assistant-footer", messageID: message.id }) } - if (terminal) { - if (measured) + if (terminal && usage) { + const stepsWithUsage = usage.steps.filter(hasTokenUsage) + const last = stepsWithUsage.at(-1) + if (last) { rows.push({ type: "turn-usage", - messageIDs: [...steps], - ...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), + messageIDs: stepsWithUsage.map((step) => step.id), + ...(usage.previousTurnCacheRead === undefined + ? {} + : { previousCacheRead: usage.previousTurnCacheRead }), }) - steps.length = 0 - turnPreviousCacheRead = undefined - measured = false + usage.previousTurnCacheRead = last.tokens.cache.read + } + usage.steps.length = 0 } return rows }, []) } +function hasTokenUsage( + message: SessionMessageAssistant, +): message is SessionMessageAssistant & { tokens: NonNullable } { + return message.tokens !== undefined && tokenTotal(message.tokens) > 0 +} + function tokenTotal(tokens: NonNullable) { return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 88b2175f26..e7e6146f4b 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -5,12 +5,14 @@ import type { OpenCodeEvent } from "@opencode-ai/client" import { SessionMessage } from "@opencode-ai/core/session/message" import { EventV2 } from "@opencode-ai/core/event" import { createEffect, onMount, type ParentProps } from "solid-js" +import { ConfigProvider } from "../../../src/config" import { ClientProvider, useClient } from "../../../src/context/client" import { DataProvider as DataProviderBase, useData } from "../../../src/context/data" import { LocationProvider, useLocation } from "../../../src/context/location" import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows" import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client" import { TestTuiContexts } from "../../fixture/tui-environment" +import { createTuiResolvedConfig } from "../../fixture/tui-runtime" const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [ { @@ -32,14 +34,18 @@ function emitEvent(events: ReturnType, event: OpenCode events.emit({ ...event, location: { directory } }) } +const config = createTuiResolvedConfig() + function DataProvider(props: ParentProps) { return ( - - - - {props.children} - - + + + + + {props.children} + + + ) } From 466b75b19d8deea761593c207d398f618ec710da Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 23 Jul 2026 21:13:46 +0530 Subject: [PATCH 18/27] feat(cli): expand acp v1 support (#38325) --- packages/cli/src/acp/agent.ts | 2 ++ packages/cli/src/acp/event.ts | 2 ++ packages/cli/src/acp/permission.ts | 3 ++- packages/cli/src/acp/service.ts | 19 +++++++++++-- packages/cli/test/acp/event-behavior.test.ts | 3 +++ packages/cli/test/acp/event.test.ts | 1 + .../acp/initialize-auth.subprocess.test.ts | 1 + .../cli/test/acp/lifecycle.subprocess.test.ts | 15 +++++++++++ .../cli/test/acp/permission-behavior.test.ts | 23 ++++++++++++++++ .../cli/test/acp/service-lifecycle.test.ts | 27 +++++++++++++++++++ 10 files changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/acp/agent.ts b/packages/cli/src/acp/agent.ts index 89ec88e8ca..c01ec94018 100644 --- a/packages/cli/src/acp/agent.ts +++ b/packages/cli/src/acp/agent.ts @@ -5,6 +5,7 @@ import { type AuthenticateRequest, type CancelNotification, type CloseSessionRequest, + type DeleteSessionRequest, type ForkSessionRequest, type InitializeRequest, type ListSessionsRequest, @@ -27,6 +28,7 @@ export function create(client: OpenCodeClient, connection: AgentSideConnection) newSession: (params: NewSessionRequest) => run(service.newSession(params)), loadSession: (params: LoadSessionRequest) => run(service.loadSession(params)), listSessions: (params: ListSessionsRequest) => run(service.listSessions(params)), + deleteSession: (params: DeleteSessionRequest) => run(service.deleteSession(params)), resumeSession: (params: ResumeSessionRequest) => run(service.resumeSession(params)), closeSession: (params: CloseSessionRequest) => run(service.closeSession(params)), unstable_forkSession: (params: ForkSessionRequest) => run(service.forkSession(params)), diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 3b2d7bc23c..1551353830 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -47,6 +47,7 @@ export async function streamTurn(input: { readonly sessionID: string readonly cwd: string readonly start: TurnStart + readonly writeTextFile: boolean readonly submit: (signal: AbortSignal) => Promise readonly control: TurnControl }): Promise { @@ -169,6 +170,7 @@ export async function streamTurn(input: { tools.delete(event.data.callID) await syncEditedFiles({ connection: input.connection, + writeTextFile: input.writeTextFile, sessionID: input.sessionID, cwd: input.cwd, toolName: current.name, diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 71086bf909..3c1d92957e 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -53,13 +53,14 @@ export async function replyPermission(input: { export async function syncEditedFiles(input: { readonly connection: Partial> + readonly writeTextFile: boolean readonly sessionID: string readonly cwd: string readonly toolName: string readonly toolInput: ToolInput readonly structured: Readonly> }) { - if (!input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return + if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return const files = Array.isArray(input.structured.files) ? input.structured.files.flatMap((file): string[] => { if (!file || typeof file !== "object") return [] diff --git a/packages/cli/src/acp/service.ts b/packages/cli/src/acp/service.ts index d9bf0ddc5b..963b5c3ed4 100644 --- a/packages/cli/src/acp/service.ts +++ b/packages/cli/src/acp/service.ts @@ -16,6 +16,8 @@ import type { CancelNotification, CloseSessionRequest, CloseSessionResponse, + DeleteSessionRequest, + DeleteSessionResponse, ForkSessionRequest, ForkSessionResponse, InitializeRequest, @@ -45,7 +47,8 @@ import { ACPError } from "./error" export const AuthMethodID = "opencode-login" -type Connection = Pick +type Connection = Pick & + Partial> type Catalog = { readonly providers: ConfigOptionProvider[] @@ -81,6 +84,7 @@ export interface Interface { newSession(input: NewSessionRequest): Promise loadSession(input: LoadSessionRequest): Promise listSessions(input: ListSessionsRequest): Promise + deleteSession(input: DeleteSessionRequest): Promise resumeSession(input: ResumeSessionRequest): Promise closeSession(input: CloseSessionRequest): Promise forkSession(input: ForkSessionRequest): Promise @@ -95,6 +99,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti const catalogs = new Map>() const registeredMcp = new Map>() const active = new Map() + const capabilities = { writeTextFile: false } const catalog = (cwd: string) => { const cached = catalogs.get(cwd) @@ -154,6 +159,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti return { initialize: async (params) => { + capabilities.writeTextFile = params.clientCapabilities?.fs?.writeTextFile === true const authMethod: AuthMethod = { description: "Run `opencode auth login` in the terminal", name: "Login with opencode", @@ -170,7 +176,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti loadSession: true, mcpCapabilities: { http: true, sse: false }, promptCapabilities: { embeddedContext: true, image: true }, - sessionCapabilities: { close: {}, fork: {}, list: {}, resume: {} }, + sessionCapabilities: { close: {}, delete: {}, fork: {}, list: {}, resume: {} }, }, authMethods: [authMethod], agentInfo: { name: "OpenCode", version: OPENCODE_VERSION }, @@ -213,6 +219,14 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti ...(page.cursor.next ? { nextCursor: page.cursor.next } : {}), } }, + deleteSession: async (params) => { + await input.client.session.remove({ sessionID: params.sessionId }).catch((error) => { + if (!isSessionNotFoundError(error)) throw error + }) + sessions.delete(params.sessionId) + registeredMcp.delete(params.sessionId) + return {} + }, resumeSession: async (params) => { const session = await getSession(input.client, params.sessionId) const state = await attach(session, session.location.directory, params.mcpServers ?? []) @@ -285,6 +299,7 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti sessionID: state.id, cwd: state.cwd, start: prepared.start, + writeTextFile: capabilities.writeTextFile, control, submit: (signal) => submitPrompt(input.client, state, prepared, signal), }).finally(() => { diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 46ecc552a4..ded7b7020a 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -439,6 +439,7 @@ describe("acp event behavior", () => { sessionID: "ses_cancel", cwd: "/workspace", start: { type: "input", id: "input_cancel" }, + writeTextFile: false, control, submit: async (signal) => { await fixture.client.session.prompt( @@ -481,6 +482,7 @@ describe("acp event behavior", () => { sessionID: "ses_cancel_admission", cwd: "/workspace", start: { type: "input", id: "input_cancel_admission" }, + writeTextFile: false, control, submit: (signal) => fixture.client.session.prompt( @@ -566,6 +568,7 @@ function turn(input: { sessionID: input.sessionID, cwd: "/workspace", start: { type: "input", id: input.inputID }, + writeTextFile: false, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }), diff --git a/packages/cli/test/acp/event.test.ts b/packages/cli/test/acp/event.test.ts index 0075f5c913..575705b294 100644 --- a/packages/cli/test/acp/event.test.ts +++ b/packages/cli/test/acp/event.test.ts @@ -96,6 +96,7 @@ test("acp prompt resolves after ordered turn updates", async () => { sessionID: "ses_test", cwd: "/workspace", start: { type: "input", id }, + writeTextFile: false, control: { cancelled: false, admission: new AbortController() }, submit: () => client.session.prompt({ sessionID: "ses_test", id, text: "hi" }), }) diff --git a/packages/cli/test/acp/initialize-auth.subprocess.test.ts b/packages/cli/test/acp/initialize-auth.subprocess.test.ts index 2303b5b3a1..904ceadd5a 100644 --- a/packages/cli/test/acp/initialize-auth.subprocess.test.ts +++ b/packages/cli/test/acp/initialize-auth.subprocess.test.ts @@ -14,6 +14,7 @@ describe("acp initialize/auth subprocess", () => { expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(false) expect(initialized.agentCapabilities?.loadSession).toBe(true) expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({}) + expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({}) expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({}) diff --git a/packages/cli/test/acp/lifecycle.subprocess.test.ts b/packages/cli/test/acp/lifecycle.subprocess.test.ts index ffd0332620..4ae855a7f3 100644 --- a/packages/cli/test/acp/lifecycle.subprocess.test.ts +++ b/packages/cli/test/acp/lifecycle.subprocess.test.ts @@ -1,5 +1,6 @@ import type { CloseSessionResponse, + DeleteSessionResponse, ListSessionsResponse, LoadSessionResponse, ResumeSessionResponse, @@ -60,6 +61,20 @@ describe("acp lifecycle subprocess", () => { expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(true) }, 60_000) + test("delete capability and delete request", async () => { + await using fixture = await createAcpFixture() + const acp = fixture.spawn() + const initialized = await initialize(acp) + expect(initialized.agentCapabilities?.sessionCapabilities?.delete).toEqual({}) + const session = await newSession(acp, fixture.home) + + expect( + expectOk(await acp.request("session/delete", { sessionId: session.sessionId })), + ).toEqual({}) + const listed = expectOk(await acp.request("session/list", { cwd: fixture.home })) + expect(listed.sessions.some((item) => item.sessionId === session.sessionId)).toBe(false) + }, 60_000) + test("resume capability advertisement", async () => { await using fixture = await createAcpFixture() const initialized = await initialize(fixture.spawn()) diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index ad8ccd128a..355ff5bb41 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -4,6 +4,7 @@ import fs from "node:fs/promises" import os from "node:os" import path from "node:path" import { streamTurn } from "../../src/acp/event" +import { syncEditedFiles } from "../../src/acp/permission" import { createSseFixture, durableEvent, ephemeralEvent, withTimeout } from "./sse-fixture" type SessionUpdateParams = Parameters[0] @@ -12,6 +13,27 @@ type Connection = Pick describe("acp permission behavior", () => { + test("does not sync edits when writeTextFile was not advertised", async () => { + const writes: Parameters[0][] = [] + + await syncEditedFiles({ + connection: { + writeTextFile: async (input) => { + writes.push(input) + return {} + }, + }, + writeTextFile: false, + sessionID: "ses_no_write", + cwd: "/workspace", + toolName: "edit", + toolInput: { filePath: "/workspace/file.ts" }, + structured: {}, + }) + + expect(writes).toEqual([]) + }) + test("forwards allow-once and allow-always selections to the generated client", async () => { const permissionRequests: RequestPermissionRequest[] = [] const fixture = createSseFixture({ @@ -465,6 +487,7 @@ function startTurn(fixture: Fixture, connection: Connection, sessionID: string, sessionID, cwd, start: { type: "input", id: inputID }, + writeTextFile: true, control: { cancelled: false, admission: new AbortController() }, submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }), }) diff --git a/packages/cli/test/acp/service-lifecycle.test.ts b/packages/cli/test/acp/service-lifecycle.test.ts index 2c4992b8e5..35a46a0ee2 100644 --- a/packages/cli/test/acp/service-lifecycle.test.ts +++ b/packages/cli/test/acp/service-lifecycle.test.ts @@ -225,6 +225,33 @@ describe("acp service lifecycle", () => { "/api/session/missing/interrupt", ]) }) + + test("deletes sessions from backing and local storage", async () => { + await using fixture = makeACPFixture({ + fetch(request) { + if (request.method === "POST" && request.path === "/api/session") { + return Response.json({ data: makeSession("ses_delete") }) + } + if (request.method === "DELETE" && request.path === "/api/session/ses_delete") { + return new Response(null, { status: 204 }) + } + return undefined + }, + }) + const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] }) + + expect(await fixture.service.deleteSession({ sessionId: session.sessionId })).toEqual({}) + expect(fixture.requests).toContainEqual({ + method: "DELETE", + path: "/api/session/ses_delete", + query: {}, + body: undefined, + }) + const missing = await fixture.service + .setSessionConfigOption({ sessionId: session.sessionId, configId: "effort", value: "high" }) + .catch((error: unknown) => error) + expect(missing).toMatchObject({ _tag: "ACPSessionNotFoundError", sessionId: session.sessionId }) + }) }) function currentValue(result: { readonly configOptions?: readonly SessionConfigOption[] | null }, id: string) { From 8f3465c951a024028a92adc9a283038a085f967f Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 23 Jul 2026 12:35:29 -0400 Subject: [PATCH 19/27] refactor(tui): load native V2 themes (#38430) --- packages/docs/script/generate-theme-tokens.ts | 6 +- packages/tui/src/app.tsx | 2 + .../tui/src/component/theme-error-toast.tsx | 20 ++++ packages/tui/src/context/theme.tsx | 92 ++++++++++++------ packages/tui/src/mini/theme.ts | 6 +- packages/tui/src/theme/index.ts | 74 +++++++++++---- packages/tui/src/theme/resolve.ts | 8 +- packages/tui/src/theme/v1.ts | 4 +- packages/tui/src/theme/v2/defaults.ts | 4 +- packages/tui/src/theme/v2/index.ts | 2 +- packages/tui/src/theme/v2/resolve.ts | 20 +--- packages/tui/src/theme/v2/schema.ts | 4 +- packages/tui/src/theme/v2/select.ts | 32 +++---- packages/tui/src/theme/v2/v1-migrate.ts | 8 +- packages/tui/test/cli/tui/theme-mode.test.tsx | 87 ++++++++++++++++- packages/tui/test/theme.test.ts | 94 ++++++++++++++++++- packages/tui/test/theme/v2/resolve.test.ts | 56 ++++++----- packages/tui/test/theme/v2/select.test.ts | 28 +++--- packages/tui/test/theme/v2/types.test.ts | 14 +-- packages/tui/test/theme/v2/v1-migrate.test.ts | 10 +- 20 files changed, 410 insertions(+), 161 deletions(-) create mode 100644 packages/tui/src/component/theme-error-toast.tsx diff --git a/packages/docs/script/generate-theme-tokens.ts b/packages/docs/script/generate-theme-tokens.ts index 0c441075f9..25f614d119 100644 --- a/packages/docs/script/generate-theme-tokens.ts +++ b/packages/docs/script/generate-theme-tokens.ts @@ -2,7 +2,7 @@ import { Schema, SchemaAST } from "effect" import { format } from "prettier" -import { ThemeDefinition, ThemeFile } from "../../tui/src/theme/v2/schema" +import { ThemeDefinition, ThemeDocument } from "../../tui/src/theme/v2/schema" const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx" const root = requireObject(ThemeDefinition.ast) @@ -52,8 +52,8 @@ const example = { default: "#101014", }, }, -} satisfies ThemeFile -Schema.decodeUnknownSync(ThemeFile)(example) +} satisfies ThemeDocument +Schema.decodeUnknownSync(ThemeDocument)(example) const output = await format( `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 5e600b65e8..91b286e798 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -65,6 +65,7 @@ import { DialogThemeList } from "./component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { DialogAgent } from "./component/dialog-agent" import { DialogSessionList } from "./component/dialog-session-list" +import { ThemeErrorToast } from "./component/theme-error-toast" import { ThemeProvider, useTheme } from "./context/theme" import { Home } from "./routes/home" import { Session } from "./routes/session" @@ -337,6 +338,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { + diff --git a/packages/tui/src/component/theme-error-toast.tsx b/packages/tui/src/component/theme-error-toast.tsx new file mode 100644 index 0000000000..f10a11bdc9 --- /dev/null +++ b/packages/tui/src/component/theme-error-toast.tsx @@ -0,0 +1,20 @@ +import { onCleanup } from "solid-js" +import { useTheme } from "../context/theme" +import { useToast } from "../ui/toast" + +export function ThemeErrorToast() { + const theme = useTheme() + const toast = useToast() + + onCleanup( + theme.onError(({ name, error }) => + toast.show({ + variant: "error", + title: `Failed to load theme: ${name}`, + message: error.message, + }), + ), + ) + + return null +} diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 16ffdeaccb..9b53e8d07a 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -5,21 +5,20 @@ import { addTheme, allThemes, hasTheme, - isTheme, + parseTheme, selectedForeground, setCustomThemes, setSystemTheme, subscribeThemes, upsertTheme, type Theme, - type ThemeJson, + type ThemeDocumentSource, } from "../theme" import { generateSyntax } from "../theme/v2/syntax" import { generateSystem, terminalMode } from "../theme/system" import { discoverThemes, themeDirectories } from "../theme/discovery" import { createComponentTheme, type ComponentTheme } from "../theme/v2/component" -import { resolveThemeFile } from "../theme/v2/resolve" -import { migrateV1 } from "../theme/v2/v1-migrate" +import { resolveThemeDocument } from "../theme/v2/resolve" import { themeModes } from "../theme/v2/select" import { createEffect, createMemo, onCleanup, onMount, type Accessor, type ParentProps } from "solid-js" import { createStore, produce } from "solid-js/store" @@ -29,6 +28,36 @@ import { Global } from "@opencode-ai/util/global" import { DevTools } from "../devtools" const themePerformance = DevTools.register({ id: "theme-performance", title: "Theme performance" }) +export type ThemeError = { name: string; error: Error } +type ThemeErrorHandler = (event: ThemeError) => void + +function createThemeErrors() { + let handler: ThemeErrorHandler | undefined + let pending: ThemeError | undefined + + return { + emit(name: string, cause: unknown) { + const event = { name, error: cause instanceof Error ? cause : new Error(String(cause)) } + if (handler) { + handler(event) + return + } + pending = event + }, + onError(next: ThemeErrorHandler) { + handler = next + if (pending) { + next(pending) + pending = undefined + } + return () => { + if (handler === next) handler = undefined + } + }, + } +} + +const themeErrors = createThemeErrors() export type ThemeSource = Readonly<{ discover(): Promise> @@ -61,7 +90,7 @@ export { const THEME_REFRESH_DELAYS = [250, 1000] as const type State = { - themes: Record + themes: Record mode: "dark" | "light" lock: "dark" | "light" | undefined active: string @@ -84,6 +113,7 @@ type ThemeService = { unlock(): void setMode(mode?: "dark" | "light", persist?: boolean): boolean set(theme: string): boolean + onError(handler: ThemeErrorHandler): () => void readonly ready: boolean } @@ -139,12 +169,7 @@ const themeContext = createSimpleContext({ return themes .discover() .then((themes) => { - setCustomThemes( - Object.entries(themes).reduce>((result, [name, theme]) => { - if (isTheme(theme)) result[name] = theme - return result - }, {}), - ) + setCustomThemes(themes) }) .catch(() => setStore("active", "opencode")) } @@ -269,30 +294,26 @@ const themeContext = createSimpleContext({ }) const initStarted = performance.now() - const source = createMemo(() => store.themes[store.active] ?? store.themes.opencode) - const sourceName = createMemo(() => (store.themes[store.active] ? store.active : "opencode")) - const file = createMemo(() => migrateV1(source())) - const modes = createMemo(() => themeModes(file())) - const mode = () => { - const supported = modes() - if (supported.includes(store.mode)) return store.mode - return supported[0] ?? store.mode - } - const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) + const selected = createMemo(() => { + const name = store.themes[store.active] ? store.active : "opencode" + try { + return loadTheme(store.themes[name], name, store.mode) + } catch (error) { + if (name === "opencode") throw error + themeErrors.emit(name, error) + setStore("active", "opencode") + return loadTheme(store.themes.opencode, "opencode", store.mode) + } + }) + const modes = () => selected().modes + const mode = () => selected().mode + const valuesV2 = () => selected().theme valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) const themeV2 = createComponentTheme(valuesV2, mode) const contextsV2 = { - elevated: createComponentTheme(() => { - const theme = valuesV2().contexts["@context:elevated"] - if (!theme) throw new Error("Theme context is not defined: elevated") - return theme - }, mode), - overlay: createComponentTheme(() => { - const theme = valuesV2().contexts["@context:overlay"] - if (!theme) throw new Error("Theme context is not defined: overlay") - return theme - }, mode), + elevated: createComponentTheme(() => valuesV2().contexts["@context:elevated"] ?? valuesV2(), mode), + overlay: createComponentTheme(() => valuesV2().contexts["@context:overlay"] ?? valuesV2(), mode), } createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) @@ -331,6 +352,7 @@ const themeContext = createSimpleContext({ .catch(() => {}) return true }, + onError: themeErrors.onError, get ready() { return store.ready }, @@ -354,6 +376,14 @@ export function ThemeContextProvider(props: ParentProps<{ context: ContextName } ) } + +function loadTheme(source: ThemeDocumentSource, name: string, requested: "dark" | "light") { + const document = parseTheme(source, name) + const modes = themeModes(document) + const mode = modes.includes(requested) ? requested : (modes[0] ?? requested) + return { modes, mode, theme: resolveThemeDocument(document, mode) } +} + export function createSyntaxStyleMemo(factory: () => SyntaxStyle) { const renderer = useRenderer() const retained = new Set() diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index 736eed08bb..3c5afed16e 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -10,7 +10,7 @@ import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" import { ansiToRgba } from "../theme/color" import { resolveThemeColors } from "../theme/resolve" import { terminalMode } from "../theme/system" -import type { ThemeJson } from "../theme/v1" +import type { ThemeV1Json } from "../theme/v1" import type { EntryKind, RunTuiConfig } from "./types" type Tone = { @@ -184,7 +184,7 @@ function splashShadow(indexed: RGBA[], base: RGBA, overlay: RGBA, value: number) return nearestIndexed(indexed, mixed) } -export function resolveTheme(theme: ThemeJson, pick: "dark" | "light"): TuiThemeCurrent { +export function resolveTheme(theme: ThemeV1Json, pick: "dark" | "light"): TuiThemeCurrent { const resolved = resolveThemeColors(theme, pick, (code) => RGBA.fromIndex(code, ansiToRgba(code))) return { ...resolved.theme, @@ -246,7 +246,7 @@ function generateMutedTextColor(bg: RGBA, isDark: boolean, map: (rgba: RGBA) => return map(RGBA.fromInts(gray, gray, gray)) } -export function generateSystem(colors: TerminalColors, pick: "dark" | "light"): ThemeJson { +export function generateSystem(colors: TerminalColors, pick: "dark" | "light"): ThemeV1Json { const bg_snapshot = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!) const fg_snapshot = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!) const bg = RGBA.defaultBackground(bg_snapshot) diff --git a/packages/tui/src/theme/index.ts b/packages/tui/src/theme/index.ts index ef522b43c2..d128a75539 100644 --- a/packages/tui/src/theme/index.ts +++ b/packages/tui/src/theme/index.ts @@ -1,16 +1,25 @@ +import { Schema } from "effect" import { resolveThemeColors } from "./resolve" -import { DEFAULT_THEMES, type Theme, type ThemeJson } from "./v1" +import { DEFAULT_THEMES, type Theme, type ThemeV1Json } from "./v1" +import { resolveThemeDocument, themeDecodeError } from "./v2/resolve" +import { ThemeDocument } from "./v2/schema" +import { migrateV1 } from "./v2/v1-migrate" -export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeJson } from "./v1" +export { DEFAULT_THEMES, generateSyntax, selectedForeground, type Theme, type ThemeV1Json } from "./v1" +export { resolveThemeDocument, type ThemeDocument } -const pluginThemes: Record = {} -let customThemes: Record = {} -let systemTheme: ThemeJson | undefined -const listeners = new Set<(themes: Record) => void>() +export type ThemeDocumentSource = Record + +const pluginThemes: Record = {} +let customThemes: Record = {} +let systemTheme: ThemeDocumentSource | undefined +const listeners = new Set<(themes: Record) => void>() +const parsed = new WeakMap() +const decodeThemeDocument = Schema.decodeUnknownSync(ThemeDocument) function listThemes() { // Priority: defaults < plugin installs < custom files < generated system. - const themes = { + const themes: Record = { ...DEFAULT_THEMES, ...pluginThemes, ...customThemes, @@ -31,23 +40,40 @@ export function allThemes() { return listThemes() } -export function isTheme(theme: unknown): theme is ThemeJson { - if (typeof theme !== "object" || theme === null || Array.isArray(theme)) return false - const value = Reflect.get(theme, "theme") - return typeof value === "object" && value !== null && !Array.isArray(value) +export function isThemeSource(source: unknown): source is ThemeDocumentSource { + if (typeof source !== "object" || source === null || Array.isArray(source)) return false + return "theme" in source || "version" in source } -export function subscribeThemes(listener: (themes: Record) => void) { +export function parseTheme(source: ThemeDocumentSource, name = "theme") { + const cached = parsed.get(source) + if (cached) return cached + + const version = source.version ?? 1 + const document = + version === 1 + ? migrateV1(source as ThemeV1Json) + : version === 2 + ? decodeV2Theme(source, name) + : unsupportedThemeVersion(version) + + parsed.set(source, document) + return document +} + +export function subscribeThemes(listener: (themes: Record) => void) { listeners.add(listener) return () => listeners.delete(listener) } -export function setCustomThemes(themes: Record) { - customThemes = themes +export function setCustomThemes(themes: Record) { + customThemes = Object.fromEntries( + Object.entries(themes).filter((entry): entry is [string, ThemeDocumentSource] => isThemeSource(entry[1])), + ) syncThemes() } -export function setSystemTheme(theme: ThemeJson | undefined) { +export function setSystemTheme(theme: ThemeDocumentSource | undefined) { systemTheme = theme syncThemes() } @@ -59,7 +85,7 @@ export function hasTheme(name: string) { export function addTheme(name: string, theme: unknown) { if (!name) return false - if (!isTheme(theme)) return false + if (!isThemeSource(theme)) return false if (hasTheme(name)) return false pluginThemes[name] = theme syncThemes() @@ -68,7 +94,7 @@ export function addTheme(name: string, theme: unknown) { export function upsertTheme(name: string, theme: unknown) { if (!name) return false - if (!isTheme(theme)) return false + if (!isThemeSource(theme)) return false if (customThemes[name] !== undefined) { customThemes[name] = theme } else { @@ -78,7 +104,7 @@ export function upsertTheme(name: string, theme: unknown) { return true } -export function resolveTheme(theme: ThemeJson, mode: "dark" | "light"): Theme { +export function resolveTheme(theme: ThemeV1Json, mode: "dark" | "light"): Theme { const resolved = resolveThemeColors(theme, mode) return { ...resolved.theme, @@ -86,3 +112,15 @@ export function resolveTheme(theme: ThemeJson, mode: "dark" | "light"): Theme { thinkingOpacity: resolved.thinkingOpacity, } } + +function decodeV2Theme(source: ThemeDocumentSource, name: string) { + try { + return decodeThemeDocument(source) + } catch (error) { + throw themeDecodeError(error, name) + } +} + +function unsupportedThemeVersion(version: unknown): never { + throw new Error(`Unsupported theme version: ${String(version)}`) +} diff --git a/packages/tui/src/theme/resolve.ts b/packages/tui/src/theme/resolve.ts index b8cf36866c..6d028e8fa9 100644 --- a/packages/tui/src/theme/resolve.ts +++ b/packages/tui/src/theme/resolve.ts @@ -1,9 +1,9 @@ import { RGBA } from "@opentui/core" import { ansiToRgba } from "./color" -import type { ColorValue, Theme, ThemeColor, ThemeJson } from "./v1" +import type { ColorValue, Theme, ThemeColor, ThemeV1Json } from "./v1" export function resolveThemeColors( - theme: ThemeJson, + theme: ThemeV1Json, mode: "dark" | "light", resolveAnsi: (code: number) => RGBA = ansiToRgba, ) { @@ -43,7 +43,9 @@ export function resolveThemeColors( ? resolveColor(theme.theme.selectedListItemText!) : resolved.background!, backgroundMenu: - theme.theme.backgroundMenu === undefined ? resolved.backgroundElement! : resolveColor(theme.theme.backgroundMenu), + theme.theme.backgroundMenu === undefined + ? resolved.backgroundElement! + : resolveColor(theme.theme.backgroundMenu), } satisfies Omit, hasSelectedListItemText, thinkingOpacity: theme.theme.thinkingOpacity ?? 0.6, diff --git a/packages/tui/src/theme/v1.ts b/packages/tui/src/theme/v1.ts index 483882e274..5cf1df1b9f 100644 --- a/packages/tui/src/theme/v1.ts +++ b/packages/tui/src/theme/v1.ts @@ -98,7 +98,7 @@ export type Variant = { light: HexColor | RefName } export type ColorValue = HexColor | RefName | Variant | RGBA | number -export type ThemeJson = { +export type ThemeV1Json = { $schema?: string defs?: Record theme: Omit, "selectedListItemText" | "backgroundMenu"> & { @@ -108,7 +108,7 @@ export type ThemeJson = { } } -export const DEFAULT_THEMES: Record = { +export const DEFAULT_THEMES: Record = { aura, ayu, catppuccin, diff --git a/packages/tui/src/theme/v2/defaults.ts b/packages/tui/src/theme/v2/defaults.ts index 666f45014c..e9eb352a07 100644 --- a/packages/tui/src/theme/v2/defaults.ts +++ b/packages/tui/src/theme/v2/defaults.ts @@ -1,4 +1,4 @@ -import type { HueName, ThemeFile } from "./schema" +import type { HueName, ThemeDocument } from "./schema" export const DEFAULT_CATEGORICAL = [ "blue", @@ -437,4 +437,4 @@ export const DEFAULT_THEME = { }, }, }, -} satisfies ThemeFile +} satisfies ThemeDocument diff --git a/packages/tui/src/theme/v2/index.ts b/packages/tui/src/theme/v2/index.ts index 953112da50..b6cfbb98f5 100644 --- a/packages/tui/src/theme/v2/index.ts +++ b/packages/tui/src/theme/v2/index.ts @@ -16,7 +16,7 @@ export { SyntaxDefinition, SyntaxToken, ThemeDefinition, - ThemeFile, + ThemeDocument, type BackgroundDefinition, type DiffDefinition, type FileThemeDefinition, diff --git a/packages/tui/src/theme/v2/resolve.ts b/packages/tui/src/theme/v2/resolve.ts index 74d06e8f1a..500f853dbd 100644 --- a/packages/tui/src/theme/v2/resolve.ts +++ b/packages/tui/src/theme/v2/resolve.ts @@ -11,7 +11,7 @@ import { HueAlias, HueStep, ThemeDefinition, - ThemeFile, + ThemeDocument, } from "./schema" import type { ActionStateKey, @@ -26,7 +26,6 @@ import type { import { selectTheme, selectThemeMode } from "./select" const decodeThemeDefinitionSchema = Schema.decodeUnknownSync(ThemeDefinition) -const decodeThemeFileSchema = Schema.decodeUnknownSync(ThemeFile) function decodeThemeDefinition(input: unknown) { try { @@ -36,27 +35,18 @@ function decodeThemeDefinition(input: unknown) { } } -function decodeThemeFile(input: unknown, name: string) { - try { - return decodeThemeFileSchema(input) - } catch (error) { - throw themeDecodeError(error, name) - } -} - -function themeDecodeError(error: unknown, name: string) { +export function themeDecodeError(error: unknown, name: string) { const message = Schema.isSchemaError(error) ? error.message : String(error) const value = /got ("[^"]*"|\S+)/.exec(message)?.[1] ?? "value" return new Error(`Invalid theme: ${name} ${value} is an invalid value`, { cause: error }) } -export function resolveThemeFile(file: ThemeFile, mode?: "light" | "dark", name = "theme") { - const decoded = decodeThemeFile(file, name) - const selected = selectThemeMode(decoded, mode) +export function resolveThemeDocument(document: ThemeDocument, mode?: "light" | "dark") { + const selected = selectThemeMode(document, mode) const definition = selected.expanded ? selected.theme : expandTheme(selected.theme) const defaults = expandTheme(selectTheme(DEFAULT_THEME, selected.mode)) const core = expandTokens(fallback()) - const merged = decoded.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition) + const merged = document.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition) if (!merged["hue"]) throw new Error("Standalone themes must provide hues") return resolveExpandedTheme({ ...merged, diff --git a/packages/tui/src/theme/v2/schema.ts b/packages/tui/src/theme/v2/schema.ts index 76a33f7a09..2256a3a1d6 100644 --- a/packages/tui/src/theme/v2/schema.ts +++ b/packages/tui/src/theme/v2/schema.ts @@ -251,8 +251,8 @@ const FileMetadata = { version: Schema.Literal(2), standalone: Schema.optional(Schema.Boolean), } -export const ThemeFile = Schema.Union([ +export const ThemeDocument = Schema.Union([ Schema.Struct({ ...FileMetadata, light: ModeDefinition, dark: Schema.optional(ModeDefinition) }), Schema.Struct({ ...FileMetadata, light: Schema.optional(ModeDefinition), dark: ModeDefinition }), ]) -export type ThemeFile = Schema.Schema.Type +export type ThemeDocument = Schema.Schema.Type diff --git a/packages/tui/src/theme/v2/select.ts b/packages/tui/src/theme/v2/select.ts index c763179f53..17d47488fa 100644 --- a/packages/tui/src/theme/v2/select.ts +++ b/packages/tui/src/theme/v2/select.ts @@ -5,45 +5,45 @@ import type { Mode, ModeDefinition, ThemeDefinition, - ThemeFile, + ThemeDocument, } from "./index" export function selectTheme( - file: ThemeFile & { light: ThemeDefinition; dark: ThemeDefinition }, + document: ThemeDocument & { light: ThemeDefinition; dark: ThemeDefinition }, mode?: Mode, ): ThemeDefinition -export function selectTheme(file: ThemeFile, mode?: Mode): FileThemeDefinition -export function selectTheme(file: ThemeFile, mode?: Mode) { - return selectThemeMode(file, mode).theme +export function selectTheme(document: ThemeDocument, mode?: Mode): FileThemeDefinition +export function selectTheme(document: ThemeDocument, mode?: Mode) { + return selectThemeMode(document, mode).theme } export function selectThemeMode( - file: ThemeFile, + document: ThemeDocument, mode: Mode = "light", ): { theme: FileThemeDefinition; mode: Mode; expanded: boolean } { - const modes = themeModes(file) + const modes = themeModes(document) const selectedMode = modes.includes(mode) ? mode : modes[0] - const selected = file[selectedMode] + const selected = document[selectedMode] if (!selected) throw new Error("Theme must provide at least one mode") - if (merges(file.light) && merges(file.dark)) throw new Error("Light and dark themes cannot both merge modes") + if (merges(document.light) && merges(document.dark)) throw new Error("Light and dark themes cannot both merge modes") if (!merges(selected)) return { theme: selected, mode: selectedMode, expanded: false } const otherMode = selectedMode === "light" ? "dark" : "light" - const other = file[otherMode] + const other = document[otherMode] if (!other) throw new Error(`The ${selectedMode} theme cannot merge without a ${otherMode} theme`) const merged = mergeTheme(expandTheme(other), expandTheme(selected)) if (!merged["hue"]) throw new Error(`The ${otherMode} theme must provide hues when ${selectedMode} merges modes`) return { theme: merged as FileThemeDefinition, mode: selectedMode, expanded: true } } -export function themeModes(file: ThemeFile): readonly Mode[] { - if (merges(file.light) && !file.dark) throw new Error("The light theme cannot merge without a dark theme") - if (merges(file.dark) && !file.light) throw new Error("The dark theme cannot merge without a light theme") - return (["light", "dark"] as const).filter((mode) => file[mode] !== undefined) +export function themeModes(document: ThemeDocument): readonly Mode[] { + if (merges(document.light) && !document.dark) throw new Error("The light theme cannot merge without a dark theme") + if (merges(document.dark) && !document.light) throw new Error("The dark theme cannot merge without a light theme") + return (["light", "dark"] as const).filter((mode) => document[mode] !== undefined) } -export function supportsThemeMode(file: ThemeFile, mode: Mode) { - return themeModes(file).includes(mode) +export function supportsThemeMode(document: ThemeDocument, mode: Mode) { + return themeModes(document).includes(mode) } function merges(definition: ModeDefinition | undefined): definition is MergeModeDefinition { diff --git a/packages/tui/src/theme/v2/v1-migrate.ts b/packages/tui/src/theme/v2/v1-migrate.ts index 42e7c31b96..4fdea0d135 100644 --- a/packages/tui/src/theme/v2/v1-migrate.ts +++ b/packages/tui/src/theme/v2/v1-migrate.ts @@ -1,8 +1,8 @@ import { RGBA } from "@opentui/core" import { oklchToHex, rgbToOklch } from "@opencode-ai/ui/theme/color" -import type { Theme, ThemeJson } from "../index" +import type { Theme, ThemeV1Json } from "../v1" import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "./defaults" -import type { FileThemeDefinition, Mode, ThemeFile } from "./index" +import type { FileThemeDefinition, Mode, ThemeDocument } from "./index" import { HueStep } from "./schema" type ThemeColor = Exclude @@ -14,7 +14,7 @@ const categoricalTokens: readonly V1HueToken[] = ["secondary", "accent", "succes const minimumChroma = 0.03 const lightThreshold = 0.6 -export function migrateV1(theme: ThemeJson): ThemeFile { +export function migrateV1(theme: ThemeV1Json): ThemeDocument { const light = resolveV1(theme, "light") const dark = resolveV1(theme, "dark") if (light.background.a > 0 && dark.background.a > 0 && light.background.equals(dark.background)) { @@ -234,7 +234,7 @@ function ambiguous(color: RGBA, chroma = toOklch(color).c) { return color.toInts()[3] === 0 || chroma < minimumChroma } -function resolveV1(theme: ThemeJson, mode: "dark" | "light"): Theme { +function resolveV1(theme: ThemeV1Json, mode: "dark" | "light"): Theme { const defs = theme.defs ?? {} function resolveColor(value: unknown, chain: string[] = []): RGBA { diff --git a/packages/tui/test/cli/tui/theme-mode.test.tsx b/packages/tui/test/cli/tui/theme-mode.test.tsx index cb195e384b..0b8123f05c 100644 --- a/packages/tui/test/cli/tui/theme-mode.test.tsx +++ b/packages/tui/test/cli/tui/theme-mode.test.tsx @@ -1,10 +1,13 @@ /** @jsxImportSource @opentui/solid */ import { testRender } from "@opentui/solid" import { expect, test } from "bun:test" +import { RGBA } from "@opentui/core" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" import { DEFAULT_THEMES } from "../../../src/theme" +import { DEFAULT_THEME } from "../../../src/theme/v2/defaults" +import { selectTheme } from "../../../src/theme/v2/select" import { ConfigProvider } from "../../../src/config" -import { ThemeProvider, useTheme } from "../../../src/context/theme" +import { ThemeProvider, useTheme, type ThemeError } from "../../../src/context/theme" async function wait(fn: () => boolean) { const started = Date.now() @@ -24,6 +27,7 @@ test("uses an available mode while retaining the pinned preference", async () => const darkOnly = structuredClone(DEFAULT_THEMES.opencode) darkOnly.theme.background = "#111111" darkOnly.theme.text = "#eeeeee" + const native = { version: 2, dark: { text: { default: "#abcdef" } } } as const let theme: ReturnType | undefined function Probe() { @@ -42,7 +46,7 @@ test("uses an available mode while retaining the pinned preference", async () => Promise.resolve({ "light-only": lightOnly, "dark-only": darkOnly, dual }) }} + source={{ discover: () => Promise.resolve({ "light-only": lightOnly, "dark-only": darkOnly, dual, native }) }} > @@ -66,6 +70,85 @@ test("uses an available mode while retaining the pinned preference", async () => expect(current().set("dual")).toBeTrue() await wait(() => current().mode() === "dark") expect(current().modes()).toEqual(["light", "dark"]) + expect(current().set("native")).toBeTrue() + await wait(() => current().selected === "native") + expect(current().modes()).toEqual(["dark"]) + expect(current().themeV2.text.default.equals(RGBA.fromHex("#abcdef"))).toBeTrue() + } finally { + app.renderer.destroy() + } +}) + +test.each([ + ["schema", { version: 2, light: { categorical: [] } }], + ["mode merging", { version: 2, light: { mergeMode: true } }], + ["token reference", { version: 2, light: { text: { default: "$missing" } } }], +] as const)("falls back to OpenCode when configured V2 theme %s is invalid", async (_label, source) => { + let theme: ReturnType | undefined + let failure: ThemeError | undefined + let unsubscribe: (() => void) | undefined + + function Probe() { + const value = useTheme() + theme = value + unsubscribe = value.onError((error) => (failure = error)) + return {value.selected} + } + + const app = await testRender( + () => ( + + Promise.resolve({ invalid: source }) }}> + + + + ), + { width: 20, height: 2 }, + ) + app.renderer.start() + + try { + await wait(() => theme?.ready === true) + expect(theme?.selected).toBe("opencode") + expect(failure?.name).toBe("invalid") + expect(failure?.error).toBeInstanceOf(Error) + expect(failure?.error.message.length).toBeGreaterThan(0) + } finally { + unsubscribe?.() + app.renderer.destroy() + } +}) + +test("contextual themes fall back to a standalone theme's base view", async () => { + const standalone = { + version: 2, + standalone: true, + dark: { hue: selectTheme(DEFAULT_THEME, "dark").hue }, + } as const + let theme: ReturnType | undefined + + function Probe() { + theme = useTheme() + return {theme.selected} + } + + const app = await testRender( + () => ( + + Promise.resolve({ standalone }) }}> + + + + ), + { width: 20, height: 2 }, + ) + app.renderer.start() + + try { + await wait(() => theme?.ready === true) + if (!theme) throw new Error("Theme provider is not mounted") + expect(theme.contextual("elevated").themeV2.text.default).toBe(theme.themeV2.text.default) + expect(theme.contextual("overlay").themeV2.background.default).toBe(theme.themeV2.background.default) } finally { app.renderer.destroy() } diff --git a/packages/tui/test/theme.test.ts b/packages/tui/test/theme.test.ts index b2208f42c4..05d13d8601 100644 --- a/packages/tui/test/theme.test.ts +++ b/packages/tui/test/theme.test.ts @@ -2,7 +2,16 @@ import { expect, test } from "bun:test" import { mkdir, writeFile } from "node:fs/promises" import path from "node:path" import type { TerminalColors } from "@opentui/core" -import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme } from "../src/theme" +import { + DEFAULT_THEMES, + addTheme, + allThemes, + hasTheme, + parseTheme, + resolveTheme, + setCustomThemes, + upsertTheme, +} from "../src/theme" import { discoverThemes, themeDirectories } from "../src/theme/discovery" import { terminalMode } from "../src/theme/system" import { tmpdir } from "./fixture/fixture" @@ -10,7 +19,7 @@ import { tmpdir } from "./fixture/fixture" test("addTheme writes into module theme store", () => { const name = `plugin-theme-${Date.now()}` expect(addTheme(name, DEFAULT_THEMES.opencode)).toBe(true) - expect(allThemes()[name]).toBeDefined() + expect(allThemes()[name]).toBe(DEFAULT_THEMES.opencode) }) test("addTheme keeps first theme for duplicate names", () => { @@ -22,15 +31,92 @@ test("addTheme keeps first theme for duplicate names", () => { expect(addTheme(name, one)).toBe(true) expect(addTheme(name, two)).toBe(false) - expect(allThemes()[name]!.theme.primary).toBe("#101010") + expect(allThemes()[name]).toBe(one) }) -test("addTheme ignores entries without a theme object", () => { +test("addTheme ignores values without a V1 theme or version", () => { const name = `plugin-theme-invalid-${Date.now()}` expect(addTheme(name, { defs: { a: "#ffffff" } })).toBe(false) + expect(addTheme(name, { light: {} })).toBe(false) expect(allThemes()[name]).toBeUndefined() }) +test("addTheme defers validation of versioned sources", () => { + const name = `plugin-theme-versioned-${Date.now()}` + expect(addTheme(name, { version: 2 })).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow(`Invalid theme: ${name}`) +}) + +test("parseTheme delegates malformed V1 sources and rejects unknown versions", () => { + expect(() => parseTheme({})).toThrow() + expect(() => parseTheme({ version: 3 })).toThrow("Unsupported theme version: 3") +}) + +test("parses unversioned and explicit V1 themes lazily once", () => { + const unversioned = structuredClone(DEFAULT_THEMES.opencode) + const explicit = { ...structuredClone(DEFAULT_THEMES.opencode), version: 1 } + const first = parseTheme(unversioned, "unversioned") + const second = parseTheme(explicit, "explicit") + + expect(first.version).toBe(2) + expect(second.version).toBe(2) + expect(parseTheme(unversioned, "unversioned")).toBe(first) + expect(parseTheme(explicit, "explicit")).toBe(second) +}) + +test("decodes native V2 themes lazily once", () => { + const name = `plugin-theme-v2-${Date.now()}` + const source = { version: 2, light: { categorical: ["red"] } } as const + + expect(addTheme(name, source)).toBe(true) + expect(allThemes()[name]).toBe(source) + const document = parseTheme(allThemes()[name]!, name) + expect(document.light?.categorical).toEqual(["red"]) + expect(parseTheme(allThemes()[name]!, name)).toBe(document) +}) + +test("defers invalid V2 errors until parsing", () => { + const name = `plugin-theme-invalid-v2-${Date.now()}` + expect(addTheme(name, { version: 2, light: { categorical: [] } })).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow(`Invalid theme: ${name}`) +}) + +test("defers invalid V1 errors until parsing", () => { + const name = `plugin-theme-invalid-v1-${Date.now()}` + const source = structuredClone(DEFAULT_THEMES.opencode) + source.defs = { ...source.defs, one: "two", two: "one" } + source.theme.primary = "one" + + expect(addTheme(name, source)).toBe(true) + expect(() => parseTheme(allThemes()[name]!, name)).toThrow("Circular color reference") +}) + +test("replacement sources receive independent parse caches", () => { + const name = `plugin-theme-replace-${Date.now()}` + const first = structuredClone(DEFAULT_THEMES.opencode) + const second = structuredClone(DEFAULT_THEMES.opencode) + second.theme.primary = "#123456" + + expect(addTheme(name, first)).toBe(true) + const previous = parseTheme(allThemes()[name]!, name) + expect(upsertTheme(name, second)).toBe(true) + const next = parseTheme(allThemes()[name]!, name) + expect(next).not.toBe(previous) + expect(parseTheme(allThemes()[name]!, name)).toBe(next) +}) + +test("custom themes retain precedence over plugin themes", () => { + const name = `plugin-theme-precedence-${Date.now()}` + const plugin = structuredClone(DEFAULT_THEMES.opencode) + const custom = structuredClone(DEFAULT_THEMES.opencode) + + expect(addTheme(name, plugin)).toBe(true) + setCustomThemes({ [name]: custom }) + expect(allThemes()[name]).toBe(custom) + setCustomThemes({}) + expect(allThemes()[name]).toBe(plugin) +}) + test("hasTheme checks theme presence", () => { const name = `plugin-theme-has-${Date.now()}` expect(hasTheme(name)).toBe(false) diff --git a/packages/tui/test/theme/v2/resolve.test.ts b/packages/tui/test/theme/v2/resolve.test.ts index e91a79d2b2..9d42b40341 100644 --- a/packages/tui/test/theme/v2/resolve.test.ts +++ b/packages/tui/test/theme/v2/resolve.test.ts @@ -1,16 +1,21 @@ import { expect, test } from "bun:test" import { RGBA } from "@opentui/core" +import { parseTheme, type ThemeDocumentSource } from "../../../src/theme" import { DEFAULT_THEME } from "../../../src/theme/v2/defaults" -import type { ThemeDefinition } from "../../../src/theme/v2" -import { resolveTheme, resolveThemeFile } from "../../../src/theme/v2/resolve" +import type { Mode, ThemeDefinition } from "../../../src/theme/v2" +import { resolveTheme, resolveThemeDocument } from "../../../src/theme/v2/resolve" import { selectTheme } from "../../../src/theme/v2/select" const light = selectTheme(DEFAULT_THEME, "light") const dark = selectTheme(DEFAULT_THEME, "dark") -test("resolves one-mode files with defaults for the available mode", () => { - const resolvedLight = resolveThemeFile({ version: 2, light: {} }, "dark") - const resolvedDark = resolveThemeFile({ version: 2, dark: {} }, "light") +function resolveSource(source: ThemeDocumentSource, mode?: Mode, name?: string) { + return resolveThemeDocument(parseTheme(source, name), mode) +} + +test("resolves one-mode documents with defaults for the available mode", () => { + const resolvedLight = resolveSource({ version: 2, light: {} }, "dark") + const resolvedDark = resolveSource({ version: 2, dark: {} }, "light") expect(resolvedLight.background.default.equals(resolveTheme(light).background.default)).toBeTrue() expect(resolvedDark.background.default.equals(resolveTheme(dark).background.default)).toBeTrue() @@ -18,26 +23,19 @@ test("resolves one-mode files with defaults for the available mode", () => { expect(resolvedDark.categorical.length).toBeGreaterThan(0) }) -test("rejects theme files without a mode", () => { - // @ts-expect-error Runtime decoding also enforces the at-least-one-mode invariant. - expect(() => resolveThemeFile({ version: 2 })).toThrow("Invalid theme") +test("rejects theme documents without a mode", () => { + expect(() => resolveSource({ version: 2 })).toThrow("Invalid theme") }) test("validates and resolves categorical hues in configured order", () => { - const theme = resolveThemeFile({ version: 2, light: { categorical: ["accent", "red", "interactive"] } }, "light") + const theme = resolveSource({ version: 2, light: { categorical: ["accent", "red", "interactive"] } }, "light") expect(theme.categorical[0]).toBe(theme.hue.accent) expect(theme.categorical[1]).toBe(theme.hue.red) expect(theme.categorical[2]).toBe(theme.hue.interactive) expect(theme.contexts["@context:elevated"]?.categorical).toBe(theme.categorical) - expect(() => resolveThemeFile({ version: 2, light: { categorical: [] } }, "light")).toThrow("Invalid theme") - expect(() => - resolveThemeFile( - // @ts-expect-error Runtime decoding rejects unknown categorical hue names. - { version: 2, light: { categorical: ["magenta"] } }, - "light", - ), - ).toThrow("Invalid theme") + expect(() => resolveSource({ version: 2, light: { categorical: [] } }, "light")).toThrow("Invalid theme") + expect(() => resolveSource({ version: 2, light: { categorical: ["magenta"] } }, "light")).toThrow("Invalid theme") }) test("uses the default categorical order for direct definitions", () => { @@ -93,7 +91,7 @@ test("resolves base hue aliases and rejects circular hue aliases", () => { ...light, hue: { ...light.hue, blue: "$hue.red", purple: "$hue.blue" }, }) - const overridden = resolveThemeFile({ version: 2, light: { hue: { blue: "$hue.red" } }, dark: {} }, "light") + const overridden = resolveSource({ version: 2, light: { hue: { blue: "$hue.red" } }, dark: {} }, "light") expect(aliased.hue.blue).not.toBe(aliased.hue.red) expect(aliased.hue.blue[500].equals(aliased.hue.red[500])).toBeTrue() @@ -131,8 +129,8 @@ test("steps by hue source when adjacent colors have equal values", () => { expect(theme.increase(theme.hue.neutral[300])).toBe(theme.hue.neutral[400]) }) -test("merges partial files with the selected OpenCode defaults", () => { - const theme = resolveThemeFile( +test("merges partial documents with the selected OpenCode defaults", () => { + const theme = resolveSource( { version: 2, light: { @@ -150,7 +148,7 @@ test("merges partial files with the selected OpenCode defaults", () => { }) test("expands user structural fallbacks before merging defaults", () => { - const expanded = resolveThemeFile( + const expanded = resolveSource( { version: 2, light: { @@ -161,7 +159,7 @@ test("expands user structural fallbacks before merging defaults", () => { }, "light", ) - const isolatedState = resolveThemeFile( + const isolatedState = resolveSource( { version: 2, light: { @@ -181,9 +179,9 @@ test("expands user structural fallbacks before merging defaults", () => { }) test("standalone themes skip OpenCode defaults and use the red core fallback", () => { - const file = { version: 2, standalone: true, light: { hue: light.hue }, dark: { hue: dark.hue } } as const - const lightTheme = resolveThemeFile(file, "light") - const darkTheme = resolveThemeFile(file, "dark") + const document = { version: 2, standalone: true, light: { hue: light.hue }, dark: { hue: dark.hue } } as const + const lightTheme = resolveSource(document, "light") + const darkTheme = resolveSource(document, "dark") expect(lightTheme.text.default.toInts()).toEqual([255, 0, 0, 255]) expect(lightTheme.background.default.toInts()).toEqual([255, 0, 0, 255]) @@ -192,7 +190,7 @@ test("standalone themes skip OpenCode defaults and use the red core fallback", ( }) test("uses defaults for the selected mode when it merges the other mode", () => { - const theme = resolveThemeFile( + const theme = resolveSource( { version: 2, light: { hue: light.hue, background: { default: "#123456" } }, @@ -217,7 +215,7 @@ test("resolves matched action variants and states", () => { }) test("resolves elevated hover surfaces from direct colors", () => { - const theme = resolveThemeFile( + const theme = resolveSource( { version: 2, light: { background: { surface: { offset: "#123456", overlay: "#234567" } } }, @@ -231,7 +229,7 @@ test("resolves elevated hover surfaces from direct colors", () => { }) test("resolves transparent colors", () => { - const theme = resolveThemeFile({ + const theme = resolveSource({ version: 2, light: { background: { formfield: { default: "transparent" } } }, dark: { background: { formfield: { default: "transparent" } } }, @@ -241,7 +239,7 @@ test("resolves transparent colors", () => { test("reports theme decoding failures as native errors", () => { expect(() => - resolveThemeFile( + resolveSource( { version: 2, light: { text: { default: "opaque" } }, diff --git a/packages/tui/test/theme/v2/select.test.ts b/packages/tui/test/theme/v2/select.test.ts index 792321de13..00f0bd437d 100644 --- a/packages/tui/test/theme/v2/select.test.ts +++ b/packages/tui/test/theme/v2/select.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import type { HueDefinition, ThemeDefinition, ThemeFile } from "../../../src/theme/v2" +import type { HueDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2" import { selectTheme, selectThemeMode, supportsThemeMode, themeModes } from "../../../src/theme/v2/select" const hue = {} as HueDefinition @@ -11,20 +11,20 @@ const dark = { } satisfies ThemeDefinition test("requires and selects independent light and dark themes", () => { - const file = { version: 2, light, dark } satisfies ThemeFile - expect(selectTheme(file)).toBe(light) - expect(selectTheme(file, "light")).toBe(light) - expect(selectTheme(file, "dark")).toBe(dark) - expect(selectThemeMode(file, "dark").mode).toBe("dark") + const document = { version: 2, light, dark } satisfies ThemeDocument + expect(selectTheme(document)).toBe(light) + expect(selectTheme(document, "light")).toBe(light) + expect(selectTheme(document, "dark")).toBe(dark) + expect(selectThemeMode(document, "dark").mode).toBe("dark") }) test("merges an expanded mode override over the other mode", () => { - const file = { + const document = { version: 2, light, dark: { mergeMode: true, text: { default: "#ffffff" } }, - } satisfies ThemeFile - const selected = selectTheme(file, "dark") + } satisfies ThemeDocument + const selected = selectTheme(document, "dark") expect(selected.hue).toBeDefined() expect(selected.text?.default).toBe("#ffffff") @@ -41,8 +41,8 @@ test("replaces categorical order in a merge mode", () => { }) test("selects the available mode when the requested mode is missing", () => { - const lightOnly = { version: 2, light } satisfies ThemeFile - const darkOnly = { version: 2, dark } satisfies ThemeFile + const lightOnly = { version: 2, light } satisfies ThemeDocument + const darkOnly = { version: 2, dark } satisfies ThemeDocument expect(themeModes(lightOnly)).toEqual(["light"]) expect(themeModes(darkOnly)).toEqual(["dark"]) @@ -62,10 +62,10 @@ test("rejects a merge mode without its base mode", () => { }) test("rejects mutual mode merging", () => { - const file = { + const document = { version: 2, light: { mergeMode: true }, dark: { mergeMode: true }, - } satisfies ThemeFile - expect(() => selectTheme(file)).toThrow("cannot both merge") + } satisfies ThemeDocument + expect(() => selectTheme(document)).toThrow("cannot both merge") }) diff --git a/packages/tui/test/theme/v2/types.test.ts b/packages/tui/test/theme/v2/types.test.ts index a44322d71a..f98a013f5e 100644 --- a/packages/tui/test/theme/v2/types.test.ts +++ b/packages/tui/test/theme/v2/types.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeFile } from "../../../src/theme/v2" +import type { BackgroundDefinition, TextDefinition, ThemeDefinition, ThemeDocument } from "../../../src/theme/v2" const text = { default: "$hue.neutral.900", @@ -51,11 +51,11 @@ const definition = { "@context:overlay": { background: { default: "$hue.neutral.300" } }, } satisfies ThemeDefinition -const file = { version: 2, light: definition, dark: definition } satisfies ThemeFile -const lightOnly = { version: 2, light: definition } satisfies ThemeFile -const darkOnly = { version: 2, dark: definition } satisfies ThemeFile -// @ts-expect-error A theme file must provide at least one mode. -const empty = { version: 2 } satisfies ThemeFile +const document = { version: 2, light: definition, dark: definition } satisfies ThemeDocument +const lightOnly = { version: 2, light: definition } satisfies ThemeDocument +const darkOnly = { version: 2, dark: definition } satisfies ThemeDocument +// @ts-expect-error A theme document must provide at least one mode. +const empty = { version: 2 } satisfies ThemeDocument test("supports property-first definitions, variants, states, and contexts", () => { expect(text.action.primary.$hovered).toBe("$hue.neutral.200") @@ -68,7 +68,7 @@ test("supports property-first definitions, variants, states, and contexts", () = expect(definition["@context:elevated"].text?.default).toBe("$hue.neutral.800") expect(definition["@context:overlay"].background?.default).toBe("$hue.neutral.300") expect(definition.categorical).toEqual(["blue", "accent"]) - expect(file.light).toBe(definition) + expect(document.light).toBe(definition) expect(lightOnly.light).toBe(definition) expect(darkOnly.dark).toBe(definition) expect(empty.version).toBe(2) diff --git a/packages/tui/test/theme/v2/v1-migrate.test.ts b/packages/tui/test/theme/v2/v1-migrate.test.ts index 84c3639b09..ced1ae4800 100644 --- a/packages/tui/test/theme/v2/v1-migrate.test.ts +++ b/packages/tui/test/theme/v2/v1-migrate.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { DEFAULT_THEMES, resolveTheme as resolveV1 } from "../../../src/theme" -import { resolveThemeFile } from "../../../src/theme/v2/resolve" +import { resolveThemeDocument } from "../../../src/theme/v2/resolve" import { selectThemeMode, themeModes } from "../../../src/theme/v2/select" import { migrateV1 } from "../../../src/theme/v2/v1-migrate" import { DEFAULT_CATEGORICAL, DEFAULT_THEME } from "../../../src/theme/v2/defaults" @@ -9,7 +9,7 @@ test("migrates resolved V1 modes into literal V2 tokens", () => { const migrated = migrateV1(DEFAULT_THEMES.opencode) if (!migrated.light || !migrated.dark) throw new Error("Expected both modes") const legacy = resolveV1(DEFAULT_THEMES.opencode, "light") - const resolved = resolveThemeFile(migrated, "light") + const resolved = resolveThemeDocument(migrated, "light") expect(migrated.standalone).toBeTrue() expect(migrated.light.categorical?.length).toBeGreaterThan(0) @@ -83,8 +83,8 @@ test("infers chromatic hues, anchors light and dark colors, and aliases ambiguou expect(migrated.light.hue?.purple).toBe("$hue.gray") expect(migrated.light.hue?.accent).toBe("$hue.gray") expect(migrated.light.hue?.interactive).toBe("$hue.gray") - expect(() => resolveThemeFile(migrated, "light")).not.toThrow() - expect(() => resolveThemeFile(migrated, "dark")).not.toThrow() + expect(() => resolveThemeDocument(migrated, "light")).not.toThrow() + expect(() => resolveThemeDocument(migrated, "dark")).not.toThrow() }) test("orders categorical hues by V1 semantic color mapping", () => { @@ -185,7 +185,7 @@ test("migrates every built-in V1 theme in its supported modes", () => { for (const source of Object.values(DEFAULT_THEMES)) { const migrated = migrateV1(source) for (const mode of themeModes(migrated)) { - expect(resolveThemeFile(migrated, mode).text.default).toBeDefined() + expect(resolveThemeDocument(migrated, mode).text.default).toBeDefined() } } }) From 74e92f73e034dfcfbd092ae12d8ff2ea2300414e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:32:33 -0500 Subject: [PATCH 20/27] refactor(ai): remove unused response format (#38540) Co-authored-by: Aiden Cline --- packages/ai/src/llm.ts | 2 +- packages/ai/src/schema/messages.ts | 9 --------- packages/core/src/aisdk.ts | 7 ------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/ai/src/llm.ts b/packages/ai/src/llm.ts index e4781d8608..ecdf30ae47 100644 --- a/packages/ai/src/llm.ts +++ b/packages/ai/src/llm.ts @@ -81,7 +81,7 @@ const GENERATE_OBJECT_TOOL_NAME = "generate_object" const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." -type GenerateObjectBase = Omit +type GenerateObjectBase = Omit export class GenerateObjectResponse { constructor( diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index 4a9de3a735..e6617ddc9e 100644 --- a/packages/ai/src/schema/messages.ts +++ b/packages/ai/src/schema/messages.ts @@ -261,13 +261,6 @@ export namespace ToolChoice { } } -export const ResponseFormat = Schema.Union([ - Schema.Struct({ type: Schema.Literal("text") }), - Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }), - Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }), -]).pipe(Schema.toTaggedUnion("type")) -export type ResponseFormat = Schema.Schema.Type - export class LLMRequest extends Schema.Class("LLM.Request")({ id: Schema.optional(Schema.String), model: ModelSchema, @@ -278,7 +271,6 @@ export class LLMRequest extends Schema.Class("LLM.Request")({ generation: Schema.optional(GenerationOptions), providerOptions: Schema.optional(ProviderOptions), http: Schema.optional(HttpOptions), - responseFormat: Schema.optional(ResponseFormat), cache: Schema.optional(CachePolicy), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), }) {} @@ -296,7 +288,6 @@ export namespace LLMRequest { generation: request.generation, providerOptions: request.providerOptions, http: request.http, - responseFormat: request.responseFormat, cache: request.cache, metadata: request.metadata, }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 6d749b8526..33a4d670af 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -422,7 +422,6 @@ function callOptions(request: LLMRequest): LanguageModelV3CallOptions { presencePenalty: request.generation?.presencePenalty, frequencyPenalty: request.generation?.frequencyPenalty, seed: request.generation?.seed, - responseFormat: responseFormat(request), tools: request.tools.map(tool), toolChoice: toolChoice(request.toolChoice), headers: request.http?.headers, @@ -527,12 +526,6 @@ function toolChoice(input: LLMRequest["toolChoice"]): LanguageModelV3ToolChoice return { type: input.type } } -function responseFormat(request: LLMRequest): LanguageModelV3CallOptions["responseFormat"] { - if (request.responseFormat?.type === "json") - return { type: "json", schema: request.responseFormat.schema as JSONSchema7 } - if (request.responseFormat) return { type: "text" } -} - function providerOptions(input: LLMRequest["providerOptions"]): SharedV3ProviderOptions | undefined { if (!input) return undefined return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)])) From ad596fb42bfbf29a2585babbae1e987c54240eea Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:34:24 -0500 Subject: [PATCH 21/27] chore(core): upgrade fff to 0.10.1 (#38545) Co-authored-by: Aiden Cline --- bun.lock | 22 +++++++++--------- package.json | 1 - packages/core/package.json | 2 +- patches/@ff-labs%2Ffff-bun@0.9.3.patch | 31 -------------------------- 4 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 patches/@ff-labs%2Ffff-bun@0.9.3.patch diff --git a/bun.lock b/bun.lock index 72db1b9e92..24e3c97c4e 100644 --- a/bun.lock +++ b/bun.lock @@ -362,7 +362,7 @@ "@aws-sdk/credential-providers": "3.1057.0", "@effect/platform-node": "catalog:", "@effect/sql-sqlite-bun": "catalog:", - "@ff-labs/fff-bun": "0.9.4", + "@ff-labs/fff-bun": "0.10.1", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/ai": "workspace:*", @@ -1641,23 +1641,25 @@ "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], - "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xyivu2xB++O5xXDx5Qm50JsU2aXt8YgXlGVhH/HE7UMYDrE6L6f1RYdYs8Y0bn0D3D0+bFBrN5ELPszK9E4Wbw=="], + "@ff-labs/fff-bin-android-arm64": ["@ff-labs/fff-bin-android-arm64@0.10.1", "", { "os": "android", "cpu": "arm64" }, "sha512-6Bsaa6yKEd2HV1M2WtqSYhoZucKYffIUms6GYoPN48QHP8hZgO8GXJ85/JDxKlkCJsN2hw1ROiwQK0+xUHYhFQ=="], - "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xLooAhCnTDCipPSMMZz7kGF3lhRHx6aP5fb6DJ0Ipyw/w/UWJb+xITJFszUl/QnIBoJ/qjDc93/FZMo1dk6gVA=="], + "@ff-labs/fff-bin-darwin-arm64": ["@ff-labs/fff-bin-darwin-arm64@0.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7yUP+56sG3UTrLg7eepOD16yM2dgiD7g6Ase2XWQB7oXwLV7mBMylPKIli2pP3kHzfw9K+BS3WAwHKHJ2QhxYw=="], - "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-m5+8vA+1veaUUWonwva1WsU6m1HRm8CpYUzr06KDB65mewlmPbqz7+Fh7hjEfiD8C4mHVHe6RysULvAH1yhsdw=="], + "@ff-labs/fff-bin-darwin-x64": ["@ff-labs/fff-bin-darwin-x64@0.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-9zb+P1xtyqu/jVklm5RKF2zm9doRRsBNbkF/a8S4aSqSJoNlQR8ZF7C129fzOLfffVAjjgcO2l8oJgxOzHYiwQ=="], - "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMeWm7CSTVkizy4ZEzUkLDP024tVcbCUthduuIhekFQRDsiaAze0YboIylWb9HBHJCZlCCoZrWAl4nnJbsX7AA=="], + "@ff-labs/fff-bin-linux-arm64-gnu": ["@ff-labs/fff-bin-linux-arm64-gnu@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-KTwr9CUfCJv0vtG2xG+nMxDae/2NJY2/oVmtgSvTnH9baI6JprqsFGphbukx7mdW/QMPwBiYtoO8ZGoC7i92jA=="], - "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-pglE0uLkhnlE6bStXqfgUjYTSj+2sVwXaPfoA0QksidAsQor6NRt8004mygzC9DPubgHq5B9QezPfEwigKaP9Q=="], + "@ff-labs/fff-bin-linux-arm64-musl": ["@ff-labs/fff-bin-linux-arm64-musl@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-oznmSpV+zjiAPiwqbA4y+SAsMaYvDhGllHiT9L4ebdQnSOfcQzlUwvo45OhBPwHBt47tIois4bkF/MITlDk54A=="], - "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-VNKxgl8qs3aTfXViX7lqRK1aLu311h8dtBFqG4Scv+9Oi7WprybUp5L7IZ8sxKERaDAaiJMXHodXa1c90QdK8w=="], + "@ff-labs/fff-bin-linux-x64-gnu": ["@ff-labs/fff-bin-linux-x64-gnu@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-KDpl8lwSEOauP/6FSJIvnARzE+ILm2rVIRQAio9dc5nn56EvlsryBWry0c5V0Tbw1PV0lNrZ+bzcIiPuTacvzw=="], - "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-uFEt0aNL54vQxq1ivjxRuo+thnhS4wLqa4INl4VXnXJUmwB42XXxD+gsj7vzhBLLx4cFf0aWgy/+TVDR8yjZtQ=="], + "@ff-labs/fff-bin-linux-x64-musl": ["@ff-labs/fff-bin-linux-x64-musl@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-mZCpojVtGNDr/wdCUEAHdfrl6qORvKHv3Pw35TaOdshnG4wocoA5bBKTGj/zmAU11o0GMK4V+wUhdNrgqoE10w=="], - "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Yd2Eyxj+slWv+0QDW9/xBpu9FXq+hwD0rXQD5184/88d+xwWCLKhEP2w8I6OO9XCg+kLT79UJb+k0WwXUtBtMw=="], + "@ff-labs/fff-bin-win32-arm64": ["@ff-labs/fff-bin-win32-arm64@0.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-SbT76ETXC5AgV9J8sVDozKG8wzrrxsOn8lbBprlOtx+O5V5EYmS1W7BlsatTHy6ddQ3uU5oOYO04PWZBuP6wXg=="], - "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.9.4", "", { "optionalDependencies": { "@ff-labs/fff-bin-darwin-arm64": "0.9.4", "@ff-labs/fff-bin-darwin-x64": "0.9.4", "@ff-labs/fff-bin-linux-arm64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-arm64-musl": "0.9.4", "@ff-labs/fff-bin-linux-x64-gnu": "0.9.4", "@ff-labs/fff-bin-linux-x64-musl": "0.9.4", "@ff-labs/fff-bin-win32-arm64": "0.9.4", "@ff-labs/fff-bin-win32-x64": "0.9.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-7HUraaK/g5dStAnuKAuzsXVOQvqqX0ylo5G+DxYwsCjCDc42bjoEAAHqz/3Sn3raUNw97KMoz87XR9QyrLEfVw=="], + "@ff-labs/fff-bin-win32-x64": ["@ff-labs/fff-bin-win32-x64@0.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-dcpHUCBEZoXKQCdKa3bADfgcNyeyfM8tatXrILqIFYi9GL8E4GnzKx1rGkgP5ukVtuj0WuoJyyqSvGjpJPIT8w=="], + + "@ff-labs/fff-bun": ["@ff-labs/fff-bun@0.10.1", "", { "optionalDependencies": { "@ff-labs/fff-bin-android-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-arm64": "0.10.1", "@ff-labs/fff-bin-darwin-x64": "0.10.1", "@ff-labs/fff-bin-linux-arm64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-arm64-musl": "0.10.1", "@ff-labs/fff-bin-linux-x64-gnu": "0.10.1", "@ff-labs/fff-bin-linux-x64-musl": "0.10.1", "@ff-labs/fff-bin-win32-arm64": "0.10.1", "@ff-labs/fff-bin-win32-x64": "0.10.1" }, "os": [ "!aix", "!sunos", "!freebsd", "!openbsd", ], "cpu": [ "x64", "arm64", ] }, "sha512-9oUCxypGbf2q3vNfKZ31wdzt5KqjhA9S6TwQaFol/j1lkSHVGvtIa3RvdGHPs1UUuvR/MO7b6pHj054BR9bXPQ=="], "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], diff --git a/package.json b/package.json index 7bb37f946f..2110c4d9af 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,6 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", diff --git a/packages/core/package.json b/packages/core/package.json index cf54590d52..7756c08d7e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,7 +91,7 @@ "@effect/sql-sqlite-bun": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", - "@ff-labs/fff-bun": "0.9.4", + "@ff-labs/fff-bun": "0.10.1", "@opencode-ai/codemode": "workspace:*", "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", diff --git a/patches/@ff-labs%2Ffff-bun@0.9.3.patch b/patches/@ff-labs%2Ffff-bun@0.9.3.patch deleted file mode 100644 index 23a7dd54fb..0000000000 --- a/patches/@ff-labs%2Ffff-bun@0.9.3.patch +++ /dev/null @@ -1,31 +0,0 @@ -diff --git a/src/download.ts b/src/download.ts -index 3454256..6dca25a 100644 ---- a/src/download.ts -+++ b/src/download.ts -@@ -7,7 +7,7 @@ - */ - -+declare const FFF_LIBC: "gnu" | "musl"; - import { existsSync } from "node:fs"; --import { createRequire } from "node:module"; - import { dirname, join } from "node:path"; - import { fileURLToPath } from "node:url"; - import { getLibFilename, getNpmPackageName } from "./platform"; -@@ -54,14 +54,10 @@ export function binaryExists(): boolean { - * in the same directory. - */ - function resolveFromNpmPackage(): string | null { -- const packageName = getNpmPackageName(); -- - try { -- // Use createRequire to resolve the platform package's location -- const require = createRequire(join(getPackageDir(), "package.json")); -- const packageJsonPath = require.resolve(`${packageName}/package.json`); -- const packageDir = dirname(packageJsonPath); -- const binaryPath = join(packageDir, getLibFilename()); -+ const binaryPath = require( -+ `@ff-labs/fff-bin-${process.platform === "linux" ? `linux-${process.arch}-${typeof FFF_LIBC === "string" ? FFF_LIBC : getNpmPackageName().endsWith("musl") ? "musl" : "gnu"}` : `${process.platform}-${process.arch}`}/${process.platform === "win32" ? "fff_c.dll" : process.platform === "darwin" ? "libfff_c.dylib" : "libfff_c.so"}`, -+ ); - - if (existsSync(binaryPath)) { - return binaryPath; From 360e7b412d64ac6ad8bd63415dbf68af0a47c60a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:37:16 -0400 Subject: [PATCH 22/27] feat(tui): expose debug settings (#38546) Co-authored-by: James Long --- packages/tui/src/component/dialog-config.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 8ff9db50c7..1ca585ec13 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -222,6 +222,14 @@ const settings: Setting[] = [ values: [false, true], labels: ["off", "on"], }, + { + title: "Turn token usage", + category: "Debug", + path: ["debug", "turn_tokens"], + default: false, + values: [false, true], + labels: ["off", "on"], + }, ] export function DialogConfig() { From 18fccac6ff8b6e27786869f2d03d86e1b6b1ac1a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:59:51 -0400 Subject: [PATCH 23/27] fix(tui): preserve first message in new sessions (#38542) Co-authored-by: Kit Langton Co-authored-by: James Long <17031+jlongster@users.noreply.github.com> --- packages/tui/src/context/data.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index ddf3823e75..a5a18ff077 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -298,6 +298,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ case "session.created": result.session.invalidate(event.data.sessionID) void result.session.sync(event.data.sessionID) + // Band-aid: a newly created session starts empty, so live events can be its source of truth. + // Fetching pending inputs and projected messages separately lets promotion move an input between snapshots, + // causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until + // hydration can load pending and projected messages atomically. + sync.complete(`session.pending:${event.data.sessionID}`) + sync.complete(`session.message:${event.data.sessionID}`) break case "session.deleted": removeSession(event.data.sessionID) From 2c814120c7f918237812510655fed067ccb902a3 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 13:52:20 -0500 Subject: [PATCH 24/27] fix(ai): keep tools when Anthropic tool_choice is none (#38553) --- .../ai/src/protocols/anthropic-messages.ts | 10 ++++--- .../test/provider/anthropic-messages.test.ts | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index f5b9ea67d9..cc03dfa65b 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -159,7 +159,7 @@ const AnthropicTool = Schema.Struct({ type AnthropicTool = Schema.Schema.Type const AnthropicToolChoice = Schema.Union([ - Schema.Struct({ type: Schema.Literals(["auto", "any"]) }), + Schema.Struct({ type: Schema.Literals(["auto", "any", "none"]) }), Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), ]) @@ -297,7 +297,7 @@ const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSc const lowerToolChoice = (toolChoice: NonNullable) => ProviderShared.matchToolChoice("Anthropic Messages", toolChoice, { auto: () => ({ type: "auto" as const }), - none: () => undefined, + none: () => ({ type: "none" as const }), required: () => ({ type: "any" as const }), tool: (name) => ({ type: "tool" as const, name }), }) @@ -542,7 +542,6 @@ const outputConfig = (request: LLMRequest) => { } const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { - const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema const outputLimit = request.model.defaults?.limits?.output ?? request.model.route.defaults.limits?.output ?? 4096 @@ -551,7 +550,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques // over-mark we keep their tool hints and shed the message-tail ones first. const breakpoints = Cache.newBreakpoints(ANTHROPIC_BREAKPOINT_CAP) const tools = - request.tools.length === 0 || request.toolChoice?.type === "none" + request.tools.length === 0 ? undefined : request.tools.map((tool) => lowerTool( @@ -560,6 +559,9 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques ToolSchemaProjection.modelCompatibility(tool.inputSchema, toolSchemaCompatibility), ), ) + // Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present. + const toolChoice = + tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice) const system = request.system.length === 0 ? undefined diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 30b3c1b6b8..da8bb34287 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -235,6 +235,34 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("keeps tools and sends tool_choice none", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + id: "req_tool_choice_none", + model, + tools: [{ name: "lookup", description: "Look things up", inputSchema: { type: "object", properties: {} } }], + messages: [ + Message.user("What is the weather?"), + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } })]), + Message.tool({ id: "call_1", name: "lookup", result: { forecast: "sunny" } }), + ], + toolChoice: "none", + cache: "none", + }), + ) + + expect(prepared.body.tools).toEqual([ + { + name: "lookup", + description: "Look things up", + input_schema: { type: "object", properties: {} }, + }, + ]) + expect(prepared.body.tool_choice).toEqual({ type: "none" }) + }), + ) + // Regression: read tool results must stay structured so base64 media data is // not JSON-stringified into `tool_result.content`. it.effect("lowers media tool-result content as structured blocks", () => From 2a9f8e3a2cb78d28f345e5f45f9ba1c222068e8f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:58:02 +0000 Subject: [PATCH 25/27] fix(tui): manage focus in devtools panels (#38555) Co-authored-by: James Long <17031+jlongster@users.noreply.github.com> --- packages/tui/src/component/devtools-bar.tsx | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index 03373110e2..45c031069c 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -1,4 +1,4 @@ -import { TextAttributes } from "@opentui/core" +import { TextAttributes, type Renderable } from "@opentui/core" import { TimeToFirstDraw, useRenderer, useTerminalDimensions } from "@opentui/solid" import { open } from "node:fs/promises" import { tmpdir } from "node:os" @@ -33,6 +33,7 @@ export function DevToolsBar() { const plugins = usePlugin() const theme = useTheme() const keymap = Keymap.use() + const renderer = useRenderer() const dimensions = useTerminalDimensions() const { themeV2, mode, supports, setMode } = theme const elevatedTheme = theme.contextual("elevated").themeV2 @@ -41,6 +42,7 @@ export function DevToolsBar() { const [dumpPath, setDumpPath] = createSignal() const [dumpError, setDumpError] = createSignal() const [frontendSamples, setFrontendSamples] = createSignal([]) + let focus: Renderable | null const connected = createMemo(() => client.connection.status() === "connected") const serverIndicator = createMemo(() => connectionIndicator(client.connection.status(), client.connection.attempt())) const themePerformance = createMemo( @@ -54,7 +56,22 @@ export function DevToolsBar() { address: info.urls[0] ? new URL(info.urls[0]).host : "Unknown", } }) - const toggle = (next: Panel) => setPanel((current) => (current === next ? undefined : next)) + const close = () => { + setPanel() + setTimeout(() => { + if (panel() || !focus || focus.isDestroyed) return + focus.focus() + focus = null + }, 1) + } + const toggle = (next: Panel) => { + if (panel() === next) return close() + if (!panel()) { + focus = renderer.currentFocusedRenderable + focus?.blur() + } + setPanel(next) + } const nextMode = () => (mode() === "dark" ? "light" : "dark") const canSwitchMode = () => supports(nextMode()) const runtime = createMemo(() => runtimeStatus(frontendSamples())) @@ -67,7 +84,7 @@ export function DevToolsBar() { if (!panel() || event.name !== "escape") return event.preventDefault() event.stopPropagation() - setPanel() + close() }, { priority: 10 }, ) @@ -205,7 +222,7 @@ export function DevToolsBar() { width={dimensions().width} height={Math.max(0, dimensions().height - 1)} backgroundColor="transparent" - onMouseUp={() => setPanel()} + onMouseUp={close} /> toggle("server")}> From 193f6be99ca3e70f8ca001a7eb4f66cf78383fe9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:09:25 -0500 Subject: [PATCH 26/27] fix(ai): keep tools when Gemini tool choice is none (#38556) --- packages/ai/src/protocols/gemini.ts | 6 +++--- packages/ai/test/provider/gemini.test.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 6cba2bc7f9..57ad066037 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -313,7 +313,7 @@ const thinkingConfig = (request: LLMRequest) => { } const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) { - const toolsEnabled = request.tools.length > 0 && request.toolChoice?.type !== "none" + const hasTools = request.tools.length > 0 const generation = request.generation const toolSchemaCompatibility = request.model.compatibility?.toolSchema const generationConfig = { @@ -329,7 +329,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque contents: yield* lowerMessages(request), systemInstruction: request.system.length === 0 ? undefined : { parts: [{ text: ProviderShared.joinText(request.system) }] }, - tools: toolsEnabled + tools: hasTools ? [ { functionDeclarations: request.tools.map((tool) => @@ -338,7 +338,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque }, ] : undefined, - toolConfig: toolsEnabled && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined, + toolConfig: hasTools && request.toolChoice ? yield* lowerToolConfig(request.toolChoice) : undefined, generationConfig: Object.values(generationConfig).some((value) => value !== undefined) ? generationConfig : undefined, diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 5195d372c9..50b5dbb1d2 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -233,11 +233,11 @@ describe("Gemini route", () => { }), ) - it.effect("omits tools when tool choice is none", () => + it.effect("keeps tools and sends function calling mode NONE", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ - id: "req_no_tools", + id: "req_tool_choice_none", model, prompt: "Say hello.", tools: [{ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } }], @@ -245,8 +245,10 @@ describe("Gemini route", () => { }), ) - expect(prepared.body).toEqual({ + expect(prepared.body).toMatchObject({ contents: [{ role: "user", parts: [{ text: "Say hello." }] }], + tools: [{ functionDeclarations: [{ name: "lookup", description: "Lookup data" }] }], + toolConfig: { functionCallingConfig: { mode: "NONE" } }, }) }), ) From 8cac010bacc9ddd374c900d6a7c98aafc1113f1f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:12:23 -0500 Subject: [PATCH 27/27] fix(core): stop forcing toolChoice none on session.generate (#38557) --- packages/core/src/session/generate-node.ts | 1 - packages/core/test/session-generate.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index f26e2847c2..96804fc57f 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -74,7 +74,6 @@ export const layer = Layer.effect( system: contextEvent.system, messages: contextEvent.messages, tools: hookedTools, - toolChoice: "none", }), ) yield* Effect.logInfo("session generation usage diagnostic", { usage: response.usage }) diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 98e7aedc0c..e78f41bdac 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -301,7 +301,7 @@ it.effect("generates from fresh settled Session context without durable mutation ), ).toEqual(["Settled partial answer"]) expect(requests[0]?.tools).toMatchObject([{ name: "lookup", description: "Hooked lookup" }]) - expect(requests[0]?.toolChoice).toMatchObject({ type: "none" }) + expect(requests[0]?.toolChoice).toBeUndefined() expect(yield* durableState(db, sessionID)).toEqual(before) }), )